github.com/geraldss/go/src@v0.0.0-20210511222824-ac7d0ebfc235/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 finfo := formatInfoEntry(info) 407 if fentry != finfo { 408 t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, finfo) 409 } 410 411 einfo, err := entry.Info() 412 if err != nil { 413 t.errorf("%s: entry.Info: %v", path, err) 414 return 415 } 416 fentry = formatInfo(einfo) 417 finfo = formatInfo(info) 418 if fentry != finfo { 419 t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, fentry, finfo) 420 } 421 422 info2, err := fs.Stat(t.fsys, path) 423 if err != nil { 424 t.errorf("%s: fs.Stat: %v", path, err) 425 return 426 } 427 finfo2 := formatInfo(info2) 428 if finfo2 != finfo { 429 t.errorf("%s: fs.Stat(...) = %s\n\twant %s", path, finfo2, finfo) 430 } 431 432 if fsys, ok := t.fsys.(fs.StatFS); ok { 433 info2, err := fsys.Stat(path) 434 if err != nil { 435 t.errorf("%s: fsys.Stat: %v", path, err) 436 return 437 } 438 finfo2 := formatInfo(info2) 439 if finfo2 != finfo { 440 t.errorf("%s: fsys.Stat(...) = %s\n\twant %s", path, finfo2, finfo) 441 } 442 } 443 } 444 445 // checkDirList checks that two directory lists contain the same files and file info. 446 // The order of the lists need not match. 447 func (t *fsTester) checkDirList(dir, desc string, list1, list2 []fs.DirEntry) { 448 old := make(map[string]fs.DirEntry) 449 checkMode := func(entry fs.DirEntry) { 450 if entry.IsDir() != (entry.Type()&fs.ModeDir != 0) { 451 if entry.IsDir() { 452 t.errorf("%s: ReadDir returned %s with IsDir() = true, Type() & ModeDir = 0", dir, entry.Name()) 453 } else { 454 t.errorf("%s: ReadDir returned %s with IsDir() = false, Type() & ModeDir = ModeDir", dir, entry.Name()) 455 } 456 } 457 } 458 459 for _, entry1 := range list1 { 460 old[entry1.Name()] = entry1 461 checkMode(entry1) 462 } 463 464 var diffs []string 465 for _, entry2 := range list2 { 466 entry1 := old[entry2.Name()] 467 if entry1 == nil { 468 checkMode(entry2) 469 diffs = append(diffs, "+ "+formatEntry(entry2)) 470 continue 471 } 472 if formatEntry(entry1) != formatEntry(entry2) { 473 diffs = append(diffs, "- "+formatEntry(entry1), "+ "+formatEntry(entry2)) 474 } 475 delete(old, entry2.Name()) 476 } 477 for _, entry1 := range old { 478 diffs = append(diffs, "- "+formatEntry(entry1)) 479 } 480 481 if len(diffs) == 0 { 482 return 483 } 484 485 sort.Slice(diffs, func(i, j int) bool { 486 fi := strings.Fields(diffs[i]) 487 fj := strings.Fields(diffs[j]) 488 // sort by name (i < j) and then +/- (j < i, because + < -) 489 return fi[1]+" "+fj[0] < fj[1]+" "+fi[0] 490 }) 491 492 t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t")) 493 } 494 495 // checkFile checks that basic file reading works correctly. 496 func (t *fsTester) checkFile(file string) { 497 t.files = append(t.files, file) 498 499 // Read entire file. 500 f, err := t.fsys.Open(file) 501 if err != nil { 502 t.errorf("%s: Open: %v", file, err) 503 return 504 } 505 506 data, err := ioutil.ReadAll(f) 507 if err != nil { 508 f.Close() 509 t.errorf("%s: Open+ReadAll: %v", file, err) 510 return 511 } 512 513 if err := f.Close(); err != nil { 514 t.errorf("%s: Close: %v", file, err) 515 } 516 517 // Check that closing twice doesn't crash. 518 // The return value doesn't matter. 519 f.Close() 520 521 // Check that ReadFile works if present. 522 if fsys, ok := t.fsys.(fs.ReadFileFS); ok { 523 data2, err := fsys.ReadFile(file) 524 if err != nil { 525 t.errorf("%s: fsys.ReadFile: %v", file, err) 526 return 527 } 528 t.checkFileRead(file, "ReadAll vs fsys.ReadFile", data, data2) 529 530 t.checkBadPath(file, "ReadFile", 531 func(name string) error { _, err := fsys.ReadFile(name); return err }) 532 } 533 534 // Check that fs.ReadFile works with t.fsys. 535 data2, err := fs.ReadFile(t.fsys, file) 536 if err != nil { 537 t.errorf("%s: fs.ReadFile: %v", file, err) 538 return 539 } 540 t.checkFileRead(file, "ReadAll vs fs.ReadFile", data, data2) 541 542 // Use iotest.TestReader to check small reads, Seek, ReadAt. 543 f, err = t.fsys.Open(file) 544 if err != nil { 545 t.errorf("%s: second Open: %v", file, err) 546 return 547 } 548 defer f.Close() 549 if err := iotest.TestReader(f, data); err != nil { 550 t.errorf("%s: failed TestReader:\n\t%s", file, strings.ReplaceAll(err.Error(), "\n", "\n\t")) 551 } 552 } 553 554 func (t *fsTester) checkFileRead(file, desc string, data1, data2 []byte) { 555 if string(data1) != string(data2) { 556 t.errorf("%s: %s: different data returned\n\t%q\n\t%q", file, desc, data1, data2) 557 return 558 } 559 } 560 561 // checkBadPath checks that various invalid forms of file's name cannot be opened using t.fsys.Open. 562 func (t *fsTester) checkOpen(file string) { 563 t.checkBadPath(file, "Open", func(file string) error { 564 f, err := t.fsys.Open(file) 565 if err == nil { 566 f.Close() 567 } 568 return err 569 }) 570 } 571 572 // checkBadPath checks that various invalid forms of file's name cannot be opened using open. 573 func (t *fsTester) checkBadPath(file string, desc string, open func(string) error) { 574 bad := []string{ 575 "/" + file, 576 file + "/.", 577 } 578 if file == "." { 579 bad = append(bad, "/") 580 } 581 if i := strings.Index(file, "/"); i >= 0 { 582 bad = append(bad, 583 file[:i]+"//"+file[i+1:], 584 file[:i]+"/./"+file[i+1:], 585 file[:i]+`\`+file[i+1:], 586 file[:i]+"/../"+file, 587 ) 588 } 589 if i := strings.LastIndex(file, "/"); i >= 0 { 590 bad = append(bad, 591 file[:i]+"//"+file[i+1:], 592 file[:i]+"/./"+file[i+1:], 593 file[:i]+`\`+file[i+1:], 594 file+"/../"+file[i+1:], 595 ) 596 } 597 598 for _, b := range bad { 599 if err := open(b); err == nil { 600 t.errorf("%s: %s(%s) succeeded, want error", file, desc, b) 601 } 602 } 603 }