Skip to content

Commit 468ce37

Browse files
Provide mechanism for managing resources across engines and executions
Introduce resource management mechanism that allows preparing and sharing state across executions or test engines via stores that are scoped to a `LauncherSession` or `ExecutionRequest`. The Jupiter API uses these stores as ancestors to the `Store` instances accessible via `ExtensionContext` and provides a new method to access them directly. The Suite engine passes along the request-scoped store and `EngineTestKit` mimicks `DefaultLauncher` by creating both stores. Resolves #2816. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent 2fea94c commit 468ce37

File tree

64 files changed

+1493
-230
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1493
-230
lines changed

Diff for: documentation/src/docs/asciidoc/link-attributes.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ endif::[]
4646
:IterationSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector]
4747
:MethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/MethodSelector.html[MethodSelector]
4848
:ModuleSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector]
49+
:NamespacedHierarchicalStore: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.html[NamespacedHierarchicalStore]
4950
:NestedClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector]
5051
:NestedMethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector]
5152
:OutputDirectoryProvider: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/reporting/OutputDirectoryProvider.html[OutputDirectoryProvider]

Diff for: documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc

+9-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,15 @@ repository on GitHub.
2626
[[release-notes-5.13.0-M3-junit-platform-new-features-and-improvements]]
2727
==== New Features and Improvements
2828

29-
* ❓
29+
* Introduce resource management mechanism that allows preparing and sharing state across
30+
executions or test engines via stores that are scoped to a `LauncherSession` or
31+
`ExecutionRequest`. The Jupiter API uses these stores as ancestors to the `Store`
32+
instances accessible via `ExtensionContext` and provides a new method to access them
33+
directly. Please refer to the User Guide for examples of managing
34+
<<../user-guide/index.adoc#launcher-api-launcher-session-listeners-tool-example-usage, session-scoped>>
35+
and
36+
<<../user-guide/index.adoc#launcher-api-managing-state-across-test-engines, request-scoped>>
37+
resources.
3038

3139

3240
[[release-notes-5.13.0-M3-junit-jupiter]]

Diff for: documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc

+76-8
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
:testDir: ../../../../../src/test/java
2+
:testResourcesDir: ../../../../../src/test/resources
3+
14
[[launcher-api]]
25
=== JUnit Platform Launcher API
36

@@ -132,10 +135,22 @@ package example.session;
132135
133136
include::{testDir}/example/session/GlobalSetupTeardownListener.java[tags=user_guide]
134137
----
135-
<1> Start the HTTP server
136-
<2> Export its host address as a system property for consumption by tests
137-
<3> Export its port as a system property for consumption by tests
138-
<4> Stop the HTTP server
138+
<1> Get the store from the launcher session
139+
<2> Lazily create the HTTP server and put it into the store
140+
<3> Start the HTTP server
141+
142+
It uses a wrapper class to ensure the server is stopped when the launcher session is
143+
closed:
144+
145+
[source,java]
146+
.src/test/java/example/session/CloseableHttpServer.java
147+
----
148+
package example.session;
149+
150+
include::{testDir}/example/session/CloseableHttpServer.java[tags=user_guide]
151+
----
152+
<1> The `close()` method is called when the launcher session is closed
153+
<2> Stop the HTTP server
139154

140155
This sample uses the HTTP server implementation from the jdk.httpserver module that comes
141156
with the JDK but would work similarly with any other server or resource. In order for the
@@ -158,10 +173,11 @@ package example.session;
158173
159174
include::{testDir}/example/session/HttpTests.java[tags=user_guide]
160175
----
161-
<1> Read the host address of the server from the system property set by the listener
162-
<2> Read the port of the server from the system property set by the listener
163-
<3> Send a request to the server
164-
<4> Check the status code of the response
176+
<1> Retrieve the HTTP server instance from the store
177+
<2> Get the host string directly from the injected HTTP server instance
178+
<3> Get the port number directly from the injected HTTP server instance
179+
<4> Send a request to the server
180+
<5> Check the status code of the response
165181

166182
[[launcher-api-launcher-interceptors-custom]]
167183
==== Registering a LauncherInterceptor
@@ -285,3 +301,55 @@ execute any tests but will notify registered `{TestExecutionListener}` instances
285301
tests had been skipped and their containers had been successful. This can be useful to
286302
test changes in the configuration of a build or to verify a listener is called as expected
287303
without having to wait for all tests to be executed.
304+
305+
[[launcher-api-managing-state-across-test-engines]]
306+
==== Managing State Across Test Engines
307+
308+
When running tests on the JUnit Platform, multiple test engines may need to access shared
309+
resources. Rather than initializing these resources multiple times, JUnit Platform
310+
provides mechanisms to share state across test engines efficiently. Test engines can use
311+
the Platform's `{NamespacedHierarchicalStore}` API to lazily initialize and share
312+
resources, ensuring they are created only once regardless of execution order. Any resource
313+
that is put into the store and implements `AutoCloseable` will be closed automatically when
314+
the execution is finished.
315+
316+
TIP: The Jupiter engine allows read and write access to such resources via its
317+
`{ExtensionContext_Store}` API.
318+
319+
The following example demonstrates two custom test engines sharing a `ServerSocket`
320+
resource. `FirstCustomEngine` attempts to retrieve an existing `ServerSocket` from the
321+
global store or creates a new one if it doesn't exist:
322+
323+
[source,java]
324+
----
325+
include::{testDir}/example/FirstCustomEngine.java[tags=user_guide]
326+
----
327+
328+
`SecondCustomEngine` follows the same pattern, ensuring that regardless whether it runs
329+
before or after `FirstCustomEngine`, it will use the same socket instance:
330+
331+
[source,java]
332+
----
333+
include::{testDir}/example/SecondCustomEngine.java[tags=user_guide]
334+
----
335+
336+
TIP: In this case, the `ServerSocket` can be stored directly in the global store while
337+
ensuring since it gets closed because it implements `AutoCloseable`. If you need to use a
338+
type that does not do so, you can wrap it in a custom class that implements
339+
`AutoCloseable` and delegates to the original type. This is important to ensure that the
340+
resource is closed properly when the test run is finished.
341+
342+
For illustration, the following test verifies that both engines are sharing the same
343+
`ServerSocket` instance and that it's closed after `Launcher.execute()` returns:
344+
345+
[source,java,indent=0]
346+
----
347+
include::{testDir}/example/sharedresources/SharedResourceDemo.java[tags=user_guide]
348+
----
349+
350+
By using the Platform's `{NamespacedHierarchicalStore}` API with shared namespaces in this
351+
way, test engines can coordinate resource creation and sharing without direct dependencies
352+
between them.
353+
354+
Alternatively, it's possible to inject resources into test engines by
355+
<<launcher-api-launcher-session-listeners-custom, registering a `LauncherSessionListener`>>.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
//tag::user_guide[]
14+
import static java.net.InetAddress.getLoopbackAddress;
15+
import static org.junit.platform.engine.TestExecutionResult.successful;
16+
17+
import java.io.IOException;
18+
import java.io.UncheckedIOException;
19+
import java.net.ServerSocket;
20+
21+
import org.junit.platform.engine.EngineDiscoveryRequest;
22+
import org.junit.platform.engine.ExecutionRequest;
23+
import org.junit.platform.engine.TestDescriptor;
24+
import org.junit.platform.engine.TestEngine;
25+
import org.junit.platform.engine.UniqueId;
26+
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
27+
import org.junit.platform.engine.support.store.Namespace;
28+
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
29+
30+
/**
31+
* First custom test engine implementation.
32+
*/
33+
public class FirstCustomEngine implements TestEngine {
34+
35+
public ServerSocket socket;
36+
37+
@Override
38+
public String getId() {
39+
return "first-custom-test-engine";
40+
}
41+
42+
@Override
43+
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
44+
return new EngineDescriptor(uniqueId, "First Custom Test Engine");
45+
}
46+
47+
@Override
48+
public void execute(ExecutionRequest request) {
49+
request.getEngineExecutionListener()
50+
// tag::custom_line_break[]
51+
.executionStarted(request.getRootTestDescriptor());
52+
53+
NamespacedHierarchicalStore<Namespace> store = request.getStore();
54+
socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
55+
try {
56+
return new ServerSocket(0, 50, getLoopbackAddress());
57+
}
58+
catch (IOException e) {
59+
throw new UncheckedIOException("Failed to start ServerSocket", e);
60+
}
61+
}, ServerSocket.class);
62+
63+
request.getEngineExecutionListener()
64+
// tag::custom_line_break[]
65+
.executionFinished(request.getRootTestDescriptor(), successful());
66+
}
67+
}
68+
//end::user_guide[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
import static java.net.InetAddress.getLoopbackAddress;
14+
import static org.junit.platform.engine.TestExecutionResult.successful;
15+
16+
import java.io.IOException;
17+
import java.io.UncheckedIOException;
18+
import java.net.ServerSocket;
19+
20+
import org.junit.platform.engine.EngineDiscoveryRequest;
21+
import org.junit.platform.engine.ExecutionRequest;
22+
import org.junit.platform.engine.TestDescriptor;
23+
import org.junit.platform.engine.TestEngine;
24+
import org.junit.platform.engine.UniqueId;
25+
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
26+
import org.junit.platform.engine.support.store.Namespace;
27+
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
28+
29+
//tag::user_guide[]
30+
/**
31+
* Second custom test engine implementation.
32+
*/
33+
public class SecondCustomEngine implements TestEngine {
34+
35+
public ServerSocket socket;
36+
37+
@Override
38+
public String getId() {
39+
return "second-custom-test-engine";
40+
}
41+
42+
@Override
43+
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
44+
return new EngineDescriptor(uniqueId, "Second Custom Test Engine");
45+
}
46+
47+
@Override
48+
public void execute(ExecutionRequest request) {
49+
request.getEngineExecutionListener()
50+
// tag::custom_line_break[]
51+
.executionStarted(request.getRootTestDescriptor());
52+
53+
NamespacedHierarchicalStore<Namespace> store = request.getStore();
54+
socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
55+
try {
56+
return new ServerSocket(0, 50, getLoopbackAddress());
57+
}
58+
catch (IOException e) {
59+
throw new UncheckedIOException("Failed to start ServerSocket", e);
60+
}
61+
}, ServerSocket.class);
62+
63+
request.getEngineExecutionListener()
64+
// tag::custom_line_break[]
65+
.executionFinished(request.getRootTestDescriptor(), successful());
66+
}
67+
}
68+
//end::user_guide[]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example.session;
12+
13+
//tag::user_guide[]
14+
import java.util.concurrent.ExecutorService;
15+
16+
import com.sun.net.httpserver.HttpServer;
17+
18+
public class CloseableHttpServer implements AutoCloseable {
19+
20+
private final HttpServer server;
21+
private final ExecutorService executorService;
22+
23+
CloseableHttpServer(HttpServer server, ExecutorService executorService) {
24+
this.server = server;
25+
this.executorService = executorService;
26+
}
27+
28+
public HttpServer getServer() {
29+
return server;
30+
}
31+
32+
@Override
33+
public void close() { // <1>
34+
server.stop(0); // <2>
35+
executorService.shutdownNow();
36+
}
37+
}
38+
//end::user_guide[]

