Skip to content

Commit f32aa0a

Browse files
Introduce PreInterruptCallback extension point (#3431)
The new extension point allows defines the API for `Extensions` that wish to be called prior to invocations of `Thread#interrupt()` by the `@Timeout` extension. When enabled via the `junit.jupiter.execution.timeout.threaddump.enabled` configuration parameter, a default extension implementation that writes a thread dump to `System.out` is registered. Resolves #2938. --------- Co-authored-by: Marc Philipp <[email protected]>
1 parent 6f821e9 commit f32aa0a

File tree

41 files changed

+766
-87
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+766
-87
lines changed

documentation/src/docs/asciidoc/link-attributes.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ endif::[]
156156
:TestTemplateInvocationContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContext.html[TestTemplateInvocationContext]
157157
:TestTemplateInvocationContextProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.html[TestTemplateInvocationContextProvider]
158158
:TestWatcher: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestWatcher.html[TestWatcher]
159+
:PreInterruptCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/PreInterruptCallback.html[PreInterruptCallback]
159160
// Jupiter Conditions
160161
:DisabledForJreRange: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledForJreRange.html[@DisabledForJreRange]
161162
:DisabledIf: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledIf.html[@DisabledIf]

documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc

+1
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ JUnit repository on GitHub.
9090
a test-scoped `ExtensionContext` in `Extension` methods called during test class
9191
instantiation. This behavior will become the default in future versions of JUnit.
9292
* `@TempDir` is now supported on test class constructors.
93+
* Added `PreInterruptCallback`
9394

9495

9596
[[release-notes-5.12.0-M1-junit-vintage]]

documentation/src/docs/asciidoc/user-guide/extensions.adoc

+9
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,15 @@ test methods.
715715
include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide]
716716
----
717717

718+
[[extensions-preinterrupt-callback]]
719+
=== Pre-Interrupt Callback
720+
721+
`{PreInterruptCallback}` defines the API for `Extensions` that wish to react on
722+
timeouts before the `Thread.interrupt()` is called.
723+
724+
Please refer to <<writing-tests-declarative-timeouts-debugging>> for additional information.
725+
726+
718727
[[extensions-intercepting-invocations]]
719728
=== Intercepting Invocations
720729

documentation/src/docs/asciidoc/user-guide/writing-tests.adoc

+16
Original file line numberDiff line numberDiff line change
@@ -2659,6 +2659,22 @@ asynchronous tests, consider using a dedicated library such as
26592659
link:https://github.com/awaitility/awaitility[Awaitility].
26602660

26612661

2662+
[[writing-tests-declarative-timeouts-debugging]]
2663+
=== Debugging Timeouts
2664+
2665+
Registered <<extensions-preinterrupt-callback>> extensions are called prior to invoking
2666+
`Thread.interrupt()` on the thread that is executing the timed out method. This allows to
2667+
inspect the application state and output additional information that might be helpful for
2668+
diagnosing the cause of a timeout.
2669+
2670+
2671+
[[writing-tests-declarative-timeouts-debugging-thread-dump]]
2672+
==== Thread Dump on Timeout
2673+
JUnit registers a default implementation of the <<extensions-preinterrupt-callback>> extension point that
2674+
dumps the stacks of all threads to `System.out` if enabled by setting the
2675+
`junit.jupiter.execution.timeout.threaddump.enabled` configuration parameter to `true`.
2676+
2677+
26622678
[[writing-tests-declarative-timeouts-mode]]
26632679
==== Disable @Timeout Globally
26642680
When stepping through your code in a debug session, a fixed timeout limit may influence

junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java

