From 3a6cabfb8a3883b7eba79e2f1835bfddeba8f55c Mon Sep 17 00:00:00 2001 From: Crypt Keeper <64215+codefromthecrypt@users.noreply.github.com> Date: Mon, 4 Apr 2022 19:47:51 +0800 Subject: [PATCH] Extracts stack trace formatting logic and adds more context (#434) Signed-off-by: Adrian Cole --- internal/testing/enginetest/enginetest.go | 65 +++++----- internal/wasm/host.go | 16 ++- internal/wasm/interpreter/interpreter.go | 28 ++--- internal/wasm/jit/engine.go | 28 +---- internal/wasm/module.go | 14 ++- internal/wasm/module_test.go | 8 +- internal/wasm/store.go | 6 +- internal/wasmdebug/debug.go | 144 +++++++++++++++++++++ internal/wasmdebug/debug_test.go | 145 ++++++++++++++++++++++ tests/engine/adhoc_test.go | 12 +- 10 files changed, 367 insertions(+), 99 deletions(-) create mode 100644 internal/wasmdebug/debug.go create mode 100644 internal/wasmdebug/debug_test.go diff --git a/internal/testing/enginetest/enginetest.go b/internal/testing/enginetest/enginetest.go index e22e46df..082cd134 100644 --- a/internal/testing/enginetest/enginetest.go +++ b/internal/testing/enginetest/enginetest.go @@ -27,6 +27,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/wasm" + "github.com/tetratelabs/wazero/internal/wasmdebug" ) type EngineTester interface { @@ -310,70 +311,70 @@ func RunTestModuleEngine_Call_Errors(t *testing.T, et EngineTester) { input: []uint64{0}, module: imported.Ctx, fn: imported.Exports[wasmFnName].Function, - expectedErr: `wasm runtime error: integer divide by zero -wasm backtrace: - 0: wasm_div_by`, + expectedErr: `wasm error: integer divide by zero +wasm stack trace: + imported.wasm_div_by(i32) i32`, }, { name: "host function that panics", input: []uint64{math.MaxUint32}, module: imported.Ctx, fn: imported.Exports[hostFnName].Function, - expectedErr: `wasm runtime error: host-function panic -wasm backtrace: - 0: host_div_by`, + expectedErr: `host-function panic (recovered by wazero) +wasm stack trace: + imported.host_div_by(i32) i32`, }, { name: "host function panics with runtime.Error", input: []uint64{0}, module: imported.Ctx, - fn: imported.Exports[hostFnName].Function, // TODO: This should be a normal runtime error - expectedErr: `wasm runtime error: runtime error: integer divide by zero -wasm backtrace: - 0: host_div_by`, + fn: imported.Exports[hostFnName].Function, + expectedErr: `runtime error: integer divide by zero (recovered by wazero) +wasm stack trace: + imported.host_div_by(i32) i32`, }, { name: "wasm calls host function that panics", input: []uint64{math.MaxUint32}, module: imported.Ctx, fn: imported.Exports[callHostFnName].Function, - expectedErr: `wasm runtime error: host-function panic -wasm backtrace: - 0: host_div_by - 1: call->host_div_by`, + expectedErr: `host-function panic (recovered by wazero) +wasm stack trace: + imported.host_div_by(i32) i32 + imported.call->host_div_by(i32) i32`, }, { name: "wasm calls imported wasm that calls host function panics with runtime.Error", input: []uint64{0}, module: importing.Ctx, fn: importing.Exports[callImportCallHostFnName].Function, - expectedErr: `wasm runtime error: runtime error: integer divide by zero -wasm backtrace: - 0: host_div_by - 1: call->host_div_by - 2: call_import->call->host_div_by`, + expectedErr: `runtime error: integer divide by zero (recovered by wazero) +wasm stack trace: + imported.host_div_by(i32) i32 + imported.call->host_div_by(i32) i32 + importing.call_import->call->host_div_by(i32) i32`, }, { name: "wasm calls imported wasm that calls host function that panics", input: []uint64{math.MaxUint32}, module: importing.Ctx, fn: importing.Exports[callImportCallHostFnName].Function, - expectedErr: `wasm runtime error: host-function panic -wasm backtrace: - 0: host_div_by - 1: call->host_div_by - 2: call_import->call->host_div_by`, + expectedErr: `host-function panic (recovered by wazero) +wasm stack trace: + imported.host_div_by(i32) i32 + imported.call->host_div_by(i32) i32 + importing.call_import->call->host_div_by(i32) i32`, }, { name: "wasm calls imported wasm calls host function panics with runtime.Error", input: []uint64{0}, module: importing.Ctx, fn: importing.Exports[callImportCallHostFnName].Function, - expectedErr: `wasm runtime error: runtime error: integer divide by zero -wasm backtrace: - 0: host_div_by - 1: call->host_div_by - 2: call_import->call->host_div_by`, + expectedErr: `runtime error: integer divide by zero (recovered by wazero) +wasm stack trace: + imported.host_div_by(i32) i32 + imported.call->host_div_by(i32) i32 + importing.call_import->call->host_div_by(i32) i32`, }, } for _, tt := range tests { @@ -433,7 +434,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, wasm.Mod } // To use the function, we first need to add it to a module. - imported := &wasm.ModuleInstance{Name: t.Name()} + imported := &wasm.ModuleInstance{Name: "imported"} addFunction(imported, wasmFnName, wasmFn) addFunction(imported, hostFnName, hostFn) addFunction(imported, callHostFnName, callHostFn) @@ -444,7 +445,7 @@ func setupCallTests(t *testing.T, e wasm.Engine) (*wasm.ModuleInstance, wasm.Mod linkModuleToEngine(imported, importedMe) // To test stack traces, call the same function from another module - importing := &wasm.ModuleInstance{Name: t.Name() + "-importing"} + importing := &wasm.ModuleInstance{Name: "importing"} // Don't add imported functions yet as NewModuleEngine requires them split. importedFunctions := []*wasm.FunctionInstance{callHostFn} @@ -484,7 +485,7 @@ func linkModuleToEngine(module *wasm.ModuleInstance, me wasm.ModuleEngine) { // addFunction assigns and adds a function to the module. func addFunction(module *wasm.ModuleInstance, funcName string, fn *wasm.FunctionInstance) { - fn.DebugName = funcName + fn.DebugName = wasmdebug.FuncName(module.Name, funcName, fn.Index) module.Functions = append(module.Functions, fn) if module.Exports == nil { module.Exports = map[string]*wasm.ExportInstance{} diff --git a/internal/wasm/host.go b/internal/wasm/host.go index 8427ec5b..00bbd592 100644 --- a/internal/wasm/host.go +++ b/internal/wasm/host.go @@ -4,6 +4,8 @@ import ( "fmt" "reflect" "sort" + + "github.com/tetratelabs/wazero/internal/wasmdebug" ) // NewHostModule is defined internally for use in WASI tests and to keep the code size in the root directory small. @@ -88,17 +90,19 @@ func (m *Module) validateHostFunctions() error { return nil } -func (m *Module) buildHostFunctionInstances() (functions []*FunctionInstance) { +func (m *Module) buildHostFunctions(moduleName string) (functions []*FunctionInstance) { + // ModuleBuilder has no imports, which means the FunctionSection index is the same as the position in the function + // index namespace. Also, it ensures every function has a name. That's why there is less error checking here. var functionNames = m.NameSection.FunctionNames for idx, typeIndex := range m.FunctionSection { fn := m.HostFunctionSection[idx] f := &FunctionInstance{ - DebugName: functionNames[idx].Name, - Kind: kind(fn.Type()), - Type: m.TypeSection[typeIndex], - GoFunc: fn, - Index: Index(idx), + Kind: kind(fn.Type()), + Type: m.TypeSection[typeIndex], + GoFunc: fn, + Index: Index(idx), } + f.DebugName = wasmdebug.FuncName(moduleName, functionNames[f.Index].Name, f.Index) functions = append(functions, f) } return diff --git a/internal/wasm/interpreter/interpreter.go b/internal/wasm/interpreter/interpreter.go index 8b25c7bb..771b19b1 100644 --- a/internal/wasm/interpreter/interpreter.go +++ b/internal/wasm/interpreter/interpreter.go @@ -6,13 +6,13 @@ import ( "math" "math/bits" "reflect" - "runtime/debug" "strings" "sync" "github.com/tetratelabs/wazero/internal/buildoptions" "github.com/tetratelabs/wazero/internal/moremath" "github.com/tetratelabs/wazero/internal/wasm" + "github.com/tetratelabs/wazero/internal/wasmdebug" "github.com/tetratelabs/wazero/internal/wasmruntime" "github.com/tetratelabs/wazero/internal/wazeroir" ) @@ -516,28 +516,14 @@ func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, pa // TODO: ^^ Will not fail if the function was imported from a closed module. if v := recover(); v != nil { - if buildoptions.IsDebugMode { - debug.PrintStack() - } - - traces := make([]string, len(ce.frames)) - for i := 0; i < len(traces); i++ { + builder := wasmdebug.NewErrorBuilder() + frameCount := len(ce.frames) + for i := 0; i < frameCount; i++ { frame := ce.popFrame() - name := frame.f.source.DebugName - // TODO: include DWARF symbols. See #58 - traces[i] = fmt.Sprintf("\t%d: %s", i, name) - } - - runtimeErr, ok := v.(error) - if ok { - err = fmt.Errorf("wasm runtime error: %w", runtimeErr) - } else { - err = fmt.Errorf("wasm runtime error: %v", v) - } - - if len(traces) > 0 { - err = fmt.Errorf("%w\nwasm backtrace:\n%s", err, strings.Join(traces, "\n")) + fn := frame.f.source + builder.AddFrame(fn.DebugName, fn.ParamTypes(), fn.ResultTypes()) } + err = builder.FromRecovered(v) } }() diff --git a/internal/wasm/jit/engine.go b/internal/wasm/jit/engine.go index 603779e5..6d6618ff 100644 --- a/internal/wasm/jit/engine.go +++ b/internal/wasm/jit/engine.go @@ -5,13 +5,12 @@ import ( "math" "reflect" "runtime" - "runtime/debug" - "strings" "sync" "unsafe" "github.com/tetratelabs/wazero/internal/buildoptions" "github.com/tetratelabs/wazero/internal/wasm" + "github.com/tetratelabs/wazero/internal/wasmdebug" "github.com/tetratelabs/wazero/internal/wasmruntime" "github.com/tetratelabs/wazero/internal/wazeroir" ) @@ -481,32 +480,17 @@ func (me *moduleEngine) Call(m *wasm.ModuleContext, f *wasm.FunctionInstance, pa // TODO: ^^ Will not fail if the function was imported from a closed module. if v := recover(); v != nil { - if buildoptions.IsDebugMode { - debug.PrintStack() - } - - var frames []string + builder := wasmdebug.NewErrorBuilder() // Handle edge-case where the host function is called directly by Go. if ce.globalContext.callFrameStackPointer == 0 { fn := compiled.source - frames = append(frames, fmt.Sprintf("\t%d: %s", 0, fn.DebugName)) + builder.AddFrame(fn.DebugName, fn.ParamTypes(), fn.ResultTypes()) } for i := uint64(0); i < ce.globalContext.callFrameStackPointer; i++ { - f := ce.callFrameStack[ce.globalContext.callFrameStackPointer-1-i].compiledFunction - frames = append(frames, fmt.Sprintf("\t%d: %s", i, f.source.DebugName)) - // TODO: include DWARF symbols. See #58 - } - - runtimeErr, ok := v.(error) - if ok { - err = fmt.Errorf("wasm runtime error: %w", runtimeErr) - } else { - err = fmt.Errorf("wasm runtime error: %v", v) - } - - if len(frames) > 0 { - err = fmt.Errorf("%w\nwasm backtrace:\n%s", err, strings.Join(frames, "\n")) + fn := ce.callFrameStack[ce.globalContext.callFrameStackPointer-1-i].compiledFunction.source + builder.AddFrame(fn.DebugName, fn.ParamTypes(), fn.ResultTypes()) } + err = builder.FromRecovered(v) } }() diff --git a/internal/wasm/module.go b/internal/wasm/module.go index d3341761..36c2c4f0 100644 --- a/internal/wasm/module.go +++ b/internal/wasm/module.go @@ -11,6 +11,7 @@ import ( "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/ieee754" "github.com/tetratelabs/wazero/internal/leb128" + "github.com/tetratelabs/wazero/internal/wasmdebug" ) // DecodeModule parses the configured source into a Module. This function returns when the source is exhausted or @@ -456,7 +457,7 @@ func (m *Module) buildGlobals(importedGlobals []*GlobalInstance) (globals []*Glo return } -func (m *Module) buildFunctions() (functions []*FunctionInstance) { +func (m *Module) buildFunctions(moduleName string) (functions []*FunctionInstance) { var functionNames NameMap if m.NameSection != nil { functionNames = m.NameSection.FunctionNames @@ -465,27 +466,28 @@ func (m *Module) buildFunctions() (functions []*FunctionInstance) { importCount := m.ImportFuncCount() n, nLen := 0, len(functionNames) for codeIndex, typeIndex := range m.FunctionSection { + // The function name section begins with imports, but can be sparse. This keeps track of how far in the name + // section we've already searched. funcIdx := importCount + uint32(codeIndex) - // Seek to see if there's a better name than "unknown" - name := "unknown" + var funcName string for ; n < nLen; n++ { next := functionNames[n] if next.Index > funcIdx { break // we have function names, but starting at a later index } else if next.Index == funcIdx { - name = next.Name + funcName = next.Name break } } f := &FunctionInstance{ - DebugName: name, Kind: FunctionKindWasm, Type: m.TypeSection[typeIndex], Body: m.CodeSection[codeIndex].Body, LocalTypes: m.CodeSection[codeIndex].LocalTypes, - Index: importCount + uint32(codeIndex), + Index: funcIdx, } + f.DebugName = wasmdebug.FuncName(moduleName, funcName, funcIdx) functions = append(functions, f) } return diff --git a/internal/wasm/module_test.go b/internal/wasm/module_test.go index 6ca12278..51a1b0c7 100644 --- a/internal/wasm/module_test.go +++ b/internal/wasm/module_test.go @@ -701,11 +701,13 @@ func TestModule_buildFunctionInstances(t *testing.T) { {Index: Index(5), Name: "five"}, }, }, - CodeSection: []*Code{nopCode, nopCode, nopCode, nopCode, nopCode}, + FunctionSection: []Index{0, 0, 0, 0, 0}, + CodeSection: []*Code{nopCode, nopCode, nopCode, nopCode, nopCode}, } - actual := m.buildFunctions() - expectedNames := []string{"unknown", "two", "unknown", "four", "five"} + // Note: This only returns module-defined functions, not imported ones. That's why the index starts with 1, not 0. + actual := m.buildFunctions("counter") + expectedNames := []string{"counter.[1]", "counter.two", "counter.[3]", "counter.four", "counter.five"} for i, f := range actual { require.Equal(t, expectedNames[i], f.DebugName) require.Equal(t, nopCode.Body, f.Body) diff --git a/internal/wasm/store.go b/internal/wasm/store.go index 53fc3050..9e20bae4 100644 --- a/internal/wasm/store.go +++ b/internal/wasm/store.go @@ -87,7 +87,7 @@ type ( // FunctionInstance represents a function instance in a Store. // See https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#function-instances%E2%91%A0 FunctionInstance struct { - // DebugName is for debugging purpose, and is used to augment the stack traces. + // DebugName is for debugging purpose, and is used to augment stack traces. DebugName string // Kind describes how this function should be called. @@ -275,10 +275,10 @@ func (s *Store) Instantiate(ctx context.Context, module *Module, name string, sy var funcSection SectionID if module.HostFunctionSection == nil { funcSection = SectionIDFunction - functions = module.buildFunctions() + functions = module.buildFunctions(name) } else { funcSection = SectionIDHostFunction - functions = module.buildHostFunctionInstances() + functions = module.buildHostFunctions(name) } // Now we have all instances from imports and local ones, so ready to create a new ModuleInstance. diff --git a/internal/wasmdebug/debug.go b/internal/wasmdebug/debug.go new file mode 100644 index 00000000..de7bf83f --- /dev/null +++ b/internal/wasmdebug/debug.go @@ -0,0 +1,144 @@ +// Package wasmdebug contains utilities used to give consistent search keys between stack traces and error messages. +// Note: This is named wasmdebug to avoid conflicts with the normal go module. +// Note: This only imports "api" as importing "wasm" would create a cyclic dependency. +package wasmdebug + +import ( + "fmt" + "runtime" + "runtime/debug" + "strconv" + "strings" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/buildoptions" + "github.com/tetratelabs/wazero/internal/wasmruntime" +) + +// FuncName returns the naming convention of "moduleName.funcName". +// +// * moduleName is the possibly empty name the module was instantiated with. +// * funcName is the name in the Custom Name section, an export name, or what the host defines. +// * funcIdx is the position in the function index namespace, prefixed with imported functions. +// +// Note: "moduleName.[funcIdx]" is used when the funcName is empty, as commonly the case in TinyGo. +func FuncName(moduleName, funcName string, funcIdx uint32) string { + var ret strings.Builder + + // Start module.function + ret.WriteString(moduleName) + ret.WriteByte('.') + if funcName == "" { + ret.WriteByte('[') + ret.WriteString(strconv.Itoa(int(funcIdx))) + ret.WriteByte(']') + } else { + ret.WriteString(funcName) + } + + return ret.String() +} + +// signature returns a formatted signature similar to how it is defined in Go. +// +// * paramTypes should be from wasm.FunctionType +// * resultTypes should be from wasm.FunctionType +func addSignature(funcName string, paramTypes []api.ValueType, resultTypes []api.ValueType) string { + var ret strings.Builder + ret.WriteString(funcName) + + // Start params + ret.WriteByte('(') + paramCount := len(paramTypes) + switch paramCount { + case 0: + case 1: + ret.WriteString(api.ValueTypeName(paramTypes[0])) + default: + ret.WriteString(api.ValueTypeName(paramTypes[0])) + for _, vt := range paramTypes[1:] { + ret.WriteByte(',') + ret.WriteString(api.ValueTypeName(vt)) + } + } + ret.WriteByte(')') + + // Start results + resultCount := len(resultTypes) + switch resultCount { + case 0: + case 1: + ret.WriteByte(' ') + ret.WriteString(api.ValueTypeName(resultTypes[0])) + default: // As this is used for errors, don't panic if there are multiple returns, even if that's invalid! + ret.WriteByte(' ') + ret.WriteByte('(') + ret.WriteString(api.ValueTypeName(resultTypes[0])) + for _, vt := range resultTypes[1:] { + ret.WriteByte(',') + ret.WriteString(api.ValueTypeName(vt)) + } + ret.WriteByte(')') + } + + return ret.String() +} + +// ErrorBuilder helps build consistent errors, particularly adding a WASM stack trace. +// +// AddFrame should be called beginning at the frame that panicked until no more frames exist. Once done, call Format. +type ErrorBuilder interface { + // AddFrame adds the next frame. + // + // * funcName should be from FuncName + // * paramTypes should be from wasm.FunctionType + // * resultTypes should be from wasm.FunctionType + // + // Note: paramTypes and resultTypes are present because signature misunderstanding, mismatch or overflow are common. + AddFrame(funcName string, paramTypes, resultTypes []api.ValueType) + + // FromRecovered returns an error with the wasm stack trace appended to it. + FromRecovered(recovered interface{}) error +} + +func NewErrorBuilder() ErrorBuilder { + return &stackTrace{} +} + +type stackTrace struct { + frames []string +} + +func (s *stackTrace) FromRecovered(recovered interface{}) error { + if buildoptions.IsDebugMode { + debug.PrintStack() + } + + stack := strings.Join(s.frames, "\n\t") + + // If the error was internal, don't mention it was recovered. + if wasmErr, ok := recovered.(*wasmruntime.Error); ok { + return fmt.Errorf("wasm error: %w\nwasm stack trace:\n\t%s", wasmErr, stack) + } + + // If we have a runtime.Error, something severe happened which should include the stack trace. This could be + // a nil pointer from wazero or a user-defined function from ModuleBuilder. + if runtimeErr, ok := recovered.(runtime.Error); ok { + // TODO: consider adding debug.Stack(), but last time we attempted, some tests became unstable. + return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack) + } + + // At this point we expect the error was from a function defined by ModuleBuilder that intentionally called panic. + if runtimeErr, ok := recovered.(error); ok { // Ex. panic(errors.New("whoops")) + return fmt.Errorf("%w (recovered by wazero)\nwasm stack trace:\n\t%s", runtimeErr, stack) + } else { // Ex. panic("whoops") + return fmt.Errorf("%v (recovered by wazero)\nwasm stack trace:\n\t%s", recovered, stack) + } +} + +// AddFrame implements ErrorBuilder.Format +func (s *stackTrace) AddFrame(funcName string, paramTypes, resultTypes []api.ValueType) { + // Format as best as we can, considering we don't yet have source and line numbers, + // TODO: include DWARF symbols. See #58 + s.frames = append(s.frames, addSignature(funcName, paramTypes, resultTypes)) +} diff --git a/internal/wasmdebug/debug_test.go b/internal/wasmdebug/debug_test.go new file mode 100644 index 00000000..4b8fd649 --- /dev/null +++ b/internal/wasmdebug/debug_test.go @@ -0,0 +1,145 @@ +package wasmdebug + +import ( + "errors" + "runtime" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/tetratelabs/wazero/api" + "github.com/tetratelabs/wazero/internal/wasmruntime" +) + +func TestFuncName(t *testing.T) { + for _, tc := range []struct { + name, moduleName, funcName string + funcIdx uint32 + expected string + }{ // Only tests a few edge cases to show what it might end up as. + {name: "empty", expected: ".[0]"}, + {name: "empty module", funcName: "y", expected: ".y"}, + {name: "empty function", moduleName: "x", funcIdx: 255, expected: "x.[255]"}, + {name: "looks like index in function", moduleName: "x", funcName: "[255]", expected: "x.[255]"}, + {name: "no special characters", moduleName: "x", funcName: "y", expected: "x.y"}, + {name: "dots in module", moduleName: "w.x", funcName: "y", expected: "w.x.y"}, + {name: "dots in function", moduleName: "x", funcName: "y.z", expected: "x.y.z"}, + {name: "spaces in module", moduleName: "w x", funcName: "y", expected: "w x.y"}, + {name: "spaces in function", moduleName: "x", funcName: "y z", expected: "x.y z"}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + funcName := FuncName(tc.moduleName, tc.funcName, tc.funcIdx) + require.Equal(t, tc.expected, funcName) + }) + } +} + +func TestAddSignature(t *testing.T) { + i32, i64, f32, f64 := api.ValueTypeI32, api.ValueTypeI64, api.ValueTypeF32, api.ValueTypeF64 + for _, tc := range []struct { + name string + paramTypes, resultTypes []api.ValueType + expected string + }{ + {name: "v_v", expected: "x.y()"}, + {name: "i32_v", paramTypes: []api.ValueType{i32}, expected: "x.y(i32)"}, + {name: "i32f64_v", paramTypes: []api.ValueType{i32, f64}, expected: "x.y(i32,f64)"}, + {name: "f32i32f64_v", paramTypes: []api.ValueType{f32, i32, f64}, expected: "x.y(f32,i32,f64)"}, + {name: "v_i64", resultTypes: []api.ValueType{i64}, expected: "x.y() i64"}, + {name: "v_i64f32", resultTypes: []api.ValueType{i64, f32}, expected: "x.y() (i64,f32)"}, + {name: "v_f32i32f64", resultTypes: []api.ValueType{f32, i32, f64}, expected: "x.y() (f32,i32,f64)"}, + {name: "i32_i64", paramTypes: []api.ValueType{i32}, resultTypes: []api.ValueType{i64}, expected: "x.y(i32) i64"}, + {name: "i64f32_i64f32", paramTypes: []api.ValueType{i64, f32}, resultTypes: []api.ValueType{i64, f32}, expected: "x.y(i64,f32) (i64,f32)"}, + {name: "i64f32f64_f32i32f64", paramTypes: []api.ValueType{i64, f32, f64}, resultTypes: []api.ValueType{f32, i32, f64}, expected: "x.y(i64,f32,f64) (f32,i32,f64)"}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + withSignature := addSignature("x.y", tc.paramTypes, tc.resultTypes) + require.Equal(t, tc.expected, withSignature) + }) + } +} + +func TestErrorBuilder(t *testing.T) { + argErr := errors.New("invalid argument") + rteErr := testRuntimeErr("index out of bounds") + i32 := api.ValueTypeI32 + i32i32i32i32 := []api.ValueType{i32, i32, i32, i32} + + for _, tc := range []struct { + name string + build func(ErrorBuilder) error + expectedErr string + expectUnwrap error + }{ + { + name: "one", + build: func(builder ErrorBuilder) error { + builder.AddFrame("x.y", nil, nil) + return builder.FromRecovered(argErr) + }, + expectedErr: `invalid argument (recovered by wazero) +wasm stack trace: + x.y()`, + expectUnwrap: argErr, + }, + { + name: "two", + build: func(builder ErrorBuilder) error { + builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}) + builder.AddFrame("x.y", nil, nil) + return builder.FromRecovered(argErr) + }, + expectedErr: `invalid argument (recovered by wazero) +wasm stack trace: + wasi_snapshot_preview1.fd_write(i32,i32,i32,i32) i32 + x.y()`, + expectUnwrap: argErr, + }, + { + name: "runtime.Error", + build: func(builder ErrorBuilder) error { + builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}) + builder.AddFrame("x.y", nil, nil) + return builder.FromRecovered(rteErr) + }, + expectedErr: `index out of bounds (recovered by wazero) +wasm stack trace: + wasi_snapshot_preview1.fd_write(i32,i32,i32,i32) i32 + x.y()`, + expectUnwrap: rteErr, + }, + { + name: "wasmruntime.Error", + build: func(builder ErrorBuilder) error { + builder.AddFrame("wasi_snapshot_preview1.fd_write", i32i32i32i32, []api.ValueType{i32}) + builder.AddFrame("x.y", nil, nil) + return builder.FromRecovered(wasmruntime.ErrRuntimeCallStackOverflow) + }, + expectedErr: `wasm error: callstack overflow +wasm stack trace: + wasi_snapshot_preview1.fd_write(i32,i32,i32,i32) i32 + x.y()`, + expectUnwrap: wasmruntime.ErrRuntimeCallStackOverflow, + }, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + withStackTrace := tc.build(NewErrorBuilder()) + require.Equal(t, tc.expectUnwrap, errors.Unwrap(withStackTrace)) + require.EqualError(t, withStackTrace, tc.expectedErr) + }) + } +} + +// compile-time check to ensure testRuntimeErr implements runtime.Error. +var _ runtime.Error = testRuntimeErr("") + +type testRuntimeErr string + +func (e testRuntimeErr) RuntimeError() {} + +func (e testRuntimeErr) Error() string { + return string(e) +} diff --git a/tests/engine/adhoc_test.go b/tests/engine/adhoc_test.go index 18a4d41a..ba835f27 100644 --- a/tests/engine/adhoc_test.go +++ b/tests/engine/adhoc_test.go @@ -85,12 +85,12 @@ func testUnreachable(t *testing.T, r wazero.Runtime) { defer module.Close() _, err = module.ExportedFunction("main").Call(nil) - exp := `wasm runtime error: panic in host function -wasm backtrace: - 0: cause_unreachable - 1: two - 2: one - 3: main` + exp := `panic in host function (recovered by wazero) +wasm stack trace: + host.cause_unreachable() + .two() + .one() + .main()` require.Equal(t, exp, err.Error()) }