Skip to content

Provide mechanism for managing resources across engines and executions #4281

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 107 commits into from
Mar 31, 2025
Merged
Changes from all commits
Commits
Show all changes
107 commits
Select commit Hold shift + click to select a range
66891a5
Generate ResourceContext for store resources
YongGoose Dec 22, 2024
7b8182c
Add getStore method in LauncherSession
YongGoose Dec 22, 2024
d475bcf
Add Namespace by reusing Jupiter's Namespace class
YongGoose Jan 6, 2025
f25bec2
Apply comment
YongGoose Jan 18, 2025
7068fff
Adding a request-level store to Launcher
YongGoose Jan 19, 2025
dea6ecc
Apply comment
YongGoose Jan 27, 2025
d940bd4
Generate ResourceContext for store resources
YongGoose Dec 22, 2024
faebb0c
Add Namespace by reusing Jupiter's Namespace class
YongGoose Jan 6, 2025
365228d
Apply comment
YongGoose Jan 18, 2025
6018ff2
Adding a request-level store to Launcher
YongGoose Jan 19, 2025
552cb0b
Apply comment
YongGoose Jan 27, 2025
715a891
Revert "Apply comment"
YongGoose Jan 27, 2025
170c5dc
Apply comment
YongGoose Jan 27, 2025
a286eff
Update junit-platform-engine/src/main/java/org/junit/platform/engine/…
YongGoose Feb 12, 2025
622c366
Update junit-platform-launcher/src/main/java/org/junit/platform/launc…
YongGoose Feb 12, 2025
24b390b
Add API annotation
YongGoose Feb 12, 2025
89a3e4d
Move Namespace class
YongGoose Feb 12, 2025
9f25956
Apply comment
YongGoose Feb 12, 2025
9a1d5be
Apply Namespace parameter
YongGoose Feb 13, 2025
6e46c70
Outline management of session-/request-level stores
marcphilipp Feb 16, 2025
2cf6c7b
Use session-/request-level store in Jupiter engine
marcphilipp Feb 16, 2025
e4baa4b
Use request-level store as parent of engine-level store
marcphilipp Feb 16, 2025
8534a08
fixup! Outline management of session-/request-level stores
marcphilipp Feb 16, 2025
533adef
Fix test
marcphilipp Feb 16, 2025
46c1daf
Fix ExecutionRequest's `NamespacedHierarchicalStore`
YongGoose Feb 18, 2025
10be1e9
Fix post-merge compile error
marcphilipp Feb 24, 2025
f94624a
Limit session-/request-level store availability to execution
marcphilipp Feb 24, 2025
7ed2ab9
Remove broken Javadoc link
marcphilipp Feb 24, 2025
bd574d9
Revert changes
YongGoose Feb 25, 2025
ebe7966
Add JupiterEngineTests
YongGoose Feb 27, 2025
1b23aeb
Add NamespaceTests
YongGoose Mar 3, 2025
3b3d227
polishing
YongGoose Mar 8, 2025
bfca3ab
Merge branch 'main' into feature/2816
YongGoose Mar 9, 2025
6b6ca12
Apply comment
YongGoose Mar 9, 2025
d2f9f98
Add SuiteEngineTest
YongGoose Mar 9, 2025
370bc6d
Add EngineTestKitTest
YongGoose Mar 9, 2025
fff3109
Add LauncherStoreFacadeTest
YongGoose Mar 9, 2025
7b49ff3
Merge branch 'main' into feature/2816
YongGoose Mar 10, 2025
7151fbd
Merge branch 'main' into feature/2816
YongGoose Mar 10, 2025
f80a176
Update junit-jupiter-api/src/main/java/org/junit/jupiter/api/extensio…
YongGoose Mar 10, 2025
65c710b
Update junit-jupiter-api/src/main/java/org/junit/jupiter/api/extensio…
YongGoose Mar 10, 2025
a6dca82
Update junit-jupiter-api/src/main/java/org/junit/jupiter/api/extensio…
YongGoose Mar 10, 2025
7fb5b18
Update junit-jupiter-api/src/main/java/org/junit/jupiter/api/extensio…
YongGoose Mar 10, 2025
bb3a61d
Add Api annotation
YongGoose Mar 10, 2025
78a6023
Introduce create(List) method in Namespace
YongGoose Mar 10, 2025
10c052a
Update junit-platform-engine/src/main/java/org/junit/platform/engine/…
YongGoose Mar 10, 2025
fb4e4d7
Revert changes
YongGoose Mar 10, 2025
f461eb3
Merge branch 'main' into feature/2816
YongGoose Mar 10, 2025
c5c07f4
Resolve conflict
YongGoose Mar 10, 2025
f3d570e
Refactoring test codes
YongGoose Mar 10, 2025
66adb2c
Update jupiter-tests/src/test/java/org/junit/jupiter/engine/descripto…
YongGoose Mar 11, 2025
30505d0
Update jupiter-tests/src/test/java/org/junit/jupiter/engine/descripto…
YongGoose Mar 11, 2025
b40b864
Update jupiter-tests/src/test/java/org/junit/jupiter/engine/descripto…
YongGoose Mar 11, 2025
c3083f4
Update junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/de…
YongGoose Mar 11, 2025
13c03de
Merge branch 'main' into feature/2816
YongGoose Mar 11, 2025
579c492
Merge branch 'main' into feature/2816
YongGoose Mar 13, 2025
3aae750
polishing
YongGoose Mar 16, 2025
59d7d0c
update tests
YongGoose Mar 16, 2025
64186b9
Merge branch 'main' into feature/2816
YongGoose Mar 17, 2025
82a4e97
Merge branch 'main' into feature/2816
YongGoose Mar 17, 2025
b76ee64
polishing
YongGoose Mar 17, 2025
6f4fc2b
Add Jupiter extension test
YongGoose Mar 17, 2025
1683573
Add dummy test engines tests
YongGoose Mar 17, 2025
478ef7d
Merge branch 'main' into feature/2816
YongGoose Mar 18, 2025
0ee7f84
Merge branch 'main' into feature/2816
YongGoose Mar 18, 2025
45a6037
Update platform-tests/src/test/java/org/junit/platform/launcher/core/…
YongGoose Mar 18, 2025
7e65f7f
Merge branch 'main' into feature/2816
YongGoose Mar 18, 2025
5e98c47
Apply comment
YongGoose Mar 18, 2025
bb2f9ed
Merge branch 'main' into feature/2816
YongGoose Mar 19, 2025
dd33bf2
Refactor `ClassExtensionContext`
YongGoose Mar 19, 2025
a2feefc
Refactor `Namespace`
YongGoose Mar 19, 2025
4a650a9
Refactor `ExtensionContextTests`
YongGoose Mar 19, 2025
0d8f63d
Refactor `JupiterTestEngineTests`
YongGoose Mar 19, 2025
e29bb85
Apply comments
YongGoose Mar 19, 2025
8185507
Update junit-platform-launcher/src/testFixtures/java/org/junit/platfo…
YongGoose Mar 22, 2025
6625f24
Update junit-platform-engine/src/main/java/org/junit/platform/engine/…
YongGoose Mar 22, 2025
5ddaa93
Update junit-platform-engine/src/main/java/org/junit/platform/engine/…
YongGoose Mar 22, 2025
b85d29f
Update junit-platform-engine/src/main/java/org/junit/platform/engine/…
YongGoose Mar 22, 2025
a6b9896
Update platform-tests/src/test/java/org/junit/platform/suite/engine/S…
YongGoose Mar 22, 2025
b1b642a
Apply comments
YongGoose Mar 22, 2025
475b354
Merge branch 'main' into feature/2816
YongGoose Mar 22, 2025
85019a3
Apply comments
YongGoose Mar 22, 2025
efaace0
Update `LauncherFactoryTests`
YongGoose Mar 22, 2025
81492f3
Update `JupiterTestEngineTests`
YongGoose Mar 22, 2025
e5a5260
Ensure AutoCloseable resources are automatically closed
YongGoose Mar 22, 2025
b0456ec
polishing
YongGoose Mar 22, 2025
efc2f40
polishing
YongGoose Mar 23, 2025
9517474
Change extension to AfterTestExecutionCallback
YongGoose Mar 23, 2025
c1ce74a
Make `closed` flag of the resource static
YongGoose Mar 24, 2025
17ee313
Merge branch 'main' into feature/2816
YongGoose Mar 24, 2025
406b7e4
Merge branch 'main' into feature/2816
YongGoose Mar 24, 2025
45d4a1d
Update user-guide
YongGoose Mar 24, 2025
4eb741d
Revise User Guide sample
marcphilipp Mar 24, 2025
7c4e5f6
Update platform-tests/src/test/java/org/junit/platform/launcher/core/…
YongGoose Mar 24, 2025
0ac066c
Update platform-tests/src/test/java/org/junit/platform/launcher/core/…
YongGoose Mar 24, 2025
ad6fae0
Update platform-tests/src/test/java/org/junit/platform/launcher/core/…
YongGoose Mar 25, 2025
a367dd9
Update platform-tests/src/test/java/org/junit/platform/launcher/core/…
YongGoose Mar 25, 2025
219a194
Merge branch 'main' into feature/2816
YongGoose Mar 25, 2025
1de6773
Merge branch 'main' into feature/2816
YongGoose Mar 26, 2025
53161c9
Add document and changes in release-notes
YongGoose Mar 27, 2025
2cc5ba1
Merge branch 'main' into feature/2816
YongGoose Mar 28, 2025
677648a
Apply feedback from team call
marcphilipp Mar 28, 2025
9cfcb92
Merge branch 'main' into feature/2816
YongGoose Mar 28, 2025
a238326
Polish documentation
marcphilipp Mar 31, 2025
e0b5482
Merge remote-tracking branch 'origin/main' into fork/YongGoose/featur…
marcphilipp Mar 31, 2025
a125cb6
Merge branch 'main' into fork/YongGoose/feature/2816
marcphilipp Mar 31, 2025
f6e21a2
Polish release note entry
marcphilipp Mar 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
@@ -46,6 +46,7 @@ endif::[]
:IterationSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/IterationSelector.html[IterationSelector]
:MethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/MethodSelector.html[MethodSelector]
:ModuleSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ModuleSelector.html[ModuleSelector]
:NamespacedHierarchicalStore: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.html[NamespacedHierarchicalStore]
:NestedClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedClassSelector.html[NestedClassSelector]
:NestedMethodSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/NestedMethodSelector.html[NestedMethodSelector]
:OutputDirectoryProvider: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/reporting/OutputDirectoryProvider.html[OutputDirectoryProvider]
Original file line number Diff line number Diff line change
@@ -26,7 +26,15 @@ repository on GitHub.
[[release-notes-5.13.0-M3-junit-platform-new-features-and-improvements]]
==== New Features and Improvements

