mirror of
https://github.com/nspcc-dev/neo-go.git
synced 2024-11-26 09:42:22 +00:00
d0c45477f5
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>
462 lines
16 KiB
Go
462 lines
16 KiB
Go
package neotest
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/big"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
|
"github.com/nspcc-dev/neo-go/pkg/core"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native/nativenames"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/state"
|
|
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
|
|
"github.com/nspcc-dev/neo-go/pkg/io"
|
|
"github.com/nspcc-dev/neo-go/pkg/smartcontract"
|
|
"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/nspcc-dev/neo-go/pkg/wallet"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// Executor is a wrapper over chain state.
|
|
type Executor struct {
|
|
Chain *core.Blockchain
|
|
Validator Signer
|
|
Committee Signer
|
|
CommitteeHash util.Uint160
|
|
// collectCoverage is true if coverage is being collected when running this executor.
|
|
collectCoverage bool
|
|
}
|
|
|
|
// NewExecutor creates a new executor instance from the provided blockchain and committee.
|
|
// By default coverage collection is enabled, but only when `go test` is running with coverage enabled.
|
|
// Use DisableCoverage and EnableCoverage to stop coverage collection for this executor when not desired.
|
|
func NewExecutor(t testing.TB, bc *core.Blockchain, validator, committee Signer) *Executor {
|
|
checkMultiSigner(t, validator)
|
|
checkMultiSigner(t, committee)
|
|
|
|
return &Executor{
|
|
Chain: bc,
|
|
Validator: validator,
|
|
Committee: committee,
|
|
CommitteeHash: committee.ScriptHash(),
|
|
collectCoverage: isCoverageEnabled(),
|
|
}
|
|
}
|
|
|
|
// TopBlock returns the block with the highest index.
|
|
func (e *Executor) TopBlock(t testing.TB) *block.Block {
|
|
b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(e.Chain.BlockHeight()))
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
// NativeHash returns a native contract hash by the name.
|
|
func (e *Executor) NativeHash(t testing.TB, name string) util.Uint160 {
|
|
h, err := e.Chain.GetNativeContractScriptHash(name)
|
|
require.NoError(t, err)
|
|
return h
|
|
}
|
|
|
|
// ContractHash returns a contract hash by the ID.
|
|
func (e *Executor) ContractHash(t testing.TB, id int32) util.Uint160 {
|
|
h, err := e.Chain.GetContractScriptHash(id)
|
|
require.NoError(t, err)
|
|
return h
|
|
}
|
|
|
|
// NativeID returns a native contract ID by the name.
|
|
func (e *Executor) NativeID(t testing.TB, name string) int32 {
|
|
h := e.NativeHash(t, name)
|
|
cs := e.Chain.GetContractState(h)
|
|
require.NotNil(t, cs)
|
|
return cs.ID
|
|
}
|
|
|
|
// NewUnsignedTx creates a new unsigned transaction which invokes the method of the contract with the hash.
|
|
func (e *Executor) NewUnsignedTx(t testing.TB, hash util.Uint160, method string, args ...any) *transaction.Transaction {
|
|
script, err := smartcontract.CreateCallScript(hash, method, args...)
|
|
require.NoError(t, err)
|
|
|
|
tx := transaction.New(script, 0)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
return tx
|
|
}
|
|
|
|
// NewTx creates a new transaction which invokes the contract method.
|
|
// The transaction is signed by the signers.
|
|
func (e *Executor) NewTx(t testing.TB, signers []Signer,
|
|
hash util.Uint160, method string, args ...any) *transaction.Transaction {
|
|
tx := e.NewUnsignedTx(t, hash, method, args...)
|
|
return e.SignTx(t, tx, -1, signers...)
|
|
}
|
|
|
|
// SignTx signs a transaction using the provided signers.
|
|
func (e *Executor) SignTx(t testing.TB, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction {
|
|
for _, acc := range signers {
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
|
Account: acc.ScriptHash(),
|
|
Scopes: transaction.Global,
|
|
})
|
|
}
|
|
AddNetworkFee(t, e.Chain, tx, signers...)
|
|
e.AddSystemFee(tx, sysFee)
|
|
|
|
for _, acc := range signers {
|
|
require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx))
|
|
}
|
|
return tx
|
|
}
|
|
|
|
// NewAccount returns a new signer holding 100.0 GAS (or given amount is specified).
|
|
// This method advances the chain by one block with a transfer transaction.
|
|
func (e *Executor) NewAccount(t testing.TB, expectedGASBalance ...int64) Signer {
|
|
acc, err := wallet.NewAccount()
|
|
require.NoError(t, err)
|
|
|
|
amount := int64(100_0000_0000)
|
|
if len(expectedGASBalance) != 0 {
|
|
amount = expectedGASBalance[0]
|
|
}
|
|
tx := e.NewTx(t, []Signer{e.Validator},
|
|
e.NativeHash(t, nativenames.Gas), "transfer",
|
|
e.Validator.ScriptHash(), acc.Contract.ScriptHash(), amount, nil)
|
|
e.AddNewBlock(t, tx)
|
|
e.CheckHalt(t, tx.Hash())
|
|
return NewSingleSigner(acc)
|
|
}
|
|
|
|
// DeployContract compiles and deploys a contract to the bc. It also checks that
|
|
// the precalculated contract hash matches the actual one.
|
|
// data is an optional argument to `_deploy`.
|
|
// It returns the hash of the deploy transaction.
|
|
func (e *Executor) DeployContract(t testing.TB, c *Contract, data any) util.Uint256 {
|
|
return e.DeployContractBy(t, e.Validator, c, data)
|
|
}
|
|
|
|
// DeployContractBy compiles and deploys a contract to the bc using the provided signer.
|
|
// It also checks that the precalculated contract hash matches the actual one.
|
|
// data is an optional argument to `_deploy`.
|
|
// It returns the hash of the deploy transaction.
|
|
func (e *Executor) DeployContractBy(t testing.TB, signer Signer, c *Contract, data any) util.Uint256 {
|
|
e.trackCoverage(t, c)
|
|
tx := e.NewDeployTxBy(t, signer, c, data)
|
|
e.AddNewBlock(t, tx)
|
|
e.CheckHalt(t, tx.Hash())
|
|
|
|
// Check that the precalculated hash matches the real one.
|
|
e.CheckTxNotificationEvent(t, tx.Hash(), -1, state.NotificationEvent{
|
|
ScriptHash: e.NativeHash(t, nativenames.Management),
|
|
Name: "Deploy",
|
|
Item: stackitem.NewArray([]stackitem.Item{
|
|
stackitem.NewByteArray(c.Hash.BytesBE()),
|
|
}),
|
|
})
|
|
|
|
return tx.Hash()
|
|
}
|
|
|
|
// DeployContractCheckFAULT compiles and deploys a contract to the bc using the validator
|
|
// account. It checks that the deploy transaction FAULTed with the specified error.
|
|
func (e *Executor) DeployContractCheckFAULT(t testing.TB, c *Contract, data any, errMessage string) {
|
|
e.trackCoverage(t, c)
|
|
tx := e.NewDeployTx(t, c, data)
|
|
e.AddNewBlock(t, tx)
|
|
e.CheckFault(t, tx.Hash(), errMessage)
|
|
}
|
|
|
|
// trackCoverage switches on coverage tracking for provided script if `go test` is running with coverage enabled.
|
|
func (e *Executor) trackCoverage(t testing.TB, c *Contract) {
|
|
if e.collectCoverage {
|
|
addScriptToCoverage(c)
|
|
t.Cleanup(func() {
|
|
reportCoverage(t)
|
|
})
|
|
}
|
|
}
|
|
|
|
// InvokeScript adds a transaction with the specified script to the chain and
|
|
// returns its hash. It does no faults check.
|
|
func (e *Executor) InvokeScript(t testing.TB, script []byte, signers []Signer) util.Uint256 {
|
|
tx := e.PrepareInvocation(t, script, signers)
|
|
e.AddNewBlock(t, tx)
|
|
return tx.Hash()
|
|
}
|
|
|
|
// PrepareInvocation creates a transaction with the specified script and signs it
|
|
// by the provided signer.
|
|
func (e *Executor) PrepareInvocation(t testing.TB, script []byte, signers []Signer, validUntilBlock ...uint32) *transaction.Transaction {
|
|
tx := e.PrepareInvocationNoSign(t, script, validUntilBlock...)
|
|
e.SignTx(t, tx, -1, signers...)
|
|
return tx
|
|
}
|
|
|
|
func (e *Executor) PrepareInvocationNoSign(t testing.TB, script []byte, validUntilBlock ...uint32) *transaction.Transaction {
|
|
tx := transaction.New(script, 0)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
if len(validUntilBlock) != 0 {
|
|
tx.ValidUntilBlock = validUntilBlock[0]
|
|
}
|
|
return tx
|
|
}
|
|
|
|
// InvokeScriptCheckHALT adds a transaction with the specified script to the chain
|
|
// and checks if it's HALTed with the specified items on stack.
|
|
func (e *Executor) InvokeScriptCheckHALT(t testing.TB, script []byte, signers []Signer, stack ...stackitem.Item) {
|
|
hash := e.InvokeScript(t, script, signers)
|
|
e.CheckHalt(t, hash, stack...)
|
|
}
|
|
|
|
// InvokeScriptCheckFAULT adds a transaction with the specified script to the
|
|
// chain and checks if it's FAULTed with the specified error.
|
|
func (e *Executor) InvokeScriptCheckFAULT(t testing.TB, script []byte, signers []Signer, errMessage string) util.Uint256 {
|
|
hash := e.InvokeScript(t, script, signers)
|
|
e.CheckFault(t, hash, errMessage)
|
|
return hash
|
|
}
|
|
|
|
// CheckHalt checks that the transaction is persisted with HALT state.
|
|
func (e *Executor) CheckHalt(t testing.TB, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult {
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
require.NoError(t, err)
|
|
require.Equal(t, vmstate.Halt, aer[0].VMState, aer[0].FaultException)
|
|
if len(stack) != 0 {
|
|
require.Equal(t, stack, aer[0].Stack)
|
|
}
|
|
return &aer[0]
|
|
}
|
|
|
|
// CheckFault checks that the transaction is persisted with FAULT state.
|
|
// The raised exception is also checked to contain the s as a substring.
|
|
func (e *Executor) CheckFault(t testing.TB, h util.Uint256, s string) {
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
require.NoError(t, err)
|
|
require.Equal(t, vmstate.Fault, aer[0].VMState)
|
|
require.True(t, strings.Contains(aer[0].FaultException, s),
|
|
"expected: %s, got: %s", s, aer[0].FaultException)
|
|
}
|
|
|
|
// CheckTxNotificationEvent checks that the specified event was emitted at the specified position
|
|
// during transaction script execution. Negative index corresponds to backwards enumeration.
|
|
func (e *Executor) CheckTxNotificationEvent(t testing.TB, h util.Uint256, index int, expected state.NotificationEvent) {
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
require.NoError(t, err)
|
|
l := len(aer[0].Events)
|
|
if index < 0 {
|
|
index = l + index
|
|
}
|
|
require.True(t, 0 <= index && index < l, fmt.Errorf("notification index is out of range: want %d, len is %d", index, l))
|
|
require.Equal(t, expected, aer[0].Events[index])
|
|
}
|
|
|
|
// CheckGASBalance ensures that the provided account owns the specified amount of GAS.
|
|
func (e *Executor) CheckGASBalance(t testing.TB, acc util.Uint160, expected *big.Int) {
|
|
actual := e.Chain.GetUtilityTokenBalance(acc)
|
|
require.Equal(t, expected, actual, fmt.Errorf("invalid GAS balance: expected %s, got %s", expected.String(), actual.String()))
|
|
}
|
|
|
|
// EnsureGASBalance ensures that the provided account owns the amount of GAS that satisfies the provided condition.
|
|
func (e *Executor) EnsureGASBalance(t testing.TB, acc util.Uint160, isOk func(balance *big.Int) bool) {
|
|
actual := e.Chain.GetUtilityTokenBalance(acc)
|
|
require.True(t, isOk(actual), fmt.Errorf("invalid GAS balance: got %s, condition is not satisfied", actual.String()))
|
|
}
|
|
|
|
// NewDeployTx returns a new deployment tx for the contract signed by the committee.
|
|
func (e *Executor) NewDeployTx(t testing.TB, c *Contract, data any) *transaction.Transaction {
|
|
return e.NewDeployTxBy(t, e.Validator, c, data)
|
|
}
|
|
|
|
// NewDeployTxBy returns a new deployment tx for the contract signed by the specified signer.
|
|
func (e *Executor) NewDeployTxBy(t testing.TB, signer Signer, c *Contract, data any) *transaction.Transaction {
|
|
rawManifest, err := json.Marshal(c.Manifest)
|
|
require.NoError(t, err)
|
|
|
|
neb, err := c.NEF.Bytes()
|
|
require.NoError(t, err)
|
|
|
|
script, err := smartcontract.CreateCallScript(e.Chain.ManagementContractHash(), "deploy", neb, rawManifest, data)
|
|
require.NoError(t, err)
|
|
|
|
tx := transaction.New(script, 0)
|
|
tx.Nonce = Nonce()
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
tx.Signers = []transaction.Signer{{
|
|
Account: signer.ScriptHash(),
|
|
Scopes: transaction.Global,
|
|
}}
|
|
AddNetworkFee(t, e.Chain, tx, signer)
|
|
e.AddSystemFee(tx, -1)
|
|
require.NoError(t, signer.SignTx(netmode.UnitTestNet, tx))
|
|
return tx
|
|
}
|
|
|
|
// AddSystemFee adds system fee to the transaction. If negative value specified,
|
|
// then system fee is defined by test invocation.
|
|
func (e *Executor) AddSystemFee(tx *transaction.Transaction, sysFee int64) {
|
|
if sysFee >= 0 {
|
|
tx.SystemFee = sysFee
|
|
return
|
|
}
|
|
v, _ := e.TestInvoke(tx) // ignore error to support failing transactions
|
|
tx.SystemFee = v.GasConsumed()
|
|
}
|
|
|
|
// AddNetworkFee adds network fee to the transaction.
|
|
func AddNetworkFee(t testing.TB, bc *core.Blockchain, tx *transaction.Transaction, signers ...Signer) {
|
|
baseFee := bc.GetBaseExecFee()
|
|
size := io.GetVarSize(tx)
|
|
for _, sgr := range signers {
|
|
csgr, ok := sgr.(SingleSigner)
|
|
if ok && csgr.Account().Contract.InvocationBuilder != nil {
|
|
sc, err := csgr.Account().Contract.InvocationBuilder(tx)
|
|
require.NoError(t, err)
|
|
|
|
txCopy := *tx
|
|
ic, err := bc.GetTestVM(trigger.Verification, &txCopy, nil)
|
|
require.NoError(t, err)
|
|
|
|
ic.UseSigners(tx.Signers)
|
|
ic.VM.GasLimit = bc.GetMaxVerificationGAS()
|
|
|
|
require.NoError(t, bc.InitVerificationContext(ic, csgr.ScriptHash(), &transaction.Witness{InvocationScript: sc, VerificationScript: csgr.Script()}))
|
|
require.NoError(t, ic.VM.Run())
|
|
|
|
tx.NetworkFee += ic.VM.GasConsumed()
|
|
size += io.GetVarSize(sc) + io.GetVarSize(csgr.Script())
|
|
} else {
|
|
netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script())
|
|
tx.NetworkFee += netFee
|
|
size += sizeDelta
|
|
}
|
|
}
|
|
tx.NetworkFee += int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(tx)
|
|
}
|
|
|
|
// NewUnsignedBlock creates a new unsigned block from txs.
|
|
func (e *Executor) NewUnsignedBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
|
|
lastBlock := e.TopBlock(t)
|
|
b := &block.Block{
|
|
Header: block.Header{
|
|
NextConsensus: e.Validator.ScriptHash(),
|
|
Script: transaction.Witness{
|
|
VerificationScript: e.Validator.Script(),
|
|
},
|
|
Timestamp: lastBlock.Timestamp + 1,
|
|
},
|
|
Transactions: txs,
|
|
}
|
|
if e.Chain.GetConfig().StateRootInHeader {
|
|
b.StateRootEnabled = true
|
|
b.PrevStateRoot = e.Chain.GetStateModule().CurrentLocalStateRoot()
|
|
}
|
|
b.PrevHash = lastBlock.Hash()
|
|
b.Index = e.Chain.BlockHeight() + 1
|
|
b.RebuildMerkleRoot()
|
|
return b
|
|
}
|
|
|
|
// AddNewBlock creates a new block from the provided transactions and adds it on the bc.
|
|
func (e *Executor) AddNewBlock(t testing.TB, txs ...*transaction.Transaction) *block.Block {
|
|
b := e.NewUnsignedBlock(t, txs...)
|
|
e.SignBlock(b)
|
|
require.NoError(t, e.Chain.AddBlock(b))
|
|
return b
|
|
}
|
|
|
|
// GenerateNewBlocks adds the specified number of empty blocks to the chain.
|
|
func (e *Executor) GenerateNewBlocks(t testing.TB, count int) []*block.Block {
|
|
blocks := make([]*block.Block, count)
|
|
for i := 0; i < count; i++ {
|
|
blocks[i] = e.AddNewBlock(t)
|
|
}
|
|
return blocks
|
|
}
|
|
|
|
// SignBlock add validators signature to b.
|
|
func (e *Executor) SignBlock(b *block.Block) *block.Block {
|
|
invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b)
|
|
b.Script.InvocationScript = invoc
|
|
return b
|
|
}
|
|
|
|
// AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt.
|
|
func (e *Executor) AddBlockCheckHalt(t testing.TB, txs ...*transaction.Transaction) *block.Block {
|
|
b := e.AddNewBlock(t, txs...)
|
|
for _, tx := range txs {
|
|
e.CheckHalt(t, tx.Hash())
|
|
}
|
|
return b
|
|
}
|
|
|
|
// TestInvoke creates a test VM with a dummy block and executes a transaction in it.
|
|
func (e *Executor) TestInvoke(tx *transaction.Transaction) (*vm.VM, error) {
|
|
lastBlock, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(e.Chain.BlockHeight()))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := &block.Block{
|
|
Header: block.Header{
|
|
Index: e.Chain.BlockHeight() + 1,
|
|
Timestamp: lastBlock.Timestamp + 1,
|
|
},
|
|
}
|
|
|
|
// `GetTestVM` as well as `Run` can use a transaction hash which will set a cached value.
|
|
// This is unwanted behavior, so we explicitly copy the transaction to perform execution.
|
|
ttx := *tx
|
|
ic, _ := e.Chain.GetTestVM(trigger.Application, &ttx, b)
|
|
|
|
if e.collectCoverage {
|
|
ic.VM.SetOnExecHook(coverageHook)
|
|
}
|
|
|
|
defer ic.Finalize()
|
|
|
|
ic.VM.LoadWithFlags(tx.Script, callflag.All)
|
|
err = ic.VM.Run()
|
|
return ic.VM, err
|
|
}
|
|
|
|
// GetTransaction returns a transaction and its height by the specified hash.
|
|
func (e *Executor) GetTransaction(t testing.TB, h util.Uint256) (*transaction.Transaction, uint32) {
|
|
tx, height, err := e.Chain.GetTransaction(h)
|
|
require.NoError(t, err)
|
|
return tx, height
|
|
}
|
|
|
|
// GetBlockByIndex returns a block by the specified index.
|
|
func (e *Executor) GetBlockByIndex(t testing.TB, idx uint32) *block.Block {
|
|
h := e.Chain.GetHeaderHash(idx)
|
|
require.NotEmpty(t, h)
|
|
b, err := e.Chain.GetBlock(h)
|
|
require.NoError(t, err)
|
|
return b
|
|
}
|
|
|
|
// GetTxExecResult returns application execution results for the specified transaction.
|
|
func (e *Executor) GetTxExecResult(t testing.TB, h util.Uint256) *state.AppExecResult {
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(aer))
|
|
return &aer[0]
|
|
}
|
|
|
|
// EnableCoverage enables coverage collection for this executor, but only when `go test` is running with coverage enabled.
|
|
func (e *Executor) EnableCoverage() {
|
|
e.collectCoverage = isCoverageEnabled()
|
|
}
|
|
|
|
// DisableCoverage disables coverage collection for this executor until enabled explicitly through EnableCoverage.
|
|
func (e *Executor) DisableCoverage() {
|
|
e.collectCoverage = false
|
|
}
|