You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by hl...@apache.org on 2012/06/21 19:28:19 UTC

[4/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/DefaultModuleDefImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/DefaultModuleDefImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/DefaultModuleDefImplSpec.groovy
new file mode 100644
index 0000000..d017dfc
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/DefaultModuleDefImplSpec.groovy
@@ -0,0 +1,450 @@
+package ioc.specs
+
+import org.apache.tapestry5.internal.plastic.PlasticClassLoader
+import org.apache.tapestry5.internal.plastic.PlasticInternalUtils
+import org.apache.tapestry5.internal.plastic.asm.ClassWriter
+import org.apache.tapestry5.ioc.def.ServiceDef3
+import org.apache.tapestry5.ioc.internal.services.PlasticProxyFactoryImpl
+import org.apache.tapestry5.ioc.services.PlasticProxyFactory
+import org.slf4j.Logger
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+import org.apache.tapestry5.ioc.*
+import org.apache.tapestry5.ioc.internal.*
+
+import static org.apache.tapestry5.internal.plastic.asm.Opcodes.*
+
+class DefaultModuleDefImplSpec extends Specification {
+
+  @Shared
+  PlasticProxyFactory proxyFactory = new PlasticProxyFactoryImpl(Thread.currentThread().contextClassLoader, null)
+
+  @Shared
+  OperationTracker tracker = new QuietOperationTracker()
+
+  Logger logger = Mock()
+
+  def "toString() of module lists services in the module"() {
+    when:
+
+    def md = module SimpleModule
+
+    then:
+
+    md.toString() == "ModuleDef[$SimpleModule.name Barney, Fred, Wilma]"
+  }
+
+  def "serviceIds contains all service ids"() {
+    def md = module SimpleModule
+
+    expect:
+
+    md.serviceIds == ["Fred", "Barney", "Wilma"] as Set
+  }
+
+  def "ServiceDef obtainable by service id"() {
+    def md = module SimpleModule
+
+    when:
+
+    def sd = md.getServiceDef "fred"
+
+    then:
+
+    sd.serviceId == "Fred"
+    sd.serviceInterface == FieService
+    sd.toString().contains "${SimpleModule.name}.buildFred()"
+    sd.serviceScope == ScopeConstants.DEFAULT
+    !sd.eagerLoad
+    sd.markers.empty
+
+    when:
+
+    sd = md.getServiceDef("Wilma")
+
+    then:
+
+    sd.eagerLoad
+  }
+
+  def "ModuleDef exposes decorator methods as DecoratorDefs"() {
+    def md = module SimpleModule
+
+    when:
+
+    def decos = md.decoratorDefs
+
+    then:
+
+    decos.size() == 1
+
+    def deco = decos.find()
+
+    deco.decoratorId == "Logging"
+    deco.toString().contains "${SimpleModule.name}.decorateLogging(Class, Object)"
+  }
+
+  def "@ServiceId annotation on service builder method overrides naming convention"() {
+    when:
+
+    def md = module ServiceIdViaAnnotationModule
+
+    then:
+
+    md.getServiceDef("FooService") != null
+  }
+
+  def "@ServiceId on implementation class overrides default id from ServiceBinder.bind() default"() {
+    when:
+
+    def md = module ServiceIdViaAnnotationModule
+
+    then:
+
+    md.getServiceDef("BarneyService") != null
+  }
+
+  def "@Named annotation on service builder method overrides naming convention"() {
+    when:
+
+    def md = module NamedServiceModule
+
+    then:
+
+    md.getServiceDef("BazService") != null
+  }
+
+  def "@Named annotation on service implementation class overrides ServiceBinder.bind() default"() {
+    when:
+
+    def md = module NamedServiceModule
+
+    then:
+
+    md.getServiceDef("QuuxService") != null
+  }
+
+  def "naming convention for a service builder method named build() is derived from the return type"() {
+    when:
+
+    def md = module DefaultServiceIdModule
+
+    then:
+
+    md.getServiceDef("FieService") != null
+  }
+
+  def "conflicting service ids result in an exception"() {
+    when:
+
+    module ServiceIdConflictMethodModule
+
+    then:
+
+    RuntimeException ex = thrown()
+
+    ex.message.contains "Service Fred (defined by ${ServiceIdConflictMethodModule.name}.buildFred()"
+    ex.message.contains "conflicts with previously defined service defined by ${ServiceIdConflictMethodModule.name}.buildFred(Object)"
+  }
+
+  def "a service builder method may not return void"() {
+    when:
+
+    module VoidBuilderMethodModule
+
+    then:
+
+    RuntimeException ex = thrown()
+
+    ex.message.contains "${VoidBuilderMethodModule.name}.buildNull()"
+    ex.message.contains "but the return type (void) is not acceptable"
+  }
+
+  def "a service builder method may not return an array"() {
+    when:
+
+    module BuilderMethodModule
+
+    then:
+
+    RuntimeException ex = thrown()
+
+    ex.message.contains "${BuilderMethodModule.name}.buildStringArray()"
+    ex.message.contains "but the return type (java.lang.String[])"
+  }
+
+  @Unroll
+  def "A decorator method #desc"() {
+    when:
+
+    module moduleClass
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains expectedText
+
+    where:
+
+    moduleClass                    | expectedText        | desc
+    PrimitiveDecoratorMethodModule | "decoratePrimitive" | "may not return a primitive type"
+    ArrayDecoratorMethodModule     | "decorateArray"     | "may not return an array"
+  }
+
+  @Unroll
+  def "#desc"() {
+    when:
+
+    def md = module moduleClass
+
+    then:
+
+    def defs = md.contributionDefs
+
+    defs.size() == 1
+
+    def cd = defs.find()
+
+    cd.serviceId == serviceId
+
+    cd.toString().contains "${moduleClass.name}.$methodSignature"
+
+    where:
+
+    moduleClass                | serviceId | methodSignature                           | desc
+    SimpleModule               | "Barney"  | "contributeBarney(Configuration)"         | "contribution without annotation to configuration"
+    OrderedConfigurationModule | "Ordered" | "contributeOrdered(OrderedConfiguration)" | "contribution to ordered configuration"
+    MappedConfigurationModule  | "Mapped"  | "contributeMapped(MappedConfiguration)"   | "contribution to mapped configuration"
+  }
+
+  @Unroll
+  def "service contribution method that #desc throws an exception"() {
+
+    when:
+
+    module moduleClass
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains message
+
+    where:
+
+    moduleClass                         | message                                                                                                | desc
+
+    NoUsableContributionParameterModule | "does not contain a parameter of type Configuration, OrderedConfiguration or MappedConfiguration"      | "does not include configuration parameter"
+    TooManyContributionParametersModule | "contains more than one parameter of type Configuration, OrderedConfiguration, or MappedConfiguration" | "includes more than one configuration parameter"
+  }
+
+  def "using defaults for ServiceBinder.bind()"() {
+
+    when:
+
+    def md = module AutobuildModule
+    ServiceDef3 sd = md.getServiceDef "stringholder"
+
+    then:
+
+    sd.serviceInterface == StringHolder
+    sd.serviceId == "StringHolder"
+    sd.serviceScope == ScopeConstants.DEFAULT
+    !sd.isEagerLoad()
+    sd.markers.empty
+    !sd.preventDecoration
+  }
+
+  def "overriding defaults for ServiceBinder.bind()"() {
+
+    when:
+
+    def md = module ComplexAutobuildModule
+    ServiceDef3 sd = md.getServiceDef "sh"
+
+    then:
+
+    sd.serviceInterface == StringHolder
+    sd.serviceId == "SH"
+    sd.serviceScope == "magic"
+    sd.eagerLoad
+    sd.preventDecoration
+  }
+
+  def "implementation class for ServiceBinder.bind() must have a public constructor"() {
+    when:
+
+    module UninstantiableAutobuildServiceModule
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Class org.apache.tapestry5.ioc.internal.RunnableServiceImpl (implementation of service 'Runnable') does not contain any public constructors."
+  }
+
+  def "the bind() method of a module class must be a static method"() {
+    when:
+
+    module NonStaticBindMethodModule
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Method org.apache.tapestry5.ioc.internal.NonStaticBindMethodModule.bind(ServiceBinder)"
+    e.message.contains "appears to be a service binder method, but is an instance method, not a static method"
+  }
+
+  def "when autobuilding a service implementation, the constructor with the most parameters is chosen"() {
+    ServiceBuilderResources resources = Mock()
+
+    when:
+
+    def md = module MutlipleAutobuildServiceConstructorsModule
+
+    def sd = md.getServiceDef "stringholder"
+
+    then:
+
+    sd != null
+
+    0 * _
+
+    when:
+
+    def oc = sd.createServiceCreator(resources)
+    def holder = oc.createObject()
+
+    holder.value = "foo"
+
+    then:
+
+    holder instanceof StringHolder
+    holder.value == "FOO"
+
+    _ * resources.serviceId >> "StringHolder"
+    _ * resources.logger >> logger
+    _ * resources.serviceInterface >> StringHolder
+    1 * resources.getService("ToUpperCaseStringHolder", StringHolder) >> new ToUpperCaseStringHolder()
+    _ * resources.tracker >> tracker
+
+    1 * logger.debug(_) >> { args ->
+      assert args[0].contains(
+          "Invoking constructor org.apache.tapestry5.ioc.internal.MultipleConstructorsAutobuildService(StringHolder)")
+    }
+
+    0 * _
+  }
+
+  def "an exception inside a bind() method bubbles up"() {
+    when:
+
+    module ExceptionInBindMethod
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Error invoking service binder method org.apache.tapestry5.ioc.internal.ExceptionInBindMethod.bind(ServiceBinder)"
+    e.message.contains "at ExceptionInBindMethod.java"
+    e.message.contains "Really, how often is this going to happen?"
+  }
+
+  def "@EagerLoad annotation on service implementation class is reflected in the ServiceDef"() {
+    when:
+
+    def md = module EagerLoadViaAnnotationModule
+    def sd = md.getServiceDef "runnable"
+
+    then:
+
+    sd.eagerLoad
+  }
+
+  private DefaultModuleDefImpl module(moduleClass) {
+    new DefaultModuleDefImpl(moduleClass, logger, proxyFactory)
+  }
+
+  def "marker annotations on the service builder method are available in the ServiceDef"() {
+
+    when:
+
+    def md = module MarkerModule
+    def sd = md.getServiceDef "greeter"
+
+    then:
+
+    sd.markers == [BlueMarker] as Set
+  }
+
+  def "marker annotations specified via ServiceBinder is available in the ServiceDef"() {
+    when:
+
+    def md = module MarkerModule
+    def sd = md.getServiceDef "redgreeter"
+
+    then:
+
+    sd.markers == [RedMarker] as Set
+  }
+
+  def "marker annotation on the implementation class is available in the ServiceDef"() {
+    when:
+
+    def md = module MarkerModule
+    def sd = md.getServiceDef "SecondRedGreeter"
+
+    then:
+
+    sd.markers == [RedMarker] as Set
+  }
+
+  def "marker annotation from ServiceBinder and implementation class are merged"() {
+    when:
+
+    def md = module MarkerModule
+    def sd = md.getServiceDef "SurprisinglyBlueGreeter"
+
+    then:
+
+    sd.markers == [RedMarker, BlueMarker] as Set
+  }
+
+  def "public synthetic methods on module class are ignored"() {
+    def moduleClass = createSyntheticModuleClass()
+
+    when:
+
+    def md = module moduleClass
+
+    then:
+
+    md.serviceIds.size() == 1
+  }
+
+  private createSyntheticModuleClass() {
+
+    def cw = new ClassWriter(ClassWriter.COMPUTE_MAXS + ClassWriter.COMPUTE_FRAMES)
+
+    cw.visit(V1_5, ACC_PUBLIC, "EnhancedSyntheticMethodModule", null,
+        PlasticInternalUtils.toInternalName(SyntheticMethodModule.name), null);
+
+    def mv = cw.visitMethod ACC_PUBLIC | ACC_STATIC | ACC_SYNTHETIC, "synth", "()V", null, null
+    mv.visitCode()
+    mv.visitInsn RETURN
+    mv.visitEnd()
+
+    cw.visitEnd()
+
+    def bytecode = cw.toByteArray()
+
+    ClassLoader loader = Thread.currentThread().contextClassLoader
+
+    PlasticClassLoader plasticLoader = new PlasticClassLoader(loader, new NoopClassLoaderDelegate())
+
+    return plasticLoader.defineClassWithBytecode("EnhancedSyntheticMethodModule", bytecode)
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/DummyLockSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/DummyLockSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/DummyLockSpec.groovy
new file mode 100644
index 0000000..8e6dadd
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/DummyLockSpec.groovy
@@ -0,0 +1,28 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.util.DummyLock
+import spock.lang.Specification
+
+import java.util.concurrent.locks.Lock
+
+class DummyLockSpec extends Specification {
+
+  def "all methods are no-ops"() {
+    Lock lock = new DummyLock()
+
+    when:
+
+    lock.lock()
+    lock.unlock()
+    lock.lockInterruptibly()
+
+    then:
+
+    noExceptionThrown()
+
+    expect:
+    lock.newCondition() == null
+    lock.tryLock()
+    lock.tryLock(0, null)
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/EagerLoadSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/EagerLoadSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/EagerLoadSpec.groovy
new file mode 100644
index 0000000..563d2c7
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/EagerLoadSpec.groovy
@@ -0,0 +1,22 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.EagerProxyReloadModule
+
+class EagerLoadSpec extends AbstractRegistrySpecification {
+
+  def "proxied service does eager load"() {
+    expect:
+
+    EagerProxyReloadModule.eagerLoadServiceDidLoad == false
+
+    when:
+
+    buildRegistry EagerProxyReloadModule
+
+    performRegistryStartup()
+
+    then:
+
+    EagerProxyReloadModule.eagerLoadServiceDidLoad == true
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionAnalyzerImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionAnalyzerImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionAnalyzerImplSpec.groovy
new file mode 100644
index 0000000..f865c1e
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionAnalyzerImplSpec.groovy
@@ -0,0 +1,214 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Location
+import org.apache.tapestry5.ioc.internal.util.TapestryException
+import org.apache.tapestry5.ioc.services.ExceptionAnalyzer
+
+class WriteOnlyPropertyException extends Exception {
+
+  private String code;
+
+  public String getCode() {
+    return code;
+  }
+
+  public void setFaultCode(int code) {
+    this.code = String.format("%04d", code);
+  }
+}
+
+class SelfCausedException extends RuntimeException {
+
+  SelfCausedException(String message) {
+    super(message);
+  }
+
+  public Throwable getCause() {
+    return this;
+  }
+}
+
+class ExceptionAnalyzerImplSpec extends AbstractSharedRegistrySpecification {
+
+  ExceptionAnalyzer analyzer = getService(ExceptionAnalyzer)
+
+  def "analysis of a simple exception"() {
+    when:
+    def ea = analyzer.analyze(t)
+
+    then:
+
+    ea.exceptionInfos.size() == 1
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.className == RuntimeException.name
+    ei.message == message
+
+    ei.propertyNames.empty
+    !ei.stackTrace.empty
+
+    where:
+
+    message = "Hey! We've Got No Tomatoes"
+    t = new RuntimeException(message)
+  }
+
+  def "access to properties of exception"() {
+    Location l = Mock()
+    def t = new TapestryException("Message", l, null)
+
+    when:
+    def ea = analyzer.analyze(t)
+
+    then:
+
+    ea.exceptionInfos.size() == 1
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.propertyNames == ["location"]
+    ei.getProperty("location").is(l)
+  }
+
+  def "access to nested exceptions"() {
+    when:
+
+    def ea = analyzer.analyze(outer)
+
+    then:
+
+    ea.exceptionInfos.size() == 2
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.message == "Outer"
+    ei.stackTrace.empty
+
+    when:
+
+    ei = ea.exceptionInfos[1]
+
+    then:
+
+    ei.message == "Inner"
+    !ei.stackTrace.empty
+
+    where:
+
+    inner = new RuntimeException("Inner")
+    outer = new RuntimeException("Outer", inner)
+  }
+
+  def "middle exception that adds no value is removed"() {
+    when:
+
+    def ea = analyzer.analyze(outer)
+
+    then:
+
+    ea.exceptionInfos.size() == 2
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.message == "Outer: Middle"
+    ei.stackTrace.empty
+
+    when:
+
+    ei = ea.exceptionInfos[1]
+
+    then:
+
+    ei.message == "Inner"
+
+    !ei.stackTrace.empty
+
+    where:
+
+    inner = new RuntimeException("Inner");
+    middle = new RuntimeException("Middle", inner);
+    outer = new RuntimeException("Outer: Middle", middle);
+  }
+
+  def "a middle exception that adds extra information is retained"() {
+    Location l = Mock()
+    def inner = new RuntimeException("Inner");
+    def middle = new TapestryException("Middle", l, inner);
+    def outer = new RuntimeException("Outer: Middle", middle);
+
+    when:
+
+    def ea = analyzer.analyze(outer)
+
+    then:
+
+    ea.exceptionInfos.size() == 3
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.message == "Outer: Middle"
+    ei.stackTrace.empty
+
+    when:
+
+    ei = ea.exceptionInfos[1]
+
+    then:
+
+    ei.message == "Middle"
+    ei.getProperty("location").is(l)
+    ei.stackTrace.empty
+
+    when:
+
+    ei = ea.exceptionInfos[2]
+
+    then:
+
+    ei.message == "Inner"
+    !ei.stackTrace.empty
+  }
+
+  def "write only properties are omitted"() {
+    WriteOnlyPropertyException ex = new WriteOnlyPropertyException();
+
+    ex.setFaultCode(99);
+
+    when:
+
+    def ea = analyzer.analyze(ex);
+
+    then:
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.propertyNames.contains("code")
+    !ei.propertyNames.contains("faultCode")
+    ei.getProperty("code") == "0099"
+  }
+
+  def "an exception that is its own cause does not cause an endless loop"() {
+    when:
+
+    def ea = analyzer.analyze(t)
+
+    then:
+
+    ea.exceptionInfos.size() == 1
+
+    def ei = ea.exceptionInfos[0]
+
+    ei.className == SelfCausedException.name
+    ei.message == message
+
+    !ei.propertyNames.contains("cause")
+
+    !ei.stackTrace.empty
+
+    where:
+
+    message = "Who you lookin at?"
+    t = new SelfCausedException(message)
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionTrackerImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionTrackerImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionTrackerImplSpec.groovy
new file mode 100644
index 0000000..136f3aa
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionTrackerImplSpec.groovy
@@ -0,0 +1,32 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.ExceptionTrackerImpl
+import spock.lang.Specification
+
+class ExceptionTrackerImplSpec extends Specification {
+
+  def "exceptions are tracked"() {
+
+    def t1 = new RuntimeException()
+    def t2 = new RuntimeException()
+
+    when: "with a new tracker"
+
+    def et = new ExceptionTrackerImpl()
+
+    then: "never logged exceptions return false"
+
+    !et.exceptionLogged(t1)
+    !et.exceptionLogged(t2)
+
+    then: "subsequently, the same exceptions return true"
+
+    et.exceptionLogged(t1)
+    et.exceptionLogged(t2)
+
+    then: "and again"
+
+    et.exceptionLogged(t1)
+    et.exceptionLogged(t2)
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionUtilsSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionUtilsSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionUtilsSpec.groovy
new file mode 100644
index 0000000..200bd9e
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ExceptionUtilsSpec.groovy
@@ -0,0 +1,53 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.PropertyAccessImpl
+import org.apache.tapestry5.ioc.internal.util.TapestryException
+import org.apache.tapestry5.ioc.util.ExceptionUtils
+import org.apache.tapestry5.ioc.util.ExceptionWrapper
+import spock.lang.Shared
+import spock.lang.Specification
+
+class ExceptionUtilsSpec extends Specification {
+
+  @Shared
+  def access = new PropertyAccessImpl()
+
+  def "find cause with match"() {
+    when:
+    def inner = new TapestryException("foo", null)
+    def outer = new RuntimeException(inner)
+
+    then:
+
+    ExceptionUtils.findCause(outer, TapestryException).is(inner)
+    ExceptionUtils.findCause(outer, TapestryException, access).is(inner)
+  }
+
+  def "find cause with no match"() {
+
+    when:
+
+    def re = new RuntimeException("No cause for you.")
+
+    then:
+
+    ExceptionUtils.findCause(re, TapestryException) == null
+    ExceptionUtils.findCause(re, TapestryException, access) == null
+  }
+
+  def "find a hidden exception"() {
+    when:
+
+    def inner = new RuntimeException()
+    def outer = new ExceptionWrapper(inner)
+
+    then:
+
+    // TAP5-1639: The old code can't find inner
+    ExceptionUtils.findCause(outer, RuntimeException) == null
+
+    // The new reflection-based on can:
+
+    ExceptionUtils.findCause(outer, RuntimeException, access).is(inner)
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/FilterMethodAnalyzerSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/FilterMethodAnalyzerSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/FilterMethodAnalyzerSpec.groovy
new file mode 100644
index 0000000..7e0e626
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/FilterMethodAnalyzerSpec.groovy
@@ -0,0 +1,37 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.FilterMethodAnalyzer
+import org.apache.tapestry5.ioc.internal.services.MethodSignature
+import org.apache.tapestry5.ioc.internal.services.SampleFilter
+import org.apache.tapestry5.ioc.internal.services.SampleService
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class FilterMethodAnalyzerSpec extends Specification {
+
+  private MethodSignature find(clazz, name) {
+    new MethodSignature(clazz.methods.find { it.name == name })
+  }
+
+  @Unroll
+  def "position of delegate parameter for #methodName should be #position"() {
+    def analyzer = new FilterMethodAnalyzer(SampleService)
+
+    def mainMethod = find SampleService, methodName
+    def filterMethod = find SampleFilter, methodName
+
+    expect:
+
+    analyzer.findServiceInterfacePosition(mainMethod, filterMethod) == position
+
+    where:
+
+    methodName                | position
+    "simpleMatch"             | 0
+    "mismatchParameterCount"  | -1
+    "mismatchReturnType"      | -1
+    "missingServiceInterface" | -1
+    "complexMatch"            | 2
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/GeneralIntegrationSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/GeneralIntegrationSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/GeneralIntegrationSpec.groovy
new file mode 100644
index 0000000..1e30463
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/GeneralIntegrationSpec.groovy
@@ -0,0 +1,25 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.Bean
+import org.apache.tapestry5.ioc.services.PropertyAccess
+
+class GeneralIntegrationSpec extends AbstractSharedRegistrySpecification {
+
+  def "PropertyAccess service is available"() {
+
+    PropertyAccess pa = getService "PropertyAccess", PropertyAccess
+
+    Bean b = new Bean()
+
+    when:
+
+    pa.set(b, "value", 99)
+
+    then:
+
+    b.value == 99
+    pa.get(b, "value") == 99
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/GenericUtilsSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/GenericUtilsSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/GenericUtilsSpec.groovy
new file mode 100644
index 0000000..6b527d2
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/GenericUtilsSpec.groovy
@@ -0,0 +1,40 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.util.GenericsUtils
+import org.apache.tapestry5.ioc.internal.util.NonGenericBean
+import org.apache.tapestry5.ioc.internal.util.StringBean
+import org.apache.tapestry5.ioc.internal.util.StringLongPair
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class GenericUtilsSpec extends Specification {
+
+  def find(clazz, name) {
+    def method = clazz.methods.find { it.name.equalsIgnoreCase(name) }
+
+    if (method == null) {
+      throw new IllegalArgumentException("Unable to find method '$name' of ${clazz.name}.")
+    }
+
+    return method
+  }
+
+  @Unroll
+  def "generic return type for #method is #expected"() {
+
+    expect:
+
+    GenericsUtils.extractGenericReturnType(clazz, method).is(expected)
+
+    where:
+
+    clazz          | name       | expected
+    NonGenericBean | "getvalue" | String
+    StringBean     | "getvalue" | String
+    StringLongPair | "getkey"   | String
+    StringLongPair | "getvalue" | Long
+
+    method = find(clazz, name)
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/GlobPatternMatcherSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/GlobPatternMatcherSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/GlobPatternMatcherSpec.groovy
new file mode 100644
index 0000000..9cb0a94
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/GlobPatternMatcherSpec.groovy
@@ -0,0 +1,63 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.GlobPatternMatcher
+import spock.lang.Specification
+import spock.lang.Unroll
+
+@Unroll
+class GlobPatternMatcherSpec extends Specification {
+
+  def "input '#input' matches pattern '#pattern'"() {
+
+    def matcher = new GlobPatternMatcher(pattern)
+
+    expect:
+
+    matcher.matches(input)
+
+    where:
+
+    input         | pattern
+    "fred"        | "fred"
+    "fred"        | "FRED"
+    "fred"        | "*"
+    ""            | "*"
+    "fred.Barney" | "*Barney"
+    "fred.Barney" | "*BARNEY"
+    "fred.Barney" | "fred*"
+    "fred.Barney" | "FRED*"
+    "fredBarney"  | "*dB*"
+    "fredBarney"  | "*DB*"
+    "fred.Barney" | "*Barney*"
+    "fred.Barney" | "*fred*"
+    "fred.Barney" | "*FRED*"
+    "MyEntityDAO" | ".*dao"
+    "FredDAO"     | "(fred|barney)dao"
+  }
+
+  def "input '#input' does not match pattern '#pattern'"() {
+
+    def matcher = new GlobPatternMatcher(pattern)
+
+    expect:
+
+    !matcher.matches(input)
+
+    where:
+
+    input          | pattern
+    "xfred"        | "fred"
+    "fredx"        | "fred"
+    "fred"         | "xfred"
+    "fred"         | "fredx"
+    "fred.Barneyx" | "*Barney"
+    "fred.Barney"  | "*Barneyx"
+    "fred.Barney"  | "*xBarney"
+    "xfred.Barney" | "fred*"
+    "fred.Barney"  | "fredx*"
+    "fred.Barney"  | "xfred*"
+    "fred.Barney"  | "*flint*"
+    "MyEntityDAL"  | ".*dao"
+    "WilmaDAO"     | "(fred|barney)dao"
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/IdAllocatorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/IdAllocatorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/IdAllocatorSpec.groovy
new file mode 100644
index 0000000..643aa3a
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/IdAllocatorSpec.groovy
@@ -0,0 +1,163 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.util.IdAllocator
+import spock.lang.Specification
+
+class IdAllocatorSpec extends Specification {
+
+  def "id is not allocated until it is allocated"() {
+    when:
+
+    IdAllocator a = new IdAllocator()
+
+    then:
+
+    !a.isAllocated("name")
+
+    when:
+
+    def actual = a.allocateId("name")
+
+    then:
+
+    actual == "name"
+    a.isAllocated("name")
+  }
+
+  def "repeatedly allocated ids are uniqued with a suffix"() {
+
+    IdAllocator a = new IdAllocator()
+
+    a.allocateId("name")
+
+    expect:
+
+    10.times {
+
+      def expected = "name_$it"
+
+      assert !a.isAllocated(expected)
+
+      assert a.allocateId("name") == expected
+    }
+  }
+
+  def "access to allocated ids"() {
+    IdAllocator a = new IdAllocator()
+
+    when:
+
+    a.allocateId("name")
+
+    then:
+
+    a.allocatedIds == ["name"]
+
+    when:
+
+    a.allocateId("name")
+
+    then:
+
+    a.allocatedIds == ["name", "name_0"]
+  }
+
+  def "allocation using a namespace"() {
+
+    IdAllocator a = new IdAllocator("_NS")
+
+    expect:
+
+    a.allocateId("name") == "name_NS"
+
+    a.allocateId("name") == "name_NS_0"
+
+    // This is current behavior, but is probably something
+    // that could be improved.
+
+    a.allocateId("name_NS") == "name_NS_NS"
+
+    a.allocateId("name_NS") == "name_NS_NS_0"
+  }
+
+  def "degenerate id allocation"() {
+    IdAllocator a = new IdAllocator()
+
+    expect:
+
+    a.allocateId("d_1") == "d_1"
+    a.allocateId("d") == "d"
+    a.allocateId("d") == "d_0"
+    a.allocateId("d") == "d_2"
+
+    a.allocateId("d") == "d_3"
+
+    // It's a collision, so a unique number is appended.
+    a.allocateId("d_1") == "d_1_0"
+  }
+
+  def "degenerate id allocation (with a namespace)"() {
+
+    IdAllocator a = new IdAllocator("_NS")
+
+    expect:
+
+    a.allocateId("d_1") == "d_1_NS"
+
+    a.allocateId("d") == "d_NS"
+    a.allocateId("d") == "d_NS_0"
+    a.allocateId("d") == "d_NS_1"
+    a.allocateId("d") == "d_NS_2"
+    a.allocateId("d") == "d_NS_3"
+
+    a.allocateId("d_1") == "d_1_NS_0"
+
+    // This is very degenerate, and maybe something that needs fixing.
+
+    a.allocateId("d_1_NS") == "d_1_NS_NS"
+  }
+
+  def "clearing an allocator forgets prior ids"() {
+    when:
+
+    IdAllocator a = new IdAllocator()
+
+
+    then:
+
+    a.allocateId("foo") == "foo"
+    a.allocateId("foo") == "foo_0"
+
+    when:
+
+    a.clear()
+
+    then:
+
+    a.allocateId("foo") == "foo"
+    a.allocateId("foo") == "foo_0"
+  }
+
+  def "cloning an id allocator does not share data with the new allocator"() {
+
+    when:
+
+    IdAllocator a = new IdAllocator();
+
+    then:
+
+    a.allocateId("foo") == "foo"
+    a.allocateId("foo") == "foo_0"
+
+    when:
+
+    IdAllocator b = a.clone()
+
+    then:
+
+    ["bar", "baz", "foo", "foo"].each {
+      assert a.allocateId(it) == b.allocateId(it)
+    }
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/InheritanceSearchSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/InheritanceSearchSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/InheritanceSearchSpec.groovy
new file mode 100644
index 0000000..eabedeb
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/InheritanceSearchSpec.groovy
@@ -0,0 +1,68 @@
+package ioc.specs
+
+import org.apache.tapestry5.plastic.PlasticUtils
+import spock.lang.Specification
+import spock.lang.Unroll
+import org.apache.tapestry5.ioc.internal.util.*
+
+class InheritanceSearchSpec extends Specification {
+
+  def "remove() is always a failure"() {
+    when:
+
+    new InheritanceSearch(Object).remove()
+
+    then:
+
+    thrown(UnsupportedOperationException)
+  }
+
+  def "exception thrown when invoking next() after Object has been reached"() {
+    def s = new InheritanceSearch(Object)
+
+    expect:
+
+    s.next() == Object
+    !s.hasNext()
+
+    when:
+
+    s.next()
+
+    then:
+
+    thrown(IllegalStateException)
+  }
+
+  @Unroll
+  def "inheritance of #className is #expectedNames"() {
+    def search = new InheritanceSearch(clazz)
+    def result = []
+    while (search.hasNext()) {
+      result << search.next()
+    }
+
+    expect:
+
+    result == expected
+
+    where:
+
+    clazz      | expected
+    Object     | [Object]
+    String     | [String, Serializable, Comparable, CharSequence, Object]
+    Comparable | [Comparable, Object]
+    FooBar     | [FooBar, Foo, Bar, Object]
+    FooBarImpl | [FooBarImpl, FooImpl, BarImpl, Bar, FooBar, Foo, Object]
+    long       | [long, Long, Number, Comparable, Serializable, Object]
+    void       | [void, Object]
+    long[]     | [long[], Cloneable, Serializable, Object]
+    int[][]    | [int[][], Cloneable, Serializable, Object]
+    String[]   | [String[], Object[], Cloneable, Serializable, Object]
+    String[][] | [String[][], Object[], Cloneable, Serializable, Object]
+
+    className = PlasticUtils.toTypeName(clazz)
+    expectedNames = expected.collect { PlasticUtils.toTypeName(it) }.join(", ")
+
+  }
+}
\ 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/InjectionSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/InjectionSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/InjectionSpec.groovy
new file mode 100644
index 0000000..7c7d549
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/InjectionSpec.groovy
@@ -0,0 +1,125 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.*
+
+class InjectionSpec extends AbstractRegistrySpecification {
+
+  def "symbol in @Inject is expanded"() {
+
+    buildRegistry GreeterModule
+
+    when:
+
+    def greeter = getService "Greeter", Greeter
+
+    then:
+
+    greeter.greeting == "Hello"
+    greeter.toString() == "<Proxy for Greeter(org.apache.tapestry5.ioc.Greeter)>"
+  }
+
+  def "injection by marker with single match"() {
+
+    buildRegistry GreeterModule
+
+    when:
+
+    def greeter = getService "InjectedBlueGreeter", Greeter
+
+    then:
+
+    greeter.greeting == "Blue"
+  }
+
+  def "verify exception for inject by marker with multiple matches"() {
+    buildRegistry GreeterModule
+
+    def greeter = getService "InjectedRedGreeter", Greeter
+
+    when:
+
+    greeter.greeting
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Unable to locate a single service assignable to type org.apache.tapestry5.ioc.Greeter with marker annotation(s) org.apache.tapestry5.ioc.RedMarker"
+    e.message.contains "org.apache.tapestry5.ioc.GreeterModule.buildRedGreeter1()"
+    e.message.contains "org.apache.tapestry5.ioc.GreeterModule.buildRedGreeter2()"
+  }
+
+  def "verify exception for injection by marker and no matches"() {
+    buildRegistry GreeterModule
+
+    def greeter = getService "InjectedYellowGreeter", Greeter
+
+    when:
+
+    greeter.greeting
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Exception constructing service"
+    e.message.contains "Unable to locate any service assignable to type org.apache.tapestry5.ioc.Greeter with marker annotation(s) org.apache.tapestry5.ioc.YellowMarker."
+  }
+
+  def "recursion handling injections (due to MasterObjectProvider) is detected"() {
+
+    buildRegistry CyclicMOPModule
+
+    def trigger = getService "Trigger", Runnable
+
+    when:
+
+    trigger.run()
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Construction of service 'TypeCoercer' has failed due to recursion"
+  }
+
+  def "A field may be annotated with @InjectResource to receive resources"() {
+
+    buildRegistry FieldResourceInjectionModule
+
+    when:
+
+    def s = getService FieldResourceService
+
+    then:
+
+    s.serviceId == "FieldResourceService"
+
+    s.labels == ["Barney", "Betty", "Fred", "Wilma"]
+  }
+
+  def "methods with @PostInjection are invoked and can be passed further injections"() {
+    buildRegistry PostInjectionMethodModule
+
+    when:
+
+    def g = getService Greeter
+
+    then:
+
+    g.greeting == "Greetings from ServiceIdGreeter."
+  }
+
+  def "a service may be overridden by contributing to ServiceOverride"() {
+    buildRegistry GreeterServiceOverrideModule
+
+    when:
+
+    def g = getObject Greeter, null
+
+    then:
+
+    g.greeting == "Override Greeting"
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/InternalUtilsSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/InternalUtilsSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/InternalUtilsSpec.groovy
new file mode 100644
index 0000000..0666198
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/InternalUtilsSpec.groovy
@@ -0,0 +1,607 @@
+package ioc.specs
+
+import org.apache.tapestry5.func.F
+import org.apache.tapestry5.func.Predicate
+import org.apache.tapestry5.ioc.annotations.Inject
+import org.apache.tapestry5.ioc.def.ServiceDef
+import org.apache.tapestry5.ioc.def.ServiceDef2
+import org.apache.tapestry5.ioc.internal.QuietOperationTracker
+import org.apache.tapestry5.ioc.services.Builtin
+import org.apache.tapestry5.ioc.services.Coercion
+import org.apache.tapestry5.ioc.services.SymbolSource
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import java.lang.reflect.Method
+
+import org.apache.tapestry5.ioc.*
+import org.apache.tapestry5.ioc.internal.util.*
+
+class InternalUtilsSpec extends Specification {
+
+  private static class PrivateInnerClass {
+
+    public PrivateInnerClass() {
+    }
+  }
+
+  static class PublicInnerClass {
+
+    protected PublicInnerClass() {
+    }
+  }
+
+  @Shared
+  def tracker = new QuietOperationTracker();
+
+
+  @Unroll
+  def "asString(): #desc"() {
+
+    when:
+
+    Method m = clazz.getMethod(methodName, * paramTypes)
+
+    then:
+
+    InternalUtils.asString(m) == expected
+
+    where:
+
+    clazz       | methodName | paramTypes         | expected                                       | desc
+    Object      | "toString" | []                 | "java.lang.Object.toString()"                  | "method with no arguments"
+    Collections | "sort"     | [List, Comparator] | "java.util.Collections.sort(List, Comparator)" | "method with multiple argments"
+    Object      | "wait"     | [long]             | "java.lang.Object.wait(long)"                  | "method with primitive argument"
+    Arrays      | "sort"     | [int[]]            | "java.util.Arrays.sort(int[])"                 | "method with primitive array argument"
+    Arrays      | "sort"     | [Object[]]         | "java.util.Arrays.sort(Object[])"              | "method with object array argument"
+  }
+
+  @Unroll
+  def "size(): #desc"() {
+    expect:
+
+    InternalUtils.size(array as Object[]) == expected
+
+    where:
+
+    array     | expected | desc
+    []        | 0        | "empty array"
+    null      | 0        | "null is size 0"
+    [1, 2, 3] | 3        | "non-empty array"
+  }
+
+  @Unroll
+  def "stripMemberName('#input') should be '#expected'"() {
+    expect:
+
+    InternalUtils.stripMemberName(input) == expected
+
+    where:
+
+    input                         | expected
+    "simple"                      | "simple"
+    "_name"                       | "name"
+    '$name'                       | "name"
+    '__$ruby_style_'              | "ruby_style"
+    '$_$__$__$_$____$_$_$_$$name' | "name"
+    "foo_"                        | "foo"
+    "_foo_"                       | "foo"
+  }
+
+  def "invalid input to stripMemberName() is an exception"() {
+    when:
+
+    InternalUtils.stripMemberName("!foo")
+
+    then:
+
+    IllegalArgumentException e = thrown()
+
+    e.message == "Input '!foo' is not a valid Java identifier."
+  }
+
+  def "toList(Enumeration) is a sorted list"() {
+    when:
+
+    def e = Collections.enumeration(["wilma", "fred", "barney"])
+
+    then:
+
+    InternalUtils.toList(e) == ["barney", "fred", "wilma"]
+  }
+
+  @Unroll
+  def "join(): #desc"() {
+    expect:
+
+    InternalUtils.join(list) == expected
+
+    where:
+
+    list                            | expected                       | desc
+    ["barney"]                      | "barney"                       | "single value"
+    ["fred", "barney", "wilma"]     | "fred, barney, wilma"          | "multiple values"
+    ["fred", "barney", "", "wilma"] | "fred, barney, (blank), wilma" | "empty string converted to '(blank)'"
+  }
+
+  @Unroll
+  def "joinSorted(): #desc"() {
+    InternalUtils.joinSorted(list) == expected
+
+    where:
+
+    list                            | expected                       | desc
+    null                            | "(none)"                       | "null list is '(none)'"
+    []                              | "(none)"                       | "empty list is '(none)'"
+    ["barney"]                      | "barney"                       | "single value"
+    ["fred", "barney", "wilma"]     | "barney, fred, wilma"          | "multiple values"
+    ["fred", "barney", "", "wilma"] | "(blank), barney, fred, wilma" | "empty string converted to '(blank)'"
+  }
+
+  @Unroll
+  def "capitalize('#input') is '#expected'"() {
+    expect:
+
+    InternalUtils.capitalize(input) == expected
+
+    where:
+
+    input     | expected
+    "hello"   | "Hello"
+    "Goodbye" | "Goodbye"
+    ""        | ""
+    "a"       | "A"
+    "A"       | "A"
+  }
+
+  def "locationOf(Object)"() {
+    Locatable locatable = Mock()
+    Location l = Mock()
+
+    expect:
+
+    InternalUtils.locationOf(null) == null
+    InternalUtils.locationOf("La! La!") == null
+
+    InternalUtils.locationOf(l).is(l)
+
+    when:
+
+    def actual = InternalUtils.locationOf(locatable)
+
+    then:
+
+    _ * locatable.location >> l
+
+    actual.is(l)
+  }
+
+  @Unroll
+  def "sortedKeys(): #desc"() {
+    expect:
+
+    InternalUtils.sortedKeys(map) == expected
+
+    where:
+
+    map                                        | expected           | desc
+    null                                       | []                 | "null map"
+    [:]                                        | []                 | "empty map"
+    ["fred": "flintstone", "barney": "rubble"] | ["barney", "fred"] | "standard map"
+  }
+
+  @Unroll
+  def "get(Map,Object): #desc"() {
+    expect:
+
+    InternalUtils.get(map, key) == expected
+
+    where:
+
+    map                    | key      | expected     | desc
+    null                   | null     | null         | "null key and map"
+    null                   | "foo"    | null         | "null map"
+    ["fred": "flintstone"] | "fred"   | "flintstone" | "real map and key"
+    ["fred": "flintstone"] | "barney" | null         | "real map with missing key"
+  }
+
+  def "reverseIterator(List)"() {
+    when:
+
+    def i = InternalUtils.reverseIterator(["a", "b", "c"])
+
+    then:
+
+    i.hasNext()
+    i.next() == "c"
+
+    i.hasNext()
+    i.next() == "b"
+
+    i.hasNext()
+    i.next() == "a"
+
+    !i.hasNext()
+  }
+
+  def "remove() not supported by reverse Iterator"() {
+    def i = InternalUtils.reverseIterator(["a", "b", "c"])
+
+    when:
+
+    i.remove()
+
+    then:
+
+    thrown(UnsupportedOperationException)
+  }
+
+  @Unroll
+  def "lastTerm(): #desc"() {
+    expect:
+
+    InternalUtils.lastTerm(input) == expected
+
+    where:
+
+    input             | expected | desc
+    "simple"          | "simple" | "single term"
+    "fee.fie.foe.fum" | "fum"    | "dotted name sequence"
+  }
+
+  def "simple value passed to lastTerm() returns the exact input value"() {
+    def input = "simple"
+
+    expect:
+
+    InternalUtils.lastTerm(input).is(input)
+  }
+
+  def "addToMapList()"() {
+    def map = [:]
+
+    when:
+
+    InternalUtils.addToMapList(map, "fred", 1)
+
+    then:
+
+    map == ["fred": [1]]
+
+    when:
+
+    InternalUtils.addToMapList(map, "fred", 2)
+
+    then:
+
+    map == ["fred": [1, 2]]
+  }
+
+  def "validateMarkerAnnotation()"() {
+
+    when:
+
+    InternalUtils.validateMarkerAnnotation(Inject)
+
+    then:
+
+    noExceptionThrown()
+
+    when:
+
+    InternalUtils.validateMarkerAnnotations([Inject, NotRetainedRuntime] as Class[])
+
+    then:
+
+    IllegalArgumentException e = thrown()
+
+    e.message == "Marker annotation class org.apache.tapestry5.ioc.internal.util.NotRetainedRuntime is not valid because it is not visible at runtime. Add a @Retention(RetentionPolicy.RUNTIME) to the class."
+  }
+
+  def "close(Closable) for null does nothing"() {
+    when:
+    InternalUtils.close(null)
+
+    then:
+    noExceptionThrown()
+  }
+
+  def "close(Closable) for success case"() {
+    Closeable c = Mock()
+
+    when:
+
+    InternalUtils.close(c)
+
+    then:
+
+    1 * c.close()
+  }
+
+  def "close(Closable) ignores exceptions"() {
+    Closeable c = Mock()
+
+    when:
+
+    InternalUtils.close(c)
+
+    then:
+
+    1 * c.close() >> {
+      throw new IOException("ignored")
+    }
+  }
+
+  def "constructor with Tapestry @Inject annotation"() {
+    when:
+
+    def c = InternalUtils.findAutobuildConstructor(InjectoBean)
+
+    then:
+
+    c.parameterTypes == [String]
+  }
+
+  def "constructor with javax @Inject annotation"() {
+    when:
+
+    def c = InternalUtils.findAutobuildConstructor(JavaxInjectBean)
+
+    then:
+
+    c.parameterTypes == [String]
+  }
+
+  def "too many autobuild constructors"() {
+    when:
+
+    InternalUtils.findAutobuildConstructor(TooManyAutobuildConstructorsBean)
+
+    then:
+
+    IllegalArgumentException e = thrown()
+
+    e.message == "Too many autobuild constructors found: use either @org.apache.tapestry5.ioc.annotations.Inject or @javax.inject.Inject annotation to mark a single constructor for autobuilding."
+  }
+
+  def "validateConstructorForAutobuild(): ensure check that the class itself is public"() {
+    def cons = PrivateInnerClass.constructors[0]
+
+    when:
+
+    InternalUtils.validateConstructorForAutobuild(cons)
+
+    then:
+
+    IllegalArgumentException e = thrown()
+
+    e.message == "Class ${PrivateInnerClass.name} is not a public class and may not be autobuilt."
+  }
+
+  def "validateConstructorForAutobuild(): ensure check that constructor is public"() {
+    def cons = PublicInnerClass.declaredConstructors[0]
+
+    when:
+
+    InternalUtils.validateConstructorForAutobuild(cons)
+
+    then:
+
+    IllegalArgumentException e = thrown()
+
+    e.message == "Constructor protected ${PublicInnerClass.name}() is not public and may not be used for autobuilding an instance of the class. " +
+        "You should make the constructor public, or mark an alternate public constructor with the @Inject annotation."
+  }
+
+  def "@Inject service annotation on a field"() {
+    ObjectLocator ol = Mock()
+    def target = new FieldInjectionViaInjectService()
+    Runnable fred = Mock()
+
+    when:
+
+    InternalUtils.injectIntoFields(target, ol, null, tracker)
+
+    then:
+
+    target.fred.is(fred)
+
+    1 * ol.getService("FredService", Runnable) >> fred
+  }
+
+  def "@javax.annotations.Inject / @Named annotation on field"() {
+    ObjectLocator ol = Mock()
+    def target = new FieldInjectionViaJavaxNamed()
+    Runnable fred = Mock()
+
+    when:
+
+    InternalUtils.injectIntoFields(target, ol, null, tracker)
+
+    then:
+
+    target.fred.is(fred)
+
+    1 * ol.getService("BarneyService", Runnable) >> fred
+  }
+
+  def "@Inject annotation on field"() {
+    ObjectLocator ol = Mock()
+    def target = new FieldInjectionViaInject()
+    SymbolSource source = Mock()
+    InjectionResources resources = Mock()
+
+    when:
+
+    InternalUtils.injectIntoFields(target, ol, resources, tracker)
+
+    then:
+
+    target.symbolSource.is(source)
+
+    1 * resources.findResource(SymbolSource, SymbolSource) >> null
+    1 * ol.getObject(SymbolSource, _) >> { type, ap ->
+      assert ap.getAnnotation(Builtin) != null
+
+      return source
+    }
+  }
+
+  def "@javax.annotation.Inject annotation on field"() {
+    ObjectLocator ol = Mock()
+    def target = new FieldInjectionViaJavaxInject()
+    SymbolSource source = Mock()
+    InjectionResources resources = Mock()
+
+    when:
+
+    InternalUtils.injectIntoFields(target, ol, resources, tracker)
+
+    then:
+
+    target.symbolSource.is(source)
+
+    1 * resources.findResource(SymbolSource, SymbolSource) >> null
+    1 * ol.getObject(SymbolSource, _) >> { type, ap ->
+      assert ap.getAnnotation(Builtin) != null
+
+      return source
+    }
+  }
+
+  def "check handling of exception while injecting into a field"() {
+    ObjectLocator ol = Mock()
+    def target = new FieldInjectionViaInjectService()
+
+    when:
+
+    InternalUtils.injectIntoFields(target, ol, null, tracker)
+
+    then:
+
+    Exception e = thrown()
+
+    1 * ol.getService("FredService", Runnable) >> "NotTheRightType"
+
+    e.message.contains "Unable to set field 'fred' of <FieldInjectionViaInjectService> to NotTheRightType"
+  }
+
+  @Unroll
+  def "keys(Map): #desc"() {
+    expect:
+
+    InternalUtils.keys(map) == (expected as Set)
+
+    where:
+
+    map                                        | expected           | desc
+    null                                       | []                 | "null map"
+    [:]                                        | []                 | "empty map"
+    ["fred": "flintstone", "barney": "rubble"] | ["fred", "barney"] | "non-empty map"
+  }
+
+  @Unroll
+  def "size(Collection): #desc"() {
+    expect:
+
+    InternalUtils.size(coll) == expected
+
+    where:
+
+    coll      | expected | desc
+    null      | 0        | "null collection"
+    []        | 0        | "empty collection"
+    [1, 2, 3] | 3        | "non-empty collection"
+  }
+
+  def "toServiceDef2() delegates most methods to ServiceDef instance"() {
+    ServiceDef delegate = Mock()
+    ServiceBuilderResources resources = Mock()
+    ObjectCreator creator = Mock()
+    def serviceId = "fred"
+    def markers = [] as Set
+
+    ServiceDef2 sd = InternalUtils.toServiceDef2(delegate)
+
+    when:
+
+    def actual = sd.createServiceCreator(resources)
+
+    then:
+
+    actual.is creator
+
+    1 * delegate.createServiceCreator(resources) >> creator
+
+
+    when:
+
+    actual = sd.getServiceId()
+
+    then:
+    actual.is serviceId
+
+    1 * delegate.serviceId >> serviceId
+
+    when:
+
+    actual = sd.markers
+
+    then:
+
+    actual.is markers
+    1 * delegate.markers >> markers
+
+
+    when:
+
+    actual = sd.serviceInterface
+
+    then:
+
+    actual == Runnable
+    1 * delegate.serviceInterface >> Runnable
+
+    when:
+
+    actual = sd.serviceScope
+
+    then:
+
+    actual == ScopeConstants.PERTHREAD
+    1 * delegate.serviceScope >> ScopeConstants.PERTHREAD
+
+    when:
+
+    actual = sd.eagerLoad
+
+    then:
+
+    actual == true
+    1 * delegate.eagerLoad >> true
+
+    expect:
+
+    !sd.preventDecoration
+  }
+
+  def "matchAndSort()"() {
+    def pred = { !it.startsWith(".") } as Predicate
+
+    expect:
+
+    InternalUtils.matchAndSort(["Fred", "Barney", "..", ".hidden", "Wilma"], pred) == ["Barney", "Fred", "Wilma"]
+  }
+
+  def "toMapper(Coercion)"() {
+    def coercion = { it.toUpperCase() } as Coercion
+
+    def flow = F.flow("Mary", "had", "a", "little", "lamb")
+
+    expect:
+
+    flow.map(InternalUtils.toMapper(coercion)).toList() == ["MARY", "HAD", "A", "LITTLE", "LAMB"]
+  }
+}
+

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/JustInTimeObjectCreatorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/JustInTimeObjectCreatorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/JustInTimeObjectCreatorSpec.groovy
new file mode 100644
index 0000000..bfd1c59
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/JustInTimeObjectCreatorSpec.groovy
@@ -0,0 +1,53 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.ObjectCreator
+import org.apache.tapestry5.ioc.internal.ServiceActivityTracker
+import org.apache.tapestry5.ioc.internal.services.JustInTimeObjectCreator
+import org.apache.tapestry5.ioc.services.Status
+import spock.lang.Specification
+
+class JustInTimeObjectCreatorSpec extends Specification {
+
+  static final String SERVICE_ID = "FooBar";
+
+  def "can not create object after shutdown"() {
+
+    ObjectCreator creator = Mock()
+
+    def jit = new JustInTimeObjectCreator(null, creator, SERVICE_ID)
+
+    // Simulate the invocation from the Registry when it shuts down.
+    jit.run()
+
+    when:
+
+    jit.createObject()
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Proxy for service FooBar is no longer active because the IOC Registry has been shut down."
+  }
+
+  def "lazily instantiates the object via its delegate creator"() {
+
+    ObjectCreator creator = Mock()
+    Object service = new Object()
+    ServiceActivityTracker tracker = Mock()
+
+    def jit = new JustInTimeObjectCreator(tracker, creator, SERVICE_ID)
+
+    when:
+
+    jit.eagerLoadService()
+
+    then:
+
+    1 * creator.createObject() >> service
+    1 * tracker.setStatus(SERVICE_ID, Status.REAL)
+    0 * _
+
+    jit.createObject().is service
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/LazyAdvisorImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/LazyAdvisorImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/LazyAdvisorImplSpec.groovy
new file mode 100644
index 0000000..cfc6fc0
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/LazyAdvisorImplSpec.groovy
@@ -0,0 +1,140 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Greeter
+import org.apache.tapestry5.ioc.annotations.NotLazy
+import org.apache.tapestry5.ioc.services.AspectDecorator
+import org.apache.tapestry5.ioc.services.LazyAdvisor
+
+import java.sql.SQLException
+
+public interface LazyService {
+
+  void notLazyBecauseVoid();
+
+  String notLazyBecauseOfReturnValue();
+
+  /**
+   * The only lazy method.
+   */
+  Greeter createGreeter() throws RuntimeException;
+
+  Greeter safeCreateCreator();
+
+  @NotLazy
+  Greeter notLazyFromAnnotationGreeter();
+
+  Greeter notLazyCreateGreeter() throws SQLException;
+}
+
+class LazyAdvisorImplSpec extends AbstractSharedRegistrySpecification {
+
+  def LazyService advise(LazyService base) {
+    def decorator = getService AspectDecorator
+    def advisor = getService LazyAdvisor
+
+    def builder = decorator.createBuilder LazyService, base, "<LazyService Proxy>"
+
+
+    advisor.addLazyMethodInvocationAdvice builder
+
+    builder.build()
+  }
+
+  LazyService service = Mock()
+  LazyService advised = advise service
+
+  def "void methods are not lazy"() {
+
+    when:
+
+    advised.notLazyBecauseVoid()
+
+    then:
+
+    service.notLazyBecauseVoid()
+  }
+
+  def "methods with a non-interface return type are not lazy"() {
+
+    when:
+
+    assert advised.notLazyBecauseOfReturnValue() == "so true"
+
+    then:
+
+    1 * service.notLazyBecauseOfReturnValue() >> "so true"
+  }
+
+  def "returned thunks cache the return value"() {
+
+    Greeter greeter = Mock()
+
+    when:
+
+    def thunk = advised.createGreeter()
+
+    then:
+
+    0 * _
+
+    when:
+
+    assert thunk.greeting == "Lazy!"
+
+    then:
+
+    1 * service.createGreeter() >> greeter
+    1 * greeter.greeting >> "Lazy!"
+    0 * _
+
+    when:
+
+    assert thunk.greeting == "Still Lazy!"
+
+    then: "the greeter instance is cached"
+
+    1 * greeter.greeting >> "Still Lazy!"
+    0 * _
+  }
+
+  def "a checked exception will prevent laziness"() {
+
+    Greeter greeter = Mock()
+
+    when:
+
+    assert advised.notLazyCreateGreeter().is(greeter)
+
+    then:
+
+    1 * service.notLazyCreateGreeter() >> greeter
+    0 * _
+  }
+
+  def "the @NotLazy annotation prevents laziness"() {
+
+    Greeter greeter = Mock()
+
+    when:
+
+    assert advised.notLazyFromAnnotationGreeter().is(greeter)
+
+    then:
+
+    1 * service.notLazyFromAnnotationGreeter() >> greeter
+    0 * _
+  }
+
+  def "thunk class is cached"() {
+
+    when:
+
+    def g1 = advised.createGreeter()
+    def g2 = advised.safeCreateCreator()
+
+    then:
+
+    g1.class == g2.class
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/LocalizedNamesGeneratorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/LocalizedNamesGeneratorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/LocalizedNamesGeneratorSpec.groovy
new file mode 100644
index 0000000..474ef00
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/LocalizedNamesGeneratorSpec.groovy
@@ -0,0 +1,39 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.util.LocalizedNameGenerator
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class LocalizedNamesGeneratorSpec extends Specification {
+
+  @Unroll
+  def "Localized names for #path and #locale are '#expected'"() {
+
+    when:
+
+    LocalizedNameGenerator g = new LocalizedNameGenerator(path, locale)
+
+    then:
+
+    expected.tokenize().each {
+      assert g.hasNext()
+      assert g.next() == it
+    }
+
+    !g.hasNext()
+
+    where:
+
+    path            | locale                       | expected
+
+    "basic.test"    | Locale.US                    | "basic_en_US.test basic_en.test basic.test"
+    "noCountry.zap" | Locale.FRENCH                | "noCountry_fr.zap noCountry.zap"
+    "fred.foo"      | new Locale("en", "", "GEEK") | "fred_en__GEEK.foo fred_en.foo fred.foo"
+    "context:/blah" | Locale.FRENCH                | "context:/blah_fr context:/blah"
+    "context:/blah" | new Locale("fr", "", "GEEK") | "context:/blah_fr__GEEK context:/blah_fr context:/blah"
+
+    // The double-underscore is correct, it's a kind of placeholder for the null country. JDK1.3 always converts the locale to upper case. JDK 1.4
+    // does not. To keep this test happyt, we selected an all-uppercase locale.
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/LocationImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/LocationImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/LocationImplSpec.groovy
new file mode 100644
index 0000000..ea71e00
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/LocationImplSpec.groovy
@@ -0,0 +1,89 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.util.ClasspathResource
+import org.apache.tapestry5.ioc.internal.util.LocationImpl
+import spock.lang.Shared
+import spock.lang.Specification
+
+class LocationImplSpec extends Specification {
+
+  @Shared
+  def random = new Random()
+
+  @Shared
+  def resource = new ClasspathResource("/foo/Bar.xml")
+
+  def "toString() with all three parameters"() {
+    def line = random.nextInt()
+    def column = random.nextInt()
+
+    when:
+
+    def location = new LocationImpl(resource, line, column)
+
+    then:
+
+    location.resource.is(resource)
+    location.line == line
+    location.column == column
+
+    location.toString() == "$resource, line $line, column $column"
+  }
+
+  def "toString() with unknown column"() {
+    def line = random.nextInt()
+
+    when:
+
+    def location = new LocationImpl(resource, line)
+
+    then:
+
+    location.resource.is(resource)
+    location.line == line
+    location.toString() == "$resource, line $line"
+  }
+
+  def "unknown line and column"() {
+    when:
+
+    def location = new LocationImpl(resource,)
+
+    then:
+
+    location.resource.is(resource)
+    location.toString() == resource.toString()
+  }
+
+  def "equality"() {
+
+    when:
+
+    def l1 = new LocationImpl(resource, 22, 7)
+    def l2 = new LocationImpl(resource, 22, 7)
+    def l3 = new LocationImpl(null, 22, 7)
+    def l4 = new LocationImpl(resource, 99, 7)
+    def l5 = new LocationImpl(resource, 22, 99)
+    def l6 = new LocationImpl(new ClasspathResource("/baz/Biff.txt"), 22, 7)
+
+    then:
+
+    l1 == l1
+    l1 != null
+
+    l1 == l2
+    l2.hashCode() == l1.hashCode()
+
+    l3 != l1
+    l3.hashCode() != l1.hashCode()
+
+    l4 != l1
+    l4.hashCode() != l1.hashCode()
+
+    l5 != l1
+    l5.hashCode() != l1.hashCode()
+
+    l6 != l1
+    l6.hashCode() != l1.hashCode()
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/LoggingDecoratorImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/LoggingDecoratorImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/LoggingDecoratorImplSpec.groovy
new file mode 100644
index 0000000..248cb2a
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/LoggingDecoratorImplSpec.groovy
@@ -0,0 +1,173 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.ToStringService
+import org.apache.tapestry5.ioc.internal.UpcaseService
+import org.apache.tapestry5.ioc.services.LoggingDecorator
+import org.slf4j.Logger
+import org.xml.sax.SAXParseException
+
+interface AdderService {
+
+  long add(long operand1, long operand2);
+}
+
+interface ExceptionService {
+
+  void parse() throws SAXParseException;
+}
+
+class LoggingDecoratorImplSpec extends AbstractSharedRegistrySpecification {
+
+  LoggingDecorator decorator = getService LoggingDecorator
+
+  Logger logger = Mock()
+
+  def "logging of void method"() {
+
+    _ * logger.debugEnabled >> true
+
+    Runnable delegate = Mock()
+
+    Runnable interceptor = decorator.build(Runnable, delegate, "foo.Bar", logger)
+
+    when:
+
+    interceptor.run()
+
+    then:
+
+    1 * logger.debug("[ENTER] run()")
+
+    then:
+
+    1 * delegate.run()
+
+    then:
+
+    1 * logger.debug("[ EXIT] run")
+
+    interceptor.toString() == "<Logging interceptor for foo.Bar(java.lang.Runnable)>"
+  }
+
+  def "runtime exception inside method is logged"() {
+    _ * logger.debugEnabled >> true
+
+    Runnable delegate = Mock()
+
+    Runnable interceptor = decorator.build(Runnable, delegate, "foo.Bar", logger)
+
+    def t = new RuntimeException("From delegate.")
+
+    when:
+
+    interceptor.run()
+
+    then:
+
+    1 * logger.debug("[ENTER] run()")
+
+    then:
+
+    1 * delegate.run() >> {
+      throw t
+    }
+
+    then:
+
+    1 * logger.debug("[ FAIL] run -- ${RuntimeException.name}", t)
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.is t
+  }
+
+  def "method throws checked exception"() {
+    Throwable t = new SAXParseException("From delegate.", null)
+    _ * logger.debugEnabled >> true
+    ExceptionService delegate = Mock()
+
+    ExceptionService service = decorator.build(ExceptionService, delegate, "MyService", logger)
+
+    when:
+
+    service.parse()
+
+    then:
+
+    Throwable actual = thrown()
+
+    actual.is(t)
+
+    1 * logger.debug("[ENTER] parse()")
+
+    1 * delegate.parse() >> { throw t }
+
+    1 * logger.debug("[ FAIL] parse -- ${SAXParseException.name}", t)
+  }
+
+  def "handling of object parameter and return type"() {
+    _ * logger.debugEnabled >> true
+
+    UpcaseService delegate = Mock()
+
+    UpcaseService service = decorator.build(UpcaseService, delegate, "MyService", logger)
+
+    when:
+
+    assert service.upcase("barney") == "BARNEY"
+
+    then:
+
+    1 * logger.debug('[ENTER] upcase("barney")')
+
+    1 * delegate.upcase(_) >> { args -> args[0].toUpperCase() }
+
+    1 * logger.debug('[ EXIT] upcase ["BARNEY"]')
+  }
+
+  def "handling of primitive parameter and return type"() {
+    _ * logger.debugEnabled >> true
+
+    AdderService delegate = Mock()
+
+    AdderService service = decorator.build(AdderService, delegate, "Adder", logger)
+
+    when:
+
+    assert service.add(6, 13) == 19
+
+    then:
+
+    1 * logger.debug("[ENTER] add(6, 13)")
+
+    1 * delegate.add(_, _) >> { args -> args[0] + args[1] }
+
+    1 * logger.debug("[ EXIT] add [19]")
+  }
+
+  def "toString() method of service interface is delegated"() {
+    _ * logger.debugEnabled >> true
+
+    // Spock's Mocking doesn't seem to be as savvy as Tapestry's about letting toString()
+    // delegate through, so we can't implement ToStringService as a Mock
+
+    ToStringService delegate = new ToStringService() {
+
+      String toString() { "FROM DELEGATE" }
+    }
+
+    ToStringService service = decorator.build(ToStringService, delegate, "ToString", logger)
+
+    when:
+
+    assert service.toString() == "FROM DELEGATE"
+
+    then:
+
+    1 * logger.debug("[ENTER] toString()")
+    1 * logger.debug('[ EXIT] toString ["FROM DELEGATE"]')
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/LoggingSourceImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/LoggingSourceImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/LoggingSourceImplSpec.groovy
new file mode 100644
index 0000000..14c61c7
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/LoggingSourceImplSpec.groovy
@@ -0,0 +1,30 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.LoggerSource
+import org.apache.tapestry5.ioc.internal.LoggerSourceImpl
+import org.slf4j.LoggerFactory
+import spock.lang.Specification
+
+class LoggingSourceImplSpec extends Specification {
+
+  LoggerSource loggerSource = new LoggerSourceImpl()
+
+  def "get logger by class"() {
+    Class clazz = getClass()
+
+    expect:
+
+    loggerSource.getLogger(clazz).is(LoggerFactory.getLogger(clazz))
+  }
+
+  def "get logger by name"() {
+    String name = "foo.Bar"
+
+    expect:
+
+    loggerSource.getLogger(name).is(LoggerFactory.getLogger(name))
+
+  }
+
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy
new file mode 100644
index 0000000..8c13398
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/ManifestProcessingSpec.groovy
@@ -0,0 +1,37 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.IOCUtilities
+import org.apache.tapestry5.ioc.RegistryBuilder
+import spock.lang.Specification
+
+class ManifestProcessingSpec extends Specification {
+
+  def "invalid class in manifest"() {
+
+    File fakejar = new File("src/test/fakejar")
+
+    expect:
+
+    // This is more to verify the module execution environment
+    fakejar.exists()
+    fakejar.isDirectory()
+
+    when:
+
+    URL url = fakejar.toURL()
+    URLClassLoader loader = new URLClassLoader([url] as URL[], Thread.currentThread().contextClassLoader)
+
+    RegistryBuilder builder = new RegistryBuilder(loader)
+
+    IOCUtilities.addDefaultModules(builder)
+
+    then:
+
+    RuntimeException e = thrown()
+
+    e.message.contains "Exception loading module(s) from manifest"
+    e.message.contains "Failure loading Tapestry IoC module class does.not.exist.Module"
+
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/MasterObjectProviderImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/MasterObjectProviderImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/MasterObjectProviderImplSpec.groovy
new file mode 100644
index 0000000..f8fbc84
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/MasterObjectProviderImplSpec.groovy
@@ -0,0 +1,110 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.AnnotationProvider
+import org.apache.tapestry5.ioc.ObjectLocator
+import org.apache.tapestry5.ioc.ObjectProvider
+import org.apache.tapestry5.ioc.OperationTracker
+import org.apache.tapestry5.ioc.internal.QuietOperationTracker
+import org.apache.tapestry5.ioc.internal.services.MasterObjectProviderImpl
+import org.apache.tapestry5.ioc.services.MasterObjectProvider
+import spock.lang.Shared
+import spock.lang.Specification
+
+class MasterObjectProviderImplSpec extends Specification {
+
+  @Shared
+  OperationTracker tracker = new QuietOperationTracker()
+
+  def "found match via first provider"() {
+    ObjectProvider prov1 = Mock()
+    ObjectProvider prov2 = Mock()
+    AnnotationProvider ap = Mock()
+    ObjectLocator locator = Mock()
+    Runnable expected = Mock()
+
+    MasterObjectProvider mop = new MasterObjectProviderImpl([prov1, prov2], tracker)
+
+    when:
+
+    assert mop.provide(Runnable, ap, locator, true).is(expected)
+
+    then:
+
+    1 * prov1.provide(Runnable, ap, locator) >> expected
+    0 * _
+  }
+
+  def "found match after first provider"() {
+    ObjectProvider prov1 = Mock()
+    ObjectProvider prov2 = Mock()
+    AnnotationProvider ap = Mock()
+    ObjectLocator locator = Mock()
+    Runnable expected = Mock()
+
+    MasterObjectProvider mop = new MasterObjectProviderImpl([prov1, prov2], tracker)
+
+    when:
+
+    assert mop.provide(Runnable, ap, locator, true).is(expected)
+
+    then:
+
+    1 * prov1.provide(Runnable, ap, locator) >> null
+
+    then:
+
+    1 * prov2.provide(Runnable, ap, locator) >> expected
+    0 * _
+  }
+
+  def "no match found on optional search returns null"() {
+    ObjectProvider prov1 = Mock()
+    ObjectProvider prov2 = Mock()
+    AnnotationProvider ap = Mock()
+    ObjectLocator locator = Mock()
+
+    MasterObjectProvider mop = new MasterObjectProviderImpl([prov1, prov2], tracker)
+
+    when:
+
+    assert mop.provide(Runnable, ap, locator, false) == null
+
+    then:
+
+    1 * prov1.provide(Runnable, ap, locator) >> null
+
+    then:
+
+    1 * prov2.provide(Runnable, ap, locator) >> null
+    0 * _
+  }
+
+  def "no match for a required search delegates to the ObjectLocator.getService(Class)"() {
+    ObjectProvider prov1 = Mock()
+    ObjectProvider prov2 = Mock()
+    AnnotationProvider ap = Mock()
+    ObjectLocator locator = Mock()
+    Runnable expected = Mock()
+
+    MasterObjectProvider mop = new MasterObjectProviderImpl([prov1, prov2], tracker)
+
+    when:
+
+    assert mop.provide(Runnable, ap, locator, true).is(expected)
+
+    then:
+
+    1 * prov1.provide(Runnable, ap, locator) >> null
+
+    then:
+
+    1 * prov2.provide(Runnable, ap, locator) >> null
+
+    then:
+
+    1 * locator.getService(Runnable) >> expected
+
+    0 * _
+
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/MessageFormatterImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/MessageFormatterImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/MessageFormatterImplSpec.groovy
new file mode 100644
index 0000000..5e5c0f1
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/MessageFormatterImplSpec.groovy
@@ -0,0 +1,28 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.util.MessageFormatterImpl
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class MessageFormatterImplSpec extends Specification {
+
+  @Unroll
+  def "standard formatting: #desc"() {
+
+    def mf = new MessageFormatterImpl(format, null)
+
+    expect:
+
+    mf.format(* args) == expected
+
+    where:
+
+    format                    | args                                            | expected                                         | desc
+
+    "Tapestry is %s."         | ["cool"]                                        | "Tapestry is cool."                              | "simple substition"
+    "Tapestry release #%d."   | [5]                                             | "Tapestry release #5."                           | "numeric conversion"
+    "%s is %s at version %d." | ["Tapestry", "cool", 5]                         | "Tapestry is cool at version 5."                 | "multiple conversions"
+    "%s failed: %s"           | ["Something", new RuntimeException("bad wolf")] | "Something failed: bad wolf"                     | "expansion of exception message"
+    "%s failed: %s"           | ["Another", new NullPointerException()]         | "Another failed: java.lang.NullPointerException" | "expansion of exception without message is exception class name"
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/MessagesImplSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/MessagesImplSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/MessagesImplSpec.groovy
new file mode 100644
index 0000000..d063a09
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/MessagesImplSpec.groovy
@@ -0,0 +1,76 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.Messages
+import org.apache.tapestry5.ioc.internal.util.MessagesImpl
+import org.apache.tapestry5.ioc.internal.util.TargetMessages
+import spock.lang.Shared
+import spock.lang.Specification
+import spock.lang.Unroll
+
+class MessagesImplSpec extends Specification {
+
+  @Shared
+  Messages messages = MessagesImpl.forClass(TargetMessages)
+
+  @Unroll
+  def "contains key: #desc"() {
+
+    expect:
+
+    messages.contains(key) == expectation
+
+    where:
+
+    key       | expectation | desc
+    "no-args" | true        | "base case"
+    "xyzzyz"  | false       | "key not present"
+    "No-Args" | true        | "case insensitive"
+  }
+
+  @Unroll
+  def "get message from catalog: #desc"() {
+    expect:
+
+    messages.get(key) == expectation
+
+    where:
+
+    key                | expectation                       | desc
+
+    "no-args"          | "No arguments."                   | "base case"
+    "something-failed" | "Something failed: %s"            | "does not attempt to expand conversions"
+    "No-Args"          | "No arguments."                   | "access is case insensitive"
+    "does-not-exist"   | "[[missing key: does-not-exist]]" | "fake value supplied for missing key"
+  }
+
+  @Unroll
+  def "format message:#desc"() {
+    expect:
+
+    messages.format(key, value) == expectation
+
+    where:
+
+    key              | value    | expectation                       | desc
+    "result"         | "good"   | "The result is 'good'."           | "standard"
+    "Result"         | "best"   | "The result is 'best'."           | "lookup is case insensitive"
+    "does-not-exist" | "xyzzyz" | "[[missing key: does-not-exist]]" | "fake value supplied for missing key"
+  }
+
+  def "access a MesageFormatter to format content"() {
+    def mf = messages.getFormatter("result")
+
+    expect:
+
+    mf.format("cool") == "The result is 'cool'."
+  }
+
+  def "MessageFormatters are cached"() {
+    def mf1 = messages.getFormatter("result")
+    def mf2 = messages.getFormatter("result")
+
+    expect:
+
+    mf1.is(mf2)
+  }
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/MethodIteratorSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/MethodIteratorSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/MethodIteratorSpec.groovy
new file mode 100644
index 0000000..a3b7af8
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/MethodIteratorSpec.groovy
@@ -0,0 +1,115 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.MethodIterator
+import org.apache.tapestry5.ioc.internal.services.MethodSignature
+import spock.lang.Specification
+import spock.lang.Unroll
+
+interface Play extends Runnable {
+
+  void jump()
+}
+
+interface Runnable2 {
+
+  void run()
+}
+
+interface Runnable3 extends Runnable, Runnable2 {
+
+}
+
+interface Openable {
+
+  public void open();
+}
+
+interface OpenableWithError {
+
+  public void open() throws IOException;
+}
+
+interface CombinedOpeneable extends Openable, OpenableWithError {
+}
+
+class MethodIteratorSpec extends Specification {
+
+  def "iterate a simple (single-method) interface"() {
+
+    MethodIterator mi = new MethodIterator(Runnable)
+
+    expect:
+
+    mi.hasNext()
+
+    when: "iterate to first method"
+
+    def actual = mi.next()
+
+    then: "first method signature returned"
+
+    actual == new MethodSignature(void, "run", null, null)
+
+    !mi.hasNext()
+
+    when: "iterating when no method signatures left"
+
+    mi.next()
+
+    then: "throws exception"
+
+    thrown(NoSuchElementException)
+  }
+
+  def "method inherited from super interface are visible"() {
+
+    MethodIterator mi = new MethodIterator(Play)
+
+    expect:
+
+    mi.hasNext()
+
+    mi.next() == new MethodSignature(void, "jump", null, null)
+
+    mi.hasNext()
+
+    mi.next() == new MethodSignature(void, "run", null, null)
+
+    !mi.hasNext()
+  }
+
+  @Unroll
+  def "getToString() on #interfaceType.name should be #expected"() {
+
+    expect:
+
+    new MethodIterator(interfaceType).getToString() == expected
+
+    where:
+
+    interfaceType | expected
+    Runnable      | false
+    Play          | false
+    ToString      | true
+  }
+
+  def "method duplicated from a base interface into a sub interface are filtered out"() {
+    MethodIterator mi = new MethodIterator(Runnable3)
+
+    expect:
+
+    mi.next() == new MethodSignature(void, "run", null, null)
+    !mi.hasNext()
+  }
+
+  def "inherited methods are filtered out if less specific"() {
+    MethodIterator mi = new MethodIterator(CombinedOpeneable)
+
+    expect:
+
+    mi.next() == new MethodSignature(void, "open", null, [IOException] as Class[])
+
+    !mi.hasNext()
+  }
+
+}

http://git-wip-us.apache.org/repos/asf/tapestry-5/blob/a1bef869/tapestry-ioc/src/test/groovy/ioc/specs/MethodSignatureSpec.groovy
----------------------------------------------------------------------
diff --git a/tapestry-ioc/src/test/groovy/ioc/specs/MethodSignatureSpec.groovy b/tapestry-ioc/src/test/groovy/ioc/specs/MethodSignatureSpec.groovy
new file mode 100644
index 0000000..5745770
--- /dev/null
+++ b/tapestry-ioc/src/test/groovy/ioc/specs/MethodSignatureSpec.groovy
@@ -0,0 +1,179 @@
+package ioc.specs
+
+import org.apache.tapestry5.ioc.internal.services.MethodSignature
+import spock.lang.Specification
+import spock.lang.Unroll
+
+import java.lang.reflect.Method
+import java.sql.SQLException
+
+class MethodSignatureSpec extends Specification {
+
+  def MethodSignature find(Class sourceClass, String methodName) {
+    Method match = sourceClass.methods.find { it.name == methodName }
+
+    if (match == null) {
+      throw new IllegalStateException("Call $sourceClass.name has no method named '$methodName'.")
+    }
+
+    return new MethodSignature(match)
+  }
+
+  @Unroll
+  def "#firstClass.name and #secondClass.name have identical MethodSignatures for method #methodName"() {
+
+    when:
+
+    def m1 = find firstClass, methodName
+    def m2 = find secondClass, methodName
+
+    then:
+
+    m1.hashCode() == m2.hashCode()
+    m1 == m2
+
+    where:
+
+    firstClass  | secondClass       | methodName
+    Object      | Boolean           | "hashCode"
+    String      | StringBuilder     | "charAt"
+    ObjectInput | ObjectInputStream | "close"
+  }
+
+  def "a null parameter or exception list is equivalent to an empty one"() {
+    def m1 = new MethodSignature(void, "foo", null, null)
+    def m2 = new MethodSignature(void, "foo", [] as Class[], [] as Class[])
+
+    expect:
+
+    m1 == m2
+    m2 == m1
+
+    m1.hashCode() == m2.hashCode()
+  }
+
+  def "a mismatch of method name causes inequality"() {
+    def m1 = new MethodSignature(void, "foo", null, null)
+    def m2 = new MethodSignature(void, "bar", null, null)
+
+    expect:
+
+    m1 != m2
+  }
+
+  def "a mismatch of parameters causes inequality"() {
+    def m1 = new MethodSignature(void, "foo", [String] as Class[], null)
+    def m2 = new MethodSignature(void, "foo", [Boolean] as Class[], null)
+
+    expect:
+
+    m1 != m2
+  }
+
+  def "a MethodSignature never equals null"() {
+
+    expect:
+
+    new MethodSignature(void, "foo", null, null) != null
+  }
+
+  def "a MethodSignature may only equal another MethodSignature"() {
+
+    expect:
+
+    new MethodSignature(void, "foo", null, null) != "Any Old Thing"
+  }
+
+  @Unroll
+  def "MethodSignature.toString() for #clazz.name #methodName is '#toString'"() {
+
+    def sig = find(clazz, methodName)
+
+    expect:
+
+    sig.toString() == toString
+
+    where:
+
+    clazz  | methodName    | toString
+    String | "getChars"    | "void getChars(int, int, char[], int)"
+    Class  | "newInstance" | "java.lang.Object newInstance() throws java.lang.IllegalAccessException, java.lang.InstantiationException"
+  }
+
+  @Unroll
+  def "MethodSignature.uniqueId for #clazz.name #methodName is '#uniqueId'"() {
+    def sig = find(clazz, methodName)
+
+    expect:
+
+    sig.uniqueId == uniqueId
+
+    where:
+
+    clazz  | methodName    | uniqueId
+    String | "getChars"    | "getChars(int,int,char[],int)"
+    Class  | "newInstance" | "newInstance()"
+  }
+
+  def "different return types will prevent override"() {
+
+    def m1 = new MethodSignature(void, "foo", null, null)
+    def m2 = new MethodSignature(int, "foo", null, null)
+
+    expect:
+
+    !m1.isOverridingSignatureOf(m2)
+  }
+
+  def "different method names will prevent override"() {
+    def m1 = new MethodSignature(int, "foo", null, null)
+    def m2 = new MethodSignature(int, "bar", null, null)
+
+    expect:
+
+    !m1.isOverridingSignatureOf(m2)
+  }
+
+  def "different parameter types will prevent override"() {
+    def m1 = new MethodSignature(int, "foo", null, null)
+    def m2 = new MethodSignature(int, "foo", [String] as Class[], null)
+
+    expect:
+
+    !m1.isOverridingSignatureOf(m2)
+  }
+
+  def "a difference of exceptions thrown allows for override"() {
+    def m1 = new MethodSignature(int, "foo", null, [Exception] as Class[])
+    def m2 = new MethodSignature(int, "foo", null, [RuntimeException] as Class[])
+
+    expect:
+
+    // All of m2's exceptions are assignable to at least one of m1's exceptions
+    m1.isOverridingSignatureOf(m2)
+    !m2.isOverridingSignatureOf(m1)
+  }
+
+  def "signature with no exceptions will not override"() {
+    def m1 = new MethodSignature(int, "foo", null, null)
+    def m2 = new MethodSignature(int, "foo", null, [RuntimeException] as Class[])
+
+    expect:
+
+    !m1.isOverridingSignatureOf(m2)
+    m2.isOverridingSignatureOf(m1)
+  }
+
+  def "complex matching of signature exceptions when determining override"() {
+    def m1 = new MethodSignature(void, "close", null,
+        [SQLException, NumberFormatException] as Class[])
+    def m2 = new MethodSignature(void.class, "close", null,
+        [SQLException, IOException] as Class[])
+
+    expect:
+
+    // NumberFormatException and IOException don't fit in either direction
+    !m1.isOverridingSignatureOf(m2)
+    !m2.isOverridingSignatureOf(m1)
+  }
+}