Skip to content

Commit 0d74b24

Browse files
committed
Provide cancellation support for Vintage engine
Issue: #4725
1 parent 2ec21d0 commit 0d74b24

File tree

8 files changed

+122
-11
lines changed

8 files changed

+122
-11
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ repository on GitHub.
4545
now causes test execution to be cancelled after the first failed test.
4646
* Provide cancellation support for implementations of `{HierarchicalTestEngine}` such as
4747
JUnit Jupiter, Spock, and Cucumber.
48-
* Provide cancellation support for Suite engine
48+
* Provide cancellation support for the Suite and Vintage test engines
4949
* Introduce `TestTask.getTestDescriptor()` method for use in
5050
`HierarchicalTestExecutorService` implementations.
5151

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ are present at runtime.
387387
At the time of writing, the following test engines support cancellation:
388388
389389
* `{junit-jupiter-engine}`
390+
* `{junit-vintage-engine}`
390391
* `{junit-platform-suite-engine}`
391392
* Any `{TestEngine}` extending `{HierarchicalTestEngine}` such as Spock and Cucumber
392393
====

junit-vintage-engine/src/main/java/org/junit/vintage/engine/VintageTestEngine.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ public void execute(ExecutionRequest request) {
7272
// TODO #4725 Provide cancellation support for Vintage engine
7373
engineExecutionListener.executionStarted(engineDescriptor);
7474
new VintageExecutor(engineDescriptor, engineExecutionListener,
75-
request.getConfigurationParameters()).executeAllChildren();
75+
request.getConfigurationParameters()).executeAllChildren(request.getCancellationToken());
7676
engineExecutionListener.executionFinished(engineDescriptor, successful());
7777
}
7878
}

junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/RunnerExecutor.java

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
import org.apiguardian.api.API;
1717
import org.junit.platform.commons.util.UnrecoverableExceptions;
18+
import org.junit.platform.engine.CancellationToken;
1819
import org.junit.platform.engine.EngineExecutionListener;
1920
import org.junit.platform.engine.TestExecutionResult;
2021
import org.junit.runner.notification.RunNotifier;
22+
import org.junit.runner.notification.StoppedByUserException;
2123
import org.junit.vintage.engine.descriptor.RunnerTestDescriptor;
2224
import org.junit.vintage.engine.descriptor.TestSourceProvider;
2325

