github.com/charlievieth/fastwalk@v1.0.3/fastwalk_test.go (about) 1 package fastwalk_test 2 3 import ( 4 "bytes" 5 "crypto/md5" 6 "errors" 7 "flag" 8 "fmt" 9 "io" 10 "io/fs" 11 "os" 12 "path/filepath" 13 "reflect" 14 "runtime" 15 "sort" 16 "strings" 17 "sync" 18 "sync/atomic" 19 "testing" 20 21 "github.com/charlievieth/fastwalk" 22 ) 23 24 func formatFileModes(m map[string]os.FileMode) string { 25 var keys []string 26 for k := range m { 27 keys = append(keys, k) 28 } 29 sort.Strings(keys) 30 var buf bytes.Buffer 31 for _, k := range keys { 32 fmt.Fprintf(&buf, "%-20s: %v\n", k, m[k]) 33 } 34 return buf.String() 35 } 36 37 func writeFile(filename string, data interface{}, perm os.FileMode) error { 38 if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { 39 return err 40 } 41 f, err := os.OpenFile(filename, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm) 42 if err != nil { 43 return err 44 } 45 switch v := data.(type) { 46 case []byte: 47 _, err = f.Write(v) 48 case string: 49 _, err = f.WriteString(v) 50 case io.Reader: 51 _, err = io.Copy(f, v) 52 default: 53 f.Close() 54 return &os.PathError{Op: "WriteFile", Path: filename, 55 Err: fmt.Errorf("invalid data type: %T", data)} 56 } 57 if err1 := f.Close(); err1 != nil && err == nil { 58 err = err1 59 } 60 return err 61 } 62 63 func symlink(t testing.TB, oldname, newname string) error { 64 err := os.Symlink(oldname, newname) 65 if err != nil { 66 if writeErr := os.WriteFile(newname, []byte(newname), 0644); writeErr == nil { 67 // Couldn't create symlink, but could write the file. 68 // Probably this filesystem doesn't support symlinks. 69 // (Perhaps we are on an older Windows and not running as administrator.) 70 t.Skipf("skipping because symlinks appear to be unsupported: %v", err) 71 } 72 } 73 return err 74 } 75 76 func cleanupOrLogTempDir(t *testing.T, tempdir string) { 77 if e := recover(); e != nil { 78 t.Log("TMPDIR:", tempdir) 79 t.Fatal(e) 80 } 81 if t.Failed() { 82 t.Log("TMPDIR:", tempdir) 83 } else { 84 os.RemoveAll(tempdir) 85 } 86 } 87 88 func testCreateFiles(t *testing.T, tempdir string, files map[string]string) { 89 symlinks := map[string]string{} 90 for path, contents := range files { 91 file := filepath.Join(tempdir, "/src", path) 92 if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil { 93 t.Fatal(err) 94 } 95 var err error 96 if strings.HasPrefix(contents, "LINK:") { 97 symlinks[file] = filepath.FromSlash(strings.TrimPrefix(contents, "LINK:")) 98 } else { 99 err = os.WriteFile(file, []byte(contents), 0644) 100 } 101 if err != nil { 102 t.Fatal(err) 103 } 104 } 105 106 // Create symlinks after all other files. Otherwise, directory symlinks on 107 // Windows are unusable (see https://golang.org/issue/39183). 108 for file, dst := range symlinks { 109 if err := symlink(t, dst, file); err != nil { 110 t.Fatal(err) 111 } 112 } 113 } 114 115 func testFastWalkConf(t *testing.T, conf *fastwalk.Config, files map[string]string, callback fs.WalkDirFunc, want map[string]os.FileMode) { 116 tempdir, err := os.MkdirTemp("", "test-fast-walk") 117 if err != nil { 118 t.Fatal(err) 119 } 120 defer cleanupOrLogTempDir(t, tempdir) 121 122 testCreateFiles(t, tempdir, files) 123 124 got := map[string]os.FileMode{} 125 var mu sync.Mutex 126 err = fastwalk.Walk(conf, tempdir, func(path string, de fs.DirEntry, err error) error { 127 if de == nil { 128 t.Errorf("nil fs.DirEntry on %q", path) 129 return nil 130 } 131 mu.Lock() 132 defer mu.Unlock() 133 if !strings.HasPrefix(path, tempdir) { 134 t.Errorf("bogus prefix on %q, expect %q", path, tempdir) 135 } 136 key := filepath.ToSlash(strings.TrimPrefix(path, tempdir)) 137 if old, dup := got[key]; dup { 138 t.Errorf("callback called twice for key %q: %v -> %v", key, old, de.Type()) 139 } 140 got[key] = de.Type() 141 return callback(path, de, err) 142 }) 143 144 if err != nil { 145 t.Fatalf("callback returned: %v", err) 146 } 147 if !reflect.DeepEqual(got, want) { 148 t.Errorf("walk mismatch.\n got:\n%v\nwant:\n%v", formatFileModes(got), formatFileModes(want)) 149 diffFileModes(t, got, want) 150 } 151 } 152 153 func testFastWalk(t *testing.T, files map[string]string, callback fs.WalkDirFunc, want map[string]os.FileMode) { 154 testFastWalkConf(t, nil, files, callback, want) 155 } 156 157 func requireNoError(t testing.TB, err error) { 158 t.Helper() 159 if err != nil { 160 t.Error("WalkDirFunc called with error:", err) 161 panic(err) 162 } 163 } 164 165 func TestFastWalk_Basic(t *testing.T) { 166 testFastWalk(t, map[string]string{ 167 "foo/foo.go": "one", 168 "bar/bar.go": "two", 169 "skip/skip.go": "skip", 170 }, 171 func(path string, typ fs.DirEntry, err error) error { 172 requireNoError(t, err) 173 return nil 174 }, 175 map[string]os.FileMode{ 176 "": os.ModeDir, 177 "/src": os.ModeDir, 178 "/src/bar": os.ModeDir, 179 "/src/bar/bar.go": 0, 180 "/src/foo": os.ModeDir, 181 "/src/foo/foo.go": 0, 182 "/src/skip": os.ModeDir, 183 "/src/skip/skip.go": 0, 184 }) 185 } 186 187 func maxFileNameLength(t testing.TB) int { 188 tmp := t.TempDir() 189 long := strings.Repeat("a", 8192) 190 191 // Returns if n is an invalid file name length 192 invalidLength := func(n int) bool { 193 path := filepath.Join(tmp, long[:n]) 194 err := os.WriteFile(path, []byte("1"), 0644) 195 if err == nil { 196 os.Remove(path) 197 } 198 return err != nil 199 } 200 201 // Use a binary search to find the max filename length (+1) 202 n := sort.Search(8192, invalidLength) 203 if n <= 1 { 204 t.Fatal("Failed to find the max filename length:", n) 205 } 206 max := n - 1 207 if invalidLength(max) { 208 t.Fatal("Failed to find the max filename length:", n) 209 } 210 return max 211 } 212 213 // This test identified a "checkptr: converted pointer straddles multiple allocations" 214 // error on darwin when getdirentries64 was used with the race-detector enabled. 215 func TestFastWalk_LongFileName(t *testing.T) { 216 maxNameLen := maxFileNameLength(t) 217 if maxNameLen > 255 { 218 maxNameLen = 255 219 } 220 want := map[string]os.FileMode{ 221 "": os.ModeDir, 222 "/src": os.ModeDir, 223 } 224 files := make(map[string]string) 225 // This triggers with only one sub-directory but use 2 just to be sure. 226 for r := 'a'; r <= 'b'; r++ { 227 s := string(r) 228 name := s + "/" + strings.Repeat(s, maxNameLen) 229 for i := len("_/") + 1; i <= len(name); i++ { 230 files[name[:i]] = "1" 231 want["/src/"+name[:i]] = 0 232 } 233 want["/src/"+s] = os.ModeDir 234 } 235 testFastWalk(t, files, 236 func(path string, typ fs.DirEntry, err error) error { 237 requireNoError(t, err) 238 return nil 239 }, 240 want, 241 ) 242 } 243 244 func TestFastWalk_Symlink(t *testing.T) { 245 testFastWalk(t, map[string]string{ 246 "foo/foo.go": "one", 247 "bar/bar.go": "LINK:../foo/foo.go", 248 "symdir": "LINK:foo", 249 "broken/broken.go": "LINK:../nonexistent", 250 }, 251 func(path string, typ fs.DirEntry, err error) error { 252 requireNoError(t, err) 253 return nil 254 }, 255 map[string]os.FileMode{ 256 "": os.ModeDir, 257 "/src": os.ModeDir, 258 "/src/bar": os.ModeDir, 259 "/src/bar/bar.go": os.ModeSymlink, 260 "/src/foo": os.ModeDir, 261 "/src/foo/foo.go": 0, 262 "/src/symdir": os.ModeSymlink, 263 "/src/broken": os.ModeDir, 264 "/src/broken/broken.go": os.ModeSymlink, 265 }) 266 } 267 268 // Test that the fs.DirEntry passed to WalkFunc is always a fastwalk.DirEntry. 269 func TestFastWalk_DirEntryType(t *testing.T) { 270 testFastWalk(t, map[string]string{ 271 "foo/foo.go": "one", 272 "bar/bar.go": "LINK:../foo/foo.go", 273 "symdir": "LINK:foo", 274 "broken/broken.go": "LINK:../nonexistent", 275 }, 276 func(path string, de fs.DirEntry, err error) error { 277 requireNoError(t, err) 278 if _, ok := de.(fastwalk.DirEntry); !ok { 279 t.Errorf("%q: not a fastwalk.DirEntry: %T", path, de) 280 } 281 if de.Type() != de.Type().Type() { 282 t.Errorf("%s: type mismatch got: %q want: %q", 283 path, de.Type(), de.Type().Type()) 284 } 285 return nil 286 }, 287 map[string]os.FileMode{ 288 "": os.ModeDir, 289 "/src": os.ModeDir, 290 "/src/bar": os.ModeDir, 291 "/src/bar/bar.go": os.ModeSymlink, 292 "/src/foo": os.ModeDir, 293 "/src/foo/foo.go": 0, 294 "/src/symdir": os.ModeSymlink, 295 "/src/broken": os.ModeDir, 296 "/src/broken/broken.go": os.ModeSymlink, 297 }) 298 } 299 300 func TestFastWalk_SkipDir(t *testing.T) { 301 testFastWalk(t, map[string]string{ 302 "foo/foo.go": "one", 303 "bar/bar.go": "two", 304 "skip/skip.go": "skip", 305 }, 306 func(path string, de fs.DirEntry, err error) error { 307 requireNoError(t, err) 308 typ := de.Type().Type() 309 if typ == os.ModeDir && strings.HasSuffix(path, "skip") { 310 return filepath.SkipDir 311 } 312 return nil 313 }, 314 map[string]os.FileMode{ 315 "": os.ModeDir, 316 "/src": os.ModeDir, 317 "/src/bar": os.ModeDir, 318 "/src/bar/bar.go": 0, 319 "/src/foo": os.ModeDir, 320 "/src/foo/foo.go": 0, 321 "/src/skip": os.ModeDir, 322 }) 323 } 324 325 func TestFastWalk_SkipFiles(t *testing.T) { 326 // Directory iteration order is undefined, so there's no way to know 327 // which file to expect until the walk happens. Rather than mess 328 // with the test infrastructure, just mutate want. 329 var mu sync.Mutex 330 want := map[string]os.FileMode{ 331 "": os.ModeDir, 332 "/src": os.ModeDir, 333 "/src/zzz": os.ModeDir, 334 "/src/zzz/c.go": 0, 335 } 336 337 testFastWalk(t, map[string]string{ 338 "a_skipfiles.go": "a", 339 "b_skipfiles.go": "b", 340 "zzz/c.go": "c", 341 }, 342 func(path string, _ fs.DirEntry, err error) error { 343 requireNoError(t, err) 344 if strings.HasSuffix(path, "_skipfiles.go") { 345 mu.Lock() 346 defer mu.Unlock() 347 want["/src/"+filepath.Base(path)] = 0 348 return fastwalk.ErrSkipFiles 349 } 350 return nil 351 }, 352 want) 353 if len(want) != 5 { 354 t.Errorf("saw too many files: wanted 5, got %v (%v)", len(want), want) 355 } 356 } 357 358 func TestFastWalk_TraverseSymlink(t *testing.T) { 359 testFastWalk(t, map[string]string{ 360 "foo/foo.go": "one", 361 "bar/bar.go": "two", 362 "symdir": "LINK:foo", 363 }, 364 func(path string, de fs.DirEntry, err error) error { 365 requireNoError(t, err) 366 typ := de.Type().Type() 367 if typ == os.ModeSymlink { 368 return fastwalk.ErrTraverseLink 369 } 370 return nil 371 }, 372 map[string]os.FileMode{ 373 "": os.ModeDir, 374 "/src": os.ModeDir, 375 "/src/bar": os.ModeDir, 376 "/src/bar/bar.go": 0, 377 "/src/foo": os.ModeDir, 378 "/src/foo/foo.go": 0, 379 "/src/symdir": os.ModeSymlink, 380 "/src/symdir/foo.go": 0, 381 }) 382 } 383 384 func TestFastWalk_Follow(t *testing.T) { 385 subTests := []struct { 386 Name string 387 OnLink func(path string, d fs.DirEntry) error 388 }{ 389 // Test that the walk func does *not* need to return 390 // ErrTraverseLink for links to be followed. 391 { 392 Name: "Default", 393 OnLink: func(path string, d fs.DirEntry) error { return nil }, 394 }, 395 396 // Test that returning ErrTraverseLink does not interfere 397 // with the Follow logic. 398 { 399 Name: "ErrTraverseLink", 400 OnLink: func(path string, d fs.DirEntry) error { 401 if d.Type()&os.ModeSymlink != 0 { 402 if fi, err := fastwalk.StatDirEntry(path, d); err == nil && fi.IsDir() { 403 return fastwalk.ErrTraverseLink 404 } 405 } 406 return nil 407 }, 408 }, 409 } 410 for _, x := range subTests { 411 t.Run(x.Name, func(t *testing.T) { 412 conf := fastwalk.Config{ 413 Follow: true, 414 } 415 testFastWalkConf(t, &conf, map[string]string{ 416 "foo/foo.go": "one", 417 "bar/bar.go": "two", 418 "foo/symlink": "LINK:foo.go", 419 "bar/symdir": "LINK:../foo/", 420 "bar/link1": "LINK:../foo/", 421 }, 422 func(path string, de fs.DirEntry, err error) error { 423 requireNoError(t, err) 424 if err != nil { 425 return err 426 } 427 if de.Type()&os.ModeSymlink != 0 { 428 return x.OnLink(path, de) 429 } 430 return nil 431 }, 432 map[string]os.FileMode{ 433 "": os.ModeDir, 434 "/src": os.ModeDir, 435 "/src/bar": os.ModeDir, 436 "/src/bar/bar.go": 0, 437 "/src/bar/link1": os.ModeSymlink, 438 "/src/bar/link1/foo.go": 0, 439 "/src/bar/link1/symlink": os.ModeSymlink, 440 "/src/bar/symdir": os.ModeSymlink, 441 "/src/bar/symdir/foo.go": 0, 442 "/src/bar/symdir/symlink": os.ModeSymlink, 443 "/src/foo": os.ModeDir, 444 "/src/foo/foo.go": 0, 445 "/src/foo/symlink": os.ModeSymlink, 446 }) 447 }) 448 } 449 } 450 451 func TestFastWalk_Follow_SkipDir(t *testing.T) { 452 conf := fastwalk.Config{ 453 Follow: true, 454 } 455 testFastWalkConf(t, &conf, map[string]string{ 456 ".dot/baz.go": "one", 457 "bar/bar.go": "three", 458 "bar/dot": "LINK:../.dot/", 459 "bar/symdir": "LINK:../foo/", 460 "foo/foo.go": "two", 461 "foo/symlink": "LINK:foo.go", 462 }, 463 func(path string, de fs.DirEntry, err error) error { 464 requireNoError(t, err) 465 if err != nil { 466 return err 467 } 468 if strings.HasPrefix(de.Name(), ".") { 469 return filepath.SkipDir 470 } 471 return nil 472 }, 473 map[string]os.FileMode{ 474 "": os.ModeDir, 475 "/src": os.ModeDir, 476 "/src/.dot": os.ModeDir, 477 "/src/bar": os.ModeDir, 478 "/src/bar/bar.go": 0, 479 "/src/bar/dot": os.ModeSymlink, 480 "/src/bar/dot/baz.go": 0, 481 "/src/bar/symdir": os.ModeSymlink, 482 "/src/bar/symdir/foo.go": 0, 483 "/src/bar/symdir/symlink": os.ModeSymlink, 484 "/src/foo": os.ModeDir, 485 "/src/foo/foo.go": 0, 486 "/src/foo/symlink": os.ModeSymlink, 487 }) 488 } 489 490 func TestFastWalk_Follow_SymlinkLoop(t *testing.T) { 491 tempdir, err := os.MkdirTemp("", "test-fast-walk") 492 if err != nil { 493 t.Fatal(err) 494 } 495 defer cleanupOrLogTempDir(t, tempdir) 496 497 if err := writeFile(tempdir+"/src/foo.go", "hello", 0644); err != nil { 498 t.Fatal(err) 499 } 500 if err := symlink(t, "../src", tempdir+"/src/loop"); err != nil { 501 t.Fatal(err) 502 } 503 504 conf := fastwalk.Config{ 505 Follow: true, 506 } 507 var walked int32 508 err = fastwalk.Walk(&conf, tempdir, func(path string, de fs.DirEntry, err error) error { 509 if err != nil { 510 return err 511 } 512 if n := atomic.AddInt32(&walked, 1); n > 20 { 513 return fmt.Errorf("symlink loop: %d", n) 514 } 515 return nil 516 }) 517 if err != nil { 518 t.Fatal(err) 519 } 520 } 521 522 // Test that ErrTraverseLink is ignored when following symlinks 523 // if it would cause a symlink loop. 524 func TestFastWalk_Follow_ErrTraverseLink(t *testing.T) { 525 conf := fastwalk.Config{ 526 Follow: true, 527 } 528 testFastWalkConf(t, &conf, map[string]string{ 529 "foo/foo.go": "one", 530 "bar/bar.go": "two", 531 "bar/symdir": "LINK:../foo/", 532 "bar/loop": "LINK:../bar/", // symlink loop 533 }, 534 func(path string, de fs.DirEntry, err error) error { 535 requireNoError(t, err) 536 if err != nil { 537 return err 538 } 539 if de.Type()&os.ModeSymlink != 0 { 540 if fi, err := fastwalk.StatDirEntry(path, de); err == nil && fi.IsDir() { 541 return fastwalk.ErrTraverseLink 542 } 543 } 544 return nil 545 }, 546 map[string]os.FileMode{ 547 "": os.ModeDir, 548 "/src": os.ModeDir, 549 "/src/bar": os.ModeDir, 550 "/src/bar/bar.go": 0, 551 "/src/bar/loop": os.ModeSymlink, 552 "/src/bar/symdir": os.ModeSymlink, 553 "/src/bar/symdir/foo.go": 0, 554 "/src/foo": os.ModeDir, 555 "/src/foo/foo.go": 0, 556 }) 557 } 558 559 func TestFastWalk_Error(t *testing.T) { 560 tmp := t.TempDir() 561 for _, child := range []string{ 562 "foo/foo.go", 563 "bar/bar.go", 564 "skip/skip.go", 565 } { 566 if err := writeFile(filepath.Join(tmp, child), child, 0644); err != nil { 567 t.Fatal(err) 568 } 569 } 570 571 exp := errors.New("expected") 572 err := fastwalk.Walk(nil, tmp, func(_ string, _ fs.DirEntry, err error) error { 573 requireNoError(t, err) 574 return exp 575 }) 576 if !errors.Is(err, exp) { 577 t.Errorf("want error: %#v got: %#v", exp, err) 578 } 579 } 580 581 func TestFastWalk_ErrNotExist(t *testing.T) { 582 tmp := t.TempDir() 583 if err := os.Remove(tmp); err != nil { 584 t.Fatal(err) 585 } 586 err := fastwalk.Walk(nil, tmp, func(_ string, _ fs.DirEntry, err error) error { 587 return err 588 }) 589 if !os.IsNotExist(err) { 590 t.Fatalf("os.IsNotExist(%+v) = false want: true", err) 591 } 592 } 593 594 func TestFastWalk_ErrPermission(t *testing.T) { 595 if runtime.GOOS == "windows" { 596 t.Skip("test not-supported for Windows") 597 } 598 tempdir := t.TempDir() 599 want := map[string]os.FileMode{ 600 "": os.ModeDir, 601 "/bad": os.ModeDir, 602 } 603 for i := 0; i < runtime.NumCPU()*4; i++ { 604 dir := fmt.Sprintf("/d%03d", i) 605 name := fmt.Sprintf("%s/f%03d.txt", dir, i) 606 if err := writeFile(filepath.Join(tempdir, name), "data", 0644); err != nil { 607 t.Fatal(err) 608 } 609 want[name] = 0 610 want[filepath.Dir(name)] = os.ModeDir 611 } 612 613 filename := filepath.Join(tempdir, "/bad/bad.txt") 614 if err := writeFile(filename, "data", 0644); err != nil { 615 t.Fatal(err) 616 } 617 // Make the directory unreadable 618 dirname := filepath.Dir(filename) 619 if err := os.Chmod(dirname, 0355); err != nil { 620 t.Fatal(err) 621 } 622 t.Cleanup(func() { 623 if err := os.Remove(filename); err != nil { 624 t.Error(err) 625 } 626 if err := os.Remove(dirname); err != nil { 627 t.Error(err) 628 } 629 }) 630 631 got := map[string]os.FileMode{} 632 var mu sync.Mutex 633 err := fastwalk.Walk(nil, tempdir, func(path string, de fs.DirEntry, err error) error { 634 if err != nil && os.IsPermission(err) { 635 return nil 636 } 637 638 mu.Lock() 639 defer mu.Unlock() 640 if !strings.HasPrefix(path, tempdir) { 641 t.Errorf("bogus prefix on %q, expect %q", path, tempdir) 642 } 643 key := filepath.ToSlash(strings.TrimPrefix(path, tempdir)) 644 if old, dup := got[key]; dup { 645 t.Errorf("callback called twice for key %q: %v -> %v", key, old, de.Type()) 646 } 647 got[key] = de.Type() 648 return nil 649 }) 650 if err != nil { 651 t.Error("Walk:", err) 652 } 653 if !reflect.DeepEqual(got, want) { 654 t.Errorf("walk mismatch.\n got:\n%v\nwant:\n%v", formatFileModes(got), formatFileModes(want)) 655 diffFileModes(t, got, want) 656 } 657 } 658 659 func diffFileModes(t *testing.T, got, want map[string]os.FileMode) { 660 type Mode struct { 661 Name string 662 Mode os.FileMode 663 } 664 var extra []Mode 665 for k, v := range got { 666 if _, ok := want[k]; !ok { 667 extra = append(extra, Mode{k, v}) 668 } 669 } 670 var missing []Mode 671 for k, v := range want { 672 if _, ok := got[k]; !ok { 673 missing = append(missing, Mode{k, v}) 674 } 675 } 676 var delta []Mode 677 for k, v := range got { 678 if vv, ok := want[k]; ok && vv != v { 679 delta = append(delta, Mode{k, v}, Mode{k, vv}) 680 } 681 } 682 w := new(strings.Builder) 683 printMode := func(name string, modes []Mode) { 684 if len(modes) == 0 { 685 return 686 } 687 sort.Slice(modes, func(i, j int) bool { 688 return modes[i].Name < modes[j].Name 689 }) 690 if w.Len() == 0 { 691 w.WriteString("\n") 692 } 693 fmt.Fprintf(w, "%s:\n", name) 694 for _, m := range modes { 695 fmt.Fprintf(w, " %-20s: %s\n", m.Name, m.Mode.String()) 696 } 697 } 698 printMode("Extra", extra) 699 printMode("Missing", missing) 700 printMode("Delta", delta) 701 if w.Len() != 0 { 702 t.Error(w.String()) 703 } 704 } 705 706 // Directory to use for benchmarks, GOROOT is used by default 707 var benchDir *string 708 709 // Make sure we don't register the "benchdir" twice. 710 func init() { 711 ff := flag.Lookup("benchdir") 712 if ff != nil { 713 value := ff.DefValue 714 if ff.Value != nil { 715 value = ff.Value.String() 716 } 717 benchDir = &value 718 } else { 719 benchDir = flag.String("benchdir", runtime.GOROOT(), "The directory to scan for BenchmarkFastWalk") 720 } 721 } 722 723 func noopWalkFunc(_ string, _ fs.DirEntry, _ error) error { return nil } 724 725 func benchmarkFastWalk(b *testing.B, conf *fastwalk.Config, 726 adapter func(fs.WalkDirFunc) fs.WalkDirFunc) { 727 728 b.ReportAllocs() 729 if adapter != nil { 730 walkFn := noopWalkFunc 731 for i := 0; i < b.N; i++ { 732 err := fastwalk.Walk(conf, *benchDir, adapter(walkFn)) 733 if err != nil { 734 b.Fatal(err) 735 } 736 } 737 } else { 738 for i := 0; i < b.N; i++ { 739 err := fastwalk.Walk(conf, *benchDir, noopWalkFunc) 740 if err != nil { 741 b.Fatal(err) 742 } 743 } 744 } 745 } 746 747 func BenchmarkFastWalk(b *testing.B) { 748 benchmarkFastWalk(b, nil, nil) 749 } 750 751 func BenchmarkFastWalkFollow(b *testing.B) { 752 benchmarkFastWalk(b, &fastwalk.Config{Follow: true}, nil) 753 } 754 755 func BenchmarkFastWalkAdapters(b *testing.B) { 756 if testing.Short() { 757 b.Skip("Skipping: short test") 758 } 759 b.Run("IgnoreDuplicateDirs", func(b *testing.B) { 760 benchmarkFastWalk(b, nil, fastwalk.IgnoreDuplicateDirs) 761 }) 762 763 b.Run("IgnoreDuplicateFiles", func(b *testing.B) { 764 benchmarkFastWalk(b, nil, fastwalk.IgnoreDuplicateFiles) 765 }) 766 } 767 768 // Benchmark various tasks with different worker counts. 769 // 770 // Observations: 771 // - Linux (Intel i9-9900K / Samsung Pro NVMe): consistently benefits from 772 // more workers 773 // - Darwin (m1): IO heavy tasks (Readfile and Stat) and Traversal perform 774 // best with 4 workers, and only CPU bound tasks benefit from more workers 775 func BenchmarkFastWalkNumWorkers(b *testing.B) { 776 if testing.Short() { 777 b.Skip("Skipping: short test") 778 } 779 780 runBench := func(b *testing.B, walkFn fs.WalkDirFunc) { 781 maxWorkers := runtime.NumCPU() 782 for i := 2; i <= maxWorkers; i += 2 { 783 b.Run(fmt.Sprint(i), func(b *testing.B) { 784 conf := fastwalk.Config{ 785 NumWorkers: i, 786 } 787 for i := 0; i < b.N; i++ { 788 if err := fastwalk.Walk(&conf, *benchDir, walkFn); err != nil { 789 b.Fatal(err) 790 } 791 } 792 }) 793 } 794 } 795 796 // Bench pure traversal speed 797 b.Run("NoOp", func(b *testing.B) { 798 runBench(b, func(path string, d fs.DirEntry, err error) error { 799 return err 800 }) 801 }) 802 803 // No IO and light CPU 804 b.Run("NoIO", func(b *testing.B) { 805 runBench(b, func(path string, d fs.DirEntry, err error) error { 806 if err == nil { 807 fmt.Fprintf(io.Discard, "%s: %q\n", d.Type(), path) 808 } 809 return err 810 }) 811 }) 812 813 // Stat each regular file 814 b.Run("Stat", func(b *testing.B) { 815 runBench(b, func(path string, d fs.DirEntry, err error) error { 816 if err == nil && d.Type().IsRegular() { 817 _, _ = d.Info() 818 } 819 return err 820 }) 821 }) 822 823 // IO heavy task 824 b.Run("ReadFile", func(b *testing.B) { 825 runBench(b, func(path string, d fs.DirEntry, err error) error { 826 if err != nil || !d.Type().IsRegular() { 827 return err 828 } 829 f, err := os.Open(path) 830 if err != nil { 831 if os.IsNotExist(err) || os.IsPermission(err) { 832 return nil 833 } 834 return err 835 } 836 defer f.Close() 837 838 _, err = io.Copy(io.Discard, f) 839 return err 840 }) 841 }) 842 843 // CPU and IO heavy task 844 b.Run("Hash", func(b *testing.B) { 845 bufPool := &sync.Pool{ 846 New: func() interface{} { 847 b := make([]byte, 96*1024) 848 return &b 849 }, 850 } 851 runBench(b, func(path string, d fs.DirEntry, err error) error { 852 if err != nil || !d.Type().IsRegular() { 853 return err 854 } 855 f, err := os.Open(path) 856 if err != nil { 857 if os.IsNotExist(err) || os.IsPermission(err) { 858 return nil 859 } 860 return err 861 } 862 defer f.Close() 863 864 p := bufPool.Get().(*[]byte) 865 h := md5.New() 866 _, err = io.CopyBuffer(h, f, *p) 867 bufPool.Put(p) 868 _ = h.Sum(nil) 869 return err 870 }) 871 }) 872 } 873 874 var benchWalkFunc = flag.String("walkfunc", "fastwalk", "The function to use for BenchmarkWalkComparison") 875 876 // BenchmarkWalkComparison generates benchmarks using different walk functions 877 // so that the results can be easily compared with `benchcmp` and `benchstat`. 878 func BenchmarkWalkComparison(b *testing.B) { 879 if testing.Short() { 880 b.Skip("Skipping: short test") 881 } 882 switch *benchWalkFunc { 883 case "fastwalk": 884 benchmarkFastWalk(b, nil, nil) 885 case "godirwalk": 886 b.Fatal("comparisons with godirwalk are no longer supported") 887 case "filepath": 888 for i := 0; i < b.N; i++ { 889 err := filepath.WalkDir(*benchDir, func(_ string, _ fs.DirEntry, _ error) error { 890 return nil 891 }) 892 if err != nil { 893 b.Fatal(err) 894 } 895 } 896 default: 897 b.Fatalf("invalid walkfunc: %q", *benchWalkFunc) 898 } 899 }