Adds IsDir and Seek to platform.File (#1441)
Signed-off-by: Adrian Cole <adrian@tetrate.io>
This commit is contained in:
@@ -67,15 +67,16 @@ func BenchmarkFsFileRead(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
b.StopTimer()
|
||||
|
||||
// Reset the read position back to the beginning of the file.
|
||||
if _, err = f.(io.Seeker).Seek(0, io.SeekStart); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
|
||||
fs := NewFsFile(name, syscall.O_RDONLY, f)
|
||||
|
||||
var n int
|
||||
var errno syscall.Errno
|
||||
|
||||
// Reset the read position back to the beginning of the file.
|
||||
if _, errno = fs.Seek(0, io.SeekStart); errno != 0 {
|
||||
b.Fatal(errno)
|
||||
}
|
||||
|
||||
b.StartTimer()
|
||||
if bc.pread {
|
||||
n, errno = fs.Pread(buf, 3)
|
||||
|
||||
@@ -21,9 +21,8 @@ func TestChown(t *testing.T) {
|
||||
dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
|
||||
defer dirF.Close()
|
||||
|
||||
dirStat, err := dirF.File().Stat()
|
||||
require.NoError(t, err)
|
||||
dirSys := dirStat.Sys().(*syscall.Stat_t)
|
||||
dirSt, errno := dirF.Stat()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// Similar to TestChown in os_unix_test.go, we can't expect to change
|
||||
// owner unless root, and with another user. Instead, test gid.
|
||||
@@ -32,13 +31,13 @@ func TestChown(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("-1 parameters means leave alone", func(t *testing.T) {
|
||||
require.Zero(t, Chown(dir, -1, -1))
|
||||
checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
|
||||
require.EqualErrno(t, 0, Chown(dir, -1, -1))
|
||||
checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
|
||||
})
|
||||
|
||||
t.Run("change gid, but not uid", func(t *testing.T) {
|
||||
require.Zero(t, Chown(dir, -1, gid))
|
||||
checkUidGid(t, dir, dirSys.Uid, uint32(gid))
|
||||
require.EqualErrno(t, 0, Chown(dir, -1, gid))
|
||||
checkUidGid(t, dir, dirSt.Uid, uint32(gid))
|
||||
})
|
||||
|
||||
// Now, try any other groups of the current user.
|
||||
@@ -46,12 +45,12 @@ func TestChown(t *testing.T) {
|
||||
g := g
|
||||
t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
|
||||
// Test using our Chown
|
||||
require.Zero(t, Chown(dir, -1, g))
|
||||
checkUidGid(t, dir, dirSys.Uid, uint32(g))
|
||||
require.EqualErrno(t, 0, Chown(dir, -1, g))
|
||||
checkUidGid(t, dir, dirSt.Uid, uint32(g))
|
||||
|
||||
// Revert back with os.File.Chown
|
||||
require.NoError(t, dirF.File().(*os.File).Chown(-1, gid))
|
||||
checkUidGid(t, dir, dirSys.Uid, uint32(gid))
|
||||
// Revert back
|
||||
require.EqualErrno(t, 0, dirF.Chown(-1, gid))
|
||||
checkUidGid(t, dir, dirSt.Uid, uint32(gid))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -69,10 +68,8 @@ func TestDefaultFileChown(t *testing.T) {
|
||||
dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
|
||||
defer dirF.Close()
|
||||
|
||||
dirStat, err := dirF.File().Stat()
|
||||
require.NoError(t, err)
|
||||
|
||||
dirSys := dirStat.Sys().(*syscall.Stat_t)
|
||||
dirSt, errno := dirF.Stat()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// Similar to TestChownFile in os_unix_test.go, we can't expect to change
|
||||
// owner unless root, and with another user. Instead, test gid.
|
||||
@@ -81,13 +78,13 @@ func TestDefaultFileChown(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("-1 parameters means leave alone", func(t *testing.T) {
|
||||
require.Zero(t, dirF.Chown(-1, -1))
|
||||
checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
|
||||
require.EqualErrno(t, 0, dirF.Chown(-1, -1))
|
||||
checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
|
||||
})
|
||||
|
||||
t.Run("change gid, but not uid", func(t *testing.T) {
|
||||
require.Zero(t, dirF.Chown(-1, gid))
|
||||
checkUidGid(t, dir, dirSys.Uid, uint32(gid))
|
||||
require.EqualErrno(t, 0, dirF.Chown(-1, gid))
|
||||
checkUidGid(t, dir, dirSt.Uid, uint32(gid))
|
||||
})
|
||||
|
||||
// Now, try any other groups of the current user.
|
||||
@@ -95,17 +92,17 @@ func TestDefaultFileChown(t *testing.T) {
|
||||
g := g
|
||||
t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
|
||||
// Test using our Chown
|
||||
require.Zero(t, dirF.Chown(-1, g))
|
||||
checkUidGid(t, dir, dirSys.Uid, uint32(g))
|
||||
require.EqualErrno(t, 0, dirF.Chown(-1, g))
|
||||
checkUidGid(t, dir, dirSt.Uid, uint32(g))
|
||||
|
||||
// Revert back with os.File.Chown
|
||||
require.NoError(t, dirF.File().(*os.File).Chown(-1, gid))
|
||||
checkUidGid(t, dir, dirSys.Uid, uint32(gid))
|
||||
// Revert back
|
||||
require.EqualErrno(t, 0, dirF.Chown(-1, gid))
|
||||
checkUidGid(t, dir, dirSt.Uid, uint32(gid))
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("closed", func(t *testing.T) {
|
||||
require.Zero(t, dirF.Close())
|
||||
require.EqualErrno(t, 0, dirF.Close())
|
||||
require.EqualErrno(t, syscall.EBADF, dirF.Chown(-1, gid))
|
||||
})
|
||||
}
|
||||
@@ -119,10 +116,8 @@ func TestLchown(t *testing.T) {
|
||||
dirF := openFsFile(t, dir, syscall.O_RDONLY, 0)
|
||||
defer dirF.Close()
|
||||
|
||||
dirStat, err := dirF.File().Stat()
|
||||
require.NoError(t, err)
|
||||
|
||||
dirSys := dirStat.Sys().(*syscall.Stat_t)
|
||||
dirSt, errno := dirF.Stat()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
link := path.Join(tmpDir, "link")
|
||||
require.NoError(t, os.Symlink(dir, link))
|
||||
@@ -130,10 +125,8 @@ func TestLchown(t *testing.T) {
|
||||
linkF := openFsFile(t, link, syscall.O_RDONLY, 0)
|
||||
defer linkF.Close()
|
||||
|
||||
linkStat, err := linkF.File().Stat()
|
||||
require.NoError(t, err)
|
||||
|
||||
linkSys := linkStat.Sys().(*syscall.Stat_t)
|
||||
linkSt, errno := linkF.Stat()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// Similar to TestLchown in os_unix_test.go, we can't expect to change
|
||||
// owner unless root, and with another user. Instead, test gid.
|
||||
@@ -142,15 +135,15 @@ func TestLchown(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("-1 parameters means leave alone", func(t *testing.T) {
|
||||
require.Zero(t, Lchown(link, -1, -1))
|
||||
checkUidGid(t, link, linkSys.Uid, linkSys.Gid)
|
||||
require.EqualErrno(t, 0, Lchown(link, -1, -1))
|
||||
checkUidGid(t, link, linkSt.Uid, linkSt.Gid)
|
||||
})
|
||||
|
||||
t.Run("change gid, but not uid", func(t *testing.T) {
|
||||
require.Zero(t, Chown(dir, -1, gid))
|
||||
checkUidGid(t, link, linkSys.Uid, uint32(gid))
|
||||
require.EqualErrno(t, 0, Chown(dir, -1, gid))
|
||||
checkUidGid(t, link, linkSt.Uid, uint32(gid))
|
||||
// Make sure the target didn't change.
|
||||
checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
|
||||
checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
|
||||
})
|
||||
|
||||
// Now, try any other groups of the current user.
|
||||
@@ -158,14 +151,14 @@ func TestLchown(t *testing.T) {
|
||||
g := g
|
||||
t.Run(fmt.Sprintf("change to gid %d", g), func(t *testing.T) {
|
||||
// Test using our Lchown
|
||||
require.Zero(t, Lchown(link, -1, g))
|
||||
checkUidGid(t, link, linkSys.Uid, uint32(g))
|
||||
require.EqualErrno(t, 0, Lchown(link, -1, g))
|
||||
checkUidGid(t, link, linkSt.Uid, uint32(g))
|
||||
// Make sure the target didn't change.
|
||||
checkUidGid(t, dir, dirSys.Uid, dirSys.Gid)
|
||||
checkUidGid(t, dir, dirSt.Uid, dirSt.Gid)
|
||||
|
||||
// Revert back with syscall.Lchown
|
||||
require.NoError(t, syscall.Lchown(link, -1, gid))
|
||||
checkUidGid(t, link, linkSys.Uid, uint32(gid))
|
||||
// Revert back
|
||||
require.EqualErrno(t, 0, Lchown(link, -1, gid))
|
||||
checkUidGid(t, link, linkSt.Uid, uint32(gid))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,19 @@ type File interface {
|
||||
// - Windows allows you to stat a closed directory.
|
||||
Stat() (Stat_t, syscall.Errno)
|
||||
|
||||
// IsDir returns true if this file is a directory or an error there was an
|
||||
// error retrieving this information.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// A zero syscall.Errno is success. The below are expected otherwise:
|
||||
// - syscall.ENOSYS: the implementation does not support this function.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - Some implementations implement this with a cached call to Stat.
|
||||
IsDir() (bool, syscall.Errno)
|
||||
|
||||
// Read attempts to read all bytes in the file into `p`, and returns the
|
||||
// count read even on error.
|
||||
//
|
||||
@@ -95,6 +108,33 @@ type File interface {
|
||||
// read the file completely, the caller must repeat until `n` is zero.
|
||||
Pread(p []byte, off int64) (n int, errno syscall.Errno)
|
||||
|
||||
// Seek attempts to set the next offset for Read or Write and returns the
|
||||
// resulting absolute offset or an error.
|
||||
//
|
||||
// # Parameters
|
||||
//
|
||||
// The `offset` parameters is interpreted in terms of `whence`:
|
||||
// - io.SeekStart: relative to the start of the file, e.g. offset=0 sets
|
||||
// the next Read or Write to the beginning of the file.
|
||||
// - io.SeekCurrent: relative to the current offset, e.g. offset=16 sets
|
||||
// the next Read or Write 16 bytes past the prior.
|
||||
// - io.SeekEnd: relative to the end of the file, e.g. offset=-1 sets the
|
||||
// next Read or Write to the last byte in the file.
|
||||
//
|
||||
// # Errors
|
||||
//
|
||||
// A zero syscall.Errno is success. The below are expected otherwise:
|
||||
// - syscall.ENOSYS: the implementation does not support this function.
|
||||
// - syscall.EBADF: the file or directory was closed or not readable.
|
||||
// - syscall.EINVAL: the offset was negative.
|
||||
// - syscall.EISDIR: the file was a directory.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
// - This is like io.Seeker and `fseek` in POSIX, preferring semantics
|
||||
// of io.Seeker. See https://pubs.opengroup.org/onlinepubs/9699919799/functions/fseek.html
|
||||
Seek(offset int64, whence int) (newOffset int64, errno syscall.Errno)
|
||||
|
||||
// Write attempts to write all bytes in `p` to the file, and returns the
|
||||
// count written even on error.
|
||||
//
|
||||
@@ -103,6 +143,7 @@ type File interface {
|
||||
// A zero syscall.Errno is success. The below are expected otherwise:
|
||||
// - syscall.ENOSYS: the implementation does not support this function.
|
||||
// - syscall.EBADF: the file or directory was closed or not writeable.
|
||||
// - syscall.EISDIR: the file was a directory.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
@@ -119,6 +160,7 @@ type File interface {
|
||||
// - syscall.ENOSYS: the implementation does not support this function.
|
||||
// - syscall.EBADF: the file or directory was closed or not writeable.
|
||||
// - syscall.EINVAL: the offset was negative.
|
||||
// - syscall.EISDIR: the file was a directory.
|
||||
//
|
||||
// # Notes
|
||||
//
|
||||
@@ -255,6 +297,11 @@ func (UnimplementedFile) Stat() (Stat_t, syscall.Errno) {
|
||||
return Stat_t{}, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// IsDir implements File.IsDir
|
||||
func (UnimplementedFile) IsDir() (bool, syscall.Errno) {
|
||||
return false, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Read implements File.Read
|
||||
func (UnimplementedFile) Read([]byte) (int, syscall.Errno) {
|
||||
return 0, syscall.ENOSYS
|
||||
@@ -265,6 +312,11 @@ func (UnimplementedFile) Pread([]byte, int64) (int, syscall.Errno) {
|
||||
return 0, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Seek implements File.Seek
|
||||
func (UnimplementedFile) Seek(int64, int) (int64, syscall.Errno) {
|
||||
return 0, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Write implements File.Write
|
||||
func (UnimplementedFile) Write([]byte) (int, syscall.Errno) {
|
||||
return 0, syscall.ENOSYS
|
||||
@@ -317,6 +369,25 @@ type fsFile struct {
|
||||
path string
|
||||
accessMode int
|
||||
file fs.File
|
||||
|
||||
// cachedStat includes fields that won't change while a file is open.
|
||||
cachedSt *cachedStat
|
||||
}
|
||||
|
||||
type cachedStat struct {
|
||||
// fileType is the same as what's documented on Dirent.
|
||||
fileType fs.FileMode
|
||||
}
|
||||
|
||||
// cachedStat returns the cacheable parts of platform.Stat_t or an error if
|
||||
// they couldn't be retrieved.
|
||||
func (f *fsFile) cachedStat() (fileType fs.FileMode, errno syscall.Errno) {
|
||||
if f.cachedSt == nil {
|
||||
if _, errno = f.Stat(); errno != 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
return f.cachedSt.fileType, 0
|
||||
}
|
||||
|
||||
// Path implements File.Path
|
||||
@@ -329,10 +400,23 @@ func (f *fsFile) AccessMode() int {
|
||||
return f.accessMode
|
||||
}
|
||||
|
||||
// IsDir implements File.IsDir
|
||||
func (f *fsFile) IsDir() (bool, syscall.Errno) {
|
||||
if ft, errno := f.cachedStat(); errno != 0 {
|
||||
return false, errno
|
||||
} else if ft.Type() == fs.ModeDir {
|
||||
return true, 0
|
||||
}
|
||||
return false, 0
|
||||
}
|
||||
|
||||
// Stat implements File.Stat
|
||||
func (f *fsFile) Stat() (Stat_t, syscall.Errno) {
|
||||
st, errno := statFile(f.file)
|
||||
if errno == syscall.EIO {
|
||||
switch errno {
|
||||
case 0:
|
||||
f.cachedSt = &cachedStat{fileType: st.Mode & fs.ModeType}
|
||||
case syscall.EIO:
|
||||
errno = syscall.EBADF
|
||||
}
|
||||
return st, errno
|
||||
@@ -344,9 +428,12 @@ func (f *fsFile) Read(p []byte) (n int, errno syscall.Errno) {
|
||||
return 0, 0 // less overhead on zero-length reads.
|
||||
}
|
||||
|
||||
if f.accessMode == syscall.O_WRONLY {
|
||||
if errno = f.isDirErrno(); errno != 0 {
|
||||
return
|
||||
} else if f.accessMode == syscall.O_WRONLY {
|
||||
return 0, syscall.EBADF
|
||||
}
|
||||
|
||||
if w, ok := f.File().(io.Reader); ok {
|
||||
n, err := w.Read(p)
|
||||
return n, UnwrapOSError(err)
|
||||
@@ -360,7 +447,9 @@ func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
|
||||
return 0, 0 // less overhead on zero-length reads.
|
||||
}
|
||||
|
||||
if f.accessMode == syscall.O_WRONLY {
|
||||
if errno = f.isDirErrno(); errno != 0 {
|
||||
return
|
||||
} else if f.accessMode == syscall.O_WRONLY {
|
||||
return 0, syscall.EBADF
|
||||
}
|
||||
|
||||
@@ -395,15 +484,32 @@ func (f *fsFile) Pread(p []byte, off int64) (n int, errno syscall.Errno) {
|
||||
return 0, syscall.ENOSYS // unsupported
|
||||
}
|
||||
|
||||
// Write implements File.Write
|
||||
func (f *fsFile) Write(p []byte) (n int, errno syscall.Errno) {
|
||||
if len(p) == 0 {
|
||||
return 0, 0 // less overhead on zero-length writes.
|
||||
// Seek implements File.Seek
|
||||
func (f *fsFile) Seek(offset int64, whence int) (int64, syscall.Errno) {
|
||||
if errno := f.isDirErrno(); errno != 0 {
|
||||
return 0, errno
|
||||
} else if uint(whence) > io.SeekEnd {
|
||||
return 0, syscall.EINVAL // negative or exceeds the largest valid whence
|
||||
}
|
||||
|
||||
if f.accessMode == syscall.O_RDONLY {
|
||||
if seeker, ok := f.file.(io.Seeker); ok {
|
||||
newOffset, err := seeker.Seek(offset, whence)
|
||||
return newOffset, UnwrapOSError(err)
|
||||
}
|
||||
return 0, syscall.ENOSYS
|
||||
}
|
||||
|
||||
// Write implements File.Write
|
||||
func (f *fsFile) Write(p []byte) (n int, errno syscall.Errno) {
|
||||
if errno = f.isDirErrno(); errno != 0 {
|
||||
return
|
||||
} else if f.accessMode == syscall.O_RDONLY {
|
||||
return 0, syscall.EBADF
|
||||
}
|
||||
|
||||
if len(p) == 0 {
|
||||
return 0, 0 // less overhead on zero-length writes.
|
||||
}
|
||||
if w, ok := f.File().(io.Writer); ok {
|
||||
n, err := w.Write(p)
|
||||
return n, UnwrapOSError(err)
|
||||
@@ -413,13 +519,16 @@ func (f *fsFile) Write(p []byte) (n int, errno syscall.Errno) {
|
||||
|
||||
// Pwrite implements File.Pwrite
|
||||
func (f *fsFile) Pwrite(p []byte, off int64) (n int, errno syscall.Errno) {
|
||||
if errno = f.isDirErrno(); errno != 0 {
|
||||
return
|
||||
} else if f.accessMode == syscall.O_RDONLY {
|
||||
return 0, syscall.EBADF
|
||||
}
|
||||
|
||||
if len(p) == 0 {
|
||||
return 0, 0 // less overhead on zero-length writes.
|
||||
}
|
||||
|
||||
if f.accessMode == syscall.O_RDONLY {
|
||||
return 0, syscall.EBADF
|
||||
}
|
||||
if w, ok := f.File().(io.WriterAt); ok {
|
||||
n, err := w.WriteAt(p, off)
|
||||
return n, UnwrapOSError(err)
|
||||
@@ -429,22 +538,29 @@ func (f *fsFile) Pwrite(p []byte, off int64) (n int, errno syscall.Errno) {
|
||||
|
||||
// Truncate implements File.Truncate
|
||||
func (f *fsFile) Truncate(size int64) syscall.Errno {
|
||||
if tf, ok := f.file.(truncateFile); ok {
|
||||
errno := UnwrapOSError(tf.Truncate(size))
|
||||
if errno == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
// Operating systems return different syscall.Errno instead of EISDIR
|
||||
// double-check on any err until we can assure this per OS.
|
||||
if isOpenDir(f) {
|
||||
return syscall.EISDIR
|
||||
}
|
||||
if errno := f.isDirErrno(); errno != 0 {
|
||||
return errno
|
||||
} else if f.accessMode == syscall.O_RDONLY {
|
||||
return syscall.EBADF
|
||||
}
|
||||
|
||||
if tf, ok := f.file.(truncateFile); ok {
|
||||
return UnwrapOSError(tf.Truncate(size))
|
||||
}
|
||||
return syscall.ENOSYS
|
||||
}
|
||||
|
||||
// isDirErrno returns syscall.EISDIR, if the file is a directory, or any error
|
||||
// calling IsDir.
|
||||
func (f *fsFile) isDirErrno() syscall.Errno {
|
||||
if isDir, errno := f.IsDir(); errno != 0 {
|
||||
return errno
|
||||
} else if isDir {
|
||||
return syscall.EISDIR
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Sync implements File.Sync
|
||||
func (f *fsFile) Sync() syscall.Errno {
|
||||
return sync(f.file)
|
||||
@@ -533,10 +649,3 @@ type (
|
||||
// truncateFile is implemented by os.File in file_posix.go
|
||||
truncateFile interface{ Truncate(size int64) error }
|
||||
)
|
||||
|
||||
func isOpenDir(f File) bool {
|
||||
if st, statErrno := f.Stat(); statErrno == 0 && st.Mode.IsDir() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -45,28 +45,57 @@ var embedFS embed.FS
|
||||
var (
|
||||
//go:embed testdata
|
||||
testdata embed.FS
|
||||
preadFile = "wazero.txt"
|
||||
readFile = "wazero.txt"
|
||||
emptyFile = "empty.txt"
|
||||
)
|
||||
|
||||
func TestFsFileIsDir(t *testing.T) {
|
||||
dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fs fs.FS
|
||||
}{
|
||||
{name: "os.DirFS", fs: dirFS},
|
||||
{name: "embed.FS", fs: embedFS},
|
||||
{name: "fstest.MapFS", fs: mapFS},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("file", func(t *testing.T) {
|
||||
f, err := tc.fs.Open(readFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
fsF := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
isDir, errno := fsF.IsDir()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
require.False(t, isDir)
|
||||
require.Equal(t, &cachedStat{fileType: 0}, fsF.(*fsFile).cachedSt)
|
||||
})
|
||||
|
||||
t.Run("dir", func(t *testing.T) {
|
||||
f, err := tc.fs.Open(".")
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
fsF := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
isDir, errno := fsF.IsDir()
|
||||
require.EqualErrno(t, 0, errno)
|
||||
require.True(t, isDir)
|
||||
require.Equal(t, &cachedStat{fileType: fs.ModeDir}, fsF.(*fsFile).cachedSt)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsFileReadAndPread(t *testing.T) {
|
||||
embedFS, err := fs.Sub(testdata, "testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := embedFS.Open(preadFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
bytes, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
mapFS := gofstest.MapFS{preadFile: &gofstest.MapFile{Data: bytes}}
|
||||
|
||||
// Write a file as can't open "testdata" in scratch tests because they
|
||||
// can't read the original filesystem.
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, preadFile), bytes, 0o600))
|
||||
dirFS := os.DirFS(tmpDir)
|
||||
dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -83,11 +112,11 @@ func TestFsFileReadAndPread(t *testing.T) {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, err := tc.fs.Open(preadFile)
|
||||
f, err := tc.fs.Open(readFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
fs := NewFsFile(preadFile, syscall.O_RDONLY, f)
|
||||
fs := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
// The file should be readable (base case)
|
||||
requireRead(t, fs, buf)
|
||||
@@ -124,20 +153,7 @@ func requirePread(t *testing.T, f File, buf []byte, off int64) {
|
||||
}
|
||||
|
||||
func TestFsFileRead_empty(t *testing.T) {
|
||||
embedFS, err := fs.Sub(testdata, "testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := embedFS.Open(preadFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
mapFS := gofstest.MapFS{emptyFile: &gofstest.MapFile{}}
|
||||
|
||||
// Write a file as can't open "testdata" in scratch tests because they
|
||||
// can't read the original filesystem.
|
||||
tmpDir := t.TempDir()
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, emptyFile), []byte{}, 0o600))
|
||||
dirFS := os.DirFS(tmpDir)
|
||||
dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
@@ -158,7 +174,7 @@ func TestFsFileRead_empty(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
fs := NewFsFile(preadFile, syscall.O_RDONLY, f)
|
||||
fs := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
t.Run("Read", func(t *testing.T) {
|
||||
// We should be able to read an empty file
|
||||
@@ -187,17 +203,190 @@ func TestFsFilePread_Unsupported(t *testing.T) {
|
||||
// mask both io.ReaderAt and io.Seeker
|
||||
f = struct{ fs.File }{f}
|
||||
|
||||
fs := NewFsFile(preadFile, syscall.O_RDONLY, f)
|
||||
fs := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
buf := make([]byte, 3)
|
||||
_, errno := fs.Pread(buf, 0)
|
||||
require.EqualErrno(t, syscall.ENOSYS, errno)
|
||||
}
|
||||
|
||||
func TestFsFileRead_Errors(t *testing.T) {
|
||||
// Create the file
|
||||
path := path.Join(t.TempDir(), emptyFile)
|
||||
of, err := os.Create(path)
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, of.Close())
|
||||
|
||||
// Open the file write-only
|
||||
flag := syscall.O_WRONLY
|
||||
f := openFsFile(t, path, flag, 0o600)
|
||||
defer f.Close()
|
||||
buf := make([]byte, 5)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(File) syscall.Errno
|
||||
}{
|
||||
{name: "Read", fn: func(f File) syscall.Errno {
|
||||
_, errno := f.Read(buf)
|
||||
return errno
|
||||
}},
|
||||
{name: "Pread", fn: func(f File) syscall.Errno {
|
||||
_, errno := f.Pread(buf, 0)
|
||||
return errno
|
||||
}},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Run("EBADF when not open for reading", func(t *testing.T) {
|
||||
// The descriptor exists, but not open for reading
|
||||
errno := tc.fn(f)
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
})
|
||||
testEISDIR(t, tc.fn)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsFileSeek(t *testing.T) {
|
||||
dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fs fs.FS
|
||||
}{
|
||||
{name: "os.DirFS", fs: dirFS},
|
||||
{name: "embed.FS", fs: embedFS},
|
||||
{name: "fstest.MapFS", fs: mapFS},
|
||||
}
|
||||
|
||||
buf := make([]byte, 3)
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, err := tc.fs.Open(readFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
fs := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
// Shouldn't be able to use an invalid whence
|
||||
_, errno := fs.Seek(0, io.SeekEnd+1)
|
||||
require.EqualErrno(t, syscall.EINVAL, errno)
|
||||
_, errno = fs.Seek(0, -1)
|
||||
require.EqualErrno(t, syscall.EINVAL, errno)
|
||||
|
||||
// Shouldn't be able to seek before the file starts.
|
||||
_, errno = fs.Seek(-1, io.SeekStart)
|
||||
require.EqualErrno(t, syscall.EINVAL, errno)
|
||||
|
||||
requireRead(t, fs, buf) // read 3 bytes
|
||||
|
||||
// Seek to the start
|
||||
newOffset, errno := fs.Seek(0, io.SeekStart)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// verify we can re-read from the beginning now.
|
||||
require.Zero(t, newOffset)
|
||||
requireRead(t, fs, buf) // read 3 bytes again
|
||||
require.Equal(t, "waz", string(buf))
|
||||
buf = buf[:]
|
||||
|
||||
// Seek to the start with zero allows you to read it back.
|
||||
newOffset, errno = fs.Seek(0, io.SeekCurrent)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
require.Equal(t, int64(3), newOffset)
|
||||
|
||||
// Seek to the last two bytes
|
||||
newOffset, errno = fs.Seek(-2, io.SeekEnd)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// verify we can read the last two bytes
|
||||
require.Equal(t, int64(5), newOffset)
|
||||
n, errno := fs.Read(buf)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
require.Equal(t, 2, n)
|
||||
require.Equal(t, "o\n", string(buf[:2]))
|
||||
})
|
||||
}
|
||||
|
||||
seekToZero := func(f File) syscall.Errno {
|
||||
_, errno := f.Seek(0, io.SeekStart)
|
||||
return errno
|
||||
}
|
||||
testEBADFIfFileClosed(t, seekToZero)
|
||||
testEISDIR(t, seekToZero)
|
||||
}
|
||||
|
||||
func requireSeek(t *testing.T, f File, off int64, whence int) int64 {
|
||||
n, errno := f.Seek(off, whence)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
return n
|
||||
}
|
||||
|
||||
func TestFsFileSeek_empty(t *testing.T) {
|
||||
dirFS, embedFS, mapFS := dirEmbedMapFS(t, t.TempDir())
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fs fs.FS
|
||||
}{
|
||||
{name: "os.DirFS", fs: dirFS},
|
||||
{name: "embed.FS", fs: embedFS},
|
||||
{name: "fstest.MapFS", fs: mapFS},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
f, err := tc.fs.Open(emptyFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
fs := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
t.Run("Start", func(t *testing.T) {
|
||||
require.Zero(t, requireSeek(t, fs, 0, io.SeekStart))
|
||||
})
|
||||
|
||||
t.Run("Current", func(t *testing.T) {
|
||||
require.Zero(t, requireSeek(t, fs, 0, io.SeekCurrent))
|
||||
})
|
||||
|
||||
t.Run("End", func(t *testing.T) {
|
||||
require.Zero(t, requireSeek(t, fs, 0, io.SeekEnd))
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsFileSeek_Unsupported(t *testing.T) {
|
||||
embedFS, err := fs.Sub(testdata, "testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := embedFS.Open(emptyFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
// mask io.Seeker
|
||||
f = struct{ fs.File }{f}
|
||||
|
||||
fs := NewFsFile(readFile, syscall.O_RDONLY, f)
|
||||
|
||||
_, errno := fs.Seek(0, io.SeekCurrent)
|
||||
require.EqualErrno(t, syscall.ENOSYS, errno)
|
||||
}
|
||||
|
||||
func TestFsFileWriteAndPwrite(t *testing.T) {
|
||||
// fs.FS doesn't support writes, and there is no other built-in
|
||||
// implementation except os.File.
|
||||
path := path.Join(t.TempDir(), preadFile)
|
||||
path := path.Join(t.TempDir(), readFile)
|
||||
f := openFsFile(t, path, syscall.O_RDWR|os.O_CREATE, 0o600)
|
||||
defer f.Close()
|
||||
|
||||
@@ -291,7 +480,7 @@ func TestFsFileWrite_Unsupported(t *testing.T) {
|
||||
embedFS, err := fs.Sub(testdata, "testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := embedFS.Open(preadFile)
|
||||
f, err := embedFS.Open(readFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
@@ -314,14 +503,14 @@ func TestFsFileWrite_Unsupported(t *testing.T) {
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
// Use syscall.O_RDWR so that it fails due to type not flags
|
||||
f := NewFsFile(preadFile, syscall.O_RDWR, f)
|
||||
f := NewFsFile(readFile, syscall.O_RDWR, f)
|
||||
_, errno := tc.fn(f, buf)
|
||||
require.EqualErrno(t, syscall.ENOSYS, errno)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFsFileWrite_BadFile(t *testing.T) {
|
||||
func TestFsFileWrite_Errors(t *testing.T) {
|
||||
// Create the file
|
||||
path := path.Join(t.TempDir(), emptyFile)
|
||||
of, err := os.Create(path)
|
||||
@@ -332,29 +521,32 @@ func TestFsFileWrite_BadFile(t *testing.T) {
|
||||
flag := syscall.O_RDONLY
|
||||
f := openFsFile(t, path, flag, 0o600)
|
||||
defer f.Close()
|
||||
buf := []byte("wazero")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
fn func(File, []byte) (int, syscall.Errno)
|
||||
fn func(File) syscall.Errno
|
||||
}{
|
||||
{name: "Write", fn: func(f File, buf []byte) (int, syscall.Errno) {
|
||||
return f.Write(buf)
|
||||
{name: "Write", fn: func(f File) syscall.Errno {
|
||||
_, errno := f.Write(buf)
|
||||
return errno
|
||||
}},
|
||||
{name: "Pwrite", fn: func(f File, buf []byte) (int, syscall.Errno) {
|
||||
return f.Pwrite(buf, 0)
|
||||
{name: "Pwrite", fn: func(f File) syscall.Errno {
|
||||
_, errno := f.Pwrite(buf, 0)
|
||||
return errno
|
||||
}},
|
||||
}
|
||||
|
||||
buf := []byte("wazero")
|
||||
|
||||
for _, tc := range tests {
|
||||
tc := tc
|
||||
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, errno := tc.fn(f, buf)
|
||||
|
||||
// The descriptor exists, but not open for writing
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
t.Run("EBADF when not open for writing", func(t *testing.T) {
|
||||
// The descriptor exists, but not open for writing
|
||||
errno := tc.fn(f)
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
})
|
||||
testEISDIR(t, tc.fn)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -400,7 +592,7 @@ func testSync_NoError(t *testing.T, sync func(File) syscall.Errno) {
|
||||
tc := tt
|
||||
|
||||
t.Run(tc.name, func(b *testing.T) {
|
||||
require.Zero(t, sync(tc.f))
|
||||
require.EqualErrno(t, 0, sync(tc.f))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -442,8 +634,8 @@ func testSync(t *testing.T, sync func(File) syscall.Errno) {
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// Rewind while the file is still open.
|
||||
_, err = f.File().(io.Seeker).Seek(0, io.SeekStart)
|
||||
require.NoError(t, err)
|
||||
_, errno = f.Seek(0, io.SeekStart)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
// Read data from the file
|
||||
buf := make([]byte, 50)
|
||||
@@ -513,8 +705,7 @@ func TestFsFileTruncate(t *testing.T) {
|
||||
}
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
// TODO: os.Truncate on windows can create the file even when it
|
||||
// doesn't exist.
|
||||
// TODO: os.Truncate on windows passes even when closed
|
||||
testEBADFIfFileClosed(t, truncateToZero)
|
||||
}
|
||||
|
||||
@@ -558,7 +749,7 @@ func testEBADFIfDirClosed(t *testing.T, fn func(File) syscall.Errno) bool {
|
||||
d := openFsFile(t, t.TempDir(), syscall.O_RDONLY, 0o755)
|
||||
|
||||
// close the directory underneath
|
||||
require.Zero(t, d.Close())
|
||||
require.EqualErrno(t, 0, d.Close())
|
||||
|
||||
require.EqualErrno(t, syscall.EBADF, fn(d))
|
||||
})
|
||||
@@ -571,7 +762,7 @@ func testEBADFIfFileClosed(t *testing.T, fn func(File) syscall.Errno) bool {
|
||||
f := openForWrite(t, path.Join(tmpDir, "EBADF"), []byte{1, 2, 3, 4})
|
||||
|
||||
// close the file underneath
|
||||
require.Zero(t, f.Close())
|
||||
require.EqualErrno(t, 0, f.Close())
|
||||
|
||||
require.EqualErrno(t, syscall.EBADF, fn(f))
|
||||
})
|
||||
@@ -596,3 +787,27 @@ func openFsFile(t *testing.T, path string, flag int, perm fs.FileMode) File {
|
||||
require.EqualErrno(t, 0, errno)
|
||||
return NewFsFile(path, flag, f)
|
||||
}
|
||||
|
||||
func dirEmbedMapFS(t *testing.T, tmpDir string) (fs.FS, fs.FS, fs.FS) {
|
||||
embedFS, err := fs.Sub(testdata, "testdata")
|
||||
require.NoError(t, err)
|
||||
|
||||
f, err := embedFS.Open(readFile)
|
||||
require.NoError(t, err)
|
||||
defer f.Close()
|
||||
|
||||
bytes, err := io.ReadAll(f)
|
||||
require.NoError(t, err)
|
||||
|
||||
mapFS := gofstest.MapFS{
|
||||
emptyFile: &gofstest.MapFile{},
|
||||
readFile: &gofstest.MapFile{Data: bytes},
|
||||
}
|
||||
|
||||
// Write a file as can't open "testdata" in scratch tests because they
|
||||
// can't read the original filesystem.
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, emptyFile), nil, 0o600))
|
||||
require.NoError(t, os.WriteFile(path.Join(tmpDir, readFile), bytes, 0o600))
|
||||
dirFS := os.DirFS(tmpDir)
|
||||
return dirFS, embedFS, mapFS
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ func testUtimens(t *testing.T, futimes bool) {
|
||||
f := openFsFile(t, path, flag, 0)
|
||||
|
||||
errno = f.Utimens(tc.times)
|
||||
require.Zero(t, f.Close())
|
||||
require.EqualErrno(t, 0, f.Close())
|
||||
require.EqualErrno(t, 0, errno)
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ func TestOpenFile_Errors(t *testing.T) {
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
})
|
||||
|
||||
t.Run("writing to a directory is EBADF", func(t *testing.T) {
|
||||
t.Run("writing to a directory is EISDIR", func(t *testing.T) {
|
||||
path := path.Join(tmpDir, "diragain")
|
||||
require.NoError(t, os.Mkdir(path, 0o755))
|
||||
|
||||
@@ -84,7 +84,7 @@ func TestOpenFile_Errors(t *testing.T) {
|
||||
defer f.Close()
|
||||
|
||||
_, errno := f.Write([]byte{1, 2, 3, 4})
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
require.EqualErrno(t, syscall.EISDIR, errno)
|
||||
})
|
||||
|
||||
// This is similar to https://github.com/WebAssembly/wasi-testsuite/blob/dc7f8d27be1030cd4788ebdf07d9b57e5d23441e/tests/rust/src/bin/dangling_symlink.rs
|
||||
|
||||
@@ -173,7 +173,7 @@ func TestStatFile(t *testing.T) {
|
||||
// not by file descriptor.
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Run("closed dir", func(t *testing.T) {
|
||||
require.Zero(t, tmpDirF.Close())
|
||||
require.EqualErrno(t, 0, tmpDirF.Close())
|
||||
_, errno := tmpDirF.Stat()
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
})
|
||||
@@ -193,7 +193,7 @@ func TestStatFile(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("closed fsFile", func(t *testing.T) {
|
||||
require.Zero(t, fileF.Close())
|
||||
require.EqualErrno(t, 0, fileF.Close())
|
||||
_, errno := fileF.Stat()
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
})
|
||||
@@ -213,7 +213,7 @@ func TestStatFile(t *testing.T) {
|
||||
|
||||
if runtime.GOOS != "windows" { // windows allows you to stat a closed dir
|
||||
t.Run("closed subdir", func(t *testing.T) {
|
||||
require.Zero(t, subdirF.Close())
|
||||
require.EqualErrno(t, 0, subdirF.Close())
|
||||
_, errno := subdirF.Stat()
|
||||
require.EqualErrno(t, syscall.EBADF, errno)
|
||||
})
|
||||
@@ -318,12 +318,12 @@ func TestStatFile_dev_inode(t *testing.T) {
|
||||
|
||||
// On Windows, we cannot rename while opening.
|
||||
// So we manually close here before renaming.
|
||||
require.Zero(t, f1.Close())
|
||||
require.Zero(t, f2.Close())
|
||||
require.Zero(t, l2.Close())
|
||||
require.EqualErrno(t, 0, f1.Close())
|
||||
require.EqualErrno(t, 0, f2.Close())
|
||||
require.EqualErrno(t, 0, l2.Close())
|
||||
|
||||
// Renaming a file shouldn't change its inodes.
|
||||
require.Zero(t, Rename(path1, path2))
|
||||
require.EqualErrno(t, 0, Rename(path1, path2))
|
||||
f1 = openFsFile(t, path2, os.O_RDONLY, 0)
|
||||
defer f1.Close()
|
||||
|
||||
@@ -361,7 +361,7 @@ func TestStat_uid_gid(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
dir := path.Join(tmpDir, "dir")
|
||||
require.NoError(t, os.Mkdir(dir, 0o0700))
|
||||
require.Zero(t, chgid(dir, gid))
|
||||
require.EqualErrno(t, 0, chgid(dir, gid))
|
||||
|
||||
st, errno := Stat(dir)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
@@ -374,7 +374,7 @@ func TestStat_uid_gid(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
link := path.Join(tmpDir, "link")
|
||||
require.NoError(t, os.Symlink(tmpDir, link))
|
||||
require.Zero(t, chgid(link, gid))
|
||||
require.EqualErrno(t, 0, chgid(link, gid))
|
||||
|
||||
st, errno := Lstat(link)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
@@ -387,7 +387,7 @@ func TestStat_uid_gid(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
file := path.Join(tmpDir, "file")
|
||||
require.NoError(t, os.WriteFile(file, nil, 0o0600))
|
||||
require.Zero(t, chgid(file, gid))
|
||||
require.EqualErrno(t, 0, chgid(file, gid))
|
||||
|
||||
st, errno := Lstat(file)
|
||||
require.EqualErrno(t, 0, errno)
|
||||
|
||||
@@ -51,7 +51,7 @@ func TestUnlink(t *testing.T) {
|
||||
|
||||
require.NoError(t, os.WriteFile(name, []byte{}, 0o600))
|
||||
|
||||
require.Zero(t, Unlink(name))
|
||||
require.EqualErrno(t, 0, Unlink(name))
|
||||
_, err := os.Stat(name)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user