Skip to content

Commit 8b69a4a

Browse files
authored
Provide cancellation support for Suite engine (#4730)
The Suite engine now passes the `CancellationToken` to downstream test engines and checks whether cancellation has been requested when about to execute a `@Suite` class. Issue: #4725
1 parent 8ab24ee commit 8b69a4a

6 files changed

Lines changed: 110 additions & 22 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ repository on GitHub.
4343
and a usage example.
4444
* Provide cancellation support for implementations of `{HierarchicalTestEngine}` such as
4545
JUnit Jupiter, Spock, and Cucumber.
46+
* Provide cancellation support for Suite engine
4647
* Introduce `TestTask.getTestDescriptor()` method for use in
4748
`HierarchicalTestExecutorService` implementations.
4849

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,8 +384,9 @@ Cancelling tests relies on <<test-engines>> checking and responding to the
384384
`Launcher` will also check the token and cancel test execution when multiple test engines
385385
are present at runtime.
386386
387-
At the time of writing the following test engines support cancellation:
387+
At the time of writing, the following test engines support cancellation:
388388
389389
* `{junit-jupiter-engine}`
390+
* `{junit-platform-suite-engine}`
390391
* Any `{TestEngine}` extending `{HierarchicalTestEngine}` such as Spock and Cucumber
391392
====

junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteLauncher.java

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,13 +59,11 @@ LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, Uniq
5959
return discoveryOrchestrator.discover(discoveryRequest, parentId);
6060
}
6161

62-
TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult,
63-
EngineExecutionListener parentEngineExecutionListener,
64-
NamespacedHierarchicalStore<Namespace> requestLevelStore) {
62+
TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener executionListener,
63+
NamespacedHierarchicalStore<Namespace> requestLevelStore, CancellationToken cancellationToken) {
6564
SummaryGeneratingListener listener = new SummaryGeneratingListener();
66-
// TODO #4725 Provide cancellation support for Suite engine
67-
executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore,
68-
CancellationToken.disabled());
65+
executionOrchestrator.execute(discoveryResult, executionListener, listener, requestLevelStore,
66+
cancellationToken);
6967
return listener.getSummary();
7068
}
7169

junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestDescriptor.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.platform.commons.support.ReflectionSupport;
2828
import org.junit.platform.commons.util.Preconditions;
2929
import org.junit.platform.commons.util.StringUtils;
30+
import org.junit.platform.engine.CancellationToken;
3031
import org.junit.platform.engine.ConfigurationParameters;
3132
import org.junit.platform.engine.DiscoveryIssue;
3233
import org.junit.platform.engine.EngineDiscoveryListener;
@@ -148,20 +149,26 @@ private static String getSuiteDisplayName(Class<?> testClass) {
148149
// @formatter:on
149150
}
150151

151-
void execute(EngineExecutionListener parentEngineExecutionListener,
152-
NamespacedHierarchicalStore<Namespace> requestLevelStore) {
153-
parentEngineExecutionListener.executionStarted(this);
152+
void execute(EngineExecutionListener executionListener, NamespacedHierarchicalStore<Namespace> requestLevelStore,
153+
CancellationToken cancellationToken) {
154+
155+
if (cancellationToken.isCancellationRequested()) {
156+
executionListener.executionSkipped(this, "Execution cancelled");
157+
return;
158+
}
159+
160+
executionListener.executionStarted(this);
154161
ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector();
155162

156163
executeBeforeSuiteMethods(throwableCollector);
157164

158-
TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore,
165+
TestExecutionSummary summary = executeTests(executionListener, requestLevelStore, cancellationToken,
159166
throwableCollector);
160167

161168
executeAfterSuiteMethods(throwableCollector);
162169

163170
TestExecutionResult testExecutionResult = computeTestExecutionResult(summary, throwableCollector);
164-
parentEngineExecutionListener.executionFinished(this, testExecutionResult);
171+
executionListener.executionFinished(this, testExecutionResult);
165172
}
166173

167174
private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
@@ -176,8 +183,10 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
176183
}
177184
}
178185

