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)
+  }
+
+}