Skip to content

Commit 3933d54

Browse files
Introduce TempDirDeletionStrategy (#5424)
`@TempDir` now allows configuring a deletion strategy for temporary directories. This is typically used to gain control over what happens when deletion of a file or directory fails. Two implementations are provided out of the box: `Standard` (which will preserve the existing behavior of failing tests when deletion of a file or directory fails) and `IgnoreFailures` (which will log failures but not fail test execution). The default can be changed using the new `junit.jupiter .tempdir.deletion.strategy.default` configuration parameter. Resolves #4567. --------- Co-authored-by: M.P. Korstanje <rien.korstanje@gmail.com>
1 parent c9f8a3d commit 3933d54

File tree

17 files changed

+1030
-369
lines changed

17 files changed

+1030
-369
lines changed

documentation/antora.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,10 @@ asciidoc:
232232
JRE: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/condition/JRE.html[JRE]'
233233
# Jupiter I/O
234234
TempDir: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDir.html[@TempDir]'
235+
TempDirDeletionStrategy: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDirDeletionStrategy.html[TempDirDeletionStrategy]'
236+
TempDirDeletionStrategyIgnoreFailures: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDirDeletionStrategy.IgnoreFailures.html[IgnoreFailures]'
237+
TempDirDeletionStrategyStandard: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDirDeletionStrategy.Standard.html[Standard]'
238+
TempDirFactory: '{javadoc-root}/org.junit.jupiter.api/org/junit/jupiter/api/io/TempDirFactory.html[TempDirFactory]'
235239
# Jupiter Params
236240
params-provider-package: '{javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/provider/package-summary.html[org.junit.jupiter.params.provider]'
237241
AfterParameterizedClassInvocation: '{javadoc-root}/org.junit.jupiter.params/org/junit/jupiter/params/AfterParameterizedClassInvocation.html[@AfterParameterizedClassInvocation]'

documentation/modules/ROOT/pages/writing-tests/built-in-extensions.adoc

Lines changed: 51 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,11 +59,14 @@ xref:running-tests/configuration-parameters.adoc[configuration parameter] to ove
5959
include::example$java/example/TempDirectoryDemo.java[tags=user_guide_cleanup_mode]
6060
----
6161

62+
[[TempDirFactory]]
63+
=== Factories
64+
6265
`@TempDir` supports the programmatic creation of temporary directories via the optional
6366
`factory` attribute. This is typically used to gain control over the temporary directory
6467
creation, like defining the parent directory or the file system that should be used.
6568

66-
Factories can be created by implementing `TempDirFactory`. Implementations must provide a
69+
Factories can be created by implementing `{TempDirFactory}`. Implementations must provide a
6770
no-args constructor and should not make any assumptions regarding when and how many times
6871
they are instantiated, but they can assume that their `createTempDirectory(...)` and
6972
`close()` methods will both be called once per instance, in this order, and from the same
@@ -118,20 +121,64 @@ parameter of the `createTempDirectory(...)` method.
118121

119122
You can use the `junit.jupiter.tempdir.factory.default`
120123
xref:running-tests/configuration-parameters.adoc[configuration parameter] to specify the
121-
fully qualified class name of the `TempDirFactory` you would like to use by default. Just
124+
fully qualified class name of the `{TempDirFactory}` you would like to use by default. Just
122125
like for factories configured via the `factory` attribute of the `@TempDir` annotation,
123-
the supplied class has to implement the `TempDirFactory` interface. The default factory
126+
the supplied class has to implement the `{TempDirFactory}` interface. The default factory
124127
will be used for all `@TempDir` annotations unless the `factory` attribute of the
125128
annotation specifies a different factory.
126129

127130
In summary, the factory for a temporary directory is determined according to the following
128131
precedence rules:
129132

130133
1. The `factory` attribute of the `@TempDir` annotation, if present
131-
2. The default `TempDirFactory` configured via the configuration
134+
2. The default `{TempDirFactory}` configured via the configuration
132135
parameter, if present
133136
3. Otherwise, `org.junit.jupiter.api.io.TempDirFactory$Standard` will be used.
134137

138+
[[TempDirDeletionStrategy]]
139+
=== Deletion
140+
141+
`@TempDir` supports the programmatic deletion of temporary directories via the optional
142+
`deletionStrategy` attribute. This is typically used to gain control over what happens
143+
when deletion of a file or directory fails.
144+
145+
Deletion strategies can be created by implementing `{TempDirDeletionStrategy}`.
146+
Implementations must provide a no-args constructor.
147+
148+
Jupiter ships with two built-in deletion strategies:
149+
150+
* `{TempDirDeletionStrategyStandard}` (the default): attempts to delete all files and
151+
directories recursively, retrying with permission resets on failure. Paths that still
152+
cannot be deleted are scheduled for deletion on JVM exit, if possible. Additionally,
153+
the test is failed.
154+
* `{TempDirDeletionStrategyIgnoreFailures}`: delegates to `{TempDirDeletionStrategyStandard}`
155+
but suppresses deletion failures by logging a warning instead of failing the test.
156+
157+
The following example uses `{TempDirDeletionStrategyIgnoreFailures}` so that any deletion
158+
failures are only logged.
159+
160+
[source,java,indent=0]
161+
.A test class with a temporary directory that ignores deletion failures
162+
----
163+
include::example$java/example/TempDirectoryDemo.java[tags=user_guide_deletion_strategy]
164+
----
165+
166+
You can use the `junit.jupiter.tempdir.deletion.strategy.default`
167+
xref:running-tests/configuration-parameters.adoc[configuration parameter] to specify the
168+
fully qualified class name of the `{TempDirDeletionStrategy}` you would like to use by
169+
default. Just like for strategies configured via the `deletionStrategy` attribute of the
170+
`@TempDir` annotation, the supplied class has to implement the `{TempDirDeletionStrategy}`
171+
interface. The default strategy will be used for all `@TempDir` annotations unless the
172+
`deletionStrategy` attribute of the annotation specifies a different strategy.
173+
174+
In summary, the deletion strategy for a temporary directory is determined according to the
175+
following precedence rules:
176+
177+
1. The `deletionStrategy` attribute of the `@TempDir` annotation, if present
178+
2. The default `{TempDirDeletionStrategy}` configured via the configuration parameter,
179+
if present
180+
3. Otherwise, `org.junit.jupiter.api.io.TempDirDeletionStrategy$Standard` will be used.
181+
135182
[[AutoClose]]
136183
== The @AutoClose Extension
137184

documentation/modules/ROOT/partials/release-notes/release-notes-6.1.0-M2.adoc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ repository on GitHub.
7575
the xref:writing-tests/built-in-extensions.adoc#system-properties[User Guide]. For
7676
details regarding implementation differences between JUnit Pioneer and Jupiter, see the
7777
corresponding https://github.com/junit-team/junit-framework/pull/5258[pull request].
78+
* `@TempDir` now allows configuring a deletion strategy for temporary directories. This is
79+
typically used to gain control over what happens when deletion of a file or directory
80+
fails (see
81+
xref:writing-tests/built-in-extensions.adoc#TempDirDeletionStrategy[User Guide] for
82+
details).
7883
* `Arguments` may now be created from instances of `Iterable` via the following new
7984
methods which make it easier to dynamically build arguments from collections when using
8085
`@ParameterizedTest`.

documentation/src/test/java/example/TempDirectoryDemo.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import org.junit.jupiter.api.extension.AnnotatedElementContext;
3636
import org.junit.jupiter.api.extension.ExtensionContext;
3737
import org.junit.jupiter.api.io.TempDir;
38+
import org.junit.jupiter.api.io.TempDirDeletionStrategy;
3839
import org.junit.jupiter.api.io.TempDirFactory;
3940

4041
@SuppressWarnings("NewClassNamingConvention")
@@ -151,6 +152,18 @@ public void close() throws IOException {
151152
}
152153
// end::user_guide_factory_jimfs[]
153154

155+
static
156+
// tag::user_guide_deletion_strategy[]
157+
class DeletionStrategyDemo {
158+
159+
@Test
160+
void test(@TempDir(deletionStrategy = TempDirDeletionStrategy.IgnoreFailures.class) Path tempDir) {
161+
// perform test
162+
}
163+
164+
}
165+
// end::user_guide_deletion_strategy[]
166+
154167
// tag::user_guide_composed_annotation[]
155168
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
156169
@Retention(RetentionPolicy.RUNTIME)

junit-jupiter-api/src/main/java/org/junit/jupiter/api/Constants.java

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@
1010

1111
package org.junit.jupiter.api;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.INTERNAL;
1415
import static org.apiguardian.api.API.Status.STABLE;
1516

1617
import org.apiguardian.api.API;
1718
import org.junit.jupiter.api.extension.PreInterruptCallback;
1819
import org.junit.jupiter.api.extension.TestInstantiationAwareExtension.ExtensionContextScope;
20+
import org.junit.jupiter.api.io.CleanupMode;
1921
import org.junit.jupiter.api.io.TempDir;
2022
import org.junit.jupiter.api.parallel.Execution;
2123

@@ -393,13 +395,32 @@ public final class Constants {
393395
public static final String DEFAULT_TIMEOUT_THREAD_MODE_PROPERTY_NAME = Timeout.DEFAULT_TIMEOUT_THREAD_MODE_PROPERTY_NAME;
394396

395397
/**
396-
* Property name used to set the default factory for temporary directories created via
397-
* the {@link TempDir @TempDir} annotation: {@value}
398+
* Property name used to set the default factory for temporary directories
399+
* created via the {@link TempDir @TempDir} annotation: {@value}
398400
*
399401
* @see TempDir#DEFAULT_FACTORY_PROPERTY_NAME
400402
*/
401403
public static final String DEFAULT_TEMP_DIR_FACTORY_PROPERTY_NAME = TempDir.DEFAULT_FACTORY_PROPERTY_NAME;
402404

405+
/**
406+
* Property name used to configure the default {@link CleanupMode} for
407+
* temporary directories created via the {@link TempDir @TempDir}
408+
* annotation: {@value}
409+
*
410+
* @see TempDir#DEFAULT_CLEANUP_MODE_PROPERTY_NAME
411+
*/
412+
public static final String DEFAULT_TEMP_DIR_CLEANUP_MODE_PROPERTY_NAME = TempDir.DEFAULT_CLEANUP_MODE_PROPERTY_NAME;
413+
414+
/**
415+
* Property name used to set the default deletion strategy class name for
416+
* temporary directories created via the {@link TempDir @TempDir}
417+
* annotation: {@value}
418+
*
419+
* @see TempDir#DEFAULT_DELETION_STRATEGY_PROPERTY_NAME
420+
*/
421+
@API(status = EXPERIMENTAL, since = "6.1")
422+
public static final String DEFAULT_TEMP_DIR_DELETION_STRATEGY_PROPERTY_NAME = TempDir.DEFAULT_DELETION_STRATEGY_PROPERTY_NAME;
423+
403424
/**
404425
* Property name used to set the default extension context scope for
405426
* extensions that participate in test instantiation: {@value}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
*
4+
* All rights reserved. This program and the accompanying materials are
5+
* made available under the terms of the Eclipse Public License v2.0 which
6+
* accompanies this distribution and is available at
7+
*
8+
* https://www.eclipse.org/legal/epl-v20.html
9+
*/
10+
11+
package org.junit.jupiter.api.io;
12+
13+
import static java.util.Comparator.comparing;
14+
import static java.util.stream.Collectors.joining;
15+
16+
import java.nio.file.Path;
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.Optional;
20+
21+
import org.junit.jupiter.api.io.TempDirDeletionStrategy.DeletionException;
22+
import org.junit.jupiter.api.io.TempDirDeletionStrategy.DeletionFailure;
23+
import org.junit.jupiter.api.io.TempDirDeletionStrategy.DeletionResult;
24+
import org.junit.platform.commons.util.Preconditions;
25+
26+
record DefaultDeletionResult(Path rootDir, List<DeletionFailure> failures) implements DeletionResult {
27+
28+
DefaultDeletionResult(Path rootDir, List<DeletionFailure> failures) {
29+
this.rootDir = rootDir;
30+
this.failures = List.copyOf(failures);
31+
}
32+
33+
@Override
34+
public Optional<DeletionException> toException() {
35+
if (isSuccessful()) {
36+
return Optional.empty();
37+
}
38+
var joinedPaths = failures().stream() //
39+
.map(DeletionFailure::path) //
40+
.sorted() //
41+
.distinct() //
42+
.map(path -> relativizeSafely(rootDir(), path).toString()) //
43+
.map(path -> path.isEmpty() ? "<root>" : path) //
44+
.collect(joining(", "));
45+
var exception = new DeletionException("Failed to delete temp directory " + rootDir().toAbsolutePath()
46+
+ ". The following paths could not be deleted (see suppressed exceptions for details): " + joinedPaths);
47+
failures().stream() //
48+
.sorted(comparing(DeletionFailure::path)) //
49+
.map(DeletionFailure::cause) //
50+
.forEach(exception::addSuppressed);
51+
return Optional.of(exception);
52+
}
53+
54+
private static Path relativizeSafely(Path rootDir, Path path) {
55+
try {
56+
return rootDir.relativize(path);
57+
}
58+
catch (IllegalArgumentException e) {
59+
return path;
60+
}
61+
}
62+
63+
static final class Builder implements DeletionResult.Builder {
64+
65+
private final Path rootDir;
66+
private final List<DeletionFailure> failures = new ArrayList<>();
67+
68+
Builder(Path rootDir) {
69+
this.rootDir = rootDir;
70+
}
71+
72+
@Override
73+
public Builder addFailure(Path path, Exception cause) {
74+
Preconditions.notNull(path, "path must not be null");
75+
Preconditions.notNull(cause, "cause must not be null");
76+
failures.add(new DefaultDeletionFailure(path, cause));
77+
return this;
78+
}
79+
80+
@Override
81+
public DefaultDeletionResult build() {
82+
return new DefaultDeletionResult(rootDir, failures);
83+
}
84+
85+
}
86+
87+
record DefaultDeletionFailure(Path path, Exception cause) implements DeletionFailure {
88+
}
89+
}

junit-jupiter-api/src/main/java/org/junit/jupiter/api/io/TempDir.java

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010

1111
package org.junit.jupiter.api.io;
1212

13+
import static org.apiguardian.api.API.Status.EXPERIMENTAL;
1314
import static org.apiguardian.api.API.Status.MAINTAINED;
1415
import static org.apiguardian.api.API.Status.STABLE;
1516

1617
import java.io.File;
17-
import java.io.IOException;
1818
import java.lang.annotation.Documented;
1919
import java.lang.annotation.ElementType;
2020
import java.lang.annotation.Retention;
@@ -62,17 +62,16 @@
6262
* {@code static} field or on a parameter of a
6363
* {@link org.junit.jupiter.api.BeforeAll @BeforeAll} method.
6464
*
65-
* <h2>Clean Up</h2>
65+
* <h2>Cleanup/Deletion</h2>
6666
*
6767
* <p>By default, when the end of the scope of a temporary directory is reached,
6868
* &mdash; when the test method or class has finished execution &mdash; JUnit will
6969
* attempt to clean up the temporary directory by recursively deleting all files
7070
* and directories in the temporary directory and, finally, the temporary directory
71-
* itself. Symbolic and other types of links, such as junctions on Windows, are
72-
* not followed. A warning is logged when deleting a link that targets a
73-
* location outside the temporary directory. In case deletion of a file or
74-
* directory fails, an {@link IOException} will be thrown that will cause the
75-
* test or test class to fail.
71+
* itself.
72+
*
73+
* <p>Two attributes allow customizing <em>when</em> (see {@link #cleanup()})
74+
* and <em>how</em> (see {@link #deletionStrategy()}) to clean up.
7675
*
7776
* <p>The {@link #cleanup} attribute allows you to configure the {@link CleanupMode}.
7877
* If the cleanup mode is set to {@link CleanupMode#NEVER NEVER}, the temporary
@@ -83,6 +82,13 @@
8382
* configured globally by setting the {@value #DEFAULT_CLEANUP_MODE_PROPERTY_NAME}
8483
* configuration parameter.
8584
*
85+
* <p>The {@link #deletionStrategy()} attribute defines the strategy for
86+
* performing the cleanup and dealing with errors such as undeletable files.
87+
* By default, the {@link TempDirDeletionStrategy.Standard Standard} strategy is
88+
* used which will cause a test or test class to fail in case deletion of a file
89+
* or directory fails. This can be configured globally by setting the
90+
* {@value #DEFAULT_DELETION_STRATEGY_PROPERTY_NAME} configuration parameter.
91+
*
8692
* @since 5.4
8793
*/
8894
@Target({ ElementType.ANNOTATION_TYPE, ElementType.FIELD, ElementType.PARAMETER })
@@ -127,8 +133,10 @@
127133
Class<? extends TempDirFactory> factory() default TempDirFactory.class;
128134

129135
/**
130-
* The name of the configuration parameter that is used to configure the
131-
* default {@link CleanupMode}.
136+
* Property name used to configure the default {@link CleanupMode}: {@value}
137+
*
138+
* <p>Supported values include names of enum constants defined in
139+
* {@link CleanupMode}, ignoring case.
132140
*
133141
* <p>If this configuration parameter is not set, {@link CleanupMode#ALWAYS}
134142
* will be used as the default.
@@ -139,11 +147,47 @@
139147
String DEFAULT_CLEANUP_MODE_PROPERTY_NAME = "junit.jupiter.tempdir.cleanup.mode.default";
140148

141149
/**
142-
* How the temporary directory gets cleaned up after the test completes.
150+
* In which cases the temporary directory gets cleaned up after the test completes.
143151
*
144152
* @since 5.9
145153
*/
146154
@API(status = STABLE, since = "5.11")
147155
CleanupMode cleanup() default CleanupMode.DEFAULT;
148156

157+
/**
158+
* Property name used to set the default deletion strategy class name:
159+
* {@value}
160+
*
161+
* <h4>Supported Values</h4>
162+
*
163+
* <p>Supported values include fully qualified class names for types that
164+
* implement {@link TempDirDeletionStrategy}.
165+
*
166+
* <p>If not specified, the default is {@link TempDirDeletionStrategy.Standard}.
167+
*
168+
* @since 6.1
169+
*/
170+
@API(status = EXPERIMENTAL, since = "6.1")
171+
String DEFAULT_DELETION_STRATEGY_PROPERTY_NAME = "junit.jupiter.tempdir.deletion.strategy.default";
172+
173+
/**
174+
* Deletion strategy for the temporary directory.
175+
*
176+
* <p>Defaults to {@link TempDirDeletionStrategy.Standard}.
177+
*
178+
* <p>As an alternative to setting this attribute, a global
179+
* {@link TempDirDeletionStrategy} can be configured for the entire test
180+
* suite via the {@value #DEFAULT_DELETION_STRATEGY_PROPERTY_NAME}
181+
* configuration parameter. See the User Guide for details. Note, however,
182+
* that a {@code @TempDir} declaration with a custom
183+
* {@code deletionStrategy} always overrides a global
184+
* {@code TempDirDeletionStrategy}.
185+
*
186+
* @return the type of {@code TempDirDeletionStrategy} to use
187+
* @since 6.1
188+
* @see TempDirDeletionStrategy
189+
*/
190+
@API(status = EXPERIMENTAL, since = "6.1")
191+
Class<? extends TempDirDeletionStrategy> deletionStrategy() default TempDirDeletionStrategy.class;
192+
149193
}

0 commit comments

Comments
 (0)