vm: add 'events' command to VM CLI

And dump events automatically after HALTed or FAULTed end of execution.
This commit is contained in:
Anna Shaleva 2022-10-04 13:05:51 +03:00
parent f45d8fc08d
commit f1ecdb82cc
3 changed files with 116 additions and 7 deletions

View file

@ -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) {

View file

@ -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
}

View file

@ -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