Skip to content

Commit

Permalink
Introduce testfeed details mode for ConsoleLauncher (junit-team#3244)
Browse files Browse the repository at this point in the history
The new `testfeed` mode provides a concise real-time stream of test
execution events.

Co-authored-by: halitanildonmez <halitanil.donmez@gmail.com>
  • Loading branch information
2 people authored and yhkuo41 committed Apr 19, 2023
1 parent 6dbbe08 commit 0f8ed39
Show file tree
Hide file tree
Showing 30 changed files with 661 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ repository on GitHub.
`ConsoleLauncher` to include or exclude methods based on fully qualified method names
without parameters. For example, `--exclude-methodname=^org\.example\..+#methodname`
will exclude all methods called `methodName` under package `org.example`.
* The new `testfeed` details mode for `ConsoleLauncher` prints test execution events as
they occur in a concise format.


[[release-notes-5.10.0-M1-junit-jupiter]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,17 @@ public enum Details {
/**
* Combines {@link #TREE} and {@link #FLAT} modes.
*/
VERBOSE;
VERBOSE,

/**
* Return lower case {@link #name} for easier usage in help text for
* Test plan execution events are rendered as they occur in a concise format.
*
* @since 1.10
*/
TESTFEED;

/**
* Return lower case {@link #name()} for easier usage in help text for
* available options.
*/
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ private Optional<DetailsPrintingListener> createDetailsPrintingListener(PrintWri
return Optional.of(new TreePrintingListener(out, colorPalette, theme));
case VERBOSE:
return Optional.of(new VerboseTreePrintingListener(out, colorPalette, 16, theme));
case TESTFEED:
return Optional.of(new TestFeedPrintingListener(out, colorPalette));
default:
return Optional.empty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,18 @@

package org.junit.platform.console.tasks;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import java.util.regex.Pattern;

import org.apiguardian.api.API;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;

/**
* @since 1.9
*/
@API(status = EXPERIMENTAL, since = "1.9")
public interface DetailsPrintingListener extends TestExecutionListener {
interface DetailsPrintingListener extends TestExecutionListener {

Pattern LINE_START_PATTERN = Pattern.compile("(?m)^");

void listTests(TestPlan testPlan);

static String indented(String message, String indentation) {
return LINE_START_PATTERN.matcher(message).replaceAll(indentation).trim();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
package org.junit.platform.console.tasks;

import java.io.PrintWriter;
import java.util.regex.Pattern;

import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.engine.TestExecutionResult;
Expand All @@ -24,8 +23,6 @@
*/
class FlatPrintingListener implements DetailsPrintingListener {

private static final Pattern LINE_START_PATTERN = Pattern.compile("(?m)^");

static final String INDENTATION = " ";

private final PrintWriter out;
Expand Down Expand Up @@ -102,7 +99,7 @@ private void println(Style style, String format, Object... args) {
* @return indented message
*/
private static String indented(String message) {
return LINE_START_PATTERN.matcher(message).replaceAll(INDENTATION).trim();
return DetailsPrintingListener.indented(message, INDENTATION);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.console.tasks;

import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;

class TestFeedPrintingListener implements DetailsPrintingListener {

private static final String INDENTATION = "\t";
private static final String STATUS_SEPARATOR = " :: ";

private final PrintWriter out;
private final ColorPalette colorPalette;
private TestPlan testPlan;

TestFeedPrintingListener(PrintWriter out, ColorPalette colorPalette) {
this.out = out;
this.colorPalette = colorPalette;
}

@Override
public void testPlanExecutionStarted(TestPlan testPlan) {
this.testPlan = testPlan;
}

@Override
public void testPlanExecutionFinished(TestPlan testPlan) {
this.testPlan = null;
}

@Override
public void executionSkipped(TestIdentifier testIdentifier, String reason) {
if (shouldPrint(testIdentifier)) {
String msg = formatTestIdentifier(testIdentifier);
String indentedReason = indented(String.format("Reason: %s", reason));
println(Style.SKIPPED,
String.format("%s" + STATUS_SEPARATOR + "SKIPPED%n" + INDENTATION + "%s", msg, indentedReason));
}
}

@Override
public void executionStarted(TestIdentifier testIdentifier) {
if (shouldPrint(testIdentifier)) {
String msg = formatTestIdentifier(testIdentifier);
println(Style.NONE, String.format("%s" + STATUS_SEPARATOR + "STARTED", msg));
}
}

@Override
public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) {
TestExecutionResult.Status status = testExecutionResult.getStatus();
if (testExecutionResult.getThrowable().isPresent()) {
Style style = Style.valueOf(testExecutionResult);
String msg = formatTestIdentifier(testIdentifier);
Throwable throwable = testExecutionResult.getThrowable().get();
String stacktrace = indented(ExceptionUtils.readStackTrace(throwable));
println(style,
String.format("%s" + STATUS_SEPARATOR + "%s%n" + INDENTATION + "%s", msg, status, stacktrace));
}
else if (shouldPrint(testIdentifier) || testExecutionResult.getStatus() != SUCCESSFUL) {
Style style = Style.valueOf(testExecutionResult);
String msg = formatTestIdentifier(testIdentifier);
println(style, String.format("%s" + STATUS_SEPARATOR + "%s", msg, status));
}
}

private String formatTestIdentifier(TestIdentifier testIdentifier) {
return String.join(" > ", collectDisplayNames(testIdentifier.getUniqueIdObject()));
}

private void println(Style style, String message) {
this.out.println(colorPalette.paint(style, message));
}

private List<String> collectDisplayNames(UniqueId uniqueId) {
int size = uniqueId.getSegments().size();
List<String> displayNames = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
displayNames.add(0, testPlan.getTestIdentifier(uniqueId).getDisplayName());
if (i < size - 1) {
uniqueId = uniqueId.removeLastSegment();
}
}
return displayNames;
}

private static String indented(String message) {
return DetailsPrintingListener.indented(message, INDENTATION);
}

@Override
public void listTests(TestPlan testPlan) {
this.testPlan = testPlan;
try {
testPlan.accept(new TestPlan.Visitor() {
@Override
public void visit(TestIdentifier testIdentifier) {
if (shouldPrint(testIdentifier)) {
println(Style.NONE, formatTestIdentifier(testIdentifier));
}
}
});
}
finally {
this.testPlan = null;
}
}

private static boolean shouldPrint(TestIdentifier testIdentifier) {
return testIdentifier.isTest();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright 2015-2023 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.platform.console.tasks;

import static org.junit.jupiter.api.Assertions.assertLinesMatch;
import static org.junit.platform.commons.util.CollectionUtils.getOnlyElement;
import static org.mockito.Mockito.mock;

import java.io.PrintWriter;
import java.io.StringWriter;
import java.util.Collections;
import java.util.stream.Stream;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
import org.junit.platform.fakes.TestDescriptorStub;
import org.junit.platform.launcher.TestIdentifier;
import org.junit.platform.launcher.TestPlan;
import org.opentest4j.TestAbortedException;

public class TestFeedPrintingListenerTests {

TestPlan testPlan;
TestIdentifier testIdentifier;

StringWriter stringWriter = new StringWriter();
TestFeedPrintingListener listener = new TestFeedPrintingListener(new PrintWriter(stringWriter), ColorPalette.NONE);

@BeforeEach
void prepareListener() {
var engineDescriptor = new EngineDescriptor(UniqueId.forEngine("demo-engine"), "Demo Engine");
var testDescriptor = new TestDescriptorStub(engineDescriptor.getUniqueId().append("type", "test"),
"%c ool test");
engineDescriptor.addChild(testDescriptor);

testPlan = TestPlan.from(Collections.singleton(engineDescriptor), mock());
testIdentifier = testPlan.getTestIdentifier(testDescriptor.getUniqueId());

listener.testPlanExecutionStarted(testPlan);
}

@Test
public void testExecutionSkipped() {
listener.executionSkipped(testIdentifier, "Test disabled");
assertLinesMatch( //
"""
Demo Engine > %c ool test :: SKIPPED
\tReason: Test disabled
""".lines(), //
actualLines() //
);
}

@Test
public void testExecutionFailed() {
listener.executionFinished(testIdentifier, TestExecutionResult.failed(new AssertionError("Boom!")));
assertLinesMatch( //
"""
Demo Engine > %c ool test :: FAILED
\tjava.lang.AssertionError: Boom!
>>>>
""".lines(), //
actualLines() //
);
}

@Test
public void testExecutionAborted() {
listener.executionFinished(testIdentifier, TestExecutionResult.aborted(new TestAbortedException("Boom!")));
assertLinesMatch( //
"""
Demo Engine > %c ool test :: ABORTED
\torg.opentest4j.TestAbortedException: Boom!
>>>>
""".lines(), //
actualLines() //
);
}

@Test
public void testExecutionSucceeded() {
listener.executionFinished(testIdentifier, TestExecutionResult.successful());
assertLinesMatch(Stream.of("Demo Engine > %c ool test :: SUCCESSFUL"), actualLines());
}

@Test
public void testExecutionFailedOfContainer() {
var engineIdentifier = getOnlyElement(testPlan.getRoots());
listener.executionFinished(engineIdentifier, TestExecutionResult.failed(new AssertionError("Boom!")));
assertLinesMatch( //
"""
Demo Engine :: FAILED
\tjava.lang.AssertionError: Boom!
>>>>
""".lines(), //
actualLines() //
);
}

private Stream<String> actualLines() {
return stringWriter.toString().lines();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
JUnit Jupiter > Basic > .oO fancy display name Oo. :: STARTED
JUnit Jupiter > Basic > .oO fancy display name Oo. :: SUCCESSFUL

Test run finished after [\d]+ ms
[ 2 containers found ]
[ 0 containers skipped ]
[ 2 containers started ]
[ 0 containers aborted ]
[ 2 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 1 tests successful ]
[ 0 tests failed ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
JUnit Jupiter > Basic > .oO fancy display name Oo. :: STARTED
JUnit Jupiter > Basic > .oO fancy display name Oo. :: SUCCESSFUL

Test run finished after [\d]+ ms
[ 2 containers found ]
[ 0 containers skipped ]
[ 2 containers started ]
[ 0 containers aborted ]
[ 2 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 1 tests successful ]
[ 0 tests failed ]
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
JUnit Jupiter > Basic > empty() :: STARTED
JUnit Jupiter > Basic > empty() :: SUCCESSFUL

Test run finished after [\d]+ ms
[ 2 containers found ]
[ 0 containers skipped ]
[ 2 containers started ]
[ 0 containers aborted ]
[ 2 containers successful ]
[ 0 containers failed ]
[ 1 tests found ]
[ 0 tests skipped ]
[ 1 tests started ]
[ 0 tests aborted ]
[ 1 tests successful ]
[ 0 tests failed ]
Loading

0 comments on commit 0f8ed39

Please sign in to comment.