diff --git a/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0-M1.adoc b/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0-M1.adoc index e2535a0aba9..981a2e48c7b 100644 --- a/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0-M1.adoc +++ b/documentation/src/docs/asciidoc/release-notes/release-notes-5.9.0-M1.adoc @@ -55,4 +55,6 @@ GitHub. ==== New Features and Improvements -* ❓ +* `@TempDir` now includes a cleanup mode attribute for preventing a temporary directory + from being deleted after a test. The default cleanup mode can be configured via a + configuration parameter. diff --git a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc index d5a54c0e317..45b6ffb780d 100644 --- a/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc +++ b/documentation/src/docs/asciidoc/user-guide/writing-tests.adoc @@ -2269,3 +2269,18 @@ method uses a separate directory. ---- include::{testDir}/example/TempDirectoryDemo.java[tags=user_guide_field_injection] ---- + +The `@TempDir` annotation has an optional `cleanup` attribute that can be set to either +`NEVER`, `ON_SUCCESS`, or `ALWAYS`. If the cleanup mode is set to `NEVER`, temporary +directories are not deleted after a test completes. If it is set to `ON_SUCCESS`, +temporary directories are deleted only after a test completed successfully. + +The default cleanup mode is `ALWAYS`. You can use the +`junit.jupiter.temp.dir.cleanup.mode.default` +<> to override this default. + +[source,java,indent=0] +.A test class with a temporary directory that doesn't get cleaned up +---- +include::{testDir}/example/TempDirCleanupModeDemo.java[tags=user_guide] +---- diff --git a/documentation/src/test/java/example/TempDirCleanupModeDemo.java b/documentation/src/test/java/example/TempDirCleanupModeDemo.java new file mode 100644 index 00000000000..223ec0ba0ee --- /dev/null +++ b/documentation/src/test/java/example/TempDirCleanupModeDemo.java @@ -0,0 +1,28 @@ +/* + * Copyright 2015-2021 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.io.CleanupMode.ON_SUCCESS; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +// tag::user_guide[] +class TempDirCleanupModeDemo { + + @Test + void fileTest(@TempDir(cleanup = ON_SUCCESS) Path tempDir) { + // perform test + } +} +// end::user_guide[] diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/CleanupMode.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/CleanupMode.java new file mode 100644 index 00000000000..a1b0b88009d --- /dev/null +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/CleanupMode.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015-2021 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.io; + +import static org.apiguardian.api.API.Status.EXPERIMENTAL; + +import org.apiguardian.api.API; + +/** + * Enumeration of cleanup modes for a {@code TempDir}. + * + *

When a test with a temporary directory completes, it might be useful in + * some cases to be able to view the contents of the directory resulting from + * the test. {@code CleanupMode} allows control of how a {@code TempDir} + * is cleaned up. + * + * @since 5.4 + * @see TempDir + */ +@API(status = EXPERIMENTAL, since = "5.4") +public enum CleanupMode { + + /** + * Defer to the configured cleanup mode. + */ + DEFAULT, + + /** + * Always clean up a temporary directory after the test has completed. + */ + ALWAYS, + + /** + * Don't clean up a temporary directory after the test has completed. + */ + NEVER, + + /** + * Only clean up a temporary directory if the test completed successfully. + */ + ON_SUCCESS +} diff --git a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java index 142e75fbea9..b57e0c60955 100644 --- a/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java +++ b/junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java @@ -69,13 +69,22 @@ * *

Deletion

* - *

When the end of the scope of a temporary directory is reached, i.e. when - * the test method or class has finished execution, JUnit will attempt to - * recursively delete all files and directories in the temporary directory + *

By default, when the end of the scope of a temporary directory is reached, + * i.e. when the test method or class has finished execution, JUnit will attempt + * to recursively delete all files and directories in the temporary directory * and, finally, the temporary directory itself. In case deletion of a file or * directory fails, an {@link IOException} will be thrown that will cause the * test or test class to fail. * + *

