From f607bf59814bf5a7c7ccdb1cb3b4392b70287c9b Mon Sep 17 00:00:00 2001 From: mobounya Date: Sat, 13 Jan 2024 19:32:12 +0100 Subject: [PATCH 1/4] Add new merged standard streams interceptor. Add method registerMergedStandardStreams in StreamInterceptor to merge stdout and stderr so both can be intercepted in an stdout interceptor. This will keep the relative order for both outputs, which makes it easier to correlate error messages with the corresponding output. Add new configuration parameter junit.platform.output.capture.merge to merge stdout and stderr and publish it as STDOUT_REPORT_ENTRY_KEY to all registered TestExecutionListener instances. Issue: #3166 --- .../platform/launcher/LauncherConstants.java | 15 +++++++++++++ ...reamInterceptingTestExecutionListener.java | 22 ++++++++++++++----- .../launcher/core/StreamInterceptor.java | 6 +++++ ...TestExecutionListenerIntegrationTests.java | 4 +++- 4 files changed, 41 insertions(+), 6 deletions(-) diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java index da4d81625f6..0d2314bb768 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherConstants.java @@ -60,6 +60,21 @@ public class LauncherConstants { */ public static final String CAPTURE_STDERR_PROPERTY_NAME = "junit.platform.output.capture.stderr"; + /** + * Property name used to enable merging and capturing output to {@link System#err} and {@link System#out}: + * {@value} + * + *

