Merge pull request #1584 from nspcc-dev/tests/vmcli

Add tests for `vmcli`
This commit is contained in:
Roman Khimov 2020-12-02 13:33:44 +03:00 committed by GitHub
commit 972b0f176d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 544 additions and 35 deletions

View file

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

1
go.mod
View file

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

2
go.sum
View file

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

View file

@ -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{
@ -183,19 +185,36 @@ 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
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(),
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)
}
@ -218,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) {
@ -226,8 +245,13 @@ func handleIP(c *ishell.Context) {
return
}
v := getVMFromContext(c)
ip, opcode := v.Context().CurrInstr()
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) {
@ -236,11 +260,12 @@ func handleBreak(c *ishell.Context) {
}
v := getVMFromContext(c)
if len(c.Args) != 1 {
c.Err(errors.New("missing parameter <ip>"))
c.Err(fmt.Errorf("%w: <ip>", 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
}
@ -256,7 +281,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 <file>"))
c.Err(fmt.Errorf("%w: <file>", ErrMissingParameter))
return
}
if err := v.LoadFile(c.Args[0]); err != nil {
@ -270,12 +295,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 <string>"))
c.Err(fmt.Errorf("%w: <string>", 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)
@ -286,12 +311,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 <string>"))
c.Err(fmt.Errorf("%w: <string>", 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)
@ -302,7 +327,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 <file>"))
c.Err(fmt.Errorf("%w: <file>", ErrMissingParameter))
return
}
b, err := compiler.Compile(c.Args[0], nil)
@ -341,22 +366,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)
}
}
@ -382,7 +410,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
}
}
@ -419,8 +447,9 @@ func handleStepType(c *ishell.Context, stepType string) {
}
if err != nil {
c.Err(err)
}
} else {
handleIP(c)
}
changePrompt(c, v)
}
@ -429,12 +458,14 @@ 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) {
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 > ")
}
@ -442,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 {
if c.printLogo {
printLogo(c.shell)
}
c.shell.Run()
return nil
}
@ -459,7 +492,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)
@ -530,12 +563,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:
@ -546,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()

461
pkg/vm/cli/cli_test.go Normal file
View file

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

View file

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

View file

@ -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()
@ -237,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.

View file

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