@@ -28,26 +30,53 @@
2830
public class RunnerExecutor {
2931

3032
private final EngineExecutionListener engineExecutionListener;
33+
private final CancellationToken cancellationToken;
3134
private final TestSourceProvider testSourceProvider = new TestSourceProvider();
3235

33-
public RunnerExecutor(EngineExecutionListener engineExecutionListener) {
36+
public RunnerExecutor(EngineExecutionListener engineExecutionListener, CancellationToken cancellationToken) {
3437
this.engineExecutionListener = engineExecutionListener;
38+
this.cancellationToken = cancellationToken;
3539
}
3640

3741
public void execute(RunnerTestDescriptor runnerTestDescriptor) {
42+
if (cancellationToken.isCancellationRequested()) {
43+
engineExecutionListener.executionSkipped(runnerTestDescriptor, "Execution cancelled");
44+
return;
45+
}
3846
var notifier = new RunNotifier();
3947
var testRun = new TestRun(runnerTestDescriptor);
4048
var listener = new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider);
4149
notifier.addListener(listener);
50+
CancellationToken.Listener cancellationListener = __ -> notifier.pleaseStop();
51+
cancellationToken.addListener(cancellationListener);
4252
try {
4353
listener.testRunStarted(runnerTestDescriptor.getDescription());
4454
runnerTestDescriptor.getRunner().run(notifier);
4555
listener.testRunFinished();
4656
}
57+
catch (StoppedByUserException e) {
58+
reportEventsForCancellation(e, testRun);
59+
}
4760
catch (Throwable t) {
4861
UnrecoverableExceptions.rethrowIfUnrecoverable(t);
4962
reportUnexpectedFailure(testRun, runnerTestDescriptor, failed(t));
5063
}
64+
finally {
65+
cancellationToken.removeListener(cancellationListener);
66+
}
67+
}
68+
69+
private void reportEventsForCancellation(StoppedByUserException exception, TestRun testRun) {
70+
testRun.getInProgressTestDescriptors().forEach(startedDescriptor -> {
71+
startedDescriptor.getChildren().forEach(child -> {
72+
if (!testRun.isFinishedOrSkipped(child)) {
73+
engineExecutionListener.executionSkipped(child, "Execution cancelled");
74+
testRun.markSkipped(child);
75+
}
76+
});
77+
engineExecutionListener.executionFinished(startedDescriptor, TestExecutionResult.aborted(exception));
78+
testRun.markFinished(startedDescriptor);
79+
});
5180
}
5281

5382
private void reportUnexpectedFailure(TestRun testRun, RunnerTestDescriptor runnerTestDescriptor,

junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/TestRun.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,12 @@ Collection<TestDescriptor> getInProgressTestDescriptorsWithSyntheticStartEvents(
8787
return result;
8888
}
8989

90+
Collection<TestDescriptor> getInProgressTestDescriptors() {
91+
List<TestDescriptor> result = new ArrayList<>(inProgressDescriptors.keySet());
92+
Collections.reverse(result);
93+
return result;
94+
}
95+
9096
boolean isDescendantOfRunnerTestDescriptor(TestDescriptor testDescriptor) {
9197
return runnerDescendants.contains(testDescriptor);
9298
}

junit-vintage-engine/src/main/java/org/junit/vintage/engine/execution/VintageExecutor.java

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import org.junit.platform.commons.logging.Logger;
2828
import org.junit.platform.commons.logging.LoggerFactory;
2929
import org.junit.platform.commons.util.ExceptionUtils;
30+
import org.junit.platform.engine.CancellationToken;
3031
import org.junit.platform.engine.ConfigurationParameters;
3132
import org.junit.platform.engine.EngineExecutionListener;
3233
import org.junit.platform.engine.TestDescriptor;
@@ -65,37 +66,37 @@ public VintageExecutor(VintageEngineDescriptor engineDescriptor, EngineExecution
6566
this.methods = configurationParameters.getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false);
6667
}
6768

68-
public void executeAllChildren() {
69+
public void executeAllChildren(CancellationToken cancellationToken) {
6970

7071
if (!parallelExecutionEnabled) {
71-
executeClassesAndMethodsSequentially();
72+
executeClassesAndMethodsSequentially(cancellationToken);
7273
return;
7374
}
7475

7576
if (!classes && !methods) {
7677
logger.warn(() -> "Parallel execution is enabled but no scope is defined. "
7778
+ "Falling back to sequential execution.");
78-
executeClassesAndMethodsSequentially();
79+
executeClassesAndMethodsSequentially(cancellationToken);
7980
return;
8081
}
8182

82-
boolean wasInterrupted = executeInParallel();
83+
boolean wasInterrupted = executeInParallel(cancellationToken);
8384
if (wasInterrupted) {
8485
Thread.currentThread().interrupt();
8586
}
8687
}
8788

88-
private void executeClassesAndMethodsSequentially() {
89-
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
89+
private void executeClassesAndMethodsSequentially(CancellationToken cancellationToken) {
90+
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener, cancellationToken);
9091
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
9192
runnerExecutor.execute((RunnerTestDescriptor) iterator.next());
9293
iterator.remove();
9394
}
9495
}
9596

96-
private boolean executeInParallel() {
97+
private boolean executeInParallel(CancellationToken cancellationToken) {
9798
ExecutorService executorService = Executors.newWorkStealingPool(getThreadPoolSize());
98-
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
99+
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener, cancellationToken);
99100

100101
List<RunnerTestDescriptor> runnerTestDescriptors = collectRunnerTestDescriptors(executorService);
101102

junit-vintage-engine/src/test/java/org/junit/vintage/engine/VintageTestEngineExecutionTests.java

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
import org.junit.jupiter.api.Test;
4545
import org.junit.jupiter.api.extension.DisabledInEclipse;
4646
import org.junit.platform.commons.util.ReflectionUtils;
47+
import org.junit.platform.engine.CancellationToken;
4748
import org.junit.platform.engine.EngineExecutionListener;
4849
import org.junit.platform.engine.ExecutionRequest;
4950
import org.junit.platform.engine.TestDescriptor;
@@ -57,13 +58,15 @@
5758
import org.junit.runner.RunWith;
5859
import org.junit.runner.Runner;
5960
import org.junit.runner.notification.RunNotifier;
61+
import org.junit.runner.notification.StoppedByUserException;
6062
import org.junit.runners.Suite;
6163
import org.junit.runners.Suite.SuiteClasses;
6264
import org.junit.vintage.engine.samples.junit3.IgnoredJUnit3TestCase;
6365
import org.junit.vintage.engine.samples.junit3.JUnit3ParallelSuiteWithSubsuites;
6466
import org.junit.vintage.engine.samples.junit3.JUnit3SuiteWithSubsuites;
6567
import org.junit.vintage.engine.samples.junit3.JUnit4SuiteWithIgnoredJUnit3TestCase;
6668
import org.junit.vintage.engine.samples.junit3.PlainJUnit3TestCaseWithSingleTestWhichFails;
69+
import org.junit.vintage.engine.samples.junit4.CancellingTestCase;
6770
import org.junit.vintage.engine.samples.junit4.CompletelyDynamicTestCase;
6871
import org.junit.vintage.engine.samples.junit4.EmptyIgnoredTestCase;
6972
import org.junit.vintage.engine.samples.junit4.EnclosedJUnit4TestCase;
@@ -926,6 +929,32 @@ void executesJUnit4SuiteWithIgnoredJUnit3TestCase() {
926929
event(engine(), finishedSuccessfully()));
927930
}
928931

932+
@Test
933+
void supportsCancellation() {
934+
CancellingTestCase.cancellationToken = CancellationToken.create();
935+
try {
936+
var results = vintageTestEngine() //
937+
.selectors(selectClass(CancellingTestCase.class),
938+
selectClass(PlainJUnit4TestCaseWithSingleTestWhichFails.class)) //
939+
.cancellationToken(CancellingTestCase.cancellationToken) //
940+
.execute();
941+
942+
results.allEvents().assertEventsMatchExactly( //
943+
event(engine(), started()), //
944+
event(container(CancellingTestCase.class), started()), //
945+
event(test(), started()), //
946+
event(test(), finishedWithFailure()), //
947+
event(test(), skippedWithReason("Execution cancelled")), //
948+
event(container(CancellingTestCase.class), abortedWithReason(instanceOf(StoppedByUserException.class))), //
949+
event(container(PlainJUnit4TestCaseWithSingleTestWhichFails.class),
950+
skippedWithReason("Execution cancelled")), //
951+
event(engine(), finishedSuccessfully()));
952+
}
953+
finally {
954+
CancellingTestCase.cancellationToken = null;
955+
}
956+
}
957+
929958
private static EngineExecutionResults execute(Class<?> testClass) {
930959
return execute(request(testClass));
931960
}
@@ -935,6 +964,12 @@ private static EngineExecutionResults execute(LauncherDiscoveryRequest request)
935964
return EngineTestKit.execute(new VintageTestEngine(), request);
936965
}
937966

