You are viewing a plain text version of this content. The canonical link for it is here.
Posted to dev@tapestry.apache.org by hl...@apache.org on 2012/06/21 19:28:19 UTC
[5/9] Refactor all the tapestry-ioc Spock specifications into the
ioc.specs package Convert some package-private classes and constructors to
public
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ModuleImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ModuleImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ModuleImplSpec.groovy
new file mode 100644
index 0000000..0479626
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ModuleImplSpec.groovy
@@ -0,0 +1,280 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.AdvisorDef2
+import org.apache.tapestry5.ioc.BlueMarker
+import org.apache.tapestry5.ioc.RedMarker
+import org.apache.tapestry5.ioc.services.PlasticProxyFactory
+import org.slf4j.Logger
+import spock.lang.Specification
+import org.apache.tapestry5.ioc.def.*
+import org.apache.tapestry5.ioc.internal.*
+
+class ModuleImplSpec extends Specification {
+
+ Logger logger = Mock()
+ InternalRegistry registry = Mock()
+ PlasticProxyFactory proxyFactory = Mock()
+ ServiceActivityTracker tracker = Mock()
+
+ def "findServiceIdsForInterface() test"() {
+
+ ModuleDef md = new DefaultModuleDefImpl(ModuleImplTestModule, logger, proxyFactory)
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ def serviceIds = module.findServiceIdsForInterface(FieService)
+
+ then:
+
+ serviceIds.size() == 2
+ serviceIds.containsAll(["Fie", "OtherFie"])
+ }
+
+ def "findMatchingDecoratorDefs() with exact DecoratorDef match"() {
+ ServiceDef sd = Mock()
+ DecoratorDef2 def1 = Mock()
+ DecoratorDef def2 = Mock()
+ ModuleDef md = Mock()
+
+ def decoratorDefs = [def1, def2] as Set
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ then:
+
+ 1 * md.serviceIds >> Collections.EMPTY_SET
+
+ when:
+
+ def matches = module.findMatchingDecoratorDefs(sd)
+
+ then:
+
+ matches.size() == 1
+ matches.contains def2
+
+ 1 * md.decoratorDefs >> decoratorDefs
+
+ 1 * sd.serviceInterface >> Runnable
+
+ 1 * def1.matches(sd) >> false
+
+ // Maybe not a complete match, so does it match by type & markers?
+ 1 * def1.serviceInterface >> ToStringService
+
+ // An exact match
+ 1 * def2.matches(sd) >> true
+
+ 0 * _
+ }
+
+ def "findDecoratorDefs() with matching service but non-matching marker annotations"() {
+ ServiceDef sd = Mock()
+ DecoratorDef2 def1 = Mock()
+ DecoratorDef def2 = Mock()
+ ModuleDef md = Mock()
+
+ def decoratorDefs = [def1, def2] as Set
+ def def1markers = [BlueMarker] as Set
+ def sdmarkers = [RedMarker] as Set
+ def registrymarkers = [RedMarker, BlueMarker] as Set
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ then:
+
+ 1 * md.serviceIds >> Collections.EMPTY_SET
+
+ when:
+
+ def matches = module.findMatchingDecoratorDefs(sd)
+
+ then:
+
+ matches.size() == 1
+ matches.contains def2
+
+ 1 * md.decoratorDefs >> decoratorDefs
+
+ 1 * def1.matches(sd) >> false
+ 1 * def1.serviceInterface >> Object
+ _ * sd.serviceInterface >> Runnable
+ 1 * def1.markers >> def1markers
+ 1 * sd.markers >> sdmarkers
+
+ 1 * def2.matches(sd) >> true
+
+ 1 * registry.markerAnnotations >> registrymarkers
+
+ 0 * _
+ }
+
+ def "findMatchingServiceAdvisors() where the advise is for a different interface than the service"() {
+ AdvisorDef2 def1 = Mock()
+ AdvisorDef2 def2 = Mock()
+ ModuleDef2 md = Mock()
+ ServiceDef sd = Mock()
+
+ def advisors = [def1, def2] as Set
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ then:
+
+ 1 * md.serviceIds >> Collections.EMPTY_SET
+
+ when:
+
+ def matches = module.findMatchingServiceAdvisors(sd)
+
+ then:
+
+ matches.size() == 1
+ matches.contains def2
+
+ 1 * md.advisorDefs >> advisors
+
+ 1 * def1.matches(sd) >> false
+ 1 * def1.serviceInterface >> ToStringService
+
+ 1 * sd.serviceInterface >> Runnable
+
+ 1 * def2.matches(sd) >> true
+
+ 0 * _
+ }
+
+ def "findMatchingServiceAdvisors() where the advice is for a matching service type but non-matching marker annotations"() {
+ AdvisorDef2 def1 = Mock()
+ AdvisorDef2 def2 = Mock()
+ ModuleDef2 md = Mock()
+ ServiceDef sd = Mock()
+
+ def advisors = [def1, def2] as Set
+ def def1markers = [BlueMarker] as Set
+ def registrymarkers = [BlueMarker, RedMarker] as Set
+ def servicemarkers = [RedMarker] as Set
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ then:
+
+ 1 * md.serviceIds >> Collections.EMPTY_SET
+
+ when:
+
+ def matches = module.findMatchingServiceAdvisors(sd)
+
+ then:
+
+ matches.size() == 1
+ matches.contains def2
+
+ 1 * registry.markerAnnotations >> registrymarkers
+
+ 1 * md.advisorDefs >> advisors
+
+ 1 * def1.matches(sd) >> false
+ 1 * def1.serviceInterface >> Object
+
+ 1 * sd.serviceInterface >> Runnable
+ 1 * sd.markers >> servicemarkers
+
+ 1 * def1.markers >> def1markers
+
+ 1 * def2.matches(sd) >> true
+
+ 0 * _
+ }
+
+ def "findMatchingServiceAdvisors() match on type and marker annotations"() {
+ AdvisorDef2 ad = Mock()
+ ModuleDef2 md = Mock()
+ ServiceDef sd = Mock()
+
+ def advisors = [ad] as Set
+ def admarkers = [RedMarker] as Set
+ def registrymarkers = [BlueMarker, RedMarker] as Set
+ def servicemarkers = [RedMarker] as Set
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ then:
+
+ 1 * md.serviceIds >> Collections.EMPTY_SET
+
+ when:
+
+ def matches = module.findMatchingServiceAdvisors(sd)
+
+ then:
+
+ matches.size() == 1
+ matches.contains ad
+
+ 1 * registry.markerAnnotations >> registrymarkers
+
+ 1 * md.advisorDefs >> advisors
+
+ 1 * ad.matches(sd) >> false
+ 1 * ad.serviceInterface >> Object
+
+ 1 * sd.serviceInterface >> Runnable
+ 1 * sd.markers >> servicemarkers
+
+ 1 * ad.markers >> admarkers
+
+ 0 * _
+ }
+
+
+ def "findMatchingServiceAdvisors() where there are no marker annotations at all"() {
+ AdvisorDef2 ad = Mock()
+ ModuleDef2 md = Mock()
+ ServiceDef sd = Mock()
+
+ def advisors = [ad] as Set
+
+ when:
+
+ Module module = new ModuleImpl(registry, tracker, md, proxyFactory, logger)
+
+ then:
+
+ 1 * md.serviceIds >> Collections.EMPTY_SET
+
+ when:
+
+ def matches = module.findMatchingServiceAdvisors(sd)
+
+ then:
+
+ matches.size() == 0
+
+ 1 * registry.markerAnnotations >> Collections.EMPTY_SET
+
+ 1 * md.advisorDefs >> advisors
+
+ 1 * ad.matches(sd) >> false
+ 1 * ad.serviceInterface >> Object
+
+ 1 * sd.serviceInterface >> Runnable
+
+ 1 * ad.markers >> Collections.EMPTY_SET
+
+ 0 * _
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ModuleInstantiationSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ModuleInstantiationSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ModuleInstantiationSpec.groovy
new file mode 100644
index 0000000..006e70b
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ModuleInstantiationSpec.groovy
@@ -0,0 +1,55 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.NameListHolder
+import org.apache.tapestry5.ioc.StaticModule
+
+class ModuleInstantiationSpec extends AbstractRegistrySpecification {
+
+ def setup() {
+ StaticModule.reset()
+ }
+
+ def "module class is not instantiated when invoking static builder method"() {
+ buildRegistry StaticModule
+
+ def fred = getService "Fred", Runnable
+
+ when:
+
+ fred.run()
+
+ then:
+
+ !StaticModule.instantiated
+ StaticModule.fredRan
+ }
+
+ def "module class is not instantiated when invoking static decorator method"() {
+ buildRegistry StaticModule
+
+ def barney = getService "Barney", Runnable
+
+ when:
+
+ barney.run()
+
+ then:
+
+ !StaticModule.instantiated
+ StaticModule.decoratorRan
+ }
+
+ def "module class is not instantiated when invoking a static contributor method"() {
+ buildRegistry StaticModule
+
+ def holder = getService "Names", NameListHolder
+
+ when:
+
+ assert holder.names == ["Fred"]
+
+ then:
+
+ !StaticModule.instantiated
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/NonParallelExecutorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/NonParallelExecutorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/NonParallelExecutorSpec.groovy
new file mode 100644
index 0000000..65e12de
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/NonParallelExecutorSpec.groovy
@@ -0,0 +1,59 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Invokable
+import org.apache.tapestry5.ioc.Registry
+import org.apache.tapestry5.ioc.RegistryBuilder
+import org.apache.tapestry5.ioc.internal.services.NonParallelModule
+import org.apache.tapestry5.ioc.services.ParallelExecutor
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+class NonParallelExecutorSpec extends Specification {
+
+ @Shared
+ @AutoCleanup("shutdown")
+ private Registry registry
+
+ @Shared
+ private ParallelExecutor executor
+
+ def setupSpec() {
+ registry = new RegistryBuilder().add(NonParallelModule).build()
+
+ executor = registry.getService ParallelExecutor
+ }
+
+ def "passing an Invokable will immediately invoke()"() {
+
+ Invokable inv = Mock()
+
+ when:
+
+ def actual = executor.invoke(String, inv)
+
+ then:
+
+ actual == "value"
+
+ 1 * inv.invoke() >> "value"
+ }
+
+ def "A returned Future object is a simple wrapper around the result"() {
+ Invokable inv = Mock()
+
+ when:
+
+ def future = executor.invoke(inv)
+
+ then:
+
+ 1 * inv.invoke() >> "right now"
+
+ !future.cancel(false)
+ !future.cancelled
+ future.done
+ future.get() == "right now"
+ future.get(0, null) == "right now"
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/OneShotLockSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/OneShotLockSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/OneShotLockSpec.groovy
new file mode 100644
index 0000000..5fea3c5
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/OneShotLockSpec.groovy
@@ -0,0 +1,42 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.util.OneShotLockSubject
+import spock.lang.Specification
+
+class OneShotLockSpec extends Specification {
+
+ def subject = new OneShotLockSubject()
+
+ def "may only invoke locked method once"() {
+ subject.go()
+ subject.done()
+
+
+ when:
+
+ subject.go()
+
+ then:
+
+ IllegalStateException e = thrown()
+
+ e.message.contains "${subject.class.name}.go("
+ e.message.contains "may no longer be invoked"
+ }
+
+ def "the method that locks is itself checked"() {
+
+ subject.go()
+ subject.done()
+
+ when:
+
+ subject.done()
+
+ then:
+
+ IllegalStateException e = thrown()
+
+ e.message.contains "${subject.class.name}.done("
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/OperationAdvisorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/OperationAdvisorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/OperationAdvisorSpec.groovy
new file mode 100644
index 0000000..819a4a7
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/OperationAdvisorSpec.groovy
@@ -0,0 +1,87 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.OperationTracker
+import org.apache.tapestry5.ioc.internal.DefaultModuleDefImpl
+import org.apache.tapestry5.ioc.internal.LoggerSourceImpl
+import org.apache.tapestry5.ioc.internal.RegistryImpl
+import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl
+import org.apache.tapestry5.ioc.services.OperationTrackedModule
+import org.apache.tapestry5.ioc.services.OperationTrackedService
+import org.apache.tapestry5.ioc.services.PlasticProxyFactory
+import org.apache.tapestry5.ioc.services.TapestryIOCModule
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Specification
+
+class OperationAdvisorSpec extends Specification {
+
+ @Shared @AutoCleanup("shutdown")
+ def registry
+
+ @Shared
+ def operations = []
+
+ def setupSpec() {
+
+ def classLoader = Thread.currentThread().contextClassLoader
+ def loggerSource = new LoggerSourceImpl()
+
+ def logger = loggerSource.getLogger(OperationAdvisorSpec)
+ def proxyFactoryLogger = loggerSource.getLogger(PlasticProxyFactory)
+
+ def plasticProxyFactory = new PlasticProxyFactoryImpl(classLoader, proxyFactoryLogger)
+
+ def simpleOperationTracker = [
+
+ run: { description, operation ->
+ operations << description
+ operation.run()
+ },
+
+ invoke: {description, operation ->
+ operations << description
+ operation.invoke()
+ }
+ ] as OperationTracker
+
+ registry = new RegistryImpl([
+ new DefaultModuleDefImpl(TapestryIOCModule, logger, plasticProxyFactory),
+ new DefaultModuleDefImpl(OperationTrackedModule, logger, plasticProxyFactory)],
+ plasticProxyFactory,
+ loggerSource,
+ simpleOperationTracker)
+ }
+
+ def "simple operation tracking"() {
+ def service = registry.getService OperationTrackedService
+
+ service.nonOperation()
+
+ when:
+
+ operations.clear()
+
+ service.first()
+
+ then:
+
+ operations == ["First operation"]
+ }
+
+ def "complex operation tracking"() {
+ def service = registry.getService OperationTrackedService
+
+ service.nonOperation()
+
+ operations.clear()
+
+ when:
+
+ service.second "foo"
+ service.second "bar"
+
+ then:
+
+ operations == ["Second operation: foo", "Second operation: bar"]
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/OrderedConstraintBuilderSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/OrderedConstraintBuilderSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/OrderedConstraintBuilderSpec.groovy
new file mode 100644
index 0000000..448eca4
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/OrderedConstraintBuilderSpec.groovy
@@ -0,0 +1,31 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.OrderConstraintBuilder
+import spock.lang.Specification
+
+class OrderedConstraintBuilderSpec extends Specification {
+
+ /** Any unrecognized methods are evaluated against {@link org.apache.tapestry5.ioc.OrderConstraintBuilder}. */
+ def methodMissing(String name, args) {
+ OrderConstraintBuilder."$name"(* args)
+ }
+
+ def "constraint ordering"() {
+ expect:
+
+ constraint.build() == values
+
+ where:
+
+ constraint || values
+
+ after("A") || ["after:A"]
+ afterAll() || ["after:*"]
+ before("B") || ["before:B"]
+ beforeAll() || ["before:*"]
+
+ before("A").after("B") || ["before:A", "after:B"]
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/OrdererSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/OrdererSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/OrdererSpec.groovy
new file mode 100644
index 0000000..f728b88
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/OrdererSpec.groovy
@@ -0,0 +1,285 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Orderable
+import org.apache.tapestry5.ioc.internal.util.Orderer
+import org.apache.tapestry5.ioc.internal.util.UtilMessages
+import org.slf4j.Logger
+import spock.lang.Specification
+
+class OrdererSpec extends Specification {
+
+ Logger logger = Mock()
+
+ def "the order of the values is unchanged when there are no dependencies"() {
+
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ orderer.ordered == ["FRED", "BARNEY", "WILMA", "BETTY"]
+ }
+
+ def "an override can change order and value"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+
+ override "barney", "Mr. Rubble", "before:*"
+ }
+
+ then:
+
+ orderer.ordered == ["Mr. Rubble", "FRED", "WILMA", "BETTY"]
+ }
+
+ def "an override must match a previously added id"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+
+ override "bambam", "Mr. Rubble JR.", "before:*"
+ }
+
+ then:
+
+ IllegalArgumentException e = thrown()
+
+ e.message == "Override for object 'bambam' is invalid as it does not match an existing object."
+ }
+
+ def "a missing constraint type is logged as a warning"() {
+
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY", "fred"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ logger.warn(UtilMessages.constraintFormat("fred", "barney"))
+
+ orderer.ordered == ["FRED", "BARNEY", "WILMA", "BETTY"]
+ }
+
+ def "an unknown constraint type is logged as a warning"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY", "nearby:fred"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ logger.warn(UtilMessages.constraintFormat("nearby:fred", "barney"))
+
+ orderer.ordered == ["FRED", "BARNEY", "WILMA", "BETTY"]
+ }
+
+ def "null values are not included in the result"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY"
+ add "zippo", null
+ add "wilma", "WILMA"
+ add "groucho", null
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ orderer.ordered == ["FRED", "BARNEY", "WILMA", "BETTY"]
+ }
+
+ def "duplicate ids are ignored"() {
+ def orderer = new Orderer(logger)
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY"
+ add "wilma", "WILMA"
+ }
+
+ when:
+
+ orderer.add("Fred", "Fred 2")
+
+ then:
+
+ // Notice it uses the previously added id, whose case is considered canonical
+ logger.warn(UtilMessages.duplicateOrderer("fred"))
+
+ when:
+
+ orderer.add "betty", "BETTY"
+
+ then:
+
+ orderer.ordered == ["FRED", "BARNEY", "WILMA", "BETTY"]
+ }
+
+ def "the special before:* moves the value to the front of the list"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY", "before:*"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ orderer.ordered == ["BARNEY", "FRED", "WILMA", "BETTY"]
+ }
+
+ def "the special after:* moves the value to the end of the list"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED"
+ add "barney", "BARNEY", "after:*"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ // A number of factors can twiddle the order of the other elements, so we just check the last
+ orderer.ordered[3] == "BARNEY"
+ }
+
+ def "use lists of pre-requisites (after:)"() {
+
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED", "after:wilma"
+ add "barney", "BARNEY", "after:fred,betty"
+ add "wilma", "WILMA"
+ add "betty", "BETTY"
+ }
+
+ then:
+
+ orderer.ordered == ["WILMA", "FRED", "BETTY", "BARNEY"]
+ }
+
+ def "use both pre- and post-requisites (before: and after:)"() {
+
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED", "after:wilma"
+ add "barney", "BARNEY", "after:fred,betty"
+ add "wilma", "WILMA"
+ add "betty", "BETTY", "before:wilma"
+ }
+
+ then:
+
+ orderer.ordered == ["BETTY", "WILMA", "FRED", "BARNEY"]
+ }
+
+ def "pre- and post-requisites are case-insensitive"() {
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED", "after:WILMA"
+ add "barney", "BARNEY", "after:fred,BETTY"
+ add "wilma", "WILMA"
+ add "betty", "BETTY", "before:Wilma"
+ }
+
+ then:
+
+ orderer.ordered == ["BETTY", "WILMA", "FRED", "BARNEY"]
+ }
+
+ def "dependency cycles are identified and logged as warnings"() {
+
+ def orderer = new Orderer(logger)
+
+ when:
+
+ orderer.with {
+ add "fred", "FRED", "after:wilma"
+ add "barney", "BARNEY", "after:fred,betty"
+ add "wilma", "WILMA"
+ add "betty", "BETTY", "before:Wilma", "after:barney"
+ }
+
+ def ordered = orderer.ordered
+
+ then:
+
+ 1 * logger.warn("Unable to add 'barney' as a dependency of 'betty', as that forms a dependency cycle ('betty' depends on itself via 'barney'). The dependency has been ignored.")
+
+
+ ordered == ["BETTY", "WILMA", "FRED", "BARNEY"]
+ }
+
+ def "Orderable has a useful toString()"() {
+
+ when:
+
+ def simple = new Orderable("simple", "SIMPLE")
+
+ then:
+
+ simple.toString() == "Orderable[simple SIMPLE]"
+
+ when:
+
+ def complex = new Orderable("complex", "COMPLEX", "after:foo", "before:bar")
+
+ then:
+
+ complex.toString() == "Orderable[complex after:foo before:bar COMPLEX]"
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ParallelExecutorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ParallelExecutorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ParallelExecutorSpec.groovy
new file mode 100644
index 0000000..6a3e632
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ParallelExecutorSpec.groovy
@@ -0,0 +1,104 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Invokable
+import org.apache.tapestry5.ioc.StringHolder
+import org.apache.tapestry5.ioc.StringHolderImpl
+import org.apache.tapestry5.ioc.services.ParallelExecutor
+import spock.lang.Shared
+
+class ParallelExecutorSpec extends AbstractSharedRegistrySpecification {
+
+ @Shared ParallelExecutor executor
+
+ def setupSpec() {
+ executor = getService ParallelExecutor
+ }
+
+ def "thunks execute in parallel and results are cached"() {
+
+ def thunks = []
+
+ when:
+
+ 100.times { i ->
+
+ def value = "Value[$i]"
+
+ def first = true
+
+ def inv = new Invokable() {
+
+ Object invoke() {
+
+ if (!first) { throw new IllegalStateException("Result of Invokable should be cached.") }
+
+ def holder = new StringHolderImpl()
+
+ holder.value = value
+
+ Thread.sleep 10
+
+ first = false
+
+ return holder
+ }
+ }
+
+ thunks.add executor.invoke(StringHolder, inv)
+ }
+
+ then:
+
+ // Not sure how to truly prove that the results are happening in parallel.
+ // I think it's basically that by the time we work our way though the list, some values
+ // will have been computed ahead.
+
+ thunks.size().times { i ->
+
+ assert thunks[i].value == "Value[$i]"
+ }
+
+ then: "a second pass to proove that the thunk caches the result"
+
+ thunks.size().times { i ->
+
+ assert thunks[i].value == "Value[$i]"
+ }
+ }
+
+ def "toString() of a thunk indicates the interface type"() {
+
+ Invokable inv = Mock()
+
+ when:
+
+ StringHolder thunk = executor.invoke StringHolder, inv
+
+ then:
+
+ thunk.toString() == "FutureThunk[org.apache.tapestry5.ioc.StringHolder]"
+ }
+
+ def "exception inside the Invokable is rethrown by the thunk"() {
+
+ def inv = new Invokable() {
+
+ Object invoke() { throw new RuntimeException("Future failure!")}
+ }
+
+ StringHolder thunk = executor.invoke StringHolder, inv
+
+
+ when:
+
+ thunk.getValue()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "Future failure!"
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadScopeSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadScopeSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadScopeSpec.groovy
new file mode 100644
index 0000000..d79c6ce
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PerThreadScopeSpec.groovy
@@ -0,0 +1,91 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.PerThreadModule
+import org.apache.tapestry5.ioc.ScopeMismatchModule
+import org.apache.tapestry5.ioc.StringHolder
+
+class PerThreadScopeSpec extends AbstractRegistrySpecification {
+
+ def "ensure that different threads see different implementations"() {
+ def threadExecuted = false
+
+ buildRegistry PerThreadModule
+
+ def holder = getService StringHolder
+
+ when:
+
+ holder.value = "fred"
+
+ then:
+
+ holder.value == "fred"
+
+ when:
+
+ Thread t = new Thread({
+ assert holder.value == null
+
+ holder.value = "barney"
+
+ assert holder.value == "barney"
+
+ threadExecuted = true
+
+ cleanupThread()
+ })
+
+ t.start()
+ t.join()
+
+ then:
+
+ threadExecuted
+ holder.value == "fred"
+ }
+
+ def "services with out a service interface must use the default scope"() {
+
+ buildRegistry ScopeMismatchModule
+
+ when:
+
+ getService StringBuilder
+
+ then:
+
+ Exception e = thrown()
+
+ e.message.contains "Error building service proxy for service 'ScopeRequiresAProxyAndNoInterfaceIsProvided'"
+ e.message.contains "Service scope 'perthread' requires a proxy"
+ }
+
+ def "ensure that perthread services are discarded by cleanupThread()"() {
+ buildRegistry PerThreadModule
+
+ when:
+
+ def holder = getService StringHolder
+
+ then:
+
+ holder.value == null
+
+ when:
+
+ holder.value = "fred"
+
+ then:
+
+ holder.value == "fred"
+
+ when:
+
+ cleanupThread()
+
+ then:
+
+ holder.value == null
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/PeriodicExecutorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PeriodicExecutorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PeriodicExecutorSpec.groovy
new file mode 100644
index 0000000..f84521d
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PeriodicExecutorSpec.groovy
@@ -0,0 +1,29 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.services.cron.IntervalSchedule
+import org.apache.tapestry5.ioc.services.cron.PeriodicExecutor
+
+import java.util.concurrent.CountDownLatch
+import java.util.concurrent.TimeUnit
+
+class PeriodicExecutorSpec extends AbstractRegistrySpecification {
+
+ def "execution intervals"() {
+
+ buildRegistry()
+
+ def countDownLatch = new CountDownLatch(5);
+
+ def schedule = new IntervalSchedule(10)
+
+ def job = getService(PeriodicExecutor).addJob(schedule, "count incrementer", { countDownLatch.countDown(); })
+
+ countDownLatch.await 30, TimeUnit.SECONDS
+
+ cleanup:
+
+ job && job.cancel()
+
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy
new file mode 100644
index 0000000..f172728
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PerthreadManagerImplSpec.groovy
@@ -0,0 +1,183 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Invokable
+import org.apache.tapestry5.ioc.internal.services.PerthreadManagerImpl
+import org.apache.tapestry5.ioc.services.ThreadCleanupListener
+import org.slf4j.Logger
+import spock.lang.Specification
+
+class PerthreadManagerImplSpec extends Specification {
+
+ def "nothing is logged when cleaning up with no listeners"() {
+ Logger logger = Mock()
+
+ def manager = new PerthreadManagerImpl(logger)
+
+ when:
+
+ manager.cleanup()
+
+ then:
+
+ 0 * _
+ }
+
+ def "listeners will only be invoked a single time, then discarded"() {
+ Logger logger = Mock()
+ ThreadCleanupListener listener = Mock()
+
+ def manager = new PerthreadManagerImpl(logger)
+
+ when:
+
+ manager.addThreadCleanupListener(listener)
+ manager.cleanup()
+
+ then:
+
+ 1 * listener.threadDidCleanup()
+ 0 * _
+
+ when:
+
+ manager.cleanup()
+
+ then:
+
+ 0 * _
+ }
+
+ def "exceptions during thread cleanup are logged and other listeners still invoked"() {
+ RuntimeException t = new RuntimeException("Boom!")
+ Logger logger = Mock()
+ ThreadCleanupListener l1 = Mock()
+ ThreadCleanupListener l2 = Mock()
+
+ def manager = new PerthreadManagerImpl(logger)
+
+ manager.addThreadCleanupListener(l1)
+ manager.addThreadCleanupListener(l2)
+
+ when:
+
+ manager.cleanup()
+
+ then:
+
+ 1 * l1.threadDidCleanup() >> { throw t }
+ 1 * logger.warn({ it.contains "Error invoking listener"}, t)
+
+ then:
+
+ 1 * l2.threadDidCleanup()
+ 0 * _
+ }
+
+ def "PerThreadValue does not initially exist"() {
+ Logger logger = Mock()
+ def manager = new PerthreadManagerImpl(logger)
+
+ when:
+
+ def value = manager.createValue()
+
+ then:
+
+ !value.exists()
+ value.get() == null
+
+ when:
+
+ value.set(this)
+
+ then:
+
+ value.exists()
+ value.get() == this
+ }
+
+ def "PerThreadValue.get() with default returns the default value when the value does not exist"() {
+ Logger logger = Mock()
+ def manager = new PerthreadManagerImpl(logger)
+ def defaultValue = new Object()
+ def nonNull = new Object()
+
+ when:
+
+ def value = manager.createValue()
+
+ then:
+
+ value.get(defaultValue).is(defaultValue)
+
+ when:
+
+ value.set(null)
+
+ then:
+
+ value.exists()
+ value.get(defaultValue) == null
+
+ when:
+
+ value.set(nonNull)
+
+ then:
+
+ value.get(defaultValue).is(nonNull)
+ }
+
+ def "PerthreadManager.run() performs an implicit cleanup"() {
+ Logger logger = Mock()
+ ThreadCleanupListener listener = Mock()
+
+ def manager = new PerthreadManagerImpl(logger)
+ manager.addThreadCleanupListener listener
+ def value = manager.createValue()
+ def didRun = false
+
+ def runnable = {
+ didRun = true
+ value.set "bar"
+ }
+
+ when:
+
+ manager.run runnable
+
+ then:
+
+ 1 * listener.threadDidCleanup()
+
+ didRun
+ !value.exists()
+ }
+
+ def "PerthreadManager.invoke() performs an implicit cleanup"() {
+ Logger logger = Mock()
+ ThreadCleanupListener listener = Mock()
+
+ def manager = new PerthreadManagerImpl(logger)
+ manager.addThreadCleanupListener listener
+ def value = manager.createValue()
+
+ def inv = {
+ value.set "bar"
+ return "baz"
+ } as Invokable
+
+ when:
+
+ assert manager.invoke(inv) == "baz"
+
+ then:
+
+ 1 * listener.threadDidCleanup()
+
+ !value.exists()
+
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/PipelineBuilderImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PipelineBuilderImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PipelineBuilderImplSpec.groovy
new file mode 100644
index 0000000..2748e61
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PipelineBuilderImplSpec.groovy
@@ -0,0 +1,87 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.StandardFilter
+import org.apache.tapestry5.ioc.internal.services.StandardService
+import org.apache.tapestry5.ioc.services.PipelineBuilder
+import org.slf4j.Logger
+import spock.lang.Shared
+
+class PipelineBuilderImplSpec extends AbstractSharedRegistrySpecification {
+
+ @Shared
+ PipelineBuilder builder
+
+ def setupSpec() { builder = getService PipelineBuilder }
+
+ Logger logger = Mock()
+
+ def "standard pipeline with filters"() {
+
+ // For some reason, this didn't work with closures, just with actual inner classes
+
+ StandardFilter subtracter = new StandardFilter() {
+
+ @Override
+ int run(int i, StandardService service) {
+ service.run(i) - 2
+ }
+ }
+
+ StandardFilter multiplier = new StandardFilter() {
+
+ @Override
+ int run(int i, StandardService service) {
+ 2 * service.run(i)
+ }
+ }
+
+ StandardFilter adder = new StandardFilter() {
+
+ @Override
+ int run(int i, StandardService service) {
+ service.run(i + 3)
+ }
+ }
+
+ StandardService terminator = new StandardService() {
+
+ @Override
+ int run(int i) {
+ i
+ }
+ }
+
+ when:
+
+ StandardService pipeline = builder.build logger, StandardService, StandardFilter, [subtracter, multiplier, adder], terminator
+
+ then:
+
+ pipeline.run(5) == 14
+ pipeline.run(10) == 24
+ }
+
+ def "a pipeline without filters is simply the temrinator"() {
+
+ StandardService terminator = Mock()
+
+ when:
+
+ StandardService pipeline = builder.build logger, StandardService, StandardFilter, [], terminator
+
+ then:
+
+ pipeline.is terminator
+ }
+
+ def "a pipeline with no filters and no terminator does nothing"() {
+ when:
+
+ StandardService pipeline = builder.build logger, StandardService, StandardFilter, []
+
+ then:
+
+ pipeline.run(99) == 0
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/PropertyAccessImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PropertyAccessImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PropertyAccessImplSpec.groovy
new file mode 100644
index 0000000..f93c3fc
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PropertyAccessImplSpec.groovy
@@ -0,0 +1,709 @@
+package ioc.specs
+
+import org.apache.tapestry5.beaneditor.DataType
+import org.apache.tapestry5.beaneditor.Validate
+import org.apache.tapestry5.ioc.annotations.Scope
+import org.apache.tapestry5.ioc.internal.util.Pair
+import org.apache.tapestry5.ioc.internal.util.StringLongPair
+import org.apache.tapestry5.ioc.services.ClassPropertyAdapter
+import org.apache.tapestry5.ioc.services.PropertyAccess
+
+import java.awt.Image
+import java.lang.reflect.Method
+
+import org.apache.tapestry5.ioc.internal.services.*
+import spock.lang.*
+
+import java.beans.*
+
+class ExceptionBean {
+
+ boolean getFailure() {
+ throw new RuntimeException("getFailure");
+ }
+
+ void setFailure(boolean b) {
+ throw new RuntimeException("setFailure");
+ }
+
+ @Override
+ String toString() {
+ return "PropertyAccessImplSpecBean";
+ }
+}
+
+class UglyBean {
+}
+
+class UglyBeanBeanInfo implements BeanInfo {
+
+ BeanInfo[] getAdditionalBeanInfo() {
+ return new BeanInfo[0];
+ }
+
+ BeanDescriptor getBeanDescriptor() {
+ return null;
+ }
+
+ int getDefaultEventIndex() {
+ return 0;
+ }
+
+ int getDefaultPropertyIndex() {
+ return 0;
+ }
+
+ EventSetDescriptor[] getEventSetDescriptors() {
+ return new EventSetDescriptor[0];
+ }
+
+ Image getIcon(int iconKind) {
+ return null;
+ }
+
+ MethodDescriptor[] getMethodDescriptors() {
+ return new MethodDescriptor[0];
+ }
+
+ PropertyDescriptor[] getPropertyDescriptors() {
+ throw new RuntimeException("This is the UglyBean.");
+ }
+
+}
+
+class ScalaBean {
+
+ private String value;
+
+ String getValue() {
+ return value;
+ }
+
+ void setValue(String value) {
+ this.value = value;
+ }
+
+ String value() {
+ return value;
+ }
+
+ void value_$eq(String value) {
+ this.value = value;
+ }
+}
+
+class ScalaClass {
+
+ private String value;
+
+ String value() {
+ return value;
+ }
+
+ void value_$eq(String value) {
+ this.value = value;
+ }
+}
+
+interface BeanInterface {
+
+ String getValue();
+
+ void setValue(String v);
+
+ String getOtherValue();
+
+ void setOtherValue(String v);
+
+ int getIntValue(); // read-only
+}
+
+abstract class AbstractBean implements BeanInterface {
+ // abstract class implements method from interface
+ private String other;
+
+ String getOtherValue() {
+ return other;
+ }
+
+ void setOtherValue(String v) {
+ other = v;
+ }
+}
+
+class ConcreteBean extends AbstractBean {
+
+ private String value;
+ private int intValue;
+
+ ConcreteBean(int intValue) {
+ this.intValue = intValue;
+ }
+
+ String getValue() {
+ return value;
+ }
+
+ void setValue(String v) {
+ value = v;
+ }
+
+ int getIntValue() {
+ return intValue;
+ }
+}
+
+abstract class GenericBean<T> {
+
+ public T value;
+}
+
+class GenericStringBean extends GenericBean<String> {
+}
+
+class PropertyAccessImplSpec extends Specification {
+
+ @Shared
+ PropertyAccess access = new PropertyAccessImpl()
+
+ @Shared
+ Random random = new Random()
+
+ def "simple read access to a standard bean"() {
+ Bean b = new Bean()
+ int value = random.nextInt()
+
+ when:
+
+ b.value = value
+
+ then:
+
+ access.get(b, "value") == value
+ }
+
+ def "property name access is case insensitive"() {
+ Bean b = new Bean()
+ int value = random.nextInt()
+
+ when:
+
+ b.value = value
+
+ then:
+
+ access.get(b, "VaLUe") == value
+ }
+
+ def "simple write access to a standard bean"() {
+ Bean b = new Bean()
+ int value = random.nextInt()
+
+ when:
+
+ access.set(b, "value", value)
+
+ then:
+
+ b.value == value
+ }
+
+ def "missing properties are an exception"() {
+ Bean b = new Bean()
+
+ when:
+
+ access.get(b, "zaphod")
+
+ then:
+
+ IllegalArgumentException e = thrown()
+
+ e.message == "Class ${b.class.name} does not contain a property named 'zaphod'."
+ }
+
+ def "it is not possible to update a read-only property"() {
+ Bean b = new Bean()
+
+ when:
+
+ access.set(b, "class", null)
+
+ then:
+
+ UnsupportedOperationException e = thrown()
+
+ e.message == "Class ${b.class.name} does not provide a mutator ('setter') method for property 'class'."
+ }
+
+ def "it is not possible to read a write-only property"() {
+ Bean b = new Bean()
+
+ when:
+
+ access.get(b, "writeOnly")
+
+ then:
+
+ UnsupportedOperationException e = thrown()
+
+ e.message == "Class ${b.class.name} does not provide an accessor ('getter') method for property 'writeOnly'."
+ }
+
+ def "when a getter method throws an exception, the exception is wrapped and rethrown"() {
+
+ ExceptionBean b = new ExceptionBean()
+
+ when:
+
+ access.get(b, "failure")
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message == "Error reading property 'failure' of ${b}: getFailure"
+ }
+
+ def "when a setter method throws an exception, the exception is wrapped and rethrown"() {
+ ExceptionBean b = new ExceptionBean()
+
+ when:
+
+ access.set(b, "failure", false)
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message == "Error updating property 'failure' of ${b.class.name}: setFailure"
+ }
+
+ @Ignore
+ def "exception throw when introspecting the class is wrapped and rethrown"() {
+
+ // Due to Groovy, the exception gets thrown here, not inside
+ // the access.get() method, thus @Ingore (for now)
+
+ UglyBean b = new UglyBean()
+
+ when:
+
+ access.get(b, "google")
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message == "java.lang.RuntimeException: This is the UglyBean."
+ }
+
+ def "clearCache() wipes internal cache"() {
+ when:
+
+ ClassPropertyAdapter cpa1 = access.getAdapter Bean
+
+ then:
+
+ cpa1.is(access.getAdapter(Bean))
+
+
+ when:
+
+ access.clearCache()
+
+ then:
+
+ !cpa1.is(access.getAdapter(Bean))
+ }
+
+ def "ClassPropertyAdapter has a useful toString()"() {
+
+ when:
+
+ def cpa = access.getAdapter Bean
+
+ then:
+
+ cpa.toString() == "<ClassPropertyAdaptor ${Bean.class.name}: PI, class, readOnly, value, writeOnly>"
+ }
+
+ @Unroll
+ def "expected properties for #beanClass.name property '#propertyName' are read=#read, update=#update, castRequired=#castRequired"() {
+
+ when:
+
+ def pa = getPropertyAdapter beanClass, propertyName
+
+ then:
+
+ pa.read == read
+ pa.update == update
+ pa.castRequired == castRequired
+ pa.writeMethod == writeMethod
+ pa.readMethod == readMethod
+
+ where:
+
+ beanClass | propertyName | read | update | castRequired | writeMethodName | readMethodName
+ Bean | "readOnly" | true | false | false | null | "getReadOnly"
+ Bean | "writeOnly" | false | true | false | "setWriteOnly" | null
+ Bean | "pi" | true | false | false | null | null
+
+ writeMethod = findMethod beanClass, writeMethodName
+ readMethod = findMethod beanClass, readMethodName
+ }
+
+ def "PropertyAdapter for unknown property name is null"() {
+ when:
+
+ ClassPropertyAdapter cpa = access.getAdapter(Bean)
+
+ then:
+
+ cpa.getPropertyAdapter("google") == null
+ }
+
+ @Unroll
+ def "PropertyAdapter.type for #beanClass.name property '#propertyName' is #type.name"() {
+
+ ClassPropertyAdapter cpa = access.getAdapter(beanClass)
+
+ when:
+
+ def adapter = cpa.getPropertyAdapter(propertyName)
+
+ then:
+
+ adapter.type.is(type)
+
+ where:
+
+ beanClass | propertyName | type
+ Bean | "value" | int
+ Bean | "readOnly" | String
+ Bean | "writeOnly" | boolean
+ }
+
+ def "ClassPropertyAdapter gives access to property names (in sorted order)"() {
+ ClassPropertyAdapter cpa = access.getAdapter(Bean)
+
+ expect:
+
+ cpa.propertyNames == ["PI", "class", "readOnly", "value", "writeOnly"]
+ }
+
+ def "public static fields are treated as properties"() {
+ when:
+
+ def adapter = getPropertyAdapter Bean, "pi"
+
+ then:
+
+ adapter.get(null).is(Bean.PI)
+ }
+
+ def "public final static fields may not be updated"() {
+ def adapter = getPropertyAdapter Bean, "pi"
+
+ when:
+
+ adapter.set(null, 3.0d)
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "final"
+ e.message.contains "PI"
+ }
+
+ def "super interface methods are inherited by sub-interface"() {
+ when:
+
+ ClassPropertyAdapter cpa = access.getAdapter SubInterface
+
+ then:
+
+ cpa.propertyNames == ["grandParentProperty", "parentProperty", "subProperty"]
+ }
+
+ def "indexed properties are ignored"() {
+ when:
+
+ ClassPropertyAdapter cpa = access.getAdapter BeanWithIndexedProperty
+
+ then:
+
+ cpa.propertyNames == ["class", "primitiveProperty"]
+ }
+
+ def "getAnnotation() when annotation is not present is null"() {
+
+ when:
+
+ def pa = getPropertyAdapter AnnotatedBean, "readWrite"
+
+ then:
+
+ pa.getAnnotation(Scope) == null
+ }
+
+ def "getAnnotation() with annotation on setter method"() {
+
+ when:
+
+ def pa = getPropertyAdapter AnnotatedBean, "annotationOnWrite"
+
+ then:
+
+ pa.getAnnotation(Scope).value() == "onwrite"
+ }
+
+ def "annotation on getter method overrides annotation on setter method"() {
+ def pa = getPropertyAdapter AnnotatedBean, "annotationOnRead"
+
+ when:
+
+ Scope annotation = pa.getAnnotation(Scope)
+
+ then:
+
+ annotation.value() == "onread"
+ }
+
+ def "getAnnotation() works on read-only properties, skipping the missing setter method"() {
+
+ when:
+
+ def pa = getPropertyAdapter AnnotatedBean, "readOnly"
+
+ then:
+
+ pa.getAnnotation(Scope) == null
+ }
+
+ def "annotations directly on fields are located"() {
+ when:
+
+ def pa = access.getAdapter(Bean).getPropertyAdapter("value")
+
+ then:
+
+ pa.getAnnotation(DataType).value() == "fred"
+ }
+
+ @Issue("TAPESTY-2448")
+ def "getAnnotation() will find annotations from an inherited field in a super-class"() {
+ when:
+
+ def pa = getPropertyAdapter BeanSubclass, "value"
+
+ then:
+
+ pa.getAnnotation(DataType).value() == "fred"
+ }
+
+ def "annotations on a getter or setter method override annotations on the field"() {
+ when:
+
+ def pa = getPropertyAdapter Bean, "value"
+
+ then:
+
+ pa.getAnnotation(Validate).value() == "getter-value-overrides"
+ }
+
+ def "PropertyAdapter.type understands (simple) generic signatures"() {
+ def cpa1 = access.getAdapter(StringLongPair)
+
+ when:
+
+ def key = cpa1.getPropertyAdapter("key")
+
+ then:
+
+ key.type == String
+ key.castRequired
+ key.declaringClass == Pair
+
+ when:
+
+ def value = cpa1.getPropertyAdapter("value")
+
+ then:
+
+ value.type == Long
+ value.castRequired
+
+ when:
+
+ def cpa2 = access.getAdapter(Pair)
+ def pkey = cpa2.getPropertyAdapter("key")
+
+ then:
+
+ pkey.type == Object
+ !pkey.castRequired
+
+ when:
+
+ def pvalue = cpa2.getPropertyAdapter("value")
+
+ then:
+
+ pvalue.type == Object
+ !pvalue.castRequired
+ }
+
+ def "PropertyAdapter prefers JavaBeans property method names to Scala method names"() {
+ when:
+
+ def pa = getPropertyAdapter ScalaBean, "value"
+
+ then:
+
+ pa.readMethod.name == "getValue"
+ pa.writeMethod.name == "setValue"
+ }
+
+ def "PropertyAdapter understands Scala accessor method naming"() {
+ when:
+
+ def pa = getPropertyAdapter ScalaClass, "value"
+
+ then:
+
+ pa.readMethod.name == "value"
+ pa.writeMethod.name == 'value_$eq'
+ }
+
+ def "PropertyAccess exposes public fields as if they were properties"() {
+ when:
+
+ def pa = getPropertyAdapter PublicFieldBean, "value"
+
+ then:
+
+ pa.field
+ pa.read
+ pa.update
+
+ when:
+
+ PublicFieldBean bean = new PublicFieldBean()
+
+ pa.set(bean, "fred")
+
+ then:
+
+ bean.value == "fred"
+
+ when:
+
+ bean.value = "barney"
+
+ then:
+
+ pa.get(bean) == "barney"
+ }
+
+ def "access to property is favored over public field when the names are the same"() {
+ def bean = new ShadowedPublicFieldBean()
+
+ when:
+
+ def pa = getPropertyAdapter ShadowedPublicFieldBean, "value"
+
+ then:
+
+ !pa.field
+
+ when:
+
+ pa.set(bean, "fred")
+
+ then:
+
+ bean.@value == null
+
+ when:
+
+ bean.@value = "barney"
+ bean.value = "wilma"
+
+ then:
+
+ pa.get(bean) == "wilma"
+ }
+
+ def "a property defined by an unimplemented inteface method of an abstract class is accessible"() {
+ AbstractBean bean = new ConcreteBean(33)
+ def ca = access.getAdapter(AbstractBean)
+
+ when:
+
+ def va = ca.getPropertyAdapter("value")
+
+ then:
+
+ !va.field
+
+ when:
+
+ va.set(bean, "hello")
+
+ then:
+
+ va.get(bean) == "hello"
+ bean.value == "hello"
+
+ when:
+
+ def ova = ca.getPropertyAdapter("otherValue")
+
+ then:
+
+ !ova.field
+
+ when:
+
+ ova.set(bean, "other value")
+
+ then:
+
+ ova.get(bean) == "other value"
+ bean.otherValue == "other value"
+
+ when:
+
+ def iva = ca.getPropertyAdapter("intvalue")
+
+ then:
+
+ iva.get(bean) == 33
+ iva.read
+ !iva.update
+ !iva.field
+ }
+
+ def "generic field is recognized"() {
+ when:
+ def pa = getPropertyAdapter GenericStringBean, "value"
+
+ then:
+
+ pa.castRequired
+ pa.type == String
+ pa.declaringClass == GenericBean
+ }
+
+
+ def getPropertyAdapter(clazz, name) {
+ access.getAdapter(clazz).getPropertyAdapter(name)
+ }
+
+ private Method findMethod(Class beanClass, String methodName) {
+ return beanClass.methods.find { it.name == methodName }
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/PropertyShadowBuilderImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/PropertyShadowBuilderImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/PropertyShadowBuilderImplSpec.groovy
new file mode 100644
index 0000000..1f202aa
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/PropertyShadowBuilderImplSpec.groovy
@@ -0,0 +1,121 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.services.PropertyShadowBuilder
+import spock.lang.Shared
+
+interface FooService {
+
+ void foo();
+}
+
+class FooHolder {
+
+ private FooService foo;
+
+ private int count = 0;
+
+ public FooService getFoo() {
+ count++;
+
+ return foo;
+ }
+
+ public int getCount() {
+ return count;
+ }
+
+ public void setFoo(FooService foo) {
+ this.foo = foo;
+ }
+
+ @Override
+ public String toString() {
+ return "[FooHolder]";
+ }
+
+ public void setWriteOnly(FooService foo) {
+
+ }
+}
+
+class PropertyShadowBuilderImplSpec extends AbstractSharedRegistrySpecification {
+
+ @Shared
+ PropertyShadowBuilder builder
+
+ FooService foo = Mock()
+ FooHolder holder = new FooHolder();
+
+ def setupSpec() {
+ builder = getService PropertyShadowBuilder
+ }
+
+
+ def "basic delegation from proxy to property"() {
+
+ FooService shadow = builder.build(holder, "foo", FooService)
+
+ holder.foo = foo
+
+
+ when:
+
+ shadow.foo()
+
+ then:
+
+ foo.foo()
+ holder.count == 1
+
+ shadow.toString() == "<Shadow: property foo of [FooHolder]>"
+
+ when:
+
+ shadow.foo()
+
+ then:
+
+ foo.foo()
+ holder.count == 2
+ }
+
+ def "verify exception when accessing the value when null"() {
+
+ FooService shadow = builder.build(holder, "foo", FooService)
+
+ when:
+
+ shadow.foo()
+
+ then:
+
+ NullPointerException e = thrown()
+
+ e.message == "Unable to delegate method invocation to property 'foo' of [FooHolder], because the property is null."
+ }
+
+ def "property type mismatch"() {
+ when:
+
+ builder.build(holder, "count", Map)
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message == "Property 'count' of class ${FooHolder.name} is of type int, which is not assignable to type java.util.Map."
+ }
+
+ def "attempting to build for a write-only property is an exception"() {
+ when:
+
+ builder.build(holder, "writeOnly", FooService)
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message == "Class ${FooHolder.name} does not provide an accessor ('getter') method for property 'writeOnly'."
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/RecursiveServiceCreationCheckWrapperSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/RecursiveServiceCreationCheckWrapperSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/RecursiveServiceCreationCheckWrapperSpec.groovy
new file mode 100644
index 0000000..1663754
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/RecursiveServiceCreationCheckWrapperSpec.groovy
@@ -0,0 +1,82 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.ObjectCreator
+import org.apache.tapestry5.ioc.def.ServiceDef
+import org.apache.tapestry5.ioc.internal.ObjectCreatorSource
+import org.apache.tapestry5.ioc.internal.RecursiveServiceCreationCheckWrapper
+import org.apache.tapestry5.ioc.internal.ServiceDefImpl
+import org.slf4j.Logger
+
+class RecursiveServiceCreationCheckWrapperSpec extends AbstractSharedRegistrySpecification {
+
+ static DESCRIPTION = "{SOURCE DESCRIPTION}"
+
+ Logger logger = Mock()
+ ObjectCreatorSource source = Mock()
+ ObjectCreator delegate = Mock()
+ Object service = Mock()
+
+ ServiceDef sd = new ServiceDefImpl(Runnable, null, "Bar", null, "singleton", false, false, source)
+
+ def "ensure that the creator is called only once"() {
+
+ when:
+
+ ObjectCreator wrapper = new RecursiveServiceCreationCheckWrapper(sd, delegate, logger)
+
+ def actual = wrapper.createObject()
+
+ then:
+
+ actual == service
+
+ 1 * delegate.createObject() >> service
+
+ when:
+
+ wrapper.createObject()
+
+ then:
+
+ IllegalStateException e = thrown()
+
+ e.message.contains "Construction of service 'Bar' has failed due to recursion"
+ e.message.contains DESCRIPTION
+
+ 1 * source.description >> DESCRIPTION
+ }
+
+ def "construction exceptions are logged properly"() {
+
+ def t = new RuntimeException("Just cranky.")
+
+ when:
+
+ ObjectCreator wrapper = new RecursiveServiceCreationCheckWrapper(sd, delegate, logger)
+
+ wrapper.createObject()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.is(t)
+
+ 1 * delegate.createObject() >> { throw t }
+
+ 1 * logger.error("Construction of service Bar failed: ${t.message}", t)
+
+
+ when: "a subsequent call"
+
+ def actual = wrapper.createObject()
+
+ then: "the delegate is reinvoked (succesfully, this time)"
+
+ actual.is(service)
+
+ 1 * delegate.createObject() >> service
+ }
+
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/RegistryBuilderSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/RegistryBuilderSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryBuilderSpec.groovy
new file mode 100644
index 0000000..3428ece
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryBuilderSpec.groovy
@@ -0,0 +1,96 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.def.ModuleDef
+import org.apache.tapestry5.ioc.internal.DefaultModuleDefImpl
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import spock.lang.Specification
+import org.apache.tapestry5.ioc.*
+
+class RegistryBuilderSpec extends Specification {
+
+ def "@SubModule annotation is honored"() {
+ when:
+
+ Registry r = new RegistryBuilder().add(MasterModule).build()
+
+ def service = r.getService("UnorderedNames", NameListHolder)
+
+ then:
+
+ service.names == ["Beta", "Gamma", "UnorderedNames"]
+
+ cleanup:
+
+ r.shutdown()
+ }
+
+ def "adding modules by name, in comma seperated list, as from a manifest"() {
+ when:
+
+ RegistryBuilder builder = new RegistryBuilder()
+
+ IOCUtilities.addModulesInList builder,
+ "${FredModule.class.name}, ${BarneyModule.class.name}, ${RegistryBuilderTestModule.class.name}"
+
+ Registry registry = builder.build()
+
+ Square service = registry.getService(Square)
+
+ then:
+
+ service.square(4) == 16
+
+ service.toString() == "<Proxy for Square(${Square.class.name})>"
+
+ cleanup:
+
+ registry.shutdown()
+ }
+
+ def "exercise RegistryBuilder.buildAndStartupRegistry()"() {
+ when:
+
+ Registry r = RegistryBuilder.buildAndStartupRegistry(MasterModule);
+
+ NameListHolder service = r.getService("UnorderedNames", NameListHolder);
+
+ then:
+
+ service.names == ["Beta", "Gamma", "UnorderedNames"]
+
+ cleanup:
+
+ r.shutdown();
+ }
+
+ def "use explicit ModuleDef with buildAndStartupRegistry()"() {
+ when:
+
+ Logger logger = LoggerFactory.getLogger(getClass());
+
+ ModuleDef module = new DefaultModuleDefImpl(ServiceBuilderModule,
+ logger, null);
+
+ Registry r = RegistryBuilder.buildAndStartupRegistry(module, MasterModule);
+
+ NameListHolder nameListHolder = r.getService("UnorderedNames", NameListHolder);
+
+ then:
+
+ nameListHolder.names == ["Beta", "Gamma", "UnorderedNames"]
+
+ when:
+
+ Greeter greeter = r.getService("Greeter", Greeter)
+
+ then:
+
+ greeter.greeting == "Greetings from service Greeter."
+
+ cleanup:
+
+ r.shutdown();
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/RegistryConstructionAndRuntimeErrorsSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/RegistryConstructionAndRuntimeErrorsSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryConstructionAndRuntimeErrorsSpec.groovy
new file mode 100644
index 0000000..a746d07
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryConstructionAndRuntimeErrorsSpec.groovy
@@ -0,0 +1,118 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.ExtraPublicConstructorsModule
+import org.apache.tapestry5.ioc.internal.PrivateConstructorModule
+import org.apache.tapestry5.ioc.internal.UpcaseService
+import org.apache.tapestry5.ioc.*
+
+/**
+ * A few tests that are easiest (or even just possible) by building a Registry and trying out a few
+ * things.
+ */
+class RegistryConstructionAndRuntimeErrorsSpec extends AbstractRegistrySpecification {
+
+ def "duplicate service names are a failure"() {
+ when:
+
+ buildRegistry FredModule, DuplicateFredModule
+
+ then:
+
+ RuntimeException ex = thrown()
+
+ ex.message.startsWith "Service id 'Fred' has already been defined by"
+
+ // Can't check entire message, can't guarantee what order modules will be processed in
+ }
+
+ def "service with unknown scope fails at service proxy creation"() {
+ buildRegistry UnknownScopeModule
+
+ when:
+
+ getService "UnknownScope", Runnable
+
+ then:
+
+ Exception e = thrown()
+
+ e.message.contains "Error building service proxy for service 'UnknownScope'"
+ e.message.contains "Unknown service scope 'magic'"
+ }
+
+ def "ensure that recursive module construction is detected"() {
+
+ buildRegistry RecursiveConstructorModule
+
+ def runnable = getService "Runnable", Runnable
+
+ when:
+
+ // We can get the proxy, but invoking a method causes
+ // the module to be instantiated ... but that also invokes a method on
+ // the proxy.
+
+ runnable.run()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "has failed due to recursion"
+ }
+
+ def "a module class must have a public constructor"() {
+
+ buildRegistry PrivateConstructorModule
+
+ def trigger = getService "Trigger", Runnable
+
+ when:
+
+ trigger.run()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "Module class org.apache.tapestry5.ioc.internal.PrivateConstructorModule does not contain any public constructors."
+ }
+
+ def "extra public constructors on a module class are ignored"() {
+
+ buildRegistry ExtraPublicConstructorsModule
+
+ when: "forcing the module to be instantiated"
+
+ def upcase = getService UpcaseService
+
+ then: "no exception when instantiating the module"
+
+ upcase.upcase('Hello, ${fred}') == "HELLO, FLINTSTONE"
+ }
+
+ def "extra public methods on module classes are exceptions"() {
+ when:
+ buildRegistry ExtraMethodsModule
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "Module class org.apache.tapestry5.ioc.ExtraMethodsModule contains unrecognized public methods: "
+ e.message.contains "thisMethodIsInvalid()"
+ e.message.contains "soIsThisMethod()"
+ }
+
+ def "can not use withSimpleId() when binding a service interface to a ServiceBuilder callback"() {
+ when:
+
+ buildRegistry NoImplementationClassForSimpleIdModule
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "No defined implementation class to generate simple id from"
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/RegistrySpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/RegistrySpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/RegistrySpec.groovy
new file mode 100644
index 0000000..ef0bbc8
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/RegistrySpec.groovy
@@ -0,0 +1,56 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Greeter
+import org.apache.tapestry5.ioc.GreeterModule
+import org.apache.tapestry5.ioc.HelterModule
+import org.apache.tapestry5.ioc.internal.services.StartupModule2
+
+class RegistrySpec extends AbstractRegistrySpecification {
+
+ def "symbol in Registry.getService() is expanded"() {
+
+ buildRegistry GreeterModule
+
+ when:
+
+ def greeter = getService '${greeter}', Greeter
+
+ then:
+
+ greeter.greeting == "Hello"
+ greeter.toString() == "<Proxy for HelloGreeter(org.apache.tapestry5.ioc.Greeter)>"
+ }
+
+ def "circular module references are ignored"() {
+ buildRegistry HelterModule
+
+ when:
+
+ def helter = getService "Helter", Runnable
+ def skelter = getService "Skelter", Runnable
+
+ then:
+
+ !helter.is(skelter)
+ }
+
+ def "@Startup annotation support"() {
+ when:
+
+ buildRegistry StartupModule2
+
+ then:
+
+ !StartupModule2.staticStartupInvoked
+ !StartupModule2.instanceStartupInvoked
+
+ when:
+
+ performRegistryStartup()
+
+ then:
+
+ StartupModule2.staticStartupInvoked
+ StartupModule2.instanceStartupInvoked
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/RegistryStartupSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/RegistryStartupSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryStartupSpec.groovy
new file mode 100644
index 0000000..8c23fc8
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryStartupSpec.groovy
@@ -0,0 +1,97 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.RegistryBuilder
+import org.apache.tapestry5.ioc.internal.services.RegistryStartup
+import org.apache.tapestry5.ioc.internal.services.StartupModule
+import org.slf4j.Logger
+import spock.lang.Specification
+
+class RegistryStartupSpec extends Specification {
+
+
+ def "ensure that RegistryStartup service runs each of its contributed callbacks"() {
+ Runnable r1 = Mock()
+ Runnable r2 = Mock()
+ Logger logger = Mock()
+ def configuration = [r1, r2]
+
+ Runnable startup = new RegistryStartup(logger, configuration)
+
+ when:
+
+ startup.run()
+
+ then:
+
+ 1 * r1.run()
+
+ then:
+
+ 1 * r2.run()
+
+ then:
+
+ configuration.empty
+ }
+
+ def "callback failure is logged and execution continues"() {
+ Runnable r1 = Mock()
+ Runnable r2 = Mock()
+ Logger logger = Mock()
+ RuntimeException ex = new RuntimeException("Crunch!")
+
+ Runnable startup = new RegistryStartup(logger, [r1, r2])
+
+ when:
+
+ startup.run()
+
+ then:
+
+ 1 * r1.run() >> { throw ex }
+ 1 * logger.error("An exception occurred during startup: Crunch!", ex)
+ 1 * r2.run()
+ }
+
+ def "run may only be invoked once"() {
+ Logger logger = Mock()
+ Runnable startup = new RegistryStartup(logger, [])
+
+ startup.run()
+
+ when:
+
+ startup.run()
+
+ then:
+
+ IllegalStateException e = thrown()
+
+ e.message.contains "Method org.apache.tapestry5.ioc.internal.services.RegistryStartup.run"
+ e.message.contains "may no longer be invoked."
+ }
+
+ def "integration test"() {
+ when:
+
+ def registry = new RegistryBuilder().add(StartupModule).build()
+
+ then:
+
+ !StartupModule.startupInvoked
+
+ when:
+
+ registry.performRegistryStartup()
+
+ then:
+
+ StartupModule.startupInvoked
+
+ cleanup:
+
+ registry.shutdown()
+
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/RegistryshutdownHubImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/RegistryshutdownHubImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryshutdownHubImplSpec.groovy
new file mode 100644
index 0000000..debae04
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/RegistryshutdownHubImplSpec.groovy
@@ -0,0 +1,106 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.RegistryShutdownHubImpl
+import org.apache.tapestry5.ioc.services.RegistryShutdownListener
+import org.slf4j.Logger
+import spock.lang.Specification
+
+class RegistryshutdownHubImplSpec extends Specification {
+
+ RegistryShutdownHubImpl hub
+ Logger logger = Mock()
+
+ def setup() {
+ hub = new RegistryShutdownHubImpl(logger)
+ }
+
+ def "add old-style listeners and verify order"() {
+ RegistryShutdownListener l1 = Mock()
+ RegistryShutdownListener l2 = Mock()
+
+ when:
+
+ hub.addRegistryShutdownListener l1
+ hub.addRegistryShutdownListener l2
+
+ then:
+
+ 0 * _
+
+ when:
+
+ hub.fireRegistryDidShutdown()
+
+ then:
+
+ 1 * l1.registryDidShutdown()
+
+ then:
+
+ 1 * l2.registryDidShutdown()
+ 0 * _
+ }
+
+ def "will-shutdown-listeners are invoked before normal shutdown listeners"() {
+ Runnable will1 = Mock()
+ Runnable will2 = Mock()
+
+ RegistryShutdownListener l1 = Mock()
+ RegistryShutdownListener l2 = Mock()
+
+ hub.addRegistryWillShutdownListener will1
+ hub.addRegistryWillShutdownListener will2
+
+ hub.addRegistryShutdownListener l1
+ hub.addRegistryShutdownListener l2
+
+ when:
+
+ hub.fireRegistryDidShutdown()
+
+ then:
+
+ 1 * will1.run()
+
+ then:
+
+ 1 * will2.run()
+
+ then:
+
+ 1 * l1.registryDidShutdown()
+ 1 * l2.registryDidShutdown()
+ 0 * _
+ }
+
+ def "an exception during notification is logged and notification continues"() {
+ Runnable l1 = Mock()
+ Runnable l2 = Mock()
+
+ hub.addRegistryShutdownListener l1
+ hub.addRegistryShutdownListener l2
+
+ RuntimeException e = new RuntimeException("Failure.")
+
+ when:
+
+ hub.fireRegistryDidShutdown()
+
+ then:
+
+ 1 * l1.run() >> { throw e }
+ 1 * logger.error(_, _) >> { message, exception ->
+ ["Error notifying", "registry shutdown", "Failure"].each {
+ assert message.contains(it)
+ }
+
+ assert exception.is(e)
+ }
+
+ then:
+
+ 1 * l2.run()
+ 0 * _
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ReloadSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ReloadSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ReloadSpec.groovy
new file mode 100644
index 0000000..cf46a1f
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ReloadSpec.groovy
@@ -0,0 +1,403 @@
+package ioc.specs
+
+import org.apache.tapestry5.internal.plastic.PlasticInternalUtils
+import org.apache.tapestry5.internal.plastic.asm.ClassWriter
+import org.apache.tapestry5.ioc.Registry
+import org.apache.tapestry5.ioc.RegistryBuilder
+import org.apache.tapestry5.services.UpdateListenerHub
+import spock.lang.AutoCleanup
+import spock.lang.Specification
+import com.example.*
+
+import static org.apache.tapestry5.internal.plastic.asm.Opcodes.*
+
+class ReloadSpec extends Specification {
+
+ private static final String PACKAGE = "com.example";
+
+ private static final String CLASS = PACKAGE + ".ReloadableServiceImpl";
+
+ private static final String BASE_CLASS = PACKAGE + ".BaseReloadableServiceImpl";
+
+
+ @AutoCleanup("shutdown")
+ Registry registry
+
+ @AutoCleanup("deleteDir")
+ File classesDir
+
+ ClassLoader classLoader
+
+ File classFile;
+
+ def createRegistry() {
+ registry = new RegistryBuilder(classLoader).add(ReloadModule).build()
+ }
+
+ /** Any unrecognized methods are evaluated against the registry. */
+ def methodMissing(String name, args) {
+ registry."$name"(* args)
+ }
+
+
+ def setup() {
+ def uid = UUID.randomUUID().toString()
+
+ classesDir = new File(System.getProperty("java.io.tmpdir"), uid)
+
+ def classesURL = new URL("file:" + classesDir.getCanonicalPath() + "/")
+
+ classLoader = new URLClassLoader([classesURL] as URL[],
+ Thread.currentThread().contextClassLoader)
+
+ classFile = new File(classesDir, PlasticInternalUtils.toClassPath(CLASS))
+ }
+
+ def createImplementationClass(String status) {
+ createImplementationClass CLASS, status
+ }
+
+ def createImplementationClass(String className, String status) {
+
+ String internalName = PlasticInternalUtils.toInternalName className
+
+ createClassWriter(internalName, "java/lang/Object", ACC_PUBLIC).with {
+
+ // Add default constructor
+
+ visitMethod(ACC_PUBLIC, "<init>", "()V", null, null).with {
+ visitCode()
+ visitVarInsn ALOAD, 0
+ visitMethodInsn INVOKESPECIAL, "java/lang/Object", "<init>", "()V"
+ visitInsn RETURN
+ visitMaxs 1, 1
+ visitEnd()
+ }
+
+
+ visitMethod(ACC_PUBLIC, "getStatus", "()Ljava/lang/String;", null, null).with {
+ visitCode()
+ visitLdcInsn status
+ visitInsn ARETURN
+ visitMaxs 1, 1
+ visitEnd()
+ }
+
+ visitEnd()
+
+ writeBytecode it, internalName
+ }
+
+ }
+
+ def createClassWriter(String internalName, String baseClassInternalName, int classModifiers) {
+ ClassWriter cw = new ClassWriter(0);
+
+ cw.visit V1_5, classModifiers, internalName, null,
+ baseClassInternalName, [
+ PlasticInternalUtils.toInternalName(ReloadableService.name)
+ ] as String[]
+
+
+ return cw
+ }
+
+
+ def writeBytecode(ClassWriter cw, String internalName) {
+ byte[] bytecode = cw.toByteArray();
+
+ writeBytecode(bytecode, pathForInternalName(internalName))
+ }
+
+ def writeBytecode(byte[] bytecode, String path) {
+ File file = new File(path)
+
+ file.parentFile.mkdirs()
+
+ file.withOutputStream { it.write bytecode }
+ }
+
+
+ def pathForInternalName(String internalName) {
+ return String.format("%s/%s.class",
+ classesDir.getAbsolutePath(),
+ internalName)
+ }
+
+ def update() {
+ getService(UpdateListenerHub).fireCheckForUpdates()
+ }
+
+ def "reload a service implementation"() {
+
+ when:
+
+ createImplementationClass "initial"
+
+ createRegistry()
+
+ ReloadableService reloadable = getService(ReloadableService);
+
+ update()
+
+ then:
+
+ reloadable.status == "initial"
+
+ when:
+
+ update()
+
+ touch classFile
+
+ createImplementationClass "updated"
+
+ then:
+
+ // Changes do not take effect until after update check
+
+ reloadable.status == "initial"
+
+ when:
+
+ update()
+
+ then:
+
+ reloadable.status == "updated"
+ }
+
+ def "reload a base class"() {
+
+ setup:
+
+ def baseClassInternalName = PlasticInternalUtils.toInternalName BASE_CLASS
+ def internalName = PlasticInternalUtils.toInternalName CLASS
+
+ createImplementationClass BASE_CLASS, "initial from base"
+
+ createClassWriter(internalName, baseClassInternalName, ACC_PUBLIC).with {
+
+ visitMethod(ACC_PUBLIC, "<init>", "()V", null, null).with {
+ visitCode()
+ visitVarInsn ALOAD, 0
+ visitMethodInsn INVOKESPECIAL, baseClassInternalName, "<init>", "()V"
+ visitInsn RETURN
+ visitMaxs 1, 1
+ visitEnd()
+ }
+
+ visitEnd()
+
+ writeBytecode it, internalName
+ }
+
+ createRegistry()
+
+ when:
+
+ ReloadableService reloadable = getService(ReloadableService)
+
+ update()
+
+ then:
+
+ reloadable.status == "initial from base"
+
+ when:
+
+ touch(new File(pathForInternalName(baseClassInternalName)))
+
+ createImplementationClass BASE_CLASS, "updated from base"
+
+ update()
+
+ then:
+
+ reloadable.status == "updated from base"
+ }
+
+ def "deleting an implementation class results in a runtime exception when reloading"() {
+
+ when:
+
+ createImplementationClass "before delete"
+
+ createRegistry()
+
+ ReloadableService reloadable = getService ReloadableService
+
+ then:
+
+ reloadable.status == "before delete"
+
+ assert classFile.exists()
+
+ when:
+
+ classFile.delete()
+
+ update()
+
+ reloadable.getStatus()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "Unable to reload class $CLASS"
+ }
+
+
+ def "reload a proxy object"() {
+ when:
+
+ createImplementationClass "initial proxy"
+
+ createRegistry()
+
+ def clazz = classLoader.loadClass CLASS
+
+ ReloadableService reloadable = proxy(ReloadableService, clazz)
+
+ then:
+
+ reloadable.status == "initial proxy"
+
+ when:
+
+ touch classFile
+
+ createImplementationClass "updated proxy"
+
+ update()
+
+ then:
+
+ reloadable.status == "updated proxy"
+
+ when:
+
+ touch classFile
+
+ createImplementationClass "re-updated proxy"
+
+ update()
+
+ then:
+
+ reloadable.status == "re-updated proxy"
+ }
+
+ def "check exception message for invalid service implementation (lacking a public constructor)"() {
+
+ when:
+
+ createImplementationClass "initial"
+
+ createRegistry()
+
+ ReloadableService reloadable = getService ReloadableService
+
+ touch classFile
+
+ createInvalidImplementationClass()
+
+ update()
+
+ reloadable.getStatus()
+
+ then:
+
+ Exception e = thrown()
+
+ e.message == "Service implementation class com.example.ReloadableServiceImpl does not have a suitable public constructor."
+ }
+
+ def "ensure ReloadAware services are notified when services are reloaded"() {
+
+ when:
+
+ registry = new RegistryBuilder().add(ReloadAwareModule).build()
+
+ then:
+
+ ReloadAwareModule.counterInstantiations == 0
+ ReloadAwareModule.counterReloads == 0
+
+ when:
+
+ Counter counter = proxy(Counter, CounterImpl)
+
+ then:
+
+ ReloadAwareModule.counterInstantiations == 0
+
+ expect:
+
+ counter.increment() == 1
+ counter.increment() == 2
+
+ ReloadAwareModule.counterInstantiations == 1
+
+ when:
+
+ def classURL = CounterImpl.getResource("CounterImpl.class")
+ def classFile = new File(classURL.toURI())
+
+ touch classFile
+
+ update()
+
+ // Check that the internal state has reset
+
+ assert counter.increment() == 1
+
+ then:
+
+ ReloadAwareModule.counterInstantiations == 2
+ ReloadAwareModule.counterReloads == 1
+ }
+
+ def createInvalidImplementationClass() {
+ def internalName = PlasticInternalUtils.toInternalName CLASS
+
+ createClassWriter(internalName, "java/lang/Object", ACC_PUBLIC).with {
+
+ visitMethod(ACC_PROTECTED, "<init>", "()V", null, null).with {
+ visitVarInsn ALOAD, 0
+ visitMethodInsn INVOKESPECIAL, "java/lang/Object", "<init>", "()V"
+ visitInsn RETURN
+ visitMaxs 1, 1
+ visitEnd()
+ }
+
+ visitEnd()
+
+ writeBytecode it, internalName
+ }
+ }
+
+
+ def touch(File f) {
+ long startModified = f.lastModified();
+
+ int index = 0;
+
+ while (true) {
+ f.setLastModified System.currentTimeMillis()
+
+ long newModified = f.lastModified()
+
+ if (newModified != startModified) {
+ return;
+ }
+
+ // Sleep an ever increasing amount, to ensure that the filesystem
+ // catches the change to the file. The Ubuntu CI Server appears
+ // to need longer waits.
+
+ Thread.sleep 50 * (2 ^ index++)
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ResourceSymbolProviderSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ResourceSymbolProviderSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ResourceSymbolProviderSpec.groovy
new file mode 100644
index 0000000..d026cd2
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ResourceSymbolProviderSpec.groovy
@@ -0,0 +1,32 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Resource
+import org.apache.tapestry5.ioc.internal.services.ResourceSymbolProvider
+import spock.lang.Specification
+
+class ResourceSymbolProviderSpec extends Specification {
+
+ static final CONTENT = 'homer=simpson\r\nmonty=burns'
+
+ def "access to contents of stream"() {
+ Resource resource = Mock()
+
+ when:
+
+ ResourceSymbolProvider provider = new ResourceSymbolProvider(resource)
+
+ then:
+
+ 1 * resource.openStream() >> { new ByteArrayInputStream(CONTENT.bytes) }
+
+ expect:
+
+ provider.valueForSymbol("homer") == "simpson"
+ provider.valueForSymbol("monty") == "burns"
+
+ provider.valueForSymbol("HOMER") == "simpson"
+
+ provider.valueForSymbol("marge") == null
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ServiceActivityScoreboardSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ServiceActivityScoreboardSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceActivityScoreboardSpec.groovy
new file mode 100644
index 0000000..0f995dd
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceActivityScoreboardSpec.groovy
@@ -0,0 +1,75 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.services.ServiceActivityScoreboard
+import org.apache.tapestry5.ioc.services.Status
+import org.apache.tapestry5.ioc.services.TypeCoercer
+import org.apache.tapestry5.ioc.*
+
+class ServiceActivityScoreboardSpec extends AbstractRegistrySpecification {
+
+ def "general cursory test"() {
+ buildRegistry GreeterModule
+
+ def scoreboard = getService ServiceActivityScoreboard
+
+ when:
+
+ def tc = getService TypeCoercer
+
+ tc.coerce "123", Integer
+
+ getService "BlueGreeter", Greeter
+
+ then:
+
+ def activity = scoreboard.serviceActivity
+
+ !activity.empty
+
+ activity.find({ it.serviceId == "TypeCoercer" }).status == Status.REAL
+
+ def ppf = activity.find { it.serviceId == "PlasticProxyFactory" }
+ ppf.status == Status.BUILTIN
+
+
+ def rg = activity.find { it.serviceId == "RedGreeter1" }
+ rg.status == Status.DEFINED
+ rg.markers.contains RedMarker
+ !rg.markers.contains(BlueMarker)
+
+ def bg = activity.find { it.serviceId == "BlueGreeter"}
+
+ bg.status == Status.VIRTUAL
+ bg.markers.contains BlueMarker
+ !bg.markers.contains(RedMarker)
+ }
+
+ def "scoreboard entry for perthread services is itself perthread"() {
+
+ buildRegistry GreeterModule, PerThreadModule
+
+ def scoreboard = getService ServiceActivityScoreboard
+
+ def holder = getService StringHolder
+
+ when:
+
+ Thread t = new Thread({
+ holder.value = "barney"
+ assert holder.value == "barney"
+
+ assert scoreboard.serviceActivity.find({ it.serviceId == "StringHolder"}).status == Status.REAL
+
+ cleanupThread()
+ })
+
+ t.start()
+ t.join()
+
+ then:
+
+ scoreboard.serviceActivity.find({ it.serviceId == "StringHolder"}).status == Status.VIRTUAL
+
+
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBinderSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBinderSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBinderSpec.groovy
new file mode 100644
index 0000000..c42976b
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBinderSpec.groovy
@@ -0,0 +1,37 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Greeter
+import org.apache.tapestry5.ioc.ServiceBuilderModule
+
+class ServiceBinderSpec extends AbstractRegistrySpecification {
+
+ def "a service implementation may be created via a ServiceBuilder callback"() {
+ buildRegistry ServiceBuilderModule
+
+ when:
+
+ def g = getService "Greeter", Greeter
+
+ then:
+ g.greeting == "Greetings from service Greeter."
+ }
+
+ def "verify exception reporting for ServiceBuilder that throws an exception"() {
+
+ buildRegistry ServiceBuilderModule
+
+ def g = getService "BrokenGreeter", Greeter
+
+ when:
+
+ g.greeting
+
+ then:
+
+ Exception e = thrown()
+
+ e.message.contains "Exception constructing service 'BrokenGreeter'"
+ e.message.contains "Failure inside ServiceBuilder callback."
+ }
+
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBuilderMethodInvokerSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBuilderMethodInvokerSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBuilderMethodInvokerSpec.groovy
new file mode 100644
index 0000000..459d7ed
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceBuilderMethodInvokerSpec.groovy
@@ -0,0 +1,199 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.AnnotationProvider
+import org.apache.tapestry5.ioc.ObjectCreator
+import org.apache.tapestry5.ioc.OperationTracker
+import org.apache.tapestry5.ioc.ServiceBuilderResources
+import org.slf4j.Logger
+import org.apache.tapestry5.ioc.internal.*
+
+class ServiceBuilderMethodInvokerSpec extends AbstractSharedRegistrySpecification {
+
+ static String DESCRIPTION = "{CREATOR DESCRIPTION}"
+ static String SERVICE_ID = "Fie"
+
+ Logger logger = Mock()
+ FieService implementation = Mock()
+ OperationTracker tracker = new QuietOperationTracker()
+ ServiceBuilderResources resources = Mock()
+ ServiceBuilderMethodFixture fixture = new ServiceBuilderMethodFixture();
+
+ def setup() {
+
+ fixture.fie = implementation
+
+ _ * resources.tracker >> tracker
+ _ * resources.moduleBuilder >> fixture
+ _ * resources.serviceId >> SERVICE_ID
+ _ * resources.serviceInterface >> FieService
+ _ * resources.logger >> logger
+ }
+
+ def "invoke a service builder method with no arguments"() {
+
+ when:
+
+ ObjectCreator oc = createObjectCreator "build_noargs"
+
+ def actual = oc.createObject()
+
+ then:
+
+ actual.is implementation
+ }
+
+ def ServiceBuilderMethodInvoker createObjectCreator(methodName) {
+ new ServiceBuilderMethodInvoker(resources, DESCRIPTION,
+ findMethod(fixture, methodName))
+ }
+
+ def invoke(methodName) {
+ createObjectCreator(methodName).createObject()
+ }
+
+ def "invoke a method with injected parameters"() {
+
+ fixture.expectedServiceInterface = FieService
+ fixture.expectedServiceResources = resources
+ fixture.expectedLogger = logger
+
+ when:
+
+ def actual = invoke "build_args"
+
+ then:
+
+ actual.is implementation
+ }
+
+ def "@Inject annotation bypasses service resources when resolving value to inject"() {
+
+ fixture.expectedString = "Injected"
+
+ when:
+
+ def actual = invoke "build_with_forced_injection"
+
+ then:
+
+ actual.is implementation
+
+ 1 * resources.getObject(String, _ as AnnotationProvider) >> "Injected"
+ }
+
+ def "@InjectService on method parameter"() {
+
+ FoeService foe = Mock()
+
+ fixture.expectedFoe = foe
+
+ when:
+
+ def actual = invoke "build_injected"
+
+ then:
+
+ actual.is implementation
+
+ 1 * resources.getService("Foe", FoeService) >> foe
+ }
+
+ def "@Named annotation on method parameter"() {
+
+ FoeService foe = Mock()
+
+ fixture.expectedFoe = foe
+
+ when:
+
+ def actual = invoke "build_named_injected"
+
+ then:
+
+ actual.is implementation
+
+ 1 * resources.getService("Foe", FoeService) >> foe
+ }
+
+ def "injection of ordered configuration as List"() {
+
+ List<Runnable> configuration = Mock()
+
+ fixture.expectedConfiguration = configuration
+
+ when:
+
+ def actual = invoke "buildWithOrderedConfiguration"
+
+ then:
+
+ actual.is implementation
+
+ 1 * resources.getOrderedConfiguration(Runnable) >> configuration
+ }
+
+ def "injection of unordered collection (as Collection)"() {
+
+ Collection<Runnable> configuration = Mock()
+
+ fixture.expectedConfiguration = configuration
+
+ when:
+
+ def actual = invoke "buildWithUnorderedConfiguration"
+
+ then:
+
+ actual.is implementation
+
+ 1 * resources.getUnorderedConfiguration(Runnable) >> configuration
+ }
+
+ def "builder method returns null"() {
+
+ fixture.fie = null
+
+ when:
+
+ createObjectCreator("buildWithUnorderedConfiguration").createObject()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message == "Builder method ${DESCRIPTION} (for service 'Fie') returned null."
+ }
+
+ def "builder method failure"() {
+
+ when:
+
+ createObjectCreator("build_fail").createObject()
+
+ then:
+
+ RuntimeException e = thrown()
+
+ e.message.contains "build_fail()"
+ e.message.contains "Method failed."
+
+ e.cause.message == "Method failed."
+ }
+
+ def "automatically injected dependency (without an annotation)"() {
+
+ FoeService foe = Mock()
+
+ fixture.expectedFoe = foe
+
+ when:
+
+ def actual = invoke "build_auto"
+
+ then:
+
+ actual.is implementation
+
+ 1 * resources.getObject(FoeService, _ as AnnotationProvider) >> foe
+ }
+}
http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ServiceCreatorGenericsSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ServiceCreatorGenericsSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceCreatorGenericsSpec.groovy
new file mode 100644
index 0000000..0e7814c
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ServiceCreatorGenericsSpec.groovy
@@ -0,0 +1,82 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.AbstractServiceCreator
+import org.apache.tapestry5.ioc.internal.IOCMessages
+import org.apache.tapestry5.ioc.internal.ServiceBuilderMethodFixture
+import spock.lang.Specification
+
+import java.lang.reflect.Method
+
+import static org.apache.tapestry5.ioc.internal.AbstractServiceCreator.findParameterizedTypeFromGenericType
+
+class ServiceCreatorGenericsSpec extends Specification {
+
+ Method findMethod(name) {
+ Method method = ServiceBuilderMethodFixture.methods.find { it.name == name}
+
+ assert method != null
+
+ return method
+ }
+
+ def methodMissing(String name, args) {
+ AbstractServiceCreator."$name"(* args)
+ }
+
+ def "parameterized type of generic method parameter is extracted"() {
+
+ when:
+
+ def method = findMethod "methodWithParameterizedList"
+
+ then:
+
+ method.parameterTypes[0] == List
+
+ def type = method.genericParameterTypes[0]
+
+ type.toString() == "java.util.List<java.lang.Runnable>"
+
+ findParameterizedTypeFromGenericType(type) == Runnable
+ }
+
+ def "parameterized type of a non-generic parameter is Object"() {
+
+ when:
+
+ def method = findMethod "methodWithList"
+
+ then:
+
+ method.parameterTypes[0] == List
+
+ def type = method.genericParameterTypes[0]
+
+ type.toString() == "interface java.util.List"
+ findParameterizedTypeFromGenericType(type) == Object
+ }
+
+ def "getting parameterized type for a non-support type is a failure"() {
+
+ when:
+
+ def method = findMethod "methodWithWildcardList"
+
+ then:
+
+ method.parameterTypes[0] == List
+
+ def type = method.genericParameterTypes[0]
+
+ when:
+
+ findParameterizedTypeFromGenericType(type)
+
+ then:
+
+ IllegalArgumentException e = thrown()
+
+ e.message == IOCMessages.genericTypeNotSupported(type)
+ }
+
+}