746 lines
19 KiB
Go
746 lines
19 KiB
Go
package sysfs
|
|
|
|
import (
|
|
"bytes"
|
|
"embed"
|
|
_ "embed"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"runtime"
|
|
"sort"
|
|
"syscall"
|
|
"testing"
|
|
gofstest "testing/fstest"
|
|
|
|
"github.com/tetratelabs/wazero/internal/platform"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
)
|
|
|
|
func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS FS) {
|
|
file := "file"
|
|
realPath := path.Join(tmpDir, file)
|
|
err := os.WriteFile(realPath, []byte{}, 0o600)
|
|
require.NoError(t, err)
|
|
|
|
f, errno := testFS.OpenFile(file, os.O_RDWR, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
w, ok := f.File().(io.Writer)
|
|
require.True(t, ok)
|
|
|
|
// If the write flag was honored, we should be able to write!
|
|
fileContents := []byte{1, 2, 3, 4}
|
|
n, err := w.Write(fileContents)
|
|
require.NoError(t, err)
|
|
require.Equal(t, len(fileContents), n)
|
|
|
|
// Verify the contents actually wrote.
|
|
b, err := os.ReadFile(realPath)
|
|
require.NoError(t, err)
|
|
require.Equal(t, fileContents, b)
|
|
|
|
require.Zero(t, f.Close())
|
|
|
|
// re-create as read-only, using 0444 to allow read-back on windows.
|
|
require.NoError(t, os.Remove(realPath))
|
|
f, errno = testFS.OpenFile(file, os.O_RDONLY|os.O_CREATE, 0o444)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
w, ok = f.File().(io.Writer)
|
|
require.True(t, ok)
|
|
|
|
if runtime.GOOS != "windows" {
|
|
// If the read-only flag was honored, we should not be able to write!
|
|
_, err = w.Write(fileContents)
|
|
require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err))
|
|
}
|
|
|
|
// Verify stat on the file
|
|
stat, errno := f.Stat()
|
|
require.Zero(t, errno)
|
|
require.Equal(t, fs.FileMode(0o444), stat.Mode.Perm())
|
|
|
|
// from os.TestDirFSPathsValid
|
|
if runtime.GOOS != "windows" {
|
|
t.Run("strange name", func(t *testing.T) {
|
|
f, errno = testFS.OpenFile(`e:xperi\ment.txt`, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
_, errno = f.Stat()
|
|
require.Zero(t, errno)
|
|
})
|
|
}
|
|
}
|
|
|
|
func testOpen_Read(t *testing.T, testFS FS, expectIno bool) {
|
|
t.Run("doesn't exist", func(t *testing.T) {
|
|
_, errno := testFS.OpenFile("nope", os.O_RDONLY, 0)
|
|
|
|
// We currently follow os.Open not syscall.Open, so the error is wrapped.
|
|
require.EqualErrno(t, syscall.ENOENT, errno)
|
|
})
|
|
|
|
t.Run("readdir . opens root", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile(".", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
dirents := requireReaddir(t, f.File(), -1, expectIno)
|
|
|
|
require.Equal(t, []*platform.Dirent{
|
|
{Name: "animals.txt", Type: 0},
|
|
{Name: "dir", Type: fs.ModeDir},
|
|
{Name: "empty.txt", Type: 0},
|
|
{Name: "emptydir", Type: fs.ModeDir},
|
|
{Name: "sub", Type: fs.ModeDir},
|
|
}, dirents)
|
|
})
|
|
|
|
t.Run("readdirnames . opens root", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile(".", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
names := requireReaddirnames(t, f.File(), -1)
|
|
require.Equal(t, []string{"animals.txt", "dir", "empty.txt", "emptydir", "sub"}, names)
|
|
})
|
|
|
|
t.Run("readdir empty", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("emptydir", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
entries := requireReaddir(t, f.File(), -1, expectIno)
|
|
require.Zero(t, len(entries))
|
|
})
|
|
|
|
t.Run("readdirnames empty", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("emptydir", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
names := requireReaddirnames(t, f.File(), -1)
|
|
require.Zero(t, len(names))
|
|
})
|
|
|
|
t.Run("readdir partial", func(t *testing.T) {
|
|
dirF, errno := testFS.OpenFile("dir", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer dirF.Close()
|
|
|
|
dirents1, errno := platform.Readdir(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, 1, len(dirents1))
|
|
|
|
dirents2, errno := platform.Readdir(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, 1, len(dirents2))
|
|
|
|
// read exactly the last entry
|
|
dirents3, errno := platform.Readdir(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, 1, len(dirents3))
|
|
|
|
dirents := []*platform.Dirent{dirents1[0], dirents2[0], dirents3[0]}
|
|
sort.Slice(dirents, func(i, j int) bool { return dirents[i].Name < dirents[j].Name })
|
|
|
|
requireIno(t, dirents, expectIno)
|
|
|
|
require.Equal(t, []*platform.Dirent{
|
|
{Name: "-", Type: 0},
|
|
{Name: "a-", Type: fs.ModeDir},
|
|
{Name: "ab-", Type: 0},
|
|
}, dirents)
|
|
|
|
// no error reading an exhausted directory
|
|
_, errno = platform.Readdir(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
})
|
|
|
|
// TODO: consolidate duplicated tests from platform once we have our own
|
|
// file type
|
|
t.Run("readdirnames partial", func(t *testing.T) {
|
|
dirF, errno := testFS.OpenFile("dir", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer dirF.Close()
|
|
|
|
names1, errno := platform.Readdirnames(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, 1, len(names1))
|
|
|
|
names2, errno := platform.Readdirnames(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, 1, len(names2))
|
|
|
|
// read exactly the last entry
|
|
names3, errno := platform.Readdirnames(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, 1, len(names3))
|
|
|
|
names := []string{names1[0], names2[0], names3[0]}
|
|
sort.Strings(names)
|
|
|
|
require.Equal(t, []string{"-", "a-", "ab-"}, names)
|
|
|
|
// no error reading an exhausted directory
|
|
_, errno = platform.Readdirnames(dirF.File(), 1)
|
|
require.Zero(t, errno)
|
|
})
|
|
|
|
t.Run("file exists", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
fileContents := []byte(`bear
|
|
cat
|
|
shark
|
|
dinosaur
|
|
human
|
|
`)
|
|
// Ensure it implements io.ReaderAt
|
|
r, ok := f.File().(io.ReaderAt)
|
|
require.True(t, ok)
|
|
lenToRead := len(fileContents) - 1
|
|
buf := make([]byte, lenToRead)
|
|
n, err := r.ReadAt(buf, 1)
|
|
require.NoError(t, err)
|
|
require.Equal(t, lenToRead, n)
|
|
require.Equal(t, fileContents[1:], buf)
|
|
|
|
// Ensure it implements io.Seeker
|
|
s, ok := f.File().(io.Seeker)
|
|
require.True(t, ok)
|
|
offset, err := s.Seek(1, io.SeekStart)
|
|
require.NoError(t, err)
|
|
require.Equal(t, int64(1), offset)
|
|
b, err := io.ReadAll(f.File())
|
|
require.NoError(t, err)
|
|
require.Equal(t, fileContents[1:], b)
|
|
})
|
|
|
|
// Make sure O_RDONLY isn't treated bitwise as it is usually zero.
|
|
t.Run("or'd flag", func(t *testing.T) {
|
|
// Example of a flag that can be or'd into O_RDONLY even if not
|
|
// currently supported in WASI or GOOS=js
|
|
const O_NOATIME = 0x40000
|
|
|
|
f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY|O_NOATIME, 0)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
})
|
|
|
|
t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("animals.txt", os.O_RDONLY, 0)
|
|
defer require.Zero(t, f.Close())
|
|
require.Zero(t, errno)
|
|
|
|
if w, ok := f.File().(io.Writer); ok {
|
|
_, err := w.Write([]byte{1, 2, 3, 4})
|
|
require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err))
|
|
} else {
|
|
t.Skip("not an io.Writer")
|
|
}
|
|
})
|
|
|
|
t.Run("writing to a directory is EBADF", func(t *testing.T) {
|
|
f, errno := testFS.OpenFile("sub", os.O_RDONLY, 0)
|
|
defer require.Zero(t, f.Close())
|
|
require.Zero(t, errno)
|
|
|
|
if w, ok := f.File().(io.Writer); ok {
|
|
_, err := w.Write([]byte{1, 2, 3, 4})
|
|
require.EqualErrno(t, syscall.EBADF, platform.UnwrapOSError(err))
|
|
} else {
|
|
t.Skip("not an io.Writer")
|
|
}
|
|
})
|
|
}
|
|
|
|
func testLstat(t *testing.T, testFS FS) {
|
|
_, errno := testFS.Lstat("cat")
|
|
require.EqualErrno(t, syscall.ENOENT, errno)
|
|
_, errno = testFS.Lstat("sub/cat")
|
|
require.EqualErrno(t, syscall.ENOENT, errno)
|
|
|
|
var st platform.Stat_t
|
|
|
|
t.Run("dir", func(t *testing.T) {
|
|
st, errno = testFS.Lstat(".")
|
|
require.Zero(t, errno)
|
|
require.True(t, st.Mode.IsDir())
|
|
require.NotEqual(t, uint64(0), st.Ino)
|
|
})
|
|
|
|
var stFile platform.Stat_t
|
|
|
|
t.Run("file", func(t *testing.T) {
|
|
stFile, errno = testFS.Lstat("animals.txt")
|
|
require.Zero(t, errno)
|
|
|
|
require.Zero(t, stFile.Mode.Type())
|
|
require.Equal(t, int64(30), stFile.Size)
|
|
require.NotEqual(t, uint64(0), st.Ino)
|
|
})
|
|
|
|
t.Run("link to file", func(t *testing.T) {
|
|
requireLinkStat(t, testFS, "animals.txt", stFile)
|
|
})
|
|
|
|
var stSubdir platform.Stat_t
|
|
t.Run("subdir", func(t *testing.T) {
|
|
stSubdir, errno = testFS.Lstat("sub")
|
|
require.Zero(t, errno)
|
|
|
|
require.True(t, stSubdir.Mode.IsDir())
|
|
require.NotEqual(t, uint64(0), st.Ino)
|
|
})
|
|
|
|
t.Run("link to dir", func(t *testing.T) {
|
|
requireLinkStat(t, testFS, "sub", stSubdir)
|
|
})
|
|
|
|
t.Run("link to dir link", func(t *testing.T) {
|
|
pathLink := "sub-link"
|
|
stLink, errno := testFS.Lstat(pathLink)
|
|
require.Zero(t, errno)
|
|
|
|
requireLinkStat(t, testFS, pathLink, stLink)
|
|
})
|
|
}
|
|
|
|
func requireLinkStat(t *testing.T, testFS FS, path string, stat platform.Stat_t) {
|
|
link := path + "-link"
|
|
stLink, errno := testFS.Lstat(link)
|
|
require.Zero(t, errno)
|
|
|
|
require.NotEqual(t, stat.Ino, stLink.Ino) // inodes are not equal
|
|
require.Equal(t, fs.ModeSymlink, stLink.Mode.Type())
|
|
// From https://linux.die.net/man/2/lstat:
|
|
// The size of a symbolic link is the length of the pathname it
|
|
// contains, without a terminating null byte.
|
|
if runtime.GOOS == "windows" { // size is zero, not the path length
|
|
require.Zero(t, stLink.Size)
|
|
} else {
|
|
require.Equal(t, int64(len(path)), stLink.Size)
|
|
}
|
|
}
|
|
|
|
func testStat(t *testing.T, testFS FS) {
|
|
_, errno := testFS.Stat("cat")
|
|
require.EqualErrno(t, syscall.ENOENT, errno)
|
|
_, errno = testFS.Stat("sub/cat")
|
|
require.EqualErrno(t, syscall.ENOENT, errno)
|
|
|
|
st, errno := testFS.Stat("sub/test.txt")
|
|
require.Zero(t, errno)
|
|
|
|
require.False(t, st.Mode.IsDir())
|
|
require.NotEqual(t, uint64(0), st.Dev)
|
|
require.NotEqual(t, uint64(0), st.Ino)
|
|
|
|
st, errno = testFS.Stat("sub")
|
|
require.Zero(t, errno)
|
|
|
|
require.True(t, st.Mode.IsDir())
|
|
// windows before go 1.20 has trouble reading the inode information on directories.
|
|
if runtime.GOOS != "windows" || platform.IsGo120 {
|
|
require.NotEqual(t, uint64(0), st.Dev)
|
|
require.NotEqual(t, uint64(0), st.Ino)
|
|
}
|
|
}
|
|
|
|
// requireReaddir ensures the input file is a directory, and returns its
|
|
// entries.
|
|
func requireReaddir(t *testing.T, f fs.File, n int, expectIno bool) []*platform.Dirent {
|
|
entries, errno := platform.Readdir(f, n)
|
|
require.Zero(t, errno)
|
|
|
|
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
|
|
if _, ok := f.(*openRootDir); ok {
|
|
// TODO: get inodes to work on the root directory of a composite FS
|
|
requireIno(t, entries, false)
|
|
} else {
|
|
requireIno(t, entries, expectIno)
|
|
}
|
|
return entries
|
|
}
|
|
|
|
// requireReaddirnames ensures the input file is a directory, and returns its
|
|
// entries.
|
|
func requireReaddirnames(t *testing.T, f fs.File, n int) []string {
|
|
names, errno := platform.Readdirnames(f, n)
|
|
require.Zero(t, errno)
|
|
sort.Strings(names)
|
|
return names
|
|
}
|
|
|
|
func testReadlink(t *testing.T, readFS, writeFS FS) {
|
|
testLinks := []struct {
|
|
old, dst string
|
|
}{
|
|
// Same dir.
|
|
{old: "animals.txt", dst: "symlinked-animals.txt"},
|
|
{old: "sub/test.txt", dst: "sub/symlinked-test.txt"},
|
|
// Parent to sub.
|
|
{old: "animals.txt", dst: "sub/symlinked-animals.txt"},
|
|
// Sub to parent.
|
|
{old: "sub/test.txt", dst: "symlinked-zoo.txt"},
|
|
}
|
|
|
|
for _, tl := range testLinks {
|
|
errno := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat
|
|
require.Zero(t, errno, "%v", tl)
|
|
|
|
dst, errno := readFS.Readlink(tl.dst)
|
|
require.Zero(t, errno)
|
|
require.Equal(t, tl.old, dst)
|
|
}
|
|
|
|
t.Run("errors", func(t *testing.T) {
|
|
_, err := readFS.Readlink("sub/test.txt")
|
|
require.Error(t, err)
|
|
_, err = readFS.Readlink("")
|
|
require.Error(t, err)
|
|
_, err = readFS.Readlink("animals.txt")
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
|
|
var (
|
|
//go:embed testdata
|
|
testdata embed.FS
|
|
readerAtFile = "wazero.txt"
|
|
emptyFile = "empty.txt"
|
|
)
|
|
|
|
func TestReaderAtOffset(t *testing.T) {
|
|
embedFS, err := fs.Sub(testdata, "testdata")
|
|
require.NoError(t, err)
|
|
|
|
d, err := embedFS.Open(readerAtFile)
|
|
require.NoError(t, err)
|
|
defer d.Close()
|
|
|
|
bytes, err := io.ReadAll(d)
|
|
require.NoError(t, err)
|
|
|
|
mapFS := gofstest.MapFS{readerAtFile: &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, readerAtFile), bytes, 0o600))
|
|
dirFS := os.DirFS(tmpDir)
|
|
|
|
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(readerAtFile)
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
|
|
var r io.Reader = f
|
|
ra := ReaderAtOffset(f, 0)
|
|
|
|
requireRead3 := func(r io.Reader, buf []byte) {
|
|
n, err := r.Read(buf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, n)
|
|
}
|
|
|
|
// The file should work as a reader (base case)
|
|
requireRead3(r, buf)
|
|
require.Equal(t, "waz", string(buf))
|
|
buf = buf[:]
|
|
|
|
// The readerAt impl should be able to start from zero also
|
|
requireRead3(ra, buf)
|
|
require.Equal(t, "waz", string(buf))
|
|
buf = buf[:]
|
|
|
|
// If the offset didn't change, we expect the next three chars.
|
|
requireRead3(r, buf)
|
|
require.Equal(t, "ero", string(buf))
|
|
buf = buf[:]
|
|
|
|
// If state was held between reader-at, we expect the same
|
|
requireRead3(ra, buf)
|
|
require.Equal(t, "ero", string(buf))
|
|
buf = buf[:]
|
|
|
|
// We should also be able to make another reader-at
|
|
ra = ReaderAtOffset(f, 3)
|
|
requireRead3(ra, buf)
|
|
require.Equal(t, "ero", string(buf))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReaderAtOffset_empty(t *testing.T) {
|
|
embedFS, err := fs.Sub(testdata, "testdata")
|
|
require.NoError(t, err)
|
|
|
|
d, err := embedFS.Open(readerAtFile)
|
|
require.NoError(t, err)
|
|
defer d.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)
|
|
|
|
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(emptyFile)
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
|
|
var r io.Reader = f
|
|
ra := ReaderAtOffset(f, 0)
|
|
|
|
requireRead3 := func(r io.Reader, buf []byte) {
|
|
n, err := r.Read(buf)
|
|
require.Equal(t, err, io.EOF)
|
|
require.Equal(t, 0, n) // file is empty
|
|
}
|
|
|
|
// The file should work as a reader (base case)
|
|
requireRead3(r, buf)
|
|
|
|
// The readerAt impl should be able to start from zero also
|
|
requireRead3(ra, buf)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestReaderAtOffset_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 both io.ReaderAt and io.Seeker
|
|
ra := ReaderAtOffset(struct{ fs.File }{f}, 0)
|
|
|
|
buf := make([]byte, 3)
|
|
_, err = ra.Read(buf)
|
|
require.Equal(t, syscall.ENOSYS, err)
|
|
}
|
|
|
|
func TestWriterAtOffset(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dirFS := NewDirFS(tmpDir)
|
|
|
|
// fs.FS doesn't support writes, and there is no other built-in
|
|
// implementation except os.File.
|
|
tests := []struct {
|
|
name string
|
|
fs FS
|
|
}{
|
|
{name: "sysfs.dirFS", fs: dirFS},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f, errno := tc.fs.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
w := f.File().(io.Writer)
|
|
wa := WriterAtOffset(f.File(), 6)
|
|
|
|
text := "wazero"
|
|
buf := make([]byte, 3)
|
|
copy(buf, text[:3])
|
|
|
|
requireWrite3 := func(r io.Writer, buf []byte) {
|
|
n, err := r.Write(buf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, n)
|
|
}
|
|
|
|
// The file should work as a writer (base case)
|
|
requireWrite3(w, buf)
|
|
|
|
// The writerAt impl should be able to start from zero also
|
|
requireWrite3(wa, buf)
|
|
|
|
copy(buf, text[3:])
|
|
|
|
// If the offset didn't change, the next chars will write after the
|
|
// first
|
|
requireWrite3(w, buf)
|
|
|
|
// If state was held between writer-at, we expect the same
|
|
requireWrite3(wa, buf)
|
|
|
|
// We should also be able to make another writer-at
|
|
wa = WriterAtOffset(f.File(), 12)
|
|
requireWrite3(wa, buf)
|
|
|
|
r := ReaderAtOffset(f.File(), 0)
|
|
b, err := io.ReadAll(r)
|
|
require.NoError(t, err)
|
|
|
|
// We expect to have written the text two and a half times:
|
|
// 1. io.Write: offset 0
|
|
// 2. io.WriterAt: offset 6
|
|
// 3. second io.WriterAt: offset 12, writing "ero"
|
|
require.Equal(t, text+text+text[3:], string(b))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriterAtOffset_empty(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dirFS := NewDirFS(tmpDir)
|
|
|
|
// fs.FS doesn't support writes, and there is no other built-in
|
|
// implementation except os.File.
|
|
tests := []struct {
|
|
name string
|
|
fs FS
|
|
}{
|
|
{name: "sysfs.dirFS", fs: dirFS},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
tc := tc
|
|
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
f, errno := tc.fs.OpenFile(emptyFile, os.O_RDWR|os.O_CREATE, 0o600)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
r := f.File().(io.Writer)
|
|
ra := WriterAtOffset(f.File(), 0)
|
|
|
|
var emptyBuf []byte
|
|
|
|
requireWrite := func(r io.Writer) {
|
|
n, err := r.Write(emptyBuf)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 0, n) // file is empty
|
|
}
|
|
|
|
// The file should work as a writer (base case)
|
|
requireWrite(r)
|
|
|
|
// The writerAt impl should be able to start from zero also
|
|
requireWrite(ra)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestWriterAtOffset_Unsupported(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
dirFS := NewDirFS(tmpDir)
|
|
|
|
f, errno := dirFS.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600)
|
|
require.Zero(t, errno)
|
|
defer f.Close()
|
|
|
|
// mask both io.WriterAt and io.Seeker
|
|
ra := WriterAtOffset(struct{ fs.File }{f.File()}, 0)
|
|
|
|
buf := make([]byte, 3)
|
|
_, err := ra.Write(buf)
|
|
require.Equal(t, syscall.ENOSYS, err)
|
|
}
|
|
|
|
// Test_FileSync doesn't guarantee sync works because the operating system may
|
|
// sync anyway. There is no test in Go for os.File Sync, but closest is similar
|
|
// to below. Effectively, this only tests that things don't error.
|
|
func Test_FileSync(t *testing.T) {
|
|
testSync(t, func(f fs.File) syscall.Errno {
|
|
return platform.UnwrapOSError(f.(interface{ Sync() error }).Sync())
|
|
})
|
|
}
|
|
|
|
// Test_FileDatasync has same issues as Test_Sync.
|
|
func Test_FileDatasync(t *testing.T) {
|
|
testSync(t, FileDatasync)
|
|
}
|
|
|
|
func testSync(t *testing.T, sync func(fs.File) syscall.Errno) {
|
|
f, err := os.CreateTemp("", t.Name())
|
|
require.NoError(t, err)
|
|
defer f.Close()
|
|
|
|
expected := "hello world!"
|
|
|
|
// Write the expected data
|
|
_, err = f.Write([]byte(expected))
|
|
require.NoError(t, err)
|
|
|
|
// Sync the data.
|
|
require.Zero(t, sync(f))
|
|
|
|
// Rewind while the file is still open.
|
|
_, err = f.Seek(0, io.SeekStart)
|
|
require.NoError(t, err)
|
|
|
|
// Read data from the file
|
|
var buf bytes.Buffer
|
|
_, err = io.Copy(&buf, f)
|
|
require.NoError(t, err)
|
|
|
|
// It may be the case that sync worked.
|
|
require.Equal(t, expected, buf.String())
|
|
}
|
|
|
|
func requireIno(t *testing.T, dirents []*platform.Dirent, expectIno bool) {
|
|
for _, e := range dirents {
|
|
if expectIno {
|
|
require.NotEqual(t, uint64(0), e.Ino, "%+v", e)
|
|
e.Ino = 0
|
|
} else {
|
|
require.Zero(t, e.Ino, "%+v", e)
|
|
}
|
|
}
|
|
}
|
|
|
|
// joinPath avoids us having to rename fields just to avoid conflict with the
|
|
// path package.
|
|
func joinPath(dirName, baseName string) string {
|
|
return path.Join(dirName, baseName)
|
|
}
|