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