Files
wazero/internal/sysfs/sysfs_test.go
2023-08-02 08:27:50 +08:00

392 lines
11 KiB
Go

package sysfs
import (
_ "embed"
"io"
"io/fs"
"os"
"path"
"runtime"
"sort"
"testing"
experimentalsys "github.com/tetratelabs/wazero/experimental/sys"
"github.com/tetratelabs/wazero/internal/platform"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
func testOpen_O_RDWR(t *testing.T, tmpDir string, testFS experimentalsys.FS) {
file := "file"
realPath := path.Join(tmpDir, file)
err := os.WriteFile(realPath, []byte{}, 0o600)
require.NoError(t, err)
f, errno := testFS.OpenFile(file, experimentalsys.O_RDWR, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
// If the write flag was honored, we should be able to write!
fileContents := []byte{1, 2, 3, 4}
n, errno := f.Write(fileContents)
require.EqualErrno(t, 0, errno)
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.EqualErrno(t, 0, 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, experimentalsys.O_RDONLY|experimentalsys.O_CREAT, 0o444)
require.EqualErrno(t, 0, errno)
defer f.Close()
if runtime.GOOS != "windows" {
// If the read-only flag was honored, we should not be able to write!
_, err = f.Write(fileContents)
require.EqualErrno(t, experimentalsys.EBADF, experimentalsys.UnwrapOSError(err))
}
// Verify stat on the file
stat, errno := f.Stat()
require.EqualErrno(t, 0, 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`, experimentalsys.O_WRONLY|experimentalsys.O_CREAT|experimentalsys.O_TRUNC, 0o600)
require.EqualErrno(t, 0, errno)
defer f.Close()
_, errno = f.Stat()
require.EqualErrno(t, 0, errno)
})
}
t.Run("O_TRUNC", func(t *testing.T) {
tmpDir := t.TempDir()
testFS := DirFS(tmpDir)
name := "truncate"
realPath := path.Join(tmpDir, name)
require.NoError(t, os.WriteFile(realPath, []byte("123456"), 0o0666))
f, errno = testFS.OpenFile(name, experimentalsys.O_RDWR|experimentalsys.O_TRUNC, 0o444)
require.EqualErrno(t, 0, errno)
require.EqualErrno(t, 0, f.Close())
actual, err := os.ReadFile(realPath)
require.NoError(t, err)
require.Equal(t, 0, len(actual))
})
}
func testOpen_Read(t *testing.T, testFS experimentalsys.FS, requireFileIno, expectDirIno bool) {
t.Helper()
t.Run("doesn't exist", func(t *testing.T) {
_, errno := testFS.OpenFile("nope", experimentalsys.O_RDONLY, 0)
// We currently follow os.Open not syscall.Open, so the error is wrapped.
require.EqualErrno(t, experimentalsys.ENOENT, errno)
})
t.Run("readdir . opens root", func(t *testing.T) {
f, errno := testFS.OpenFile(".", experimentalsys.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
dirents := requireReaddir(t, f, -1, expectDirIno)
// Scrub inodes so we can compare expectations without them.
for i := range dirents {
dirents[i].Ino = 0
}
require.Equal(t, []experimentalsys.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("readdir empty", func(t *testing.T) {
f, errno := testFS.OpenFile("emptydir", experimentalsys.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
entries := requireReaddir(t, f, -1, expectDirIno)
require.Zero(t, len(entries))
})
t.Run("readdir partial", func(t *testing.T) {
dirF, errno := testFS.OpenFile("dir", experimentalsys.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer dirF.Close()
dirents1, errno := dirF.Readdir(1)
require.EqualErrno(t, 0, errno)
require.Equal(t, 1, len(dirents1))
dirents2, errno := dirF.Readdir(1)
require.EqualErrno(t, 0, errno)
require.Equal(t, 1, len(dirents2))
// read exactly the last entry
dirents3, errno := dirF.Readdir(1)
require.EqualErrno(t, 0, errno)
require.Equal(t, 1, len(dirents3))
dirents := []experimentalsys.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, expectDirIno)
// Scrub inodes so we can compare expectations without them.
for i := range dirents {
dirents[i].Ino = 0
}
require.Equal(t, []experimentalsys.Dirent{
{Name: "-", Type: 0},
{Name: "a-", Type: fs.ModeDir},
{Name: "ab-", Type: 0},
}, dirents)
// no error reading an exhausted directory
_, errno = dirF.Readdir(1)
require.EqualErrno(t, 0, errno)
})
t.Run("file exists", func(t *testing.T) {
f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
fileContents := []byte(`bear
cat
shark
dinosaur
human
`)
// Ensure it implements Pread
lenToRead := len(fileContents) - 1
buf := make([]byte, lenToRead)
n, errno := f.Pread(buf, 1)
require.EqualErrno(t, 0, errno)
require.Equal(t, lenToRead, n)
require.Equal(t, fileContents[1:], buf)
// Ensure it implements Seek
offset, errno := f.Seek(1, io.SeekStart)
require.EqualErrno(t, 0, errno)
require.Equal(t, int64(1), offset)
// Read should pick up from position 1
n, errno = f.Read(buf)
require.EqualErrno(t, 0, errno)
require.Equal(t, lenToRead, n)
require.Equal(t, fileContents[1:], buf)
})
t.Run("file stat includes inode", func(t *testing.T) {
f, errno := testFS.OpenFile("empty.txt", experimentalsys.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
st, errno := f.Stat()
require.EqualErrno(t, 0, errno)
// Results are inconsistent, so don't validate the opposite.
if requireFileIno {
require.NotEqual(t, uint64(0), st.Ino, "%+v", st)
}
})
// 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 = experimentalsys.Oflag(0x40000)
f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY|O_NOATIME, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
})
t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
f, errno := testFS.OpenFile("animals.txt", experimentalsys.O_RDONLY, 0)
require.EqualErrno(t, 0, errno)
defer f.Close()
_, errno = f.Write([]byte{1, 2, 3, 4})
require.EqualErrno(t, experimentalsys.EBADF, errno)
})
t.Run("opening a directory with O_RDWR is EISDIR", func(t *testing.T) {
_, errno := testFS.OpenFile("sub", experimentalsys.O_DIRECTORY|experimentalsys.O_RDWR, 0)
require.EqualErrno(t, experimentalsys.EISDIR, errno)
})
}
func testLstat(t *testing.T, testFS experimentalsys.FS) {
_, errno := testFS.Lstat("cat")
require.EqualErrno(t, experimentalsys.ENOENT, errno)
_, errno = testFS.Lstat("sub/cat")
require.EqualErrno(t, experimentalsys.ENOENT, errno)
var st sys.Stat_t
t.Run("dir", func(t *testing.T) {
st, errno = testFS.Lstat(".")
require.EqualErrno(t, 0, errno)
require.True(t, st.Mode.IsDir())
require.NotEqual(t, uint64(0), st.Ino)
})
var stFile sys.Stat_t
t.Run("file", func(t *testing.T) {
stFile, errno = testFS.Lstat("animals.txt")
require.EqualErrno(t, 0, 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 sys.Stat_t
t.Run("subdir", func(t *testing.T) {
stSubdir, errno = testFS.Lstat("sub")
require.EqualErrno(t, 0, 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.EqualErrno(t, 0, errno)
requireLinkStat(t, testFS, pathLink, stLink)
})
}
func requireLinkStat(t *testing.T, testFS experimentalsys.FS, path string, stat sys.Stat_t) {
link := path + "-link"
stLink, errno := testFS.Lstat(link)
require.EqualErrno(t, 0, 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 experimentalsys.FS) {
_, errno := testFS.Stat("cat")
require.EqualErrno(t, experimentalsys.ENOENT, errno)
_, errno = testFS.Stat("sub/cat")
require.EqualErrno(t, experimentalsys.ENOENT, errno)
st, errno := testFS.Stat("sub/test.txt")
require.EqualErrno(t, 0, 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.EqualErrno(t, 0, 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.IsAtLeastGo120 {
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 experimentalsys.File, n int, expectDirIno bool) []experimentalsys.Dirent {
entries, errno := f.Readdir(n)
require.EqualErrno(t, 0, errno)
sort.Slice(entries, func(i, j int) bool { return entries[i].Name < entries[j].Name })
requireIno(t, entries, expectDirIno)
return entries
}
func testReadlink(t *testing.T, readFS, writeFS experimentalsys.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.EqualErrno(t, 0, 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)
})
}
func requireIno(t *testing.T, dirents []experimentalsys.Dirent, expectDirIno bool) {
for i := range dirents {
d := dirents[i]
if expectDirIno {
require.NotEqual(t, uint64(0), d.Ino, "%+v", d)
d.Ino = 0
} else {
require.Zero(t, d.Ino, "%+v", d)
}
}
}
// 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)
}