Skip to content

Commit 782d69d

Browse files
committed
Use deterministic unique ids in Descriptions
Rerunning failed tests with Surefire requires a method to identify the tests. Surefire currently uses a tuple of (Class, Method) to identify failed tests. Cucumber-jvm uses test suites which do not consist of Classes or methods. Therefore another method is needed. JUnit provides the option to select tests that match a Description. Descriptions are compared using an unique Id. To identify tests between different runs we should provide Descriptions with a predictable unique id. The current implementation did not suffice. While the provided cucumber elements are unique, they are not predictable. Each run would create a new instance of the identifier making it impossible to use their descriptions to rerun a failed test. After this change it will be possible to update Surefire to use descriptions to rerun failed tests.   Related Issues:   - https://issues.apache.org/jira/browse/SUREFIRE-1372   - #1120   - temyers/cucumber-jvm-parallel-plugin#31
1 parent 019f9a3 commit 782d69d

File tree

3 files changed

+150
-20
lines changed

3 files changed

+150
-20
lines changed

junit/src/main/java/cucumber/runtime/junit/ExecutionUnitRunner.java

+62-15
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public String getName() {
4949
public Description getDescription() {
5050
if (description == null) {
5151
String nameForDescription = getName().isEmpty() ? "EMPTY_NAME" : getName();
52-
description = Description.createSuiteDescription(nameForDescription, new PickleWrapper(pickleEvent));
52+
description = Description.createSuiteDescription(nameForDescription, new PickleId(pickleEvent));
5353

5454
for (PickleStep step : getChildren()) {
5555
description.addChild(describeChild(step));
@@ -68,7 +68,7 @@ protected Description describeChild(PickleStep step) {
6868
} else {
6969
testName = step.getText();
7070
}
71-
description = Description.createTestDescription(getName(), testName, new PickleStepWrapper(step));
71+
description = Description.createTestDescription(getName(), testName, new PickleStepId(pickleEvent, step));
7272
stepDescriptions.put(step, description);
7373
}
7474
return description;
@@ -93,22 +93,69 @@ protected void runChild(PickleStep step, RunNotifier notifier) {
9393
private String makeNameFilenameCompatible(String name) {
9494
return name.replaceAll("[^A-Za-z0-9_]", "_");
9595
}
96-
}
9796

98-
class PickleWrapper implements Serializable {
99-
private static final long serialVersionUID = 1L;
100-
private PickleEvent pickleEvent;
97+
private static final class PickleId implements Serializable {
98+
private static final long serialVersionUID = 1L;
99+
private final String uri;
100+
private int pickleLine;
101101

102-
PickleWrapper(PickleEvent pickleEvent) {
103-
this.pickleEvent = pickleEvent;
102+
PickleId(PickleEvent pickleEvent) {
103+
this.uri = pickleEvent.uri;
104+
this.pickleLine = pickleEvent.pickle.getLocations().get(0).getLine();
105+
}
106+
107+
@Override
108+
public boolean equals(Object o) {
109+
if (this == o) return true;
110+
if (o == null || getClass() != o.getClass()) return false;
111+
PickleId that = (PickleId) o;
112+
return pickleLine == that.pickleLine && uri.equals(that.uri);
113+
}
114+
115+
@Override
116+
public int hashCode() {
117+
int result = uri.hashCode();
118+
result = 31 * result + pickleLine;
119+
return result;
120+
}
121+
122+
@Override
123+
public String toString() {
124+
return uri + ":" + pickleLine;
125+
}
104126
}
105-
}
106127

107-
class PickleStepWrapper implements Serializable {
108-
private static final long serialVersionUID = 1L;
109-
private PickleStep step;
128+
private static final class PickleStepId implements Serializable {
129+
private static final long serialVersionUID = 1L;
130+
private final String uri;
131+
private final int pickleLine;
132+
private final int pickleStepLine;
110133

111-
PickleStepWrapper(PickleStep step) {
112-
this.step = step;
134+
PickleStepId(PickleEvent pickleEvent, PickleStep pickleStepLine) {
135+
this.uri = pickleEvent.uri;
136+
this.pickleLine = pickleEvent.pickle.getLocations().get(0).getLine();
137+
this.pickleStepLine = pickleStepLine.getLocations().get(0).getLine();
138+
}
139+
140+
@Override
141+
public boolean equals(Object o) {
142+
if (this == o) return true;
143+
if (o == null || getClass() != o.getClass()) return false;
144+
PickleStepId that = (PickleStepId) o;
145+
return pickleLine == that.pickleLine && pickleStepLine == that.pickleStepLine && uri.equals(that.uri);
146+
}
147+
148+
@Override
149+
public int hashCode() {
150+
int result = pickleLine;
151+
result = 31 * result + uri.hashCode();
152+
result = 31 * result + pickleStepLine;
153+
return result;
154+
}
155+
156+
@Override
157+
public String toString() {
158+
return uri + ":" + pickleLine + ":" + pickleStepLine;
159+
}
113160
}
114-
}
161+
}

junit/src/main/java/cucumber/runtime/junit/FeatureRunner.java

+30-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import org.junit.runners.ParentRunner;
1313
import org.junit.runners.model.InitializationError;
1414

15+
import java.io.Serializable;
1516
import java.util.ArrayList;
1617
import java.util.List;
1718

@@ -36,7 +37,7 @@ public String getName() {
3637
@Override
3738
public Description getDescription() {
3839
if (description == null) {
39-
description = Description.createSuiteDescription(getName(), cucumberFeature);
40+
description = Description.createSuiteDescription(getName(), new FeatureId(cucumberFeature));
4041
for (ParentRunner child : getChildren()) {
4142
description.addChild(describeChild(child));
4243
}
@@ -87,4 +88,32 @@ private void buildFeatureElementRunners(Runtime runtime, JUnitReporter jUnitRepo
8788
}
8889
}
8990

91+
private static final class FeatureId implements Serializable {
92+
private static final long serialVersionUID = 1L;
93+
private final String uri;
94+
95+
FeatureId(CucumberFeature feature) {
96+
this.uri = feature.getPath();
97+
}
98+
99+
@Override
100+
public boolean equals(Object o) {
101+
if (this == o) return true;
102+
if (o == null || getClass() != o.getClass()) return false;
103+
FeatureId featureId = (FeatureId) o;
104+
return uri.equals(featureId.uri);
105+
106+
}
107+
108+
@Override
109+
public int hashCode() {
110+
return uri.hashCode();
111+
}
112+
113+
@Override
114+
public String toString() {
115+
return uri;
116+
}
117+
}
118+
90119
}

junit/src/test/java/cucumber/runtime/junit/FeatureRunnerTest.java

+58-4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import cucumber.runtime.RuntimeOptions;
88
import cucumber.runtime.io.ClasspathResourceLoader;
99
import cucumber.runtime.model.CucumberFeature;
10+
import org.junit.Assert;
1011
import org.junit.Test;
1112
import org.junit.runner.Description;
1213
import org.junit.runner.notification.RunNotifier;
@@ -15,8 +16,11 @@
1516
import org.mockito.InOrder;
1617

1718
import java.util.Collections;
19+
import java.util.HashSet;
20+
import java.util.Set;
1821

1922
import static java.util.Arrays.asList;
23+
import static org.junit.Assert.assertTrue;
2024
import static org.mockito.Matchers.argThat;
2125
import static org.mockito.Mockito.inOrder;
2226
import static org.mockito.Mockito.mock;
@@ -89,17 +93,67 @@ public void should_call_formatter_for_scenario_outline_with_two_examples_table_a
8993
}
9094

9195
private RunNotifier runFeatureWithNotifier(CucumberFeature cucumberFeature) throws InitializationError {
96+
FeatureRunner runner = createFeatureRunner(cucumberFeature);
97+
RunNotifier notifier = mock(RunNotifier.class);
98+
runner.run(notifier);
99+
return notifier;
100+
}
101+
102+
private FeatureRunner createFeatureRunner(CucumberFeature cucumberFeature) throws InitializationError {
92103
final RuntimeOptions runtimeOptions = new RuntimeOptions("-p null");
93104
final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
94105
final ClasspathResourceLoader resourceLoader = new ClasspathResourceLoader(classLoader);
95106
final RuntimeGlue glue = mock(RuntimeGlue.class);
96107
final Runtime runtime = new Runtime(resourceLoader, classLoader, asList(mock(Backend.class)), runtimeOptions, new TimeService.Stub(0l), glue);
97-
FeatureRunner runner = new FeatureRunner(cucumberFeature, runtime, new JUnitReporter(runtime.getEventBus(), false, new JUnitOptions(Collections.<String>emptyList())));
98-
RunNotifier notifier = mock(RunNotifier.class);
99-
runner.run(notifier);
100-
return notifier;
108+
return new FeatureRunner(cucumberFeature, runtime, new JUnitReporter(runtime.getEventBus(), false, new JUnitOptions(Collections.<String>emptyList())));
109+
}
110+
111+
112+
@Test
113+
public void shouldPopulateDescriptionsWithStableUniqueIds() throws Exception {
114+
CucumberFeature cucumberFeature = TestPickleBuilder.parseFeature("path/test.feature", "" +
115+
"Feature: feature name\n" +
116+
" Background:\n" +
117+
" Given background step\n" +
118+
" Scenario: A\n" +
119+
" Then scenario name\n" +
120+
" Scenario: B\n" +
121+
" Then scenario name\n" +
122+
" Scenario Outline: C\n" +
123+
" Then scenario <name>\n" +
124+
" Examples:\n" +
125+
" | name |\n" +
126+
" | C |\n" +
127+
" | D |\n" +
128+
" | E |\n"
129+
130+
);
131+
132+
FeatureRunner runner = createFeatureRunner(cucumberFeature);
133+
FeatureRunner rerunner = createFeatureRunner(cucumberFeature);
134+
135+
Set<Description> descriptions = new HashSet<Description>();
136+
assertDescriptionIsUnique(runner.getDescription(), descriptions);
137+
assertDescriptionIsPredictable(runner.getDescription(), descriptions);
138+
assertDescriptionIsPredictable(rerunner.getDescription(), descriptions);
139+
101140
}
102141

142+
private static void assertDescriptionIsUnique(Description description, Set<Description> descriptions) {
143+
// Note, JUnit uses the the serializable parameter (in this case the step)
144+
// as the unique id when comparing Descriptions
145+
assertTrue(descriptions.add(description));
146+
for (Description each : description.getChildren()) {
147+
assertDescriptionIsUnique(each, descriptions);
148+
}
149+
}
150+
151+
private static void assertDescriptionIsPredictable(Description description, Set<Description> descriptions) {
152+
assertTrue(descriptions.contains(description));
153+
for (Description each : description.getChildren()) {
154+
assertDescriptionIsPredictable(each, descriptions);
155+
}
156+
}
103157
}
104158

105159
class DescriptionMatcher extends ArgumentMatcher<Description> {

0 commit comments

Comments
 (0)