// Copyright 2017 The go-interpreter Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package exec_test import ( "encoding/json" "fmt" "math" "math/big" "os" "path/filepath" "reflect" "regexp" "strconv" "strings" "testing" "github.com/go-interpreter/wagon/exec" "github.com/go-interpreter/wagon/validate" "github.com/go-interpreter/wagon/wasm" ) const ( nonSpecTestsDir = "./testdata" specTestsDir = "./testdata/spec" ) type testCase struct { Function string `json:"function"` Args []string `json:"args"` Return string `json:"return"` Trap string `json:"trap"` RecoverPanic bool `json:"recoverpanic,omitempty"` ErrorMsg string `json:"errormsg"` } type file struct { FileName string `json:"file"` Tests []testCase `json:"tests"` } var reValue = regexp.MustCompile(`(.+)\:(.+)`) func parseFloat(str string, bitSize int) float64 { hexFloat, err := regexp.MatchString("0x", str) if err != nil { panic(err) } isNanOrInf, err := regexp.MatchString("(nan|inf)", str) if err != nil { panic(err) } if isNanOrInf { if strings.HasPrefix(str, "-") { str = strings.TrimPrefix(str, "-") if str == "inf" { return math.Inf(-1) } else if str == "nan" { return math.NaN() } } if str == "inf" { return math.Inf(+1) } else if str == "nan" { return math.NaN() } } if hexFloat { if strings.HasPrefix(str, "-0x") { str = strings.TrimPrefix(str, "-0x") if str == "inf" { return math.Inf(-1) } str = "-" + str } else { if str == "inf" { return math.Inf(+1) } else if str == "nan" { return math.NaN() } str = strings.TrimPrefix(str, "0x") } f, _, err := big.ParseFloat(str, 16, big.MaxPrec, big.ToNearestEven) if err != nil { panic(err) } n, _ := f.Float64() return n } n, err := strconv.ParseFloat(str, bitSize) if err != nil { panic(err) } return n } func TestParseFloat(t *testing.T) { tests := []struct { str string want float64 }{ {"0xf32", 3890.0}, {"3.4", 3.4}, {"0.0", 0.0}, {"-123.0", -123.0}, {"0x1.fffffep+127", 340282346638528859811704183484516925440.0}, {"nan", math.NaN()}, {"inf", math.Inf(+1)}, {"-inf", math.Inf(-1)}, } for _, test := range tests { f := parseFloat(test.str, 64) if f != test.want && !(math.IsNaN(test.want) && math.IsNaN(f)) { t.Errorf("unexpected value returned by parseFloat: got=%f want=%f", f, test.want) } } } func parseInt(str string, bitSize int) uint64 { isHex, _ := regexp.MatchString("0x", str) base := 10 if isHex { if strings.HasPrefix(str, "-") { str = strings.TrimPrefix(str, "-0x") str = "-" + str } else { str = strings.TrimPrefix(str, "0x") } base = 16 } n, err := strconv.ParseUint(str, base, bitSize) if err != nil { // try parsing as an int n2, err := strconv.ParseInt(str, base, bitSize) if err != nil { panic(err) } n = uint64(n2) } return n } func TestParseInt(t *testing.T) { tests := []struct { str string want uint64 }{ {"45", 45}, {"0", 0}, {"0xABADCAFEDEAD1DEA", 0xABADCAFEDEAD1DEA}, {"-1", 18446744073709551615}, } for _, test := range tests { i := parseInt(test.str, 64) if i != test.want { t.Errorf("unexpected value returned by parseInt: got=%d want=%d", i, test.want) } } } func parseValue(str string) interface{} { if str == "" { return nil } matches := reValue.FindStringSubmatch(str) if matches == nil { panic("Invalid value expression: " + str) } switch matches[1] { case "i32": n := parseInt(matches[2], 32) return uint32(n) case "i64": n := parseInt(matches[2], 64) return n case "f32": return float32(parseFloat(matches[2], 32)) case "f64": return parseFloat(matches[2], 64) default: panic("invalid value_type prefix " + matches[1]) } } func TestParseValue(t *testing.T) { tests := []struct { str string want interface{} }{ {"i64:45", uint64(45)}, {"f32:0.0", float32(0)}, {"f64:0x1.fffffep+127", float64(340282346638528859811704183484516925440.0)}, {"f64:inf", float64(math.Inf(+1))}, {"f32:inf", float32(math.Inf(+1))}, } for _, test := range tests { v := parseValue(test.str) if !reflect.DeepEqual(test.want, v) { t.Errorf("unexpected value returned by parseInt: got=%v want=%v", v, test.want) } } } func parseArgs(args []string) (arr []uint64) { for _, str := range args { v := parseValue(str) var n uint64 switch v.(type) { case uint64: n = v.(uint64) case uint32: n = uint64(v.(uint32)) case float32: n = uint64(math.Float32bits(v.(float32))) case float64: n = math.Float64bits(v.(float64)) default: panic(fmt.Sprintf("invalid value type: %v(%v)", reflect.TypeOf(v), v)) } arr = append(arr, n) } return arr } func fnString(fn string, args []string) string { if len(args) == 0 { return fn } return fmt.Sprintf("%s(%v)", fn, args) } func panics(fn func()) (panicked bool, msg string) { defer func() { r := recover() panicked = r != nil msg = fmt.Sprint(r) }() fn() return } func runTest(fileName string, testCases []testCase, t testing.TB) { file, err := os.Open(fileName) if err != nil { t.Fatal(err) } defer file.Close() module, err := wasm.ReadModule(file, nil) if err != nil { t.Fatal(err) } if err = validate.VerifyModule(module); err != nil { t.Fatalf("%s: %v", fileName, err) } vm, err := exec.NewVM(module) if err != nil { t.Fatalf("%s: %v", fileName, err) } b, ok := t.(*testing.B) for _, testCase := range testCases { var expected interface{} index := module.Export.Entries[testCase.Function].Index args := parseArgs(testCase.Args) if testCase.Return != "" { expected = parseValue(testCase.Return) } if testCase.Trap != "" { // don't benchmark tests that involve trapping the VM fn := func() { _, err := vm.ExecCode(int64(index), args...) if err != nil { t.Fatalf("%s, %s: %v", fileName, testCase.Function, err) } } if p, msg := panics(fn); p && msg != testCase.Trap { t.Errorf("%s, %s: unexpected trap message: got=%s, want=%s", fileName, fnString(testCase.Function, testCase.Args), msg, testCase.Trap) } continue } vm.RecoverPanic = (testCase.RecoverPanic == true) times := 1 if ok { times = b.N b.ResetTimer() } var res interface{} var err error for i := 0; i < times; i++ { res, err = vm.ExecCode(int64(index), args...) } if ok { b.StopTimer() } if err != nil && err.Error() != testCase.ErrorMsg { t.Fatalf("%s, %s: %v", fileName, testCase.Function, err) } nanEq := false if reflect.TypeOf(res) == reflect.TypeOf(expected) { switch v := expected.(type) { case float32: nanEq = math.IsNaN(float64(v)) && math.IsNaN(float64(res.(float32))) case float64: nanEq = math.IsNaN(v) && math.IsNaN(res.(float64)) } } if nanEq { continue } if !reflect.DeepEqual(res, expected) { t.Fatalf("%s, %s (%d): unexpected return value: got=%v(%v), want=%v(%v) (%s)", fileName, fnString(testCase.Function, testCase.Args), index, reflect.TypeOf(res), res, reflect.TypeOf(expected), expected, testCase.Return) } } } func testModules(t *testing.T, dir string) { files := []file{} file, err := os.Open(filepath.Join(dir, "modules.json")) if err != nil { t.Fatal(err) } defer file.Close() err = json.NewDecoder(file).Decode(&files) if err != nil { t.Fatal(err) } for _, file := range files { fileName := filepath.Join(dir, file.FileName) testCases := file.Tests t.Run(fileName, func(t *testing.T) { t.Parallel() path, err := filepath.Abs(fileName) if err != nil { t.Fatal(err) } runTest(path, testCases, t) }) } } func BenchmarkModules(b *testing.B) { files := []file{} file, err := os.Open(filepath.Join("testdata/spec", "modules.json")) if err != nil { b.Fatal(err) } defer file.Close() err = json.NewDecoder(file).Decode(&files) if err != nil { b.Fatal(err) } for _, file := range files { fileName := filepath.Join("testdata/spec", file.FileName) testCases := file.Tests b.Run(fileName, func(b *testing.B) { path, err := filepath.Abs(fileName) if err != nil { b.Fatal(err) } runTest(path, testCases, b) }) } } func TestNonSpec(t *testing.T) { testModules(t, nonSpecTestsDir) } func TestSpec(t *testing.T) { testModules(t, specTestsDir) }