+2-2
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ private static <T, E extends Throwable> T resolveFutureAndHandleException(Future
113113
cause = new ExecutionTimeoutException("Execution timed out in thread " + thread.getName());
114114
cause.setStackTrace(thread.getStackTrace());
115115
}
116-
throw failureFactory.createTimeoutFailure(timeout, messageSupplier, cause);
116+
throw failureFactory.createTimeoutFailure(timeout, messageSupplier, cause, thread);
117117
}
118118
catch (ExecutionException ex) {
119119
throw throwAsUncheckedException(ex.getCause());
@@ -124,7 +124,7 @@ private static <T, E extends Throwable> T resolveFutureAndHandleException(Future
124124
}
125125

126126
private static AssertionFailedError createAssertionFailure(Duration timeout, Supplier<String> messageSupplier,
127-
Throwable cause) {
127+
Throwable cause, Thread thread) {
128128
return assertionFailure() //
129129
.message(messageSupplier) //
130130
.reason("execution timed out after " + timeout.toMillis() + " ms") //

junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -3662,6 +3662,6 @@ public interface TimeoutFailureFactory<T extends Throwable> {
36623662
*
36633663
* @return timeout failure; never {@code null}
36643664
*/
3665-
T createTimeoutFailure(Duration timeout, Supplier<String> messageSupplier, Throwable cause);
3665+
T createTimeoutFailure(Duration timeout, Supplier<String> messageSupplier, Throwable cause, Thread testThread);
36663666
}
36673667
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/*
2+
* Copyright 2015-2024 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.jupiter.api.extension;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import org.apiguardian.api.API;
16+
17+
/**
18+
* {@code PreInterruptCallback} defines the API for {@link Extension
19+
* Extensions} that wish to be called prior to invocations of
20+
* {@link Thread#interrupt()} by the {@link org.junit.jupiter.api.Timeout}
21+
* extension.
22+
*
23+
* <p>JUnit registers a default implementation that dumps the stacks of all
24+
* {@linkplain Thread threads} to {@code System.out} if the
25+
* {@value #THREAD_DUMP_ENABLED_PROPERTY_NAME} configuration parameter is set to
26+
* {@code true}.
27+
*
28+
* @since 5.12
29+
* @see org.junit.jupiter.api.Timeout
30+
*/
31+
@API(status = EXPERIMENTAL, since = "5.12")
32+
public interface PreInterruptCallback extends Extension {
33+
34+
/**
35+
* Property name used to enable dumping the stack of all
36+
* {@linkplain Thread threads} to {@code System.out} when a timeout has occurred.
37+
*
38+
* <p>This behavior is disabled by default.
39+
*
40+
* @since 5.12
41+
*/
42+
@API(status = EXPERIMENTAL, since = "5.12")
43+
String THREAD_DUMP_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.timeout.threaddump.enabled";
44+
45+
/**
46+
* Callback that is invoked <em>before</em> a {@link Thread} is interrupted with
47+
* {@link Thread#interrupt()}.
48+
*
49+
* <p>Note: There is no guarantee on which {@link Thread} this callback will be
50+
* executed.
51+
*
52+
* @param preInterruptContext the context with the target {@link Thread}, which will get interrupted.
53+
* @param extensionContext the extension context for the callback; never {@code null}
54+
* @since 5.12
55+
* @see PreInterruptContext
56+
*/
57+
@API(status = EXPERIMENTAL, since = "5.12")
58+
void beforeThreadInterrupt(PreInterruptContext preInterruptContext, ExtensionContext extensionContext)
59+
throws Exception;
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2015-2024 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.jupiter.api.extension;
12+
13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
14+
15+
import org.apiguardian.api.API;
16+
17+
/**
18+
* {@code PreInterruptContext} encapsulates the <em>context</em> in which an
19+
* {@link PreInterruptCallback#beforeThreadInterrupt(PreInterruptContext, ExtensionContext) beforeThreadInterrupt} method is called.
20+
*
21+
* @since 5.12
22+
* @see PreInterruptCallback
23+
*/
24+
@API(status = EXPERIMENTAL, since = "5.12")
25+
public interface PreInterruptContext {
26+
27+
/**
28+
* Get the {@link Thread} which will be interrupted.
29+
*
30+
* @return the Thread; never {@code null}
31+
* @since 5.12
32+
*/
33+
@API(status = EXPERIMENTAL, since = "5.12")
34+
Thread getThreadToInterrupt();
35+
}

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java

+12-1
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,17 @@ public final class Constants {
108108
*/
109109
public static final String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME;
110110

111+
/**
112+
* Property name used to enable dumping the stack of all
113+
* {@linkplain Thread threads} to {@code System.out} when a timeout has occurred.
114+
*
115+
* <p>This behavior is disabled by default.
116+
*
117+
* @since 5.12
118+
*/
119+
@API(status = EXPERIMENTAL, since = "5.12")
120+
public static final String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME;
121+
111122
/**
112123
* Property name used to set the default test instance lifecycle mode: {@value}
113124
*
@@ -192,7 +203,7 @@ public final class Constants {
192203
* <p>When set to {@code false} the underlying fork-join pool will reject
193204
* additional tasks if all available workers are busy and the maximum
194205
* pool-size would be exceeded.
195-
206+
*
196207
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
197208
*
198209
* <p>Note: This property only takes affect on Java 9+.

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java

+6
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ public boolean isExtensionAutoDetectionEnabled() {
6868
__ -> delegate.isExtensionAutoDetectionEnabled());
6969
}
7070

71+
@Override
72+
public boolean isThreadDumpOnTimeoutEnabled() {
73+
return (boolean) cache.computeIfAbsent(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME,
74+
__ -> delegate.isThreadDumpOnTimeoutEnabled());
75+
}
76+
7177
@Override
7278
public ExecutionMode getDefaultExecutionMode() {
7379
return (ExecutionMode) cache.computeIfAbsent(DEFAULT_EXECUTION_MODE_PROPERTY_NAME,

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java

+5
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,11 @@ public boolean isExtensionAutoDetectionEnabled() {
9393
return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false);
9494
}
9595

96+
@Override
97+
public boolean isThreadDumpOnTimeoutEnabled() {
98+
return configurationParameters.getBoolean(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME).orElse(false);
99+
}
100+
96101
@Override
97102
public ExecutionMode getDefaultExecutionMode() {
98103
return executionModeConverter.get(configurationParameters, DEFAULT_EXECUTION_MODE_PROPERTY_NAME,

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java

+4
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import org.junit.jupiter.api.MethodOrderer;
2424
import org.junit.jupiter.api.TestInstance;
2525
import org.junit.jupiter.api.extension.ExecutionCondition;
26+
import org.junit.jupiter.api.extension.PreInterruptCallback;
2627
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
2728
import org.junit.jupiter.api.io.CleanupMode;
2829
import org.junit.jupiter.api.io.TempDirFactory;
@@ -40,6 +41,7 @@ public interface JupiterConfiguration {
4041
String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME;
4142
String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME;
4243
String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";
44+
String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = PreInterruptCallback.THREAD_DUMP_ENABLED_PROPERTY_NAME;
4345
String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = TestInstance.Lifecycle.DEFAULT_LIFECYCLE_PROPERTY_NAME;
4446
String DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME = DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME;
4547
String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = MethodOrderer.DEFAULT_ORDER_PROPERTY_NAME;
@@ -54,6 +56,8 @@ public interface JupiterConfiguration {
5456

5557
boolean isExtensionAutoDetectionEnabled();
5658

59+
boolean isThreadDumpOnTimeoutEnabled();
60+
5761
ExecutionMode getDefaultExecutionMode();
5862

5963
ExecutionMode getDefaultClassesExecutionMode();

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java

+16-5
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,22 @@
1515

1616
import java.util.Collections;
1717
import java.util.LinkedHashSet;
18+
import java.util.List;
1819
import java.util.Map;
1920
import java.util.Optional;
2021
import java.util.Set;
2122
import java.util.function.Function;
2223

2324
import org.junit.jupiter.api.extension.ExecutableInvoker;
25+
import org.junit.jupiter.api.extension.Extension;
2426
import org.junit.jupiter.api.extension.ExtensionContext;
2527
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
2628
import org.junit.jupiter.api.parallel.ExecutionMode;
2729
import org.junit.jupiter.engine.config.JupiterConfiguration;
30+
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
2831
import org.junit.jupiter.engine.execution.NamespaceAwareStore;
32+
import org.junit.jupiter.engine.extension.ExtensionContextInternal;
33+
import org.junit.jupiter.engine.extension.ExtensionRegistry;
2934
import org.junit.platform.commons.JUnitException;
3035
import org.junit.platform.commons.util.Preconditions;
3136
import org.junit.platform.engine.EngineExecutionListener;
@@ -38,7 +43,7 @@
3843
/**
3944
* @since 5.0
4045
*/
41-
abstract class AbstractExtensionContext<T extends TestDescriptor> implements ExtensionContext, AutoCloseable {
46+
abstract class AbstractExtensionContext<T extends TestDescriptor> implements ExtensionContextInternal, AutoCloseable {
4247

4348
private static final NamespacedHierarchicalStore.CloseAction<Namespace> CLOSE_RESOURCES = (__, ___, value) -> {
4449
if (value instanceof CloseableResource) {
@@ -53,20 +58,21 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
5358
private final JupiterConfiguration configuration;
5459
private final NamespacedHierarchicalStore<Namespace> valuesStore;
5560
private final ExecutableInvoker executableInvoker;
61+
private final ExtensionRegistry extensionRegistry;
5662

5763
AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor,
58-
JupiterConfiguration configuration,
59-
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
60-
this.executableInvoker = executableInvokerFactory.apply(this);
64+
JupiterConfiguration configuration, ExtensionRegistry extensionRegistry) {
6165

6266
Preconditions.notNull(testDescriptor, "TestDescriptor must not be null");
6367
Preconditions.notNull(configuration, "JupiterConfiguration must not be null");
64-
68+
Preconditions.notNull(extensionRegistry, "ExtensionRegistry must not be null");
69+
this.executableInvoker = new DefaultExecutableInvoker(this, extensionRegistry);
6570
this.parent = parent;
6671
this.engineExecutionListener = engineExecutionListener;
6772
this.testDescriptor = testDescriptor;
6873
this.configuration = configuration;
6974
this.valuesStore = createStore(parent);
75+
this.extensionRegistry = extensionRegistry;
7076

7177
// @formatter:off
7278
this.tags = testDescriptor.getTags().stream()
@@ -152,6 +158,11 @@ public ExecutableInvoker getExecutableInvoker() {
152158
return executableInvoker;
153159
}
154160

161+
@Override
162+
public <E extends Extension> List<E> getExtensions(Class<E> extensionType) {
163+
return extensionRegistry.getExtensions(extensionType);
164+
}
165+
155166
protected abstract Node.ExecutionMode getPlatformExecutionMode();
156167

157168
private ExecutionMode toJupiterExecutionMode(Node.ExecutionMode mode) {

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java

+2-3
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@
5555
import org.junit.jupiter.engine.config.JupiterConfiguration;
5656
import org.junit.jupiter.engine.execution.AfterEachMethodAdapter;
5757
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
58-
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
5958
import org.junit.jupiter.engine.execution.DefaultTestInstances;
6059
import org.junit.jupiter.engine.execution.ExtensionContextSupplier;
6160
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker;
@@ -181,8 +180,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
181180

182181
ThrowableCollector throwableCollector = createThrowableCollector();
183182
ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(),
184-
context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), throwableCollector,
185-
it -> new DefaultExecutableInvoker(it, registry));
183+
context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), registry,
184+
throwableCollector);
186185

187186
// @formatter:off
188187
return context.extend()

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java

+7-10
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,12 @@
1313
import java.lang.reflect.AnnotatedElement;
1414
import java.lang.reflect.Method;
1515
import java.util.Optional;
16-
import java.util.function.Function;
1716

1817
import org.junit.jupiter.api.TestInstance.Lifecycle;
19-
import org.junit.jupiter.api.extension.ExecutableInvoker;
2018
import org.junit.jupiter.api.extension.ExtensionContext;
2119
import org.junit.jupiter.api.extension.TestInstances;
2220
import org.junit.jupiter.engine.config.JupiterConfiguration;
21+
import org.junit.jupiter.engine.extension.ExtensionRegistry;
2322
import org.junit.platform.engine.EngineExecutionListener;
2423
import org.junit.platform.engine.support.hierarchical.Node;
2524
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
@@ -39,23 +38,21 @@ final class ClassExtensionContext extends AbstractExtensionContext<ClassBasedTes
3938
* Create a new {@code ClassExtensionContext} with {@link Lifecycle#PER_METHOD}.
4039
*
4140
* @see #ClassExtensionContext(ExtensionContext, EngineExecutionListener, ClassBasedTestDescriptor,
42-
* Lifecycle, JupiterConfiguration, ThrowableCollector, Function)
41+
* Lifecycle, JupiterConfiguration, ExtensionRegistry, ThrowableCollector)
4342
*/
4443
ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
4544
ClassBasedTestDescriptor testDescriptor, JupiterConfiguration configuration,
46-
ThrowableCollector throwableCollector,
47-
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
45+
ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) {
4846

49-
this(parent, engineExecutionListener, testDescriptor, Lifecycle.PER_METHOD, configuration, throwableCollector,
50-
executableInvokerFactory);
47+
this(parent, engineExecutionListener, testDescriptor, Lifecycle.PER_METHOD, configuration, extensionRegistry,
48+
throwableCollector);
5149
}
5250

5351
ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
5452
ClassBasedTestDescriptor testDescriptor, Lifecycle lifecycle, JupiterConfiguration configuration,
55-
ThrowableCollector throwableCollector,
56-
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
53+
ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) {
5754

58-
super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory);
55+
super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
5956

6057
this.lifecycle = lifecycle;
6158
this.throwableCollector = throwableCollector;

0 commit comments

Comments
 (0)