Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make @PendingFeature repeatable (#1030) #1190

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 20 additions & 4 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ include::include.adoc[]

- `@Issue` is now repeatable

- `@PendingFeature` is now repeatable


== 2.0-M3 (2020-06-11)

Expand Down
11 changes: 11 additions & 0 deletions spock-core/src/main/java/spock/lang/PendingFeature.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe include an example that has a list of exceptions

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,29 @@ def bar() {
}
}

def "@PendingFeature marks failing feature as skipped even if applied twice"() {
when:
def result = runner.runSpecBody """
@PendingFeature
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While not "wrong", it is redundant, we should check for redundant annotations, as the user probably intended something different.

Copy link
Member Author

@Vampire Vampire Jul 14, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The question is, how much logic to stuff in for those redundancy checks and how much to trust the user.
You can also prevent multiple @Issue annotations with the same value or.
Or you could even prevent @Issue(['1', '2'], ['2', '3']) as the '2' is redundant.
Same for two @PendingFeature annotations with the same expected exception in their lists.

@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 """
Expand All @@ -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')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks even more like an error

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While there is probably no sensible use-case for this in the wild, the test is just intended to ensure a stable reason being shown in the error message. As for the redundancy-check or whatever, let's continue above.

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 """
Expand Down Expand Up @@ -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 """
Expand All @@ -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 """
Expand Down