neo-go/pkg/neotest/client.go
Slava0135 d0c45477f5 neotest: implement coverage collection
Test coverage is automatically enabled when go test is running with coverage
enabled. It can be disabled for any Executor by using relevant methods.
Coverage is gathered by capturing VM OPs during test contract execution and
mapping them to the contract source code using the DebugInfo information.

Signed-off-by: Slava0135 <super.novalskiy_0135@inbox.ru>
2024-08-19 14:39:18 +03:00

150 lines
5 KiB
Go

package neotest
import (
"testing"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"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/util"
"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/vmstate"
"github.com/stretchr/testify/require"
)
// ContractInvoker is a client for a specific contract.
type ContractInvoker struct {
*Executor
Hash util.Uint160
Signers []Signer
}
// NewInvoker creates a new ContractInvoker for the contract with hash h and the specified signers.
func (e *Executor) NewInvoker(h util.Uint160, signers ...Signer) *ContractInvoker {
return &ContractInvoker{
Executor: e,
Hash: h,
Signers: signers,
}
}
// CommitteeInvoker creates a new ContractInvoker for the contract with hash h and a committee multisignature signer.
func (e *Executor) CommitteeInvoker(h util.Uint160) *ContractInvoker {
return &ContractInvoker{
Executor: e,
Hash: h,
Signers: []Signer{e.Committee},
}
}
// ValidatorInvoker creates a new ContractInvoker for the contract with hash h and a validators multisignature signer.
func (e *Executor) ValidatorInvoker(h util.Uint160) *ContractInvoker {
return &ContractInvoker{
Executor: e,
Hash: h,
Signers: []Signer{e.Validator},
}
}
// TestInvokeScript creates test VM and invokes the script with the args and signers.
func (c *ContractInvoker) TestInvokeScript(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) (*vm.Stack, error) {
tx := c.PrepareInvocationNoSign(t, script, validUntilBlock...)
for _, acc := range signers {
tx.Signers = append(tx.Signers, transaction.Signer{
Account: acc.ScriptHash(),
Scopes: transaction.Global,
})
}
b := c.NewUnsignedBlock(t, tx)
ic, err := c.Chain.GetTestVM(trigger.Application, tx, b)
if err != nil {
return nil, err
}
t.Cleanup(ic.Finalize)
if c.collectCoverage {
ic.VM.SetOnExecHook(coverageHook)
}
ic.VM.LoadWithFlags(tx.Script, callflag.All)
err = ic.VM.Run()
return ic.VM.Estack(), err
}
// TestInvoke creates test VM and invokes the method with the args.
func (c *ContractInvoker) TestInvoke(t testing.TB, method string, args ...any) (*vm.Stack, error) {
tx := c.PrepareInvokeNoSign(t, method, args...)
b := c.NewUnsignedBlock(t, tx)
ic, err := c.Chain.GetTestVM(trigger.Application, tx, b)
if err != nil {
return nil, err
}
t.Cleanup(ic.Finalize)
if c.collectCoverage {
ic.VM.SetOnExecHook(coverageHook)
}
ic.VM.LoadWithFlags(tx.Script, callflag.All)
err = ic.VM.Run()
return ic.VM.Estack(), err
}
// WithSigners creates a new client with the provided signer.
func (c *ContractInvoker) WithSigners(signers ...Signer) *ContractInvoker {
newC := *c
newC.Signers = signers
return &newC
}
// PrepareInvoke creates a new invocation transaction.
func (c *ContractInvoker) PrepareInvoke(t testing.TB, method string, args ...any) *transaction.Transaction {
return c.Executor.NewTx(t, c.Signers, c.Hash, method, args...)
}
// PrepareInvokeNoSign creates a new unsigned invocation transaction.
func (c *ContractInvoker) PrepareInvokeNoSign(t testing.TB, method string, args ...any) *transaction.Transaction {
return c.Executor.NewUnsignedTx(t, c.Hash, method, args...)
}
// Invoke invokes the method with the args, persists the transaction and checks the result.
// Returns transaction hash.
func (c *ContractInvoker) Invoke(t testing.TB, result any, method string, args ...any) util.Uint256 {
tx := c.PrepareInvoke(t, method, args...)
c.AddNewBlock(t, tx)
c.CheckHalt(t, tx.Hash(), stackitem.Make(result))
return tx.Hash()
}
// InvokeAndCheck invokes the method with the args, persists the transaction and checks the result
// using the provided function. It returns the transaction hash.
func (c *ContractInvoker) InvokeAndCheck(t testing.TB, checkResult func(t testing.TB, stack []stackitem.Item), method string, args ...any) util.Uint256 {
tx := c.PrepareInvoke(t, method, args...)
c.AddNewBlock(t, tx)
aer, err := c.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, vmstate.Halt, aer[0].VMState, aer[0].FaultException)
if checkResult != nil {
checkResult(t, aer[0].Stack)
}
return tx.Hash()
}
// InvokeWithFeeFail is like InvokeFail but sets the custom system fee for the transaction.
func (c *ContractInvoker) InvokeWithFeeFail(t testing.TB, message string, sysFee int64, method string, args ...any) util.Uint256 {
tx := c.PrepareInvokeNoSign(t, method, args...)
c.Executor.SignTx(t, tx, sysFee, c.Signers...)
c.AddNewBlock(t, tx)
c.CheckFault(t, tx.Hash(), message)
return tx.Hash()
}
// InvokeFail invokes the method with the args, persists the transaction and checks the error message.
// It returns the transaction hash.
func (c *ContractInvoker) InvokeFail(t testing.TB, message string, method string, args ...any) util.Uint256 {
tx := c.PrepareInvoke(t, method, args...)
c.AddNewBlock(t, tx)
c.CheckFault(t, tx.Hash(), message)
return tx.Hash()
}