This drops the text format (%.wat) and renames InstantiateModuleFromCode to InstantiateModuleFromBinary as it is no longer ambiguous. We decided to stop supporting the text format as it isn't typically used in production, yet costs a lot of work to develop. Given the resources available and the increased work added with WebAssembly 2.0 and soon WASI 2, we can't afford to spend the time on it. The old parser is used only internally and will eventually be moved to its own repository named watzero, possibly towards archival. See #59 Signed-off-by: Adrian Cole <adrian@tetrate.io>
486 lines
12 KiB
Go
486 lines
12 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/internal/watzero"
|
|
"github.com/tetratelabs/wazero/sys"
|
|
)
|
|
|
|
var abortWat = `(module
|
|
(import "env" "abort" (func $~lib/builtins/abort (param i32 i32 i32 i32)))
|
|
(memory 1 1)
|
|
(export "abort" (func 0))
|
|
)`
|
|
|
|
var seedWat = `(module
|
|
(import "env" "seed" (func $~lib/builtins/seed (result f64)))
|
|
(memory 1 1)
|
|
(export "seed" (func 0))
|
|
)`
|
|
|
|
var traceWat = `(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 := NewBuilder(r).WithAbortMessageDisabled().Instantiate(testCtx, r)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
abortWasm, err := watzero.Wat2Wasm(abortWat)
|
|
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)
|
|
|
|
seedWasm, err := watzero.Wat2Wasm(seedWat)
|
|
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 := NewBuilder(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, r)
|
|
require.NoError(t, err)
|
|
|
|
traceWasm, err := watzero.Wat2Wasm(traceWat)
|
|
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, r)
|
|
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)
|
|
|
|
abortWasm, err := watzero.Wat2Wasm(abortWat)
|
|
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)
|
|
|
|
seedWasm, err := watzero.Wat2Wasm(seedWat)
|
|
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 := NewBuilder(r).WithTraceToStdout().Instantiate(testCtx, r)
|
|
require.NoError(t, err)
|
|
|
|
traceWasm, err := watzero.Wat2Wasm(traceWat)
|
|
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
|
|
}
|