Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(gnovm): add stacktraces and log them in panic messages #2145

Merged
merged 38 commits into from
Aug 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
7cfe142
feat: add stack trace on machine
omarsy May 20, 2024
3435ac4
Merge branch 'master' into feat/1812
omarsy May 21, 2024
b66cc58
Merge branch 'master' into feat/1812
omarsy May 27, 2024
149c1fa
feat: new design for stacktrace
omarsy Jun 1, 2024
eb9c736
feat: use loadpkg
omarsy Jun 8, 2024
44a00cb
Merge branch 'master' into feat/1812
omarsy Jun 23, 2024
096eaed
feat: Use a stack trace struct type which contains the frame and stat…
omarsy Jun 23, 2024
9809878
feat: include goNative
omarsy Jun 24, 2024
c310e25
feat: improve the readability + use named return on Stacktrace
omarsy Jun 24, 2024
004be13
feat: add stacktrace on gno file test
omarsy Jun 27, 2024
f5b83bc
refactor: remove todo
omarsy Jun 27, 2024
e8b1a2c
refactor:removal of unnecessary else
omarsy Jun 27, 2024
afe0356
Merge branch 'master' into feat/1812
omarsy Jun 27, 2024
4274e36
feat: add stacktrace size mechanism
omarsy Jun 27, 2024
7cf4c76
feat: use ExceptionsStacktrace on run function
omarsy Jun 27, 2024
2489375
refactor: trim execution when we build the struct
omarsy Jun 29, 2024
d3f5672
Merge branch 'master' into feat/1812
omarsy Jul 9, 2024
76faf71
refactor: use fmt.Fprint
omarsy Jul 18, 2024
bc4ee21
refactor: delete stacktrace file
omarsy Jul 18, 2024
bc40a40
feat: add stacktrace check on debug test
omarsy Jul 20, 2024
0bbc0c2
refactor: change execution to stacktracecall
omarsy Jul 20, 2024
0a88b09
feat: make stacktrace more golike
omarsy Jul 20, 2024
789dd14
feat: add more tests
omarsy Jul 21, 2024
c7820cd
feat: improve how to display argument
omarsy Jul 21, 2024
4ee0eeb
feat: don't print all exceptions
omarsy Jul 21, 2024
5e0cf7e
feat: print primitive
omarsy Jul 22, 2024
8bf31cc
Merge branch 'master' into feat/1812
omarsy Jul 22, 2024
fb2c678
fix: lint
omarsy Jul 22, 2024
f52079a
feat: add machine string when we panic
omarsy Jul 23, 2024
51202f2
fix: typo
omarsy Jul 23, 2024
44793f4
refactor: some suggestion
omarsy Jul 24, 2024
6e30e4e
feat: improve coverage
omarsy Jul 24, 2024
85dbb1f
Merge branch 'master' into feat/1812
omarsy Jul 24, 2024
8f148b1
feat: clean some primitive type
omarsy Jul 25, 2024
eead428
feat: introduces the `RealmUnhandledPanicException` type to represent…
omarsy Jul 25, 2024
887dae2
feat: add stacktrave in panic2b.gno file
omarsy Jul 25, 2024
d3f6b4a
Merge branch 'master' into feat/1812
omarsy Jul 31, 2024
778965e
chore: rename `RealmUnhandledPanicException` type to `UnhandledPanicE…
omarsy Aug 1, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions gno.land/cmd/gnoland/testdata/panic.txtar
Original file line number Diff line number Diff line change
@@ -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\<VPBlock\(3\,0\)\>\(\)'
stderr 'gno.land/r/demo/panic/panic.gno:5'
stderr 'pkg\<VPBlock\(1\,0\)\>\.Trigger\(\)'
stderr 'gno.land/r/demo/panic/panic.gno:9'

-- panic.gno --
package main

func p() {
i := "here"
panic(i)
}

func Trigger() {
p()
}

9 changes: 6 additions & 3 deletions gno.land/pkg/sdk/vm/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand Down
10 changes: 8 additions & 2 deletions gnovm/cmd/gno/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,14 @@
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())

Check warning on line 194 in gnovm/cmd/gno/run.go

View check run for this annotation

Codecov / codecov/patch

gnovm/cmd/gno/run.go#L192-L194

Added lines #L192 - L194 were not covered by tests
default:
fmt.Printf("panic running expression %s: %v\nMachine State:%s\nStacktrace: %s\n",
expr, r, m.String(), m.Stacktrace().String())
}
panic(r)
}
}()
Expand Down
20 changes: 16 additions & 4 deletions gnovm/pkg/gnolang/debugger_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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
}

Expand All @@ -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)
Expand Down Expand Up @@ -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") {
Expand Down
66 changes: 52 additions & 14 deletions gnovm/pkg/gnolang/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package gnolang_test
import (
"os"
"path"
"sort"
"strings"
"testing"
)
Expand All @@ -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)
}
Expand All @@ -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
}
123 changes: 123 additions & 0 deletions gnovm/pkg/gnolang/frame.go
thehowl marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,11 @@

import (
"fmt"
"strings"
)

const maxStacktraceSize = 128

//----------------------------------------
// (runtime) Frame

Expand Down Expand Up @@ -64,6 +67,10 @@
}
}

func (fr *Frame) IsCall() bool {
deelawn marked this conversation as resolved.
Show resolved Hide resolved
return fr.Func != nil || fr.GoFunc != nil
}

func (fr *Frame) PushDefer(dfr Defer) {
fr.Defers = append(fr.Defers, dfr)
}
Expand Down Expand Up @@ -92,3 +99,119 @@
// 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")

Check warning on line 133 in gnovm/pkg/gnolang/frame.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L132-L133

Added lines #L132 - L133 were not covered by tests
}
}
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("<elided><len=%d>", lenEl)

Check warning on line 162 in gnovm/pkg/gnolang/frame.go

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L162

Added line #L162 was not covered by tests
}

return fmt.Sprintf("%s<len=%d>", 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()
}
Loading
Loading