Skip to content

Commit f6de527

Browse files
committed
Merge branch '2102-fix-dry-run' into main
Given a scenario with several steps: ```feature Scenario: Given a passed step And a skipped step And an pending step And an undefined step And an ambiguous step ``` When executing this the outcome of the result is: ``` a passed step -> PASSED a skipped step -> SKIPPED an pending step -> PENDING an undefined step -> SKIPPED an ambiguous step -> AMBIGUOUS ``` This is wrong, because after the first non-passed result all other steps should be skipped. So: ``` a passed step -> PASSED a skipped step -> SKIPPED an pending step -> SKIPPED an undefined step -> SKIPPED an ambiguous step -> SKIPPED ``` When using executing this scenario with `--dry-run` the result is: ``` a passed step -> PASSED a skipped step -> PASSED an pending step -> PASSED an undefined step -> UNDEFINED an ambiguous step -> AMBIGUOUS ``` And surprisingly enough this is also wrong. While the skipped and pending states can only be detected by executing an implemented step, the undefined step should be marked as undefined and the ambiguous step that follows it should be skipped. ``` a passed step -> PASSED a skipped step -> PASSED an pending step -> PASSED an undefined step -> UNDEFINED an ambiguous step -> SKIPPED ``` The cause for this confusion lies in the fact that `--dry-run` was implemented using the skip mechanism rather then its own execution mode that is distinct from both a regular run and skip mode. By implementing these as individual execution modes we can avoid this confusion. Implementing this however revealed that our formatters were often being tested with completely undefined scenarios. This does not provide a representative test and allowed #2102 to come into existence. Fixing this was rather complicated, the formatters were being tested with an overly complicated mock implementation. Replacing this mock implementation with stubs made the tests more readable and removed a significant chunk of complexity. Fixes: #2102
2 parents b4ed58f + 16e7a33 commit f6de527

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+2142
-2120
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1818
### Fixed
1919
* [Core] CucumberOptions default snippet type should not override properties ([2107](https://github.com/cucumber/cucumber-jvm/pull/2107) M.P. Korstanje)
2020
* [Core] Replace parentFile.makeDirs with Files.createDirectories(parentFile) ([2104](https://github.com/cucumber/cucumber-jvm/pull/2104) M.P. Korstanje)
21+
* [Core] Separate run, dry-run and skip execution modes ([2102](https://github.com/cucumber/cucumber-jvm/pull/2109), [2102](https://github.com/cucumber/cucumber-jvm/pull/2109) M.P. Korstanje)
22+
* Fixes `--dry-run` not failing on undefined steps
2123

2224
### Security
2325
* [Core] Update `create-meta` to 2.0.2 to avoid sharing credentials ([2110](https://github.com/cucumber/cucumber-jvm/pull/2110) vincent-psarga)

core/src/main/java/io/cucumber/core/runner/AmbiguousPickleStepDefinitionsMatch.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public void runStep(TestCaseState state) throws AmbiguousStepDefinitionsExceptio
2222

2323
@Override
2424
public void dryRunStep(TestCaseState state) throws AmbiguousStepDefinitionsException {
25-
runStep(state);
25+
throw exception;
2626
}
2727

2828
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
package io.cucumber.core.runner;
2+
3+
import io.cucumber.plugin.event.Status;
4+
5+
enum ExecutionMode {
6+
7+
RUN {
8+
@Override
9+
Status execute(StepDefinitionMatch stepDefinitionMatch, TestCaseState state) throws Throwable {
10+
stepDefinitionMatch.runStep(state);
11+
return Status.PASSED;
12+
}
13+
14+
},
15+
DRY_RUN {
16+
@Override
17+
Status execute(StepDefinitionMatch stepDefinitionMatch, TestCaseState state) throws Throwable {
18+
stepDefinitionMatch.dryRunStep(state);
19+
return Status.PASSED;
20+
}
21+
},
22+
SKIP {
23+
@Override
24+
Status execute(StepDefinitionMatch stepDefinitionMatch, TestCaseState state) {
25+
return Status.SKIPPED;
26+
}
27+
};
28+
29+
abstract Status execute(StepDefinitionMatch stepDefinitionMatch, TestCaseState state) throws Throwable;
30+
31+
ExecutionMode next(ExecutionMode current) {
32+
return current == SKIP ? current : this;
33+
}
34+
}

core/src/main/java/io/cucumber/core/runner/PickleStepTestStep.java

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,20 +39,25 @@ final class PickleStepTestStep extends TestStep implements io.cucumber.plugin.ev
3939
}
4040

4141
@Override
42-
boolean run(TestCase testCase, EventBus bus, TestCaseState state, boolean skipSteps) {
43-
boolean skipNextStep = skipSteps;
42+
ExecutionMode run(TestCase testCase, EventBus bus, TestCaseState state, ExecutionMode executionMode) {
43+
ExecutionMode nextExecutionMode = executionMode;
4444

4545
for (HookTestStep before : beforeStepHookSteps) {
46-
skipNextStep |= before.run(testCase, bus, state, skipSteps);
46+
nextExecutionMode = before
47+
.run(testCase, bus, state, executionMode)
48+
.next(nextExecutionMode);
4749
}
4850

49-
skipNextStep |= super.run(testCase, bus, state, skipNextStep);
51+
nextExecutionMode = super.run(testCase, bus, state, nextExecutionMode)
52+
.next(nextExecutionMode);
5053

5154
for (HookTestStep after : afterStepHookSteps) {
52-
skipNextStep |= after.run(testCase, bus, state, skipSteps);
55+
nextExecutionMode = after
56+
.run(testCase, bus, state, executionMode)
57+
.next(nextExecutionMode);
5358
}
5459

55-
return skipNextStep;
60+
return nextExecutionMode;
5661
}
5762

5863
List<HookTestStep> getBeforeStepHookSteps() {

core/src/main/java/io/cucumber/core/runner/TestCase.java

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
import java.util.List;
2323
import java.util.UUID;
2424

25+
import static io.cucumber.core.runner.ExecutionMode.DRY_RUN;
26+
import static io.cucumber.core.runner.ExecutionMode.RUN;
2527
import static io.cucumber.core.runner.TestStepResultStatus.from;
2628
import static io.cucumber.messages.TimeConversion.javaDurationToDuration;
2729
import static io.cucumber.messages.TimeConversion.javaInstantToTimestamp;
@@ -32,7 +34,7 @@ final class TestCase implements io.cucumber.plugin.event.TestCase {
3234

3335
private final Pickle pickle;
3436
private final List<PickleStepTestStep> testSteps;
35-
private final boolean dryRun;
37+
private final ExecutionMode executionMode;
3638
private final List<HookTestStep> beforeHooks;
3739
private final List<HookTestStep> afterHooks;
3840
private final UUID id;
@@ -49,7 +51,7 @@ final class TestCase implements io.cucumber.plugin.event.TestCase {
4951
this.beforeHooks = beforeHooks;
5052
this.afterHooks = afterHooks;
5153
this.pickle = pickle;
52-
this.dryRun = dryRun;
54+
this.executionMode = dryRun ? DRY_RUN : RUN;
5355
}
5456

5557
private static StepMatchArgument.Group makeMessageGroup(
@@ -82,7 +84,7 @@ private static String toString(Throwable error) {
8284
}
8385

8486
void run(EventBus bus) {
85-
boolean skipNextStep = this.dryRun;
87+
ExecutionMode nextExecutionMode = this.executionMode;
8688
emitTestCaseMessage(bus);
8789

8890
Instant start = bus.getInstant();
@@ -92,15 +94,21 @@ void run(EventBus bus) {
9294
TestCaseState state = new TestCaseState(bus, executionId, this);
9395

9496
for (HookTestStep before : beforeHooks) {
95-
skipNextStep |= before.run(this, bus, state, dryRun);
97+
nextExecutionMode = before
98+
.run(this, bus, state, executionMode)
99+
.next(nextExecutionMode);
96100
}
97101

98102
for (PickleStepTestStep step : testSteps) {
99-
skipNextStep |= step.run(this, bus, state, skipNextStep);
103+
nextExecutionMode = step
104+
.run(this, bus, state, nextExecutionMode)
105+
.next(nextExecutionMode);
100106
}
101107

102108
for (HookTestStep after : afterHooks) {
103-
after.run(this, bus, state, dryRun);
109+
nextExecutionMode = after
110+
.run(this, bus, state, executionMode)
111+
.next(nextExecutionMode);
104112
}
105113

106114
Instant stop = bus.getInstant();

core/src/main/java/io/cucumber/core/runner/TestStep.java

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import java.util.Arrays;
1818
import java.util.UUID;
1919

20+
import static io.cucumber.core.runner.ExecutionMode.SKIP;
2021
import static io.cucumber.core.runner.TestStepResultStatus.from;
2122
import static io.cucumber.messages.TimeConversion.javaDurationToDuration;
2223
import static io.cucumber.messages.TimeConversion.javaInstantToTimestamp;
@@ -53,14 +54,14 @@ public UUID getId() {
5354
return id;
5455
}
5556

56-
boolean run(TestCase testCase, EventBus bus, TestCaseState state, boolean skipSteps) {
57+
ExecutionMode run(TestCase testCase, EventBus bus, TestCaseState state, ExecutionMode executionMode) {
5758
Instant startTime = bus.getInstant();
5859
emitTestStepStarted(testCase, bus, state.getTestExecutionId(), startTime);
5960

6061
Status status;
6162
Throwable error = null;
6263
try {
63-
status = executeStep(state, skipSteps);
64+
status = executeStep(state, executionMode);
6465
} catch (Throwable t) {
6566
error = t;
6667
status = mapThrowableToStatus(t);
@@ -72,7 +73,7 @@ boolean run(TestCase testCase, EventBus bus, TestCaseState state, boolean skipSt
7273

7374
emitTestStepFinished(testCase, bus, state.getTestExecutionId(), stopTime, duration, result);
7475

75-
return !result.getStatus().is(Status.PASSED);
76+
return result.getStatus().is(Status.PASSED) ? executionMode : SKIP;
7677
}
7778

7879
private void emitTestStepStarted(TestCase testCase, EventBus bus, UUID textExecutionId, Instant startTime) {
@@ -85,16 +86,10 @@ private void emitTestStepStarted(TestCase testCase, EventBus bus, UUID textExecu
8586
.build());
8687
}
8788

88-
private Status executeStep(TestCaseState state, boolean skipSteps) throws Throwable {
89+
private Status executeStep(TestCaseState state, ExecutionMode executionMode) throws Throwable {
8990
state.setCurrentTestStepId(id);
9091
try {
91-
if (!skipSteps) {
92-
stepDefinitionMatch.runStep(state);
93-
return Status.PASSED;
94-
} else {
95-
stepDefinitionMatch.dryRunStep(state);
96-
return Status.SKIPPED;
97-
}
92+
return executionMode.execute(stepDefinitionMatch, state);
9893
} finally {
9994
state.clearCurrentTestStepId();
10095
}

core/src/main/java/io/cucumber/core/runner/UndefinedPickleStepDefinitionMatch.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ public void runStep(TestCaseState state) {
1919

2020
@Override
2121
public void dryRunStep(TestCaseState state) {
22-
22+
throw new UndefinedStepDefinitionException();
2323
}
2424

2525
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
package io.cucumber.core.backend;
2+
3+
import java.util.function.Consumer;
4+
5+
public class StubHookDefinition implements HookDefinition {
6+
7+
private static final String STUBBED_LOCATION_WITH_DETAILS = "{stubbed location with details}";
8+
private final String location;
9+
private final RuntimeException exception;
10+
private final Consumer<TestCaseState> action;
11+
12+
public StubHookDefinition(String location, RuntimeException exception, Consumer<TestCaseState> action) {
13+
this.location = location;
14+
this.exception = exception;
15+
this.action = action;
16+
}
17+
18+
public StubHookDefinition(String location, Consumer<TestCaseState> action) {
19+
this(location, null, action);
20+
}
21+
22+
public StubHookDefinition() {
23+
this(STUBBED_LOCATION_WITH_DETAILS, null, null);
24+
}
25+
26+
public StubHookDefinition(Consumer<TestCaseState> action) {
27+
this(STUBBED_LOCATION_WITH_DETAILS, null, action);
28+
}
29+
30+
public StubHookDefinition(RuntimeException exception) {
31+
this(STUBBED_LOCATION_WITH_DETAILS, exception, null);
32+
}
33+
34+
public StubHookDefinition(String location) {
35+
this(location, null, null);
36+
}
37+
38+
@Override
39+
public void execute(TestCaseState state) {
40+
if (action != null) {
41+
action.accept(state);
42+
}
43+
if (exception != null) {
44+
throw exception;
45+
}
46+
}
47+
48+
@Override
49+
public String getTagExpression() {
50+
return "";
51+
}
52+
53+
@Override
54+
public int getOrder() {
55+
return 0;
56+
}
57+
58+
@Override
59+
public boolean isDefinedAt(StackTraceElement stackTraceElement) {
60+
return false;
61+
}
62+
63+
@Override
64+
public String getLocation() {
65+
return location;
66+
}
67+
68+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package io.cucumber.core.backend;
2+
3+
import io.cucumber.core.backend.Pending;
4+
5+
@Pending
6+
public final class StubPendingException extends RuntimeException {
7+
8+
public StubPendingException() {
9+
this("TODO: implement me");
10+
}
11+
12+
public StubPendingException(String message) {
13+
super(message);
14+
}
15+
16+
}

core/src/test/java/io/cucumber/core/runtime/StubStepDefinition.java renamed to core/src/test/java/io/cucumber/core/backend/StubStepDefinition.java

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,4 @@
1-
package io.cucumber.core.runtime;
2-
3-
import io.cucumber.core.backend.ParameterInfo;
4-
import io.cucumber.core.backend.StepDefinition;
5-
import io.cucumber.core.backend.TypeResolver;
1+
package io.cucumber.core.backend;
62

73
import java.lang.reflect.Type;
84
import java.util.List;
@@ -13,17 +9,28 @@
139

1410
public class StubStepDefinition implements StepDefinition {
1511

12+
private static final String STUBBED_LOCATION_WITH_DETAILS = "{stubbed location with details}";
1613
private final List<ParameterInfo> parameterInfos;
1714
private final String expression;
1815
private final RuntimeException exception;
16+
private final String location;
17+
18+
public StubStepDefinition(String pattern, String location, Type... types) {
19+
this(pattern, location, null, types);
20+
}
1921

2022
public StubStepDefinition(String pattern, Type... types) {
21-
this(pattern, null, types);
23+
this(pattern, STUBBED_LOCATION_WITH_DETAILS, null, types);
2224
}
2325

2426
public StubStepDefinition(String pattern, RuntimeException exception, Type... types) {
27+
this(pattern, STUBBED_LOCATION_WITH_DETAILS, exception, types);
28+
}
29+
30+
public StubStepDefinition(String pattern, String location, RuntimeException exception, Type... types) {
2531
this.parameterInfos = Stream.of(types).map(StubParameterInfo::new).collect(Collectors.toList());
2632
this.expression = pattern;
33+
this.location = location;
2734
this.exception = exception;
2835
}
2936

@@ -34,7 +41,7 @@ public boolean isDefinedAt(StackTraceElement stackTraceElement) {
3441

3542
@Override
3643
public String getLocation() {
37-
return "{stubbed location with details}";
44+
return location;
3845
}
3946

4047
@Override

core/src/test/java/io/cucumber/core/feature/TestFeatureParser.java

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import java.io.ByteArrayInputStream;
77
import java.io.InputStream;
88
import java.net.URI;
9-
import java.nio.charset.StandardCharsets;
109
import java.util.UUID;
1110

11+
import static java.nio.charset.StandardCharsets.UTF_8;
12+
1213
public class TestFeatureParser {
1314

1415
public static Feature parse(final String source) {
@@ -20,6 +21,14 @@ public static Feature parse(final String uri, final String source) {
2021
}
2122

2223
public static Feature parse(final URI uri, final String source) {
24+
return parse(uri, new ByteArrayInputStream(source.getBytes(UTF_8)));
25+
}
26+
27+
public static Feature parse(final String uri, final InputStream source) {
28+
return parse(FeatureIdentifier.parse(uri), source);
29+
}
30+
31+
public static Feature parse(final URI uri, final InputStream source) {
2332
return new FeatureParser(UUID::randomUUID).parseResource(new Resource() {
2433
@Override
2534
public URI getUri() {
@@ -28,7 +37,7 @@ public URI getUri() {
2837

2938
@Override
3039
public InputStream getInputStream() {
31-
return new ByteArrayInputStream(source.getBytes(StandardCharsets.UTF_8));
40+
return source;
3241
}
3342

3443
}).orElse(null);

0 commit comments

Comments
 (0)