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 <mail@marcphilipp.de>
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
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

Lines changed: 9 additions & 0 deletions
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

Lines changed: 16 additions & 0 deletions
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

Lines changed: 2 additions & 2 deletions
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

Lines changed: 1 addition & 1 deletion
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
}
Lines changed: 60 additions & 0 deletions
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+
}
Lines changed: 35 additions & 0 deletions
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

Lines changed: 12 additions & 1 deletion
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

Lines changed: 6 additions & 0 deletions
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,

0 commit comments

Comments
 (0)