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.err.
It is disabled by default and must be enabled with:
junit.jupiter.execution.timeout.threaddump.enabled = true

Issue: junit-team#2938
  • Loading branch information
AndreasTu committed Oct 15, 2024
1 parent 72d643c commit bcc6fa4
Show file tree
Hide file tree
Showing 35 changed files with 573 additions and 51 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,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.err`.
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.err}. 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.err} 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 isExtensionTimeoutThreadDumpEnabled() {
return (boolean) cache.computeIfAbsent(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME,
__ -> delegate.isExtensionTimeoutThreadDumpEnabled());
}

@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 isExtensionTimeoutThreadDumpEnabled() {
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 isExtensionTimeoutThreadDumpEnabled();

ExecutionMode getDefaultExecutionMode();

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

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.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,9 +56,10 @@ 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,
JupiterConfiguration configuration, ExtensionRegistry extensionRegistry,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
this.executableInvoker = executableInvokerFactory.apply(this);

Expand All @@ -67,6 +71,7 @@ abstract class AbstractExtensionContext<T extends TestDescriptor> implements Ext
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,14 @@ public ExecutableInvoker getExecutableInvoker() {
return executableInvoker;
}

@Override
public <E extends Extension> List<E> getExtensions(Class<E> extensionType) {
if (extensionRegistry == null) {
return Collections.emptyList();
}
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 @@ -181,8 +181,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(),
context.getExtensionRegistry(), throwableCollector, it -> new DefaultExecutableInvoker(it, registry));

// @formatter:off
return context.extend()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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 +40,24 @@ 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, Function)
*/
ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
ClassBasedTestDescriptor testDescriptor, JupiterConfiguration configuration,
ThrowableCollector throwableCollector,
ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {

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

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

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

this.lifecycle = lifecycle;
this.throwableCollector = throwableCollector;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,15 +20,18 @@
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,
ExtensionRegistry extensionRegistry,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {
super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory);
super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry,
executableInvokerFactory);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public String getLegacyReportingName() {
@Override
public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) {
DynamicExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(),
context.getExecutionListener(), this, context.getConfiguration(),
context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry(),
it -> new DefaultExecutableInvoker(it, context.getExtensionRegistry()));
// @formatter:off
return context.extend()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@ 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(), context.getExtensionRegistry(),
it -> new DefaultExecutableInvoker(it, extensionRegistry));

// @formatter:off
return context.extend()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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 +31,11 @@ final class JupiterEngineExtensionContext extends AbstractExtensionContext<Jupit

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

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

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
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 @@ -35,10 +36,11 @@ final class MethodExtensionContext extends AbstractExtensionContext<TestMethodTe

MethodExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener,
TestMethodTestDescriptor testDescriptor, JupiterConfiguration configuration,
ThrowableCollector throwableCollector,
ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector,
Function<ExtensionContext, ExecutableInvoker> executableInvokerFactory) {

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

this.throwableCollector = throwableCollector;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte
MutableExtensionRegistry registry = populateNewExtensionRegistry(context);
ThrowableCollector throwableCollector = createThrowableCollector();
MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(),
context.getExecutionListener(), this, context.getConfiguration(), throwableCollector,
it -> new DefaultExecutableInvoker(it, registry));
context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry(),
throwableCollector, it -> new DefaultExecutableInvoker(it, registry));
// @formatter:off
JupiterEngineExecutionContext newContext = context.extend()
.withExtensionRegistry(registry)
Expand Down
Loading

0 comments on commit bcc6fa4

Please sign in to comment.