From 56f83579ae78fc1b5718d719f255b18c9c66f0cc Mon Sep 17 00:00:00 2001 From: 6h057 <15034695+omarsy@users.noreply.github.com> Date: Fri, 2 Aug 2024 16:10:44 +0200 Subject: [PATCH] feat(gnovm): add stacktraces and log them in panic messages (#2145) Closes #1812 #### Summary This pull request introduces a new `StackTrace` mechanism to the `Machine.Exceptions` class, enhancing its exception handling capabilities by generating and appending detailed stacktraces during panic situations. **Panic Handling:** - When a panic occurs, the current stack state is copied and appended to the exception details. - This includes the value of the panic, the last call frame, and the copied stack trace. **Code Example:** ```go package main func main() { f() } func f() { defer func() { panic("third") }() defer func() { panic("second") }() panic("first") } ``` **Sample Output:** ``` Stacktrace: Exception 0: panic((const ("first" string))) main/files/panic0b.gno:14 f() main/files/panic0b.gno:4 Exception 1: panic((const ("second" string))) main/files/panic0b.gno:12 f() main/files/panic0b.gno:4 Exception 2: panic((const ("third" string))) main/files/panic0b.gno:9 f() main/files/panic0b.gno:4 ```
Contributors' checklist... - [ ] Added new tests, or not needed, or not feasible - [ ] Provided an example (e.g. screenshot) to aid review or the PR is self-explanatory - [ ] Updated the official documentation or not needed - [ ] No breaking changes were made, or a `BREAKING CHANGE: xxx` message was included in the description - [ ] Added references to related issues and PRs - [ ] Provided any useful hints for running manual tests - [ ] Added new benchmarks to [generated graphs](https://gnoland.github.io/benchmarks), if any. More info [here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
--------- Co-authored-by: Morgan --- gno.land/cmd/gnoland/testdata/panic.txtar | 27 +++ gno.land/pkg/sdk/vm/keeper.go | 9 +- gnovm/cmd/gno/run.go | 10 +- gnovm/pkg/gnolang/debugger_test.go | 20 +- gnovm/pkg/gnolang/eval_test.go | 66 ++++-- gnovm/pkg/gnolang/frame.go | 123 ++++++++++ gnovm/pkg/gnolang/machine.go | 115 +++++++-- gnovm/pkg/gnolang/op_call.go | 4 +- gnovm/tests/file.go | 56 ++++- gnovm/tests/files/panic0.gno | 5 + gnovm/tests/files/panic0a.gno | 45 ++++ gnovm/tests/files/panic0b.gno | 13 + gnovm/tests/files/panic0c.gno | 87 +++++++ gnovm/tests/files/panic1.gno | 5 + gnovm/tests/files/panic2a.gno | 275 ++++++++++++++++++++++ gnovm/tests/files/panic2b.gno | 44 ++++ gnovm/tests/files/recover10.gno | 5 + gnovm/tests/files/recover1b.gno | 5 + gnovm/tests/files/recover8.gno | 7 + gnovm/tests/files/std5_stdlibs.gno | 9 + gnovm/tests/files/std8_stdlibs.gno | 13 + gnovm/tests/files/time17_native.gno | 20 ++ gnovm/tests/files/typeassert1.gno | 5 + gnovm/tests/files/typeassert2a.gno | 5 + gnovm/tests/files/typeassert9.gno | 5 + gnovm/tests/files/zrealm_panic.gno | 20 ++ 26 files changed, 947 insertions(+), 51 deletions(-) create mode 100644 gno.land/cmd/gnoland/testdata/panic.txtar create mode 100644 gnovm/tests/files/panic0a.gno create mode 100644 gnovm/tests/files/panic0c.gno create mode 100644 gnovm/tests/files/panic2a.gno create mode 100644 gnovm/tests/files/panic2b.gno create mode 100644 gnovm/tests/files/time17_native.gno create mode 100644 gnovm/tests/files/zrealm_panic.gno diff --git a/gno.land/cmd/gnoland/testdata/panic.txtar b/gno.land/cmd/gnoland/testdata/panic.txtar new file mode 100644 index 00000000000..2b964d80751 --- /dev/null +++ b/gno.land/cmd/gnoland/testdata/panic.txtar @@ -0,0 +1,27 @@ +# test panic + +loadpkg gno.land/r/demo/panic $WORK + +# start a new node +gnoland start + + +! gnokey maketx call -pkgpath gno.land/r/demo/panic --func Trigger --gas-fee 1000000ugnot --gas-wanted 2000000 --broadcast -chainid=tendermint_test test1 + +stderr 'p\\(\)' +stderr 'gno.land/r/demo/panic/panic.gno:5' +stderr 'pkg\\.Trigger\(\)' +stderr 'gno.land/r/demo/panic/panic.gno:9' + +-- panic.gno -- +package main + +func p() { + i := "here" + panic(i) +} + +func Trigger() { + p() +} + diff --git a/gno.land/pkg/sdk/vm/keeper.go b/gno.land/pkg/sdk/vm/keeper.go index 934c4557dd0..615be2029fe 100644 --- a/gno.land/pkg/sdk/vm/keeper.go +++ b/gno.land/pkg/sdk/vm/keeper.go @@ -537,12 +537,15 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) { m.SetActivePackage(mpv) defer func() { if r := recover(); r != nil { - switch r.(type) { + switch r := r.(type) { case store.OutOfGasException: // panic in consumeGas() panic(r) + case gno.UnhandledPanicError: + err = errors.Wrap(fmt.Errorf("%v", r.Error()), "VM call panic: %s\nStacktrace: %s\n", + r.Error(), m.ExceptionsStacktrace()) default: - err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\n%s\n", - r, m.String()) + err = errors.Wrap(fmt.Errorf("%v", r), "VM call panic: %v\nMachine State:%s\nStacktrace: %s\n", + r, m.String(), m.Stacktrace().String()) return } } diff --git a/gnovm/cmd/gno/run.go b/gnovm/cmd/gno/run.go index 39306d59cb0..cfbfe995a46 100644 --- a/gnovm/cmd/gno/run.go +++ b/gnovm/cmd/gno/run.go @@ -188,8 +188,14 @@ func listNonTestFiles(dir string) ([]string, error) { func runExpr(m *gno.Machine, expr string) { defer func() { if r := recover(); r != nil { - fmt.Printf("panic running expression %s: %v\n%s\n", - expr, r, m.String()) + switch r := r.(type) { + case gno.UnhandledPanicError: + fmt.Printf("panic running expression %s: %v\nStacktrace: %s\n", + expr, r.Error(), m.ExceptionsStacktrace()) + default: + fmt.Printf("panic running expression %s: %v\nMachine State:%s\nStacktrace: %s\n", + expr, r, m.String(), m.Stacktrace().String()) + } panic(r) } }() diff --git a/gnovm/pkg/gnolang/debugger_test.go b/gnovm/pkg/gnolang/debugger_test.go index 10d7c5ce250..16ecb91fb3c 100644 --- a/gnovm/pkg/gnolang/debugger_test.go +++ b/gnovm/pkg/gnolang/debugger_test.go @@ -24,7 +24,7 @@ type writeNopCloser struct{ io.Writer } func (writeNopCloser) Close() error { return nil } // TODO (Marc): move evalTest to gnovm/tests package and remove code duplicates -func evalTest(debugAddr, in, file string) (out, err string) { +func evalTest(debugAddr, in, file string) (out, err, stacktrace string) { bout := bytes.NewBufferString("") berr := bytes.NewBufferString("") stdin := bytes.NewBufferString(in) @@ -58,6 +58,18 @@ func evalTest(debugAddr, in, file string) (out, err string) { }) defer m.Release() + defer func() { + if r := recover(); r != nil { + switch r.(type) { + case gnolang.UnhandledPanicError: + stacktrace = m.ExceptionsStacktrace() + default: + stacktrace = m.Stacktrace().String() + } + stacktrace = strings.TrimSpace(strings.ReplaceAll(stacktrace, "../../tests/files/", "files/")) + panic(r) + } + }() if debugAddr != "" { if e := m.Debugger.Serve(debugAddr); e != nil { @@ -69,7 +81,7 @@ func evalTest(debugAddr, in, file string) (out, err string) { m.RunFiles(f) ex, _ := gnolang.ParseExpr("main()") m.Eval(ex) - out, err = bout.String(), berr.String() + out, err, stacktrace = bout.String(), berr.String(), m.ExceptionsStacktrace() return } @@ -78,7 +90,7 @@ func runDebugTest(t *testing.T, targetPath string, tests []dtest) { for _, test := range tests { t.Run("", func(t *testing.T) { - out, err := evalTest("", test.in, targetPath) + out, err, _ := evalTest("", test.in, targetPath) t.Log("in:", test.in, "out:", out, "err:", err) if !strings.Contains(out, test.out) { t.Errorf("unexpected output\nwant\"%s\"\n got \"%s\"", test.out, out) @@ -194,7 +206,7 @@ func TestRemoteDebug(t *testing.T) { } func TestRemoteError(t *testing.T) { - _, err := evalTest(":xxx", "", debugTarget) + _, err, _ := evalTest(":xxx", "", debugTarget) t.Log("err:", err) if !strings.Contains(err, "tcp/xxx: unknown port") && !strings.Contains(err, "tcp/xxx: nodename nor servname provided, or not known") { diff --git a/gnovm/pkg/gnolang/eval_test.go b/gnovm/pkg/gnolang/eval_test.go index fdd8e0204d1..5aa5bcca462 100644 --- a/gnovm/pkg/gnolang/eval_test.go +++ b/gnovm/pkg/gnolang/eval_test.go @@ -3,6 +3,7 @@ package gnolang_test import ( "os" "path" + "sort" "strings" "testing" ) @@ -14,17 +15,21 @@ func TestEvalFiles(t *testing.T) { t.Fatal(err) } for _, f := range files { - wantOut, wantErr, ok := testData(dir, f) + wantOut, wantErr, wantStacktrace, ok := testData(dir, f) if !ok { continue } t.Run(f.Name(), func(t *testing.T) { - out, err := evalTest("", "", path.Join(dir, f.Name())) + out, err, stacktrace := evalTest("", "", path.Join(dir, f.Name())) if wantErr != "" && !strings.Contains(err, wantErr) || wantErr == "" && err != "" { t.Fatalf("unexpected error\nWant: %s\n Got: %s", wantErr, err) } + + if wantStacktrace != "" && !strings.Contains(stacktrace, wantStacktrace) { + t.Fatalf("unexpected stacktrace\nWant: %s\n Got: %s", wantStacktrace, stacktrace) + } if wantOut != "" && out != wantOut { t.Fatalf("unexpected output\nWant: %s\n Got: %s", wantOut, out) } @@ -33,30 +38,63 @@ func TestEvalFiles(t *testing.T) { } // testData returns the expected output and error string, and true if entry is valid. -func testData(dir string, f os.DirEntry) (testOut, testErr string, ok bool) { +func testData(dir string, f os.DirEntry) (testOut, testErr, testStacktrace string, ok bool) { if f.IsDir() { - return "", "", false + return } name := path.Join(dir, f.Name()) if !strings.HasSuffix(name, ".gno") || strings.HasSuffix(name, "_long.gno") { - return "", "", false + return } buf, err := os.ReadFile(name) if err != nil { - return "", "", false + return } str := string(buf) if strings.Contains(str, "// PKGPATH:") { - return "", "", false + return } - return commentFrom(str, "\n// Output:"), commentFrom(str, "\n// Error:"), true + + res := commentFrom(str, []string{"\n// Output:", "\n// Error:", "\n// Stacktrace:"}) + + return res[0], res[1], res[2], true } -// commentFrom returns the content from a trailing comment block in s starting with delim. -func commentFrom(s, delim string) string { - index := strings.Index(s, delim) - if index < 0 { - return "" +type directive struct { + delim string + res string + index int +} + +// commentFrom returns the comments from s that are between the delimiters. +func commentFrom(s string, delims []string) []string { + directives := make([]directive, len(delims)) + directivesFound := make([]*directive, 0, len(delims)) + + for i, delim := range delims { + index := strings.Index(s, delim) + directives[i] = directive{delim: delim, index: index} + if index >= 0 { + directivesFound = append(directivesFound, &directives[i]) + } } - return strings.TrimSpace(strings.ReplaceAll(s[index+len(delim):], "\n// ", "\n")) + sort.Slice(directivesFound, func(i, j int) bool { + return directivesFound[i].index < directivesFound[j].index + }) + + for i := range directivesFound { + next := len(s) + if i != len(directivesFound)-1 { + next = directivesFound[i+1].index + } + + directivesFound[i].res = strings.TrimSpace(strings.ReplaceAll(s[directivesFound[i].index+len(directivesFound[i].delim):next], "\n// ", "\n")) + } + + res := make([]string, len(directives)) + for i, d := range directives { + res[i] = d.res + } + + return res } diff --git a/gnovm/pkg/gnolang/frame.go b/gnovm/pkg/gnolang/frame.go index c808fc111b0..2ac1027eb32 100644 --- a/gnovm/pkg/gnolang/frame.go +++ b/gnovm/pkg/gnolang/frame.go @@ -2,8 +2,11 @@ package gnolang import ( "fmt" + "strings" ) +const maxStacktraceSize = 128 + //---------------------------------------- // (runtime) Frame @@ -64,6 +67,10 @@ func (fr Frame) String() string { } } +func (fr *Frame) IsCall() bool { + return fr.Func != nil || fr.GoFunc != nil +} + func (fr *Frame) PushDefer(dfr Defer) { fr.Defers = append(fr.Defers, dfr) } @@ -92,3 +99,119 @@ type Defer struct { // a panic occurs and is decremented each time a panic is recovered. PanicScope uint } + +type StacktraceCall struct { + Stmt Stmt + Frame *Frame +} +type Stacktrace struct { + Calls []StacktraceCall + NumFramesElided int +} + +func (s Stacktrace) String() string { + var builder strings.Builder + + for i := 0; i < len(s.Calls); i++ { + if s.NumFramesElided > 0 && i == maxStacktraceSize/2 { + fmt.Fprintf(&builder, "...%d frame(s) elided...\n", s.NumFramesElided) + } + + call := s.Calls[i] + cx := call.Frame.Source.(*CallExpr) + switch { + case call.Frame.Func != nil && call.Frame.Func.IsNative(): + fmt.Fprintf(&builder, "%s\n", toExprTrace(cx)) + fmt.Fprintf(&builder, " gonative:%s.%s\n", call.Frame.Func.NativePkg, call.Frame.Func.NativeName) + case call.Frame.Func != nil: + fmt.Fprintf(&builder, "%s\n", toExprTrace(cx)) + fmt.Fprintf(&builder, " %s/%s:%d\n", call.Frame.Func.PkgPath, call.Frame.Func.FileName, call.Stmt.GetLine()) + case call.Frame.GoFunc != nil: + fmt.Fprintf(&builder, "%s\n", toExprTrace(cx)) + fmt.Fprintf(&builder, " gofunction:%s\n", call.Frame.GoFunc.Value.Type()) + default: + panic("StacktraceCall has a non-call Frame") + } + } + return builder.String() +} + +func toExprTrace(ex Expr) string { + switch ex := ex.(type) { + case *CallExpr: + s := make([]string, len(ex.Args)) + for i, arg := range ex.Args { + s[i] = toExprTrace(arg) + } + return fmt.Sprintf("%s(%s)", toExprTrace(ex.Func), strings.Join(s, ",")) + case *BinaryExpr: + return fmt.Sprintf("%s %s %s", toExprTrace(ex.Left), ex.Op.TokenString(), toExprTrace(ex.Right)) + case *UnaryExpr: + return fmt.Sprintf("%s%s", ex.Op.TokenString(), toExprTrace(ex.X)) + case *SelectorExpr: + return fmt.Sprintf("%s.%s", toExprTrace(ex.X), ex.Sel) + case *IndexExpr: + return fmt.Sprintf("%s[%s]", toExprTrace(ex.X), toExprTrace(ex.Index)) + case *StarExpr: + return fmt.Sprintf("*%s", toExprTrace(ex.X)) + case *RefExpr: + return fmt.Sprintf("&%s", toExprTrace(ex.X)) + case *CompositeLitExpr: + lenEl := len(ex.Elts) + if ex.Type == nil { + return fmt.Sprintf("", lenEl) + } + + return fmt.Sprintf("%s", toExprTrace(ex.Type), lenEl) + case *FuncLitExpr: + return fmt.Sprintf("%s{ ... }", toExprTrace(&ex.Type)) + case *TypeAssertExpr: + return fmt.Sprintf("%s.(%s)", toExprTrace(ex.X), toExprTrace(ex.Type)) + case *ConstExpr: + return toConstExpTrace(ex) + case *NameExpr, *BasicLitExpr, *SliceExpr: + return ex.String() + } + + return ex.String() +} + +func toConstExpTrace(cte *ConstExpr) string { + tv := cte.TypedValue + + switch bt := baseOf(tv.T).(type) { + case PrimitiveType: + switch bt { + case UntypedBoolType, BoolType: + return fmt.Sprintf("%t", tv.GetBool()) + case UntypedStringType, StringType: + return tv.GetString() + case IntType: + return fmt.Sprintf("%d", tv.GetInt()) + case Int8Type: + return fmt.Sprintf("%d", tv.GetInt8()) + case Int16Type: + return fmt.Sprintf("%d", tv.GetInt16()) + case UntypedRuneType, Int32Type: + return fmt.Sprintf("%d", tv.GetInt32()) + case Int64Type: + return fmt.Sprintf("%d", tv.GetInt64()) + case UintType: + return fmt.Sprintf("%d", tv.GetUint()) + case Uint8Type: + return fmt.Sprintf("%d", tv.GetUint8()) + case Uint16Type: + return fmt.Sprintf("%d", tv.GetUint16()) + case Uint32Type: + return fmt.Sprintf("%d", tv.GetUint32()) + case Uint64Type: + return fmt.Sprintf("%d", tv.GetUint64()) + case Float32Type: + return fmt.Sprintf("%v", tv.GetFloat32()) + case Float64Type: + return fmt.Sprintf("%v", tv.GetFloat64()) + } + } + + return tv.T.String() +} diff --git a/gnovm/pkg/gnolang/machine.go b/gnovm/pkg/gnolang/machine.go index 850da3d3c0f..24f94abc10b 100644 --- a/gnovm/pkg/gnolang/machine.go +++ b/gnovm/pkg/gnolang/machine.go @@ -25,12 +25,23 @@ type Exception struct { // Frame is used to reference the frame a panic occurred in so that recover() knows if the // currently executing deferred function is able to recover from the panic. Frame *Frame + + Stacktrace Stacktrace } func (e Exception) Sprint(m *Machine) string { return e.Value.Sprint(m) } +// UnhandledPanicError represents an error thrown when a panic is not handled in the realm. +type UnhandledPanicError struct { + Descriptor string // Description of the unhandled panic. +} + +func (e UnhandledPanicError) Error() string { + return e.Descriptor +} + //---------------------------------------- // Machine @@ -457,6 +468,43 @@ func (m *Machine) TestFunc(t *testing.T, tv TypedValue) { }) } +// Stacktrace returns the stack trace of the machine. +// It collects the executions and frames from the machine's frames and statements. +func (m *Machine) Stacktrace() (stacktrace Stacktrace) { + if len(m.Frames) == 0 { + return + } + + calls := make([]StacktraceCall, 0, len(m.Stmts)) + nextStmtIndex := len(m.Stmts) - 1 + for i := len(m.Frames) - 1; i >= 0; i-- { + if m.Frames[i].IsCall() { + stm := m.Stmts[nextStmtIndex] + bs := stm.(*bodyStmt) + stm = bs.Body[bs.NextBodyIndex-1] + calls = append(calls, StacktraceCall{ + Stmt: stm, + Frame: m.Frames[i], + }) + } + // if the frame is a call, the next statement is the last statement of the frame. + nextStmtIndex = m.Frames[i].NumStmts - 1 + } + + // if the stacktrace is too long, we trim it down to maxStacktraceSize + if len(calls) > maxStacktraceSize { + const halfMax = maxStacktraceSize / 2 + + stacktrace.NumFramesElided = len(calls) - maxStacktraceSize + calls = append(calls[:halfMax], calls[len(calls)-halfMax:]...) + calls = calls[:len(calls):len(calls)] // makes remaining part of "calls" GC'able + } + + stacktrace.Calls = calls + + return +} + // in case of panic, inject location information to exception. func (m *Machine) injectLocOnPanic() { if r := recover(); r != nil { @@ -736,8 +784,14 @@ func (m *Machine) resavePackageValues(rlm *Realm) { func (m *Machine) RunFunc(fn Name) { defer func() { if r := recover(); r != nil { - fmt.Printf("Machine.RunFunc(%q) panic: %v\n%s\n", - fn, r, m.String()) + switch r := r.(type) { + case UnhandledPanicError: + fmt.Printf("Machine.RunFunc(%q) panic: %s\nStacktrace: %s\n", + fn, r.Error(), m.ExceptionsStacktrace()) + default: + fmt.Printf("Machine.RunFunc(%q) panic: %v\nMachine State:%s\nStacktrace: %s\n", + fn, r, m.String(), m.Stacktrace().String()) + } panic(r) } }() @@ -747,8 +801,14 @@ func (m *Machine) RunFunc(fn Name) { func (m *Machine) RunMain() { defer func() { if r := recover(); r != nil { - fmt.Printf("Machine.RunMain() panic: %v\n%s\n", - r, m.String()) + switch r := r.(type) { + case UnhandledPanicError: + fmt.Printf("Machine.RunMain() panic: %s\nStacktrace: %s\n", + r.Error(), m.ExceptionsStacktrace()) + default: + fmt.Printf("Machine.RunMain() panic: %v\nMachine State:%s\nStacktrace: %s\n", + r, m.String(), m.Stacktrace()) + } panic(r) } }() @@ -1606,11 +1666,11 @@ func (m *Machine) PopStmt() Stmt { } if bs, ok := s.(*bodyStmt); ok { return bs.PopActiveStmt() - } else { - // general case. - m.Stmts = m.Stmts[:numStmts-1] - return s } + + m.Stmts = m.Stmts[:numStmts-1] + + return s } func (m *Machine) ForcePopStmt() (s Stmt) { @@ -1855,6 +1915,7 @@ func (m *Machine) PopFrame() Frame { m.Printf("-F %#v\n", f) } m.Frames = m.Frames[:numFrames-1] + return *f } @@ -1874,8 +1935,7 @@ func (m *Machine) PopFrameAndReturn() { fr := m.PopFrame() fr.Popped = true if debug { - // TODO: optimize with fr.IsCall - if fr.Func == nil && fr.GoFunc == nil { + if !fr.IsCall() { panic("unexpected non-call (loop) frame") } } @@ -1951,8 +2011,7 @@ func (m *Machine) lastCallFrame(n int, mustBeFound bool) *Frame { } for i := len(m.Frames) - 1; i >= 0; i-- { fr := m.Frames[i] - if fr.Func != nil || fr.GoFunc != nil { - // TODO: optimize with fr.IsCall + if fr.IsCall() { if n == 1 { return fr } else { @@ -1973,8 +2032,7 @@ func (m *Machine) lastCallFrame(n int, mustBeFound bool) *Frame { func (m *Machine) PopUntilLastCallFrame() *Frame { for i := len(m.Frames) - 1; i >= 0; i-- { fr := m.Frames[i] - if fr.Func != nil || fr.GoFunc != nil { - // TODO: optimize with fr.IsCall + if fr.IsCall() { m.Frames = m.Frames[:i+1] return fr } @@ -2085,8 +2143,9 @@ func (m *Machine) Panic(ex TypedValue) { m.Exceptions = append( m.Exceptions, Exception{ - Value: ex, - Frame: m.MustLastCallFrame(1), + Value: ex, + Frame: m.MustLastCallFrame(1), + Stacktrace: m.Stacktrace(), }, ) @@ -2231,6 +2290,30 @@ func (m *Machine) String() string { return builder.String() } +func (m *Machine) ExceptionsStacktrace() string { + if len(m.Exceptions) == 0 { + return "" + } + + var builder strings.Builder + + ex := m.Exceptions[0] + builder.WriteString("panic: " + ex.Sprint(m) + "\n") + builder.WriteString(ex.Stacktrace.String()) + + switch { + case len(m.Exceptions) > 2: + fmt.Fprintf(&builder, "... %d panic(s) elided ...\n", len(m.Exceptions)-2) + fallthrough // to print last exception + case len(m.Exceptions) == 2: + ex = m.Exceptions[len(m.Exceptions)-1] + builder.WriteString("panic: " + ex.Sprint(m) + "\n") + builder.WriteString(ex.Stacktrace.String()) + } + + return builder.String() +} + //---------------------------------------- // utility diff --git a/gnovm/pkg/gnolang/op_call.go b/gnovm/pkg/gnolang/op_call.go index 5479ee6d5ae..15531ec610d 100644 --- a/gnovm/pkg/gnolang/op_call.go +++ b/gnovm/pkg/gnolang/op_call.go @@ -427,7 +427,9 @@ func (m *Machine) doOpPanic2() { for i, ex := range m.Exceptions { exs[i] = ex.Sprint(m) } - panic(strings.Join(exs, "\n\t")) + panic(UnhandledPanicError{ + Descriptor: strings.Join(exs, "\n\t"), + }) } m.PushOp(OpPanic2) m.PushOp(OpReturnCallDefers) // XXX rename, not return? diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index 8ab60145bd5..3fea714b142 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -109,7 +109,7 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { opt(&f) } - directives, pkgPath, resWanted, errWanted, rops, maxAlloc, send := wantedFromComment(path) + directives, pkgPath, resWanted, errWanted, rops, stacktraceWanted, maxAlloc, send := wantedFromComment(path) if pkgPath == "" { pkgPath = "main" } @@ -124,6 +124,7 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { store := TestStore(rootDir, "./files", stdin, stdout, stderr, mode) store.SetLogStoreOps(true) m := testMachineCustom(store, pkgPath, stdout, maxAlloc, send) + checkMachineIsEmpty := true // TODO support stdlib groups, but make testing safe; // e.g. not be able to make network connections. @@ -259,6 +260,8 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { errstr = v.Sprint(m) case *gno.PreprocessError: errstr = v.Unwrap().Error() + case gno.UnhandledPanicError: + errstr = v.Error() default: errstr = strings.TrimSpace(fmt.Sprintf("%v", pnc)) } @@ -279,7 +282,7 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { // NOTE: ignores any gno.GetDebugErrors(). gno.ClearDebugErrors() - return nil // nothing more to do. + checkMachineIsEmpty = false // nothing more to do. } else { // record errors when errWanted is empty and pnc not nil if pnc != nil { @@ -307,7 +310,7 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { panic(fmt.Sprintf("fail on %s: got unexpected debug error(s): %v", path, gno.GetDebugErrors())) } // pnc is nil, errWanted empty, no gno debug errors - return nil + checkMachineIsEmpty = false } case "Output": // panic if got unexpected error @@ -373,24 +376,51 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { } } } + case "Stacktrace": + if stacktraceWanted != "" { + var stacktrace string + + switch pnc.(type) { + case gno.UnhandledPanicError: + stacktrace = m.ExceptionsStacktrace() + default: + stacktrace = m.Stacktrace().String() + } + + if !strings.Contains(stacktrace, stacktraceWanted) { + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(stacktraceWanted), + B: difflib.SplitLines(stacktrace), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) + } + } + checkMachineIsEmpty = false default: - return nil + checkMachineIsEmpty = false } } } - // Check that machine is empty. - err = m.CheckEmpty() - if err != nil { - if f.logger != nil { - f.logger("last state: \n", m.String()) + if checkMachineIsEmpty { + // Check that machine is empty. + err = m.CheckEmpty() + if err != nil { + if f.logger != nil { + f.logger("last state: \n", m.String()) + } + panic(fmt.Sprintf("fail on %s: machine not empty after main: %v", path, err)) } - panic(fmt.Sprintf("fail on %s: machine not empty after main: %v", path, err)) } return nil } -func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops string, maxAlloc int64, send std.Coins) { +func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops, stacktrace string, maxAlloc int64, send std.Coins) { fset := token.NewFileSet() f, err2 := parser.ParseFile(fset, p, nil, parser.ParseComments) if err2 != nil { @@ -432,6 +462,10 @@ func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops s rops = strings.TrimPrefix(text, "Realm:\n") rops = strings.TrimSpace(rops) directives = append(directives, "Realm") + } else if strings.HasPrefix(text, "Stacktrace:\n") { + stacktrace = strings.TrimPrefix(text, "Stacktrace:\n") + stacktrace = strings.TrimSpace(stacktrace) + directives = append(directives, "Stacktrace") } else { // ignore unexpected. } diff --git a/gnovm/tests/files/panic0.gno b/gnovm/tests/files/panic0.gno index 66a38b42c46..06460ca9d07 100644 --- a/gnovm/tests/files/panic0.gno +++ b/gnovm/tests/files/panic0.gno @@ -4,5 +4,10 @@ func main() { panic("wtf") } +// Stacktrace: +// panic: wtf +// main() +// main/files/panic0.gno:4 + // Error: // wtf diff --git a/gnovm/tests/files/panic0a.gno b/gnovm/tests/files/panic0a.gno new file mode 100644 index 00000000000..575bb7cce91 --- /dev/null +++ b/gnovm/tests/files/panic0a.gno @@ -0,0 +1,45 @@ +// Test panic with function call with all kind of expressions +package main + +type S struct { + s string +} + +func f(it1 int, it2, it3 int, pit *int, b bool, strs []string, s S, m map[string]string, t func(s string) string) { + panic("wtf") +} + +func main() { + vit := 1 + lit := []int{1} + var ( + pit *int = &vit + v interface{} + ) + b := true + v = 1 + + f( + v.(int), + lit[0], + *pit, + &vit, + !b, + []string{"a", "b"}, + S{s: "c"}, + map[string]string{"d": "gg", "test": "test"}, + func(s string) string { + return s + }, + ) +} + +// Stacktrace: +// panic: wtf +// f(v.((const-type int)),lit[0],*pit,&vit,!b,[](const-type string),S,map[(const-type string)] (const-type string),func(s (const-type string)) (const-type string){ ... }) +// main/files/panic0a.gno:9 +// main() +// main/files/panic0a.gno:22 + +// Error: +// wtf diff --git a/gnovm/tests/files/panic0b.gno b/gnovm/tests/files/panic0b.gno index bf3b343f785..55a7b21015a 100644 --- a/gnovm/tests/files/panic0b.gno +++ b/gnovm/tests/files/panic0b.gno @@ -14,6 +14,19 @@ func f() { panic("first") } +// Stacktrace: +// panic: first +// f() +// main/files/panic0b.gno:14 +// main() +// main/files/panic0b.gno:4 +// ... 1 panic(s) elided ... +// panic: third +// f() +// main/files/panic0b.gno:9 +// main() +// main/files/panic0b.gno:4 + // Error: // first // second diff --git a/gnovm/tests/files/panic0c.gno b/gnovm/tests/files/panic0c.gno new file mode 100644 index 00000000000..c331ee3bd17 --- /dev/null +++ b/gnovm/tests/files/panic0c.gno @@ -0,0 +1,87 @@ +package main + +type S struct { + s string +} + +func f( + s string, + b bool, + by byte, + it int, + it8 int8, + it16 int16, + it32 int32, + it64 int64, + uit uint, + uit8 uint8, + uit16 uint16, + uit32 uint32, + uit64 uint64, + ft32 float32, + ft64 float64, + strs []string, + st S, + m map[string]string, + t func(s string) string, +) { + panic("wtf") +} + +func main() { + strs := []string{"a", "b"} + st := S{"c"} + m := map[string]string{"d": "gg", "test": "test"} + t := func(s string) string { + return s + } + + const s string = "a" + + const b bool = true + + const by byte = 0 + + const it int = 1 + const it8 int8 = 1 + const it16 int16 = 1 + const it32 int32 = 1 + const it64 int64 = 1 + const uit uint = 1 + const uit8 uint8 = 1 + const uit16 uint16 = 1 + const uit32 uint32 = 1 + const uit64 uint64 = 1 + const ft32 float32 = 1 + const ft64 float64 = 1 + f( + s, + b, + by, + it, + it8, + it16, + it32, + it64, + uit, + uit8, + uit16, + uit32, + uit64, + ft32, + ft64, + strs, + st, + m, + t) +} + +// Stacktrace: +// panic: wtf +// f(a,true,0,1,1,1,1,1,1,1,1,1,1,1,1,strs,st,m,t) +// main/files/panic0c.gno:28 +// main() +// main/files/panic0c.gno:57 + +// Error: +// wtf diff --git a/gnovm/tests/files/panic1.gno b/gnovm/tests/files/panic1.gno index 483d43e53d1..235ba4a0b34 100644 --- a/gnovm/tests/files/panic1.gno +++ b/gnovm/tests/files/panic1.gno @@ -22,5 +22,10 @@ func main() { panic("here") } +// Stacktrace: +// panic: here +// main() +// main/files/panic1.gno:22 + // Error: // here diff --git a/gnovm/tests/files/panic2a.gno b/gnovm/tests/files/panic2a.gno new file mode 100644 index 00000000000..7310d6cce71 --- /dev/null +++ b/gnovm/tests/files/panic2a.gno @@ -0,0 +1,275 @@ +package main + +func p(i int) { + if i == 200 { + panic("here") + } + p(i + 1) +} + +func main() { + p(0) +} + +// Stacktrace: +// panic: here +// p(i + 1) +// main/files/panic2a.gno:5 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// ...74 frame(s) elided... +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(i + 1) +// main/files/panic2a.gno:7 +// p(0) +// main/files/panic2a.gno:7 +// main() +// main/files/panic2a.gno:11 + +// Error: +// here diff --git a/gnovm/tests/files/panic2b.gno b/gnovm/tests/files/panic2b.gno new file mode 100644 index 00000000000..7c356409aad --- /dev/null +++ b/gnovm/tests/files/panic2b.gno @@ -0,0 +1,44 @@ +package main + +func p(i int) { + defer func() { + panic("here") + }() + if i == 4 { + panic("here") + } + p(i + 1) +} + +func main() { + p(0) +} + +// Stacktrace: +// panic: here +// p(i + 1) +// main/files/panic2b.gno:8 +// p(i + 1) +// main/files/panic2b.gno:10 +// p(i + 1) +// main/files/panic2b.gno:10 +// p(i + 1) +// main/files/panic2b.gno:10 +// p(0) +// main/files/panic2b.gno:10 +// main() +// main/files/panic2b.gno:14 +// ... 4 panic(s) elided ... +// panic: here +// p(0) +// main/files/panic2b.gno:5 +// main() +// main/files/panic2b.gno:14 + +// Error: +// here +// here +// here +// here +// here +// here diff --git a/gnovm/tests/files/recover10.gno b/gnovm/tests/files/recover10.gno index 16dff4d4fed..de083a322a4 100644 --- a/gnovm/tests/files/recover10.gno +++ b/gnovm/tests/files/recover10.gno @@ -6,5 +6,10 @@ func main() { panic("ahhhhh") } +// Stacktrace: +// panic: ahhhhh +// main() +// main/files/recover10.gno:6 + // Error: // ahhhhh diff --git a/gnovm/tests/files/recover1b.gno b/gnovm/tests/files/recover1b.gno index 9e3b9ba72b6..978b4988329 100644 --- a/gnovm/tests/files/recover1b.gno +++ b/gnovm/tests/files/recover1b.gno @@ -10,5 +10,10 @@ func main() { panic("test panic") } +// Stacktrace: +// panic: other panic +// main() +// main/files/recover1b.gno:8 + // Error: // other panic diff --git a/gnovm/tests/files/recover8.gno b/gnovm/tests/files/recover8.gno index 53b31f05468..d144a4f986f 100644 --- a/gnovm/tests/files/recover8.gno +++ b/gnovm/tests/files/recover8.gno @@ -20,5 +20,12 @@ func main() { doSomething() } +// Stacktrace: +// panic: do something panic +// doSomething() +// main/files/recover8.gno:7 +// main() +// main/files/recover8.gno:20 + // Error: // do something panic diff --git a/gnovm/tests/files/std5_stdlibs.gno b/gnovm/tests/files/std5_stdlibs.gno index d8de58518f1..4afa09da8d3 100644 --- a/gnovm/tests/files/std5_stdlibs.gno +++ b/gnovm/tests/files/std5_stdlibs.gno @@ -11,5 +11,14 @@ func main() { println(caller2) } +// Stacktrace: +// panic: frame not found +// callerAt(n) +// gonative:std.callerAt +// std.GetCallerAt(2) +// std/native.gno:44 +// main() +// main/files/std5_stdlibs.gno:10 + // Error: // frame not found diff --git a/gnovm/tests/files/std8_stdlibs.gno b/gnovm/tests/files/std8_stdlibs.gno index 6964cec1d3d..ab5e15bd618 100644 --- a/gnovm/tests/files/std8_stdlibs.gno +++ b/gnovm/tests/files/std8_stdlibs.gno @@ -21,5 +21,18 @@ func main() { testutils.WrapCall(inner) } +// Stacktrace: +// panic: frame not found +// callerAt(n) +// gonative:std.callerAt +// std.GetCallerAt(4) +// std/native.gno:44 +// fn() +// main/files/std8_stdlibs.gno:16 +// testutils.WrapCall(inner) +// gno.land/p/demo/testutils/misc.gno:5 +// main() +// main/files/std8_stdlibs.gno:21 + // Error: // frame not found diff --git a/gnovm/tests/files/time17_native.gno b/gnovm/tests/files/time17_native.gno new file mode 100644 index 00000000000..6733c1381cb --- /dev/null +++ b/gnovm/tests/files/time17_native.gno @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "time" +) + +func main() { + now := time.Now() + now.In(nil) +} + +// Error: +// time: missing Location in call to Time.In + +// Stacktrace: +// now.In(gonative{*time.Location}) +// gofunction:func(*time.Location) time.Time +// main() +// main/files/time17_native.gno:10 diff --git a/gnovm/tests/files/typeassert1.gno b/gnovm/tests/files/typeassert1.gno index 041034e4bd0..f6609a3d18c 100644 --- a/gnovm/tests/files/typeassert1.gno +++ b/gnovm/tests/files/typeassert1.gno @@ -9,5 +9,10 @@ func main() { _ = a.(A) } +// Stacktrace: +// panic: interface conversion: interface is nil, not main.A +// main() +// main/files/typeassert1.gno:9 + // Error: // interface conversion: interface is nil, not main.A diff --git a/gnovm/tests/files/typeassert2a.gno b/gnovm/tests/files/typeassert2a.gno index 0441bf83437..bfbd24d38bd 100644 --- a/gnovm/tests/files/typeassert2a.gno +++ b/gnovm/tests/files/typeassert2a.gno @@ -11,5 +11,10 @@ func main() { }() } +// Stacktrace: +// panic: interface conversion: interface is nil, not main.A +// main() +// main/files/typeassert2a.gno:10 + // Error: // interface conversion: interface is nil, not main.A diff --git a/gnovm/tests/files/typeassert9.gno b/gnovm/tests/files/typeassert9.gno index d9d5bad55af..6ea072661c1 100644 --- a/gnovm/tests/files/typeassert9.gno +++ b/gnovm/tests/files/typeassert9.gno @@ -16,5 +16,10 @@ func main() { _ = reader.(Writer) } +// Stacktrace: +// panic: interface conversion: interface is nil, not main.Writer +// main() +// main/files/typeassert9.gno:16 + // Error: // interface conversion: interface is nil, not main.Writer diff --git a/gnovm/tests/files/zrealm_panic.gno b/gnovm/tests/files/zrealm_panic.gno new file mode 100644 index 00000000000..3864e2a7f7f --- /dev/null +++ b/gnovm/tests/files/zrealm_panic.gno @@ -0,0 +1,20 @@ +// PKGPATH: gno.land/r/test +package test + +type MyStruct struct{} + +func (ms MyStruct) Panic() { + panic("panic") +} + +func main() { + ms := MyStruct{} + ms.Panic() +} + +// Stacktrace: +// panic: panic +// ms.Panic() +// gno.land/r/test/main.gno:7 +// main() +// gno.land/r/test/main.gno:12