Files
wazero/wasm/spectests/spec_test.go
2021-10-15 14:01:08 +09:00

388 lines
11 KiB
Go

package spec
import (
"encoding/json"
"fmt"
"math"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/mathetake/gasm/wasm"
)
type (
testbase struct {
SourceFile string `json:"source_filename"`
Commands []command `json:"commands"`
}
command struct {
CommandType string `json:"type"`
Line int `json:"line"`
// type == "module" || "register"
Name string `json:"name,omitempty"`
// type == "module" || "assert_uninstantiable" || "assert_malformed"
Filename string `json:"filename,omitempty"`
// type == "register"
As string `json:"as,omitempty"`
// type == "assert_return" || "action"
Action commandAction `json:"action,omitempty"`
Exps []commandActionVal `json:"expected"`
// type == "assert_malformed"
ModuleType string `json:"module_type"`
}
commandAction struct {
ActionType string `json:"type"`
Args []commandActionVal `json:"args"`
// 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":
// TODO:
case "assert_invalid":
// TODO:
case "assert_exhaustion":
// TODO:
case "assert_unlinkable":
// TODO:
case "assert_uninstantiable":
// TODO:
}
return "{" + msg + "}"
}
func (c command) getAssertReturnArgs(t *testing.T) []uint64 {
var args []uint64
for _, arg := range c.Action.Args {
args = append(args, arg.ToUint64(t))
}
return args
}
func (c command) getAssertReturnArgsExps(t *testing.T) ([]uint64, []uint64) {
var args, exps []uint64
for _, arg := range c.Action.Args {
args = append(args, arg.ToUint64(t))
}
for _, exp := range c.Exps {
exps = append(exps, exp.ToUint64(t))
}
return args, exps
}
func (v commandActionVal) ToUint64(t *testing.T) uint64 {
if strings.Contains(v.Value, "nan") {
if v.ValType == "f32" {
return uint64(math.Float32bits(float32(math.NaN())))
}
return math.Float64bits(math.NaN())
}
if strings.Contains(v.ValType, "32") {
ret, err := strconv.ParseUint(v.Value, 10, 32)
require.NoError(t, err)
return ret
} else {
ret, err := strconv.ParseUint(v.Value, 10, 64)
require.NoError(t, err)
return ret
}
}
func requireAddSpectestModule(t *testing.T, vm *wasm.VirtualMachine) {
// Add functions
err := vm.AddHostFunction("spectest", "print", reflect.ValueOf(func(*wasm.VirtualMachine) {}))
require.NoError(t, err)
err = vm.AddHostFunction("spectest", "print_i32", reflect.ValueOf(func(*wasm.VirtualMachine, uint32) {}))
require.NoError(t, err)
err = vm.AddHostFunction("spectest", "print_f32", reflect.ValueOf(func(*wasm.VirtualMachine, float32) {}))
require.NoError(t, err)
err = vm.AddHostFunction("spectest", "print_i64", reflect.ValueOf(func(*wasm.VirtualMachine, uint64) {}))
require.NoError(t, err)
err = vm.AddHostFunction("spectest", "print_f64", reflect.ValueOf(func(*wasm.VirtualMachine, float64) {}))
require.NoError(t, err)
err = vm.AddHostFunction("spectest", "print_i32_f32", reflect.ValueOf(func(*wasm.VirtualMachine, uint32, float32) {}))
require.NoError(t, err)
err = vm.AddHostFunction("spectest", "print_f64_f64", reflect.ValueOf(func(*wasm.VirtualMachine, float64, float64) {}))
require.NoError(t, err)
// Register globals.
for _, g := range []struct {
name string
valueType wasm.ValueType
value uint64
}{
{name: "global_i32", valueType: wasm.ValueTypeI32, value: uint64(int32(666))},
{name: "global_i64", valueType: wasm.ValueTypeI64, value: uint64(int64(666))},
{name: "global_f32", valueType: wasm.ValueTypeF32, value: uint64(uint32(0x44268000))},
{name: "global_f64", valueType: wasm.ValueTypeF64, value: uint64(0x4084d00000000000)},
} {
err = vm.AddGlobal("spectest", g.name, g.value, g.valueType, false)
require.NoError(t, err)
}
// Register table export.
tableLimitMax := uint32(20)
err = vm.AddTableInstance("spectest", "table", 10, &tableLimitMax)
require.NoError(t, err)
// Register table export.
memoryLimitMax := uint32(2)
err = vm.AddMemoryInstance("spectest", "memory", 1, &memoryLimitMax)
require.NoError(t, err)
}
func TestSpecification(t *testing.T) {
const caseDir = "./cases"
files, err := os.ReadDir(caseDir)
require.NoError(t, err)
for _, f := range files {
if filepath.Ext(f.Name()) != ".json" {
continue
}
jsonPath := filepath.Join(caseDir, f.Name())
raw, err := os.ReadFile(jsonPath)
require.NoError(t, err)
var base testbase
require.NoError(t, json.Unmarshal(raw, &base))
wastName := filepath.Base(base.SourceFile)
t.Run(wastName, func(t *testing.T) {
vm, err := wasm.NewVM()
require.NoError(t, err)
requireAddSpectestModule(t, vm)
var lastInstanceName string
for _, c := range base.Commands {
t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) {
msg := fmt.Sprintf("%s:%d", wastName, c.Line)
switch c.CommandType {
case "module":
buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
require.NoError(t, err, msg)
mod, err := wasm.DecodeModule(buf)
require.NoError(t, err, msg)
lastInstanceName = c.Name
if lastInstanceName == "" {
lastInstanceName = c.Filename
}
err = vm.InstantiateModule(mod, lastInstanceName)
require.NoError(t, err)
case "register":
name := lastInstanceName
if c.Name != "" {
name = c.Name
}
vm.Store.ModuleInstances[c.As] = vm.Store.ModuleInstances[name]
case "assert_return", "action":
moduleName := lastInstanceName
if c.Action.Module != "" {
moduleName = c.Action.Module
}
require.NotNil(t, vm)
switch c.Action.ActionType {
case "invoke":
args, exps := c.getAssertReturnArgsExps(t)
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 := vm.ExecExportedFunction(moduleName, c.Action.Field, args...)
if assert.NoError(t, err, msg) &&
assert.Equal(t, len(exps), len(vals), msg) &&
assert.Equal(t, len(exps), len(types), msg) {
for i, exp := range exps {
assertValueEq(t, vals[i], exp, types[i], msg)
}
}
case "get":
_, exps := c.getAssertReturnArgsExps(t)
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
}
inst, ok := vm.Store.ModuleInstances[moduleName]
require.True(t, ok, msg)
addr := inst.Exports[c.Action.Field]
if addr.Kind != wasm.ExportKindGlobal {
t.Fatal()
}
actual := vm.Store.Globals[addr.Addr]
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.NotNil(t, actual, msg)
assert.Equal(t, expType, actual.Type.ValType, msg)
assert.Equal(t, exps[0], actual.Val, expType, 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 := os.ReadFile(filepath.Join(caseDir, c.Filename))
require.NoError(t, err, msg)
mod, err := wasm.DecodeModule(buf)
if err == nil {
err = vm.InstantiateModule(mod, "")
}
require.Error(t, err, msg)
case "assert_trap":
moduleName := lastInstanceName
if c.Action.Module != "" {
moduleName = c.Action.Module
}
require.NotNil(t, vm)
switch c.Action.ActionType {
case "invoke":
args := c.getAssertReturnArgs(t)
msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
if c.Action.Module != "" {
msg += " in module " + c.Action.Module
}
assert.Panics(t, func() {
_, _, _ = vm.ExecExportedFunction(moduleName, c.Action.Field, args...)
}, 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 := os.ReadFile(filepath.Join(caseDir, c.Filename))
require.NoError(t, err, msg)
mod, err := wasm.DecodeModule(buf)
if err == nil {
err = vm.InstantiateModule(mod, "")
}
require.Error(t, err, msg)
case "assert_exhaustion":
// TODO:
case "assert_unlinkable":
if c.ModuleType == "text" {
// We don't support direct loading of wast yet.
t.Skip()
}
buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
require.NoError(t, err, msg)
mod, err := wasm.DecodeModule(buf)
if err == nil {
err = vm.InstantiateModule(mod, "")
}
require.Error(t, err, msg)
case "assert_uninstantiable":
buf, err := os.ReadFile(filepath.Join(caseDir, c.Filename))
require.NoError(t, err, msg)
mod, err := wasm.DecodeModule(buf)
require.NoError(t, err, msg)
err = vm.InstantiateModule(mod, "")
require.Error(t, err, msg)
default:
t.Fatalf("unsupported command type: %s", c)
}
})
}
})
}
}
func assertValueEq(t *testing.T, actual, expected uint64, valType wasm.ValueType, msg string) {
switch valType {
case wasm.ValueTypeI32:
assert.Equal(t, uint32(expected), uint32(actual), msg)
case wasm.ValueTypeI64:
assert.Equal(t, expected, actual, msg)
case wasm.ValueTypeF32:
expF := math.Float32frombits(uint32(expected))
actualF := math.Float32frombits(uint32(actual))
if math.IsNaN(float64(expF)) {
assert.True(t, math.IsNaN(float64(actualF)), msg)
} else {
assert.Equal(t, expF, actualF, msg)
}
case wasm.ValueTypeF64:
expF := math.Float64frombits(expected)
actualF := math.Float64frombits(actual)
if math.IsNaN(expF) {
assert.True(t, math.IsNaN(actualF), msg)
} else {
assert.Equal(t, expF, actualF, msg)
}
default:
t.Fail()
}
}