@@ -12,6 +12,7 @@ import (
12
12
"os"
13
13
"path/filepath"
14
14
"reflect"
15
+ "regexp"
15
16
"runtime"
16
17
"sort"
17
18
"strings"
@@ -388,6 +389,207 @@ func TestFastWalk_LongPath(t *testing.T) {
388
389
}
389
390
}
390
391
392
+ func TestFastWalk_WindowsRootPaths (t * testing.T ) {
393
+ if runtime .GOOS != "windows" {
394
+ t .Skip ("test only supported on Windows" )
395
+ }
396
+
397
+ sameFile := func (t * testing.T , name1 , name2 string ) bool {
398
+ fi1 , err := os .Stat (name1 )
399
+ if err != nil {
400
+ t .Fatal (err )
401
+ }
402
+ fi2 , err := os .Stat (name2 )
403
+ if err != nil {
404
+ t .Fatal (err )
405
+ }
406
+ return os .SameFile (fi1 , fi2 )
407
+ }
408
+
409
+ walk := func (t * testing.T , root string ) map [string ]fs.DirEntry {
410
+ var mu sync.Mutex
411
+ seen := make (map [string ]fs.DirEntry )
412
+ errStop := errors .New ("errStop" )
413
+ fn := func (path string , de fs.DirEntry , err error ) error {
414
+ if err != nil {
415
+ return err
416
+ }
417
+ mu .Lock ()
418
+ seen [path ] = de
419
+ mu .Unlock ()
420
+ if path != root && de .IsDir () {
421
+ return fs .SkipDir
422
+ }
423
+ return nil
424
+ }
425
+ err := fastwalk .Walk (nil , root , fastwalk .IgnorePermissionErrors (fn ))
426
+ if err != nil && err != errStop {
427
+ t .Fatal (err )
428
+ }
429
+ if len (seen ) <= 1 {
430
+ // If we are a child of the root directory we should have visited at
431
+ // least two entries: the root itself and a directory that leads to,
432
+ // or is, our current working directory.
433
+ t .Fatalf ("empty directory: %s" , root )
434
+ }
435
+ return seen
436
+ }
437
+
438
+ pwd , err := filepath .Abs ("." )
439
+ if err != nil {
440
+ t .Fatal (err )
441
+ }
442
+
443
+ vol := filepath .VolumeName (pwd )
444
+ if ! regexp .MustCompile (`^[A-Za-z]:$` ).MatchString (vol ) {
445
+ // Ignore UNC names and other weird Windows paths to keep this simple.
446
+ t .Skipf ("unsupported volume name: %s for path: %s" , vol , pwd )
447
+ }
448
+ if ! sameFile (t , pwd , vol ) {
449
+ t .Skipf ("skipping %s and %s should be considered the same file" , pwd , vol )
450
+ }
451
+
452
+ // Test that walking the disk root ("C:\") actually walks the disk root.
453
+ // Previously, there was a bug where the path "C:\" was transformed to "C:"
454
+ // before walking which caused fastwalk to walk the current directory.
455
+ //
456
+ // https://github.com/charlievieth/fastwalk/issues/37
457
+ t .Run ("FullyQualified" , func (t * testing.T ) {
458
+ root := vol + `\`
459
+ if sameFile (t , pwd , root ) {
460
+ t .Skipf ("the current working directory (%s) is the disk root: %s" , pwd , root )
461
+ }
462
+ seen := walk (t , root )
463
+
464
+ // Make sure we don't append an extraneous slash to the root ("C:\" => "C:\\a").
465
+ for path := range seen {
466
+ rest := strings .TrimPrefix (path , vol )
467
+ if strings .Contains (rest , `\\` ) {
468
+ t .Errorf (`path contains multiple consecutive slashes after volume (%s): "%s"` ,
469
+ vol , path )
470
+ }
471
+ if s := filepath .Clean (path ); s != path {
472
+ t .Errorf (`filepath.Clean("%s") == "%s"` , path , s )
473
+ }
474
+ }
475
+
476
+ // Make sure we didn't walk the current directory. This will happen if
477
+ // the root argument to Walk is a drive letter ("C:\") but we strip off
478
+ // the trailing slash ("C:\" => "C:") since this makes the path relative
479
+ // to the current directory on drive "C".
480
+ //
481
+ // See: https://github.com/charlievieth/fastwalk/issues/37
482
+ //
483
+ // Docs: https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#fully-qualified-vs-relative-paths
484
+ for path , de := range seen {
485
+ if path == root {
486
+ // Ignore root since filepath.Base("C:\") == "\" and "C:\" and "\"
487
+ // are equivalent.
488
+ continue
489
+ }
490
+ fi1 , err := de .Info ()
491
+ if err != nil {
492
+ if os .IsNotExist (err ) || os .IsPermission (err ) {
493
+ continue
494
+ }
495
+ t .Fatal (err )
496
+ }
497
+ name := filepath .Base (path )
498
+ fi2 , err := os .Lstat (name )
499
+ if err != nil {
500
+ continue
501
+ }
502
+ if os .SameFile (fi1 , fi2 ) {
503
+ t .Errorf ("Walking root (%s) returned entries for the current working " +
504
+ "directory (%s): file %s is the same as %s" , root , pwd , path , name )
505
+ }
506
+ }
507
+
508
+ // Add file base name mappings
509
+ for _ , de := range seen {
510
+ seen [de .Name ()] = de
511
+ }
512
+
513
+ // Make sure we read some files from the disk root.
514
+ des , err := os .ReadDir (root )
515
+ if err != nil {
516
+ t .Fatal (err )
517
+ }
518
+ if len (des ) == 0 {
519
+ t .Fatalf ("Disk root %s contains no files!" , root )
520
+ }
521
+ same := 0
522
+ for _ , d2 := range des {
523
+ d1 := seen [d2 .Name ()]
524
+ if d1 == nil {
525
+ continue
526
+ }
527
+ fi1 , err := d1 .Info ()
528
+ if err != nil {
529
+ t .Log (err )
530
+ continue
531
+ }
532
+ fi2 , err := d2 .Info ()
533
+ if err != nil {
534
+ t .Log (err )
535
+ continue
536
+ }
537
+ if os .SameFile (fi1 , fi2 ) {
538
+ same ++
539
+ }
540
+ }
541
+ // TODO: Expect to see N% of files and use
542
+ // a more descriptive error message
543
+ if same == 0 {
544
+ t .Fatalf (`Error failed to walk dist root: "%s"` , root )
545
+ }
546
+ })
547
+
548
+ // Test that paths like "C:" are treated as a relative path.
549
+ t .Run ("Relative" , func (t * testing.T ) {
550
+ seen := walk (t , vol )
551
+
552
+ // Make sure we don't append an extraneous slash to the root ("C:\" => "C:\\a").
553
+ for path := range seen {
554
+ rest := strings .TrimPrefix (path , vol )
555
+ if strings .Contains (rest , `\\` ) {
556
+ t .Errorf (`path contains multiple consecutive slashes after volume (%s): "%s"` ,
557
+ vol , path )
558
+ }
559
+ if path == vol {
560
+ continue // Clean("C:") => "C:."
561
+ }
562
+ if s := filepath .Clean (path ); s != path {
563
+ t .Errorf (`filepath.Clean("%s") == "%s"` , path , s )
564
+ }
565
+ }
566
+
567
+ // Make sure we walk the current directory.
568
+ for path , de := range seen {
569
+ if path == vol {
570
+ // Ignore the volume since filepath.Base("C:") == "\" and "C:" and "\"
571
+ // are not equivalent.
572
+ continue
573
+ }
574
+ fi1 , err := de .Info ()
575
+ if err != nil {
576
+ t .Fatal (err )
577
+ }
578
+ name := filepath .Base (path )
579
+ fi2 , err := os .Lstat (name )
580
+ if err != nil {
581
+ // NB: This test will fail if this file is removed while it's
582
+ // running. There are workarounds for this, but for now it's
583
+ // simpler to just error if that happens.
584
+ t .Fatal (err )
585
+ }
586
+ if ! os .SameFile (fi1 , fi2 ) {
587
+ t .Errorf ("Expected files (%s) and (%s) to be the same" , path , name )
588
+ }
589
+ }
590
+ })
591
+ }
592
+
391
593
func TestFastWalk_Symlink (t * testing.T ) {
392
594
testFastWalk (t , map [string ]string {
393
595
"foo/foo.go" : "one" ,
0 commit comments