Skip to content

Commit 4b35d68

Browse files
committed
Formalize feature path
The feature path was poorly defined. It could either be a path or a path with a `classpath:` prefix. This ambiguity allowed room for various inconsistencies and misinterpretations. 1. Pickle URI's were not actually uniform. Depending on operating system different path seperators would be used. 2. Pickle URI's did not identify a resource. The `classpath:` prefix would be dropped after loading a feature file. This lead to a fairly complex try-catch-try-again-on-the-classpath mechanism when loading rerun file. By properly defining the feature identifier it becomes possible to parse it in `RuntimeOptions` and propagate a consistent URI to other parts of cucumber-jvm. This reduces the need for work arounds in various places. Features are identified by a URI. This URI can either be absolute: `scheme:/absolute/path/to.feature`, or relative to the current working directory:`scheme:relative/path/to.feature`. In either form, when the scheme is omitted `file` will be assumed: `path/to.feature`. On systems that use a `File.separatorChar` other then `/` `File.separatorChar` can be used as a path separator. When doing so when the scheme must be omitted: `path\to.feature`. It is recommended to use `/` as the path separator.
1 parent e09d147 commit 4b35d68

File tree

59 files changed

+876
-603
lines changed

Some content is hidden

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

59 files changed

+876
-603
lines changed

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

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import gherkin.pickles.PickleLocation;
99
import gherkin.pickles.PickleTag;
1010

11+
import java.net.URI;
1112
import java.util.ArrayList;
1213
import java.util.List;
1314

@@ -93,7 +94,7 @@ public List<Integer> getLines() {
9394
}
9495

9596
private String fileColonLine(PickleLocation location) {
96-
return pickleEvent.uri + ":" + location.getLine();
97+
return URI.create(pickleEvent.uri).getSchemeSpecificPart() + ":" + location.getLine();
9798
}
9899

99100
@Override

core/src/main/java/cucumber/runtime/RuntimeOptions.java

+39-43
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import cucumber.runtime.io.MultiLoader;
66
import cucumber.runtime.io.Resource;
77
import cucumber.runtime.io.ResourceLoader;
8-
import cucumber.runtime.model.PathWithLines;
9-
import cucumber.util.Encoding;
8+
import io.cucumber.core.model.FeatureWithLines;
109
import cucumber.util.FixJava;
1110
import cucumber.util.Mapper;
1211
import gherkin.GherkinDialect;
@@ -22,12 +21,14 @@
2221
import java.io.IOException;
2322
import java.io.InputStreamReader;
2423
import java.io.Reader;
24+
import java.net.URI;
2525
import java.util.ArrayList;
26-
import java.util.Collections;
2726
import java.util.HashMap;
2827
import java.util.List;
2928
import java.util.Map;
3029
import java.util.ResourceBundle;
30+
import java.util.Set;
31+
import java.util.TreeSet;
3132
import java.util.regex.Matcher;
3233
import java.util.regex.Pattern;
3334

