This reduces some boilerplate by extracting UnimplementedFS from the existing FS implementations, such that it returns ENOSYS. This also removes inconsistency where some methods on FS returned syscall.Errno and others PathError. Note: this doesn't get rid of all PathError, yet. We still need to create a syscallfs.File type which would be able to do that. This is just one preliminary cleanup before refactoring out the `fs.FS` embedding from `syscallfs.DS`. P.S. naming convention is arbitrary, so I took UnimplementedXXX from grpc. This pattern is used a lot of places, also proxy-wasm-go-sdk, e.g. `DefaultVMContext`. Signed-off-by: Adrian Cole <adrian@tetrate.io>
156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
package sys
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"errors"
|
|
"io"
|
|
"io/fs"
|
|
"os"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"github.com/tetratelabs/wazero/internal/syscallfs"
|
|
testfs "github.com/tetratelabs/wazero/internal/testing/fs"
|
|
"github.com/tetratelabs/wazero/internal/testing/require"
|
|
)
|
|
|
|
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
|
|
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
|
|
|
|
var (
|
|
noopStdin = &FileEntry{File: &stdioFileReader{r: eofReader{}, s: noopStdinStat}}
|
|
noopStdout = &FileEntry{File: &stdioFileWriter{w: io.Discard, s: noopStdoutStat}}
|
|
noopStderr = &FileEntry{File: &stdioFileWriter{w: io.Discard, s: noopStderrStat}}
|
|
)
|
|
|
|
//go:embed testdata
|
|
var testdata embed.FS
|
|
|
|
func TestNewFSContext(t *testing.T) {
|
|
embedFS, err := fs.Sub(testdata, "testdata")
|
|
require.NoError(t, err)
|
|
|
|
dirfs, err := syscallfs.NewDirFS(".", "/")
|
|
require.NoError(t, err)
|
|
|
|
// Test various usual configuration for the file system.
|
|
tests := []struct {
|
|
name string
|
|
fs syscallfs.FS
|
|
}{
|
|
{
|
|
name: "embed.FS",
|
|
fs: syscallfs.Adapt(embedFS, "/"),
|
|
},
|
|
{
|
|
name: "syscallfs.NewDirFS",
|
|
// Don't use "testdata" because it may not be present in
|
|
// cross-architecture (a.k.a. scratch) build containers.
|
|
fs: dirfs,
|
|
},
|
|
{
|
|
name: "syscallfs.NewReadFS",
|
|
fs: syscallfs.NewReadFS(dirfs),
|
|
},
|
|
{
|
|
name: "fstest.MapFS",
|
|
fs: syscallfs.Adapt(fstest.MapFS{}, "/"),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
tc := tt
|
|
|
|
t.Run(tc.name, func(b *testing.T) {
|
|
fsc, err := NewFSContext(nil, nil, nil, tc.fs)
|
|
require.NoError(t, err)
|
|
defer fsc.Close(testCtx)
|
|
|
|
preopenedDir, _ := fsc.openedFiles.Lookup(FdPreopen)
|
|
preopenedPath := fsc.fs.GuestDir()
|
|
require.Equal(t, tc.fs, fsc.fs)
|
|
require.NotNil(t, preopenedDir)
|
|
require.Equal(t, "", preopenedDir.Name)
|
|
|
|
// Verify that each call to OpenFile returns a different file
|
|
// descriptor.
|
|
f1, err := fsc.OpenFile(preopenedPath, 0, 0)
|
|
require.NoError(t, err)
|
|
require.NotEqual(t, FdPreopen, f1)
|
|
|
|
// Verify that file descriptors are reused.
|
|
//
|
|
// Note that this specific behavior is not required by WASI which
|
|
// only documents that file descriptor numbers will be selected
|
|
// randomly and applications should not rely on them. We added this
|
|
// test to ensure that our implementation properly reuses descriptor
|
|
// numbers but if we were to change the reuse strategy, this test
|
|
// would likely break and need to be updated.
|
|
require.NoError(t, fsc.CloseFile(f1))
|
|
f2, err := fsc.OpenFile(preopenedPath, 0, 0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, f1, f2)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUnimplementedFSContext(t *testing.T) {
|
|
testFS, err := NewFSContext(nil, nil, nil, syscallfs.UnimplementedFS{})
|
|
require.NoError(t, err)
|
|
|
|
expected := &FSContext{fs: syscallfs.UnimplementedFS{}}
|
|
expected.openedFiles.Insert(noopStdin)
|
|
expected.openedFiles.Insert(noopStdout)
|
|
expected.openedFiles.Insert(noopStderr)
|
|
|
|
t.Run("Close closes", func(t *testing.T) {
|
|
err := testFS.Close(testCtx)
|
|
require.NoError(t, err)
|
|
|
|
// Closes opened files
|
|
require.Equal(t, &FSContext{fs: syscallfs.UnimplementedFS{}}, testFS)
|
|
})
|
|
}
|
|
|
|
func TestContext_Close(t *testing.T) {
|
|
testFS := syscallfs.Adapt(testfs.FS{"foo": &testfs.File{}}, "/")
|
|
|
|
fsc, err := NewFSContext(nil, nil, nil, testFS)
|
|
require.NoError(t, err)
|
|
|
|
// Verify base case
|
|
require.Equal(t, 1+FdPreopen, uint32(fsc.openedFiles.Len()))
|
|
|
|
_, err = fsc.OpenFile("foo", os.O_RDONLY, 0)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2+FdPreopen, uint32(fsc.openedFiles.Len()))
|
|
|
|
// Closing should not err.
|
|
require.NoError(t, fsc.Close(testCtx))
|
|
|
|
// Verify our intended side-effect
|
|
require.Zero(t, fsc.openedFiles.Len())
|
|
|
|
// Verify no error closing again.
|
|
require.NoError(t, fsc.Close(testCtx))
|
|
}
|
|
|
|
func TestContext_Close_Error(t *testing.T) {
|
|
file := &testfs.File{CloseErr: errors.New("error closing")}
|
|
|
|
testFS := syscallfs.Adapt(testfs.FS{"foo": file}, "/")
|
|
|
|
fsc, err := NewFSContext(nil, nil, nil, testFS)
|
|
require.NoError(t, err)
|
|
|
|
// open another file
|
|
_, err = fsc.OpenFile("foo", os.O_RDONLY, 0)
|
|
require.NoError(t, err)
|
|
|
|
require.EqualError(t, fsc.Close(testCtx), "error closing")
|
|
|
|
// Paths should clear even under error
|
|
require.Zero(t, fsc.openedFiles.Len(), "expected no opened files")
|
|
}
|