Skip to content

Commit abc5d96

Browse files
committed
[Core] Message based progress and rerun formatter
1 parent 5d6be05 commit abc5d96

File tree

5 files changed

+235
-163
lines changed

5 files changed

+235
-163
lines changed

cucumber-bom/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
<junit-xml-formatter.version>0.8.0</junit-xml-formatter.version>
2121
<messages.version>28.0.0</messages.version>
2222
<pretty-formatter.version>0.3.0</pretty-formatter.version>
23-
<query.version>13.5.0</query.version>
23+
<query.version>13.5.1-SNAPSHOT</query.version>
2424
<tag-expressions.version>6.1.2</tag-expressions.version>
2525
<testng-xml-formatter.version>0.4.0</testng-xml-formatter.version>
2626
</properties>
Lines changed: 73 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,64 @@
11
package io.cucumber.core.plugin;
22

3+
import io.cucumber.messages.types.Envelope;
4+
import io.cucumber.messages.types.TestCase;
5+
import io.cucumber.messages.types.TestRunFinished;
6+
import io.cucumber.messages.types.TestStep;
7+
import io.cucumber.messages.types.TestStepFinished;
8+
import io.cucumber.messages.types.TestStepResultStatus;
39
import io.cucumber.plugin.ColorAware;
410
import io.cucumber.plugin.ConcurrentEventListener;
511
import io.cucumber.plugin.event.EventPublisher;
6-
import io.cucumber.plugin.event.PickleStepTestStep;
7-
import io.cucumber.plugin.event.Status;
8-
import io.cucumber.plugin.event.TestRunFinished;
9-
import io.cucumber.plugin.event.TestStepFinished;
1012

1113
import java.io.OutputStream;
14+
import java.io.OutputStreamWriter;
15+
import java.io.PrintWriter;
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.EnumMap;
1218
import java.util.HashMap;
1319
import java.util.Map;
1420

