From bcc6fa44e925da8437cee1add6a96137f450f43d 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.err. It is disabled by default and must be enabled with: junit.jupiter.execution.timeout.threaddump.enabled = true Issue: #2938 --- .../release-notes-5.12.0-M1.adoc | 1 + .../docs/asciidoc/user-guide/extensions.adoc | 15 ++ .../asciidoc/user-guide/writing-tests.adoc | 6 + .../api/extension/ExtensionContext.java | 12 ++ .../api/extension/PreInterruptCallback.java | 45 +++++ .../org/junit/jupiter/engine/Constants.java | 12 +- .../config/CachingJupiterConfiguration.java | 6 + .../config/DefaultJupiterConfiguration.java | 5 + .../engine/config/JupiterConfiguration.java | 3 + .../descriptor/AbstractExtensionContext.java | 15 +- .../descriptor/ClassBasedTestDescriptor.java | 4 +- .../descriptor/ClassExtensionContext.java | 14 +- .../descriptor/DynamicExtensionContext.java | 5 +- .../descriptor/DynamicNodeTestDescriptor.java | 2 +- .../descriptor/JupiterEngineDescriptor.java | 3 +- .../JupiterEngineExtensionContext.java | 5 +- .../descriptor/MethodExtensionContext.java | 6 +- .../descriptor/TestMethodTestDescriptor.java | 4 +- .../TestTemplateExtensionContext.java | 7 +- .../TestTemplateTestDescriptor.java | 4 +- .../extension/MutableExtensionRegistry.java | 7 + .../PreInterruptCallbackInvocation.java | 24 +++ ...PreInterruptCallbackInvocationFactory.java | 45 +++++ .../PreInterruptThreadDumpPrinter.java | 73 +++++++ .../SameThreadTimeoutInvocation.java | 21 +- .../engine/extension/TimeoutExtension.java | 4 +- .../extension/TimeoutInvocationFactory.java | 12 +- .../api/extension/KitchenSinkExtension.java | 10 +- .../descriptor/ExtensionContextTests.java | 55 ++++-- .../extension/PreInterruptCallbackTests.java | 180 ++++++++++++++++++ .../SameThreadTimeoutInvocationTests.java | 3 +- .../SeparateThreadTimeoutInvocationTests.java | 3 +- .../TimeoutInvocationFactoryTests.java | 3 +- .../ParameterizedTestExtensionTests.java | 8 + .../tooling/support/tests/ArchUnitTests.java | 2 + 35 files changed, 573 insertions(+), 51 deletions(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.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/release-notes/release-notes-5.12.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.12.0-M1.adoc index 0e6785e9f512..a709f6329871 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 @@ -79,6 +79,7 @@ JUnit repository on GitHub. a test-scoped `ExtensionContext` in `Extension` methods called during test class instantiation. This behavior will become the default in future versions of JUnit. * `@TempDir` is now supported on test class constructors. +* Added `PreInterruptCallback` [[release-notes-5.12.0-M1-junit-vintage]] diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 3e8c551415dc..2d44b5a91a19 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -715,6 +715,21 @@ test methods. include::{testDir}/example/exception/MultipleHandlersTestCase.java[tags=user_guide] ---- +[[extensions-preinterrupt-callback]] +=== PreInterrupt Callback + +`{PreInterruptCallback}` defines the API for `Extensions` that wish to react on +`Thread.interrupt()` calls issued by Jupiter before the `Thread.interrupt()` is executed. + +This can be used to dump stacks for diagnostics, when the `Timeout` extension +interrupts tests. + +There is also a default implementation available, which will dump the stacks of all +`Threads` to `System.err`. +This default implementation need to be enabled with the +<>: +`junit.jupiter.execution.timeout.threaddump.enabled` + [[extensions-intercepting-invocations]] === Intercepting Invocations diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 8df05f70c3ba..f28e04a88c0d 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -2658,6 +2658,12 @@ NOTE: If you need more control over polling intervals and greater flexibility wi asynchronous tests, consider using a dedicated library such as link:https://github.com/awaitility/awaitility[Awaitility]. +[[writing-tests-dump-stack-timeout]] +=== Dump Stacks on Timeout + +It can be helpful for debugging to dump the stacks of all Threads, when a Timeout happened. +The <> provides a default +implementation for that. [[writing-tests-declarative-timeouts-mode]] ==== Disable @Timeout Globally diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/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..fcdc6f6cde67 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/PreInterruptCallback.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api.extension; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * {@code PreInterruptCallback} defines the API for {@link Extension + * Extensions} that wish to react on {@link Thread#interrupt()} calls issued by Jupiter + * before the {@link Thread#interrupt()} is executed. + * + *

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

