Skip to content

Commit 0c2a0ed

Browse files
committed
PreInterruptCallback extension
Added PreInterruptCallback extension to allow to hook into the @timeout extension before the executing Thread is interrupted. The default implementation of PreInterruptCallback will simply print the stacks of all Thread to System.out. It is disabled by default and must be enabled with: junit.jupiter.extensions.preinterruptcallback.default.enabled = true Issue: #2938
1 parent 9a9063d commit 0c2a0ed

20 files changed

+472
-14
lines changed

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ JUnit repository on GitHub.
3939

4040
==== New Features and Improvements
4141

42-
*
42+
* Added `PreInterruptCallback`
4343

4444

4545
[[release-notes-5.11.0-M1-junit-vintage]]

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,21 @@ test methods.
599599
include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide]
600600
----
601601

602+
[[extensions-preinterrupt-callback]]
603+
=== PreInterrupt Callback
604+
605+
`{PreInterruptCallback}` defines the API for `Extensions` that wish to react on
606+
`Thread.interrupt()` calls issued by Jupiter before the `Thread.interrupt()` is executed.
607+
608+
This can be used to dump stacks for diagnostics, when the `Timeout` extension
609+
interrupts tests.
610+
611+
There is also a default implementation available, which will dump the stacks of all
612+
`Threads` to `System.out`.
613+
This default implementation need to be enabled with the
614+
<<running-tests-config-params,configuration parameter>>:
615+
`junit.jupiter.extensions.preinterruptcallback.default.enabled`
616+
602617
[[extensions-intercepting-invocations]]
603618
=== Intercepting Invocations
604619

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2244,6 +2244,12 @@ NOTE: If you need more control over polling intervals and greater flexibility wi
22442244
asynchronous tests, consider using a dedicated library such as
22452245
link:https://github.com/awaitility/awaitility[Awaitility].
22462246

2247+
[[writing-tests-dump-stack-timeout]]
2248+
=== Dump Stacks on Timeout
2249+
2250+
It can be helpful for debugging to dump the stacks of all Threads, when a Timeout happened.
2251+
The <<extensions-preinterrupt-callback, PreInterruptCallback>> provides a default
2252+
implementation for that.
22472253

