Files
wazero/internal/sysfs/sysfs_test.go
Crypt Keeper a9b3301862 gojs: implements remaining link functions (#1198)
This implements the last remaining link functions using the same logic
as WASI. In doing so, this changes the signature for FS.ReadLink to be
more similar to the rest of the functions. Particularly, it stops
reading the result into a buffer.

After this, the only syscalls left to implement in gojs are chown.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2023-03-05 16:11:36 +08:00

792 lines
20 KiB
Go

package sysfs
import (
"bytes"
"embed"
_ "embed"
"io"
"io/fs"
"os"
"path"
"runtime"
"sort"
"syscall"
"testing"
gofstest "testing/fstest"
"time"
"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, err := testFS.OpenFile(file, os.O_RDWR, 0)
require.NoError(t, err)
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, err = testFS.OpenFile(file, os.O_RDONLY|os.O_CREATE, 0o444)
require.NoError(t, err)
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())
}
func testOpen_Read(t *testing.T, testFS FS, expectIno bool) {
t.Run("doesn't exist", func(t *testing.T) {
_, err := 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, err)
})
t.Run("readdir . opens root", func(t *testing.T) {
f, err := testFS.OpenFile(".", os.O_RDONLY, 0)
require.NoError(t, err)
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, err := testFS.OpenFile(".", os.O_RDONLY, 0)
require.NoError(t, err)
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, err := testFS.OpenFile("emptydir", os.O_RDONLY, 0)
require.NoError(t, err)
defer f.Close()
entries := requireReaddir(t, f, -1, expectIno)
require.Zero(t, len(entries))
})
t.Run("readdirnames empty", func(t *testing.T) {
f, err := testFS.OpenFile("emptydir", os.O_RDONLY, 0)
require.NoError(t, err)
defer f.Close()
names := requireReaddirnames(t, f, -1)
require.Zero(t, len(names))
})
t.Run("readdir partial", func(t *testing.T) {
dirF, err := testFS.OpenFile("dir", os.O_RDONLY, 0)
require.NoError(t, err)
defer dirF.Close()
dirents1, err := platform.Readdir(dirF, 1)
require.NoError(t, err)
require.Equal(t, 1, len(dirents1))
dirents2, err := platform.Readdir(dirF, 1)
require.NoError(t, err)
require.Equal(t, 1, len(dirents2))
// read exactly the last entry
dirents3, err := platform.Readdir(dirF, 1)
require.NoError(t, err)
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
_, err = platform.Readdir(dirF, 1)
require.NoError(t, err)
})
// TODO: consolidate duplicated tests from platform once we have our own
// file type
t.Run("readdirnames partial", func(t *testing.T) {
dirF, err := testFS.OpenFile("dir", os.O_RDONLY, 0)
require.NoError(t, err)
defer dirF.Close()
names1, err := platform.Readdirnames(dirF, 1)
require.NoError(t, err)
require.Equal(t, 1, len(names1))
names2, err := platform.Readdirnames(dirF, 1)
require.NoError(t, err)
require.Equal(t, 1, len(names2))
// read exactly the last entry
names3, err := platform.Readdirnames(dirF, 1)
require.NoError(t, err)
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
_, err = platform.Readdirnames(dirF, 1)
require.NoError(t, err)
})
t.Run("file exists", func(t *testing.T) {
f, err := testFS.OpenFile("animals.txt", os.O_RDONLY, 0)
require.NoError(t, err)
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, err := testFS.OpenFile("animals.txt", os.O_RDONLY|O_NOATIME, 0)
require.NoError(t, err)
defer f.Close()
})
t.Run("writing to a read-only file is EBADF", func(t *testing.T) {
f, err := testFS.OpenFile("animals.txt", os.O_RDONLY, 0)
defer require.NoError(t, f.Close())
require.NoError(t, err)
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, err := testFS.OpenFile("sub", os.O_RDONLY, 0)
defer require.NoError(t, f.Close())
require.NoError(t, err)
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) {
var stat platform.Stat_t
require.EqualErrno(t, syscall.ENOENT, testFS.Lstat("cat", &stat))
require.EqualErrno(t, syscall.ENOENT, testFS.Lstat("sub/cat", &stat))
t.Run("dir", func(t *testing.T) {
err := testFS.Lstat(".", &stat)
require.NoError(t, err)
require.True(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
var statFile platform.Stat_t
t.Run("file", func(t *testing.T) {
require.NoError(t, testFS.Lstat("animals.txt", &statFile))
require.Zero(t, statFile.Mode.Type())
require.Equal(t, int64(30), statFile.Size)
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("link to file", func(t *testing.T) {
requireLinkStat(t, testFS, "animals.txt", &statFile)
})
var statSubdir platform.Stat_t
t.Run("subdir", func(t *testing.T) {
require.NoError(t, testFS.Lstat("sub", &statSubdir))
require.True(t, statSubdir.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Ino)
})
t.Run("link to dir", func(t *testing.T) {
requireLinkStat(t, testFS, "sub", &statSubdir)
})
t.Run("link to dir link", func(t *testing.T) {
pathLink := "sub-link"
var statLink platform.Stat_t
require.NoError(t, testFS.Lstat(pathLink, &statLink))
requireLinkStat(t, testFS, pathLink, &statLink)
})
}
func requireLinkStat(t *testing.T, testFS FS, path string, stat *platform.Stat_t) {
link := path + "-link"
var linkStat platform.Stat_t
require.NoError(t, testFS.Lstat(link, &linkStat))
require.NotEqual(t, stat.Ino, linkStat.Ino) // inodes are not equal
require.Equal(t, fs.ModeSymlink, linkStat.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, linkStat.Size)
} else {
require.Equal(t, int64(len(path)), linkStat.Size)
}
}
func testStat(t *testing.T, testFS FS) {
var stat platform.Stat_t
require.EqualErrno(t, syscall.ENOENT, testFS.Stat("cat", &stat))
require.EqualErrno(t, syscall.ENOENT, testFS.Stat("sub/cat", &stat))
err := testFS.Stat("sub/test.txt", &stat)
require.NoError(t, err)
require.False(t, stat.Mode.IsDir())
require.NotEqual(t, uint64(0), stat.Dev)
require.NotEqual(t, uint64(0), stat.Ino)
err = testFS.Stat("sub", &stat)
require.NoError(t, err)
require.True(t, stat.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), stat.Dev)
require.NotEqual(t, uint64(0), stat.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, err := platform.Readdir(f, n)
require.NoError(t, err)
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, err := platform.Readdirnames(f, n)
require.NoError(t, err)
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 {
err := writeFS.Symlink(tl.old, tl.dst) // not os.Symlink for windows compat
require.NoError(t, err, "%v", tl)
dst, err := readFS.Readlink(tl.dst)
require.NoError(t, err)
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 testUtimes(t *testing.T, tmpDir string, testFS FS) {
file := "file"
err := os.WriteFile(path.Join(tmpDir, file), []byte{}, 0o700)
require.NoError(t, err)
dir := "dir"
err = os.Mkdir(path.Join(tmpDir, dir), 0o700)
require.NoError(t, err)
t.Run("doesn't exist", func(t *testing.T) {
err := testFS.Utimes("nope",
time.Unix(123, 4*1e3).UnixNano(),
time.Unix(567, 8*1e3).UnixNano())
require.EqualErrno(t, syscall.ENOENT, err)
})
type test struct {
name string
path string
atimeNsec, mtimeNsec int64
}
// Note: This sets microsecond granularity because Windows doesn't support
// nanosecond.
tests := []test{
{
name: "file positive",
path: file,
atimeNsec: time.Unix(123, 4*1e3).UnixNano(),
mtimeNsec: time.Unix(567, 8*1e3).UnixNano(),
},
{
name: "dir positive",
path: dir,
atimeNsec: time.Unix(123, 4*1e3).UnixNano(),
mtimeNsec: time.Unix(567, 8*1e3).UnixNano(),
},
{name: "file zero", path: file},
{name: "dir zero", path: dir},
}
// linux and freebsd report inaccurate results when the input ts is negative.
if runtime.GOOS == "windows" || runtime.GOOS == "darwin" {
tests = append(tests,
test{
name: "file negative",
path: file,
atimeNsec: time.Unix(-123, -4*1e3).UnixNano(),
mtimeNsec: time.Unix(-567, -8*1e3).UnixNano(),
},
test{
name: "dir negative",
path: dir,
atimeNsec: time.Unix(-123, -4*1e3).UnixNano(),
mtimeNsec: time.Unix(-567, -8*1e3).UnixNano(),
},
)
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
err := testFS.Utimes(tc.path, tc.atimeNsec, tc.mtimeNsec)
require.NoError(t, err)
var stat platform.Stat_t
require.NoError(t, testFS.Stat(tc.path, &stat))
if platform.CompilerSupported() {
require.Equal(t, stat.Atim, tc.atimeNsec)
} // else only mtimes will return.
require.Equal(t, stat.Mtim, tc.mtimeNsec)
})
}
}
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, err := tc.fs.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600)
require.NoError(t, err)
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, err := tc.fs.OpenFile(emptyFile, os.O_RDWR|os.O_CREATE, 0o600)
require.NoError(t, err)
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, err := dirFS.OpenFile(readerAtFile, os.O_RDWR|os.O_CREATE, 0o600)
require.NoError(t, err)
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) error {
return 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) error) {
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.NoError(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)
}
}
}