Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds test delegating to iotest against a wasi fs.Fs implementation #553

Merged
merged 2 commits into from
May 13, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 198 additions & 0 deletions internal/integration_test/fs/fs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
package fs

import (
"context"
_ "embed"
"fmt"
"io"
"io/fs"
"testing"
"testing/fstest"
"testing/iotest"

"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/wasi"
)

var testCtx = context.Background()

//go:embed testdata/animals.txt
var animals []byte

// wasiFs is an implementation of fs.Fs calling into wasi. Not thread-safe because we use
// fixed Memory offsets for transferring data with wasm.
type wasiFs struct {
t *testing.T

wasm wazero.Runtime
memory api.Memory

workdirFd uint32

pathOpen api.Function
fdClose api.Function
fdRead api.Function
fdSeek api.Function
}

func (fs *wasiFs) Open(name string) (fs.File, error) {
pathBytes := []byte(name)
// Pick anywhere in memory to write the path to.
pathPtr := uint32(0)
ok := fs.memory.Write(testCtx, pathPtr, pathBytes)
require.True(fs.t, ok)
resultOpenedFd := pathPtr + uint32(len(pathBytes))

fd := fs.workdirFd
dirflags := uint32(0) // arbitrary dirflags
pathLen := len(pathBytes)
oflags := uint32(0) // arbitrary oflags
// rights are ignored per https://github.com/WebAssembly/WASI/issues/469#issuecomment-1045251844
fsRightsBase, fsRightsInheriting := uint64(1), uint64(2)
fdflags := uint32(0) // arbitrary fdflags
res, err := fs.pathOpen.Call(
testCtx,
uint64(fd), uint64(dirflags), uint64(pathPtr), uint64(pathLen), uint64(oflags),
fsRightsBase, fsRightsInheriting, uint64(fdflags), uint64(resultOpenedFd))
require.NoError(fs.t, err)
require.Equal(fs.t, uint64(wasi.ErrnoSuccess), res[0])

resFd, ok := fs.memory.ReadUint32Le(testCtx, resultOpenedFd)
require.True(fs.t, ok)

return &wasiFile{fd: resFd, fs: fs}, nil
}

// wasiFile implements io.Reader and io.Seeker using wasi functions. It does not
// implement io.ReaderAt because there is no wasi function for directly reading
// from an offset.
type wasiFile struct {
fd uint32
fs *wasiFs
}

func (f *wasiFile) Stat() (fs.FileInfo, error) {
// We currently don't implement wasi's fd_stat but also don't use this method from this test.
panic("unused")
}

func (f *wasiFile) Read(bytes []byte) (int, error) {
// Pick anywhere in memory for wasm to write resultSize too. We do this first since it's fixed length
// while iovs is variable.
resultSizeOff := uint32(0)
// Next place iovs
iovsOff := uint32(4)
// We do not directly write to hardware, there is no need for more than one iovec
iovsCount := uint32(1)
// iov starts at iovsOff + 8 because we first write four bytes for the offset itself, and
// four bytes for the length of the iov.
iovOff := iovsOff + uint32(8)
ok := f.fs.memory.WriteUint32Le(testCtx, iovsOff, iovOff)
require.True(f.fs.t, ok)
// Next write the length.
ok = f.fs.memory.WriteUint32Le(testCtx, iovsOff+uint32(4), uint32(len(bytes)))
require.True(f.fs.t, ok)

res, err := f.fs.fdRead.Call(testCtx, uint64(f.fd), uint64(iovsOff), uint64(iovsCount), uint64(resultSizeOff))
require.NoError(f.fs.t, err)

require.NotEqual(f.fs.t, uint64(wasi.ErrnoFault), res[0])

numRead, ok := f.fs.memory.ReadUint32Le(testCtx, resultSizeOff)
require.True(f.fs.t, ok)

if numRead == 0 {
if len(bytes) == 0 {
return 0, nil
}
if wasi.Errno(res[0]) == wasi.ErrnoSuccess {
return 0, io.EOF
} else {
return 0, fmt.Errorf("could not read from file")
}
}

buf, ok := f.fs.memory.Read(testCtx, iovOff, numRead)
require.True(f.fs.t, ok)
copy(bytes, buf)
return int(numRead), nil
}

