Skip to content

Commit 4605c55

Browse files
scottadavScott Davismpkorstanje
authored
Only flush output frequently for ProgressFormatter (#2541)
Co-authored-by: Scott Davis <[email protected]> Co-authored-by: M.P. Korstanje <[email protected]>
1 parent 549fe86 commit 4605c55

File tree

7 files changed

+174
-13
lines changed

7 files changed

+174
-13
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
1616
### Removed
1717

1818
### Fixed
19+
* [Core] Pretty print plugin performance issues; incorrect DataTable format in Gradle console ([#2541](https://github.com/cucumber/cucumber-jvm/pull/2541) Scott Davis)
1920

2021
## [7.3.2] (2022-04-22)
2122

core/src/main/java/io/cucumber/core/plugin/NiceAppendable.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,15 @@ final class NiceAppendable implements Appendable {
1111

1212
private static final CharSequence NL = "\n";
1313
private final Appendable out;
14+
private final boolean flushEveryWrite;
1415

1516
public NiceAppendable(Appendable out) {
17+
this(out, false);
18+
}
19+
20+
public NiceAppendable(Appendable out, boolean flushEveryWrite) {
1621
this.out = out;
22+
this.flushEveryWrite = flushEveryWrite;
1723
}
1824

1925
public NiceAppendable println() {
@@ -55,6 +61,10 @@ private void tryFlush() {
5561
return;
5662
}
5763

64+
if (!flushEveryWrite) {
65+
return;
66+
}
67+
5868
try {
5969
((Flushable) out).flush();
6070
} catch (IOException e) {

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -181,14 +181,17 @@ private void printError(Result result) {
181181
}
182182

183183
private void printText(WriteEvent event) {
184+
// Prevent interleaving when multiple threads write to System.out
185+
StringBuilder builder = new StringBuilder();
184186
try (BufferedReader lines = new BufferedReader(new StringReader(event.getText()))) {
185187
String line;
186188
while ((line = lines.readLine()) != null) {
187-
out.println(STEP_SCENARIO_INDENT + line);
189+
builder.append(String.format(STEP_SCENARIO_INDENT + line + "%n"));
188190
}
189191
} catch (IOException e) {
190192
throw new CucumberException(e);
191193
}
194+
out.append(builder);
192195
}
193196

194197
private void printEmbedding(EmbedEvent event) {
@@ -230,7 +233,6 @@ private String calculateLocationIndent(TestCase testStep, String prefix) {
230233
if (padding < 0) {
231234
return " ";
232235
}
233-
234236
StringBuilder builder = new StringBuilder(padding);
235237
for (int i = 0; i < padding; i++) {
236238
builder.append(" ");

core/src/main/java/io/cucumber/core/plugin/ProgressFormatter.java

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ public final class ProgressFormatter implements ConcurrentEventListener, ColorAw
3939
private boolean monochrome = false;
4040

4141
public ProgressFormatter(OutputStream out) {
42-
this.out = new NiceAppendable(new UTF8OutputStreamWriter(out));
42+
// Configure the NiceAppendable to flush on every append, since the
43+
// point of this formatter is to display a progress bar.
44+
this.out = new NiceAppendable(new UTF8OutputStreamWriter(out), true);
4345
}
4446

4547
@Override
@@ -50,22 +52,28 @@ public void setMonochrome(boolean monochrome) {
5052
@Override
5153
public void setEventPublisher(EventPublisher publisher) {
5254
publisher.registerHandlerFor(TestStepFinished.class, this::handleTestStepFinished);
53-
publisher.registerHandlerFor(TestRunFinished.class, event -> handleTestRunFinished());
55+
publisher.registerHandlerFor(TestRunFinished.class, this::handleTestRunFinished);
5456
}
5557

5658
private void handleTestStepFinished(TestStepFinished event) {
57-
if (event.getTestStep() instanceof PickleStepTestStep || event.getResult().getStatus().is(Status.FAILED)) {
58-
if (!monochrome) {
59-
ANSI_ESCAPES.get(event.getResult().getStatus()).appendTo(out);
60-
}
61-
out.append(CHARS.get(event.getResult().getStatus()));
62-
if (!monochrome) {
63-
AnsiEscapes.RESET.appendTo(out);
64-
}
59+
boolean isTestStep = event.getTestStep() instanceof PickleStepTestStep;
60+
boolean isFailedHookOrTestStep = event.getResult().getStatus().is(Status.FAILED);
61+
if (!(isTestStep || isFailedHookOrTestStep)) {
62+
return;
6563
}
64+
// Prevent tearing in output when multiple threads write to System.out
65+
StringBuilder buffer = new StringBuilder();
66+
if (!monochrome) {
67+
ANSI_ESCAPES.get(event.getResult().getStatus()).appendTo(buffer);
68+
}
69+
buffer.append(CHARS.get(event.getResult().getStatus()));
70+
if (!monochrome) {
71+
AnsiEscapes.RESET.appendTo(buffer);
72+
}
73+
out.append(buffer);
6674
}
6775

68-
private void handleTestRunFinished() {
76+
private void handleTestRunFinished(TestRunFinished testRunFinished) {
6977
out.println();
7078
out.close();
7179
}

core/src/main/java/io/cucumber/core/plugin/UnusedStepsSummaryPrinter.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,8 @@ private void finishReport() {
6868
String pattern = entry.getValue();
6969
out.println(format.text(location) + " # " + pattern);
7070
}
71+
72+
out.close();
7173
}
7274

7375
@Override
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package io.cucumber.core.plugin;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.io.ByteArrayOutputStream;
6+
import java.io.IOException;
7+
import java.io.OutputStreamWriter;
8+
9+
import static io.cucumber.core.plugin.BytesEqualTo.isBytesEqualTo;
10+
import static org.hamcrest.MatcherAssert.assertThat;
11+
import static org.mockito.Mockito.spy;
12+
import static org.mockito.Mockito.times;
13+
import static org.mockito.Mockito.verify;
14+
15+
class NiceAppendableTest {
16+
17+
@Test
18+
public void should_flush_every_call_if_configured() throws IOException {
19+
ByteArrayOutputStream out = new ByteArrayOutputStream();
20+
OutputStreamWriter writer = spy(new OutputStreamWriter(out));
21+
NiceAppendable appendable = new NiceAppendable(writer, true);
22+
23+
appendable
24+
.append("First String,")
25+
.append("__Second String__", 2, 15)
26+
.append("\n")
27+
.println("Second line")
28+
.println()
29+
.close();
30+
31+
assertThat(out, isBytesEqualTo("First String,Second String\nSecond line\n\n"));
32+
verify(writer, times(6)).flush(); // Each method call flushes
33+
}
34+
35+
@Test
36+
public void should_not_flush_unless_configured() throws IOException {
37+
ByteArrayOutputStream out = new ByteArrayOutputStream();
38+
OutputStreamWriter writer = spy(new OutputStreamWriter(out));
39+
NiceAppendable appendable = new NiceAppendable(writer);
40+
41+
appendable
42+
.append("First String,")
43+
.append("__Second String__", 2, 15)
44+
.append("\n")
45+
.println("Second line")
46+
.println()
47+
.close();
48+
49+
assertThat(out, isBytesEqualTo("First String,Second String\nSecond line\n\n"));
50+
verify(writer, times(0)).flush();
51+
}
52+
53+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package io.cucumber.core.plugin;
2+
3+
import io.cucumber.core.eventbus.EventBus;
4+
import io.cucumber.core.runtime.TimeServiceEventBus;
5+
import io.cucumber.plugin.event.HookTestStep;
6+
import io.cucumber.plugin.event.PickleStepTestStep;
7+
import io.cucumber.plugin.event.Result;
8+
import io.cucumber.plugin.event.TestCase;
9+
import io.cucumber.plugin.event.TestRunFinished;
10+
import io.cucumber.plugin.event.TestStepFinished;
11+
import org.junit.jupiter.api.BeforeEach;
12+
import org.junit.jupiter.api.Test;
13+
14+
import java.io.ByteArrayOutputStream;
15+
import java.time.Clock;
16+
import java.time.Duration;
17+
import java.time.Instant;
18+
import java.util.UUID;
19+
20+
import static io.cucumber.core.plugin.BytesEqualTo.isBytesEqualTo;
21+
import static io.cucumber.plugin.event.Status.FAILED;
22+
import static io.cucumber.plugin.event.Status.PASSED;
23+
import static io.cucumber.plugin.event.Status.UNDEFINED;
24+
import static org.hamcrest.MatcherAssert.assertThat;
25+
import static org.mockito.Mockito.mock;
26+
27+
class ProgressFormatterTest {
28+
29+
final EventBus bus = new TimeServiceEventBus(Clock.systemUTC(), UUID::randomUUID);
30+
final ByteArrayOutputStream out = new ByteArrayOutputStream();
31+
final ProgressFormatter formatter = new ProgressFormatter(out);
32+
33+
@BeforeEach
34+
void setup() {
35+
formatter.setEventPublisher(bus);
36+
}
37+
38+
@Test
39+
void prints_empty_line_for_empty_test_run() {
40+
Result runResult = new Result(PASSED, Duration.ZERO, null);
41+
bus.send(new TestRunFinished(Instant.now(), runResult));
42+
assertThat(out, isBytesEqualTo("\n"));
43+
}
44+
45+
@Test
46+
void print_green_dot_for_passing_step() {
47+
Result result = new Result(PASSED, Duration.ZERO, null);
48+
bus.send(new TestStepFinished(Instant.now(), mock(TestCase.class), mock(PickleStepTestStep.class), result));
49+
bus.send(new TestRunFinished(Instant.now(), result));
50+
assertThat(out, isBytesEqualTo(AnsiEscapes.GREEN + "." + AnsiEscapes.RESET + "\n"));
51+
}
52+
53+
@Test
54+
void print_yellow_U_for_undefined_step() {
55+
Result result = new Result(UNDEFINED, Duration.ZERO, null);
56+
bus.send(new TestStepFinished(Instant.now(), mock(TestCase.class), mock(PickleStepTestStep.class), result));
57+
bus.send(new TestRunFinished(Instant.now(), result));
58+
assertThat(out, isBytesEqualTo(AnsiEscapes.YELLOW + "U" + AnsiEscapes.RESET + "\n"));
59+
}
60+
61+
@Test
62+
void print_nothing_for_passed_hook() {
63+
Result result = new Result(PASSED, Duration.ZERO, null);
64+
bus.send(new TestStepFinished(Instant.now(), mock(TestCase.class), mock(HookTestStep.class), result));
65+
bus.send(new TestRunFinished(Instant.now(), result));
66+
assertThat(out, isBytesEqualTo("\n"));
67+
}
68+
69+
@Test
70+
void print_red_F_for_failed_step() {
71+
Result result = new Result(FAILED, Duration.ZERO, null);
72+
bus.send(new TestStepFinished(Instant.now(), mock(TestCase.class), mock(PickleStepTestStep.class), result));
73+
bus.send(new TestRunFinished(Instant.now(), result));
74+
assertThat(out, isBytesEqualTo(AnsiEscapes.RED + "F" + AnsiEscapes.RESET + "\n"));
75+
}
76+
77+
@Test
78+
void print_red_F_for_failed_hook() {
79+
Result result = new Result(FAILED, Duration.ZERO, null);
80+
bus.send(new TestStepFinished(Instant.now(), mock(TestCase.class), mock(HookTestStep.class), result));
81+
bus.send(new TestRunFinished(Instant.now(), result));
82+
assertThat(out, isBytesEqualTo(AnsiEscapes.RED + "F" + AnsiEscapes.RESET + "\n"));
83+
}
84+
85+
}

0 commit comments

Comments
 (0)