From 349e286da7556351e0fa056049e110e97ad1000c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Tue, 5 Dec 2023 11:54:14 +0100 Subject: [PATCH 01/11] Introduce @AutoClose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../asciidoc/user-guide/writing-tests.adoc | 1 + .../java/org/junit/jupiter/api/AutoClose.java | 57 ++++++++++ .../engine/extension/AutoCloseExtension.java | 101 ++++++++++++++++++ .../extension/MutableExtensionRegistry.java | 1 + .../extension/ExtensionRegistryTests.java | 2 +- 5 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java create mode 100644 junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 4f8d59925fbf..2bcf6e4ad3da 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -45,6 +45,7 @@ in the `junit-jupiter-api` module. | `@ExtendWith` | Used to <>. Such annotations are _inherited_. | `@RegisterExtension` | Used to <> via fields. Such fields are _inherited_ unless they are _shadowed_. | `@TempDir` | Used to supply a <> via field injection or parameter injection in a lifecycle method or test method; located in the `org.junit.jupiter.api.io` package. +| @AutoClose | Indicates that a field in a JUnit 5 test class represents a resource that should be automatically closed after test execution. |=== WARNING: Some annotations may currently be _experimental_. Consult the table in diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java new file mode 100644 index 000000000000..f20b4cf5bd43 --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java @@ -0,0 +1,57 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.apiguardian.api.API; + +/** + * The {@code AutoClose} annotation is used to automatically close resources used in JUnit 5 tests. + * + *

+ * This annotation should be applied to fields within JUnit 5 test classes. It indicates that the annotated + * resource should be automatically closed after the test execution. The annotation targets + * {@link java.lang.annotation.ElementType#FIELD} elements, allowing it to be applied to instance variables. + *

+ * + *

+ * By default, the {@code AutoClose} annotation expects the annotated resource to provide a {@code close()} method + * that will be invoked for closing the resource. However, developers can customize the closing behavior by providing + * a different method name through the {@code value} attribute. For example, setting {@code value = "destroy"} will + * look for a method named {@code destroy()} to close the resource. + *

+ * + *

+ * The {@code AutoClose} annotation is retained at runtime, allowing it to be accessed and processed during test execution. + *

+ * + * @see java.lang.annotation.Retention + * @see java.lang.annotation.Target + * @since 5.11 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@API(status = API.Status.EXPERIMENTAL, since = "5.11") +public @interface AutoClose { + + /** + * Specifies the name of the method to invoke for closing the resource. + * The default value is "close". + * + * @return the method name for closing the resource + */ + String value() default "close"; + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java new file mode 100644 index 000000000000..965f89a168d2 --- /dev/null +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java @@ -0,0 +1,101 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.function.Predicate; + +import org.apiguardian.api.API; +import org.junit.jupiter.api.AutoClose; +import org.junit.jupiter.api.extension.AfterAllCallback; +import org.junit.jupiter.api.extension.AfterEachCallback; +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; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; +import org.junit.platform.commons.util.ExceptionUtils; +import org.junit.platform.commons.util.ReflectionUtils; + +/** + * {@code AutoCloseExtension} is a JUnit Jupiter extension that closes resources if a field in a test class is annotated + * with {@link AutoClose @AutoClose}. + * + *

Consult the Javadoc for {@link AutoClose} for details on the contract. + * + * @since 5.11 + * @see AutoClose + * @see AutoCloseable + */ +@API(status = API.Status.EXPERIMENTAL, since = "5.11") +public class AutoCloseExtension implements AfterAllCallback, AfterEachCallback { + + private static final Logger logger = LoggerFactory.getLogger(AutoCloseExtension.class); + static final Namespace NAMESPACE = Namespace.create(AutoClose.class); + + @Override + public void afterAll(ExtensionContext context) { + Store contextStore = context.getStore(NAMESPACE); + Class testClass = context.getRequiredTestClass(); + + registerCloseables(contextStore, testClass, null); + } + + @Override + public void afterEach(ExtensionContext context) { + Store contextStore = context.getStore(NAMESPACE); + + for (Object instance : context.getRequiredTestInstances().getAllInstances()) { + registerCloseables(contextStore, instance.getClass(), instance); + } + } + + private void registerCloseables(Store contextStore, Class testClass, /* @Nullable */ Object testInstance) { + Predicate isStatic = testInstance == null ? ReflectionUtils::isStatic : ReflectionUtils::isNotStatic; + findAnnotatedFields(testClass, AutoClose.class, isStatic).forEach(field -> { + try { + contextStore.put(field, asCloseableResource(testInstance, field)); + } + catch (Throwable t) { + ExceptionUtils.throwAsUncheckedException(t); + } + }); + } + + private static Store.CloseableResource asCloseableResource(/* @Nullable */ Object testInstance, Field field) { + return () -> { + Object toBeClosed = ReflectionUtils.tryToReadFieldValue(field, testInstance).get(); + if (toBeClosed == null) { + logger.warn(() -> "@AutoClose: Field " + getQualifiedFieldName(field) + + " couldn't be closed because it was null."); + return; + } + getAndDestroy(field, toBeClosed); + }; + } + + private static void getAndDestroy(Field field, Object toBeClosed) { + String methodName = field.getAnnotation(AutoClose.class).value(); + Method destroyMethod = ReflectionUtils.findMethod(toBeClosed.getClass(), methodName).orElseThrow( + () -> new ExtensionConfigurationException("@AutoClose: Cannot resolve the destroy method " + methodName + + "() at " + getQualifiedFieldName(field) + ": " + field.getType().getSimpleName())); + ReflectionUtils.invokeMethod(destroyMethod, toBeClosed); + } + + private static String getQualifiedFieldName(Field field) { + return field.getDeclaringClass().getSimpleName() + "." + field.getName(); + } + +} diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/MutableExtensionRegistry.java index 0a1433505c7b..8bbd37f99f68 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 @@ -73,6 +73,7 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit DEFAULT_STATELESS_EXTENSIONS.forEach(extensionRegistry::registerDefaultExtension); extensionRegistry.registerDefaultExtension(new TempDirectory(configuration)); + extensionRegistry.registerDefaultExtension(new AutoCloseExtension()); if (configuration.isExtensionAutoDetectionEnabled()) { registerAutoDetectedExtensions(extensionRegistry); diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java index cbe91c6ea9f6..93e345326345 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java @@ -39,7 +39,7 @@ */ class ExtensionRegistryTests { - private static final int NUM_DEFAULT_EXTENSIONS = 6; + private static final int NUM_DEFAULT_EXTENSIONS = 7; private final JupiterConfiguration configuration = mock(); From e7aba0bef2a2c235c2fc32fd0d820e2435439af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Fri, 8 Dec 2023 06:55:13 +0100 Subject: [PATCH 02/11] Introduce @AutoClose #2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Test AutoCloseExtension * Register extension as stateless Signed-off-by: Björn Michael --- .../java/org/junit/jupiter/api/AutoClose.java | 5 +- .../extension/MutableExtensionRegistry.java | 4 +- .../engine/extension/AutoCloseTests.java | 191 ++++++++++++++++++ 3 files changed, 196 insertions(+), 4 deletions(-) create mode 100644 junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java index f20b4cf5bd43..5dac568dece0 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java @@ -29,8 +29,9 @@ *

* By default, the {@code AutoClose} annotation expects the annotated resource to provide a {@code close()} method * that will be invoked for closing the resource. However, developers can customize the closing behavior by providing - * a different method name through the {@code value} attribute. For example, setting {@code value = "destroy"} will - * look for a method named {@code destroy()} to close the resource. + * a different method name through the {@code value} attribute. For example, setting {@code value = "shutdown"} will + * look for a method named {@code shutdown()} to close the resource. + * When multiple annotated resources exist the order of closing them is unspecified. *

* *

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 8bbd37f99f68..c60a4703e370 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 @@ -52,7 +52,8 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg new TimeoutExtension(), // new RepeatedTestExtension(), // new TestInfoParameterResolver(), // - new TestReporterParameterResolver())); + new TestReporterParameterResolver(), // + new AutoCloseExtension())); /** * Factory for creating and populating a new root registry with the default @@ -73,7 +74,6 @@ public static MutableExtensionRegistry createRegistryWithDefaultExtensions(Jupit DEFAULT_STATELESS_EXTENSIONS.forEach(extensionRegistry::registerDefaultExtension); extensionRegistry.registerDefaultExtension(new TempDirectory(configuration)); - extensionRegistry.registerDefaultExtension(new AutoCloseExtension()); if (configuration.isExtensionAutoDetectionEnabled()) { registerAutoDetectedExtensions(extensionRegistry); diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java new file mode 100644 index 000000000000..71dd5a8ff3bb --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java @@ -0,0 +1,191 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package org.junit.jupiter.engine.extension; + +import static java.util.Arrays.asList; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.extension.ExtensionContext.Namespace; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.cause; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.jupiter.api.AutoClose; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.Events; + +/** + * Integration tests for the behavior of the {@link AutoCloseExtension} to release resources after test execution. + * + * @since 5.11 + */ +class AutoCloseTests extends AbstractJupiterTestEngineTests { + + private static final List recorder = new ArrayList<>(); + + @BeforeEach + void resetRecorder() { + recorder.clear(); + } + + @Test + void fieldsAreProperlyClosed() { + EngineExecutionResults engineExecutionResults = executeTestsForClass(AutoCloseTestCase.class); + + Events tests = engineExecutionResults.testEvents(); + tests.assertStatistics(stats -> stats.succeeded(2)); + // @formatter:off + assertEquals(asList( + "afterEach-close()", "afterEach-run()", + "afterEach-close()", "afterEach-run()", + "afterAll-close()"), recorder); + // @formatter:onf + } + + @Test + void noCloseMethod() { + String msg = "@AutoClose: Cannot resolve the destroy method close() at AutoCloseNoCloseMethodFailingTestCase.resource: String"; + + Events tests = executeTestsForClass(AutoCloseNoCloseMethodFailingTestCase.class).testEvents(); + assertFailingWithMessage(tests, msg); + } + + @Test + void noShutdownMethod() { + String msg = "@AutoClose: Cannot resolve the destroy method shutdown() at AutoCloseNoShutdownMethodFailingTestCase.resource: String"; + + Events tests = executeTestsForClass(AutoCloseNoShutdownMethodFailingTestCase.class).testEvents(); + assertFailingWithMessage(tests, msg); + } + + @Test + void namespace() { + assertEquals(Namespace.create(AutoClose.class), AutoCloseExtension.NAMESPACE); + } + + @Test + void spyPermitsOnlyASingleAction() { + AutoCloseSpy spy = new AutoCloseSpy(""); + + spy.close(); + + assertThrows(IllegalStateException.class, spy::close); + assertThrows(IllegalStateException.class, spy::run); + assertEquals(asList("close()"), recorder); + } + + private static void assertFailingWithMessage(Events testEvent, String msg) { + testEvent.assertStatistics(stats -> stats.failed(1)).assertThatEvents().haveExactly(1, + finishedWithFailure(cause(message(actual -> actual.contains(msg))))); + } + + static class AutoCloseTestCase { + + private static @AutoClose AutoCloseable staticClosable; + private static @AutoClose AutoCloseable nullStatic; + + private final @AutoClose AutoCloseable closable = new AutoCloseSpy("afterEach-"); + private final @AutoClose("run") Runnable runnable = new AutoCloseSpy("afterEach-"); + private @AutoClose AutoCloseable nullField; + + @Test + void justPass() { + assertFields(); + } + + @Test + void anotherPass() { + assertFields(); + } + + private void assertFields() { + assertNotNull(staticClosable); + assertNull(nullStatic); + + assertNotNull(closable); + assertNotNull(runnable); + assertNull(nullField); + } + + @BeforeAll + static void setup() { + staticClosable = new AutoCloseSpy("afterAll-"); + } + + } + + static class AutoCloseNoCloseMethodFailingTestCase { + + @AutoClose + private final String resource = "nothing to close()"; + + @Test + void alwaysPass() { + assertNotNull(resource); + } + + } + + static class AutoCloseNoShutdownMethodFailingTestCase { + + @AutoClose("shutdown") + private final String resource = "nothing to shutdown()"; + + @Test + void alwaysPass() { + assertNotNull(resource); + } + + } + + static class AutoCloseSpy implements AutoCloseable, Runnable { + + private final String prefix; + private String invokedMethod = ""; + + public AutoCloseSpy(String prefix) { + this.prefix = prefix; + } + + @Override + public void run() { + checkIfAlreadyInvoked(); + recordInvocation("run()"); + } + + @Override + public void close() { + checkIfAlreadyInvoked(); + recordInvocation("close()"); + } + + private void checkIfAlreadyInvoked() { + if (!invokedMethod.isEmpty()) + throw new IllegalStateException(); + } + + private void recordInvocation(String methodName) { + invokedMethod = methodName; + recorder.add(prefix + methodName); + } + + } + +} From 0c704e27b8f95f26b05827da2e4073faf565e2d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Fri, 22 Dec 2023 09:07:37 +0100 Subject: [PATCH 03/11] Code review amendments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../asciidoc/user-guide/writing-tests.adoc | 2 +- .../java/org/junit/jupiter/api/AutoClose.java | 34 ++++++------ .../engine/extension/AutoCloseExtension.java | 36 ++++++------- .../engine/extension/AutoCloseTests.java | 52 +++++++++++-------- 4 files changed, 63 insertions(+), 61 deletions(-) diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 2bcf6e4ad3da..3cd6a9a33353 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -45,7 +45,7 @@ in the `junit-jupiter-api` module. | `@ExtendWith` | Used to <>. Such annotations are _inherited_. | `@RegisterExtension` | Used to <> via fields. Such fields are _inherited_ unless they are _shadowed_. | `@TempDir` | Used to supply a <> via field injection or parameter injection in a lifecycle method or test method; located in the `org.junit.jupiter.api.io` package. -| @AutoClose | Indicates that a field in a JUnit 5 test class represents a resource that should be automatically closed after test execution. +| `@AutoClose` | Denotes that the annotated field represents a resource that should be automatically closed after test execution. |=== WARNING: Some annotations may currently be _experimental_. Consult the table in diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java index 5dac568dece0..6e53f2786473 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java @@ -18,29 +18,24 @@ import org.apiguardian.api.API; /** - * The {@code AutoClose} annotation is used to automatically close resources used in JUnit 5 tests. + * The {@code AutoClose} annotation is used to automatically close resources + * used in tests. * - *

- * This annotation should be applied to fields within JUnit 5 test classes. It indicates that the annotated - * resource should be automatically closed after the test execution. The annotation targets - * {@link java.lang.annotation.ElementType#FIELD} elements, allowing it to be applied to instance variables. - *

+ *

This annotation should be applied to fields within test classes. It + * indicates that the annotated resource should be automatically closed after + * the test execution. * - *

- * By default, the {@code AutoClose} annotation expects the annotated resource to provide a {@code close()} method - * that will be invoked for closing the resource. However, developers can customize the closing behavior by providing - * a different method name through the {@code value} attribute. For example, setting {@code value = "shutdown"} will - * look for a method named {@code shutdown()} to close the resource. - * When multiple annotated resources exist the order of closing them is unspecified. - *

- * - *

- * The {@code AutoClose} annotation is retained at runtime, allowing it to be accessed and processed during test execution. - *

+ *

By default, the {@code AutoClose} annotation expects the annotated + * resource to provide a {@code close()} method that will be invoked for closing + * the resource. However, developers can customize the closing behavior by + * providing a different method name through the {@link #value} attribute. For + * example, setting {@code value = "shutdown"} will look for a method named + * {@code shutdown()} to close the resource. When multiple annotated resources + * exist the order of closing them is unspecified. * + * @since 5.11 * @see java.lang.annotation.Retention * @see java.lang.annotation.Target - * @since 5.11 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @@ -49,7 +44,8 @@ /** * Specifies the name of the method to invoke for closing the resource. - * The default value is "close". + * + *

The default value is {@code close}. * * @return the method name for closing the resource */ diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java index 965f89a168d2..269fadc6d6fb 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java +++ b/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java @@ -16,7 +16,6 @@ import java.lang.reflect.Method; import java.util.function.Predicate; -import org.apiguardian.api.API; import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.extension.AfterAllCallback; import org.junit.jupiter.api.extension.AfterEachCallback; @@ -30,20 +29,20 @@ import org.junit.platform.commons.util.ReflectionUtils; /** - * {@code AutoCloseExtension} is a JUnit Jupiter extension that closes resources if a field in a test class is annotated - * with {@link AutoClose @AutoClose}. + * {@code AutoCloseExtension} is a JUnit Jupiter extension that closes resources + * if a field in a test class is annotated with {@link AutoClose @AutoClose}. * - *

Consult the Javadoc for {@link AutoClose} for details on the contract. + *

Consult the Javadoc for {@link AutoClose @AutoClose} for details on the + * contract. * * @since 5.11 * @see AutoClose * @see AutoCloseable */ -@API(status = API.Status.EXPERIMENTAL, since = "5.11") -public class AutoCloseExtension implements AfterAllCallback, AfterEachCallback { +class AutoCloseExtension implements AfterAllCallback, AfterEachCallback { private static final Logger logger = LoggerFactory.getLogger(AutoCloseExtension.class); - static final Namespace NAMESPACE = Namespace.create(AutoClose.class); + private static final Namespace NAMESPACE = Namespace.create(AutoClose.class); @Override public void afterAll(ExtensionContext context) { @@ -62,9 +61,9 @@ public void afterEach(ExtensionContext context) { } } - private void registerCloseables(Store contextStore, Class testClass, /* @Nullable */ Object testInstance) { - Predicate isStatic = testInstance == null ? ReflectionUtils::isStatic : ReflectionUtils::isNotStatic; - findAnnotatedFields(testClass, AutoClose.class, isStatic).forEach(field -> { + private void registerCloseables(Store contextStore, Class testClass, Object testInstance) { + Predicate predicate = testInstance == null ? ReflectionUtils::isStatic : ReflectionUtils::isNotStatic; + findAnnotatedFields(testClass, AutoClose.class, predicate).forEach(field -> { try { contextStore.put(field, asCloseableResource(testInstance, field)); } @@ -74,28 +73,29 @@ private void registerCloseables(Store contextStore, Class testClass, /* @Null }); } - private static Store.CloseableResource asCloseableResource(/* @Nullable */ Object testInstance, Field field) { + private static Store.CloseableResource asCloseableResource(Object testInstance, Field field) { return () -> { Object toBeClosed = ReflectionUtils.tryToReadFieldValue(field, testInstance).get(); if (toBeClosed == null) { - logger.warn(() -> "@AutoClose: Field " + getQualifiedFieldName(field) - + " couldn't be closed because it was null."); + logger.warn(() -> "@AutoClose couldn't close object for field " + getQualifiedFieldName(field) + + " because it was null."); return; } - getAndDestroy(field, toBeClosed); + invokeCloseMethod(field, toBeClosed); }; } - private static void getAndDestroy(Field field, Object toBeClosed) { + private static void invokeCloseMethod(Field field, Object toBeClosed) { String methodName = field.getAnnotation(AutoClose.class).value(); Method destroyMethod = ReflectionUtils.findMethod(toBeClosed.getClass(), methodName).orElseThrow( - () -> new ExtensionConfigurationException("@AutoClose: Cannot resolve the destroy method " + methodName - + "() at " + getQualifiedFieldName(field) + ": " + field.getType().getSimpleName())); + () -> new ExtensionConfigurationException( + "@AutoClose failed to close object for field " + getQualifiedFieldName(field) + " because the " + + methodName + "() method could not be " + "resolved.")); ReflectionUtils.invokeMethod(destroyMethod, toBeClosed); } private static String getQualifiedFieldName(Field field) { - return field.getDeclaringClass().getSimpleName() + "." + field.getName(); + return field.getDeclaringClass().getName() + "." + field.getName(); } } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java index 71dd5a8ff3bb..e051d93ebe56 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java @@ -11,11 +11,11 @@ package org.junit.jupiter.engine.extension; import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.extension.ExtensionContext.Namespace; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.cause; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; @@ -32,7 +32,8 @@ import org.junit.platform.testkit.engine.Events; /** - * Integration tests for the behavior of the {@link AutoCloseExtension} to release resources after test execution. + * Integration tests for the behavior of the {@link AutoCloseExtension} to + * release resources after test execution. * * @since 5.11 */ @@ -52,16 +53,18 @@ void fieldsAreProperlyClosed() { Events tests = engineExecutionResults.testEvents(); tests.assertStatistics(stats -> stats.succeeded(2)); // @formatter:off - assertEquals(asList( + assertThat(recorder).containsExactly( "afterEach-close()", "afterEach-run()", "afterEach-close()", "afterEach-run()", - "afterAll-close()"), recorder); - // @formatter:onf + "afterAll-close()"); + // @formatter:on } @Test void noCloseMethod() { - String msg = "@AutoClose: Cannot resolve the destroy method close() at AutoCloseNoCloseMethodFailingTestCase.resource: String"; + String msg = "@AutoClose failed to close object for field " + + "org.junit.jupiter.engine.extension.AutoCloseTests$AutoCloseNoCloseMethodFailingTestCase.resource " + + "because the close() method could not be resolved."; Events tests = executeTestsForClass(AutoCloseNoCloseMethodFailingTestCase.class).testEvents(); assertFailingWithMessage(tests, msg); @@ -69,17 +72,14 @@ void noCloseMethod() { @Test void noShutdownMethod() { - String msg = "@AutoClose: Cannot resolve the destroy method shutdown() at AutoCloseNoShutdownMethodFailingTestCase.resource: String"; + String msg = "@AutoClose failed to close object for field " + + "org.junit.jupiter.engine.extension.AutoCloseTests$AutoCloseNoShutdownMethodFailingTestCase.resource " + + "because the shutdown() method could not be resolved."; Events tests = executeTestsForClass(AutoCloseNoShutdownMethodFailingTestCase.class).testEvents(); assertFailingWithMessage(tests, msg); } - @Test - void namespace() { - assertEquals(Namespace.create(AutoClose.class), AutoCloseExtension.NAMESPACE); - } - @Test void spyPermitsOnlyASingleAction() { AutoCloseSpy spy = new AutoCloseSpy(""); @@ -98,12 +98,22 @@ private static void assertFailingWithMessage(Events testEvent, String msg) { static class AutoCloseTestCase { - private static @AutoClose AutoCloseable staticClosable; - private static @AutoClose AutoCloseable nullStatic; + @AutoClose + private static AutoCloseable staticClosable; + @AutoClose + private static AutoCloseable nullStatic; - private final @AutoClose AutoCloseable closable = new AutoCloseSpy("afterEach-"); - private final @AutoClose("run") Runnable runnable = new AutoCloseSpy("afterEach-"); - private @AutoClose AutoCloseable nullField; + @AutoClose + private final AutoCloseable closable = new AutoCloseSpy("afterEach-"); + @AutoClose("run") + private final Runnable runnable = new AutoCloseSpy("afterEach-"); + @AutoClose + private AutoCloseable nullField; + + @BeforeAll + static void setup() { + staticClosable = new AutoCloseSpy("afterAll-"); + } @Test void justPass() { @@ -124,11 +134,6 @@ private void assertFields() { assertNull(nullField); } - @BeforeAll - static void setup() { - staticClosable = new AutoCloseSpy("afterAll-"); - } - } static class AutoCloseNoCloseMethodFailingTestCase { @@ -177,8 +182,9 @@ public void close() { } private void checkIfAlreadyInvoked() { - if (!invokedMethod.isEmpty()) + if (!invokedMethod.isEmpty()) { throw new IllegalStateException(); + } } private void recordInvocation(String methodName) { From 0284703eabc1e2243784f43bb46a24603eaa515d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Fri, 22 Dec 2023 11:46:32 +0100 Subject: [PATCH 04/11] Example for User Guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../src/docs/asciidoc/link-attributes.adoc | 1 + .../asciidoc/user-guide/writing-tests.adoc | 23 +++++++++++ .../src/test/java/example/AutoCloseDemo.java | 40 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 documentation/src/test/java/example/AutoCloseDemo.java diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 4c9d02b679b5..7a5151946aca 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -153,6 +153,7 @@ endif::[] // Jupiter Engine :junit-jupiter-engine: {javadoc-root}/org.junit.jupiter.engine/org/junit/jupiter/engine/package-summary.html[junit-jupiter-engine] // Jupiter Extension Implementations +:AutoCloseExtension: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java[AutoCloseExtension] :DisabledCondition: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DisabledCondition.java[DisabledCondition] :RepetitionExtension: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/RepetitionExtension.java[RepetitionExtension] :TempDirectory: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java[TempDirectory] diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index 3cd6a9a33353..809c0b4d5238 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -2710,3 +2710,26 @@ following precedence rules: 2. The default `TempDirFactory` configured via the configuration parameter, if present 3. Otherwise, `org.junit.jupiter.api.io.TempDirFactory$Standard` will be used. + +[[writing-tests-built-in-extensions-AutoClose]] +==== The AutoClose Extension + +The built-in `{AutoCloseExtension}` is used to automatically close resources used in +tests. Therefore, the `@AutoClose` annotation is applied to fields within the +test class to indicate that the annotated resource should be automatically closed after +the test execution. + +By default, the `@AutoClose` annotation expects the annotated resource to provide +a `close()` method that will be invoked for closing the resource. However, developers +can customize the closing behavior by providing a different method name through the +`value` attribute. For example, setting `value = "shutdown"` will look +for a method named `shutdown()` to close the resource. + +For example, the following test declares a database connection field annotated with +`@AutoClose` that is automatically closed afterward. + +[source,java,indent=0] +.A test class using @AutoClose annotation to close used resource +---- +include::{testDir}/example/AutoCloseDemo.java[tags=user_guide_example] +---- diff --git a/documentation/src/test/java/example/AutoCloseDemo.java b/documentation/src/test/java/example/AutoCloseDemo.java new file mode 100644 index 000000000000..91be0226dd2c --- /dev/null +++ b/documentation/src/test/java/example/AutoCloseDemo.java @@ -0,0 +1,40 @@ +/* + * Copyright 2015-2023 the original author or authors. + * + * All rights reserved. This program and the accompanying materials are + * made available under the terms of the Eclipse Public License v2.0 which + * accompanies this distribution and is available at + * + * https://www.eclipse.org/legal/epl-v20.html + */ + +package example; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.ResultSet; +import java.sql.SQLException; + +import org.junit.jupiter.api.AutoClose; +import org.junit.jupiter.api.Test; + +// tag::user_guide_example[] +class AutoCloseDemo { + + @AutoClose + Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/testdb"); + + @Test + void usersTableHasEntries() throws SQLException { + ResultSet resultSet = connection.createStatement().executeQuery("SELECT * FROM users"); + + assertTrue(resultSet.next()); + } + + AutoCloseDemo() throws SQLException { + } + +} +// end::user_guide_example[] From 6387f787014762e56ffe68c726749300726b2730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Fri, 22 Dec 2023 12:53:16 +0100 Subject: [PATCH 05/11] @AutoClose + @ExtendWith(AutoCloseExtension.class) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement TestInstancePreDestroyCallback instead Signed-off-by: Björn Michael --- .../java/org/junit/jupiter/api/AutoClose.java | 2 + .../jupiter/api}/AutoCloseExtension.java | 13 +++-- .../extension/MutableExtensionRegistry.java | 3 +- .../engine/extension/AutoCloseTests.java | 47 +++++++++++++++---- .../extension/ExtensionRegistryTests.java | 2 +- 5 files changed, 47 insertions(+), 20 deletions(-) rename {junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension => junit-jupiter-api/src/main/java/org/junit/jupiter/api}/AutoCloseExtension.java (88%) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java index 6e53f2786473..82f7ae7be894 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java @@ -16,6 +16,7 @@ import java.lang.annotation.Target; import org.apiguardian.api.API; +import org.junit.jupiter.api.extension.ExtendWith; /** * The {@code AutoClose} annotation is used to automatically close resources @@ -40,6 +41,7 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) @API(status = API.Status.EXPERIMENTAL, since = "5.11") +@ExtendWith(AutoCloseExtension.class) public @interface AutoClose { /** diff --git a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java similarity index 88% rename from junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java rename to junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java index 269fadc6d6fb..6dfbbe03e17a 100644 --- a/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java @@ -8,7 +8,7 @@ * https://www.eclipse.org/legal/epl-v20.html */ -package org.junit.jupiter.engine.extension; +package org.junit.jupiter.api; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; @@ -16,13 +16,12 @@ import java.lang.reflect.Method; import java.util.function.Predicate; -import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.extension.AfterAllCallback; -import org.junit.jupiter.api.extension.AfterEachCallback; 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; +import org.junit.jupiter.api.extension.TestInstancePreDestroyCallback; import org.junit.platform.commons.logging.Logger; import org.junit.platform.commons.logging.LoggerFactory; import org.junit.platform.commons.util.ExceptionUtils; @@ -39,7 +38,7 @@ * @see AutoClose * @see AutoCloseable */ -class AutoCloseExtension implements AfterAllCallback, AfterEachCallback { +class AutoCloseExtension implements AfterAllCallback, TestInstancePreDestroyCallback { private static final Logger logger = LoggerFactory.getLogger(AutoCloseExtension.class); private static final Namespace NAMESPACE = Namespace.create(AutoClose.class); @@ -53,7 +52,7 @@ public void afterAll(ExtensionContext context) { } @Override - public void afterEach(ExtensionContext context) { + public void preDestroyTestInstance(ExtensionContext context) { Store contextStore = context.getStore(NAMESPACE); for (Object instance : context.getRequiredTestInstances().getAllInstances()) { @@ -87,11 +86,11 @@ private static Store.CloseableResource asCloseableResource(Object testInstance, private static void invokeCloseMethod(Field field, Object toBeClosed) { String methodName = field.getAnnotation(AutoClose.class).value(); - Method destroyMethod = ReflectionUtils.findMethod(toBeClosed.getClass(), methodName).orElseThrow( + Method closeMethod = ReflectionUtils.findMethod(toBeClosed.getClass(), methodName).orElseThrow( () -> new ExtensionConfigurationException( "@AutoClose failed to close object for field " + getQualifiedFieldName(field) + " because the " + methodName + "() method could not be " + "resolved.")); - ReflectionUtils.invokeMethod(destroyMethod, toBeClosed); + ReflectionUtils.invokeMethod(closeMethod, toBeClosed); } private static String getQualifiedFieldName(Field field) { 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 c60a4703e370..0a1433505c7b 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 @@ -52,8 +52,7 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg new TimeoutExtension(), // new RepeatedTestExtension(), // new TestInfoParameterResolver(), // - new TestReporterParameterResolver(), // - new AutoCloseExtension())); + new TestReporterParameterResolver())); /** * Factory for creating and populating a new root registry with the default diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java index e051d93ebe56..18da81f433d6 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java @@ -13,9 +13,11 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.cause; import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; @@ -27,13 +29,14 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; -import org.junit.platform.testkit.engine.EngineExecutionResults; import org.junit.platform.testkit.engine.Events; /** - * Integration tests for the behavior of the {@link AutoCloseExtension} to - * release resources after test execution. + * Integration tests for the behavior of the + * {@link org.junit.jupiter.api.AutoCloseExtension} to release resources after + * test execution. * * @since 5.11 */ @@ -48,9 +51,7 @@ void resetRecorder() { @Test void fieldsAreProperlyClosed() { - EngineExecutionResults engineExecutionResults = executeTestsForClass(AutoCloseTestCase.class); - - Events tests = engineExecutionResults.testEvents(); + Events tests = executeTestsForClass(AutoCloseTestCase.class).testEvents(); tests.assertStatistics(stats -> stats.succeeded(2)); // @formatter:off assertThat(recorder).containsExactly( @@ -91,6 +92,12 @@ void spyPermitsOnlyASingleAction() { assertEquals(asList("close()"), recorder); } + @Test + void instancePerClass() { + Events tests = executeTestsForClass(AutoCloseInstancePerClassTestCase.class).testEvents(); + tests.assertStatistics(stats -> stats.succeeded(2)); + } + private static void assertFailingWithMessage(Events testEvent, String msg) { testEvent.assertStatistics(stats -> stats.failed(1)).assertThatEvents().haveExactly(1, finishedWithFailure(cause(message(actual -> actual.contains(msg))))); @@ -139,11 +146,11 @@ private void assertFields() { static class AutoCloseNoCloseMethodFailingTestCase { @AutoClose - private final String resource = "nothing to close()"; + private final String field = "nothing to close()"; @Test void alwaysPass() { - assertNotNull(resource); + assertNotNull(field); } } @@ -151,11 +158,31 @@ void alwaysPass() { static class AutoCloseNoShutdownMethodFailingTestCase { @AutoClose("shutdown") - private final String resource = "nothing to shutdown()"; + private final String field = "nothing to shutdown()"; @Test void alwaysPass() { - assertNotNull(resource); + assertNotNull(field); + } + + } + + @TestInstance(PER_CLASS) + static class AutoCloseInstancePerClassTestCase { + + static boolean closed; + + @AutoClose + AutoCloseable field = () -> closed = true; + + @Test + void test1() { + assertFalse(closed); + } + + @Test + void test2() { + assertFalse(closed); } } diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java index 93e345326345..cbe91c6ea9f6 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/ExtensionRegistryTests.java @@ -39,7 +39,7 @@ */ class ExtensionRegistryTests { - private static final int NUM_DEFAULT_EXTENSIONS = 7; + private static final int NUM_DEFAULT_EXTENSIONS = 6; private final JupiterConfiguration configuration = mock(); From 18f4784fa3ed39f83001a56aac09a0cde23bb006 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Fri, 22 Dec 2023 15:23:33 +0100 Subject: [PATCH 06/11] Try: opens org.junit.jupiter.api to org.junit.platform.commons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../src/main/java/org/junit/jupiter/api/AutoCloseExtension.java | 1 - .../src/module/org.junit.jupiter.api/module-info.java | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java index 6dfbbe03e17a..32ec00c80578 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java @@ -36,7 +36,6 @@ * * @since 5.11 * @see AutoClose - * @see AutoCloseable */ class AutoCloseExtension implements AfterAllCallback, TestInstancePreDestroyCallback { diff --git a/junit-jupiter-api/src/module/org.junit.jupiter.api/module-info.java b/junit-jupiter-api/src/module/org.junit.jupiter.api/module-info.java index b6856c78a11e..e7a7668b7873 100644 --- a/junit-jupiter-api/src/module/org.junit.jupiter.api/module-info.java +++ b/junit-jupiter-api/src/module/org.junit.jupiter.api/module-info.java @@ -23,5 +23,6 @@ exports org.junit.jupiter.api.io; exports org.junit.jupiter.api.parallel; + opens org.junit.jupiter.api to org.junit.platform.commons; opens org.junit.jupiter.api.condition to org.junit.platform.commons; } From 2a378503815a72f5166e46712e6aa5c1a2fd1881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Sun, 24 Dec 2023 08:38:48 +0100 Subject: [PATCH 07/11] Code review amendments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../src/test/java/example/AutoCloseDemo.java | 15 ++++++++++++--- .../java/org/junit/jupiter/api/AutoClose.java | 1 + .../jupiter/engine/extension/AutoCloseTests.java | 4 ++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/documentation/src/test/java/example/AutoCloseDemo.java b/documentation/src/test/java/example/AutoCloseDemo.java index 91be0226dd2c..1f89d9fd30f8 100644 --- a/documentation/src/test/java/example/AutoCloseDemo.java +++ b/documentation/src/test/java/example/AutoCloseDemo.java @@ -18,13 +18,15 @@ import java.sql.SQLException; import org.junit.jupiter.api.AutoClose; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +@Disabled // tag::user_guide_example[] class AutoCloseDemo { @AutoClose - Connection connection = DriverManager.getConnection("jdbc:mysql://localhost/testdb"); + Connection connection = getJdbcConnection("jdbc:mysql://localhost/testdb"); @Test void usersTableHasEntries() throws SQLException { @@ -33,8 +35,15 @@ void usersTableHasEntries() throws SQLException { assertTrue(resultSet.next()); } - AutoCloseDemo() throws SQLException { + // ... + // end::user_guide_example[] + private static Connection getJdbcConnection(String url) { + try { + return DriverManager.getConnection(url); + } + catch (SQLException ex) { + throw new RuntimeException(ex); + } } } -// end::user_guide_example[] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java index 82f7ae7be894..8b07ccc5e61b 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java @@ -42,6 +42,7 @@ @Target(ElementType.FIELD) @API(status = API.Status.EXPERIMENTAL, since = "5.11") @ExtendWith(AutoCloseExtension.class) +@SuppressWarnings("exports") public @interface AutoClose { /** diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java index 18da81f433d6..47eef593b92a 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/AutoCloseTests.java @@ -64,7 +64,7 @@ void fieldsAreProperlyClosed() { @Test void noCloseMethod() { String msg = "@AutoClose failed to close object for field " - + "org.junit.jupiter.engine.extension.AutoCloseTests$AutoCloseNoCloseMethodFailingTestCase.resource " + + "org.junit.jupiter.engine.extension.AutoCloseTests$AutoCloseNoCloseMethodFailingTestCase.field " + "because the close() method could not be resolved."; Events tests = executeTestsForClass(AutoCloseNoCloseMethodFailingTestCase.class).testEvents(); @@ -74,7 +74,7 @@ void noCloseMethod() { @Test void noShutdownMethod() { String msg = "@AutoClose failed to close object for field " - + "org.junit.jupiter.engine.extension.AutoCloseTests$AutoCloseNoShutdownMethodFailingTestCase.resource " + + "org.junit.jupiter.engine.extension.AutoCloseTests$AutoCloseNoShutdownMethodFailingTestCase.field " + "because the shutdown() method could not be resolved."; Events tests = executeTestsForClass(AutoCloseNoShutdownMethodFailingTestCase.class).testEvents(); From a1e519bdba00508bc130b8abf5d181dc5c159427 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Sun, 24 Dec 2023 13:43:18 +0100 Subject: [PATCH 08/11] Fix AutoCloseExtension location in docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- documentation/src/docs/asciidoc/link-attributes.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/link-attributes.adoc b/documentation/src/docs/asciidoc/link-attributes.adoc index 7a5151946aca..5822bffb57aa 100644 --- a/documentation/src/docs/asciidoc/link-attributes.adoc +++ b/documentation/src/docs/asciidoc/link-attributes.adoc @@ -153,7 +153,7 @@ endif::[] // Jupiter Engine :junit-jupiter-engine: {javadoc-root}/org.junit.jupiter.engine/org/junit/jupiter/engine/package-summary.html[junit-jupiter-engine] // Jupiter Extension Implementations -:AutoCloseExtension: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/AutoCloseExtension.java[AutoCloseExtension] +:AutoCloseExtension: {current-branch}/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java[AutoCloseExtension] :DisabledCondition: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/DisabledCondition.java[DisabledCondition] :RepetitionExtension: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/RepetitionExtension.java[RepetitionExtension] :TempDirectory: {current-branch}/junit-jupiter-engine/src/main/java/org/junit/jupiter/engine/extension/TempDirectory.java[TempDirectory] From c73a072900a3d7627bfdc857ceb104586541d9e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Wed, 27 Dec 2023 06:41:49 +0100 Subject: [PATCH 09/11] Refine documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc | 5 ++++- .../jar-describe-module/junit-jupiter-api.expected.txt | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc index 2f7dc8a4d1ec..8a07333dea11 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.11.0-M1.adoc @@ -45,7 +45,10 @@ repository on GitHub. ==== New Features and Improvements -* ❓ +* The new `@AutoClose` annotation can be applied to fields within tests to automatically + close the annotated resource after test execution. See + <<../user-guide/index.adoc#writing-tests-built-in-extensions-AutoClose, User Guide>> for + details. [[release-notes-5.11.0-M1-junit-vintage]] diff --git a/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt b/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt index 0b4810a6cc60..1fdcabd2d101 100644 --- a/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt +++ b/platform-tooling-support-tests/projects/jar-describe-module/junit-jupiter-api.expected.txt @@ -9,4 +9,5 @@ requires java.base mandated requires org.apiguardian.api static transitive requires org.junit.platform.commons transitive requires org.opentest4j transitive +qualified opens org.junit.jupiter.api to org.junit.platform.commons qualified opens org.junit.jupiter.api.condition to org.junit.platform.commons From 8b699f7d6e419a134c02c5cf52f06b8e2080bd4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Sat, 30 Dec 2023 06:55:51 +0100 Subject: [PATCH 10/11] @AutoClose deserves @Documented MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../src/main/java/org/junit/jupiter/api/AutoClose.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java index 8b07ccc5e61b..575b60d8c0fd 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoClose.java @@ -10,6 +10,7 @@ package org.junit.jupiter.api; +import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; @@ -38,10 +39,11 @@ * @see java.lang.annotation.Retention * @see java.lang.annotation.Target */ -@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) -@API(status = API.Status.EXPERIMENTAL, since = "5.11") +@Retention(RetentionPolicy.RUNTIME) +@Documented @ExtendWith(AutoCloseExtension.class) +@API(status = API.Status.EXPERIMENTAL, since = "5.11") @SuppressWarnings("exports") public @interface AutoClose { From f41304a79e045f04b07a4ec870d23f86f5f1e1a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Michael?= Date: Sat, 30 Dec 2023 08:57:10 +0100 Subject: [PATCH 11/11] Use throw with throwAsUncheckedException(..) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Björn Michael --- .../src/main/java/org/junit/jupiter/api/AutoCloseExtension.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java index 32ec00c80578..52bd5cd86b11 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/AutoCloseExtension.java @@ -66,7 +66,7 @@ private void registerCloseables(Store contextStore, Class testClass, Object t contextStore.put(field, asCloseableResource(testInstance, field)); } catch (Throwable t) { - ExceptionUtils.throwAsUncheckedException(t); + throw ExceptionUtils.throwAsUncheckedException(t); } }); }