You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@tapestry.apache.org by th...@apache.org on 2021/05/31 17:23:08 UTC

[tapestry-5] branch master updated: TAP5-2668: Move spock-tapestry extension to tapestry-test

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

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


The following commit(s) were added to refs/heads/master by this push:
     new f9576b8  TAP5-2668: Move spock-tapestry extension to tapestry-test
f9576b8 is described below

commit f9576b8a5a548ed3edd547d1b495f6d2a68195cb
Author: Thiago H. de Paula Figueiredo <th...@arsmachina.com.br>
AuthorDate: Mon May 31 14:20:42 2021 -0300

    TAP5-2668: Move spock-tapestry extension to tapestry-test
    
    Thanks Volker Lamp for the patch!
---
 settings.gradle                                    |   1 +
 tapestry-spock/README.md                           |  43 +++++
 tapestry-spock/build.gradle                        |  13 ++
 .../apache/tapestry5/spock/ExtensionModule.java    |  37 +++++
 .../org/apache/tapestry5/spock/SpockTapestry.java  |  37 +++++
 .../tapestry5/spock/TapestryInterceptor.java       | 182 +++++++++++++++++++++
 .../tapestry5/spock/TapestrySpockExtension.java    | 148 +++++++++++++++++
 ...ockframework.runtime.extension.IGlobalExtension |   1 +
 .../spock/BeforeRegistryCreatedMethod.groovy       |  83 ++++++++++
 .../tapestry5/spock/InjectionExamples.groovy       | 119 ++++++++++++++
 .../spock/InjectionExamplesWithImportModule.groovy | 116 +++++++++++++
 .../tapestry5/spock/TapestrySpecInheritance.groovy |  72 ++++++++
 .../java/org/apache/tapestry5/spock/Module1.java   |  39 +++++
 .../java/org/apache/tapestry5/spock/Module2.java   |  29 ++++
 .../java/org/apache/tapestry5/spock/Service1.java  |  22 +++
 .../org/apache/tapestry5/spock/Service1Impl.java   |  32 ++++
 .../java/org/apache/tapestry5/spock/Service2.java  |  22 +++
 .../org/apache/tapestry5/spock/Service2Impl.java   |  27 +++
 .../java/org/apache/tapestry5/spock/Service3.java  |  22 +++
 .../org/apache/tapestry5/spock/Service3Impl.java   |  36 ++++
 20 files changed, 1081 insertions(+)

diff --git a/settings.gradle b/settings.gradle
index 9921957..afc6fbe 100644
--- a/settings.gradle
+++ b/settings.gradle
@@ -6,3 +6,4 @@ include "tapestry-test-data", 'tapestry-internal-test', "tapestry-ioc-junit"
 include "tapestry-webresources", "tapestry-runner", "tapestry-test-constants"
 include "tapestry-ioc-jcache", "beanmodel", "commons", "genericsresolver-guava", "tapestry-version-migrator"
 // include "tapestry-cdi"
