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