package vm import ( "bytes" "encoding/base64" "encoding/binary" "encoding/hex" "encoding/json" "fmt" "io/ioutil" "math/big" "os" "path/filepath" "regexp" "strconv" "strings" "testing" "github.com/nspcc-dev/neo-go/pkg/vm/emit" "github.com/nspcc-dev/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 } vmUTExecutionContextState struct { Instruction string `json:"nextInstruction"` InstructionPointer int `json:"instructionPointer"` EStack []vmUTStackItem `json:"evaluationStack"` StaticFields []vmUTStackItem `json:"staticFields"` } vmUTExecutionEngineState struct { State State `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"` } 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" typeBuffer vmUTStackItemType = "buffer" typeByteString vmUTStackItemType = "bytestring" typeInteger vmUTStackItemType = "integer" typeInterop vmUTStackItemType = "interop" typeMap vmUTStackItemType = "map" typeNull vmUTStackItemType = "null" typePointer vmUTStackItemType = "pointer" typeString vmUTStackItemType = "string" typeStruct vmUTStackItemType = "struct" testsDir = "testdata/neo-vm/tests/neo-vm.Tests/Tests/" ) func TestUT(t *testing.T) { testsRan := false err := filepath.Walk(testsDir, func(path string, info os.FileInfo, err error) error { if !strings.HasSuffix(path, ".json") { return nil } testFile(t, path) testsRan = true return nil }) require.NoError(t, err) require.Equal(t, true, testsRan, "neo-vm tests should be available (check submodules)") } func getTestingInterop(id uint32) *InteropFuncPrice { if id == binary.LittleEndian.Uint32([]byte{0x77, 0x77, 0x77, 0x77}) { return &InteropFuncPrice{InteropFunc(func(v *VM) error { v.estack.PushVal(&InteropItem{new(int)}) return nil }), 0} } return nil } func testFile(t *testing.T, filename string) { data, err := ioutil.ReadFile(filename) require.NoError(t, err) // get rid of possible BOM if len(data) > 2 && data[0] == 0xef && data[1] == 0xbb && data[2] == 0xbf { data = data[3:] } if strings.HasSuffix(filename, "MEMCPY.json") { return // FIXME not a valid JSON https://github.com/neo-project/neo-vm/issues/322 } ut := new(vmUT) require.NoErrorf(t, json.Unmarshal(data, ut), "file: %s", filename) return t.Run(ut.Category+":"+ut.Name, func(t *testing.T) { isRot := strings.HasSuffix(filename, "ROT.json") for i := range ut.Tests { test := ut.Tests[i] if isRot && test.Name == "Without push" { return // FIXME #927 single ROT is interpreted as PUSH1 } t.Run(ut.Tests[i].Name, func(t *testing.T) { prog := []byte(test.Script) vm := load(prog) vm.state = breakState vm.RegisterInteropGetter(getTestingInterop) for i := range test.Steps { execStep(t, vm, test.Steps[i]) result := test.Steps[i].Result require.Equal(t, result.State, vm.state) if result.State == 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) op, err := opcode.FromString(s.Instruction) require.NoError(t, err) require.Equal(t, op, opcode.Opcode(ctx.prog[ctx.nextip])) } compareStacks(t, s.EStack, vm.estack) compareSlots(t, s.StaticFields, vm.static) } } if len(result.ResultStack) != 0 { compareStacks(t, result.ResultStack, vm.estack) } } }) } }) } 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, emit.BytesToInt(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") } case *PointerItem: p, ok := b.(*PointerItem) require.True(t, ok) require.Equal(t, si.pos, p.pos) // there no script in test files default: require.Equal(t, a, b) } } func compareStacks(t *testing.T, expected []vmUTStackItem, actual *Stack) { compareItemArrays(t, expected, actual.Len(), func(i int) StackItem { return actual.Peek(i).Item() }) } func compareSlots(t *testing.T, expected []vmUTStackItem, actual *Slot) { if actual == nil && len(expected) == 0 { return } require.NotNil(t, actual) compareItemArrays(t, expected, actual.Size(), actual.Get) } func compareItemArrays(t *testing.T, expected []vmUTStackItem, n int, getItem func(i int) StackItem) { if expected == nil { return } require.Equal(t, len(expected), n) for i, item := range expected { it := getItem(i) require.NotNil(t, it) if item.Type == typeInterop { require.IsType(t, (*InteropItem)(nil), it) continue } compareItems(t, item.toStackItem(), it) } } func (v *vmUTStackItem) toStackItem() StackItem { switch v.Type.toLower() { 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 { item := jsonStringToInteger(k) if item == nil { panic(fmt.Sprintf("can't unmarshal StackItem %s", k)) } result.Add(item, v.toStackItem()) } return result case typeInterop: panic("not implemented") case typeByteString: return &ByteArrayItem{ v.Value.([]byte), } case typeBuffer: return &BufferItem{v.Value.([]byte)} case typePointer: return NewPointerItem(v.Value.(int), nil) case typeNull: return NullItem{} 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(fmt.Sprintf("invalid type: %s", v.Type)) } } func execStep(t *testing.T, v *VM, step vmUTStep) { for i, a := range step.Actions { var err error switch a.toLower() { 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 jsonStringToInteger(s string) StackItem { b, err := decodeHex(s) if err == nil { return NewBigIntegerItem(new(big.Int).SetBytes(b)) } return nil } func (v vmUTStackItemType) toLower() vmUTStackItemType { return vmUTStackItemType(strings.ToLower(string(v))) } func (v *vmUTScript) UnmarshalJSON(data []byte) error { var ops []string if err := json.Unmarshal(data, &ops); err != nil { return err } var script []byte for i := range ops { if b, ok := decodeSingle(ops[i]); ok { script = append(script, b...) } else { const regex = `(?P(?:0x)?[0-9a-zA-Z]+)\*(?P[0-9]+)` re := regexp.MustCompile(regex) ss := re.FindStringSubmatch(ops[i]) if len(ss) != 3 { return fmt.Errorf("invalid script part: %s", ops[i]) } b, ok := decodeSingle(ss[1]) if !ok { return fmt.Errorf("invalid script part: %s", ops[i]) } num, err := strconv.Atoi(ss[2]) if err != nil { return fmt.Errorf("invalid script part: %s", ops[i]) } for i := 0; i < num; i++ { script = append(script, b...) } } } *v = script return nil } func decodeSingle(s string) ([]byte, bool) { if op, err := opcode.FromString(s); err == nil { return []byte{byte(op)}, true } b, err := decodeHex(s) return b, err == nil } func (v vmUTActionType) toLower() vmUTActionType { return vmUTActionType(strings.ToLower(string(v))) } 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 typ := si.Type.toLower(); typ { case typeArray, typeStruct: var a []vmUTStackItem if err := json.Unmarshal(si.Value, &a); err != nil { return err } v.Value = a case typeInteger, typePointer: 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)) } if typ == typePointer { v.Value = int(num.Int64()) } else { v.Value = num } case typeBoolean: var b bool if err := json.Unmarshal(si.Value, &b); err != nil { return err } v.Value = b case typeByteString, typeBuffer: b, err := decodeBytes(si.Value) if err != nil { return err } v.Value = b case typeInterop, typeNull: 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 } data = data[1 : len(data)-1] // strip quotes if b, err := decodeHex(string(data)); err == nil { return b, nil } r := base64.NewDecoder(base64.StdEncoding, bytes.NewReader(data)) return ioutil.ReadAll(r) } func decodeHex(s string) ([]byte, error) { if strings.HasPrefix(s, "0x") { s = s[2:] } return hex.DecodeString(s) }