@@ -58,8 +59,8 @@ public String map(String keyword) {
5859
private final List<String> glue = new ArrayList<String>();
5960
private final List<String> tagFilters = new ArrayList<String>();
6061
private final List<Pattern> nameFilters = new ArrayList<Pattern>();
61-
private final Map<String, List<Long>> lineFilters = new HashMap<String, List<Long>>();
62-
private final List<String> featurePaths = new ArrayList<String>();
62+
private final Map<URI, Set<Integer>> lineFilters = new HashMap<>();
63+
private final List<URI> featurePaths = new ArrayList<>();
6364

6465
private final List<String> junitOptions = new ArrayList<String>();
6566
private final ResourceLoader resourceLoader;
@@ -137,8 +138,8 @@ public RuntimeOptions noSummaryPrinter() {
137138
private void parse(List<String> args) {
138139
List<String> parsedTagFilters = new ArrayList<String>();
139140
List<Pattern> parsedNameFilters = new ArrayList<Pattern>();
140-
Map<String, List<Long>> parsedLineFilters = new HashMap<String, List<Long>>();
141-
List<String> parsedFeaturePaths = new ArrayList<String>();
141+
Map<URI, Set<Integer>> parsedLineFilters = new HashMap<>();
142+
List<URI> parsedFeaturePaths = new ArrayList<>();
142143
List<String> parsedGlue = new ArrayList<String>();
143144
ParsedPluginData parsedPluginData = new ParsedPluginData();
144145
List<String> parsedJunitOptions = new ArrayList<String>();
@@ -188,29 +189,21 @@ private void parse(List<String> args) {
188189
} else if (arg.startsWith("-")) {
189190
printUsage();
190191
throw new CucumberException("Unknown option: " + arg);
191-
} else {
192-
List<PathWithLines> paths;
193-
if (arg.startsWith("@")) {
194-
paths = loadRerunFile(arg.substring(1));
195-
} else {
196-
paths = parsePathWithLines(arg);
197-
}
198-
for (PathWithLines pathWithLines : paths) {
199-
parsedFeaturePaths.add(pathWithLines.path);
200-
if (!pathWithLines.lines.isEmpty()) {
201-
String key = pathWithLines.path.replace("classpath:", "");
202-
addLineFilters(parsedLineFilters, key, pathWithLines.lines);
203-
}
204-
}
192+
} else if (arg.startsWith("@")) {
193+
FeatureWithLines featureWithLines = parseFeatureWithLines(arg.substring(1));
194+
processPathWitheLinesFromRerunFile(parsedLineFilters, parsedFeaturePaths, featureWithLines.uri());
195+
} else if (!arg.isEmpty()){
196+
FeatureWithLines featureWithLines = parseFeatureWithLines(arg);
197+
processFeatureWithLines(parsedLineFilters, parsedFeaturePaths, featureWithLines);
205198
}
206199
}
207-
if (!parsedTagFilters.isEmpty() || !parsedNameFilters.isEmpty() || !parsedLineFilters.isEmpty() || haveLineFilters(parsedFeaturePaths)) {
200+
if (!parsedTagFilters.isEmpty() || !parsedNameFilters.isEmpty() || !parsedLineFilters.isEmpty()) {
208201
tagFilters.clear();
209202
tagFilters.addAll(parsedTagFilters);
210203
nameFilters.clear();
211204
nameFilters.addAll(parsedNameFilters);
212205
lineFilters.clear();
213-
for (String path : parsedLineFilters.keySet()) {
206+
for (URI path : parsedLineFilters.keySet()) {
214207
lineFilters.put(path, parsedLineFilters.get(path));
215208
}
216209
}
@@ -233,47 +226,50 @@ private void parse(List<String> args) {
233226
parsedPluginData.updatePluginSummaryPrinterNames(pluginSummaryPrinterNames);
234227
}
235228

236-
private void addLineFilters(Map<String, List<Long>> parsedLineFilters, String key, List<Long> lines) {
229+
private FeatureWithLines parseFeatureWithLines(String pathWithLines) {
230+
return FeatureWithLines.parse(pathWithLines);
231+
}
232+
233+
private void addLineFilters(Map<URI, Set<Integer>> parsedLineFilters, URI key, Set<Integer> lines) {
234+
if(lines.isEmpty()){
235+
return;
236+
}
237237
if (parsedLineFilters.containsKey(key)) {
238238
parsedLineFilters.get(key).addAll(lines);
239239
} else {
240-
parsedLineFilters.put(key, lines);
240+
parsedLineFilters.put(key, new TreeSet<>(lines));
241241
}
242242
}
243243

244-
private boolean haveLineFilters(List<String> parsedFeaturePaths) {
245-
for (String pathName : parsedFeaturePaths) {
246-
if (PathWithLines.hasLineFilters(pathName)) {
247-
return true;
248-
}
244+
private void processFeatureWithLines(Map<URI, Set<Integer>> parsedLineFilters, List<URI> parsedFeaturePaths, FeatureWithLines featureWithLines) {
245+
parsedFeaturePaths.add(featureWithLines.uri());
246+
addLineFilters(parsedLineFilters, featureWithLines.uri(), featureWithLines.lines());
247+
}
248+
249+
private void processPathWitheLinesFromRerunFile(Map<URI, Set<Integer>> parsedLineFilters, List<URI> parsedFeaturePaths, URI rerunPath) {
250+
for (FeatureWithLines featureWithLines : loadRerunFile(rerunPath)) {
251+
processFeatureWithLines(parsedLineFilters, parsedFeaturePaths, featureWithLines);
249252
}
250-
return false;
251253
}
252254

253-
private List<PathWithLines> loadRerunFile(String rerunPath) {
254-
List<PathWithLines> featurePaths = new ArrayList<>();
255+
private List<FeatureWithLines> loadRerunFile(URI rerunPath) {
256+
List<FeatureWithLines> featurePaths = new ArrayList<>();
255257
Iterable<Resource> resources = resourceLoader.resources(rerunPath, null);
256258
for (Resource resource : resources) {
257259
String source = read(resource);
258260
if (!source.isEmpty()) {
259261
Matcher matcher = RERUN_PATH_SPECIFICATION.matcher(source);
260262
while (matcher.find()) {
261-
featurePaths.addAll(parsePathWithLines(matcher.group(1)));
263+
featurePaths.add(parseFeatureWithLines(matcher.group(1)));
262264
}
263265
}
264266
}
265267
return featurePaths;
266268
}
267269

268-
private List<PathWithLines> parsePathWithLines(String pathWithLineFilter) {
269-
String normalizedPath = convertFileSeparatorToForwardSlash(pathWithLineFilter);
270-
PathWithLines pathWithLines = new PathWithLines(normalizedPath);
271-
return Collections.singletonList(pathWithLines);
272-
}
273-
274270
private static String read(Resource resource) {
275271
try {
276-
return Encoding.readFile(resource);
272+
return FixJava.readReader(new InputStreamReader(resource.getInputStream()));
277273
} catch (IOException e) {
278274
throw new CucumberException("Failed to read resource:" + resource.getPath(), e);
279275
}
@@ -380,7 +376,7 @@ public boolean isWip() {
380376
}
381377

382378
@Override
383-
public List<String> getFeaturePaths() {
379+
public List<URI> getFeaturePaths() {
384380
return featurePaths;
385381
}
386382

@@ -395,7 +391,7 @@ public List<String> getTagFilters() {
395391
}
396392

397393
@Override
398-
public Map<String, List<Long>> getLineFilters() {
394+
public Map<URI, Set<Integer>> getLineFilters() {
399395
return lineFilters;
400396
}
401397

core/src/main/java/cucumber/runtime/RuntimeOptionsFactory.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ private void addFeatures(CucumberOptions options, List<String> args) {
9595

9696
private void addDefaultFeaturePathIfNoFeaturePathIsSpecified(List<String> args, Class clazz) {
9797
if (!featuresSpecified) {
98-
args.add(MultiLoader.CLASSPATH_SCHEME + packagePath(clazz));
98+
args.add(MultiLoader.CLASSPATH_SCHEME_PREFIX + packagePath(clazz));
9999
}
100100
}
101101

@@ -125,7 +125,7 @@ private void addGlue(CucumberOptions options, List<String> args) {
125125
private void addDefaultGlueIfNoOverridingGlueIsSpecified(List<String> args, Class clazz) {
126126
if (!overridingGlueSpecified) {
127127
args.add("--glue");
128-
args.add(MultiLoader.CLASSPATH_SCHEME + packagePath(clazz));
128+
args.add(MultiLoader.CLASSPATH_SCHEME_PREFIX + packagePath(clazz));
129129
}
130130
}
131131

core/src/main/java/cucumber/runtime/filter/Filters.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@
33
import gherkin.events.PickleEvent;
44
import io.cucumber.core.options.FilterOptions;
55

6+
import java.net.URI;
67
import java.util.ArrayList;
8+
import java.util.Collection;
79
import java.util.List;
810
import java.util.Map;
911
import java.util.regex.Pattern;
@@ -23,7 +25,7 @@ public Filters(FilterOptions filterOPtions) {
2325
if (!nameFilters.isEmpty()) {
2426
this.filters.add(new NamePredicate(nameFilters));
2527
}
26-
Map<String, List<Long>> lineFilters = filterOPtions.getLineFilters();
28+
Map<URI, ? extends Collection<Integer>> lineFilters = filterOPtions.getLineFilters();
2729
if (!lineFilters.isEmpty()) {
2830
this.filters.add(new LinePredicate(lineFilters));
2931
}

core/src/main/java/cucumber/runtime/filter/LinePredicate.java

+6-5
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,24 @@
33
import gherkin.events.PickleEvent;
44
import gherkin.pickles.PickleLocation;
55

6-
import java.util.List;
6+
import java.net.URI;
7+
import java.util.Collection;
78
import java.util.Map;
89

910
class LinePredicate implements PicklePredicate {
10-
private Map<String, List<Long>> lineFilters;
11+
private Map<URI, ? extends Collection<Integer>> lineFilters;
1112

12-
LinePredicate(Map<String, List<Long>> lineFilters) {
13+
LinePredicate(Map<URI, ? extends Collection<Integer>> lineFilters) {
1314
this.lineFilters = lineFilters;
1415
}
1516

1617
@Override
1718
public boolean apply(PickleEvent pickleEvent) {
18-
String picklePath = pickleEvent.uri;
19+
URI picklePath = URI.create(pickleEvent.uri);
1920
if (!lineFilters.containsKey(picklePath)) {
2021
return true;
2122
}
22-
for (Long line : lineFilters.get(picklePath)) {
23+
for (Integer line : lineFilters.get(picklePath)) {
2324
for (PickleLocation location : pickleEvent.pickle.getLocations()) {
2425
if (line == location.getLine()) {
2526
return true;

core/src/main/java/cucumber/runtime/formatter/PrettyFormatter.java

+3-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import gherkin.ast.Tag;
2929
import gherkin.pickles.PickleTag;
3030

31+
import java.net.URI;
3132
import java.util.List;
3233

3334
final class PrettyFormatter implements EventListener, ColorAware {
@@ -243,7 +244,8 @@ private String getScenarioDefinitionText(ScenarioDefinition definition) {
243244
}
244245

245246
private String getLocationText(String file, int line) {
246-
return getLocationText(file + ":" + line);
247+
String path = URI.create(file).getSchemeSpecificPart();
248+
return getLocationText(path + ":" + line);
247249
}
248250

249251
private String getLocationText(String location) {

core/src/main/java/cucumber/runtime/formatter/RerunFormatter.java

+34-44
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,12 @@
88
import cucumber.api.event.TestRunFinished;
99
import cucumber.api.formatter.NiceAppendable;
1010
import cucumber.api.formatter.StrictAware;
11+
import io.cucumber.core.model.FeatureWithLines;
1112

1213
import java.util.ArrayList;
14+
import java.util.Collection;
1315
import java.util.HashMap;
1416
import java.util.Map;
15-
import java.util.Set;
1617

1718

1819
/**
@@ -21,19 +22,42 @@
2122
*/
2223
final class RerunFormatter implements EventListener, StrictAware {
2324
private final NiceAppendable out;
24-
private Map<String, ArrayList<Integer>> featureAndFailedLinesMapping = new HashMap<String, ArrayList<Integer>>();
25-
private boolean isStrict = false;
25+
private final Map<String, Collection<Integer>> featureAndFailedLinesMapping = new HashMap<>();
26+
private final EventHandler<TestRunFinished> runFinishHandler = new EventHandler<TestRunFinished>() {
2627

27-
private EventHandler<TestCaseFinished> testCaseFinishedHandler = new EventHandler<TestCaseFinished>() {
2828
@Override
29-
public void receive(TestCaseFinished event) {
30-
handleTestCaseFinished(event);
29+
public void receive(TestRunFinished event) {
30+
for (Map.Entry<String, Collection<Integer>> entry : featureAndFailedLinesMapping.entrySet()) {
31+
FeatureWithLines featureWithLines = FeatureWithLines.parse(entry.getKey(), entry.getValue());
32+
out.println(featureWithLines.toString());
33+
}
34+
35+
out.close();
3136
}
3237
};
33-
private EventHandler<TestRunFinished> runFinishHandler = new EventHandler<TestRunFinished>() {
38+
private boolean isStrict = false;
39+
private final EventHandler<TestCaseFinished> testCaseFinishedHandler = new EventHandler<TestCaseFinished>() {
40+
3441
@Override
35-
public void receive(TestRunFinished event) {
36-
handleTestRunFinished();
42+
public void receive(TestCaseFinished event) {
43+
if (!event.result.isOk(isStrict)) {
44+
recordTestFailed(event.testCase);
45+
}
46+
}
47+
48+
private void recordTestFailed(TestCase testCase) {
49+
String uri = testCase.getUri();
50+
Collection<Integer> failedTestCaseLines = getFailedTestCaseLines(uri);
51+
failedTestCaseLines.add(testCase.getLine());
52+
}
53+
54+
private Collection<Integer> getFailedTestCaseLines(String uri) {
55+
Collection<Integer> failedTestCases = featureAndFailedLinesMapping.get(uri);
56+
if (failedTestCases == null) {
57+
failedTestCases = new ArrayList<>();
58+
featureAndFailedLinesMapping.put(uri, failedTestCases);
59+
}
60+
return failedTestCases;
3761
}
3862
};
3963

@@ -52,39 +76,5 @@ public void setEventPublisher(EventPublisher publisher) {
5276
public void setStrict(boolean strict) {
5377
isStrict = strict;
5478
}
55-
56-
private void handleTestCaseFinished(TestCaseFinished event) {
57-
if (!event.result.isOk(isStrict)) {
58-
recordTestFailed(event.testCase);
59-
}
60-
}
61-
62-
private void handleTestRunFinished() {
63-
reportFailedTestCases();
64-
out.close();
65-
}
66-
67-
private void recordTestFailed(TestCase testCase) {
68-
String path = testCase.getUri();
69-
ArrayList<Integer> failedTestCases = this.featureAndFailedLinesMapping.get(path);
70-
if (failedTestCases == null) {
71-
failedTestCases = new ArrayList<Integer>();
72-
this.featureAndFailedLinesMapping.put(path, failedTestCases);
73-
}
74-
75-
failedTestCases.add(testCase.getLine());
76-
}
77-
78-
private void reportFailedTestCases() {
79-
Set<Map.Entry<String, ArrayList<Integer>>> entries = featureAndFailedLinesMapping.entrySet();
80-
for (Map.Entry<String, ArrayList<Integer>> entry : entries) {
81-
if (!entry.getValue().isEmpty()) {
82-
out.append(entry.getKey());
83-
for (Integer line : entry.getValue()) {
84-
out.append(":").append(line.toString());
85-
}
86-
out.println();
87-
}
88-
}
89-
}
9079
}
80+

0 commit comments

Comments
 (0)