Add a custom vm.Run() function #9
58
covertest/run.go
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
package covertest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/opcode"
|
||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InstrHash maps instruction with scripthash of a contract it belongs to.
|
||||||
fyrchik marked this conversation as resolved
Outdated
|
|||||||
|
type InstrHash struct {
|
||||||
|
Offset int
|
||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
`Position` or `Offset` would be more descriptive IMO (each opcode has a "number" which is `Instruction` here)
elebedeva
commented
Fixed Fixed
|
|||||||
|
Instruction opcode.Opcode
|
||||||
|
ScriptHash util.Uint160
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run starts execution of the loaded program and accumulates all seen opcodes
|
||||||
|
// together with the scripthash of a contract they belong to.
|
||||||
|
// Original vm.Run(): https://github.com/nspcc-dev/neo-go/blob/v0.101.3/pkg/vm/vm.go#L418
|
||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
Could you give a permalink to the original code of this function in comment? Could you give a permalink to the original code of this function in comment?
It would benefit both reviewers and anyone who touches this code in a year.
elebedeva
commented
Fixed Fixed
|
|||||||
|
func Run(v *vm.VM) ([]InstrHash, error) {
|
||||||
fyrchik marked this conversation as resolved
fyrchik
commented
First line of the function should not be empty (there is a linter for this, we just don't have it in this repo) First line of the function should not be empty (there is a linter for this, we just don't have it in this repo)
elebedeva
commented
Fixed Fixed
|
|||||||
|
if !v.Ready() {
|
||||||
|
return nil, errors.New("no program loaded")
|
||||||
|
}
|
||||||
|
|
||||||
|
if v.HasFailed() {
|
||||||
|
// VM already ran something and failed, in general its state is
|
||||||
|
// undefined in this case so we can't run anything.
|
||||||
|
return nil, errors.New("VM has failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// vmstate.Halt (the default) or vmstate.Break are safe to continue.
|
||||||
|
var ops []InstrHash
|
||||||
|
for {
|
||||||
|
switch {
|
||||||
|
case v.HasFailed():
|
||||||
|
// Should be caught and reported already by the v.Step(),
|
||||||
|
// but we're checking here anyway just in case.
|
||||||
|
return ops, errors.New("VM has failed")
|
||||||
|
case v.HasHalted(), v.AtBreakpoint():
|
||||||
|
// Normal exit from this loop.
|
||||||
|
return ops, nil
|
||||||
|
case v.State() == vmstate.None:
|
||||||
|
nStr, curInstr := v.Context().NextInstr()
|
||||||
|
ops = append(ops, InstrHash{
|
||||||
|
Offset: nStr,
|
||||||
|
Instruction: curInstr,
|
||||||
|
ScriptHash: v.Context().ScriptHash(),
|
||||||
|
})
|
||||||
|
if err := v.Step(); err != nil {
|
||||||
|
return ops, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return ops, errors.New("unknown state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,18 +1,26 @@
|
||||||||
package contract
|
package contract
|
||||||||
|
|
||||||||
import (
|
import (
|
||||||||
|
"errors"
|
||||||||
|
"math/rand"
|
||||||||
"path"
|
"path"
|
||||||||
"testing"
|
"testing"
|
||||||||
|
|
||||||||
"git.frostfs.info/TrueCloudLab/contract-coverage-primer/covertest"
|
"git.frostfs.info/TrueCloudLab/contract-coverage-primer/covertest"
|
||||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||||
|
"github.com/nspcc-dev/neo-go/pkg/compiler"
|
||||||||
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
"github.com/nspcc-dev/neo-go/pkg/neotest"
|
||||||||
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
|
"github.com/nspcc-dev/neo-go/pkg/neotest/chain"
|
||||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/callflag"
|
||||||||
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
|
||||||||
|
"github.com/nspcc-dev/neo-go/pkg/vm"
|
||||||||
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
||||||||
|
"github.com/stretchr/testify/require"
|
||||||||
)
|
)
|
||||||||
|
|
||||||||
const ctrPath = "../impulse"
|
const ctrPath = "../impulse"
|
||||||||
|
|
||||||||
// Key for tests
|
// keys for tests
|
||||||||
var (
|
var (
|
||||||||
validKey = []byte{1, 2, 3, 4, 5}
|
validKey = []byte{1, 2, 3, 4, 5}
|
||||||||
invalidKey = []byte{1, 2, 3}
|
invalidKey = []byte{1, 2, 3}
|
||||||||
|
@ -41,3 +49,69 @@ func TestContract(t *testing.T) {
|
||||||||
inv.InvokeFail(t, "Invalid key size", "putNumber", invalidKey, 42)
|
inv.InvokeFail(t, "Invalid key size", "putNumber", invalidKey, 42)
|
||||||||
inv.InvokeFail(t, "Invalid key size", "getNumber", invalidKey)
|
inv.InvokeFail(t, "Invalid key size", "getNumber", invalidKey)
|
||||||||
}
|
}
|
||||||||
|
|
||||||||
|
func TestRun(t *testing.T) {
|
||||||||
|
e := newExecutor(t)
|
||||||||
|
ctrDI := covertest.CompileFile(t, e.CommitteeHash, ctrPath, path.Join(ctrPath, "config.yml"))
|
||||||||
|
e.DeployContract(t, ctrDI.Contract, nil)
|
||||||||
fyrchik marked this conversation as resolved
fyrchik
commented
`101` and `2` look like magic constants. Can we calculate them somehow?
elebedeva
commented
Fixed Fixed
|
|||||||||
|
|
||||||||
|
startOffsetPutNumber, err := getStartOffset(ctrDI.DebugInfo, "PutNumber")
|
||||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
We use We use `spew` for debug-printing big structs. In other cases (tests) `t.Log` or `t.Logf` might be more appropriate.
elebedeva
commented
Fixed Fixed
|
|||||||||
|
require.NoError(t, err)
|
||||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
`require.NoError(t, err)`?
elebedeva
commented
Fixed Fixed
|
|||||||||
|
|
||||||||
|
hasResult, err := hasResult(ctrDI.DebugInfo, "PutNumber")
|
||||||||
|
require.NoError(t, err)
|
||||||||
|
|
||||||||
|
someNum := getNumToPut()
|
||||||||
|
|
||||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
same here same here
elebedeva
commented
Fixed Fixed
|
|||||||||
|
// set up a VM for covertest.Run()
|
||||||||
|
covertestRunVM := setUpVMForPut(t, e, ctrDI.Contract, hasResult, startOffsetPutNumber, someNum, invalidKey)
|
||||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
Comment should have a space after Comment should have a space after `// ` (this is not a rule per se, but being uniform is a nice quality for a codebase). See also https://tip.golang.org/doc/comment
elebedeva
commented
Fixed Fixed
|
|||||||||
|
res, covErr := covertest.Run(covertestRunVM)
|
||||||||
|
t.Log("Printing collected instructions:")
|
||||||||
|
spew.Dump(res)
|
||||||||
|
t.Log("covertest.Run() returned an error: ", covErr)
|
||||||||
fyrchik marked this conversation as resolved
Outdated
fyrchik
commented
For this case you might want to look at https://pkg.go.dev/github.com/stretchr/testify/require package. Example of usage: Line 17 in TrueCloudLab/frostfs-sdk-go@3dc8129
For this case you might want to look at https://pkg.go.dev/github.com/stretchr/testify/require package.
We want the test to _fail_, not just print info.
Example of usage: https://git.frostfs.info/TrueCloudLab/frostfs-sdk-go/src/commit/3dc8129ed79406b7f0e523be471f30e3014b9107/object/attribute_test.go#L17
elebedeva
commented
Fixed Fixed
|
|||||||||
|
|
||||||||
|
// set up a VM for vm.Run()
|
||||||||
|
origRunVM := setUpVMForPut(t, e, ctrDI.Contract, hasResult, startOffsetPutNumber, someNum, invalidKey)
|
||||||||
|
runerr := origRunVM.Run()
|
||||||||
|
t.Log("vm.Run() returned an error: ", covErr)
|
||||||||
|
|
||||||||
|
// check if errors are the same
|
||||||||
|
require.Equal(t, runerr.Error(), covErr.Error())
|
||||||||
|
|
||||||||
|
// check if the number of elements on the stack is the same
|
||||||||
|
require.Equal(t, origRunVM.Estack().Len(), covertestRunVM.Estack().Len())
|
||||||||
|
}
|
||||||||
|
|
||||||||
|
func setUpVMForPut(t *testing.T, e *neotest.Executor, contract *neotest.Contract, hasResult bool, methodOff int, num int, key []byte) (v *vm.VM) {
|
||||||||
|
ic, err := e.Chain.GetTestVM(trigger.Application, nil, nil)
|
||||||||
|
require.NoError(t, err)
|
||||||||
|
ic.VM.LoadNEFMethod(contract.NEF, contract.Hash, contract.Hash, callflag.All, hasResult, methodOff, -1, nil)
|
||||||||
|
ic.VM.Context().Estack().PushVal(num)
|
||||||||
|
ic.VM.Context().Estack().PushVal(key)
|
||||||||
|
return ic.VM
|
||||||||
|
}
|
||||||||
|
|
||||||||
|
func getStartOffset(di *compiler.DebugInfo, methodID string) (int, error) {
|
||||||||
|
for _, method := range di.Methods {
|
||||||||
|
if method.ID == methodID {
|
||||||||
|
return int(method.Range.Start), nil
|
||||||||
|
}
|
||||||||
|
}
|
||||||||
|
return 0, errors.New("Method not found")
|
||||||||
|
}
|
||||||||
|
|
||||||||
|
func hasResult(di *compiler.DebugInfo, methodID string) (bool, error) {
|
||||||||
|
for _, method := range di.Methods {
|
||||||||
|
if method.ID == methodID {
|
||||||||
|
if method.ReturnType == "Void" {
|
||||||||
|
return false, nil
|
||||||||
|
}
|
||||||||
|
return true, nil
|
||||||||
|
}
|
||||||||
|
}
|
||||||||
|
return false, errors.New("Method not found")
|
||||||||
|
}
|
||||||||
|
|
||||||||
|
func getNumToPut() int {
|
||||||||
|
return rand.Intn(100)
|
||||||||
|
}
|
||||||||
|
|
Instruction doesn't have a script hash, a contract it belongs to does.
Fixed