Skip to content

Commit 4655366

Browse files
Copilotlaeubi
authored andcommitted
Add socket support to OpenTestReportGeneratingListener
- Add SOCKET_PROPERTY_NAME constant for junit.platform.reporting.open.xml.socket - Implement createDocumentWriter method to support both file and socket outputs - When socket property is set, connect to localhost:<port> and write XML to socket - Add comprehensive test for socket-based reporting - Update documentation to describe socket configuration parameter - Update release notes with new feature Co-authored-by: laeubi <[email protected]>
1 parent debc12f commit 4655366

File tree

4 files changed

+110
-4
lines changed

4 files changed

+110
-4
lines changed

documentation/src/docs/asciidoc/release-notes/release-notes-6.1.0-M1.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ repository on GitHub.
2828

2929
* Support for creating a `ModuleSelector` from a `java.lang.Module` and using
3030
its classloader for test discovery.
31+
* `OpenTestReportGeneratingListener` now supports redirecting XML events to a socket via
32+
the new `junit.platform.reporting.open.xml.socket` configuration parameter. When set to a
33+
port number, events are sent to `localhost:<port>` instead of being written to a file.
3134

3235

3336
[[release-notes-6.1.0-M1-junit-jupiter]]

documentation/src/docs/asciidoc/user-guide/advanced-topics/junit-platform-reporting.adoc

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,15 @@ The listener is auto-registered and can be configured via the following
4242
Enable/disable writing the report; defaults to `false`.
4343
`junit.platform.reporting.open.xml.git.enabled=true|false`::
4444
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`.
45+
`junit.platform.reporting.open.xml.socket=<port>`::
46+
Optional port number to redirect events to a socket instead of a file. When specified, the
47+
listener will connect to `localhost:<port>` and send the XML events to the socket. The socket
48+
connection is automatically closed when the test execution completes.
4549

4650
If enabled, the listener creates an XML report file named `open-test-report.xml` in the
47-
configured <<junit-platform-reporting-output-directory, output directory>>.
51+
configured <<junit-platform-reporting-output-directory, output directory>>, unless the
52+
`junit.platform.reporting.open.xml.socket` configuration parameter is set, in which case the
53+
events are sent to the specified socket instead.
4854

4955
If <<running-tests-capturing-output, output capturing>> is enabled, the captured output
5056
written to `System.out` and `System.err` will be included in the report as well.

junit-platform-reporting/src/main/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListener.java

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,13 @@
5353
import static org.opentest4j.reporting.events.root.RootFactory.started;
5454

5555
import java.io.IOException;
56+
import java.io.OutputStreamWriter;
5657
import java.io.UncheckedIOException;
58+
import java.io.Writer;
5759
import java.net.InetAddress;
60+
import java.net.Socket;
5861
import java.net.UnknownHostException;
62+
import java.nio.charset.StandardCharsets;
5963
import java.nio.file.Path;
6064
import java.time.Instant;
6165
import java.time.LocalDateTime;
@@ -102,6 +106,7 @@ public class OpenTestReportGeneratingListener implements TestExecutionListener {
102106

103107
static final String ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.enabled";
104108
static final String GIT_ENABLED_PROPERTY_NAME = "junit.platform.reporting.open.xml.git.enabled";
109+
static final String SOCKET_PROPERTY_NAME = "junit.platform.reporting.open.xml.socket";
105110

106111
private final AtomicInteger idCounter = new AtomicInteger();
107112
private final Map<UniqueId, String> inProgressIds = new ConcurrentHashMap<>();
@@ -130,17 +135,46 @@ public void testPlanExecutionStarted(TestPlan testPlan) {
130135
.add("junit", JUnitFactory.NAMESPACE, "https://schemas.junit.org/open-test-reporting/junit-1.9.xsd") //
131136
.build();
132137
outputDir = testPlan.getOutputDirectoryCreator().getRootDirectory();
133-
Path eventsXml = outputDir.resolve("open-test-report.xml");
134138
try {
135-
eventsFileWriter = Events.createDocumentWriter(namespaceRegistry, eventsXml);
139+
eventsFileWriter = createDocumentWriter(config, namespaceRegistry);
136140
reportInfrastructure(config);
137141
}
138142
catch (Exception e) {
139-
throw new JUnitException("Failed to initialize XML events file: " + eventsXml, e);
143+
throw new JUnitException("Failed to initialize XML events writer", e);
140144
}
141145
}
142146
}
143147

148+
private DocumentWriter<Events> createDocumentWriter(ConfigurationParameters config,
149+
NamespaceRegistry namespaceRegistry) throws Exception {
150+
return config.get(SOCKET_PROPERTY_NAME) //
151+
.map(portString -> {
152+
try {
153+
int port = Integer.parseInt(portString);
154+
Socket socket = new Socket("localhost", port);
155+
Writer writer = new OutputStreamWriter(socket.getOutputStream(), StandardCharsets.UTF_8);
156+
return Events.createDocumentWriter(namespaceRegistry, writer);
157+
}
158+
catch (NumberFormatException e) {
159+
throw new JUnitException(
160+
"Invalid port number for socket configuration: " + portString + ". Expected an integer.",
161+
e);
162+
}
163+
catch (Exception e) {
164+
throw new JUnitException("Failed to connect to socket on port: " + portString, e);
165+
}
166+
}) //
167+
.orElseGet(() -> {
168+
try {
169+
Path eventsXml = requireNonNull(outputDir).resolve("open-test-report.xml");
170+
return Events.createDocumentWriter(namespaceRegistry, eventsXml);
171+
}
172+
catch (Exception e) {
173+
throw new JUnitException("Failed to create XML events file", e);
174+
}
175+
});
176+
}
177+
144178
private boolean isEnabled(ConfigurationParameters config) {
145179
return config.getBoolean(ENABLED_PROPERTY_NAME).orElse(false);
146180
}

platform-tests/src/test/java/org/junit/platform/reporting/open/xml/OpenTestReportGeneratingListenerTests.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,22 @@
2424
import static org.junit.platform.launcher.core.LauncherFactoryForTestingPurposesOnly.createLauncher;
2525
import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.ENABLED_PROPERTY_NAME;
2626
import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.GIT_ENABLED_PROPERTY_NAME;
27+
import static org.junit.platform.reporting.open.xml.OpenTestReportGeneratingListener.SOCKET_PROPERTY_NAME;
2728
import static org.junit.platform.reporting.testutil.FileUtils.findPath;
2829

30+
import java.io.BufferedReader;
31+
import java.io.InputStreamReader;
2932
import java.io.PrintStream;
33+
import java.net.ServerSocket;
34+
import java.net.Socket;
3035
import java.net.URISyntaxException;
36+
import java.nio.charset.StandardCharsets;
3137
import java.nio.file.Files;
3238
import java.nio.file.Path;
3339
import java.util.Map;
40+
import java.util.concurrent.CountDownLatch;
41+
import java.util.concurrent.TimeUnit;
42+
import java.util.concurrent.atomic.AtomicReference;
3443

3544
import org.junit.jupiter.api.AfterEach;
3645
import org.junit.jupiter.api.BeforeEach;
@@ -311,4 +320,58 @@ private static void executeTests(Path tempDirectory, TestEngine engine, Path out
311320
createLauncher(engine).execute(request);
312321
}
313322

323+
@Test
324+
void writesXmlReportToSocket(@TempDir Path tempDirectory) throws Exception {
325+
var engine = new DemoHierarchicalTestEngine("dummy");
326+
engine.addTest("test1", "Test 1", (context, descriptor) -> {
327+
// Simple test
328+
});
329+
330+
// Start a server socket to receive the XML
331+
var xmlContent = new AtomicReference<String>();
332+
var latch = new CountDownLatch(1);
333+
334+
try (var serverSocket = new ServerSocket(0)) { // Use any available port
335+
int port = serverSocket.getLocalPort();
336+
337+
// Start a daemon thread to accept the connection and read the XML
338+
Thread serverThread = new Thread(() -> {
339+
try (Socket clientSocket = serverSocket.accept();
340+
var reader = new BufferedReader(
341+
new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8))) {
342+
var builder = new StringBuilder();
343+
String line;
344+
while ((line = reader.readLine()) != null) {
345+
builder.append(line).append("\n");
346+
}
347+
xmlContent.set(builder.toString());
348+
}
349+
catch (Exception e) {
350+
// Only throw if not interrupted during cleanup
351+
if (!Thread.currentThread().isInterrupted()) {
352+
throw new RuntimeException(e);
353+
}
354+
}
355+
finally {
356+
latch.countDown();
357+
}
358+
});
359+
serverThread.setDaemon(true);
360+
serverThread.start();
361+
362+
// Execute tests with socket configuration
363+
executeTests(tempDirectory, engine, tempDirectory.resolve("junit-reports"),
364+
Map.of(SOCKET_PROPERTY_NAME, String.valueOf(port)));
365+
366+
// Wait for the server to receive the data
367+
assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue();
368+
369+
// Verify XML was received
370+
assertThat(xmlContent.get()).isNotNull();
371+
assertThat(xmlContent.get()).contains("<e:events");
372+
assertThat(xmlContent.get()).contains("dummy");
373+
assertThat(xmlContent.get()).contains("Test 1");
374+
}
375+
}
376+
314377
}

0 commit comments

Comments
 (0)