The {@code @TempDir} annotation has a {@link CleanupMode} parameter that + * allows overriding the default behavior. If the cleanup mode is set to + * {@link CleanupMode#NEVER}, then the temporary directory will not be deleted + * after the test completes. If the cleanup mode is set to + * {@link CleanupMode#ON_SUCCESS}, then the temporary directory will only be + * deleted if the test completes successfully. The default behavior can be + * altered by setting the {@value #DEFAULT_CLEANUP_MODE_PROPERTY_NAME} + * configuration parameter. + * * @since 5.4 */ @Target({ ElementType.FIELD, ElementType.PARAMETER }) @@ -83,4 +92,12 @@ @Documented @API(status = EXPERIMENTAL, since = "5.4") public @interface TempDir { + + String DEFAULT_CLEANUP_MODE_PROPERTY_NAME = "junit.jupiter.cleanup.mode.default"; + + /** + * How the temporary directory gets cleaned up after the test completes. + */ + CleanupMode cleanup() default CleanupMode.DEFAULT; + } 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 c7762626628..bad0d073f76 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 @@ -11,6 +11,7 @@ package org.junit.jupiter.engine.config; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.api.io.TempDir.DEFAULT_CLEANUP_MODE_PROPERTY_NAME; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; @@ -23,6 +24,7 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.parallel.ExecutionMode; /** @@ -107,4 +109,10 @@ public Optional getDefaultTestClassOrderer() { key -> delegate.getDefaultTestClassOrderer()); } + @Override + public CleanupMode getDefaultTempDirCleanupMode() { + return (CleanupMode) cache.computeIfAbsent(DEFAULT_CLEANUP_MODE_PROPERTY_NAME, + key -> delegate.getDefaultTempDirCleanupMode()); + } + } 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 f8823892a5d..ddf98efd806 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 @@ -11,6 +11,8 @@ package org.junit.jupiter.engine.config; import static org.apiguardian.api.API.Status.INTERNAL; +import static org.junit.jupiter.api.io.CleanupMode.ALWAYS; +import static org.junit.jupiter.api.io.TempDir.DEFAULT_CLEANUP_MODE_PROPERTY_NAME; import java.util.Optional; import java.util.function.Function; @@ -22,6 +24,7 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.TestInstance.Lifecycle; import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.util.ClassNamePatternFilterUtils; import org.junit.platform.commons.util.Preconditions; @@ -50,6 +53,9 @@ public class DefaultJupiterConfiguration implements JupiterConfiguration { private static final InstantiatingConfigurationParameterConverter classOrdererConverter = // new InstantiatingConfigurationParameterConverter<>(ClassOrderer.class, "class orderer"); + private static final EnumConfigurationParameterConverter cleanupModeConverter = // + new EnumConfigurationParameterConverter<>(CleanupMode.class, "cleanup mode"); + private final ConfigurationParameters configurationParameters; public DefaultJupiterConfiguration(ConfigurationParameters configurationParameters) { @@ -117,4 +123,9 @@ public Optional getDefaultTestClassOrderer() { return classOrdererConverter.get(configurationParameters, DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME); } + @Override + public CleanupMode getDefaultTempDirCleanupMode() { + return cleanupModeConverter.get(configurationParameters, DEFAULT_CLEANUP_MODE_PROPERTY_NAME, ALWAYS); + } + } 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 ce1c9670475..49f65897595 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 @@ -22,6 +22,7 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExecutionCondition; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.parallel.ExecutionMode; import org.junit.platform.commons.util.ClassNamePatternFilterUtils; @@ -42,7 +43,6 @@ public interface JupiterConfiguration { String DEFAULT_TEST_METHOD_ORDER_PROPERTY_NAME = "junit.jupiter.testmethod.order.default"; String DEFAULT_TEST_CLASS_ORDER_PROPERTY_NAME = "junit.jupiter.testclass.order.default"; String TEMP_DIR_SCOPE_PROPERTY_NAME = "junit.jupiter.tempdir.scope"; - String DEFAULT_TIMEOUT_PROPERTY_NAME = "junit.jupiter.execution.timeout.default"; String DEFAULT_TESTABLE_METHOD_TIMEOUT_PROPERTY_NAME = "junit.jupiter.execution.timeout.testable.method.default"; String DEFAULT_TEST_METHOD_TIMEOUT_PROPERTY_NAME = "junit.jupiter.execution.timeout.test.method.default"; @@ -77,4 +77,6 @@ public interface JupiterConfiguration { Optional getDefaultTestClassOrderer(); + CleanupMode getDefaultTempDirCleanupMode(); + } 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 6a739ca3958..5d3d3d2e8c6 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 @@ -47,9 +47,8 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg private static final Logger logger = LoggerFactory.getLogger(MutableExtensionRegistry.class); - private static final List DEFAULT_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(// + private static final List DEFAULT_STATELESS_EXTENSIONS = Collections.unmodifiableList(Arrays.asList(// new DisabledCondition(), // - new TempDirectory(), // new TimeoutExtension(), // new RepeatedTestExtension(), // new TestInfoParameterResolver(), // @@ -71,7 +70,9 @@ public class MutableExtensionRegistry implements ExtensionRegistry, ExtensionReg public static MutableExtensionRegistry createRegistryWithDefaultExtensions(JupiterConfiguration configuration) { MutableExtensionRegistry extensionRegistry = new MutableExtensionRegistry(null); - DEFAULT_EXTENSIONS.forEach(extensionRegistry::registerDefaultExtension); + DEFAULT_STATELESS_EXTENSIONS.forEach(extensionRegistry::registerDefaultExtension); + + extensionRegistry.registerDefaultExtension(new TempDirectory(configuration)); if (configuration.isExtensionAutoDetectionEnabled()) { registerAutoDetectedExtensions(extensionRegistry); 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 8849f2bacb2..5f1931f7b1c 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 @@ -12,6 +12,10 @@ import static java.nio.file.FileVisitResult.CONTINUE; import static java.util.stream.Collectors.joining; +import static org.junit.jupiter.api.io.CleanupMode.ALWAYS; +import static org.junit.jupiter.api.io.CleanupMode.DEFAULT; +import static org.junit.jupiter.api.io.CleanupMode.NEVER; +import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS; import static org.junit.jupiter.engine.config.JupiterConfiguration.TEMP_DIR_SCOPE_PROPERTY_NAME; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotatedFields; import static org.junit.platform.commons.util.ReflectionUtils.makeAccessible; @@ -30,6 +34,7 @@ import java.nio.file.SimpleFileVisitor; import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; +import java.util.Optional; import java.util.SortedMap; import java.util.TreeMap; import java.util.function.Predicate; @@ -43,8 +48,13 @@ import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.api.extension.ParameterResolutionException; import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.engine.config.EnumConfigurationParameterConverter; +import org.junit.jupiter.engine.config.JupiterConfiguration; +import org.junit.platform.commons.logging.Logger; +import org.junit.platform.commons.logging.LoggerFactory; +import org.junit.platform.commons.support.AnnotationSupport; import org.junit.platform.commons.util.ExceptionUtils; import org.junit.platform.commons.util.ReflectionUtils; @@ -65,6 +75,12 @@ class TempDirectory implements BeforeAllCallback, BeforeEachCallback, ParameterR private static final String KEY = "temp.dir"; private static final String TEMP_DIR_PREFIX = "junit"; + private final JupiterConfiguration configuration; + + public TempDirectory(JupiterConfiguration configuration) { + this.configuration = configuration; + } + /** * Perform field injection for non-private, {@code static} fields (i.e., * class fields) of type {@link Path} or {@link File} that are annotated with @@ -99,8 +115,10 @@ private void injectFields(ExtensionContext context, Object testInstance, Class { assertSupportedType("field", field.getType()); + try { - makeAccessible(field).set(testInstance, getPathOrFile(field, field.getType(), context)); + CleanupMode mode = findCleanupModeForField(field); + makeAccessible(field).set(testInstance, getPathOrFile(field, field.getType(), mode, context)); } catch (Throwable t) { ExceptionUtils.throwAsUncheckedException(t); @@ -130,7 +148,32 @@ public boolean supportsParameter(ParameterContext parameterContext, ExtensionCon public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { Class parameterType = parameterContext.getParameter().getType(); assertSupportedType("parameter", parameterType); - return getPathOrFile(parameterContext.getParameter(), parameterType, extensionContext); + CleanupMode cleanupMode = findCleanupModeForParameter(parameterContext); + return getPathOrFile(parameterContext.getParameter(), parameterType, cleanupMode, extensionContext); + } + + private CleanupMode findCleanupModeForParameter(ParameterContext parameterContext) { + CleanupMode cleanupMode = null; + Optional optional = parameterContext.findAnnotation(TempDir.class); + if (optional.isPresent()) { + cleanupMode = optional.get().cleanup(); + } + if (cleanupMode == null || cleanupMode == DEFAULT) { + cleanupMode = ALWAYS; + } + return cleanupMode; + } + + private CleanupMode findCleanupModeForField(Field field) { + CleanupMode cleanupMode = null; + Optional optional = AnnotationSupport.findAnnotation(field, TempDir.class); + if (optional.isPresent()) { + cleanupMode = optional.get().cleanup(); + } + if (cleanupMode == null || cleanupMode == DEFAULT) { + cleanupMode = configuration.getDefaultTempDirCleanupMode(); + } + return cleanupMode; } private void assertSupportedType(String target, Class type) { @@ -140,12 +183,13 @@ private void assertSupportedType(String target, Class type) { } } - private Object getPathOrFile(AnnotatedElement sourceElement, Class type, ExtensionContext extensionContext) { + private Object getPathOrFile(AnnotatedElement sourceElement, Class type, CleanupMode cleanupMode, + ExtensionContext extensionContext) { Namespace namespace = getScope(extensionContext) == Scope.PER_DECLARATION // ? NAMESPACE.append(sourceElement) // : NAMESPACE; Path path = extensionContext.getStore(namespace) // - .getOrComputeIfAbsent(KEY, __ -> createTempDir(), CloseablePath.class) // + .getOrComputeIfAbsent(KEY, __ -> createTempDir(cleanupMode, extensionContext), CloseablePath.class) // .get(); return (type == Path.class) ? path : path.toFile(); @@ -160,21 +204,27 @@ private Scope getScope(ExtensionContext context) { ); } - private static CloseablePath createTempDir() { + static CloseablePath createTempDir(CleanupMode cleanupMode, ExtensionContext executionContext) { try { - return new CloseablePath(Files.createTempDirectory(TEMP_DIR_PREFIX)); + return new CloseablePath(Files.createTempDirectory(TEMP_DIR_PREFIX), cleanupMode, executionContext); } catch (Exception ex) { throw new ExtensionConfigurationException("Failed to create default temp directory", ex); } } - private static class CloseablePath implements CloseableResource { + static class CloseablePath implements CloseableResource { + + private static final Logger logger = LoggerFactory.getLogger(CloseablePath.class); private final Path dir; + private final CleanupMode cleanupMode; + private final ExtensionContext executionContext; - CloseablePath(Path dir) { + CloseablePath(Path dir, CleanupMode cleanupMode, ExtensionContext executionContext) { this.dir = dir; + this.cleanupMode = cleanupMode; + this.executionContext = executionContext; } Path get() { @@ -183,6 +233,12 @@ Path get() { @Override public void close() throws IOException { + if (cleanupMode == NEVER + || (cleanupMode == ON_SUCCESS && executionContext.getExecutionException().isPresent())) { + logger.info(() -> "Skipping cleanup of temp dir " + dir + " due to cleanup mode configuration."); + return; + } + SortedMap failures = deleteAllFilesAndDirectories(); if (!failures.isEmpty()) { throw createIOExceptionWithAttachedFailures(failures); diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DefaultExecutionCleanupModeTests.java similarity index 98% rename from junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java rename to junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DefaultExecutionCleanupModeTests.java index a2e15e6bb4f..a8af6e6e201 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DefaultExecutionModeTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/DefaultExecutionCleanupModeTests.java @@ -32,7 +32,7 @@ import org.junit.platform.engine.support.hierarchical.Node.ExecutionMode; import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; -class DefaultExecutionModeTests extends AbstractJupiterTestEngineTests { +class DefaultExecutionCleanupModeTests extends AbstractJupiterTestEngineTests { @Test void defaultExecutionModeIsReadFromConfigurationParameter() { diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/CachingJupiterConfigurationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/CachingJupiterConfigurationTests.java index de3a389ccbe..dc29bb1b319 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/CachingJupiterConfigurationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/CachingJupiterConfigurationTests.java @@ -11,6 +11,7 @@ package org.junit.jupiter.engine.config; import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.io.CleanupMode.NEVER; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.only; import static org.mockito.Mockito.times; @@ -111,6 +112,17 @@ void cachesDefaultTestMethodOrderer() { verify(delegate, only()).getDefaultTestMethodOrderer(); } + @Test + void cachesDefaultTempDirCleanupMode() { + when(delegate.getDefaultTempDirCleanupMode()).thenReturn(NEVER); + + // call `cache.getDefaultTempStrategyDirCleanupMode()` twice to verify the delegate method is called only once. + assertThat(cache.getDefaultTempDirCleanupMode()).isSameAs(NEVER); + assertThat(cache.getDefaultTempDirCleanupMode()).isSameAs(NEVER); + + verify(delegate, only()).getDefaultTempDirCleanupMode(); + } + @Test void doesNotCacheRawParameters() { when(delegate.getRawConfigurationParameter("foo")).thenReturn(Optional.of("bar")).thenReturn( diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java index 5a27ae57ce6..b530c70c45c 100644 --- a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/config/DefaultJupiterConfigurationTests.java @@ -15,6 +15,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS; import static org.junit.jupiter.api.TestInstance.Lifecycle.PER_METHOD; +import static org.junit.jupiter.api.io.CleanupMode.ALWAYS; import static org.junit.jupiter.engine.Constants.DEFAULT_TEST_INSTANCE_LIFECYCLE_PROPERTY_NAME; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.MethodOrderer; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance.Lifecycle; +import org.junit.jupiter.api.io.CleanupMode; import org.junit.jupiter.engine.Constants; import org.junit.jupiter.engine.descriptor.CustomDisplayNameGenerator; import org.junit.platform.commons.PreconditionViolationException; @@ -48,6 +50,13 @@ void getDefaultTestInstanceLifecycleWithNoConfigParamSet() { assertThat(lifecycle).isEqualTo(PER_METHOD); } + @Test + void getDefaultTempDirCleanupModeWithNoConfigParamSet() { + JupiterConfiguration configuration = new DefaultJupiterConfiguration(mock(ConfigurationParameters.class)); + CleanupMode cleanupMode = configuration.getDefaultTempDirCleanupMode(); + assertThat(cleanupMode).isEqualTo(ALWAYS); + } + @Test void getDefaultTestInstanceLifecycleWithConfigParamSet() { assertAll(// diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/CloseablePathCleanupTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/CloseablePathCleanupTests.java new file mode 100644 index 00000000000..fd990b336bf --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/CloseablePathCleanupTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2015-2021 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.nio.file.Files.deleteIfExists; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.io.CleanupMode.ALWAYS; +import static org.junit.jupiter.api.io.CleanupMode.NEVER; +import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.Optional; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.opentest4j.TestAbortedException; + +/** + * Integration tests for cleanup of the {@link TempDirectory} + * when {@link TempDir} is set to {@link CleanupMode#ALWAYS} or + * {@link CleanupMode#NEVER}. + * + * @since 5.9 + * + * @see TempDir + * @see CleanupMode + */ +class CloseablePathCleanupTests extends AbstractJupiterTestEngineTests { + + private TempDirectory.CloseablePath path; + + @AfterEach + void cleanupTempDirectory() throws IOException { + deleteIfExists(path.get()); + } + + /** + * Ensure a closeable path is cleaned up for a cleanup mode of ALWAYS. + */ + @Test + void testAlways() throws IOException { + ExtensionContext extensionContext = mock(ExtensionContext.class); + path = TempDirectory.createTempDir(ALWAYS, extensionContext); + assertTrue(path.get().toFile().exists()); + + path.close(); + assertFalse(path.get().toFile().exists()); + } + + /** + * Ensure a closeable path is not cleaned up for a cleanup mode of NEVER. + */ + @Test + void testNever() throws IOException { + ExtensionContext extensionContext = mock(ExtensionContext.class); + path = TempDirectory.createTempDir(NEVER, extensionContext); + assertTrue(path.get().toFile().exists()); + + path.close(); + assertTrue(path.get().toFile().exists()); + } + + /** + * Ensure a closeable path is not cleaned up for a cleanup mode of ON_SUCCESS, if there is a TestAbortedException. + */ + @Test + void testOnSuccessWithTestAbortedException() throws IOException { + ExtensionContext extensionContext = mock(ExtensionContext.class); + when(extensionContext.getExecutionException()).thenReturn(Optional.of(new TestAbortedException())); + + path = TempDirectory.createTempDir(ON_SUCCESS, extensionContext); + assertTrue(path.get().toFile().exists()); + + path.close(); + assertTrue(path.get().toFile().exists()); + } + + /** + * Ensure a closeable path is cleaned up for a cleanup mode of ON_SUCCESS, if there is no exception. + */ + @Test + void testOnSuccessWithNoTestAbortedException() throws IOException { + ExtensionContext extensionContext = mock(ExtensionContext.class); + when(extensionContext.getExecutionException()).thenReturn(Optional.empty()); + + path = TempDirectory.createTempDir(ON_SUCCESS, extensionContext); + assertTrue(path.get().toFile().exists()); + + path.close(); + assertFalse(path.get().toFile().exists()); + } + +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java new file mode 100644 index 00000000000..d4144dcc333 --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryCleanupTests.java @@ -0,0 +1,327 @@ +/* + * Copyright 2015-2021 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.nio.file.Files.deleteIfExists; +import static java.nio.file.Files.exists; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; +import static org.junit.jupiter.api.io.CleanupMode.ALWAYS; +import static org.junit.jupiter.api.io.CleanupMode.NEVER; +import static org.junit.jupiter.api.io.CleanupMode.ON_SUCCESS; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; +import static org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder.request; + +import java.io.IOException; +import java.nio.file.Path; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.CleanupMode; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.platform.launcher.LauncherDiscoveryRequest; + +/** + * Test that {@link TempDir temporary directories} are not deleted if set for {@link CleanupMode#NEVER}, + * deletes any if set for {@link CleanupMode#ON_SUCCESS} only if the test passes, + * and deletes any if set for {@link CleanupMode#ALWAYS}. + * + * @see CleanupMode + * @see TempDir + * @since 5.9 + */ +class TempDirectoryCleanupTests extends AbstractJupiterTestEngineTests { + + @Nested + class TempDirFieldTests { + + private static Path defaultFieldDir; + private static Path neverFieldDir; + private static Path alwaysFieldDir; + private static Path onSuccessFailingFieldDir; + private static Path onSuccessPassingFieldDir; + + /** + * Ensure the cleanup modes defaults to ALWAYS for fields. + *

+ * Expect the TempDir to be cleaned up. + */ + @Test + void testCleanupModeDefaultField() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(DefaultFieldCase.class, "testDefaultField")).build(); + executeTests(request); + + assertFalse(exists(defaultFieldDir)); + } + + /** + * Ensure that NEVER cleanup modes are obeyed for fields. + *

+ * Expect the TempDir not to be cleaned up. + */ + @Test + void testCleanupModeNeverField() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(NeverFieldCase.class, "testNeverField")).build(); + executeTests(request); + + assertTrue(exists(neverFieldDir)); + } + + /** + * Ensure that ALWAYS cleanup modes are obeyed for fields. + *

+ * Expect the TempDir to be cleaned up. + */ + @Test + void testCleanupModeAlwaysField() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(AlwaysFieldCase.class, "testAlwaysField")).build(); + executeTests(request); + + assertFalse(exists(alwaysFieldDir)); + } + + /** + * Ensure that ON_SUCCESS cleanup modes are obeyed for passing field tests. + *

+ * Expect the TempDir to be cleaned up. + */ + @Test + void testCleanupModeOnSuccessPassingField() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(OnSuccessPassingFieldCase.class, "testOnSuccessPassingField")).build(); + executeTests(request); + + assertFalse(exists(onSuccessPassingFieldDir)); + } + + /** + * Ensure that ON_SUCCESS cleanup modes are obeyed for failing field tests. + *

+ * Expect the TempDir not to be cleaned up. + */ + @Test + void testCleanupModeOnSuccessFailingField() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(OnSuccessFailingFieldCase.class, "testOnSuccessFailingField")).build(); + executeTests(request); + + assertTrue(exists(onSuccessFailingFieldDir)); + } + + @AfterAll + static void afterAll() throws IOException { + deleteIfExists(defaultFieldDir); + deleteIfExists(neverFieldDir); + deleteIfExists(alwaysFieldDir); + deleteIfExists(onSuccessFailingFieldDir); + deleteIfExists(onSuccessPassingFieldDir); + } + + // ------------------------------------------------------------------- + + static class DefaultFieldCase { + + @TempDir + Path defaultFieldDir; + + @Test + void testDefaultField() { + TempDirFieldTests.defaultFieldDir = defaultFieldDir; + } + } + + static class NeverFieldCase { + + @TempDir(cleanup = NEVER) + Path neverFieldDir; + + @Test + void testNeverField() { + TempDirFieldTests.neverFieldDir = neverFieldDir; + } + } + + static class AlwaysFieldCase { + + @TempDir(cleanup = ALWAYS) + Path alwaysFieldDir; + + @Test + void testAlwaysField() { + TempDirFieldTests.alwaysFieldDir = alwaysFieldDir; + } + } + + static class OnSuccessPassingFieldCase { + + @TempDir(cleanup = ON_SUCCESS) + Path onSuccessPassingFieldDir; + + @Test + void testOnSuccessPassingField() { + TempDirFieldTests.onSuccessPassingFieldDir = onSuccessPassingFieldDir; + } + } + + static class OnSuccessFailingFieldCase { + + @TempDir(cleanup = ON_SUCCESS) + Path onSuccessFailingFieldDir; + + @Test + void testOnSuccessFailingField() { + TempDirFieldTests.onSuccessFailingFieldDir = onSuccessFailingFieldDir; + fail(); + } + } + + } + + @Nested + class TempDirParameterTests { + + private static Path defaultParameterDir; + private static Path neverParameterDir; + private static Path alwaysParameterDir; + private static Path onSuccessFailingParameterDir; + private static Path onSuccessPassingParameterDir; + + /** + * Ensure the cleanup modes defaults to ALWAYS for parameters. + *

