Skip to content

Commit

Permalink
Add support for using @stepwise on data-driven features (#1442)
Browse files Browse the repository at this point in the history
`@Stepwise` can now be applied to feature methods. The effects are:
  1. the feature be switched to `ExecutionMode.SAME_THREAD`, similarly to
     how whole specs are switched to the same execution mode when using
     the annotation on class level.
  2. After the first error or failure occurs in any iteration, all
     subsequent iterations are going to be skipped.

Fixes #1008
  • Loading branch information
kriegaex committed Mar 21, 2022
1 parent 224a2a0 commit 1e45839
Show file tree
Hide file tree
Showing 8 changed files with 322 additions and 29 deletions.
19 changes: 13 additions & 6 deletions docs/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -270,13 +270,9 @@ include::{sourcedir}/extension/PendingFeatureIfDocSpec.groovy[tag=example-c]

To execute features in the order that they are declared, annotate a spec class with `spock.lang.Stepwise`:

[source,groovy]
[source,groovy,indent=0]
----
@Stepwise
class RunInOrderSpec extends Specification {
def "I run first"() { ... }
def "I run second"() { ... }
}
include::{sourcedir}/extension/StepwiseDocSpec.groovy[tag=example-a]
----

`Stepwise` only affects the class carrying the annotation; not sub or super classes. Features after the first
Expand All @@ -287,6 +283,17 @@ should be taken when ignoring feature methods in spec classes annotated with `St

NOTE: This will also set the execution mode to `SAME_THREAD`, see <<parallel-execution.adoc#parallel-execution, Parallel Execution>> for more information.

Since Spock 2.2, `Stepwise` can be applied to data-driven feature methods, having the effect of executing them sequentially (even if concurrent test mode is active) and to skip subsequent iterations if one iteration fails:

[source,groovy,indent=0]
----
include::{sourcedir}/extension/StepwiseDocSpec.groovy[tag=example-b]
----

This will pass for the first two iterations, fail on the third and skip the remaining two. Without `Stepwise` on feature method level, the third iteration would fail and the remaining 4 iterations would pass.

NOTE: For backward compatibility with Spock versions prior to 2.2, applying the annotation on spec level will _not_ automatically skip subsequent feature method iterations upon failure in a previous iteration. If you want that in addition to (or instead of) step-wise spec mode, you do have to annotate each individual feature method you wish to have that capability. This also conforms to the principle that if you want to skip tests under whatever conditions, you ought to document your intent explicitly.

=== Timeout

To fail a feature method, fixture, or class that exceeds a given execution duration, use `spock.lang.Timeout`,
Expand Down
2 changes: 2 additions & 0 deletions docs/release_notes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ include::include.adoc[]

== 2.2 (tbd)

* `@Stepwise` can be applied to data-driven feature methods, having the effect of executing them sequentially (even if concurrent test mode is active) and to skip subsequent iterations is one iteration fails.

== 2.2-M1 (2022-02-16)

* Add Groovy 4 support https://github.com/spockframework/spock/pull/1382[#1382]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,49 @@
package org.spockframework.runtime.extension.builtin;

import org.spockframework.runtime.AbstractRunListener;
import org.spockframework.runtime.InvalidSpecException;
import org.spockframework.runtime.extension.IAnnotationDrivenExtension;
import org.spockframework.runtime.model.*;
import org.spockframework.runtime.model.parallel.ExecutionMode;
import spock.lang.Stepwise;

import java.lang.annotation.Annotation;
import java.util.List;

public class StepwiseExtension implements IAnnotationDrivenExtension {
public class StepwiseExtension implements IAnnotationDrivenExtension<Stepwise> {
@Override
public void visitSpecAnnotation(Annotation annotation, final SpecInfo spec) {
public void visitSpecAnnotation(Stepwise annotation, final SpecInfo spec) {
sortFeaturesInDeclarationOrder(spec);
includeFeaturesBeforeLastIncludedFeature(spec);
skipFeaturesAfterFirstFailingFeature(spec);

// Disable parallel child execution for @Stepwise tests
// Disable parallel child execution for @Stepwise specs
spec.setChildExecutionMode(ExecutionMode.SAME_THREAD);
}

@Override
public void visitFeatureAnnotation(Stepwise annotation, FeatureInfo feature) {
if (!feature.isParameterized())
throw new InvalidSpecException(String.format(
"Cannot use @Stepwise, feature method %s.%s is not data-driven",
feature.getSpec().getReflection().getCanonicalName(),
feature.getDisplayName()
));

// Disable parallel iteration execution for @Stepwise features
feature.setExecutionMode(ExecutionMode.SAME_THREAD);

// If an error occurs in this feature, skip remaining iterations
feature.getFeatureMethod().addInterceptor(invocation -> {
try {
invocation.proceed();
}
catch (Throwable t) {
invocation.getFeature().skip("skipping subsequent iterations after failure");
throw t;
}
});
}

private void sortFeaturesInDeclarationOrder(SpecInfo spec) {
for (FeatureInfo feature : spec.getFeatures())
feature.setExecutionOrder(feature.getDeclarationOrder());
Expand Down
45 changes: 32 additions & 13 deletions spock-core/src/main/java/spock/lang/Stepwise.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,24 +20,43 @@
import java.lang.annotation.*;

/**
* Indicates that a spec's feature methods should be run sequentially
* in their declared order (even in the presence of a parallel spec runner),
* always starting from the first method. If a method fails, the remaining
* methods will be skipped. Feature methods declared in super- and subspecs
* are not affected.
* <b>When applied to a spec,</b> the annotation indicates that the spec's feature methods should be run sequentially
* in their declared order (even in the presence of a parallel spec runner), always starting from the first method.
* If a method fails, the remaining methods will be skipped. Feature methods declared in super- and subspecs are not
* affected.
*
* <p><tt>&#64;Stepwise</tt> is useful for specs with
* (logical) dependencies between methods. In particular, it helps to avoid
* consecutive errors after a method has failed, which makes it easier to
* understand what really went wrong.
* <p><b>When applied to a feature method,</b> the annotation analogically indicates that the feature's iterations
* should be run sequentially in their declared order. If an iteration fails, the remaining iterations will be skipped.
*
* <p>Note: If this extension is applied on the Specification, then it will use
* {@link org.spockframework.runtime.model.parallel.ExecutionMode#SAME_THREAD}
* for the whole Spec.
* <p><tt>&#64;Stepwise</tt> is useful for specs with (logical) dependencies between methods. In particular, it helps to
* avoid consecutive errors after a method has failed, which makes it easier to understand what really went wrong.
* Analogically, it helps to avoid consecutive errors in subsequent iterations, when it is clear that the reason for one
* iteration failure usually also causes subsequent iterations to fail, such as the availability of an external resource
* in an integration test.
*
* <p><b><i>Please try to use this annotation as infrequently as possible</i></b> by refactoring to avoid dependencies
* between feature methods or iterations whenever possible. Otherwise, you will lose opportunities to get meaningful
* test results and good coverage.
*
* <p>Please note:
* <ul>
* <li>Applying this annotation on a specification will activate
* {@link org.spockframework.runtime.model.parallel.ExecutionMode#SAME_THREAD ExecutionMode.SAME_THREAD} for the whole
* spec, all of its feature methods and their iterations (if any). If it is applied to a data-driven feature method
* only, it will set the same flag only per annotated method.
* </li>
* <li>
* <tt>&#64;Stepwise</tt> can be applied to methods since Spock 2.2. Therefore, for backward compatibility applying
* the annotation on spec level will <i>not</i> automatically skip subsequent feature method iterations upon failure
* in a previous iteration. If you want that in addition to (or instead of) step-wise spec mode, you do have to
* annotate each individual feature method you wish to have that capability. This also conforms to the principle that
* if you want to skip tests under whatever conditions, you ought to document your intent explicitly.
* </li>
* </ul>
*
* @author Peter Niederwieser
*/
@Target(ElementType.TYPE)
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@ExtensionAnnotation(StepwiseExtension.class)
public @interface Stepwise {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package org.spockframework.docs.extension

import org.spockframework.EmbeddedSpecification
import org.spockframework.runtime.extension.builtin.PreconditionContext
import spock.lang.IgnoreIf

class StepwiseDocSpec extends EmbeddedSpecification {
def "Annotation on spec"() {
runner.throwFailure = false

when:
def result = runner.runWithImports(/* tag::example-a[] */"""
@Stepwise
class RunInOrderSpec extends Specification {
def "I run first"() { expect: true }
def "I run second"() { expect: false }
def "I will be skipped"() { expect: true }
}
""") // end::example-a[]

then:
result.testsSucceededCount == 1
result.testsStartedCount == 2
result.testsFailedCount == 1
result.testsSkippedCount == 1
}

def "Annotation on feature"() {
runner.throwFailure = false

when:
def result = runner.runWithImports(/* tag::example-b[] */"""
class SkipAfterFailingIterationSpec extends Specification {
@Stepwise
def "iteration #count"() {
expect:
count != 3
where:
count << (1..5)
}
}
""") // end::example-b[]

then:
result.testsStartedCount == 3 + 1
result.testsSucceededCount == 2 + 1
result.testsFailedCount == 1
result.testsSkippedCount == 2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
/*
* 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.spockframework.smoke.extension

import org.spockframework.EmbeddedSpecification
import org.spockframework.runtime.InvalidSpecException

class StepwiseFeatures extends EmbeddedSpecification {
def "basic usage"() {
runner.throwFailure = false

when:
def result = runner.runWithImports("""
class Foo extends Specification {
@Stepwise
def step1() {
expect: count != 3
where: count << (1..5)
}
}
""")
def expectedSkipMessagesCount = result.testEvents()
.filter({ event ->
event.payload.present &&
event.payload.get() == "skipping subsequent iterations after failure"
})
.count()

then:
result.testsStartedCount == 3 + 1
result.testsSucceededCount == 2 + 1
result.testsFailedCount == 1
result.testsSkippedCount == 2
expectedSkipMessagesCount == 2
}

def "basic usage rolled-up"() {
runner.throwFailure = false
runner.addClassImport(RollupIterationCounter)
RollupIterationCounter.count.set(0)

when:
def result = runner.runWithImports("""
class Foo extends Specification {
@Stepwise
@Rollup
def step1() {
// Keep track of the number of started iterations for later verification in the outer feature method
RollupIterationCounter.count.set(RollupIterationCounter.count.get() + 1)
expect: count != 3
where: count << (1..5)
}
}
""")

then:
RollupIterationCounter.count.get() == 3
result.testsStartedCount == 1
result.testsSucceededCount == 0
result.testsFailedCount == 1
result.testsSkippedCount == 0
}

/**
* Has the sole purpose of reporting back the number of started iterations from the rolled-up, stepwise feature method
* used in the embedded spec under test in feature "basic usage rolled-up".
*/
private static class RollupIterationCounter {
static ThreadLocal<Integer> count = new ThreadLocal<>()
}

def "annotated feature must be data-driven"() {
when:
def result = runner.runWithImports("""
class Foo extends Specification {
@Stepwise
def step1() { expect: true }
}
""")

then:
thrown(InvalidSpecException)
}

def "feature annotation sets executionMode to SAME_THREAD"() {
when:
def result = runner.runWithImports("""
import org.spockframework.runtime.model.parallel.ExecutionMode
class Foo extends Specification {
@Stepwise
def step1() {
expect: specificationContext.currentFeature.executionMode.get() == ExecutionMode.SAME_THREAD
where: count << (1..2)
}
}
""")

then:
result.testsStartedCount == 2 + 1
result.testsSucceededCount == 2 + 1
result.testsFailedCount == 0
result.testsSkippedCount == 0
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import org.junit.platform.engine.discovery.DiscoverySelectors

import org.spockframework.EmbeddedSpecification

class StepwiseExtension extends EmbeddedSpecification {
class StepwiseSpecs extends EmbeddedSpecification {
def "basic usage"() {
runner.throwFailure = false

Expand All @@ -29,14 +29,22 @@ class Foo extends Specification {
def step1() { expect: true }
def step2() { expect: false }
def step3() { expect: true }
def step4() { expect: true }
}
""")
def expectedSkipMessagesCount = result.testEvents()
.filter({ event ->
event.payload.present &&
event.payload.get() == "Skipped due to previous Error (by @Stepwise)"
})
.count()

then:
result.testsSucceededCount == 1
result.testsStartedCount == 2
result.testsFailedCount == 1
result.testsSkippedCount == 1
result.testsSkippedCount == 2
expectedSkipMessagesCount == 2
}

def "automatically runs excluded methods that lead up to an included method"() {
Expand All @@ -55,7 +63,7 @@ class Foo extends Specification {
then:
result.testsSucceededCount == 3
result.testsFailedCount == 0
result.testsFailedCount == 0
result.testsSkippedCount == 0
}

def "honors method-level @Ignore"() {
Expand All @@ -78,7 +86,6 @@ class Foo extends Specification {
result.testsSkippedCount == 1
}


def "sets childExecutionMode to SAME_THREAD"() {
when:
def result = runner.runWithImports("""
Expand All @@ -92,7 +99,6 @@ class Foo extends Specification {
then:
result.testsSucceededCount == 1
result.testsFailedCount == 0
result.testsFailedCount == 0
result.testsSkippedCount == 0
}
}

Loading

0 comments on commit 1e45839

Please sign in to comment.