diff --git a/docs/extensions.adoc b/docs/extensions.adoc index 0381838b14..4261e4c971 100644 --- a/docs/extensions.adoc +++ b/docs/extensions.adoc @@ -12,7 +12,7 @@ meta-annotation. === Ignore -To temporarily prevent a feature method from getting executed, annotate it with +spock.lang.Ignore+: +To temporarily prevent a feature method from getting executed, annotate it with `spock.lang.Ignore`: [source,groovy] ---- @@ -40,7 +40,7 @@ In most execution environments, ignored feature methods and specs will be report === IgnoreRest -To ignore all but a (typically) small subset of methods, annotate the latter with +spock.lang.IgnoreRest+: +To ignore all but a (typically) small subset of methods, annotate the latter with `spock.lang.IgnoreRest`: [source,groovy] ---- @@ -56,7 +56,7 @@ def "I'll also be ignored"() { ... } === IgnoreIf -To ignore a feature method under certain conditions, annotate it with +spock.lang.IgnoreIf+, +To ignore a feature method under certain conditions, annotate it with `spock.lang.IgnoreIf`, followed by a predicate: [source,groovy] @@ -95,6 +95,32 @@ def "I'll only run on Windows"() { ... } to state the conditions under which a method gets executed, rather than the conditions under which it gets ignored. +=== PendingFeature + +To indicate that the feature is not fully implemented yet and should not be reported as error, annotate it with `spock.lang.PendingFeature`. + +The use case is to annotate tests that can not yet run but should already be committed. +The main difference to `Ignore` is that the test are executed, but test failures are ignored. +If the test passes without an error, then it will be reported as failure since the `PendingFeature` annotation should be removed. +This way the tests will become part of the normal tests instead of being ignored forever. + +Groovy has the `groovy.transform.NotYetImplemented` annotation which is similar but behaves a differently. + +* it will mark failing tests as passed +* if at least one iteration of a data-driven test passes it will be reported as error + +`PendingFeature`: + +* it will mark failing tests as skipped +* if at least one iteration of a data-driven test fails it will be reported as skipped +* if every iteration of a data-driven test passes it will be reported as error + +[source,groovy] +---- +@PendingFeature +def "not implemented yet"() { ... } +---- + === Stepwise To execute features in the order that they are declared, annotate a spec class with `spock.lang.Stepwise`: diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureExtension.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureExtension.java new file mode 100644 index 0000000000..19be34a84d --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureExtension.java @@ -0,0 +1,19 @@ +package org.spockframework.runtime.extension.builtin; + +import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension; +import org.spockframework.runtime.model.FeatureInfo; +import spock.lang.PendingFeature; + +/** + * @author Leonard Brünings + */ +public class PendingFeatureExtension extends AbstractAnnotationDrivenExtension { + @Override + public void visitFeatureAnnotation(PendingFeature annotation, FeatureInfo feature) { + if (feature.isParameterized()) { + feature.addInterceptor(new PendingFeatureIterationInterceptor()); + } else { + feature.getFeatureMethod().addInterceptor(new PendingFeatureInterceptor()); + } + } +} diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureInterceptor.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureInterceptor.java new file mode 100644 index 0000000000..567df67104 --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureInterceptor.java @@ -0,0 +1,20 @@ +package org.spockframework.runtime.extension.builtin; + +import org.junit.AssumptionViolatedException; +import org.spockframework.runtime.extension.IMethodInterceptor; +import org.spockframework.runtime.extension.IMethodInvocation; + +/** + * @author Leonard Brünings + */ +class PendingFeatureInterceptor implements IMethodInterceptor { + @Override + public void intercept(IMethodInvocation invocation) throws Throwable { + try { + invocation.proceed(); + } catch (AssertionError e) { + throw new AssumptionViolatedException("Feature not yet implemented correctly."); + } + throw new AssertionError("Feature is marked with @PendingFeature but passes unexpectedly"); + } +} diff --git a/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureIterationInterceptor.java b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureIterationInterceptor.java new file mode 100644 index 0000000000..45ef89177b --- /dev/null +++ b/spock-core/src/main/java/org/spockframework/runtime/extension/builtin/PendingFeatureIterationInterceptor.java @@ -0,0 +1,42 @@ +package org.spockframework.runtime.extension.builtin; + +import org.junit.AssumptionViolatedException; +import org.spockframework.runtime.extension.IMethodInterceptor; +import org.spockframework.runtime.extension.IMethodInvocation; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * @author Leonard Brünings + */ +class PendingFeatureIterationInterceptor implements IMethodInterceptor { + @Override + public void intercept(IMethodInvocation invocation) throws Throwable { + + AtomicBoolean pass = new AtomicBoolean(false); + invocation.getFeature().getFeatureMethod().addInterceptor(new InnerIterationInterceptor(pass)); + invocation.proceed(); + if (pass.get()) { + throw new AssumptionViolatedException("Feature not yet implemented correctly."); + } else { + throw new AssertionError("Feature is marked with @PendingFeature but passes unexpectedly"); + } + } + + private static class InnerIterationInterceptor implements IMethodInterceptor { + private final AtomicBoolean pass; + + public InnerIterationInterceptor(AtomicBoolean pass) { + this.pass = pass; + } + + @Override + public void intercept(IMethodInvocation invocation) throws Throwable { + try { + invocation.proceed(); + } catch (AssertionError e) { + pass.set(true); + } + } + } +} diff --git a/spock-core/src/main/java/spock/lang/PendingFeature.java b/spock-core/src/main/java/spock/lang/PendingFeature.java new file mode 100644 index 0000000000..1ac5e54df8 --- /dev/null +++ b/spock-core/src/main/java/spock/lang/PendingFeature.java @@ -0,0 +1,43 @@ +package spock.lang; + + +import org.spockframework.runtime.extension.ExtensionAnnotation; +import org.spockframework.runtime.extension.builtin.IgnoreExtension; +import org.spockframework.runtime.extension.builtin.PendingFeatureExtension; +import org.spockframework.util.Beta; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Indicates that the feature is not fully implemented yet and should not be reported as error. + *

