diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc index 0c9907c5208d..c52ed87f12df 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.13.0-M3.adoc @@ -66,6 +66,8 @@ to start reporting discovery issues. `static`) - Cyclic dependencies between `@Suite` classes +* Introduce feature flag for auto-closing `AutoCloseable` in Jupiter's ExtensionContext.Store + [[release-notes-5.13.0-M3-junit-jupiter]] === JUnit Jupiter diff --git a/documentation/src/docs/asciidoc/user-guide/extensions.adoc b/documentation/src/docs/asciidoc/user-guide/extensions.adoc index 5b705d057ac6..e5dd0bc4274b 100644 --- a/documentation/src/docs/asciidoc/user-guide/extensions.adoc +++ b/documentation/src/docs/asciidoc/user-guide/extensions.adoc @@ -863,17 +863,20 @@ surrounding `ExtensionContext`. Since `ExtensionContexts` may be nested, the sco inner contexts may also be limited. Consult the corresponding Javadoc for details on the methods available for storing and retrieving values via the `{ExtensionContext_Store}`. -.`ExtensionContext.Store.CloseableResource` +.`AutoCloseable` NOTE: An extension context store is bound to its extension context lifecycle. When an -extension context lifecycle ends it closes its associated store. All stored values -that are instances of `CloseableResource` are notified by an invocation of their `close()` -method in the inverse order they were added in. - -An example implementation of `CloseableResource` is shown below, using an `HttpServer` +extension context lifecycle ends it closes its associated store. As of JUnit 5.13, +all stored values that are instances of `AutoCloseable` are notified by an invocation of +their `close()` method in the inverse order they were added in. (unless the +`junit.jupiter.extensions.store.close.autocloseable.enabled` configuration parameter +is set to false). Older versions supported `CloseableResource`, which is still +available for backward compatibility but is no longer recommended. + +An example implementation of `AutoCloseable` is shown below, using an `HttpServer` resource. [source,java,indent=0] -.`HttpServer` resource implementing `CloseableResource` +.`HttpServer` resource implementing `AutoCloseable` ---- include::{testDir}/example/extensions/HttpServerResource.java[tags=user_guide] ---- @@ -896,6 +899,28 @@ include::{testDir}/example/extensions/HttpServerExtension.java[tags=user_guide] include::{testDir}/example/HttpServerDemo.java[tags=user_guide] ---- +[[extensions-keeping-state-migration]] +==== Migration Note for Resource Cleanup + +Starting with JUnit Jupiter 5.13, the framework automatically closes resources stored in the +`ExtensionContext.Store` that implement `AutoCloseable` when auto-close is enabled (which is the default behavior). +Prior to 5.13, only resources implementing `Store.CloseableResource` were automatically closed. + +If you're developing an extension that needs to support both JUnit Jupiter 5.13+ and earlier versions, +and your extension stores resources that need to be cleaned up, you should implement both interfaces: + +[source,java,indent=0] +---- +public class MyResource implements Store.CloseableResource, AutoCloseable { + @Override + public void close() throws Exception { + // Resource cleanup code + } +} +---- + +This ensures that your resource will be properly closed regardless of which JUnit Jupiter version is being used. + [[extensions-supported-utilities]] === Supported Utilities in Extensions diff --git a/documentation/src/test/java/example/extensions/HttpServerResource.java b/documentation/src/test/java/example/extensions/HttpServerResource.java index 845e88773fdc..24108f7a6484 100644 --- a/documentation/src/test/java/example/extensions/HttpServerResource.java +++ b/documentation/src/test/java/example/extensions/HttpServerResource.java @@ -19,13 +19,11 @@ import com.sun.net.httpserver.HttpServer; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; - /** - * Demonstrates an implementation of {@link CloseableResource} using an {@link HttpServer}. + * Demonstrates an implementation of {@link AutoCloseable} using an {@link HttpServer}. */ // tag::user_guide[] -class HttpServerResource implements CloseableResource { +class HttpServerResource implements AutoCloseable { private final HttpServer httpServer; 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 194215938d69..ae6daf29e2c3 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 @@ -503,7 +503,9 @@ interface Store { * inverse order they were added in. * * @since 5.1 + * @deprecated Please extend {@code AutoCloseable} directly. */ + @Deprecated @API(status = STABLE, since = "5.1") interface CloseableResource { @@ -595,9 +597,11 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { *

See {@link #getOrComputeIfAbsent(Object, Function, Class)} for * further details. * - *

If {@code type} implements {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If {@code type} implements {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param type the type of object to retrieve; never {@code null} * @param the key and value type @@ -606,6 +610,7 @@ default V getOrDefault(Object key, Class requiredType, V defaultValue) { * @see #getOrComputeIfAbsent(Object, Function) * @see #getOrComputeIfAbsent(Object, Function, Class) * @see CloseableResource + * @see AutoCloseable */ @API(status = STABLE, since = "5.1") default V getOrComputeIfAbsent(Class type) { @@ -625,9 +630,11 @@ default V getOrComputeIfAbsent(Class type) { *

For greater type safety, consider using * {@link #getOrComputeIfAbsent(Object, Function, Class)} instead. * - *

If the created value is an instance of {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If the created value is an instance of {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param key the key; never {@code null} * @param defaultCreator the function called with the supplied {@code key} @@ -638,6 +645,7 @@ default V getOrComputeIfAbsent(Class type) { * @see #getOrComputeIfAbsent(Class) * @see #getOrComputeIfAbsent(Object, Function, Class) * @see CloseableResource + * @see AutoCloseable */ Object getOrComputeIfAbsent(K key, Function defaultCreator); @@ -652,9 +660,11 @@ default V getOrComputeIfAbsent(Class type) { * a new value will be computed by the {@code defaultCreator} (given * the {@code key} as input), stored, and returned. * - *

If {@code requiredType} implements {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If {@code requiredType} implements {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param key the key; never {@code null} * @param defaultCreator the function called with the supplied {@code key} @@ -666,6 +676,7 @@ default V getOrComputeIfAbsent(Class type) { * @see #getOrComputeIfAbsent(Class) * @see #getOrComputeIfAbsent(Object, Function) * @see CloseableResource + * @see AutoCloseable */ V getOrComputeIfAbsent(K key, Function defaultCreator, Class requiredType); @@ -676,14 +687,17 @@ default V getOrComputeIfAbsent(Class type) { * ExtensionContexts} for the store's {@code Namespace} unless they * overwrite it. * - *

If the {@code value} is an instance of {@link ExtensionContext.Store.CloseableResource} - * the {@code close()} method will be invoked on the stored object when - * the store is closed. + *

If the {@code value} is an instance of {@link CloseableResource} or + * {@link AutoCloseable} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then the {@code close()} + * method will be invoked on the stored object when the store is closed. * * @param key the key under which the value should be stored; never * {@code null} * @param value the value to store; may be {@code null} * @see CloseableResource + * @see AutoCloseable */ void put(Object key, Object value); @@ -691,8 +705,8 @@ default V getOrComputeIfAbsent(Class type) { * Remove the value that was previously stored under the supplied {@code key}. * *

The value will only be removed in the current {@link ExtensionContext}, - * not in ancestors. In addition, the {@link CloseableResource} API will not - * be honored for values that are manually removed via this method. + * not in ancestors. In addition, the {@link CloseableResource} and {@link AutoCloseable} + * API will not be honored for values that are manually removed via this method. * *

For greater type safety, consider using {@link #remove(Object, Class)} * instead. @@ -709,8 +723,8 @@ default V getOrComputeIfAbsent(Class type) { * under the supplied {@code key}. * *

The value will only be removed in the current {@link ExtensionContext}, - * not in ancestors. In addition, the {@link CloseableResource} API will not - * be honored for values that are manually removed via this method. + * not in ancestors. In addition, the {@link CloseableResource} and {@link AutoCloseable} + * API will not be honored for values that are manually removed via this method. * * @param key the key; never {@code null} * @param requiredType the required type of the value; never {@code null} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java index 81bae81f853c..7f4847304c06 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/extension/TestInstantiationAwareExtension.java @@ -16,7 +16,6 @@ import org.apiguardian.api.API; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtensionContext.Store; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; /** * Interface for {@link Extension Extensions} that are aware and can influence @@ -65,9 +64,11 @@ public interface TestInstantiationAwareExtension extends Extension { *

  • {@link ExtensionContext#getTestMethod() getTestMethod()} is no longer * empty, unless the {@link TestInstance.Lifecycle#PER_CLASS PER_CLASS} * lifecycle is used.
  • - *
  • If the callback adds a new {@link CloseableResource} to the - * {@link Store Store}, the resource is closed just after the instance is - * destroyed.
  • + *
  • If the callback adds a new {@link Store.CloseableResource} or + * {@link AutoCloseable} to the {@link Store Store} (unless the + * {@code junit.jupiter.extensions.store.close.autocloseable.enabled} + * configuration parameter is set to {@code false}), then + * the resource is closed just after the instance is destroyed.
  • *
  • The callbacks can now access data previously stored by * {@link TestTemplateInvocationContext}, unless the * {@link TestInstance.Lifecycle#PER_CLASS PER_CLASS} lifecycle is used.
  • 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 51ca2c102ca2..321a707be394 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 @@ -210,6 +210,16 @@ public final class Constants { @API(status = STABLE, since = "5.10") public static final String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = JupiterConfiguration.PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME; + /** + * Property name used to enable auto-closing of {@link AutoCloseable} instances + * + *

    By default, auto-closing is enabled. + * + * @since 5.13 + */ + @API(status = EXPERIMENTAL, since = "5.13") + public static final String CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME = JupiterConfiguration.CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME; + /** * Property name used to set the default test execution mode: {@value} * diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/config/CachingJupiterConfiguration.java index 170a8c2be817..ecab219838f3 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 @@ -69,6 +69,12 @@ public boolean isParallelExecutionEnabled() { __ -> delegate.isParallelExecutionEnabled()); } + @Override + public boolean isClosingStoredAutoCloseablesEnabled() { + return (boolean) cache.computeIfAbsent(CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME, + __ -> delegate.isClosingStoredAutoCloseablesEnabled()); + } + @Override public boolean isExtensionAutoDetectionEnabled() { return (boolean) cache.computeIfAbsent(EXTENSIONS_AUTODETECTION_ENABLED_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 7f24180acea7..c6ab8b0d5508 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 @@ -112,6 +112,11 @@ public boolean isParallelExecutionEnabled() { return configurationParameters.getBoolean(PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME).orElse(false); } + @Override + public boolean isClosingStoredAutoCloseablesEnabled() { + return configurationParameters.getBoolean(CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME).orElse(true); + } + @Override public boolean isExtensionAutoDetectionEnabled() { return configurationParameters.getBoolean(EXTENSIONS_AUTODETECTION_ENABLED_PROPERTY_NAME).orElse(false); 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 c9b2781ea73e..ca4f8ff76a7d 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 @@ -42,6 +42,7 @@ public interface JupiterConfiguration { String EXTENSIONS_AUTODETECTION_EXCLUDE_PROPERTY_NAME = "junit.jupiter.extensions.autodetection.exclude"; String DEACTIVATE_CONDITIONS_PATTERN_PROPERTY_NAME = "junit.jupiter.conditions.deactivate"; String PARALLEL_EXECUTION_ENABLED_PROPERTY_NAME = "junit.jupiter.execution.parallel.enabled"; + String CLOSING_STORED_AUTO_CLOSEABLE_ENABLED_PROPERTY_NAME = "junit.jupiter.extensions.store.close.autocloseable.enabled"; 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"; @@ -49,7 +50,7 @@ public interface JupiterConfiguration { 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; - String DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME;; + String DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME = ClassOrderer.DEFAULT_ORDER_PROPERTY_NAME; String DEFAULT_TEST_INSTANTIATION_EXTENSION_CONTEXT_SCOPE_PROPERTY_NAME = ExtensionContextScope.DEFAULT_SCOPE_PROPERTY_NAME; Predicate> getFilterForAutoDetectedExtensions(); @@ -60,6 +61,8 @@ public interface JupiterConfiguration { boolean isParallelExecutionEnabled(); + boolean isClosingStoredAutoCloseablesEnabled(); + boolean isExtensionAutoDetectionEnabled(); boolean isThreadDumpOnTimeoutEnabled(); 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 84e716e01428..219acbb474c7 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 @@ -27,7 +27,6 @@ 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.extension.MediaType; import org.junit.jupiter.api.function.ThrowingConsumer; import org.junit.jupiter.api.parallel.ExecutionMode; @@ -36,6 +35,8 @@ import org.junit.jupiter.engine.extension.ExtensionContextInternal; import org.junit.jupiter.engine.extension.ExtensionRegistry; import org.junit.platform.commons.JUnitException; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.Preconditions; import org.junit.platform.commons.util.UnrecoverableExceptions; import org.junit.platform.engine.EngineExecutionListener; @@ -51,22 +52,17 @@ */ abstract class AbstractExtensionContext implements ExtensionContextInternal, AutoCloseable { - private static final NamespacedHierarchicalStore.CloseAction CLOSE_RESOURCES = ( - __, ___, value) -> { - if (value instanceof CloseableResource) { - ((CloseableResource) value).close(); - } - }; + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractExtensionContext.class); private final ExtensionContext parent; private final EngineExecutionListener engineExecutionListener; private final T testDescriptor; private final Set tags; private final JupiterConfiguration configuration; - private final NamespacedHierarchicalStore valuesStore; private final ExecutableInvoker executableInvoker; private final ExtensionRegistry extensionRegistry; private final LauncherStoreFacade launcherStoreFacade; + private final NamespacedHierarchicalStore valuesStore; AbstractExtensionContext(ExtensionContext parent, EngineExecutionListener engineExecutionListener, T testDescriptor, JupiterConfiguration configuration, ExtensionRegistry extensionRegistry, @@ -80,7 +76,6 @@ abstract class AbstractExtensionContext implements Ext this.engineExecutionListener = engineExecutionListener; this.testDescriptor = testDescriptor; this.configuration = configuration; - this.valuesStore = createStore(parent, launcherStoreFacade); this.extensionRegistry = extensionRegistry; this.launcherStoreFacade = launcherStoreFacade; @@ -89,10 +84,33 @@ abstract class AbstractExtensionContext implements Ext .map(TestTag::getName) .collect(collectingAndThen(toCollection(LinkedHashSet::new), Collections::unmodifiableSet)); // @formatter:on + + this.valuesStore = createStore(parent, launcherStoreFacade, createCloseAction()); + } + + @SuppressWarnings("deprecation") + private NamespacedHierarchicalStore.CloseAction createCloseAction() { + return (__, ___, value) -> { + boolean isAutoCloseEnabled = this.configuration.isClosingStoredAutoCloseablesEnabled(); + + if (value instanceof AutoCloseable && isAutoCloseEnabled) { + ((AutoCloseable) value).close(); + return; + } + + if (value instanceof Store.CloseableResource) { + if (isAutoCloseEnabled) { + LOGGER.warn( + () -> "Type implements CloseableResource but not AutoCloseable: " + value.getClass().getName()); + } + ((Store.CloseableResource) value).close(); + } + }; } private static NamespacedHierarchicalStore createStore( - ExtensionContext parent, LauncherStoreFacade launcherStoreFacade) { + ExtensionContext parent, LauncherStoreFacade launcherStoreFacade, + NamespacedHierarchicalStore.CloseAction closeAction) { NamespacedHierarchicalStore parentStore; if (parent == null) { parentStore = launcherStoreFacade.getRequestLevelStore(); @@ -100,7 +118,7 @@ private static NamespacedHierarchicalStore) parent).valuesStore; } - return new NamespacedHierarchicalStore<>(parentStore, CLOSE_RESOURCES); + return new NamespacedHierarchicalStore<>(parentStore, closeAction); } @Override diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java index 7d00f097e345..d619da57f849 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java @@ -53,7 +53,7 @@ import org.junit.jupiter.api.extension.ExtensionConfigurationException; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; +import org.junit.jupiter.api.extension.ExtensionContext.Store; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; import org.junit.jupiter.api.io.CleanupMode; @@ -135,11 +135,7 @@ private static void installFailureTracker(ExtensionContext context) { } private static void installFailureTracker(ExtensionContext context, ExtensionContext parentContext) { - context.getStore(NAMESPACE).put(FAILURE_TRACKER, (CloseableResource) () -> { - if (selfOrChildFailed(context)) { - getContextSpecificStore(parentContext).put(CHILD_FAILED, true); - } - }); + context.getStore(NAMESPACE).put(FAILURE_TRACKER, new FailureTracker(context, parentContext)); } private void injectStaticFields(ExtensionContext context, Class testClass) { @@ -298,7 +294,8 @@ private static ExtensionContext.Store getContextSpecificStore(ExtensionContext c return context.getStore(NAMESPACE.append(context)); } - static class CloseablePath implements CloseableResource { + @SuppressWarnings("deprecation") + static class CloseablePath implements Store.CloseableResource, AutoCloseable { private static final Logger LOGGER = LoggerFactory.getLogger(CloseablePath.class); @@ -613,4 +610,23 @@ public String toString() { } + @SuppressWarnings("deprecation") + private static class FailureTracker implements Store.CloseableResource, AutoCloseable { + + private final ExtensionContext context; + private final ExtensionContext parentContext; + + private FailureTracker(ExtensionContext context, ExtensionContext parentContext) { + this.context = context; + this.parentContext = parentContext; + } + + @Override + public void close() { + if (selfOrChildFailed(context)) { + getContextSpecificStore(parentContext).put(CHILD_FAILED, true); + } + } + } + } 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 5e9b04c18ef8..7843c012711b 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 @@ -17,7 +17,6 @@ import org.junit.jupiter.api.Timeout.ThreadMode; import org.junit.jupiter.api.extension.ExtensionContext.Store; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; import org.junit.platform.commons.JUnitException; import org.junit.platform.commons.util.Preconditions; @@ -52,7 +51,8 @@ private ScheduledExecutorService getThreadExecutorForSameThreadInvocation() { return store.getOrComputeIfAbsent(SingleThreadExecutorResource.class).get(); } - private static abstract class ExecutorResource implements CloseableResource { + @SuppressWarnings({ "deprecation", "try" }) + private static abstract class ExecutorResource implements Store.CloseableResource, AutoCloseable { protected final ScheduledExecutorService executor; @@ -65,7 +65,7 @@ ScheduledExecutorService get() { } @Override - public void close() throws Throwable { + public void close() throws Exception { executor.shutdown(); boolean terminated = executor.awaitTermination(5, TimeUnit.SECONDS); if (!terminated) { @@ -75,6 +75,7 @@ public void close() throws Throwable { } } + @SuppressWarnings("try") static class SingleThreadExecutorResource extends ExecutorResource { @SuppressWarnings("unused") diff --git a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java index 207817c69ebb..f439e8a23afb 100644 --- a/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java +++ b/junit-jupiter-params/src/main/java/org/junit/jupiter/params/ParameterizedInvocationContext.java @@ -76,7 +76,8 @@ private void storeParameterInfo(ExtensionContext context) { new DefaultParameterInfo(declarations, accessor).store(context); } - private static class CloseableArgument implements ExtensionContext.Store.CloseableResource { + @SuppressWarnings({ "deprecation", "try" }) + private static class CloseableArgument implements ExtensionContext.Store.CloseableResource, AutoCloseable { private final AutoCloseable autoCloseable; @@ -85,7 +86,7 @@ private static class CloseableArgument implements ExtensionContext.Store.Closeab } @Override - public void close() throws Throwable { + public void close() throws Exception { this.autoCloseable.close(); } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java index d6a60aa09e08..0cec81f6cdf2 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/api/extension/CloseableResourceIntegrationTests.java @@ -46,8 +46,7 @@ void closesCloseableResourcesInExtensionContext(ExtensionContext extensionContex store.put("baz", reportEntryOnClose(extensionContext, "3")); } - private ExtensionContext.Store.CloseableResource reportEntryOnClose(ExtensionContext extensionContext, - String key) { + private AutoCloseable reportEntryOnClose(ExtensionContext extensionContext, String key) { return () -> extensionContext.publishReportEntry(Map.of(key, "closed")); } } @@ -80,9 +79,16 @@ static class ThrowingOnCloseExtension implements BeforeEachCallback { @Override public void beforeEach(ExtensionContext context) { - context.getStore(GLOBAL).put("throwingResource", (ExtensionContext.Store.CloseableResource) () -> { - throw new RuntimeException("Exception in onClose"); - }); + context.getStore(GLOBAL).put("throwingResource", new ThrowingResource()); + } + } + + @SuppressWarnings({ "deprecation", "try" }) + static class ThrowingResource implements ExtensionContext.Store.CloseableResource, AutoCloseable { + + @Override + public void close() throws Exception { + throw new RuntimeException("Exception in onClose"); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java index be8a076bfe45..cd15da225011 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/ClassTemplateInvocationTests.java @@ -74,7 +74,6 @@ 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.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -1175,7 +1174,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } } - static class SomeResource implements CloseableResource { + static class SomeResource implements AutoCloseable { private boolean closed; @Override @@ -1439,7 +1438,8 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } - private static class CustomCloseableResource implements CloseableResource { + @SuppressWarnings("deprecation") + private static class CustomCloseableResource implements ExtensionContext.Store.CloseableResource { static boolean closed; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java index 677df08911b0..5cb6dba4dd82 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/TestTemplateInvocationTests.java @@ -61,7 +61,6 @@ 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.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -924,7 +923,7 @@ public Object resolveParameter(ParameterContext parameterContext, ExtensionConte } - private static class CustomCloseableResource implements CloseableResource { + private static class CustomCloseableResource implements AutoCloseable { static boolean closed; diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java new file mode 100644 index 000000000000..f8393cbc8905 --- /dev/null +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/descriptor/ResourceAutoClosingTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2015-2025 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.descriptor; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.util.logging.Level; +import java.util.logging.LogRecord; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.fixtures.TrackLogRecords; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.jupiter.engine.extension.ExtensionRegistry; +import org.junit.platform.commons.logging.LogRecordListener; +import org.junit.platform.launcher.core.NamespacedHierarchicalStoreProviders; +import org.junit.platform.testkit.engine.ExecutionRecorder; + +class ResourceAutoClosingTests { + + private final JupiterConfiguration configuration = mock(); + private final ExtensionRegistry extensionRegistry = mock(); + private final JupiterEngineDescriptor testDescriptor = mock(); + private final LauncherStoreFacade launcherStoreFacade = new LauncherStoreFacade( + NamespacedHierarchicalStoreProviders.dummyNamespacedHierarchicalStore()); + + @Test + void shouldCloseAutoCloseableWhenIsClosingStoredAutoCloseablesEnabledIsTrue() throws Exception { + AutoCloseableResource resource = new AutoCloseableResource(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(true); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(null, testDescriptor, configuration, + extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(resource.closed).isTrue(); + } + + @Test + void shouldNotCloseAutoCloseableWhenIsClosingStoredAutoCloseablesEnabledIsFalse() throws Exception { + AutoCloseableResource resource = new AutoCloseableResource(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(false); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(null, testDescriptor, configuration, + extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(resource.closed).isFalse(); + } + + @Test + void shouldLogWarningWhenResourceImplementsCloseableResourceButNotAutoCloseableAndConfigIsTrue( + @TrackLogRecords LogRecordListener listener) throws Exception { + ExecutionRecorder executionRecorder = new ExecutionRecorder(); + CloseableResource resource = new CloseableResource(); + String msg = "Type implements CloseableResource but not AutoCloseable: " + resource.getClass().getName(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(true); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionRecorder, testDescriptor, + configuration, extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(listener.stream(Level.WARNING)).map(LogRecord::getMessage).anyMatch(msg::equals); + assertThat(resource.closed).isTrue(); + } + + @Test + void shouldNotLogWarningWhenResourceImplementsCloseableResourceAndAutoCloseableAndConfigIsFalse( + @TrackLogRecords LogRecordListener listener) throws Exception { + ExecutionRecorder executionRecorder = new ExecutionRecorder(); + CloseableResource resource = new CloseableResource(); + when(configuration.isClosingStoredAutoCloseablesEnabled()).thenReturn(false); + + ExtensionContext extensionContext = new JupiterEngineExtensionContext(executionRecorder, testDescriptor, + configuration, extensionRegistry, launcherStoreFacade); + ExtensionContext.Store store = extensionContext.getStore(ExtensionContext.Namespace.GLOBAL); + store.put("resource", resource); + + ((AutoCloseable) extensionContext).close(); + + assertThat(listener.stream(Level.WARNING)).isEmpty(); + assertThat(resource.closed).isTrue(); + } + + static class AutoCloseableResource implements AutoCloseable { + private boolean closed = false; + + @Override + public void close() { + closed = true; + } + } + + @SuppressWarnings("deprecation") + static class CloseableResource implements ExtensionContext.Store.CloseableResource { + private boolean closed = false; + + @Override + public void close() { + closed = true; + } + } +} diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java index 8e670d0a3601..8433ad7f5824 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstanceFactoryTests.java @@ -743,8 +743,7 @@ public Object createTestInstance(TestInstanceFactoryContext factoryContext, Exte instantiated(getClass(), testClass); extensionContext.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add( - "close " + testClass.getSimpleName())); + (AutoCloseable) () -> callSequence.add("close " + testClass.getSimpleName())); if (factoryContext.getOuterInstance().isPresent()) { return ReflectionSupport.newInstance(testClass, factoryContext.getOuterInstance().get()); diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java index 8299c1cbff1e..698c59068383 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePostProcessorTests.java @@ -195,8 +195,7 @@ public void postProcessTestInstance(Object testInstance, ExtensionContext contex String instanceType = testInstance.getClass().getSimpleName(); callSequence.add(name + ":" + instanceType); context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add( - "close:" + name + ":" + instanceType)); + (AutoCloseable) () -> callSequence.add("close:" + name + ":" + instanceType)); } } diff --git a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java index a73e0c494505..11be7325151a 100644 --- a/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java +++ b/jupiter-tests/src/test/java/org/junit/jupiter/engine/extension/TestInstancePreConstructCallbackTests.java @@ -553,8 +553,7 @@ else if (context.getTestInstanceLifecycle().orElse(null) != TestInstance.Lifecyc callSequence.add("PreConstructCallback: name=" + name + ", testClass=" + testClass + ", outerInstance: " + factoryContext.getOuterInstance().orElse(null)); context.getStore(ExtensionContext.Namespace.create(this)).put(new Object(), - (ExtensionContext.Store.CloseableResource) () -> callSequence.add( - "close: name=" + name + ", testClass=" + testClass)); + (AutoCloseable) () -> callSequence.add("close: name=" + name + ", testClass=" + testClass)); } } diff --git a/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java b/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java index ce5370f1ec28..ab9207301eff 100644 --- a/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java +++ b/platform-tests/src/test/java/org/junit/jupiter/extensions/Heavyweight.java @@ -18,7 +18,6 @@ import org.junit.jupiter.api.extension.BeforeEachCallback; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolver; @@ -52,12 +51,12 @@ interface Resource { /** * Demo resource class. * - *

    The class implements interface {@link CloseableResource} + *

    The class implements interface {@link AutoCloseable} * and interface {@link AutoCloseable} to show and ensure that a single * {@link ResourceValue#close()} method implementation is needed to comply * with both interfaces. */ - static class ResourceValue implements Resource, CloseableResource, AutoCloseable { + static class ResourceValue implements Resource, AutoCloseable { static final AtomicInteger creations = new AtomicInteger(); private final AtomicInteger usages = new AtomicInteger(); @@ -80,7 +79,7 @@ public int usages() { } } - private static class CloseableOnlyOnceResource implements CloseableResource { + private static class CloseableOnlyOnceResource implements AutoCloseable { private final AtomicBoolean closed = new AtomicBoolean(); diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java index c278f0a8c0e8..ce872f8f0f95 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/ManagedResource.java @@ -20,7 +20,6 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtensionContext; import org.junit.jupiter.api.extension.ExtensionContext.Namespace; -import org.junit.jupiter.api.extension.ExtensionContext.Store.CloseableResource; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; @@ -100,7 +99,8 @@ private Resource getOrCreateResource(ExtensionContext extensionContext, C } } - class Resource implements CloseableResource { + @SuppressWarnings("try") + class Resource implements AutoCloseable { private final T value; @@ -115,7 +115,7 @@ private T get() { } @Override - public void close() throws Throwable { + public void close() throws Exception { ((AutoCloseable) value).close(); } } diff --git a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java index 7d31baf23423..9621414e9d4c 100644 --- a/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java +++ b/platform-tooling-support-tests/src/test/java/platform/tooling/support/tests/OutputAttachingExtension.java @@ -76,10 +76,11 @@ private static boolean notEmpty(Path file) { } } - record OutputDir(Path root) implements ExtensionContext.Store.CloseableResource { + @SuppressWarnings("try") + record OutputDir(Path root) implements AutoCloseable { @Override - public void close() throws Throwable { + public void close() throws Exception { try (var stream = Files.walk(root).sorted(Comparator. naturalOrder().reversed())) { stream.forEach(path -> { try {