diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 0fc1d24435cd..1330707d6f4e 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -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] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc index 464ac8fca81b..58da108e380b 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -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]] diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc index e1fb5e37efca..9f8db34b21f1 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc @@ -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 +<>. diff --git a/documentation/src/test/java/example/FirstCustomEngine.java b/documentation/src/test/java/example/FirstCustomEngine.java new file mode 100644 index 000000000000..efd9b14a7f0c --- /dev/null +++ b/documentation/src/test/java/example/FirstCustomEngine.java @@ -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 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[] diff --git a/documentation/src/test/java/example/SecondCustomEngine.java b/documentation/src/test/java/example/SecondCustomEngine.java new file mode 100644 index 000000000000..3d11c13ac18d --- /dev/null +++ b/documentation/src/test/java/example/SecondCustomEngine.java @@ -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 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[] diff --git a/documentation/src/test/java/example/session/CloseableHttpServer.java b/documentation/src/test/java/example/session/CloseableHttpServer.java new file mode 100644 index 000000000000..996fd85d8029 --- /dev/null +++ b/documentation/src/test/java/example/session/CloseableHttpServer.java @@ -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[] diff --git a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java index 8db5232d5bcb..fdddad84ea4e 100644 --- a/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java +++ b/documentation/src/test/java/example/session/GlobalSetupTeardownListener.java @@ -21,6 +21,8 @@ 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; @@ -28,8 +30,6 @@ 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 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(); - } + }); } } diff --git a/documentation/src/test/java/example/session/HttpTests.java b/documentation/src/test/java/example/session/HttpTests.java index fdb560b66fa6..97a13439a3e4 100644 --- a/documentation/src/test/java/example/session/HttpTests.java +++ b/documentation/src/test/java/example/session/HttpTests.java @@ -13,25 +13,50 @@ //tag::user_guide[] import static org.junit.jupiter.api.Assertions.assertEquals; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URI; import java.net.URL; +import com.sun.net.httpserver.HttpServer; + import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolver; +@ExtendWith(HttpServerParameterResolver.class) class HttpTests { @Test - void respondsWith204() throws Exception { - String host = System.getProperty("http.server.host"); // <1> - String port = System.getProperty("http.server.port"); // <2> + void respondsWith204(HttpServer server) throws IOException { + String host = server.getAddress().getHostString(); // <2> + int port = server.getAddress().getPort(); // <3> URL url = URI.create("http://" + host + ":" + port + "/test").toURL(); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); connection.setRequestMethod("GET"); - int responseCode = connection.getResponseCode(); // <3> + int responseCode = connection.getResponseCode(); // <4> + + assertEquals(204, responseCode); // <5> + } +} + +class HttpServerParameterResolver implements ParameterResolver { + @Override + public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return HttpServer.class.equals(parameterContext.getParameter().getType()); + } - assertEquals(204, responseCode); // <4> + @Override + public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + return extensionContext + // tag::custom_line_break[] + .getStore(ExtensionContext.Namespace.GLOBAL) + // tag::custom_line_break[] + .get("httpServer", CloseableHttpServer.class) // <1> + .getServer(); } } //end::user_guide[] diff --git a/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java b/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java new file mode 100644 index 000000000000..52b00c624471 --- /dev/null +++ b/documentation/src/test/java/example/sharedresources/SharedResourceDemo.java @@ -0,0 +1,47 @@ +/* + * 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.sharedresources; + +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import example.FirstCustomEngine; +import example.SecondCustomEngine; + +import org.junit.jupiter.api.Test; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.core.LauncherConfig; +import org.junit.platform.launcher.core.LauncherFactory; + +class SharedResourceDemo { + + //tag::user_guide[] + @Test + void runBothCustomEnginesTest() { + FirstCustomEngine firstCustomEngine = new FirstCustomEngine(); + SecondCustomEngine secondCustomEngine = new SecondCustomEngine(); + + Launcher launcher = LauncherFactory.create(LauncherConfig.builder() + // tag::custom_line_break[] + .addTestEngines(firstCustomEngine, secondCustomEngine) + // tag::custom_line_break[] + .enableTestEngineAutoRegistration(false) + // tag::custom_line_break[] + .build()); + + launcher.execute(request().build()); + + assertSame(firstCustomEngine.socket, secondCustomEngine.socket); + assertTrue(firstCustomEngine.socket.isClosed(), "socket should be closed"); + } + //end::user_guide[] +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java index 6d047292a98d..194215938d69 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java @@ -10,7 +10,9 @@ package org.junit.jupiter.api.extension; +import static java.util.Collections.unmodifiableList; import static org.apiguardian.api.API.Status.EXPERIMENTAL; +import static org.apiguardian.api.API.Status.INTERNAL; import static org.apiguardian.api.API.Status.STABLE; import java.lang.reflect.AnnotatedElement; @@ -442,9 +444,28 @@ default void publishReportEntry(String value) { * @return the store in which to put and get objects for other invocations * working in the same namespace; never {@code null} * @see Namespace#GLOBAL + * @see #getStore(StoreScope, Namespace) */ Store getStore(Namespace namespace); + /** + * Returns the store for supplied scope and namespace. + * + *

