diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/CancellationToken.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/CancellationToken.java
new file mode 100644
index 000000000000..fa25414529c6
--- /dev/null
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/CancellationToken.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 org.junit.platform.engine;
+
+import org.apiguardian.api.API;
+
+/**
+ * Token that should be checked to determine whether an operation was requested
+ * to be cancelled.
+ *
+ *
For example, this is used by {@link org.junit.platform.engine.TestEngine}
+ * implementations to determine whether
+ *
+ *
This interface is not intended to be implemented by clients.
+ *
+ * @since 6.0
+ * @see ExecutionRequest#getCancellationToken()
+ */
+@API(status = API.Status.EXPERIMENTAL, since = "6.0")
+public interface CancellationToken {
+
+ static CancellationToken create() {
+ return new DefaultCancellationToken();
+ }
+
+ boolean isCancellationRequested();
+
+ void cancel();
+
+}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/DefaultCancellationToken.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/DefaultCancellationToken.java
new file mode 100644
index 000000000000..b354baa3a4ed
--- /dev/null
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/DefaultCancellationToken.java
@@ -0,0 +1,28 @@
+/*
+ * 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;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+class DefaultCancellationToken implements CancellationToken {
+
+ private final AtomicBoolean cancelled = new AtomicBoolean();
+
+ @Override
+ public boolean isCancellationRequested() {
+ return cancelled.get();
+ }
+
+ @Override
+ public void cancel() {
+ cancelled.set(true);
+ }
+}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java
index 620f6057b94b..3b23eb59c85c 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/ExecutionRequest.java
@@ -43,21 +43,21 @@ public class ExecutionRequest {
private final TestDescriptor rootTestDescriptor;
private final EngineExecutionListener engineExecutionListener;
private final ConfigurationParameters configurationParameters;
-
private final @Nullable OutputDirectoryProvider outputDirectoryProvider;
-
private final @Nullable NamespacedHierarchicalStore requestLevelStore;
+ private final @Nullable CancellationToken cancellationToken;
@Deprecated
@API(status = DEPRECATED, since = "1.11")
public ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener,
ConfigurationParameters configurationParameters) {
- this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null);
+ this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null, null);
}
private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener,
ConfigurationParameters configurationParameters, @Nullable OutputDirectoryProvider outputDirectoryProvider,
- @Nullable NamespacedHierarchicalStore requestLevelStore) {
+ @Nullable NamespacedHierarchicalStore requestLevelStore,
+ @Nullable CancellationToken cancellationToken) {
this.rootTestDescriptor = Preconditions.notNull(rootTestDescriptor, "rootTestDescriptor must not be null");
this.engineExecutionListener = Preconditions.notNull(engineExecutionListener,
"engineExecutionListener must not be null");
@@ -65,6 +65,7 @@ private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListe
"configurationParameters must not be null");
this.outputDirectoryProvider = outputDirectoryProvider;
this.requestLevelStore = requestLevelStore;
+ this.cancellationToken = cancellationToken;
}
/**
@@ -105,11 +106,13 @@ public static ExecutionRequest create(TestDescriptor rootTestDescriptor,
@API(status = INTERNAL, since = "1.13")
public static ExecutionRequest create(TestDescriptor rootTestDescriptor,
EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters,
- OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore requestLevelStore) {
+ OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore requestLevelStore,
+ CancellationToken cancellationToken) {
return new ExecutionRequest(rootTestDescriptor, engineExecutionListener, configurationParameters,
Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null"),
- Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null"));
+ Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null"),
+ Preconditions.notNull(cancellationToken, "cancellationToken must not be null"));
}
/**
@@ -171,4 +174,8 @@ public NamespacedHierarchicalStore getStore() {
"No NamespacedHierarchicalStore was configured for this request");
}
+ public CancellationToken getCancellationToken() {
+ return Preconditions.notNull(this.cancellationToken, "No CancellationToken was configured for this request");
+ }
+
}
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java
index 68549dbf632a..888e7b30384c 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/HierarchicalTestExecutor.java
@@ -13,6 +13,7 @@
import java.util.concurrent.Future;
import org.jspecify.annotations.Nullable;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
@@ -51,8 +52,9 @@ class HierarchicalTestExecutor {
TestDescriptor rootTestDescriptor = this.request.getRootTestDescriptor();
EngineExecutionListener executionListener = this.request.getEngineExecutionListener();
NodeExecutionAdvisor executionAdvisor = new NodeTreeWalker().walk(rootTestDescriptor);
+ CancellationToken cancellationToken = this.request.getCancellationToken();
NodeTestTaskContext taskContext = new NodeTestTaskContext(executionListener, this.executorService,
- this.throwableCollectorFactory, executionAdvisor);
+ this.throwableCollectorFactory, executionAdvisor, cancellationToken);
NodeTestTask rootTestTask = new NodeTestTask<>(taskContext, rootTestDescriptor);
rootTestTask.setParentContext(this.rootContext);
return this.executorService.submit(rootTestTask);
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java
index 5249236fd729..1937e1b77cc7 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTask.java
@@ -46,6 +46,9 @@
class NodeTestTask implements TestTask {
private static final Logger logger = LoggerFactory.getLogger(NodeTestTask.class);
+
+ private static final SkipResult CANCELLED_SKIP_RESULT = SkipResult.skip("Test execution cancelled");
+
private static final Runnable NOOP = () -> {
};
@@ -101,7 +104,7 @@ public void execute() {
throwableCollector = taskContext.throwableCollectorFactory().create();
prepare();
if (throwableCollector.isEmpty()) {
- checkWhetherSkipped();
+ throwableCollector.execute(() -> skipResult = checkWhetherSkipped());
}
if (throwableCollector.isEmpty() && !requiredSkipResult().isSkipped()) {
executeRecursively();
@@ -139,8 +142,10 @@ private void prepare() {
parentContext = null;
}
- private void checkWhetherSkipped() {
- requiredThrowableCollector().execute(() -> skipResult = node.shouldBeSkipped(requiredContext()));
+ private SkipResult checkWhetherSkipped() throws Exception {
+ return taskContext.cancellationToken().isCancellationRequested() //
+ ? CANCELLED_SKIP_RESULT //
+ : node.shouldBeSkipped(requiredContext());
}
private void executeRecursively() {
diff --git a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskContext.java b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskContext.java
index 005d8ab6d02c..d5dbc34c5e09 100644
--- a/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskContext.java
+++ b/junit-platform-engine/src/main/java/org/junit/platform/engine/support/hierarchical/NodeTestTaskContext.java
@@ -10,19 +10,22 @@
package org.junit.platform.engine.support.hierarchical;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.EngineExecutionListener;
/**
* @since 1.3.1
*/
record NodeTestTaskContext(EngineExecutionListener listener, HierarchicalTestExecutorService executorService,
- ThrowableCollector.Factory throwableCollectorFactory, NodeExecutionAdvisor executionAdvisor) {
+ ThrowableCollector.Factory throwableCollectorFactory, NodeExecutionAdvisor executionAdvisor,
+ CancellationToken cancellationToken) {
NodeTestTaskContext withListener(EngineExecutionListener listener) {
if (this.listener == listener) {
return this;
}
- return new NodeTestTaskContext(listener, executorService, throwableCollectorFactory, executionAdvisor);
+ return new NodeTestTaskContext(listener, executorService, throwableCollectorFactory, executionAdvisor,
+ cancellationToken);
}
}
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/Launcher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/Launcher.java
index 75a5f07deabf..4c25ec3939cc 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/Launcher.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/Launcher.java
@@ -10,9 +10,11 @@
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.CancellationToken;
/**
* The {@code Launcher} API is the main entry point for client code that
@@ -108,6 +110,37 @@ public interface Launcher {
*/
void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners);
+ /**
+ * Execute a {@link TestPlan} which is built according to the supplied
+ * {@link LauncherDiscoveryRequest} by querying all registered engines and
+ * collecting their results, and notify
+ * {@linkplain #registerTestExecutionListeners registered listeners} about
+ * the progress and results of the execution.
+ *
+ * Supplied test execution listeners are registered in addition to already
+ * registered listeners but only for the supplied launcher discovery request.
+ *
+ *
Additionally, it's possible to request cancellation of the started
+ * execution via the supplied {@link CancellationToken}.
+ *
+ * @apiNote Calling this method will cause test discovery to be executed for
+ * all registered engines. If the same {@link LauncherDiscoveryRequest} was
+ * previously passed to {@link #discover(LauncherDiscoveryRequest)}, you
+ * should instead call {@link #execute(TestPlan, TestExecutionListener...)}
+ * and pass the already acquired {@link TestPlan} to avoid the potential
+ * performance degradation (e.g., classpath scanning) of running test
+ * discovery twice.
+ *
+ * @param launcherDiscoveryRequest the launcher discovery request; never {@code null}
+ * @param cancellationToken the token used to request cancellation of the
+ * started execution; never {@code null}
+ * @param listeners additional test execution listeners; never {@code null}
+ * @since 6.0
+ */
+ @API(status = EXPERIMENTAL, since = "6.0")
+ void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, CancellationToken cancellationToken,
+ TestExecutionListener... listeners);
+
/**
* Execute the supplied {@link TestPlan} and notify
* {@linkplain #registerTestExecutionListeners registered listeners} about
@@ -127,4 +160,28 @@ public interface Launcher {
@API(status = STABLE, since = "1.4")
void execute(TestPlan testPlan, TestExecutionListener... listeners);
+ /**
+ * Execute the supplied {@link TestPlan} and notify
+ * {@linkplain #registerTestExecutionListeners registered listeners} about
+ * the progress and results of the execution.
+ *
+ *
Supplied test execution listeners are registered in addition to
+ * already registered listeners but only for the execution of the supplied
+ * test plan.
+ *
+ *
Additionally, it's possible to request cancellation of the started
+ * execution via the supplied {@link CancellationToken}.
+ *
+ * @apiNote The supplied {@link TestPlan} must not have been executed
+ * previously.
+ *
+ * @param testPlan the test plan to execute; never {@code null}
+ * @param cancellationToken the token used to request cancellation of the
+ * started execution; never {@code null}
+ * @param listeners additional test execution listeners; never {@code null}
+ * @since 6.0
+ */
+ @API(status = EXPERIMENTAL, since = "6.0")
+ void execute(TestPlan testPlan, CancellationToken cancellationToken, TestExecutionListener... listeners);
+
}
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 509467bf108a..9cb3d611f225 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
@@ -18,6 +18,7 @@
import java.util.Collection;
import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
@@ -85,28 +86,42 @@ public TestPlan discover(LauncherDiscoveryRequest discoveryRequest) {
@Override
public void execute(LauncherDiscoveryRequest discoveryRequest, TestExecutionListener... listeners) {
+ execute(discoveryRequest, CancellationToken.create(), listeners);
+ }
+
+ @Override
+ public void execute(LauncherDiscoveryRequest discoveryRequest, CancellationToken cancellationToken,
+ TestExecutionListener... listeners) {
+
Preconditions.notNull(discoveryRequest, "LauncherDiscoveryRequest must not be null");
Preconditions.notNull(listeners, "TestExecutionListener array must not be null");
Preconditions.containsNoNullElements(listeners, "individual listeners must not be null");
- execute(InternalTestPlan.from(discover(discoveryRequest, EXECUTION)), listeners);
+ execute(InternalTestPlan.from(discover(discoveryRequest, EXECUTION)), cancellationToken, listeners);
}
@Override
public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
+ execute(testPlan, CancellationToken.create(), listeners);
+ }
+
+ @Override
+ public void execute(TestPlan testPlan, CancellationToken cancellationToken, TestExecutionListener... listeners) {
Preconditions.notNull(testPlan, "TestPlan must not be null");
Preconditions.condition(testPlan instanceof InternalTestPlan, "TestPlan was not returned by this Launcher");
+ Preconditions.notNull(cancellationToken, "CancellationToken must not be null");
Preconditions.notNull(listeners, "TestExecutionListener array must not be null");
Preconditions.containsNoNullElements(listeners, "individual listeners must not be null");
- execute((InternalTestPlan) testPlan, listeners);
+ execute((InternalTestPlan) testPlan, cancellationToken, listeners);
}
private LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, LauncherPhase phase) {
return discoveryOrchestrator.discover(discoveryRequest, phase);
}
- private void execute(InternalTestPlan internalTestPlan, TestExecutionListener[] listeners) {
+ private void execute(InternalTestPlan internalTestPlan, CancellationToken cancellationToken,
+ TestExecutionListener[] listeners) {
try (NamespacedHierarchicalStore requestLevelStore = createRequestLevelStore()) {
- executionOrchestrator.execute(internalTestPlan, requestLevelStore, listeners);
+ executionOrchestrator.execute(internalTestPlan, requestLevelStore, cancellationToken, listeners);
}
}
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 b128e09b1258..a842233660a5 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
@@ -17,6 +17,7 @@
import java.util.function.Supplier;
import org.junit.platform.commons.PreconditionViolationException;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
import org.junit.platform.launcher.Launcher;
@@ -121,10 +122,22 @@ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecu
throw new PreconditionViolationException("Launcher session has already been closed");
}
+ @Override
+ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, CancellationToken cancellationToken,
+ TestExecutionListener... listeners) {
+ throw new PreconditionViolationException("Launcher session has already been closed");
+ }
+
@Override
public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
throw new PreconditionViolationException("Launcher session has already been closed");
}
+
+ @Override
+ public void execute(TestPlan testPlan, CancellationToken cancellationToken,
+ TestExecutionListener... listeners) {
+ throw new PreconditionViolationException("Launcher session has already been closed");
+ }
}
private static LauncherInterceptor composite(List interceptors) {
diff --git a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java
index 3cc6be2316f0..41af9e39d6fb 100644
--- a/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java
+++ b/junit-platform-launcher/src/main/java/org/junit/platform/launcher/core/DelegatingLauncher.java
@@ -10,6 +10,7 @@
package org.junit.platform.launcher.core;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.launcher.Launcher;
import org.junit.platform.launcher.LauncherDiscoveryListener;
import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -47,9 +48,19 @@ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecu
delegate.execute(launcherDiscoveryRequest, listeners);
}
+ @Override
+ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, CancellationToken cancellationToken,
+ TestExecutionListener... listeners) {
+ delegate.execute(launcherDiscoveryRequest, cancellationToken, listeners);
+ }
+
@Override
public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
delegate.execute(testPlan, listeners);
}
+ @Override
+ public void execute(TestPlan testPlan, CancellationToken cancellationToken, TestExecutionListener... listeners) {
+ delegate.execute(testPlan, cancellationToken, listeners);
+ }
}
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 0536b5134736..f1328cc0c44c 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
@@ -23,6 +23,7 @@
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.UnrecoverableExceptions;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
@@ -56,13 +57,13 @@ public EngineExecutionOrchestrator() {
}
void execute(InternalTestPlan internalTestPlan, NamespacedHierarchicalStore requestLevelStore,
- TestExecutionListener... listeners) {
+ CancellationToken cancellationToken, TestExecutionListener... listeners) {
ConfigurationParameters configurationParameters = internalTestPlan.getConfigurationParameters();
ListenerRegistry testExecutionListenerListeners = buildListenerRegistryForExecution(
listeners);
withInterceptedStreams(configurationParameters, testExecutionListenerListeners,
testExecutionListener -> execute(internalTestPlan, EngineExecutionListener.NOOP, testExecutionListener,
- requestLevelStore));
+ requestLevelStore, cancellationToken));
}
/**
@@ -72,20 +73,22 @@ void execute(InternalTestPlan internalTestPlan, NamespacedHierarchicalStore requestLevelStore) {
+ TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore,
+ CancellationToken cancellationToken) {
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, requestLevelStore);
+ execute(internalTestPlan, engineExecutionListener, testExecutionListener, requestLevelStore, cancellationToken);
}
private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener parentEngineExecutionListener,
- TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore) {
+ TestExecutionListener testExecutionListener, NamespacedHierarchicalStore requestLevelStore,
+ CancellationToken cancellationToken) {
internalTestPlan.markStarted();
// Do not directly pass the internal test plan to test execution listeners.
@@ -100,7 +103,7 @@ private void execute(InternalTestPlan internalTestPlan, EngineExecutionListener
else {
execute(discoveryResult,
buildEngineExecutionListener(parentEngineExecutionListener, testExecutionListener, testPlan),
- requestLevelStore);
+ requestLevelStore, cancellationToken);
}
testExecutionListener.testPlanExecutionFinished(testPlan);
}
@@ -159,9 +162,9 @@ private void withInterceptedStreams(ConfigurationParameters configurationParamet
* discovery results} and notifies the supplied {@linkplain
* EngineExecutionListener listener} of execution events.
*/
- @API(status = INTERNAL, since = "1.7", consumers = { "org.junit.platform.testkit" })
+ @API(status = INTERNAL, since = "6.0", consumers = { "org.junit.platform.testkit" })
public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener engineExecutionListener,
- NamespacedHierarchicalStore requestLevelStore) {
+ NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken) {
Preconditions.notNull(discoveryResult, "discoveryResult must not be null");
Preconditions.notNull(engineExecutionListener, "engineExecutionListener must not be null");
@@ -169,7 +172,7 @@ public void execute(LauncherDiscoveryResult discoveryResult, EngineExecutionList
EngineExecutionListener listener = selectExecutionListener(engineExecutionListener, configurationParameters);
for (TestEngine testEngine : discoveryResult.getTestEngines()) {
- failOrExecuteEngine(discoveryResult, listener, testEngine, requestLevelStore);
+ failOrExecuteEngine(discoveryResult, listener, testEngine, requestLevelStore, cancellationToken);
}
}
@@ -184,7 +187,8 @@ private static EngineExecutionListener selectExecutionListener(EngineExecutionLi
}
private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, EngineExecutionListener listener,
- TestEngine testEngine, NamespacedHierarchicalStore requestLevelStore) {
+ TestEngine testEngine, NamespacedHierarchicalStore requestLevelStore,
+ CancellationToken cancellationToken) {
EngineResultInfo engineDiscoveryResult = discoveryResult.getEngineResult(testEngine);
DiscoveryIssueNotifier discoveryIssueNotifier = shouldReportDiscoveryIssues(discoveryResult) //
? engineDiscoveryResult.getDiscoveryIssueNotifier() //
@@ -202,7 +206,8 @@ private void failOrExecuteEngine(LauncherDiscoveryResult discoveryResult, Engine
}
else {
executeEngine(engineDescriptor, listener, discoveryResult.getConfigurationParameters(), testEngine,
- discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier, requestLevelStore);
+ discoveryResult.getOutputDirectoryProvider(), discoveryIssueNotifier, requestLevelStore,
+ cancellationToken);
}
}
@@ -223,12 +228,21 @@ private ListenerRegistry buildListenerRegistryForExecutio
private void executeEngine(TestDescriptor engineDescriptor, EngineExecutionListener listener,
ConfigurationParameters configurationParameters, TestEngine testEngine,
OutputDirectoryProvider outputDirectoryProvider, DiscoveryIssueNotifier discoveryIssueNotifier,
- NamespacedHierarchicalStore requestLevelStore) {
+ NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken) {
+
+ if (cancellationToken.isCancellationRequested()) {
+ listener.executionStarted(engineDescriptor);
+ engineDescriptor.getChildren().forEach(
+ child -> listener.executionSkipped(child, "Cancellation of execution requested"));
+ listener.executionFinished(engineDescriptor, TestExecutionResult.aborted(null));
+ return;
+ }
+
OutcomeDelayingEngineExecutionListener delayingListener = new OutcomeDelayingEngineExecutionListener(listener,
engineDescriptor);
try {
testEngine.execute(ExecutionRequest.create(engineDescriptor, delayingListener, configurationParameters,
- outputDirectoryProvider, requestLevelStore));
+ outputDirectoryProvider, requestLevelStore, cancellationToken));
discoveryIssueNotifier.logNonCriticalIssues(testEngine);
delayingListener.reportEngineOutcome();
}
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 cb12eafb7282..3457d4d050e0 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
@@ -14,6 +14,7 @@
import java.util.function.Function;
import java.util.function.Supplier;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.support.store.Namespace;
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
import org.junit.platform.launcher.Launcher;
@@ -67,6 +68,14 @@ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecu
}
}
+ @Override
+ public void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, CancellationToken cancellationToken,
+ TestExecutionListener... listeners) {
+ try (LauncherSession session = createSession()) {
+ session.getLauncher().execute(launcherDiscoveryRequest, cancellationToken, listeners);
+ }
+ }
+
@Override
public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
try (LauncherSession session = createSession()) {
@@ -74,6 +83,13 @@ public void execute(TestPlan testPlan, TestExecutionListener... listeners) {
}
}
+ @Override
+ public void execute(TestPlan testPlan, CancellationToken cancellationToken, TestExecutionListener... listeners) {
+ try (LauncherSession session = createSession()) {
+ session.getLauncher().execute(testPlan, cancellationToken, listeners);
+ }
+ }
+
private LauncherSession createSession() {
LauncherSession session = new DefaultLauncherSession(interceptorFactory.get(), sessionListenerSupplier,
this.launcherFactory);
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 640d90f984e1..88dfdbf8fecc 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
@@ -16,6 +16,7 @@
import java.util.Set;
import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestEngine;
import org.junit.platform.engine.UniqueId;
@@ -60,9 +61,10 @@ LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, Uniq
TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult,
EngineExecutionListener parentEngineExecutionListener,
- NamespacedHierarchicalStore requestLevelStore) {
+ NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken) {
SummaryGeneratingListener listener = new SummaryGeneratingListener();
- executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore);
+ executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore,
+ cancellationToken);
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 356fe4e5ff17..1cbcb4089f46 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
@@ -27,6 +27,7 @@
import org.junit.platform.commons.support.ReflectionSupport;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.commons.util.StringUtils;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.DiscoveryIssue;
import org.junit.platform.engine.EngineDiscoveryListener;
@@ -149,13 +150,13 @@ private static String getSuiteDisplayName(Class> testClass) {
}
void execute(EngineExecutionListener parentEngineExecutionListener,
- NamespacedHierarchicalStore requestLevelStore) {
+ NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken) {
parentEngineExecutionListener.executionStarted(this);
ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector();
executeBeforeSuiteMethods(throwableCollector);
- TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore,
+ TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore, cancellationToken,
throwableCollector);
executeAfterSuiteMethods(throwableCollector);
@@ -177,7 +178,8 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
}
private @Nullable TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener,
- NamespacedHierarchicalStore requestLevelStore, ThrowableCollector throwableCollector) {
+ NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken,
+ ThrowableCollector throwableCollector) {
if (throwableCollector.isNotEmpty()) {
return null;
}
@@ -187,7 +189,8 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
// be pruned accordingly.
LauncherDiscoveryResult discoveryResult = requireNonNull(this.launcherDiscoveryResult).withRetainedEngines(
getChildren()::contains);
- return requireNonNull(launcher).execute(discoveryResult, parentEngineExecutionListener, requestLevelStore);
+ return requireNonNull(launcher).execute(discoveryResult, parentEngineExecutionListener, requestLevelStore,
+ cancellationToken);
}
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 d75cf8c3dfbf..9a95190fe0db 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
@@ -73,7 +73,7 @@ public void execute(ExecutionRequest request) {
suiteEngineDescriptor.getChildren()
.stream()
.map(SuiteTestDescriptor.class::cast)
- .forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore));
+ .forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore, request.getCancellationToken()));
// @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 b12e781325bb..e7a4ef26556c 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
@@ -12,6 +12,7 @@
import static java.util.Collections.emptySet;
import static java.util.Collections.singleton;
+import static java.util.Objects.requireNonNullElseGet;
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.MAINTAINED;
import static org.apiguardian.api.API.Status.STABLE;
@@ -30,6 +31,7 @@
import org.junit.platform.commons.PreconditionViolationException;
import org.junit.platform.commons.util.CollectionUtils;
import org.junit.platform.commons.util.Preconditions;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.DiscoveryFilter;
import org.junit.platform.engine.DiscoveryIssue;
import org.junit.platform.engine.DiscoverySelector;
@@ -244,16 +246,18 @@ public static EngineExecutionResults execute(TestEngine testEngine, LauncherDisc
Preconditions.notNull(discoveryRequest, "EngineDiscoveryRequest must not be null");
ExecutionRecorder executionRecorder = new ExecutionRecorder();
- executeUsingLauncherOrchestration(testEngine, discoveryRequest, executionRecorder);
+ executeUsingLauncherOrchestration(testEngine, discoveryRequest, executionRecorder, CancellationToken.create());
return executionRecorder.getExecutionResults();
}
private static void executeUsingLauncherOrchestration(TestEngine testEngine,
- LauncherDiscoveryRequest discoveryRequest, EngineExecutionListener listener) {
+ LauncherDiscoveryRequest discoveryRequest, EngineExecutionListener listener,
+ CancellationToken cancellationToken) {
LauncherDiscoveryResult discoveryResult = discoverUsingOrchestrator(testEngine, discoveryRequest);
TestDescriptor engineTestDescriptor = discoveryResult.getEngineTestDescriptor(testEngine);
Preconditions.notNull(engineTestDescriptor, "TestEngine did not yield a TestDescriptor");
- withRequestLevelStore(store -> new EngineExecutionOrchestrator().execute(discoveryResult, listener, store));
+ withRequestLevelStore(
+ store -> new EngineExecutionOrchestrator().execute(discoveryResult, listener, store, cancellationToken));
}
private static void withRequestLevelStore(Consumer> action) {
@@ -308,8 +312,11 @@ public static final class Builder {
private final LauncherDiscoveryRequestBuilder requestBuilder = LauncherDiscoveryRequestBuilder.request() //
.enableImplicitConfigurationParameters(false) //
.outputDirectoryProvider(DisabledOutputDirectoryProvider.INSTANCE);
+
private final TestEngine testEngine;
+ private @Nullable CancellationToken cancellationToken;
+
private Builder(TestEngine testEngine) {
this.testEngine = testEngine;
}
@@ -430,6 +437,15 @@ public Builder outputDirectoryProvider(OutputDirectoryProvider outputDirectoryPr
return this;
}
+ /**
+ * @since 6.0
+ */
+ @API(status = EXPERIMENTAL, since = "6.0")
+ public Builder cancellationToken(CancellationToken cancellationToken) {
+ this.cancellationToken = Preconditions.notNull(cancellationToken, "cancellationToken must not be null");
+ return this;
+ }
+
/**
* Discover tests for the configured {@link TestEngine},
* {@linkplain DiscoverySelector discovery selectors},
@@ -464,7 +480,8 @@ public EngineDiscoveryResults discover() {
public EngineExecutionResults execute() {
LauncherDiscoveryRequest request = this.requestBuilder.build();
ExecutionRecorder executionRecorder = new ExecutionRecorder();
- EngineTestKit.executeUsingLauncherOrchestration(this.testEngine, request, executionRecorder);
+ EngineTestKit.executeUsingLauncherOrchestration(this.testEngine, request, executionRecorder,
+ requireNonNullElseGet(this.cancellationToken, CancellationToken::create));
return executionRecorder.getExecutionResults();
}
diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java
index 2fa6af237698..23752532c65f 100644
--- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java
+++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java
@@ -74,6 +74,10 @@ public Request toRequest() {
return new RunnerRequest(this.runner);
}
+ public Runner getRunner() {
+ return this.runner;
+ }
+
@Override
protected boolean tryToExcludeFromRunner(Description description) {
boolean excluded = tryToFilterRunner(description);
diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunListenerAdapter.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunListenerAdapter.java
index 6a0b4756f7fd..6c48e48e81a4 100644
--- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunListenerAdapter.java
+++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunListenerAdapter.java
@@ -103,6 +103,10 @@ public void testSuiteFinished(Description description) {
@Override
public void testRunFinished(Result result) {
+ testRunFinished();
+ }
+
+ void testRunFinished() {
reportContainerFinished(testRun.getRunnerTestDescriptor());
}
diff --git a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunnerExecutor.java b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunnerExecutor.java
index 7e5bf6b1306a..921418c6ec58 100644
--- a/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunnerExecutor.java
+++ b/junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunnerExecutor.java
@@ -11,13 +11,13 @@
package org.junit.vintage.engine.execution;
import static org.apiguardian.api.API.Status.INTERNAL;
+import static org.junit.platform.commons.util.UnrecoverableExceptions.rethrowIfUnrecoverable;
import static org.junit.platform.engine.TestExecutionResult.failed;
import org.apiguardian.api.API;
-import org.junit.platform.commons.util.UnrecoverableExceptions;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestExecutionResult;
-import org.junit.runner.JUnitCore;
+import org.junit.runner.notification.RunNotifier;
import org.junit.vintage.engine.descriptor.RunnerTestDescriptor;
import org.junit.vintage.engine.descriptor.TestSourceProvider;
@@ -35,14 +35,17 @@ public RunnerExecutor(EngineExecutionListener engineExecutionListener) {
}
public void execute(RunnerTestDescriptor runnerTestDescriptor) {
- TestRun testRun = new TestRun(runnerTestDescriptor);
- JUnitCore core = new JUnitCore();
- core.addListener(new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider));
+ var notifier = new RunNotifier();
+ var testRun = new TestRun(runnerTestDescriptor);
+ var listener = new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider);
+ notifier.addListener(listener);
try {
- core.run(runnerTestDescriptor.toRequest());
+ listener.testRunStarted(runnerTestDescriptor.getDescription());
+ runnerTestDescriptor.getRunner().run(notifier);
+ listener.testRunFinished();
}
catch (Throwable t) {
- UnrecoverableExceptions.rethrowIfUnrecoverable(t);
+ rethrowIfUnrecoverable(t);
reportUnexpectedFailure(testRun, runnerTestDescriptor, failed(t));
}
}
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 7044e3f5076a..81d7c376ffbd 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
@@ -44,6 +44,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.DisabledInEclipse;
import org.junit.platform.commons.util.ReflectionUtils;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
@@ -943,7 +944,7 @@ private static void execute(Class> testClass, EngineExecutionListener listener
var engineTestDescriptor = testEngine.discover(discoveryRequest, UniqueId.forEngine(testEngine.getId()));
testEngine.execute(
ExecutionRequest.create(engineTestDescriptor, listener, discoveryRequest.getConfigurationParameters(),
- dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore()));
+ dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore(), CancellationToken.create()));
}
private static LauncherDiscoveryRequest request(Class> testClass) {
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java
index 8b6b7c5ae009..02d084aa65be 100644
--- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/AbstractJupiterTestEngineTests.java
@@ -97,6 +97,14 @@ protected EngineDiscoveryResults discoverTests(LauncherDiscoveryRequest request)
return EngineTestKit.discover(this.engine, request);
}
+ protected EngineTestKit.Builder jupiterTestEngine() {
+ return EngineTestKit.engine(this.engine) //
+ .outputDirectoryProvider(dummyOutputDirectoryProvider()) //
+ .configurationParameter(STACKTRACE_PRUNING_ENABLED_PROPERTY_NAME, String.valueOf(false)) //
+ .configurationParameter(CRITICAL_DISCOVERY_ISSUE_SEVERITY_PROPERTY_NAME, Severity.INFO.name()) //
+ .enableImplicitConfigurationParameters(false);
+ }
+
protected static LauncherDiscoveryRequestBuilder defaultRequest() {
return request() //
.outputDirectoryProvider(dummyOutputDirectoryProvider()) //
diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ExecutionCancellationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ExecutionCancellationTests.java
new file mode 100644
index 000000000000..9dcd0677b50d
--- /dev/null
+++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ExecutionCancellationTests.java
@@ -0,0 +1,94 @@
+/*
+ * 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 java.util.Objects.requireNonNull;
+import static org.junit.jupiter.api.Assertions.fail;
+import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
+import static org.junit.platform.testkit.engine.EventConditions.container;
+import static org.junit.platform.testkit.engine.EventConditions.engine;
+import static org.junit.platform.testkit.engine.EventConditions.event;
+import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
+import static org.junit.platform.testkit.engine.EventConditions.reportEntry;
+import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
+import static org.junit.platform.testkit.engine.EventConditions.started;
+import static org.junit.platform.testkit.engine.EventConditions.test;
+
+import java.util.Map;
+
+import org.jspecify.annotations.Nullable;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.TestReporter;
+import org.junit.platform.engine.CancellationToken;
+
+class ExecutionCancellationTests extends AbstractJupiterTestEngineTests {
+
+ @Test
+ void canCancelExecutionWhileTestsAreRunning() {
+ var testClass = TestCase.class;
+ var cancellationToken = CancellationToken.create();
+
+ TestCase.cancellationToken = cancellationToken;
+
+ var results = jupiterTestEngine() //
+ .selectors(selectClass(testClass)) //
+ .cancellationToken(cancellationToken) //
+ .execute();
+
+ results.allEvents().assertEventsMatchExactly( //
+ event(engine(), started()), //
+ event(container(testClass), started()), //
+ event(test("first"), started()), //
+ event(test("first"), reportEntry(Map.of("cancelled", "true"))), //
+ event(test("first"), finishedSuccessfully()), //
+ event(test("second"), skippedWithReason("Test execution cancelled")), //
+ event(container(testClass), finishedSuccessfully()), //
+ event(engine(), finishedSuccessfully()));
+ }
+
+ @AfterEach
+ void resetCancellationToken() {
+ TestCase.cancellationToken = null;
+ }
+
+ @SuppressWarnings("JUnitMalformedDeclaration")
+ @TestMethodOrder(OrderAnnotation.class)
+ static class TestCase {
+
+ static @Nullable CancellationToken cancellationToken;
+
+ @Test
+ @Order(1)
+ void first() {
+ requiredCancellationToken().cancel();
+ }
+
+ @AfterEach
+ void afterEach(TestReporter reporter) {
+ reporter.publishEntry("cancelled", String.valueOf(requiredCancellationToken().isCancellationRequested()));
+ }
+
+ @Test
+ @Order(2)
+ void second() {
+ fail("should not be called");
+ }
+
+ private CancellationToken requiredCancellationToken() {
+ return requireNonNull(cancellationToken);
+ }
+ }
+
+}
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 85fb2c028746..16df6e590b78 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
@@ -39,6 +39,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.function.ThrowingConsumer;
+import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
@@ -81,7 +82,7 @@ void init() {
private HierarchicalTestExecutor createExecutor(
HierarchicalTestExecutorService executorService) {
var request = ExecutionRequest.create(root, listener, mock(ConfigurationParameters.class),
- dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore());
+ dummyOutputDirectoryProvider(), dummyNamespacedHierarchicalStore(), CancellationToken.create());
return new HierarchicalTestExecutor<>(request, rootContext, executorService,
OpenTest4JAwareThrowableCollector::new);
}
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 91b8b5d73676..daed711ad368 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
@@ -38,6 +38,7 @@
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.CancellationToken;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.DiscoveryIssue;
import org.junit.platform.engine.DiscoveryIssue.Severity;
@@ -644,13 +645,15 @@ void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() {
EngineExecutionListener listener = mock(EngineExecutionListener.class);
NamespacedHierarchicalStore requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore();
+ CancellationToken cancellationToken = CancellationToken.create();
ExecutionRequest request = ExecutionRequest.create(engineDescriptor, listener,
- mock(ConfigurationParameters.class), mock(OutputDirectoryProvider.class), requestLevelStore);
+ mock(ConfigurationParameters.class), mock(OutputDirectoryProvider.class), requestLevelStore,
+ cancellationToken);
new SuiteTestEngine().execute(request);
- verify(mockDescriptor).execute(same(listener), same(requestLevelStore));
+ verify(mockDescriptor).execute(same(listener), same(requestLevelStore), same(cancellationToken));
}
@Suite
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 dffb0255fe7d..3fbfe54c359d 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
@@ -80,7 +80,7 @@ void verifyRequestLevelStoreIsUsedInExecution() {
ArgumentCaptor> storeCaptor = forClass(
NamespacedHierarchicalStore.class);
- verify(mockOrchestrator).execute(any(), any(), storeCaptor.capture());
+ verify(mockOrchestrator).execute(any(), any(), storeCaptor.capture(), any());
assertNotNull(storeCaptor.getValue(), "Request level store should be passed to execute");
}
}