diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 09ca2ec5e88b..9f15dffa3f17 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -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 +<>: +`junit.jupiter.extensions.preinterruptcallback.default.enabled` + [[extensions-intercepting-invocations]] === Intercepting Invocations diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 0a0862b6f2b8..3ddb4d873889 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -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 <> provides a default +implementation for that. [[writing-tests-declarative-timeouts-mode]] ==== Disable @Timeout Globally diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java new file mode 100644 index 000000000000..c34f41631b08 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java @@ -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. + * + *

This can be used to e.g. dump stacks for diagnostics, when the {@link org.junit.jupiter.api.Timeout} + * extension is used.

+ * + *

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.21 + * @see org.junit.jupiter.api.Timeout + */ +@API(status = EXPERIMENTAL, since = "5.21") +public interface PreInterruptCallback extends Extension { + + /** + * Callback that is invoked before a {@link Thread} is interrupted with {@link Thread#interrupt()}. + * + *

Caution: There is no guarantee on which {@link Thread} this callback will be executed.

+ * + * @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; +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java index 2c7b509c9964..180cc74cac27 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java @@ -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. + * + *

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} * diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java index 2d61b58c1c32..47b740f9cf26 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java @@ -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, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java index d64c4ceee318..68e8c8982491 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/DefaultJupiterConfiguration.java @@ -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, diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java index 559b4d7d5715..0956e05f54e4 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/JupiterConfiguration.java @@ -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; @@ -52,6 +53,8 @@ public interface JupiterConfiguration { boolean isExtensionAutoDetectionEnabled(); + boolean isExtensionDefaultPreInterruptCallbackEnabled(); + ExecutionMode getDefaultExecutionMode(); ExecutionMode getDefaultClassesExecutionMode(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java index a31c9c02ad27..fc81750e0e40 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/execution/JupiterEngineExecutionContext.java @@ -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(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptCallback.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptCallback.java new file mode 100644 index 000000000000..b4d698c0b93c --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptCallback.java @@ -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}. + * + *

Note: This is disabled by default, and must be enabled with + * {@link Constants#EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME} + * + * @since 5.21 + */ +public class DefaultPreInterruptCallback implements PreInterruptCallback { + private static final String NL = "\n"; + + @Override + public void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) { + Map stackTraces = Thread.getAllStackTraces(); + StringBuilder sb = new StringBuilder(); + sb.append("Thread "); + appendThreadName(sb, threadToInterrupt); + sb.append(" will be interrupted."); + sb.append(NL); + for (Map.Entry 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"); + } + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java index 0a1433505c7b..4c54a2079520 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java @@ -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; @@ -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 DEFAULT_STATELESS_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(// @@ -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. * + *

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} @@ -77,7 +82,9 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit if (configuration.isExtensionAutoDetectionEnabled()) { registerAutoDetectedExtensions(extensionRegistry); } - + if (configuration.isExtensionDefaultPreInterruptCallbackEnabled()) { + extensionRegistry.registerDefaultExtension(new DefaultPreInterruptCallback()); + } return extensionRegistry; } @@ -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); } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocation.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocation.java new file mode 100644 index 000000000000..c646da27a35d --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocation.java @@ -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.21 + */ +@FunctionalInterface +interface PreInterruptCallbackInvocation { + PreInterruptCallbackInvocation NOOP = (t, e) -> { + }; + + void executePreInterruptCallback(Thread threadToInterrupt, Consumer errorHandler); +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java new file mode 100644 index 000000000000..6778698d83dd --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java @@ -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.21 + */ +final class PreInterruptCallbackInvocationFactory { + + private PreInterruptCallbackInvocationFactory() { + + } + + static PreInterruptCallbackInvocation create(ExtensionContext extensionContext) { + ExtensionRegistry registry = MutableExtensionRegistry.getRegistryFromExtensionContext(extensionContext); + if (registry == null) { + return PreInterruptCallbackInvocation.NOOP; + } + List 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); + } + } + }; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocation.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocation.java index e620151dffd7..172457f00ba2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocation.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocation.java @@ -10,6 +10,8 @@ package org.junit.jupiter.engine.extension; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.function.Supplier; @@ -26,18 +28,20 @@ class SameThreadTimeoutInvocation implements Invocation { private final TimeoutDuration timeout; private final ScheduledExecutorService executor; private final Supplier descriptionSupplier; + private final PreInterruptCallbackInvocation preInterruptCallback; SameThreadTimeoutInvocation(Invocation delegate, TimeoutDuration timeout, ScheduledExecutorService executor, - Supplier descriptionSupplier) { + Supplier descriptionSupplier, PreInterruptCallbackInvocation preInterruptCallback) { this.delegate = delegate; this.timeout = timeout; this.executor = executor; this.descriptionSupplier = descriptionSupplier; + this.preInterruptCallback = preInterruptCallback; } @Override public T proceed() throws Throwable { - InterruptTask interruptTask = new InterruptTask(Thread.currentThread()); + InterruptTask interruptTask = new InterruptTask(Thread.currentThread(), preInterruptCallback); ScheduledFuture future = executor.schedule(interruptTask, timeout.getValue(), timeout.getUnit()); Throwable failure = null; T result = null; @@ -56,6 +60,7 @@ public T proceed() throws Throwable { if (interruptTask.executed) { Thread.interrupted(); failure = TimeoutExceptionFactory.create(descriptionSupplier.get(), timeout, failure); + interruptTask.attachSuppressedExceptions(failure); } } if (failure != null) { @@ -65,20 +70,28 @@ public T proceed() throws Throwable { } static class InterruptTask implements Runnable { - + private final PreInterruptCallbackInvocation preInterruptCallback; + private final List exceptionsDuringInterruption = new CopyOnWriteArrayList<>(); private final Thread thread; private volatile boolean executed; - InterruptTask(Thread thread) { + InterruptTask(Thread thread, PreInterruptCallbackInvocation preInterruptCallback) { this.thread = thread; + this.preInterruptCallback = preInterruptCallback; } @Override public void run() { executed = true; + preInterruptCallback.executePreInterruptCallback(thread, exceptionsDuringInterruption::add); thread.interrupt(); } + void attachSuppressedExceptions(Throwable outerException) { + for (Throwable throwable : exceptionsDuringInterruption) { + outerException.addSuppressed(throwable); + } + } } } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java index 9efe2255e9d3..061d97be08f0 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java @@ -177,8 +177,8 @@ private Invocation decorate(Invocation invocation, ReflectiveInvocatio ThreadMode threadMode = resolveTimeoutThreadMode(extensionContext); return new TimeoutInvocationFactory(extensionContext.getRoot().getStore(NAMESPACE)).create(threadMode, - new TimeoutInvocationParameters<>(invocation, timeout, - () -> describe(invocationContext, extensionContext))); + new TimeoutInvocationParameters<>(invocation, timeout, () -> describe(invocationContext, extensionContext), + PreInterruptCallbackInvocationFactory.create(extensionContext))); } private ThreadMode resolveTimeoutThreadMode(ExtensionContext extensionContext) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java index 08f6851f1943..8739cb75dc62 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactory.java @@ -43,7 +43,8 @@ Invocation create(ThreadMode threadMode, TimeoutInvocationParameters t } return new SameThreadTimeoutInvocation<>(timeoutInvocationParameters.getInvocation(), timeoutInvocationParameters.getTimeoutDuration(), getThreadExecutorForSameThreadInvocation(), - timeoutInvocationParameters.getDescriptionSupplier()); + timeoutInvocationParameters.getDescriptionSupplier(), + timeoutInvocationParameters.getPreInterruptCallback()); } private ScheduledExecutorService getThreadExecutorForSameThreadInvocation() { @@ -90,13 +91,16 @@ static class TimeoutInvocationParameters { private final Invocation invocation; private final TimeoutDuration timeout; private final Supplier descriptionSupplier; + private final PreInterruptCallbackInvocation preInterruptCallback; TimeoutInvocationParameters(Invocation invocation, TimeoutDuration timeout, - Supplier descriptionSupplier) { + Supplier descriptionSupplier, PreInterruptCallbackInvocation preInterruptCallback) { this.invocation = Preconditions.notNull(invocation, "invocation must not be null"); this.timeout = Preconditions.notNull(timeout, "timeout must not be null"); this.descriptionSupplier = Preconditions.notNull(descriptionSupplier, "description supplier must not be null"); + this.preInterruptCallback = Preconditions.notNull(preInterruptCallback, + "preInterruptCallback must not be null"); } public Invocation getInvocation() { @@ -110,5 +114,9 @@ public TimeoutDuration getTimeoutDuration() { public Supplier getDescriptionSupplier() { return descriptionSupplier; } + + public PreInterruptCallbackInvocation getPreInterruptCallback() { + return preInterruptCallback; + } } } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java new file mode 100644 index 000000000000..2fb8fadad80d --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java @@ -0,0 +1,177 @@ +/* + * 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 static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; +import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.PreInterruptCallback; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.jupiter.engine.Constants; +import org.junit.platform.testkit.engine.Events; + +/** + * @since 5.21 + */ +@Isolated +class PreInterruptCallbackTests extends AbstractJupiterTestEngineTests { + private static final String TC = "test"; + private static final String TIMEOUT_ERROR_MSG = TC + "() timed out after 1 microsecond"; + private static final String DEFAULT_ENABLE_PROPERTY = Constants.EXTENSIONS_DEFAULT_PRE_INTERRUPT_CALLBACK_ENABLED_PROPERTY_NAME; + private static final AtomicBoolean interruptedTest = new AtomicBoolean(); + private static final AtomicBoolean interruptCallbackCalled = new AtomicBoolean(); + private static final AtomicBoolean interruptCallbackShallThrowException = new AtomicBoolean(); + + @BeforeEach + void setUp() { + interruptedTest.set(true); + interruptCallbackCalled.set(false); + interruptCallbackShallThrowException.set(false); + } + + @Test + @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) + void testCaseWithDefaultInterruptCallbackEnabled() { + String orgValue = System.getProperty(DEFAULT_ENABLE_PROPERTY); + System.setProperty(DEFAULT_ENABLE_PROPERTY, Boolean.TRUE.toString()); + PrintStream orgOut = System.out; + Events tests; + String output; + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + PrintStream outStream = new PrintStream(buffer); + System.setOut(outStream); + tests = executeTestsForClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class).testEvents(); + output = buffer.toString(StandardCharsets.UTF_8); + } + finally { + System.setOut(orgOut); + if (orgValue != null) { + System.setProperty(DEFAULT_ENABLE_PROPERTY, orgValue); + } + else { + System.clearProperty(DEFAULT_ENABLE_PROPERTY); + } + } + + assertTestHasTimedOut(tests); + assertTrue(interruptedTest.get()); + Thread thread = Thread.currentThread(); + assertTrue(output.contains("Thread \"" + thread.getName() + "\" #" + thread.getId() + " will be interrupted."), + output); + assertTrue(output.contains("java.lang.Thread.sleep"), output); + assertTrue(output.contains( + "org.junit.jupiter.engine.extension.PreInterruptCallbackTests$DefaultPreInterruptCallbackTimeoutOnMethodTestCase.test(PreInterruptCallbackTests.java"), + output); + + assertTrue(output.contains("junit-jupiter-timeout-watcher"), output); + assertTrue( + output.contains("org.junit.jupiter.engine.extension.DefaultPreInterruptCallback.beforeThreadInterrupt"), + output); + } + + @Test + void testCaseWithNoInterruptCallbackEnabled() { + + Events tests = executeTestsForClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class).testEvents(); + assertTestHasTimedOut(tests); + assertTrue(interruptedTest.get()); + } + + @Test + void testCaseWithDeclaredInterruptCallbackEnabled() { + Events tests = executeTestsForClass(DefaultPreInterruptCallbackWithExplicitCallbackTestCase.class).testEvents(); + assertTestHasTimedOut(tests); + assertTrue(interruptedTest.get()); + assertTrue(interruptCallbackCalled.get()); + } + + @Test + void testCaseWithDeclaredInterruptCallbackThrowsException() { + interruptCallbackShallThrowException.set(true); + Events tests = executeTestsForClass(DefaultPreInterruptCallbackWithExplicitCallbackTestCase.class).testEvents(); + tests.failed().assertEventsMatchExactly(event(test(TC), + finishedWithFailure(instanceOf(TimeoutException.class), message(TIMEOUT_ERROR_MSG), + suppressed(0, instanceOf(InterruptedException.class)), + suppressed(1, instanceOf(IllegalStateException.class))))); + assertTrue(interruptedTest.get()); + assertTrue(interruptCallbackCalled.get()); + } + + private static void assertTestHasTimedOut(Events tests) { + tests.assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)); + tests.failed().assertEventsMatchExactly( + event(test(TC), finishedWithFailure(instanceOf(TimeoutException.class), message(TIMEOUT_ERROR_MSG), // + suppressed(0, instanceOf(InterruptedException.class))// + ))); + } + + static class TestPreInterruptCallback implements PreInterruptCallback { + + @Override + public void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) { + interruptCallbackCalled.set(true); + if (interruptCallbackShallThrowException.get()) { + throw new IllegalStateException("Test-Ex"); + } + } + } + + static class DefaultPreInterruptCallbackTimeoutOnMethodTestCase { + @Test + @Timeout(value = 1, unit = TimeUnit.MICROSECONDS) + void test() throws InterruptedException { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + interruptedTest.set(true); + throw ex; + } + } + } + + @ExtendWith(TestPreInterruptCallback.class) + static class DefaultPreInterruptCallbackWithExplicitCallbackTestCase { + @Test + @Timeout(value = 1, unit = TimeUnit.MICROSECONDS) + void test() throws InterruptedException { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + interruptedTest.set(true); + throw ex; + } + } + } +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java index 81630493d66e..a166dbdca3ca 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java @@ -34,7 +34,8 @@ void resetsInterruptFlag() { var exception = assertThrows(TimeoutException.class, () -> withExecutor(executor -> { var delegate = new EventuallyInterruptibleInvocation(); var duration = new TimeoutDuration(1, NANOSECONDS); - var timeoutInvocation = new SameThreadTimeoutInvocation<>(delegate, duration, executor, () -> "execution"); + var timeoutInvocation = new SameThreadTimeoutInvocation<>(delegate, duration, executor, () -> "execution", + PreInterruptCallbackInvocation.NOOP); timeoutInvocation.proceed(); })); assertFalse(Thread.currentThread().isInterrupted()); diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java index c8d51e7987a8..638d23626ca0 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java @@ -73,7 +73,8 @@ private static SeparateThreadTimeoutInvocation aSeparateThreadInvocation( var namespace = ExtensionContext.Namespace.create(SeparateThreadTimeoutInvocationTests.class); var store = new NamespaceAwareStore(new NamespacedHierarchicalStore<>(null), namespace); var parameters = new TimeoutInvocationParameters<>(invocation, - new TimeoutDuration(PREEMPTIVE_TIMEOUT_MILLIS, MILLISECONDS), () -> "method()"); + new TimeoutDuration(PREEMPTIVE_TIMEOUT_MILLIS, MILLISECONDS), () -> "method()", + PreInterruptCallbackInvocation.NOOP); return (SeparateThreadTimeoutInvocation) new TimeoutInvocationFactory(store) // .create(ThreadMode.SEPARATE_THREAD, parameters); } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java index 9d3c46487dab..e0832cd1b86b 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java @@ -46,7 +46,8 @@ class TimeoutInvocationFactoryTests { @BeforeEach void setUp() { - parameters = new TimeoutInvocationParameters<>(invocation, timeoutDuration, () -> "description"); + parameters = new TimeoutInvocationParameters<>(invocation, timeoutDuration, () -> "description", + PreInterruptCallbackInvocation.NOOP); timeoutInvocationFactory = new TimeoutInvocationFactory(store); }