Skip to content

Commit c943e4c

Browse files
committed
WIP: Use messages in PrettyFormatter
1 parent a1ab42c commit c943e4c

File tree

1 file changed

+146
-70
lines changed

1 file changed

+146
-70
lines changed

cucumber-core/src/main/java/io/cucumber/core/plugin/PrettyFormatter.java

Lines changed: 146 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,30 @@
77
import io.cucumber.datatable.DataTableFormatter;
88
import io.cucumber.docstring.DocString;
99
import io.cucumber.docstring.DocStringFormatter;
10+
import io.cucumber.messages.types.Envelope;
11+
import io.cucumber.messages.types.Exception;
12+
import io.cucumber.messages.types.Group;
13+
import io.cucumber.messages.types.Location;
14+
import io.cucumber.messages.types.Pickle;
15+
import io.cucumber.messages.types.PickleStep;
16+
import io.cucumber.messages.types.PickleTag;
17+
import io.cucumber.messages.types.Scenario;
18+
import io.cucumber.messages.types.Step;
19+
import io.cucumber.messages.types.StepDefinition;
20+
import io.cucumber.messages.types.StepMatchArgument;
21+
import io.cucumber.messages.types.StepMatchArgumentsList;
22+
import io.cucumber.messages.types.TestCaseStarted;
1023
import io.cucumber.plugin.ColorAware;
1124
import io.cucumber.plugin.ConcurrentEventListener;
12-
import io.cucumber.plugin.event.Argument;
1325
import io.cucumber.plugin.event.EmbedEvent;
1426
import io.cucumber.plugin.event.EventPublisher;
1527
import io.cucumber.plugin.event.PickleStepTestStep;
16-
import io.cucumber.plugin.event.Result;
1728
import io.cucumber.plugin.event.StepArgument;
18-
import io.cucumber.plugin.event.TestCase;
19-
import io.cucumber.plugin.event.TestCaseStarted;
2029
import io.cucumber.plugin.event.TestRunFinished;
2130
import io.cucumber.plugin.event.TestStepFinished;
2231
import io.cucumber.plugin.event.WriteEvent;
32+
import io.cucumber.query.Lineage;
33+
import io.cucumber.query.Query;
2334

2435
import java.io.BufferedReader;
2536
import java.io.File;
@@ -28,17 +39,19 @@
2839
import java.io.StringReader;
2940
import java.net.URI;
3041
import java.net.URISyntaxException;
31-
import java.util.Comparator;
42+
import java.util.Collection;
43+
import java.util.Collections;
3244
import java.util.HashMap;
3345
import java.util.List;
3446
import java.util.Map;
35-
import java.util.UUID;
47+
import java.util.Optional;
48+
import java.util.stream.Collectors;
3649

37-
import static io.cucumber.core.exception.ExceptionUtils.printStackTrace;
3850
import static io.cucumber.core.plugin.Formats.ansi;
3951
import static io.cucumber.core.plugin.Formats.monochrome;
4052
import static java.lang.Math.max;
4153
import static java.util.Locale.ROOT;
54+
import static java.util.stream.Collectors.joining;
4255

