From f1ecdb82cc3a0a0bcf3f6b4bf38f2f1d06532ab9 Mon Sep 17 00:00:00 2001 From: Anna Shaleva Date: Tue, 4 Oct 2022 13:05:51 +0300 Subject: [PATCH] vm: add 'events' command to VM CLI And dump events automatically after HALTed or FAULTed end of execution. --- cli/vm/cli.go | 45 +++++++++++++++++++++++++++- cli/vm/cli_test.go | 58 ++++++++++++++++++++++++++++++++++++ internal/basicchain/basic.go | 20 +++++++++---- 3 files changed, 116 insertions(+), 7 deletions(-) diff --git a/cli/vm/cli.go b/cli/vm/cli.go index b57c6c81c..58495944c 100644 --- a/cli/vm/cli.go +++ b/cli/vm/cli.go @@ -225,6 +225,12 @@ example: Description: "Dump opcodes of the current loaded program", Action: handleOps, }, + { + Name: "events", + Usage: "Dump events emitted by the current loaded program", + Description: "Dump events emitted by the current loaded program", + Action: handleEvents, + }, } var completer *readline.PrefixCompleter @@ -626,12 +632,17 @@ func runVMWithHandling(c *cli.Context) { writeErr(c.App.ErrWriter, err) } - var message string + var ( + message string + dumpNtf bool + ) switch { case v.HasFailed(): message = "" // the error will be printed on return + dumpNtf = true case v.HasHalted(): message = v.DumpEStack() + dumpNtf = true case v.AtBreakpoint(): ctx := v.Context() if ctx.NextIP() < ctx.LenInstr() { @@ -641,6 +652,16 @@ func runVMWithHandling(c *cli.Context) { message = "execution has finished" } } + if dumpNtf { + var e string + e, err = dumpEvents(c.App) + if err == nil && len(e) != 0 { + if message != "" { + message += "\n" + } + message += "Events:\n" + e + } + } if message != "" { fmt.Fprintln(c.App.Writer, message) } @@ -733,6 +754,28 @@ func changePrompt(app *cli.App) { } } +func handleEvents(c *cli.Context) error { + e, err := dumpEvents(c.App) + if err != nil { + writeErr(c.App.ErrWriter, err) + return nil + } + fmt.Fprintln(c.App.Writer, e) + return nil +} + +func dumpEvents(app *cli.App) (string, error) { + ic := getInteropContextFromContext(app) + if len(ic.Notifications) == 0 { + return "", nil + } + b, err := json.MarshalIndent(ic.Notifications, "", "\t") + if err != nil { + return "", fmt.Errorf("failed to marshal notifications: %w", err) + } + return string(b), nil +} + // Run waits for user input from Stdin and executes the passed command. func (c *VMCLI) Run() error { if getPrintLogoFromContext(c.shell) { diff --git a/cli/vm/cli_test.go b/cli/vm/cli_test.go index a84141930..4c71304fe 100644 --- a/cli/vm/cli_test.go +++ b/cli/vm/cli_test.go @@ -20,6 +20,7 @@ import ( "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/config" "github.com/nspcc-dev/neo-go/pkg/core/interop/interopnames" + "github.com/nspcc-dev/neo-go/pkg/core/state" "github.com/nspcc-dev/neo-go/pkg/core/storage" "github.com/nspcc-dev/neo-go/pkg/core/storage/dbconfig" "github.com/nspcc-dev/neo-go/pkg/encoding/address" @@ -158,6 +159,12 @@ func (e *executor) checkNextLine(t *testing.T, expected string) { require.Regexp(t, expected, line) } +func (e *executor) checkNextLineExact(t *testing.T, expected string) { + line, err := e.out.ReadString('\n') + require.NoError(t, err) + require.Equal(t, expected, line) +} + func (e *executor) checkError(t *testing.T, expectedErr error) { line, err := e.out.ReadString('\n') require.NoError(t, err) @@ -190,6 +197,30 @@ func (e *executor) checkStack(t *testing.T, items ...interface{}) { require.NoError(t, err) } +func (e *executor) checkEvents(t *testing.T, isKeywordExpected bool, events ...state.NotificationEvent) { + if isKeywordExpected { + e.checkNextLine(t, "Events:") + } + d := json.NewDecoder(e.out) + var actual interface{} + require.NoError(t, d.Decode(&actual)) + rawActual, err := json.Marshal(actual) + require.NoError(t, err) + + rawExpected, err := json.Marshal(events) + require.NoError(t, err) + require.JSONEq(t, string(rawExpected), string(rawActual)) + + // Decoder has it's own buffer, we need to return unread part to the output. + outRemain := e.out.String() + e.out.Reset() + _, err = gio.Copy(e.out, d.Buffered()) + require.NoError(t, err) + e.out.WriteString(outRemain) + _, err = e.out.ReadString('\n') + require.NoError(t, err) +} + func (e *executor) checkSlot(t *testing.T, items ...interface{}) { d := json.NewDecoder(e.out) var actual interface{} @@ -728,3 +759,30 @@ func TestRunWithState(t *testing.T) { e.checkNextLine(t, "READY: loaded 37 instructions") e.checkStack(t, 3) } + +func TestEvents(t *testing.T) { + e := newTestVMClIWithState(t) + + script := io.NewBufBinWriter() + h, err := e.cli.chain.GetContractScriptHash(2) // examples/runtime/runtime.go + require.NoError(t, err) + emit.AppCall(script.BinWriter, h, "notify", callflag.All, []interface{}{true, 5}) + e.runProg(t, + "loadhex "+hex.EncodeToString(script.Bytes()), + "run", + "events") + expectedEvent := state.NotificationEvent{ + ScriptHash: h, + Name: "Event", + Item: stackitem.NewArray([]stackitem.Item{ + stackitem.NewArray([]stackitem.Item{ + stackitem.Make(true), + stackitem.Make(5), + }), + }), + } + e.checkNextLine(t, "READY: loaded 44 instructions") + e.checkStack(t, stackitem.Null{}) + e.checkEvents(t, true, expectedEvent) // automatically printed after `run` command + e.checkEvents(t, false, expectedEvent) // printed after `events` command +} diff --git a/internal/basicchain/basic.go b/internal/basicchain/basic.go index 944008bfa..fc18d1153 100644 --- a/internal/basicchain/basic.go +++ b/internal/basicchain/basic.go @@ -23,7 +23,7 @@ import ( const neoAmount = 99999000 -// InitSimple initializes chain with single contract storing several storage values. +// InitSimple initializes chain with simple contracts from 'examples' folder. // It's not as complicated as chain got after Init and may be used for tests where // chain with a small amount of data is needed and for historical functionality testing. // Needs a path to the root directory. @@ -31,14 +31,19 @@ func InitSimple(t *testing.T, rootpath string, e *neotest.Executor) { // examplesPrefix is a prefix of the example smart-contracts. var examplesPrefix = filepath.Join(rootpath, "examples") + deployExample := func(t *testing.T, name string) util.Uint160 { + _, h := newDeployTx(t, e, e.Validator, + filepath.Join(examplesPrefix, name, name+".go"), + filepath.Join(examplesPrefix, name, name+".yml"), + true) + return h + } + // Block #1: deploy storage contract (examples/storage/storage.go). - _, storageHash := newDeployTx(t, e, e.Validator, - filepath.Join(examplesPrefix, "storage", "storage.go"), - filepath.Join(examplesPrefix, "storage", "storage.yml"), - true) + storageHash := deployExample(t, "storage") + storageValidatorInvoker := e.ValidatorInvoker(storageHash) // Block #2: put (1, 1) kv pair. - storageValidatorInvoker := e.ValidatorInvoker(storageHash) storageValidatorInvoker.Invoke(t, 1, "put", 1, 1) // Block #3: put (2, 2) kv pair. @@ -46,6 +51,9 @@ func InitSimple(t *testing.T, rootpath string, e *neotest.Executor) { // Block #4: update (1, 1) -> (1, 2). storageValidatorInvoker.Invoke(t, 1, "put", 1, 2) + + // Block #5: deploy runtime contract (examples/runtime/runtime.go). + _ = deployExample(t, "runtime") } // Init pushes some predefined set of transactions into the given chain, it needs a path to