Skip to content

Commit d7f7355

Browse files
authored
Restore TestSourcesModel (#3077)
Unfortunately Serenity is shadowing the `io.cucumber.plugin` package and using our internal APIs. hey shouldn't be doing that, and it will irreparably break once we upgrade to Java 17 because of the module system. But until then we can keep this around. Fixes: #3076
1 parent 99f3dd8 commit d7f7355

File tree

3 files changed

+301
-0
lines changed

3 files changed

+301
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1717

1818
### Fixed
1919
- [Core] Emit StepMatchArgumentsList for ambiguous steps ([#3066](https://github.com/cucumber/cucumber-jvm/pull/3066) M.P. Korstanje)
20+
- [Core] Restore `TestSourcesModel` ([#3076](https://github.com/cucumber/cucumber-jvm/pull/3076) M.P. Korstanje)
2021

2122
### Changed
2223
- [Core] Use a message based `RerunFormatter` ([#3075](https://github.com/cucumber/cucumber-jvm/pull/3075) M.P. Korstanje)
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package io.cucumber.core.plugin;
2+
3+
import io.cucumber.core.resource.Resource;
4+
import io.cucumber.plugin.event.TestSourceRead;
5+
6+
import java.io.ByteArrayInputStream;
7+
import java.io.InputStream;
8+
import java.net.URI;
9+
10+
import static java.nio.charset.StandardCharsets.UTF_8;
11+
12+
/**
13+
* Internal class, potentially still used by others.
14+
*
15+
* @see <a href=
16+
* "https://github.com/cucumber/cucumber-jvm/issues/3076">cucumber/cucumber-jvm#3076</a>
17+
* @deprecated for removal, use messages + query.
18+
*/
19+
@Deprecated
20+
final class TestSourceReadResource implements Resource {
21+
22+
private final TestSourceRead event;
23+
24+
TestSourceReadResource(TestSourceRead event) {
25+
this.event = event;
26+
}
27+
28+
@Override
29+
public URI getUri() {
30+
return event.getUri();
31+
}
32+
33+
@Override
34+
public InputStream getInputStream() {
35+
return new ByteArrayInputStream(event.getSource().getBytes(UTF_8));
36+
}
37+
38+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package io.cucumber.core.plugin;
2+
3+
import io.cucumber.gherkin.GherkinParser;
4+
import io.cucumber.messages.types.Background;
5+
import io.cucumber.messages.types.Envelope;
6+
import io.cucumber.messages.types.Examples;
7+
import io.cucumber.messages.types.Feature;
8+
import io.cucumber.messages.types.FeatureChild;
9+
import io.cucumber.messages.types.GherkinDocument;
10+
import io.cucumber.messages.types.Rule;
11+
import io.cucumber.messages.types.RuleChild;
12+
import io.cucumber.messages.types.Scenario;
13+
import io.cucumber.messages.types.Source;
14+
import io.cucumber.messages.types.SourceMediaType;
15+
import io.cucumber.messages.types.Step;
16+
import io.cucumber.messages.types.TableRow;
17+
import io.cucumber.plugin.event.TestSourceRead;
18+
19+
import java.io.File;
20+
import java.net.URI;
21+
import java.net.URISyntaxException;
22+
import java.util.HashMap;
23+
import java.util.Map;
24+
import java.util.Optional;
25+
import java.util.regex.Pattern;
26+
import java.util.stream.Stream;
27+
28+
/**
29+
* Internal class, still used by Serenity.
30+
*
31+
* @see <a href=
32+
* "https://github.com/cucumber/cucumber-jvm/issues/3076">cucumber/cucumber-jvm#3076</a>
33+
* @deprecated for removal, use messages + query.
34+
*/
35+
@SuppressWarnings("unused")
36+
@Deprecated
37+
final class TestSourcesModel {
38+
39+
private final Map<URI, TestSourceRead> pathToReadEventMap = new HashMap<>();
40+
private final Map<URI, GherkinDocument> pathToAstMap = new HashMap<>();
41+
private final Map<URI, Map<Long, AstNode>> pathToNodeMap = new HashMap<>();
42+
43+
static Scenario getScenarioDefinition(AstNode astNode) {
44+
AstNode candidate = astNode;
45+
while (candidate != null && !(candidate.node instanceof Scenario)) {
46+
candidate = candidate.parent;
47+
}
48+
return candidate == null ? null : (Scenario) candidate.node;
49+
}
50+
51+
static boolean isBackgroundStep(AstNode astNode) {
52+
return astNode.parent.node instanceof Background;
53+
}
54+
55+
static String calculateId(AstNode astNode) {
56+
Object node = astNode.node;
57+
if (node instanceof Rule) {
58+
return calculateId(astNode.parent) + ";" + convertToId(((Rule) node).getName());
59+
}
60+
if (node instanceof Scenario) {
61+
return calculateId(astNode.parent) + ";" + convertToId(((Scenario) node).getName());
62+
}
63+
if (node instanceof ExamplesRowWrapperNode) {
64+
return calculateId(astNode.parent) + ";" + (((ExamplesRowWrapperNode) node).bodyRowIndex + 2);
65+
}
66+
if (node instanceof TableRow) {
67+
return calculateId(astNode.parent) + ";" + 1;
68+
}
69+
if (node instanceof Examples) {
70+
return calculateId(astNode.parent) + ";" + convertToId(((Examples) node).getName());
71+
}
72+
if (node instanceof Feature) {
73+
return convertToId(((Feature) node).getName());
74+
}
75+
return "";
76+
}
77+
78+
private static final Pattern replacementPattern = Pattern.compile("[\\s'_,!]");
79+
80+
static String convertToId(String name) {
81+
return replacementPattern.matcher(name).replaceAll("-").toLowerCase();
82+
}
83+
84+
static URI relativize(URI uri) {
85+
if (!"file".equals(uri.getScheme())) {
86+
return uri;
87+
}
88+
if (!uri.isAbsolute()) {
89+
return uri;
90+
}
91+
92+
try {
93+
URI root = new File("").toURI();
94+
URI relative = root.relativize(uri);
95+
// Scheme is lost by relativize
96+
return new URI("file", relative.getSchemeSpecificPart(), relative.getFragment());
97+
} catch (URISyntaxException e) {
98+
throw new IllegalArgumentException(e.getMessage(), e);
99+
}
100+
}
101+
102+
void addTestSourceReadEvent(URI path, TestSourceRead event) {
103+
pathToReadEventMap.put(path, event);
104+
}
105+
106+
Feature getFeature(URI path) {
107+
if (!pathToAstMap.containsKey(path)) {
108+
parseGherkinSource(path);
109+
}
110+
if (pathToAstMap.containsKey(path)) {
111+
return pathToAstMap.get(path).getFeature().orElse(null);
112+
}
113+
return null;
114+
}
115+
116+
private void parseGherkinSource(URI path) {
117+
if (!pathToReadEventMap.containsKey(path)) {
118+
return;
119+
}
120+
String source = pathToReadEventMap.get(path).getSource();
121+
122+
GherkinParser parser = GherkinParser.builder()
123+
.build();
124+
125+
Stream<Envelope> envelopes = parser.parse(
126+
Envelope.of(new Source(path.toString(), source, SourceMediaType.TEXT_X_CUCUMBER_GHERKIN_PLAIN)));
127+
128+
// TODO: What about empty gherkin docs?
129+
GherkinDocument gherkinDocument = envelopes
130+
.map(Envelope::getGherkinDocument)
131+
.filter(Optional::isPresent)
132+
.map(Optional::get)
133+
.findFirst()
134+
.orElse(null);
135+
136+
pathToAstMap.put(path, gherkinDocument);
137+
Map<Long, AstNode> nodeMap = new HashMap<>();
138+
// TODO: What about gherkin docs with no features?
139+
Feature feature = gherkinDocument.getFeature().get();
140+
AstNode currentParent = new AstNode(feature, null);
141+
for (FeatureChild child : feature.getChildren()) {
142+
processFeatureDefinition(nodeMap, child, currentParent);
143+
}
144+
pathToNodeMap.put(path, nodeMap);
145+
146+
}
147+
148+
private void processFeatureDefinition(Map<Long, AstNode> nodeMap, FeatureChild child, AstNode currentParent) {
149+
child.getBackground().ifPresent(background -> processBackgroundDefinition(nodeMap, background, currentParent));
150+
child.getScenario().ifPresent(scenario -> processScenarioDefinition(nodeMap, scenario, currentParent));
151+
child.getRule().ifPresent(rule -> {
152+
AstNode childNode = new AstNode(rule, currentParent);
153+
nodeMap.put(rule.getLocation().getLine(), childNode);
154+
rule.getChildren().forEach(ruleChild -> processRuleDefinition(nodeMap, ruleChild, childNode));
155+
});
156+
}
157+
158+
private void processBackgroundDefinition(
159+
Map<Long, AstNode> nodeMap, Background background, AstNode currentParent
160+
) {
161+
AstNode childNode = new AstNode(background, currentParent);
162+
nodeMap.put(background.getLocation().getLine(), childNode);
163+
for (Step step : background.getSteps()) {
164+
nodeMap.put(step.getLocation().getLine(), new AstNode(step, childNode));
165+
}
166+
}
167+
168+
private void processScenarioDefinition(Map<Long, AstNode> nodeMap, Scenario child, AstNode currentParent) {
169+
AstNode childNode = new AstNode(child, currentParent);
170+
nodeMap.put(child.getLocation().getLine(), childNode);
171+
for (io.cucumber.messages.types.Step step : child.getSteps()) {
172+
nodeMap.put(step.getLocation().getLine(), new AstNode(step, childNode));
173+
}
174+
if (!child.getExamples().isEmpty()) {
175+
processScenarioOutlineExamples(nodeMap, child, childNode);
176+
}
177+
}
178+
179+
private void processRuleDefinition(Map<Long, AstNode> nodeMap, RuleChild child, AstNode currentParent) {
180+
child.getBackground().ifPresent(background -> processBackgroundDefinition(nodeMap, background, currentParent));
181+
child.getScenario().ifPresent(scenario -> processScenarioDefinition(nodeMap, scenario, currentParent));
182+
}
183+
184+
private void processScenarioOutlineExamples(
185+
Map<Long, AstNode> nodeMap, Scenario scenarioOutline, AstNode parent
186+
) {
187+
for (Examples examples : scenarioOutline.getExamples()) {
188+
AstNode examplesNode = new AstNode(examples, parent);
189+
// TODO: Can tables without headers even exist?
190+
TableRow headerRow = examples.getTableHeader().get();
191+
AstNode headerNode = new AstNode(headerRow, examplesNode);
192+
nodeMap.put(headerRow.getLocation().getLine(), headerNode);
193+
for (int i = 0; i < examples.getTableBody().size(); ++i) {
194+
TableRow examplesRow = examples.getTableBody().get(i);
195+
Object rowNode = new ExamplesRowWrapperNode(examplesRow, i);
196+
AstNode expandedScenarioNode = new AstNode(rowNode, examplesNode);
197+
nodeMap.put(examplesRow.getLocation().getLine(), expandedScenarioNode);
198+
}
199+
}
200+
}
201+
202+
AstNode getAstNode(URI path, int line) {
203+
if (!pathToNodeMap.containsKey(path)) {
204+
parseGherkinSource(path);
205+
}
206+
if (pathToNodeMap.containsKey(path)) {
207+
return pathToNodeMap.get(path).get((long) line);
208+
}
209+
return null;
210+
}
211+
212+
boolean hasBackground(URI path, int line) {
213+
if (!pathToNodeMap.containsKey(path)) {
214+
parseGherkinSource(path);
215+
}
216+
if (pathToNodeMap.containsKey(path)) {
217+
AstNode astNode = pathToNodeMap.get(path).get((long) line);
218+
return getBackgroundForTestCase(astNode).isPresent();
219+
}
220+
return false;
221+
}
222+
223+
static Optional<Background> getBackgroundForTestCase(AstNode astNode) {
224+
Feature feature = getFeatureForTestCase(astNode);
225+
return feature.getChildren()
226+
.stream()
227+
.map(FeatureChild::getBackground)
228+
.filter(Optional::isPresent)
229+
.map(Optional::get)
230+
.findFirst();
231+
}
232+
233+
private static Feature getFeatureForTestCase(AstNode astNode) {
234+
while (astNode.parent != null) {
235+
astNode = astNode.parent;
236+
}
237+
return (Feature) astNode.node;
238+
}
239+
240+
static class ExamplesRowWrapperNode {
241+
242+
final int bodyRowIndex;
243+
244+
ExamplesRowWrapperNode(Object examplesRow, int bodyRowIndex) {
245+
this.bodyRowIndex = bodyRowIndex;
246+
}
247+
248+
}
249+
250+
static class AstNode {
251+
252+
final Object node;
253+
final AstNode parent;
254+
255+
AstNode(Object node, AstNode parent) {
256+
this.node = node;
257+
this.parent = parent;
258+
}
259+
260+
}
261+
262+
}

0 commit comments

Comments
 (0)