ConditionParserTest.java
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.maven.impl.model.profile;
import java.util.Map;
import java.util.function.UnaryOperator;
import org.apache.maven.api.model.Model;
import org.apache.maven.api.services.model.ProfileActivationContext;
import org.apache.maven.impl.DefaultModelVersionParser;
import org.apache.maven.impl.DefaultVersionParser;
import org.apache.maven.impl.model.DefaultInterpolator;
import org.apache.maven.impl.model.DefaultPathTranslator;
import org.apache.maven.impl.model.DefaultProfileActivationContext;
import org.apache.maven.impl.model.profile.ConditionParser.ExpressionFunction;
import org.eclipse.aether.util.version.GenericVersionScheme;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
class ConditionParserTest {
ConditionParser parser;
Map<String, ExpressionFunction> functions;
UnaryOperator<String> propertyResolver;
@BeforeEach
void setUp() {
ProfileActivationContext context = createMockContext();
DefaultVersionParser versionParser =
new DefaultVersionParser(new DefaultModelVersionParser(new GenericVersionScheme()));
ConditionProfileActivator activator = new ConditionProfileActivator(versionParser, new DefaultInterpolator());
functions = activator.registerFunctions(context, versionParser);
propertyResolver = s -> activator.property(context, s);
parser = new ConditionParser(functions, propertyResolver);
}
private ProfileActivationContext createMockContext() {
DefaultProfileActivationContext context = new DefaultProfileActivationContext(
new DefaultPathTranslator(),
new AbstractProfileActivatorTest.FakeRootLocator(),
new DefaultInterpolator());
context.setSystemProperties(Map.of(
"os.name", "windows",
"os.arch", "amd64",
"os.version", "10.0",
"java.version", "1.8.0_292"));
context.setModel(Model.newInstance());
return context;
}
@Test
void testStringLiterals() {
assertEquals("Hello, World!", parser.parse("'Hello, World!'"));
assertEquals("Hello, World!", parser.parse("\"Hello, World!\""));
}
@Test
void testStringConcatenation() {
assertEquals("HelloWorld", parser.parse("'Hello' + 'World'"));
assertEquals("Hello123", parser.parse("'Hello' + 123"));
}
@Test
void testLengthFunction() {
assertEquals(13, parser.parse("length('Hello, World!')"));
assertEquals(5, parser.parse("length(\"Hello\")"));
}
@Test
void testCaseConversionFunctions() {
assertEquals("HELLO", parser.parse("upper('hello')"));
assertEquals("world", parser.parse("lower('WORLD')"));
}
@Test
void testConcatFunction() {
assertEquals("HelloWorld", parser.parse("'Hello' + 'World'"));
assertEquals("The answer is 42", parser.parse("'The answer is ' + 42"));
assertEquals("The answer is 42", parser.parse("'The answer is ' + 42.0"));
assertEquals("The answer is 42", parser.parse("'The answer is ' + 42.0f"));
assertEquals("Pi is approximately 3.14", parser.parse("'Pi is approximately ' + 3.14"));
assertEquals("Pi is approximately 3.14", parser.parse("'Pi is approximately ' + 3.14f"));
}
@Test
void testToString() {
assertEquals("42", ConditionParser.toString(42));
assertEquals("42", ConditionParser.toString(42L));
assertEquals("42", ConditionParser.toString(42.0));
assertEquals("42", ConditionParser.toString(42.0f));
assertEquals("3.14", ConditionParser.toString(3.14));
assertEquals("3.14", ConditionParser.toString(3.14f));
assertEquals("true", ConditionParser.toString(true));
assertEquals("false", ConditionParser.toString(false));
assertEquals("hello", ConditionParser.toString("hello"));
}
@Test
void testSubstringFunction() {
assertEquals("World", parser.parse("substring('Hello, World!', 7, 12)"));
assertEquals("World!", parser.parse("substring('Hello, World!', 7)"));
}
@Test
void testIndexOf() {
assertEquals(7, parser.parse("indexOf('Hello, World!', 'World')"));
assertEquals(-1, parser.parse("indexOf('Hello, World!', 'OpenAI')"));
}
@Test
void testInRange() {
assertTrue((Boolean) parser.parse("inrange('1.8.0_292', '[1.8,2.0)')"));
assertFalse((Boolean) parser.parse("inrange('1.7.0', '[1.8,2.0)')"));
}
@Test
void testIfFunction() {
assertEquals("long", parser.parse("if(length('test') > 3, 'long', 'short')"));
assertEquals("short", parser.parse("if(length('hi') > 3, 'long', 'short')"));
}
@Test
void testContainsFunction() {
assertTrue((Boolean) parser.parse("contains('Hello, World!', 'World')"));
assertFalse((Boolean) parser.parse("contains('Hello, World!', 'OpenAI')"));
}
@Test
void testMatchesFunction() {
assertTrue((Boolean) parser.parse("matches('test123', '\\w+')"));
assertFalse((Boolean) parser.parse("matches('test123', '\\d+')"));
}
@Test
void testComplexExpression() {
String expression = "if(contains(lower('HELLO WORLD'), 'hello'), upper('success') + '!', 'failure')";
assertEquals("SUCCESS!", parser.parse(expression));
}
@Test
void testStringComparison() {
assertTrue((Boolean) parser.parse("'abc' != 'cdf'"));
assertFalse((Boolean) parser.parse("'abc' != 'abc'"));
assertTrue((Boolean) parser.parse("'abc' == 'abc'"));
assertFalse((Boolean) parser.parse("'abc' == 'cdf'"));
}
@Test
void testParenthesesMismatch() {
functions.put("property", args -> "foo");
functions.put("inrange", args -> false);
assertThrows(
RuntimeException.class,
() -> parser.parse(
"property('os.name') == 'windows' && property('os.arch') != 'amd64') && inrange(property('os.version'), '[99,)')"),
"Should throw RuntimeException due to parentheses mismatch");
assertThrows(
RuntimeException.class,
() -> parser.parse(
"(property('os.name') == 'windows' && property('os.arch') != 'amd64') && inrange(property('os.version'), '[99,)'"),
"Should throw RuntimeException due to parentheses mismatch");
// This should not throw an exception if parentheses are balanced and properties are handled correctly
assertDoesNotThrow(
() -> parser.parse(
"(property('os.name') == 'windows' && property('os.arch') != 'amd64') && inrange(property('os.version'), '[99,)')"));
}
@Test
void testBasicArithmetic() {
assertEquals(5.0, parser.parse("2 + 3"));
assertEquals(10.0, parser.parse("15 - 5"));
assertEquals(24.0, parser.parse("6 * 4"));
assertEquals(3.0, parser.parse("9 / 3"));
}
@Test
void testArithmeticPrecedence() {
assertEquals(14.0, parser.parse("2 + 3 * 4"));
assertEquals(20.0, parser.parse("(2 + 3) * 4"));
assertEquals(11.0, parser.parse("15 - 6 + 2"));
assertEquals(10.0, parser.parse("10 / 2 + 2 * 2.5"));
}
@Test
void testFloatingPointArithmetic() {
assertEquals(5.5, parser.parse("2.2 + 3.3"));
assertEquals(0.1, (Double) parser.parse("3.3 - 3.2"), 1e-10);
assertEquals(6.25, parser.parse("2.5 * 2.5"));
assertEquals(2.5, parser.parse("5 / 2"));
}
@Test
void testArithmeticComparisons() {
assertTrue((Boolean) parser.parse("5 > 3"));
assertTrue((Boolean) parser.parse("3 < 5"));
assertTrue((Boolean) parser.parse("5 >= 5"));
assertTrue((Boolean) parser.parse("3 <= 3"));
assertTrue((Boolean) parser.parse("5 == 5"));
assertTrue((Boolean) parser.parse("5 != 3"));
assertFalse((Boolean) parser.parse("5 < 3"));
assertFalse((Boolean) parser.parse("3 > 5"));
assertFalse((Boolean) parser.parse("5 != 5"));
}
@Test
void testComplexArithmeticExpressions() {
assertFalse((Boolean) parser.parse("(2 + 3 * 4) > (10 + 5)"));
assertTrue((Boolean) parser.parse("(2 + 3 * 4) < (10 + 5)"));
assertTrue((Boolean) parser.parse("(10 / 2 + 3) == 8"));
assertTrue((Boolean) parser.parse("(10 / 2 + 3) != 9"));
}
@Test
void testArithmeticFunctions() {
assertEquals(5.0, parser.parse("2 + 3"));
assertEquals(2.0, parser.parse("5 - 3"));
assertEquals(15.0, parser.parse("3 * 5"));
assertEquals(2.5, parser.parse("5 / 2"));
}
@Test
void testCombinedArithmeticAndLogic() {
assertTrue((Boolean) parser.parse("(5 > 3) && (10 / 2 == 5)"));
assertFalse((Boolean) parser.parse("(5 < 3) || (10 / 2 != 5)"));
assertTrue((Boolean) parser.parse("2 + 3 == 1 * 5"));
}
@Test
void testDivisionByZero() {
assertThrows(ArithmeticException.class, () -> parser.parse("5 / 0"));
}
@Test
void testPropertyAlias() {
assertTrue((Boolean) parser.parse("${os.name} == 'windows'"));
assertFalse((Boolean) parser.parse("${os.name} == 'linux'"));
assertTrue((Boolean) parser.parse("${os.arch} == 'amd64' && ${os.name} == 'windows'"));
assertThrows(RuntimeException.class, () -> parser.parse("${unclosed"));
}
@Test
void testNestedPropertyAlias() {
functions.put("property", args -> {
if (args.get(0).equals("project.rootDirectory")) {
return "/home/user/project";
}
return null;
});
functions.put("exists", args -> true); // Mock implementation
Object result = parser.parse("exists('${project.rootDirectory}/someFile.txt')");
assertTrue((Boolean) result);
result = parser.parse("exists('${project.rootDirectory}/${nested.property}/someFile.txt')");
assertTrue((Boolean) result);
assertDoesNotThrow(() -> parser.parse("property('')"));
}
@Test
void testToInt() {
assertEquals(123, ConditionParser.toInt(123));
assertEquals(123, ConditionParser.toInt(123L));
assertEquals(123, ConditionParser.toInt(123.0));
assertEquals(123, ConditionParser.toInt(123.5)); // This will truncate the decimal part
assertEquals(123, ConditionParser.toInt("123"));
assertEquals(123, ConditionParser.toInt("123.0"));
assertEquals(123, ConditionParser.toInt("123.5")); // This will truncate the decimal part
assertEquals(1, ConditionParser.toInt(true));
assertEquals(0, ConditionParser.toInt(false));
assertThrows(RuntimeException.class, () -> ConditionParser.toInt("not a number"));
assertThrows(RuntimeException.class, () -> ConditionParser.toInt(new Object()));
}
}