From fddd6b92a9e66e98b588f2edbb4612fdeb17ba05 Mon Sep 17 00:00:00 2001 From: AndreasTu Date: Tue, 15 Oct 2024 19:48:25 +0200 Subject: [PATCH 1/3] PreInterruptCallback extension 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 --- .../src/docs/asciidoc/link-attributes.adoc | 1 + .../release-notes-5.12.0-M1.adoc | 1 + .../docs/asciidoc/user-guide/extensions.adoc | 9 + .../asciidoc/user-guide/writing-tests.adoc | 16 ++ .../api/AssertTimeoutPreemptively.java | 4 +- .../org/junit/jupiter/api/Assertions.java | 2 +- .../api/extension/PreInterruptCallback.java | 60 +++++ .../api/extension/PreInterruptContext.java | 35 +++ .../org/junit/jupiter/engine/Constants.java | 13 +- .../config/CachingJupiterConfiguration.java | 6 + .../config/DefaultJupiterConfiguration.java | 5 + .../engine/config/JupiterConfiguration.java | 4 + .../descriptor/AbstractExtensionContext.java | 21 +- .../descriptor/ClassBasedTestDescriptor.java | 5 +- .../descriptor/ClassExtensionContext.java | 17 +- .../descriptor/DynamicExtensionContext.java | 7 +- .../descriptor/DynamicNodeTestDescriptor.java | 4 +- .../descriptor/JupiterEngineDescriptor.java | 3 +- .../JupiterEngineExtensionContext.java | 8 +- .../descriptor/MethodExtensionContext.java | 8 +- .../descriptor/TestMethodTestDescriptor.java | 4 +- .../TestTemplateExtensionContext.java | 9 +- .../TestTemplateTestDescriptor.java | 4 +- .../extension/DefaultPreInterruptContext.java | 41 +++ .../extension/ExtensionContextInternal.java | 40 +++ .../extension/MutableExtensionRegistry.java | 9 + .../PreInterruptCallbackInvocation.java | 24 ++ ...PreInterruptCallbackInvocationFactory.java | 46 ++++ .../PreInterruptThreadDumpPrinter.java | 74 ++++++ .../SameThreadTimeoutInvocation.java | 21 +- .../SeparateThreadTimeoutInvocation.java | 7 +- .../engine/extension/TimeoutExtension.java | 4 +- .../extension/TimeoutInvocationFactory.java | 15 +- ...ertTimeoutPreemptivelyAssertionsTests.java | 4 +- .../api/extension/KitchenSinkExtension.java | 11 +- .../descriptor/ExtensionContextTests.java | 58 +++-- .../extension/PreInterruptCallbackTests.java | 244 ++++++++++++++++++ .../SameThreadTimeoutInvocationTests.java | 3 +- .../SeparateThreadTimeoutInvocationTests.java | 3 +- .../TimeoutInvocationFactoryTests.java | 3 +- .../tooling/support/tests/ArchUnitTests.java | 2 + 41 files changed, 768 insertions(+), 87 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptContext.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocation.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java create mode 100644 jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 5c55bec09cf7..46da6e279efd 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -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] diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 520da97eef40..6f116f4d4907 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc @@ -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]] diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 3e8c551415dc..d50940991b1c 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -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 +timeouts before the `Thread.interrupt()` is called. + +Please refer to <> for additional information. + + [[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 8df05f70c3ba..b973308ddee4 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -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 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 <> 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 diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java index 4585bb0e8927..aa68efbbcab8 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AssertTimeoutPreemptively.java @@ -113,7 +113,7 @@ private static 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()); @@ -124,7 +124,7 @@ private static T resolveFutureAndHandleException(Future } private static AssertionFailedError createAssertionFailure(Duration timeout, Supplier messageSupplier, - Throwable cause) { + Throwable cause, Thread thread) { return assertionFailure() // .message(messageSupplier) // .reason("execution timed out after " + timeout.toMillis() + " ms") // diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java index 3f5df08971a0..3b0bf640ac47 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/Assertions.java @@ -3662,6 +3662,6 @@ public interface TimeoutFailureFactory { * * @return timeout failure; never {@code null} */ - T createTimeoutFailure(Duration timeout, Supplier messageSupplier, Throwable cause); + T createTimeoutFailure(Duration timeout, Supplier messageSupplier, Throwable cause, Thread testThread); } } 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..d32d77e34406 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java @@ -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. + * + *

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. + * + *

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 before a {@link Thread} is interrupted with + * {@link Thread#interrupt()}. + * + *

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; +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java new file mode 100644 index 000000000000..263fc12ba9cd --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java @@ -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 context in which an + * {@link PreInterruptCallback#beforeThreadInterrupt(PreInterruptContext) 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(); +} 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 2f64dd866cd2..92c6990cb7ed 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 @@ -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. + * + *

This behavior is disabled by default. + * + * @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} * @@ -192,7 +203,7 @@ public final class Constants { *

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. - + * *

Value must either {@code true} or {@code false}; defaults to {@code true}. * *

Note: This property only takes affect on Java 9+. 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 3082830d2146..f2e24ba494dc 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 @@ -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, 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 1057ab563b1f..b72b0362e638 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 @@ -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, 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 c695e7e4b10b..b342054c5cbf 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 @@ -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; @@ -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; @@ -54,6 +56,8 @@ public interface JupiterConfiguration { boolean isExtensionAutoDetectionEnabled(); + boolean isThreadDumpOnTimeoutEnabled(); + ExecutionMode getDefaultExecutionMode(); ExecutionMode getDefaultClassesExecutionMode(); diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java index a1772e796146..4ff9573a32b6 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/AbstractExtensionContext.java @@ -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; @@ -38,7 +43,7 @@ /** * @since 5.0 */ -abstract class AbstractExtensionContext implements ExtensionContext, AutoCloseable { +abstract class AbstractExtensionContext implements ExtensionContextInternal, AutoCloseable { private static final NamespacedHierarchicalStore.CloseAction CLOSE_RESOURCES = (__, ___, value) -> { if (value instanceof CloseableResource) { @@ -53,20 +58,21 @@ abstract class AbstractExtensionContext implements Ext private final JupiterConfiguration configuration; private final NamespacedHierarchicalStore valuesStore; private final ExecutableInvoker executableInvoker; + private final ExtensionRegistry extensionRegistry; AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor, - JupiterConfiguration configuration, - Function 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() @@ -152,6 +158,11 @@ public ExecutableInvoker getExecutableInvoker() { return executableInvoker; } + @Override + public List getExtensions(Class extensionType) { + return extensionRegistry.getExtensions(extensionType); + } + protected abstract Node.ExecutionMode getPlatformExecutionMode(); private ExecutionMode toJupiterExecutionMode(Node.ExecutionMode mode) { diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java index ee53adcb2500..2b6b93e79bc3 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassBasedTestDescriptor.java @@ -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; @@ -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() diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java index ddeb750dbb29..cc4fd4bec672 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/ClassExtensionContext.java @@ -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; @@ -39,23 +38,21 @@ final class ClassExtensionContext extends AbstractExtensionContext 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 executableInvokerFactory) { + ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry); this.lifecycle = lifecycle; this.throwableCollector = throwableCollector; diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java index 0bc5c0542167..2cc4e130a5b2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicExtensionContext.java @@ -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; -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; @@ -27,8 +26,8 @@ class DynamicExtensionContext extends AbstractExtensionContext executableInvokerFactory) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + ExtensionRegistry extensionRegistry) { + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java index ba07a11b6ec4..4fc200a9e4a2 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/DynamicNodeTestDescriptor.java @@ -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; @@ -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) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java index 0f87b7a182b2..50d694b46f5e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineDescriptor.java @@ -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; @@ -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() diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java index 988dc8ea0254..88d94db6225d 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/JupiterEngineExtensionContext.java @@ -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; @@ -30,9 +28,9 @@ final class JupiterEngineExtensionContext extends AbstractExtensionContext executableInvokerFactory) { + ExtensionRegistry extensionRegistry) { - super(null, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(null, engineExecutionListener, testDescriptor, configuration, extensionRegistry); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java index 6c5e2efc6fbf..2c51ad7e5862 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/MethodExtensionContext.java @@ -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; @@ -35,10 +34,9 @@ final class MethodExtensionContext extends AbstractExtensionContext executableInvokerFactory) { + ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry); this.throwableCollector = throwableCollector; } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java index d9da3cb4da7a..b2c7830da1a9 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestMethodTestDescriptor.java @@ -35,7 +35,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.InterceptingExecutableInvoker; import org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.ReflectiveInterceptorCall; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; @@ -99,8 +98,7 @@ 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(), registry, throwableCollector); // @formatter:off JupiterEngineExecutionContext newContext = context.extend() .withExtensionRegistry(registry) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java index ae4f92b8195f..9d40ec8fa1bb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateExtensionContext.java @@ -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; @@ -31,10 +30,10 @@ final class TestTemplateExtensionContext extends AbstractExtensionContext executableInvokerFactory) { + TestTemplateTestDescriptor testDescriptor, JupiterConfiguration configuration, + ExtensionRegistry extensionRegistry, TestInstances testInstances) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry); this.testInstances = testInstances; } diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java index fee7c3906d15..53206b685d07 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/descriptor/TestTemplateTestDescriptor.java @@ -26,7 +26,6 @@ import org.junit.jupiter.api.extension.TestTemplateInvocationContext; import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; 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.ExtensionRegistry; import org.junit.jupiter.engine.extension.MutableExtensionRegistry; @@ -81,8 +80,7 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte TestInstances testInstances = context.getExtensionContext().getTestInstances().orElse(null); ExtensionContext extensionContext = new TestTemplateExtensionContext(context.getExtensionContext(), - context.getExecutionListener(), this, context.getConfiguration(), testInstances, - it -> new DefaultExecutableInvoker(it, registry)); + context.getExecutionListener(), this, context.getConfiguration(), registry, testInstances); // @formatter:off return context.extend() diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptContext.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptContext.java new file mode 100644 index 000000000000..5dfb552d9412 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptContext.java @@ -0,0 +1,41 @@ +/* + * 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.engine.extension; + +import org.junit.jupiter.api.extension.PreInterruptContext; +import org.junit.platform.commons.util.Preconditions; +import org.junit.platform.commons.util.ToStringBuilder; + +/** + * @since 5.12 + */ +class DefaultPreInterruptContext implements PreInterruptContext { + private final Thread threadToInterrupt; + + DefaultPreInterruptContext(Thread threadToInterrupt) { + Preconditions.notNull(threadToInterrupt, "threadToInterrupt must not be null"); + this.threadToInterrupt = threadToInterrupt; + } + + @Override + public Thread getThreadToInterrupt() { + return threadToInterrupt; + } + + @Override + public String toString() { + // @formatter:off + return new ToStringBuilder(this) + .append("threadToInterrupt", this.threadToInterrupt) + .toString(); + // @formatter:on + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java new file mode 100644 index 000000000000..3de267e72f31 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java @@ -0,0 +1,40 @@ +/* + * 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.engine.extension; + +import static org.apiguardian.api.API.Status.INTERNAL; + +import java.util.List; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.Extension; +import org.junit.jupiter.api.extension.ExtensionContext; + +/** + * {@code ExtensionContextInternal} extends the {@link ExtensionContext} with internal API. + * + * @since 5.12 + * @see ExtensionContext + */ +@API(status = INTERNAL, since = "5.12") +public interface ExtensionContextInternal extends ExtensionContext { + + /** + * Returns a list of registered extension at this context of the passed {@code extensionType}. + * + * @param the extension type + * @param extensionType the extension type + * @return the list of extensions + * @since 5.12 + */ + @API(status = INTERNAL, since = "5.12") + List getExtensions(Class extensionType); +} 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 5a719f8b2817..676598c7abe0 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 @@ -66,6 +66,11 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg * auto-detected using Java's {@link ServiceLoader} mechanism and automatically * registered after the default extensions. * + *

If the + * {@value org.junit.jupiter.engine.Constants#EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME} + * configuration parameter has been set to {@code true}, the + * {@link PreInterruptThreadDumpPrinter} will be registered. + * * @param configuration configuration parameters used to retrieve the extension * auto-detection flag; never {@code null} * @return a new {@code ExtensionRegistry}; never {@code null} @@ -81,6 +86,10 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit registerAutoDetectedExtensions(extensionRegistry); } + if (configuration.isThreadDumpOnTimeoutEnabled()) { + extensionRegistry.registerDefaultExtension(new PreInterruptThreadDumpPrinter()); + } + return extensionRegistry; } 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..290c3464ca2a --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocation.java @@ -0,0 +1,24 @@ +/* + * 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.engine.extension; + +import java.util.function.Consumer; + +/** + * @since 5.12 + */ +@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..a265a2435a76 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java @@ -0,0 +1,46 @@ +/* + * 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.engine.extension; + +import java.util.List; + +import org.junit.jupiter.api.extension.PreInterruptCallback; +import org.junit.jupiter.api.extension.PreInterruptContext; +import org.junit.platform.commons.util.UnrecoverableExceptions; + +/** + * @since 5.12 + * @see PreInterruptCallbackInvocation + */ +final class PreInterruptCallbackInvocationFactory { + + private PreInterruptCallbackInvocationFactory() { + } + + static PreInterruptCallbackInvocation create(ExtensionContextInternal extensionContext) { + final List callbacks = extensionContext.getExtensions(PreInterruptCallback.class); + if (callbacks.isEmpty()) { + return PreInterruptCallbackInvocation.NOOP; + } + return (thread, errorHandler) -> { + PreInterruptContext preInterruptContext = new DefaultPreInterruptContext(thread); + for (PreInterruptCallback callback : callbacks) { + try { + callback.beforeThreadInterrupt(preInterruptContext, extensionContext); + } + catch (Throwable ex) { + UnrecoverableExceptions.rethrowIfUnrecoverable(ex); + errorHandler.accept(ex); + } + } + }; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java new file mode 100644 index 000000000000..b454d96a7277 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java @@ -0,0 +1,74 @@ +/* + * 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.engine.extension; + +import java.util.Map; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.PreInterruptCallback; +import org.junit.jupiter.api.extension.PreInterruptContext; +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_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME} + * + * @since 5.12 + */ +final class PreInterruptThreadDumpPrinter implements PreInterruptCallback { + private static final String NL = "\n"; + + @Override + public void beforeThreadInterrupt(PreInterruptContext preInterruptContext, ExtensionContext extensionContext) { + Map stackTraces = Thread.getAllStackTraces(); + StringBuilder sb = new StringBuilder(); + sb.append("Thread "); + appendThreadName(sb, preInterruptContext.getThreadToInterrupt()); + 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/SameThreadTimeoutInvocation.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocation.java index fc4834eff4e9..38d7526a7875 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/SeparateThreadTimeoutInvocation.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java index f102bae0cf9a..df176021f96e 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocation.java @@ -25,19 +25,22 @@ class SeparateThreadTimeoutInvocation implements Invocation { private final Invocation delegate; private final TimeoutDuration timeout; private final Supplier descriptionSupplier; + private final PreInterruptCallbackInvocation preInterruptCallback; SeparateThreadTimeoutInvocation(Invocation delegate, TimeoutDuration timeout, - Supplier descriptionSupplier) { + Supplier descriptionSupplier, PreInterruptCallbackInvocation preInterruptCallback) { this.delegate = delegate; this.timeout = timeout; this.descriptionSupplier = descriptionSupplier; + this.preInterruptCallback = preInterruptCallback; } @Override public T proceed() throws Throwable { return assertTimeoutPreemptively(timeout.toDuration(), delegate::proceed, descriptionSupplier, - (__, messageSupplier, cause) -> { + (__, messageSupplier, cause, testThread) -> { TimeoutException exception = TimeoutExceptionFactory.create(messageSupplier.get(), timeout, null); + preInterruptCallback.executePreInterruptCallback(testThread, exception::addSuppressed); exception.initCause(cause); return exception; }); 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 f8b87a62bed8..44d0a402c387 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 @@ -182,8 +182,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((ExtensionContextInternal) 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 004915069e32..a782c8e6bee8 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 @@ -39,11 +39,13 @@ Invocation create(ThreadMode threadMode, TimeoutInvocationParameters t Preconditions.notNull(timeoutInvocationParameters, "timeout invocation parameters must not be null"); if (threadMode == ThreadMode.SEPARATE_THREAD) { return new SeparateThreadTimeoutInvocation<>(timeoutInvocationParameters.getInvocation(), - timeoutInvocationParameters.getTimeoutDuration(), timeoutInvocationParameters.getDescriptionSupplier()); + timeoutInvocationParameters.getTimeoutDuration(), timeoutInvocationParameters.getDescriptionSupplier(), + timeoutInvocationParameters.getPreInterruptCallback()); } return new SameThreadTimeoutInvocation<>(timeoutInvocationParameters.getInvocation(), timeoutInvocationParameters.getTimeoutDuration(), getThreadExecutorForSameThreadInvocation(), - timeoutInvocationParameters.getDescriptionSupplier()); + timeoutInvocationParameters.getDescriptionSupplier(), + timeoutInvocationParameters.getPreInterruptCallback()); } private ScheduledExecutorService getThreadExecutorForSameThreadInvocation() { @@ -90,13 +92,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 +115,9 @@ public TimeoutDuration getTimeoutDuration() { public Supplier getDescriptionSupplier() { return descriptionSupplier; } + + public PreInterruptCallbackInvocation getPreInterruptCallback() { + return preInterruptCallback; + } } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java index c445f86f67fc..94ee07931088 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/AssertTimeoutPreemptivelyAssertionsTests.java @@ -40,8 +40,8 @@ class AssertTimeoutPreemptivelyAssertionsTests { private static final Duration PREEMPTIVE_TIMEOUT = ofMillis(WINDOWS.isCurrentOs() ? 1000 : 100); - private static final Assertions.TimeoutFailureFactory TIMEOUT_EXCEPTION_FACTORY = (__, ___, - ____) -> new TimeoutException(); + private static final Assertions.TimeoutFailureFactory TIMEOUT_EXCEPTION_FACTORY = (__, ___, ____, + _____) -> new TimeoutException(); private static final ThreadLocal changed = ThreadLocal.withInitial(() -> new AtomicBoolean(false)); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java index 03106324eec7..8ee6b62a4075 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/KitchenSinkExtension.java @@ -60,7 +60,8 @@ public class KitchenSinkExtension implements // Miscellaneous TestWatcher, - InvocationInterceptor + InvocationInterceptor, + PreInterruptCallback // @formatter:on { @@ -254,4 +255,12 @@ public void interceptAfterAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { InvocationInterceptor.super.interceptAfterAllMethod(invocation, invocationContext, extensionContext); } + + // --- PreInterruptCallback ------------------------------------------------ + + @Override + public void beforeThreadInterrupt(PreInterruptContext preInterruptContext, ExtensionContext extensionContext) + throws Exception { + + } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java index d8c6014eadd7..779ff8620904 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ExtensionContextTests.java @@ -32,12 +32,15 @@ import org.junit.jupiter.api.Named; import org.junit.jupiter.api.Tag; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import org.junit.jupiter.api.extension.PreInterruptCallback; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.config.DefaultJupiterConfiguration; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.DefaultTestInstances; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; import org.junit.platform.commons.PreconditionViolationException; @@ -62,6 +65,7 @@ public class ExtensionContextTests { private final JupiterConfiguration configuration = mock(); + private final ExtensionRegistry extensionRegistry = mock(); @BeforeEach void setUp() { @@ -76,7 +80,7 @@ void fromJupiterEngineDescriptor() { UniqueId.root("engine", "junit-jupiter"), configuration); try (var engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor, configuration, - __ -> null)) { + extensionRegistry)) { // @formatter:off assertAll("engineContext", () -> assertThat(engineContext.getElement()).isEmpty(), @@ -89,7 +93,8 @@ void fromJupiterEngineDescriptor() { () -> assertThat(engineContext.getDisplayName()).isEqualTo(engineTestDescriptor.getDisplayName()), () -> assertThat(engineContext.getParent()).isEmpty(), () -> assertThat(engineContext.getRoot()).isSameAs(engineContext), - () -> assertThat(engineContext.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD) + () -> assertThat(engineContext.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD), + () -> assertThat(engineContext.getExtensions(PreInterruptCallback.class)).isEmpty() ); // @formatter:on } @@ -102,7 +107,7 @@ void fromClassTestDescriptor() { ClassTestDescriptor outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); ClassExtensionContext outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, - configuration, null, __ -> null); + configuration, extensionRegistry, null); // @formatter:off assertAll("outerContext", @@ -115,15 +120,29 @@ void fromClassTestDescriptor() { () -> assertThrows(PreconditionViolationException.class, outerExtensionContext::getRequiredTestMethod), () -> assertThat(outerExtensionContext.getDisplayName()).isEqualTo(outerClassDescriptor.getDisplayName()), () -> assertThat(outerExtensionContext.getParent()).isEmpty(), - () -> assertThat(outerExtensionContext.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD) + () -> assertThat(outerExtensionContext.getExecutionMode()).isEqualTo(ExecutionMode.SAME_THREAD), + () -> assertThat(outerExtensionContext.getExtensions(PreInterruptCallback.class)).isEmpty() ); // @formatter:on ClassExtensionContext nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, - nestedClassDescriptor, configuration, null, __ -> null); + nestedClassDescriptor, configuration, extensionRegistry, null); assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext); } + @Test + void ExtensionContext_With_ExtensionRegistry_getExtensions() { + NestedClassTestDescriptor classTestDescriptor = nestedClassDescriptor(); + try (ClassExtensionContext ctx = new ClassExtensionContext(null, null, classTestDescriptor, configuration, + extensionRegistry, null)) { + + Extension ext = mock(); + when(extensionRegistry.getExtensions(Extension.class)).thenReturn(List.of(ext)); + + assertThat(ctx.getExtensions(Extension.class)).isEqualTo(List.of(ext)); + } + } + @Test @SuppressWarnings("resource") void tagsCanBeRetrievedInExtensionContext() { @@ -133,18 +152,18 @@ void tagsCanBeRetrievedInExtensionContext() { outerClassDescriptor.addChild(methodTestDescriptor); ClassExtensionContext outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, - configuration, null, __ -> null); + configuration, extensionRegistry, null); assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag"); assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext); ClassExtensionContext nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, - nestedClassDescriptor, configuration, null, __ -> null); + nestedClassDescriptor, configuration, extensionRegistry, null); assertThat(nestedExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "nested-tag"); assertThat(nestedExtensionContext.getRoot()).isSameAs(outerExtensionContext); MethodExtensionContext methodExtensionContext = new MethodExtensionContext(outerExtensionContext, null, - methodTestDescriptor, configuration, new OpenTest4JAwareThrowableCollector(), __ -> null); + methodTestDescriptor, configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(new OuterClass())); assertThat(methodExtensionContext.getTags()).containsExactlyInAnyOrder("outer-tag", "method-tag"); assertThat(methodExtensionContext.getRoot()).isSameAs(outerExtensionContext); @@ -163,11 +182,11 @@ void fromMethodTestDescriptor() { Method testMethod = methodTestDescriptor.getTestMethod(); JupiterEngineExtensionContext engineExtensionContext = new JupiterEngineExtensionContext(null, engineDescriptor, - configuration, __ -> null); + configuration, extensionRegistry); ClassExtensionContext classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, - classTestDescriptor, configuration, null, __ -> null); + classTestDescriptor, configuration, extensionRegistry, null); MethodExtensionContext methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, - methodTestDescriptor, configuration, new OpenTest4JAwareThrowableCollector(), __ -> null); + methodTestDescriptor, configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); methodExtensionContext.setTestInstances(DefaultTestInstances.of(testInstance)); // @formatter:off @@ -193,7 +212,7 @@ void reportEntriesArePublishedToExecutionContext() { ClassTestDescriptor classTestDescriptor = outerClassDescriptor(null); EngineExecutionListener engineExecutionListener = Mockito.spy(EngineExecutionListener.class); ExtensionContext extensionContext = new ClassExtensionContext(null, engineExecutionListener, - classTestDescriptor, configuration, null, __ -> null); + classTestDescriptor, configuration, extensionRegistry, null); Map map1 = Collections.singletonMap("key", "value"); Map map2 = Collections.singletonMap("other key", "other value"); @@ -223,10 +242,10 @@ void reportEntriesArePublishedToExecutionContext() { void usingStore() { TestMethodTestDescriptor methodTestDescriptor = methodDescriptor(); ClassTestDescriptor classTestDescriptor = outerClassDescriptor(methodTestDescriptor); - ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, configuration, null, - __ -> null); + ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, configuration, + extensionRegistry, null); MethodExtensionContext childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, - configuration, new OpenTest4JAwareThrowableCollector(), __ -> null); + configuration, extensionRegistry, new OpenTest4JAwareThrowableCollector()); childContext.setTestInstances(DefaultTestInstances.of(new OuterClass())); ExtensionContext.Store childStore = childContext.getStore(Namespace.GLOBAL); @@ -271,25 +290,28 @@ void configurationParameter(Function>> extensionContextFactories() { + ExtensionRegistry extensionRegistry = mock(); Class testClass = ExtensionContextTests.class; return List.of( // named("engine", (JupiterConfiguration configuration) -> { UniqueId engineUniqueId = UniqueId.parse("[engine:junit-jupiter]"); JupiterEngineDescriptor engineDescriptor = new JupiterEngineDescriptor(engineUniqueId, configuration); - return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, __ -> null); + return new JupiterEngineExtensionContext(null, engineDescriptor, configuration, extensionRegistry); }), // named("class", (JupiterConfiguration configuration) -> { UniqueId classUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]"); ClassTestDescriptor classTestDescriptor = new ClassTestDescriptor(classUniqueId, testClass, configuration); - return new ClassExtensionContext(null, null, classTestDescriptor, configuration, null, __ -> null); + return new ClassExtensionContext(null, null, classTestDescriptor, configuration, extensionRegistry, + null); }), // named("method", (JupiterConfiguration configuration) -> { Method method = ReflectionSupport.findMethod(testClass, "extensionContextFactories").orElseThrow(); UniqueId methodUniqueId = UniqueId.parse("[engine:junit-jupiter]/[class:MyClass]/[method:myMethod]"); TestMethodTestDescriptor methodTestDescriptor = new TestMethodTestDescriptor(methodUniqueId, testClass, method, configuration); - return new MethodExtensionContext(null, null, methodTestDescriptor, configuration, null, __ -> null); + return new MethodExtensionContext(null, null, methodTestDescriptor, configuration, extensionRegistry, + null); }) // ); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java new file mode 100644 index 000000000000..09361e45095e --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java @@ -0,0 +1,244 @@ +/* + * 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.engine.extension; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +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_OUT; +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.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import org.junit.jupiter.api.AfterEach; +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.extension.PreInterruptContext; +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.12 + */ +@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_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME; + private static final AtomicBoolean interruptedTest = new AtomicBoolean(); + private static final CompletableFuture testThreadExecutionDone = new CompletableFuture<>(); + private static final AtomicReference interruptedTestThread = new AtomicReference<>(); + private static final AtomicBoolean interruptCallbackShallThrowException = new AtomicBoolean(); + private static final AtomicReference calledPreInterruptContext = new AtomicReference<>(); + + @BeforeEach + void setUp() { + interruptedTest.set(false); + interruptCallbackShallThrowException.set(false); + calledPreInterruptContext.set(null); + } + + @AfterEach + void tearDown() { + calledPreInterruptContext.set(null); + interruptedTestThread.set(null); + } + + @Test + @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) + @ResourceLock(value = SYSTEM_OUT, mode = READ_WRITE) + void testCaseWithDefaultInterruptCallbackEnabled() { + String orgValue = System.getProperty(DEFAULT_ENABLE_PROPERTY); + System.setProperty(DEFAULT_ENABLE_PROPERTY, Boolean.TRUE.toString()); + PrintStream orgOutStream = 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(orgOutStream); + 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.threadId() + " 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.PreInterruptThreadDumpPrinter.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()); + PreInterruptContext preInterruptContext = calledPreInterruptContext.get(); + assertNotNull(preInterruptContext); + assertNotNull(preInterruptContext.getThreadToInterrupt()); + assertEquals(preInterruptContext.getThreadToInterrupt(), interruptedTestThread.get()); + } + + @Test + void testCaseWithDeclaredInterruptCallbackEnabledWithSeparateThread() throws Exception { + Events tests = executeTestsForClass( + DefaultPreInterruptCallbackWithExplicitCallbackWithSeparateThreadTestCase.class).testEvents(); + assertOneFailedTest(tests); + tests.failed().assertEventsMatchExactly( + event(test(TC), finishedWithFailure(instanceOf(TimeoutException.class)))); + + //Wait until the real test thread was interrupted due to executor.shutdown(), otherwise the asserts below will be flaky. + testThreadExecutionDone.get(1, TimeUnit.SECONDS); + + assertTrue(interruptedTest.get()); + PreInterruptContext preInterruptContext = calledPreInterruptContext.get(); + assertNotNull(preInterruptContext); + assertNotNull(preInterruptContext.getThreadToInterrupt()); + assertEquals(preInterruptContext.getThreadToInterrupt(), interruptedTestThread.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()); + PreInterruptContext preInterruptContext = calledPreInterruptContext.get(); + assertNotNull(preInterruptContext); + assertNotNull(preInterruptContext.getThreadToInterrupt()); + assertEquals(preInterruptContext.getThreadToInterrupt(), interruptedTestThread.get()); + } + + private static void assertTestHasTimedOut(Events tests) { + assertOneFailedTest(tests); + tests.failed().assertEventsMatchExactly( + event(test(TC), finishedWithFailure(instanceOf(TimeoutException.class), message(TIMEOUT_ERROR_MSG), // + suppressed(0, instanceOf(InterruptedException.class))// + ))); + } + + private static void assertOneFailedTest(Events tests) { + tests.assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)); + } + + static class TestPreInterruptCallback implements PreInterruptCallback { + + @Override + public void beforeThreadInterrupt(PreInterruptContext preInterruptContext, ExtensionContext extensionContext) { + assertNotNull(extensionContext); + + calledPreInterruptContext.set(preInterruptContext); + 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); + interruptedTestThread.set(Thread.currentThread()); + 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); + interruptedTestThread.set(Thread.currentThread()); + throw ex; + } + } + } + + @ExtendWith(TestPreInterruptCallback.class) + static class DefaultPreInterruptCallbackWithExplicitCallbackWithSeparateThreadTestCase { + @Test + @Timeout(value = 200, unit = TimeUnit.MILLISECONDS, threadMode = Timeout.ThreadMode.SEPARATE_THREAD) + void test() throws InterruptedException { + try { + Thread.sleep(2000); + } + catch (InterruptedException ex) { + interruptedTest.set(true); + interruptedTestThread.set(Thread.currentThread()); + throw ex; + } + finally { + testThreadExecutionDone.complete(null); + } + } + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java index d7440eb1c985..6a2191f8e015 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SameThreadTimeoutInvocationTests.java +++ b/jupiter-tests/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/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java index 23307e43dd7b..166fcb0897e2 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/SeparateThreadTimeoutInvocationTests.java +++ b/jupiter-tests/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/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java index 01b20579b1d8..8c91d4b231c3 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TimeoutInvocationFactoryTests.java @@ -56,7 +56,8 @@ class TimeoutInvocationFactoryTests { @BeforeEach void setUp() { - parameters = new TimeoutInvocationParameters<>(invocation, timeoutDuration, () -> "description"); + parameters = new TimeoutInvocationParameters<>(invocation, timeoutDuration, () -> "description", + PreInterruptCallbackInvocation.NOOP); timeoutInvocationFactory = new TimeoutInvocationFactory(store); } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java index 1ecbc4764582..9cc73d479795 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ArchUnitTests.java @@ -108,6 +108,8 @@ void avoidAccessingStandardStreams(JavaClasses classes) { .that(are(not(name("org.junit.platform.runner.JUnitPlatformRunnerListener")))) // .that(are(not(name("org.junit.platform.testkit.engine.Events")))) // .that(are(not(name("org.junit.platform.testkit.engine.Executions")))) // + //The PreInterruptThreadDumpPrinter writes to StdOut by contract to dump threads + .that(are(not(name("org.junit.jupiter.engine.extension.PreInterruptThreadDumpPrinter")))) // .that(are(not(resideInAPackage("org.junit.platform.console.shadow.picocli")))); GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS.check(subset); } From e12e65c74be336957738847a4c1bdfdce6da0150 Mon Sep 17 00:00:00 2001 From: AndreasTu Date: Fri, 25 Oct 2024 20:43:51 +0200 Subject: [PATCH 2/3] Fixup wrong documentation link. --- .../org/junit/jupiter/api/extension/PreInterruptContext.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java index 263fc12ba9cd..117ca0c9a457 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java @@ -16,7 +16,7 @@ /** * {@code PreInterruptContext} encapsulates the context in which an - * {@link PreInterruptCallback#beforeThreadInterrupt(PreInterruptContext) beforeThreadInterrupt} method is called. + * {@link PreInterruptCallback#beforeThreadInterrupt(PreInterruptContext, ExtensionContext) beforeThreadInterrupt} method is called. * * @since 5.12 * @see PreInterruptCallback From 04a45b25f75c930d2476b162c0fb0b933289bf36 Mon Sep 17 00:00:00 2001 From: Marc Philipp Date: Tue, 29 Oct 2024 10:22:53 +0100 Subject: [PATCH 3/3] Remove redundant info --- .../jupiter/engine/extension/ExtensionContextInternal.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java index 3de267e72f31..26a1f3f316aa 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/ExtensionContextInternal.java @@ -33,8 +33,6 @@ public interface ExtensionContextInternal extends ExtensionContext { * @param the extension type * @param extensionType the extension type * @return the list of extensions - * @since 5.12 */ - @API(status = INTERNAL, since = "5.12") List getExtensions(Class extensionType); }