If {@code scope} is + * {@link StoreScope#EXTENSION_CONTEXT EXTENSION_CONTEXT}, the store behaves + * exactly like the one returned by {@link #getStore(Namespace)}. If the + * {@code scope} is {@link StoreScope#LAUNCHER_SESSION LAUNCHER_SESSION} or + * {@link StoreScope#EXECUTION_REQUEST EXECUTION_REQUEST}, all stored values + * that are instances of {@link AutoCloseable} are notified by invoking + * their {@code close()} methods when the scope is closed. + * + * @since 5.13 + * @see StoreScope + * @see #getStore(Namespace) + */ + @API(status = EXPERIMENTAL, since = "5.13") + Store getStore(StoreScope scope, Namespace namespace); + /** * Get the {@link ExecutionMode} associated with the current test or container. * @@ -771,6 +792,57 @@ public Namespace append(Object... parts) { Collections.addAll(newParts, parts); return new Namespace(newParts); } + + @API(status = INTERNAL, since = "5.13") + public List getParts() { + return unmodifiableList(parts); + } + } + + /** + * {@code StoreScope} is an enumeration of the different scopes for + * {@link Store} instances. + * + * @since 5.13 + * @see #getStore(StoreScope, Namespace) + */ + @API(status = EXPERIMENTAL, since = "5.13") + enum StoreScope { + + /** + * The store is scoped to the current {@code LauncherSession}. + * + *

Any data that is stored in a {@code Store} with this scope will be + * available throughout the entire launcher session. Therefore, it may + * be used to inject values from registered + * {@code LauncherSessionListener} implementations, to share data across + * multiple executions of the Jupiter engine within the same session, or + * even to share data across multiple engines. + * + * @see org.junit.platform.launcher.LauncherSession#getStore() + * @see org.junit.platform.launcher.LauncherSessionListener + */ + LAUNCHER_SESSION, + + /** + * The store is scoped to the current {@code ExecutionRequest} of the + * JUnit Platform {@code Launcher}. + * + *

Any data that is stored in a {@code Store} with this scope will be + * available for the duration of the current execution request. + * Therefore, it may be used to share data across multiple engines. + * + * @see org.junit.platform.engine.ExecutionRequest#getStore() + */ + EXECUTION_REQUEST, + + /** + * The store is scoped to the current {@code ExtensionContext}. + * + *

Any data that is stored in a {@code Store} with this scope will be + * bound to the current extension context lifecycle. + */ + EXTENSION_CONTEXT } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java index 0de9bbb308ec..c3878b9c84b7 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/JupiterTestEngine.java @@ -19,6 +19,7 @@ import org.junit.jupiter.engine.config.DefaultJupiterConfiguration; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.descriptor.LauncherStoreFacade; import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.jupiter.engine.support.JupiterThrowableCollectorFactory; @@ -82,8 +83,8 @@ protected HierarchicalTestExecutorService createExecutorService(ExecutionRequest @Override protected JupiterEngineExecutionContext createExecutionContext(ExecutionRequest request) { - return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), - getJupiterConfiguration(request)); + return new JupiterEngineExecutionContext(request.getEngineExecutionListener(), getJupiterConfiguration(request), + new LauncherStoreFacade(request.getStore())); } /** diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java index bf7cb5d9a059..84e716e01428 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java @@ -33,7 +33,6 @@ import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.DefaultExecutableInvoker; -import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.engine.extension.ExtensionContextInternal; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.JUnitException; @@ -52,7 +51,8 @@ */ abstract class AbstractExtensionContext implements ExtensionContextInternal, AutoCloseable { - private static final NamespacedHierarchicalStore.CloseAction CLOSE_RESOURCES = (__, ___, value) -> { + private static final NamespacedHierarchicalStore.CloseAction CLOSE_RESOURCES = ( + __, ___, value) -> { if (value instanceof CloseableResource) { ((CloseableResource) value).close(); } @@ -63,12 +63,14 @@ abstract class AbstractExtensionContext implements Ext private final T testDescriptor; private final Set tags; private final JupiterConfiguration configuration; - private final NamespacedHierarchicalStore valuesStore; + private final NamespacedHierarchicalStore valuesStore; private final ExecutableInvoker executableInvoker; private final ExtensionRegistry extensionRegistry; + private final LauncherStoreFacade launcherStoreFacade; AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor, - JupiterConfiguration configuration, ExtensionRegistry extensionRegistry) { + JupiterConfiguration configuration, ExtensionRegistry extensionRegistry, + LauncherStoreFacade launcherStoreFacade) { Preconditions.notNull(testDescriptor, "TestDescriptor must not be null"); Preconditions.notNull(configuration, "JupiterConfiguration must not be null"); @@ -78,8 +80,9 @@ abstract class AbstractExtensionContext implements Ext this.engineExecutionListener = engineExecutionListener; this.testDescriptor = testDescriptor; this.configuration = configuration; - this.valuesStore = createStore(parent); + this.valuesStore = createStore(parent, launcherStoreFacade); this.extensionRegistry = extensionRegistry; + this.launcherStoreFacade = launcherStoreFacade; // @formatter:off this.tags = testDescriptor.getTags().stream() @@ -88,9 +91,13 @@ abstract class AbstractExtensionContext implements Ext // @formatter:on } - private static NamespacedHierarchicalStore createStore(ExtensionContext parent) { - NamespacedHierarchicalStore parentStore = null; - if (parent != null) { + private static NamespacedHierarchicalStore createStore( + ExtensionContext parent, LauncherStoreFacade launcherStoreFacade) { + NamespacedHierarchicalStore parentStore; + if (parent == null) { + parentStore = launcherStoreFacade.getRequestLevelStore(); + } + else { parentStore = ((AbstractExtensionContext) parent).valuesStore; } return new NamespacedHierarchicalStore<>(parentStore, CLOSE_RESOURCES); @@ -188,8 +195,21 @@ protected T getTestDescriptor() { @Override public Store getStore(Namespace namespace) { - Preconditions.notNull(namespace, "Namespace must not be null"); - return new NamespaceAwareStore(this.valuesStore, namespace); + return launcherStoreFacade.getStoreAdapter(this.valuesStore, namespace); + } + + @Override + public Store getStore(StoreScope scope, Namespace namespace) { + // TODO [#4246] Use switch expression + switch (scope) { + case LAUNCHER_SESSION: + return launcherStoreFacade.getSessionLevelStore(namespace); + case EXECUTION_REQUEST: + return launcherStoreFacade.getRequestLevelStore(namespace); + case EXTENSION_CONTEXT: + return getStore(namespace); + } + throw new JUnitException("Unknown StoreScope: " + scope); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index 95b36fcda748..bad5573bf57d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -201,7 +201,7 @@ public final JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext ThrowableCollector throwableCollector = createThrowableCollector(); ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(), context.getExecutionListener(), this, this.classInfo.lifecycle, context.getConfiguration(), registry, - throwableCollector); + context.getLauncherStoreFacade(), throwableCollector); // @formatter:off return context.extend() diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java index 9350bfe4ea2d..cc052d0a60f5 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java @@ -37,9 +37,10 @@ final class ClassExtensionContext extends AbstractExtensionContext this.invocationContext.prepareInvocation(extensionContext)); return context.extend() // diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java index fc87ef3f3a68..8f5a0ad0f911 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java @@ -29,8 +29,8 @@ class DynamicExtensionContext extends AbstractExtensionContext requestLevelStore; + private final NamespacedHierarchicalStore sessionLevelStore; + + public LauncherStoreFacade(NamespacedHierarchicalStore requestLevelStore) { + this.requestLevelStore = requestLevelStore; + this.sessionLevelStore = requestLevelStore.getParent().orElseThrow( + () -> new JUnitException("Request-level store must have a parent")); + } + + NamespacedHierarchicalStore getRequestLevelStore() { + return this.requestLevelStore; + } + + ExtensionContext.Store getRequestLevelStore(ExtensionContext.Namespace namespace) { + return getStoreAdapter(this.requestLevelStore, namespace); + } + + ExtensionContext.Store getSessionLevelStore(ExtensionContext.Namespace namespace) { + return getStoreAdapter(this.sessionLevelStore, namespace); + } + + NamespaceAwareStore getStoreAdapter(NamespacedHierarchicalStore valuesStore, + ExtensionContext.Namespace namespace) { + Preconditions.notNull(namespace, "Namespace must not be null"); + return new NamespaceAwareStore(valuesStore, convert(namespace)); + } + + private Namespace convert(ExtensionContext.Namespace namespace) { + return namespace.equals(ExtensionContext.Namespace.GLOBAL) // + ? Namespace.GLOBAL // + : Namespace.create(namespace.getParts()); + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java index 24cdd6914eb3..eaed0fe9418a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java @@ -35,9 +35,10 @@ final class MethodExtensionContext extends AbstractExtensionContext requestLevelStore; @Deprecated @API(status = DEPRECATED, since = "1.11") public ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters) { - this(rootTestDescriptor, engineExecutionListener, configurationParameters, null); + this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null); } private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, - ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider) { + ConfigurationParameters configurationParameters, OutputDirectoryProvider outputDirectoryProvider, + NamespacedHierarchicalStore requestLevelStore) { this.rootTestDescriptor = Preconditions.notNull(rootTestDescriptor, "rootTestDescriptor must not be null"); this.engineExecutionListener = Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null"); this.configurationParameters = Preconditions.notNull(configurationParameters, "configurationParameters must not be null"); this.outputDirectoryProvider = outputDirectoryProvider; + this.requestLevelStore = requestLevelStore; } /** @@ -68,7 +73,7 @@ private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListe * engine may use to influence test execution * @return a new {@code ExecutionRequest}; never {@code null} * @since 1.9 - * @deprecated Use {@link #create(TestDescriptor, EngineExecutionListener, ConfigurationParameters, OutputDirectoryProvider)} + * @deprecated without replacement */ @Deprecated @API(status = DEPRECATED, since = "1.11") @@ -88,16 +93,19 @@ public static ExecutionRequest create(TestDescriptor rootTestDescriptor, * engine may use to influence test execution; never {@code null} * @param outputDirectoryProvider {@link OutputDirectoryProvider} for * writing reports and other output files; never {@code null} + * @param requestLevelStore {@link NamespacedHierarchicalStore} for storing + * request-scoped data; never {@code null} * @return a new {@code ExecutionRequest}; never {@code null} - * @since 1.12 + * @since 1.13 */ - @API(status = INTERNAL, since = "1.12") + @API(status = INTERNAL, since = "1.13") public static ExecutionRequest create(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters, - OutputDirectoryProvider outputDirectoryProvider) { + OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore requestLevelStore) { return new ExecutionRequest(rootTestDescriptor, engineExecutionListener, configurationParameters, - Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null")); + Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null"), + Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null")); } /** @@ -138,8 +146,25 @@ public ConfigurationParameters getConfigurationParameters() { */ @API(status = EXPERIMENTAL, since = "1.12") public OutputDirectoryProvider getOutputDirectoryProvider() { - return Preconditions.notNull(outputDirectoryProvider, + return Preconditions.notNull(this.outputDirectoryProvider, "No OutputDirectoryProvider was configured for this request"); } + /** + * {@return the {@link NamespacedHierarchicalStore} for this request for + * storing request-scoped data} + * + *

All stored values that implement {@link AutoCloseable} are notified by + * invoking their {@code close()} methods when this request has been + * executed. + * + * @since 1.13 + * @see NamespacedHierarchicalStore + */ + @API(status = EXPERIMENTAL, since = "1.13") + public NamespacedHierarchicalStore getStore() { + return Preconditions.notNull(this.requestLevelStore, + "No NamespacedHierarchicalStore was configured for this request"); + } + } diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java new file mode 100644 index 000000000000..dcf06d571833 --- /dev/null +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/Namespace.java @@ -0,0 +1,105 @@ +/* + * 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 org.junit.platform.engine.support.store; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.apiguardian.api.API; +import org.junit.platform.commons.util.Preconditions; + +/** + * A {@code Namespace} is used to provide a scope for data saved by + * extensions within a {@link NamespacedHierarchicalStore}. + * + *

Storing data in custom namespaces allows extensions to avoid accidentally + * mixing data between extensions or across different invocations within the + * lifecycle of a single extension. + */ +@API(status = EXPERIMENTAL, since = "1.13") +public class Namespace { + + /** + * The default, global namespace which allows access to stored data from + * all extensions. + */ + public static final Namespace GLOBAL = Namespace.create(new Object()); + + /** + * Create a namespace which restricts access to data to all extensions + * which use the same sequence of {@code parts} for creating a namespace. + * + *

The order of the {@code parts} is significant. + * + *

Internally the {@code parts} are compared using {@link Object#equals(Object)}. + */ + public static Namespace create(Object... parts) { + Preconditions.notEmpty(parts, "parts array must not be null or empty"); + Preconditions.containsNoNullElements(parts, "individual parts must not be null"); + return new Namespace(Arrays.asList(parts)); + } + + /** + * Create a namespace which restricts access to data to all extensions + * which use the same sequence of {@code objects} for creating a namespace. + * + *

The order of the {@code objects} is significant. + * + *

Internally the {@code objects} are compared using {@link Object#equals(Object)}. + */ + public static Namespace create(List objects) { + Preconditions.notEmpty(objects, "objects list must not be null or empty"); + Preconditions.containsNoNullElements(objects, "individual objects must not be null"); + return new Namespace(objects); + } + + private final List parts; + + private Namespace(List parts) { + this.parts = new ArrayList<>(parts); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Namespace that = (Namespace) o; + return this.parts.equals(that.parts); + } + + @Override + public int hashCode() { + return this.parts.hashCode(); + } + + /** + * Create a new namespace by appending the supplied {@code parts} to the + * existing sequence of parts in this namespace. + * + * @return new namespace; never {@code null} + */ + public Namespace append(Object... parts) { + Preconditions.notEmpty(parts, "parts array must not be null or empty"); + Preconditions.containsNoNullElements(parts, "individual parts must not be null"); + ArrayList newParts = new ArrayList<>(this.parts.size() + parts.length); + newParts.addAll(this.parts); + Collections.addAll(newParts, parts); + return new Namespace(newParts); + } +} diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java index 9167dde42b13..bd27996973b1 100644 --- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java +++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStore.java @@ -17,6 +17,7 @@ import java.util.Comparator; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicInteger; @@ -85,6 +86,19 @@ public NamespacedHierarchicalStore newChild() { return new NamespacedHierarchicalStore<>(this, this.closeAction); } + /** + * Returns the parent store of this {@code NamespacedHierarchicalStore}. + * + *

If this store does not have a parent, an empty {@code Optional} is returned. + * + * @return an {@code Optional} containing the parent store, or an empty {@code Optional} if there is no parent + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + public Optional> getParent() { + return Optional.ofNullable(this.parentStore); + } + /** * Determine if this store has been {@linkplain #close() closed}. * @@ -447,6 +461,15 @@ public Failure(Throwable throwable) { @FunctionalInterface public interface CloseAction { + @API(status = EXPERIMENTAL, since = "1.13") + static CloseAction closeAutoCloseables() { + return (__, ___, value) -> { + if (value instanceof AutoCloseable) { + ((AutoCloseable) value).close(); + } + }; + } + /** * Close the supplied {@code value}. * diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java index c78ed7761888..285621b59812 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/LauncherSession.java @@ -10,9 +10,12 @@ package org.junit.platform.launcher; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import org.apiguardian.api.API; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.core.LauncherFactory; /** @@ -47,4 +50,19 @@ public interface LauncherSession extends AutoCloseable { @Override void close(); + /** + * Get the {@link NamespacedHierarchicalStore} associated with this session. + * + *

All stored values that implement {@link AutoCloseable} are notified by + * invoking their {@code close()} methods when this session is closed. + * + *

Any call to the store returned by this method after the session has + * been closed will throw an exception. + * + * @since 1.13 + * @see NamespacedHierarchicalStore + */ + @API(status = EXPERIMENTAL, since = "1.13") + NamespacedHierarchicalStore getStore(); + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java index 6bd73c4b641e..70106c48fb72 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultDiscoveryRequest.java @@ -103,4 +103,5 @@ public LauncherDiscoveryListener getDiscoveryListener() { public OutputDirectoryProvider getOutputDirectoryProvider() { return this.outputDirectoryProvider; } + } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java index 5ebc4f9a638e..2a9fa3bf1ed4 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncher.java @@ -11,6 +11,7 @@ package org.junit.platform.launcher.core; import static java.util.Collections.unmodifiableCollection; +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION; @@ -18,6 +19,8 @@ import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -41,6 +44,7 @@ class DefaultLauncher implements Launcher { private final EngineExecutionOrchestrator executionOrchestrator = new EngineExecutionOrchestrator( listenerRegistry.testExecutionListeners); private final EngineDiscoveryOrchestrator discoveryOrchestrator; + private final NamespacedHierarchicalStore sessionLevelStore; /** * Construct a new {@code DefaultLauncher} with the supplied test engines. @@ -50,7 +54,8 @@ class DefaultLauncher implements Launcher { * @param postDiscoveryFilters the additional post discovery filters for * discovery requests; never {@code null} */ - DefaultLauncher(Iterable testEngines, Collection postDiscoveryFilters) { + DefaultLauncher(Iterable testEngines, Collection postDiscoveryFilters, + NamespacedHierarchicalStore sessionLevelStore) { Preconditions.condition(testEngines != null && testEngines.iterator().hasNext(), () -> "Cannot create Launcher without at least one TestEngine; " + "consider adding an engine implementation JAR to the classpath"); @@ -59,6 +64,7 @@ class DefaultLauncher implements Launcher { "PostDiscoveryFilter array must not contain null elements"); this.discoveryOrchestrator = new EngineDiscoveryOrchestrator(testEngines, unmodifiableCollection(postDiscoveryFilters), listenerRegistry.launcherDiscoveryListeners); + this.sessionLevelStore = sessionLevelStore; } @Override @@ -100,7 +106,13 @@ private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryReque } private void execute(InternalTestPlan internalTestPlan, TestExecutionListener[] listeners) { - executionOrchestrator.execute(internalTestPlan, listeners); + try (NamespacedHierarchicalStore requestLevelStore = createRequestLevelStore()) { + executionOrchestrator.execute(internalTestPlan, requestLevelStore, listeners); + } + } + + private NamespacedHierarchicalStore createRequestLevelStore() { + return new NamespacedHierarchicalStore<>(sessionLevelStore, closeAutoCloseables()); } } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java index 018eb41cf8a5..b128e09b1258 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DefaultLauncherSession.java @@ -10,10 +10,15 @@ package org.junit.platform.launcher.core; +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; + import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; import org.junit.platform.commons.PreconditionViolationException; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -40,21 +45,26 @@ public void close() { } }; + private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>(null, + closeAutoCloseables()); private final LauncherInterceptor interceptor; private final LauncherSessionListener listener; private final DelegatingLauncher launcher; - DefaultLauncherSession(List interceptors, Supplier listenerSupplier, - Supplier launcherSupplier) { + DefaultLauncherSession(List interceptors, // + Supplier listenerSupplier, // + Function, Launcher> launcherFactory // + ) { interceptor = composite(interceptors); Launcher launcher; if (interceptor == NOOP_INTERCEPTOR) { this.listener = listenerSupplier.get(); - launcher = launcherSupplier.get(); + launcher = launcherFactory.apply(this.store); } else { this.listener = interceptor.intercept(listenerSupplier::get); - launcher = new InterceptingLauncher(interceptor.intercept(launcherSupplier::get), interceptor); + launcher = new InterceptingLauncher(interceptor.intercept(() -> launcherFactory.apply(this.store)), + interceptor); } this.launcher = new DelegatingLauncher(launcher); listener.launcherSessionOpened(this); @@ -74,10 +84,16 @@ public void close() { if (launcher.delegate != ClosedLauncher.INSTANCE) { launcher.delegate = ClosedLauncher.INSTANCE; listener.launcherSessionClosed(this); + store.close(); interceptor.close(); } } + @Override + public NamespacedHierarchicalStore getStore() { + return store; + } + private static class ClosedLauncher implements Launcher { static final ClosedLauncher INSTANCE = new ClosedLauncher(); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java index b2ed7779d471..00bcdae9ada8 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineDiscoveryOrchestrator.java @@ -28,7 +28,6 @@ import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.UnrecoverableExceptions; -import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.Filter; import org.junit.platform.engine.FilterResult; import org.junit.platform.engine.TestDescriptor; @@ -91,8 +90,8 @@ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase *

Note: The test descriptors in the discovery result can safely be used * as non-root descriptors. Engine-test descriptor entries are pruned from * the returned result. As such execution by - * {@link EngineExecutionOrchestrator#execute(LauncherDiscoveryResult, EngineExecutionListener)} - * will not emit start or emit events for engines without tests. + * {@link EngineExecutionOrchestrator} will not emit start or emit events + * for engines without tests. */ public LauncherDiscoveryResult discover(LauncherDiscoveryRequest request, Phase phase, UniqueId parentId) { LauncherDiscoveryResult result = discover(request, phase, parentId::appendEngine); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java index 9c5e1b3eaaf1..eccb29fd82df 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/EngineExecutionOrchestrator.java @@ -29,6 +29,8 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.TestExecutionListener; import org.junit.platform.launcher.TestIdentifier; import org.junit.platform.launcher.TestPlan; @@ -52,12 +54,14 @@ public EngineExecutionOrchestrator() { this.listenerRegistry = listenerRegistry; } - void execute(InternalTestPlan internalTestPlan, TestExecutionListener... listeners) { + void execute(InternalTestPlan internalTestPlan, NamespacedHierarchicalStore requestLevelStore, + TestExecutionListener... listeners) { ConfigurationParameters configurationParameters = internalTestPlan.getConfigurationParameters(); ListenerRegistry testExecutionListenerListeners = buildListenerRegistryForExecution( listeners); withInterceptedStreams(configurationParameters, testExecutionListenerListeners, - testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener)); + testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener, + requestLevelStore)); } /** @@ -69,17 +73,18 @@ void execute(InternalTestPlan internalTestPlan, TestExecutionListener... listene */ @API(status = INTERNAL, since = "1.9", consumers = { "org.junit.platform.suite.engine" }) public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener, - TestExecutionListener testExecutionListener) { + TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore) { Preconditions.notNull(discoveryResult, "discoveryResult must not be null"); Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null"); Preconditions.notNull(testExecutionListener, "testExecutionListener must not be null"); + Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null"); InternalTestPlan internalTestPlan = InternalTestPlan.from(discoveryResult); - execute(internalTestPlan, engineExecutionListener, testExecutionListener); + execute(internalTestPlan, engineExecutionListener, testExecutionListener, requestLevelStore); } private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener parentEngineExecutionListener, - TestExecutionListener testExecutionListener) { + TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore) { internalTestPlan.markStarted(); // Do not directly pass the internal test plan to test execution listeners. @@ -93,7 +98,8 @@ private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener } else { execute(discoveryResult, - buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan)); + buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan), + requestLevelStore); } testExecutionListener.testPlanExecutionFinished(testPlan); } @@ -153,7 +159,8 @@ private void withInterceptedStreams(ConfigurationParameters configurationParamet * EngineExecutionListener listener} of execution events. */ @API(status = INTERNAL, since = "1.7", consumers = { "org.junit.platform.testkit" }) - public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener) { + public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener, + NamespacedHierarchicalStore requestLevelStore) { Preconditions.notNull(discoveryResult, "discoveryResult must not be null"); Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null"); @@ -161,7 +168,7 @@ public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionList EngineExecutionListener listener = selectExecutionListener(engineExecutionListener, configurationParameters); for (TestEngine testEngine : discoveryResult.getTestEngines()) { - failOrExecuteEngine(discoveryResult, listener, testEngine); + failOrExecuteEngine(discoveryResult, listener, testEngine, requestLevelStore); } } @@ -176,8 +183,7 @@ private static EngineExecutionListener selectExecutionListener(EngineExecutionLi } private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, EngineExecutionListener listener, - TestEngine testEngine) { - + TestEngine testEngine, NamespacedHierarchicalStore requestLevelStore) { EngineResultInfo engineDiscoveryResult = discoveryResult.getEngineResult(testEngine); DiscoveryIssueNotifier discoveryIssueNotifier = engineDiscoveryResult.getDiscoveryIssueNotifier(); TestDescriptor engineDescriptor = engineDiscoveryResult.getRootDescriptor(); @@ -193,7 +199,7 @@ private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, Engine } else { executeEngine(engineDescriptor, listener, discoveryResult.getConfigurationParameters(), testEngine, - discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier); + discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier, requestLevelStore); } } @@ -207,13 +213,13 @@ private ListenerRegistry buildListenerRegistryForExecutio private void executeEngine(TestDescriptor engineDescriptor, EngineExecutionListener listener, ConfigurationParameters configurationParameters, TestEngine testEngine, - OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier) { - + OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier, + NamespacedHierarchicalStore requestLevelStore) { OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener, engineDescriptor); try { testEngine.execute(ExecutionRequest.create(engineDescriptor, delayingListener, configurationParameters, - outputDirectoryProvider)); + outputDirectoryProvider, requestLevelStore)); discoveryIssueNotifier.logNonCriticalIssues(testEngine); delayingListener.reportEngineOutcome(); } diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java index c756f27351f0..b3d6ab2613a0 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/LauncherFactory.java @@ -26,6 +26,8 @@ import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.TestEngine; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherInterceptor; @@ -96,7 +98,8 @@ public static LauncherSession openSession(LauncherConfig config) throws Precondi Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); return new DefaultLauncherSession(collectLauncherInterceptors(configurationParameters), - () -> createLauncherSessionListener(config), () -> createDefaultLauncher(config, configurationParameters)); + () -> createLauncherSessionListener(config), + sessionLevelStore -> createDefaultLauncher(config, configurationParameters, sessionLevelStore)); } /** @@ -125,17 +128,17 @@ public static Launcher create() throws PreconditionViolationException { public static Launcher create(LauncherConfig config) throws PreconditionViolationException { Preconditions.notNull(config, "LauncherConfig must not be null"); LauncherConfigurationParameters configurationParameters = LauncherConfigurationParameters.builder().build(); - return new SessionPerRequestLauncher(() -> createDefaultLauncher(config, configurationParameters), + return new SessionPerRequestLauncher( + sessionLevelStore -> createDefaultLauncher(config, configurationParameters, sessionLevelStore), () -> createLauncherSessionListener(config), () -> collectLauncherInterceptors(configurationParameters)); } private static DefaultLauncher createDefaultLauncher(LauncherConfig config, - LauncherConfigurationParameters configurationParameters) { + LauncherConfigurationParameters configurationParameters, + NamespacedHierarchicalStore sessionLevelStore) { Set engines = collectTestEngines(config); List filters = collectPostDiscoveryFilters(config); - - DefaultLauncher launcher = new DefaultLauncher(engines, filters); - + DefaultLauncher launcher = new DefaultLauncher(engines, filters, sessionLevelStore); registerLauncherDiscoveryListeners(config, launcher); registerTestExecutionListeners(config, launcher, configurationParameters); diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java index dffde014867a..cb12eafb7282 100644 --- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java +++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/SessionPerRequestLauncher.java @@ -11,8 +11,11 @@ package org.junit.platform.launcher.core; import java.util.List; +import java.util.function.Function; import java.util.function.Supplier; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.Launcher; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; @@ -28,14 +31,14 @@ class SessionPerRequestLauncher implements Launcher { private final LauncherListenerRegistry listenerRegistry = new LauncherListenerRegistry(); - private final Supplier launcherSupplier; + private final Function, Launcher> launcherFactory; private final Supplier sessionListenerSupplier; private final Supplier> interceptorFactory; - SessionPerRequestLauncher(Supplier launcherSupplier, + SessionPerRequestLauncher(Function, Launcher> launcherFactory, Supplier sessionListenerSupplier, Supplier> interceptorFactory) { - this.launcherSupplier = launcherSupplier; + this.launcherFactory = launcherFactory; this.sessionListenerSupplier = sessionListenerSupplier; this.interceptorFactory = interceptorFactory; } @@ -73,7 +76,7 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) { private LauncherSession createSession() { LauncherSession session = new DefaultLauncherSession(interceptorFactory.get(), sessionListenerSupplier, - launcherSupplier); + this.launcherFactory); Launcher launcher = session.getLauncher(); listenerRegistry.launcherDiscoveryListeners.getListeners().forEach( launcher::registerLauncherDiscoveryListeners); diff --git a/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java new file mode 100644 index 000000000000..7beef2b5f0f1 --- /dev/null +++ b/junit-platform-launcher/src/testFixtures/java/org/junit/platform/launcher/core/NamespacedHierarchicalStoreProviders.java @@ -0,0 +1,27 @@ +/* + * 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 org.junit.platform.launcher.core; + +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; + +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; + +public class NamespacedHierarchicalStoreProviders { + + public static NamespacedHierarchicalStore dummyNamespacedHierarchicalStore() { + return new NamespacedHierarchicalStore<>(dummyNamespacedHierarchicalStoreWithNoParent(), closeAutoCloseables()); + } + + public static NamespacedHierarchicalStore dummyNamespacedHierarchicalStoreWithNoParent() { + return new NamespacedHierarchicalStore<>(null, closeAutoCloseables()); + } +} diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java index c3a62006e859..6e3bf311ccfb 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java @@ -19,6 +19,8 @@ import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase; @@ -58,9 +60,10 @@ LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, Uniq } TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult, - EngineExecutionListener parentEngineExecutionListener) { + EngineExecutionListener parentEngineExecutionListener, + NamespacedHierarchicalStore requestLevelStore) { SummaryGeneratingListener listener = new SummaryGeneratingListener(); - executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener); + executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore); return listener.getSummary(); } diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java index 0bcc205217a8..d9837a6a2319 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java @@ -40,6 +40,8 @@ import org.junit.platform.engine.support.discovery.DiscoveryIssueReporter; import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector; import org.junit.platform.engine.support.hierarchical.ThrowableCollector; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.LauncherDiscoveryListener; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.LauncherDiscoveryResult; @@ -144,13 +146,15 @@ private static String getSuiteDisplayName(Class testClass) { // @formatter:on } - void execute(EngineExecutionListener parentEngineExecutionListener) { + void execute(EngineExecutionListener parentEngineExecutionListener, + NamespacedHierarchicalStore requestLevelStore) { parentEngineExecutionListener.executionStarted(this); ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector(); executeBeforeSuiteMethods(throwableCollector); - TestExecutionSummary summary = executeTests(parentEngineExecutionListener, throwableCollector); + TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore, + throwableCollector); executeAfterSuiteMethods(throwableCollector); @@ -171,7 +175,7 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) { } private TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener, - ThrowableCollector throwableCollector) { + NamespacedHierarchicalStore requestLevelStore, ThrowableCollector throwableCollector) { if (throwableCollector.isNotEmpty()) { return null; } @@ -181,7 +185,7 @@ private TestExecutionSummary executeTests(EngineExecutionListener parentEngineEx // be pruned accordingly. LauncherDiscoveryResult discoveryResult = this.launcherDiscoveryResult.withRetainedEngines( getChildren()::contains); - return launcher.execute(discoveryResult, parentEngineExecutionListener); + return launcher.execute(discoveryResult, parentEngineExecutionListener, requestLevelStore); } private void executeAfterSuiteMethods(ThrowableCollector throwableCollector) { diff --git a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java index c0f754639c80..d75cf8c3dfbf 100644 --- a/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java +++ b/junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java @@ -22,6 +22,8 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** * The JUnit Platform Suite {@link org.junit.platform.engine.TestEngine TestEngine}. @@ -63,6 +65,7 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId public void execute(ExecutionRequest request) { SuiteEngineDescriptor suiteEngineDescriptor = (SuiteEngineDescriptor) request.getRootTestDescriptor(); EngineExecutionListener engineExecutionListener = request.getEngineExecutionListener(); + NamespacedHierarchicalStore requestLevelStore = request.getStore(); engineExecutionListener.executionStarted(suiteEngineDescriptor); @@ -70,7 +73,7 @@ public void execute(ExecutionRequest request) { suiteEngineDescriptor.getChildren() .stream() .map(SuiteTestDescriptor.class::cast) - .forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener)); + .forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore)); // @formatter:on engineExecutionListener.executionFinished(suiteEngineDescriptor, TestExecutionResult.successful()); } diff --git a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java index df1505b1c4d8..9f7f3e18501a 100644 --- a/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java +++ b/junit-platform-testkit/src/main/java/org/junit/platform/testkit/engine/EngineTestKit.java @@ -16,6 +16,7 @@ import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.MAINTAINED; import static org.apiguardian.api.API.Status.STABLE; +import static org.junit.platform.engine.support.store.NamespacedHierarchicalStore.CloseAction.closeAutoCloseables; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.DISCOVERY; import static org.junit.platform.launcher.core.EngineDiscoveryOrchestrator.Phase.EXECUTION; @@ -23,6 +24,7 @@ import java.util.List; import java.util.Map; import java.util.ServiceLoader; +import java.util.function.Consumer; import java.util.stream.Stream; import org.apiguardian.api.API; @@ -41,6 +43,8 @@ import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.reporting.OutputDirectoryProvider; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.LauncherDiscoveryRequest; import org.junit.platform.launcher.core.EngineDiscoveryOrchestrator; import org.junit.platform.launcher.core.EngineExecutionOrchestrator; @@ -324,15 +328,30 @@ private static void executeDirectly(TestEngine testEngine, EngineDiscoveryReques EngineExecutionListener listener) { UniqueId engineUniqueId = UniqueId.forEngine(testEngine.getId()); TestDescriptor engineTestDescriptor = testEngine.discover(discoveryRequest, engineUniqueId); - ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener, - discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider()); - testEngine.execute(request); + withRequestLevelStore(store -> { + ExecutionRequest request = ExecutionRequest.create(engineTestDescriptor, listener, + discoveryRequest.getConfigurationParameters(), discoveryRequest.getOutputDirectoryProvider(), store); + testEngine.execute(request); + }); } private static void executeUsingLauncherOrchestration(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest, EngineExecutionListener listener) { LauncherDiscoveryResult discoveryResult = discover(testEngine, discoveryRequest, EXECUTION); - new EngineExecutionOrchestrator().execute(discoveryResult, listener); + TestDescriptor engineTestDescriptor = discoveryResult.getEngineTestDescriptor(testEngine); + Preconditions.notNull(engineTestDescriptor, "TestEngine did not yield a TestDescriptor"); + withRequestLevelStore(store -> new EngineExecutionOrchestrator().execute(discoveryResult, listener, store)); + } + + private static void withRequestLevelStore(Consumer> action) { + try (NamespacedHierarchicalStore sessionLevelStore = newStore(null); + NamespacedHierarchicalStore requestLevelStore = newStore(sessionLevelStore)) { + action.accept(requestLevelStore); + } + } + + private static NamespacedHierarchicalStore newStore(NamespacedHierarchicalStore parentStore) { + return new NamespacedHierarchicalStore<>(parentStore, closeAutoCloseables()); } private static LauncherDiscoveryResult discover(TestEngine testEngine, LauncherDiscoveryRequest discoveryRequest, diff --git a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java index 2fd7a4b4080b..8811f97d404d 100644 --- a/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java +++ b/junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java @@ -16,6 +16,7 @@ import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; +import static org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.junit.platform.testkit.engine.EventConditions.abortedWithReason; import static org.junit.platform.testkit.engine.EventConditions.container; @@ -924,8 +925,9 @@ private static void execute(Class testClass, EngineExecutionListener listener TestEngine testEngine = new VintageTestEngine(); var discoveryRequest = request(testClass); var engineTestDescriptor = testEngine.discover(discoveryRequest, UniqueId.forEngine(testEngine.getId())); - testEngine.execute(ExecutionRequest.create(engineTestDescriptor, listener, - discoveryRequest.getConfigurationParameters(), dummyOutputDirectoryProvider())); + testEngine.execute( + ExecutionRequest.create(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters(), + dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore())); } private static LauncherDiscoveryRequest request(Class testClass) { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java deleted file mode 100644 index dd242d20132a..000000000000 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineBasicTests.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * 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 org.junit.jupiter.engine; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; - -import org.junit.jupiter.api.Test; - -/** - * Basic assertions regarding {@link org.junit.platform.engine.TestEngine} - * functionality in JUnit Jupiter. - * - * @since 5.0 - */ -class JupiterTestEngineBasicTests { - - private final JupiterTestEngine jupiter = new JupiterTestEngine(); - - @Test - void id() { - assertEquals("junit-jupiter", jupiter.getId()); - } - - @Test - void groupId() { - assertEquals("org.junit.jupiter", jupiter.getGroupId().orElseThrow()); - } - - @Test - void artifactId() { - assertEquals("junit-jupiter-engine", jupiter.getArtifactId().orElseThrow()); - } - - @Test - void version() { - assertThat(jupiter.getVersion().orElseThrow()).isIn( // - System.getProperty("developmentVersion"), // with Test Distribution - "DEVELOPMENT" // without Test Distribution - ); - } - -} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java new file mode 100644 index 000000000000..79b2652c2028 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/JupiterTestEngineTests.java @@ -0,0 +1,96 @@ +/* + * 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 org.junit.jupiter.engine; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; +import org.junit.platform.commons.JUnitException; +import org.junit.platform.engine.ConfigurationParameters; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; + +/** + * @since 5.13 + */ +public class JupiterTestEngineTests { + + private final JupiterEngineDescriptor jupiterEngineDescriptor = mock(); + + private final ConfigurationParameters configurationParameters = mock(); + + private final EngineExecutionListener engineExecutionListener = mock(); + + private final ExecutionRequest executionRequest = mock(); + + private final JupiterTestEngine engine = new JupiterTestEngine(); + + private final JupiterTestEngine jupiter = new JupiterTestEngine(); + + @BeforeEach + void setUp() { + when(executionRequest.getEngineExecutionListener()).thenReturn(engineExecutionListener); + when(executionRequest.getConfigurationParameters()).thenReturn(configurationParameters); + when(executionRequest.getRootTestDescriptor()).thenReturn(jupiterEngineDescriptor); + } + + @Test + void createExecutionContextWithValidRequest() { + when(executionRequest.getStore()).thenReturn( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); + + JupiterEngineExecutionContext context = engine.createExecutionContext(executionRequest); + assertThat(context).isNotNull(); + } + + @Test + void createExecutionContextWithNoParentsRequestLevelStore() { + when(executionRequest.getStore()).thenReturn( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStoreWithNoParent()); + + assertThatThrownBy(() -> engine // + .createExecutionContext(executionRequest)) // + .isInstanceOf(JUnitException.class) // + .hasMessageContaining("Request-level store must have a parent"); + } + + @Test + void id() { + assertEquals("junit-jupiter", jupiter.getId()); + } + + @Test + void groupId() { + assertEquals("org.junit.jupiter", jupiter.getGroupId().orElseThrow()); + } + + @Test + void artifactId() { + assertEquals("junit-jupiter-engine", jupiter.getArtifactId().orElseThrow()); + } + + @Test + void version() { + assertThat(jupiter.getVersion().orElseThrow()).isIn( // + System.getProperty("developmentVersion"), // with Test Distribution + "DEVELOPMENT" // without Test Distribution + ); + } + +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java index c011777ffa1f..2417b145c380 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java @@ -65,6 +65,7 @@ import org.junit.platform.engine.reporting.FileEntry; import org.junit.platform.engine.reporting.ReportEntry; import org.junit.platform.engine.support.hierarchical.OpenTest4JAwareThrowableCollector; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; import org.mockito.ArgumentCaptor; /** @@ -78,6 +79,8 @@ public class ExtensionContextTests { private final JupiterConfiguration configuration = mock(); private final ExtensionRegistry extensionRegistry = mock(); + private final LauncherStoreFacade launcherStoreFacade = new LauncherStoreFacade( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); @BeforeEach void setUp() { @@ -92,7 +95,7 @@ void fromJupiterEngineDescriptor() { var engineTestDescriptor = new JupiterEngineDescriptor(UniqueId.root("engine", "junit-jupiter"), configuration); try (var engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor, configuration, - extensionRegistry)) { + extensionRegistry, launcherStoreFacade)) { // @formatter:off assertAll("engineContext", () -> assertThat(engineContext.getElement()).isEmpty(), @@ -123,7 +126,7 @@ void fromClassTestDescriptor() { nestedClassDescriptor.addChild(methodTestDescriptor); var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, - configuration, extensionRegistry, null); + configuration, extensionRegistry, launcherStoreFacade, null); // @formatter:off assertAll("outerContext", @@ -143,7 +146,7 @@ void fromClassTestDescriptor() { // @formatter:on var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - PER_METHOD, configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); // @formatter:off assertAll("nestedContext", () -> assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext), @@ -153,7 +156,7 @@ void fromClassTestDescriptor() { // @formatter:on var doublyNestedExtensionContext = new ClassExtensionContext(nestedExtensionContext, null, - doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, null); + doublyNestedClassDescriptor, PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); // @formatter:off assertAll("doublyNestedContext", () -> assertThat(doublyNestedExtensionContext.getParent()).containsSame(nestedExtensionContext), @@ -163,7 +166,7 @@ void fromClassTestDescriptor() { // @formatter:on var methodExtensionContext = new MethodExtensionContext(nestedExtensionContext, null, methodTestDescriptor, - configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); // @formatter:off assertAll("methodContext", () -> assertThat(methodExtensionContext.getParent()).containsSame(nestedExtensionContext), @@ -177,7 +180,7 @@ void fromClassTestDescriptor() { void ExtensionContext_With_ExtensionRegistry_getExtensions() { var classTestDescriptor = nestedClassDescriptor(); try (var ctx = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, - extensionRegistry, null)) { + extensionRegistry, launcherStoreFacade, null)) { Extension ext = mock(); when(extensionRegistry.getExtensions(Extension.class)).thenReturn(List.of(ext)); @@ -195,18 +198,18 @@ void tagsCanBeRetrievedInExtensionContext() { outerClassDescriptor.addChild(methodTestDescriptor); var outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, PER_METHOD, - configuration, extensionRegistry, null); + configuration, extensionRegistry, launcherStoreFacade, null); assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag"); assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext); var nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, nestedClassDescriptor, - PER_METHOD, configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); assertThat(nestedExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "nested-tag"); assertThat(nestedExtensionContext.getRoot()).isSameAs(outerExtensionContext); var methodExtensionContext = new MethodExtensionContext(outerExtensionContext, null, methodTestDescriptor, - configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase())); assertThat(methodExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "method-tag"); assertThat(methodExtensionContext.getRoot()).isSameAs(outerExtensionContext); @@ -224,11 +227,11 @@ void fromMethodTestDescriptor() { var testMethod = methodTestDescriptor.getTestMethod(); var engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, configuration, - extensionRegistry); + extensionRegistry, launcherStoreFacade); var classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, classTestDescriptor, - PER_METHOD, configuration, extensionRegistry, null); + PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); var methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, methodTestDescriptor, - configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); + configuration, extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(testInstance)); // @formatter:off @@ -255,7 +258,7 @@ void reportEntriesArePublishedToExecutionListener() { var classTestDescriptor = outerClassDescriptor(null); var engineExecutionListener = spy(EngineExecutionListener.class); ExtensionContext extensionContext = new ClassExtensionContext(null, engineExecutionListener, - classTestDescriptor, PER_METHOD, configuration, extensionRegistry, null); + classTestDescriptor, PER_METHOD, configuration, extensionRegistry, launcherStoreFacade, null); var map1 = Collections.singletonMap("key", "value"); var map2 = Collections.singletonMap("other key", "other value"); @@ -381,7 +384,7 @@ private ExtensionContext createExtensionContextForFilePublishing(Path tempDir, when(configuration.getOutputDirectoryProvider()) // .thenReturn(hierarchicalOutputDirectoryProvider(tempDir)); return new ClassExtensionContext(null, engineExecutionListener, classTestDescriptor, PER_METHOD, configuration, - extensionRegistry, null); + extensionRegistry, launcherStoreFacade, null); } @Test @@ -390,9 +393,9 @@ void usingStore() { var methodTestDescriptor = methodDescriptor(); var classTestDescriptor = outerClassDescriptor(methodTestDescriptor); ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, - configuration, extensionRegistry, null); + configuration, extensionRegistry, launcherStoreFacade, null); var childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, configuration, - extensionRegistry, new OpenTest4JAwareThrowableCollector()); + extensionRegistry, launcherStoreFacade, new OpenTest4JAwareThrowableCollector()); childContext.setTestInstances(DefaultTestInstances.of(new OuterClassTestCase())); var childStore = childContext.getStore(Namespace.GLOBAL); @@ -439,18 +442,20 @@ void configurationParameter(Function>> extensionContextFactories() { ExtensionRegistry extensionRegistry = mock(); + LauncherStoreFacade launcherStoreFacade = mock(); var testClass = ExtensionContextTests.class; return List.of( // named("engine", (JupiterConfiguration configuration) -> { var engineUniqueId = UniqueId.parse("[engine:junit-jupiter]"); var engineDescriptor = new JupiterEngineDescriptor(engineUniqueId, configuration); - return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry); + return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry, + launcherStoreFacade); }), // named("class", (JupiterConfiguration configuration) -> { var classUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]"); var classTestDescriptor = new ClassTestDescriptor(classUniqueId, testClass, configuration); return new ClassExtensionContext(null, null, classTestDescriptor, PER_METHOD, configuration, - extensionRegistry, null); + extensionRegistry, launcherStoreFacade, null); }), // named("method", (JupiterConfiguration configuration) -> { var method = ReflectionSupport.findMethod(testClass, "extensionContextFactories").orElseThrow(); @@ -458,7 +463,7 @@ void configurationParameter(Function requestLevelStore; + private NamespacedHierarchicalStore sessionLevelStore; + private ExtensionContext.Namespace extensionNamespace; + + @BeforeEach + void setUp() { + sessionLevelStore = new NamespacedHierarchicalStore<>(null); + requestLevelStore = new NamespacedHierarchicalStore<>(sessionLevelStore); + extensionNamespace = ExtensionContext.Namespace.create("foo", "bar"); + } + + @Test + void createsInstanceSuccessfullyWithValidStore() { + assertDoesNotThrow(() -> new LauncherStoreFacade(requestLevelStore)); + } + + @Test + void throwsExceptionWhenRequestLevelStoreHasNoParent() { + assertThrowsExactly(JUnitException.class, () -> new LauncherStoreFacade(sessionLevelStore), () -> { + throw new JUnitException("Request-level store must have a parent"); + }); + } + + @Test + void returnsRequestLevelStore() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + assertEquals(requestLevelStore, facade.getRequestLevelStore()); + } + + @Test + void returnsNamespaceAwareStoreWithRequestLevelStore() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + ExtensionContext.Store store = facade.getRequestLevelStore(extensionNamespace); + + assertNotNull(store); + assertInstanceOf(NamespaceAwareStore.class, store); + } + + @Test + void returnsNamespaceAwareStore() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + NamespaceAwareStore adapter = facade.getStoreAdapter(requestLevelStore, extensionNamespace); + + assertNotNull(adapter); + } + + @Test + void throwsExceptionWhenNamespaceIsNull() { + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + assertThrows(PreconditionViolationException.class, () -> facade.getStoreAdapter(requestLevelStore, null)); + } + + @Test + void returnsNamespaceAwareStoreWithGlobalNamespace() { + requestLevelStore.put(Namespace.GLOBAL, "foo", "bar"); + + LauncherStoreFacade facade = new LauncherStoreFacade(requestLevelStore); + ExtensionContext.Store store = facade.getRequestLevelStore(ExtensionContext.Namespace.GLOBAL); + + assertEquals("bar", store.get("foo")); + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java index 4ebb656d3f00..d072738824d9 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/TestFactoryTestDescriptorTests.java @@ -163,7 +163,7 @@ void before() throws Exception { extensionContext = mock(); isClosed = false; - context = new JupiterEngineExecutionContext(null, null) // + context = new JupiterEngineExecutionContext(null, null, null) // .extend() // .withThrowableCollector(new OpenTest4JAwareThrowableCollector()) // .withExtensionContext(extensionContext) // diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java index 199b6903f46b..c39cc37aae7c 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreConcurrencyTests.java @@ -16,8 +16,8 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java index 2c62a186b9fa..849d481212c6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/ExtensionContextStoreTests.java @@ -18,9 +18,9 @@ import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ExtensionContextException; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.engine.support.store.NamespacedHierarchicalStoreException; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java index 80eeca958fbd..5e0b21658b32 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContextTests.java @@ -20,6 +20,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.descriptor.LauncherStoreFacade; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; @@ -34,8 +35,10 @@ class JupiterEngineExecutionContextTests { private final EngineExecutionListener engineExecutionListener = mock(); + private final LauncherStoreFacade launcherStoreFacade = mock(); + private final JupiterEngineExecutionContext originalContext = new JupiterEngineExecutionContext( - engineExecutionListener, configuration); + engineExecutionListener, configuration, launcherStoreFacade); @Test void executionListenerIsHandedOnWhenContextIsExtended() { diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java index 37cfcdd57f92..5c66048d03f6 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/CloseablePathTests.java @@ -62,7 +62,6 @@ import org.junit.jupiter.api.extension.AnnotatedElementContext; import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Namespace; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; @@ -73,6 +72,7 @@ import org.junit.jupiter.params.provider.ValueSource; import org.junit.platform.commons.PreconditionViolationException; import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java index 750611a5c67a..04e2735f4805 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.TimeoutInvocationParameters; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; /** @@ -71,7 +72,8 @@ void shouldThrowInvocationException() { private static SeparateThreadTimeoutInvocation aSeparateThreadInvocation(Invocation invocation) { var namespace = ExtensionContext.Namespace.create(SeparateThreadTimeoutInvocationTests.class); - var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), namespace); + var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), + Namespace.create(namespace.getParts())); var parameters = new TimeoutInvocationParameters<>(invocation, new TimeoutDuration(PREEMPTIVE_TIMEOUT_MILLIS, MILLISECONDS), () -> "method()", PreInterruptCallbackInvocation.NOOP); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java index 234a2c19e63e..b630fd112054 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java @@ -20,12 +20,12 @@ import org.junit.jupiter.api.Timeout.ThreadMode; import org.junit.jupiter.api.condition.DisabledIf; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.jupiter.engine.execution.NamespaceAwareStore; import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.SingleThreadExecutorResource; import org.junit.jupiter.engine.extension.TimeoutInvocationFactory.TimeoutInvocationParameters; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.mockito.Mock; import org.mockito.Spy; @@ -42,7 +42,7 @@ class TimeoutInvocationFactoryTests { @Spy private final Store store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), - ExtensionContext.Namespace.create(TimeoutInvocationFactoryTests.class)); + Namespace.create(TimeoutInvocationFactoryTests.class)); @Mock private Invocation invocation; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java index 472429698597..24dba2894cd7 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java @@ -215,7 +215,8 @@ private ExtensionContext getExtensionContextReturningSingleMethod(Object testCas return new ExtensionContext() { - private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>(null); + private final NamespacedHierarchicalStore store = new NamespacedHierarchicalStore<>( + null); @Override public Optional getTestMethod() { @@ -268,7 +269,7 @@ public Optional getTestInstanceLifecycle() { } @Override - public java.util.Optional getTestInstance() { + public Optional getTestInstance() { return Optional.empty(); } @@ -306,7 +307,8 @@ public void publishDirectory(String name, ThrowingConsumer action) { @Override public Store getStore(Namespace namespace) { - var store = new NamespaceAwareStore(this.store, namespace); + var store = new NamespaceAwareStore(this.store, + org.junit.platform.engine.support.store.Namespace.create(namespace.getParts())); method // .map(it -> new ParameterizedTestContext(testClass, it, it.getAnnotation(ParameterizedTest.class))) // @@ -314,6 +316,11 @@ public Store getStore(Namespace namespace) { return store; } + @Override + public Store getStore(StoreScope scope, Namespace namespace) { + return getStore(namespace); + } + @Override public ExecutionMode getExecutionMode() { return ExecutionMode.SAME_THREAD; diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java index 09c83e273e60..85fb2c028746 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutorTests.java @@ -18,6 +18,7 @@ import static org.junit.platform.engine.TestExecutionResult.Status.FAILED; import static org.junit.platform.engine.TestExecutionResult.Status.SUCCESSFUL; import static org.junit.platform.engine.TestExecutionResult.successful; +import static org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore; import static org.junit.platform.launcher.core.OutputDirectoryProviders.dummyOutputDirectoryProvider; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -80,7 +81,7 @@ void init() { private HierarchicalTestExecutor createExecutor( HierarchicalTestExecutorService executorService) { var request = ExecutionRequest.create(root, listener, mock(ConfigurationParameters.class), - dummyOutputDirectoryProvider()); + dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore()); return new HierarchicalTestExecutor<>(request, rootContext, executorService, OpenTest4JAwareThrowableCollector::new); } diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java new file mode 100644 index 000000000000..512d2ad9fef8 --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespaceTests.java @@ -0,0 +1,36 @@ +/* + * 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 org.junit.platform.engine.support.store; + +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.EqualsAndHashCodeAssertions.assertEqualsAndHashCode; + +import org.junit.jupiter.api.Test; + +public class NamespaceTests { + + @Test + void namespacesEqualForSamePartsSequence() { + Namespace ns1 = Namespace.create("part1", "part2"); + Namespace ns2 = Namespace.create("part1", "part2"); + Namespace ns3 = Namespace.create("part2", "part1"); + + assertEqualsAndHashCode(ns1, ns2, ns3); + } + + @Test + void orderOfNamespacePartsDoesMatter() { + Namespace ns1 = Namespace.create("part1", "part2"); + Namespace ns2 = Namespace.create("part2", "part1"); + + assertNotEquals(ns1, ns2); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java index 77b14f19ca6a..81b1797c8b2b 100644 --- a/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java +++ b/platform-tests/src/test/java/org/junit/platform/engine/support/store/NamespacedHierarchicalStoreTests.java @@ -569,5 +569,4 @@ public String toString() { } }; } - } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java index 06f6c308242d..f1687ea26d97 100644 --- a/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/LauncherFactoryTests.java @@ -28,6 +28,10 @@ import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.BeforeAllCallback; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.ExtensionContext.StoreScope; import org.junit.jupiter.api.fixtures.TrackLogRecords; import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.platform.commons.PreconditionViolationException; @@ -37,11 +41,13 @@ import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.TestExecutionResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.support.store.Namespace; import org.junit.platform.fakes.TestEngineSpy; import org.junit.platform.launcher.InterceptedTestEngine; import org.junit.platform.launcher.InterceptorInjectedLauncherSessionListener; import org.junit.platform.launcher.LauncherConstants; import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.LauncherSession; import org.junit.platform.launcher.LauncherSessionListener; import org.junit.platform.launcher.TagFilter; import org.junit.platform.launcher.TestExecutionListener; @@ -333,6 +339,83 @@ public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult })); } + @Test + void extensionCanReadValueFromSessionStoreAndReadByLauncherSessionListenerOnOpened() { + var config = LauncherConfig.builder() // + .addLauncherSessionListeners(new LauncherSessionListenerOpenedExample()) // + .build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(SessionTrackingTestCase.class)).build(); + + AtomicReference errorRef = new AtomicReference<>(); + launcher.execute(request, new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + testExecutionResult.getThrowable().ifPresent(errorRef::set); + } + }); + + assertThat(errorRef.get()).isNull(); + } + } + + @Test + void extensionCanReadValueFromSessionStoreAndReadByLauncherSessionListenerOnClose() { + var config = LauncherConfig.builder() // + .addLauncherSessionListeners(new LauncherSessionListenerClosedExample()) // + .build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(SessionStoringTestCase.class)).build(); + + AtomicReference errorRef = new AtomicReference<>(); + launcher.execute(request, new TestExecutionListener() { + @Override + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + testExecutionResult.getThrowable().ifPresent(errorRef::set); + } + }); + + assertThat(errorRef.get()).isNull(); + } + } + + @Test + void sessionResourceClosedOnSessionClose() { + CloseTrackingResource.closed = false; + var config = LauncherConfig.builder() // + .addLauncherSessionListeners(new AutoCloseCheckListener()) // + .build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(SessionResourceAutoCloseTestCase.class)).build(); + + launcher.execute(request); + assertThat(CloseTrackingResource.closed).isFalse(); + } + + assertThat(CloseTrackingResource.closed).isTrue(); + } + + @Test + void requestResourceClosedOnExecutionClose() { + CloseTrackingResource.closed = false; + var config = LauncherConfig.builder().build(); + + try (LauncherSession session = LauncherFactory.openSession(config)) { + var launcher = session.getLauncher(); + var request = request().selectors(selectClass(RequestResourceAutoCloseTestCase.class)).build(); + + launcher.execute(request); + + assertThat(CloseTrackingResource.closed).isTrue(); + } + } + @SuppressWarnings("SameParameterValue") private static void withSystemProperty(String key, String value, Runnable runnable) { var oldValue = System.getProperty(key); @@ -390,6 +473,125 @@ static class JUnit5Example { @Test void testJ5() { } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(SessionTrackingExtension.class) + static class SessionTrackingTestCase { + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(SessionStoringExtension.class) + static class SessionStoringTestCase { + + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + static class LauncherSessionListenerOpenedExample implements LauncherSessionListener { + @Override + public void launcherSessionOpened(LauncherSession session) { + session.getStore().put(Namespace.GLOBAL, "testKey", "testValue"); + } + } + + static class LauncherSessionListenerClosedExample implements LauncherSessionListener { + @Override + public void launcherSessionClosed(LauncherSession session) { + Object storedValue = session.getStore().get(Namespace.GLOBAL, "testKey"); + assertThat(storedValue).isEqualTo("testValue"); + } + } + + static class SessionTrackingExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + var value = context.getStore(ExtensionContext.Namespace.GLOBAL).get("testKey"); + if (!"testValue".equals(value)) { + throw new IllegalStateException("Expected 'testValue' but got: " + value); + } + + value = context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).get("testKey"); + if (!"testValue".equals(value)) { + throw new IllegalStateException("Expected 'testValue' but got: " + value); + } + } + } + + static class SessionStoringExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).put("testKey", + "testValue"); + } + } + + private static class CloseTrackingResource implements AutoCloseable { + private static boolean closed = false; + + @Override + public void close() { + closed = true; + } + + public boolean isClosed() { + return closed; + } + } + + private static class SessionResourceStoreUsingExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + CloseTrackingResource sessionResource = new CloseTrackingResource(); + context.getStore(StoreScope.LAUNCHER_SESSION, ExtensionContext.Namespace.GLOBAL).put("sessionResource", + sessionResource); + } + } + + private static class RequestResourceStoreUsingExtension implements BeforeAllCallback { + @Override + public void beforeAll(ExtensionContext context) { + CloseTrackingResource requestResource = new CloseTrackingResource(); + context.getStore(StoreScope.EXECUTION_REQUEST, ExtensionContext.Namespace.GLOBAL).put("requestResource", + requestResource); + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(SessionResourceStoreUsingExtension.class) + static class SessionResourceAutoCloseTestCase { + + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + @SuppressWarnings("JUnitMalformedDeclaration") + @ExtendWith(RequestResourceStoreUsingExtension.class) + static class RequestResourceAutoCloseTestCase { + + @Test + void dummyTest() { + // Just a placeholder to trigger the extension + } + } + + private static class AutoCloseCheckListener implements LauncherSessionListener { + @Override + public void launcherSessionClosed(LauncherSession session) { + CloseTrackingResource sessionResource = session // + .getStore() // + .get(Namespace.GLOBAL, "sessionResource", CloseTrackingResource.class); + + assertThat(sessionResource.isClosed()).isFalse(); + } } } diff --git a/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java b/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java new file mode 100644 index 000000000000..043a88952cda --- /dev/null +++ b/platform-tests/src/test/java/org/junit/platform/launcher/core/StoreSharingTests.java @@ -0,0 +1,63 @@ +/* + * 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 org.junit.platform.launcher.core; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.junit.jupiter.api.Test; +import org.junit.platform.engine.ExecutionRequest; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.fakes.TestEngineSpy; +import org.junit.platform.fakes.TestEngineStub; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; + +/** + * @since 5.13 + */ +class StoreSharingTests { + + @Test + void twoDummyEnginesUseRequestLevelStore() { + TestEngineSpy engineWriter = new TestEngineSpy("Writer") { + @Override + public void execute(ExecutionRequest request) { + request.getStore().put(Namespace.GLOBAL, "sharedKey", "Hello from Writer"); + super.execute(request); + } + }; + + TestEngineStub engineReader = new TestEngineStub("Reader") { + @Override + public void execute(ExecutionRequest request) { + Object value = request.getStore().get(Namespace.GLOBAL, "sharedKey"); + assertEquals("Hello from Writer", value); + super.execute(request); + } + }; + + ExecutionRequest request = mock(ExecutionRequest.class); + when(request.getStore()).thenReturn(NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); + + Launcher launcher = LauncherFactory.create( // + LauncherConfig.builder() // + .addTestEngines(engineWriter, engineReader) // + .build()); + + LauncherDiscoveryRequest discoveryRequest = LauncherDiscoveryRequestBuilder // + .request() // + .build(); + + launcher.execute(discoveryRequest); + } +} diff --git a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java index 589ddf72e73a..e776e897f20f 100644 --- a/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java +++ b/platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java @@ -25,6 +25,9 @@ import static org.junit.platform.testkit.engine.EventConditions.test; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import static org.mockito.ArgumentMatchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import java.nio.file.Path; @@ -35,13 +38,20 @@ import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; +import org.junit.platform.engine.ConfigurationParameters; import org.junit.platform.engine.DiscoveryIssue; import org.junit.platform.engine.DiscoveryIssue.Severity; +import org.junit.platform.engine.EngineExecutionListener; +import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.FilterResult; import org.junit.platform.engine.UniqueId; +import org.junit.platform.engine.reporting.OutputDirectoryProvider; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.descriptor.MethodSource; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.PostDiscoveryFilter; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; import org.junit.platform.suite.engine.testcases.ConfigurationSensitiveTestCase; @@ -624,6 +634,25 @@ void discoveryIssueOfNestedTestEnginesAreReported() throws Exception { abstract private static class AbstractPrivateSuite { } + @Test + void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() { + UniqueId engineId = UniqueId.forEngine(SuiteEngineDescriptor.ENGINE_ID); + SuiteEngineDescriptor engineDescriptor = new SuiteEngineDescriptor(engineId); + + SuiteTestDescriptor mockDescriptor = mock(SuiteTestDescriptor.class); + engineDescriptor.addChild(mockDescriptor); + + EngineExecutionListener listener = mock(EngineExecutionListener.class); + NamespacedHierarchicalStore requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore(); + + ExecutionRequest request = ExecutionRequest.create(engineDescriptor, listener, + mock(ConfigurationParameters.class), mock(OutputDirectoryProvider.class), requestLevelStore); + + new SuiteTestEngine().execute(request); + + verify(mockDescriptor).execute(same(listener), same(requestLevelStore)); + } + @Suite @SelectClasses(SingleTestTestCase.class) private static class PrivateSuite { diff --git a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java index 55325c1962c4..60776a7b6c4e 100644 --- a/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java +++ b/platform-tests/src/test/java/org/junit/platform/testkit/engine/EngineTestKitTests.java @@ -11,7 +11,14 @@ package org.junit.platform.testkit.engine; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; +import static org.mockito.ArgumentCaptor.forClass; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.util.Optional; import java.util.function.UnaryOperator; @@ -23,7 +30,15 @@ import org.junit.jupiter.api.extension.RegisterExtension; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.platform.engine.TestEngine; import org.junit.platform.engine.reporting.ReportEntry; +import org.junit.platform.engine.support.store.Namespace; +import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; +import org.junit.platform.launcher.LauncherDiscoveryListener; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.core.EngineExecutionOrchestrator; +import org.mockito.ArgumentCaptor; +import org.mockito.MockedConstruction; class EngineTestKitTests { @@ -46,6 +61,29 @@ void ignoresImplicitConfigurationParametersByDefault() { assertThat(value).isEmpty(); } + @Test + @SuppressWarnings("unchecked") + void verifyRequestLevelStoreIsUsedInExecution() { + TestEngine testEngine = mock(TestEngine.class); + when(testEngine.getId()).thenReturn("test-engine"); + + LauncherDiscoveryRequest request = mock(LauncherDiscoveryRequest.class); + when(request.getDiscoveryListener()).thenReturn(LauncherDiscoveryListener.NOOP); + + try (MockedConstruction mockedConstruction = mockConstruction( + EngineExecutionOrchestrator.class)) { + EngineTestKit.execute(testEngine, request); + assertThat(mockedConstruction.constructed()).isNotEmpty(); + + EngineExecutionOrchestrator mockOrchestrator = mockedConstruction.constructed().getFirst(); + ArgumentCaptor> storeCaptor = forClass( + NamespacedHierarchicalStore.class); + + verify(mockOrchestrator).execute(any(), any(), storeCaptor.capture()); + assertNotNull(storeCaptor.getValue(), "Request level store should be passed to execute"); + } + } + @ParameterizedTest @CsvSource({ "true, from system property", "false," }) void usesImplicitConfigurationParametersWhenEnabled(boolean enabled, String expectedValue) {