Answer
The @TempDir extension creates a temporary directory before a test method (or class) runs and attempts to delete it after the test finishes. The deletion is performed by an AfterEachCallback (or AfterAllCallback for class‑scoped directories) that simply calls java.nio.file.Files.deleteRecursively. The call is made immediately after the test method returns, but the actual removal depends on the underlying filesystem’s ability to delete the directory.
Confirmed facts
- JUnit 5.7‑5.10 all use the same lifecycle callback mechanism; the only difference is that 5.8 added more robust error handling and logs a warning when deletion fails.
- The cleanup is attempted synchronously in the test thread; there is no background deferral unless the delete operation itself fails.
- If the directory cannot be deleted because a file handle remains open (JVM or external process), JUnit leaves the directory on disk and does not retry automatically.
Likely explanation of persistence
When a test spawns a long‑running external process (e.g., a server, a command‑line tool) that keeps a file inside the @TempDir open, the operating system may lock the directory. On Windows this prevents DeleteFile from succeeding; on Unix‑like systems the directory entry can be removed but the file’s inode stays allocated until the last handle is closed, which can appear as “leftover” space until the process exits. If the JVM crashes or is killed with SIGKILL, the cleanup callback never runs, so the directory persists until the next system cleanup.
Steps to ensure immediate cleanup for this case
- Close all streams, readers/writers, and any
java.nio.channels.FileChannel objects that reference files in the @TempDir before the test method returns.
- If you launch external processes, wait for them to terminate (e.g.,
Process.waitFor()) or explicitly destroy them (process.destroyForcibly()) in an @AfterEach method.
- On Windows, consider using a temporary directory on a RAM‑disk or a location where anti‑virus or indexing services are less likely to hold handles.
- As a safety net, add an
@AfterEach that manually deletes the directory with retries:
@AfterEach
void cleanTempDir(Path tempDir) throws IOException {
if (Files.exists(tempDir)) {
deleteRecursivelyWithRetries(tempDir, 3);
}
}
void deleteRecursivelyWithRetries(Path path, int maxAttempts) throws IOException {
for (int i = 0; i < maxAttempts; i++) {
try {
Files.walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
Files.delete(file);
return FileVisitResult.CONTINUE;
}
@Override public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
if (exc != null) throw exc;
Files.delete(dir);
return FileVisitResult.CONTINUE;
}
});
break; // success
} catch (DirectoryNotEmptyException e) {
// likely a handle still open; wait briefly and retry
Thread.sleep(100);
}
}
}
When to ask for more detail
If you are running tests on Windows and suspect anti‑virus or file‑indexing software is locking the TempDir, confirming the exact OS version and any security software in use would change the recommendation (e.g., adding exclusions). Otherwise, the steps above apply universally.