+include "tapestry-spock"
diff --git a/tapestry-spock/README.md b/tapestry-spock/README.md
new file mode 100644
index 0000000..d545dcb
--- /dev/null
+++ b/tapestry-spock/README.md
@@ -0,0 +1,43 @@
+# Support for Tapestry injections in Spock specifications
+This sub-project, `tapestry-spock`, provides support for Tapestry injections in [Spock](https://spockframework.org/) specifications. Spock is a testing and specification framework for Java and Groovy applications.
+
+## Usage example
+
+```java
+ @ImportModule(UniverseModule)
+ class UniverseSpec extends Specification {
+  
+    @Inject
+    UniverseService service
+ 
+    UniverseService copy = service
+ 
+    def "service knows the answer to the universe"() {
+      expect:
+      copy == service        // injection occurred before 'copy' was initialized
+      service.answer() == 42 // what else did you expect?!
+    }
+  }
+```
+
+`@ImportModule` indicates which Tapestry module(s) should be started (and subsequently shut down). The deprecated `@SubModule` annotation is still supported for compatibility reasons.
+
+`@Inject` marks fields which should be injected with a Tapestry service or symbol. Related Tapestry annotations, such as `@Service` and `@Symbol`, are also supported.
+
+## Drop-in replacement for spock-tapestry
+This software was originally part of the Spock project, known as `spock-tapestry`. The Tapestry project adopted the software as `tapestry-spock` to overcome the backwards incompatibilities introduced by Tapestry 5.7.
+
+`tapestry-spock` works as a drop-in replacement for `spock-tapestry`. So, when upgrading to Tapestry 5.7, use 
+
+`testRuntimeOnly "org.apache.tapestry:tapestry-spock:$tapestryVersion"` (Gradle)
+
+instead of
+
+`testRuntimeOnly "org.spockframework:spock-tapestry:$spockVersion"`.
+
+
+## References
+
+1. [https://issues.apache.org/jira/browse/TAP5-2668](https://issues.apache.org/jira/browse/TAP5-2668)
+2. [https://github.com/spockframework/spock/issues/1312](https://github.com/spockframework/spock/issues/1312)
+3. [https://github.com/spockframework/spock/pull/1315](https://github.com/spockframework/spock/pull/1315)
diff --git a/tapestry-spock/build.gradle b/tapestry-spock/build.gradle
new file mode 100644
index 0000000..5c94718
--- /dev/null
+++ b/tapestry-spock/build.gradle
@@ -0,0 +1,13 @@
+description = "Provides support Tapestry injections in Spock specifications"
+
+dependencies {
+	compile project(':commons')
+	compile project(':tapestry-ioc')
+	compile "org.spockframework:spock-core:${versions.spock}"
+	
+	testCompile "javax.inject:javax.inject:1"
+}
+
+test {
+	useJUnit()
+}
diff --git a/tapestry-spock/src/main/java/org/apache/tapestry5/spock/ExtensionModule.java b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/ExtensionModule.java
new file mode 100644
index 0000000..d678cbc
--- /dev/null
+++ b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/ExtensionModule.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2009, 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import org.apache.tapestry5.commons.ObjectLocator;
+import org.apache.tapestry5.ioc.annotations.Marker;
+
+/**
+ * A Tapestry module that is started for every specification which uses Spock's
+ * Tapestry extension.
+ *
+ * @author Peter Niederwieser
+ */
+@Marker(SpockTapestry.class)
+public class ExtensionModule
+{
+	
+    public static ObjectLocator build(ObjectLocator locator)
+    {
+        return locator;
+    }
+    
+}
diff --git a/tapestry-spock/src/main/java/org/apache/tapestry5/spock/SpockTapestry.java b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/SpockTapestry.java
new file mode 100644
index 0000000..393b8c5
--- /dev/null
+++ b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/SpockTapestry.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2011 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Marker annotation for services that are specific to the Spock/Tapestry
+ * integration module.
+ *
+ * @see ExtensionModule
+ */
+@Target({ PARAMETER, FIELD, METHOD })
+@Retention(RUNTIME)
+@Documented
+public @interface SpockTapestry {}
\ No newline at end of file
diff --git a/tapestry-spock/src/main/java/org/apache/tapestry5/spock/TapestryInterceptor.java b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/TapestryInterceptor.java
new file mode 100644
index 0000000..4fe0ac9
--- /dev/null
+++ b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/TapestryInterceptor.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.tapestry5.commons.AnnotationProvider;
+import org.apache.tapestry5.ioc.Registry;
+import org.apache.tapestry5.ioc.RegistryBuilder;
+import org.apache.tapestry5.ioc.annotations.Autobuild;
+import org.apache.tapestry5.ioc.annotations.Inject;
+import org.apache.tapestry5.ioc.annotations.InjectService;
+import org.spockframework.runtime.extension.AbstractMethodInterceptor;
+import org.spockframework.runtime.extension.IMethodInvocation;
+import org.spockframework.runtime.model.FieldInfo;
+import org.spockframework.runtime.model.SpecInfo;
+import org.spockframework.util.ReflectionUtil;
+
+import spock.lang.Shared;
+import spock.lang.Specification;
+
+/**
+ * Controls creation, startup and shutdown of the Tapestry container,
+ * and injects specifications with Tapestry-provided objects.
+ *
+ * @author Peter Niederwieser
+ */
+public class TapestryInterceptor extends AbstractMethodInterceptor
+{
+    private final SpecInfo spec;
+    
+    private final Set<Class<?>> modules;
+
+    private Registry registry;
+
+    public TapestryInterceptor(SpecInfo spec, Set<Class<?>> modules)
+    {
+        this.spec = spec;
+        this.modules = modules;
+    }
+
+    @Override
+    public void interceptSharedInitializerMethod(IMethodInvocation invocation) throws Throwable
+    {
+        runBeforeRegistryCreatedMethods((Specification) invocation.getSharedInstance());
+        
+        createAndStartupRegistry();
+        
+        injectServices(invocation.getSharedInstance(), true);
+        
+        invocation.proceed();
+    }
+
+    @Override
+    public void interceptCleanupSpecMethod(IMethodInvocation invocation) throws Throwable
+    {
+        try
+        {
+            invocation.proceed();
+        }
+        finally 
+        {
+            shutdownRegistry();
+        }
+    }
+
+    @Override
+    public void interceptInitializerMethod(IMethodInvocation invocation) throws Throwable
+    {
+        injectServices(invocation.getInstance(), false);
+        
+        invocation.proceed();
+    }
+
+    private void runBeforeRegistryCreatedMethods(Specification spec)
+    {
+        Object returnValue;
+
+        for (Method method : findAllBeforeRegistryCreatedMethods())
+        {
+            returnValue = ReflectionUtil.invokeMethod(spec, method);
+
+            // Return values of a type other than Registry are silently ignored.
+            // This avoids problems in case the method unintentionally returns a
+            // value (which is common due to Groovy's implicit return).
+            if (returnValue instanceof Registry)
+                registry = (Registry) returnValue;
+        }
+    }
+
+    private void createAndStartupRegistry()
+    {
+        if (registry != null) return;
+
+        RegistryBuilder builder = new RegistryBuilder();
+        builder.add(ExtensionModule.class);
+        
+        for (Class<?> module : modules) builder.add(module);
+        
+        registry = builder.build();
+        registry.performRegistryStartup();
+    }
+
+    private List<Method> findAllBeforeRegistryCreatedMethods()
+    {
+        List<Method> methods = new ArrayList<>();
+
+        for (SpecInfo curr : spec.getSpecsTopToBottom())
+        {
+            Method method = ReflectionUtil.getDeclaredMethodByName(curr.getReflection(), "beforeRegistryCreated");
+            if (method != null)
+            {
+                method.setAccessible(true);
+                methods.add(method);
+            }
+        }
+
+        return methods;
+    }
+
+    private void injectServices(Object target, boolean sharedFields) throws IllegalAccessException
+    {
+        for (final FieldInfo field : spec.getAllFields())
+        {
+            Field rawField = field.getReflection();
+            
+            if ((rawField.isAnnotationPresent(Inject.class)
+            || ReflectionUtil.isAnnotationPresent(rawField, "javax.inject.Inject")
+            || rawField.isAnnotationPresent(Autobuild.class))
+            && rawField.isAnnotationPresent(Shared.class) == sharedFields)
+            {
+                Object value = registry.getObject(rawField.getType(), createAnnotationProvider(field));
+                rawField.setAccessible(true);
+                rawField.set(target, value);
+            }
+            else if (rawField.isAnnotationPresent(InjectService.class))
+            {
+                String serviceName = rawField.getAnnotation(InjectService.class).value();
+                Object value = registry.getService(serviceName, rawField.getType());
+                rawField.setAccessible(true);
+                rawField.set(target, value);
+            }
+        }
+    }
+
+    private AnnotationProvider createAnnotationProvider(final FieldInfo field)
+    {
+        return new AnnotationProvider()
+        {
+            @Override
+            public <T extends Annotation> T getAnnotation(Class<T> annotationClass)
+            {
+                return field.getAnnotation(annotationClass);
+            }
+        };
+    }
+
+    private void shutdownRegistry()
+    {
+        if (registry != null)
+            registry.shutdown();
+    }
+}
diff --git a/tapestry-spock/src/main/java/org/apache/tapestry5/spock/TapestrySpockExtension.java b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/TapestrySpockExtension.java
new file mode 100644
index 0000000..22e3cb7
--- /dev/null
+++ b/tapestry-spock/src/main/java/org/apache/tapestry5/spock/TapestrySpockExtension.java
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import org.spockframework.runtime.extension.*;
+import org.spockframework.runtime.model.SpecInfo;
+import org.spockframework.util.ReflectionUtil;
+
+import java.lang.annotation.Annotation;
+import java.util.*;
+
+import org.apache.tapestry5.ioc.annotations.*;
+
+/**
+ * Facilitates <a href="https://spockframework.org/">Spock</a>-based integration testing
+ * of Tapestry modules. Spock is a testing and specification framework for Java and Groovy
+ * applications.
+ * 
+ * Supports injection of Tapestry services into Spock specifications.
+ * 
+ * <p><b>Usage example:</b>
+ *
+ * <pre>
+ * &#64;ImportModule(UniverseModule)
+ * class UniverseSpec extends Specification {
+ * 
+ *   &#64;Inject
+ *   UniverseService service
+ *
+ *   UniverseService copy = service
+ *
+ *   def "service knows the answer to the universe"() {
+ *     expect:
+ *     copy == service        // injection occurred before 'copy' was initialized
+ *     service.answer() == 42 // what else did you expect?!
+ *   }
+ * }
+ * </pre>
+ * 
+ * <p><tt>&#64;ImportModule</tt> indicates which Tapestry module(s) should be started
+ *  (and subsequently shut down). The deprecated <tt>&#64;SubModule</tt> annotation
+ *  is still supported for compatibility reasons.
+ *  
+ * <p><tt>&#64;Inject</tt> marks fields which should be injected with a Tapestry service or
+ * symbol. Related Tapestry annotations, such as <tt>&#64;Service</tt> and <tt>&#64;Symbol</tt>,
+ * are also supported.
+ * 
+ * <p>Note: Only field (and no constructor) injection is supported.
+ * 
+ * <p>To interact directly with the Tapestry registry, an injection point of type
+ * <tt>ObjectLocator</tt> may be defined. However, this should be rarely needed.
+ *
+ * <p>For every specification annotated with <tt>&#64;ImportModule</tt> or
+ * <tt>&#64;SubModule</tt>, the Tapestry registry will be started up (and subsequently shut down)
+ * once. Because fields are injected <em>before</em> field initializers and the <tt>setup()</tt>/
+ * <tt>setupSpec()</tt> methods are run, they can be safely accessed from these places.
+ *
+ * <p>Fields marked as <tt>&#64;Shared</tt> are injected once per specification; regular
+ * fields once per feature (iteration). However, this does <em>not</em> mean that each
+ * feature will receive a fresh service instance; rather, it is left to the Tapestry
+ * registry to control the lifecycle of a service. Most Tapestry services use the default
+ * "singleton" scope, which results in the same service instance being shared between all
+ * features.
+ *
+ * <p>Features that require their own service instance(s) should be moved into separate
+ * specifications. To avoid code fragmentation and duplication, you might want to put
+ * multiple (micro-)specifications into the same source file, and factor out their
+ * commonalities into a base class.
+ *
+ *
+ * @author Peter Niederwieser
+ */
+public class TapestrySpockExtension extends AbstractGlobalExtension
+{
+
+    // since Tapestry 5.4
+    @SuppressWarnings("unchecked")
+    private static final Class<? extends Annotation> importModuleAnnotation =
+        (Class)ReflectionUtil.loadClassIfAvailable("org.apache.tapestry5.ioc.annotations.ImportModule");
+
+    // deprecated as of Tapestry 5.4
+    @SuppressWarnings("unchecked")
+    private static final Class<? extends Annotation> submoduleAnnotation =
+        (Class)ReflectionUtil.loadClassIfAvailable("org.apache.tapestry5.ioc.annotations.SubModule");
+
+    @Override
+    public void visitSpec(final SpecInfo spec)
+    {
+        Set<Class<?>> modules = collectModules(spec);
+        if (modules == null) return;
+
+        IMethodInterceptor interceptor = new TapestryInterceptor(spec, modules);
+        spec.addSharedInitializerInterceptor(interceptor);
+        spec.addInitializerInterceptor(interceptor);
+        spec.addCleanupSpecInterceptor(interceptor);
+    }
+
+    // Returns null if no ImportModule or SubModule annotation was found.
+    // Returns an empty list if one or more ImportModule or SubModule annotations were found,
+    // but they didn't specify any modules. This distinction is important to
+    // allow activation of the extension w/o specifying any modules.
+    private Set<Class<?>> collectModules(SpecInfo spec)
+    {
+        Set<Class<?>> modules = null;
+
+        for (SpecInfo curr : spec.getSpecsTopToBottom())
+        {
+            if (importModuleAnnotation != null && spec.isAnnotationPresent(importModuleAnnotation))
+            {
+                ImportModule importModule = curr.getAnnotation(ImportModule.class);
+                if (importModule != null)
+                {
+                    if (modules == null) { modules = new HashSet<>(); }
+                    modules.addAll(Arrays.<Class<?>>asList(importModule.value()));
+                }
+            }
+            
+            // Support for deprecated @SubModule.
+            if (submoduleAnnotation != null && spec.isAnnotationPresent(submoduleAnnotation))
+            {
+                @SuppressWarnings("deprecation")
+                SubModule subModule = curr.getAnnotation(SubModule.class);
+                if (subModule != null)
+                {
+                    if (modules == null) { modules = new HashSet<>(); }
+                    modules.addAll(Arrays.<Class<?>>asList(subModule.value()));
+                }
+            }
+        }
+
+        return modules;
+    }
+
+}
diff --git a/tapestry-spock/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension b/tapestry-spock/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension
new file mode 100644
index 0000000..dae201e
--- /dev/null
+++ b/tapestry-spock/src/main/resources/META-INF/services/org.spockframework.runtime.extension.IGlobalExtension
@@ -0,0 +1 @@
+org.apache.tapestry5.spock.TapestrySpockExtension
\ No newline at end of file
diff --git a/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/BeforeRegistryCreatedMethod.groovy b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/BeforeRegistryCreatedMethod.groovy
new file mode 100644
index 0000000..a83128a
--- /dev/null
+++ b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/BeforeRegistryCreatedMethod.groovy
@@ -0,0 +1,83 @@
+/*
+ * Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock
+
+import org.apache.tapestry5.ioc.RegistryBuilder
+import org.apache.tapestry5.ioc.annotations.*
+
+import spock.lang.*
+
+@SubModule(Module1)
+class BeforeRegistryCreatedMethod extends Specification {
+  @Shared
+  @Inject
+  Service1 sharedServiceField
+
+  @Inject
+  Service1 instanceServiceField
+
+  @Shared
+  String sharedField = "sharedField"
+
+  String instanceField = "instanceField"
+
+  @Shared
+  boolean beforeRegistryCreatedRun
+
+  def beforeRegistryCreated() {
+    assert sharedServiceField == null
+    assert instanceServiceField == null
+    assert sharedField == null
+    assert instanceField == null
+    assert !beforeRegistryCreatedRun
+    beforeRegistryCreatedRun = true
+  }
+
+  def beforeSpec() {
+    assert sharedServiceField instanceof Service1Impl
+    assert instanceServiceField == null
+    assert sharedField == "sharedField"
+    assert instanceField == null
+    assert beforeRegistryCreatedRun
+  }
+
+  def feature() {
+    expect:
+    sharedServiceField instanceof Service1
+    instanceServiceField instanceof Service1
+    sharedField == "sharedField"
+    instanceField == "instanceField"
+    beforeRegistryCreatedRun
+  }
+}
+
+@SubModule([])
+class CustomTapestryRegistry extends Specification {
+  @Inject
+  Service1 service1
+
+  def beforeRegistryCreated() {
+    def builder = new RegistryBuilder()
+    builder.add(Module1)
+    def registry = builder.build()
+    registry.performRegistryStartup()
+    registry
+  }
+
+  def "custom registry is used for injection"() {
+    expect:
+    service1 instanceof Service1
+  }
+}
\ No newline at end of file
diff --git a/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/InjectionExamples.groovy b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/InjectionExamples.groovy
new file mode 100644
index 0000000..9537f5f
--- /dev/null
+++ b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/InjectionExamples.groovy
@@ -0,0 +1,119 @@
+package org.apache.tapestry5.spock
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+
+import org.apache.tapestry5.commons.ObjectLocator
+import org.apache.tapestry5.ioc.annotations.*
+
+import spock.lang.*
+
+@SubModule(Module1)
+class ServiceInjection extends Specification {
+  @Inject
+  Service1 service1
+
+  @Inject
+  Service2 service2
+
+  def "injected services"() {
+	expect:
+	service1.generateString() == service2.generateQuickBrownFox()
+  }
+}
+
+@SubModule(Module1)
+class ServiceInjectionWithJavaxInject extends Specification {
+  @javax.inject.Inject
+  org.apache.tapestry5.spock.Service1 service1
+
+  @javax.inject.Inject
+  org.apache.tapestry5.spock.Service2 service2
+
+  def "injected services"() {
+	expect:
+	service1.generateString() == service2.generateQuickBrownFox()
+  }
+}
+
+@SubModule(Module1)
+class ServiceInjectionWithServiceId extends Specification {
+  @InjectService("Service3")
+  Service3 service3
+
+  @Inject
+  Service2 service2
+
+  def "injected services"() {
+	expect:
+	service3.generateString() == service2.generateQuickBrownFox()
+  }
+}
+
+@SubModule(Module1)
+class ObjectLocatorInjection extends Specification {
+  @Inject
+  private ObjectLocator locator
+
+  def "injected object locator"() {
+	expect:
+	locator.getService(Service1).generateString() == locator.getService(Service2).generateQuickBrownFox()
+  }
+}
+
+@SubModule(Module1)
+class SymbolInjection extends Specification {
+  @Inject
+  @Symbol("java.version")
+  String javaVersion
+
+  @Inject
+  @Symbol("configKey") // Groovy doesn't yet support constants in annotations, so we have to use a literal
+  String configValue
+
+  @Inject
+  @Value("\${java.version} and \${configKey}")
+  String computedValue
+
+  def "injected system property"() {
+	expect:
+	javaVersion == System.getProperty("java.version")
+  }
+
+  def "injected application default"() {
+	expect:
+	configValue == "configValue"
+  }
+
+  def "injected computed value"() {
+	expect:
+	computedValue == "${System.getProperty("java.version")} and configValue"
+  }
+}
+
+@SubModule(Module2)
+class AutobuildInjection extends Specification {
+  @Autobuild
+  Service1Impl service1
+
+  def "auto-built service"() {
+	expect:
+	service1 instanceof Service1Impl
+  }
+}
+
+class InjectionExamples {
+}
diff --git a/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/InjectionExamplesWithImportModule.groovy b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/InjectionExamplesWithImportModule.groovy
new file mode 100644
index 0000000..a8a006c
--- /dev/null
+++ b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/InjectionExamplesWithImportModule.groovy
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock
+
+import org.apache.tapestry5.commons.ObjectLocator
+import org.apache.tapestry5.ioc.annotations.*
+
+import spock.lang.*
+
+@ImportModule(Module1)
+class ServiceInjectionWithImportModule extends Specification {
+  @Inject
+  Service1 service1
+
+  @Inject
+  Service2 service2
+
+  def "injected services"() {
+    expect:
+    service1.generateString() == service2.generateQuickBrownFox()
+  }
+}
+
+@ImportModule(Module1)
+class ServiceInjectionWithJavaxInjectWithImportModule extends Specification {
+  @javax.inject.Inject
+  org.apache.tapestry5.spock.Service1 service1
+
+  @javax.inject.Inject
+  org.apache.tapestry5.spock.Service2 service2
+
+  def "injected services"() {
+    expect:
+    service1.generateString() == service2.generateQuickBrownFox()
+  }
+}
+
+@ImportModule(Module1)
+class ServiceInjectionWithServiceIdWithImportModule extends Specification {
+  @InjectService("Service3")
+  Service3 service3
+
+  @Inject
+  Service2 service2
+
+  def "injected services"() {
+    expect:
+    service3.generateString() == service2.generateQuickBrownFox()
+  }
+}
+
+@ImportModule(Module1)
+class ObjectLocatorInjectionWithImportModule extends Specification {
+  @Inject
+  private ObjectLocator locator
+
+  def "injected object locator"() {
+    expect:
+    locator.getService(Service1).generateString() == locator.getService(Service2).generateQuickBrownFox()
+  }
+}
+
+@ImportModule(Module1)
+class SymbolInjectionWithImportModule extends Specification {
+  @Inject
+  @Symbol("java.version")
+  String javaVersion
+
+  @Inject
+  @Symbol("configKey") // Groovy doesn't yet support constants in annotations, so we have to use a literal
+  String configValue
+
+  @Inject
+  @Value('${java.version} and ${configKey}')
+  String computedValue
+
+  def "injected system property"() {
+    expect:
+    javaVersion == System.getProperty("java.version")
+  }
+
+  def "injected application default"() {
+    expect:
+    configValue == "configValue"
+  }
+
+  def "injected computed value"() {
+    expect:
+    computedValue == "${System.getProperty("java.version")} and configValue"
+  }
+}
+
+@ImportModule(Module2)
+class AutobuildInjectionWithImportModule extends Specification {
+  @Autobuild
+  Service1Impl service1
+
+  def "auto-built service"() {
+    expect:
+    service1 instanceof Service1Impl
+  }
+}
diff --git a/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/TapestrySpecInheritance.groovy b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/TapestrySpecInheritance.groovy
new file mode 100644
index 0000000..35c4c37
--- /dev/null
+++ b/tapestry-spock/src/test/groovy/org/apache/tapestry5/spock/TapestrySpecInheritance.groovy
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock
+
+import org.apache.tapestry5.ioc.annotations.*
+
+import spock.lang.*
+
+@SubModule(Module1)
+abstract class BaseSpec extends Specification {
+  static beforeRegistryCreatedCount = 0
+
+  @Inject
+  Service1 service1
+
+  @Inject
+  Service2 service2
+
+  private beforeRegistryCreated() {
+	assert beforeRegistryCreatedCount == 0
+	beforeRegistryCreatedCount++
+  }
+
+  def setupSpec() {
+	assert beforeRegistryCreatedCount == 2
+  }
+
+  def setup() {
+	assert service1 instanceof Service1
+	assert service2 instanceof Service2
+  }
+}
+
+@SubModule(Module2)
+class TapestrySpecInheritance extends BaseSpec {
+  @Inject
+  Service2 anotherService2
+
+  def beforeRegistryCreated() {
+	assert beforeRegistryCreatedCount == 1
+	beforeRegistryCreatedCount++
+  }
+
+  def setup() {
+	assert service1 instanceof Service1
+	assert service2 instanceof Service2
+	assert anotherService2 instanceof Service2
+  }
+
+  def "fields of base class have been injected"() {
+	expect:
+	service1 instanceof Service1
+	service2 instanceof Service2
+  }
+
+  def "fields of derived class have been injected"() {
+	expect:
+	anotherService2 instanceof Service2
+  }
+}
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Module1.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Module1.java
new file mode 100644
index 0000000..0dfbfad
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Module1.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2009, 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import org.apache.tapestry5.commons.MappedConfiguration;
+import org.apache.tapestry5.ioc.ServiceBinder;
+import org.apache.tapestry5.ioc.annotations.SubModule;
+
+// Tapestry module classes cannot currently be written in Groovy
+// See https://issues.apache.org/jira/browse/TAPESTRY-2746
+@SubModule(Module2.class)
+public class Module1
+{
+    public static void bind(ServiceBinder binder)
+    {
+        binder.bind(Service1.class, Service1Impl.class);
+        binder.bind(Service3.class, Service3Impl.class);
+    }
+
+    public void contributeApplicationDefaults(MappedConfiguration<String, String> configuration)
+    {
+        configuration.add("configKey", "configValue");
+    }
+    
+}
\ No newline at end of file
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Module2.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Module2.java
new file mode 100644
index 0000000..6ab9e3a
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Module2.java
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import org.apache.tapestry5.ioc.ServiceBinder;
+
+// Tapestry module classes cannot currently be written in Groovy
+// See https://issues.apache.org/jira/browse/TAPESTRY-2746
+public class Module2
+{
+    public static void bind(ServiceBinder binder)
+    {
+        binder.bind(Service2.class, Service2Impl.class);
+    }
+}
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service1.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service1.java
new file mode 100644
index 0000000..a4384cf
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service1.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+public interface Service1
+{
+    String generateString();
+}
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service1Impl.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service1Impl.java
new file mode 100644
index 0000000..50ee5ac
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service1Impl.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+public class Service1Impl implements Service1
+{
+    private final Service2 service2;
+
+    public Service1Impl(Service2 service2)
+    {
+        this.service2 = service2;
+    }
+
+    public String generateString()
+    {
+        return service2.generateQuickBrownFox();
+    }
+}
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service2.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service2.java
new file mode 100644
index 0000000..b4ff046
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service2.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+public interface Service2
+{
+    String generateQuickBrownFox();
+}
\ No newline at end of file
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service2Impl.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service2Impl.java
new file mode 100644
index 0000000..63ea6f4
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service2Impl.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2009 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+public class Service2Impl implements Service2
+{
+	
+    public String generateQuickBrownFox()
+    {
+        return "The quick brown fox jumps over the lazy dog.";
+    }
+    
+}
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service3.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service3.java
new file mode 100644
index 0000000..d7f5b44
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service3.java
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+public interface Service3
+{
+    String generateString();
+}
diff --git a/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service3Impl.java b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service3Impl.java
new file mode 100644
index 0000000..e6aca66
--- /dev/null
+++ b/tapestry-spock/src/test/java/org/apache/tapestry5/spock/Service3Impl.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2013 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.tapestry5.spock;
+
+import org.apache.tapestry5.ioc.annotations.ServiceId;
+
+@ServiceId("Service3")
+public class Service3Impl implements Service3
+{
+    private final Service2 service2;
+
+    public Service3Impl(Service2 service2)
+    {
+        this.service2 = service2;
+    }
+
+    public String generateString()
+    {
+        return service2.generateQuickBrownFox();
+    }
+    
+}