+ * Expect the TempDir to be cleaned up. + */ + @Test + void testCleanupModeDefaultParameter() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(DefaultParameterCase.class, "testDefaultParameter", "java.nio.file.Path")).build(); + executeTests(request); + + assertFalse(exists(defaultParameterDir)); + } + + /** + * Ensure that NEVER cleanup modes are obeyed for parameters. + *

+ * Expect the TempDir not to be cleaned up. + */ + @Test + void testCleanupModeNeverParameter() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(NeverParameterCase.class, "testNeverParameter", "java.nio.file.Path")).build(); + executeTests(request); + + assertTrue(exists(neverParameterDir)); + } + + /** + * Ensure that ALWAYS cleanup modes are obeyed for parameters. + *

+ * Expect the TempDir to be cleaned up. + */ + @Test + void testCleanupModeAlwaysParameter() { + LauncherDiscoveryRequest request = request().selectors( + selectMethod(AlwaysParameterCase.class, "testAlwaysParameter", "java.nio.file.Path")).build(); + executeTests(request); + + assertFalse(exists(alwaysParameterDir)); + } + + /** + * Ensure that ON_SUCCESS cleanup modes are obeyed for passing parameter tests. + *

+ * Expect the TempDir to be cleaned up. + */ + @Test + void testCleanupModeOnSuccessPassingParameter() { + LauncherDiscoveryRequest request = request().selectors(selectMethod(OnSuccessPassingParameterCase.class, + "testOnSuccessPassingParameter", "java.nio.file.Path")).build(); + executeTests(request); + + assertFalse(exists(onSuccessPassingParameterDir)); + } + + /** + * Ensure that ON_SUCCESS cleanup modes are obeyed for failing parameter tests. + *

+ * Expect the TempDir not to be cleaned up. + */ + @Test + void testCleanupModeOnSuccessFailingParameter() { + LauncherDiscoveryRequest request = request().selectors(selectMethod(OnSuccessFailingParameterCase.class, + "testOnSuccessFailingParameter", "java.nio.file.Path")).build(); + executeTests(request); + + assertTrue(exists(onSuccessFailingParameterDir)); + } + + @AfterAll + static void afterAll() throws IOException { + deleteIfExists(defaultParameterDir); + deleteIfExists(neverParameterDir); + deleteIfExists(alwaysParameterDir); + deleteIfExists(onSuccessFailingParameterDir); + deleteIfExists(onSuccessPassingParameterDir); + } + + // ------------------------------------------------------------------- + + static class DefaultParameterCase { + + @Test + void testDefaultParameter(@TempDir Path defaultParameterDir) { + TempDirParameterTests.defaultParameterDir = defaultParameterDir; + } + } + + static class NeverParameterCase { + + @Test + void testNeverParameter(@TempDir(cleanup = NEVER) Path neverParameterDir) { + TempDirParameterTests.neverParameterDir = neverParameterDir; + } + } + + static class AlwaysParameterCase { + + @Test + void testAlwaysParameter(@TempDir(cleanup = ALWAYS) Path alwaysParameterDir) { + TempDirParameterTests.alwaysParameterDir = alwaysParameterDir; + } + } + + static class OnSuccessPassingParameterCase { + + @Test + void testOnSuccessPassingParameter(@TempDir(cleanup = ON_SUCCESS) Path onSuccessPassingParameterDir) { + TempDirParameterTests.onSuccessPassingParameterDir = onSuccessPassingParameterDir; + } + } + + static class OnSuccessFailingParameterCase { + + @Test + void testOnSuccessFailingParameter(@TempDir(cleanup = ON_SUCCESS) Path onSuccessFailingParameterDir) { + TempDirParameterTests.onSuccessFailingParameterDir = onSuccessFailingParameterDir; + fail(); + } + } + + } + +} diff --git a/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryParameterResolverTests.java b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryParameterResolverTests.java new file mode 100644 index 00000000000..d4fd10d8709 --- /dev/null +++ b/junit-jupiter-engine/src/test/java/org/junit/jupiter/engine/extension/TempDirectoryParameterResolverTests.java @@ -0,0 +1,61 @@ +/* + * Copyright 2015-2021 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.testkit.engine.EventConditions.event; +import static org.junit.platform.testkit.engine.EventConditions.finishedSuccessfully; +import static org.junit.platform.testkit.engine.EventConditions.finishedWithFailure; +import static org.junit.platform.testkit.engine.EventConditions.test; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.instanceOf; +import static org.junit.platform.testkit.engine.TestExecutionResultConditions.message; + +import java.io.File; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.engine.AbstractJupiterTestEngineTests; +import org.junit.platform.testkit.engine.EngineExecutionResults; +import org.junit.platform.testkit.engine.Events; + +/** + * @since 5.9 + */ +class TempDirectoryParameterResolverTests extends AbstractJupiterTestEngineTests { + + @Test + @DisplayName("Test good and bad @TempDir parameters") + void testTempDirType() { + EngineExecutionResults executionResults = executeTestsForClass(ATestCase.class); + Events tests = executionResults.testEvents(); + tests.assertStatistics(stats -> stats.started(2).failed(1).succeeded(1)); + tests.succeeded().assertEventsMatchExactly(event(test("testGoodTempDirType"), finishedSuccessfully())); + tests.failed().assertEventsMatchExactly(event(test("testBadTempDirType"), + finishedWithFailure(instanceOf(ParameterResolutionException.class), message( + "Failed to resolve parameter [java.lang.String badTempDir] in method [void org.junit.jupiter.engine.extension.TempDirectoryParameterResolverTests$ATestCase.testBadTempDirType(java.lang.String)]: Can only resolve @TempDir parameter of type java.nio.file.Path or java.io.File but was: java.lang.String")))); + } + + // ------------------------------------------------------------------- + + static class ATestCase { + + @Test + void testGoodTempDirType(@TempDir File goodTempDir) { + } + + @Test + void testBadTempDirType(@TempDir String badTempDir) { + } + + } + +}