diff options
Diffstat (limited to 'src/testing/fstest')
-rw-r--r-- | src/testing/fstest/mapfs.go | 238 | ||||
-rw-r--r-- | src/testing/fstest/mapfs_test.go | 19 | ||||
-rw-r--r-- | src/testing/fstest/testfs.go | 614 | ||||
-rw-r--r-- | src/testing/fstest/testfs_test.go | 31 |
4 files changed, 902 insertions, 0 deletions
diff --git a/src/testing/fstest/mapfs.go b/src/testing/fstest/mapfs.go new file mode 100644 index 0000000..a5d4a23 --- /dev/null +++ b/src/testing/fstest/mapfs.go @@ -0,0 +1,238 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fstest + +import ( + "io" + "io/fs" + "path" + "sort" + "strings" + "time" +) + +// A MapFS is a simple in-memory file system for use in tests, +// represented as a map from path names (arguments to Open) +// to information about the files or directories they represent. +// +// The map need not include parent directories for files contained +// in the map; those will be synthesized if needed. +// But a directory can still be included by setting the MapFile.Mode's ModeDir bit; +// this may be necessary for detailed control over the directory's FileInfo +// or to create an empty directory. +// +// File system operations read directly from the map, +// so that the file system can be changed by editing the map as needed. +// An implication is that file system operations must not run concurrently +// with changes to the map, which would be a race. +// Another implication is that opening or reading a directory requires +// iterating over the entire map, so a MapFS should typically be used with not more +// than a few hundred entries or directory reads. +type MapFS map[string]*MapFile + +// A MapFile describes a single file in a MapFS. +type MapFile struct { + Data []byte // file content + Mode fs.FileMode // FileInfo.Mode + ModTime time.Time // FileInfo.ModTime + Sys interface{} // FileInfo.Sys +} + +var _ fs.FS = MapFS(nil) +var _ fs.File = (*openMapFile)(nil) + +// Open opens the named file. +func (fsys MapFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + file := fsys[name] + if file != nil && file.Mode&fs.ModeDir == 0 { + // Ordinary file + return &openMapFile{name, mapFileInfo{path.Base(name), file}, 0}, nil + } + + // Directory, possibly synthesized. + // Note that file can be nil here: the map need not contain explicit parent directories for all its files. + // But file can also be non-nil, in case the user wants to set metadata for the directory explicitly. + // Either way, we need to construct the list of children of this directory. + var list []mapFileInfo + var elem string + var need = make(map[string]bool) + if name == "." { + elem = "." + for fname, f := range fsys { + i := strings.Index(fname, "/") + if i < 0 { + list = append(list, mapFileInfo{fname, f}) + } else { + need[fname[:i]] = true + } + } + } else { + elem = name[strings.LastIndex(name, "/")+1:] + prefix := name + "/" + for fname, f := range fsys { + if strings.HasPrefix(fname, prefix) { + felem := fname[len(prefix):] + i := strings.Index(felem, "/") + if i < 0 { + list = append(list, mapFileInfo{felem, f}) + } else { + need[fname[len(prefix):len(prefix)+i]] = true + } + } + } + // If the directory name is not in the map, + // and there are no children of the name in the map, + // then the directory is treated as not existing. + if file == nil && list == nil && len(need) == 0 { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist} + } + } + for _, fi := range list { + delete(need, fi.name) + } + for name := range need { + list = append(list, mapFileInfo{name, &MapFile{Mode: fs.ModeDir}}) + } + sort.Slice(list, func(i, j int) bool { + return list[i].name < list[j].name + }) + + if file == nil { + file = &MapFile{Mode: fs.ModeDir} + } + return &mapDir{name, mapFileInfo{elem, file}, list, 0}, nil +} + +// fsOnly is a wrapper that hides all but the fs.FS methods, +// to avoid an infinite recursion when implementing special +// methods in terms of helpers that would use them. +// (In general, implementing these methods using the package fs helpers +// is redundant and unnecessary, but having the methods may make +// MapFS exercise more code paths when used in tests.) +type fsOnly struct{ fs.FS } + +func (fsys MapFS) ReadFile(name string) ([]byte, error) { + return fs.ReadFile(fsOnly{fsys}, name) +} + +func (fsys MapFS) Stat(name string) (fs.FileInfo, error) { + return fs.Stat(fsOnly{fsys}, name) +} + +func (fsys MapFS) ReadDir(name string) ([]fs.DirEntry, error) { + return fs.ReadDir(fsOnly{fsys}, name) +} + +func (fsys MapFS) Glob(pattern string) ([]string, error) { + return fs.Glob(fsOnly{fsys}, pattern) +} + +type noSub struct { + MapFS +} + +func (noSub) Sub() {} // not the fs.SubFS signature + +func (fsys MapFS) Sub(dir string) (fs.FS, error) { + return fs.Sub(noSub{fsys}, dir) +} + +// A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file. +type mapFileInfo struct { + name string + f *MapFile +} + +func (i *mapFileInfo) Name() string { return i.name } +func (i *mapFileInfo) Size() int64 { return int64(len(i.f.Data)) } +func (i *mapFileInfo) Mode() fs.FileMode { return i.f.Mode } +func (i *mapFileInfo) Type() fs.FileMode { return i.f.Mode.Type() } +func (i *mapFileInfo) ModTime() time.Time { return i.f.ModTime } +func (i *mapFileInfo) IsDir() bool { return i.f.Mode&fs.ModeDir != 0 } +func (i *mapFileInfo) Sys() interface{} { return i.f.Sys } +func (i *mapFileInfo) Info() (fs.FileInfo, error) { return i, nil } + +// An openMapFile is a regular (non-directory) fs.File open for reading. +type openMapFile struct { + path string + mapFileInfo + offset int64 +} + +func (f *openMapFile) Stat() (fs.FileInfo, error) { return &f.mapFileInfo, nil } + +func (f *openMapFile) Close() error { return nil } + +func (f *openMapFile) Read(b []byte) (int, error) { + if f.offset >= int64(len(f.f.Data)) { + return 0, io.EOF + } + if f.offset < 0 { + return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} + } + n := copy(b, f.f.Data[f.offset:]) + f.offset += int64(n) + return n, nil +} + +func (f *openMapFile) Seek(offset int64, whence int) (int64, error) { + switch whence { + case 0: + // offset += 0 + case 1: + offset += f.offset + case 2: + offset += int64(len(f.f.Data)) + } + if offset < 0 || offset > int64(len(f.f.Data)) { + return 0, &fs.PathError{Op: "seek", Path: f.path, Err: fs.ErrInvalid} + } + f.offset = offset + return offset, nil +} + +func (f *openMapFile) ReadAt(b []byte, offset int64) (int, error) { + if offset < 0 || offset > int64(len(f.f.Data)) { + return 0, &fs.PathError{Op: "read", Path: f.path, Err: fs.ErrInvalid} + } + n := copy(b, f.f.Data[offset:]) + if n < len(b) { + return n, io.EOF + } + return n, nil +} + +// A mapDir is a directory fs.File (so also an fs.ReadDirFile) open for reading. +type mapDir struct { + path string + mapFileInfo + entry []mapFileInfo + offset int +} + +func (d *mapDir) Stat() (fs.FileInfo, error) { return &d.mapFileInfo, nil } +func (d *mapDir) Close() error { return nil } +func (d *mapDir) Read(b []byte) (int, error) { + return 0, &fs.PathError{Op: "read", Path: d.path, Err: fs.ErrInvalid} +} + +func (d *mapDir) ReadDir(count int) ([]fs.DirEntry, error) { + n := len(d.entry) - d.offset + if count > 0 && n > count { + n = count + } + if n == 0 && count > 0 { + return nil, io.EOF + } + list := make([]fs.DirEntry, n) + for i := range list { + list[i] = &d.entry[d.offset+i] + } + d.offset += n + return list, nil +} diff --git a/src/testing/fstest/mapfs_test.go b/src/testing/fstest/mapfs_test.go new file mode 100644 index 0000000..2abedd6 --- /dev/null +++ b/src/testing/fstest/mapfs_test.go @@ -0,0 +1,19 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fstest + +import ( + "testing" +) + +func TestMapFS(t *testing.T) { + m := MapFS{ + "hello": {Data: []byte("hello, world\n")}, + "fortune/k/ken.txt": {Data: []byte("If a program is too slow, it must have a loop.\n")}, + } + if err := TestFS(m, "hello", "fortune/k/ken.txt"); err != nil { + t.Fatal(err) + } +} diff --git a/src/testing/fstest/testfs.go b/src/testing/fstest/testfs.go new file mode 100644 index 0000000..8fc8aca --- /dev/null +++ b/src/testing/fstest/testfs.go @@ -0,0 +1,614 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package fstest implements support for testing implementations and users of file systems. +package fstest + +import ( + "errors" + "fmt" + "io" + "io/fs" + "io/ioutil" + "path" + "reflect" + "sort" + "strings" + "testing/iotest" +) + +// TestFS tests a file system implementation. +// It walks the entire tree of files in fsys, +// opening and checking that each file behaves correctly. +// It also checks that the file system contains at least the expected files. +// As a special case, if no expected files are listed, fsys must be empty. +// Otherwise, fsys must only contain at least the listed files: it can also contain others. +// The contents of fsys must not change concurrently with TestFS. +// +// If TestFS finds any misbehaviors, it returns an error reporting all of them. +// The error text spans multiple lines, one per detected misbehavior. +// +// Typical usage inside a test is: +// +// if err := fstest.TestFS(myFS, "file/that/should/be/present"); err != nil { +// t.Fatal(err) +// } +// +func TestFS(fsys fs.FS, expected ...string) error { + if err := testFS(fsys, expected...); err != nil { + return err + } + for _, name := range expected { + if i := strings.Index(name, "/"); i >= 0 { + dir, dirSlash := name[:i], name[:i+1] + var subExpected []string + for _, name := range expected { + if strings.HasPrefix(name, dirSlash) { + subExpected = append(subExpected, name[len(dirSlash):]) + } + } + sub, err := fs.Sub(fsys, dir) + if err != nil { + return err + } + if err := testFS(sub, subExpected...); err != nil { + return fmt.Errorf("testing fs.Sub(fsys, %s): %v", dir, err) + } + break // one sub-test is enough + } + } + return nil +} + +func testFS(fsys fs.FS, expected ...string) error { + t := fsTester{fsys: fsys} + t.checkDir(".") + t.checkOpen(".") + found := make(map[string]bool) + for _, dir := range t.dirs { + found[dir] = true + } + for _, file := range t.files { + found[file] = true + } + delete(found, ".") + if len(expected) == 0 && len(found) > 0 { + var list []string + for k := range found { + if k != "." { + list = append(list, k) + } + } + sort.Strings(list) + if len(list) > 15 { + list = append(list[:10], "...") + } + t.errorf("expected empty file system but found files:\n%s", strings.Join(list, "\n")) + } + for _, name := range expected { + if !found[name] { + t.errorf("expected but not found: %s", name) + } + } + if len(t.errText) == 0 { + return nil + } + return errors.New("TestFS found errors:\n" + string(t.errText)) +} + +// An fsTester holds state for running the test. +type fsTester struct { + fsys fs.FS + errText []byte + dirs []string + files []string +} + +// errorf adds an error line to errText. +func (t *fsTester) errorf(format string, args ...interface{}) { + if len(t.errText) > 0 { + t.errText = append(t.errText, '\n') + } + t.errText = append(t.errText, fmt.Sprintf(format, args...)...) +} + +func (t *fsTester) openDir(dir string) fs.ReadDirFile { + f, err := t.fsys.Open(dir) + if err != nil { + t.errorf("%s: Open: %v", dir, err) + return nil + } + d, ok := f.(fs.ReadDirFile) + if !ok { + f.Close() + t.errorf("%s: Open returned File type %T, not a fs.ReadDirFile", dir, f) + return nil + } + return d +} + +// checkDir checks the directory dir, which is expected to exist +// (it is either the root or was found in a directory listing with IsDir true). +func (t *fsTester) checkDir(dir string) { + // Read entire directory. + t.dirs = append(t.dirs, dir) + d := t.openDir(dir) + if d == nil { + return + } + list, err := d.ReadDir(-1) + if err != nil { + d.Close() + t.errorf("%s: ReadDir(-1): %v", dir, err) + return + } + + // Check all children. + var prefix string + if dir == "." { + prefix = "" + } else { + prefix = dir + "/" + } + for _, info := range list { + name := info.Name() + switch { + case name == ".", name == "..", name == "": + t.errorf("%s: ReadDir: child has invalid name: %#q", dir, name) + continue + case strings.Contains(name, "/"): + t.errorf("%s: ReadDir: child name contains slash: %#q", dir, name) + continue + case strings.Contains(name, `\`): + t.errorf("%s: ReadDir: child name contains backslash: %#q", dir, name) + continue + } + path := prefix + name + t.checkStat(path, info) + t.checkOpen(path) + if info.IsDir() { + t.checkDir(path) + } else { + t.checkFile(path) + } + } + + // Check ReadDir(-1) at EOF. + list2, err := d.ReadDir(-1) + if len(list2) > 0 || err != nil { + d.Close() + t.errorf("%s: ReadDir(-1) at EOF = %d entries, %v, wanted 0 entries, nil", dir, len(list2), err) + return + } + + // Check ReadDir(1) at EOF (different results). + list2, err = d.ReadDir(1) + if len(list2) > 0 || err != io.EOF { + d.Close() + t.errorf("%s: ReadDir(1) at EOF = %d entries, %v, wanted 0 entries, EOF", dir, len(list2), err) + return + } + + // Check that close does not report an error. + if err := d.Close(); err != nil { + t.errorf("%s: Close: %v", dir, err) + } + + // Check that closing twice doesn't crash. + // The return value doesn't matter. + d.Close() + + // Reopen directory, read a second time, make sure contents match. + if d = t.openDir(dir); d == nil { + return + } + defer d.Close() + list2, err = d.ReadDir(-1) + if err != nil { + t.errorf("%s: second Open+ReadDir(-1): %v", dir, err) + return + } + t.checkDirList(dir, "first Open+ReadDir(-1) vs second Open+ReadDir(-1)", list, list2) + + // Reopen directory, read a third time in pieces, make sure contents match. + if d = t.openDir(dir); d == nil { + return + } + defer d.Close() + list2 = nil + for { + n := 1 + if len(list2) > 0 { + n = 2 + } + frag, err := d.ReadDir(n) + if len(frag) > n { + t.errorf("%s: third Open: ReadDir(%d) after %d: %d entries (too many)", dir, n, len(list2), len(frag)) + return + } + list2 = append(list2, frag...) + if err == io.EOF { + break + } + if err != nil { + t.errorf("%s: third Open: ReadDir(%d) after %d: %v", dir, n, len(list2), err) + return + } + if n == 0 { + t.errorf("%s: third Open: ReadDir(%d) after %d: 0 entries but nil error", dir, n, len(list2)) + return + } + } + t.checkDirList(dir, "first Open+ReadDir(-1) vs third Open+ReadDir(1,2) loop", list, list2) + + // If fsys has ReadDir, check that it matches and is sorted. + if fsys, ok := t.fsys.(fs.ReadDirFS); ok { + list2, err := fsys.ReadDir(dir) + if err != nil { + t.errorf("%s: fsys.ReadDir: %v", dir, err) + return + } + t.checkDirList(dir, "first Open+ReadDir(-1) vs fsys.ReadDir", list, list2) + + for i := 0; i+1 < len(list2); i++ { + if list2[i].Name() >= list2[i+1].Name() { + t.errorf("%s: fsys.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name()) + } + } + } + + // Check fs.ReadDir as well. + list2, err = fs.ReadDir(t.fsys, dir) + if err != nil { + t.errorf("%s: fs.ReadDir: %v", dir, err) + return + } + t.checkDirList(dir, "first Open+ReadDir(-1) vs fs.ReadDir", list, list2) + + for i := 0; i+1 < len(list2); i++ { + if list2[i].Name() >= list2[i+1].Name() { + t.errorf("%s: fs.ReadDir: list not sorted: %s before %s", dir, list2[i].Name(), list2[i+1].Name()) + } + } + + t.checkGlob(dir, list) +} + +// formatEntry formats an fs.DirEntry into a string for error messages and comparison. +func formatEntry(entry fs.DirEntry) string { + return fmt.Sprintf("%s IsDir=%v Type=%v", entry.Name(), entry.IsDir(), entry.Type()) +} + +// formatInfoEntry formats an fs.FileInfo into a string like the result of formatEntry, for error messages and comparison. +func formatInfoEntry(info fs.FileInfo) string { + return fmt.Sprintf("%s IsDir=%v Type=%v", info.Name(), info.IsDir(), info.Mode().Type()) +} + +// formatInfo formats an fs.FileInfo into a string for error messages and comparison. +func formatInfo(info fs.FileInfo) string { + return fmt.Sprintf("%s IsDir=%v Mode=%v Size=%d ModTime=%v", info.Name(), info.IsDir(), info.Mode(), info.Size(), info.ModTime()) +} + +// checkGlob checks that various glob patterns work if the file system implements GlobFS. +func (t *fsTester) checkGlob(dir string, list []fs.DirEntry) { + if _, ok := t.fsys.(fs.GlobFS); !ok { + return + } + + // Make a complex glob pattern prefix that only matches dir. + var glob string + if dir != "." { + elem := strings.Split(dir, "/") + for i, e := range elem { + var pattern []rune + for j, r := range e { + if r == '*' || r == '?' || r == '\\' || r == '[' { + pattern = append(pattern, '\\', r) + continue + } + switch (i + j) % 5 { + case 0: + pattern = append(pattern, r) + case 1: + pattern = append(pattern, '[', r, ']') + case 2: + pattern = append(pattern, '[', r, '-', r, ']') + case 3: + pattern = append(pattern, '[', '\\', r, ']') + case 4: + pattern = append(pattern, '[', '\\', r, '-', '\\', r, ']') + } + } + elem[i] = string(pattern) + } + glob = strings.Join(elem, "/") + "/" + } + + // Test that malformed patterns are detected. + // The error is likely path.ErrBadPattern but need not be. + if _, err := t.fsys.(fs.GlobFS).Glob(glob + "nonexist/[]"); err == nil { + t.errorf("%s: Glob(%#q): bad pattern not detected", dir, glob+"nonexist/[]") + } + + // Try to find a letter that appears in only some of the final names. + c := rune('a') + for ; c <= 'z'; c++ { + have, haveNot := false, false + for _, d := range list { + if strings.ContainsRune(d.Name(), c) { + have = true + } else { + haveNot = true + } + } + if have && haveNot { + break + } + } + if c > 'z' { + c = 'a' + } + glob += "*" + string(c) + "*" + + var want []string + for _, d := range list { + if strings.ContainsRune(d.Name(), c) { + want = append(want, path.Join(dir, d.Name())) + } + } + + names, err := t.fsys.(fs.GlobFS).Glob(glob) + if err != nil { + t.errorf("%s: Glob(%#q): %v", dir, glob, err) + return + } + if reflect.DeepEqual(want, names) { + return + } + + if !sort.StringsAreSorted(names) { + t.errorf("%s: Glob(%#q): unsorted output:\n%s", dir, glob, strings.Join(names, "\n")) + sort.Strings(names) + } + + var problems []string + for len(want) > 0 || len(names) > 0 { + switch { + case len(want) > 0 && len(names) > 0 && want[0] == names[0]: + want, names = want[1:], names[1:] + case len(want) > 0 && (len(names) == 0 || want[0] < names[0]): + problems = append(problems, "missing: "+want[0]) + want = want[1:] + default: + problems = append(problems, "extra: "+names[0]) + names = names[1:] + } + } + t.errorf("%s: Glob(%#q): wrong output:\n%s", dir, glob, strings.Join(problems, "\n")) +} + +// checkStat checks that a direct stat of path matches entry, +// which was found in the parent's directory listing. +func (t *fsTester) checkStat(path string, entry fs.DirEntry) { + file, err := t.fsys.Open(path) + if err != nil { + t.errorf("%s: Open: %v", path, err) + return + } + info, err := file.Stat() + file.Close() + if err != nil { + t.errorf("%s: Stat: %v", path, err) + return + } + fentry := formatEntry(entry) + fientry := formatInfoEntry(info) + // Note: mismatch here is OK for symlink, because Open dereferences symlink. + if fentry != fientry && entry.Type()&fs.ModeSymlink == 0 { + t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, fientry) + } + + einfo, err := entry.Info() + if err != nil { + t.errorf("%s: entry.Info: %v", path, err) + return + } + finfo := formatInfo(info) + if entry.Type()&fs.ModeSymlink != 0 { + // For symlink, just check that entry.Info matches entry on common fields. + // Open deferences symlink, so info itself may differ. + feentry := formatInfoEntry(einfo) + if fentry != feentry { + t.errorf("%s: mismatch\n\tentry = %s\n\tentry.Info() = %s\n", path, fentry, feentry) + } + } else { + feinfo := formatInfo(einfo) + if feinfo != finfo { + t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, feinfo, finfo) + } + } + + // Stat should be the same as Open+Stat, even for symlinks. + info2, err := fs.Stat(t.fsys, path) + if err != nil { + t.errorf("%s: fs.Stat: %v", path, err) + return + } + finfo2 := formatInfo(info2) + if finfo2 != finfo { + t.errorf("%s: fs.Stat(...) = %s\n\twant %s", path, finfo2, finfo) + } + + if fsys, ok := t.fsys.(fs.StatFS); ok { + info2, err := fsys.Stat(path) + if err != nil { + t.errorf("%s: fsys.Stat: %v", path, err) + return + } + finfo2 := formatInfo(info2) + if finfo2 != finfo { + t.errorf("%s: fsys.Stat(...) = %s\n\twant %s", path, finfo2, finfo) + } + } +} + +// checkDirList checks that two directory lists contain the same files and file info. +// The order of the lists need not match. +func (t *fsTester) checkDirList(dir, desc string, list1, list2 []fs.DirEntry) { + old := make(map[string]fs.DirEntry) + checkMode := func(entry fs.DirEntry) { + if entry.IsDir() != (entry.Type()&fs.ModeDir != 0) { + if entry.IsDir() { + t.errorf("%s: ReadDir returned %s with IsDir() = true, Type() & ModeDir = 0", dir, entry.Name()) + } else { + t.errorf("%s: ReadDir returned %s with IsDir() = false, Type() & ModeDir = ModeDir", dir, entry.Name()) + } + } + } + + for _, entry1 := range list1 { + old[entry1.Name()] = entry1 + checkMode(entry1) + } + + var diffs []string + for _, entry2 := range list2 { + entry1 := old[entry2.Name()] + if entry1 == nil { + checkMode(entry2) + diffs = append(diffs, "+ "+formatEntry(entry2)) + continue + } + if formatEntry(entry1) != formatEntry(entry2) { + diffs = append(diffs, "- "+formatEntry(entry1), "+ "+formatEntry(entry2)) + } + delete(old, entry2.Name()) + } + for _, entry1 := range old { + diffs = append(diffs, "- "+formatEntry(entry1)) + } + + if len(diffs) == 0 { + return + } + + sort.Slice(diffs, func(i, j int) bool { + fi := strings.Fields(diffs[i]) + fj := strings.Fields(diffs[j]) + // sort by name (i < j) and then +/- (j < i, because + < -) + return fi[1]+" "+fj[0] < fj[1]+" "+fi[0] + }) + + t.errorf("%s: diff %s:\n\t%s", dir, desc, strings.Join(diffs, "\n\t")) +} + +// checkFile checks that basic file reading works correctly. +func (t *fsTester) checkFile(file string) { + t.files = append(t.files, file) + + // Read entire file. + f, err := t.fsys.Open(file) + if err != nil { + t.errorf("%s: Open: %v", file, err) + return + } + + data, err := ioutil.ReadAll(f) + if err != nil { + f.Close() + t.errorf("%s: Open+ReadAll: %v", file, err) + return + } + + if err := f.Close(); err != nil { + t.errorf("%s: Close: %v", file, err) + } + + // Check that closing twice doesn't crash. + // The return value doesn't matter. + f.Close() + + // Check that ReadFile works if present. + if fsys, ok := t.fsys.(fs.ReadFileFS); ok { + data2, err := fsys.ReadFile(file) + if err != nil { + t.errorf("%s: fsys.ReadFile: %v", file, err) + return + } + t.checkFileRead(file, "ReadAll vs fsys.ReadFile", data, data2) + + t.checkBadPath(file, "ReadFile", + func(name string) error { _, err := fsys.ReadFile(name); return err }) + } + + // Check that fs.ReadFile works with t.fsys. + data2, err := fs.ReadFile(t.fsys, file) + if err != nil { + t.errorf("%s: fs.ReadFile: %v", file, err) + return + } + t.checkFileRead(file, "ReadAll vs fs.ReadFile", data, data2) + + // Use iotest.TestReader to check small reads, Seek, ReadAt. + f, err = t.fsys.Open(file) + if err != nil { + t.errorf("%s: second Open: %v", file, err) + return + } + defer f.Close() + if err := iotest.TestReader(f, data); err != nil { + t.errorf("%s: failed TestReader:\n\t%s", file, strings.ReplaceAll(err.Error(), "\n", "\n\t")) + } +} + +func (t *fsTester) checkFileRead(file, desc string, data1, data2 []byte) { + if string(data1) != string(data2) { + t.errorf("%s: %s: different data returned\n\t%q\n\t%q", file, desc, data1, data2) + return + } +} + +// checkBadPath checks that various invalid forms of file's name cannot be opened using t.fsys.Open. +func (t *fsTester) checkOpen(file string) { + t.checkBadPath(file, "Open", func(file string) error { + f, err := t.fsys.Open(file) + if err == nil { + f.Close() + } + return err + }) +} + +// checkBadPath checks that various invalid forms of file's name cannot be opened using open. +func (t *fsTester) checkBadPath(file string, desc string, open func(string) error) { + bad := []string{ + "/" + file, + file + "/.", + } + if file == "." { + bad = append(bad, "/") + } + if i := strings.Index(file, "/"); i >= 0 { + bad = append(bad, + file[:i]+"//"+file[i+1:], + file[:i]+"/./"+file[i+1:], + file[:i]+`\`+file[i+1:], + file[:i]+"/../"+file, + ) + } + if i := strings.LastIndex(file, "/"); i >= 0 { + bad = append(bad, + file[:i]+"//"+file[i+1:], + file[:i]+"/./"+file[i+1:], + file[:i]+`\`+file[i+1:], + file+"/../"+file[i+1:], + ) + } + + for _, b := range bad { + if err := open(b); err == nil { + t.errorf("%s: %s(%s) succeeded, want error", file, desc, b) + } + } +} diff --git a/src/testing/fstest/testfs_test.go b/src/testing/fstest/testfs_test.go new file mode 100644 index 0000000..5b8813c --- /dev/null +++ b/src/testing/fstest/testfs_test.go @@ -0,0 +1,31 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package fstest + +import ( + "internal/testenv" + "os" + "path/filepath" + "testing" +) + +func TestSymlink(t *testing.T) { + testenv.MustHaveSymlink(t) + + tmp := t.TempDir() + tmpfs := os.DirFS(tmp) + + if err := os.WriteFile(filepath.Join(tmp, "hello"), []byte("hello, world\n"), 0644); err != nil { + t.Fatal(err) + } + + if err := os.Symlink(filepath.Join(tmp, "hello"), filepath.Join(tmp, "hello.link")); err != nil { + t.Fatal(err) + } + + if err := TestFS(tmpfs, "hello", "hello.link"); err != nil { + t.Fatal(err) + } +} |