Skip to content

Commit

Permalink
Introduce @autoclose
Browse files Browse the repository at this point in the history
Signed-off-by: Björn Michael <b.michael@gmx.de>
  • Loading branch information
bjmi committed Dec 5, 2023
1 parent 9115e23 commit 965af1b
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ in the `junit-jupiter-api` module.
| `@ExtendWith` | Used to <<extensions-registration-declarative,register extensions declaratively>>. Such annotations are _inherited_.
| `@RegisterExtension` | Used to <<extensions-registration-programmatic,register extensions programmatically>> via fields. Such fields are _inherited_ unless they are _shadowed_.
| `@TempDir` | Used to supply a <<writing-tests-built-in-extensions-TempDirectory,temporary directory>> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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.
*
* <p>
* 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.
* </p>
*
* <p>
* 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.
* </p>
*
* <p>
* The {@code AutoClose} annotation is retained at runtime, allowing it to be accessed and processed during test execution.
* </p>
*
* @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";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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}.
*
* <p>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<Field> 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();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down

0 comments on commit 965af1b

Please sign in to comment.