Skip to content

Commit fc8b066

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 fc8b066

File tree

11 files changed

+182
-41
lines changed

11 files changed

+182
-41
lines changed

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

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
import io.cucumber.core.filter.Filters;
66
import io.cucumber.core.gherkin.Feature;
77
import io.cucumber.core.gherkin.Pickle;
8-
import io.cucumber.core.logging.Logger;
9-
import io.cucumber.core.logging.LoggerFactory;
108
import io.cucumber.core.options.Constants;
119
import io.cucumber.core.options.CucumberOptionsAnnotationParser;
1210
import io.cucumber.core.options.CucumberProperties;
@@ -40,10 +38,14 @@
4038

4139
import java.time.Clock;
4240
import java.util.List;
41+
import java.util.Map;
42+
import java.util.Optional;
4343
import java.util.UUID;
4444
import java.util.function.Predicate;
4545
import java.util.function.Supplier;
4646

47+
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
48+
import static java.util.stream.Collectors.groupingBy;
4749
import static java.util.stream.Collectors.toList;
4850

4951
/**
@@ -88,8 +90,6 @@
8890
@API(status = API.Status.STABLE)
8991
public final class Cucumber extends ParentRunner<ParentRunner<?>> {
9092

91-
private static final Logger log = LoggerFactory.getLogger(Cucumber.class);
92-
9393
private final List<ParentRunner<?>> children;
9494
private final EventBus bus;
9595
private final List<Feature> features;
@@ -171,8 +171,14 @@ public Cucumber(Class<?> clazz) throws InitializationError {
171171
objectFactorySupplier, typeRegistryConfigurerSupplier);
172172
this.context = new CucumberExecutionContext(bus, exitStatus, runnerSupplier);
173173
Predicate<Pickle> filters = new Filters(runtimeOptions);
174+
175+
Map<Optional<String>, List<Feature>> groupedByName = features.stream()
176+
.collect(groupingBy(Feature::getName));
174177
this.children = features.stream()
175-
.map(feature -> FeatureRunner.create(feature, filters, runnerSupplier, junitOptions))
178+
.map(feature -> {
179+
Integer uniqueSuffix = uniqueSuffix(groupedByName, feature, Feature::getName);
180+
return FeatureRunner.create(feature, uniqueSuffix, filters, runnerSupplier, junitOptions);
181+
})
176182
.filter(runner -> !runner.isEmpty())
177183
.collect(toList());
178184
}

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

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,38 +14,55 @@
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;
1819

1920
import static io.cucumber.junit.FileNameCompatibleNames.createName;
21+
import static io.cucumber.junit.FileNameCompatibleNames.uniqueSuffix;
2022
import static io.cucumber.junit.PickleRunners.withNoStepDescriptions;
2123
import static io.cucumber.junit.PickleRunners.withStepDescriptions;
24+
import static java.util.stream.Collectors.groupingBy;
2225
import static java.util.stream.Collectors.toList;
2326

2427
final class FeatureRunner extends ParentRunner<PickleRunner> {
2528

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+
45+
Map<String, List<Pickle>> groupedByName = feature.getPickles().stream()
46+
.filter(filter)
47+
.collect(groupingBy(Pickle::getName));
48+
this.children = feature.getPickles()
49+
.stream()
50+
.map(pickle -> {
51+
String featureName = getName();
52+
Integer exampleId = uniqueSuffix(groupedByName, pickle, Pickle::getName);
53+
return options.stepNotifications()
54+
? withStepDescriptions(runners, pickle, exampleId, options)
55+
: withNoStepDescriptions(featureName, runners, pickle, exampleId, options);
56+
})
4157
.collect(toList());
4258
}
4359

4460
static FeatureRunner create(
45-
Feature feature, Predicate<Pickle> filter, RunnerSupplier runners, JUnitOptions options
61+
Feature feature, Integer uniqueSuffix, Predicate<Pickle> filter, RunnerSupplier runners,
62+
JUnitOptions options
4663
) {
4764
try {
48-
return new FeatureRunner(feature, filter, runners, options);
65+
return new FeatureRunner(feature, uniqueSuffix, filter, runners, options);
4966
} catch (InitializationError e) {
5067
throw new CucumberException("Failed to create scenario runner", e);
5168
}
@@ -89,7 +106,7 @@ public String toString() {
89106
@Override
90107
protected String getName() {
91108
String name = feature.getName().orElse("EMPTY_NAME");
92-
return createName(name, options.filenameCompatibleNames());
109+
return createName(name, uniqueSuffix, options.filenameCompatibleNames());
93110
}
94111

95112
@Override
Lines changed: 17 additions & 1 deletion
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

Lines changed: 21 additions & 10 deletions
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

Lines changed: 44 additions & 14 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 3 additions & 0 deletions
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)