1
2
3
4
5
6 package fstest
7
8 import (
9 "errors"
10 "fmt"
11 "io"
12 "io/fs"
13 "maps"
14 "path"
15 "slices"
16 "strings"
17 "testing/iotest"
18 )
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 func TestFS(fsys fs.FS, expected ...string) error {
40 if err := testFS(fsys, expected...); err != nil {
41 return err
42 }
43 for _, name := range expected {
44 if i := strings.Index(name, "/"); i >= 0 {
45 dir, dirSlash := name[:i], name[:i+1]
46 var subExpected []string
47 for _, name := range expected {
48 if strings.HasPrefix(name, dirSlash) {
49 subExpected = append(subExpected, name[len(dirSlash):])
50 }
51 }
52 sub, err := fs.Sub(fsys, dir)
53 if err != nil {
54 return err
55 }
56 if err := testFS(sub, subExpected...); err != nil {
57 return fmt.Errorf("testing fs.Sub(fsys, %s): %w", dir, err)
58 }
59 break
60 }
61 }
62 return nil
63 }
64
65 func testFS(fsys fs.FS, expected ...string) error {
66 t := fsTester{fsys: fsys}
67 t.checkDir(".")
68 t.checkOpen(".")
69 found := make(map[string]bool)
70 for _, dir := range t.dirs {
71 found[dir] = true
72 }
73 for _, file := range t.files {
74 found[file] = true
75 }
76 delete(found, ".")
77 if len(expected) == 0 && len(found) > 0 {
78 list := slices.Sorted(maps.Keys(found))
79 if len(list) > 15 {
80 list = append(list[:10], "...")
81 }
82 t.errorf("expected empty file system but found files:\n%s", strings.Join(list, "\n"))
83 }
84 for _, name := range expected {
85 if !found[name] {
86 t.errorf("expected but not found: %s", name)
87 }
88 }
89 if len(t.errors) == 0 {
90 return nil
91 }
92 return fmt.Errorf("TestFS found errors:\n%w", errors.Join(t.errors...))
93 }
94
95
96 type fsTester struct {
97 fsys fs.FS
98 errors []error
99 dirs []string
100 files []string
101 }
102
103
104 func (t *fsTester) errorf(format string, args ...any) {
105 t.errors = append(t.errors, fmt.Errorf(format, args...))
106 }
107
108 func (t *fsTester) openDir(dir string) fs.ReadDirFile {
109 f, err := t.fsys.Open(dir)
110 if err != nil {
111 t.errorf("%s: Open: %w", dir, err)
112 return nil
113 }
114 d, ok := f.(fs.ReadDirFile)
115 if !ok {
116 f.Close()
117 t.errorf("%s: Open returned File type %T, not a fs.ReadDirFile", dir, f)
118 return nil
119 }
120 return d
121 }
122
123
124
125 func (t *fsTester) checkDir(dir string) {
126
127 t.dirs = append(t.dirs, dir)
128 d := t.openDir(dir)
129 if d == nil {
130 return
131 }
132 list, err := d.ReadDir(-1)
133 if err != nil {
134 d.Close()
135 t.errorf("%s: ReadDir(-1): %w", dir, err)
136 return
137 }
138
139
140 var prefix string
141 if dir == "." {
142 prefix = ""
143 } else {
144 prefix = dir + "/"
145 }
146 for _, info := range list {
147 name := info.Name()
148 switch {
149 case name == ".", name == "..", name == "":
150 t.errorf("%s: ReadDir: child has invalid name: %#q", dir, name)
151 continue
152 case strings.Contains(name, "/"):
153 t.errorf("%s: ReadDir: child name contains slash: %#q", dir, name)
154 continue
155 case strings.Contains(name, `\`):
156 t.errorf("%s: ReadDir: child name contains backslash: %#q", dir, name)
157 continue
158 }
159 path := prefix + name
160 t.checkStat(path, info)
161 t.checkOpen(path)
162 switch info.Type() {
163 case fs.ModeDir:
164 t.checkDir(path)
165 case fs.ModeSymlink:
166
167
168 t.files = append(t.files, path)
169 default:
170 t.checkFile(path)
171 }
172 }
173
174
175 list2, err := d.ReadDir(-1)
176 if len(list2) > 0 || err != nil {
177 d.Close()
178 t.errorf("%s: ReadDir(-1) at EOF = %d entries, %w, wanted 0 entries, nil", dir, len(list2), err)
179 return
180 }
181
182
183 list2, err = d.ReadDir(1)
184 if len(list2) > 0 || err != io.EOF {
185 d.Close()
186 t.errorf("%s: ReadDir(1) at EOF = %d entries, %w, wanted 0 entries, EOF", dir, len(list2), err)
187 return
188 }
189
190
191 if err := d.Close(); err != nil {
192 t.errorf("%s: Close: %w", dir, err)
193 }
194
195
196
197 d.Close()
198
199
200 if d = t.openDir(dir); d == nil {
201 return
202 }
203 defer d.Close()
204 list2, err = d.ReadDir(-1)
205 if err != nil {
206 t.errorf("%s: second Open+ReadDir(-1): %w", dir, err)
207 return
208 }
209 t.checkDirList(dir, "first Open+ReadDir(-1) vs second Open+ReadDir(-1)", list, list2)
210
211
212 if d = t.openDir(dir); d == nil {
213 return
214 }
215 defer d.Close()
216 list2 = nil
217 for {
218 n := 1
219 if len(list2) > 0 {
220 n = 2
221 }
222 frag, err := d.ReadDir(n)
223 if len(frag) > n {
224 t.errorf("%s: third Open: ReadDir(%d) after %d: %d entries (too many)", dir, n, len(list2), len(frag))
225 return
226 }
227 list2 = append(list2, frag...)
228 if err == io.EOF {
229 break
230 }
231 if err != nil {
232 t.errorf("%s: third Open: ReadDir(%d) after %d: %w", dir, n, len(list2), err)
233 return
234 }
235 if n == 0 {
236 t.errorf("%s: third Open: ReadDir(%d) after %d: 0 entries but nil error", dir, n, len(list2))
237 return
238 }
239 }
240 t.checkDirList(dir, "first Open+ReadDir(-1) vs third Open+ReadDir(1,2) loop", list, list2)
241
242
243 if fsys, ok := t.fsys.(fs.ReadDirFS); ok {
244 list2, err := fsys.ReadDir(dir)
245 if err != nil {
246 t.errorf("%s: fsys.ReadDir: %w", dir, err)
247 return
248 }
249 t.checkDirList(dir, "first Open+ReadDir(-1) vs fsys.ReadDir", list, list2)
250
251 for i := 0; i+1 < len(list2); i++ {
252 if list2[i].Name() >= list2[i+1].Name() {
253 t.errorf("%s: fsys.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name())
254 }
255 }
256 }
257
258
259 list2, err = fs.ReadDir(t.fsys, dir)
260 if err != nil {
261 t.errorf("%s: fs.ReadDir: %w", dir, err)
262 return
263 }
264 t.checkDirList(dir, "first Open+ReadDir(-1) vs fs.ReadDir", list, list2)
265
266 for i := 0; i+1 < len(list2); i++ {
267 if list2[i].Name() >= list2[i+1].Name() {
268 t.errorf("%s: fs.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name())
269 }
270 }
271
272 t.checkGlob(dir, list2)
273 }
274
275
276 func formatEntry(entry fs.DirEntry) string {
277 return fmt.Sprintf("%s IsDir=%v Type=%v", entry.Name(), entry.IsDir(), entry.Type())
278 }
279
280
281 func formatInfoEntry(info fs.FileInfo) string {
282 return fmt.Sprintf("%s IsDir=%v Type=%v", info.Name(), info.IsDir(), info.Mode().Type())
283 }
284
285
286 func formatInfo(info fs.FileInfo) string {
287 return fmt.Sprintf("%s IsDir=%v Mode=%v Size=%d ModTime=%v", info.Name(), info.IsDir(), info.Mode(), info.Size(), info.ModTime())
288 }
289
290
291 func (t *fsTester) checkGlob(dir string, list []fs.DirEntry) {
292 if _, ok := t.fsys.(fs.GlobFS); !ok {
293 return
294 }
295
296
297 var glob string
298 if dir != "." {
299 elem := strings.Split(dir, "/")
300 for i, e := range elem {
301 var pattern []rune
302 for j, r := range e {
303 if r == '*' || r == '?' || r == '\\' || r == '[' || r == '-' {
304 pattern = append(pattern, '\\', r)
305 continue
306 }
307 switch (i + j) % 5 {
308 case 0:
309 pattern = append(pattern, r)
310 case 1:
311 pattern = append(pattern, '[', r, ']')
312 case 2:
313 pattern = append(pattern, '[', r, '-', r, ']')
314 case 3:
315 pattern = append(pattern, '[', '\\', r, ']')
316 case 4:
317 pattern = append(pattern, '[', '\\', r, '-', '\\', r, ']')
318 }
319 }
320 elem[i] = string(pattern)
321 }
322 glob = strings.Join(elem, "/") + "/"
323 }
324
325
326
327 if _, err := t.fsys.(fs.GlobFS).Glob(glob + "nonexist/[]"); err == nil {
328 t.errorf("%s: Glob(%#q): bad pattern not detected", dir, glob+"nonexist/[]")
329 }
330
331
332 c := rune('a')
333 for ; c <= 'z'; c++ {
334 have, haveNot := false, false
335 for _, d := range list {
336 if strings.ContainsRune(d.Name(), c) {
337 have = true
338 } else {
339 haveNot = true
340 }
341 }
342 if have && haveNot {
343 break
344 }
345 }
346 if c > 'z' {
347 c = 'a'
348 }
349 glob += "*" + string(c) + "*"
350
351 var want []string
352 for _, d := range list {
353 if strings.ContainsRune(d.Name(), c) {
354 want = append(want, path.Join(dir, d.Name()))
355 }
356 }
357
358 names, err := t.fsys.(fs.GlobFS).Glob(glob)
359 if err != nil {
360 t.errorf("%s: Glob(%#q): %w", dir, glob, err)
361 return
362 }
363 if slices.Equal(want, names) {
364 return
365 }
366
367 if !slices.IsSorted(names) {
368 t.errorf("%s: Glob(%#q): unsorted output:\n%s", dir, glob, strings.Join(names, "\n"))
369 slices.Sort(names)
370 }
371
372 var problems []string
373 for len(want) > 0 || len(names) > 0 {
374 switch {
375 case len(want) > 0 && len(names) > 0 && want[0] == names[0]:
376 want, names = want[1:], names[1:]
377 case len(want) > 0 && (len(names) == 0 || want[0] < names[0]):
378 problems = append(problems, "missing: "+want[0])
379 want = want[1:]
380 default:
381 problems = append(problems, "extra: "+names[0])
382 names = names[1:]
383 }
384 }
385 t.errorf("%s: Glob(%#q): wrong output:\n%s", dir, glob, strings.Join(problems, "\n"))
386 }
387
388
389
390 func (t *fsTester) checkStat(path string, entry fs.DirEntry) {
391 file, err := t.fsys.Open(path)
392 if err != nil {
393 t.errorf("%s: Open: %w", path, err)
394 return
395 }
396 info, err := file.Stat()
397 file.Close()
398 if err != nil {
399 t.errorf("%s: Stat: %w", path, err)
400 return
401 }
402 fentry := formatEntry(entry)
403 fientry := formatInfoEntry(info)
404
405 if fentry != fientry && entry.Type()&fs.ModeSymlink == 0 {
406 t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, fientry)
407 }
408
409 einfo, err := entry.Info()
410 if err != nil {
411 t.errorf("%s: entry.Info: %w", path, err)
412 return
413 }
414 finfo := formatInfo(info)
415 if entry.Type()&fs.ModeSymlink != 0 {
416
417
418 feentry := formatInfoEntry(einfo)
419 if fentry != feentry {
420 t.errorf("%s: mismatch\n\tentry = %s\n\tentry.Info() = %s\n", path, fentry, feentry)
421 }
422 } else {
423 feinfo := formatInfo(einfo)
424 if feinfo != finfo {
425 t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, feinfo, finfo)
426 }
427 }
428
429
430 info2, err := fs.Stat(t.fsys, path)
431 if err != nil {
432 t.errorf("%s: fs.Stat: %w", path, err)
433 return
434 }
435 finfo2 := formatInfo(info2)
436 if finfo2 != finfo {
437 t.errorf("%s: fs.Stat(...) = %s\n\twant %s", path, finfo2, finfo)
438 }
439
440 if fsys, ok := t.fsys.(fs.StatFS); ok {
441 info2, err := fsys.Stat(path)
442 if err != nil {
443 t.errorf("%s: fsys.Stat: %w", path, err)
444 return
445 }
446 finfo2 := formatInfo(info2)
447 if finfo2 != finfo {
448 t.errorf("%s: fsys.Stat(...) = %s\n\twant %s", path, finfo2, finfo)
449 }
450 }
451
452 if fsys, ok := t.fsys.(fs.ReadLinkFS); ok {
453 info2, err := fsys.Lstat(path)
454 if err != nil {
455 t.errorf("%s: fsys.Lstat: %v", path, err)
456 return
457 }
458 fientry2 := formatInfoEntry(info2)
459 if fentry != fientry2 {
460 t.errorf("%s: mismatch:\n\tentry = %s\n\tfsys.Lstat(...) = %s", path, fentry, fientry2)
461 }
462 feinfo := formatInfo(einfo)
463 finfo2 := formatInfo(info2)
464 if feinfo != finfo2 {
465 t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfsys.Lstat(...) = %s\n", path, feinfo, finfo2)
466 }
467 }
468 }
469
470
471
472 func (t *fsTester) checkDirList(dir, desc string, list1, list2 []fs.DirEntry) {
473 old := make(map[string]fs.DirEntry)
474 checkMode := func(entry fs.DirEntry) {
475 if entry.IsDir() != (entry.Type()&fs.ModeDir != 0) {
476 if entry.IsDir() {
477 t.errorf("%s: ReadDir returned %s with IsDir() = true, Type() & ModeDir = 0", dir, entry.Name())
478 } else {
479 t.errorf("%s: ReadDir returned %s with IsDir() = false, Type() & ModeDir = ModeDir", dir, entry.Name())
480 }
481 }
482 }
483
484 for _, entry1 := range list1 {
485 old[entry1.Name()] = entry1
486 checkMode(entry1)
487 }
488
489 var diffs []string
490 for _, entry2 := range list2 {
491 entry1 := old[entry2.Name()]
492 if entry1 == nil {
493 checkMode(entry2)
494 diffs = append(diffs, "+ "+formatEntry(entry2))
495 continue
496 }
497 if formatEntry(entry1) != formatEntry(entry2) {
498 diffs = append(diffs, "- "+formatEntry(entry1), "+ "+formatEntry(entry2))
499 }
500 delete(old, entry2.Name())
501 }
502 for _, entry1 := range old {
503 diffs = append(diffs, "- "+formatEntry(entry1))
504 }
505
506 if len(diffs) == 0 {
507 return
508 }
509
510 slices.SortFunc(diffs, func(a, b string) int {
511 fa := strings.Fields(a)
512 fb := strings.Fields(b)
513
514 return strings.Compare(fa[1]+" "+fb[0], fb[1]+" "+fa[0])
515 })
516
517 t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t"))
518 }
519
520
521 func (t *fsTester) checkFile(file string) {
522 t.files = append(t.files, file)
523
524
525 f, err := t.fsys.Open(file)
526 if err != nil {
527 t.errorf("%s: Open: %w", file, err)
528 return
529 }
530
531 data, err := io.ReadAll(f)
532 if err != nil {
533 f.Close()
534 t.errorf("%s: Open+ReadAll: %w", file, err)
535 return
536 }
537
538 if err := f.Close(); err != nil {
539 t.errorf("%s: Close: %w", file, err)
540 }
541
542
543
544 f.Close()
545
546
547 if fsys, ok := t.fsys.(fs.ReadFileFS); ok {
548 data2, err := fsys.ReadFile(file)
549 if err != nil {
550 t.errorf("%s: fsys.ReadFile: %w", file, err)
551 return
552 }
553 t.checkFileRead(file, "ReadAll vs fsys.ReadFile", data, data2)
554
555
556
557 for i := range data2 {
558 data2[i]++
559 }
560 data2, err = fsys.ReadFile(file)
561 if err != nil {
562 t.errorf("%s: second call to fsys.ReadFile: %w", file, err)
563 return
564 }
565 t.checkFileRead(file, "Readall vs second fsys.ReadFile", data, data2)
566
567 t.checkBadPath(file, "ReadFile",
568 func(name string) error { _, err := fsys.ReadFile(name); return err })
569 }
570
571
572 data2, err := fs.ReadFile(t.fsys, file)
573 if err != nil {
574 t.errorf("%s: fs.ReadFile: %w", file, err)
575 return
576 }
577 t.checkFileRead(file, "ReadAll vs fs.ReadFile", data, data2)
578
579
580 f, err = t.fsys.Open(file)
581 if err != nil {
582 t.errorf("%s: second Open: %w", file, err)
583 return
584 }
585 defer f.Close()
586 if err := iotest.TestReader(f, data); err != nil {
587 t.errorf("%s: failed TestReader:\n\t%s", file, strings.ReplaceAll(err.Error(), "\n", "\n\t"))
588 }
589 }
590
591 func (t *fsTester) checkFileRead(file, desc string, data1, data2 []byte) {
592 if string(data1) != string(data2) {
593 t.errorf("%s: %s: different data returned\n\t%q\n\t%q", file, desc, data1, data2)
594 return
595 }
596 }
597
598
599 func (t *fsTester) checkOpen(file string) {
600 t.checkBadPath(file, "Open", func(file string) error {
601 f, err := t.fsys.Open(file)
602 if err == nil {
603 f.Close()
604 }
605 return err
606 })
607 }
608
609
610 func (t *fsTester) checkBadPath(file string, desc string, open func(string) error) {
611 bad := []string{
612 "/" + file,
613 file + "/.",
614 }
615 if file == "." {
616 bad = append(bad, "/")
617 }
618 if i := strings.Index(file, "/"); i >= 0 {
619 bad = append(bad,
620 file[:i]+"//"+file[i+1:],
621 file[:i]+"/./"+file[i+1:],
622 file[:i]+`\`+file[i+1:],
623 file[:i]+"/../"+file,
624 )
625 }
626 if i := strings.LastIndex(file, "/"); i >= 0 {
627 bad = append(bad,
628 file[:i]+"//"+file[i+1:],
629 file[:i]+"/./"+file[i+1:],
630 file[:i]+`\`+file[i+1:],
631 file+"/../"+file[i+1:],
632 )
633 }
634
635 for _, b := range bad {
636 if err := open(b); err == nil {
637 t.errorf("%s: %s(%s) succeeded, want error", file, desc, b)
638 }
639 }
640 }
641
View as plain text