This is an automated email from the ASF dual-hosted git repository.

benw pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/tapestry-5.git


The following commit(s) were added to refs/heads/master by this push:
     new 9ec4acb6a TAP5-2806: beanmodel setup junit, add ProperyConduitSpec
9ec4acb6a is described below

commit 9ec4acb6ae5702d04567d13350aec18e5efa8b9c
Author: Ben Weidig <b...@netzgut.net>
AuthorDate: Thu May 29 10:52:38 2025 +0200

    TAP5-2806: beanmodel setup junit, add ProperyConduitSpec
---
 beanmodel/build.gradle                             |   5 +
 .../tapestry5/beanmodel/PropertyConduitSpec.groovy | 598 +++++++++++++++++++++
 2 files changed, 603 insertions(+)

diff --git a/beanmodel/build.gradle b/beanmodel/build.gradle
index f6f679012..5adf920bd 100644
--- a/beanmodel/build.gradle
+++ b/beanmodel/build.gradle
@@ -25,10 +25,15 @@ dependencies {
     testImplementation "org.testng:testng:${versions.testng}", { transitive = 
false }
     testImplementation "org.easymock:easymock:${versions.easymock}"
     testImplementation 
"org.junit.jupiter:junit-jupiter:${versions.junitJupiter}"
+    testImplementation "org.spockframework:spock-core:${versions.spock}"
 }
 
 clean.delete generateGrammarSource.outputDirectory
 
 compileJava {
     options.fork(memoryMaximumSize: '512m')
+}
+
+test {
+    useJUnit()
 }
\ No newline at end of file
diff --git 
a/beanmodel/src/test/groovy/org/apache/tapestry5/beanmodel/PropertyConduitSpec.groovy
 
