Polish SpEL documentation and tests

This commit is contained in:
Sam Brannen
2024-01-26 12:37:44 +01:00
parent 9df94357de
commit 00b07659d9
8 changed files with 293 additions and 292 deletions

View File

@@ -21,13 +21,12 @@ import java.util.List;
import org.junit.jupiter.api.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.expression.ConstructorExecutor;
import org.springframework.expression.ConstructorResolver;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import org.springframework.expression.spel.testresources.Fruit;
import org.springframework.expression.spel.testresources.PlaceOfBirth;
import static org.assertj.core.api.Assertions.assertThat;
@@ -41,19 +40,145 @@ import static org.assertj.core.api.Assertions.assertThatException;
class ConstructorInvocationTests extends AbstractExpressionTests {
@Test
void testTypeConstructors() {
void constructorWithArgument() {
evaluate("new String('hello world')", "hello world", String.class);
}
@Test
void testNonExistentType() {
void nonExistentType() {
evaluateAndCheckError("new FooBar()", SpelMessage.CONSTRUCTOR_INVOCATION_PROBLEM);
}
@Test
void constructorThrowingException() {
// Test ctor on inventor:
// On 1 it will throw an IllegalArgumentException
// On 2 it will throw a RuntimeException
// On 3 it will exit normally
// In each case it increments the Tester field 'counter' when invoked
SpelExpressionParser parser = new SpelExpressionParser();
Expression expr = parser.parseExpression("new org.springframework.expression.spel.ConstructorInvocationTests$Tester(#bar).i");
// Normal exit
StandardEvaluationContext context = TestScenarioCreator.getTestEvaluationContext();
context.setRootObject(new Tester());
context.setVariable("bar", 3);
Object o = expr.getValue(context);
assertThat(o).isEqualTo(3);
assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(1);
// Now the expression has cached that throwException(int) is the right thing to
// call. Let's change 'bar' to be a PlaceOfBirth which indicates the cached
// reference is out of date.
context.setVariable("bar", new PlaceOfBirth("London"));
o = expr.getValue(context);
assertThat(o).isEqualTo(0);
// That confirms the logic to mark the cached reference stale and retry is working
// Now let's cause the method to exit via exception and ensure it doesn't cause
// a retry.
// First, switch back to throwException(int)
context.setVariable("bar", 3);
o = expr.getValue(context);
assertThat(o).isEqualTo(3);
assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(2);
// 4 will make it throw a checked exception - this will be wrapped by spel on the
// way out
context.setVariable("bar", 4);
assertThatException()
.isThrownBy(() -> expr.getValue(context))
.withMessageContaining("Tester");
// A problem occurred whilst attempting to construct an object of type
// 'org.springframework.expression.spel.ConstructorInvocationTests$Tester'
// using arguments '(java.lang.Integer)'
// If counter is 4 then the method got called twice!
assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(3);
// 1 will make it throw a RuntimeException - SpEL will let this through
context.setVariable("bar", 1);
assertThatException()
.isThrownBy(() -> expr.getValue(context))
.isNotInstanceOf(SpelEvaluationException.class);
// A problem occurred whilst attempting to construct an object of type
// 'org.springframework.expression.spel.ConstructorInvocationTests$Tester'
// using arguments '(java.lang.Integer)'
// If counter is 5 then the method got called twice!
assertThat(parser.parseExpression("counter").getValue(context)).isEqualTo(4);
}
@Test
void constructorResolvers() {
StandardEvaluationContext ctx = new StandardEvaluationContext();
// reflective constructor accessor is the only one by default
List<ConstructorResolver> constructorResolvers = ctx.getConstructorResolvers();
assertThat(constructorResolvers).hasSize(1);
ConstructorResolver dummy = (context, typeName, argumentTypes) -> {
throw new UnsupportedOperationException();
};
ctx.addConstructorResolver(dummy);
assertThat(ctx.getConstructorResolvers()).hasSize(2);
List<ConstructorResolver> copy = new ArrayList<>(ctx.getConstructorResolvers());
assertThat(ctx.removeConstructorResolver(dummy)).isTrue();
assertThat(ctx.removeConstructorResolver(dummy)).isFalse();
assertThat(ctx.getConstructorResolvers()).hasSize(1);
ctx.setConstructorResolvers(copy);
assertThat(ctx.getConstructorResolvers()).hasSize(2);
}
@Test
void varargsConstructors() {
((StandardTypeLocator) super.context.getTypeLocator()).registerImport(Fruit.class.getPackageName());
// Calling 'Fruit(String... strings)' - returns length_of_strings
evaluate("new Fruit('a','b','c').stringscount()", 3, Integer.class);
evaluate("new Fruit('a').stringscount()", 1, Integer.class);
evaluate("new Fruit().stringscount()", 0, Integer.class);
// all need converting to strings
evaluate("new Fruit(1,2,3).stringscount()", 3, Integer.class);
// needs string conversion
evaluate("new Fruit(1).stringscount()", 1, Integer.class);
// first and last need conversion
evaluate("new Fruit(1,'a',3.0d).stringscount()", 3, Integer.class);
// Calling 'Fruit(int i, String... strings)' - returns int + length_of_strings
evaluate("new Fruit(5,'a','b','c').stringscount()", 8, Integer.class);
evaluate("new Fruit(2,'a').stringscount()", 3, Integer.class);
evaluate("new Fruit(4).stringscount()", 4, Integer.class);
evaluate("new Fruit(8,2,3).stringscount()", 10, Integer.class);
evaluate("new Fruit(9).stringscount()", 9, Integer.class);
evaluate("new Fruit(2,'a',3.0d).stringscount()", 4, Integer.class);
evaluate("new Fruit(8,stringArrayOfThreeItems).stringscount()", 11, Integer.class);
}
/*
* These tests are attempting to call constructors where we need to widen or convert
* the argument in order to satisfy a suitable constructor.
*/
@Test
void widening() {
// widening of int 3 to double 3 is OK
evaluate("new Double(3)", 3.0d, Double.class);
// widening of int 3 to long 3 is OK
evaluate("new Long(3)", 3L, Long.class);
}
@Test
void argumentConversion() {
evaluate("new String(3.0d)", "3.0", String.class);
}
@SuppressWarnings("serial")
static class TestException extends Exception {
}
static class Tester {
@@ -80,154 +205,7 @@ class ConstructorInvocationTests extends AbstractExpressionTests {
}
public Tester(PlaceOfBirth pob) {
}
}
@Test
void testConstructorThrowingException_SPR6760() {
// Test ctor on inventor:
// On 1 it will throw an IllegalArgumentException
// On 2 it will throw a RuntimeException
// On 3 it will exit normally
// In each case it increments the Tester field 'counter' when invoked
SpelExpressionParser parser = new SpelExpressionParser();
Expression expr = parser.parseExpression("new org.springframework.expression.spel.ConstructorInvocationTests$Tester(#bar).i");
// Normal exit
StandardEvaluationContext eContext = TestScenarioCreator.getTestEvaluationContext();
eContext.setRootObject(new Tester());
eContext.setVariable("bar", 3);
Object o = expr.getValue(eContext);
assertThat(o).isEqualTo(3);
assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(1);
// Now the expression has cached that throwException(int) is the right thing to
// call. Let's change 'bar' to be a PlaceOfBirth which indicates the cached
// reference is out of date.
eContext.setVariable("bar", new PlaceOfBirth("London"));
o = expr.getValue(eContext);
assertThat(o).isEqualTo(0);
// That confirms the logic to mark the cached reference stale and retry is working
// Now let's cause the method to exit via exception and ensure it doesn't cause
// a retry.
// First, switch back to throwException(int)
eContext.setVariable("bar", 3);
o = expr.getValue(eContext);
assertThat(o).isEqualTo(3);
assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(2);
// 4 will make it throw a checked exception - this will be wrapped by spel on the
// way out
eContext.setVariable("bar", 4);
assertThatException()
.isThrownBy(() -> expr.getValue(eContext))
.withMessageContaining("Tester");
// A problem occurred whilst attempting to construct an object of type
// 'org.springframework.expression.spel.ConstructorInvocationTests$Tester'
// using arguments '(java.lang.Integer)'
// If counter is 4 then the method got called twice!
assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(3);
// 1 will make it throw a RuntimeException - SpEL will let this through
eContext.setVariable("bar", 1);
assertThatException()
.isThrownBy(() -> expr.getValue(eContext))
.isNotInstanceOf(SpelEvaluationException.class);
// A problem occurred whilst attempting to construct an object of type
// 'org.springframework.expression.spel.ConstructorInvocationTests$Tester'
// using arguments '(java.lang.Integer)'
// If counter is 5 then the method got called twice!
assertThat(parser.parseExpression("counter").getValue(eContext)).isEqualTo(4);
}
@Test
void testAddingConstructorResolvers() {
StandardEvaluationContext ctx = new StandardEvaluationContext();
// reflective constructor accessor is the only one by default
List<ConstructorResolver> constructorResolvers = ctx.getConstructorResolvers();
assertThat(constructorResolvers).hasSize(1);
ConstructorResolver dummy = new DummyConstructorResolver();
ctx.addConstructorResolver(dummy);
assertThat(ctx.getConstructorResolvers()).hasSize(2);
List<ConstructorResolver> copy = new ArrayList<>(ctx.getConstructorResolvers());
assertThat(ctx.removeConstructorResolver(dummy)).isTrue();
assertThat(ctx.removeConstructorResolver(dummy)).isFalse();
assertThat(ctx.getConstructorResolvers()).hasSize(1);
ctx.setConstructorResolvers(copy);
assertThat(ctx.getConstructorResolvers()).hasSize(2);
}
static class DummyConstructorResolver implements ConstructorResolver {
@Override
public ConstructorExecutor resolve(EvaluationContext context, String typeName,
List<TypeDescriptor> argumentTypes) {
throw new UnsupportedOperationException("Auto-generated method stub");
}
}
@Test
void testVarargsInvocation01() {
// Calling 'Fruit(String... strings)'
evaluate("new org.springframework.expression.spel.testresources.Fruit('a','b','c').stringscount()", 3,
Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit('a').stringscount()", 1, Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit().stringscount()", 0, Integer.class);
// all need converting to strings
evaluate("new org.springframework.expression.spel.testresources.Fruit(1,2,3).stringscount()", 3, Integer.class);
// needs string conversion
evaluate("new org.springframework.expression.spel.testresources.Fruit(1).stringscount()", 1, Integer.class);
// first and last need conversion
evaluate("new org.springframework.expression.spel.testresources.Fruit(1,'a',3.0d).stringscount()", 3,
Integer.class);
}
@Test
void testVarargsInvocation02() {
// Calling 'Fruit(int i, String... strings)' - returns int+length_of_strings
evaluate("new org.springframework.expression.spel.testresources.Fruit(5,'a','b','c').stringscount()", 8,
Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a').stringscount()", 3, Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit(4).stringscount()", 4, Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit(8,2,3).stringscount()", 10, Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit(9).stringscount()", 9, Integer.class);
evaluate("new org.springframework.expression.spel.testresources.Fruit(2,'a',3.0d).stringscount()", 4,
Integer.class);
evaluate(
"new org.springframework.expression.spel.testresources.Fruit(8,stringArrayOfThreeItems).stringscount()",
11, Integer.class);
}
/*
* These tests are attempting to call constructors where we need to widen or convert
* the argument in order to satisfy a suitable constructor.
*/
@Test
void testWidening01() {
// widening of int 3 to double 3 is OK
evaluate("new Double(3)", 3.0d, Double.class);
// widening of int 3 to long 3 is OK
evaluate("new Long(3)", 3L, Long.class);
}
@Test
void testArgumentConversion01() {
evaluate("new String(3.0d)", "3.0", String.class);
}
}

View File

@@ -19,83 +19,78 @@ package org.springframework.expression.spel;
import org.junit.jupiter.api.Test;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests the evaluation of basic literals: boolean, integer, hex integer, long, real, null, date
* Tests the evaluation of basic literals: boolean, string, integer, long,
* hex integer, hex long, float, double, null.
*
* @author Andy Clement
* @author Sam Brannen
*/
class LiteralTests extends AbstractExpressionTests {
@Test
void testLiteralBoolean01() {
evaluate("false", "false", Boolean.class);
void booleans() {
evaluate("false", false, Boolean.class);
evaluate("true", true, Boolean.class);
}
@Test
void testLiteralBoolean02() {
evaluate("true", "true", Boolean.class);
}
@Test
void testLiteralInteger01() {
evaluate("1", "1", Integer.class);
}
@Test
void testLiteralInteger02() {
evaluate("1415", "1415", Integer.class);
}
@Test
void testLiteralString01() {
void strings() {
evaluate("'hello'", "hello", String.class);
evaluate("'joe bloggs'", "joe bloggs", String.class);
evaluate("'Hello World'", "Hello World", String.class);
}
@Test
void testLiteralString02() {
evaluate("'joe bloggs'", "joe bloggs", String.class);
}
@Test
void testLiteralString03() {
evaluate("'hello'", "hello", String.class);
}
@Test
void testLiteralString04() {
void stringsContainingQuotes() {
evaluate("'Tony''s Pizza'", "Tony's Pizza", String.class);
evaluate("'Tony\\r''s Pizza'", "Tony\\r's Pizza", String.class);
}
@Test
void testLiteralString05() {
evaluate("\"Hello World\"", "Hello World", String.class);
}
@Test
void testLiteralString06() {
evaluate("\"Hello ' World\"", "Hello ' World", String.class);
}
@Test
void testHexIntLiteral01() {
evaluate("0x7FFFF", "524287", Integer.class);
evaluate("0x7FFFFL", 524287L, Long.class);
evaluate("0X7FFFF", "524287", Integer.class);
evaluate("0X7FFFFl", 524287L, Long.class);
void integers() {
evaluate("1", 1, Integer.class);
evaluate("1415", 1415, Integer.class);
}
@Test
void testLongIntLiteral01() {
void longs() {
evaluate("1L", 1L, Long.class);
evaluate("1415L", 1415L, Long.class);
}
@Test
void signedIntegers() {
evaluate("-1", -1, Integer.class);
evaluate("-0xa", -10, Integer.class);
}
@Test
void signedLongs() {
evaluate("-1L", -1L, Long.class);
evaluate("-0x20l", -32L, Long.class);
}
@Test
void hexIntegers() {
evaluate("0x7FFFF", 524287, Integer.class);
evaluate("0X7FFFF", 524287, Integer.class);
}
@Test
void hexLongs() {
evaluate("0X7FFFFl", 524287L, Long.class);
evaluate("0x7FFFFL", 524287L, Long.class);
evaluate("0xCAFEBABEL", 3405691582L, Long.class);
}
@Test
void testLongIntInteractions01() {
void hexLongAndIntInteractions() {
evaluate("0x20 * 2L", 64L, Long.class);
// ask for the result to be made into an Integer
evaluateAndAskForReturnType("0x20 * 2L", 64, Integer.class);
@@ -104,29 +99,8 @@ class LiteralTests extends AbstractExpressionTests {
}
@Test
void testSignedIntLiterals() {
evaluate("-1", -1, Integer.class);
evaluate("-0xa", -10, Integer.class);
evaluate("-1L", -1L, Long.class);
evaluate("-0x20l", -32L, Long.class);
}
@Test
void testLiteralReal01_CreatingDoubles() {
evaluate("1.25", 1.25d, Double.class);
evaluate("2.99", 2.99d, Double.class);
evaluate("-3.141", -3.141d, Double.class);
evaluate("1.25d", 1.25d, Double.class);
evaluate("2.99d", 2.99d, Double.class);
evaluate("-3.141d", -3.141d, Double.class);
evaluate("1.25D", 1.25d, Double.class);
evaluate("2.99D", 2.99d, Double.class);
evaluate("-3.141D", -3.141d, Double.class);
}
@Test
void testLiteralReal02_CreatingFloats() {
// For now, everything becomes a double...
void floats() {
// "f" or "F" must be explicitly specified.
evaluate("1.25f", 1.25f, Float.class);
evaluate("2.5f", 2.5f, Float.class);
evaluate("-3.5f", -3.5f, Float.class);
@@ -136,7 +110,23 @@ class LiteralTests extends AbstractExpressionTests {
}
@Test
void testLiteralReal03_UsingExponents() {
void doubles() {
// Real numbers are Doubles by default
evaluate("1.25", 1.25d, Double.class);
evaluate("2.99", 2.99d, Double.class);
evaluate("-3.141", -3.141d, Double.class);
// But "d" or "D" can also be explicitly specified.
evaluate("1.25d", 1.25d, Double.class);
evaluate("2.99d", 2.99d, Double.class);
evaluate("-3.141d", -3.141d, Double.class);
evaluate("1.25D", 1.25d, Double.class);
evaluate("2.99D", 2.99d, Double.class);
evaluate("-3.141D", -3.141d, Double.class);
}
@Test
void doublesUsingExponents() {
evaluate("6.0221415E+23", "6.0221415E23", Double.class);
evaluate("6.0221415e+23", "6.0221415E23", Double.class);
evaluate("6.0221415E+23d", "6.0221415E23", Double.class);
@@ -145,30 +135,33 @@ class LiteralTests extends AbstractExpressionTests {
}
@Test
void testLiteralReal04_BadExpressions() {
void doublesUsingExponentsWithInvalidInput() {
parseAndCheckError("6.1e23e22", SpelMessage.MORE_INPUT, 6, "e22");
parseAndCheckError("6.1f23e22", SpelMessage.MORE_INPUT, 4, "23e22");
}
@Test
void testLiteralNull01() {
void nullLiteral() {
evaluate("null", null, null);
}
@Test
void testConversions() {
void conversions() {
// getting the expression type to be what we want - either:
evaluate("new Integer(37).byteValue()", (byte) 37, Byte.class); // calling byteValue() on Integer.class
evaluateAndAskForReturnType("new Integer(37)", (byte) 37, Byte.class); // relying on registered type converters
evaluate("37.byteValue", (byte) 37, Byte.class); // calling byteValue() on Integer.class
evaluateAndAskForReturnType("37", (byte) 37, Byte.class); // relying on registered type converters
}
@Test
void testNotWritable() {
SpelExpression expr = (SpelExpression)parser.parseExpression("37");
assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse();
expr = (SpelExpression)parser.parseExpression("37L");
assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse();
expr = (SpelExpression)parser.parseExpression("true");
assertThat(expr.isWritable(new StandardEvaluationContext())).isFalse();
void notWritable() {
SpelExpression expr = (SpelExpression) parser.parseExpression("37");
assertThat(expr.isWritable(context)).isFalse();
expr = (SpelExpression) parser.parseExpression("37L");
assertThat(expr.isWritable(context)).isFalse();
expr = (SpelExpression) parser.parseExpression("true");
assertThat(expr.isWritable(context)).isFalse();
}
}

View File

@@ -99,7 +99,7 @@ class ParsingTests {
}
@Test
void literalNull() {
void nullLiteral() {
parseCheck("null");
}
@@ -399,12 +399,12 @@ class ParsingTests {
@Test
void mathOperatorsAddStrings() {
parseCheck("'a'+'b'", "('a' + 'b')");
parseCheck("'a' + 'b'", "('a' + 'b')");
}
@Test
void mathOperatorsAddMultipleStrings() {
parseCheck("'hello'+' '+'world'", "(('hello' + ' ') + 'world')");
parseCheck("'hello' + ' ' + 'world'", "(('hello' + ' ') + 'world')");
}
@Test
@@ -429,7 +429,7 @@ class ParsingTests {
}
@Nested
class References {
class BeanReferences {
@Test
void references() {