967+
@SuppressWarnings("deprecation")
968+
private static EngineTestKit.Builder vintageTestEngine() {
969+
return EngineTestKit.engine(new VintageTestEngine()) //
970+
.enableImplicitConfigurationParameters(false);
971+
}
972+
938973
@SuppressWarnings("deprecation")
939974
private static void execute(Class<?> testClass, EngineExecutionListener listener) {
940975
var testEngine = new VintageTestEngine();
@@ -943,6 +978,7 @@ private static void execute(Class<?> testClass, EngineExecutionListener listener
943978
when(executionRequest.getRootTestDescriptor()).thenReturn(engineTestDescriptor);
944979
when(executionRequest.getEngineExecutionListener()).thenReturn(listener);
945980
when(executionRequest.getConfigurationParameters()).thenReturn(mock());
981+
when(executionRequest.getCancellationToken()).thenReturn(CancellationToken.disabled());
946982
testEngine.execute(executionRequest);
947983
}
948984

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
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.vintage.engine.samples.junit4;
12+
13+
import static java.util.Objects.requireNonNull;
14+
import static org.junit.Assert.fail;
15+
16+
import org.junit.Before;
17+
import org.junit.Test;
18+
import org.junit.platform.engine.CancellationToken;
19+
20+
public class CancellingTestCase {
21+
22+
public static CancellationToken cancellationToken;
23+
24+
@Before
25+
public void cancelExecution() {
26+
requireNonNull(cancellationToken).cancel();
27+
}
28+
29+
@Test
30+
public void first() {
31+
fail();
32+
}
33+
34+
@Test
35+
public void second() {
36+
fail();
37+
}
38+
}

0 commit comments

Comments
 (0)