Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ repository on GitHub.
[[release-notes-5.13.0-M1-junit-platform-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* New optional CLI options `--redirect-stdout` and `--redirect-stderr` to redirect stdout
and stderr outputs to a file.


[[release-notes-5.13.0-M1-junit-jupiter]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,21 @@ $ java -jar junit-platform-console-standalone-{platform-version}.jar <OPTIONS> \
--config-resource=configuration.properties
----

You can redirect standard output and standard error using the `--redirect-stdout` and
`--redirect-stderr` options:

[source,console,subs=attributes+]
----
$ java -jar junit-platform-console-standalone-{platform-version}.jar <OPTIONS> \
--redirect-stdout=foo.txt \
--redirect-stderr=bar.txt
----

If both the `--redirect-stdout` and `--redirect-stderr` parameters point to the same
file, the output will be merged into that file.

The default charset is used for writing to the files.

[[junit-platform-reporting-legacy-xml]]
==== Legacy XML format

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ public class TestConsoleOutputOptions {
private boolean isSingleColorPalette;
private Details details = DEFAULT_DETAILS;
private Theme theme = DEFAULT_THEME;
private Path stdoutPath;
private Path stderrPath;

public boolean isAnsiColorOutputDisabled() {
return this.ansiColorOutputDisabled;
Expand Down Expand Up @@ -73,4 +75,24 @@ public void setTheme(Theme theme) {
this.theme = theme;
}

@API(status = INTERNAL, since = "1.13")
public Path getStdoutPath() {
return this.stdoutPath;
}

@API(status = INTERNAL, since = "1.13")
public void setStdoutPath(Path stdoutPath) {
this.stdoutPath = stdoutPath;
}

@API(status = INTERNAL, since = "1.13")
public Path getStderrPath() {
return this.stderrPath;
}

@API(status = INTERNAL, since = "1.13")
public void setStderrPath(Path stderrPath) {
this.stderrPath = stderrPath;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,19 @@ static class ConsoleOutputOptions {
@Option(names = "-details-theme", hidden = true)
private final Theme theme2 = DEFAULT_THEME;

@Option(names = "--redirect-stdout", paramLabel = "FILE", description = "Redirect test output to stdout to a file.")
private Path stdout;

@Option(names = "--redirect-stderr", paramLabel = "FILE", description = "Redirect test output to stderr to a file.")
private Path stderr;

private void applyTo(TestConsoleOutputOptions result) {
result.setColorPalettePath(choose(colorPalette, colorPalette2, null));
result.setSingleColorPalette(singleColorPalette || singleColorPalette2);
result.setDetails(choose(details, details2, DEFAULT_DETAILS));
result.setTheme(choose(theme, theme2, DEFAULT_THEME));
result.setStdoutPath(stdout);
result.setStderrPath(stderr);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import static org.junit.platform.console.tasks.DiscoveryRequestCreator.toDiscoveryRequestBuilder;
import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_PROPERTY_NAME;

import java.io.PrintStream;
import java.io.PrintWriter;
import java.net.URL;
import java.net.URLClassLoader;
Expand Down Expand Up @@ -101,10 +102,16 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional<Path> report
Launcher launcher = launcherSupplier.get();
SummaryGeneratingListener summaryListener = registerListeners(out, reportsDir, launcher);

LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions);
reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME,
dir.toAbsolutePath().toString()));
launcher.execute(discoveryRequestBuilder.build());
PrintStream originalOut = System.out;
PrintStream originalErr = System.err;
try (StdStreamHandler stdStreamHandler = new StdStreamHandler()) {
stdStreamHandler.redirectStdStreams(outputOptions.getStdoutPath(), outputOptions.getStderrPath());
launchTests(launcher, reportsDir);
}
finally {
System.setOut(originalOut);
System.setErr(originalErr);
}

TestExecutionSummary summary = summaryListener.getSummary();
if (summary.getTotalFailureCount() > 0 || outputOptions.getDetails() != Details.NONE) {
Expand All @@ -114,6 +121,13 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional<Path> report
return summary;
}

private void launchTests(Launcher launcher, Optional<Path> reportsDir) {
LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions);
reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME,
dir.toAbsolutePath().toString()));
launcher.execute(discoveryRequestBuilder.build());
}

private Optional<ClassLoader> createCustomClassLoader() {
List<Path> additionalClasspathEntries = discoveryOptions.getExistingAdditionalClasspathEntries();
if (!additionalClasspathEntries.isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Copyright 2015-2025 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 java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;

import org.junit.platform.commons.JUnitException;

class StdStreamHandler implements AutoCloseable {
private PrintStream stdout;
private PrintStream stderr;

public StdStreamHandler() {
}

private boolean isSameFile(Path path1, Path path2) {
if (path1 == null || path2 == null)
return false;
return (path1.normalize().toAbsolutePath().equals(path2.normalize().toAbsolutePath()));
}

/**
* Redirects standard output (stdout) and standard error (stderr) to the specified file paths.
* If the paths are the same, both streams are redirected to the same file.
* The default charset is used for writing to the files.
*
* @param stdoutPath The file path for standard output. {@code null} means no redirection.
* @param stderrPath The file path for standard error. {@code null} means no redirection.
*/
public void redirectStdStreams(Path stdoutPath, Path stderrPath) {
if (isSameFile(stdoutPath, stderrPath)) {
try {
PrintStream commonStream = new PrintStream(Files.newOutputStream(stdoutPath), true);
this.stdout = commonStream;
this.stderr = commonStream;
}
catch (IOException e) {
throw new JUnitException("Error setting up stream for Stdout and Stderr at path: " + stdoutPath, e);
}
}
else {
if (stdoutPath != null) {
try {
this.stdout = new PrintStream(Files.newOutputStream(stdoutPath), true);
}
catch (IOException e) {
throw new JUnitException("Error setting up stream for Stdout at path: " + stdoutPath, e);
}
}

if (stderrPath != null) {
try {
this.stderr = new PrintStream(Files.newOutputStream(stderrPath), true);
}
catch (IOException e) {
throw new JUnitException("Error setting up stream for Stderr at path: " + stderrPath, e);
}
}
}

if (stdout != null) {
System.setOut(stdout);
}
if (stderr != null) {
System.setErr(stderr);
}
}

@Override
public void close() {
try {
if (stdout != null) {
stdout.close();
}
}
finally {
if (stderr != null) {
stderr.close();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,20 @@
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.junit.jupiter.params.provider.ValueSource;
import org.junit.platform.console.options.StdStreamTestCase;

/**
* @since 1.0
Expand Down Expand Up @@ -92,4 +102,35 @@ void executeScanModules(final String line) {
assertEquals(0, new ConsoleLauncherWrapper().execute(args1).getTestsFoundCount());
}

private static Stream<Arguments> redirectStreamParams() {
return Stream.of(Arguments.of("--redirect-stdout", StdStreamTestCase.getStdoutOutputFileSize()),
Arguments.of("--redirect-stderr", StdStreamTestCase.getStderrOutputFileSize()));
}

@ParameterizedTest
@MethodSource("redirectStreamParams")
void executeWithRedirectedStdStream(String redirectedStream, int outputFileSize, @TempDir Path tempDir)
throws IOException {
Path outputFile = tempDir.resolve("output.txt");
var line = String.format("execute -e junit-jupiter --select-class %s %s %s", StdStreamTestCase.class.getName(),
redirectedStream, outputFile);
var args = line.split(" ");
new ConsoleLauncherWrapper().execute(args);

assertTrue(Files.exists(outputFile), "File does not exist.");
assertEquals(outputFileSize, Files.size(outputFile), "Invalid file size.");
}

@Test
void executeWithRedirectedStdStreamsToSameFile(@TempDir Path tempDir) throws IOException {
Path outputFile = tempDir.resolve("output.txt");
var line = String.format("execute -e junit-jupiter --select-class %s --redirect-stdout %s --redirect-stderr %s",
StdStreamTestCase.class.getName(), outputFile, outputFile);
var args = line.split(" ");
new ConsoleLauncherWrapper().execute(args);

assertTrue(Files.exists(outputFile), "File does not exist.");
assertEquals(StdStreamTestCase.getStdoutOutputFileSize() + StdStreamTestCase.getStderrOutputFileSize(),
Files.size(outputFile), "Invalid file size.");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.platform.engine.discovery.ClassNameFilter.STANDARD_INCLUDE_PATTERN;
Expand Down Expand Up @@ -60,6 +61,8 @@ void parseNoArguments() {
// @formatter:off
assertAll(
() -> assertFalse(options.output.isAnsiColorOutputDisabled()),
() -> assertNull(options.output.getStdoutPath()),
() -> assertNull(options.output.getStderrPath()),
() -> assertEquals(TestConsoleOutputOptions.DEFAULT_DETAILS, options.output.getDetails()),
() -> assertFalse(options.discovery.isScanClasspath()),
() -> assertEquals(List.of(STANDARD_INCLUDE_PATTERN), options.discovery.getIncludedClassNamePatterns()),
Expand Down Expand Up @@ -632,6 +635,44 @@ void parseInvalidConfigurationParameters() {
assertOptionWithMissingRequiredArgumentThrowsException("-config", "--config");
}

@ParameterizedTest
@EnumSource
void parseValidStdoutRedirectionFile(ArgsType type) {
var file = Paths.get("foo.txt");
// @formatter:off
assertAll(
() -> assertNull(type.parseArgLine("").output.getStdoutPath()),
() -> assertEquals(file, type.parseArgLine("--redirect-stdout=foo.txt").output.getStdoutPath()),
() -> assertEquals(file, type.parseArgLine("--redirect-stdout foo.txt").output.getStdoutPath()),
() -> assertEquals(file, type.parseArgLine("--redirect-stdout bar.txt --redirect-stdout foo.txt").output.getStdoutPath())
);
// @formatter:on
}

@Test
void parseInvalidStdoutRedirectionFile() {
assertOptionWithMissingRequiredArgumentThrowsException("--redirect-stdout");
}

@ParameterizedTest
@EnumSource
void parseValidStderrRedirectionFile(ArgsType type) {
var file = Paths.get("foo.txt");
// @formatter:off
assertAll(
() -> assertNull(type.parseArgLine("").output.getStderrPath()),
() -> assertEquals(file, type.parseArgLine("--redirect-stderr=foo.txt").output.getStderrPath()),
() -> assertEquals(file, type.parseArgLine("--redirect-stderr foo.txt").output.getStderrPath()),
() -> assertEquals(file, type.parseArgLine("--redirect-stderr bar.txt --redirect-stderr foo.txt").output.getStderrPath())
);
// @formatter:on
}

@Test
void parseInvalidStderrRedirectionFile() {
assertOptionWithMissingRequiredArgumentThrowsException("--redirect-stderr");
}

@Test
void parseInvalidConfigurationParametersResource() {
assertOptionWithMissingRequiredArgumentThrowsException("--config-resource");
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* Copyright 2015-2025 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.options;

import org.junit.jupiter.api.Test;

public class StdStreamTestCase {

private static final String STDOUT_DATA = "Writing to STDOUT...";
private static final String STDERR_DATA = "Writing to STDERR...";

public static int getStdoutOutputFileSize() {
return STDOUT_DATA.length();
}

public static int getStderrOutputFileSize() {
return STDERR_DATA.length();
}

@Test
void printTest() {
System.out.print(STDOUT_DATA);
System.err.print(STDERR_DATA);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ void avoidAccessingStandardStreams(JavaClasses classes) {
// ConsoleLauncher, StreamInterceptor, Picocli et al...
var subset = classes //
.that(are(not(name("org.junit.platform.console.ConsoleLauncher")))) //
.that(are(not(name("org.junit.platform.console.tasks.ConsoleTestExecutor")))) //
.that(are(not(name("org.junit.platform.launcher.core.StreamInterceptor")))) //
.that(are(not(name("org.junit.platform.runner.JUnitPlatformRunnerListener")))) //
.that(are(not(name("org.junit.platform.testkit.engine.Events")))) //
Expand Down