diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc index f6f48e5bbea4..9b0b2d4a0daa 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-6.0.0-M2.adoc @@ -43,6 +43,7 @@ repository on GitHub. and a usage example. * Provide cancellation support for implementations of `{HierarchicalTestEngine}` such as JUnit Jupiter, Spock, and Cucumber. +* Provide cancellation support for Suite engine * Introduce `TestTask.getTestDescriptor()` method for use in `HierarchicalTestExecutorService` implementations. diff --git a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc index 35c8fe642cf5..603a451a304f 100644 --- a/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc +++ b/documentation/src/docs/asciidoc/user-guide/advanced-topics/launcher-api.adoc @@ -384,8 +384,9 @@ Cancelling tests relies on <> checking and responding to the `Launcher` will also check the token and cancel test execution when multiple test engines are present at runtime. -At the time of writing the following test engines support cancellation: +At the time of writing, the following test engines support cancellation: * `{junit-jupiter-engine}` +* `{junit-platform-suite-engine}` * Any `{TestEngine}` extending `{HierarchicalTestEngine}` such as Spock and Cucumber ==== 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 eb830c130507..f468c70ec799 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 @@ -59,13 +59,11 @@ LauncherDiscoveryResult discover(LauncherDiscoveryRequest discoveryRequest, Uniq return discoveryOrchestrator.discover(discoveryRequest, parentId); } - TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult, - EngineExecutionListener parentEngineExecutionListener, - NamespacedHierarchicalStore requestLevelStore) { + TestExecutionSummary execute(LauncherDiscoveryResult discoveryResult, EngineExecutionListener executionListener, + NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken) { SummaryGeneratingListener listener = new SummaryGeneratingListener(); - // TODO #4725 Provide cancellation support for Suite engine - executionOrchestrator.execute(discoveryResult, parentEngineExecutionListener, listener, requestLevelStore, - CancellationToken.disabled()); + executionOrchestrator.execute(discoveryResult, executionListener, 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..0edf9c1eefbe 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; @@ -148,20 +149,26 @@ private static String getSuiteDisplayName(Class testClass) { // @formatter:on } - void execute(EngineExecutionListener parentEngineExecutionListener, - NamespacedHierarchicalStore requestLevelStore) { - parentEngineExecutionListener.executionStarted(this); + void execute(EngineExecutionListener executionListener, NamespacedHierarchicalStore requestLevelStore, + CancellationToken cancellationToken) { + + if (cancellationToken.isCancellationRequested()) { + executionListener.executionSkipped(this, "Execution cancelled"); + return; + } + + executionListener.executionStarted(this); ThrowableCollector throwableCollector = new OpenTest4JAwareThrowableCollector(); executeBeforeSuiteMethods(throwableCollector); - TestExecutionSummary summary = executeTests(parentEngineExecutionListener, requestLevelStore, + TestExecutionSummary summary = executeTests(executionListener, requestLevelStore, cancellationToken, throwableCollector); executeAfterSuiteMethods(throwableCollector); TestExecutionResult testExecutionResult = computeTestExecutionResult(summary, throwableCollector); - parentEngineExecutionListener.executionFinished(this, testExecutionResult); + executionListener.executionFinished(this, testExecutionResult); } private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) { @@ -176,8 +183,10 @@ private void executeBeforeSuiteMethods(ThrowableCollector throwableCollector) { } } - private @Nullable TestExecutionSummary executeTests(EngineExecutionListener parentEngineExecutionListener, - NamespacedHierarchicalStore requestLevelStore, ThrowableCollector throwableCollector) { + private @Nullable TestExecutionSummary executeTests(EngineExecutionListener executionListener, + NamespacedHierarchicalStore requestLevelStore, CancellationToken cancellationToken, + ThrowableCollector throwableCollector) { + if (throwableCollector.isNotEmpty()) { return null; } @@ -187,7 +196,9 @@ 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, executionListener, 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..3f13da412285 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 @@ -15,6 +15,7 @@ import java.util.Optional; import org.apiguardian.api.API; +import org.junit.platform.engine.CancellationToken; import org.junit.platform.engine.EngineDiscoveryRequest; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.ExecutionRequest; @@ -66,6 +67,7 @@ public void execute(ExecutionRequest request) { SuiteEngineDescriptor suiteEngineDescriptor = (SuiteEngineDescriptor) request.getRootTestDescriptor(); EngineExecutionListener engineExecutionListener = request.getEngineExecutionListener(); NamespacedHierarchicalStore requestLevelStore = request.getStore(); + CancellationToken cancellationToken = request.getCancellationToken(); engineExecutionListener.executionStarted(suiteEngineDescriptor); @@ -73,7 +75,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, cancellationToken)); // @formatter:on engineExecutionListener.executionFinished(suiteEngineDescriptor, TestExecutionResult.successful()); } 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 2050d1635f9d..12cb46e6d928 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 @@ -10,6 +10,7 @@ package org.junit.platform.suite.engine; +import static java.util.Objects.requireNonNull; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectUniqueId; @@ -22,6 +23,8 @@ 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.finishedWithFailure; +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 static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; @@ -32,6 +35,7 @@ import java.nio.file.Path; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; @@ -39,6 +43,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.DiscoveryIssue; import org.junit.platform.engine.DiscoveryIssue.Severity; import org.junit.platform.engine.EngineExecutionListener; @@ -51,6 +56,8 @@ import org.junit.platform.engine.support.store.NamespacedHierarchicalStore; import org.junit.platform.launcher.PostDiscoveryFilter; import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; +import org.junit.platform.suite.api.AfterSuite; +import org.junit.platform.suite.api.BeforeSuite; import org.junit.platform.suite.api.SelectClasses; import org.junit.platform.suite.api.Suite; import org.junit.platform.suite.engine.testcases.ConfigurationSensitiveTestCase; @@ -628,11 +635,6 @@ void discoveryIssueOfNestedTestEnginesAreReported() throws Exception { // @formatter:on } - @Suite - @SelectClasses(SingleTestTestCase.class) - abstract private static class AbstractPrivateSuite { - } - @Test void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() { UniqueId engineId = UniqueId.forEngine(SuiteEngineDescriptor.ENGINE_ID); @@ -643,15 +645,88 @@ void suiteEnginePassesRequestLevelStoreToSuiteTestDescriptors() { EngineExecutionListener listener = mock(EngineExecutionListener.class); NamespacedHierarchicalStore requestLevelStore = NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore(); + var cancellationToken = CancellationToken.create(); ExecutionRequest request = mock(); when(request.getRootTestDescriptor()).thenReturn(engineDescriptor); when(request.getEngineExecutionListener()).thenReturn(listener); when(request.getStore()).thenReturn(requestLevelStore); + when(request.getCancellationToken()).thenReturn(cancellationToken); new SuiteTestEngine().execute(request); - verify(mockDescriptor).execute(same(listener), same(requestLevelStore)); + verify(mockDescriptor).execute(same(listener), same(requestLevelStore), same(cancellationToken)); + } + + @Test + void reportsSuiteClassAsSkippedWhenCancelledBeforeExecution() { + CancellingSuite.cancellationToken = CancellationToken.create(); + try { + var testKit = EngineTestKit.engine(ENGINE_ID) // + .selectors(selectClass(CancellingSuite.class), selectClass(SelectMethodsSuite.class)) // + .cancellationToken(CancellingSuite.cancellationToken); + + var results = testKit.execute(); + + results.allEvents() // + .assertStatistics(stats -> stats.started(3).succeeded(2).aborted(1).skipped(2)) // + .assertEventsMatchLooselyInOrder( // + event(container(CancellingSuite.class), started()), // + event(container(SingleTestTestCase.class), skippedWithReason("Execution cancelled")), // + event(container(CancellingSuite.class), finishedSuccessfully()), // + event(container(SelectMethodsSuite.class), skippedWithReason("Execution cancelled")) // + ); + } + finally { + CancellingSuite.cancellationToken = null; + } + } + + @Test + void reportsChildrenOfEnginesInSuiteAsSkippedWhenCancelledDuringExecution() { + CancellingSuite.cancellationToken = CancellationToken.create(); + try { + var testKit = EngineTestKit.engine(ENGINE_ID) // + .selectors(selectClass(CancellingSuite.class)) // + .cancellationToken(CancellingSuite.cancellationToken); + + var results = testKit.execute(); + + results.allEvents().assertThatEvents() // + .haveExactly(1, event(container(SingleTestTestCase.class), + skippedWithReason("Execution cancelled"))).haveExactly(0, event(test(), started())); + + assertThat(CancellingSuite.afterCalled) // + .describedAs("@AfterSuite method was called") // + .isTrue(); + } + finally { + CancellingSuite.cancellationToken = null; + } + } + + // ----------------------------------------------------------------------------------------------------------------- + + static class CancellingSuite extends SelectClassesSuite { + + static @Nullable CancellationToken cancellationToken; + static boolean afterCalled; + + @BeforeSuite + static void beforeSuite() { + CancellingSuite.afterCalled = false; + requireNonNull(cancellationToken).cancel(); + } + + @AfterSuite + static void afterSuite() { + afterCalled = true; + } + } + + @Suite + @SelectClasses(SingleTestTestCase.class) + abstract private static class AbstractPrivateSuite { } @Suite