From 0f6b3578043e35bd1c12aa587c655302bb4e08e7 Mon Sep 17 00:00:00 2001 From: Marc Vertes Date: Thu, 19 Dec 2024 17:51:15 +0100 Subject: [PATCH] feat(gnovm): enable debugger for gno test This change brings interactive debugging to gno tests, using -debug flag. --- gnovm/cmd/gno/lint.go | 2 +- gnovm/cmd/gno/test.go | 17 +++++++++++++++++ gnovm/pkg/gnolang/debugger.go | 22 +++++++++++----------- gnovm/pkg/repl/repl.go | 2 +- gnovm/pkg/test/filetest.go | 1 + gnovm/pkg/test/test.go | 29 ++++++++++++++++++++++++----- 6 files changed, 55 insertions(+), 18 deletions(-) diff --git a/gnovm/cmd/gno/lint.go b/gnovm/cmd/gno/lint.go index a3e7f5310e1..19f506b2866 100644 --- a/gnovm/cmd/gno/lint.go +++ b/gnovm/cmd/gno/lint.go @@ -148,7 +148,7 @@ func execLint(cfg *lintCfg, args []string, io commands.IO) error { // Handle runtime errors hasRuntimeErr := catchRuntimeError(pkgPath, io.Err(), func() { - tm := test.Machine(testStore, stdout, memPkg.Path) + tm := test.Machine(testStore, stdout, memPkg.Path, false) defer tm.Release() // Check package diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index fec0de7c221..b3d5971965d 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -26,6 +26,8 @@ type testCfg struct { updateGoldenTests bool printRuntimeMetrics bool printEvents bool + debug bool + debugAddr string } func newTestCmd(io commands.IO) *commands.Command { @@ -143,6 +145,20 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { false, "print emitted events", ) + + fs.BoolVar( + &c.debug, + "debug", + false, + "enable interactive debugger using stdin and stdout", + ) + + fs.StringVar( + &c.debugAddr, + "debug-addr", + "", + "enable interactive debugger using tcp address in the form [host]:port", + ) } func execTest(cfg *testCfg, args []string, io commands.IO) error { @@ -187,6 +203,7 @@ func execTest(cfg *testCfg, args []string, io commands.IO) error { opts.Verbose = cfg.verbose opts.Metrics = cfg.printRuntimeMetrics opts.Events = cfg.printEvents + opts.Debug = cfg.debug buildErrCount := 0 testErrCount := 0 diff --git a/gnovm/pkg/gnolang/debugger.go b/gnovm/pkg/gnolang/debugger.go index f047a176af7..1dfa20e7b26 100644 --- a/gnovm/pkg/gnolang/debugger.go +++ b/gnovm/pkg/gnolang/debugger.go @@ -43,19 +43,19 @@ type Debugger struct { out io.Writer // debugger output, defaults to Stdout scanner *bufio.Scanner // to parse input per line - state DebugState // current state of debugger - lastCmd string // last debugger command - lastArg string // last debugger command arguments - loc Location // source location of the current machine instruction - prevLoc Location // source location of the previous machine instruction - breakpoints []Location // list of breakpoints set by user, as source locations - call []Location // for function tracking, ideally should be provided by machine frame - frameLevel int // frame level of the current machine instruction - getSrc func(string) string // helper to access source from repl or others + state DebugState // current state of debugger + lastCmd string // last debugger command + lastArg string // last debugger command arguments + loc Location // source location of the current machine instruction + prevLoc Location // source location of the previous machine instruction + breakpoints []Location // list of breakpoints set by user, as source locations + call []Location // for function tracking, ideally should be provided by machine frame + frameLevel int // frame level of the current machine instruction + getSrc func(string, string) string // helper to access source from repl or others } // Enable makes the debugger d active, using in as input reader, out as output writer and f as a source helper. -func (d *Debugger) Enable(in io.Reader, out io.Writer, f func(string) string) { +func (d *Debugger) Enable(in io.Reader, out io.Writer, f func(string, string) string) { d.in = in d.out = out d.enabled = true @@ -505,7 +505,7 @@ func debugList(m *Machine, arg string) (err error) { if err != nil { // Use optional getSrc helper as fallback to get source. if m.Debugger.getSrc != nil { - src = m.Debugger.getSrc(loc.File) + src = m.Debugger.getSrc(loc.PkgPath, loc.File) } if src == "" { return err diff --git a/gnovm/pkg/repl/repl.go b/gnovm/pkg/repl/repl.go index b0944d21646..4baec45aa7b 100644 --- a/gnovm/pkg/repl/repl.go +++ b/gnovm/pkg/repl/repl.go @@ -156,7 +156,7 @@ func (r *Repl) Process(input string) (out string, err error) { r.state.id++ if r.debug { - r.state.machine.Debugger.Enable(os.Stdin, os.Stdout, func(file string) string { + r.state.machine.Debugger.Enable(os.Stdin, os.Stdout, func(ppath, file string) string { return r.state.files[file] }) r.debug = false diff --git a/gnovm/pkg/test/filetest.go b/gnovm/pkg/test/filetest.go index 12bc9ed7f28..f2bf6bcb353 100644 --- a/gnovm/pkg/test/filetest.go +++ b/gnovm/pkg/test/filetest.go @@ -63,6 +63,7 @@ func (opts *TestOptions) runFiletest(filename string, source []byte) (string, er Store: opts.TestStore.BeginTransaction(cw, cw), Context: ctx, MaxAllocBytes: maxAlloc, + Debug: opts.Debug, }) defer m.Release() result := opts.runTest(m, pkgPath, filename, source) diff --git a/gnovm/pkg/test/test.go b/gnovm/pkg/test/test.go index 3ea3d4bc9bd..a7df187e2c9 100644 --- a/gnovm/pkg/test/test.go +++ b/gnovm/pkg/test/test.go @@ -71,11 +71,12 @@ func Context(pkgPath string, send std.Coins) *teststd.TestExecContext { } // Machine is a minimal machine, set up with just the Store, Output and Context. -func Machine(testStore gno.Store, output io.Writer, pkgPath string) *gno.Machine { +func Machine(testStore gno.Store, output io.Writer, pkgPath string, debug bool) *gno.Machine { return gno.NewMachineWithOptions(gno.MachineOptions{ Store: testStore, Output: output, Context: Context(pkgPath, nil), + Debug: debug, }) } @@ -108,6 +109,8 @@ type TestOptions struct { Output io.Writer // Used for os.Stderr, and for printing errors. Error io.Writer + // Debug enables the interactive debugger on gno tests. + Debug bool // Not set by NewTestOptions: @@ -288,9 +291,8 @@ func (opts *TestOptions) runTestFiles( // reset store ops, if any - we only need them for some filetests. opts.TestStore.SetLogStoreOps(false) - // Check if we already have the package - it may have been eagerly - // loaded. - m = Machine(gs, opts.WriterForStore(), memPkg.Path) + // Check if we already have the package - it may have been eagerly loaded. + m = Machine(gs, opts.WriterForStore(), memPkg.Path, opts.Debug) m.Alloc = alloc if opts.TestStore.GetMemPackage(memPkg.Path) == nil { m.RunMemPackage(memPkg, true) @@ -310,7 +312,7 @@ func (opts *TestOptions) runTestFiles( // - Run the test files before this for loop (but persist it to store; // RunFiles doesn't do that currently) // - Wrap here. - m = Machine(gs, opts.Output, memPkg.Path) + m = Machine(gs, opts.Output, memPkg.Path, opts.Debug) m.Alloc = alloc m.SetActivePackage(pv) @@ -318,6 +320,23 @@ func (opts *TestOptions) runTestFiles( testingtv := gno.TypedValue{T: &gno.PackageType{}, V: testingpv} testingcx := &gno.ConstExpr{TypedValue: testingtv} + if opts.Debug { + fileContent := func(ppath, name string) string { + p := filepath.Join(opts.RootDir, ppath, name) + b, err := os.ReadFile(p) + if err != nil { + p = filepath.Join(opts.RootDir, "gnovm", "stdlibs", ppath, name) + b, err = os.ReadFile(p) + } + if err != nil { + p = filepath.Join(opts.RootDir, "examples", ppath, name) + b, err = os.ReadFile(p) + } + return string(b) + } + m.Debugger.Enable(os.Stdin, os.Stdout, fileContent) + } + eval := m.Eval(gno.Call( gno.Sel(testingcx, "RunTest"), // Call testing.RunTest gno.Str(opts.RunFlag), // run flag