Skip to content

Commit

Permalink
PreInterruptCallback extension
Browse files Browse the repository at this point in the history
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: junit-team#2938

Co-authored-by: Marc Philipp <[email protected]>
  • Loading branch information
AndreasTu and marcphilipp committed Oct 21, 2024
1 parent 906a739 commit c9ee729
Show file tree
Hide file tree
Showing 42 changed files with 761 additions and 86 deletions.
1 change: 1 addition & 0 deletions documentation/src/docs/asciidoc/link-attributes.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ endif::[]
:TestTemplateInvocationContext: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContext.html[TestTemplateInvocationContext]
:TestTemplateInvocationContextProvider: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestTemplateInvocationContextProvider.html[TestTemplateInvocationContextProvider]
:TestWatcher: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/TestWatcher.html[TestWatcher]
:PreInterruptCallback: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/extension/PreInterruptCallback.html[PreInterruptCallback]
// Jupiter Conditions
:DisabledForJreRange: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledForJreRange.html[@DisabledForJreRange]
:DisabledIf: {javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/DisabledIf.html[@DisabledIf]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ JUnit repository on GitHub.
a test-scoped `ExtensionContext` in `Extension` methods called during test class
instantiation. This behavior will become the default in future versions of JUnit.
* `@TempDir` is now supported on test class constructors.
* Added `PreInterruptCallback`


[[release-notes-5.12.0-M1-junit-vintage]]
Expand Down
9 changes: 9 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -715,6 +715,15 @@ test methods.
include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide]
----

[[extensions-preinterrupt-callback]]
=== Pre-Interrupt Callback

`{PreInterruptCallback}` defines the API for `Extensions` that wish to react on
timeouts before the `Thread.interrupt()` is called.

Please refer to <<writing-tests-declarative-timeouts-debugging>> for additional information.


[[extensions-intercepting-invocations]]
=== Intercepting Invocations

Expand Down
16 changes: 16 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2659,6 +2659,22 @@ asynchronous tests, consider using a dedicated library such as
link:https://github.com/awaitility/awaitility[Awaitility].


[[writing-tests-declarative-timeouts-debugging]]
=== Debugging Timeouts

Registered <<extensions-preinterrupt-callback>> extensions are called prior to invoking
`Thread.interrupt()` on the thread that is executing the timed out method. This allows to
inspect the application state and output additional information that might be helpful for
diagnosing the cause of a timeout.


[[writing-tests-declarative-timeouts-debugging-thread-dump]]
==== Thread Dump on Timeout
JUnit registers a default implementation of the <<extensions-preinterrupt-callback>> extension point that
dumps the stacks of all threads to `System.out` if enabled by setting the
`junit.jupiter.execution.timeout.threaddump.enabled` configuration parameter to `true`.


