diff --git a/docs/extensions.adoc b/docs/extensions.adoc index 96249f4075..f05f93a181 100644 --- a/docs/extensions.adoc +++ b/docs/extensions.adoc @@ -156,7 +156,7 @@ The main difference to `Ignore` is that the test are executed, but test failures 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. +Groovy has the `groovy.transform.NotYetImplemented` annotation which is similar but behaves 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 @@ -167,10 +167,26 @@ Groovy has the `groovy.transform.NotYetImplemented` annotation which is similar * 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] +[source,groovy,indent=0] +---- +include::{sourcedir}/extension/PendingFeatureDocSpec.groovy[tag=example-a] +---- + +You can also list one or multiple exceptions that are expected and cause the test to be skipped. +If other exceptions occur, the test fails as if no annotation would be present. + +[source,groovy,indent=0] +---- +include::{sourcedir}/extension/PendingFeatureDocSpec.groovy[tag=example-b] +---- + +It is also supported to have multiple `@PendingFeature` annotations or a mixture of `@PendingFeature` and +`@PendingFeatureIf`, for example to ignore certain exceptions only under certain conditions or show different +reasons for different exceptions. + +[source,groovy,indent=0] ---- -@PendingFeature -def "not implemented yet"() { ... } +include::{sourcedir}/extension/PendingFeatureDocSpec.groovy[tag=example-c] ---- === PendingFeatureIf diff --git a/docs/release_notes.adoc b/docs/release_notes.adoc index c68fc8a46b..5ccdf34be8 100644 --- a/docs/release_notes.adoc +++ b/docs/release_notes.adoc @@ -16,6 +16,8 @@ include::include.adoc[] - `@Issue` is now repeatable +- `@PendingFeature` is now repeatable + == 2.0-M3 (2020-06-11) diff --git a/spock-core/src/main/java/spock/lang/PendingFeature.java b/spock-core/src/main/java/spock/lang/PendingFeature.java index 21cd93ac6f..872fd451d5 100644 --- a/spock-core/src/main/java/spock/lang/PendingFeature.java +++ b/spock-core/src/main/java/spock/lang/PendingFeature.java @@ -35,6 +35,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target({ElementType.METHOD}) @ExtensionAnnotation(PendingFeatureExtension.class) +@Repeatable(PendingFeature.Container.class) public @interface PendingFeature { /** * Configures which types of Exceptions are expected in the pending feature. @@ -51,4 +52,14 @@ * @return reason why this feature is pending */ String reason() default ""; + + /** + * @since 2.0 + */ + @Beta + @Retention(RetentionPolicy.RUNTIME) + @Target({ElementType.METHOD}) + @interface Container { + PendingFeature[] value(); + } } diff --git a/spock-specs/src/test/groovy/org/spockframework/docs/extension/PendingFeatureDocSpec.groovy b/spock-specs/src/test/groovy/org/spockframework/docs/extension/PendingFeatureDocSpec.groovy new file mode 100644 index 0000000000..38c640ea09 --- /dev/null +++ b/spock-specs/src/test/groovy/org/spockframework/docs/extension/PendingFeatureDocSpec.groovy @@ -0,0 +1,42 @@ +package org.spockframework.docs.extension + +import spock.lang.PendingFeature +import spock.lang.PendingFeatureIf +import spock.lang.Specification + +class PendingFeatureDocSpec extends Specification { +// tag::example-a[] + @PendingFeature + def "not implemented yet"() { +// end::example-a[] + expect: + false + } + +// tag::example-b[] + @PendingFeature(exceptions = [ + UnsupportedOperationException, + IllegalArgumentException + ]) + def "I throw one of two exceptions"() { +// end::example-b[] + expect: + throw new IllegalArgumentException() + } + +// tag::example-c[] + @PendingFeature( + exceptions = UnsupportedOperationException, + reason = 'operation not yet supported') + @PendingFeature( + exceptions = IllegalArgumentException, + reason = 'arguments are broken') + @PendingFeatureIf( + value = { os.windows }, + reason = 'Does not yet work on Windows') + def "I have various problems in certain situations"() { +// end::example-c[] + expect: + throw new IllegalArgumentException() + } +} 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 index 117674e167..3749fbf54b 100644 --- a/spock-specs/src/test/groovy/org/spockframework/smoke/extension/PendingFeatureExtensionSpec.groovy +++ b/spock-specs/src/test/groovy/org/spockframework/smoke/extension/PendingFeatureExtensionSpec.groovy @@ -39,6 +39,29 @@ def bar() { } } + def "@PendingFeature marks failing feature as skipped even if applied twice"() { + when: + def result = runner.runSpecBody """ +@PendingFeature +@PendingFeature +def bar() { + expect: false +} +""" + + then: + verifyAll(result) { + testsStartedCount == 1 + testsSucceededCount == 0 + testsFailedCount == 0 + testsSkippedCount == 0 + testsAbortedCount == 1 + testEvents().aborted().assertEventsMatchExactly( + abortedWithReason(message('Feature not yet implemented correctly.')) + ) + } + } + def "@PendingFeature includes reason in exception message"() { when: def result = runner.runSpecBody """ @@ -61,6 +84,52 @@ def bar() { } } + def "@PendingFeature includes last reason in exception message if applied twice"() { + when: + def result = runner.runSpecBody """ +@PendingFeature(reason='42') +@PendingFeature(reason='4711') +def bar() { + expect: false +} +""" + + then: + verifyAll(result) { + testsStartedCount == 1 + testsSucceededCount == 0 + testsFailedCount == 0 + testsSkippedCount == 0 + testsAbortedCount == 1 + testEvents().aborted().assertEventsMatchExactly( + abortedWithReason(message('Feature not yet implemented correctly. Reason: 4711')) + ) + } + } + + def "@PendingFeature includes reason of matching annotation in exception message if applied twice"() { + when: + def result = runner.runSpecBody """ +@PendingFeature(exceptions=IllegalArgumentException, reason='42') +@PendingFeature(exceptions=IllegalAccessException, reason='4711') +def bar() { + expect: throw new IllegalArgumentException() +} +""" + + then: + verifyAll(result) { + testsStartedCount == 1 + testsSucceededCount == 0 + testsFailedCount == 0 + testsSkippedCount == 0 + testsAbortedCount == 1 + testEvents().aborted().assertEventsMatchExactly( + abortedWithReason(message('Feature not yet implemented correctly. Reason: 42')) + ) + } + } + def "@PendingFeature marks feature that fails with exception as skipped"() { when: def result = runner.runSpecBody """ @@ -107,6 +176,30 @@ def bar() { } } + def "@PendingFeature rethrows non handled exceptions even if applied twice"() { + when: + def result = runner.runSpecBody """ +@PendingFeature(exceptions=IndexOutOfBoundsException) +@PendingFeature(exceptions=IllegalAccessException) +def bar() { + expect: + throw new IllegalArgumentException() +} +""" + + then: + verifyAll(result) { + testsStartedCount == 1 + testsSucceededCount == 0 + testsFailedCount == 1 + testsSkippedCount == 0 + testsAbortedCount == 0 + testEvents().failed().assertEventsMatchExactly( + finishedWithFailure(instanceOf(IllegalArgumentException)) + ) + } + } + def "@PendingFeature marks passing feature as failed"() { when: def result = runner.runSpecBody """ @@ -129,6 +222,29 @@ def bar() { } } + def "@PendingFeature marks passing feature as failed even if applied twice"() { + when: + def result = runner.runSpecBody """ +@PendingFeature +@PendingFeature +def bar() { + expect: true +} +""" + + then: + verifyAll(result) { + testsStartedCount == 1 + testsSucceededCount == 0 + testsFailedCount == 1 + testsSkippedCount == 0 + testsAbortedCount == 0 + testEvents().failed().assertEventsMatchExactly( + finishedWithFailure(message('Feature is marked with @PendingFeature but passes unexpectedly')) + ) + } + } + def "@PendingFeature ignores aborted feature"() { when: def result = runner.runSpecBody """