Files
wazero/tests/spectest/spec_test.go
Crypt Keeper f5598c9a8e Fixes global numeric types to have max of signed encoding (#442)
This adjusts towards the exiting code which used int32/64 instead of
uint32/64. The reason is that the spec indicates intepretation as signed
numbers, which affects the maximum value.

See https://www.w3.org/TR/wasm-core-1/#value-types%E2%91%A2

Signed-off-by: Adrian Cole <adrian@tetrate.io>
2022-04-06 06:35:31 +08:00

512 lines
16 KiB
Go

package spectests
import (
"context"
"embed"
"encoding/json"
"fmt"
"math"
"runtime"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/require"
"github.com/tetratelabs/wazero/api"
"github.com/tetratelabs/wazero/internal/leb128"
"github.com/tetratelabs/wazero/internal/u64"
"github.com/tetratelabs/wazero/internal/wasm"
"github.com/tetratelabs/wazero/internal/wasm/binary"
"github.com/tetratelabs/wazero/internal/wasm/interpreter"
"github.com/tetratelabs/wazero/internal/wasm/jit"
"github.com/tetratelabs/wazero/internal/wasm/text"
"github.com/tetratelabs/wazero/internal/wasmruntime"
)
//go:embed testdata/*.wasm
//go:embed testdata/*.json
var testcases embed.FS
// TODO: complete porting this to wazero API
type (
testbase struct {
SourceFile string `json:"source_filename"`
Commands []command `json:"commands"`
}
command struct {
CommandType string `json:"type"`
Line int `json:"line"`
// Set when type == "module" || "register"
Name string `json:"name,omitempty"`
// Set when type == "module" || "assert_uninstantiable" || "assert_malformed"
Filename string `json:"filename,omitempty"`
// Set when type == "register"
As string `json:"as,omitempty"`
// Set when type == "assert_return" || "action"
Action commandAction `json:"action,omitempty"`
Exps []commandActionVal `json:"expected"`
// Set when type == "assert_malformed"
ModuleType string `json:"module_type"`
// Set when type == "assert_trap"
Text string `json:"text"`
}
commandAction struct {
ActionType string `json:"type"`
Args []commandActionVal `json:"args"`
// Set when ActionType == "invoke"
Field string `json:"field,omitempty"`
Module string `json:"module,omitempty"`
}
commandActionVal struct {
ValType string `json:"type"`
Value string `json:"value"`
}
)
func (c commandActionVal) String() string {
var v string
switch c.ValType {
case "i32":
v = c.Value
case "f32":
ret, _ := strconv.ParseUint(c.Value, 10, 32)
v = fmt.Sprintf("%f", math.Float32frombits(uint32(ret)))
case "i64":
v = c.Value
case "f64":
ret, _ := strconv.ParseUint(c.Value, 10, 64)
v = fmt.Sprintf("%f", math.Float64frombits(ret))
}
return fmt.Sprintf("{type: %s, value: %v}", c.ValType, v)
}
func (c command) String() string {
msg := fmt.Sprintf("line: %d, type: %s", c.Line, c.CommandType)
switch c.CommandType {
case "register":
msg += fmt.Sprintf(", name: %s, as: %s", c.Name, c.As)
case "module":
if c.Name != "" {
msg += fmt.Sprintf(", name: %s, filename: %s", c.Name, c.Filename)
} else {
msg += fmt.Sprintf(", filename: %s", c.Filename)
}
case "assert_return", "action":
msg += fmt.Sprintf(", action type: %s", c.Action.ActionType)
if c.Action.Module != "" {
msg += fmt.Sprintf(", module: %s", c.Action.Module)
}
msg += fmt.Sprintf(", field: %s", c.Action.Field)
msg += fmt.Sprintf(", args: %v, expected: %v", c.Action.Args, c.Exps)
case "assert_malformed":
// TODO:
case "assert_trap":
msg += fmt.Sprintf(", args: %v, error text: %s", c.Action.Args, c.Text)
case "assert_invalid":
// TODO:
case "assert_exhaustion":
// TODO:
case "assert_unlinkable":
// TODO:
case "assert_uninstantiable":
// TODO:
}
return "{" + msg + "}"
}
func (c command) moduleName(lastInstantiatedModuleName string) string {
if c.Action.Module != "" {
// If the module name is specified for the command, it almost always starts with $, but
// it might be aliased as the one without $. To remove the necessity for "register"
// command, we always treat module name without $
return strings.TrimPrefix(c.Action.Module, "$")
}
return lastInstantiatedModuleName
}
func (c command) getAssertReturnArgs() []uint64 {
var args []uint64
for _, arg := range c.Action.Args {
args = append(args, arg.toUint64())
}
return args
}
func (c command) getAssertReturnArgsExps() ([]uint64, []uint64) {
var args, exps []uint64
for _, arg := range c.Action.Args {
args = append(args, arg.toUint64())
}
for _, exp := range c.Exps {
exps = append(exps, exp.toUint64())
}
return args, exps
}
func (c commandActionVal) toUint64() uint64 {
if strings.Contains(c.Value, "nan") {
if c.ValType == "f32" {
return uint64(math.Float32bits(float32(math.NaN())))
}
return math.Float64bits(math.NaN())
}
if strings.Contains(c.ValType, "32") {
ret, _ := strconv.ParseUint(c.Value, 10, 32)
return ret
} else {
ret, _ := strconv.ParseUint(c.Value, 10, 64)
return ret
}
}
// expectedError returns the expected runtime error when the command type equals assert_trap
// which expects engines to emit the errors corresponding command.Text field.
func (c command) expectedError() (err error) {
if c.CommandType != "assert_trap" {
panic("unreachable")
}
switch c.Text {
case "out of bounds memory access":
err = wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess
case "indirect call type mismatch", "indirect call":
err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch
case "undefined element", "undefined":
err = wasmruntime.ErrRuntimeInvalidTableAccess
case "integer overflow":
err = wasmruntime.ErrRuntimeIntegerOverflow
case "invalid conversion to integer":
err = wasmruntime.ErrRuntimeInvalidConversionToInteger
case "integer divide by zero":
err = wasmruntime.ErrRuntimeIntegerDivideByZero
case "unreachable":
err = wasmruntime.ErrRuntimeUnreachable
default:
if strings.HasPrefix(c.Text, "uninitialized") {
err = wasmruntime.ErrRuntimeInvalidTableAccess
}
}
return
}
// addSpectestModule adds a module that drops inputs and returns globals as 666 per the default test harness.
//
// See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast
// See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25
func addSpectestModule(t *testing.T, store *wasm.Store) {
mod, err := text.DecodeModule([]byte(`(module $spectest
(; TODO
(global (export "global_i32") i32)
(global (export "global_f32") f32)
(global (export "global_f64") f64)
(table (export "table") 10 20 funcref)
;)
;; TODO: revisit inlining after #215
(memory 1 2)
(export "memory" (memory 0))
;; Note: the following aren't host functions that print to console as it would clutter it. These only drop the inputs.
(func)
(export "print" (func 0))
(func (param i32) local.get 0 drop)
(export "print_i32" (func 1))
(func (param i64) local.get 0 drop)
(export "print_i64" (func 2))
(func (param f32) local.get 0 drop)
(export "print_f32" (func 3))
(func (param f64) local.get 0 drop)
(export "print_f64" (func 4))
(func (param i32 f32) local.get 0 drop local.get 1 drop)
(export "print_i32_f32" (func 5))
(func (param f64 f64) local.get 0 drop local.get 1 drop)
(export "print_f64_f64" (func 6))
)`), wasm.Features20191205, wasm.MemoryMaxPages)
require.NoError(t, err)
// (global (export "global_i32") i32 (i32.const 666))
mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32},
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeUint32(666)},
})
mod.ExportSection["global_i32"] = &wasm.Export{Name: "global_i32", Index: 0, Type: wasm.ExternTypeGlobal}
// (global (export "global_f32") f32 (f32.const 666))
mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeF32},
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: u64.LeBytes(api.EncodeF32(666))},
})
mod.ExportSection["global_f32"] = &wasm.Export{Name: "global_f32", Index: 1, Type: wasm.ExternTypeGlobal}
// (global (export "global_f64") f64 (f64.const 666))
mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{
Type: &wasm.GlobalType{ValType: wasm.ValueTypeF64},
Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: u64.LeBytes(api.EncodeF64(666))},
})
mod.ExportSection["global_f64"] = &wasm.Export{Name: "global_f64", Index: 2, Type: wasm.ExternTypeGlobal}
// (table (export "table") 10 20 funcref)
tableLimitMax := uint32(20)
mod.TableSection = &wasm.Table{Min: 10, Max: &tableLimitMax}
mod.ExportSection["table"] = &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable}
_, err = store.Instantiate(context.Background(), mod, mod.NameSection.ModuleName, wasm.DefaultSysContext())
require.NoError(t, err)
}
func TestJIT(t *testing.T) {
if runtime.GOARCH != "amd64" && runtime.GOARCH != "arm64" {
t.Skip()
}
runTest(t, jit.NewEngine)
}
func TestInterpreter(t *testing.T) {
runTest(t, interpreter.NewEngine)
}
func runTest(t *testing.T, newEngine func() wasm.Engine) {
files, err := testcases.ReadDir("testdata")
require.NoError(t, err)
jsonfiles := make([]string, 0, len(files))
for _, f := range files {
filename := f.Name()
if strings.HasSuffix(filename, ".json") {
jsonfiles = append(jsonfiles, testdataPath(filename))
}
}
// If the go:embed path resolution was wrong, this fails.
// https://github.com/tetratelabs/wazero/issues/247
require.Greater(t, len(jsonfiles), 1)
for _, f := range jsonfiles {
raw, err := testcases.ReadFile(f)
require.NoError(t, err)
var base testbase
require.NoError(t, json.Unmarshal(raw, &base))
wastName := basename(base.SourceFile)
t.Run(wastName, func(t *testing.T) {
store := wasm.NewStore(newEngine(), wasm.Features20191205)
addSpectestModule(t, store)
var lastInstantiatedModuleName string
for i, c := range base.Commands {
t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) {
msg := fmt.Sprintf("%s:%d %s", wastName, c.Line, c.CommandType)
switch c.CommandType {
case "module":
buf, err := testcases.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
mod, err := binary.DecodeModule(buf, wasm.Features20191205, wasm.MemoryMaxPages)
require.NoError(t, err, msg)
require.NoError(t, mod.Validate(wasm.Features20191205))
moduleName := c.Name
if moduleName == "" { // When "(module ...) directive doesn't have name.
if i+1 < len(base.Commands) && base.Commands[i+1].CommandType == "register" {
// If the next command is "(register ...)", we use that name for this module.
moduleName = base.Commands[i+1].As
} else {
// Otherwise, use the file name as the name.
moduleName = c.Filename
}
}
moduleName = strings.TrimPrefix(moduleName, "$")
_, err = store.Instantiate(context.Background(), mod, moduleName, nil)
lastInstantiatedModuleName = moduleName
require.NoError(t, err)
case "register":
case "assert_return", "action":
moduleName := c.moduleName(lastInstantiatedModuleName)
switch c.Action.ActionType {
case "invoke":
args, exps := c.getAssertReturnArgsExps()
msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
if c.Action.Module != "" {
msg += " in module " + c.Action.Module
}
vals, types, err := callFunction(store, moduleName, c.Action.Field, args...)
require.NoError(t, err, msg)
require.Equal(t, len(exps), len(vals), msg)
require.Equal(t, len(exps), len(types), msg)
for i, exp := range exps {
requireValueEq(t, vals[i], exp, types[i], msg)
}
case "get":
_, exps := c.getAssertReturnArgsExps()
require.Len(t, exps, 1)
msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
if c.Action.Module != "" {
msg += " in module " + c.Action.Module
}
module := store.Module(moduleName)
require.NotNil(t, module)
global := module.ExportedGlobal(c.Action.Field)
require.NotNil(t, global)
var expType wasm.ValueType
switch c.Exps[0].ValType {
case "i32":
expType = wasm.ValueTypeI32
case "i64":
expType = wasm.ValueTypeI64
case "f32":
expType = wasm.ValueTypeF32
case "f64":
expType = wasm.ValueTypeF64
}
require.Equal(t, expType, global.Type(), msg)
require.Equal(t, exps[0], global.Get(), msg)
default:
t.Fatalf("unsupported action type type: %v", c)
}
case "assert_malformed":
if c.ModuleType == "text" {
// We don't support direct loading of wast yet.
t.Skip()
}
buf, err := testcases.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
requireInstantiationError(t, store, buf, msg)
case "assert_trap":
moduleName := c.moduleName(lastInstantiatedModuleName)
switch c.Action.ActionType {
case "invoke":
args := c.getAssertReturnArgs()
msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
if c.Action.Module != "" {
msg += " in module " + c.Action.Module
}
_, _, err := callFunction(store, moduleName, c.Action.Field, args...)
require.ErrorIs(t, err, c.expectedError(), msg)
default:
t.Fatalf("unsupported action type type: %v", c)
}
case "assert_invalid":
if c.ModuleType == "text" {
// We don't support direct loading of wast yet.
t.Skip()
}
buf, err := testcases.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
requireInstantiationError(t, store, buf, msg)
case "assert_exhaustion":
moduleName := c.moduleName(lastInstantiatedModuleName)
switch c.Action.ActionType {
case "invoke":
args := c.getAssertReturnArgs()
msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
if c.Action.Module != "" {
msg += " in module " + c.Action.Module
}
_, _, err := callFunction(store, moduleName, c.Action.Field, args...)
require.ErrorIs(t, err, wasmruntime.ErrRuntimeCallStackOverflow, msg)
default:
t.Fatalf("unsupported action type type: %v", c)
}
case "assert_unlinkable":
if c.ModuleType == "text" {
// We don't support direct loading of wast yet.
t.Skip()
}
buf, err := testcases.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
requireInstantiationError(t, store, buf, msg)
case "assert_uninstantiable":
buf, err := testcases.ReadFile(testdataPath(c.Filename))
require.NoError(t, err, msg)
requireInstantiationError(t, store, buf, msg)
default:
t.Fatalf("unsupported command type: %s", c)
}
})
}
})
}
}
func requireInstantiationError(t *testing.T, store *wasm.Store, buf []byte, msg string) {
mod, err := binary.DecodeModule(buf, store.EnabledFeatures, wasm.MemoryMaxPages)
if err != nil {
return
}
err = mod.Validate(store.EnabledFeatures)
if err != nil {
return
}
_, err = store.Instantiate(context.Background(), mod, t.Name(), nil)
require.Error(t, err, msg)
}
// basename avoids filepath.Base to ensure a forward slash is used even in Windows.
// See https://pkg.go.dev/embed#hdr-Directives
func basename(path string) string {
lastSlash := strings.LastIndexByte(path, '/')
return path[lastSlash+1:]
}
// testdataPath avoids filepath.Join to ensure a forward slash is used even in Windows.
// See https://pkg.go.dev/embed#hdr-Directives
func testdataPath(filename string) string {
return fmt.Sprintf("testdata/%s", filename)
}
func requireValueEq(t *testing.T, actual, expected uint64, valType wasm.ValueType, msg string) {
switch valType {
case wasm.ValueTypeI32:
require.Equal(t, uint32(expected), uint32(actual), msg)
case wasm.ValueTypeI64:
require.Equal(t, expected, actual, msg)
case wasm.ValueTypeF32:
expF := math.Float32frombits(uint32(expected))
actualF := math.Float32frombits(uint32(actual))
if math.IsNaN(float64(expF)) { // NaN cannot be compared with themselves, so we have to use IsNaN
require.True(t, math.IsNaN(float64(actualF)), msg)
} else {
require.Equal(t, expF, actualF, msg)
}
case wasm.ValueTypeF64:
expF := math.Float64frombits(expected)
actualF := math.Float64frombits(actual)
if math.IsNaN(expF) { // NaN cannot be compared with themselves, so we have to use IsNaN
require.True(t, math.IsNaN(actualF), msg)
} else {
require.Equal(t, expF, actualF, msg)
}
default:
t.Fail()
}
}
// callFunction is inlined here as the spectest needs to validate the signature was correct
// TODO: This is likely already covered with unit tests!
func callFunction(s *wasm.Store, moduleName, funcName string, params ...uint64) ([]uint64, []wasm.ValueType, error) {
fn := s.Module(moduleName).ExportedFunction(funcName)
results, err := fn.Call(nil, params...)
return results, fn.ResultTypes(), err
}