b/beanmodel/src/test/groovy/org/apache/tapestry5/beanmodel/PropertyConduitSpec.groovy
new file mode 100644
index 000000000..57d43ba2a
--- /dev/null
+++ 
b/beanmodel/src/test/groovy/org/apache/tapestry5/beanmodel/PropertyConduitSpec.groovy
@@ -0,0 +1,598 @@
+package org.apache.tapestry5.beanmodel
+
+import 
org.apache.tapestry5.beanmodel.BeanModelSourceBuilder.CoercionTupleConfiguration
+import org.apache.tapestry5.beanmodel.internal.services.PropertyAccessImpl
+import 
org.apache.tapestry5.beanmodel.internal.services.PropertyConduitSourceImpl
+import 
org.apache.tapestry5.beanmodel.internal.services.PropertyExpressionException
+import org.apache.tapestry5.beanmodel.services.PlasticProxyFactoryImpl
+import org.apache.tapestry5.beanmodel.services.PropertyConduitSource
+import org.apache.tapestry5.commons.internal.BasicTypeCoercions
+import org.apache.tapestry5.commons.internal.services.StringInternerImpl
+import org.apache.tapestry5.commons.internal.services.TypeCoercerImpl
+import org.apache.tapestry5.commons.services.PlasticProxyFactory
+import org.apache.tapestry5.commons.util.IntegerRange
+import org.slf4j.LoggerFactory
+
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+
+class PropertyConduitSpec extends Specification {
+
+  static class TestBean {
+    String stringProperty
+    Integer intProperty
+    boolean booleanProperty
+    Boolean wrapperBooleanProperty
+    TestBean nestedBean
+    List<String> stringList
+    List<TestBean> beanList
+    Map<String, String> stringMap
+    Map<String, TestBean> beanMap
+    private String privateString = "initialPrivate"
+
+    String getStringMethod() {
+      "methodResult"
+    }
+
+    String getStringMethodWithArg(String arg) {
+      "methodResult:" + arg
+    }
+
+    int getIntMethodWithArgs(int a, int b) {
+      a + b
+    }
+
+    void setStringPropertyFromMethod(String value) {
+      this.stringProperty = "setViaMethod:" + value
+    }
+
+    boolean isFlag() {
+      booleanProperty
+    }
+
+    void setFlag(boolean flag) {
+      this.booleanProperty = flag
+    }
+
+    TestBean() {}
+    TestBean(String stringProperty) {
+      this.stringProperty = stringProperty
+    }
+
+    String getPrivateString() {
+      privateString
+    }
+    void setPrivateString(String privateString) {
+      this.privateString = privateString
+    }
+  }
+
+  static class RootBean {
+    TestBean testBean = new TestBean()
+    String topLevelString = "top"
+    Integer topLevelInt = 100
+    boolean topLevelBoolean = true
+    List<Integer> topLevelList = [10, 20, 30]
+    Map<String, Integer> topLevelMap = [a: 1, b: 2]
+    Object nullObject = null
+    final String finalProperty = "finalValue"
+    static String staticProperty = "staticValue"
+
+    String getCalculated() {
+      "calculatedValue"
+    }
+
+    void setWriteOnly(String value) {
+    }
+  }
+
+  RootBean root
+
+  @Shared
+  PropertyConduitSource propertyConduitSource
+
+  def setupSpec() {
+    def plasticProxyFactory = new 
PlasticProxyFactoryImpl(getClass().getClassLoader(), 
LoggerFactory.getLogger(PlasticProxyFactory.class))
+
+    CoercionTupleConfiguration configuration = new CoercionTupleConfiguration()
+    BasicTypeCoercions.provideBasicTypeCoercions(configuration)
+    BasicTypeCoercions.provideJSR310TypeCoercions(configuration)
+    def typeCoercer = new TypeCoercerImpl(configuration.getTuples())
+
+    propertyConduitSource = new PropertyConduitSourceImpl(new 
PropertyAccessImpl(), plasticProxyFactory, typeCoercer, new 
StringInternerImpl())
+  }
+
+  def setup() {
+    root = new RootBean()
+    root.testBean = new TestBean()
+    root.testBean.stringProperty = "beanString"
+    root.testBean.intProperty = 123
+    root.testBean.booleanProperty = true
+    root.testBean.wrapperBooleanProperty = Boolean.TRUE
+    root.testBean.nestedBean = new TestBean("nestedBeanString")
+    root.testBean.stringList = ["a", "b", "c"]
+    root.testBean.beanList = [
+      new TestBean("beanInList1"),
+      new TestBean("beanInList2")
+    ]
+    root.testBean.stringMap = [key1: "value1", key2: "value2"]
+    root.testBean.beanMap = [mapKey1: new TestBean("beanInMap1")]
+    root.topLevelList = [10, 20, 30]
+    root.topLevelMap = [mapA: 100, mapB: 200]
+  }
+
+  @Unroll
+  def "property access for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedValue
+    conduitType == expectedType
+
+    where:
+    expression                          | expectedValue        | expectedType
+    "topLevelString"                    | "top"                | String.class
+    "topLevelInt"                       | 100                  | Integer.class
+    "topLevelBoolean"                   | true                 | boolean.class
+    "testBean.stringProperty"           | "beanString"         | String.class
+    "testBean.intProperty"              | 123                  | Integer.class
+    "testBean.booleanProperty"          | true                 | boolean.class
+    "testBean.wrapperBooleanProperty"   | Boolean.TRUE         | Boolean.class
+    "testBean.nestedBean.stringProperty"| "nestedBeanString"   | String.class
+    "testBean.flag"                     | true                 | boolean.class
+    "testBean.privateString"            | "initialPrivate"     | String.class
+  }
+
+  @Unroll
+  def "safe dereference ('?.') for '#expression'"() {
+
+    given:
+    if (makePathNull) {
+      if (expression.startsWith("nullObject")) {
+          assert root.nullObject == null
+      } else if (expression == "testBean.nestedBean?.stringProperty") {
+          assert root.testBean != null
+          root.testBean.nestedBean = null
+      } else if (expression == "testBean?.stringProperty") {
+          root.testBean = null
+      }
+    }
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedValue
+    conduitType == expectedTypeIfNotNull // Type is determined even if current 
value is null
+
+    where:
+    expression                             | makePathNull | expectedValue      
| expectedTypeIfNotNull
+    "testBean?.stringProperty"             | false        | "beanString"       
| String.class
+    "testBean.nestedBean?.stringProperty"  | false        | "nestedBeanString" 
| String.class
+    "testBean.nestedBean?.stringProperty"  | true         | null               
| String.class // nestedBean is null
+    "testBean?.stringProperty"             | true         | null               
| String.class // testBean itself is null
+  }
+
+  @Unroll
+  def "safe dereference ('?.') failing for non-existent properties for 
'#expression'"() {
+
+    given:
+    if (makePathNull) {
+      if (expression.startsWith("nullObject")) {
+          assert root.nullObject == null
+      } else if (expression == "testBean?.stringProperty") {
+          root.testBean = null
+      }
+    }
+
+    when:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    then:
+    thrown(PropertyExpressionException)
+
+    where:
+    expression                             | makePathNull
+    "nullObject?.someProperty"             | true
+    "testBean?.nonExistentProperty"        | false
+  }
+
+  @Unroll
+  def "method invocation for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedValue
+    conduitType == expectedType
+
+    where:
+    expression                                                 | expectedValue 
            | expectedType