If enabled, the JUnit Platform merges stdout and stderr and publishes + * it as a {@link ReportEntry} using the + * {@value #STDOUT_REPORT_ENTRY_KEY} key immediately before reporting the + * test identifier as finished. + * + * @see #STDOUT_REPORT_ENTRY_KEY + * @see ReportEntry + * @see TestExecutionListener#reportingEntryPublished(TestIdentifier, ReportEntry) + */ + public static final String CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME = "junit.platform.output.capture.merge"; + /** * Property name used to configure the maximum number of bytes for buffering * to use per thread and output type if output capturing is enabled: diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java index 32a13589655..a9e7e647f21 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListener.java @@ -12,6 +12,7 @@ import static org.junit.platform.launcher.LauncherConstants.CAPTURE_MAX_BUFFER_DEFAULT; import static org.junit.platform.launcher.LauncherConstants.CAPTURE_MAX_BUFFER_PROPERTY_NAME; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.STDERR_REPORT_ENTRY_KEY; @@ -43,17 +44,28 @@ static Optional create(ConfigurationPar boolean captureStdout = configurationParameters.getBoolean(CAPTURE_STDOUT_PROPERTY_NAME).orElse(false); boolean captureStderr = configurationParameters.getBoolean(CAPTURE_STDERR_PROPERTY_NAME).orElse(false); - if (!captureStdout && !captureStderr) { + boolean captureMergeStandardStreams = configurationParameters.getBoolean( + CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME).orElse(false); + + if (!captureStdout && !captureStderr && !captureMergeStandardStreams) { return Optional.empty(); } int maxSize = configurationParameters.get(CAPTURE_MAX_BUFFER_PROPERTY_NAME, Integer::valueOf) // .orElse(CAPTURE_MAX_BUFFER_DEFAULT); - Optional stdoutInterceptor = captureStdout ? StreamInterceptor.registerStdout(maxSize) - : Optional.empty(); - Optional stderrInterceptor = captureStderr ? StreamInterceptor.registerStderr(maxSize) - : Optional.empty(); + Optional stdoutInterceptor = Optional.empty(); + Optional stderrInterceptor = Optional.empty(); + + if (captureMergeStandardStreams) { + stdoutInterceptor = StreamInterceptor.registerMergedStandardStreams(maxSize); + captureStderr = false; + captureStdout = true; + } + else { + stdoutInterceptor = captureStdout ? StreamInterceptor.registerStdout(maxSize) : Optional.empty(); + stderrInterceptor = captureStderr ? StreamInterceptor.registerStderr(maxSize) : Optional.empty(); + } if ((!stdoutInterceptor.isPresent() && captureStdout) || (!stderrInterceptor.isPresent() && captureStderr)) { stdoutInterceptor.ifPresent(StreamInterceptor::unregister); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java index b34fd72c1a1..150b15880da 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/StreamInterceptor.java @@ -37,6 +37,12 @@ static Optional registerStderr(int maxNumberOfBytesPerThread) return register(System.err, System::setErr, maxNumberOfBytesPerThread); } + static Optional registerMergedStandardStreams(int maxNumberOfBytesPerThread) { + Optional interceptor = registerStdout(maxNumberOfBytesPerThread); + interceptor.ifPresent((System::setErr)); + return interceptor; + } + static Optional register(PrintStream originalStream, Consumer streamSetter, int maxNumberOfBytesPerThread) { if (originalStream instanceof StreamInterceptor) { diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java index 01aacc458db..b6ffc1c049f 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/StreamInterceptingTestExecutionListenerIntegrationTests.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.junit.platform.engine.TestExecutionResult.successful; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.LauncherConstants.CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME; import static org.junit.platform.launcher.LauncherConstants.STDERR_REPORT_ENTRY_KEY; @@ -119,7 +120,8 @@ void doesNotInterceptStreamWhenAlreadyBeingIntercepted(String configParam, private static Stream systemStreams() { return Stream.of(// streamType(CAPTURE_STDOUT_PROPERTY_NAME, () -> System.out, STDOUT_REPORT_ENTRY_KEY), // - streamType(CAPTURE_STDERR_PROPERTY_NAME, () -> System.err, STDERR_REPORT_ENTRY_KEY)); + streamType(CAPTURE_STDERR_PROPERTY_NAME, () -> System.err, STDERR_REPORT_ENTRY_KEY), // + streamType(CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME, () -> System.out, STDOUT_REPORT_ENTRY_KEY)); } private static Arguments streamType(String configParam, Supplier printStreamSupplier, From 933c41c554677e37d07bde5d1a152d43f428b370 Mon Sep 17 00:00:00 2001 From: mobounya Date: Sat, 13 Jan 2024 20:13:35 +0100 Subject: [PATCH 2/4] Add CLI options to redirect stderr and stdout. Add new CLI options --redirect-stdout and --redirect-stderr to redirect stdout and stderr to a file, if both are set to the same file, stdout and stderr will be merged so we can keep the relative order of messages. This makes it easier to correlate error messages with the corresponding output. Issue: #3166 --- .../options/TestConsoleOutputOptions.java | 18 +++ .../TestConsoleOutputOptionsMixin.java | 14 +++ .../console/tasks/ConsoleTestExecutor.java | 47 +++++++- .../RedirectStdoutAndStderrListener.java | 105 ++++++++++++++++++ .../CommandLineOptionsParsingTests.java | 79 ++++++++++--- 5 files changed, 245 insertions(+), 18 deletions(-) create mode 100644 junit-platform-console/src/main/java/org/junit/platform/console/tasks/RedirectStdoutAndStderrListener.java 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 c0ff2aa5b77..66574850d1a 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,20 @@ public void setTheme(Theme theme) { this.theme = theme; } + public Path getStdoutPath() { + return this.stdoutPath; + } + + public void setStdoutPath(Path stdoutPath) { + this.stdoutPath = stdoutPath; + } + + public Path getStderrPath() { + return this.stderrPath; + } + + 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 98e3e0cc556..c2a45f46cf0 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,25 @@ static class ConsoleOutputOptions { @Option(names = "-details-theme", hidden = true) private Theme theme2 = DEFAULT_THEME; + @Option(names = "--redirect-stdout", paramLabel = "FILE", description = "Redirect tests stdout to a file.") + private Path stdout; + + @Option(names = "-redirect-stdout", hidden = true) + private Path stdout2; + + @Option(names = "--redirect-stderr", paramLabel = "FILE", description = "Redirect tests stderr to a file.") + private Path stderr; + + @Option(names = "-redirect-stderr", hidden = true) + private Path stderr2; + 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(choose(stdout, stdout2, null)); + result.setStderrPath(choose(stderr, stderr2, null)); } } 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 1cdddf814e1..2da7c75a233 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 @@ -17,7 +17,9 @@ import java.net.URLClassLoader; import java.nio.file.Path; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.function.Supplier; @@ -29,6 +31,7 @@ import org.junit.platform.console.options.TestDiscoveryOptions; import org.junit.platform.console.options.Theme; import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestPlan; @@ -98,6 +101,24 @@ private TestExecutionSummary executeTests(PrintWriter out, Optional report Launcher launcher = launcherSupplier.get(); SummaryGeneratingListener summaryListener = registerListeners(out, reportsDir, launcher); + if (isSameFile(outputOptions.getStdoutPath(), outputOptions.getStderrPath())) { + captureMergedStandardStreams(); + } + else { + if (outputOptions.getStdoutPath() != null) { + captureStdout(); + } + if (outputOptions.getStderrPath() != null) { + captureStderr(); + } + } + + if (outputOptions.getStdoutPath() != null || outputOptions.getStderrPath() != null) { + TestExecutionListener redirectionListener = new RedirectStdoutAndStderrListener( + outputOptions.getStdoutPath(), outputOptions.getStderrPath(), out); + launcher.registerTestExecutionListeners(redirectionListener); + } + LauncherDiscoveryRequest discoveryRequest = new DiscoveryRequestCreator().toDiscoveryRequest(discoveryOptions); launcher.execute(discoveryRequest); @@ -135,7 +156,7 @@ private SummaryGeneratingListener registerListeners(PrintWriter out, Optional configParameters = new HashMap<>(discoveryOptions.getConfigurationParameters()); + configParameters.put(LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME, "true"); + discoveryOptions.setConfigurationParameters(configParameters); + } + + private void captureStderr() { + Map configParameters = new HashMap<>(discoveryOptions.getConfigurationParameters()); + configParameters.put(LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME, "true"); + discoveryOptions.setConfigurationParameters(configParameters); + } + + private void captureMergedStandardStreams() { + Map configParameters = new HashMap<>(discoveryOptions.getConfigurationParameters()); + configParameters.put(LauncherConstants.CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME, "true"); + discoveryOptions.setConfigurationParameters(configParameters); + } + @FunctionalInterface public interface Factory { ConsoleTestExecutor create(TestDiscoveryOptions discoveryOptions, TestConsoleOutputOptions outputOptions); diff --git a/junit-platform-console/src/main/java/org/junit/platform/console/tasks/RedirectStdoutAndStderrListener.java b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/RedirectStdoutAndStderrListener.java new file mode 100644 index 00000000000..20275b3451d --- /dev/null +++ b/junit-platform-console/src/main/java/org/junit/platform/console/tasks/RedirectStdoutAndStderrListener.java @@ -0,0 +1,105 @@ +/* + * Copyright 2015-2024 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.apiguardian.api.API.Status.INTERNAL; + +import java.io.IOException; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.io.Writer; +import java.nio.file.Files; +import java.nio.file.Path; + +import org.apiguardian.api.API; +import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.launcher.LauncherConstants; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.TestPlan; + +@API(status = INTERNAL, since = "5.11") +public class RedirectStdoutAndStderrListener implements TestExecutionListener { + private final Path stdoutOutputPath; + private final Path stderrOutputPath; + private final StringWriter stdoutBuffer; + private final StringWriter stderrBuffer; + private final PrintWriter out; + + public RedirectStdoutAndStderrListener(Path stdoutOutputPath, Path stderrOutputPath, PrintWriter out) { + this.stdoutOutputPath = stdoutOutputPath; + this.stderrOutputPath = stderrOutputPath; + this.stdoutBuffer = new StringWriter(); + this.stderrBuffer = new StringWriter(); + this.out = out; + } + + public void reportingEntryPublished(TestIdentifier testIdentifier, ReportEntry entry) { + if (testIdentifier.isTest()) { + String redirectedStdoutContent = entry.getKeyValuePairs().get(LauncherConstants.STDOUT_REPORT_ENTRY_KEY); + String redirectedStderrContent = entry.getKeyValuePairs().get(LauncherConstants.STDERR_REPORT_ENTRY_KEY); + + if (redirectedStdoutContent != null && !redirectedStdoutContent.isEmpty()) { + this.stdoutBuffer.append(redirectedStdoutContent); + } + if (redirectedStderrContent != null && !redirectedStderrContent.isEmpty()) { + this.stderrBuffer.append(redirectedStderrContent); + } + } + } + + public void testPlanExecutionFinished(TestPlan testPlan) { + if (stdoutBuffer.getBuffer().length() > 0) { + flushBufferedOutputToFile(this.stdoutOutputPath, this.stdoutBuffer); + } + if (stderrBuffer.getBuffer().length() > 0) { + flushBufferedOutputToFile(this.stderrOutputPath, this.stderrBuffer); + } + } + + private void flushBufferedOutputToFile(Path file, StringWriter buffer) { + deleteFile(file); + createFile(file); + writeContentToFile(file, buffer.toString()); + } + + private void writeContentToFile(Path file, String buffer) { + try (Writer fileWriter = Files.newBufferedWriter(file)) { + fileWriter.write(buffer); + } + catch (IOException e) { + printException("Failed to write content to file: " + file, e); + } + } + + private void deleteFile(Path file) { + try { + Files.deleteIfExists(file); + } + catch (IOException e) { + printException("Failed to delete file: " + file, e); + } + } + + private void createFile(Path file) { + try { + Files.createFile(file); + } + catch (IOException e) { + printException("Failed to create file: " + file, e); + } + } + + private void printException(String message, Exception exception) { + out.println(message); + exception.printStackTrace(out); + } +} 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 0dd2f08502a..17ea158dcdf 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; @@ -56,23 +57,25 @@ void parseNoArguments() { // @formatter:off assertAll( () -> assertFalse(options.output.isAnsiColorOutputDisabled()), - () -> assertEquals(TestConsoleOutputOptions.DEFAULT_DETAILS, options.output.getDetails()), - () -> assertFalse(options.discovery.isScanClasspath()), - () -> assertEquals(List.of(STANDARD_INCLUDE_PATTERN), options.discovery.getIncludedClassNamePatterns()), - () -> assertEquals(List.of(), options.discovery.getExcludedClassNamePatterns()), - () -> assertEquals(List.of(), options.discovery.getIncludedPackages()), - () -> assertEquals(List.of(), options.discovery.getExcludedPackages()), - () -> assertEquals(List.of(), options.discovery.getIncludedTagExpressions()), - () -> assertEquals(List.of(), options.discovery.getExcludedTagExpressions()), - () -> assertEquals(List.of(), options.discovery.getAdditionalClasspathEntries()), - () -> assertEquals(List.of(), options.discovery.getSelectedUris()), - () -> assertEquals(List.of(), options.discovery.getSelectedFiles()), - () -> assertEquals(List.of(), options.discovery.getSelectedDirectories()), - () -> assertEquals(List.of(), options.discovery.getSelectedModules()), - () -> assertEquals(List.of(), options.discovery.getSelectedPackages()), - () -> assertEquals(List.of(), options.discovery.getSelectedMethods()), - () -> assertEquals(List.of(), options.discovery.getSelectedClasspathEntries()), - () -> assertEquals(Map.of(), options.discovery.getConfigurationParameters()) + () -> 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()), + () -> assertEquals(List.of(), options.discovery.getExcludedClassNamePatterns()), + () -> assertEquals(List.of(), options.discovery.getIncludedPackages()), + () -> assertEquals(List.of(), options.discovery.getExcludedPackages()), + () -> assertEquals(List.of(), options.discovery.getIncludedTagExpressions()), + () -> assertEquals(List.of(), options.discovery.getExcludedTagExpressions()), + () -> assertEquals(List.of(), options.discovery.getAdditionalClasspathEntries()), + () -> assertEquals(List.of(), options.discovery.getSelectedUris()), + () -> assertEquals(List.of(), options.discovery.getSelectedFiles()), + () -> assertEquals(List.of(), options.discovery.getSelectedDirectories()), + () -> assertEquals(List.of(), options.discovery.getSelectedModules()), + () -> assertEquals(List.of(), options.discovery.getSelectedPackages()), + () -> assertEquals(List.of(), options.discovery.getSelectedMethods()), + () -> assertEquals(List.of(), options.discovery.getSelectedClasspathEntries()), + () -> assertEquals(Map.of(), options.discovery.getConfigurationParameters()) ); // @formatter:on } @@ -541,6 +544,48 @@ 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 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", "-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 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", "-redirect-stderr"); + } + @ParameterizedTest @EnumSource void parseInvalidConfigurationParametersWithDuplicateKey(ArgsType type) { From 1f2bd90b9a957cfd1a1821c69529b9884696a55d Mon Sep 17 00:00:00 2001 From: mobounya Date: Sat, 13 Jan 2024 21:43:17 +0100 Subject: [PATCH 3/4] Document output redirecion changes. Document the changes for the previous two commit in the 5.11.0-M1 release notes and in the user guide. Issue: #3166 --- .../docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc | 4 +++- .../src/docs/asciidoc/user-guide/running-tests.adoc | 5 ++++- .../junit/platform/console/tasks/ConsoleTestExecutor.java | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc index 5e6ade32021..d01e4b4b900 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc @@ -30,7 +30,9 @@ repository on GitHub. -- for use in third-party extensions and test engines. * Error messages for type mismatches in `NamespacedHierarchicalStore` now include the actual type and value in addition to the required type. - +* New optional config parameter `junit.platform.output.capture.merge` that can be used to merge stdout and stderr and + capture it as stdout. +* New optional CLI options `--redirect-stdout` and `--redirect-stderr` to redirect stdout and stderr outputs to a file. [[release-notes-5.11.0-M1-junit-jupiter]] === JUnit Jupiter diff --git a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc index 1900545858f..dc1f234ea42 100644 --- a/documentation/src/docs/asciidoc/user-guide/running-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/running-tests.adoc @@ -1019,6 +1019,8 @@ expressions can be useful. Since version 1.3, the JUnit Platform provides opt-in support for capturing output printed to `System.out` and `System.err`. To enable it, set the `junit.platform.output.capture.stdout` and/or `junit.platform.output.capture.stderr` +<> to `true`. And since version 5.11 +you can capture a merged stdout/stderr output by setting the `junit.platform.output.capture.merge` <> to `true`. In addition, you may configure the maximum number of buffered bytes to be used per executed test or container using `junit.platform.output.capture.maxBuffer`. @@ -1026,7 +1028,8 @@ using `junit.platform.output.capture.maxBuffer`. If enabled, the JUnit Platform captures the corresponding output and publishes it as a report entry using the `stdout` or `stderr` keys to all registered `{TestExecutionListener}` instances immediately before reporting the test or container as -finished. +finished, in case of a `junit.platform.output.capture.merge` <>, +the merged output will be published as a report entry using the `stdout` key. Please note that the captured output will only contain output emitted by the thread that was used to execute a container or test. Any output by other threads will be omitted 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 2da7c75a233..f2387305203 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 @@ -156,7 +156,7 @@ private SummaryGeneratingListener registerListeners(PrintWriter out, Optional Date: Sun, 14 Jan 2024 13:58:10 +0100 Subject: [PATCH 4/4] Annotate new methods with @API annotation Add @API annotation to new methods. Issue: #3166 --- .../platform/console/options/TestConsoleOutputOptions.java | 4 ++++ .../org/junit/platform/console/tasks/ConsoleTestExecutor.java | 4 ++++ 2 files changed, 8 insertions(+) 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 66574850d1a..3e28f96d021 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,18 +75,22 @@ public void setTheme(Theme theme) { this.theme = theme; } + @API(status = INTERNAL, since = "5.11") public Path getStdoutPath() { return this.stdoutPath; } + @API(status = INTERNAL, since = "5.11") public void setStdoutPath(Path stdoutPath) { this.stdoutPath = stdoutPath; } + @API(status = INTERNAL, since = "5.11") public Path getStderrPath() { return this.stderrPath; } + @API(status = INTERNAL, since = "5.11") public void setStderrPath(Path stderrPath) { this.stderrPath = stderrPath; } 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 f2387305203..5e0206e5283 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 @@ -206,24 +206,28 @@ private void printSummary(TestExecutionSummary summary, PrintWriter out) { summary.printTo(out); } + @API(status = INTERNAL, since = "5.11") private boolean isSameFile(Path path1, Path path2) { if (path1 == null || path2 == null) return false; return (path1.normalize().toAbsolutePath().equals(path2.normalize().toAbsolutePath())); } + @API(status = INTERNAL, since = "5.11") private void captureStdout() { Map configParameters = new HashMap<>(discoveryOptions.getConfigurationParameters()); configParameters.put(LauncherConstants.CAPTURE_STDOUT_PROPERTY_NAME, "true"); discoveryOptions.setConfigurationParameters(configParameters); } + @API(status = INTERNAL, since = "5.11") private void captureStderr() { Map configParameters = new HashMap<>(discoveryOptions.getConfigurationParameters()); configParameters.put(LauncherConstants.CAPTURE_STDERR_PROPERTY_NAME, "true"); discoveryOptions.setConfigurationParameters(configParameters); } + @API(status = INTERNAL, since = "5.11") private void captureMergedStandardStreams() { Map configParameters = new HashMap<>(discoveryOptions.getConfigurationParameters()); configParameters.put(LauncherConstants.CAPTURE_MERGED_STANDARD_STREAMS_PROPERTY_NAME, "true");