11
11
package org .junit .jupiter .engine .extension ;
12
12
13
13
import static java .nio .file .FileVisitResult .CONTINUE ;
14
+ import static java .nio .file .FileVisitResult .SKIP_SUBTREE ;
14
15
import static java .util .stream .Collectors .joining ;
15
16
import static org .junit .jupiter .api .extension .TestInstantiationAwareExtension .ExtensionContextScope .TEST_METHOD ;
16
17
import static org .junit .jupiter .api .io .CleanupMode .DEFAULT ;
32
33
import java .nio .file .FileSystems ;
33
34
import java .nio .file .FileVisitResult ;
34
35
import java .nio .file .Files ;
36
+ import java .nio .file .LinkOption ;
35
37
import java .nio .file .NoSuchFileException ;
36
38
import java .nio .file .Path ;
37
39
import java .nio .file .Paths ;
@@ -289,7 +291,7 @@ private static boolean selfOrChildFailed(ExtensionContext context) {
289
291
290
292
static class CloseablePath implements CloseableResource {
291
293
292
- private static final Logger logger = LoggerFactory .getLogger (CloseablePath .class );
294
+ private static final Logger LOGGER = LoggerFactory .getLogger (CloseablePath .class );
293
295
294
296
private final Path dir ;
295
297
private final TempDirFactory factory ;
@@ -327,15 +329,27 @@ public void close() throws IOException {
327
329
try {
328
330
if (this .cleanupMode == NEVER
329
331
|| (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." ,
331
333
this .dir , descriptionFor (this .annotatedElement ), this .cleanupMode .name ()));
332
334
return ;
333
335
}
334
336
335
337
FileOperations fileOperations = this .extensionContext .getStore (NAMESPACE ) //
336
338
.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
+ };
337
350
338
- SortedMap <Path , IOException > failures = deleteAllFilesAndDirectories (fileOperations );
351
+ LOGGER .trace (() -> "Cleaning up temp dir " + this .dir );
352
+ SortedMap <Path , IOException > failures = deleteAllFilesAndDirectories (loggingFileOperations );
339
353
if (!failures .isEmpty ()) {
340
354
throw createIOExceptionWithAttachedFailures (failures );
341
355
}
@@ -375,26 +389,41 @@ private static String descriptionFor(Executable executable) {
375
389
private SortedMap <Path , IOException > deleteAllFilesAndDirectories (FileOperations fileOperations )
376
390
throws IOException {
377
391
378
- if (this .dir == null || Files .notExists (this .dir )) {
392
+ Path rootDir = this .dir ;
393
+ if (rootDir == null || Files .notExists (rootDir )) {
379
394
return Collections .emptySortedMap ();
380
395
}
381
396
382
397
SortedMap <Path , IOException > failures = new TreeMap <>();
383
398
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 >() {
386
403
387
404
@ 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 )) {
390
412
tryToResetPermissions (dir );
391
413
}
392
414
return CONTINUE ;
393
415
}
394
416
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
+
395
423
@ Override
396
424
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 )) {
398
427
return CONTINUE ;
399
428
}
400
429
// IOException includes `AccessDeniedException` thrown by non-readable or non-executable flags
@@ -404,15 +433,19 @@ public FileVisitResult visitFileFailed(Path file, IOException exc) {
404
433
405
434
@ Override
406
435
public FileVisitResult visitFile (Path file , BasicFileAttributes attributes ) {
407
- return deleteAndContinue (file );
436
+ LOGGER .trace (() -> "visitFile: " + file );
437
+ delete (file );
438
+ return CONTINUE ;
408
439
}
409
440
410
441
@ Override
411
442
public FileVisitResult postVisitDirectory (Path dir , IOException exc ) {
412
- return deleteAndContinue (dir );
443
+ LOGGER .trace (exc , () -> "postVisitDirectory: " + dir );
444
+ delete (dir );
445
+ return CONTINUE ;
413
446
}
414
447
415
- private FileVisitResult deleteAndContinue (Path path ) {
448
+ private void delete (Path path ) {
416
449
try {
417
450
fileOperations .delete (path );
418
451
}
@@ -426,7 +459,6 @@ private FileVisitResult deleteAndContinue(Path path) {
426
459
// IOException includes `AccessDeniedException` thrown by non-readable or non-executable flags
427
460
resetPermissionsAndTryToDeleteAgain (path , exception );
428
461
}
429
- return CONTINUE ;
430
462
}
431
463
432
464
private void resetPermissionsAndTryToDeleteAgain (Path path , IOException exception ) {
0 commit comments