Skip to content

Commit bcc6fa4

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.err. It is disabled by default and must be enabled with: junit.jupiter.execution.timeout.threaddump.enabled = true Issue: #2938
1 parent 72d643c commit bcc6fa4

35 files changed

+573
-51
lines changed

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
@@ -79,6 +79,7 @@ JUnit repository on GitHub.
7979
a test-scoped `ExtensionContext` in `Extension` methods called during test class
8080
instantiation. This behavior will become the default in future versions of JUnit.
8181
* `@TempDir` is now supported on test class constructors.
82+
* Added `PreInterruptCallback`
8283

8384

8485
[[release-notes-5.12.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
@@ -715,6 +715,21 @@ test methods.
715715
include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide]
716716
----
717717

718+
[[extensions-preinterrupt-callback]]
719+
=== PreInterrupt Callback
720+
721+
`{PreInterruptCallback}` defines the API for `Extensions` that wish to react on
722+
`Thread.interrupt()` calls issued by Jupiter before the `Thread.interrupt()` is executed.
723+
724+
This can be used to dump stacks for diagnostics, when the `Timeout` extension
725+
interrupts tests.
726+
727+
There is also a default implementation available, which will dump the stacks of all
728+
`Threads` to `System.err`.
729+
This default implementation need to be enabled with the
730+
<<running-tests-config-params,configuration parameter>>:
731+
`junit.jupiter.execution.timeout.threaddump.enabled`
732+
718733
[[extensions-intercepting-invocations]]
719734
=== Intercepting Invocations
720735

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

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

2661+
[[writing-tests-dump-stack-timeout]]
2662+
=== Dump Stacks on Timeout
2663+
2664+
It can be helpful for debugging to dump the stacks of all Threads, when a Timeout happened.
2665+
The <<extensions-preinterrupt-callback, PreInterruptCallback>> provides a default
2666+
implementation for that.
26612667

26622668
[[writing-tests-declarative-timeouts-mode]]
26632669
==== Disable @Timeout Globally

junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
package org.junit.jupiter.api.extension;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.STABLE;
1415

1516
import java.lang.reflect.AnnotatedElement;
@@ -401,6 +402,17 @@ default void publishReportEntry(String value) {
401402
@API(status = STABLE, since = "5.11")
402403
ExecutableInvoker getExecutableInvoker();
403404

405+
/**
406+
* Returns a list of registered extension at this context of the passed {@code extensionType}.
407+
*
408+
* @param <E> the extension type
409+
* @param extensionType the extension type
410+
* @return the list of extensions
411+
* @since 5.12
412+
*/
413+
@API(status = EXPERIMENTAL, since = "5.12")
414+
<E extends Extension> List<E> getExtensions(Class<E> extensionType);
415+
404416
/**
405417
* {@code Store} provides methods for extensions to save and retrieve data.
406418
*/
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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 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.err}. This default implementation need to be enabled with the jupiter property:
27+
* {@code junit.jupiter.execution.timeout.threaddump.enabled}
28+
*
29+
*
30+
* @since 5.12
31+
* @see org.junit.jupiter.api.Timeout
32+
*/
33+
@API(status = EXPERIMENTAL, since = "5.12")
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: 11 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 the default behavior of {@link org.junit.jupiter.api.extension.PreInterruptCallback}
113+
* extension to print the stacks of all {@link Thread}s to {@code System.err} before the test is interrupted.
114+
*
115+
* <p>The default behavior is not to enable the dump fo threads.
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,6 @@ 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-
196206
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
197207
*
198208
* <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 isExtensionTimeoutThreadDumpEnabled() {
73+
return (boolean) cache.computeIfAbsent(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME,
74+
__ -> delegate.isExtensionTimeoutThreadDumpEnabled());
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

Lines changed: 5 additions & 0 deletions
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 isExtensionTimeoutThreadDumpEnabled() {
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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ public interface JupiterConfiguration {
4040
String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME;
4141
String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME;
4242
String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";
43+
String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.timeout.threaddump.enabled";
4344
String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = TestInstance.Lifecycle.DEFAULT_LIFECYCLE_PROPERTY_NAME;
4445
String DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME = DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME;
4546
String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = MethodOrderer.DEFAULT_ORDER_PROPERTY_NAME;
@@ -54,6 +55,8 @@ public interface JupiterConfiguration {
5455

5556
boolean isExtensionAutoDetectionEnabled();
5657

58+
boolean isExtensionTimeoutThreadDumpEnabled();
59+
5760
ExecutionMode getDefaultExecutionMode();
5861

5962
ExecutionMode getDefaultClassesExecutionMode();

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,20 @@
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;
2830
import org.junit.jupiter.engine.execution.NamespaceAwareStore;
31+
import org.junit.jupiter.engine.extension.ExtensionRegistry;
2932
import org.junit.platform.commons.JUnitException;
3033
import org.junit.platform.commons.util.Preconditions;
3134
import org.junit.platform.engine.EngineExecutionListener;
@@ -53,9 +56,10 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
5356
private final JupiterConfiguration configuration;
5457
private final NamespacedHierarchicalStore<Namespace> valuesStore;
5558
private final ExecutableInvoker executableInvoker;
59+
private final ExtensionRegistry extensionRegistry;
5660

5761
AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor,
58-
JupiterConfiguration configuration,
62+
JupiterConfiguration configuration, ExtensionRegistry extensionRegistry,
5963
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
6064
this.executableInvoker = executableInvokerFactory.apply(this);
6165

@@ -67,6 +71,7 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
6771
this.testDescriptor = testDescriptor;
6872
this.configuration = configuration;
6973
this.valuesStore = createStore(parent);
74+
this.extensionRegistry = extensionRegistry;
7075

7176
// @formatter:off
7277
this.tags = testDescriptor.getTags().stream()
@@ -152,6 +157,14 @@ public ExecutableInvoker getExecutableInvoker() {
152157
return executableInvoker;
153158
}
154159

160+
@Override
161+
public <E extends Extension> List<E> getExtensions(Class<E> extensionType) {
162+
if (extensionRegistry == null) {
163+
return Collections.emptyList();
164+
}
165+
return extensionRegistry.getExtensions(extensionType);
166+
}
167+
155168
protected abstract Node.ExecutionMode getPlatformExecutionMode();
156169

157170
private ExecutionMode toJupiterExecutionMode(Node.ExecutionMode mode) {

0 commit comments

Comments
 (0)