From 8e5b738d1a3ba4b8121766f17df9fab501fc8de7 Mon Sep 17 00:00:00 2001 From: AndreasTu Date: Tue, 15 Oct 2024 19:48:25 +0200 Subject: [PATCH] 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/extension/ExtensionContext.java | 12 ++ .../api/extension/PreInterruptCallback.java | 58 ++++++ .../api/extension/PreInterruptContext.java | 44 ++++ .../org/junit/jupiter/engine/Constants.java | 12 +- .../config/CachingJupiterConfiguration.java | 6 + .../config/DefaultJupiterConfiguration.java | 5 + .../engine/config/JupiterConfiguration.java | 4 + .../descriptor/AbstractExtensionContext.java | 18 +- .../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 | 51 +++++ .../extension/MutableExtensionRegistry.java | 9 + .../PreInterruptCallbackInvocation.java | 24 +++ ...PreInterruptCallbackInvocationFactory.java | 47 +++++ .../PreInterruptThreadDumpPrinter.java | 73 +++++++ .../SameThreadTimeoutInvocation.java | 21 +- .../engine/extension/TimeoutExtension.java | 4 +- .../extension/TimeoutInvocationFactory.java | 12 +- .../api/extension/KitchenSinkExtension.java | 10 +- .../descriptor/ExtensionContextTests.java | 58 ++++-- .../extension/PreInterruptCallbackTests.java | 193 ++++++++++++++++++ .../SameThreadTimeoutInvocationTests.java | 3 +- .../SeparateThreadTimeoutInvocationTests.java | 3 +- .../TimeoutInvocationFactoryTests.java | 3 +- .../ParameterizedTestExtensionTests.java | 8 + .../tooling/support/tests/ArchUnitTests.java | 2 + 38 files changed, 698 insertions(+), 78 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/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 5f8f864a4a6b..f8a2e3377829 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 @@ -83,6 +83,7 @@ JUnit repository on GitHub. a test-scoped `ExtensionContext` in `Extension` methods called during test class instantiation. This behavior will become the default in future versions of JUnit. * `@TempDir` is now supported on test class constructors. +* Added `PreInterruptCallback` [[release-notes-5.12.0-M1-junit-vintage]] 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/extension/ExtensionContext.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java index 44a3447a4e7b..d3a2622cd715 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/ExtensionContext.java @@ -10,6 +10,7 @@ package org.junit.jupiter.api.extension; +import static org.apiguardian.api.API.Status.EXPERIMENTAL; import static org.apiguardian.api.API.Status.STABLE; import java.lang.reflect.AnnotatedElement; @@ -401,6 +402,17 @@ default void publishReportEntry(String value) { @API(status = STABLE, since = "5.11") ExecutableInvoker getExecutableInvoker(); + /** + * Returns a list of registered extension at this context of the passed {@code extensionType}. + * + * @param the extension type + * @param extensionType the extension type + * @return the list of extensions + * @since 5.12 + */ + @API(status = EXPERIMENTAL, since = "5.12") + List getExtensions(Class extensionType); + /** * {@code Store} provides methods for extensions to save and retrieve data. */ 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..fba27b19b1c1 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java @@ -0,0 +1,58 @@ +/* + * 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. + * @since 5.12 + * @see PreInterruptContext + */ + @API(status = EXPERIMENTAL, since = "5.12") + void beforeThreadInterrupt(PreInterruptContext preInterruptContext) 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..795953264f25 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptContext.java @@ -0,0 +1,44 @@ +/* + * 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(); + + /** + * Get the current {@link ExtensionContext}. + * + * @return the current extension context; never {@code null} + * @since 5.12 + */ + @API(status = EXPERIMENTAL, since = "5.12") + ExtensionContext getExtensionContext(); +} 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..cc06a8006982 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,6 @@ 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..e40e92d6d39d 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,21 @@ import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import org.junit.jupiter.api.extension.ExecutableInvoker; +import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.execution.DefaultExecutableInvoker; import org.junit.jupiter.engine.execution.NamespaceAwareStore; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.EngineExecutionListener; @@ -53,20 +57,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 +157,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..12cdcc7b9978 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DefaultPreInterruptContext.java @@ -0,0 +1,51 @@ +/* + * 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.ExtensionContext; +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; + private final ExtensionContext extensionContext; + + DefaultPreInterruptContext(Thread threadToInterrupt, ExtensionContext extensionContext) { + Preconditions.notNull(threadToInterrupt, "threadToInterrupt must not be null"); + Preconditions.notNull(extensionContext, "ExtensionContext must not be null"); + this.threadToInterrupt = threadToInterrupt; + this.extensionContext = extensionContext; + } + + @Override + public Thread getThreadToInterrupt() { + return threadToInterrupt; + } + + @Override + public ExtensionContext getExtensionContext() { + return extensionContext; + } + + @Override + public String toString() { + // @formatter:off + return new ToStringBuilder(this) + .append("threadToInterrupt", this.threadToInterrupt) + .append("extensionContext", this.extensionContext) + .toString(); + // @formatter:on + } +} 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..1dcc8538c9de --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java @@ -0,0 +1,47 @@ +/* + * 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.ExtensionContext; +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(ExtensionContext extensionContext) { + final List callbacks = extensionContext.getExtensions(PreInterruptCallback.class); + if (callbacks.isEmpty()) { + return PreInterruptCallbackInvocation.NOOP; + } + return (thread, errorHandler) -> { + PreInterruptContext preInterruptContext = new DefaultPreInterruptContext(thread, extensionContext); + for (PreInterruptCallback callback : callbacks) { + try { + callback.beforeThreadInterrupt(preInterruptContext); + } + 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..bd1352c605b4 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java @@ -0,0 +1,73 @@ +/* + * 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.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) { + 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/TimeoutExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TimeoutExtension.java index f8b87a62bed8..fa393118635f 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(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..784669b4cade 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/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..74545c317acc 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,11 @@ public void interceptAfterAllMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { InvocationInterceptor.super.interceptAfterAllMethod(invocation, invocationContext, extensionContext); } + + // --- PreInterruptCallback ------------------------------------------------ + + @Override + public void beforeThreadInterrupt(PreInterruptContext preInterruptContext) 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 532f2679a0b3..0a58c37e65f9 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 } @@ -101,7 +106,7 @@ void fromClassTestDescriptor() { ClassTestDescriptor outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); ClassExtensionContext outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, - configuration, null, __ -> null); + configuration, extensionRegistry, null); // @formatter:off assertAll("outerContext", @@ -114,15 +119,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 void tagsCanBeRetrievedInExtensionContext() { NestedClassTestDescriptor nestedClassDescriptor = nestedClassDescriptor(); @@ -131,18 +150,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); @@ -160,11 +179,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 @@ -190,7 +209,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"); @@ -220,10 +239,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); @@ -268,25 +287,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..28bda0d4fe4d --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java @@ -0,0 +1,193 @@ +/* + * 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.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.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.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 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); + } + + @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()); + assertNotNull(preInterruptContext.getExtensionContext()); + } + + @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()); + assertNotNull(preInterruptContext.getExtensionContext()); + } + + 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(PreInterruptContext preInterruptContext) { + 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); + 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/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 ba4536af5594..a7d67d5af437 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 @@ -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); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java index f8d6dee01831..e0b3209c9ed0 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/params/ParameterizedTestExtensionTests.java @@ -22,6 +22,8 @@ import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -32,6 +34,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance.Lifecycle; 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.TestInstances; import org.junit.jupiter.api.parallel.ExecutionMode; @@ -289,6 +292,11 @@ public ExecutionMode getExecutionMode() { return ExecutionMode.SAME_THREAD; } + @Override + public List getExtensions(Class extensionType) { + return Collections.emptyList(); + } + @Override public ExecutableInvoker getExecutableInvoker() { return new ExecutableInvoker() { 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 3091293004dd..cbe876e600ae 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 @@ -111,6 +111,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); }