Skip to content

Commit 35d51eb

Browse files
authored
Provide cancellation support for Vintage engine (#4735)
Issue: #4725
1 parent 6c33cd4 commit 35d51eb

File tree

11 files changed

+178
-26
lines changed

11 files changed

+178
-26
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: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ public void execute(ExecutionRequest request) {
7171
VintageEngineDescriptor engineDescriptor = (VintageEngineDescriptor) request.getRootTestDescriptor();
7272
// TODO #4725 Provide cancellation support for Vintage engine
7373
engineExecutionListener.executionStarted(engineDescriptor);
74-
new VintageExecutor(engineDescriptor, engineExecutionListener, request).executeAllChildren();
74+
new VintageExecutor(engineDescriptor, engineExecutionListener,
75+
request.getConfigurationParameters()).executeAllChildren(request.getCancellationToken());
7576
engineExecutionListener.executionFinished(engineDescriptor, successful());
7677
}
7778
}

junit-vintage-engine/src/main/java/org/junit/vintage/engine/descriptor/RunnerTestDescriptor.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,10 @@ public Request toRequest() {
7474
return new RunnerRequest(this.runner);
7575
}
7676

77+
public Runner getRunner() {
78+
return runner;
79+
}
80+
7781
@Override
7882
protected boolean tryToExcludeFromRunner(Description description) {
7983
boolean excluded = tryToFilterRunner(description);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
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.execution;
12+
13+
import org.junit.platform.engine.CancellationToken;
14+
import org.junit.runner.Description;
15+
import org.junit.runner.notification.RunNotifier;
16+
import org.junit.runner.notification.StoppedByUserException;
17+
18+
/**
19+
* @since 6.0
20+
*/
21+
class CancellationTokenAwareRunNotifier extends RunNotifier {
22+
23+
private final CancellationToken cancellationToken;
24+
25+
CancellationTokenAwareRunNotifier(CancellationToken cancellationToken) {
26+
this.cancellationToken = cancellationToken;
27+
}
28+
29+
@Override
30+
public void fireTestStarted(Description description) throws StoppedByUserException {
31+
if (cancellationToken.isCancellationRequested()) {
32+
pleaseStop();
33+
}
34+
super.fireTestStarted(description);
35+
}
36+
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,10 @@ public void testSuiteFinished(Description description) {
103103

104104
@Override
105105
public void testRunFinished(Result result) {
106+
testRunFinished();
107+
}
108+
109+
void testRunFinished() {
106110
reportContainerFinished(testRun.getRunnerTestDescriptor());
107111
}
108112

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

Lines changed: 33 additions & 6 deletions
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;
20-
import org.junit.runner.JUnitCore;
21+
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,25 +30,50 @@
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) {
38-
TestRun testRun = new TestRun(runnerTestDescriptor);
39-
JUnitCore core = new JUnitCore();
40-
core.addListener(new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider));
42+
if (cancellationToken.isCancellationRequested()) {
43+
engineExecutionListener.executionSkipped(runnerTestDescriptor, "Execution cancelled");
44+
return;
45+
}
46+
RunNotifier notifier = new CancellationTokenAwareRunNotifier(cancellationToken);
47+
var testRun = new TestRun(runnerTestDescriptor);
48+
var listener = new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider);
49+
notifier.addListener(listener);
4150
try {
42-
core.run(runnerTestDescriptor.toRequest());
51+
listener.testRunStarted(runnerTestDescriptor.getDescription());
52+
runnerTestDescriptor.getRunner().run(notifier);
53+
listener.testRunFinished();
54+
}
55+
catch (StoppedByUserException e) {
56+
reportEventsForCancellation(e, testRun);
4357
}
4458
catch (Throwable t) {
4559
UnrecoverableExceptions.rethrowIfUnrecoverable(t);
4660
reportUnexpectedFailure(testRun, runnerTestDescriptor, failed(t));
4761
}
4862
}
4963

