forked from TrueCloudLab/neoneo-go
vm: add 'events' command to VM CLI
And dump events automatically after HALTed or FAULTed end of execution.
This commit is contained in:
parent
f45d8fc08d
commit
f1ecdb82cc
3 changed files with 116 additions and 7 deletions
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue