Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ repository on GitHub.
now causes test execution to be cancelled after the first failed test.
* Provide cancellation support for implementations of `{HierarchicalTestEngine}` such as
JUnit Jupiter, Spock, and Cucumber.
* Provide cancellation support for Suite engine
* Provide cancellation support for the Suite and Vintage test engines
* Introduce `TestTask.getTestDescriptor()` method for use in
`HierarchicalTestExecutorService` implementations.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,7 @@ are present at runtime.
At the time of writing, the following test engines support cancellation:

* `{junit-jupiter-engine}`
* `{junit-vintage-engine}`
* `{junit-platform-suite-engine}`
* Any `{TestEngine}` extending `{HierarchicalTestEngine}` such as Spock and Cucumber
====
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,8 @@ public void execute(ExecutionRequest request) {
VintageEngineDescriptor engineDescriptor = (VintageEngineDescriptor) request.getRootTestDescriptor();
// TODO #4725 Provide cancellation support for Vintage engine
engineExecutionListener.executionStarted(engineDescriptor);
new VintageExecutor(engineDescriptor, engineExecutionListener, request).executeAllChildren();
new VintageExecutor(engineDescriptor, engineExecutionListener,
request.getConfigurationParameters()).executeAllChildren(request.getCancellationToken());
engineExecutionListener.executionFinished(engineDescriptor, successful());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public Request toRequest() {
return new RunnerRequest(this.runner);
}

public Runner getRunner() {
return runner;
}

@Override
protected boolean tryToExcludeFromRunner(Description description) {
boolean excluded = tryToFilterRunner(description);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.vintage.engine.execution;

import org.junit.platform.engine.CancellationToken;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;

/**
* @since 6.0
*/
class CancellationTokenAwareRunNotifier extends RunNotifier {

private final CancellationToken cancellationToken;

CancellationTokenAwareRunNotifier(CancellationToken cancellationToken) {
this.cancellationToken = cancellationToken;
}

@Override
public void fireTestStarted(Description description) throws StoppedByUserException {
if (cancellationToken.isCancellationRequested()) {
pleaseStop();
}
super.fireTestStarted(description);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,10 @@ public void testSuiteFinished(Description description) {

@Override
public void testRunFinished(Result result) {
testRunFinished();
}

void testRunFinished() {
reportContainerFinished(testRun.getRunnerTestDescriptor());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@

import org.apiguardian.api.API;
import org.junit.platform.commons.util.UnrecoverableExceptions;
import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.TestExecutionResult;
import org.junit.runner.JUnitCore;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.vintage.engine.descriptor.RunnerTestDescriptor;
import org.junit.vintage.engine.descriptor.TestSourceProvider;

Expand All @@ -28,25 +30,50 @@
public class RunnerExecutor {

private final EngineExecutionListener engineExecutionListener;
private final CancellationToken cancellationToken;
private final TestSourceProvider testSourceProvider = new TestSourceProvider();

public RunnerExecutor(EngineExecutionListener engineExecutionListener) {
public RunnerExecutor(EngineExecutionListener engineExecutionListener, CancellationToken cancellationToken) {
this.engineExecutionListener = engineExecutionListener;
this.cancellationToken = cancellationToken;
}

public void execute(RunnerTestDescriptor runnerTestDescriptor) {
TestRun testRun = new TestRun(runnerTestDescriptor);
JUnitCore core = new JUnitCore();
core.addListener(new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider));
if (cancellationToken.isCancellationRequested()) {
engineExecutionListener.executionSkipped(runnerTestDescriptor, "Execution cancelled");
return;
}
RunNotifier notifier = new CancellationTokenAwareRunNotifier(cancellationToken);
var testRun = new TestRun(runnerTestDescriptor);
var listener = new RunListenerAdapter(testRun, engineExecutionListener, testSourceProvider);
notifier.addListener(listener);
try {
core.run(runnerTestDescriptor.toRequest());
listener.testRunStarted(runnerTestDescriptor.getDescription());
runnerTestDescriptor.getRunner().run(notifier);
listener.testRunFinished();
}
catch (StoppedByUserException e) {
reportEventsForCancellation(e, testRun);
}
catch (Throwable t) {
UnrecoverableExceptions.rethrowIfUnrecoverable(t);
reportUnexpectedFailure(testRun, runnerTestDescriptor, failed(t));
}
}

private void reportEventsForCancellation(StoppedByUserException exception, TestRun testRun) {
testRun.getInProgressTestDescriptors().forEach(startedDescriptor -> {
startedDescriptor.getChildren().forEach(child -> {
if (!testRun.isFinishedOrSkipped(child)) {
engineExecutionListener.executionSkipped(child, "Execution cancelled");
testRun.markSkipped(child);
}
});
engineExecutionListener.executionFinished(startedDescriptor, TestExecutionResult.aborted(exception));
testRun.markFinished(startedDescriptor);
});
}

private void reportUnexpectedFailure(TestRun testRun, RunnerTestDescriptor runnerTestDescriptor,
TestExecutionResult result) {
if (testRun.isNotStarted(runnerTestDescriptor)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,12 @@ Collection<TestDescriptor> getInProgressTestDescriptorsWithSyntheticStartEvents(
return result;
}

Collection<TestDescriptor> getInProgressTestDescriptors() {
List<TestDescriptor> result = new ArrayList<>(inProgressDescriptors.keySet());
Collections.reverse(result);
return result;
}

boolean isDescendantOfRunnerTestDescriptor(TestDescriptor testDescriptor) {
return runnerDescendants.contains(testDescriptor);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
import org.junit.platform.commons.util.ExceptionUtils;
import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.ConfigurationParameters;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
import org.junit.vintage.engine.Constants;
import org.junit.vintage.engine.descriptor.RunnerTestDescriptor;
Expand All @@ -48,56 +49,54 @@ public class VintageExecutor {

private final VintageEngineDescriptor engineDescriptor;
private final EngineExecutionListener engineExecutionListener;
private final ExecutionRequest request;
private final ConfigurationParameters configurationParameters;

private final boolean parallelExecutionEnabled;
private final boolean classes;
private final boolean methods;

public VintageExecutor(VintageEngineDescriptor engineDescriptor, EngineExecutionListener engineExecutionListener,
ExecutionRequest request) {
ConfigurationParameters configurationParameters) {
this.engineDescriptor = engineDescriptor;
this.engineExecutionListener = engineExecutionListener;
this.request = request;
this.parallelExecutionEnabled = request.getConfigurationParameters().getBoolean(
Constants.PARALLEL_EXECUTION_ENABLED).orElse(false);
this.classes = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(
false);
this.methods = request.getConfigurationParameters().getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(
this.configurationParameters = configurationParameters;
this.parallelExecutionEnabled = configurationParameters.getBoolean(Constants.PARALLEL_EXECUTION_ENABLED).orElse(
false);
this.classes = configurationParameters.getBoolean(Constants.PARALLEL_CLASS_EXECUTION).orElse(false);
this.methods = configurationParameters.getBoolean(Constants.PARALLEL_METHOD_EXECUTION).orElse(false);
}

public void executeAllChildren() {
public void executeAllChildren(CancellationToken cancellationToken) {

if (!parallelExecutionEnabled) {
executeClassesAndMethodsSequentially();
executeClassesAndMethodsSequentially(cancellationToken);
return;
}

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

boolean wasInterrupted = executeInParallel();
boolean wasInterrupted = executeInParallel(cancellationToken);
if (wasInterrupted) {
Thread.currentThread().interrupt();
}
}

private void executeClassesAndMethodsSequentially() {
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
private void executeClassesAndMethodsSequentially(CancellationToken cancellationToken) {
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener, cancellationToken);
for (Iterator<TestDescriptor> iterator = engineDescriptor.getModifiableChildren().iterator(); iterator.hasNext();) {
runnerExecutor.execute((RunnerTestDescriptor) iterator.next());
iterator.remove();
}
}

private boolean executeInParallel() {
private boolean executeInParallel(CancellationToken cancellationToken) {
ExecutorService executorService = Executors.newWorkStealingPool(getThreadPoolSize());
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener);
RunnerExecutor runnerExecutor = new RunnerExecutor(engineExecutionListener, cancellationToken);

List<RunnerTestDescriptor> runnerTestDescriptors = collectRunnerTestDescriptors(executorService);

Expand All @@ -110,7 +109,7 @@ private boolean executeInParallel() {
}

private int getThreadPoolSize() {
Optional<String> optionalPoolSize = request.getConfigurationParameters().get(Constants.PARALLEL_POOL_SIZE);
Optional<String> optionalPoolSize = configurationParameters.get(Constants.PARALLEL_POOL_SIZE);
if (optionalPoolSize.isPresent()) {
try {
int poolSize = Integer.parseInt(optionalPoolSize.get());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.DisabledInEclipse;
import org.junit.platform.commons.util.ReflectionUtils;
import org.junit.platform.engine.CancellationToken;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.ExecutionRequest;
import org.junit.platform.engine.TestDescriptor;
Expand All @@ -57,13 +58,15 @@
import org.junit.runner.RunWith;
import org.junit.runner.Runner;
import org.junit.runner.notification.RunNotifier;
import org.junit.runner.notification.StoppedByUserException;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.junit.vintage.engine.samples.junit3.IgnoredJUnit3TestCase;
import org.junit.vintage.engine.samples.junit3.JUnit3ParallelSuiteWithSubsuites;
import org.junit.vintage.engine.samples.junit3.JUnit3SuiteWithSubsuites;
import org.junit.vintage.engine.samples.junit3.JUnit4SuiteWithIgnoredJUnit3TestCase;
import org.junit.vintage.engine.samples.junit3.PlainJUnit3TestCaseWithSingleTestWhichFails;
import org.junit.vintage.engine.samples.junit4.CancellingTestCase;
import org.junit.vintage.engine.samples.junit4.CompletelyDynamicTestCase;
import org.junit.vintage.engine.samples.junit4.EmptyIgnoredTestCase;
import org.junit.vintage.engine.samples.junit4.EnclosedJUnit4TestCase;
Expand Down Expand Up @@ -926,6 +929,32 @@ void executesJUnit4SuiteWithIgnoredJUnit3TestCase() {
event(engine(), finishedSuccessfully()));
}

@Test
void supportsCancellation() {
CancellingTestCase.cancellationToken = CancellationToken.create();
try {
var results = vintageTestEngine() //
.selectors(selectClass(CancellingTestCase.class),
selectClass(PlainJUnit4TestCaseWithSingleTestWhichFails.class)) //
.cancellationToken(CancellingTestCase.cancellationToken) //
.execute();

results.allEvents().assertEventsMatchExactly( //
event(engine(), started()), //
event(container(CancellingTestCase.class), started()), //
event(test(), started()), //
event(test(), finishedWithFailure()), //
event(test(), skippedWithReason("Execution cancelled")), //
event(container(CancellingTestCase.class), abortedWithReason(instanceOf(StoppedByUserException.class))), //
event(container(PlainJUnit4TestCaseWithSingleTestWhichFails.class),
skippedWithReason("Execution cancelled")), //
event(engine(), finishedSuccessfully()));
}
finally {
CancellingTestCase.cancellationToken = null;
}
}

private static EngineExecutionResults execute(Class<?> testClass) {
return execute(request(testClass));
}
Expand All @@ -935,6 +964,12 @@ private static EngineExecutionResults execute(LauncherDiscoveryRequest request)
return EngineTestKit.execute(new VintageTestEngine(), request);
}

@SuppressWarnings("deprecation")
private static EngineTestKit.Builder vintageTestEngine() {
return EngineTestKit.engine(new VintageTestEngine()) //
.enableImplicitConfigurationParameters(false);
}

@SuppressWarnings("deprecation")
private static void execute(Class<?> testClass, EngineExecutionListener listener) {
var testEngine = new VintageTestEngine();
Expand All @@ -943,6 +978,7 @@ private static void execute(Class<?> testClass, EngineExecutionListener listener
when(executionRequest.getRootTestDescriptor()).thenReturn(engineTestDescriptor);
when(executionRequest.getEngineExecutionListener()).thenReturn(listener);
when(executionRequest.getConfigurationParameters()).thenReturn(mock());
when(executionRequest.getCancellationToken()).thenReturn(CancellationToken.disabled());
testEngine.execute(executionRequest);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* Copyright 2015-2025 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.vintage.engine.samples.junit4;

import static java.util.Objects.requireNonNull;
import static org.junit.Assert.fail;

import org.junit.Before;
import org.junit.Test;
import org.junit.platform.engine.CancellationToken;

public class CancellingTestCase {

public static CancellationToken cancellationToken;

@Before
public void cancelExecution() {
requireNonNull(cancellationToken).cancel();
}

@Test
public void first() {
fail();
}

@Test
public void second() {
fail();
}
}