* ❓
* 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. Please refer to the User Guide for examples of managing
<<../user-guide/index.adoc#launcher-api-launcher-session-listeners-tool-example-usage, session-scoped>>
and
<<../user-guide/index.adoc#launcher-api-managing-state-across-test-engines, request-scoped>>
resources.


[[release-notes-5.13.0-M3-junit-jupiter]]
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
:testDir: ../../../../../src/test/java
:testResourcesDir: ../../../../../src/test/resources

[[launcher-api]]
=== JUnit Platform Launcher API

@@ -132,10 +135,22 @@ package example.session;

include::{testDir}/example/session/GlobalSetupTeardownListener.java[tags=user_guide]
----
<1> Start the HTTP server
<2> Export its host address as a system property for consumption by tests
<3> Export its port as a system property for consumption by tests
<4> Stop the HTTP server
<1> Get the store from the launcher session
<2> Lazily create the HTTP server and put it into the store
<3> Start the HTTP server

It uses a wrapper class to ensure the server is stopped when the launcher session is
closed:

[source,java]
.src/test/java/example/session/CloseableHttpServer.java
----
package example.session;

include::{testDir}/example/session/CloseableHttpServer.java[tags=user_guide]
----
<1> The `close()` method is called when the launcher session is closed
<2> Stop the HTTP server

