Skip to content

Commit fade188

Browse files
committed
Provide cancellation support for Suite engine
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 fade188

File tree

6 files changed

+109
-22
lines changed

6 files changed

+109
-22
lines changed

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: 18 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,25 @@ 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+
}
158+
159+
executionListener.executionStarted(this);
154160
ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector();
155161

156162
executeBeforeSuiteMethods(throwableCollector);
157163

158-
TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore,
164+
TestExecutionSummary summary = executeTests(executionListener, requestLevelStore, cancellationToken,
159165
throwableCollector);
160166

161167
executeAfterSuiteMethods(throwableCollector);
162168

163169
TestExecutionResult testExecutionResult = computeTestExecutionResult(summary, throwableCollector);
164-
parentEngineExecutionListener.executionFinished(this, testExecutionResult);
170+
executionListener.executionFinished(this, testExecutionResult);
165171
}
166172

167173
private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
@@ -176,8 +182,10 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
176182
}
177183
}
178184

179-
private @Nullable TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener,
180-
NamespacedHierarchicalStore<Namespace> requestLevelStore, ThrowableCollector throwableCollector) {
185+
private @Nullable TestExecutionSummary executeTests(EngineExecutionListener executionListener,
186+
NamespacedHierarchicalStore<Namespace> requestLevelStore, CancellationToken cancellationToken,
187+
ThrowableCollector throwableCollector) {
188+
181189
if (throwableCollector.isNotEmpty()) {
182190
return null;
183191
}
@@ -187,7 +195,9 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) {
187195
// be pruned accordingly.
188196
LauncherDiscoveryResult discoveryResult = requireNonNull(this.launcherDiscoveryResult).withRetainedEngines(
189197
getChildren()::contains);
190-
return requireNonNull(launcher).execute(discoveryResult, parentEngineExecutionListener, requestLevelStore);
198+
199+
return requireNonNull(launcher).execute(discoveryResult, executionListener, requestLevelStore,
200+
cancellationToken);
191201
}
192202

193203
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,7 +10,9 @@
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;
15+
import static org.assertj.core.api.Assertions.not;
1416
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass;
1517
import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId;
1618
import static org.junit.platform.launcher.TagFilter.excludeTags;
@@ -22,6 +24,8 @@
2224
import static org.junit.platform.testkit.engine.EventConditions.event;
2325
import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully;
2426
import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure;
27+
import static org.junit.platform.testkit.engine.EventConditions.skippedWithReason;
28+
import static org.junit.platform.testkit.engine.EventConditions.started;
2529
import static org.junit.platform.testkit.engine.EventConditions.test;
2630
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf;
2731
import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message;
@@ -32,13 +36,15 @@
3236

3337
import java.nio.file.Path;
3438

39+
import org.jspecify.annotations.Nullable;
3540
import org.junit.jupiter.api.Test;
3641
import org.junit.jupiter.api.io.TempDir;
3742
import org.junit.jupiter.engine.descriptor.ClassTestDescriptor;
3843
import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor;
3944
import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor;
4045
import org.junit.jupiter.params.ParameterizedTest;
4146
import org.junit.jupiter.params.provider.ValueSource;
47+
import org.junit.platform.engine.CancellationToken;
4248
import org.junit.platform.engine.DiscoveryIssue;
4349
import org.junit.platform.engine.DiscoveryIssue.Severity;
4450
import org.junit.platform.engine.EngineExecutionListener;
@@ -51,6 +57,8 @@
5157
import org.junit.platform.engine.support.store.NamespacedHierarchicalStore;
5258
import org.junit.platform.launcher.PostDiscoveryFilter;
5359
import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders;
60+
import org.junit.platform.suite.api.AfterSuite;
61+
import org.junit.platform.suite.api.BeforeSuite;
5462
import org.junit.platform.suite.api.SelectClasses;
5563
import org.junit.platform.suite.api.Suite;
5664
import org.junit.platform.suite.engine.testcases.ConfigurationSensitiveTestCase;
@@ -628,11 +636,6 @@ void discoveryIssueOfNestedTestEnginesAreReported() throws Exception {
628636
// @formatter:on
629637
}
630638

631-
@Suite
632-
@SelectClasses(SingleTestTestCase.class)
633-
abstract private static class AbstractPrivateSuite {
634-
}
635-
636639
@Test
637640
void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() {
638641
UniqueId engineId = UniqueId.forEngine(SuiteEngineDescriptor.ENGINE_ID);
@@ -643,15 +646,87 @@ void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() {
643646

644647
EngineExecutionListener listener = mock(EngineExecutionListener.class);
645648
NamespacedHierarchicalStore<Namespace> requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore();
649+
var cancellationToken = CancellationToken.create();
646650

647651
ExecutionRequest request = mock();
648652
when(request.getRootTestDescriptor()).thenReturn(engineDescriptor);
649653
when(request.getEngineExecutionListener()).thenReturn(listener);
650654
when(request.getStore()).thenReturn(requestLevelStore);
655+
when(request.getCancellationToken()).thenReturn(cancellationToken);
651656

652657
new SuiteTestEngine().execute(request);
653658

654-
verify(mockDescriptor).execute(same(listener), same(requestLevelStore));
659+
verify(mockDescriptor).execute(same(listener), same(requestLevelStore), same(cancellationToken));
660+
}
661+
662+
@Test
663+
void reportsSuiteClassAsSkippedWhenCancelledBeforeExecution() {
664+
CancellingSuite.cancellationToken = CancellationToken.create();
665+
try {
666+
var testKit = EngineTestKit.engine(ENGINE_ID) //
667+
.selectors(selectClass(CancellingSuite.class), selectClass(SelectMethodsSuite.class)) //
668+
.cancellationToken(CancellingSuite.cancellationToken);
669+
670+
var results = testKit.execute();
671+
672+
results.allEvents().debug().assertEventsMatchLooselyInOrder( //
673+
event(container(CancellingSuite.class), started()), //
674+
event(container(SingleTestTestCase.class), skippedWithReason("Execution cancelled")), //
675+
event(container(CancellingSuite.class), finishedSuccessfully()), //
676+
event(container(SelectMethodsSuite.class), not(container(MultipleTestsTestCase.class)),
677+
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)