+ * + *

There is also a default implementation available, which will dump the stacks of all {@link Thread Threads} + * to {@code System.err}. This default implementation need to be enabled with the jupiter property: + * {@code junit.jupiter.execution.timeout.threaddump.enabled} + * + * + * @since 5.12 + * @see org.junit.jupiter.api.Timeout + */ +@API(status = EXPERIMENTAL, since = "5.12") +public interface PreInterruptCallback extends Extension { + + /** + * Callback that is invoked before a {@link Thread} is interrupted with {@link Thread#interrupt()}. + * + *

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

+ * + * @param threadToInterrupt the target {@link Thread}, which will get interrupted. + * @param context the current extension context; never {@code null} + */ + void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) throws Exception; +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/Constants.java index 2f64dd866cd2..420d31554864 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 the default behavior of {@link org.junit.jupiter.api.extension.PreInterruptCallback} + * extension to print the stacks of all {@link Thread}s to {@code System.err} before the test is interrupted. + * + *

The default behavior is not to enable the dump fo threads. + * + * @since 5.12 + */ + @API(status = EXPERIMENTAL, since = "5.12") + public static final String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = JupiterConfiguration.EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME; + /** * Property name used to set the default test instance lifecycle mode: {@value} * @@ -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..0d9579edf323 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 isExtensionTimeoutThreadDumpEnabled() { + return (boolean) cache.computeIfAbsent(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME, + __ -> delegate.isExtensionTimeoutThreadDumpEnabled()); + } + @Override public ExecutionMode getDefaultExecutionMode() { return (ExecutionMode) cache.computeIfAbsent(DEFAULT_EXECUTION_MODE_PROPERTY_NAME, 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..838cfd5d4f87 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 isExtensionTimeoutThreadDumpEnabled() { + return configurationParameters.getBoolean(EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME).orElse(false); + } + @Override public ExecutionMode getDefaultExecutionMode() { return executionModeConverter.get(configurationParameters, DEFAULT_EXECUTION_MODE_PROPERTY_NAME, 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..4f30982b0002 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 @@ -40,6 +40,7 @@ public interface JupiterConfiguration { String DEFAULT_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_EXECUTION_MODE_PROPERTY_NAME; String DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME = Execution.DEFAULT_CLASSES_EXECUTION_MODE_PROPERTY_NAME; String EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.enabled"; + String EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.timeout.threaddump.enabled"; String DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME = TestInstance.Lifecycle.DEFAULT_LIFECYCLE_PROPERTY_NAME; String DEFAULT_DISPLAY_NAME_GENERATOR_PROPERTY_NAME = DisplayNameGenerator.DEFAULT_GENERATOR_PROPERTY_NAME; String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = MethodOrderer.DEFAULT_ORDER_PROPERTY_NAME; @@ -54,6 +55,8 @@ public interface JupiterConfiguration { boolean isExtensionAutoDetectionEnabled(); + boolean isExtensionTimeoutThreadDumpEnabled(); + 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..29886f552c1c 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,20 @@ import java.util.Collections; import java.util.LinkedHashSet; +import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Function; import org.junit.jupiter.api.extension.ExecutableInvoker; +import org.junit.jupiter.api.extension.Extension; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.execution.NamespaceAwareStore; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.engine.EngineExecutionListener; @@ -53,9 +56,10 @@ 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, + JupiterConfiguration configuration, ExtensionRegistry extensionRegistry, Function executableInvokerFactory) { this.executableInvoker = executableInvokerFactory.apply(this); @@ -67,6 +71,7 @@ abstract class AbstractExtensionContext implements Ext this.testDescriptor = testDescriptor; this.configuration = configuration; this.valuesStore = createStore(parent); + this.extensionRegistry = extensionRegistry; // @formatter:off this.tags = testDescriptor.getTags().stream() @@ -152,6 +157,14 @@ public ExecutableInvoker getExecutableInvoker() { return executableInvoker; } + @Override + public List getExtensions(Class extensionType) { + if (extensionRegistry == null) { + return Collections.emptyList(); + } + 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..2663cc052d7e 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 @@ -181,8 +181,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte ThrowableCollector throwableCollector = createThrowableCollector(); ClassExtensionContext extensionContext = new ClassExtensionContext(context.getExtensionContext(), - context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), throwableCollector, - it -> new DefaultExecutableInvoker(it, registry)); + context.getExecutionListener(), this, this.lifecycle, context.getConfiguration(), + context.getExtensionRegistry(), throwableCollector, it -> new DefaultExecutableInvoker(it, registry)); // @formatter:off return context.extend() 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..91c9cab491b1 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 @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.support.hierarchical.Node; import org.junit.platform.engine.support.hierarchical.ThrowableCollector; @@ -39,23 +40,24 @@ final class ClassExtensionContext extends AbstractExtensionContext executableInvokerFactory) { - this(parent, engineExecutionListener, testDescriptor, Lifecycle.PER_METHOD, configuration, throwableCollector, - executableInvokerFactory); + this(parent, engineExecutionListener, testDescriptor, Lifecycle.PER_METHOD, configuration, extensionRegistry, + throwableCollector, executableInvokerFactory); } ClassExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, ClassBasedTestDescriptor testDescriptor, Lifecycle lifecycle, JupiterConfiguration configuration, - ThrowableCollector throwableCollector, + ExtensionRegistry extensionRegistry, ThrowableCollector throwableCollector, Function executableInvokerFactory) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, + executableInvokerFactory); 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..a33baa6766fa 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 @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.support.hierarchical.Node; @@ -27,8 +28,10 @@ class DynamicExtensionContext extends AbstractExtensionContext executableInvokerFactory) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, + executableInvokerFactory); } @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..2dba4c9cb3a8 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 @@ -46,7 +46,7 @@ public String getLegacyReportingName() { @Override public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext context) { DynamicExtensionContext extensionContext = new DynamicExtensionContext(context.getExtensionContext(), - context.getExecutionListener(), this, context.getConfiguration(), + context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry(), it -> new DefaultExecutableInvoker(it, context.getExtensionRegistry())); // @formatter:off return context.extend() 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..527e4763974a 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 @@ -53,7 +53,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte context.getConfiguration()); EngineExecutionListener executionListener = context.getExecutionListener(); ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionListener, this, - context.getConfiguration(), it -> new DefaultExecutableInvoker(it, extensionRegistry)); + context.getConfiguration(), context.getExtensionRegistry(), + it -> new DefaultExecutableInvoker(it, extensionRegistry)); // @formatter:off return context.extend() 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..6cc26b3e9ea5 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 @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.support.hierarchical.Node; @@ -30,9 +31,11 @@ final class JupiterEngineExtensionContext extends AbstractExtensionContext executableInvokerFactory) { - super(null, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(null, engineExecutionListener, testDescriptor, configuration, extensionRegistry, + executableInvokerFactory); } @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..a29d65befbe9 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 @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.support.hierarchical.Node; import org.junit.platform.engine.support.hierarchical.ThrowableCollector; @@ -35,10 +36,11 @@ final class MethodExtensionContext extends AbstractExtensionContext executableInvokerFactory) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, + executableInvokerFactory); 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..b3c58e5b2320 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 @@ -99,8 +99,8 @@ public JupiterEngineExecutionContext prepare(JupiterEngineExecutionContext conte MutableExtensionRegistry registry = populateNewExtensionRegistry(context); ThrowableCollector throwableCollector = createThrowableCollector(); MethodExtensionContext extensionContext = new MethodExtensionContext(context.getExtensionContext(), - context.getExecutionListener(), this, context.getConfiguration(), throwableCollector, - it -> new DefaultExecutableInvoker(it, registry)); + context.getExecutionListener(), this, context.getConfiguration(), context.getExtensionRegistry(), + throwableCollector, it -> new DefaultExecutableInvoker(it, registry)); // @formatter:off JupiterEngineExecutionContext newContext = context.extend() .withExtensionRegistry(registry) 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..fdc98da361eb 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 @@ -20,6 +20,7 @@ import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.TestInstances; import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.support.hierarchical.Node; @@ -31,10 +32,12 @@ final class TestTemplateExtensionContext extends AbstractExtensionContext executableInvokerFactory) { - super(parent, engineExecutionListener, testDescriptor, configuration, executableInvokerFactory); + super(parent, engineExecutionListener, testDescriptor, configuration, extensionRegistry, + executableInvokerFactory); 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..51ed072d8e21 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 @@ -81,8 +81,8 @@ 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(), context.getExtensionRegistry(), + testInstances, it -> new DefaultExecutableInvoker(it, registry)); // @formatter:off return context.extend() 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..10a899b59c26 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,9 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg * auto-detected using Java's {@link ServiceLoader} mechanism and automatically * registered after the default extensions. * + *

If the {@link org.junit.jupiter.engine.Constants#EXTENSIONS_TIMEOUT_THREAD_DUMP_ENABLED_PROPERTY_NAME} + * configuration parameter has been set to {@code true}, the {@link PreInterruptThreadDumpPrinter} will be installed. + * * @param configuration configuration parameters used to retrieve the extension * auto-detection flag; never {@code null} * @return a new {@code ExtensionRegistry}; never {@code null} @@ -81,6 +84,10 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit registerAutoDetectedExtensions(extensionRegistry); } + if (configuration.isExtensionTimeoutThreadDumpEnabled()) { + 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..6e58a075e421 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptCallbackInvocationFactory.java @@ -0,0 +1,45 @@ +/* + * Copyright 2015-2024 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import java.util.List; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.PreInterruptCallback; +import org.junit.platform.commons.util.UnrecoverableExceptions; + +/** + * @since 5.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) -> { + for (PreInterruptCallback callback : callbacks) { + try { + callback.beforeThreadInterrupt(thread, extensionContext); + } + catch (Throwable ex) { + UnrecoverableExceptions.rethrowIfUnrecoverable(ex); + errorHandler.accept(ex); + } + } + }; + } +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/PreInterruptThreadDumpPrinter.java new file mode 100644 index 000000000000..f0801ab00eb3 --- /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.ExtensionContext; +import org.junit.jupiter.api.extension.PreInterruptCallback; +import org.junit.jupiter.engine.Constants; + +/** + * The default implementation for {@link PreInterruptCallback}, + * which will print the stacks of all {@link Thread}s to {@code System.err}. + * + *

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(Thread threadToInterrupt, ExtensionContext context) { + Map stackTraces = Thread.getAllStackTraces(); + StringBuilder sb = new StringBuilder(); + sb.append("Thread "); + appendThreadName(sb, threadToInterrupt); + sb.append(" will be interrupted."); + sb.append(NL); + for (Map.Entry entry : stackTraces.entrySet()) { + Thread thread = entry.getKey(); + StackTraceElement[] stack = entry.getValue(); + if (stack.length > 0) { + sb.append(NL); + appendThreadName(sb, thread); + for (StackTraceElement stackTraceElement : stack) { + sb.append(NL); + //Do the same prefix as java.lang.Throwable.printStackTrace(java.lang.Throwable.PrintStreamOrWriter) + sb.append("\tat "); + sb.append(stackTraceElement.toString()); + + } + sb.append(NL); + } + } + System.err.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..048e96e77260 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(Thread threadToInterrupt, ExtensionContext context) 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..9a7fbecf9e42 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; @@ -75,7 +78,7 @@ void fromJupiterEngineDescriptor() { JupiterEngineDescriptor engineTestDescriptor = new JupiterEngineDescriptor( UniqueId.root("engine", "junit-jupiter"), configuration); - try (var engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor, configuration, + try (var engineContext = new JupiterEngineExtensionContext(null, engineTestDescriptor, configuration, null, __ -> null)) { // @formatter:off assertAll("engineContext", @@ -89,7 +92,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 +105,7 @@ void fromClassTestDescriptor() { ClassTestDescriptor outerClassDescriptor = outerClassDescriptor(nestedClassDescriptor); ClassExtensionContext outerExtensionContext = new ClassExtensionContext(null, null, outerClassDescriptor, - configuration, null, __ -> null); + configuration, null, null, __ -> null); // @formatter:off assertAll("outerContext", @@ -114,15 +118,30 @@ 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, null, null, __ -> null); assertThat(nestedExtensionContext.getParent()).containsSame(outerExtensionContext); } + @Test + void ExtensionContext_With_ExtensionRegistry_getExtensions() { + NestedClassTestDescriptor classTestDescriptor = nestedClassDescriptor(); + ExtensionRegistry registry = mock(); + try (ClassExtensionContext ctx = new ClassExtensionContext(null, null, classTestDescriptor, configuration, + registry, null, __ -> null)) { + + Extension ext = mock(); + when(registry.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, null, null, __ -> null); assertThat(outerExtensionContext.getTags()).containsExactly("outer-tag"); assertThat(outerExtensionContext.getRoot()).isSameAs(outerExtensionContext); ClassExtensionContext nestedExtensionContext = new ClassExtensionContext(outerExtensionContext, null, - nestedClassDescriptor, configuration, null, __ -> null); + nestedClassDescriptor, configuration, null, null, __ -> 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, null, new OpenTest4JAwareThrowableCollector(), __ -> null); 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, null, __ -> null); ClassExtensionContext classExtensionContext = new ClassExtensionContext(engineExtensionContext, null, - classTestDescriptor, configuration, null, __ -> null); + classTestDescriptor, configuration, null, null, __ -> null); MethodExtensionContext methodExtensionContext = new MethodExtensionContext(classExtensionContext, null, - methodTestDescriptor, configuration, new OpenTest4JAwareThrowableCollector(), __ -> null); + methodTestDescriptor, configuration, null, new OpenTest4JAwareThrowableCollector(), __ -> null); 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, null, null, __ -> null); Map map1 = Collections.singletonMap("key", "value"); Map map2 = Collections.singletonMap("other key", "other value"); @@ -221,9 +240,9 @@ void usingStore() { TestMethodTestDescriptor methodTestDescriptor = methodDescriptor(); ClassTestDescriptor classTestDescriptor = outerClassDescriptor(methodTestDescriptor); ExtensionContext parentContext = new ClassExtensionContext(null, null, classTestDescriptor, configuration, null, - __ -> null); + null, __ -> null); MethodExtensionContext childContext = new MethodExtensionContext(parentContext, null, methodTestDescriptor, - configuration, new OpenTest4JAwareThrowableCollector(), __ -> null); + configuration, null, new OpenTest4JAwareThrowableCollector(), __ -> null); childContext.setTestInstances(DefaultTestInstances.of(new OuterClass())); ExtensionContext.Store childStore = childContext.getStore(Namespace.GLOBAL); @@ -273,20 +292,22 @@ void configurationParameter(Function { 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, null, __ -> null); }), // 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, null, null, + __ -> 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, null, null, + __ -> 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..68d95ff6e55a --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/PreInterruptCallbackTests.java @@ -0,0 +1,180 @@ +/* + * 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.assertTrue; +import static org.junit.jupiter.api.parallel.ResourceAccessMode.READ_WRITE; +import static org.junit.jupiter.api.parallel.Resources.SYSTEM_ERR; +import static org.junit.jupiter.api.parallel.Resources.SYSTEM_PROPERTIES; +import static org.junit.platform.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.suppressed; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.charset.StandardCharsets; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.PreInterruptCallback; +import org.junit.jupiter.api.parallel.Isolated; +import org.junit.jupiter.api.parallel.ResourceLock; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.jupiter.engine.Constants; +import org.junit.platform.testkit.engine.Events; + +/** + * @since 5.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 interruptCallbackCalled = new AtomicBoolean(); + private static final AtomicBoolean interruptCallbackShallThrowException = new AtomicBoolean(); + + @BeforeEach + void setUp() { + interruptedTest.set(true); + interruptCallbackCalled.set(false); + interruptCallbackShallThrowException.set(false); + } + + @Test + @ResourceLock(value = SYSTEM_PROPERTIES, mode = READ_WRITE) + @ResourceLock(value = SYSTEM_ERR, mode = READ_WRITE) + void testCaseWithDefaultInterruptCallbackEnabled() { + String orgValue = System.getProperty(DEFAULT_ENABLE_PROPERTY); + System.setProperty(DEFAULT_ENABLE_PROPERTY, Boolean.TRUE.toString()); + PrintStream orgErrStream = System.err; + Events tests; + String output; + try { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + PrintStream outStream = new PrintStream(buffer); + System.setErr(outStream); + tests = executeTestsForClass(DefaultPreInterruptCallbackTimeoutOnMethodTestCase.class).testEvents(); + output = buffer.toString(StandardCharsets.UTF_8); + } + finally { + System.setErr(orgErrStream); + 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()); + assertTrue(interruptCallbackCalled.get()); + } + + @Test + void testCaseWithDeclaredInterruptCallbackThrowsException() { + interruptCallbackShallThrowException.set(true); + Events tests = executeTestsForClass(DefaultPreInterruptCallbackWithExplicitCallbackTestCase.class).testEvents(); + tests.failed().assertEventsMatchExactly(event(test(TC), + finishedWithFailure(instanceOf(TimeoutException.class), message(TIMEOUT_ERROR_MSG), + suppressed(0, instanceOf(InterruptedException.class)), + suppressed(1, instanceOf(IllegalStateException.class))))); + assertTrue(interruptedTest.get()); + assertTrue(interruptCallbackCalled.get()); + } + + private static void assertTestHasTimedOut(Events tests) { + tests.assertStatistics(stats -> stats.started(1).succeeded(0).failed(1)); + tests.failed().assertEventsMatchExactly( + event(test(TC), finishedWithFailure(instanceOf(TimeoutException.class), message(TIMEOUT_ERROR_MSG), // + suppressed(0, instanceOf(InterruptedException.class))// + ))); + } + + static class TestPreInterruptCallback implements PreInterruptCallback { + + @Override + public void beforeThreadInterrupt(Thread threadToInterrupt, ExtensionContext context) { + interruptCallbackCalled.set(true); + if (interruptCallbackShallThrowException.get()) { + throw new IllegalStateException("Test-Ex"); + } + } + } + + static class DefaultPreInterruptCallbackTimeoutOnMethodTestCase { + @Test + @Timeout(value = 1, unit = TimeUnit.MICROSECONDS) + void test() throws InterruptedException { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + interruptedTest.set(true); + throw ex; + } + } + } + + @ExtendWith(TestPreInterruptCallback.class) + static class DefaultPreInterruptCallbackWithExplicitCallbackTestCase { + @Test + @Timeout(value = 1, unit = TimeUnit.MICROSECONDS) + void test() throws InterruptedException { + try { + Thread.sleep(1000); + } + catch (InterruptedException ex) { + interruptedTest.set(true); + throw ex; + } + } + } +} diff --git a/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..4ff149b738ef 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 StdErr 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); }