22482254
[[writing-tests-declarative-timeouts-mode]]
22492255
==== Disable @Timeout Globally
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2015-2023 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 react on {@link Thread#interrupt()} calls issued by Jupiter
20+
* before the {@link Thread#interrupt()} is executed.
21+
*
22+
* <p>This can be used to e.g. dump stacks for diagnostics, when the {@link org.junit.jupiter.api.Timeout}
23+
* extension is used.</p>
24+
*
25+
* <p>There is also a default implementation available, which will dump the stacks of all {@link Thread Threads}
26+
* to {@code System.out}. This default implementation need to be enabled with the jupiter property:
27+
* {@code junit.jupiter.extensions.preinterruptcallback.default.enabled}
28+
*
29+
*
30+
* @since 5.11
31+
* @see org.junit.jupiter.api.Timeout
32+
*/
33+
@API(status = EXPERIMENTAL, since = "5.11")
34+
public interface PreInterruptCallback extends Extension {
35+
36+
/**
37+
* Callback that is invoked <em>before</em> a {@link Thread} is interrupted with {@link Thread#interrupt()}.
38+
*
39+
* <p>Caution: There is no guarantee on which {@link Thread} this callback will be executed.</p>
40+
*
41+
* @param threadToInterrupt the target {@link Thread}, which will get interrupted.
42+
* @param context the current extension context; never {@code null}
43+
*/
44+
void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) throws Exception;
45+
}

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,13 @@ public final class Constants {
107107
*/
108108
public static final String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME;
109109

110+
/**
111+
* Property name used to enable the default {@link org.junit.jupiter.api.extension.PreInterruptCallback} extension.
112+
*
113+
* <p>The default behavior is not to enable the pre interrupt callback.
114+
*/
115+
public static final String EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME;
116+
110117
/**
111118
* Property name used to set the default test instance lifecycle mode: {@value}
112119
*

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
@@ -67,6 +67,12 @@ public boolean isExtensionAutoDetectionEnabled() {
6767
key -> delegate.isExtensionAutoDetectionEnabled());
6868
}
6969

70+
@Override
71+
public boolean isExtensionDefaultPreInterruptCallbackEnabled() {
72+
return (boolean) cache.computeIfAbsent(EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME,
73+
key -> delegate.isExtensionDefaultPreInterruptCallbackEnabled());
74+
}
75+
7076
@Override
7177
public ExecutionMode getDefaultExecutionMode() {
7278
return (ExecutionMode) cache.computeIfAbsent(DEFAULT_EXECUTION_MODE_PROPERTY_NAME,

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,12 @@ public boolean isExtensionAutoDetectionEnabled() {
8989
return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false);
9090
}
9191

92+
@Override
93+
public boolean isExtensionDefaultPreInterruptCallbackEnabled() {
94+
return configurationParameters.getBoolean(EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME)//
95+
.orElse(false);
96+
}
97+
9298
@Override
9399
public ExecutionMode getDefaultExecutionMode() {
94100
return executionModeConverter.get(configurationParameters, DEFAULT_EXECUTION_MODE_PROPERTY_NAME,

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ public interface JupiterConfiguration {
3939
String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME;
4040
String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME;
4141
String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";
42+
String EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.preinterruptcallback.default.enabled";
4243
String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = TestInstance.Lifecycle.DEFAULT_LIFECYCLE_PROPERTY_NAME;
4344
String DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME = DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME;
4445
String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = MethodOrderer.DEFAULT_ORDER_PROPERTY_NAME;
@@ -52,6 +53,8 @@ public interface JupiterConfiguration {
5253

5354
boolean isExtensionAutoDetectionEnabled();
5455

56+
boolean isExtensionDefaultPreInterruptCallbackEnabled();
57+
5558
ExecutionMode getDefaultExecutionMode();
5659

5760
ExecutionMode getDefaultClassesExecutionMode();

junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,12 +177,19 @@ public Builder withThrowableCollector(ThrowableCollector throwableCollector) {
177177

178178
public JupiterEngineExecutionContext build() {
179179
if (newState != null) {
180+
storeExtensionRegistryInExtensionContext();
180181
originalState = newState;
181182
newState = null;
182183
}
183184
return new JupiterEngineExecutionContext(originalState);
184185
}
185186

187+
private void storeExtensionRegistryInExtensionContext() {
188+
if (newState.extensionRegistry != null && newState.extensionContext != null) {
189+
newState.extensionRegistry.storeInExtensionContext(newState.extensionContext);
190+
}
191+
}
192+
186193
private State newState() {
187194
if (newState == null) {
188195
this.newState = originalState.clone();
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
/*
2+
* Copyright 2015-2023 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.engine.extension;
12+
13+
import java.util.Map;
14+
15+
import org.junit.jupiter.api.extension.ExtensionContext;
16+
import org.junit.jupiter.api.extension.PreInterruptCallback;
17+
import org.junit.jupiter.engine.Constants;
18+
19+
/**
20+
* The default implementation for {@link PreInterruptCallback},
21+
* which will print the stacks of all {@link Thread}s to {@code System.out}.
22+
*
23+
* <p>Note: This is disabled by default, and must be enabled with
24+
* {@link Constants#EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME}
25+
*
26+
* @since 5.11
27+
*/
28+
public class DefaultPreInterruptCallback implements PreInterruptCallback {
29+
private static final String NL = "\n";
30+
31+
@Override
32+
public void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) {
33+
Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
34+
StringBuilder sb = new StringBuilder();
35+
sb.append("Thread ");
36+
appendThreadName(sb, threadToInterrupt);
37+
sb.append(" will be interrupted.");
38+
sb.append(NL);
39+
for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet()) {
40+
Thread thread = entry.getKey();
41+
StackTraceElement[] stack = entry.getValue();
42+
if (stack.length > 0) {
43+
sb.append(NL);
44+
appendThreadName(sb, thread);
45+
for (StackTraceElement stackTraceElement : stack) {
46+
sb.append(NL);
47+
//Do the same prefix as java.lang.Throwable.printStackTrace(java.lang.Throwable.PrintStreamOrWriter)
48+
sb.append("\tat ");
49+
sb.append(stackTraceElement.toString());
50+
51+
}
52+
sb.append(NL);
53+
}
54+
}
55+
System.out.println(sb);
56+
}
57+
58+
/**
59+
* Appends the {@link Thread} name and ID in a similar fashion as {@code jstack}.
60+
* @param sb the buffer
61+
* @param th the thread to append
62+
*/
63+
private void appendThreadName(StringBuilder sb, Thread th) {
64+
sb.append("\"");
65+
sb.append(th.getName());
66+
sb.append("\"");
67+
sb.append(" #");
68+
sb.append(th.getId());
69+
if (th.isDaemon()) {
70+
sb.append(" daemon");
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)