Skip to content

Commit 2828077

Browse files
committed
[JUnit] Make duplicate pickle names unique
Individual examples do not have a unique name. And while a unique name is not required by Junit, various integrations such as sbt-junit assume this the case. By appending ` [#n]` to a scenario name it becomes unique again. So JUnit 4s output becomes: ``` A feature with scenario outlines A scenario outline #1 A scenario outline #2 A scenario outline #3 A scenario outline #4 ``` The `#n` was taken from JUnit 5 which renders examples as: ``` A feature with scenario outlines A scenario outline With some other text Example #1 Example #2 With some text Example #1 Example #2 ``` Fixes: cucumber/cucumber-jvm-scala#102
1 parent b2c3b0e commit 2828077

File tree

11 files changed

+182
-39
lines changed

11 files changed

+182
-39
lines changed

junit/src/main/java/io/cucumber/junit/Cucumber.java

+12-3
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,14 @@
4040

4141
import java.time.Clock;
4242
import java.util.List;
43+
import java.util.Map;
44+
import java.util.Optional;
4345
import java.util.UUID;
4446
import java.util.function.Predicate;
4547
import java.util.function.Supplier;
48+
import java.util.stream.Collectors;
4649

50+
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
4751
import static java.util.stream.Collectors.toList;
4852

