neoneo-go/pkg/vm/json_test.go
Roman Khimov 8d4dd2d2e1 vm: move opcodes into their own package
This allows easier reuse of opcodes and in some cases allows to eliminate
dependencies on the whole vm package, like in compiler that only needs opcodes
and doesn't care about VM for any other purpose.

And yes, they're opcodes because an instruction is a whole thing with
operands, that's what context.Next() returns.
2019-12-03 18:22:14 +03:00

397 lines
9.2 KiB
Go

package vm
import (
"bytes"
"encoding/base64"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"math/big"
"os"
"path/filepath"
"strings"
"testing"
"github.com/CityOfZion/neo-go/pkg/crypto/hash"
"github.com/CityOfZion/neo-go/pkg/util"
"github.com/CityOfZion/neo-go/pkg/vm/opcode"
"github.com/stretchr/testify/require"
)
type (
vmUT struct {
Category string `json:"category"`
Name string `json:"name"`
Tests []vmUTEntry `json:"tests"`
}
vmUTActionType string
vmUTEntry struct {
Name string
Script vmUTScript
Steps []vmUTStep
// FIXME remove when NEO 3.0 https://github.com/nspcc-dev/neo-go/issues/477
ScriptTable []map[string]vmUTScript
}
vmUTExecutionContextState struct {
Instruction string `json:"nextInstruction"`
InstructionPointer int `json:"instructionPointer"`
AStack []vmUTStackItem `json:"altStack"`
EStack []vmUTStackItem `json:"evaluationStack"`
}
vmUTExecutionEngineState struct {
State vmUTState `json:"state"`
ResultStack []vmUTStackItem `json:"resultStack"`
InvocationStack []vmUTExecutionContextState `json:"invocationStack"`
}
vmUTScript []byte
vmUTStackItem struct {
Type vmUTStackItemType
Value interface{}
}
vmUTStep struct {
Actions []vmUTActionType `json:"actions"`
Result vmUTExecutionEngineState `json:"result"`
}
vmUTState State
vmUTStackItemType string
)
// stackItemAUX is used as an intermediate structure
// to conditionally unmarshal vmUTStackItem based
// on the value of Type field.
type stackItemAUX struct {
Type vmUTStackItemType `json:"type"`
Value json.RawMessage `json:"value"`
}
const (
vmExecute vmUTActionType = "Execute"
vmStepInto vmUTActionType = "StepInto"
vmStepOut vmUTActionType = "StepOut"
vmStepOver vmUTActionType = "StepOver"
typeArray vmUTStackItemType = "Array"
typeBoolean vmUTStackItemType = "Boolean"
typeByteArray vmUTStackItemType = "ByteArray"
typeInteger vmUTStackItemType = "Integer"
typeInterop vmUTStackItemType = "Interop"
typeMap vmUTStackItemType = "Map"
typeString vmUTStackItemType = "String"
typeStruct vmUTStackItemType = "Struct"
testsDir = "neo-vm/tests/neo-vm.Tests/Tests/"
)
func TestUT(t *testing.T) {
err := filepath.Walk(testsDir, func(path string, info os.FileInfo, err error) error {
if !strings.HasSuffix(path, ".json") {
return nil
}
testFile(t, path)
return nil
})
require.NoError(t, err)
}
func testFile(t *testing.T, filename string) {
data, err := ioutil.ReadFile(filename)
if err != nil {
t.Fatal(err)
}
// FIXME remove when NEO 3.0 https://github.com/nspcc-dev/neo-go/issues/477
if len(data) > 2 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf {
data = data[3:]
}
ut := new(vmUT)
if err = json.Unmarshal(data, ut); err != nil {
t.Fatal(err)
}
t.Run(ut.Category+":"+ut.Name, func(t *testing.T) {
for i := range ut.Tests {
test := ut.Tests[i]
t.Run(ut.Tests[i].Name, func(t *testing.T) {
prog := []byte(test.Script)
vm := load(prog)
vm.state = breakState
// FIXME remove when NEO 3.0 https://github.com/nspcc-dev/neo-go/issues/477
vm.getScript = getScript(test.ScriptTable)
// FIXME in NEO 3.0 it is []byte{0x77, 0x77, 0x77, 0x77} https://github.com/nspcc-dev/neo-go/issues/477
vm.RegisterInteropFunc("Test.ExecutionEngine.GetScriptContainer", InteropFunc(func(v *VM) error {
v.estack.Push(&Element{value: (*InteropItem)(nil)})
return nil
}), 0)
vm.RegisterInteropFunc("System.ExecutionEngine.GetScriptContainer", InteropFunc(func(v *VM) error {
v.estack.Push(&Element{value: (*InteropItem)(nil)})
return nil
}), 0)
for i := range test.Steps {
execStep(t, vm, test.Steps[i])
result := test.Steps[i].Result
require.Equal(t, State(result.State), vm.state)
if result.State == vmUTState(faultState) { // do not compare stacks on fault
continue
}
if len(result.InvocationStack) > 0 {
for i, s := range result.InvocationStack {
ctx := vm.istack.Peek(i).Value().(*Context)
if ctx.nextip < len(ctx.prog) {
require.Equal(t, s.InstructionPointer, ctx.nextip)
require.Equal(t, s.Instruction, opcode.Opcode(ctx.prog[ctx.nextip]).String())
}
compareStacks(t, s.EStack, vm.estack)
compareStacks(t, s.AStack, vm.astack)
}
}
if len(result.ResultStack) != 0 {
compareStacks(t, result.ResultStack, vm.estack)
}
}
})
}
})
}
func getScript(scripts []map[string]vmUTScript) func(util.Uint160) []byte {
store := make(map[util.Uint160][]byte)
for i := range scripts {
for _, v := range scripts[i] {
store[hash.Hash160(v)] = []byte(v)
}
}
return func(a util.Uint160) []byte { return store[a] }
}
func compareItems(t *testing.T, a, b StackItem) {
switch si := a.(type) {
case *BigIntegerItem:
val := si.value.Int64()
switch ac := b.(type) {
case *BigIntegerItem:
require.Equal(t, val, ac.value.Int64())
case *ByteArrayItem:
require.Equal(t, val, new(big.Int).SetBytes(util.ArrayReverse(ac.value)).Int64())
case *BoolItem:
if ac.value {
require.Equal(t, val, int64(1))
} else {
require.Equal(t, val, int64(0))
}
default:
require.Fail(t, "wrong type")
}
default:
require.Equal(t, a, b)
}
}
func compareStacks(t *testing.T, expected []vmUTStackItem, actual *Stack) {
if expected == nil {
return
}
require.Equal(t, len(expected), actual.Len())
for i, item := range expected {
e := actual.Peek(i)
require.NotNil(t, e)
if item.Type == typeInterop {
require.IsType(t, (*InteropItem)(nil), e.value)
continue
}
compareItems(t, item.toStackItem(), e.value)
}
}
func (v *vmUTStackItem) toStackItem() StackItem {
switch v.Type {
case typeArray:
items := v.Value.([]vmUTStackItem)
result := make([]StackItem, len(items))
for i := range items {
result[i] = items[i].toStackItem()
}
return &ArrayItem{
value: result,
}
case typeString:
panic("not implemented")
case typeMap:
items := v.Value.(map[string]vmUTStackItem)
result := NewMapItem()
for k, v := range items {
var item vmUTStackItem
_ = json.Unmarshal([]byte(`"`+k+`"`), &item)
result.Add(item.toStackItem(), v.toStackItem())
}
return result
case typeInterop:
panic("not implemented")
case typeByteArray:
return &ByteArrayItem{
v.Value.([]byte),
}
case typeBoolean:
return &BoolItem{
v.Value.(bool),
}
case typeInteger:
return &BigIntegerItem{
value: v.Value.(*big.Int),
}
case typeStruct:
items := v.Value.([]vmUTStackItem)
result := make([]StackItem, len(items))
for i := range items {
result[i] = items[i].toStackItem()
}
return &StructItem{
value: result,
}
default:
panic("invalid type")
}
}
func execStep(t *testing.T, v *VM, step vmUTStep) {
for i, a := range step.Actions {
var err error
switch a {
case vmExecute:
err = v.Run()
case vmStepInto:
err = v.StepInto()
case vmStepOut:
err = v.StepOut()
case vmStepOver:
err = v.StepOver()
default:
panic(fmt.Sprintf("invalid action: %s", a))
}
// only the last action is allowed to fail
if i+1 < len(step.Actions) {
require.NoError(t, err)
}
}
}
func (v *vmUTState) UnmarshalJSON(data []byte) error {
switch s := string(data); s {
case `"Break"`:
*v = vmUTState(breakState)
case `"Fault"`:
*v = vmUTState(faultState)
case `"Halt"`:
*v = vmUTState(haltState)
default:
panic(fmt.Sprintf("invalid state: %s", s))
}
return nil
}
func (v *vmUTScript) UnmarshalJSON(data []byte) error {
b, err := decodeBytes(data)
if err != nil {
return err
}
*v = vmUTScript(b)
return nil
}
func (v *vmUTActionType) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, (*string)(v))
}
func (v *vmUTStackItem) UnmarshalJSON(data []byte) error {
var si stackItemAUX
if err := json.Unmarshal(data, &si); err != nil {
return err
}
v.Type = si.Type
switch si.Type {
case typeArray, typeStruct:
var a []vmUTStackItem
if err := json.Unmarshal(si.Value, &a); err != nil {
return err
}
v.Value = a
case typeInteger:
num := new(big.Int)
var a int64
var s string
if err := json.Unmarshal(si.Value, &a); err == nil {
num.SetInt64(a)
} else if err := json.Unmarshal(si.Value, &s); err == nil {
num.SetString(s, 10)
} else {
panic(fmt.Sprintf("invalid integer: %v", si.Value))
}
v.Value = num
case typeBoolean:
var b bool
if err := json.Unmarshal(si.Value, &b); err != nil {
return err
}
v.Value = b
case typeByteArray:
b, err := decodeBytes(si.Value)
if err != nil {
return err
}
v.Value = b
case typeInterop:
v.Value = nil
case typeMap:
var m map[string]vmUTStackItem
if err := json.Unmarshal(si.Value, &m); err != nil {
return err
}
v.Value = m
case typeString:
panic("not implemented")
default:
panic(fmt.Sprintf("unknown type: %s", si.Type))
}
return nil
}
// decodeBytes tries to decode bytes from string.
// It tries hex and base64 encodings.
func decodeBytes(data []byte) ([]byte, error) {
if len(data) == 2 {
return []byte{}, nil
}
hdata := data[3 : len(data)-1]
if b, err := hex.DecodeString(string(hdata)); err == nil {
return b, nil
}
data = data[1 : len(data)-1]
r := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(data))
return ioutil.ReadAll(r)
}