Files
wazero/internal/sysfs/rootfs_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

474 lines
12 KiB
Go

package sysfs
import (
"errors"
"io"
"io/fs"
"os"
"path"
"sort"
"strings"
"syscall"
"testing"
gofstest "testing/fstest"
"github.com/tetratelabs/wazero/internal/fstest"
"github.com/tetratelabs/wazero/internal/platform"
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
"github.com/tetratelabs/wazero/internal/testing/require"
)
func TestNewRootFS(t *testing.T) {
t.Run("empty", func(t *testing.T) {
rootFS, err := NewRootFS(nil, nil)
require.NoError(t, err)
require.Equal(t, UnimplementedFS{}, rootFS)
})
t.Run("only root", func(t *testing.T) {
testFS := NewDirFS(t.TempDir())
rootFS, err := NewRootFS([]FS{testFS}, []string{""})
require.NoError(t, err)
// Should not be a composite filesystem
require.Equal(t, testFS, rootFS)
})
t.Run("only non root", func(t *testing.T) {
testFS := NewDirFS(".")
rootFS, err := NewRootFS([]FS{testFS}, []string{"/tmp"})
require.NoError(t, err)
// unwrapping returns in original order
require.Equal(t, []FS{testFS}, rootFS.(*CompositeFS).FS())
require.Equal(t, []string{"/tmp"}, rootFS.(*CompositeFS).GuestPaths())
// String is human-readable
require.Equal(t, "[.:/tmp]", rootFS.String())
// Guest can look up /tmp
f, errno := rootFS.OpenFile("/tmp", os.O_RDONLY, 0)
require.Zero(t, errno)
require.NoError(t, f.Close())
// Guest can look up / and see "/tmp" in it
f, errno = rootFS.OpenFile("/", os.O_RDONLY, 0)
require.Zero(t, errno)
dirents, err := f.(fs.ReadDirFile).ReadDir(-1)
require.NoError(t, err)
require.Equal(t, 1, len(dirents))
require.Equal(t, "tmp", dirents[0].Name())
require.True(t, dirents[0].IsDir())
})
t.Run("multiple roots unsupported", func(t *testing.T) {
testFS := NewDirFS(".")
_, err := NewRootFS([]FS{testFS, testFS}, []string{"/", "/"})
require.EqualError(t, err, "multiple root filesystems are invalid: [.:/ .:/]")
})
t.Run("virtual paths unsupported", func(t *testing.T) {
testFS := NewDirFS(".")
_, err := NewRootFS([]FS{testFS}, []string{"usr/bin"})
require.EqualError(t, err, "only single-level guest paths allowed: [.:usr/bin]")
})
t.Run("multiple matches", func(t *testing.T) {
tmpDir1 := t.TempDir()
testFS1 := NewDirFS(tmpDir1)
require.NoError(t, os.Mkdir(path.Join(tmpDir1, "tmp"), 0o700))
require.NoError(t, os.WriteFile(path.Join(tmpDir1, "a"), []byte{1}, 0o600))
tmpDir2 := t.TempDir()
testFS2 := NewDirFS(tmpDir2)
require.NoError(t, os.WriteFile(path.Join(tmpDir2, "a"), []byte{2}, 0o600))
rootFS, err := NewRootFS([]FS{testFS2, testFS1}, []string{"/tmp", "/"})
require.NoError(t, err)
// unwrapping returns in original order
require.Equal(t, []FS{testFS2, testFS1}, rootFS.(*CompositeFS).FS())
require.Equal(t, []string{"/tmp", "/"}, rootFS.(*CompositeFS).GuestPaths())
// Should be a composite filesystem
require.NotEqual(t, testFS1, rootFS)
require.NotEqual(t, testFS2, rootFS)
t.Run("last wins", func(t *testing.T) {
f, errno := rootFS.OpenFile("/tmp/a", os.O_RDONLY, 0)
require.Zero(t, errno)
defer f.Close()
b, err := io.ReadAll(f)
require.NoError(t, err)
require.Equal(t, []byte{2}, b)
})
// This test is covered by fstest.TestFS, but doing again here
t.Run("root includes prefix mount", func(t *testing.T) {
f, errno := rootFS.OpenFile(".", os.O_RDONLY, 0)
require.Zero(t, errno)
defer f.Close()
require.Equal(t, []string{"a", "tmp"}, readDirNames(t, f))
})
})
}
func readDirNames(t *testing.T, f fs.File) []string {
names, errno := platform.Readdirnames(f, -1)
require.Zero(t, errno)
sort.Strings(names)
return names
}
func TestRootFS_String(t *testing.T) {
tmpFS := NewDirFS(".")
rootFS := NewDirFS(".")
testFS, err := NewRootFS([]FS{rootFS, tmpFS}, []string{"/", "/tmp"})
require.NoError(t, err)
require.Equal(t, "[.:/ .:/tmp]", testFS.String())
}
func TestRootFS_Open(t *testing.T) {
tmpDir := t.TempDir()
// Create a subdirectory, so we can test reads outside the FS root.
tmpDir = path.Join(tmpDir, t.Name())
require.NoError(t, os.Mkdir(tmpDir, 0o700))
require.NoError(t, fstest.WriteTestFiles(tmpDir))
testRootFS := NewDirFS(tmpDir)
testDirFS := NewDirFS(t.TempDir())
testFS, err := NewRootFS([]FS{testRootFS, testDirFS}, []string{"/", "/emptydir"})
require.NoError(t, err)
testOpen_Read(t, testFS, true)
testOpen_O_RDWR(t, tmpDir, testFS)
t.Run("path outside root valid", func(t *testing.T) {
_, err := testFS.OpenFile("../foo", os.O_RDONLY, 0)
// syscall.FS allows relative path lookups
require.True(t, errors.Is(err, fs.ErrNotExist))
})
}
func TestRootFS_Stat(t *testing.T) {
tmpDir := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir))
tmpFS := NewDirFS(t.TempDir())
testFS, err := NewRootFS([]FS{NewDirFS(tmpDir), tmpFS}, []string{"/", "/tmp"})
require.NoError(t, err)
testStat(t, testFS)
}
func TestRootFS_TestFS(t *testing.T) {
t.Parallel()
// Set up the test files
tmpDir1 := t.TempDir()
require.NoError(t, fstest.WriteTestFiles(tmpDir1))
// move one directory outside the other
tmpDir2 := t.TempDir()
require.NoError(t, os.Rename(path.Join(tmpDir1, "dir"), path.Join(tmpDir2, "dir")))
// Create a root mount
testFS1 := NewDirFS(tmpDir1)
// Create a dir mount
testFS2 := NewDirFS(path.Join(tmpDir2, "dir"))
testFS, err := NewRootFS([]FS{testFS1, testFS2}, []string{"/", "/dir"})
require.NoError(t, err)
// Run TestFS via the adapter
require.NoError(t, fstest.TestFS(testFS.(fs.FS)))
}
func TestRootFS_examples(t *testing.T) {
tests := []struct {
name string
fs []FS
guestPaths []string
expected, unexpected []string
}{
// e.g. from Go project root:
// $ GOOS=js GOARCH=wasm bin/go test -c -o template.wasm text/template
// $ wazero run -mount=src/text/template:/ template.wasm -test.v
{
name: "go test text/template",
fs: []FS{
&adapter{fs: testfs.FS{"go-example-stdout-ExampleTemplate-0.txt": &testfs.File{}}},
&adapter{fs: testfs.FS{"testdata/file1.tmpl": &testfs.File{}}},
},
guestPaths: []string{"/tmp", "/"},
expected: []string{"/tmp/go-example-stdout-ExampleTemplate-0.txt", "testdata/file1.tmpl"},
unexpected: []string{"DOES NOT EXIST"},
},
// e.g. from TinyGo project root:
// $ ./build/tinygo test -target wasi -c -o flate.wasm compress/flate
// $ wazero run -mount=$(go env GOROOT)/src/compress/flate:/ flate.wasm -test.v
{
name: "tinygo test compress/flate",
fs: []FS{
&adapter{fs: testfs.FS{}},
&adapter{fs: testfs.FS{"testdata/e.txt": &testfs.File{}}},
&adapter{fs: testfs.FS{"testdata/Isaac.Newton-Opticks.txt": &testfs.File{}}},
},
guestPaths: []string{"/", "../", "../../"},
expected: []string{"../testdata/e.txt", "../../testdata/Isaac.Newton-Opticks.txt"},
unexpected: []string{"../../testdata/e.txt"},
},
// e.g. from Go project root:
// $ GOOS=js GOARCH=wasm bin/go test -c -o net.wasm ne
// $ wazero run -mount=src/net:/ net.wasm -test.v -test.short
{
name: "go test net",
fs: []FS{
&adapter{fs: testfs.FS{"services": &testfs.File{}}},
&adapter{fs: testfs.FS{"testdata/aliases": &testfs.File{}}},
},
guestPaths: []string{"/etc", "/"},
expected: []string{"/etc/services", "testdata/aliases"},
unexpected: []string{"services"},
},
// e.g. from wagi-python project root:
// $ GOOS=js GOARCH=wasm bin/go test -c -o net.wasm ne
// $ wazero run -hostlogging=filesystem -mount=.:/ -env=PYTHONHOME=/opt/wasi-python/lib/python3.11 \
// -env=PYTHONPATH=/opt/wasi-python/lib/python3.11 opt/wasi-python/bin/python3.wasm
{
name: "python",
fs: []FS{
&adapter{fs: gofstest.MapFS{ // to allow resolution of "."
"pybuilddir.txt": &gofstest.MapFile{},
"opt/wasi-python/lib/python3.11/__phello__/__init__.py": &gofstest.MapFile{},
}},
},
guestPaths: []string{"/"},
expected: []string{
".",
"pybuilddir.txt",
"opt/wasi-python/lib/python3.11/__phello__/__init__.py",
},
},
// e.g. from Zig project root: TODO: verify this once cli works with multiple mounts
// $ zig test --test-cmd wazero --test-cmd run --test-cmd -mount=.:/ -mount=/tmp:/tmp \
// --test-cmd-bin -target wasm32-wasi --zig-lib-dir ./lib ./lib/std/std.zig
{
name: "zig",
fs: []FS{
&adapter{fs: testfs.FS{"zig-cache": &testfs.File{}}},
&adapter{fs: testfs.FS{"qSQRrUkgJX9L20mr": &testfs.File{}}},
},
guestPaths: []string{"/", "/tmp"},
expected: []string{"zig-cache", "/tmp/qSQRrUkgJX9L20mr"},
unexpected: []string{"/qSQRrUkgJX9L20mr"},
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
root, err := NewRootFS(tc.fs, tc.guestPaths)
require.NoError(t, err)
for _, p := range tc.expected {
f, errno := root.OpenFile(p, os.O_RDONLY, 0)
require.Zero(t, errno, p)
require.NoError(t, f.Close(), p)
}
for _, p := range tc.unexpected {
_, err := root.OpenFile(p, os.O_RDONLY, 0)
require.EqualErrno(t, syscall.ENOENT, err)
}
})
}
}
func Test_stripPrefixesAndTrailingSlash(t *testing.T) {
tests := []struct {
path, expected string
}{
{
path: ".",
expected: "",
},
{
path: "/",
expected: "",
},
{
path: "./",
expected: "",
},
{
path: "./foo",
expected: "foo",
},
{
path: ".foo",
expected: ".foo",
},
{
path: "././foo",
expected: "foo",
},
{
path: "/foo",
expected: "foo",
},
{
path: "foo/",
expected: "foo",
},
{
path: "//",
expected: "",
},
{
path: "../../",
expected: "../..",
},
{
path: "./../../",
expected: "../..",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.path, func(t *testing.T) {
pathI, pathLen := stripPrefixesAndTrailingSlash(tc.path)
require.Equal(t, tc.expected, tc.path[pathI:pathLen])
})
}
}
func Test_hasPathPrefix(t *testing.T) {
tests := []struct {
name string
path, prefix string
expectEq, expectMatch bool
}{
{
name: "empty prefix",
path: "foo",
prefix: "",
expectEq: false,
expectMatch: true,
},
{
name: "equal prefix",
path: "foo",
prefix: "foo",
expectEq: true,
expectMatch: true,
},
{
name: "sub path",
path: "foo/bar",
prefix: "foo",
expectMatch: true,
},
{
name: "different sub path",
path: "foo/bar",
prefix: "bar",
expectMatch: false,
},
{
name: "different path same length",
path: "foo",
prefix: "bar",
expectMatch: false,
},
{
name: "longer path",
path: "foo",
prefix: "foo/bar",
expectMatch: false,
},
{
name: "path shorter",
path: "foo",
prefix: "fooo",
expectMatch: false,
},
{
name: "path longer",
path: "fooo",
prefix: "foo",
expectMatch: false,
},
{
name: "shorter path",
path: "foo",
prefix: "foo/bar",
expectMatch: false,
},
{
name: "wrong and shorter path",
path: "foo",
prefix: "bar/foo",
expectMatch: false,
},
{
name: "same relative",
path: "../..",
prefix: "../..",
expectEq: true,
expectMatch: true,
},
{
name: "longer relative",
path: "..",
prefix: "../..",
expectMatch: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
path := "././." + tc.path + "/"
eq, match := hasPathPrefix(path, 5, 5+len(tc.path), tc.prefix)
require.Equal(t, tc.expectEq, eq)
require.Equal(t, tc.expectMatch, match)
})
}
}
// BenchmarkHasPrefixVsIterate shows that iteration is faster than re-slicing
// for a prefix match.
func BenchmarkHasPrefixVsIterate(b *testing.B) {
s := "../../.."
prefix := "../bar"
prefixLen := len(prefix)
b.Run("strings.HasPrefix", func(b *testing.B) {
for i := 0; i < b.N; i++ {
if strings.HasPrefix(s, prefix) { //nolint
}
}
})
b.Run("iterate", func(b *testing.B) {
for i := 0; i < b.N; i++ {
for i := 0; i < prefixLen; i++ {
if s[i] != prefix[i] {
break
}
}
}
})
}