64+
private void reportEventsForCancellation(StoppedByUserException exception, TestRun testRun) {
65+
testRun.getInProgressTestDescriptors().forEach(startedDescriptor -> {
66+
startedDescriptor.getChildren().forEach(child -> {
67+
if (!testRun.isFinishedOrSkipped(child)) {
68+
engineExecutionListener.executionSkipped(child, "Execution cancelled");
69+
testRun.markSkipped(child);
70+
}
71+
});
72+
engineExecutionListener.executionFinished(startedDescriptor, TestExecutionResult.aborted(exception));
73+
testRun.markFinished(startedDescriptor);
74+
});
75+
}
76+
5077
private void reportUnexpectedFailure(TestRun testRun, RunnerTestDescriptor runnerTestDescriptor,
5178
TestExecutionResult result) {
5279
if (testRun.isNotStarted(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: 17 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,9 @@
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;
31+
import org.junit.platform.engine.ConfigurationParameters;
3032
import org.junit.platform.engine.EngineExecutionListener;
31-
import org.junit.platform.engine.ExecutionRequest;
3233
import org.junit.platform.engine.TestDescriptor;
3334
import org.junit.vintage.engine.Constants;
3435
import org.junit.vintage.engine.descriptor.RunnerTestDescriptor;
@@ -48,56 +49,54 @@ public class VintageExecutor {
4849

4950
private final VintageEngineDescriptor engineDescriptor;
5051
private final EngineExecutionListener engineExecutionListener;
51-
private final ExecutionRequest request;
52+
private final ConfigurationParameters configurationParameters;
5253

5354
private final boolean parallelExecutionEnabled;
5455
private final boolean classes;
5556
private final boolean methods;
5657

5758
public VintageExecutor(VintageEngineDescriptor engineDescriptor, EngineExecutionListener engineExecutionListener,
58-
ExecutionRequest request) {
59+
ConfigurationParameters configurationParameters) {
5960
this.engineDescriptor = engineDescriptor;
6061
this.engineExecutionListener = engineExecutionListener;
61-
this.request = request;
62-
this.parallelExecutionEnabled = request.getConfigurationParameters().getBoolean(
63-
Constants.PARALLEL_EXECUTION_ENABLED).orElse(false);
64-
this.classes = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(
65-
false);
66-
this.methods = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(
62+
this.configurationParameters = configurationParameters;
63+
this.parallelExecutionEnabled = configurationParameters.getBoolean(Constants.PARALLEL_EXECUTION_ENABLED).orElse(
6764
false);
65+
this.classes = configurationParameters.getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(false);
66+
this.methods = configurationParameters.getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false);
6867
}
6968

70-
public void executeAllChildren() {
69+
public void executeAllChildren(CancellationToken cancellationToken) {
7170

7271
if (!parallelExecutionEnabled) {
73-
executeClassesAndMethodsSequentially();
72+
executeClassesAndMethodsSequentially(cancellationToken);
7473
return;
7574
}
7675

7776
if (!classes && !methods) {
7877
logger.warn(() -> "Parallel execution is enabled but no scope is defined. "
7978
+ "Falling back to sequential execution.");
80-
executeClassesAndMethodsSequentially();
79+
executeClassesAndMethodsSequentially(cancellationToken);
8180
return;
8281
}
8382

84-
boolean wasInterrupted = executeInParallel();
83+
boolean wasInterrupted = executeInParallel(cancellationToken);
8584
if (wasInterrupted) {
8685
Thread.currentThread().interrupt();
8786
}
8887
}
8988

90-
private void executeClassesAndMethodsSequentially() {
91-
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
89+
private void executeClassesAndMethodsSequentially(CancellationToken cancellationToken) {
90+
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener, cancellationToken);
9291
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
9392
runnerExecutor.execute((RunnerTestDescriptor) iterator.next());
9493
iterator.remove();
9594
}
9695
}
9796

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

102101
List<RunnerTestDescriptor> runnerTestDescriptors = collectRunnerTestDescriptors(executorService);
103102

@@ -110,7 +109,7 @@ private boolean executeInParallel() {
110109
}
111110

112111
private int getThreadPoolSize() {
113-
Optional<String> optionalPoolSize = request.getConfigurationParameters().get(Constants.PARALLEL_POOL_SIZE);
112+
Optional<String> optionalPoolSize = configurationParameters.get(Constants.PARALLEL_POOL_SIZE);
114113
if (optionalPoolSize.isPresent()) {
115114
try {
116115
int poolSize = Integer.parseInt(optionalPoolSize.get());

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

0 commit comments

Comments
 (0)