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.extensions.preinterruptcallback.default.enabled = true

Issue: #2938
  • Loading branch information
AndreasTu committed Aug 19, 2023
1 parent 9a9063d commit 0c2a0ed
Show file tree
Hide file tree
Showing 20 changed files with 472 additions and 14 deletions.
Expand Up @@ -39,7 +39,7 @@ JUnit repository on GitHub.

==== New Features and Improvements

*
* Added `PreInterruptCallback`


[[release-notes-5.11.0-M1-junit-vintage]]
Expand Down
15 changes: 15 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/extensions.adoc
Expand Up @@ -599,6 +599,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.extensions.preinterruptcallback.default.enabled`

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

Expand Down
6 changes: 6 additions & 0 deletions documentation/src/docs/asciidoc/user-guide/writing-tests.adoc
Expand Up @@ -2244,6 +2244,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
@@ -0,0 +1,45 @@
/*
* Copyright 2015-2023 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.extensions.preinterruptcallback.default.enabled}
*
*
* @since 5.11
* @see org.junit.jupiter.api.Timeout
*/
@API(status = EXPERIMENTAL, since = "5.11")
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;
}
Expand Up @@ -107,6 +107,13 @@ 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 {@link org.junit.jupiter.api.extension.PreInterruptCallback} extension.
*
* <p>The default behavior is not to enable the pre interrupt callback.
*/
public static final String EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME;

/**
* Property name used to set the default test instance lifecycle mode: {@value}
*
Expand Down
Expand Up @@ -67,6 +67,12 @@ public boolean isExtensionAutoDetectionEnabled() {
key -> delegate.isExtensionAutoDetectionEnabled());
}

@Override
public boolean isExtensionDefaultPreInterruptCallbackEnabled() {
return (boolean) cache.computeIfAbsent(EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME,
key -> delegate.isExtensionDefaultPreInterruptCallbackEnabled());
}

@Override
public ExecutionMode getDefaultExecutionMode() {
return (ExecutionMode) cache.computeIfAbsent(DEFAULT_EXECUTION_MODE_PROPERTY_NAME,
Expand Down
Expand Up @@ -89,6 +89,12 @@ public boolean isExtensionAutoDetectionEnabled() {
return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false);
}

@Override
public boolean isExtensionDefaultPreInterruptCallbackEnabled() {
return configurationParameters.getBoolean(EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME)//
.orElse(false);
}

@Override
public ExecutionMode getDefaultExecutionMode() {
return executionModeConverter.get(configurationParameters, DEFAULT_EXECUTION_MODE_PROPERTY_NAME,
Expand Down
Expand Up @@ -39,6 +39,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_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.preinterruptcallback.default.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 @@ -52,6 +53,8 @@ public interface JupiterConfiguration {

boolean isExtensionAutoDetectionEnabled();

boolean isExtensionDefaultPreInterruptCallbackEnabled();

ExecutionMode getDefaultExecutionMode();

ExecutionMode getDefaultClassesExecutionMode();
Expand Down
Expand Up @@ -177,12 +177,19 @@ public Builder withThrowableCollector(ThrowableCollector throwableCollector) {

public JupiterEngineExecutionContext build() {
if (newState != null) {
storeExtensionRegistryInExtensionContext();
originalState = newState;
newState = null;
}
return new JupiterEngineExecutionContext(originalState);
}

private void storeExtensionRegistryInExtensionContext() {
if (newState.extensionRegistry != null && newState.extensionContext != null) {
newState.extensionRegistry.storeInExtensionContext(newState.extensionContext);
}
}

private State newState() {
if (newState == null) {
this.newState = originalState.clone();
Expand Down
@@ -0,0 +1,73 @@
/*
* Copyright 2015-2023 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.engine.extension;

import java.util.Map;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.PreInterruptCallback;
import org.junit.jupiter.engine.Constants;

/**
* The default implementation for {@link PreInterruptCallback},
* which will print the stacks of all {@link Thread}s to {@code System.out}.
*
* <p>Note: This is disabled by default, and must be enabled with
* {@link Constants#EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME}
*
* @since 5.11
*/
public class DefaultPreInterruptCallback implements PreInterruptCallback {
private static final String NL = "\n";

@Override
public void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) {
Map<Thread, StackTraceElement[]> stackTraces = Thread.getAllStackTraces();
StringBuilder sb = new StringBuilder();
sb.append("Thread ");
appendThreadName(sb, threadToInterrupt);
sb.append(" will be interrupted.");
sb.append(NL);
for (Map.Entry<Thread, StackTraceElement[]> entry : stackTraces.entrySet()) {
Thread thread = entry.getKey();
StackTraceElement[] stack = entry.getValue();
if (stack.length > 0) {
sb.append(NL);
appendThreadName(sb, thread);
for (StackTraceElement stackTraceElement : stack) {
sb.append(NL);
//Do the same prefix as java.lang.Throwable.printStackTrace(java.lang.Throwable.PrintStreamOrWriter)
sb.append("\tat ");
sb.append(stackTraceElement.toString());

}
sb.append(NL);
}
}
System.out.println(sb);
}

/**
* Appends the {@link Thread} name and ID in a similar fashion as {@code jstack}.
* @param sb the buffer
* @param th the thread to append
*/
private void appendThreadName(StringBuilder sb, Thread th) {
sb.append("\"");
sb.append(th.getName());
sb.append("\"");
sb.append(" #");
sb.append(th.getId());
if (th.isDaemon()) {
sb.append(" daemon");
}
}
}
Expand Up @@ -26,6 +26,7 @@

import org.apiguardian.api.API;
import org.junit.jupiter.api.extension.Extension;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.engine.config.JupiterConfiguration;
import org.junit.platform.commons.logging.Logger;
import org.junit.platform.commons.logging.LoggerFactory;
Expand All @@ -44,7 +45,8 @@
*/
@API(status = INTERNAL, since = "5.5")
public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionRegistrar {

private static final ExtensionContext.Namespace NAMESPACE = ExtensionContext.Namespace.create(
MutableExtensionRegistry.class);
private static final Logger logger = LoggerFactory.getLogger(MutableExtensionRegistry.class);

private static final List<Extension> DEFAULT_STATELESS_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(//
Expand All @@ -63,6 +65,9 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg
* auto-detected using Java's {@link ServiceLoader} mechanism and automatically
* registered after the default extensions.
*
* <p>If the {@link org.junit.jupiter.engine.Constants#EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME}
* configuration parameter has been set to {@code true}, the {@link DefaultPreInterruptCallback} will be installed.
*
* @param configuration configuration parameters used to retrieve the extension
* auto-detection flag; never {@code null}
* @return a new {@code ExtensionRegistry}; never {@code null}
Expand All @@ -77,7 +82,9 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit
if (configuration.isExtensionAutoDetectionEnabled()) {
registerAutoDetectedExtensions(extensionRegistry);
}

if (configuration.isExtensionDefaultPreInterruptCallbackEnabled()) {
extensionRegistry.registerDefaultExtension(new DefaultPreInterruptCallback());
}
return extensionRegistry;
}

Expand Down Expand Up @@ -166,6 +173,18 @@ public void registerSyntheticExtension(Extension extension, Object source) {
registerExtension("synthetic", extension, source);
}

public void storeInExtensionContext(ExtensionContext extensionContext) {
ExtensionContext.Store store = extensionContext.getStore(MutableExtensionRegistry.NAMESPACE);
if (store != null) {
store.put(ExtensionRegistry.class, this);
}
}

static ExtensionRegistry getRegistryFromExtensionContext(ExtensionContext extensionContext) {
return (ExtensionRegistry) extensionContext.getStore(MutableExtensionRegistry.NAMESPACE).get(
ExtensionRegistry.class);
}

private void registerDefaultExtension(Extension extension) {
registerExtension("default", extension);
}
Expand Down
@@ -0,0 +1,24 @@
/*
* Copyright 2015-2023 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.engine.extension;

import java.util.function.Consumer;

/**
* @since 5.11
*/
@FunctionalInterface
interface PreInterruptCallbackInvocation {
PreInterruptCallbackInvocation NOOP = (t, e) -> {
};

void executePreInterruptCallback(Thread threadToInterrupt, Consumer<Throwable> errorHandler);
}
@@ -0,0 +1,46 @@
/*
* Copyright 2015-2023 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.engine.extension;

import java.util.List;

import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.PreInterruptCallback;
import org.junit.platform.commons.util.UnrecoverableExceptions;

/**
* @since 5.11
*/
final class PreInterruptCallbackInvocationFactory {

private PreInterruptCallbackInvocationFactory() {

}

static PreInterruptCallbackInvocation create(ExtensionContext extensionContext) {
ExtensionRegistry registry = MutableExtensionRegistry.getRegistryFromExtensionContext(extensionContext);
if (registry == null) {
return PreInterruptCallbackInvocation.NOOP;
}
List<PreInterruptCallback> callbacks = registry.getExtensions(PreInterruptCallback.class);
return (thread, errorHandler) -> {
for (PreInterruptCallback callback : callbacks) {
try {
callback.beforeThreadInterrupt(thread, extensionContext);
}
catch (Throwable ex) {
UnrecoverableExceptions.rethrowIfUnrecoverable(ex);
errorHandler.accept(ex);
}
}
};
}
}

0 comments on commit 0c2a0ed

Please sign in to comment.