[[writing-tests-declarative-timeouts-mode]]
==== Disable @Timeout Globally
When stepping through your code in a debug session, a fixed timeout limit may influence
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ private static <T, E extends Throwable> T resolveFutureAndHandleException(Future
cause = new ExecutionTimeoutException("Execution timed out in thread " + thread.getName());
cause.setStackTrace(thread.getStackTrace());
}
throw failureFactory.createTimeoutFailure(timeout, messageSupplier, cause);
throw failureFactory.createTimeoutFailure(timeout, messageSupplier, cause, thread);
}
catch (ExecutionException ex) {
throw throwAsUncheckedException(ex.getCause());
Expand All @@ -124,7 +124,7 @@ private static <T, E extends Throwable> T resolveFutureAndHandleException(Future
}

private static AssertionFailedError createAssertionFailure(Duration timeout, Supplier<String> messageSupplier,
Throwable cause) {
Throwable cause, Thread thread) {
return assertionFailure() //
.message(messageSupplier) //
.reason("execution timed out after " + timeout.toMillis() + " ms") //
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3662,6 +3662,6 @@ public interface TimeoutFailureFactory<T extends Throwable> {
*
* @return timeout failure; never {@code null}
*/
T createTimeoutFailure(Duration timeout, Supplier<String> messageSupplier, Throwable cause);
T createTimeoutFailure(Duration timeout, Supplier<String> messageSupplier, Throwable cause, Thread testThread);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;
import static org.apiguardian.api.API.Status.STABLE;

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

/**
* Returns a list of registered extension at this context of the passed {@code extensionType}.
*
* @param <E> the extension type
* @param extensionType the extension type
* @return the list of extensions
* @since 5.12
*/
@API(status = EXPERIMENTAL, since = "5.12")
<E extends Extension> List<E> getExtensions(Class<E> extensionType);

/**
* {@code Store} provides methods for extensions to save and retrieve data.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import org.apiguardian.api.API;

/**
* {@code PreInterruptCallback} defines the API for {@link Extension
* Extensions} that wish to be called prior to invocations of
* {@link Thread#interrupt()} by the {@link org.junit.jupiter.api.Timeout}
* extension.
*
* <p>JUnit registers a default implementation that dumps the stacks of all
* {@linkplain Thread threads} to {@code System.out} if the
* {@value #THREAD_DUMP_ENABLED_PROPERTY_NAME} configuration parameter is set to
* {@code true}.
*
* @since 5.12
* @see org.junit.jupiter.api.Timeout
*/
@API(status = EXPERIMENTAL, since = "5.12")
public interface PreInterruptCallback extends Extension {

/**
* Property name used to enable dumping the stack of all
* {@linkplain Thread threads} to {@code System.out} when a timeout has occurred.
*
* <p>This behavior is disabled by default.
*
* @since 5.12
*/
@API(status = EXPERIMENTAL, since = "5.12")
String THREAD_DUMP_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.timeout.threaddump.enabled";

/**
* Callback that is invoked <em>before</em> a {@link Thread} is interrupted with
* {@link Thread#interrupt()}.
*
* <p>Note: There is no guarantee on which {@link Thread} this callback will be
* executed.
*
* @param preInterruptContext the context with the target {@link Thread}, which will get interrupted.
* @since 5.12
* @see PreInterruptContext
*/
@API(status = EXPERIMENTAL, since = "5.12")
void beforeThreadInterrupt(PreInterruptContext preInterruptContext) throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
* Copyright 2015-2024 the original author or authors.
*
* All rights reserved. This program and the accompanying materials are
* made available under the terms of the Eclipse Public License v2.0 which
* accompanies this distribution and is available at
*
* https://www.eclipse.org/legal/epl-v20.html
*/

package org.junit.jupiter.api.extension;

import static org.apiguardian.api.API.Status.EXPERIMENTAL;

import org.apiguardian.api.API;

/**
* {@code PreInterruptContext} encapsulates the <em>context</em> in which an
* {@link PreInterruptCallback#beforeThreadInterrupt(PreInterruptContext) beforeThreadInterrupt} method is called.
*
* @since 5.12
* @see PreInterruptCallback
*/
@API(status = EXPERIMENTAL, since = "5.12")
public interface PreInterruptContext {

/**
* Get the {@link Thread} which will be interrupted.
*
* @return the Thread; never {@code null}
* @since 5.12
*/
@API(status = EXPERIMENTAL, since = "5.12")
Thread getThreadToInterrupt();

/**
* Get the current {@link ExtensionContext}.
*
* @return the current extension context; never {@code null}
* @since 5.12
*/
@API(status = EXPERIMENTAL, since = "5.12")
ExtensionContext getExtensionContext();
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,17 @@ public final class Constants {
*/
public static final String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME;

/**
* Property name used to enable dumping the stack of all
* {@linkplain Thread threads} to {@code System.out} when a timeout has occurred.
*
* <p>This behavior is disabled by default.
*
* @since 5.12
*/
@API(status = EXPERIMENTAL, since = "5.12")
public static final String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME;

/**
* Property name used to set the default test instance lifecycle mode: {@value}
*
Expand Down Expand Up @@ -192,7 +203,6 @@ public final class Constants {
* <p>When set to {@code false} the underlying fork-join pool will reject
* additional tasks if all available workers are busy and the maximum
* pool-size would be exceeded.
* <p>Value must either {@code true} or {@code false}; defaults to {@code true}.
*
* <p>Note: This property only takes affect on Java 9+.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ public boolean isExtensionAutoDetectionEnabled() {
__ -> delegate.isExtensionAutoDetectionEnabled());
}

@Override
public boolean isThreadDumpOnTimeoutEnabled() {
return (boolean) cache.computeIfAbsent(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME,
__ -> delegate.isThreadDumpOnTimeoutEnabled());
}

@Override
public ExecutionMode getDefaultExecutionMode() {
return (ExecutionMode) cache.computeIfAbsent(DEFAULT_EXECUTION_MODE_PROPERTY_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,11 @@ public boolean isExtensionAutoDetectionEnabled() {
return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false);
}

@Override
public boolean isThreadDumpOnTimeoutEnabled() {
return configurationParameters.getBoolean(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME).orElse(false);
}

@Override
public ExecutionMode getDefaultExecutionMode() {
return executionModeConverter.get(configurationParameters, DEFAULT_EXECUTION_MODE_PROPERTY_NAME,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.TestInstance;
import org.junit.jupiter.api.extension.ExecutionCondition;
import org.junit.jupiter.api.extension.PreInterruptCallback;
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
import org.junit.jupiter.api.io.CleanupMode;
import org.junit.jupiter.api.io.TempDirFactory;
Expand All @@ -40,6 +41,7 @@ public interface JupiterConfiguration {
String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME;
String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME;
String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled";
String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = PreInterruptCallback.THREAD_DUMP_ENABLED_PROPERTY_NAME;
String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = TestInstance.Lifecycle.DEFAULT_LIFECYCLE_PROPERTY_NAME;
String DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME = DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME;
String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = MethodOrderer.DEFAULT_ORDER_PROPERTY_NAME;
Expand All @@ -54,6 +56,8 @@ public interface JupiterConfiguration {

boolean isExtensionAutoDetectionEnabled();

boolean isThreadDumpOnTimeoutEnabled();

ExecutionMode getDefaultExecutionMode();

ExecutionMode getDefaultClassesExecutionMode();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,21 @@

import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;

import org.junit.jupiter.api.extension.ExecutableInvoker;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource;
import org.junit.jupiter.api.parallel.ExecutionMode;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
import org.junit.jupiter.engine.execution.NamespaceAwareStore;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.commons.JUnitException;
import org.junit.platform.commons.util.Preconditions;
import org.junit.platform.engine.EngineExecutionListener;
Expand Down Expand Up @@ -53,20 +57,21 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
private final JupiterConfiguration configuration;
private final NamespacedHierarchicalStore<Namespace> valuesStore;
private final ExecutableInvoker executableInvoker;
private final ExtensionRegistry extensionRegistry;

AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor,
JupiterConfiguration configuration,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
this.executableInvoker = executableInvokerFactory.apply(this);
JupiterConfiguration configuration, ExtensionRegistry extensionRegistry) {

Preconditions.notNull(testDescriptor, "TestDescriptor must not be null");
Preconditions.notNull(configuration, "JupiterConfiguration must not be null");

Preconditions.notNull(extensionRegistry, "ExtensionRegistry must not be null");
this.executableInvoker = new DefaultExecutableInvoker(this, extensionRegistry);
this.parent = parent;
this.engineExecutionListener = engineExecutionListener;
this.testDescriptor = testDescriptor;
this.configuration = configuration;
this.valuesStore = createStore(parent);
this.extensionRegistry = extensionRegistry;

// @formatter:off
this.tags = testDescriptor.getTags().stream()
Expand Down Expand Up @@ -152,6 +157,11 @@ public ExecutableInvoker getExecutableInvoker() {
return executableInvoker;
}

@Override
public <E extends Extension> List<E> getExtensions(Class<E> extensionType) {
return extensionRegistry.getExtensions(extensionType);
}

protected abstract Node.ExecutionMode getPlatformExecutionMode();

private ExecutionMode toJupiterExecutionMode(Node.ExecutionMode mode) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.AfterEachMethodAdapter;
import org.junit.jupiter.engine.execution.BeforeEachMethodAdapter;
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
import org.junit.jupiter.engine.execution.DefaultTestInstances;
import org.junit.jupiter.engine.execution.ExtensionContextSupplier;
import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker;
Expand Down Expand Up @@ -181,8 +180,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte

ThrowableCollector throwableCollector = createThrowableCollector();
ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(),
context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), throwableCollector,
it -> new DefaultExecutableInvoker(it, registry));
context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), registry,
throwableCollector);

// @formatter:off
return context.extend()
Expand Down
Loading

0 comments on commit c9ee729

Please sign in to comment.