From 2f39701d76a0b394313ba319ad0a397f20bd10db Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Dec 2020 16:52:23 +0300 Subject: [PATCH 1/5] vm: provide writer in `PrintOps()` Make it more flexible and testable. Fallback to using stdout if no writer is provided. --- cli/smartcontract/smart_contract.go | 2 +- pkg/vm/cli/cli.go | 4 +++- pkg/vm/vm.go | 8 ++++++-- pkg/vm/vm_test.go | 2 -- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/cli/smartcontract/smart_contract.go b/cli/smartcontract/smart_contract.go index a556570de..dffb75829 100644 --- a/cli/smartcontract/smart_contract.go +++ b/cli/smartcontract/smart_contract.go @@ -678,7 +678,7 @@ func inspect(ctx *cli.Context) error { } v := vm.New() v.LoadScript(b) - v.PrintOps() + v.PrintOps(ctx.App.Writer) return nil } diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 61e00d585..53d6bc8df 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -429,7 +429,9 @@ func handleOps(c *ishell.Context) { return } v := getVMFromContext(c) - v.PrintOps() + out := bytes.NewBuffer(nil) + v.PrintOps(out) + c.Println(out.String()) } func changePrompt(c ishell.Actions, v *vm.VM) { diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 9fd3d4540..63a020b05 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -6,6 +6,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "io/ioutil" "math" "math/big" @@ -151,8 +152,11 @@ func (v *VM) LoadArgs(method []byte, args []stackitem.Item) { } // PrintOps prints the opcodes of the current loaded program to stdout. -func (v *VM) PrintOps() { - w := tabwriter.NewWriter(os.Stdout, 0, 0, 4, ' ', 0) +func (v *VM) PrintOps(out io.Writer) { + if out == nil { + out = os.Stdout + } + w := tabwriter.NewWriter(out, 0, 0, 4, ' ', 0) fmt.Fprintln(w, "INDEX\tOPCODE\tPARAMETER\t") realctx := v.Context() ctx := realctx.Copy() diff --git a/pkg/vm/vm_test.go b/pkg/vm/vm_test.go index 046bc9548..56d8598e9 100644 --- a/pkg/vm/vm_test.go +++ b/pkg/vm/vm_test.go @@ -225,8 +225,6 @@ func TestISTYPE(t *testing.T) { func testCONVERT(to stackitem.Type, item, res stackitem.Item) func(t *testing.T) { return func(t *testing.T) { prog := []byte{byte(opcode.CONVERT), byte(to)} - v := load(prog) - v.PrintOps() runWithArgs(t, prog, res, item) } } From d7ffa89811d94c63568902bc587e609c68f30cc5 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Dec 2020 16:53:38 +0300 Subject: [PATCH 2/5] vmcli: set breakpoint before the instruction Breakpoint should occur before actual instruction execution. --- pkg/vm/cli/cli.go | 29 +++++++++++++++++++---------- pkg/vm/context.go | 9 +++++++++ pkg/vm/vm.go | 2 +- 3 files changed, 29 insertions(+), 11 deletions(-) diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 53d6bc8df..db3a7b894 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -226,8 +226,13 @@ func handleIP(c *ishell.Context) { return } v := getVMFromContext(c) - ip, opcode := v.Context().CurrInstr() - c.Printf("instruction pointer at %d (%s)\n", ip, opcode) + ctx := v.Context() + if ctx.NextIP() < ctx.LenInstr() { + ip, opcode := v.Context().NextInstr() + c.Printf("instruction pointer at %d (%s)\n", ip, opcode) + } else { + c.Println("execution has finished") + } } func handleBreak(c *ishell.Context) { @@ -341,22 +346,25 @@ func runVMWithHandling(c *ishell.Context, v *vm.VM) { err := v.Run() if err != nil { c.Err(err) - return } var message string switch { case v.HasFailed(): - message = "FAILED" + message = "" // the error will be printed on return case v.HasHalted(): message = v.Stack("estack") case v.AtBreakpoint(): ctx := v.Context() - i, op := ctx.CurrInstr() - message = fmt.Sprintf("at breakpoint %d (%s)\n", i, op.String()) + if ctx.NextIP() < ctx.LenInstr() { + i, op := ctx.NextInstr() + message = fmt.Sprintf("at breakpoint %d (%s)", i, op) + } else { + message = "execution has finished" + } } if message != "" { - c.Printf(message) + c.Println(message) } } @@ -419,8 +427,9 @@ func handleStepType(c *ishell.Context, stepType string) { } if err != nil { c.Err(err) + } else { + handleIP(c) } - handleIP(c) changePrompt(c, v) } @@ -435,8 +444,8 @@ func handleOps(c *ishell.Context) { } func changePrompt(c ishell.Actions, v *vm.VM) { - if v.Ready() && v.Context().IP() >= 0 { - c.SetPrompt(fmt.Sprintf("NEO-GO-VM %d > ", v.Context().IP())) + if v.Ready() && v.Context().NextIP() >= 0 && v.Context().NextIP() < v.Context().LenInstr() { + c.SetPrompt(fmt.Sprintf("NEO-GO-VM %d > ", v.Context().NextIP())) } else { c.SetPrompt("NEO-GO-VM > ") } diff --git a/pkg/vm/context.go b/pkg/vm/context.go index 85d6e2611..6cb7300a3 100644 --- a/pkg/vm/context.go +++ b/pkg/vm/context.go @@ -185,6 +185,15 @@ func (c *Context) CurrInstr() (int, opcode.Opcode) { return c.ip, opcode.Opcode(c.prog[c.ip]) } +// NextInstr returns the next instruction and opcode. +func (c *Context) NextInstr() (int, opcode.Opcode) { + op := opcode.RET + if c.nextip < len(c.prog) { + op = opcode.Opcode(c.prog[c.nextip]) + } + return c.nextip, op +} + // Copy returns an new exact copy of c. func (c *Context) Copy() *Context { ctx := new(Context) diff --git a/pkg/vm/vm.go b/pkg/vm/vm.go index 63a020b05..c96ad4676 100644 --- a/pkg/vm/vm.go +++ b/pkg/vm/vm.go @@ -241,7 +241,7 @@ func (v *VM) AddBreakPoint(n int) { // instruction pointer. func (v *VM) AddBreakPointRel(n int) { ctx := v.Context() - v.AddBreakPoint(ctx.ip + n) + v.AddBreakPoint(ctx.nextip + n) } // LoadFile loads a program in NEF format from the given path, ready to execute it. From bea5125d423714231181848aaf44f227bd63fec6 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Dec 2020 18:30:19 +0300 Subject: [PATCH 3/5] vmcli: return after error in `break` --- pkg/vm/cli/cli.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index db3a7b894..4fc56439d 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -242,6 +242,7 @@ func handleBreak(c *ishell.Context) { v := getVMFromContext(c) if len(c.Args) != 1 { c.Err(errors.New("missing parameter ")) + return } n, err := strconv.Atoi(c.Args[0]) if err != nil { From f8728e4f44410180c864d66d4ce458c346b29b0f Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Dec 2020 17:15:01 +0300 Subject: [PATCH 4/5] vmcli: unify error messages --- pkg/vm/cli/cli.go | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 4fc56439d..94b85fa9b 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -183,6 +183,12 @@ example: }, } +// Various errors. +var ( + ErrMissingParameter = errors.New("missing argument") + ErrInvalidParameter = errors.New("can't parse argument") +) + // VMCLI object for interacting with the VM. type VMCLI struct { vm *vm.VM @@ -241,12 +247,12 @@ func handleBreak(c *ishell.Context) { } v := getVMFromContext(c) if len(c.Args) != 1 { - c.Err(errors.New("missing parameter ")) + c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } n, err := strconv.Atoi(c.Args[0]) if err != nil { - c.Err(fmt.Errorf("argument conversion error: %w", err)) + c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) return } @@ -262,7 +268,7 @@ func handleXStack(c *ishell.Context) { func handleLoadNEF(c *ishell.Context) { v := getVMFromContext(c) if len(c.Args) < 1 { - c.Err(errors.New("missing parameter ")) + c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } if err := v.LoadFile(c.Args[0]); err != nil { @@ -276,12 +282,12 @@ func handleLoadNEF(c *ishell.Context) { func handleLoadBase64(c *ishell.Context) { v := getVMFromContext(c) if len(c.Args) < 1 { - c.Err(errors.New("missing parameter ")) + c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } b, err := base64.StdEncoding.DecodeString(c.Args[0]) if err != nil { - c.Err(err) + c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) return } v.Load(b) @@ -292,12 +298,12 @@ func handleLoadBase64(c *ishell.Context) { func handleLoadHex(c *ishell.Context) { v := getVMFromContext(c) if len(c.Args) < 1 { - c.Err(errors.New("missing parameter ")) + c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } b, err := hex.DecodeString(c.Args[0]) if err != nil { - c.Err(err) + c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) return } v.Load(b) @@ -308,7 +314,7 @@ func handleLoadHex(c *ishell.Context) { func handleLoadGo(c *ishell.Context) { v := getVMFromContext(c) if len(c.Args) < 1 { - c.Err(errors.New("missing parameter ")) + c.Err(fmt.Errorf("%w: ", ErrMissingParameter)) return } b, err := compiler.Compile(c.Args[0], nil) @@ -391,7 +397,7 @@ func handleStep(c *ishell.Context) { if len(c.Args) > 0 { n, err = strconv.Atoi(c.Args[0]) if err != nil { - c.Err(fmt.Errorf("argument conversion error: %w", err)) + c.Err(fmt.Errorf("%w: %v", ErrInvalidParameter, err)) return } } @@ -471,7 +477,7 @@ func handleParse(c *ishell.Context) { // Parse converts it's argument to other formats. func Parse(args []string) (string, error) { if len(args) < 1 { - return "", errors.New("missing argument") + return "", ErrMissingParameter } arg := args[0] buf := bytes.NewBuffer(nil) @@ -542,12 +548,12 @@ func parseArgs(args []string) ([]stackitem.Item, error) { } else if value == boolTrue { items[i] = stackitem.NewBool(true) } else { - return nil, errors.New("failed to parse bool parameter") + return nil, fmt.Errorf("%w: invalid bool value", ErrInvalidParameter) } case intType: val, err := strconv.ParseInt(value, 10, 64) if err != nil { - return nil, err + return nil, fmt.Errorf("%w: invalid integer value", ErrInvalidParameter) } items[i] = stackitem.NewBigInteger(big.NewInt(val)) case stringType: From 33f13ab1c0bb3959f0fc160c79916a31523cfc77 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Dec 2020 18:27:38 +0300 Subject: [PATCH 5/5] vmcli: add tests --- go.mod | 1 + go.sum | 2 + pkg/vm/cli/cli.go | 28 ++- pkg/vm/cli/cli_test.go | 461 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 486 insertions(+), 6 deletions(-) create mode 100644 pkg/vm/cli/cli_test.go diff --git a/go.mod b/go.mod index b5238152a..ff2ab6dff 100644 --- a/go.mod +++ b/go.mod @@ -2,6 +2,7 @@ module github.com/nspcc-dev/neo-go require ( github.com/Workiva/go-datastructures v1.0.50 + github.com/abiosoft/readline v0.0.0-20180607040430-155bce2042db github.com/alicebob/miniredis v2.5.0+incompatible github.com/btcsuite/btcd v0.20.1-beta github.com/dgraph-io/badger/v2 v2.0.3 diff --git a/go.sum b/go.sum index 6487e5e15..49528471c 100644 --- a/go.sum +++ b/go.sum @@ -53,6 +53,7 @@ github.com/cespare/xxhash/v2 v2.1.0 h1:yTUvW7Vhb89inJ+8irsUqiWjh8iT6sQPZiQzI6ReG github.com/cespare/xxhash/v2 v2.1.0/go.mod h1:dgIUBU3pDso/gPgZ1osOZ0iQf77oPR28Tjxl5dIMyVM= github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= +github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -232,6 +233,7 @@ github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJy github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73 h1:I2drr5K0tykBofr74ZEGliE/Hf6fNkEbcPyFvsy7wZk= github.com/syndtr/goleveldb v0.0.0-20180307113352-169b1b37be73/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= +github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8 h1:3SVOIvH7Ae1KRYyQWRjXWJEA9sS/c/pjvH++55Gr648= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= diff --git a/pkg/vm/cli/cli.go b/pkg/vm/cli/cli.go index 94b85fa9b..c717937d3 100644 --- a/pkg/vm/cli/cli.go +++ b/pkg/vm/cli/cli.go @@ -12,6 +12,7 @@ import ( "strings" "text/tabwriter" + "github.com/abiosoft/readline" "github.com/nspcc-dev/neo-go/pkg/compiler" "github.com/nspcc-dev/neo-go/pkg/encoding/address" "github.com/nspcc-dev/neo-go/pkg/encoding/bigint" @@ -28,6 +29,7 @@ const ( boolTrue = "true" intType = "int" stringType = "string" + exitFunc = "exitFunc" ) var commands = []*ishell.Cmd{ @@ -193,15 +195,26 @@ var ( type VMCLI struct { vm *vm.VM shell *ishell.Shell + // printLogo specifies if logo is printed. + printLogo bool } // New returns a new VMCLI object. func New() *VMCLI { + return NewWithConfig(true, os.Exit, &readline.Config{ + Prompt: ">>>", + }) +} + +// NewWithConfig returns new VMCLI instance using provided config. +func NewWithConfig(printLogo bool, onExit func(int), c *readline.Config) *VMCLI { vmcli := VMCLI{ - vm: vm.New(), - shell: ishell.New(), + vm: vm.New(), + shell: ishell.NewWithConfig(c), + printLogo: printLogo, } vmcli.shell.Set(vmKey, vmcli.vm) + vmcli.shell.Set(exitFunc, onExit) for _, c := range commands { vmcli.shell.AddCmd(c) } @@ -224,7 +237,7 @@ func checkVMIsReady(c *ishell.Context) bool { func handleExit(c *ishell.Context) { c.Println("Bye!") - os.Exit(0) + c.Get(exitFunc).(func(int))(0) } func handleIP(c *ishell.Context) { @@ -460,7 +473,9 @@ func changePrompt(c ishell.Actions, v *vm.VM) { // Run waits for user input from Stdin and executes the passed command. func (c *VMCLI) Run() error { - printLogo(c.shell) + if c.printLogo { + printLogo(c.shell) + } c.shell.Run() return nil } @@ -564,14 +579,15 @@ func parseArgs(args []string) ([]stackitem.Item, error) { return items, nil } -func printLogo(c *ishell.Shell) { - logo := ` +const logo = ` _ ____________ __________ _ ____ ___ / | / / ____/ __ \ / ____/ __ \ | | / / |/ / / |/ / __/ / / / /_____/ / __/ / / /____| | / / /|_/ / / /| / /___/ /_/ /_____/ /_/ / /_/ /_____/ |/ / / / / /_/ |_/_____/\____/ \____/\____/ |___/_/ /_/ ` + +func printLogo(c *ishell.Shell) { c.Print(logo) c.Println() c.Println() diff --git a/pkg/vm/cli/cli_test.go b/pkg/vm/cli/cli_test.go new file mode 100644 index 000000000..87efc1656 --- /dev/null +++ b/pkg/vm/cli/cli_test.go @@ -0,0 +1,461 @@ +package cli + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "encoding/json" + "fmt" + gio "io" + "io/ioutil" + "os" + "path" + "strings" + "sync" + "testing" + "time" + + "github.com/abiosoft/readline" + "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/io" + "github.com/nspcc-dev/neo-go/pkg/smartcontract/nef" + "github.com/nspcc-dev/neo-go/pkg/util" + "github.com/nspcc-dev/neo-go/pkg/vm" + "github.com/nspcc-dev/neo-go/pkg/vm/emit" + "github.com/nspcc-dev/neo-go/pkg/vm/opcode" + "github.com/stretchr/testify/require" + "go.uber.org/atomic" +) + +type readCloser struct { + sync.Mutex + bytes.Buffer +} + +func (r *readCloser) Close() error { + return nil +} + +func (r *readCloser) Read(p []byte) (int, error) { + r.Lock() + defer r.Unlock() + return r.Buffer.Read(p) +} + +func (r *readCloser) WriteString(s string) { + r.Lock() + defer r.Unlock() + r.Buffer.WriteString(s) +} + +type executor struct { + in *readCloser + out *bytes.Buffer + cli *VMCLI + ch chan struct{} + exit atomic.Bool +} + +func newTestVMCLI(t *testing.T) *executor { + return newTestVMCLIWithLogo(t, false) +} + +func newTestVMCLIWithLogo(t *testing.T, printLogo bool) *executor { + e := &executor{ + in: &readCloser{Buffer: *bytes.NewBuffer(nil)}, + out: bytes.NewBuffer(nil), + ch: make(chan struct{}), + } + e.cli = NewWithConfig(printLogo, + func(int) { e.exit.Store(true) }, + &readline.Config{ + Prompt: "", + Stdin: e.in, + Stdout: e.out, + }) + go func() { + require.NoError(t, e.cli.Run()) + close(e.ch) + }() + return e +} + +func (e *executor) runProg(t *testing.T, commands ...string) { + cmd := strings.Join(commands, "\n") + "\n" + e.in.WriteString(cmd + "\n") + select { + case <-e.ch: + case <-time.After(time.Second): + require.Fail(t, "command took too long time") + } +} + +func (e *executor) checkNextLine(t *testing.T, expected string) { + line, err := e.out.ReadString('\n') + require.NoError(t, err) + require.Regexp(t, expected, line) +} + +func (e *executor) checkError(t *testing.T, expectedErr error) { + line, err := e.out.ReadString('\n') + require.NoError(t, err) + require.True(t, strings.HasPrefix(line, "Error: "+expectedErr.Error())) +} + +func (e *executor) checkStack(t *testing.T, items ...interface{}) { + d := json.NewDecoder(e.out) + var actual interface{} + require.NoError(t, d.Decode(&actual)) + rawActual, err := json.Marshal(actual) + require.NoError(t, err) + + expected := vm.NewStack("") + for i := range items { + expected.PushVal(items[i]) + } + rawExpected, err := json.Marshal(expected) + 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 TestLoad(t *testing.T) { + script := []byte{byte(opcode.PUSH3), byte(opcode.PUSH4), byte(opcode.ADD)} + t.Run("loadhex", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadhex", + "loadhex notahex", + "loadhex "+hex.EncodeToString(script)) + + e.checkError(t, ErrMissingParameter) + e.checkError(t, ErrInvalidParameter) + e.checkNextLine(t, "READY: loaded 3 instructions") + }) + t.Run("loadbase64", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadbase64", + "loadbase64 not_a_base64", + "loadbase64 "+base64.StdEncoding.EncodeToString(script)) + + e.checkError(t, ErrMissingParameter) + e.checkError(t, ErrInvalidParameter) + e.checkNextLine(t, "READY: loaded 3 instructions") + }) + + src := `package kek + func Main(op string, args []interface{}) int { + a := args[0].(int) + b := args[1].(int) + if op == "add" { + return a + b + } else { + return a * b + } + }` + tmpDir := path.Join(os.TempDir(), "vmcliloadtest") + require.NoError(t, os.Mkdir(tmpDir, os.ModePerm)) + defer os.RemoveAll(tmpDir) + + t.Run("loadgo", func(t *testing.T) { + filename := path.Join(tmpDir, "vmtestcontract.go") + require.NoError(t, ioutil.WriteFile(filename, []byte(src), os.ModePerm)) + filenameErr := path.Join(tmpDir, "vmtestcontract_err.go") + require.NoError(t, ioutil.WriteFile(filenameErr, []byte(src+"invalid_token"), os.ModePerm)) + + e := newTestVMCLI(t) + e.runProg(t, + "loadgo", + "loadgo "+filenameErr, + "loadgo "+filename, + "run add 3 5") + + e.checkError(t, ErrMissingParameter) + e.checkNextLine(t, "Error:") + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t, 8) + }) + t.Run("loadnef", func(t *testing.T) { + config.Version = "0.92.0-test" + + script, err := compiler.Compile("test", strings.NewReader(src)) + require.NoError(t, err) + nefFile, err := nef.NewFile(script) + require.NoError(t, err) + filename := path.Join(tmpDir, "vmtestcontract.nef") + rawNef, err := nefFile.Bytes() + require.NoError(t, err) + require.NoError(t, ioutil.WriteFile(filename, rawNef, os.ModePerm)) + filenameErr := path.Join(tmpDir, "vmtestcontract_err.nef") + require.NoError(t, ioutil.WriteFile(filenameErr, append([]byte{1, 2, 3, 4}, rawNef...), os.ModePerm)) + + e := newTestVMCLI(t) + e.runProg(t, + "loadnef", + "loadnef "+filenameErr, + "loadnef "+filename, + "run add 3 5") + + e.checkError(t, ErrMissingParameter) + e.checkNextLine(t, "Error:") + e.checkNextLine(t, "READY: loaded \\d* instructions") + e.checkStack(t, 8) + }) +} + +func TestRunWithDifferentArguments(t *testing.T) { + src := `package kek + func Main(op string, args []interface{}) interface{} { + switch op { + case "getbool": + return args[0].(bool) + case "getint": + return args[0].(int) + case "getstring": + return args[0].(string) + default: + return nil + } + }` + + filename := path.Join(os.TempDir(), "run_vmtestcontract.go") + require.NoError(t, ioutil.WriteFile(filename, []byte(src), os.ModePerm)) + defer os.Remove(filename) + + e := newTestVMCLI(t) + e.runProg(t, + "loadgo "+filename, "run getbool true", + "loadgo "+filename, "run getbool false", + "loadgo "+filename, "run getbool bool:invalid", + "loadgo "+filename, "run getint 123", + "loadgo "+filename, "run getint int:invalid", + "loadgo "+filename, "run getstring validstring", + ) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, true) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, false) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkError(t, ErrInvalidParameter) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, 123) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkError(t, ErrInvalidParameter) + + e.checkNextLine(t, "READY: loaded \\d.* instructions") + e.checkStack(t, "validstring") +} + +func TestPrintOps(t *testing.T) { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.PUSH1) + emit.Syscall(w.BinWriter, interopnames.SystemBinarySerialize) + emit.Instruction(w.BinWriter, opcode.PUSHDATA1, []byte{3, 1, 2, 3}) + script := w.Bytes() + e := newTestVMCLI(t) + e.runProg(t, + "ops", + "loadhex "+hex.EncodeToString(script), + "ops") + + e.checkNextLine(t, ".*no program loaded") + e.checkNextLine(t, fmt.Sprintf("READY: loaded %d instructions", len(script))) + e.checkNextLine(t, "INDEX.*OPCODE.*PARAMETER") + e.checkNextLine(t, "0.*PUSH1") + e.checkNextLine(t, "1.*SYSCALL.*System\\.Binary\\.Serialize") + e.checkNextLine(t, "6.*PUSHDATA1.*010203") +} + +func TestLoadAbort(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "loadhex "+hex.EncodeToString([]byte{byte(opcode.PUSH1), byte(opcode.ABORT)}), + "run", + ) + + e.checkNextLine(t, fmt.Sprintf("READY: loaded 2 instructions")) + e.checkNextLine(t, "Error:.*at instruction 1.*ABORT") +} + +func TestBreakpoint(t *testing.T) { + w := io.NewBufBinWriter() + emit.Opcodes(w.BinWriter, opcode.PUSH1, opcode.PUSH2, opcode.ADD, opcode.PUSH6, opcode.ADD) + e := newTestVMCLI(t) + e.runProg(t, + "break 3", + "cont", + "ip", + "loadhex "+hex.EncodeToString(w.Bytes()), + "break", + "break second", + "break 2", + "break 4", + "cont", "estack", + "run", "estack", + "cont", + ) + + e.checkNextLine(t, "no program loaded") + e.checkNextLine(t, "no program loaded") + e.checkNextLine(t, "no program loaded") + e.checkNextLine(t, fmt.Sprintf("READY: loaded 5 instructions")) + e.checkError(t, ErrMissingParameter) + e.checkError(t, ErrInvalidParameter) + e.checkNextLine(t, "breakpoint added at instruction 2") + e.checkNextLine(t, "breakpoint added at instruction 4") + + e.checkNextLine(t, "at breakpoint 2.*ADD") + e.checkStack(t, 1, 2) + + e.checkNextLine(t, "at breakpoint 4.*ADD") + e.checkStack(t, 3, 6) + + e.checkStack(t, 9) +} + +func TestStep(t *testing.T) { + script := hex.EncodeToString([]byte{ + byte(opcode.PUSH0), byte(opcode.PUSH1), byte(opcode.PUSH2), byte(opcode.PUSH3), + }) + e := newTestVMCLI(t) + e.runProg(t, + "step", + "loadhex "+script, + "step invalid", + "step", + "step 2", + "ip", "step", "ip") + + e.checkNextLine(t, "no program loaded") + e.checkNextLine(t, "READY: loaded \\d+ instructions") + e.checkError(t, ErrInvalidParameter) + e.checkNextLine(t, "at breakpoint 1.*PUSH1") + e.checkNextLine(t, "at breakpoint 3.*PUSH3") + e.checkNextLine(t, "instruction pointer at 3.*PUSH3") + e.checkNextLine(t, "execution has finished") + e.checkNextLine(t, "execution has finished") +} + +func TestErrorOnStepInto(t *testing.T) { + script := hex.EncodeToString([]byte{byte(opcode.ADD)}) + e := newTestVMCLI(t) + e.runProg(t, + "stepover", + "loadhex "+script, + "stepover") + + e.checkNextLine(t, "Error:.*no program loaded") + e.checkNextLine(t, "READY: loaded 1 instructions") + e.checkNextLine(t, "Error:") +} + +func TestStepIntoOverOut(t *testing.T) { + script := hex.EncodeToString([]byte{ + byte(opcode.PUSH2), byte(opcode.CALL), 4, byte(opcode.NOP), byte(opcode.RET), + byte(opcode.PUSH3), byte(opcode.ADD), byte(opcode.RET), + }) + + e := newTestVMCLI(t) + e.runProg(t, + "loadhex "+script, + "step", "stepover", "run", + "loadhex "+script, + "step", "stepinto", "step", "estack", "run", + "loadhex "+script, + "step", "stepinto", "stepout", "run") + + e.checkNextLine(t, fmt.Sprintf("READY: loaded \\d+ instructions")) + e.checkNextLine(t, "at breakpoint 1.*CALL") + e.checkNextLine(t, "instruction pointer at.*NOP") + e.checkStack(t, 5) + + e.checkNextLine(t, fmt.Sprintf("READY: loaded \\d+ instructions")) + e.checkNextLine(t, "at breakpoint.*CALL") + e.checkNextLine(t, "instruction pointer at.*PUSH3") + e.checkNextLine(t, "at breakpoint.*ADD") + e.checkStack(t, 2, 3) + e.checkStack(t, 5) + + e.checkNextLine(t, fmt.Sprintf("READY: loaded \\d+ instructions")) + e.checkNextLine(t, "at breakpoint 1.*CALL") + e.checkNextLine(t, "instruction pointer at.*PUSH3") + e.checkNextLine(t, "instruction pointer at.*NOP") + e.checkStack(t, 5) +} + +// `Parse` output is written via `tabwriter` so if any problems +// are encountered in this test, try to replace ' ' with '\\s+'. +func TestParse(t *testing.T) { + t.Run("Integer", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, + "parse", + "parse 6667") + + e.checkError(t, ErrMissingParameter) + e.checkNextLine(t, "Integer to Hex.*0b1a") + e.checkNextLine(t, "Integer to Base64.*Cxo=") + e.checkNextLine(t, "Hex to String.*\"fg\"") + e.checkNextLine(t, "Hex to Integer.*26470") + e.checkNextLine(t, "Swap Endianness.*6766") + e.checkNextLine(t, "Base64 to String.*\"뮻\"") + e.checkNextLine(t, "Base64 to BigInteger.*-4477205") + e.checkNextLine(t, "String to Hex.*36363637") + e.checkNextLine(t, "String to Base64.*NjY2Nw==") + }) + t.Run("Address", func(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, "parse "+"NbTiM6h8r99kpRtb428XcsUk1TzKed2gTc") + e.checkNextLine(t, "Address to BE ScriptHash.*aa8acf859d4fe402b34e673f2156821796a488eb") + e.checkNextLine(t, "Address to LE ScriptHash.*eb88a496178256213f674eb302e44f9d85cf8aaa") + e.checkNextLine(t, "Address to Base64.*(BE).*qorPhZ1P5AKzTmc/IVaCF5akiOs=") + e.checkNextLine(t, "Address to Base64.*(LE).*64iklheCViE/Z06zAuRPnYXPiqo=") + e.checkNextLine(t, "String to Hex.*4e6254694d3668387239396b70527462343238586373556b31547a4b656432675463") + e.checkNextLine(t, "String to Base64.*TmJUaU02aDhyOTlrcFJ0YjQyOFhjc1VrMVR6S2VkMmdUYw==") + }) + t.Run("Uint160", func(t *testing.T) { + u := util.Uint160{66, 67, 68} + e := newTestVMCLI(t) + e.runProg(t, "parse "+u.StringLE()) + e.checkNextLine(t, "Integer to Hex.*b6c706") + e.checkNextLine(t, "Integer to Base64.*tscG") + e.checkNextLine(t, "BE ScriptHash to Address.*NKuyBkoGdZZSLyPbJEetheRhQKhATAzN2A") + e.checkNextLine(t, "LE ScriptHash to Address.*NRxLN7apYwKJihzMt4eSSnU9BJ77dp2TNj") + e.checkNextLine(t, "Hex to String") + e.checkNextLine(t, "Hex to Integer.*378293464438118320046642359484100328446970822656") + e.checkNextLine(t, "Swap Endianness.*4243440000000000000000000000000000000000") + e.checkNextLine(t, "Base64 to String.*") + e.checkNextLine(t, "Base64 to BigInteger.*376115185060690908522683414825349447309891933036899526770189324554358227") + e.checkNextLine(t, "String to Hex.*30303030303030303030303030303030303030303030303030303030303030303030343434333432") + e.checkNextLine(t, "String to Base64.*MDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDQ0NDM0Mg==") + }) +} + +func TestPrintLogo(t *testing.T) { + e := newTestVMCLIWithLogo(t, true) + e.runProg(t) + require.True(t, strings.HasPrefix(e.out.String(), logo)) + require.False(t, e.exit.Load()) +} + +func TestExit(t *testing.T) { + e := newTestVMCLI(t) + e.runProg(t, "exit") + require.True(t, e.exit.Load()) +}