4953
/**
@@ -88,8 +92,6 @@
8892
@API(status = API.Status.STABLE)
8993
public final class Cucumber extends ParentRunner<ParentRunner<?>> {
9094

91-
private static final Logger log = LoggerFactory.getLogger(Cucumber.class);
92-
9395
private final List<ParentRunner<?>> children;
9496
private final EventBus bus;
9597
private final List<Feature> features;
@@ -171,8 +173,15 @@ public Cucumber(Class<?> clazz) throws InitializationError {
171173
objectFactorySupplier, typeRegistryConfigurerSupplier);
172174
this.context = new CucumberExecutionContext(bus, exitStatus, runnerSupplier);
173175
Predicate<Pickle> filters = new Filters(runtimeOptions);
176+
177+
Map<Optional<String>, List<Feature>> groupedByName = features.stream()
178+
.collect(Collectors.groupingBy(Feature::getName));
179+
174180
this.children = features.stream()
175-
.map(feature -> FeatureRunner.create(feature, filters, runnerSupplier, junitOptions))
181+
.map(feature -> {
182+
Integer uniqueSuffix = uniqueSuffix(groupedByName, feature, Feature::getName);
183+
return FeatureRunner.create(feature, uniqueSuffix, filters, runnerSupplier, junitOptions);
184+
})
176185
.filter(runner -> !runner.isEmpty())
177186
.collect(toList());
178187
}

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

+25-9
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,12 @@
1414
import java.io.Serializable;
1515
import java.net.URI;
1616
import java.util.List;
17+
import java.util.Map;
1718
import java.util.function.Predicate;
19+
import java.util.stream.Collectors;
1820

1921
import static io.cucumber.junit.FileNameCompatibleNames.createName;
22+
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
2023
import static io.cucumber.junit.PickleRunners.withNoStepDescriptions;
2124
import static io.cucumber.junit.PickleRunners.withStepDescriptions;
2225
import static java.util.stream.Collectors.toList;
@@ -26,26 +29,39 @@ final class FeatureRunner extends ParentRunner<PickleRunner> {
2629
private final List<PickleRunner> children;
2730
private final Feature feature;
2831
private final JUnitOptions options;
32+
private final Integer uniqueSuffix;
2933
private Description description;
3034

31-
private FeatureRunner(Feature feature, Predicate<Pickle> filter, RunnerSupplier runners, JUnitOptions options)
35+
private FeatureRunner(
36+
Feature feature, Integer uniqueSuffix, Predicate<Pickle> filter, RunnerSupplier runners,
37+
JUnitOptions options
38+
)
3239
throws InitializationError {
3340
super((Class<?>) null);
3441
this.feature = feature;
42+
this.uniqueSuffix = uniqueSuffix;
3543
this.options = options;
36-
String name = feature.getName().orElse("EMPTY_NAME");
37-
this.children = feature.getPickles().stream()
38-
.filter(filter).map(pickle -> options.stepNotifications()
39-
? withStepDescriptions(runners, pickle, options)
40-
: withNoStepDescriptions(name, runners, pickle, options))
44+
Map<String, List<Pickle>> groupedByName = feature.getPickles().stream()
45+
.filter(filter)
46+
.collect(Collectors.groupingBy(Pickle::getName));
47+
this.children = feature.getPickles()
48+
.stream()
49+
.map(pickle -> {
50+
String featureName = getName();
51+
Integer exampleId = uniqueSuffix(groupedByName, pickle, Pickle::getName);
52+
return options.stepNotifications()
53+
? withStepDescriptions(runners, pickle, exampleId, options)
54+
: withNoStepDescriptions(featureName, runners, pickle, exampleId, options);
55+
})
4156
.collect(toList());
4257
}
4358

4459
static FeatureRunner create(
45-
Feature feature, Predicate<Pickle> filter, RunnerSupplier runners, JUnitOptions options
60+
Feature feature, Integer uniqueSuffix, Predicate<Pickle> filter, RunnerSupplier runners,
61+
JUnitOptions options
4662
) {
4763
try {
48-
return new FeatureRunner(feature, filter, runners, options);
64+
return new FeatureRunner(feature, uniqueSuffix, filter, runners, options);
4965
} catch (InitializationError e) {
5066
throw new CucumberException("Failed to create scenario runner", e);
5167
}
@@ -89,7 +105,7 @@ public String toString() {
89105
@Override
90106
protected String getName() {
91107
String name = feature.getName().orElse("EMPTY_NAME");
92-
return createName(name, options.filenameCompatibleNames());
108+
return createName(name, uniqueSuffix, options.filenameCompatibleNames());
93109
}
94110

95111
@Override
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
11
package io.cucumber.junit;
22

3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.function.Function;
6+
37
final class FileNameCompatibleNames {
48

9+
static String createName(String name, Integer uniqueSuffix, boolean useFilenameCompatibleNames) {
10+
if (uniqueSuffix == null) {
11+
return createName(name, useFilenameCompatibleNames);
12+
}
13+
return createName(name + " #" + uniqueSuffix + "", useFilenameCompatibleNames);
14+
}
15+
516
static String createName(final String name, boolean useFilenameCompatibleNames) {
617
if (useFilenameCompatibleNames) {
718
return makeNameFilenameCompatible(name);
819
}
9-
1020
return name;
1121
}
1222

1323
private static String makeNameFilenameCompatible(String name) {
1424
return name.replaceAll("[^A-Za-z0-9_]", "_");
1525
}
1626

27+
static <V, K> Integer uniqueSuffix(Map<K, List<V>> groupedByName, V pickle, Function<V, K> nameOf) {
28+
List<V> withSameName = groupedByName.get(nameOf.apply(pickle));
29+
boolean makeNameUnique = withSameName.size() > 1;
30+
return makeNameUnique ? withSameName.indexOf(pickle) + 1 : null;
31+
}
32+
1733
}

junit/src/main/java/io/cucumber/junit/PickleRunners.java

+21-10
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,21 @@
2121

2222
final class PickleRunners {
2323

24-
static PickleRunner withStepDescriptions(RunnerSupplier runnerSupplier, Pickle pickle, JUnitOptions options) {
24+
static PickleRunner withStepDescriptions(
25+
RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix, JUnitOptions options
26+
) {
2527
try {
26-
return new WithStepDescriptions(runnerSupplier, pickle, options);
28+
return new WithStepDescriptions(runnerSupplier, pickle, uniqueSuffix, options);
2729
} catch (InitializationError e) {
2830
throw new CucumberException("Failed to create scenario runner", e);
2931
}
3032
}
3133

3234
static PickleRunner withNoStepDescriptions(
33-
String featureName, RunnerSupplier runnerSupplier, Pickle pickle, JUnitOptions jUnitOptions
35+
String featureName, RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix,
36+
JUnitOptions jUnitOptions
3437
) {
35-
return new NoStepDescriptions(featureName, runnerSupplier, pickle, jUnitOptions);
38+
return new NoStepDescriptions(featureName, runnerSupplier, pickle, uniqueSuffix, jUnitOptions);
3639
}
3740

3841
interface PickleRunner {
@@ -51,14 +54,18 @@ static class WithStepDescriptions extends ParentRunner<Step> implements PickleRu
5154
private final Pickle pickle;
5255
private final JUnitOptions jUnitOptions;
5356
private final Map<Step, Description> stepDescriptions = new HashMap<>();
57+
private final Integer uniqueSuffix;
5458
private Description description;
5559

56-
WithStepDescriptions(RunnerSupplier runnerSupplier, Pickle pickle, JUnitOptions jUnitOptions)
60+
WithStepDescriptions(
61+
RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix, JUnitOptions jUnitOptions
62+
)
5763
throws InitializationError {
5864
super((Class<?>) null);
5965
this.runnerSupplier = runnerSupplier;
6066
this.pickle = pickle;
6167
this.jUnitOptions = jUnitOptions;
68+
this.uniqueSuffix = uniqueSuffix;
6269
}
6370

6471
@Override
@@ -70,7 +77,7 @@ protected List<Step> getChildren() {
7077

7178
@Override
7279
protected String getName() {
73-
return createName(pickle.getName(), jUnitOptions.filenameCompatibleNames());
80+
return createName(pickle.getName(), uniqueSuffix, jUnitOptions.filenameCompatibleNames());
7481
}
7582

7683
@Override
@@ -86,8 +93,9 @@ public Description getDescription() {
8693
public Description describeChild(Step step) {
8794
Description description = stepDescriptions.get(step);
8895
if (description == null) {
89-
String testName = createName(step.getText(), jUnitOptions.filenameCompatibleNames());
90-
description = Description.createTestDescription(getName(), testName, new PickleStepId(pickle, step));
96+
String className = getName();
97+
String name = createName(step.getText(), jUnitOptions.filenameCompatibleNames());
98+
description = Description.createTestDescription(className, name, new PickleStepId(pickle, step));
9199
stepDescriptions.put(step, description);
92100
}
93101
return description;
@@ -120,15 +128,18 @@ static final class NoStepDescriptions implements PickleRunner {
120128
private final RunnerSupplier runnerSupplier;
121129
private final Pickle pickle;
122130
private final JUnitOptions jUnitOptions;
131+
private final Integer uniqueSuffix;
123132
private Description description;
124133

125134
NoStepDescriptions(
126-
String featureName, RunnerSupplier runnerSupplier, Pickle pickle, JUnitOptions jUnitOptions
135+
String featureName, RunnerSupplier runnerSupplier, Pickle pickle, Integer uniqueSuffix,
136+
JUnitOptions jUnitOptions
127137
) {
128138
this.featureName = featureName;
129139
this.runnerSupplier = runnerSupplier;
130140
this.pickle = pickle;
131141
this.jUnitOptions = jUnitOptions;
142+
this.uniqueSuffix = uniqueSuffix;
132143
}
133144

134145
@Override
@@ -145,7 +156,7 @@ public void run(final RunNotifier notifier) {
145156
public Description getDescription() {
146157
if (description == null) {
147158
String className = createName(featureName, jUnitOptions.filenameCompatibleNames());
148-
String name = createName(pickle.getName(), jUnitOptions.filenameCompatibleNames());
159+
String name = createName(pickle.getName(), uniqueSuffix, jUnitOptions.filenameCompatibleNames());
149160
description = Description.createTestDescription(className, name, new PickleId(pickle));
150161
}
151162
return description;

junit/src/test/java/io/cucumber/junit/CucumberTest.java

+44-14
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ void ensureOriginalDirectory() {
5353
@Test
5454
void finds_features_based_on_implicit_package() throws InitializationError {
5555
Cucumber cucumber = new Cucumber(ImplicitFeatureAndGluePath.class);
56-
assertThat(cucumber.getChildren().size(), is(equalTo(6)));
56+
assertThat(cucumber.getChildren().size(), is(equalTo(7)));
5757
assertThat(cucumber.getChildren().get(1).getDescription().getDisplayName(), is(equalTo("Feature A")));
5858
}
5959

6060
@Test
6161
void finds_features_based_on_explicit_root_package() throws InitializationError {
6262
Cucumber cucumber = new Cucumber(ExplicitFeaturePath.class);
63-
assertThat(cucumber.getChildren().size(), is(equalTo(6)));
63+
assertThat(cucumber.getChildren().size(), is(equalTo(7)));
6464
assertThat(cucumber.getChildren().get(1).getDescription().getDisplayName(), is(equalTo("Feature A")));
6565
}
6666

@@ -104,28 +104,58 @@ void cucumber_can_run_features_in_parallel() throws Exception {
104104
Request.classes(computer, ValidEmpty.class).getRunner().run(notifier);
105105
{
106106
InOrder order = Mockito.inOrder(listener);
107-
order.verify(listener).testStarted(argThat(new DescriptionMatcher("Followed by some examples(Feature A)")));
107+
108+
order.verify(listener)
109+
.testStarted(argThat(new DescriptionMatcher("Followed by some examples #1(Feature A)")));
110+
order.verify(listener)
111+
.testFinished(argThat(new DescriptionMatcher("Followed by some examples #1(Feature A)")));
108112
order.verify(listener)
109-
.testFinished(argThat(new DescriptionMatcher("Followed by some examples(Feature A)")));
110-
order.verify(listener).testStarted(argThat(new DescriptionMatcher("Followed by some examples(Feature A)")));
113+
.testStarted(argThat(new DescriptionMatcher("Followed by some examples #2(Feature A)")));
111114
order.verify(listener)
112-
.testFinished(argThat(new DescriptionMatcher("Followed by some examples(Feature A)")));
113-
order.verify(listener).testStarted(argThat(new DescriptionMatcher("Followed by some examples(Feature A)")));
115+
.testFinished(argThat(new DescriptionMatcher("Followed by some examples #2(Feature A)")));
114116
order.verify(listener)
115-
.testFinished(argThat(new DescriptionMatcher("Followed by some examples(Feature A)")));
117+
.testStarted(argThat(new DescriptionMatcher("Followed by some examples #3(Feature A)")));
118+
order.verify(listener)
119+
.testFinished(argThat(new DescriptionMatcher("Followed by some examples #3(Feature A)")));
116120
}
117121
{
118122
InOrder order = Mockito.inOrder(listener);
119123
order.verify(listener).testStarted(argThat(new DescriptionMatcher("A(Feature B)")));
120124
order.verify(listener).testFinished(argThat(new DescriptionMatcher("A(Feature B)")));
121125
order.verify(listener).testStarted(argThat(new DescriptionMatcher("B(Feature B)")));
122126
order.verify(listener).testFinished(argThat(new DescriptionMatcher("B(Feature B)")));
123-
order.verify(listener).testStarted(argThat(new DescriptionMatcher("C(Feature B)")));
124-
order.verify(listener).testFinished(argThat(new DescriptionMatcher("C(Feature B)")));
125-
order.verify(listener).testStarted(argThat(new DescriptionMatcher("C(Feature B)")));
126-
order.verify(listener).testFinished(argThat(new DescriptionMatcher("C(Feature B)")));
127-
order.verify(listener).testStarted(argThat(new DescriptionMatcher("C(Feature B)")));
128-
order.verify(listener).testFinished(argThat(new DescriptionMatcher("C(Feature B)")));
127+
order.verify(listener).testStarted(argThat(new DescriptionMatcher("C #1(Feature B)")));
128+
order.verify(listener).testFinished(argThat(new DescriptionMatcher("C #1(Feature B)")));
129+
order.verify(listener).testStarted(argThat(new DescriptionMatcher("C #2(Feature B)")));
130+
order.verify(listener).testFinished(argThat(new DescriptionMatcher("C #2(Feature B)")));
131+
order.verify(listener).testStarted(argThat(new DescriptionMatcher("C #3(Feature B)")));
132+
order.verify(listener).testFinished(argThat(new DescriptionMatcher("C #3(Feature B)")));
133+
}
134+
}
135+
136+
@Test
137+
void cucumber_distinguishes_between_identical_features() throws Exception {
138+
RunNotifier notifier = new RunNotifier();
139+
RunListener listener = Mockito.mock(RunListener.class);
140+
notifier.addListener(listener);
141+
Request.classes(ValidEmpty.class).getRunner().run(notifier);
142+
{
143+
InOrder order = Mockito.inOrder(listener);
144+
145+
order.verify(listener)
146+
.testStarted(
147+
argThat(new DescriptionMatcher("A single scenario(A feature with a single scenario #1)")));
148+
order.verify(listener)
149+
.testFinished(
150+
argThat(new DescriptionMatcher("A single scenario(A feature with a single scenario #1)")));
151+
152+
order.verify(listener)
153+
.testStarted(
154+
argThat(new DescriptionMatcher("A single scenario(A feature with a single scenario #2)")));
155+
order.verify(listener)
156+
.testFinished(
157+
argThat(new DescriptionMatcher("A single scenario(A feature with a single scenario #2)")));
158+
129159
}
130160
}
131161

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

+2-2
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ public Instant instant() {
121121
classLoader, runtimeOptions);
122122
ThreadLocalRunnerSupplier runnerSupplier = new ThreadLocalRunnerSupplier(runtimeOptions, bus, backendSupplier,
123123
objectFactory, typeRegistrySupplier);
124-
return FeatureRunner.create(feature, filters, runnerSupplier, junitOption);
124+
return FeatureRunner.create(feature, null, filters, runnerSupplier, junitOption);
125125
}
126126

127127
@Test
@@ -365,7 +365,7 @@ void should_notify_of_failure_to_create_runners_and_request_test_execution_to_st
365365
throw illegalStateException;
366366
};
367367

368-
FeatureRunner featureRunner = FeatureRunner.create(feature, filters, runnerSupplier, new JUnitOptions());
368+
FeatureRunner featureRunner = FeatureRunner.create(feature, null, filters, runnerSupplier, new JUnitOptions());
369369

370370
RunNotifier notifier = mock(RunNotifier.class);
371371
PickleRunners.PickleRunner pickleRunner = featureRunner.getChildren().get(0);

junit/src/test/java/io/cucumber/junit/PickleRunnerWithNoStepDescriptionsTest.java

+3
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ void shouldUseScenarioNameWithFeatureNameAsClassNameForDisplayName() {
2525
"feature name",
2626
mock(RunnerSupplier.class),
2727
pickles.get(0),
28+
null,
2829
createJunitOptions());
2930

3031
assertThat(runner.getDescription().getDisplayName(), is(equalTo("scenario name(feature name)")));
@@ -45,6 +46,7 @@ void shouldConvertTextFromFeatureFileForNamesWithFilenameCompatibleNameOption()
4546
"feature name",
4647
mock(RunnerSupplier.class),
4748
pickles.get(0),
49+
null,
4850
createFileNameCompatibleJUnitOptions());
4951

5052
assertThat(runner.getDescription().getDisplayName(), is(equalTo("scenario_name(feature_name)")));
@@ -66,6 +68,7 @@ void shouldConvertTextFromFeatureFileWithRussianLanguage() {
6668
"имя функции",
6769
mock(RunnerSupplier.class),
6870
pickles.get(0),
71+
null,
6972
createFileNameCompatibleJUnitOptions());
7073

7174
assertThat(runner.getDescription().getDisplayName(), is(equalTo("____________(___________)")));

0 commit comments

Comments
 (0)