Add Stat helper function, StatFS interface, and test.
Add Stat method to fstest.MapFS.
Add testing of Stat method to fstest.TestFS.
For #41190.
Change-Id: Icf8b6eb1c3fa6f93a9be8405ec5a9468fb1da97b
Reviewed-on: https://go-review.googlesource.com/c/go/+/243913
Trust: Russ Cox <rsc@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
--- /dev/null
+// 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 fs
+
+// A StatFS is a file system with a Stat method.
+type StatFS interface {
+       FS
+
+       // Stat returns a FileInfo describing the file.
+       // If there is an error, it should be of type *PathError.
+       Stat(name string) (FileInfo, error)
+}
+
+// Stat returns a FileInfo describing the named file from the file system.
+//
+// If fs implements StatFS, Stat calls fs.Stat.
+// Otherwise, Stat opens the file to stat it.
+func Stat(fsys FS, name string) (FileInfo, error) {
+       if fsys, ok := fsys.(StatFS); ok {
+               return fsys.Stat(name)
+       }
+
+       file, err := fsys.Open(name)
+       if err != nil {
+               return nil, err
+       }
+       defer file.Close()
+       return file.Stat()
+}
 
--- /dev/null
+// 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 fs_test
+
+import (
+       "fmt"
+       . "io/fs"
+       "testing"
+)
+
+type statOnly struct{ StatFS }
+
+func (statOnly) Open(name string) (File, error) { return nil, ErrNotExist }
+
+func TestStat(t *testing.T) {
+       check := func(desc string, info FileInfo, err error) {
+               t.Helper()
+               if err != nil || info == nil || info.Mode() != 0456 {
+                       infoStr := "<nil>"
+                       if info != nil {
+                               infoStr = fmt.Sprintf("FileInfo(Mode: %#o)", info.Mode())
+                       }
+                       t.Fatalf("Stat(%s) = %v, %v, want Mode:0456, nil", desc, infoStr, err)
+               }
+       }
+
+       // Test that Stat uses the method when present.
+       info, err := Stat(statOnly{testFsys}, "hello.txt")
+       check("statOnly", info, err)
+
+       // Test that Stat uses Open when the method is not present.
+       info, err = Stat(openOnly{testFsys}, "hello.txt")
+       check("openOnly", info, err)
+}
 
        return fs.ReadFile(fsOnly{fsys}, name)
 }
 
+func (fsys MapFS) Stat(name string) (fs.FileInfo, error) {
+       return fs.Stat(fsOnly{fsys}, name)
+}
+
 // A mapFileInfo implements fs.FileInfo and fs.DirEntry for a given map file.
 type mapFileInfo struct {
        name string
 
        fentry := formatEntry(entry)
        finfo := formatInfoEntry(info)
        if fentry != finfo {
-               t.errorf("%s: mismatch:\n\tentry = %v\n\tfile.Stat() = %v", path, fentry, finfo)
+               t.errorf("%s: mismatch:\n\tentry = %s\n\tfile.Stat() = %s", path, fentry, finfo)
+       }
+
+       info2, err := fs.Stat(t.fsys, path)
+       if err != nil {
+               t.errorf("%s: fs.Stat: %v", path, err)
+               return
+       }
+       finfo = formatInfo(info)
+       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)
+               }
        }
 }