This sample uses the HTTP server implementation from the jdk.httpserver module that comes
with the JDK but would work similarly with any other server or resource. In order for the
@@ -158,10 +173,11 @@ package example.session;

include::{testDir}/example/session/HttpTests.java[tags=user_guide]
----
<1> Read the host address of the server from the system property set by the listener
<2> Read the port of the server from the system property set by the listener
<3> Send a request to the server
<4> Check the status code of the response
<1> Retrieve the HTTP server instance from the store
<2> Get the host string directly from the injected HTTP server instance
<3> Get the port number directly from the injected HTTP server instance
<4> Send a request to the server
<5> Check the status code of the response

[[launcher-api-launcher-interceptors-custom]]
==== Registering a LauncherInterceptor
@@ -285,3 +301,55 @@ execute any tests but will notify registered `{TestExecutionListener}` instances
tests had been skipped and their containers had been successful. This can be useful to
test changes in the configuration of a build or to verify a listener is called as expected
without having to wait for all tests to be executed.

[[launcher-api-managing-state-across-test-engines]]
==== Managing State Across Test Engines

When running tests on the JUnit Platform, multiple test engines may need to access shared
resources. Rather than initializing these resources multiple times, JUnit Platform
provides mechanisms to share state across test engines efficiently. Test engines can use
the Platform's `{NamespacedHierarchicalStore}` API to lazily initialize and share
resources, ensuring they are created only once regardless of execution order. Any resource
that is put into the store and implements `AutoCloseable` will be closed automatically when
the execution is finished.

