Skip to content

Commit 226058c

Browse files
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.execution.timeout.threaddump.enabled = true Issue: #2938 Co-authored-by: Marc Philipp <mail@marcphilipp.de>
1 parent 906a739 commit 226058c

File tree

42 files changed

+752
-86
lines changed

Some content is hidden

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

42 files changed

+752
-86
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
@@ -83,6 +83,7 @@ JUnit repository on GitHub.
8383
a test-scoped `ExtensionContext` in `Extension` methods called during test class
8484
instantiation. This behavior will become the default in future versions of JUnit.
8585
* `@TempDir` is now supported on test class constructors.
86+
* Added `PreInterruptCallback`
8687

8788

8889
[[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
}

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: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
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+
* @since 5.12
54+
* @see PreInterruptContext
55+
*/
56+
@API(status = EXPERIMENTAL, since = "5.12")
57+
void beforeThreadInterrupt(PreInterruptContext preInterruptContext) throws Exception;
58+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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) 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+
36+
/**
37+
* Get the current {@link ExtensionContext}.
38+
*
39+
* @return the current extension context; never {@code null}
40+
* @since 5.12
41+
*/
42+
@API(status = EXPERIMENTAL, since = "5.12")
43+
ExtensionContext getExtensionContext();
44+
}

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 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,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 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

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 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

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,21 @@
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.ExtensionRegistry;
2933
import org.junit.platform.commons.JUnitException;
3034
import org.junit.platform.commons.util.Preconditions;
3135
import org.junit.platform.engine.EngineExecutionListener;
@@ -53,20 +57,21 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
5357
private final JupiterConfiguration configuration;
5458
private final NamespacedHierarchicalStore<Namespace> valuesStore;
5559
private final ExecutableInvoker executableInvoker;
60+
private final ExtensionRegistry extensionRegistry;
5661

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

6265
Preconditions.notNull(testDescriptor, "TestDescriptor must not be null");
6366
Preconditions.notNull(configuration, "JupiterConfiguration must not be null");
64-
67+
Preconditions.notNull(extensionRegistry, "ExtensionRegistry must not be null");
68+
this.executableInvoker = new DefaultExecutableInvoker(this, extensionRegistry);
6569
this.parent = parent;
6670
this.engineExecutionListener = engineExecutionListener;
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,11 @@ public ExecutableInvoker getExecutableInvoker() {
152157
return executableInvoker;
153158
}
154159

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

157167
private ExecutionMode toJupiterExecutionMode(Node.ExecutionMode mode) {

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

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

0 commit comments

Comments
 (0)