func (f *wasiFile) Close() error {
res, err := f.fs.fdClose.Call(testCtx, uint64(f.fd))
require.NoError(f.fs.t, err)
require.NotEqual(f.fs.t, uint64(wasi.ErrnoFault), res[0])
return nil
}

func (f *wasiFile) Seek(offset int64, whence int) (int64, error) {
// Pick anywhere in memory for wasm to write the result newOffset to
resultNewoffsetOff := uint32(0)

res, err := f.fs.fdSeek.Call(testCtx, uint64(f.fd), uint64(offset), uint64(whence), uint64(resultNewoffsetOff))
require.NoError(f.fs.t, err)
require.NotEqual(f.fs.t, uint64(wasi.ErrnoFault), res[0])

newOffset, ok := f.fs.memory.ReadUint32Le(testCtx, resultNewoffsetOff)
require.True(f.fs.t, ok)

return int64(newOffset), nil
}

func TestReader(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)

_, err := wasi.InstantiateSnapshotPreview1(testCtx, r)
require.NoError(t, err)

realFs := fstest.MapFS{"animals.txt": &fstest.MapFile{Data: animals}}
sys := wazero.NewModuleConfig().WithWorkDirFS(realFs)

// Create a module that just delegates to wasi functions.
compiled, err := r.CompileModule(testCtx, []byte(`(module
(import "wasi_snapshot_preview1" "path_open"
(func $wasi.path_open (param $fd i32) (param $dirflags i32) (param $path i32) (param $path_len i32) (param $oflags i32) (param $fs_rights_base i64) (param $fs_rights_inheriting i64) (param $fdflags i32) (param $result.opened_fd i32) (result (;errno;) i32)))
(import "wasi_snapshot_preview1" "fd_close"
(func $wasi.fd_close (param $fd i32) (result (;errno;) i32)))
(import "wasi_snapshot_preview1" "fd_read"
(func $wasi.fd_read (param $fd i32) (param $iovs i32) (param $iovs_len i32) (param $result.size i32) (result (;errno;) i32)))
(import "wasi_snapshot_preview1" "fd_seek"
(func $wasi.fd_seek (param $fd i32) (param $offset i64) (param $whence i32) (param $result.newoffset i32) (result (;errno;) i32)))
(memory 1 1) ;; just an arbitrary size big enough for tests
(export "memory" (memory 0))
(export "path_open" (func $wasi.path_open))
(export "fd_close" (func $wasi.fd_close))
(export "fd_read" (func $wasi.fd_read))
(export "fd_seek" (func $wasi.fd_seek))
)`), wazero.NewCompileConfig())
require.NoError(t, err)

mod, err := r.InstantiateModule(testCtx, compiled, sys)
require.NoError(t, err)

pathOpen := mod.ExportedFunction("path_open")
fdClose := mod.ExportedFunction("fd_close")
fdRead := mod.ExportedFunction("fd_read")
fdSeek := mod.ExportedFunction("fd_seek")

wasiFs := &wasiFs{
t: t,
wasm: r,
memory: mod.Memory(),
workdirFd: uint32(3),
pathOpen: pathOpen,
fdClose: fdClose,
fdRead: fdRead,
fdSeek: fdSeek,
}

f, err := wasiFs.Open("animals.txt")
require.NoError(t, err)
defer f.Close()

err = iotest.TestReader(f, animals)
require.NoError(t, err)
}
5 changes: 5 additions & 0 deletions internal/integration_test/fs/testdata/animals.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
bear
cat
shark
dinosaur
human