+    "testBean.getStringMethod()"                               | 
"methodResult"            | String.class
+    "testBean.getStringMethodWithArg('hello')"                 | 
"methodResult:hello"      | String.class
+    "testBean.getStringMethodWithArg(testBean.stringProperty)" | 
"methodResult:beanString" | String.class
+    "testBean.getIntMethodWithArgs(5, 3)"                      | 8             
            | int.class
+    "testBean.getIntMethodWithArgs(testBean.intProperty, 2)"   | 125           
            | int.class
+    "getCalculated()"                                          | 
"calculatedValue"         | String.class
+  }
+
+  def "keyword 'this' evaluates to the root object"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, "this")
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result === root
+    conduitType == RootBean.class
+  }
+
+  @Unroll
+  def "keyword evaluation for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedValue
+    conduitType == expectedType
+
+    where:
+    expression | expectedValue | expectedType
+    "null"     | null          | Void.class
+    "true"     | true          | Boolean.class
+    "false"    | false         | Boolean.class
+  }
+
+  @Unroll
+  def "literal evaluation for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedValue
+    // ANTLR is more open for numeric literals, but PropertyConduit might
+    // normalize these, so checking is a little more involved.
+    if (expectedType == double.class && result instanceof Float) {
+      (result as Float).toDouble() == (expectedValue as Double)
+    } else {
+      result.getClass() == expectedValue.getClass()
+    }
+    conduitType == expectedType
+
+    where:
+    expression | expectedValue | expectedType
+    "123"      | 123           | Long.class
+    "-45"      | -45           | Long.class
+    "1.23"     | 1.23d         | Double.class
+    "-.5"      | -0.5d         | Double.class
+    "'hello'"  | "hello"       | String.class
+    "''"       | ""            | String.class
+  }
+
+  @Unroll
+  def "not operator evaluation for '#expression'"() {
+
+    given:
+    setupAction(root)
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedValue
+    conduitType == boolean.class
+
+    where:
+    expression         | setupAction                    | expectedValue
+    "!true"            | { }                            | false
+    "!false"           | { }                            | true
+    "!topLevelBoolean" | { }                            | false
+    "!topLevelBoolean" | { it.topLevelBoolean = false } | true
+    "!nullObject"      | { }                            | true
+    "!testBean"        | { }                            | false
+  }
+
+  @Unroll
+  def "range operator for '#expression'"() {
+
+    given:
+    setupAction(root)
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result instanceof IntegerRange
+    result.iterator().collect { it } == expectedList
+    conduitType == IntegerRange.class
+
+    where:
+    expression                  | setupAction            | expectedList
+    "1..3"                      | { }                    | [1, 2, 3]
+    "topLevelInt .. 102"        | { }                    | [100, 101, 102]
+    "topLevelInt .. 7"          | { it.topLevelInt = 5 } | [5, 6, 7]
+    "2..topLevelInt"            | { it.topLevelInt = 4 } | [2, 3, 4]
+    "120..testBean.intProperty" | { }                    | [120, 121, 122, 123]
+    "-2..0"                     | { }                    | [-2, -1, 0]
+  }
+
+  def "list literal evaluation"() {
+
+    given:
+    // TAP5-2808: RANGEOP isn't working as a sub-expression, even though the 
grammar defines it;
+    // NOT WORKING: "[1, 'two', true, testBean.intProperty, [1..2]]"
+    def conduit = propertyConduitSource.create(RootBean, "[1, 'two', true, 
testBean.intProperty]")
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result instanceof List
+    result.size() == 4
+    result[0] == 1
+    result[1] == "two"
+    result[2] == true
+    result[3] == 123 // testBean.intProperty
+    conduitType == List.class
+  }
+
+  def "map literal evaluation"() {
+
+    given:
+    // TAP5-2808: RANGEOP isn't working as a sub-expression, even though the 
grammar defines it;
+    // NOT WORKING: "{'a': 1, 'b': testBean.stringProperty, 'c': true, 123: 
'numKey', d: [1..2] }"
+    def conduit = propertyConduitSource.create(RootBean, "{'a': 1, 'b': 
testBean.stringProperty, 'c': true }")
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result instanceof Map
+    result.size() == 3
+    result.a == 1
+    result.b == "beanString"
+    result.c == true
+  }
+
+  def "empty list/map literal for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    def result = conduit.get(root)
+    def conduitType = conduit.getPropertyType()
+
+    then:
+    result == expectedResult
+    expectedType.isInstance(result)
+    conduitType == expectedType
+
+    where:
+    expression | expectedResult | expectedType
+    '[]'       | []             | List
+    '{}'       | [:]            | Map
+  }
+
+  @Unroll
+  def "set property for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    def originalValue = conduit.get(root)
+
+    when:
+    conduit.set(root, newValue)
+    def updatedValue = conduit.get(root)
+
+    then:
+    originalValue != newValue
+    updatedValue == newValue
+
+    where:
+    expression                          | newValue
+    "topLevelString"                    | "newTop"
+    "topLevelInt"                       | 999
+    "topLevelBoolean"                   | false
+    "testBean.stringProperty"           | "newBeanString"
+    "testBean.intProperty"              | 789
+    "testBean.booleanProperty"          | false
+    "testBean.wrapperBooleanProperty"   | Boolean.FALSE
+    "testBean.nestedBean.stringProperty"| "newNestedString"
+    "testBean.flag"                     | false
+    "testBean.privateString"            | "newPrivate"
+  }
+
+  def "method used as property setter (stringPropertyFromMethod)"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, 
"testBean.stringPropertyFromMethod")
+
+    when:
+    conduit.set(root, "directValue")
+    then:
+    root.testBean.stringProperty == "setViaMethod:directValue"
+  }
+
+  def "final property should be read-only"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, "finalProperty")
+
+    when:
+    conduit.set(root, "newValue")
+
+    then:
+    thrown(RuntimeException)
+    root.finalProperty == "finalValue"
+  }
+
+  def "write-only property (no getter)"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean.class, "writeOnly")
+
+    expect: "property type for write-only property is bassed on setter"
+    conduit.getPropertyType() == String.class
+
+    when: "getting a write-only property"
+    conduit.get(root)
+
+    then: "should throw exception as there's no getter"
+    thrown(RuntimeException)
+
+    when: "setting a write-only property"
+    conduit.set(root, "testSet")
+
+    then: "should succeed"
+    noExceptionThrown()
+  }
+
+  def "set property through safe dereference (last segment is settable)"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, 
"testBean?.stringProperty")
+
+    when: "set on valid path"
+    conduit.set(root, "safeSet")
+
+    then:
+    root.testBean.stringProperty == "safeSet"
+
+    when: "path before ?. is null"
+    root.testBean = null
+    conduit.set(root, "ignoredSet")
+
+    then:
+    noExceptionThrown()
+  }
+
+  @Unroll
+  def "literals are read-only for '#expression'"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, expression)
+
+    when:
+    conduit.set(root,  newValue)
+
+    then:
+    thrown(RuntimeException)
+
+    where:
+    expression | newValue
+    "'aString'"| "new"
+    "123"      | 456
+    "1..3"     | [1, 2]
+    "[1,2]"    | [3, 4]
+    "{'a':1}"  | ['b':2]
+    "true"     | false
+    "null"     | "not null"
+  }
+
+  @Unroll
+  def "non-existent property path '#expression' should fail"() {
+
+    when:
+    // Depending on when the failure occurs (conduit creation vs. get),
+    // we test both.
+    // Usually conduit creation fails for totally unknown property.
+    // If it's a nested path where an intermediate part is valid, get() might 
fail.
+    try {
+      def conduit = propertyConduitSource.create(RootBean.class, expression)
+      conduit.get(root)
+    } catch (RuntimeException e) {
+      assert e.getMessage().toLowerCase().contains("property") ||
+      e.getMessage().toLowerCase().contains("could not find") ||
+      e.getMessage().toLowerCase().contains("unable to parse") ||
+      e.getMessage().toLowerCase().contains("no read property")
+      throw e
+    }
+
+    then:
+    thrown(RuntimeException)
+
+    where:
+    expression << [
+      "nonExistentProperty",
+      "testBean.nonExistent",
+      "topLevelString.nonExistentPart"
+    ]
+  }
+
+  def "method call with wrong number of arguments"() {
+
+    when:
+    propertyConduitSource.create(RootBean, "testBean.getIntMethodWithArgs(5)")
+
+    then:
+    thrown(PropertyExpressionException)
+  }
+
+  def "method call with wrong type of arguments (success due to 
TypeCoercer)"() {
+
+    given:
+    def conduit = propertyConduitSource.create(RootBean, 
"testBean.getStringMethodWithArg(testBean.intProperty)")
+
+    when:
+    def result = conduit.get(root)
+
+    then:
+    result == "methodResult:123"
+  }
+
+  @Unroll
+  def "invalid expression syntax '#expression' (#description)"() {
+
+    when:
+    propertyConduitSource.create(RootBean, expression)
+
+    then:
+    thrown(RuntimeException)
+
+    where:
+    expression | description
+    "a.b..c"   | "invalid '..' syntax"
+    "foo."     | "trailing dot"
+    ".foo"     | "leading dot not part of number"
+    "a b c"    | "unparseable sequence"
+    "foo("     | "unmatched parenthesis"
+    "{'a':1,}" | "trailing comma in map"
+    "[1,2,]"   |  "trailing comma in list"
+  }
+}

Reply via email to