github.com/code-reading/golang@v0.0.0-20220303082512-ba5bc0e589a3/go/src/testing/fstest/testfs.go (about) 1 // Copyright 2020 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 // Package fstest implements support for testing implementations and users of file systems. 6 package fstest 7 8 import ( 9 "errors" 10 "fmt" 11 "io" 12 "io/fs" 13 "path" 14 "reflect" 15 "sort" 16 "strings" 17 "testing/iotest" 18 ) 19 20 // TestFS tests a file system implementation. 21 // It walks the entire tree of files in fsys, 22 // opening and checking that each file behaves correctly. 23 // It also checks that the file system contains at least the expected files. 24 // As a special case, if no expected files are listed, fsys must be empty. 25 // Otherwise, fsys must contain at least the listed files; it can also contain others. 26 // The contents of fsys must not change concurrently with TestFS. 27 // 28 // If TestFS finds any misbehaviors, it returns an error reporting all of them. 29 // The error text spans multiple lines, one per detected misbehavior. 30 // 31 // Typical usage inside a test is: 32 // 33 // if err := fstest.TestFS(myFS, "file/that/should/be/present"); err != nil { 34 // t.Fatal(err) 35 // } 36 // 37 func TestFS(fsys fs.FS, expected ...string) error { 38 if err := testFS(fsys, expected...); err != nil { 39 return err 40 } 41 for _, name := range expected { 42 if i := strings.Index(name, "/"); i >= 0 { 43 dir, dirSlash := name[:i], name[:i+1] 44 var subExpected []string 45 for _, name := range expected { 46 if strings.HasPrefix(name, dirSlash) { 47 subExpected = append(subExpected, name[len(dirSlash):]) 48 } 49 } 50 sub, err := fs.Sub(fsys, dir) 51 if err != nil { 52 return err 53 } 54 if err := testFS(sub, subExpected...); err != nil { 55 return fmt.Errorf("testing fs.Sub(fsys, %s): %v", dir, err) 56 } 57 break // one sub-test is enough 58 } 59 } 60 return nil 61 } 62 63 func testFS(fsys fs.FS, expected ...string) error { 64 t := fsTester{fsys: fsys} 65 t.checkDir(".") 66 t.checkOpen(".") 67 found := make(map[string]bool) 68 for _, dir := range t.dirs { 69 found[dir] = true 70 } 71 for _, file := range t.files { 72 found[file] = true 73 } 74 delete(found, ".") 75 if len(expected) == 0 && len(found) > 0 { 76 var list []string 77 for k := range found { 78 if k != "." { 79 list = append(list, k) 80 } 81 } 82 sort.Strings(list) 83 if len(list) > 15 { 84 list = append(list[:10], "...") 85 } 86 t.errorf("expected empty file system but found files:\n%s", strings.Join(list, "\n")) 87 } 88 for _, name := range expected { 89 if !found[name] { 90 t.errorf("expected but not found: %s", name) 91 } 92 } 93 if len(t.errText) == 0 { 94 return nil 95 } 96 return errors.New("TestFS found errors:\n" + string(t.errText)) 97 } 98 99 // An fsTester holds state for running the test. 100 type fsTester struct { 101 fsys fs.FS 102 errText []byte 103 dirs []string 104 files []string 105 } 106 107 // errorf adds an error line to errText. 108 func (t *fsTester) errorf(format string, args ...interface{}) { 109 if len(t.errText) > 0 { 110 t.errText = append(t.errText, '\n') 111 } 112 t.errText = append(t.errText, fmt.Sprintf(format, args...)...) 113 } 114 115 func (t *fsTester) openDir(dir string) fs.ReadDirFile { 116 f, err := t.fsys.Open(dir) 117 if err != nil { 118 t.errorf("%s: Open: %v", dir, err) 119 return nil 120 } 121 d, ok := f.(fs.ReadDirFile) 122 if !ok { 123 f.Close() 124 t.errorf("%s: Open returned File type %T, not a fs.ReadDirFile", dir, f) 125 return nil 126 } 127 return d 128 } 129 130 // checkDir checks the directory dir, which is expected to exist 131 // (it is either the root or was found in a directory listing with IsDir true). 132 func (t *fsTester) checkDir(dir string) { 133 // Read entire directory. 134 t.dirs = append(t.dirs, dir) 135 d := t.openDir(dir) 136 if d == nil { 137 return 138 } 139 list, err := d.ReadDir(-1) 140 if err != nil { 141 d.Close() 142 t.errorf("%s: ReadDir(-1): %v", dir, err) 143 return 144 } 145 146 // Check all children. 147 var prefix string 148 if dir == "." { 149 prefix = "" 150 } else { 151 prefix = dir + "/" 152 } 153 for _, info := range list { 154 name := info.Name() 155 switch { 156 case name == ".", name == "..", name == "": 157 t.errorf("%s: ReadDir: child has invalid name: %#q", dir, name) 158 continue 159 case strings.Contains(name, "/"): 160 t.errorf("%s: ReadDir: child name contains slash: %#q", dir, name) 161 continue 162 case strings.Contains(name, `\`): 163 t.errorf("%s: ReadDir: child name contains backslash: %#q", dir, name) 164 continue 165 } 166 path := prefix + name 167 t.checkStat(path, info) 168 t.checkOpen(path) 169 if info.IsDir() { 170 t.checkDir(path) 171 } else { 172 t.checkFile(path) 173 } 174 } 175 176 // Check ReadDir(-1) at EOF. 177 list2, err := d.ReadDir(-1) 178 if len(list2) > 0 || err != nil { 179 d.Close() 180 t.errorf("%s: ReadDir(-1) at EOF = %d entries, %v, wanted 0 entries, nil", dir, len(list2), err) 181 return 182 } 183 184 // Check ReadDir(1) at EOF (different results). 185 list2, err = d.ReadDir(1) 186 if len(list2) > 0 || err != io.EOF { 187 d.Close() 188 t.errorf("%s: ReadDir(1) at EOF = %d entries, %v, wanted 0 entries, EOF", dir, len(list2), err) 189 return 190 } 191 192 // Check that close does not report an error. 193 if err := d.Close(); err != nil { 194 t.errorf("%s: Close: %v", dir, err) 195 } 196 197 // Check that closing twice doesn't crash. 198 // The return value doesn't matter. 199 d.Close() 200 201 // Reopen directory, read a second time, make sure contents match. 202 if d = t.openDir(dir); d == nil { 203 return 204 } 205 defer d.Close() 206 list2, err = d.ReadDir(-1) 207 if err != nil { 208 t.errorf("%s: second Open+ReadDir(-1): %v", dir, err) 209 return 210 } 211 t.checkDirList(dir, "first Open+ReadDir(-1) vs second Open+ReadDir(-1)", list, list2) 212 213 // Reopen directory, read a third time in pieces, make sure contents match. 214 if d = t.openDir(dir); d == nil { 215 return 216 } 217 defer d.Close() 218 list2 = nil 219 for { 220 n := 1 221 if len(list2) > 0 { 222 n = 2 223 } 224 frag, err := d.ReadDir(n) 225 if len(frag) > n { 226 t.errorf("%s: third Open: ReadDir(%d) after %d: %d entries (too many)", dir, n, len(list2), len(frag)) 227 return 228 } 229 list2 = append(list2, frag...) 230 if err == io.EOF { 231 break 232 } 233 if err != nil { 234 t.errorf("%s: third Open: ReadDir(%d) after %d: %v", dir, n, len(list2), err) 235 return 236 } 237 if n == 0 { 238 t.errorf("%s: third Open: ReadDir(%d) after %d: 0 entries but nil error", dir, n, len(list2)) 239 return 240 } 241 } 242 t.checkDirList(dir, "first Open+ReadDir(-1) vs third Open+ReadDir(1,2) loop", list, list2) 243 244 // If fsys has ReadDir, check that it matches and is sorted. 245 if fsys, ok := t.fsys.(fs.ReadDirFS); ok { 246 list2, err := fsys.ReadDir(dir) 247 if err != nil { 248 t.errorf("%s: fsys.ReadDir: %v", dir, err) 249 return 250 } 251 t.checkDirList(dir, "first Open+ReadDir(-1) vs fsys.ReadDir", list, list2) 252 253 for i := 0; i+1 < len(list2); i++ { 254 if list2[i].Name() >= list2[i+1].Name() { 255 t.errorf("%s: fsys.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name()) 256 } 257 } 258 } 259 260 // Check fs.ReadDir as well. 261 list2, err = fs.ReadDir(t.fsys, dir) 262 if err != nil { 263 t.errorf("%s: fs.ReadDir: %v", dir, err) 264 return 265 } 266 t.checkDirList(dir, "first Open+ReadDir(-1) vs fs.ReadDir", list, list2) 267 268 for i := 0; i+1 < len(list2); i++ { 269 if list2[i].Name() >= list2[i+1].Name() { 270 t.errorf("%s: fs.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name()) 271 } 272 } 273 274 t.checkGlob(dir, list) 275 } 276 277 // formatEntry formats an fs.DirEntry into a string for error messages and comparison. 278 func formatEntry(entry fs.DirEntry) string { 279 return fmt.Sprintf("%s IsDir=%v Type=%v", entry.Name(), entry.IsDir(), entry.Type()) 280 } 281 282 // formatInfoEntry formats an fs.FileInfo into a string like the result of formatEntry, for error messages and comparison. 283 func formatInfoEntry(info fs.FileInfo) string { 284 return fmt.Sprintf("%s IsDir=%v Type=%v", info.Name(), info.IsDir(), info.Mode().Type()) 285 } 286 287 // formatInfo formats an fs.FileInfo into a string for error messages and comparison. 288 func formatInfo(info fs.FileInfo) string { 289 return fmt.Sprintf("%s IsDir=%v Mode=%v Size=%d ModTime=%v", info.Name(), info.IsDir(), info.Mode(), info.Size(), info.ModTime()) 290 } 291 292 // checkGlob checks that various glob patterns work if the file system implements GlobFS. 293 func (t *fsTester) checkGlob(dir string, list []fs.DirEntry) { 294 if _, ok := t.fsys.(fs.GlobFS); !ok { 295 return 296 } 297 298 // Make a complex glob pattern prefix that only matches dir. 299 var glob string 300 if dir != "." { 301 elem := strings.Split(dir, "/") 302 for i, e := range elem { 303 var pattern []rune 304 for j, r := range e { 305 if r == '*' || r == '?' || r == '\\' || r == '[' || r == '-' { 306 pattern = append(pattern, '\\', r) 307 continue 308 } 309 switch (i + j) % 5 { 310 case 0: 311 pattern = append(pattern, r) 312 case 1: 313 pattern = append(pattern, '[', r, ']') 314 case 2: 315 pattern = append(pattern, '[', r, '-', r, ']') 316 case 3: 317 pattern = append(pattern, '[', '\\', r, ']') 318 case 4: 319 pattern = append(pattern, '[', '\\', r, '-', '\\', r, ']') 320 } 321 } 322 elem[i] = string(pattern) 323 } 324 glob = strings.Join(elem, "/") + "/" 325 } 326 327 // Test that malformed patterns are detected. 328 // The error is likely path.ErrBadPattern but need not be. 329 if _, err := t.fsys.(fs.GlobFS).Glob(glob + "nonexist/[]"); err == nil { 330 t.errorf("%s: Glob(%#q): bad pattern not detected", dir, glob+"nonexist/[]") 331 } 332 333 // Try to find a letter that appears in only some of the final names. 334 c := rune('a') 335 for ; c <= 'z'; c++ { 336 have, haveNot := false, false 337 for _, d := range list { 338 if strings.ContainsRune(d.Name(), c) { 339 have = true 340 } else { 341 haveNot = true 342 } 343 } 344 if have && haveNot { 345 break 346 } 347 } 348 if c > 'z' { 349 c = 'a' 350 } 351 glob += "*" + string(c) + "*" 352 353 var want []string 354 for _, d := range list { 355 if strings.ContainsRune(d.Name(), c) { 356 want = append(want, path.Join(dir, d.Name())) 357 } 358 } 359 360 names, err := t.fsys.(fs.GlobFS).Glob(glob) 361 if err != nil { 362 t.errorf("%s: Glob(%#q): %v", dir, glob, err) 363 return 364 } 365 if reflect.DeepEqual(want, names) { 366 return 367 } 368 369 if !sort.StringsAreSorted(names) { 370 t.errorf("%s: Glob(%#q): unsorted output:\n%s", dir, glob, strings.Join(names, "\n")) 371 sort.Strings(names) 372 } 373 374 var problems []string 375 for len(want) > 0 || len(names) > 0 { 376 switch { 377 case len(want) > 0 && len(names) > 0 && want[0] == names[0]: 378 want, names = want[1:], names[1:] 379 case len(want) > 0 && (len(names) == 0 || want[0] < names[0]): 380 problems = append(problems, "missing: "+want[0]) 381 want = want[1:] 382 default: 383 problems = append(problems, "extra: "+names[0]) 384 names = names[1:] 385 } 386 } 387 t.errorf("%s: Glob(%#q): wrong output:\n%s", dir, glob, strings.Join(problems, "\n")) 388 } 389 390 // checkStat checks that a direct stat of path matches entry, 391 // which was found in the parent's directory listing. 392 func (t *fsTester) checkStat(path string, entry fs.DirEntry) { 393 file, err := t.fsys.Open(path) 394 if err != nil { 395 t.errorf("%s: Open: %v", path, err) 396 return 397 } 398 info, err := file.Stat() 399 file.Close() 400 if err != nil { 401 t.errorf("%s: Stat: %v", path, err) 402 return 403 } 404 fentry := formatEntry(entry) 405 fientry := formatInfoEntry(info) 406 // Note: mismatch here is OK for symlink, because Open dereferences symlink. 407 if fentry != fientry && entry.Type()&fs.ModeSymlink == 0 { 408 t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, fientry) 409 } 410 411 einfo, err := entry.Info() 412 if err != nil { 413 t.errorf("%s: entry.Info: %v", path, err) 414 return 415 } 416 finfo := formatInfo(info) 417 if entry.Type()&fs.ModeSymlink != 0 { 418 // For symlink, just check that entry.Info matches entry on common fields. 419 // Open deferences symlink, so info itself may differ. 420 feentry := formatInfoEntry(einfo) 421 if fentry != feentry { 422 t.errorf("%s: mismatch\n\tentry = %s\n\tentry.Info() = %s\n", path, fentry, feentry) 423 } 424 } else { 425 feinfo := formatInfo(einfo) 426 if feinfo != finfo { 427 t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, feinfo, finfo) 428 } 429 } 430 431 // Stat should be the same as Open+Stat, even for symlinks. 432 info2, err := fs.Stat(t.fsys, path) 433 if err != nil { 434 t.errorf("%s: fs.Stat: %v", path, err) 435 return 436 } 437 finfo2 := formatInfo(info2) 438 if finfo2 != finfo { 439 t.errorf("%s: fs.Stat(...) = %s\n\twant %s", path, finfo2, finfo) 440 } 441 442 if fsys, ok := t.fsys.(fs.StatFS); ok { 443 info2, err := fsys.Stat(path) 444 if err != nil { 445 t.errorf("%s: fsys.Stat: %v", path, err) 446 return 447 } 448 finfo2 := formatInfo(info2) 449 if finfo2 != finfo { 450 t.errorf("%s: fsys.Stat(...) = %s\n\twant %s", path, finfo2, finfo) 451 } 452 } 453 } 454 455 // checkDirList checks that two directory lists contain the same files and file info. 456 // The order of the lists need not match. 457 func (t *fsTester) checkDirList(dir, desc string, list1, list2 []fs.DirEntry) { 458 old := make(map[string]fs.DirEntry) 459 checkMode := func(entry fs.DirEntry) { 460 if entry.IsDir() != (entry.Type()&fs.ModeDir != 0) { 461 if entry.IsDir() { 462 t.errorf("%s: ReadDir returned %s with IsDir() = true, Type() & ModeDir = 0", dir, entry.Name()) 463 } else { 464 t.errorf("%s: ReadDir returned %s with IsDir() = false, Type() & ModeDir = ModeDir", dir, entry.Name()) 465 } 466 } 467 } 468 469 for _, entry1 := range list1 { 470 old[entry1.Name()] = entry1 471 checkMode(entry1) 472 } 473 474 var diffs []string 475 for _, entry2 := range list2 { 476 entry1 := old[entry2.Name()] 477 if entry1 == nil { 478 checkMode(entry2) 479 diffs = append(diffs, "+ "+formatEntry(entry2)) 480 continue 481 } 482 if formatEntry(entry1) != formatEntry(entry2) { 483 diffs = append(diffs, "- "+formatEntry(entry1), "+ "+formatEntry(entry2)) 484 } 485 delete(old, entry2.Name()) 486 } 487 for _, entry1 := range old { 488 diffs = append(diffs, "- "+formatEntry(entry1)) 489 } 490 491 if len(diffs) == 0 { 492 return 493 } 494 495 sort.Slice(diffs, func(i, j int) bool { 496 fi := strings.Fields(diffs[i]) 497 fj := strings.Fields(diffs[j]) 498 // sort by name (i < j) and then +/- (j < i, because + < -) 499 return fi[1]+" "+fj[0] < fj[1]+" "+fi[0] 500 }) 501 502 t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t")) 503 } 504 505 // checkFile checks that basic file reading works correctly. 506 func (t *fsTester) checkFile(file string) { 507 t.files = append(t.files, file) 508 509 // Read entire file. 510 f, err := t.fsys.Open(file) 511 if err != nil { 512 t.errorf("%s: Open: %v", file, err) 513 return 514 } 515 516 data, err := io.ReadAll(f) 517 if err != nil { 518 f.Close() 519 t.errorf("%s: Open+ReadAll: %v", file, err) 520 return 521 } 522 523 if err := f.Close(); err != nil { 524 t.errorf("%s: Close: %v", file, err) 525 } 526 527 // Check that closing twice doesn't crash. 528 // The return value doesn't matter. 529 f.Close() 530 531 // Check that ReadFile works if present. 532 if fsys, ok := t.fsys.(fs.ReadFileFS); ok { 533 data2, err := fsys.ReadFile(file) 534 if err != nil { 535 t.errorf("%s: fsys.ReadFile: %v", file, err) 536 return 537 } 538 t.checkFileRead(file, "ReadAll vs fsys.ReadFile", data, data2) 539 540 // Modify the data and check it again. Modifying the 541 // returned byte slice should not affect the next call. 542 for i := range data2 { 543 data2[i]++ 544 } 545 data2, err = fsys.ReadFile(file) 546 if err != nil { 547 t.errorf("%s: second call to fsys.ReadFile: %v", file, err) 548 return 549 } 550 t.checkFileRead(file, "Readall vs second fsys.ReadFile", data, data2) 551 552 t.checkBadPath(file, "ReadFile", 553 func(name string) error { _, err := fsys.ReadFile(name); return err }) 554 } 555 556 // Check that fs.ReadFile works with t.fsys. 557 data2, err := fs.ReadFile(t.fsys, file) 558 if err != nil { 559 t.errorf("%s: fs.ReadFile: %v", file, err) 560 return 561 } 562 t.checkFileRead(file, "ReadAll vs fs.ReadFile", data, data2) 563 564 // Use iotest.TestReader to check small reads, Seek, ReadAt. 565 f, err = t.fsys.Open(file) 566 if err != nil { 567 t.errorf("%s: second Open: %v", file, err) 568 return 569 } 570 defer f.Close() 571 if err := iotest.TestReader(f, data); err != nil { 572 t.errorf("%s: failed TestReader:\n\t%s", file, strings.ReplaceAll(err.Error(), "\n", "\n\t")) 573 } 574 } 575 576 func (t *fsTester) checkFileRead(file, desc string, data1, data2 []byte) { 577 if string(data1) != string(data2) { 578 t.errorf("%s: %s: different data returned\n\t%q\n\t%q", file, desc, data1, data2) 579 return 580 } 581 } 582 583 // checkBadPath checks that various invalid forms of file's name cannot be opened using t.fsys.Open. 584 func (t *fsTester) checkOpen(file string) { 585 t.checkBadPath(file, "Open", func(file string) error { 586 f, err := t.fsys.Open(file) 587 if err == nil { 588 f.Close() 589 } 590 return err 591 }) 592 } 593 594 // checkBadPath checks that various invalid forms of file's name cannot be opened using open. 595 func (t *fsTester) checkBadPath(file string, desc string, open func(string) error) { 596 bad := []string{ 597 "/" + file, 598 file + "/.", 599 } 600 if file == "." { 601 bad = append(bad, "/") 602 } 603 if i := strings.Index(file, "/"); i >= 0 { 604 bad = append(bad, 605 file[:i]+"//"+file[i+1:], 606 file[:i]+"/./"+file[i+1:], 607 file[:i]+`\`+file[i+1:], 608 file[:i]+"/../"+file, 609 ) 610 } 611 if i := strings.LastIndex(file, "/"); i >= 0 { 612 bad = append(bad, 613 file[:i]+"//"+file[i+1:], 614 file[:i]+"/./"+file[i+1:], 615 file[:i]+`\`+file[i+1:], 616 file+"/../"+file[i+1:], 617 ) 618 } 619 620 for _, b := range bad { 621 if err := open(b); err == nil { 622 t.errorf("%s: %s(%s) succeeded, want error", file, desc, b) 623 } 624 } 625 }