+ * The use case is to annotate tests that can not yet run but should already be committed. + * The main difference to {@link Ignore} is that the test are executed, but test failures are ignored. + * If the test passes without an error, then it will be reported as failure since the {@link PendingFeature} + * annotation should be removed. This way the tests will become part of the normal tests + * instead of being ignored forever. + *

+ *

+ * Groovy has the {@link groovy.transform.NotYetImplemented} annotation which is similar but behaves a differently. + *

+ * {@link PendingFeature}: + * + *

+ * @author Leonard Brünings + */ +@Beta +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@ExtensionAnnotation(PendingFeatureExtension.class) +public @interface PendingFeature { +} diff --git a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/PendingFeatureExtensionSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/PendingFeatureExtensionSpec.groovy new file mode 100644 index 0000000000..110ceb9177 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/PendingFeatureExtensionSpec.groovy @@ -0,0 +1,175 @@ +package org.spockframework.smoke.extension + +import org.spockframework.EmbeddedSpecification + +class PendingFeatureExtensionSpec extends EmbeddedSpecification { + + def "@PendingFeature marks failing feature as skipped"() { + when: + def result = runner.runWithImports(""" + +class Foo extends Specification { + @PendingFeature + def bar() { + expect: false + } +} + """) + + then: + notThrown(AssertionError) + result.runCount == 1 + result.failureCount == 0 + result.ignoreCount == 0 + } + + def "@PendingFeature marks passing feature as failed"() { + when: + runner.runWithImports(""" + +class Foo extends Specification { + @PendingFeature + def bar() { + expect: true + } +} + """) + + then: + AssertionError e = thrown(AssertionError) + e.message == "Feature is marked with @PendingFeature but passes unexpectedly" + } + + def "@PendingFeature marks data driven feature where every iteration fails as skipped"() { + when: + def result = runner.runWithImports(""" + +class Foo extends Specification { + @PendingFeature + def bar() { + expect: test + + where: + test << [false, false, false] + } +} + """) + + then: + notThrown(AssertionError) + result.runCount == 1 + result.failureCount == 0 + result.ignoreCount == 0 + } + + def "@PendingFeature marks @Unroll'ed data driven feature where every iteration fails as skipped"() { + when: + def result = runner.runWithImports(""" + +class Foo extends Specification { + @Unroll + @PendingFeature + def bar() { + expect: test + + where: + test << [false, false, false] + } +} + """) + + then: + notThrown(AssertionError) + result.runCount == 3 + result.failureCount == 0 + result.ignoreCount == 0 + } + + def "@PendingFeature marks data driven feature where at least one iteration fails as skipped"() { + when: + def result = runner.runWithImports(""" + +class Foo extends Specification { + @PendingFeature + def bar() { + expect: test + + where: + test << [true, false, true] + } +} + """) + + then: + notThrown(AssertionError) + result.runCount == 1 + result.failureCount == 0 + result.ignoreCount == 0 + } + + + def "@PendingFeature marks @Unroll'ed data driven feature where at least one iteration fails as skipped"() { + when: + def result = runner.runWithImports(""" + +class Foo extends Specification { + @Unroll + @PendingFeature + def bar() { + expect: test + + where: + test << [true, false, true] + } +} + """) + + then: + notThrown(AssertionError) + result.runCount == 3 + result.failureCount == 0 + result.ignoreCount == 0 + } + + def "@PendingFeature marks data driven feature where all iterations pass as failed"() { + when: + runner.runWithImports(""" + +class Foo extends Specification { + @PendingFeature + def bar() { + expect: test + + where: + test << [true, true, true] + } +} + """) + + then: + AssertionError e = thrown(AssertionError) + e.message == "Feature is marked with @PendingFeature but passes unexpectedly" + } + + + def "@PendingFeature marks @Unroll'ed data driven feature where all iterations pass as failed"() { + when: + runner.runWithImports(""" + +class Foo extends Specification { + @Unroll + @PendingFeature + def bar() { + expect: test + + where: + test << [true, true, true] + } +} + """) + + then: + AssertionError e = thrown(AssertionError) + e.message == "Feature is marked with @PendingFeature but passes unexpectedly" + } +}