Diff for: documentation/src/test/java/example/session/GlobalSetupTeardownListener.java

+22-44
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,15 @@
2121

2222
import com.sun.net.httpserver.HttpServer;
2323

24+
import org.junit.platform.engine.support.store.Namespace;
25+
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
2426
import org.junit.platform.launcher.LauncherSession;
2527
import org.junit.platform.launcher.LauncherSessionListener;
2628
import org.junit.platform.launcher.TestExecutionListener;
2729
import org.junit.platform.launcher.TestPlan;
2830

2931
public class GlobalSetupTeardownListener implements LauncherSessionListener {
3032

31-
private Fixture fixture;
32-
3333
@Override
3434
public void launcherSessionOpened(LauncherSession session) {
3535
// Avoid setup for test discovery by delaying it until tests are about to be executed
@@ -42,50 +42,28 @@ public void testPlanExecutionStarted(TestPlan testPlan) {
4242
return;
4343
}
4444
//tag::user_guide[]
45-
if (fixture == null) {
46-
fixture = new Fixture();
47-
fixture.setUp();
48-
}
49-
}
50-
});
51-
}
45+
NamespacedHierarchicalStore<Namespace> store = session.getStore(); // <1>
46+
store.getOrComputeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2>
47+
InetSocketAddress address = new InetSocketAddress(getLoopbackAddress(), 0);
48+
HttpServer server;
49+
try {
50+
server = HttpServer.create(address, 0);
51+
}
52+
catch (IOException e) {
53+
throw new UncheckedIOException("Failed to start HTTP server", e);
54+
}
55+
server.createContext("/test", exchange -> {
56+
exchange.sendResponseHeaders(204, -1);
57+
exchange.close();
58+
});
59+
ExecutorService executorService = Executors.newCachedThreadPool();
60+
server.setExecutor(executorService);
61+
server.start(); // <3>
5262

53-
@Override
54-
public void launcherSessionClosed(LauncherSession session) {
55-
if (fixture != null) {
56-
fixture.tearDown();
57-
fixture = null;
58-
}
59-
}
60-
61-
static class Fixture {
62-
63-
private HttpServer server;
64-
private ExecutorService executorService;
65-
66-
void setUp() {
67-
try {
68-
server = HttpServer.create(new InetSocketAddress(getLoopbackAddress(), 0), 0);
63+
return new CloseableHttpServer(server, executorService);
64+
});
6965
}
70-
catch (IOException e) {
71-
throw new UncheckedIOException("Failed to start HTTP server", e);
72-
}
73-
server.createContext("/test", exchange -> {
74-
exchange.sendResponseHeaders(204, -1);
75-
exchange.close();
76-
});
77-
executorService = Executors.newCachedThreadPool();
78-
server.setExecutor(executorService);
79-
server.start(); // <1>
80-
int port = server.getAddress().getPort();
81-
System.setProperty("http.server.host", getLoopbackAddress().getHostAddress()); // <2>
82-
System.setProperty("http.server.port", String.valueOf(port)); // <3>
83-
}
84-
85-
void tearDown() {
86-
server.stop(0); // <4>
87-
executorService.shutdownNow();
88-
}
66+
});
8967
}
9068

9169
}

0 commit comments

Comments
 (0)