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 29 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()
}

4 changes: 2 additions & 2 deletions gno.land/pkg/sdk/vm/keeper.go
Original file line number Diff line number Diff line change
Expand Up @@ -540,8 +540,8 @@ func (vm *VMKeeper) Call(ctx sdk.Context, msg MsgCall) (res string, err error) {
case store.OutOfGasException: // panic in consumeGas()
panic(r)
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.ExceptionsStacktrace())
return
}
}
Expand Down
4 changes: 2 additions & 2 deletions gnovm/cmd/gno/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -188,8 +188,8 @@ 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())
fmt.Printf("panic running expression %s: %v\nMachine State:%s\nStacktrace: %s\n",
expr, r, m.String(), m.ExceptionsStacktrace())
panic(r)
}
}()
Expand Down
11 changes: 7 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, qtacktrace string) {
bout := bytes.NewBufferString("")
berr := bytes.NewBufferString("")
stdin := bytes.NewBufferString(in)
Expand Down Expand Up @@ -58,6 +58,9 @@ func evalTest(debugAddr, in, file string) (out, err string) {
})

defer m.Release()
defer func() {
qtacktrace = strings.TrimSpace(strings.ReplaceAll(m.ExceptionsStacktrace(), "../../tests/files/", "files/"))
}()

if debugAddr != "" {
if e := m.Debugger.Serve(debugAddr); e != nil {
Expand All @@ -69,7 +72,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, qtacktrace = bout.String(), berr.String(), m.ExceptionsStacktrace()
return
}

Expand All @@ -78,7 +81,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 +197,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
67 changes: 53 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,22 @@ 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, qtacktrace := 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(qtacktrace, wantStacktrace) ||
wantStacktrace == "" && qtacktrace != "" {
t.Fatalf("unexpected stacktrace\nWant: %s\n Got: %s", wantStacktrace, qtacktrace)
}
if wantOut != "" && out != wantOut {
t.Fatalf("unexpected output\nWant: %s\n Got: %s", wantOut, out)
}
Expand All @@ -33,30 +39,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
}
137 changes: 137 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,133 @@
// 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())
default:
thehowl marked this conversation as resolved.
Show resolved Hide resolved
fmt.Fprintf(&builder, "%s\n", toExprTrace(cx))
fmt.Fprintf(&builder, " %s\n", call.Frame.GoFunc.Value.Type())

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L129-L131

Added lines #L129 - L131 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, toExprTrace(ex.X))

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L147-L148

Added lines #L147 - L148 were not covered by tests
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))

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L151-L156

Added lines #L151 - L156 were not covered by tests
case *CompositeLitExpr:
lenEl := len(ex.Elts)
if ex.Type == nil {
return fmt.Sprintf("<elided><len=%d>", lenEl)

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L160

Added line #L160 was not covered by tests
}

return fmt.Sprintf("%s<len=%d>", toExprTrace(ex.Type), lenEl)

case *KeyValueExpr:
return fmt.Sprintf("%s: %s", toExprTrace(ex.Key), toExprTrace(ex.Value))

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L165-L166

Added lines #L165 - L166 were not covered by tests
case *FuncLitExpr:
return fmt.Sprintf("%s{ ... }", toExprTrace(&ex.Type))
case *TypeAssertExpr:
return fmt.Sprintf("%s.(%s)", toExprTrace(ex.X), toExprTrace(ex.Type))

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L169-L170

Added lines #L169 - L170 were not covered by tests
case *ConstExpr:
return toTypeValueTrace(ex.TypedValue)
case *NameExpr, *BasicLitExpr, *SliceExpr:
return ex.String()
}

return ex.String()
}

func toTypeValueTrace(tv TypedValue) string {
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()

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L184-L187

Added lines #L184 - L187 were not covered by tests
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 DataByteType:
return fmt.Sprintf("%d", tv.GetDataByte())
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())
case UntypedBigintType, BigintType:
return tv.V.(BigintValue).V.String()
case UntypedBigdecType, BigdecType:
return tv.V.(BigdecValue).V.String()

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L190-L217

Added lines #L190 - L217 were not covered by tests
}
case *ArrayType:
v := tv.V.(*ArrayValue)
return fmt.Sprintf("%s<len=%d>", tv.T.String(), v.GetLength())

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

View check run for this annotation

Codecov / codecov/patch

gnovm/pkg/gnolang/frame.go#L219-L221

Added lines #L219 - L221 were not covered by tests
case *SliceType:
v := tv.V.(*SliceValue)
return fmt.Sprintf("%s<len=%d>", tv.T.String(), v.Length)
case *MapType:
v := tv.V.(*MapValue)
return fmt.Sprintf("%s<len=%d>", tv.T.String(), v.List.Size)
}

return tv.T.String()
}
Loading
Loading