diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc index ee0700a3c59f..a42b54393899 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc @@ -28,6 +28,9 @@ repository on GitHub. * Support for creating a `ModuleSelector` from a `java.lang.Module` and using its classloader for test discovery. +* `OpenTestReportGeneratingListener` now supports redirecting XML events to a socket via + the new `junit.platform.reporting.open.xml.socket` configuration parameter. When set to a + port number, events are sent to `127.0.0.1:` instead of being written to a file. [[release-notes-6.1.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 66851d49dd9b..10f7744f54b0 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 @@ -42,9 +42,15 @@ The listener is auto-registered and can be configured via the following Enable/disable writing the report; defaults to `false`. `junit.platform.reporting.open.xml.git.enabled=true|false`:: Enable/disable including information about the Git repository (see https://github.com/ota4j-team/open-test-reporting#git[Git extension schema] of open-test-reporting); defaults to `false`. +`junit.platform.reporting.open.xml.socket=`:: + Optional port number to redirect events to a socket instead of a file. When specified, the + listener will connect to `127.0.0.1:` and send the XML events to the socket. The socket + connection is automatically closed when the test execution completes. If enabled, the listener creates an XML report file named `open-test-report.xml` in the -configured <>. +configured <>, unless the +`junit.platform.reporting.open.xml.socket` configuration parameter is set, in which case the +events are sent to the specified socket instead. If <> is enabled, the captured output written to `System.out` and `System.err` will be included in the report as well. diff --git a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java index 5037090412a1..76e40bd0b249 100644 --- a/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java +++ b/junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java @@ -53,9 +53,13 @@ import static org.opentest4j.reporting.events.root.RootFactory.started; import java.io.IOException; +import java.io.OutputStreamWriter; import java.io.UncheckedIOException; +import java.io.Writer; import java.net.InetAddress; +import java.net.Socket; import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.time.Instant; import java.time.LocalDateTime; @@ -102,6 +106,7 @@ public class OpenTestReportGeneratingListener implements TestExecutionListener { static final String ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.enabled"; static final String GIT_ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.git.enabled"; + static final String SOCKET_PROPERTY_NAME = "junit.platform.reporting.open.xml.socket"; private final AtomicInteger idCounter = new AtomicInteger(); private final Map inProgressIds = new ConcurrentHashMap<>(); @@ -130,17 +135,40 @@ public void testPlanExecutionStarted(TestPlan testPlan) { .add("junit", JUnitFactory.NAMESPACE, "https://schemas.junit.org/open-test-reporting/junit-1.9.xsd") // .build(); outputDir = testPlan.getOutputDirectoryCreator().getRootDirectory(); - Path eventsXml = outputDir.resolve("open-test-report.xml"); try { - eventsFileWriter = Events.createDocumentWriter(namespaceRegistry, eventsXml); + eventsFileWriter = createDocumentWriter(config, namespaceRegistry); reportInfrastructure(config); } catch (Exception e) { - throw new JUnitException("Failed to initialize XML events file: " + eventsXml, e); + throw new JUnitException("Failed to initialize XML events writer", e); } } } + private DocumentWriter createDocumentWriter(ConfigurationParameters config, + NamespaceRegistry namespaceRegistry) throws Exception { + return config.get(SOCKET_PROPERTY_NAME, Integer::valueOf) // + .map(port -> { + try { + Socket socket = new Socket(InetAddress.getLoopbackAddress(), port); + Writer writer = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8); + return Events.createDocumentWriter(namespaceRegistry, writer); + } + catch (Exception e) { + throw new JUnitException("Failed to connect to socket on port " + port, e); + } + }) // + .orElseGet(() -> { + try { + Path eventsXml = requireNonNull(outputDir).resolve("open-test-report.xml"); + return Events.createDocumentWriter(namespaceRegistry, eventsXml); + } + catch (Exception e) { + throw new JUnitException("Failed to create XML events file", e); + } + }); + } + private boolean isEnabled(ConfigurationParameters config) { return config.getBoolean(ENABLED_PROPERTY_NAME).orElse(false); } diff --git a/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java b/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java index 686844b0857e..5db92bbd1aa5 100644 --- a/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java +++ b/platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java @@ -24,12 +24,20 @@ import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher; import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.ENABLED_PROPERTY_NAME; import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.GIT_ENABLED_PROPERTY_NAME; +import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.SOCKET_PROPERTY_NAME; import static org.junit.platform.reporting.testutil.FileUtils.findPath; +import java.io.BufferedReader; +import java.io.InputStreamReader; import java.io.PrintStream; +import java.net.InetAddress; +import java.net.ServerSocket; +import java.net.Socket; import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.time.Duration; import java.util.Map; import org.junit.jupiter.api.AfterEach; @@ -265,6 +273,90 @@ void stripsCredentialsFromOriginUrl(String configuredUrl, String reportedUrl, @T .isEqualTo(reportedUrl); } + @Test + void writesXmlReportToSocket(@TempDir Path tempDirectory) throws Exception { + var engine = new DemoHierarchicalTestEngine("dummy"); + engine.addTest("test1", "Test 1", (context, descriptor) -> { + // Simple test + }); + + // Start a server socket to receive the XML + var builder = new StringBuilder(); + + try (var serverSocket = new ServerSocket(0, 50, InetAddress.getLoopbackAddress())) { // Use any available port + int port = serverSocket.getLocalPort(); + + // Start a daemon thread to accept the connection and read the XML + Thread serverThread = new Thread(() -> { + try (Socket clientSocket = serverSocket.accept(); + var reader = new BufferedReader( + new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + builder.append(line).append("\n"); + } + } + catch (Exception e) { + fail(e); + } + }); + serverThread.setDaemon(true); + serverThread.start(); + + // Execute tests with socket configuration + executeTests(tempDirectory, engine, tempDirectory.resolve("junit-reports"), + Map.of(SOCKET_PROPERTY_NAME, String.valueOf(port))); + + // Wait for the server to receive the data + assertThat(serverThread.join(Duration.ofSeconds(10))).isTrue(); + + // Verify XML was received + var expected = """ + + + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + ${xmlunit.ignore} + + + + + [engine:dummy] + dummy + CONTAINER + + + + + [engine:dummy]/[test:test1] + Test 1 + TEST + + + + + + + + + + """; + XmlAssert.assertThat(builder.toString()).and(expected) // + .withDifferenceEvaluator(new PlaceholderDifferenceEvaluator()) // + .ignoreWhitespace() // + .areIdentical(); + } + } + private static XmlAssert assertThatXml(Path xmlFile) { return XmlAssert.assertThat(xmlFile) // .withNamespaceContext(NAMESPACE_CONTEXT);