aboutsummaryrefslogtreecommitdiff
path: root/src/testing
diff options
context:
space:
mode:
Diffstat (limited to 'src/testing')
-rw-r--r--src/testing/fstest/mapfs.go10
-rw-r--r--src/testing/fstest/testfs.go70
-rw-r--r--src/testing/fstest/testfs_test.go31
-rw-r--r--src/testing/testing.go61
-rw-r--r--src/testing/testing_test.go7
5 files changed, 147 insertions, 32 deletions
diff --git a/src/testing/fstest/mapfs.go b/src/testing/fstest/mapfs.go
index 10e56f5b3c..a5d4a23fac 100644
--- a/src/testing/fstest/mapfs.go
+++ b/src/testing/fstest/mapfs.go
@@ -132,6 +132,16 @@ 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
diff --git a/src/testing/fstest/testfs.go b/src/testing/fstest/testfs.go
index 4912a271b2..8fc8acaaf3 100644
--- a/src/testing/fstest/testfs.go
+++ b/src/testing/fstest/testfs.go
@@ -22,6 +22,9 @@ import (
// 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.
@@ -33,6 +36,32 @@ import (
// }
//
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(".")
@@ -43,6 +72,20 @@ func TestFS(fsys fs.FS, expected ...string) error {
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)
@@ -79,7 +122,7 @@ func (t *fsTester) openDir(dir string) fs.ReadDirFile {
d, ok := f.(fs.ReadDirFile)
if !ok {
f.Close()
- t.errorf("%s: Open returned File type %T, not a io.ReadDirFile", dir, f)
+ t.errorf("%s: Open returned File type %T, not a fs.ReadDirFile", dir, f)
return nil
}
return d
@@ -360,9 +403,10 @@ func (t *fsTester) checkStat(path string, entry fs.DirEntry) {
return
}
fentry := formatEntry(entry)
- finfo := formatInfoEntry(info)
- if fentry != finfo {
- t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, finfo)
+ 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()
@@ -370,12 +414,22 @@ func (t *fsTester) checkStat(path string, entry fs.DirEntry) {
t.errorf("%s: entry.Info: %v", path, err)
return
}
- fentry = formatInfo(einfo)
- finfo = formatInfo(info)
- if fentry != finfo {
- t.errorf("%s: mismatch:\n\tentry.Info() = %s\n\tfile.Stat() = %s\n", path, fentry, finfo)
+ 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)
diff --git a/src/testing/fstest/testfs_test.go b/src/testing/fstest/testfs_test.go
new file mode 100644
index 0000000000..5b8813c343
--- /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)
+ }
+}
diff --git a/src/testing/testing.go b/src/testing/testing.go
index a44c0a0749..80354d5ce8 100644
--- a/src/testing/testing.go
+++ b/src/testing/testing.go
@@ -242,7 +242,6 @@ import (
"fmt"
"internal/race"
"io"
- "io/ioutil"
"os"
"runtime"
"runtime/debug"
@@ -384,17 +383,18 @@ const maxStackLen = 50
// common holds the elements common between T and B and
// captures common methods such as Errorf.
type common struct {
- mu sync.RWMutex // guards this group of fields
- output []byte // Output generated by test or benchmark.
- w io.Writer // For flushToParent.
- ran bool // Test or benchmark (or one of its subtests) was executed.
- failed bool // Test or benchmark has failed.
- skipped bool // Test of benchmark has been skipped.
- done bool // Test is finished and all subtests have completed.
- helpers map[string]struct{} // functions to be skipped when writing file/line info
- cleanups []func() // optional functions to be called at the end of the test
- cleanupName string // Name of the cleanup function.
- cleanupPc []uintptr // The stack trace at the point where Cleanup was called.
+ mu sync.RWMutex // guards this group of fields
+ output []byte // Output generated by test or benchmark.
+ w io.Writer // For flushToParent.
+ ran bool // Test or benchmark (or one of its subtests) was executed.
+ failed bool // Test or benchmark has failed.
+ skipped bool // Test of benchmark has been skipped.
+ done bool // Test is finished and all subtests have completed.
+ helperPCs map[uintptr]struct{} // functions to be skipped when writing file/line info
+ helperNames map[string]struct{} // helperPCs converted to function names
+ cleanups []func() // optional functions to be called at the end of the test
+ cleanupName string // Name of the cleanup function.
+ cleanupPc []uintptr // The stack trace at the point where Cleanup was called.
chatty *chattyPrinter // A copy of chattyPrinter, if the chatty flag is set.
bench bool // Whether the current test is a benchmark.
@@ -509,7 +509,7 @@ func (c *common) frameSkip(skip int) runtime.Frame {
}
return prevFrame
}
- if _, ok := c.helpers[frame.Function]; !ok {
+ if _, ok := c.helperNames[frame.Function]; !ok {
// Found a frame that wasn't inside a helper function.
return frame
}
@@ -521,6 +521,14 @@ func (c *common) frameSkip(skip int) runtime.Frame {
// and inserts the final newline if needed and indentation spaces for formatting.
// This function must be called with c.mu held.
func (c *common) decorate(s string, skip int) string {
+ // If more helper PCs have been added since we last did the conversion
+ if c.helperNames == nil {
+ c.helperNames = make(map[string]struct{})
+ for pc := range c.helperPCs {
+ c.helperNames[pcToName(pc)] = struct{}{}
+ }
+ }
+
frame := c.frameSkip(skip)
file := frame.File
line := frame.Line
@@ -853,10 +861,19 @@ func (c *common) Skipped() bool {
func (c *common) Helper() {
c.mu.Lock()
defer c.mu.Unlock()
- if c.helpers == nil {
- c.helpers = make(map[string]struct{})
+ if c.helperPCs == nil {
+ c.helperPCs = make(map[uintptr]struct{})
+ }
+ // repeating code from callerName here to save walking a stack frame
+ var pc [1]uintptr
+ n := runtime.Callers(2, pc[:]) // skip runtime.Callers + Helper
+ if n == 0 {
+ panic("testing: zero callers found")
+ }
+ if _, found := c.helperPCs[pc[0]]; !found {
+ c.helperPCs[pc[0]] = struct{}{}
+ c.helperNames = nil // map will be recreated next time it is needed
}
- c.helpers[callerName(1)] = struct{}{}
}
// Cleanup registers a function to be called when the test and all its
@@ -918,14 +935,14 @@ func (c *common) TempDir() string {
if nonExistent {
c.Helper()
- // ioutil.TempDir doesn't like path separators in its pattern,
+ // os.MkdirTemp doesn't like path separators in its pattern,
// so mangle the name to accommodate subtests.
tempDirReplacer.Do(func() {
tempDirReplacer.r = strings.NewReplacer("/", "_", "\\", "_", ":", "_")
})
pattern := tempDirReplacer.r.Replace(c.Name())
- c.tempDir, c.tempDirErr = ioutil.TempDir("", pattern)
+ c.tempDir, c.tempDirErr = os.MkdirTemp("", pattern)
if c.tempDirErr == nil {
c.Cleanup(func() {
if err := os.RemoveAll(c.tempDir); err != nil {
@@ -995,13 +1012,17 @@ func (c *common) runCleanup(ph panicHandling) (panicVal interface{}) {
// callerName gives the function name (qualified with a package path)
// for the caller after skip frames (where 0 means the current function).
func callerName(skip int) string {
- // Make room for the skip PC.
var pc [1]uintptr
n := runtime.Callers(skip+2, pc[:]) // skip + runtime.Callers + callerName
if n == 0 {
panic("testing: zero callers found")
}
- frames := runtime.CallersFrames(pc[:n])
+ return pcToName(pc[0])
+}
+
+func pcToName(pc uintptr) string {
+ pcs := []uintptr{pc}
+ frames := runtime.CallersFrames(pcs)
frame, _ := frames.Next()
return frame.Function
}
diff --git a/src/testing/testing_test.go b/src/testing/testing_test.go
index d665a334e4..0f096980ca 100644
--- a/src/testing/testing_test.go
+++ b/src/testing/testing_test.go
@@ -5,7 +5,6 @@
package testing_test
import (
- "io/ioutil"
"os"
"path/filepath"
"testing"
@@ -102,11 +101,11 @@ func testTempDir(t *testing.T) {
if !fi.IsDir() {
t.Errorf("dir %q is not a dir", dir)
}
- fis, err := ioutil.ReadDir(dir)
+ files, err := os.ReadDir(dir)
if err != nil {
t.Fatal(err)
}
- if len(fis) > 0 {
- t.Errorf("unexpected %d files in TempDir: %v", len(fis), fis)
+ if len(files) > 0 {
+ t.Errorf("unexpected %d files in TempDir: %v", len(files), files)
}
}