TIP: The Jupiter engine allows read and write access to such resources via its
`{ExtensionContext_Store}` API.

The following example demonstrates two custom test engines sharing a `ServerSocket`
resource. `FirstCustomEngine` attempts to retrieve an existing `ServerSocket` from the
global store or creates a new one if it doesn't exist:

[source,java]
----
include::{testDir}/example/FirstCustomEngine.java[tags=user_guide]
----

`SecondCustomEngine` follows the same pattern, ensuring that regardless whether it runs
before or after `FirstCustomEngine`, it will use the same socket instance:

[source,java]
----
include::{testDir}/example/SecondCustomEngine.java[tags=user_guide]
----

TIP: In this case, the `ServerSocket` can be stored directly in the global store while
ensuring since it gets closed because it implements `AutoCloseable`. If you need to use a
type that does not do so, you can wrap it in a custom class that implements
`AutoCloseable` and delegates to the original type. This is important to ensure that the
resource is closed properly when the test run is finished.

For illustration, the following test verifies that both engines are sharing the same
`ServerSocket` instance and that it's closed after `Launcher.execute()` returns:

[source,java,indent=0]
----
include::{testDir}/example/sharedresources/SharedResourceDemo.java[tags=user_guide]
----

By using the Platform's `{NamespacedHierarchicalStore}` API with shared namespaces in this
way, test engines can coordinate resource creation and sharing without direct dependencies
between them.

Alternatively, it's possible to inject resources into test engines by
<<launcher-api-launcher-session-listeners-custom, registering a `LauncherSessionListener`>>.
68 changes: 68 additions & 0 deletions documentation/src/test/java/example/FirstCustomEngine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 example;

//tag::user_guide[]
import static java.net.InetAddress.getLoopbackAddress;
import static org.junit.platform.engine.TestExecutionResult.successful;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;

import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;

/**
* First custom test engine implementation.
*/
public class FirstCustomEngine implements TestEngine {

public ServerSocket socket;

@Override
public String getId() {
return "first-custom-test-engine";
}

@Override
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
return new EngineDescriptor(uniqueId, "First Custom Test Engine");
}

@Override
public void execute(ExecutionRequest request) {
request.getEngineExecutionListener()
// tag::custom_line_break[]
.executionStarted(request.getRootTestDescriptor());

NamespacedHierarchicalStore<Namespace> store = request.getStore();
socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
try {
return new ServerSocket(0, 50, getLoopbackAddress());
}
catch (IOException e) {
throw new UncheckedIOException("Failed to start ServerSocket", e);
}
}, ServerSocket.class);

request.getEngineExecutionListener()
// tag::custom_line_break[]
.executionFinished(request.getRootTestDescriptor(), successful());
}
}
//end::user_guide[]
68 changes: 68 additions & 0 deletions documentation/src/test/java/example/SecondCustomEngine.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* 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 example;

import static java.net.InetAddress.getLoopbackAddress;
import static org.junit.platform.engine.TestExecutionResult.successful;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.net.ServerSocket;

