diff --git a/.circleci/config.yml b/.circleci/config.yml index 3924b1db..f33f9de3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -102,9 +102,9 @@ commands: - run: make fmt-check jobs: - test-llvm11-go115: + test-llvm11-go116: docker: - - image: circleci/golang:1.15-buster + - image: circleci/golang:1.16-buster steps: - test-linux: llvm: "11" @@ -118,5 +118,5 @@ jobs: workflows: test-all: jobs: - - test-llvm11-go115 + - test-llvm11-go116 - test-llvm12-go117 diff --git a/BUILDING.md b/BUILDING.md index 16d5c714..1c1b7945 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -18,7 +18,7 @@ tarball. If you want to help with development of TinyGo itself, you should follo LLVM, Clang and LLD are quite light on dependencies, requiring only standard build tools to be built. Go is of course necessary to build TinyGo itself. - * Go (1.15+) + * Go (1.16+) * Standard build tools (gcc/clang) * git * CMake diff --git a/Makefile b/Makefile index dc388c21..f05dde62 100644 --- a/Makefile +++ b/Makefile @@ -300,6 +300,7 @@ TEST_PACKAGES_LINUX := \ debug/dwarf \ debug/plan9obj \ io/fs \ + io/ioutil \ testing/fstest TEST_PACKAGES_DARWIN := $(TEST_PACKAGES_LINUX) diff --git a/builder/config.go b/builder/config.go index a1930507..3f06c968 100644 --- a/builder/config.go +++ b/builder/config.go @@ -33,8 +33,8 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) { if err != nil { return nil, fmt.Errorf("could not read version from GOROOT (%v): %v", goroot, err) } - if major != 1 || minor < 15 || minor > 18 { - return nil, fmt.Errorf("requires go version 1.15 through 1.18, got go%d.%d", major, minor) + if major != 1 || minor < 16 || minor > 18 { + return nil, fmt.Errorf("requires go version 1.16 through 1.18, got go%d.%d", major, minor) } clangHeaderPath := getClangHeaderPath(goenv.Get("TINYGOROOT")) diff --git a/go.mod b/go.mod index 84f7d9f0..0b03168e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tinygo-org/tinygo -go 1.15 +go 1.16 require ( github.com/aykevl/go-wasm v0.0.2-0.20211119014117-0761b1ddcd1a diff --git a/src/os/dir.go b/src/os/dir.go index f3ad9f69..5306bcb3 100644 --- a/src/os/dir.go +++ b/src/os/dir.go @@ -1,6 +1,3 @@ -//go:build go1.16 -// +build go1.16 - // Copyright 2016 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. diff --git a/src/os/dir_other.go b/src/os/dir_other.go index 0451d7dc..aebeed4e 100644 --- a/src/os/dir_other.go +++ b/src/os/dir_other.go @@ -1,5 +1,5 @@ -//go:build (go1.16 && baremetal) || (go1.16 && js) || (go1.16 && wasi) || (go1.16 && windows) -// +build go1.16,baremetal go1.16,js go1.16,wasi go1.16,windows +//go:build baremetal || js || wasi || windows +// +build baremetal js wasi windows // Copyright 2009 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style diff --git a/src/os/dir_other_go115.go b/src/os/dir_other_go115.go deleted file mode 100644 index 028b5b99..00000000 --- a/src/os/dir_other_go115.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build !go1.16 -// +build !go1.16 - -// Copyright 2009 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 os - -func (f *File) Readdirnames(n int) (names []string, err error) { - return nil, ErrInvalid -} diff --git a/src/os/errors.go b/src/os/errors.go index 36e3c55e..83f04b0f 100644 --- a/src/os/errors.go +++ b/src/os/errors.go @@ -1,21 +1,32 @@ +// Copyright 2009 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 os import ( "errors" + "io/fs" "syscall" ) +// Portable analogs of some common system call errors. +// +// Errors returned from this package may be tested against these errors +// with errors.Is. var ( - ErrInvalid = errors.New("invalid argument") - ErrPermission = errors.New("permission denied") - ErrClosed = errors.New("file already closed") + // ErrInvalid indicates an invalid argument. + // Methods on File will return this error when the receiver is nil. + ErrInvalid = fs.ErrInvalid // "invalid argument" + + ErrPermission = fs.ErrPermission // "permission denied" + ErrExist = fs.ErrExist // "file already exists" + ErrNotExist = fs.ErrNotExist // "file does not exist" + ErrClosed = fs.ErrClosed // "file already closed" - // Portable analogs of some common system call errors. // Note that these are exported for use in the Filesystem interface. ErrUnsupported = errors.New("operation not supported") ErrNotImplemented = errors.New("operation not implemented") - ErrNotExist = errors.New("file not found") - ErrExist = errors.New("file exists") ) // The following code is copied from the official implementation. @@ -46,6 +57,9 @@ func NewSyscallError(syscall string, err error) error { return &SyscallError{syscall, err} } +// PathError records an error and the operation and file path that caused it. +type PathError = fs.PathError + // SyscallError records an error from a specific system call. type SyscallError struct { Syscall string diff --git a/src/os/file.go b/src/os/file.go index 9a4c3091..05e742b6 100644 --- a/src/os/file.go +++ b/src/os/file.go @@ -12,6 +12,7 @@ package os import ( "errors" "io" + "io/fs" "runtime" "syscall" ) @@ -200,23 +201,6 @@ func (f *File) Truncate(size int64) error { return &PathError{"truncate", f.name, ErrNotImplemented} } -// PathError records an error and the operation and file path that caused it. -// TODO: PathError moved to io/fs in go 1.16 and left an alias in os/errors.go. -// Do the same once we drop support for go 1.15. -type PathError struct { - Op string - Path string - Err error -} - -func (e *PathError) Error() string { - return e.Op + " " + e.Path + ": " + e.Err.Error() -} - -func (e *PathError) Unwrap() error { - return e.Err -} - // LinkError records an error during a link or symlink or rename system call and // the paths that caused it. type LinkError struct { @@ -286,3 +270,146 @@ func UserHomeDir() (string, error) { } return "", errors.New(enverr + " is not defined") } + +type ( + FileMode = fs.FileMode + FileInfo = fs.FileInfo +) + +// The followings are copied from Go 1.16 or 1.17 official implementation: +// https://github.com/golang/go/blob/go1.16/src/os/file.go + +// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir. +// +// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the +// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the +// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside +// the /prefix tree, then using DirFS does not stop the access any more than using +// os.Open does. DirFS is therefore not a general substitute for a chroot-style security +// mechanism when the directory tree contains arbitrary content. +func DirFS(dir string) fs.FS { + return dirFS(dir) +} + +func containsAny(s, chars string) bool { + for i := 0; i < len(s); i++ { + for j := 0; j < len(chars); j++ { + if s[i] == chars[j] { + return true + } + } + } + return false +} + +type dirFS string + +func (dir dirFS) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { + return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid} + } + f, err := Open(string(dir) + "/" + name) + if err != nil { + return nil, err // nil fs.File + } + return f, nil +} + +func (dir dirFS) Stat(name string) (fs.FileInfo, error) { + if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { + return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid} + } + f, err := Stat(string(dir) + "/" + name) + if err != nil { + return nil, err + } + return f, nil +} + +// ReadFile reads the named file and returns the contents. +// A successful call returns err == nil, not err == EOF. +// Because ReadFile reads the whole file, it does not treat an EOF from Read +// as an error to be reported. +func ReadFile(name string) ([]byte, error) { + f, err := Open(name) + if err != nil { + return nil, err + } + defer f.Close() + + var size int + if info, err := f.Stat(); err == nil { + size64 := info.Size() + if int64(int(size64)) == size64 { + size = int(size64) + } + } + size++ // one byte for final read at EOF + + // If a file claims a small size, read at least 512 bytes. + // In particular, files in Linux's /proc claim size 0 but + // then do not work right if read in small pieces, + // so an initial read of 1 byte would not work correctly. + if size < 512 { + size = 512 + } + + data := make([]byte, 0, size) + for { + if len(data) >= cap(data) { + d := append(data[:cap(data)], 0) + data = d[:len(data)] + } + n, err := f.Read(data[len(data):cap(data)]) + data = data[:len(data)+n] + if err != nil { + if err == io.EOF { + err = nil + } + return data, err + } + } +} + +// WriteFile writes data to the named file, creating it if necessary. +// If the file does not exist, WriteFile creates it with permissions perm (before umask); +// otherwise WriteFile truncates it before writing, without changing permissions. +func WriteFile(name string, data []byte, perm FileMode) error { + f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm) + if err != nil { + return err + } + _, err = f.Write(data) + if err1 := f.Close(); err1 != nil && err == nil { + err = err1 + } + return err +} + +// The defined file mode bits are the most significant bits of the FileMode. +// The nine least-significant bits are the standard Unix rwxrwxrwx permissions. +// The values of these bits should be considered part of the public API and +// may be used in wire protocols or disk representations: they must not be +// changed, although new bits might be added. +const ( + // The single letters are the abbreviations + // used by the String method's formatting. + ModeDir = fs.ModeDir // d: is a directory + ModeAppend = fs.ModeAppend // a: append-only + ModeExclusive = fs.ModeExclusive // l: exclusive use + ModeTemporary = fs.ModeTemporary // T: temporary file; Plan 9 only + ModeSymlink = fs.ModeSymlink // L: symbolic link + ModeDevice = fs.ModeDevice // D: device file + ModeNamedPipe = fs.ModeNamedPipe // p: named pipe (FIFO) + ModeSocket = fs.ModeSocket // S: Unix domain socket + ModeSetuid = fs.ModeSetuid // u: setuid + ModeSetgid = fs.ModeSetgid // g: setgid + ModeCharDevice = fs.ModeCharDevice // c: Unix character device, when ModeDevice is set + ModeSticky = fs.ModeSticky // t: sticky + ModeIrregular = fs.ModeIrregular // ?: non-regular file; nothing else is known about this file + + // Mask for the type bits. For regular files, none will be set. + ModeType = fs.ModeType + + ModePerm = fs.ModePerm // Unix permission bits, 0o777 +) diff --git a/src/os/file_go_116.go b/src/os/file_go_116.go deleted file mode 100644 index e383392a..00000000 --- a/src/os/file_go_116.go +++ /dev/null @@ -1,153 +0,0 @@ -//go:build go1.16 -// +build go1.16 - -package os - -import ( - "io" - "io/fs" - "runtime" -) - -type ( - FileMode = fs.FileMode - FileInfo = fs.FileInfo -) - -// The followings are copied from Go 1.16 or 1.17 official implementation: -// https://github.com/golang/go/blob/go1.16/src/os/file.go - -// DirFS returns a file system (an fs.FS) for the tree of files rooted at the directory dir. -// -// Note that DirFS("/prefix") only guarantees that the Open calls it makes to the -// operating system will begin with "/prefix": DirFS("/prefix").Open("file") is the -// same as os.Open("/prefix/file"). So if /prefix/file is a symbolic link pointing outside -// the /prefix tree, then using DirFS does not stop the access any more than using -// os.Open does. DirFS is therefore not a general substitute for a chroot-style security -// mechanism when the directory tree contains arbitrary content. -func DirFS(dir string) fs.FS { - return dirFS(dir) -} - -func containsAny(s, chars string) bool { - for i := 0; i < len(s); i++ { - for j := 0; j < len(chars); j++ { - if s[i] == chars[j] { - return true - } - } - } - return false -} - -type dirFS string - -func (dir dirFS) Open(name string) (fs.File, error) { - if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { - return nil, &PathError{Op: "open", Path: name, Err: ErrInvalid} - } - f, err := Open(string(dir) + "/" + name) - if err != nil { - return nil, err // nil fs.File - } - return f, nil -} - -func (dir dirFS) Stat(name string) (fs.FileInfo, error) { - if !fs.ValidPath(name) || runtime.GOOS == "windows" && containsAny(name, `\:`) { - return nil, &PathError{Op: "stat", Path: name, Err: ErrInvalid} - } - f, err := Stat(string(dir) + "/" + name) - if err != nil { - return nil, err - } - return f, nil -} - -// ReadFile reads the named file and returns the contents. -// A successful call returns err == nil, not err == EOF. -// Because ReadFile reads the whole file, it does not treat an EOF from Read -// as an error to be reported. -func ReadFile(name string) ([]byte, error) { - f, err := Open(name) - if err != nil { - return nil, err - } - defer f.Close() - - var size int - if info, err := f.Stat(); err == nil { - size64 := info.Size() - if int64(int(size64)) == size64 { - size = int(size64) - } - } - size++ // one byte for final read at EOF - - // If a file claims a small size, read at least 512 bytes. - // In particular, files in Linux's /proc claim size 0 but - // then do not work right if read in small pieces, - // so an initial read of 1 byte would not work correctly. - if size < 512 { - size = 512 - } - - data := make([]byte, 0, size) - for { - if len(data) >= cap(data) { - d := append(data[:cap(data)], 0) - data = d[:len(data)] - } - n, err := f.Read(data[len(data):cap(data)]) - data = data[:len(data)+n] - if err != nil { - if err == io.EOF { - err = nil - } - return data, err - } - } -} - -// WriteFile writes data to the named file, creating it if necessary. -// If the file does not exist, WriteFile creates it with permissions perm (before umask); -// otherwise WriteFile truncates it before writing, without changing permissions. -func WriteFile(name string, data []byte, perm FileMode) error { - f, err := OpenFile(name, O_WRONLY|O_CREATE|O_TRUNC, perm) - if err != nil { - return err - } - _, err = f.Write(data) - if err1 := f.Close(); err1 != nil && err == nil { - err = err1 - } - return err -} - -// The defined file mode bits are the most significant bits of the FileMode. -// The nine least-significant bits are the standard Unix rwxrwxrwx permissions. -// The values of these bits should be considered part of the public API and -// may be used in wire protocols or disk representations: they must not be -// changed, although new bits might be added. -const ( - // The single letters are the abbreviations - // used by the String method's formatting. - ModeDir = fs.ModeDir // d: is a directory - ModeAppend = fs.ModeAppend // a: append-only - ModeExclusive = fs.ModeExclusive // l: exclusive use - ModeTemporary = fs.ModeTemporary // T: temporary file; Plan 9 only - ModeSymlink = fs.ModeSymlink // L: symbolic link - ModeDevice = fs.ModeDevice // D: device file - ModeNamedPipe = fs.ModeNamedPipe // p: named pipe (FIFO) - ModeSocket = fs.ModeSocket // S: Unix domain socket - ModeSetuid = fs.ModeSetuid // u: setuid - ModeSetgid = fs.ModeSetgid // g: setgid - ModeCharDevice = fs.ModeCharDevice // c: Unix character device, when ModeDevice is set - ModeSticky = fs.ModeSticky // t: sticky - ModeIrregular = fs.ModeIrregular // ?: non-regular file; nothing else is known about this file - - // Mask for the type bits. For regular files, none will be set. - ModeType = fs.ModeType - - ModePerm = fs.ModePerm // Unix permission bits, 0o777 -) diff --git a/src/os/file_go_116_test.go b/src/os/file_go_116_test.go deleted file mode 100644 index c218a02d..00000000 --- a/src/os/file_go_116_test.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2009 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. - -//go:build go1.16 && !baremetal && !js && !wasi -// +build go1.16,!baremetal,!js,!wasi - -// DirFS tests copied verbatim from upstream os_test.go, and adjusted minimally to fit tinygo. - -package os_test - -import ( - "io/fs" - "os" - . "os" - "path/filepath" - "runtime" - "testing" - "testing/fstest" -) - -func TestDirFS(t *testing.T) { - if runtime.GOOS == "windows" { - t.Log("TODO: implement Readdir for Windows") - return - } - if err := fstest.TestFS(DirFS("./testdata/dirfs"), "a", "b", "dir/x"); err != nil { - t.Fatal(err) - } - - // Test that Open does not accept backslash as separator. - d := DirFS(".") - _, err := d.Open(`testdata\dirfs`) - if err == nil { - t.Fatalf(`Open testdata\dirfs succeeded`) - } -} - -func TestDirFSPathsValid(t *testing.T) { - if runtime.GOOS == "windows" { - t.Log("skipping on Windows") - return - } - - // TODO: switch back to t.TempDir once it's implemented - d, err := MkdirTemp("", "TestDirFSPathsValid") - if err != nil { - t.Fatal(err) - } - defer Remove(d) - if err := os.WriteFile(filepath.Join(d, "control.txt"), []byte(string("Hello, world!")), 0644); err != nil { - t.Fatal(err) - } - defer Remove(filepath.Join(d, "control.txt")) - if err := os.WriteFile(filepath.Join(d, `e:xperi\ment.txt`), []byte(string("Hello, colon and backslash!")), 0644); err != nil { - t.Fatal(err) - } - defer Remove(filepath.Join(d, `e:xperi\ment.txt`)) - - fsys := os.DirFS(d) - err = fs.WalkDir(fsys, ".", func(path string, e fs.DirEntry, err error) error { - if fs.ValidPath(e.Name()) { - t.Logf("%q ok", e.Name()) - } else { - t.Errorf("%q INVALID", e.Name()) - } - return nil - }) - if err != nil { - t.Fatal(err) - } -} diff --git a/src/os/file_go_other.go b/src/os/file_go_other.go deleted file mode 100644 index c03ff468..00000000 --- a/src/os/file_go_other.go +++ /dev/null @@ -1,57 +0,0 @@ -//go:build !go1.16 -// +build !go1.16 - -package os - -import "time" - -// A FileInfo describes a file and is returned by Stat and Lstat. -type FileInfo interface { - Name() string // base name of the file - Size() int64 // length in bytes for regular files; system-dependent for others - Mode() FileMode // file mode bits - ModTime() time.Time // modification time - IsDir() bool // abbreviation for Mode().IsDir() - Sys() interface{} // underlying data source (can return nil) -} - -type FileMode uint32 - -// Mode constants, copied from the mainline Go source -// https://github.com/golang/go/blob/4ce6a8e89668b87dce67e2f55802903d6eb9110a/src/os/types.go#L35-L63 -const ( - // The single letters are the abbreviations used by the String method's formatting. - ModeDir FileMode = 1 << (32 - 1 - iota) // d: is a directory - ModeAppend // a: append-only - ModeExclusive // l: exclusive use - ModeTemporary // T: temporary file; Plan 9 only - ModeSymlink // L: symbolic link - ModeDevice // D: device file - ModeNamedPipe // p: named pipe (FIFO) - ModeSocket // S: Unix domain socket - ModeSetuid // u: setuid - ModeSetgid // g: setgid - ModeCharDevice // c: Unix character device, when ModeDevice is set - ModeSticky // t: sticky - ModeIrregular // ?: non-regular file; nothing else is known about this file - - // Mask for the type bits. For regular files, none will be set. - ModeType = ModeDir | ModeSymlink | ModeNamedPipe | ModeSocket | ModeDevice | ModeCharDevice | ModeIrregular - - ModePerm FileMode = 0777 // Unix permission bits -) - -// IsDir is a stub, always returning false -func (m FileMode) IsDir() bool { - return false -} - -// IsRegular is a stub, always returning false -func (m FileMode) IsRegular() bool { - return false -} - -// Perm is a stub, always returning 0. -func (m FileMode) Perm() FileMode { - return 0 -} diff --git a/src/os/os_anyos_test.go b/src/os/os_anyos_test.go index 643c5bd9..185974e6 100644 --- a/src/os/os_anyos_test.go +++ b/src/os/os_anyos_test.go @@ -4,6 +4,7 @@ package os_test import ( + "io/fs" "io/ioutil" . "os" "path/filepath" @@ -11,6 +12,7 @@ import ( "strconv" "strings" "testing" + "testing/fstest" "time" ) @@ -55,8 +57,7 @@ func TestStatBadDir(t *testing.T) { dir := TempDir() badDir := filepath.Join(dir, "not-exist/really-not-exist") _, err := Stat(badDir) - // TODO: PathError moved to io/fs in go 1.16; fix next line once we drop go 1.15 support. - if pe, ok := err.(*PathError); !ok || !IsNotExist(err) || pe.Path != badDir { + if pe, ok := err.(*fs.PathError); !ok || !IsNotExist(err) || pe.Path != badDir { t.Errorf("Mkdir error = %#v; want PathError for path %q satisifying IsNotExist", err, badDir) } } @@ -123,8 +124,7 @@ func TestRemove(t *testing.T) { if err == nil { t.Errorf("TestRemove: remove of nonexistent file did not fail") } else { - // FIXME: once we drop go 1.15, switch this to fs.PathError - if pe, ok := err.(*PathError); !ok { + if pe, ok := err.(*fs.PathError); !ok { t.Errorf("TestRemove: expected PathError, got err %q", err.Error()) } else { if pe.Path != f { @@ -271,3 +271,55 @@ func TestUserHomeDir(t *testing.T) { t.Fatalf("dir %s is not directory; type = %v", dir, fi.Mode()) } } + +func TestDirFS(t *testing.T) { + if runtime.GOOS == "windows" { + t.Log("TODO: implement Readdir for Windows") + return + } + if err := fstest.TestFS(DirFS("./testdata/dirfs"), "a", "b", "dir/x"); err != nil { + t.Fatal(err) + } + + // Test that Open does not accept backslash as separator. + d := DirFS(".") + _, err := d.Open(`testdata\dirfs`) + if err == nil { + t.Fatalf(`Open testdata\dirfs succeeded`) + } +} + +func TestDirFSPathsValid(t *testing.T) { + if runtime.GOOS == "windows" { + t.Log("skipping on Windows") + return + } + + // TODO: switch back to t.TempDir once it's implemented + d, err := MkdirTemp("", "TestDirFSPathsValid") + if err != nil { + t.Fatal(err) + } + defer Remove(d) + if err := WriteFile(filepath.Join(d, "control.txt"), []byte(string("Hello, world!")), 0644); err != nil { + t.Fatal(err) + } + defer Remove(filepath.Join(d, "control.txt")) + if err := WriteFile(filepath.Join(d, `e:xperi\ment.txt`), []byte(string("Hello, colon and backslash!")), 0644); err != nil { + t.Fatal(err) + } + defer Remove(filepath.Join(d, `e:xperi\ment.txt`)) + + fsys := DirFS(d) + err = fs.WalkDir(fsys, ".", func(path string, e fs.DirEntry, err error) error { + if fs.ValidPath(e.Name()) { + t.Logf("%q ok", e.Name()) + } else { + t.Errorf("%q INVALID", e.Name()) + } + return nil + }) + if err != nil { + t.Fatal(err) + } +} diff --git a/src/os/tempfile_test.go b/src/os/tempfile_test.go index 72444934..7c3e706e 100644 --- a/src/os/tempfile_test.go +++ b/src/os/tempfile_test.go @@ -9,6 +9,7 @@ package os_test import ( "errors" + "io/fs" . "os" "path/filepath" "regexp" @@ -157,8 +158,7 @@ func TestMkdirTempBadDir(t *testing.T) { badDir := filepath.Join(dir, "not-exist") _, err = MkdirTemp(badDir, "foo") - // TODO: when we drop support for go 1.15, PathError should move to fs - if pe, ok := err.(*PathError); !ok || !IsNotExist(err) || pe.Path != badDir { + if pe, ok := err.(*fs.PathError); !ok || !IsNotExist(err) || pe.Path != badDir { t.Errorf("TempDir error = %#v; want PathError for path %q satisifying IsNotExist", err, badDir) } }