Files
wazero/assemblyscript/assemblyscript_test.go
Crypt Keeper 9414b0bb74 Switches default random source to deterministic (#736)
This changes the default random source to provide deterministic values
similar to how nanotime and walltime do. This also prevents any worries
about if wasm can deplete the host's underlying source of entropy.

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-08-06 17:47:50 +08:00

445 lines
12 KiB
Go

package assemblyscript
import (
"bytes"
"context"
_ "embed"
"encoding/hex"
"errors"
"io"
"strings"
"testing"
"testing/iotest"
"unicode/utf16"
"github.com/tetratelabs/wazero"
"github.com/tetratelabs/wazero/api"
. "github.com/tetratelabs/wazero/experimental"
"github.com/tetratelabs/wazero/experimental/logging"
"github.com/tetratelabs/wazero/internal/testing/proxy"
"github.com/tetratelabs/wazero/internal/testing/require"
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/sys"
)
// 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
exporter FunctionExporter
expected string
}{
{
name: "enabled",
exporter: NewFunctionExporter(),
expected: "message at filename:1:2\n",
},
{
name: "disabled",
exporter: NewFunctionExporter().WithAbortMessageDisabled(),
expected: "",
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
var stderr bytes.Buffer
mod, r, log := requireProxyModule(t, tc.exporter, wazero.NewModuleConfig().WithStderr(&stderr))
defer r.Close(testCtx)
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), encodeUTF16("message"), encodeUTF16("filename"))
_, err := mod.ExportedFunction(functionAbort).
Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
require.Error(t, err)
sysErr, ok := err.(*sys.ExitError)
require.True(t, ok, err)
require.Equal(t, uint32(255), sysErr.ExitCode())
require.Equal(t, `
--> proxy.abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
`, "\n"+log.String())
require.Equal(t, tc.expected, stderr.String())
})
}
}
func TestAbort_Error(t *testing.T) {
var stderr bytes.Buffer
mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithStderr(&stderr))
defer r.Close(testCtx)
tests := []struct {
name string
messageUTF16 []byte
fileNameUTF16 []byte
expectedLog string
}{
{
name: "bad message",
messageUTF16: encodeUTF16("message")[:5],
fileNameUTF16: encodeUTF16("filename"),
expectedLog: `
--> proxy.abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
==> env.~lib/builtins/abort(message=4,fileName=13,lineNumber=1,columnNumber=2)
`,
},
{
name: "bad filename",
messageUTF16: encodeUTF16("message"),
fileNameUTF16: encodeUTF16("filename")[:5],
expectedLog: `
--> proxy.abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
==> env.~lib/builtins/abort(message=4,fileName=22,lineNumber=1,columnNumber=2)
`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
defer log.Reset()
defer stderr.Reset()
messageOff, filenameOff := writeAbortMessageAndFileName(t, mod.Memory(), tc.messageUTF16, tc.fileNameUTF16)
_, err := mod.ExportedFunction(functionAbort).
Call(testCtx, uint64(messageOff), uint64(filenameOff), uint64(1), uint64(2))
require.Error(t, err)
sysErr, ok := err.(*sys.ExitError)
require.True(t, ok, err)
require.Equal(t, uint32(255), sysErr.ExitCode())
require.Equal(t, tc.expectedLog, "\n"+log.String())
require.Equal(t, "", stderr.String()) // nothing output if strings fail to read.
})
}
}
func TestSeed(t *testing.T) {
mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig())
defer r.Close(testCtx)
ret, err := mod.ExportedFunction(functionSeed).Call(testCtx)
require.NoError(t, err)
require.Equal(t, `
--> proxy.seed()
==> env.~lib/builtins/seed()
<== (4.958153677776298e-175)
<-- (4.958153677776298e-175)
`, "\n"+log.String())
require.Equal(t, "538c7f96b164bf1b", hex.EncodeToString(u64.LeBytes(ret[0])))
}
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 random seed: unexpected EOF (recovered by wazero)
wasm stack trace:
env.~lib/builtins/seed() f64
proxy.seed() f64`,
},
{
name: "error reading",
source: iotest.ErrReader(errors.New("ice cream")),
expectedErr: `error reading random seed: ice cream (recovered by wazero)
wasm stack trace:
env.~lib/builtins/seed() f64
proxy.seed() f64`,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
mod, r, log := requireProxyModule(t, NewFunctionExporter(), wazero.NewModuleConfig().WithRandSource(tc.source))
defer r.Close(testCtx)
_, err := mod.ExportedFunction(functionSeed).Call(testCtx)
require.EqualError(t, err, tc.expectedErr)
require.Equal(t, `
--> proxy.seed()
==> env.~lib/builtins/seed()
`, "\n"+log.String())
})
}
}
// TestFunctionExporter_Trace ensures the trace output is according to configuration.
func TestFunctionExporter_Trace(t *testing.T) {
noArgs := []uint64{4, 0, 0, 0, 0, 0, 0}
noArgsLog := `
--> proxy.trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
==> env.~lib/builtins/trace(message=4,nArgs=0,arg0=0,arg1=0,arg2=0,arg3=0,arg4=0)
<== ()
<-- ()
`
tests := []struct {
name string
exporter FunctionExporter
params []uint64
message []byte
outErr bool
expected, expectedLog string
}{
{
name: "disabled",
exporter: NewFunctionExporter(),
params: noArgs,
expected: "",
// expect no host call since it is disabled. ==> is host and --> is wasm.
expectedLog: strings.ReplaceAll(noArgsLog, "==", "--"),
},
{
name: "ToStderr",
exporter: NewFunctionExporter().WithTraceToStderr(),
params: noArgs,
expected: "trace: hello\n",
expectedLog: noArgsLog,
},
{
name: "ToStdout - no args",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: noArgs,
expected: "trace: hello\n",
expectedLog: noArgsLog,
},
{
name: "ToStdout - one arg",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 1, api.EncodeF64(1), 0, 0, 0, 0},
expected: "trace: hello 1\n",
expectedLog: `
--> proxy.trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
==> env.~lib/builtins/trace(message=4,nArgs=1,arg0=1,arg1=0,arg2=0,arg3=0,arg4=0)
<== ()
<-- ()
`,
},
{
name: "ToStdout - two args",
exporter: NewFunctionExporter().WithTraceToStdout(),
params: []uint64{4, 2, api.EncodeF64(1), api.EncodeF64(2), 0, 0, 0},
expected: "trace: hello 1,2\n",
expectedLog: `
--> proxy.trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
==> env.~lib/builtins/trace(message=4,nArgs=2,arg0=1,arg1=2,arg2=0,arg3=0,arg4=0)
<== ()
<-- ()
`,
},
{
name: "ToStdout - five args",
exporter: NewFunctionExporter().WithTraceToStdout(),
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",
expectedLog: `
--> proxy.trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
==> env.~lib/builtins/trace(message=4,nArgs=5,arg0=1,arg1=2,arg2=3.3,arg3=4.4,arg4=5)
<== ()
<-- ()
`,
},
{
name: "not 8 bytes",
exporter: NewFunctionExporter().WithTraceToStderr(),
message: encodeUTF16("hello")[:5],
params: noArgs,
expectedLog: noArgsLog,
},
{
name: "error writing",
exporter: NewFunctionExporter().WithTraceToStderr(),
outErr: true,
params: noArgs,
expectedLog: noArgsLog,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
var out bytes.Buffer
config := wazero.NewModuleConfig()
if strings.Contains("ToStderr", tc.name) {
config = config.WithStderr(&out)
} else {
config = config.WithStdout(&out)
}
if tc.outErr {
config = config.WithStderr(&errWriter{err: errors.New("ice cream")})
}
mod, r, log := requireProxyModule(t, tc.exporter, config)
defer r.Close(testCtx)
message := tc.message
if message == nil {
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(functionTrace).Call(testCtx, tc.params...)
require.NoError(t, err)
require.Equal(t, tc.expected, out.String())
require.Equal(t, tc.expectedLog, "\n"+log.String())
})
}
}
func Test_readAssemblyScriptString(t *testing.T) {
tests := []struct {
name string
memory func(context.Context, api.Memory)
offset int
expected string
expectedOk bool
}{
{
name: "success",
memory: func(testCtx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 10)
b := encodeUTF16("hello")
memory.Write(testCtx, 4, b)
},
offset: 4,
expected: "hello",
expectedOk: true,
},
{
name: "can't read size",
memory: func(testCtx context.Context, memory api.Memory) {
b := encodeUTF16("hello")
memory.Write(testCtx, 0, b)
},
offset: 0, // will attempt to read size from offset -4
expectedOk: false,
},
{
name: "odd size",
memory: func(testCtx context.Context, memory api.Memory) {
memory.WriteUint32Le(testCtx, 0, 9)
b := encodeUTF16("hello")
memory.Write(testCtx, 4, b)
},
offset: 4,
expectedOk: false,
},
{
name: "can't read string",
memory: func(testCtx context.Context, 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,
expectedOk: false,
},
}
for _, tt := range tests {
tc := tt
t.Run(tc.name, func(t *testing.T) {
mem := wasm.NewMemoryInstance(&wasm.Memory{Min: 1, Cap: 1, Max: 1})
tc.memory(testCtx, mem)
s, ok := readAssemblyScriptString(testCtx, mem, uint32(tc.offset))
require.Equal(t, tc.expectedOk, ok)
require.Equal(t, tc.expected, s)
})
}
}
func writeAbortMessageAndFileName(t *testing.T, mem api.Memory, messageUTF16, fileNameUTF16 []byte) (uint32, uint32) {
off := uint32(0)
ok := mem.WriteUint32Le(testCtx, off, uint32(len(messageUTF16)))
require.True(t, ok)
off += 4
messageOff := off
ok = mem.Write(testCtx, off, messageUTF16)
require.True(t, ok)
off += uint32(len(messageUTF16))
ok = mem.WriteUint32Le(testCtx, off, uint32(len(fileNameUTF16)))
require.True(t, ok)
off += 4
filenameOff := off
ok = mem.Write(testCtx, off, fileNameUTF16)
require.True(t, ok)
return messageOff, filenameOff
}
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
}
func requireProxyModule(t *testing.T, fns FunctionExporter, config wazero.ModuleConfig) (api.Module, api.Closer, *bytes.Buffer) {
var log bytes.Buffer
// Set context to one that has an experimental listener
ctx := context.WithValue(testCtx, FunctionListenerFactoryKey{}, logging.NewLoggingListenerFactory(&log))
r := wazero.NewRuntimeWithConfig(wazero.NewRuntimeConfigInterpreter())
builder := r.NewModuleBuilder("env")
fns.ExportFunctions(builder)
envCompiled, err := builder.Compile(ctx, wazero.NewCompileConfig())
require.NoError(t, err)
_, err = r.InstantiateModule(ctx, envCompiled, config)
require.NoError(t, err)
proxyBin := proxy.GetProxyModuleBinary("env", envCompiled)
proxyCompiled, err := r.CompileModule(ctx, proxyBin, wazero.NewCompileConfig())
require.NoError(t, err)
mod, err := r.InstantiateModule(ctx, proxyCompiled, config)
require.NoError(t, err)
return mod, r, &log
}