From f4c4204c183cd234f3eefa284da1e19110ba712c Mon Sep 17 00:00:00 2001 From: Manfred Touron <94029+moul@users.noreply.github.com> Date: Wed, 23 Oct 2024 13:55:31 -0400 Subject: [PATCH] feat(gnovm): add 'gno test -print-events' + cleanup machine between tests (#2975) - [x] add `gno test -print-events` flag for unit tests. Props to @r3v4s for his work on #2071 - [x] add `// Events:` support in `_filetests.gno`. - [x] cleanup `gno.Machine` between unit tests (\o/) . Fixes #1982 Closes #2071 Addresses #2007 --------- Signed-off-by: moul <94029+moul@users.noreply.github.com> --- .github/workflows/examples.yml | 2 +- .../gno.land/p/demo/ownable/ownable_test.gno | 9 --- .../gno.land/r/demo/event/z1_filetest.gno | 11 ++++ examples/gno.land/r/gnoland/events/events.gno | 9 +-- .../gno.land/r/gnoland/events/events_test.gno | 13 ++-- gnovm/cmd/gno/test.go | 29 ++++++++- .../testdata/gno_test/filetest_events.txtar | 33 ++++++++++ .../testdata/gno_test/multitest_events.txtar | 26 ++++++++ gnovm/tests/file.go | 63 ++++++++++++++++++- 9 files changed, 170 insertions(+), 25 deletions(-) create mode 100644 examples/gno.land/r/demo/event/z1_filetest.gno create mode 100644 gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar create mode 100644 gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 5b3c3c1fbf1..77d40098900 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -47,7 +47,7 @@ jobs: echo "LOG_LEVEL=debug" >> $GITHUB_ENV echo "LOG_PATH_DIR=$LOG_PATH_DIR" >> $GITHUB_ENV - run: go install -v ./gnovm/cmd/gno - - run: go run ./gnovm/cmd/gno test -v ./examples/... + - run: go run ./gnovm/cmd/gno test -v -print-events ./examples/... lint: strategy: fail-fast: false diff --git a/examples/gno.land/p/demo/ownable/ownable_test.gno b/examples/gno.land/p/demo/ownable/ownable_test.gno index a9d97154f45..dee40fa6e1d 100644 --- a/examples/gno.land/p/demo/ownable/ownable_test.gno +++ b/examples/gno.land/p/demo/ownable/ownable_test.gno @@ -33,15 +33,6 @@ func TestNewWithAddress(t *testing.T) { } } -func TestOwner(t *testing.T) { - std.TestSetRealm(std.NewUserRealm(alice)) - - o := New() - expected := alice - got := o.Owner() - uassert.Equal(t, expected, got) -} - func TestTransferOwnership(t *testing.T) { std.TestSetRealm(std.NewUserRealm(alice)) diff --git a/examples/gno.land/r/demo/event/z1_filetest.gno b/examples/gno.land/r/demo/event/z1_filetest.gno new file mode 100644 index 00000000000..1fcfa1a0e4f --- /dev/null +++ b/examples/gno.land/r/demo/event/z1_filetest.gno @@ -0,0 +1,11 @@ +package main + +import "gno.land/r/demo/event" + +func main() { + event.Emit("foo") + event.Emit("bar") +} + +// Events: +// [{"type":"TAG","attrs":[{"key":"key","value":"foo"}],"pkg_path":"gno.land/r/demo/event","func":"Emit"},{"type":"TAG","attrs":[{"key":"key","value":"bar"}],"pkg_path":"gno.land/r/demo/event","func":"Emit"}] diff --git a/examples/gno.land/r/gnoland/events/events.gno b/examples/gno.land/r/gnoland/events/events.gno index 0984edf75a9..baf9ba3d4af 100644 --- a/examples/gno.land/r/gnoland/events/events.gno +++ b/examples/gno.land/r/gnoland/events/events.gno @@ -73,8 +73,7 @@ func AddEvent(name, description, link, location, startTime, endTime string) (str sort.Sort(events) std.Emit(EventAdded, - "id", - e.id, + "id", e.id, ) return id, nil @@ -92,8 +91,7 @@ func DeleteEvent(id string) { events = append(events[:idx], events[idx+1:]...) std.Emit(EventDeleted, - "id", - e.id, + "id", e.id, ) } @@ -142,8 +140,7 @@ func EditEvent(id string, name, description, link, location, startTime, endTime } std.Emit(EventEdited, - "id", - e.id, + "id", e.id, ) } diff --git a/examples/gno.land/r/gnoland/events/events_test.gno b/examples/gno.land/r/gnoland/events/events_test.gno index 357857352d8..1d79b754ee4 100644 --- a/examples/gno.land/r/gnoland/events/events_test.gno +++ b/examples/gno.land/r/gnoland/events/events_test.gno @@ -85,7 +85,8 @@ func TestAddEventErrors(t *testing.T) { } func TestDeleteEvent(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) e1End := e1Start.Add(time.Hour * 4) @@ -107,7 +108,8 @@ func TestDeleteEvent(t *testing.T) { } func TestEditEvent(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) e1Start := parsedTimeNow.Add(time.Hour * 24 * 5) e1End := e1Start.Add(time.Hour * 4) @@ -136,7 +138,8 @@ func TestEditEvent(t *testing.T) { } func TestInvalidEdit(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) uassert.PanicsWithMessage(t, ErrNoSuchID.Error(), func() { EditEvent("123123", "", "", "", "", "", "") @@ -162,9 +165,11 @@ func TestParseTimes(t *testing.T) { } func TestRenderEventWidget(t *testing.T) { - events = nil // remove elements from previous tests - see issue #1982 + std.TestSetOrigCaller(su) + std.TestSetRealm(suRealm) // No events yet + events = nil out, err := RenderEventWidget(1) uassert.NoError(t, err) uassert.Equal(t, out, "No events.") diff --git a/gnovm/cmd/gno/test.go b/gnovm/cmd/gno/test.go index af7fa28a14d..e23f9fa2750 100644 --- a/gnovm/cmd/gno/test.go +++ b/gnovm/cmd/gno/test.go @@ -21,6 +21,7 @@ import ( gno "github.com/gnolang/gno/gnovm/pkg/gnolang" "github.com/gnolang/gno/gnovm/pkg/gnomod" "github.com/gnolang/gno/gnovm/tests" + teststd "github.com/gnolang/gno/gnovm/tests/stdlibs/std" "github.com/gnolang/gno/tm2/pkg/commands" "github.com/gnolang/gno/tm2/pkg/errors" "github.com/gnolang/gno/tm2/pkg/random" @@ -35,6 +36,7 @@ type testCfg struct { timeout time.Duration updateGoldenTests bool printRuntimeMetrics bool + printEvents bool withNativeFallback bool } @@ -149,6 +151,13 @@ func (c *testCfg) RegisterFlags(fs *flag.FlagSet) { false, "print runtime metrics (gas, memory, cpu cycles)", ) + + fs.BoolVar( + &c.printEvents, + "print-events", + false, + "print emitted events", + ) } func execTest(cfg *testCfg, args []string, io commands.IO) error { @@ -228,6 +237,7 @@ func gnoTestPkg( rootDir = cfg.rootDir runFlag = cfg.run printRuntimeMetrics = cfg.printRuntimeMetrics + printEvents = cfg.printEvents stdin = io.In() stdout = io.Out() @@ -295,7 +305,7 @@ func gnoTestPkg( m.Alloc = gno.NewAllocator(maxAllocTx) } m.RunMemPackage(memPkg, true) - err := runTestFiles(m, tfiles, memPkg.Name, verbose, printRuntimeMetrics, runFlag, io) + err := runTestFiles(m, tfiles, memPkg.Name, verbose, printRuntimeMetrics, printEvents, runFlag, io) if err != nil { errs = multierr.Append(errs, err) } @@ -329,7 +339,7 @@ func gnoTestPkg( memPkg.Path = memPkg.Path + "_test" m.RunMemPackage(memPkg, true) - err := runTestFiles(m, ifiles, testPkgName, verbose, printRuntimeMetrics, runFlag, io) + err := runTestFiles(m, ifiles, testPkgName, verbose, printRuntimeMetrics, printEvents, runFlag, io) if err != nil { errs = multierr.Append(errs, err) } @@ -419,6 +429,7 @@ func runTestFiles( pkgName string, verbose bool, printRuntimeMetrics bool, + printEvents bool, runFlag string, io commands.IO, ) (errs error) { @@ -448,10 +459,24 @@ func runTestFiles( m.RunFiles(n) for _, test := range testFuncs.Tests { + // cleanup machine between tests + tests.CleanupMachine(m) + testFuncStr := fmt.Sprintf("%q", test.Name) eval := m.Eval(gno.Call("runtest", testFuncStr)) + if printEvents { + events := m.Context.(*teststd.TestExecContext).EventLogger.Events() + if events != nil { + res, err := json.Marshal(events) + if err != nil { + panic(err) + } + io.ErrPrintfln("EVENTS: %s", string(res)) + } + } + ret := eval[0].GetString() if ret == "" { err := errors.New("failed to execute unit test: %q", test.Name) diff --git a/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar b/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar new file mode 100644 index 00000000000..5e0520a2e85 --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/filetest_events.txtar @@ -0,0 +1,33 @@ +# Test with a valid _filetest.gno file + +gno test -print-events . + +! stdout .+ +stderr 'ok \. \d\.\d\ds' + +gno test -print-events -v . + +! stdout .+ +stderr '=== RUN file/valid_filetest.gno' +stderr '--- PASS: file/valid_filetest.gno \(\d\.\d\ds\)' +stderr 'ok \. \d\.\d\ds' + +-- valid.gno -- +package valid + +-- valid_filetest.gno -- +package main + +import "std" + +func main() { + println("test") + std.Emit("EventA") + std.Emit("EventB", "keyA", "valA") +} + +// Output: +// test + +// Events: +// [{"type":"EventA","attrs":[],"pkg_path":"","func":"main"},{"type":"EventB","attrs":[{"key":"keyA","value":"valA"}],"pkg_path":"","func":"main"}] diff --git a/gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar b/gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar new file mode 100644 index 00000000000..321c790561a --- /dev/null +++ b/gnovm/cmd/gno/testdata/gno_test/multitest_events.txtar @@ -0,0 +1,26 @@ +# Test with a valid _test.gno file + +gno test -print-events . + +! stdout .+ +stderr 'EVENTS: \[{\"type\":\"EventA\",\"attrs\":\[\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestA\"}\]' +stderr 'EVENTS: \[{\"type\":\"EventB\",\"attrs\":\[{\"key\":\"keyA\",\"value\":\"valA\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"},{\"type\":\"EventC\",\"attrs\":\[{\"key\":\"keyD\",\"value\":\"valD\"}\],\"pkg_path\":\"gno.land/r/.*\",\"func\":\"TestB\"}\]' +stderr 'ok \. \d\.\d\ds' + +-- valid.gno -- +package valid + +-- valid_test.gno -- +package valid + +import "testing" +import "std" + +func TestA(t *testing.T) { + std.Emit("EventA") +} + +func TestB(t *testing.T) { + std.Emit("EventB", "keyA", "valA") + std.Emit("EventC", "keyD", "valD") +} diff --git a/gnovm/tests/file.go b/gnovm/tests/file.go index f45beffe648..5449adc01d2 100644 --- a/gnovm/tests/file.go +++ b/gnovm/tests/file.go @@ -2,6 +2,7 @@ package tests import ( "bytes" + "encoding/json" "fmt" "go/ast" "go/parser" @@ -54,7 +55,7 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { pkgAddr := gno.DerivePkgAddr(pkgPath) // the addr of the pkgPath called. caller := gno.DerivePkgAddr("user1.gno") - pkgCoins := std.MustParseCoins(ugnot.ValueString(200000000)).Add(send) // >= send. + pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(send) // >= send. banker := newTestBanker(pkgAddr.Bech32(), pkgCoins) ctx := stdlibs.ExecContext{ ChainID: "dev", @@ -74,6 +75,19 @@ func TestContext(pkgPath string, send std.Coins) *teststd.TestExecContext { } } +// CleanupMachine can be called during two tests while reusing the same Machine instance. +func CleanupMachine(m *gno.Machine) { + prevCtx := m.Context.(*teststd.TestExecContext) + prevSend := prevCtx.OrigSend + + newCtx := TestContext("", prevCtx.OrigSend) + pkgCoins := std.MustParseCoins(ugnot.ValueString(200_000_000)).Add(prevSend) // >= send. + banker := newTestBanker(prevCtx.OrigPkgAddr, pkgCoins) + newCtx.OrigPkgAddr = prevCtx.OrigPkgAddr + newCtx.Banker = banker + m.Context = newCtx +} + type runFileTestOptions struct { nativeLibs bool logger loggerFunc @@ -110,7 +124,7 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { opt(&f) } - directives, pkgPath, resWanted, errWanted, rops, stacktraceWanted, maxAlloc, send, preWanted := wantedFromComment(path) + directives, pkgPath, resWanted, errWanted, rops, eventsWanted, stacktraceWanted, maxAlloc, send, preWanted := wantedFromComment(path) if pkgPath == "" { pkgPath = "main" } @@ -347,6 +361,45 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { } } } + case "Events": + // panic if got unexpected error + + if pnc != nil { + if tv, ok := pnc.(*gno.TypedValue); ok { + panic(fmt.Sprintf("fail on %s: got unexpected error: %s", path, tv.Sprint(m))) + } else { // happens on 'unknown import path ...' + panic(fmt.Sprintf("fail on %s: got unexpected error: %v", path, pnc)) + } + } + // check result + events := m.Context.(*teststd.TestExecContext).EventLogger.Events() + evtjson, err := json.Marshal(events) + if err != nil { + panic(err) + } + evtstr := trimTrailingSpaces(string(evtjson)) + if evtstr != eventsWanted { + if f.syncWanted { + // write output to file. + replaceWantedInPlace(path, "Events", evtstr) + } else { + // panic so tests immediately fail (for now). + if eventsWanted == "" { + panic(fmt.Sprintf("fail on %s: got unexpected events: %s", path, evtstr)) + } else { + diff, _ := difflib.GetUnifiedDiffString(difflib.UnifiedDiff{ + A: difflib.SplitLines(eventsWanted), + B: difflib.SplitLines(evtstr), + FromFile: "Expected", + FromDate: "", + ToFile: "Actual", + ToDate: "", + Context: 1, + }) + panic(fmt.Sprintf("fail on %s: diff:\n%s\n", path, diff)) + } + } + } case "Realm": // panic if got unexpected error if pnc != nil { @@ -448,7 +501,7 @@ func RunFileTest(rootDir string, path string, opts ...RunFileTestOption) error { return nil } -func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops, stacktrace string, maxAlloc int64, send std.Coins, pre string) { +func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops, events, stacktrace string, maxAlloc int64, send std.Coins, pre string) { fset := token.NewFileSet() f, err2 := parser.ParseFile(fset, p, nil, parser.ParseComments) if err2 != nil { @@ -490,6 +543,10 @@ func wantedFromComment(p string) (directives []string, pkgPath, res, err, rops, rops = strings.TrimPrefix(text, "Realm:\n") rops = strings.TrimSpace(rops) directives = append(directives, "Realm") + } else if strings.HasPrefix(text, "Events:\n") { + events = strings.TrimPrefix(text, "Events:\n") + events = strings.TrimSpace(events) + directives = append(directives, "Events") } else if strings.HasPrefix(text, "Preprocessed:\n") { pre = strings.TrimPrefix(text, "Preprocessed:\n") pre = strings.TrimSpace(pre)