Skip to content

Commit 6153c35

Browse files
authored
Introduce support for cancelling a running execution to the Launcher API (#4728)
This commit introduces `CancellationToken` that can be created and passed to the `Launcher` as part of a `LauncherExecutionRequest`. The `Launcher` checks whether cancellation has been requested on the token prior to asking each test engine to execute tests. If cancellation has been requested, the `Launcher` reports all direct children of engine descriptors as skipped. Moreover, it passes the `CancellationToken` to each engine so they can check and respond to cancellation as well which will be implemented separately as indicated by TODO comments. `EngineTestKit` also supports passing a `CancellationToken` to the engine under test. The documentation now contains an example for implementing a fail-fast listener and documents the additional requirement for test engines that wish to support cancellation. Resolves #1880.
1 parent f6f1a70 commit 6153c35

File tree

24 files changed

+503
-91
lines changed

24 files changed

+503
-91
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ endif::[]
2222
// Platform Engine
2323
:junit-platform-engine: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/package-summary.html[junit-platform-engine]
2424
:junit-platform-engine-support-discovery: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/support/discovery/package-summary.html[org.junit.platform.engine.support.discovery]
25+
:CancellationToken: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/CancellationToken.html[CancellationToken]
2526
:ClasspathResourceSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClasspathResourceSelector.html[ClasspathResourceSelector]
2627
:ClasspathRootSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClasspathRootSelector.html[ClasspathRootSelector]
2728
:ClassSelector: {javadoc-root}/org.junit.platform.engine/org/junit/platform/engine/discovery/ClassSelector.html[ClassSelector]
@@ -66,6 +67,7 @@ endif::[]
6667
:LauncherDiscoveryListener: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherDiscoveryListener.html[LauncherDiscoveryListener]
6768
:LauncherDiscoveryRequest: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherDiscoveryRequest.html[LauncherDiscoveryRequest]
6869
:LauncherDiscoveryRequestBuilder: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/core/LauncherDiscoveryRequestBuilder.html[LauncherDiscoveryRequestBuilder]
70+
:LauncherExecutionRequest: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherExecutionRequest.html[LauncherExecutionRequest]
6971
:LauncherFactory: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/core/LauncherFactory.html[LauncherFactory]
7072
:LauncherInterceptor: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherInterceptor.html[LauncherInterceptor]
7173
:LauncherSession: {javadoc-root}/org.junit.platform.launcher/org/junit/platform/launcher/LauncherSession.html[LauncherSession]

documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,17 @@ repository on GitHub.
3030
[[release-notes-6.0.0-M2-junit-platform-new-features-and-improvements]]
3131
==== New Features and Improvements
3232

33-
* Introduce new `Launcher.execute(LauncherExecutionRequest)` API with corresponding
33+
* Introduce new `Launcher.execute({LauncherExecutionRequest})` API with corresponding
3434
`LauncherExecutionRequestBuilder` to enable the addition of parameters to test
3535
executions without additional overloads of `execute`.
3636
* Introduce `LauncherDiscoveryRequestBuilder.forExecution()` method as a convenience
37-
method for constructing a `LauncherExecutionRequest` that contains a
38-
`LauncherDiscoveryRequest`.
37+
method for constructing a `{LauncherExecutionRequest}` that contains a
38+
`{LauncherDiscoveryRequest}`.
39+
* Introduce support for cancelling a running test execution via a `{CancellationToken}`
40+
passed to the `{Launcher}` as part of a `{LauncherExecutionRequest}` and from there to
41+
all registered test engines. Please refer to the
42+
<<../user-guide/index.adoc#launcher-api-launcher-cancellation, User Guide>> for details
43+
and a usage example.
3944
* Introduce `TestTask.getTestDescriptor()` method for use in
4045
`HierarchicalTestExecutorService` implementations.
4146

documentation/src/docs/asciidoc/user-guide/advanced-topics/engines.adoc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,13 @@ compatibility with build tools and IDEs:
120120
siblings or other nodes that are required for the execution of the selected tests.
121121
* `TestEngines` _should_ support <<running-tests-tags, tagging>> tests and containers so
122122
that tag filters can be applied when discovering tests.
123+
* [[test-engines-requirements-cancellation]] A `TestEngine` _should_ cancel its execution
124+
when the `{CancellationToken}` it is passed as part of the `ExecutionRequest` indicates
125+
that cancellation has been requested. In this case, it _should_ report any remaining
126+
`TestDescriptors` as skipped but not report any events for their descendants. It _may_
127+
report already started `TestDescriptors` as aborted in case they have not been executed
128+
completely. If a `TestEngine` supports cancellation, it should clean up any resources
129+
that it has created just like if execution had finished regularly.
123130

124131
[[test-engines-discovery-issues]]
125132
==== Reporting Discovery Issues

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

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,12 @@ Usage Example:
3030

3131
[source,java,indent=0]
3232
----
33-
include::{testDir}/example/UsingTheLauncherDemo.java[tags=imports]
33+
include::{testDir}/example/UsingTheLauncherForDiscoveryDemo.java[tags=imports]
3434
----
3535

3636
[source,java,indent=0]
3737
----
38-
include::{testDir}/example/UsingTheLauncherDemo.java[tags=discovery]
38+
include::{testDir}/example/UsingTheLauncherForDiscoveryDemo.java[tags=discovery]
3939
----
4040

4141
You can select classes, methods, and all classes in a package or even search for all tests
@@ -353,3 +353,31 @@ between them.
353353

354354
Alternatively, it's possible to inject resources into test engines by
355355
<<launcher-api-launcher-session-listeners-custom, registering a `LauncherSessionListener`>>.
356+
357+
[[launcher-api-launcher-cancellation]]
358+
==== Cancelling a Running Test Execution
359+
360+
The launcher API provides the ability to cancel a running test execution mid-flight while
361+
allowing engines to clean up resources. To request an execution to be cancelled, you need
362+
to call `cancel()` on the `{CancellationToken}` that is passed to `Launcher.execute` as
363+
part of the `{LauncherExecutionRequest}`.
364+
365+
For example, implementing a listener that cancels test execution after the first test
366+
failed can be achieved as follows.
367+
368+
[source,java,indent=0]
369+
----
370+
include::{testDir}/example/UsingTheLauncherDemo.java[tags=cancellation]
371+
----
372+
<1> Create a `{CancellationToken}`
373+
<2> Implement a `{TestExecutionListener}` that calls `cancel()` when a test fails
374+
<3> Register the cancellation token
375+
<4> Register the listener
376+
<5> Pass the `{LauncherExecutionRequest}` to `Launcher.execute`
377+
378+
WARNING: Cancelling tests relies on <<test-engines>> checking and responding to the
379+
`{CancellationToken}` appropriately (see
380+
<<test-engines-requirements-cancellation, Test Engine Requirements>> for details). The
381+
`Launcher` will also check the token and cancel test execution when multiple test engines
382+
are present at runtime.
383+
// TODO #4725 List engines that are known to support cancellation here

documentation/src/test/java/example/UsingTheLauncherDemo.java

Lines changed: 44 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,20 @@
1010

1111
package example;
1212

13-
// tag::imports[]
13+
import static org.junit.platform.engine.TestExecutionResult.Status.FAILED;
1414
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
1515
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
1616
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
1717

1818
import java.io.PrintWriter;
1919
import java.nio.file.Path;
2020

21+
import org.junit.jupiter.api.Tag;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.platform.engine.CancellationToken;
2124
import org.junit.platform.engine.FilterResult;
2225
import org.junit.platform.engine.TestDescriptor;
26+
import org.junit.platform.engine.TestExecutionResult;
2327
import org.junit.platform.launcher.Launcher;
2428
import org.junit.platform.launcher.LauncherDiscoveryListener;
2529
import org.junit.platform.launcher.LauncherDiscoveryRequest;
@@ -28,6 +32,7 @@
2832
import org.junit.platform.launcher.LauncherSessionListener;
2933
import org.junit.platform.launcher.PostDiscoveryFilter;
3034
import org.junit.platform.launcher.TestExecutionListener;
35+
import org.junit.platform.launcher.TestIdentifier;
3136
import org.junit.platform.launcher.TestPlan;
3237
import org.junit.platform.launcher.core.LauncherConfig;
3338
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
@@ -36,42 +41,14 @@
3641
import org.junit.platform.launcher.listeners.SummaryGeneratingListener;
3742
import org.junit.platform.launcher.listeners.TestExecutionSummary;
3843
import org.junit.platform.reporting.legacy.xml.LegacyXmlReportGeneratingListener;
39-
// end::imports[]
4044

4145
/**
4246
* @since 5.0
4347
*/
4448
class UsingTheLauncherDemo {
4549

46-
@org.junit.jupiter.api.Test
47-
@SuppressWarnings("unused")
48-
void discovery() {
49-
// @formatter:off
50-
// tag::discovery[]
51-
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
52-
.selectors(
53-
selectPackage("com.example.mytests"),
54-
selectClass(MyTestClass.class)
55-
)
56-
.filters(
57-
includeClassNamePatterns(".*Tests")
58-
)
59-
// end::discovery[]
60-
.configurationParameter("enableHttpServer", "false")
61-
// tag::discovery[]
62-
.build();
63-
64-
try (LauncherSession session = LauncherFactory.openSession()) {
65-
TestPlan testPlan = session.getLauncher().discover(request);
66-
67-
// ... discover additional test plans or execute tests
68-
}
69-
// end::discovery[]
70-
// @formatter:on
71-
}
72-
73-
@org.junit.jupiter.api.Tag("exclude")
74-
@org.junit.jupiter.api.Test
50+
@Tag("exclude")
51+
@Test
7552
@SuppressWarnings("unused")
7653
void execution() {
7754
// @formatter:off
@@ -110,7 +87,7 @@ void execution() {
11087
// @formatter:on
11188
}
11289

113-
@org.junit.jupiter.api.Test
90+
@Test
11491
void launcherConfig() {
11592
Path reportsDir = Path.of("target", "xml-reports");
11693
PrintWriter out = new PrintWriter(System.out);
@@ -142,6 +119,41 @@ void launcherConfig() {
142119
// @formatter:on
143120
}
144121

122+
@Test
123+
@SuppressWarnings("unused")
124+
void cancellation() {
125+
// tag::cancellation[]
126+
CancellationToken cancellationToken = CancellationToken.create(); // <1>
127+
128+
TestExecutionListener failFastListener = new TestExecutionListener() {
129+
@Override
130+
public void executionFinished(TestIdentifier identifier, TestExecutionResult result) {
131+
if (result.getStatus() == FAILED) {
132+
cancellationToken.cancel(); // <2>
133+
}
134+
}
135+
};
136+
137+
// end::cancellation[]
138+
// @formatter:off
139+
// tag::cancellation[]
140+
LauncherExecutionRequest executionRequest = LauncherDiscoveryRequestBuilder.request()
141+
.selectors(selectClass(MyTestClass.class))
142+
.forExecution()
143+
.cancellationToken(cancellationToken) // <3>
144+
.listeners(failFastListener) // <4>
145+
.build();
146+
// end::cancellation[]
147+
// @formatter:off
148+
// tag::cancellation[]
149+
150+
try (LauncherSession session = LauncherFactory.openSession()) {
151+
session.getLauncher().execute(executionRequest); // <5>
152+
}
153+
// end::cancellation[]
154+
// @formatter:on
155+
}
156+
145157
}
146158

147159
class MyTestClass {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package example;
12+
13+
// tag::imports[]
14+
import static org.junit.platform.engine.discovery.ClassNameFilter.includeClassNamePatterns;
15+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
16+
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectPackage;
17+
18+
import org.junit.platform.launcher.LauncherDiscoveryRequest;
19+
import org.junit.platform.launcher.LauncherSession;
20+
import org.junit.platform.launcher.TestPlan;
21+
import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder;
22+
import org.junit.platform.launcher.core.LauncherFactory;
23+
// end::imports[]
24+
25+
/**
26+
* @since 6.0
27+
*/
28+
class UsingTheLauncherForDiscoveryDemo {
29+
30+
@org.junit.jupiter.api.Test
31+
@SuppressWarnings("unused")
32+
void discovery() {
33+
// @formatter:off
34+
// tag::discovery[]
35+
LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request()
36+
.selectors(
37+
selectPackage("com.example.mytests"),
38+
selectClass(MyTestClass.class)
39+
)
40+
.filters(
41+
includeClassNamePatterns(".*Tests")
42+
)
43+
.build();
44+
45+
try (LauncherSession session = LauncherFactory.openSession()) {
46+
TestPlan testPlan = session.getLauncher().discover(request);
47+
48+
// ... discover additional test plans or execute tests
49+
}
50+
// end::discovery[]
51+
// @formatter:on
52+
}
53+
54+
static class MyTestClass {
55+
}
56+
57+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.engine;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import org.apiguardian.api.API;
16+
17+
/**
18+
* Token that should be checked to determine whether an operation was requested
19+
* to be cancelled.
20+
*
21+
* <p>For example, this is used by the
22+
* {@link org.junit.platform.launcher.Launcher} and
23+
* {@link org.junit.platform.engine.TestEngine} implementations to determine
24+
* whether the current test execution should be cancelled.
25+
*
26+
* <p>This interface is not intended to be implemented by clients.
27+
*
28+
* @since 6.0
29+
* @see org.junit.platform.launcher.core.LauncherExecutionRequestBuilder#cancellationToken(CancellationToken)
30+
* @see org.junit.platform.launcher.LauncherExecutionRequest#getCancellationToken()
31+
* @see ExecutionRequest#getCancellationToken()
32+
*/
33+
@API(status = EXPERIMENTAL, since = "6.0")
34+
public sealed interface CancellationToken permits RegularCancellationToken, DisabledCancellationToken {
35+
36+
/**
37+
* Create a new, uncancelled cancellation token.
38+
*/
39+
static CancellationToken create() {
40+
return new RegularCancellationToken();
41+
}
42+
43+
/**
44+
* Get a new cancellation token that cannot be cancelled.
45+
*
46+
* <p>This is only useful for cases when a cancellation token is required
47+
* but is not supported or irrelevant, for example, in tests.
48+
*/
49+
static CancellationToken disabled() {
50+
return DisabledCancellationToken.INSTANCE;
51+
}
52+
53+
/**
54+
* {@return whether cancellation has been requested}
55+
*
56+
* <p>Once this method returns {@code true}, it will never return
57+
* {@code false} in a subsequent call.
58+
*/
59+
boolean isCancellationRequested();
60+
61+
/**
62+
* Request cancellation.
63+
*
64+
* <p>This will call subsequent calls to {@link #isCancellationRequested()}
65+
* to return {@code true}.
66+
*/
67+
void cancel();
68+
69+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2015-2025 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.platform.engine;
12+
13+
/**
14+
* @since 6.0
15+
*/
16+
final class DisabledCancellationToken implements CancellationToken {
17+
18+
static final DisabledCancellationToken INSTANCE = new DisabledCancellationToken();
19+
20+
private DisabledCancellationToken() {
21+
}
22+
23+
@Override
24+
public boolean isCancellationRequested() {
25+
return false;
26+
}
27+
28+
@Override
29+
public void cancel() {
30+
}
31+
}

0 commit comments

Comments
 (0)