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 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): %w", dir, err)
55 }
56 break
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 list := slices.Sorted(maps.Keys(found))
76 if len(list) > 15 {
77 list = append(list[:10], "...")
78 }
79 t.errorf("expected empty file system but found files:\n%s", strings.Join(list, "\n"))
80 }
81 for _, name := range expected {
82 if !found[name] {
83 t.errorf("expected but not found: %s", name)
84 }
85 }
86 if len(t.errors) == 0 {
87 return nil
88 }
89 return fmt.Errorf("TestFS found errors:\n%w", errors.Join(t.errors...))
90 }
91
92
93 type fsTester struct {
94 fsys fs.FS
95 errors []error
96 dirs []string
97 files []string
98 }
99
100
101 func (t *fsTester) errorf(format string, args ...any) {
102 t.errors = append(t.errors, fmt.Errorf(format, args...))
103 }
104
105 func (t *fsTester) openDir(dir string) fs.ReadDirFile {
106 f, err := t.fsys.Open(dir)
107 if err != nil {
108 t.errorf("%s: Open: %w", dir, err)
109 return nil
110 }
111 d, ok := f.(fs.ReadDirFile)
112 if !ok {
113 f.Close()
114 t.errorf("%s: Open returned File type %T, not a fs.ReadDirFile", dir, f)
115 return nil
116 }
117 return d
118 }
119
120
121
122 func (t *fsTester) checkDir(dir string) {
123
124 t.dirs = append(t.dirs, dir)
125 d := t.openDir(dir)
126 if d == nil {
127 return
128 }
129 list, err := d.ReadDir(-1)
130 if err != nil {
131 d.Close()
132 t.errorf("%s: ReadDir(-1): %w", dir, err)
133 return
134 }
135
136
137 var prefix string
138 if dir == "." {
139 prefix = ""
140 } else {
141 prefix = dir + "/"
142 }
143 for _, info := range list {
144 name := info.Name()
145 switch {
146 case name == ".", name == "..", name == "":
147 t.errorf("%s: ReadDir: child has invalid name: %#q", dir, name)
148 continue
149 case strings.Contains(name, "/"):
150 t.errorf("%s: ReadDir: child name contains slash: %#q", dir, name)
151 continue
152 case strings.Contains(name, `\`):
153 t.errorf("%s: ReadDir: child name contains backslash: %#q", dir, name)
154 continue
155 }
156 path := prefix + name
157 t.checkStat(path, info)
158 t.checkOpen(path)
159 if info.IsDir() {
160 t.checkDir(path)
161 } else {
162 t.checkFile(path)
163 }
164 }
165
166
167 list2, err := d.ReadDir(-1)
168 if len(list2) > 0 || err != nil {
169 d.Close()
170 t.errorf("%s: ReadDir(-1) at EOF = %d entries, %w, wanted 0 entries, nil", dir, len(list2), err)
171 return
172 }
173
174
175 list2, err = d.ReadDir(1)
176 if len(list2) > 0 || err != io.EOF {
177 d.Close()
178 t.errorf("%s: ReadDir(1) at EOF = %d entries, %w, wanted 0 entries, EOF", dir, len(list2), err)
179 return
180 }
181
182
183 if err := d.Close(); err != nil {
184 t.errorf("%s: Close: %w", dir, err)
185 }
186
187
188
189 d.Close()
190
191
192 if d = t.openDir(dir); d == nil {
193 return
194 }
195 defer d.Close()
196 list2, err = d.ReadDir(-1)
197 if err != nil {
198 t.errorf("%s: second Open+ReadDir(-1): %w", dir, err)
199 return
200 }
201 t.checkDirList(dir, "first Open+ReadDir(-1) vs second Open+ReadDir(-1)", list, list2)
202
203
204 if d = t.openDir(dir); d == nil {
205 return
206 }
207 defer d.Close()
208 list2 = nil
209 for {
210 n := 1
211 if len(list2) > 0 {
212 n = 2
213 }
214 frag, err := d.ReadDir(n)
215 if len(frag) > n {
216 t.errorf("%s: third Open: ReadDir(%d) after %d: %d entries (too many)", dir, n, len(list2), len(frag))
217 return
218 }
219 list2 = append(list2, frag...)
220 if err == io.EOF {
221 break
222 }
223 if err != nil {
224 t.errorf("%s: third Open: ReadDir(%d) after %d: %w", dir, n, len(list2), err)
225 return
226 }
227 if n == 0 {
228 t.errorf("%s: third Open: ReadDir(%d) after %d: 0 entries but nil error", dir, n, len(list2))
229 return
230 }
231 }
232 t.checkDirList(dir, "first Open+ReadDir(-1) vs third Open+ReadDir(1,2) loop", list, list2)
233
234
235 if fsys, ok := t.fsys.(fs.ReadDirFS); ok {
236 list2, err := fsys.ReadDir(dir)
237 if err != nil {
238 t.errorf("%s: fsys.ReadDir: %w", dir, err)
239 return
240 }
241 t.checkDirList(dir, "first Open+ReadDir(-1) vs fsys.ReadDir", list, list2)
242
243 for i := 0; i+1 < len(list2); i++ {
244 if list2[i].Name() >= list2[i+1].Name() {
245 t.errorf("%s: fsys.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name())
246 }
247 }
248 }
249
250
251 list2, err = fs.ReadDir(t.fsys, dir)
252 if err != nil {
253 t.errorf("%s: fs.ReadDir: %w", dir, err)
254 return
255 }
256 t.checkDirList(dir, "first Open+ReadDir(-1) vs fs.ReadDir", list, list2)
257
258 for i := 0; i+1 < len(list2); i++ {
259 if list2[i].Name() >= list2[i+1].Name() {
260 t.errorf("%s: fs.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name())
261 }
262 }
263
264 t.checkGlob(dir, list2)
265 }
266
267
268 func formatEntry(entry fs.DirEntry) string {
269 return fmt.Sprintf("%s IsDir=%v Type=%v", entry.Name(), entry.IsDir(), entry.Type())
270 }
271
272
273 func formatInfoEntry(info fs.FileInfo) string {
274 return fmt.Sprintf("%s IsDir=%v Type=%v", info.Name(), info.IsDir(), info.Mode().Type())
275 }
276
277
278 func formatInfo(info fs.FileInfo) string {
279 return fmt.Sprintf("%s IsDir=%v Mode=%v Size=%d ModTime=%v", info.Name(), info.IsDir(), info.Mode(), info.Size(), info.ModTime())
280 }
281
282
283 func (t *fsTester) checkGlob(dir string, list []fs.DirEntry) {
284 if _, ok := t.fsys.(fs.GlobFS); !ok {
285 return
286 }
287
288
289 var glob string
290 if dir != "." {
291 elem := strings.Split(dir, "/")
292 for i, e := range elem {
293 var pattern []rune
294 for j, r := range e {
295 if r == '*' || r == '?' || r == '\\' || r == '[' || r == '-' {
296 pattern = append(pattern, '\\', r)
297 continue
298 }
299 switch (i + j) % 5 {
300 case 0:
301 pattern = append(pattern, r)
302 case 1:
303 pattern = append(pattern, '[', r, ']')
304 case 2:
305 pattern = append(pattern, '[', r, '-', r, ']')
306 case 3:
307 pattern = append(pattern, '[', '\\', r, ']')
308 case 4:
309 pattern = append(pattern, '[', '\\', r, '-', '\\', r, ']')
310 }
311 }
312 elem[i] = string(pattern)
313 }
314 glob = strings.Join(elem, "/") + "/"
315 }
316
317
318
319 if _, err := t.fsys.(fs.GlobFS).Glob(glob + "nonexist/[]"); err == nil {
320 t.errorf("%s: Glob(%#q): bad pattern not detected", dir, glob+"nonexist/[]")
321 }
322
323
324 c := rune('a')
325 for ; c <= 'z'; c++ {
326 have, haveNot := false, false
327 for _, d := range list {
328 if strings.ContainsRune(d.Name(), c) {
329 have = true
330 } else {
331 haveNot = true
332 }
333 }
334 if have && haveNot {
335 break
336 }
337 }
338 if c > 'z' {
339 c = 'a'
340 }
341 glob += "*" + string(c) + "*"
342
343 var want []string
344 for _, d := range list {
345 if strings.ContainsRune(d.Name(), c) {
346 want = append(want, path.Join(dir, d.Name()))
347 }
348 }
349
350 names, err := t.fsys.(fs.GlobFS).Glob(glob)
351 if err != nil {
352 t.errorf("%s: Glob(%#q): %w", dir, glob, err)
353 return
354 }
355 if slices.Equal(want, names) {
356 return
357 }
358
359 if !slices.IsSorted(names) {
360 t.errorf("%s: Glob(%#q): unsorted output:\n%s", dir, glob, strings.Join(names, "\n"))
361 slices.Sort(names)
362 }
363
364 var problems []string
365 for len(want) > 0 || len(names) > 0 {
366 switch {
367 case len(want) > 0 && len(names) > 0 && want[0] == names[0]:
368 want, names = want[1:], names[1:]
369 case len(want) > 0 && (len(names) == 0 || want[0] < names[0]):
370 problems = append(problems, "missing: "+want[0])
371 want = want[1:]
372 default:
373 problems = append(problems, "extra: "+names[0])
374 names = names[1:]
375 }
376 }
377 t.errorf("%s: Glob(%#q): wrong output:\n%s", dir, glob, strings.Join(problems, "\n"))
378 }
379
380
381
382 func (t *fsTester) checkStat(path string, entry fs.DirEntry) {
383 file, err := t.fsys.Open(path)
384 if err != nil {
385 t.errorf("%s: Open: %w", path, err)
386 return
387 }
388 info, err := file.Stat()
389 file.Close()
390 if err != nil {
391 t.errorf("%s: Stat: %w", path, err)
392 return
393 }
394 fentry := formatEntry(entry)
395 fientry := formatInfoEntry(info)
396
397 if fentry != fientry && entry.Type()&fs.ModeSymlink == 0 {
398 t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, fientry)
399 }
400
401 einfo, err := entry.Info()
402 if err != nil {
403 t.errorf("%s: entry.Info: %w", path, err)
404 return
405 }
406 finfo := formatInfo(info)
407 if entry.Type()&fs.ModeSymlink != 0 {
408
409
410 feentry := formatInfoEntry(einfo)
411 if fentry != feentry {
412 t.errorf("%s: mismatch\n\tentry = %s\n\tentry.Info() = %s\n", path, fentry, feentry)
413 }
414 } else {
415 feinfo := formatInfo(einfo)
416 if feinfo != finfo {
417 t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, feinfo, finfo)
418 }
419 }
420
421
422 info2, err := fs.Stat(t.fsys, path)
423 if err != nil {
424 t.errorf("%s: fs.Stat: %w", 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: %w", 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
446
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 slices.SortFunc(diffs, func(a, b string) int {
486 fa := strings.Fields(a)
487 fb := strings.Fields(b)
488
489 return strings.Compare(fa[1]+" "+fb[0], fb[1]+" "+fa[0])
490 })
491
492 t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t"))
493 }
494
495
496 func (t *fsTester) checkFile(file string) {
497 t.files = append(t.files, file)
498
499
500 f, err := t.fsys.Open(file)
501 if err != nil {
502 t.errorf("%s: Open: %w", file, err)
503 return
504 }
505
506 data, err := io.ReadAll(f)
507 if err != nil {
508 f.Close()
509 t.errorf("%s: Open+ReadAll: %w", file, err)
510 return
511 }
512
513 if err := f.Close(); err != nil {
514 t.errorf("%s: Close: %w", file, err)
515 }
516
517
518
519 f.Close()
520
521
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: %w", file, err)
526 return
527 }
528 t.checkFileRead(file, "ReadAll vs fsys.ReadFile", data, data2)
529
530
531
532 for i := range data2 {
533 data2[i]++
534 }
535 data2, err = fsys.ReadFile(file)
536 if err != nil {
537 t.errorf("%s: second call to fsys.ReadFile: %w", file, err)
538 return
539 }
540 t.checkFileRead(file, "Readall vs second fsys.ReadFile", data, data2)
541
542 t.checkBadPath(file, "ReadFile",
543 func(name string) error { _, err := fsys.ReadFile(name); return err })
544 }
545
546
547 data2, err := fs.ReadFile(t.fsys, file)
548 if err != nil {
549 t.errorf("%s: fs.ReadFile: %w", file, err)
550 return
551 }
552 t.checkFileRead(file, "ReadAll vs fs.ReadFile", data, data2)
553
554
555 f, err = t.fsys.Open(file)
556 if err != nil {
557 t.errorf("%s: second Open: %w", file, err)
558 return
559 }
560 defer f.Close()
561 if err := iotest.TestReader(f, data); err != nil {
562 t.errorf("%s: failed TestReader:\n\t%s", file, strings.ReplaceAll(err.Error(), "\n", "\n\t"))
563 }
564 }
565
566 func (t *fsTester) checkFileRead(file, desc string, data1, data2 []byte) {
567 if string(data1) != string(data2) {
568 t.errorf("%s: %s: different data returned\n\t%q\n\t%q", file, desc, data1, data2)
569 return
570 }
571 }
572
573
574 func (t *fsTester) checkOpen(file string) {
575 t.checkBadPath(file, "Open", func(file string) error {
576 f, err := t.fsys.Open(file)
577 if err == nil {
578 f.Close()
579 }
580 return err
581 })
582 }
583
584
585 func (t *fsTester) checkBadPath(file string, desc string, open func(string) error) {
586 bad := []string{
587 "/" + file,
588 file + "/.",
589 }
590 if file == "." {
591 bad = append(bad, "/")
592 }
593 if i := strings.Index(file, "/"); i >= 0 {
594 bad = append(bad,
595 file[:i]+"//"+file[i+1:],
596 file[:i]+"/./"+file[i+1:],
597 file[:i]+`\`+file[i+1:],
598 file[:i]+"/../"+file,
599 )
600 }
601 if i := strings.LastIndex(file, "/"); i >= 0 {
602 bad = append(bad,
603 file[:i]+"//"+file[i+1:],
604 file[:i]+"/./"+file[i+1:],
605 file[:i]+`\`+file[i+1:],
606 file+"/../"+file[i+1:],
607 )
608 }
609
610 for _, b := range bad {
611 if err := open(b); err == nil {
612 t.errorf("%s: %s(%s) succeeded, want error", file, desc, b)
613 }
614 }
615 }
616
View as plain text