15-
public final class ProgressFormatter implements ConcurrentEventListener, ColorAware {
21+
import static io.cucumber.messages.types.TestStepResultStatus.AMBIGUOUS;
22+
import static io.cucumber.messages.types.TestStepResultStatus.FAILED;
23+
import static io.cucumber.messages.types.TestStepResultStatus.PASSED;
24+
import static io.cucumber.messages.types.TestStepResultStatus.PENDING;
25+
import static io.cucumber.messages.types.TestStepResultStatus.SKIPPED;
26+
import static io.cucumber.messages.types.TestStepResultStatus.UNDEFINED;
27+
import static java.util.Objects.requireNonNull;
1628

17-
private static final Map<Status, Character> CHARS = new HashMap<Status, Character>() {
18-
{
19-
put(Status.PASSED, '.');
20-
put(Status.UNDEFINED, 'U');
21-
put(Status.PENDING, 'P');
22-
put(Status.SKIPPED, '-');
23-
put(Status.FAILED, 'F');
24-
put(Status.AMBIGUOUS, 'A');
25-
}
26-
};
27-
private static final Map<Status, AnsiEscapes> ANSI_ESCAPES = new HashMap<Status, AnsiEscapes>() {
28-
{
29-
put(Status.PASSED, AnsiEscapes.GREEN);
30-
put(Status.UNDEFINED, AnsiEscapes.YELLOW);
31-
put(Status.PENDING, AnsiEscapes.YELLOW);
32-
put(Status.SKIPPED, AnsiEscapes.CYAN);
33-
put(Status.FAILED, AnsiEscapes.RED);
34-
put(Status.AMBIGUOUS, AnsiEscapes.RED);
35-
}
36-
};
29+
public final class ProgressFormatter implements ConcurrentEventListener, ColorAware {
3730

38-
private final UTF8PrintWriter out;
31+
private final Map<String, TestStep> testStepById = new HashMap<>();
32+
private final Map<TestStepResultStatus, String> chars = new EnumMap<>(TestStepResultStatus.class);
33+
private final Map<TestStepResultStatus, AnsiEscapes> escapes = new EnumMap<>(TestStepResultStatus.class);
34+
private final PrintWriter writer;
3935
private boolean monochrome = false;
40-
36+
4137
public ProgressFormatter(OutputStream out) {
42-
this.out = new UTF8PrintWriter(out);
38+
this.writer = createPrintWriter(out);
39+
40+
chars.put(PASSED, ".");
41+
chars.put(UNDEFINED, "U");
42+
chars.put(PENDING, "P");
43+
chars.put(SKIPPED, "-");
44+
chars.put(FAILED, "F");
45+
chars.put(AMBIGUOUS, "A");
46+
47+
escapes.put(PASSED, AnsiEscapes.GREEN);
48+
escapes.put(UNDEFINED, AnsiEscapes.YELLOW);
49+
escapes.put(PENDING, AnsiEscapes.YELLOW);
50+
escapes.put(SKIPPED, AnsiEscapes.CYAN);
51+
escapes.put(FAILED, AnsiEscapes.RED);
52+
escapes.put(AMBIGUOUS, AnsiEscapes.RED);
53+
}
54+
55+
private static PrintWriter createPrintWriter(OutputStream out) {
56+
return new PrintWriter(
57+
new OutputStreamWriter(
58+
requireNonNull(out),
59+
StandardCharsets.UTF_8
60+
)
61+
);
4362
}
4463

4564
@Override
@@ -49,32 +68,47 @@ public void setMonochrome(boolean monochrome) {
4968

5069
@Override
5170
public void setEventPublisher(EventPublisher publisher) {
52-
publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
53-
publisher.registerHandlerFor(TestRunFinished.class, this::handleTestRunFinished);
71+
publisher.registerHandlerFor(Envelope.class, event -> {
72+
event.getTestCase().ifPresent(this::updateTestCase);
73+
event.getTestStepFinished().ifPresent(this::handleTestStepFinished);
74+
event.getTestRunFinished().ifPresent(this::handleTestRunFinished);
75+
});
76+
}
77+
78+
private void updateTestCase(TestCase event) {
79+
event.getTestSteps().forEach(testStep -> testStepById.put(testStep.getId(), testStep));
5480
}
5581

5682
private void handleTestStepFinished(TestStepFinished event) {
57-
boolean isTestStep = event.getTestStep() instanceof PickleStepTestStep;
58-
boolean isFailedHookOrTestStep = event.getResult().getStatus().is(Status.FAILED);
59-
if (!(isTestStep || isFailedHookOrTestStep)) {
83+
if (!includeStep(event)) {
6084
return;
6185
}
86+
TestStepResultStatus status = event.getTestStepResult().getStatus();
6287
// Prevent tearing in output when multiple threads write to System.out
6388
StringBuilder buffer = new StringBuilder();
6489
if (!monochrome) {
65-
ANSI_ESCAPES.get(event.getResult().getStatus()).appendTo(buffer);
90+
escapes.get(status).appendTo(buffer);
6691
}
67-
buffer.append(CHARS.get(event.getResult().getStatus()));
92+
buffer.append(chars.get(status));
6893
if (!monochrome) {
6994
AnsiEscapes.RESET.appendTo(buffer);
7095
}
71-
out.append(buffer);
72-
out.flush();
96+
writer.append(buffer);
97+
writer.flush();
98+
}
99+
100+
private boolean includeStep(TestStepFinished testStepFinished) {
101+
TestStepResultStatus status = testStepFinished.getTestStepResult().getStatus();
102+
if (status == FAILED) {
103+
return true;
104+
}
105+
TestStep testStep = testStepById.get(testStepFinished.getTestStepId());
106+
return testStep != null && testStep.getPickleStepId().isPresent();
73107
}
74108

75109
private void handleTestRunFinished(TestRunFinished testRunFinished) {
76-
out.println();
77-
out.close();
110+
writer.println();
111+
writer.close();
78112
}
79113

80114
}
Lines changed: 64 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,50 @@
11
package io.cucumber.core.plugin;
22

33
import io.cucumber.core.feature.FeatureWithLines;
4+
import io.cucumber.messages.types.Envelope;
5+
import io.cucumber.messages.types.TestCaseFinished;
6+
import io.cucumber.messages.types.TestStepResult;
7+
import io.cucumber.messages.types.TestStepResultStatus;
48
import io.cucumber.plugin.ConcurrentEventListener;
59
import io.cucumber.plugin.event.EventPublisher;
6-
import io.cucumber.plugin.event.TestCase;
7-
import io.cucumber.plugin.event.TestCaseFinished;
8-
import io.cucumber.plugin.event.TestRunFinished;
10+
import io.cucumber.query.Query;
911

1012
import java.io.File;
1113
import java.io.OutputStream;
14+
import java.io.OutputStreamWriter;
15+
import java.io.PrintWriter;
1216
import java.net.URI;
1317
import java.net.URISyntaxException;
14-
import java.util.ArrayList;
15-
import java.util.Collection;
18+
import java.nio.charset.StandardCharsets;
19+
import java.util.HashSet;
1620
import java.util.LinkedHashMap;
1721
import java.util.Map;
22+
import java.util.Set;
1823

1924
import static io.cucumber.core.feature.FeatureWithLines.create;
25+
import static java.util.Objects.requireNonNull;
2026

2127
/**
2228
* Formatter for reporting all failed test cases and print their locations
2329
* Failed means: results that make the exit code non-zero.
2430
*/
2531
public final class RerunFormatter implements ConcurrentEventListener {
2632

27-
private final UTF8PrintWriter out;
28-
private final Map<URI, Collection<Integer>> featureAndFailedLinesMapping = new LinkedHashMap<>();
33+
private final PrintWriter writer;
34+
private final Map<String, Set<Integer>> featureAndFailedLinesMapping = new LinkedHashMap<>();
35+
private final Query query = new Query();
2936

3037
public RerunFormatter(OutputStream out) {
31-
this.out = new UTF8PrintWriter(out);
38+
this.writer = createPrintWriter(out);
3239
}
3340

34-
@Override
35-
public void setEventPublisher(EventPublisher publisher) {
36-
publisher.registerHandlerFor(TestCaseFinished.class, this::handleTestCaseFinished);
37-
publisher.registerHandlerFor(TestRunFinished.class, event -> finishReport());
38-
}
39-
40-
private void handleTestCaseFinished(TestCaseFinished event) {
41-
if (!event.getResult().getStatus().isOk()) {
42-
recordTestFailed(event.getTestCase());
43-
}
44-
}
45-
46-
private void finishReport() {
47-
for (Map.Entry<URI, Collection<Integer>> entry : featureAndFailedLinesMapping.entrySet()) {
48-
FeatureWithLines featureWithLines = create(relativize(entry.getKey()), entry.getValue());
49-
out.println(featureWithLines.toString());
50-
}
51-
52-
out.close();
53-
}
54-
55-
private void recordTestFailed(TestCase testCase) {
56-
URI uri = testCase.getUri();
57-
Collection<Integer> failedTestCaseLines = getFailedTestCaseLines(uri);
58-
failedTestCaseLines.add(testCase.getLocation().getLine());
59-
}
60-
61-
private Collection<Integer> getFailedTestCaseLines(URI uri) {
62-
return featureAndFailedLinesMapping.computeIfAbsent(uri, k -> new ArrayList<>());
41+
private static PrintWriter createPrintWriter(OutputStream out) {
42+
return new PrintWriter(
43+
new OutputStreamWriter(
44+
requireNonNull(out),
45+
StandardCharsets.UTF_8
46+
)
47+
);
6348
}
6449

6550
static URI relativize(URI uri) {
@@ -79,4 +64,46 @@ static URI relativize(URI uri) {
7964
throw new IllegalArgumentException(e.getMessage(), e);
8065
}
8166
}
67+
68+
@Override
69+
public void setEventPublisher(EventPublisher publisher) {
70+
publisher.registerHandlerFor(Envelope.class, event -> {
71+
query.update(event);
72+
event.getTestCaseFinished().ifPresent(this::handleTestCaseFinished);
73+
event.getTestRunFinished().ifPresent(testRunFinished -> finishReport());
74+
});
75+
}
76+
77+
private void handleTestCaseFinished(TestCaseFinished event) {
78+
TestStepResultStatus testStepResultStatus = query.findMostSevereTestStepResultBy(event)
79+
.map(TestStepResult::getStatus)
80+
// By definition
81+
.orElse(TestStepResultStatus.PASSED);
82+
83+
if (testStepResultStatus == TestStepResultStatus.PASSED
84+
|| testStepResultStatus == TestStepResultStatus.SKIPPED) {
85+
return;
86+
}
87+
88+
query.findPickleBy(event).ifPresent(pickle -> {
89+
query.findLocationOf(pickle).ifPresent(location -> {
90+
Set<Integer> lines = featureAndFailedLinesMapping.computeIfAbsent(pickle.getUri(),
91+
s -> new HashSet<>());
92+
// TODO: Messages are silly
93+
lines.add((int) (long) location.getLine());
94+
});
95+
});
96+
}
97+
98+
private void finishReport() {
99+
for (Map.Entry<String, Set<Integer>> entry : featureAndFailedLinesMapping.entrySet()) {
100+
String key = entry.getKey();
101+
// TODO: Should these be relative?
102+
FeatureWithLines featureWithLines = create(relativize(URI.create(key)), entry.getValue());
103+
writer.println(featureWithLines.toString());
104+
}
105+
106+
writer.close();
107+
}
108+
82109
}

cucumber-core/src/test/java/io/cucumber/core/plugin/PluginFactoryTest.java

Lines changed: 0 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,29 @@
11
package io.cucumber.core.plugin;
22

3-
import io.cucumber.core.eventbus.EventBus;
43
import io.cucumber.core.exception.CucumberException;
54
import io.cucumber.core.options.PluginOption;
6-
import io.cucumber.core.runner.ClockStub;
7-
import io.cucumber.core.runtime.TimeServiceEventBus;
85
import io.cucumber.messages.types.Envelope;
96
import io.cucumber.plugin.ConcurrentEventListener;
107
import io.cucumber.plugin.EventListener;
118
import io.cucumber.plugin.event.EventHandler;
129
import io.cucumber.plugin.event.EventPublisher;
13-
import io.cucumber.plugin.event.PickleStepTestStep;
1410
import io.cucumber.plugin.event.Result;
1511
import io.cucumber.plugin.event.Status;
16-
import io.cucumber.plugin.event.TestCase;
1712
import io.cucumber.plugin.event.TestRunFinished;
1813
import io.cucumber.plugin.event.TestRunStarted;
19-
import io.cucumber.plugin.event.TestStepFinished;
2014
import org.junit.jupiter.api.AfterEach;
2115
import org.junit.jupiter.api.Test;
2216
import org.junit.jupiter.api.function.Executable;
2317
import org.junit.jupiter.api.io.TempDir;
2418

25-
import java.io.ByteArrayOutputStream;
2619
import java.io.Closeable;
2720
import java.io.File;
2821
import java.io.IOException;
2922
import java.io.OutputStream;
30-
import java.io.PrintStream;
3123
import java.net.URL;
3224
import java.nio.file.Files;
3325
import java.nio.file.Path;
3426
import java.util.Objects;
35-
import java.util.UUID;
3627

3728
import static io.cucumber.core.options.TestPluginOption.parse;
3829
import static io.cucumber.messages.Convertor.toMessage;
@@ -48,7 +39,6 @@
4839
import static org.junit.jupiter.api.Assertions.assertAll;
4940
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
5041
import static org.junit.jupiter.api.Assertions.assertThrows;
51-
import static org.mockito.Mockito.mock;
5242

5343
class PluginFactoryTest {
5444

@@ -179,33 +169,6 @@ void instantiates_usage_plugin_with_file_arg() {
179169
assertThat(plugin.getClass(), is(equalTo(UsageFormatter.class)));
180170
}
181171

182-
@Test
183-
void plugin_does_not_buffer_its_output() {
184-
PrintStream previousSystemOut = System.out;
185-
OutputStream mockSystemOut = new ByteArrayOutputStream();
186-
187-
try {
188-
System.setOut(new PrintStream(mockSystemOut));
189-
190-
// Need to create a new plugin factory here since we need it to pick
191-
// up the new value of System.out
192-
fc = new PluginFactory();
193-
194-
PluginOption option = parse("progress");
195-
ProgressFormatter plugin = (ProgressFormatter) fc.create(option);
196-
EventBus bus = new TimeServiceEventBus(new ClockStub(ZERO), UUID::randomUUID);
197-
plugin.setEventPublisher(bus);
198-
Result result = new Result(Status.PASSED, ZERO, null);
199-
TestStepFinished event = new TestStepFinished(bus.getInstant(), mock(TestCase.class),
200-
mock(PickleStepTestStep.class), result);
201-
bus.send(event);
202-
203-
assertThat(mockSystemOut.toString(), is(not(equalTo(""))));
204-
} finally {
205-
System.setOut(previousSystemOut);
206-
}
207-
}
208-
209172
@Test
210173
void instantiates_single_custom_appendable_plugin_with_stdout() {
211174
PluginOption option = parse(WantsOutputStream.class.getName());

0 commit comments

Comments
 (0)