Skip to content

Commit a73086b

Browse files
authored
Extract query object for reuse in other formatters (#29)
Coincidentally this also fixes a bug where the results for a retried scenario were overwritten. Working with messages can be pretty cumbersome and repetitive. Wrapping the messages in a database like object that can be queried should make it easier to write other formatters.
1 parent 720e08a commit a73086b

File tree

6 files changed

+428
-204
lines changed

6 files changed

+428
-204
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

88
## [Unreleased]
9+
### Fixed
10+
- Do not overwrite results of retried tests ([#29](https://github.com/cucumber/cucumber-junit-xml-formatter/pull/29), M.P. Korstanje)
911

1012
## [0.2.1] - 2024-02-15
1113
### Fixed
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package io.cucumber.junitxmlformatter;
2+
3+
import io.cucumber.messages.types.Examples;
4+
import io.cucumber.messages.types.Feature;
5+
import io.cucumber.messages.types.Rule;
6+
import io.cucumber.messages.types.Scenario;
7+
import io.cucumber.messages.types.TableRow;
8+
9+
import java.util.Optional;
10+
11+
import static java.util.Objects.requireNonNull;
12+
13+
class GherkingAstNodes {
14+
private final Feature feature;
15+
private final Rule rule;
16+
private final Scenario scenario;
17+
private final Examples examples;
18+
private final TableRow example;
19+
private final Integer examplesIndex;
20+
private final Integer exampleIndex;
21+
22+
public GherkingAstNodes(Feature feature, Rule rule, Scenario scenario) {
23+
this(feature, rule, scenario, null, null, null, null);
24+
}
25+
26+
public GherkingAstNodes(Feature feature, Rule rule, Scenario scenario, Integer examplesIndex, Examples examples, Integer exampleIndex, TableRow example) {
27+
this.feature = requireNonNull(feature);
28+
this.rule = rule;
29+
this.scenario = requireNonNull(scenario);
30+
this.examplesIndex = examplesIndex;
31+
this.examples = examples;
32+
this.exampleIndex = exampleIndex;
33+
this.example = example;
34+
}
35+
36+
public Feature feature() {
37+
return feature;
38+
}
39+
40+
public Optional<Rule> rule() {
41+
return Optional.ofNullable(rule);
42+
}
43+
44+
public Scenario scenario() {
45+
return scenario;
46+
}
47+
48+
public Optional<Examples> examples() {
49+
return Optional.ofNullable(examples);
50+
}
51+
52+
public Optional<TableRow> example() {
53+
return Optional.ofNullable(example);
54+
}
55+
56+
public Optional<Integer> examplesIndex() {
57+
return Optional.ofNullable(examplesIndex);
58+
}
59+
60+
public Optional<Integer> exampleIndex() {
61+
return Optional.ofNullable(exampleIndex);
62+
}
63+
}
Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package io.cucumber.junitxmlformatter;
2+
3+
import io.cucumber.messages.Convertor;
4+
import io.cucumber.messages.types.Envelope;
5+
import io.cucumber.messages.types.Examples;
6+
import io.cucumber.messages.types.Feature;
7+
import io.cucumber.messages.types.GherkinDocument;
8+
import io.cucumber.messages.types.Pickle;
9+
import io.cucumber.messages.types.PickleStep;
10+
import io.cucumber.messages.types.Rule;
11+
import io.cucumber.messages.types.Scenario;
12+
import io.cucumber.messages.types.Step;
13+
import io.cucumber.messages.types.TableRow;
14+
import io.cucumber.messages.types.TestCase;
15+
import io.cucumber.messages.types.TestCaseFinished;
16+
import io.cucumber.messages.types.TestCaseStarted;
17+
import io.cucumber.messages.types.TestRunFinished;
18+
import io.cucumber.messages.types.TestRunStarted;
19+
import io.cucumber.messages.types.TestStep;
20+
import io.cucumber.messages.types.TestStepFinished;
21+
import io.cucumber.messages.types.TestStepResult;
22+
import io.cucumber.messages.types.Timestamp;
23+
24+
import java.time.Duration;
25+
import java.util.AbstractMap.SimpleEntry;
26+
import java.util.ArrayList;
27+
import java.util.Comparator;
28+
import java.util.Deque;
29+
import java.util.List;
30+
import java.util.Map;
31+
import java.util.Optional;
32+
import java.util.concurrent.ConcurrentHashMap;
33+
import java.util.concurrent.ConcurrentLinkedDeque;
34+
import java.util.function.BiFunction;
35+
36+
import static java.util.Collections.emptyList;
37+
import static java.util.Comparator.comparing;
38+
import static java.util.Comparator.nullsFirst;
39+
import static java.util.Objects.requireNonNull;
40+
import static java.util.Optional.ofNullable;
41+
import static java.util.stream.Collectors.toList;
42+
43+
/**
44+
* Given one Cucumber Message, find another.
45+
* <p>
46+
* This class is effectively a simple in memory database. It can be updated in
47+
* real time through the {@link #update(Envelope)} method. Queries can be made
48+
* while the test run is incomplete - and this will of-course return incomplete
49+
* results.
50+
* <p>
51+
* It is safe to query and update concurrently.
52+
*
53+
* @see <a href=https://github.com/cucumber/messages?tab=readme-ov-file#message-overview>Cucumber Messages - Message Overview</a>
54+
*/
55+
class Query {
56+
private final Comparator<TestStepResult> testStepResultComparator = nullsFirst(comparing(o -> o.getStatus().ordinal()));
57+
private final Deque<TestCaseStarted> testCaseStarted = new ConcurrentLinkedDeque<>();
58+
private final Map<String, TestCaseFinished> testCaseFinishedByTestCaseStartedId = new ConcurrentHashMap<>();
59+
private final Map<String, List<TestStepFinished>> testStepsFinishedByTestCaseStartedId = new ConcurrentHashMap<>();
60+
private final Map<String, Pickle> pickleById = new ConcurrentHashMap<>();
61+
private final Map<String, TestCase> testCaseById = new ConcurrentHashMap<>();
62+
private final Map<String, Step> stepById = new ConcurrentHashMap<>();
63+
private final Map<String, TestStep> testStepById = new ConcurrentHashMap<>();
64+
private final Map<String, PickleStep> pickleStepById = new ConcurrentHashMap<>();
65+
private final Map<String, GherkingAstNodes> gherkinAstNodesById = new ConcurrentHashMap<>();
66+
private TestRunStarted testRunStarted;
67+
private TestRunFinished testRunFinished;
68+
69+
public List<TestCaseStarted> findAllTestCaseStarted() {
70+
// Concurrency
71+
return new ArrayList<>(testCaseStarted);
72+
}
73+
74+
public Optional<GherkingAstNodes> findGherkinAstNodesBy(Pickle pickle) {
75+
requireNonNull(pickle);
76+
List<String> astNodeIds = pickle.getAstNodeIds();
77+
String pickleAstNodeId = astNodeIds.get(astNodeIds.size() - 1);
78+
return Optional.ofNullable(gherkinAstNodesById.get(pickleAstNodeId));
79+
}
80+
81+
public Optional<GherkingAstNodes> findGherkinAstNodesBy(TestCaseStarted testCaseStarted) {
82+
return findPickleBy(testCaseStarted)
83+
.flatMap(this::findGherkinAstNodesBy);
84+
}
85+
86+
public Optional<TestStepResult> findMostSevereTestStepResultStatusBy(TestCaseStarted testCaseStarted) {
87+
requireNonNull(testCaseStarted);
88+
return findTestStepsFinishedBy(testCaseStarted)
89+
.stream()
90+
.map(TestStepFinished::getTestStepResult)
91+
.max(testStepResultComparator);
92+
}
93+
94+
public Optional<Pickle> findPickleBy(TestCaseStarted testCaseStarted) {
95+
requireNonNull(testCaseStarted);
96+
return findTestCaseBy(testCaseStarted)
97+
.map(TestCase::getPickleId)
98+
.map(pickleById::get);
99+
}
100+
101+
public Optional<PickleStep> findPickleStepBy(TestStep testStep) {
102+
requireNonNull(testCaseStarted);
103+
return testStep.getPickleStepId()
104+
.map(pickleStepById::get);
105+
}
106+
107+
public Optional<Step> findStepBy(PickleStep pickleStep) {
108+
requireNonNull(pickleStep);
109+
String stepId = pickleStep.getAstNodeIds().get(0);
110+
return ofNullable(stepById.get(stepId));
111+
}
112+
113+
public Optional<TestCase> findTestCaseBy(TestCaseStarted testCaseStarted) {
114+
requireNonNull(testCaseStarted);
115+
return ofNullable(testCaseById.get(testCaseStarted.getTestCaseId()));
116+
}
117+
118+
public Optional<Duration> findTestCaseDurationBy(TestCaseStarted testCaseStarted) {
119+
requireNonNull(testCaseStarted);
120+
Timestamp started = testCaseStarted.getTimestamp();
121+
return findTestCaseFinishedBy(testCaseStarted)
122+
.map(TestCaseFinished::getTimestamp)
123+
.map(finished -> Duration.between(
124+
Convertor.toInstant(started),
125+
Convertor.toInstant(finished)
126+
));
127+
}
128+
129+
public Optional<TestCaseFinished> findTestCaseFinishedBy(TestCaseStarted testCaseStarted) {
130+
requireNonNull(testCaseStarted);
131+
return ofNullable(testCaseFinishedByTestCaseStartedId.get(testCaseStarted.getId()));
132+
}
133+
134+
public Optional<Duration> findTestRunDuration() {
135+
if (testRunStarted == null || testRunFinished == null) {
136+
return Optional.empty();
137+
}
138+
Duration between = Duration.between(
139+
Convertor.toInstant(testRunStarted.getTimestamp()),
140+
Convertor.toInstant(testRunFinished.getTimestamp())
141+
);
142+
return Optional.of(between);
143+
}
144+
145+
public Optional<TestRunFinished> findTestRunFinished() {
146+
return ofNullable(testRunFinished);
147+
}
148+
149+
public Optional<TestRunStarted> findTestRunStarted() {
150+
return ofNullable(testRunStarted);
151+
}
152+
153+
public List<SimpleEntry<TestStep, TestStepFinished>> findTestStepAndTestStepFinishedBy(TestCaseStarted testCaseStarted) {
154+
return findTestStepsFinishedBy(testCaseStarted).stream()
155+
.map(testStepFinished -> findTestStepBy(testStepFinished).map(testStep -> new SimpleEntry<>(testStep, testStepFinished)))
156+
.filter(Optional::isPresent)
157+
.map(Optional::get)
158+
.collect(toList());
159+
}
160+
161+
public Optional<TestStep> findTestStepBy(TestStepFinished testStepFinished) {
162+
requireNonNull(testStepFinished);
163+
return ofNullable(testStepById.get(testStepFinished.getTestStepId()));
164+
}
165+
166+
public List<TestStepFinished> findTestStepsFinishedBy(TestCaseStarted testCaseStarted) {
167+
requireNonNull(testCaseStarted);
168+
List<TestStepFinished> testStepsFinished = testStepsFinishedByTestCaseStartedId.
169+
getOrDefault(testCaseStarted.getId(), emptyList());
170+
// Concurrency
171+
return new ArrayList<>(testStepsFinished);
172+
}
173+
174+
public void update(Envelope envelope) {
175+
envelope.getTestRunStarted().ifPresent(this::updateTestRunStarted);
176+
envelope.getTestRunFinished().ifPresent(this::updateTestRunFinished);
177+
envelope.getTestCaseStarted().ifPresent(this::updateTestCaseStarted);
178+
envelope.getTestCaseFinished().ifPresent(this::updateTestCaseFinished);
179+
envelope.getTestStepFinished().ifPresent(this::updateTestStepFinished);
180+
envelope.getGherkinDocument().ifPresent(this::updateGherkinDocument);
181+
envelope.getPickle().ifPresent(this::updatePickle);
182+
envelope.getTestCase().ifPresent(this::updateTestCase);
183+
}
184+
185+
private void updateTestCaseStarted(TestCaseStarted testCaseStarted) {
186+
this.testCaseStarted.add(testCaseStarted);
187+
}
188+
189+
private void updateTestCase(TestCase event) {
190+
this.testCaseById.put(event.getId(), event);
191+
event.getTestSteps().forEach(testStep -> testStepById.put(testStep.getId(), testStep));
192+
}
193+
194+
private void updatePickle(Pickle event) {
195+
this.pickleById.put(event.getId(), event);
196+
event.getSteps().forEach(pickleStep -> pickleStepById.put(pickleStep.getId(), pickleStep));
197+
}
198+
199+
private void updateGherkinDocument(GherkinDocument gherkinDocument) {
200+
gherkinDocument.getFeature().ifPresent(this::updateFeature);
201+
}
202+
203+
private void updateFeature(Feature feature) {
204+
feature.getChildren()
205+
.forEach(featureChild -> {
206+
featureChild.getBackground().ifPresent(background -> updateSteps(background.getSteps()));
207+
featureChild.getScenario().ifPresent(scenario -> updateScenario(feature, null, scenario));
208+
featureChild.getRule().ifPresent(rule -> rule.getChildren().forEach(ruleChild -> {
209+
ruleChild.getBackground().ifPresent(background -> updateSteps(background.getSteps()));
210+
ruleChild.getScenario().ifPresent(scenario -> updateScenario(feature, rule, scenario));
211+
}));
212+
});
213+
}
214+
215+
private void updateSteps(List<Step> steps) {
216+
steps.forEach(step -> stepById.put(step.getId(), step));
217+
}
218+
219+
private void updateTestStepFinished(TestStepFinished event) {
220+
this.testStepsFinishedByTestCaseStartedId.compute(event.getTestCaseStartedId(), updateList(event));
221+
}
222+
223+
private void updateTestCaseFinished(TestCaseFinished event) {
224+
this.testCaseFinishedByTestCaseStartedId.put(event.getTestCaseStartedId(), event);
225+
}
226+
227+
private void updateTestRunFinished(TestRunFinished event) {
228+
this.testRunFinished = event;
229+
}
230+
231+
private void updateTestRunStarted(TestRunStarted event) {
232+
this.testRunStarted = event;
233+
}
234+
235+
private void updateScenario(Feature feature, Rule rule, Scenario scenario) {
236+
this.gherkinAstNodesById.put(scenario.getId(), new GherkingAstNodes(feature, rule, scenario));
237+
updateSteps(scenario.getSteps());
238+
239+
List<Examples> examples = scenario.getExamples();
240+
for (int examplesIndex = 0; examplesIndex < examples.size(); examplesIndex++) {
241+
Examples currentExamples = examples.get(examplesIndex);
242+
List<TableRow> tableRows = currentExamples.getTableBody();
243+
for (int exampleIndex = 0; exampleIndex < tableRows.size(); exampleIndex++) {
244+
TableRow currentExample = tableRows.get(exampleIndex);
245+
gherkinAstNodesById.put(currentExample.getId(), new GherkingAstNodes(feature, rule, scenario, examplesIndex, currentExamples, exampleIndex, currentExample));
246+
}
247+
}
248+
}
249+
250+
private <K, E> BiFunction<K, List<E>, List<E>> updateList(E element) {
251+
return (key, existing) -> {
252+
if (existing != null) {
253+
existing.add(element);
254+
return existing;
255+
}
256+
List<E> list = new ArrayList<>();
257+
list.add(element);
258+
return list;
259+
};
260+
}
261+
262+
}

0 commit comments

Comments
 (0)