4356
/**
4457
* Prints a pretty report of the scenario execution as it happens.
@@ -53,33 +66,40 @@ public final class PrettyFormatter implements ConcurrentEventListener, ColorAwar
5366
private static final String STEP_SCENARIO_INDENT = STEP_INDENT + " ";
5467
private static final String STACK_TRACE_INDENT = STEP_SCENARIO_INDENT + " ";
5568

56-
private final Map<UUID, Integer> commentStartIndex = new HashMap<>();
69+
private final Map<String, Integer> commentStartIndexByTestCaseId = new HashMap<>();
5770

5871
private final UTF8PrintWriter out;
5972
private Formats formats = ansi();
73+
private final Query query = new Query();
74+
private final Map<String, StepDefinition> stepDefinitionsById = new HashMap<>();
6075

6176
public PrettyFormatter(OutputStream out) {
6277
this.out = new UTF8PrintWriter(out);
6378
}
6479

6580
@Override
6681
public void setEventPublisher(EventPublisher publisher) {
67-
publisher.registerHandlerFor(TestCaseStarted.class, this::handleTestCaseStarted);
68-
publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
82+
publisher.registerHandlerFor(Envelope.class, event -> {
83+
query.update(event);
84+
event.getStepDefinition().ifPresent(stepDefinition -> stepDefinitionsById.put(stepDefinition.getId(), stepDefinition));
85+
event.getTestCaseStarted().ifPresent(this::handleTestCaseStarted);
86+
event.getTestStepFinished().ifPresent(this::handleTestStepFinished);
87+
});
88+
6989
publisher.registerHandlerFor(WriteEvent.class, this::handleWrite);
7090
publisher.registerHandlerFor(EmbedEvent.class, this::handleEmbed);
7191
publisher.registerHandlerFor(TestRunFinished.class, this::handleTestRunFinished);
7292
}
7393

74-
private void handleTestCaseStarted(TestCaseStarted event) {
94+
private void handleTestCaseStarted(io.cucumber.messages.types.TestCaseStarted event) {
7595
out.println();
7696
preCalculateLocationIndent(event);
7797
printTags(event);
7898
printScenarioDefinition(event);
7999
out.flush();
80100
}
81101

82-
private void handleTestStepFinished(TestStepFinished event) {
102+
private void handleTestStepFinished(io.cucumber.messages.types.TestStepFinished event) {
83103
printStep(event);
84104
printError(event);
85105
out.flush();
@@ -99,49 +119,104 @@ private void handleEmbed(EmbedEvent event) {
99119
out.flush();
100120
}
101121

102-
private void handleTestRunFinished(TestRunFinished event) {
122+
private void handleTestRunFinished(io.cucumber.messages.types.TestRunFinished event) {
103123
printError(event);
104124
out.close();
105125
}
106126

107-
private void preCalculateLocationIndent(TestCaseStarted event) {
108-
TestCase testCase = event.getTestCase();
109-
Integer longestStep = testCase.getTestSteps().stream()
110-
.filter(PickleStepTestStep.class::isInstance)
111-
.map(PickleStepTestStep.class::cast)
112-
.map(PickleStepTestStep::getStep)
113-
.map(step -> formatPlainStep(step.getKeyword(), step.getText()).length())
114-
.max(Comparator.naturalOrder())
115-
.orElse(0);
116-
117-
int scenarioLength = formatScenarioDefinition(testCase).length();
118-
commentStartIndex.put(testCase.getId(), max(longestStep, scenarioLength) + 1);
127+
private void preCalculateLocationIndent(io.cucumber.messages.types.TestCaseStarted event) {
128+
query.findLineageBy(event)
129+
.flatMap(Lineage::scenario)
130+
.ifPresent(scenario -> {
131+
query.findPickleBy(event).ifPresent(pickle -> {
132+
int longestLine = calculateScenarioLineLength(pickle, scenario);
133+
134+
List<Step> steps = scenario.getSteps();
135+
List<PickleStep> pickleSteps = pickle.getSteps();
136+
137+
int stepSize = pickleSteps.size();
138+
for (int i = 0; i < stepSize; i++) {
139+
Step step = steps.get(i);
140+
PickleStep pickleStep = pickleSteps.get(i);
141+
longestLine = Math.max(longestLine, calculateStepLineLength(step, pickleStep));
142+
}
143+
144+
commentStartIndexByTestCaseId.put(event.getTestCaseId(), longestLine + 1);
145+
});
146+
});
119147
}
120148

121-
private void printTags(TestCaseStarted event) {
122-
List<String> tags = event.getTestCase().getTags();
123-
if (!tags.isEmpty()) {
124-
out.println(SCENARIO_INDENT + String.join(" ", tags));
125-
}
149+
private static int calculateStepLineLength(Step step, PickleStep pickleStep) {
150+
String keyword = step.getKeyword();
151+
String text = pickleStep.getText();
152+
// The ": " add 2
153+
return STEP_INDENT.length() + keyword.length() + text.length() + 2;
154+
}
155+
156+
private static int calculateScenarioLineLength(Pickle pickle, Scenario scenario) {
157+
String pickleName = pickle.getName();
158+
String pickleKeyword = scenario.getKeyword();
159+
// The ": " add 2
160+
return SCENARIO_INDENT.length() + pickleName.length() + pickleKeyword.length() + 2;
161+
}
162+
163+
private void printTags(io.cucumber.messages.types.TestCaseStarted event) {
164+
query.findPickleBy(event)
165+
.map(Pickle::getTags)
166+
.filter(pickleTags -> !pickleTags.isEmpty())
167+
.map(pickleTags -> pickleTags.stream()
168+
.map(PickleTag::getName)
169+
.collect(joining(" ")))
170+
.ifPresent(tags -> out.println(SCENARIO_INDENT + tags));
126171
}
127172

128173
private void printScenarioDefinition(TestCaseStarted event) {
129-
TestCase testCase = event.getTestCase();
130-
String definitionText = formatScenarioDefinition(testCase);
131-
String path = relativize(testCase.getUri()).getSchemeSpecificPart();
132-
String locationIndent = calculateLocationIndent(event.getTestCase(), SCENARIO_INDENT + definitionText);
133-
out.println(SCENARIO_INDENT + definitionText + locationIndent
134-
+ formatLocation(path + ":" + testCase.getLocation().getLine()));
174+
query.findLineageBy(event)
175+
.flatMap(Lineage::scenario)
176+
.ifPresent(scenario -> {
177+
query.findPickleBy(event)
178+
.ifPresent(pickle -> {
179+
String definitionText = formatScenarioDefinition(scenario, pickle);
180+
String path = relativize(pickle.getUri()).getSchemeSpecificPart();
181+
String locationIndent = calculateLocationIndent(event.getTestCaseId(), definitionText);
182+
String pathWithLine = query.findLocationOf(pickle).map(Location::getLine).map(line -> path + ":" + line).orElse(path);
183+
out.println(definitionText + locationIndent + formatLocation(path + ":" + pathWithLine));
184+
});
185+
});
186+
}
187+
188+
private static String formatScenarioDefinition(Scenario scenario, Pickle pickle) {
189+
return SCENARIO_INDENT + scenario.getKeyword() + ": " + pickle.getName();
135190
}
136191

137-
private void printStep(TestStepFinished event) {
192+
private void printStep(io.cucumber.messages.types.TestStepFinished event) {
193+
query.findTestStepBy(event)
194+
.ifPresent(testStep -> {
195+
query.findPickleStepBy(testStep).ifPresent(pickleStep -> {
196+
query.findStepBy(pickleStep).ifPresent(step -> {
197+
String keyword = step.getKeyword();
198+
String stepText = pickleStep.getText();
199+
// TODO: Use proper enum map.
200+
String status = event.getTestStepResult().getStatus().toString().toLowerCase(ROOT);
201+
List<StepMatchArgument> stepMatchArgumentsLists = testStep.getStepMatchArgumentsLists()
202+
.map(stepMatchArgumentsLists1 -> stepMatchArgumentsLists1.stream().map(StepMatchArgumentsList::getStepMatchArguments).flatMap(Collection::stream).collect(Collectors.toList()))
203+
.orElseGet(Collections::emptyList);// TODO: Create separate _arg map
204+
205+
String formattedStepText = STEP_INDENT + formatStepText(keyword, stepText, formats.get(status), formats.get(status + "_arg"), stepMatchArgumentsLists);
206+
String locationComment = formatLocationComment(event, testStep, keyword, stepText);
207+
out.println(formattedStepText + locationComment);
208+
});
209+
});
210+
})
211+
212+
138213
if (event.getTestStep() instanceof PickleStepTestStep) {
139214
PickleStepTestStep testStep = (PickleStepTestStep) event.getTestStep();
140215
String keyword = testStep.getStep().getKeyword();
141216
String stepText = testStep.getStep().getText();
142217
String status = event.getResult().getStatus().name().toLowerCase(ROOT);
143218
String formattedStepText = formatStepText(keyword, stepText, formats.get(status),
144-
formats.get(status + "_arg"), testStep.getDefinitionArgument());
219+
formats.get(status + "_arg"), testStep.getDefinitionArgument());
145220
String locationComment = formatLocationComment(event, testStep, keyword, stepText);
146221
out.println(STEP_INDENT + formattedStepText + locationComment);
147222
StepArgument stepArgument = testStep.getStep().getArgument();
@@ -165,7 +240,7 @@ private void printStep(TestStepFinished event) {
165240
.build();
166241
DocStringArgument docStringArgument = (DocStringArgument) stepArgument;
167242
DocString docString = DocString.create(docStringArgument.getContent(),
168-
docStringArgument.getContentType());
243+
docStringArgument.getContentType());
169244
try {
170245
docStringFormatter.formatTo(docString, out);
171246
} catch (IOException e) {
@@ -186,26 +261,25 @@ private String formatLocationComment(
186261
return locationIndent + formatLocation(codeLocation);
187262
}
188263

189-
private void printError(TestStepFinished event) {
190-
Result result = event.getResult();
191-
printError(STACK_TRACE_INDENT, result);
264+
private void printError(io.cucumber.messages.types.TestStepFinished event) {
265+
event.getTestStepResult()
266+
.getException()
267+
.ifPresent(exception -> {
268+
String name = event.getTestStepResult().getStatus().name().toLowerCase(ROOT);
269+
printError(STACK_TRACE_INDENT, exception, formats.get(name));
270+
});
192271
}
193272

194-
private void printError(TestRunFinished event) {
195-
Result result = event.getResult();
196-
printError(SCENARIO_INDENT, result);
273+
private void printError(io.cucumber.messages.types.TestRunFinished event) {
274+
event.getException()
275+
.ifPresent(exception -> printError(SCENARIO_INDENT, exception, formats.get("failed")));
197276
}
198277

199-
private void printError(String prefix, Result result) {
200-
Throwable error = result.getError();
201-
if (error != null) {
202-
String name = result.getStatus().name().toLowerCase(ROOT);
203-
Format format = formats.get(name);
204-
String text = printStackTrace(error);
205-
// TODO: Java 12+ use String.indent
206-
String indented = text.replaceAll("(\r\n|\r|\n)", "$1" + prefix).trim();
207-
out.println(prefix + format.text(indented));
208-
}
278+
private void printError(String scenarioIndent, Exception exception, Format format) {
279+
String text = exception.getStackTrace().orElse("");
280+
// TODO: Java 12+ use String.indent
281+
String indented = text.replaceAll("(\r\n|\r|\n)", "$1" + scenarioIndent).trim();
282+
out.println(scenarioIndent + format.text(indented));
209283
}
210284

211285
private void printText(WriteEvent event) {
@@ -235,10 +309,14 @@ private String formatPlainStep(String keyword, String stepText) {
235309
return STEP_INDENT + keyword + stepText;
236310
}
237311

238-
private String formatScenarioDefinition(TestCase testCase) {
312+
private String formatScenarioDefinition(io.cucumber.messages.types.TestCase testCase) {
239313
return testCase.getKeyword() + ": " + testCase.getName();
240314
}
241315

316+
static URI relativize(String uri) {
317+
return relativize(URI.create(uri));
318+
}
319+
242320
static URI relativize(URI uri) {
243321
if (!"file".equals(uri.getScheme())) {
244322
return uri;
@@ -257,8 +335,8 @@ static URI relativize(URI uri) {
257335
}
258336
}
259337

260-
private String calculateLocationIndent(TestCase testStep, String prefix) {
261-
Integer commentStartAt = commentStartIndex.getOrDefault(testStep.getId(), 0);
338+
private String calculateLocationIndent(String testCaseId, String prefix) {
339+
Integer commentStartAt = commentStartIndexByTestCaseId.getOrDefault(testCaseId, 0);
262340
int padding = commentStartAt - prefix.length();
263341

264342
if (padding < 0) {
@@ -276,29 +354,27 @@ private String formatLocation(String location) {
276354
}
277355

278356
String formatStepText(
279-
String keyword, String stepText, Format textFormat, Format argFormat, List<Argument> arguments
357+
String keyword, String stepText, Format textFormat, Format argFormat, List<StepMatchArgument> arguments
280358
) {
281359
int beginIndex = 0;
282360
StringBuilder result = new StringBuilder(textFormat.text(keyword));
283-
for (Argument argument : arguments) {
361+
for (StepMatchArgument argument : arguments) {
284362
// can be null if the argument is missing.
285-
if (argument.getValue() != null) {
286-
int argumentOffset = argument.getStart();
363+
Group group = argument.getGroup();
364+
Optional<String> value = group.getValue();
365+
if (value.isPresent()) {
366+
// TODO: Messages are silly
367+
int argumentOffset = (int) (long) group.getStart().orElse(-1L);
287368
// a nested argument starts before the enclosing argument ends;
288369
// ignore it when formatting
289370
if (argumentOffset < beginIndex) {
290371
continue;
291372
}
292373
String text = stepText.substring(beginIndex, argumentOffset);
293374
result.append(textFormat.text(text));
294-
}
295-
// val can be null if the argument isn't there, for example
296-
// @And("(it )?has something")
297-
if (argument.getValue() != null) {
298-
String text = stepText.substring(argument.getStart(), argument.getEnd());
299-
result.append(argFormat.text(text));
300-
// set beginIndex to end of argument
301-
beginIndex = argument.getEnd();
375+
int argumentEndIndex = argumentOffset + value.get().length();
376+
result.append(argFormat.text(stepText.substring(argumentOffset, argumentEndIndex)));
377+
beginIndex = argumentEndIndex;
302378
}
303379
}
304380
if (beginIndex != stepText.length()) {

0 commit comments

Comments
 (0)