import org.junit.platform.engine.EngineDiscoveryRequest;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.engine.UniqueId;
import org.junit.platform.engine.support.descriptor.EngineDescriptor;
import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;

//tag::user_guide[]
/**
* Second custom test engine implementation.
*/
public class SecondCustomEngine implements TestEngine {

public ServerSocket socket;

@Override
public String getId() {
return "second-custom-test-engine";
}

@Override
public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) {
return new EngineDescriptor(uniqueId, "Second Custom Test Engine");
}

@Override
public void execute(ExecutionRequest request) {
request.getEngineExecutionListener()
// tag::custom_line_break[]
.executionStarted(request.getRootTestDescriptor());

NamespacedHierarchicalStore<Namespace> store = request.getStore();
socket = store.getOrComputeIfAbsent(Namespace.GLOBAL, "serverSocket", key -> {
try {
return new ServerSocket(0, 50, getLoopbackAddress());
}
catch (IOException e) {
throw new UncheckedIOException("Failed to start ServerSocket", e);
}
}, ServerSocket.class);

request.getEngineExecutionListener()
// tag::custom_line_break[]
.executionFinished(request.getRootTestDescriptor(), successful());
}
}
//end::user_guide[]
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* 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 example.session;

//tag::user_guide[]
import java.util.concurrent.ExecutorService;

import com.sun.net.httpserver.HttpServer;

public class CloseableHttpServer implements AutoCloseable {

private final HttpServer server;
private final ExecutorService executorService;

CloseableHttpServer(HttpServer server, ExecutorService executorService) {
this.server = server;
this.executorService = executorService;
}

public HttpServer getServer() {
return server;
}

@Override
public void close() { // <1>
server.stop(0); // <2>
executorService.shutdownNow();
}
}
//end::user_guide[]
Original file line number Diff line number Diff line change
@@ -21,15 +21,15 @@

import com.sun.net.httpserver.HttpServer;

import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
import org.junit.platform.launcher.LauncherSession;
import org.junit.platform.launcher.LauncherSessionListener;
import org.junit.platform.launcher.TestExecutionListener;
import org.junit.platform.launcher.TestPlan;

public class GlobalSetupTeardownListener implements LauncherSessionListener {

private Fixture fixture;

@Override
public void launcherSessionOpened(LauncherSession session) {
// Avoid setup for test discovery by delaying it until tests are about to be executed
@@ -42,50 +42,28 @@ public void testPlanExecutionStarted(TestPlan testPlan) {
return;
}
//tag::user_guide[]
if (fixture == null) {
fixture = new Fixture();
fixture.setUp();
}
}
});
}
NamespacedHierarchicalStore<Namespace> store = session.getStore(); // <1>
store.getOrComputeIfAbsent(Namespace.GLOBAL, "httpServer", key -> { // <2>
InetSocketAddress address = new InetSocketAddress(getLoopbackAddress(), 0);
HttpServer server;
try {
server = HttpServer.create(address, 0);
}
catch (IOException e) {
throw new UncheckedIOException("Failed to start HTTP server", e);
}
server.createContext("/test", exchange -> {
exchange.sendResponseHeaders(204, -1);
exchange.close();
});
ExecutorService executorService = Executors.newCachedThreadPool();
server.setExecutor(executorService);
server.start(); // <3>

@Override
public void launcherSessionClosed(LauncherSession session) {
if (fixture != null) {
fixture.tearDown();
fixture = null;
}
}

static class Fixture {

private HttpServer server;
private ExecutorService executorService;

void setUp() {
try {
server = HttpServer.create(new InetSocketAddress(getLoopbackAddress(), 0), 0);
return new CloseableHttpServer(server, executorService);
});
}
catch (IOException e) {
throw new UncheckedIOException("Failed to start HTTP server", e);
}
server.createContext("/test", exchange -> {
exchange.sendResponseHeaders(204, -1);
exchange.close();
});
executorService = Executors.newCachedThreadPool();
server.setExecutor(executorService);
server.start(); // <1>
int port = server.getAddress().getPort();
System.setProperty("http.server.host", getLoopbackAddress().getHostAddress()); // <2>
System.setProperty("http.server.port", String.valueOf(port)); // <3>
}

void tearDown() {
server.stop(0); // <4>
executorService.shutdownNow();
}
});
}

}
Loading