From 33f13ab1c0bb3959f0fc160c79916a31523cfc77 Mon Sep 17 00:00:00 2001 From: Evgenii Stratonikov Date: Tue, 1 Dec 2020 18:27:38 +0300 Subject: [PATCH] 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()) +}