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: #2938

Co-authored-by: Marc Philipp <[email protected]>
  • Loading branch information
AndreasTu and marcphilipp committed Oct 17, 2024
1 parent 0de9d10 commit 6d2b949
Show file tree
Hide file tree
Showing 35 changed files with 567 additions and 78 deletions.
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
15 changes: 15 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,21 @@ test methods.
include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide]
----

[[extensions-preinterrupt-callback]]
=== PreInterrupt Callback

`{PreInterruptCallback}` defines the API for `Extensions` that wish to react on
`Thread.interrupt()` calls issued by Jupiter before the `Thread.interrupt()` is executed.

This can be used to dump stacks for diagnostics, when the `Timeout` extension
interrupts tests.

There is also a default implementation available, which will dump the stacks of all
`Threads` to `System.out`.
This default implementation need to be enabled with the
<<running-tests-config-params,configuration parameter>>:
`junit.jupiter.execution.timeout.threaddump.enabled`

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

Expand Down
6 changes: 6 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -2658,6 +2658,12 @@ NOTE: If you need more control over polling intervals and greater flexibility wi
asynchronous tests, consider using a dedicated library such as
link:https://github.com/awaitility/awaitility[Awaitility].

[[writing-tests-dump-stack-timeout]]
=== Dump Stacks on Timeout

It can be helpful for debugging to dump the stacks of all Threads, when a Timeout happened.
The <<extensions-preinterrupt-callback, PreInterruptCallback>> provides a default
implementation for that.

[[writing-tests-declarative-timeouts-mode]]
==== Disable @Timeout Globally
Expand Down
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,45 @@
/*
* 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 react on {@link Thread#interrupt()} calls issued by Jupiter
* before the {@link Thread#interrupt()} is executed.
*
* <p>This can be used to e.g. dump stacks for diagnostics, when the {@link org.junit.jupiter.api.Timeout}
* extension is used.</p>
*
* <p>There is also a default implementation available, which will dump the stacks of all {@link Thread Threads}
* to {@code System.out}. This default implementation need to be enabled with the jupiter property:
* {@code junit.jupiter.execution.timeout.threaddump.enabled}
*
*
* @since 5.12
* @see org.junit.jupiter.api.Timeout
*/
@API(status = EXPERIMENTAL, since = "5.12")
public interface PreInterruptCallback extends Extension {

/**
* Callback that is invoked <em>before</em> a {@link Thread} is interrupted with {@link Thread#interrupt()}.
*
* <p>Caution: There is no guarantee on which {@link Thread} this callback will be executed.</p>
*
* @param threadToInterrupt the target {@link Thread}, which will get interrupted.
* @param context the current extension context; never {@code null}
*/
void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) throws Exception;
}
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 the default behavior of {@link org.junit.jupiter.api.extension.PreInterruptCallback}
* extension to print the stacks of all {@link Thread}s to {@code System.out} before the test is interrupted.
*
* <p>The default behavior is not to enable the dump fo threads.
*
* @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 @@ -40,6 +40,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 = "junit.jupiter.execution.timeout.threaddump.enabled";
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 +55,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
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,21 @@
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;
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;

class DynamicExtensionContext extends AbstractExtensionContext<DynamicNodeTestDescriptor> {

DynamicExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
DynamicNodeTestDescriptor testDescriptor, JupiterConfiguration configuration,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory);
ExtensionRegistry extensionRegistry) {
super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@

import org.junit.jupiter.api.DynamicNode;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.platform.engine.TestDescriptor;
import org.junit.platform.engine.TestSource;
Expand Down Expand Up @@ -46,8 +45,7 @@ public String getLegacyReportingName() {
@Override
public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) {
DynamicExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(),
context.getExecutionListener(), this, context.getConfiguration(),
it -> new DefaultExecutableInvoker(it, context.getExtensionRegistry()));
context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry());
// @formatter:off
return context.extend()
.withExtensionContext(extensionContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.jupiter.engine.execution.DefaultExecutableInvoker;
import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext;
import org.junit.jupiter.engine.extension.MutableExtensionRegistry;
import org.junit.platform.engine.EngineExecutionListener;
Expand Down Expand Up @@ -53,7 +52,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
context.getConfiguration());
EngineExecutionListener executionListener = context.getExecutionListener();
ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionListener, this,
context.getConfiguration(), it -> new DefaultExecutableInvoker(it, extensionRegistry));
context.getConfiguration(), extensionRegistry);

// @formatter:off
return context.extend()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,11 @@
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;

Expand All @@ -30,9 +28,9 @@ final class JupiterEngineExtensionContext extends AbstractExtensionContext<Jupit

JupiterEngineExtensionContext(EngineExecutionListener engineExecutionListener,
JupiterEngineDescriptor testDescriptor, JupiterConfiguration configuration,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
ExtensionRegistry extensionRegistry) {

super(null, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory);
super(null, engineExecutionListener, testDescriptor, configuration, extensionRegistry);
}

@Override
Expand Down
Loading

0 comments on commit 6d2b949

Please sign in to comment.