From 216bfdb6a2dcecf28849fce58fe3d89c44ce3f5c Mon Sep 17 00:00:00 2001 From: Oyster-zx Date: Tue, 11 Feb 2025 22:21:21 -0800 Subject: [PATCH] Issue: #3166: Introduce --redirect-stdout and --redirect-stderr CLI options to redirect STDOUT and STDERR --- .../release-notes-5.12.0-RC2.adoc | 2 +- .../junit-platform-reporting.adoc | 9 ++++ .../options/TestConsoleOutputOptions.java | 22 +++++++++ .../TestConsoleOutputOptionsMixin.java | 8 ++++ .../console/tasks/ConsoleTestExecutor.java | 46 +++++++++++++++++++ .../CommandLineOptionsParsingTests.java | 41 +++++++++++++++++ 6 files changed, 127 insertions(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc index f2e111d13f7e..c47918fd66ec 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc @@ -48,7 +48,7 @@ repository on GitHub. * The `@TempDir` now warns when deleting symlinks that target locations outside the temporary directory to signal that the target file or directory is _not_ deleted, only the link to it. - +* New optional CLI options `--redirect-stdout` and `--redirect-stderr` to redirect stdout and stderr outputs to a file. [[release-notes-5.12.0-RC2-junit-vintage]] === JUnit Vintage diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc index 2e5b4d7369f1..7f143a266b87 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc @@ -154,6 +154,15 @@ $ java -jar junit-platform-console-standalone-{platform-version}.jar \ --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 \ + --redirect-stdout=foo.txt + --redirect-stderr=bar.txt +---- + [[junit-platform-reporting-legacy-xml]] ==== Legacy XML format diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java index 06067a1022f5..e22e46f518ee 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptions.java @@ -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; @@ -73,4 +75,24 @@ public void setTheme(Theme theme) { this.theme = theme; } + @API(status = INTERNAL, since = "5.12") + public Path getStdoutPath() { + return this.stdoutPath; + } + + @API(status = INTERNAL, since = "5.12") + public void setStdoutPath(Path stdoutPath) { + this.stdoutPath = stdoutPath; + } + + @API(status = INTERNAL, since = "5.12") + public Path getStderrPath() { + return this.stderrPath; + } + + @API(status = INTERNAL, since = "5.12") + public void setStderrPath(Path stderrPath) { + this.stderrPath = stderrPath; + } + } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java index 44ee07588e4a..d099d8064dd4 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/options/TestConsoleOutputOptionsMixin.java @@ -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 tests stdout to a file.") + private Path stdout; + + @Option(names = "--redirect-stderr", paramLabel = "FILE", description = "Redirect tests 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); } } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java index ab64005eded8..236524d13190 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/ConsoleTestExecutor.java @@ -14,9 +14,12 @@ import static org.junit.platform.console.tasks.DiscoveryRequestCreator.toDiscoveryRequestBuilder; import static org.junit.platform.launcher.LauncherConstants.OUTPUT_DIR_PROPERTY_NAME; +import java.io.IOException; +import java.io.PrintStream; import java.io.PrintWriter; import java.net.URL; import java.net.URLClassLoader; +import java.nio.file.Files; import java.nio.file.Path; import java.util.EnumSet; import java.util.List; @@ -101,6 +104,8 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report Launcher launcher = launcherSupplier.get(); SummaryGeneratingListener summaryListener = registerListeners(out, reportsDir, launcher); + redirectStdStreams(outputOptions.getStdoutPath(), outputOptions.getStderrPath()); + LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions); reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME, dir.toAbsolutePath().toString())); @@ -190,6 +195,47 @@ private void printSummary(TestExecutionSummary summary, PrintWriter out) { summary.printTo(out); } + @API(status = INTERNAL, since = "5.12") + private boolean isSameFile(Path path1, Path path2) { + if (path1 == null || path2 == null) + return false; + return (path1.normalize().toAbsolutePath().equals(path2.normalize().toAbsolutePath())); + } + + private void redirectStdStreams(Path stdoutPath, Path stderrPath) { + if (isSameFile(stdoutPath, stderrPath)) { + try { + PrintStream commonStream = new PrintStream(Files.newOutputStream(stdoutPath), true); + System.setOut(commonStream); + System.setErr(commonStream); + } + catch (IOException e) { + throw new JUnitException("Error setting up stream for Stdout and Stderr at path: " + stdoutPath, e); + } + } + else { + if (stdoutPath != null) { + try { + PrintStream outStream = new PrintStream(Files.newOutputStream(stdoutPath), true); + System.setOut(outStream); + } + catch (IOException e) { + throw new JUnitException("Error setting up stream for Stdout at path: " + stdoutPath, e); + } + } + + if (stderrPath != null) { + try { + PrintStream errStream = new PrintStream(Files.newOutputStream(stderrPath), true); + System.setErr(errStream); + } + catch (IOException e) { + throw new JUnitException("Error setting up stream for Stderr at path: " + stderrPath, e); + } + } + } + } + @FunctionalInterface public interface Factory { ConsoleTestExecutor create(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions); diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java index 0745f27a914a..67573f23d1eb 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/options/CommandLineOptionsParsingTests.java @@ -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; @@ -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()), @@ -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");