From 9c2b4a348a63bb69cb52a259e2b5ad3a1f230eb4 Mon Sep 17 00:00:00 2001 From: Oyster-zx Date: Tue, 11 Feb 2025 22:21:21 -0800 Subject: [PATCH 1/4] Issue: #3166: Introduce --redirect-stdout and --redirect-stderr CLI options to redirect STDOUT and STDERR --- .../release-notes-5.12.0-RC2.adoc | 71 +++++++++++++++++++ .../junit-platform-reporting.adoc | 9 +++ .../options/TestConsoleOutputOptions.java | 22 ++++++ .../TestConsoleOutputOptionsMixin.java | 8 +++ .../console/tasks/ConsoleTestExecutor.java | 46 ++++++++++++ .../CommandLineOptionsParsingTests.java | 41 +++++++++++ 6 files changed, 197 insertions(+) create mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc 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 new file mode 100644 index 000000000000..c47918fd66ec --- /dev/null +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc @@ -0,0 +1,71 @@ +[[release-notes-5.12.0-RC2]] +== 5.12.0-RC2 + +*Date of Release:* February 14, 2025 + +*Scope:* Minor enhancements since JUnit 5.12.0-RC1. + +For a complete list of all _closed_ issues and pull requests for this release, consult the +link:{junit5-repo}+/milestone/90?closed=1+[5.12.0-RC2] milestone page in the JUnit +repository on GitHub. + + +[[release-notes-5.12.0-RC2-junit-platform]] +=== JUnit Platform + +[[release-notes-5.12.0-RC2-junit-platform-bug-fixes]] +==== Bug Fixes + +* ❓ + +[[release-notes-5.12.0-RC2-junit-platform-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* ❓ + +[[release-notes-5.12.0-RC2-junit-platform-new-features-and-improvements]] +==== New Features and Improvements + +* ❓ + + +[[release-notes-5.12.0-RC2-junit-jupiter]] +=== JUnit Jupiter + +[[release-notes-5.12.0-RC2-junit-jupiter-bug-fixes]] +==== Bug Fixes + +* ❓ + +[[release-notes-5.12.0-RC2-junit-jupiter-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* ❓ + +[[release-notes-5.12.0-RC2-junit-jupiter-new-features-and-improvements]] +==== New Features and Improvements + +* 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 + +[[release-notes-5.12.0-RC2-junit-vintage-bug-fixes]] +==== Bug Fixes + +* ❓ + +[[release-notes-5.12.0-RC2-junit-vintage-deprecations-and-breaking-changes]] +==== Deprecations and Breaking Changes + +* ❓ + +[[release-notes-5.12.0-RC2-junit-vintage-new-features-and-improvements]] +==== New Features and Improvements + +* Support for executing test methods in parallel. Please refer to the + <<../user-guide/index.adoc#migrating-from-junit4-parallel-execution, User Guide>> for + more information. 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"); From 703bc008711630c5f428c58a64899d00f682e79d Mon Sep 17 00:00:00 2001 From: Oyster-zx Date: Wed, 12 Feb 2025 19:58:35 -0800 Subject: [PATCH 2/4] Issue #3166: STDOUT and STDERR are reset after console test execution --- .../release-notes-5.12.0-RC2.adoc | 71 ------------------- .../release-notes-5.13.0-M1.adoc | 3 +- .../console/tasks/ConsoleTestExecutor.java | 23 ++++-- .../ConsoleLauncherIntegrationTests.java | 37 ++++++++++ .../console/options/StdStreamTest.java | 22 ++++++ .../tooling/support/tests/ArchUnitTests.java | 1 + 6 files changed, 79 insertions(+), 78 deletions(-) delete mode 100644 documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc create mode 100644 platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java 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 deleted file mode 100644 index c47918fd66ec..000000000000 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-RC2.adoc +++ /dev/null @@ -1,71 +0,0 @@ -[[release-notes-5.12.0-RC2]] -== 5.12.0-RC2 - -*Date of Release:* February 14, 2025 - -*Scope:* Minor enhancements since JUnit 5.12.0-RC1. - -For a complete list of all _closed_ issues and pull requests for this release, consult the -link:{junit5-repo}+/milestone/90?closed=1+[5.12.0-RC2] milestone page in the JUnit -repository on GitHub. - - -[[release-notes-5.12.0-RC2-junit-platform]] -=== JUnit Platform - -[[release-notes-5.12.0-RC2-junit-platform-bug-fixes]] -==== Bug Fixes - -* ❓ - -[[release-notes-5.12.0-RC2-junit-platform-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* ❓ - -[[release-notes-5.12.0-RC2-junit-platform-new-features-and-improvements]] -==== New Features and Improvements - -* ❓ - - -[[release-notes-5.12.0-RC2-junit-jupiter]] -=== JUnit Jupiter - -[[release-notes-5.12.0-RC2-junit-jupiter-bug-fixes]] -==== Bug Fixes - -* ❓ - -[[release-notes-5.12.0-RC2-junit-jupiter-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* ❓ - -[[release-notes-5.12.0-RC2-junit-jupiter-new-features-and-improvements]] -==== New Features and Improvements - -* 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 - -[[release-notes-5.12.0-RC2-junit-vintage-bug-fixes]] -==== Bug Fixes - -* ❓ - -[[release-notes-5.12.0-RC2-junit-vintage-deprecations-and-breaking-changes]] -==== Deprecations and Breaking Changes - -* ❓ - -[[release-notes-5.12.0-RC2-junit-vintage-new-features-and-improvements]] -==== New Features and Improvements - -* Support for executing test methods in parallel. Please refer to the - <<../user-guide/index.adoc#migrating-from-junit4-parallel-execution, User Guide>> for - more information. diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index 3c513d5f7ab8..6b7147750ef7 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -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]] 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 236524d13190..47c18eb48f89 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 @@ -104,12 +104,16 @@ 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())); - launcher.execute(discoveryRequestBuilder.build()); + PrintStream originalOut = System.out; + PrintStream originalErr = System.err; + try { + 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) { @@ -119,6 +123,13 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report return summary; } + private void launchTests(Launcher launcher, Optional reportsDir) { + LauncherDiscoveryRequestBuilder discoveryRequestBuilder = toDiscoveryRequestBuilder(discoveryOptions); + reportsDir.ifPresent(dir -> discoveryRequestBuilder.configurationParameter(OUTPUT_DIR_PROPERTY_NAME, + dir.toAbsolutePath().toString())); + launcher.execute(discoveryRequestBuilder.build()); + } + private Optional createCustomClassLoader() { List additionalClasspathEntries = discoveryOptions.getExistingAdditionalClasspathEntries(); if (!additionalClasspathEntries.isEmpty()) { diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java index a9f0be73a810..99e2e324f69f 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java @@ -10,10 +10,16 @@ package org.junit.platform.console; +import static java.nio.file.Files.deleteIfExists; import static org.assertj.core.api.Assertions.assertThat; 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 org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -92,4 +98,35 @@ void executeScanModules(final String line) { assertEquals(0, new ConsoleLauncherWrapper().execute(args1).getTestsFoundCount()); } + @ParameterizedTest + @ValueSource(strings = { "--redirect-stdout", "--redirect-stderr" }) + void executeWithRedirectedStdStream(String redirectedStream) throws IOException { + + Path outputFile = Path.of("foo.txt"); + var line = String.format( + "execute -e junit-jupiter --select-method org.junit.platform.console.options.StdStreamTest#printTest " + + "%s %s", + redirectedStream, outputFile); + var args = line.split(" "); + new ConsoleLauncherWrapper().execute(args); + + assertTrue(Files.exists(outputFile), "File does not exist."); + assertEquals(Files.size(outputFile), 20, "Invalid file size."); + deleteIfExists(outputFile); + } + + @Test + void executeWithRedirectedStdStreamsToSameFile() throws IOException { + Path outputFile = Path.of("foo.txt"); + var line = String.format( + "execute -e junit-jupiter --select-method org.junit.platform.console.options.StdStreamTest#printTest " + + "--redirect-stdout %s --redirect-stderr %s", + outputFile, outputFile); + var args = line.split(" "); + new ConsoleLauncherWrapper().execute(args); + + assertTrue(Files.exists(outputFile), "File does not exist."); + assertEquals(Files.size(outputFile), 40, "Invalid file size."); + deleteIfExists(outputFile); + } } diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java new file mode 100644 index 000000000000..516da8b20e14 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java @@ -0,0 +1,22 @@ +/* + * 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 StdStreamTest { + + @Test + void printTest() { + System.out.print("Writing to STDOUT..."); + System.err.print("Writing to STDERR..."); + } +} diff --git a/platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java b/platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java index 1d51755ede76..005c5e1b5fec 100644 --- a/platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java +++ b/platform-tooling-support-tests/src/archUnit/java/platform/tooling/support/tests/ArchUnitTests.java @@ -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")))) // From b64113f805d897942e41d96d528b6d79d317778f Mon Sep 17 00:00:00 2001 From: Oyster-zx Date: Tue, 18 Feb 2025 20:29:17 -0800 Subject: [PATCH 3/4] Issue #3166: STDOUT and STDERR are auto closed after test execution --- .../release-notes-5.13.0-M1.adoc | 2 +- .../junit-platform-reporting.adoc | 5 +- .../TestConsoleOutputOptionsMixin.java | 4 +- .../console/tasks/ConsoleTestExecutor.java | 47 +---------- .../console/tasks/StdStreamHandler.java | 81 +++++++++++++++++++ .../ConsoleLauncherIntegrationTests.java | 21 +++-- ...StreamTest.java => StdStreamTestCase.java} | 2 +- 7 files changed, 99 insertions(+), 63 deletions(-) create mode 100644 junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java rename platform-tests/src/test/java/org/junit/platform/console/options/{StdStreamTest.java => StdStreamTestCase.java} (94%) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc index 6b7147750ef7..8c3a8e535760 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M1.adoc @@ -27,7 +27,7 @@ repository on GitHub. ==== New Features and Improvements * New optional CLI options `--redirect-stdout` and `--redirect-stderr` to redirect stdout -and stderr outputs to a file. + and stderr outputs to a file. [[release-notes-5.13.0-M1-junit-jupiter]] 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 7f143a266b87..0f6d62832592 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,12 +154,13 @@ $ 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: +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-stdout=foo.txt \ --redirect-stderr=bar.txt ---- 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 d099d8064dd4..81089ddc8f23 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,10 +51,10 @@ 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.") + @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 tests stderr to a file.") + @Option(names = "--redirect-stderr", paramLabel = "FILE", description = "Redirect test output to stderr to a file.") private Path stderr; private void applyTo(TestConsoleOutputOptions result) { 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 47c18eb48f89..9ff9ccdf6396 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,12 +14,10 @@ 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; @@ -106,8 +104,8 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report PrintStream originalOut = System.out; PrintStream originalErr = System.err; - try { - redirectStdStreams(outputOptions.getStdoutPath(), outputOptions.getStderrPath()); + try (StdStreamHandler stdStreamHandler = new StdStreamHandler()) { + stdStreamHandler.redirectStdStreams(outputOptions.getStdoutPath(), outputOptions.getStderrPath()); launchTests(launcher, reportsDir); } finally { @@ -206,47 +204,6 @@ 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/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java new file mode 100644 index 000000000000..ab4b8fede1f0 --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java @@ -0,0 +1,81 @@ +/* + * 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())); + } + + 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() { + if (stdout != null) { + stdout.close(); + } + if (stderr != null) { + stderr.close(); + } + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java index 99e2e324f69f..2d2d5ee4ff3a 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java @@ -10,7 +10,6 @@ package org.junit.platform.console; -import static java.nio.file.Files.deleteIfExists; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertArrayEquals; @@ -22,6 +21,7 @@ import java.nio.file.Path; 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.ValueSource; @@ -100,33 +100,30 @@ void executeScanModules(final String line) { @ParameterizedTest @ValueSource(strings = { "--redirect-stdout", "--redirect-stderr" }) - void executeWithRedirectedStdStream(String redirectedStream) throws IOException { - - Path outputFile = Path.of("foo.txt"); + void executeWithRedirectedStdStream(String redirectedStream, @TempDir Path tempDir) throws IOException { + Path outputFile = tempDir.resolve("output.txt"); var line = String.format( - "execute -e junit-jupiter --select-method org.junit.platform.console.options.StdStreamTest#printTest " + "execute -e junit-jupiter --select-method org.junit.platform.console.options.StdStreamTestCase#printTest " + "%s %s", redirectedStream, outputFile); var args = line.split(" "); new ConsoleLauncherWrapper().execute(args); assertTrue(Files.exists(outputFile), "File does not exist."); - assertEquals(Files.size(outputFile), 20, "Invalid file size."); - deleteIfExists(outputFile); + assertEquals(20, Files.size(outputFile), "Invalid file size."); } @Test - void executeWithRedirectedStdStreamsToSameFile() throws IOException { - Path outputFile = Path.of("foo.txt"); + void executeWithRedirectedStdStreamsToSameFile(@TempDir Path tempDir) throws IOException { + Path outputFile = tempDir.resolve("output.txt"); var line = String.format( - "execute -e junit-jupiter --select-method org.junit.platform.console.options.StdStreamTest#printTest " + "execute -e junit-jupiter --select-method org.junit.platform.console.options.StdStreamTestCase#printTest " + "--redirect-stdout %s --redirect-stderr %s", outputFile, outputFile); var args = line.split(" "); new ConsoleLauncherWrapper().execute(args); assertTrue(Files.exists(outputFile), "File does not exist."); - assertEquals(Files.size(outputFile), 40, "Invalid file size."); - deleteIfExists(outputFile); + assertEquals(40, Files.size(outputFile), "Invalid file size."); } } diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java similarity index 94% rename from platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java rename to platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java index 516da8b20e14..7a99168385b0 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTest.java +++ b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java @@ -12,7 +12,7 @@ import org.junit.jupiter.api.Test; -public class StdStreamTest { +public class StdStreamTestCase { @Test void printTest() { From 59c6e3674fb242dfdfcdd3362cd335c56b0cfcb2 Mon Sep 17 00:00:00 2001 From: Oyster-zx Date: Wed, 5 Mar 2025 20:31:41 -0800 Subject: [PATCH 4/4] Issue #3166: detailed docmentation provided for redirecting standard streams --- .../junit-platform-reporting.adoc | 5 ++++ .../options/TestConsoleOutputOptions.java | 8 ++--- .../console/tasks/StdStreamHandler.java | 20 ++++++++++--- .../ConsoleLauncherIntegrationTests.java | 29 ++++++++++++------- .../console/options/StdStreamTestCase.java | 15 ++++++++-- 5 files changed, 56 insertions(+), 21 deletions(-) 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 0f6d62832592..453b015e85c7 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 @@ -164,6 +164,11 @@ $ java -jar junit-platform-console-standalone-{platform-version}.jar \ --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 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 e22e46f518ee..73f4fc4a2416 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 @@ -75,22 +75,22 @@ public void setTheme(Theme theme) { this.theme = theme; } - @API(status = INTERNAL, since = "5.12") + @API(status = INTERNAL, since = "1.13") public Path getStdoutPath() { return this.stdoutPath; } - @API(status = INTERNAL, since = "5.12") + @API(status = INTERNAL, since = "1.13") public void setStdoutPath(Path stdoutPath) { this.stdoutPath = stdoutPath; } - @API(status = INTERNAL, since = "5.12") + @API(status = INTERNAL, since = "1.13") public Path getStderrPath() { return this.stderrPath; } - @API(status = INTERNAL, since = "5.12") + @API(status = INTERNAL, since = "1.13") public void setStderrPath(Path stderrPath) { this.stderrPath = stderrPath; } diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java index ab4b8fede1f0..cf6c2105a7c0 100644 --- a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/StdStreamHandler.java @@ -30,6 +30,14 @@ private boolean isSameFile(Path path1, Path path2) { 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 { @@ -71,11 +79,15 @@ public void redirectStdStreams(Path stdoutPath, Path stderrPath) { @Override public void close() { - if (stdout != null) { - stdout.close(); + try { + if (stdout != null) { + stdout.close(); + } } - if (stderr != null) { - stderr.close(); + finally { + if (stderr != null) { + stderr.close(); + } } } } diff --git a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java index 2d2d5ee4ff3a..c4acff2d3964 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/console/ConsoleLauncherIntegrationTests.java @@ -19,11 +19,15 @@ 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 @@ -98,32 +102,35 @@ void executeScanModules(final String line) { assertEquals(0, new ConsoleLauncherWrapper().execute(args1).getTestsFoundCount()); } + private static Stream redirectStreamParams() { + return Stream.of(Arguments.of("--redirect-stdout", StdStreamTestCase.getStdoutOutputFileSize()), + Arguments.of("--redirect-stderr", StdStreamTestCase.getStderrOutputFileSize())); + } + @ParameterizedTest - @ValueSource(strings = { "--redirect-stdout", "--redirect-stderr" }) - void executeWithRedirectedStdStream(String redirectedStream, @TempDir Path tempDir) throws IOException { + @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-method org.junit.platform.console.options.StdStreamTestCase#printTest " - + "%s %s", + 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(20, Files.size(outputFile), "Invalid file size."); + 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-method org.junit.platform.console.options.StdStreamTestCase#printTest " - + "--redirect-stdout %s --redirect-stderr %s", - outputFile, outputFile); + 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(40, Files.size(outputFile), "Invalid file size."); + assertEquals(StdStreamTestCase.getStdoutOutputFileSize() + StdStreamTestCase.getStderrOutputFileSize(), + Files.size(outputFile), "Invalid file size."); } } diff --git a/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java index 7a99168385b0..e3e14ceb1322 100644 --- a/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java +++ b/platform-tests/src/test/java/org/junit/platform/console/options/StdStreamTestCase.java @@ -14,9 +14,20 @@ 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("Writing to STDOUT..."); - System.err.print("Writing to STDERR..."); + System.out.print(STDOUT_DATA); + System.err.print(STDERR_DATA); } }