Files
wazero/assemblyscript/assemblyscript_test.go
Crypt Keeper 7fec94b2e1 experimental: consolidates context patterns (#579)
This consolidates the pattern used for context overrides, notably
replacing clock overrides via experimental.WithTimeNowUnixNano
and making all context keys internal.

This also makes sure experimental example tests are handled the same
way, notably backfilling one for WithFS
2022-05-26 18:22:29 -07:00

467 lines
11 KiB
Go

package assemblyscript
import (
"bytes"
"context"
_ "embed"
"errors"
"io"
"testing"
"testing/iotest"
"unicode/utf16"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/sys"
)
var abortWasm = []byte(`(module
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
(memory 1 1)
(export "abort" (func 0))
)`)
var seedWasm = []byte(`(module
(import "env" "seed" (func $~lib/builtins/seed (result f64)))
(memory 1 1)
(export "seed" (func 0))
)`)
var traceWasm = []byte(`(module
(import "env" "trace" (func $~lib/builtins/trace (param i32 i32 f64 f64 f64 f64 f64)))
(memory 1 1)
(export "trace" (func 0))
)`)
// testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors.
var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary")
func TestAbort(t *testing.T) {
tests := []struct {
name string
enabled bool
expected string
}{
{
name: "enabled",
enabled: true,
expected: "message at filename:1:2\n",
},
{
name: "disabled",
enabled: false,
expected: "",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
out := &bytes.Buffer{}
if tc.enabled {
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
} else {
_, err := NewModuleBuilder(r).WithAbortMessageDisabled().Instantiate(testCtx)
require.NoError(t, err)
}
code, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithStderr(out))
require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
require.Error(t, err)
require.Equal(t, uint32(255), err.(*sys.ExitError).ExitCode())
require.Equal(t, tc.expected, out.String())
})
}
}
func TestSeed(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
seed := []byte{0, 1, 2, 3, 4, 5, 6, 7}
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
code, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, code, wazero.NewModuleConfig().WithRandSource(bytes.NewReader(seed)))
require.NoError(t, err)
seedFn := mod.ExportedFunction("seed")
res, err := seedFn.Call(testCtx)
require.NoError(t, err)
// If this test doesn't break, the seed is deterministic.
require.Equal(t, uint64(506097522914230528), res[0])
}
func TestTrace(t *testing.T) {
noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
tests := []struct {
name string
mode traceMode
params []uint64
expected string
}{
{
name: "stderr",
mode: traceStderr,
params: noArgs,
expected: "trace: hello\n",
},
{
name: "stdout",
mode: traceStdout,
params: noArgs,
expected: "trace: hello\n",
},
{
name: "disabled",
mode: traceDisabled,
params: noArgs,
expected: "",
},
{
name: "one",
mode: traceStdout,
params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
expected: "trace: hello 1\n",
},
{
name: "two",
mode: traceStdout,
params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
expected: "trace: hello 1,2\n",
},
{
name: "five",
mode: traceStdout,
params: []uint64{
4,
5,
api.EncodeF64(1),
api.EncodeF64(2),
api.EncodeF64(3.3),
api.EncodeF64(4.4),
api.EncodeF64(5),
},
expected: "trace: hello 1,2,3.3,4.4,5\n",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
out := &bytes.Buffer{}
as := NewModuleBuilder(r)
modConfig := wazero.NewModuleConfig()
switch tc.mode {
case traceStderr:
as = as.WithTraceToStderr()
modConfig = modConfig.WithStderr(out)
case traceStdout:
as = as.WithTraceToStdout()
modConfig = modConfig.WithStdout(out)
case traceDisabled:
// Set but not used
modConfig = modConfig.WithStderr(out)
modConfig = modConfig.WithStdout(out)
}
_, err := as.Instantiate(testCtx)
require.NoError(t, err)
code, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(testCtx, code, modConfig)
require.NoError(t, err)
message := encodeUTF16("hello")
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(message)))
require.True(t, ok)
ok = mod.Memory().Write(testCtx, uint32(4), message)
require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(testCtx, tc.params...)
require.NoError(t, err)
require.Equal(t, tc.expected, out.String())
})
}
}
func TestReadAssemblyScriptString(t *testing.T) {
tests := []struct {
name string
memory func(api.Memory)
offset int
expected, expectedErr string
}{
{
name: "success",
memory: func(memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10)
b := encodeUTF16("hello")
memory.Write(testCtx, 4, b)
},
offset: 4,
expected: "hello",
},
{
name: "can't read size",
memory: func(memory api.Memory) {
b := encodeUTF16("hello")
memory.Write(testCtx, 0, b)
},
offset: 0, // will attempt to read size from offset -4
expectedErr: "Memory.ReadUint32Le(4294967292) out of range",
},
{
name: "odd size",
memory: func(memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 9)
b := encodeUTF16("hello")
memory.Write(testCtx, 4, b)
},
offset: 4,
expectedErr: "read an odd number of bytes for utf-16 string: 9",
},
{
name: "can't read string",
memory: func(memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10_000_000) // set size to too large value
b := encodeUTF16("hello")
memory.Write(testCtx, 4, b)
},
offset: 4,
expectedErr: "Memory.Read(4, 10000000) out of range",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
mod, err := r.NewModuleBuilder("mod").
ExportMemory("memory", 1).
Instantiate(testCtx)
require.NoError(t, err)
tc.memory(mod.Memory())
s, err := readAssemblyScriptString(testCtx, mod, uint32(tc.offset))
if tc.expectedErr != "" {
require.EqualError(t, err, tc.expectedErr)
} else {
require.NoError(t, err)
require.Equal(t, tc.expected, s)
}
})
}
}
func TestAbort_error(t *testing.T) {
tests := []struct {
name string
messageUTF16 []byte
fileNameUTF16 []byte
}{
{
name: "bad message",
messageUTF16: encodeUTF16("message")[:5],
fileNameUTF16: encodeUTF16("filename"),
},
{
name: "bad filename",
messageUTF16: encodeUTF16("message"),
fileNameUTF16: encodeUTF16("filename")[:5],
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, abortWasm, wazero.NewCompileConfig())
require.NoError(t, err)
out := &bytes.Buffer{}
config := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(out)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
_, err = mod.ExportedFunction("abort").Call(testCtx, uint64(messageOff), uint64(filenameOff), 1, 2)
require.NoError(t, err)
require.Equal(t, "", out.String()) // nothing output if strings fail to read.
})
}
}
func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (int, int) {
off := 0
ok := mem.WriteUint32Le(testCtx, uint32(off), uint32(len(messageUTF16)))
require.True(t, ok)
off += 4
messageOff := off
ok = mem.Write(testCtx, uint32(off), messageUTF16)
require.True(t, ok)
off += len(messageUTF16)
ok = mem.WriteUint32Le(testCtx, uint32(off), uint32(len(fileNameUTF16)))
require.True(t, ok)
off += 4
filenameOff := off
ok = mem.Write(testCtx, uint32(off), fileNameUTF16)
require.True(t, ok)
return messageOff, filenameOff
}
func TestSeed_error(t *testing.T) {
tests := []struct {
name string
source io.Reader
expectedErr string
}{
{
name: "not 8 bytes",
source: bytes.NewReader([]byte{0, 1}),
expectedErr: `error reading Module.RandSource: unexpected EOF (recovered by wazero)
wasm stack trace:
env.seed() f64`,
},
{
name: "error reading",
source: iotest.ErrReader(errors.New("ice cream")),
expectedErr: `error reading Module.RandSource: ice cream (recovered by wazero)
wasm stack trace:
env.seed() f64`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := Instantiate(testCtx, r)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, seedWasm, wazero.NewCompileConfig())
require.NoError(t, err)
config := wazero.NewModuleConfig().WithName(t.Name()).WithRandSource(tc.source)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
_, err = mod.ExportedFunction("seed").Call(testCtx)
require.EqualError(t, err, tc.expectedErr)
})
}
}
func TestTrace_error(t *testing.T) {
tests := []struct {
name string
message []byte
out io.Writer
expectedErr string
}{
{
name: "not 8 bytes",
message: encodeUTF16("hello")[:5],
out: &bytes.Buffer{},
expectedErr: `read an odd number of bytes for utf-16 string: 5 (recovered by wazero)
wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
},
{
name: "error writing",
message: encodeUTF16("hello"),
out: &errWriter{err: errors.New("ice cream")},
expectedErr: `ice cream (recovered by wazero)
wasm stack trace:
env.trace(i32,i32,f64,f64,f64,f64,f64)`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
r := wazero.NewRuntime()
defer r.Close(testCtx)
_, err := NewModuleBuilder(r).WithTraceToStdout().Instantiate(testCtx)
require.NoError(t, err)
compiled, err := r.CompileModule(testCtx, traceWasm, wazero.NewCompileConfig())
require.NoError(t, err)
config := wazero.NewModuleConfig().WithName(t.Name()).WithStdout(tc.out)
mod, err := r.InstantiateModule(testCtx, compiled, config)
require.NoError(t, err)
ok := mod.Memory().WriteUint32Le(testCtx, 0, uint32(len(tc.message)))
require.True(t, ok)
ok = mod.Memory().Write(testCtx, uint32(4), tc.message)
require.True(t, ok)
_, err = mod.ExportedFunction("trace").Call(testCtx, 4, 0, 0, 0, 0, 0, 0)
require.EqualError(t, err, tc.expectedErr)
})
}
}
func encodeUTF16(s string) []byte {
runes := utf16.Encode([]rune(s))
b := make([]byte, len(runes)*2)
for i, r := range runes {
b[i*2] = byte(r)
b[i*2+1] = byte(r >> 8)
}
return b
}
type errWriter struct {
err error
}
func (w *errWriter) Write([]byte) (int, error) {
return 0, w.err
}