-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Draft support for cancelling test execution #4709
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
1172d71
1b9086f
617b66e
c02615e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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.platform.engine; | ||
|
||
import org.apiguardian.api.API; | ||
|
||
/** | ||
* Token that should be checked to determine whether an operation was requested | ||
* to be cancelled. | ||
* | ||
* <p>For example, this is used by {@link org.junit.platform.engine.TestEngine} | ||
* implementations to determine whether | ||
* | ||
* <p>This interface is not intended to be implemented by clients. | ||
* | ||
* @since 6.0 | ||
* @see ExecutionRequest#getCancellationToken() | ||
*/ | ||
@API(status = API.Status.EXPERIMENTAL, since = "6.0") | ||
public interface CancellationToken { | ||
|
||
static CancellationToken create() { | ||
return new DefaultCancellationToken(); | ||
} | ||
|
||
boolean isCancellationRequested(); | ||
|
||
void cancel(); | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
/* | ||
* 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.platform.engine; | ||
|
||
import java.util.concurrent.atomic.AtomicBoolean; | ||
|
||
class DefaultCancellationToken implements CancellationToken { | ||
|
||
private final AtomicBoolean cancelled = new AtomicBoolean(); | ||
|
||
@Override | ||
public boolean isCancellationRequested() { | ||
return cancelled.get(); | ||
} | ||
|
||
@Override | ||
public void cancel() { | ||
cancelled.set(true); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -43,28 +43,29 @@ public class ExecutionRequest { | |
private final TestDescriptor rootTestDescriptor; | ||
private final EngineExecutionListener engineExecutionListener; | ||
private final ConfigurationParameters configurationParameters; | ||
|
||
private final @Nullable OutputDirectoryProvider outputDirectoryProvider; | ||
|
||
private final @Nullable NamespacedHierarchicalStore<Namespace> requestLevelStore; | ||
private final @Nullable CancellationToken cancellationToken; | ||
|
||
@Deprecated | ||
@API(status = DEPRECATED, since = "1.11") | ||
public ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, | ||
ConfigurationParameters configurationParameters) { | ||
this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null); | ||
this(rootTestDescriptor, engineExecutionListener, configurationParameters, null, null, null); | ||
} | ||
|
||
private ExecutionRequest(TestDescriptor rootTestDescriptor, EngineExecutionListener engineExecutionListener, | ||
ConfigurationParameters configurationParameters, @Nullable OutputDirectoryProvider outputDirectoryProvider, | ||
@Nullable NamespacedHierarchicalStore<Namespace> requestLevelStore) { | ||
@Nullable NamespacedHierarchicalStore<Namespace> requestLevelStore, | ||
@Nullable CancellationToken cancellationToken) { | ||
this.rootTestDescriptor = Preconditions.notNull(rootTestDescriptor, "rootTestDescriptor must not be null"); | ||
this.engineExecutionListener = Preconditions.notNull(engineExecutionListener, | ||
"engineExecutionListener must not be null"); | ||
this.configurationParameters = Preconditions.notNull(configurationParameters, | ||
"configurationParameters must not be null"); | ||
this.outputDirectoryProvider = outputDirectoryProvider; | ||
this.requestLevelStore = requestLevelStore; | ||
this.cancellationToken = cancellationToken; | ||
} | ||
|
||
/** | ||
|
@@ -105,11 +106,13 @@ public static ExecutionRequest create(TestDescriptor rootTestDescriptor, | |
@API(status = INTERNAL, since = "1.13") | ||
public static ExecutionRequest create(TestDescriptor rootTestDescriptor, | ||
EngineExecutionListener engineExecutionListener, ConfigurationParameters configurationParameters, | ||
OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore<Namespace> requestLevelStore) { | ||
OutputDirectoryProvider outputDirectoryProvider, NamespacedHierarchicalStore<Namespace> requestLevelStore, | ||
CancellationToken cancellationToken) { | ||
|
||
return new ExecutionRequest(rootTestDescriptor, engineExecutionListener, configurationParameters, | ||
Preconditions.notNull(outputDirectoryProvider, "outputDirectoryProvider must not be null"), | ||
Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null")); | ||
Preconditions.notNull(requestLevelStore, "requestLevelStore must not be null"), | ||
Preconditions.notNull(cancellationToken, "cancellationToken must not be null")); | ||
} | ||
|
||
/** | ||
|
@@ -171,4 +174,8 @@ public NamespacedHierarchicalStore<Namespace> getStore() { | |
"No NamespacedHierarchicalStore was configured for this request"); | ||
} | ||
|
||
public CancellationToken getCancellationToken() { | ||
return Preconditions.notNull(this.cancellationToken, "No CancellationToken was configured for this request"); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I had some difficulty inferring from what code how I could safely use the cancellation token. Instead of making the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can instead return a default one here since it has a sensible default. |
||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -46,6 +46,9 @@ | |
class NodeTestTask<C extends EngineExecutionContext> implements TestTask { | ||
|
||
private static final Logger logger = LoggerFactory.getLogger(NodeTestTask.class); | ||
|
||
private static final SkipResult CANCELLED_SKIP_RESULT = SkipResult.skip("Test execution cancelled"); | ||
|
||
private static final Runnable NOOP = () -> { | ||
}; | ||
|
||
|
@@ -101,7 +104,7 @@ public void execute() { | |
throwableCollector = taskContext.throwableCollectorFactory().create(); | ||
prepare(); | ||
if (throwableCollector.isEmpty()) { | ||
checkWhetherSkipped(); | ||
throwableCollector.execute(() -> skipResult = checkWhetherSkipped()); | ||
} | ||
if (throwableCollector.isEmpty() && !requiredSkipResult().isSkipped()) { | ||
executeRecursively(); | ||
|
@@ -139,8 +142,10 @@ private void prepare() { | |
parentContext = null; | ||
} | ||
|
||
private void checkWhetherSkipped() { | ||
requiredThrowableCollector().execute(() -> skipResult = node.shouldBeSkipped(requiredContext())); | ||
private SkipResult checkWhetherSkipped() throws Exception { | ||
return taskContext.cancellationToken().isCancellationRequested() // | ||
? CANCELLED_SKIP_RESULT // | ||
: node.shouldBeSkipped(requiredContext()); | ||
Comment on lines
+145
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Support for engines extending There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For Cucumber this means that cancellation will work out of the box. 👍 |
||
} | ||
|
||
private void executeRecursively() { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,11 @@ | |
|
||
package org.junit.platform.launcher; | ||
|
||
import static org.apiguardian.api.API.Status.EXPERIMENTAL; | ||
import static org.apiguardian.api.API.Status.STABLE; | ||
|
||
import org.apiguardian.api.API; | ||
import org.junit.platform.engine.CancellationToken; | ||
|
||
/** | ||
* The {@code Launcher} API is the main entry point for client code that | ||
|
@@ -108,6 +110,37 @@ public interface Launcher { | |
*/ | ||
void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, TestExecutionListener... listeners); | ||
|
||
/** | ||
* Execute a {@link TestPlan} which is built according to the supplied | ||
* {@link LauncherDiscoveryRequest} by querying all registered engines and | ||
* collecting their results, and notify | ||
* {@linkplain #registerTestExecutionListeners registered listeners} about | ||
* the progress and results of the execution. | ||
* | ||
* <p>Supplied test execution listeners are registered in addition to already | ||
* registered listeners but only for the supplied launcher discovery request. | ||
* | ||
* <p>Additionally, it's possible to request cancellation of the started | ||
* execution via the supplied {@link CancellationToken}. | ||
* | ||
* @apiNote Calling this method will cause test discovery to be executed for | ||
* all registered engines. If the same {@link LauncherDiscoveryRequest} was | ||
* previously passed to {@link #discover(LauncherDiscoveryRequest)}, you | ||
* should instead call {@link #execute(TestPlan, TestExecutionListener...)} | ||
* and pass the already acquired {@link TestPlan} to avoid the potential | ||
* performance degradation (e.g., classpath scanning) of running test | ||
* discovery twice. | ||
* | ||
* @param launcherDiscoveryRequest the launcher discovery request; never {@code null} | ||
* @param cancellationToken the token used to request cancellation of the | ||
* started execution; never {@code null} | ||
* @param listeners additional test execution listeners; never {@code null} | ||
* @since 6.0 | ||
*/ | ||
@API(status = EXPERIMENTAL, since = "6.0") | ||
void execute(LauncherDiscoveryRequest launcherDiscoveryRequest, CancellationToken cancellationToken, | ||
TestExecutionListener... listeners); | ||
|
||
/** | ||
* Execute the supplied {@link TestPlan} and notify | ||
* {@linkplain #registerTestExecutionListeners registered listeners} about | ||
|
@@ -127,4 +160,28 @@ public interface Launcher { | |
@API(status = STABLE, since = "1.4") | ||
void execute(TestPlan testPlan, TestExecutionListener... listeners); | ||
|
||
/** | ||
* Execute the supplied {@link TestPlan} and notify | ||
* {@linkplain #registerTestExecutionListeners registered listeners} about | ||
* the progress and results of the execution. | ||
* | ||
* <p>Supplied test execution listeners are registered in addition to | ||
* already registered listeners but only for the execution of the supplied | ||
* test plan. | ||
* | ||
* <p>Additionally, it's possible to request cancellation of the started | ||
* execution via the supplied {@link CancellationToken}. | ||
* | ||
* @apiNote The supplied {@link TestPlan} must not have been executed | ||
* previously. | ||
* | ||
* @param testPlan the test plan to execute; never {@code null} | ||
* @param cancellationToken the token used to request cancellation of the | ||
* started execution; never {@code null} | ||
* @param listeners additional test execution listeners; never {@code null} | ||
* @since 6.0 | ||
*/ | ||
@API(status = EXPERIMENTAL, since = "6.0") | ||
void execute(TestPlan testPlan, CancellationToken cancellationToken, TestExecutionListener... listeners); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. New API for build tools and IDEs |
||
|
||
} |
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
New API for
TestEngine
implementors