179-
private @Nullable TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener,
180-
NamespacedHierarchicalStore<Namespace> requestLevelStore, ThrowableCollector throwableCollector) {
186+
private @Nullable TestExecutionSummary executeTests(EngineExecutionListener executionListener,
187+
NamespacedHierarchicalStore<Namespace> requestLevelStore, CancellationToken cancellationToken,
188+
ThrowableCollector throwableCollector) {
189+
181190
if (throwableCollector.isNotEmpty()) {
182191
return null;
183192
}
@@ -187,7 +196,9 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
187196
// be pruned accordingly.
188197
LauncherDiscoveryResult discoveryResult = requireNonNull(this.launcherDiscoveryResult).withRetainedEngines(
189198
getChildren()::contains);
190-
return requireNonNull(launcher).execute(discoveryResult, parentEngineExecutionListener, requestLevelStore);
199+
200+
return requireNonNull(launcher).execute(discoveryResult, executionListener, requestLevelStore,
201+
cancellationToken);
191202
}
192203

193204
private void executeAfterSuiteMethods(ThrowableCollector throwableCollector) {

junit-platform-suite-engine/src/main/java/org/junit/platform/suite/engine/SuiteTestEngine.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import java.util.Optional;
1616

1717
import org.apiguardian.api.API;
18+
import org.junit.platform.engine.CancellationToken;
1819
import org.junit.platform.engine.EngineDiscoveryRequest;
1920
import org.junit.platform.engine.EngineExecutionListener;
2021
import org.junit.platform.engine.ExecutionRequest;
@@ -66,14 +67,15 @@ public void execute(ExecutionRequest request) {
6667
SuiteEngineDescriptor suiteEngineDescriptor = (SuiteEngineDescriptor) request.getRootTestDescriptor();
6768
EngineExecutionListener engineExecutionListener = request.getEngineExecutionListener();
6869
NamespacedHierarchicalStore<Namespace> requestLevelStore = request.getStore();
70+
CancellationToken cancellationToken = request.getCancellationToken();
6971

7072
engineExecutionListener.executionStarted(suiteEngineDescriptor);
7173

7274
// @formatter:off
7375
suiteEngineDescriptor.getChildren()
7476
.stream()
7577
.map(SuiteTestDescriptor.class::cast)
76-
.forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore));
78+
.forEach(suiteTestDescriptor -> suiteTestDescriptor.execute(engineExecutionListener, requestLevelStore, cancellationToken));
7779
// @formatter:on
7880
engineExecutionListener.executionFinished(suiteEngineDescriptor, TestExecutionResult.successful());
7981
}

platform-tests/src/test/java/org/junit/platform/suite/engine/SuiteEngineTests.java

Lines changed: 81 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
package org.junit.platform.suite.engine;
1212

13+
import static java.util.Objects.requireNonNull;
1314
import static org.assertj.core.api.Assertions.assertThat;
1415
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
1516
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
@@ -22,6 +23,8 @@
2223
import static org.junit.platform.testkit.engine.EventConditions.event;
2324
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
2425
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
26+
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
27+
import static org.junit.platform.testkit.engine.EventConditions.started;
2528
import static org.junit.platform.testkit.engine.EventConditions.test;
2629
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
2730
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
@@ -32,13 +35,15 @@
3235

3336
import java.nio.file.Path;
3437

38+
import org.jspecify.annotations.Nullable;
3539
import org.junit.jupiter.api.Test;
3640
import org.junit.jupiter.api.io.TempDir;
3741
import org.junit.jupiter.engine.descriptor.ClassTestDescriptor;
3842
import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor;
3943
import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor;
4044
import org.junit.jupiter.params.ParameterizedTest;
4145
import org.junit.jupiter.params.provider.ValueSource;
46+
import org.junit.platform.engine.CancellationToken;
4247
import org.junit.platform.engine.DiscoveryIssue;
4348
import org.junit.platform.engine.DiscoveryIssue.Severity;
4449
import org.junit.platform.engine.EngineExecutionListener;
@@ -51,6 +56,8 @@
5156
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
5257
import org.junit.platform.launcher.PostDiscoveryFilter;
5358
import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders;
59+
import org.junit.platform.suite.api.AfterSuite;
60+
import org.junit.platform.suite.api.BeforeSuite;
5461
import org.junit.platform.suite.api.SelectClasses;
5562
import org.junit.platform.suite.api.Suite;
5663
import org.junit.platform.suite.engine.testcases.ConfigurationSensitiveTestCase;
@@ -628,11 +635,6 @@ void discoveryIssueOfNestedTestEnginesAreReported() throws Exception {
628635
// @formatter:on
629636
}
630637

