Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce PreInterruptCallback extension point #3431

Merged
merged 3 commits into from
Oct 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@ -87,6 +87,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
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -0,0 +1,60 @@
/*
* 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.
*
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
* <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.
* @param extensionContext the extension context for the callback; never {@code null}
* @since 5.12
* @see PreInterruptContext
*/
@API(status = EXPERIMENTAL, since = "5.12")
void beforeThreadInterrupt(PreInterruptContext preInterruptContext, ExtensionContext extensionContext)
throws Exception;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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, ExtensionContext) 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();
}
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
*/
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
@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,7 @@ 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}.
AndreasTu marked this conversation as resolved.
Show resolved Hide resolved
*
* <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,22 @@

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.ExtensionContextInternal;
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 All @@ -38,7 +43,7 @@
/**
* @since 5.0
*/
abstract class AbstractExtensionContext<T extends TestDescriptor> implements ExtensionContext, AutoCloseable {
abstract class AbstractExtensionContext<T extends TestDescriptor> implements ExtensionContextInternal, AutoCloseable {

private static final NamespacedHierarchicalStore.CloseAction<Namespace> CLOSE_RESOURCES = (__, ___, value) -> {
if (value instanceof CloseableResource) {
Expand All @@ -53,20 +58,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 +158,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
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,12 @@
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.Optional;
import java.util.function.Function;

import org.junit.jupiter.api.TestInstance.Lifecycle;
import org.junit.jupiter.api.extension.ExecutableInvoker;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.TestInstances;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.extension.ExtensionRegistry;
import org.junit.platform.engine.EngineExecutionListener;
import org.junit.platform.engine.support.hierarchical.Node;
import org.junit.platform.engine.support.hierarchical.ThrowableCollector;
Expand All @@ -39,23 +38,21 @@ final class ClassExtensionContext extends AbstractExtensionContext<ClassBasedTes
* Create a new {@code ClassExtensionContext} with {@link Lifecycle#PER_METHOD}.
*
* @see #ClassExtensionContext(ExtensionContext, EngineExecutionListener, ClassBasedTestDescriptor,
* Lifecycle, JupiterConfiguration, ThrowableCollector, Function)
* Lifecycle, JupiterConfiguration, ExtensionRegistry, ThrowableCollector)
*/
ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
ClassBasedTestDescriptor testDescriptor, JupiterConfiguration configuration,
ThrowableCollector throwableCollector,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) {

this(parent, engineExecutionListener, testDescriptor, Lifecycle.PER_METHOD, configuration, throwableCollector,
executableInvokerFactory);
this(parent, engineExecutionListener, testDescriptor, Lifecycle.PER_METHOD, configuration, extensionRegistry,
throwableCollector);
}

ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
ClassBasedTestDescriptor testDescriptor, Lifecycle lifecycle, JupiterConfiguration configuration,
ThrowableCollector throwableCollector,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) {

super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory);
super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);

this.lifecycle = lifecycle;
this.throwableCollector = throwableCollector;
Expand Down
Loading