mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-22 09:29:38 +00:00
vm: implement json tests from neoVM
Add neo-vm submodule @master2.x . Closes #196.
This commit is contained in:
parent
7d40d2f71e
commit
6c002297cd
6 changed files with 401 additions and 113 deletions
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
[submodule "pkg/vm/testdata/neo-vm"]
|
||||||
|
path = pkg/vm/testdata/neo-vm
|
||||||
|
url = https://github.com/neo-project/neo-vm.git
|
||||||
|
branch = master-2.x
|
|
@ -1,81 +0,0 @@
|
||||||
{
|
|
||||||
"category": "Push",
|
|
||||||
"name": "PUSHBYTES1",
|
|
||||||
"tests":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"name": "Good definition",
|
|
||||||
"script": "0x0100",
|
|
||||||
"steps":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"actions":
|
|
||||||
[
|
|
||||||
"StepInto"
|
|
||||||
],
|
|
||||||
"result":
|
|
||||||
{
|
|
||||||
"state": "Break",
|
|
||||||
"invocationStack":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"scriptHash": "0xFBC22D517F38E7612798ECE8E5957CF6C41D8CAF",
|
|
||||||
"instructionPointer": 2,
|
|
||||||
"nextInstruction": "RET",
|
|
||||||
"evaluationStack":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "ByteArray",
|
|
||||||
"value": "0x00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"actions":
|
|
||||||
[
|
|
||||||
"StepInto"
|
|
||||||
],
|
|
||||||
"result":
|
|
||||||
{
|
|
||||||
"state": "Halt",
|
|
||||||
"resultStack":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"type": "ByteArray",
|
|
||||||
"value": "0x00"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "Wrong definition (without enough length)",
|
|
||||||
"script": "0x01",
|
|
||||||
"steps":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"actions":
|
|
||||||
[
|
|
||||||
"StepInto"
|
|
||||||
],
|
|
||||||
"result":
|
|
||||||
{
|
|
||||||
"state": "Fault",
|
|
||||||
"invocationStack":
|
|
||||||
[
|
|
||||||
{
|
|
||||||
"scriptHash": "0xC51B66BCED5E4491001BD702669770DCCF440982",
|
|
||||||
"instructionPointer": 1,
|
|
||||||
"nextInstruction": "RET"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
|
@ -1,6 +0,0 @@
|
||||||
## Package VM Interop
|
|
||||||
|
|
||||||
|
|
||||||
This package will use the tests in the neo-vm repo to test interopabilty
|
|
||||||
|
|
||||||
|
|
|
@ -1,26 +0,0 @@
|
||||||
package csharpinterop
|
|
||||||
|
|
||||||
// VMUnitTest is a struct for capturing the fields in the json files
|
|
||||||
type VMUnitTest struct {
|
|
||||||
Category string `json:"category"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Tests []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Script string `json:"script"`
|
|
||||||
Steps []struct {
|
|
||||||
Actions []string `json:"actions"`
|
|
||||||
Result struct {
|
|
||||||
State string `json:"state"`
|
|
||||||
InvocationStack []struct {
|
|
||||||
ScriptHash string `json:"scriptHash"`
|
|
||||||
InstructionPointer int `json:"instructionPointer"`
|
|
||||||
NextInstruction string `json:"nextInstruction"`
|
|
||||||
EvaluationStack []struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
} `json:"evaluationStack"`
|
|
||||||
} `json:"invocationStack"`
|
|
||||||
} `json:"result"`
|
|
||||||
} `json:"steps"`
|
|
||||||
} `json:"tests"`
|
|
||||||
}
|
|
396
pkg/vm/json_test.go
Normal file
396
pkg/vm/json_test.go
Normal file
|
@ -0,0 +1,396 @@
|
||||||
|
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/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, Instruction(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)
|
||||||
|
}
|
1
pkg/vm/testdata/neo-vm
vendored
Submodule
1
pkg/vm/testdata/neo-vm
vendored
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit 15170c46609ea61f6fcf6f36a90033bf658f2ab7
|
Loading…
Reference in a new issue