631-
@Suite
632-
@SelectClasses(SingleTestTestCase.class)
633-
abstract private static class AbstractPrivateSuite {
634-
}
635-
636638
@Test
637639
void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() {
638640
UniqueId engineId = UniqueId.forEngine(SuiteEngineDescriptor.ENGINE_ID);
@@ -643,15 +645,88 @@ void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() {
643645

644646
EngineExecutionListener listener = mock(EngineExecutionListener.class);
645647
NamespacedHierarchicalStore<Namespace> requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore();
648+
var cancellationToken = CancellationToken.create();
646649

647650
ExecutionRequest request = mock();
648651
when(request.getRootTestDescriptor()).thenReturn(engineDescriptor);
649652
when(request.getEngineExecutionListener()).thenReturn(listener);
650653
when(request.getStore()).thenReturn(requestLevelStore);
654+
when(request.getCancellationToken()).thenReturn(cancellationToken);
651655

652656
new SuiteTestEngine().execute(request);
653657

654-
verify(mockDescriptor).execute(same(listener), same(requestLevelStore));
658+
verify(mockDescriptor).execute(same(listener), same(requestLevelStore), same(cancellationToken));
659+
}
660+
661+
@Test
662+
void reportsSuiteClassAsSkippedWhenCancelledBeforeExecution() {
663+
CancellingSuite.cancellationToken = CancellationToken.create();
664+
try {
665+
var testKit = EngineTestKit.engine(ENGINE_ID) //
666+
.selectors(selectClass(CancellingSuite.class), selectClass(SelectMethodsSuite.class)) //
667+
.cancellationToken(CancellingSuite.cancellationToken);
668+
669+
var results = testKit.execute();
670+
671+
results.allEvents() //
672+
.assertStatistics(stats -> stats.started(3).succeeded(2).aborted(1).skipped(2)) //
673+
.assertEventsMatchLooselyInOrder( //
674+
event(container(CancellingSuite.class), started()), //
675+
event(container(SingleTestTestCase.class), skippedWithReason("Execution cancelled")), //
676+
event(container(CancellingSuite.class), finishedSuccessfully()), //
677+
event(container(SelectMethodsSuite.class), skippedWithReason("Execution cancelled")) //
678+
);
679+
}
680+
finally {
681+
CancellingSuite.cancellationToken = null;
682+
}
683+
}
684+
685+
@Test
686+
void reportsChildrenOfEnginesInSuiteAsSkippedWhenCancelledDuringExecution() {
687+
CancellingSuite.cancellationToken = CancellationToken.create();
688+
try {
689+
var testKit = EngineTestKit.engine(ENGINE_ID) //
690+
.selectors(selectClass(CancellingSuite.class)) //
691+
.cancellationToken(CancellingSuite.cancellationToken);
692+
693+
var results = testKit.execute();
694+
695+
results.allEvents().assertThatEvents() //
696+
.haveExactly(1, event(container(SingleTestTestCase.class),
697+
skippedWithReason("Execution cancelled"))).haveExactly(0, event(test(), started()));
698+
699+
assertThat(CancellingSuite.afterCalled) //
700+
.describedAs("@AfterSuite method was called") //
701+
.isTrue();
702+
}
703+
finally {
704+
CancellingSuite.cancellationToken = null;
705+
}
706+
}
707+
708+
// -----------------------------------------------------------------------------------------------------------------
709+
710+
static class CancellingSuite extends SelectClassesSuite {
711+
712+
static @Nullable CancellationToken cancellationToken;
713+
static boolean afterCalled;
714+
715+
@BeforeSuite
716+
static void beforeSuite() {
717+
CancellingSuite.afterCalled = false;
718+
requireNonNull(cancellationToken).cancel();
719+
}
720+
721+
@AfterSuite
722+
static void afterSuite() {
723+
afterCalled = true;
724+
}
725+
}
726+
727+
@Suite
728+
@SelectClasses(SingleTestTestCase.class)
729+
abstract private static class AbstractPrivateSuite {
655730
}
656731

657732
@Suite

0 commit comments

Comments
 (0)