Files
wazero/internal/sysfs/sysfs_test.go
Crypt Keeper 36bf277534 sysfs: requires all methods to return syscall.Errno (#1264)
This forces all syscall functions, notably filesystem, to return numeric
codes as opposed to mapping in two different areas. The result of this
change is better consolidation in call sites of `sysfs.FS`, while
further refactoring is needed to address consolidation of file errors.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-03-22 07:47:57 +01: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.(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.NoError(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.(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, err := f.Stat()
require.NoError(t, err)
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 = platform.StatFile(f)
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, -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, -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, -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, -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, 1)
require.Zero(t, errno)
require.Equal(t, 1, len(dirents1))
dirents2, errno := platform.Readdir(dirF, 1)
require.Zero(t, errno)
require.Equal(t, 1, len(dirents2))
// read exactly the last entry
dirents3, errno := platform.Readdir(dirF, 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, 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, 1)
require.Zero(t, errno)
require.Equal(t, 1, len(names1))
names2, errno := platform.Readdirnames(dirF, 1)
require.Zero(t, errno)
require.Equal(t, 1, len(names2))
// read exactly the last entry
names3, errno := platform.Readdirnames(dirF, 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, 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.(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.(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)
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.NoError(t, f.Close())
require.Zero(t, errno)
if w, ok := f.(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.NoError(t, f.Close())
require.Zero(t, errno)
if w, ok := f.(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.(io.Writer)
wa := WriterAtOffset(f, 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, 12)
requireWrite3(wa, buf)
r := ReaderAtOffset(f, 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.(io.Writer)
ra := WriterAtOffset(f, 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}, 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)
}