1111package org .junit .jupiter .engine .extension ;
1212
1313import static java .nio .file .FileVisitResult .CONTINUE ;
14+ import static java .nio .file .FileVisitResult .SKIP_SUBTREE ;
1415import static java .util .stream .Collectors .joining ;
1516import static org .junit .jupiter .api .extension .TestInstantiationAwareExtension .ExtensionContextScope .TEST_METHOD ;
1617import static org .junit .jupiter .api .io .CleanupMode .DEFAULT ;
3233import java .nio .file .FileSystems ;
3334import java .nio .file .FileVisitResult ;
3435import java .nio .file .Files ;
36+ import java .nio .file .LinkOption ;
3537import java .nio .file .NoSuchFileException ;
3638import java .nio .file .Path ;
3739import java .nio .file .Paths ;
@@ -289,7 +291,7 @@ private static boolean selfOrChildFailed(ExtensionContext context) {
289291
290292 static class CloseablePath implements CloseableResource {
291293
292- private static final Logger logger = LoggerFactory .getLogger (CloseablePath .class );
294+ private static final Logger LOGGER = LoggerFactory .getLogger (CloseablePath .class );
293295
294296 private final Path dir ;
295297 private final TempDirFactory factory ;
@@ -327,15 +329,27 @@ public void close() throws IOException {
327329 try {
328330 if (this .cleanupMode == NEVER
329331 || (this .cleanupMode == ON_SUCCESS && selfOrChildFailed (this .extensionContext ))) {
330- logger .info (() -> String .format ("Skipping cleanup of temp dir %s for %s due to CleanupMode.%s." ,
332+ LOGGER .info (() -> String .format ("Skipping cleanup of temp dir %s for %s due to CleanupMode.%s." ,
331333 this .dir , descriptionFor (this .annotatedElement ), this .cleanupMode .name ()));
332334 return ;
333335 }
334336
335337 FileOperations fileOperations = this .extensionContext .getStore (NAMESPACE ) //
336338 .getOrDefault (FILE_OPERATIONS_KEY , FileOperations .class , FileOperations .DEFAULT );
339+ FileOperations loggingFileOperations = file -> {
340+ LOGGER .trace (() -> "Attempting to delete " + file );
341+ try {
342+ fileOperations .delete (file );
343+ LOGGER .trace (() -> "Successfully deleted " + file );
344+ }
345+ catch (IOException e ) {
346+ LOGGER .trace (e , () -> "Failed to delete " + file );
347+ throw e ;
348+ }
349+ };
337350
338- SortedMap <Path , IOException > failures = deleteAllFilesAndDirectories (fileOperations );
351+ LOGGER .trace (() -> "Cleaning up temp dir " + this .dir );
352+ SortedMap <Path , IOException > failures = deleteAllFilesAndDirectories (loggingFileOperations );
339353 if (!failures .isEmpty ()) {
340354 throw createIOExceptionWithAttachedFailures (failures );
341355 }
@@ -375,26 +389,41 @@ private static String descriptionFor(Executable executable) {
375389 private SortedMap <Path , IOException > deleteAllFilesAndDirectories (FileOperations fileOperations )
376390 throws IOException {
377391
378- if (this .dir == null || Files .notExists (this .dir )) {
392+ Path rootDir = this .dir ;
393+ if (rootDir == null || Files .notExists (rootDir )) {
379394 return Collections .emptySortedMap ();
380395 }
381396
382397 SortedMap <Path , IOException > failures = new TreeMap <>();
383398 Set <Path > retriedPaths = new HashSet <>();
384- tryToResetPermissions (this .dir );
385- Files .walkFileTree (this .dir , new SimpleFileVisitor <Path >() {
399+ Path rootRealPath = rootDir .toRealPath ();
400+
401+ tryToResetPermissions (rootDir );
402+ Files .walkFileTree (rootDir , new SimpleFileVisitor <Path >() {
386403
387404 @ Override
388- public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) {
389- if (!dir .equals (CloseablePath .this .dir )) {
405+ public FileVisitResult preVisitDirectory (Path dir , BasicFileAttributes attrs ) throws IOException {
406+ LOGGER .trace (() -> "preVisitDirectory: " + dir );
407+ if (isLink (dir )) {
408+ delete (dir );
409+ return SKIP_SUBTREE ;
410+ }
411+ if (!dir .equals (rootDir )) {
390412 tryToResetPermissions (dir );
391413 }
392414 return CONTINUE ;
393415 }
394416
417+ private boolean isLink (Path dir ) throws IOException {
418+ // While `Files.walkFileTree` does not follow symbolic links, it may follow other links
419+ // such as "junctions" on Windows
420+ return !dir .toRealPath ().startsWith (rootRealPath );
421+ }
422+
395423 @ Override
396424 public FileVisitResult visitFileFailed (Path file , IOException exc ) {
397- if (exc instanceof NoSuchFileException ) {
425+ LOGGER .trace (exc , () -> "visitFileFailed: " + file );
426+ if (exc instanceof NoSuchFileException && !Files .exists (file , LinkOption .NOFOLLOW_LINKS )) {
398427 return CONTINUE ;
399428 }
400429 // IOException includes `AccessDeniedException` thrown by non-readable or non-executable flags
@@ -404,15 +433,19 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) {
404433
405434 @ Override
406435 public FileVisitResult visitFile (Path file , BasicFileAttributes attributes ) {
407- return deleteAndContinue (file );
436+ LOGGER .trace (() -> "visitFile: " + file );
437+ delete (file );
438+ return CONTINUE ;
408439 }
409440
410441 @ Override
411442 public FileVisitResult postVisitDirectory (Path dir , IOException exc ) {
412- return deleteAndContinue (dir );
443+ LOGGER .trace (exc , () -> "postVisitDirectory: " + dir );
444+ delete (dir );
445+ return CONTINUE ;
413446 }
414447
415- private FileVisitResult deleteAndContinue (Path path ) {
448+ private void delete (Path path ) {
416449 try {
417450 fileOperations .delete (path );
418451 }
@@ -426,7 +459,6 @@ private FileVisitResult deleteAndContinue(Path path) {
426459 // IOException includes `AccessDeniedException` thrown by non-readable or non-executable flags
427460 resetPermissionsAndTryToDeleteAgain (path , exception );
428461 }
429- return CONTINUE ;
430462 }
431463
432464 private void resetPermissionsAndTryToDeleteAgain (Path path , IOException exception ) {
0 commit comments