Files
wazero/internal/sysfs/sysfs_test.go
2023-05-01 12:33:40 +08:00

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)
}