From dcb06163da6958924718e03c3660f06c1dbd05af Mon Sep 17 00:00:00 2001
From: Anna Shaleva <anna@nspcc.ru>
Date: Fri, 3 Dec 2021 18:45:55 +0300
Subject: [PATCH] neotest: improve neotest API

Added several methods that are useful for testing.
---
 pkg/neotest/basic.go       | 46 ++++++++++++++++++++++++++++++++++++++
 pkg/neotest/chain/chain.go | 20 +++++++++++++++++
 pkg/neotest/client.go      | 15 +++++++++++++
 3 files changed, 81 insertions(+)

diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go
index 72d8f9cd0..501254ae9 100644
--- a/pkg/neotest/basic.go
+++ b/pkg/neotest/basic.go
@@ -3,6 +3,7 @@ package neotest
 import (
 	"encoding/json"
 	"fmt"
+	"math/big"
 	"strings"
 	"testing"
 
@@ -62,6 +63,14 @@ func (e *Executor) NativeHash(t *testing.T, name string) util.Uint160 {
 	return h
 }
 
+// NativeID returns native contract ID by name.
+func (e *Executor) NativeID(t *testing.T, name string) int32 {
+	h := e.NativeHash(t, name)
+	cs := e.Chain.GetContractState(h)
+	require.NotNil(t, cs)
+	return cs.ID
+}
+
 // NewUnsignedTx creates new unsigned transaction which invokes method of contract with hash.
 func (e *Executor) NewUnsignedTx(t *testing.T, hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
 	w := io.NewBufBinWriter()
@@ -202,6 +211,12 @@ func (e *Executor) CheckTxNotificationEvent(t *testing.T, h util.Uint256, index
 	require.Equal(t, expected, aer[0].Events[index])
 }
 
+// CheckGASBalance ensures that provided account owns specified amount of GAS.
+func (e *Executor) CheckGASBalance(t *testing.T, 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()))
+}
+
 // NewDeployTx returns new deployment tx for contract signed by committee.
 func (e *Executor) NewDeployTx(t *testing.T, bc blockchainer.Blockchainer, c *Contract, data interface{}) *transaction.Transaction {
 	rawManifest, err := json.Marshal(c.Manifest)
@@ -277,6 +292,13 @@ func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *b
 	return b
 }
 
+// GenerateNewBlocks adds specified number of empty blocks to the chain.
+func (e *Executor) GenerateNewBlocks(t *testing.T, count int) {
+	for i := 0; i < count; i++ {
+		e.AddNewBlock(t)
+	}
+}
+
 // 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)
@@ -316,3 +338,27 @@ func TestInvoke(bc blockchainer.Blockchainer, tx *transaction.Transaction) (*vm.
 	err = v.Run()
 	return v, err
 }
+
+// GetTransaction returns transaction and its height by the specified hash.
+func (e *Executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) {
+	tx, height, err := e.Chain.GetTransaction(h)
+	require.NoError(t, err)
+	return tx, height
+}
+
+// GetBlockByIndex returns block by the specified index.
+func (e *Executor) GetBlockByIndex(t *testing.T, idx int) *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.T, 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]
+}
diff --git a/pkg/neotest/chain/chain.go b/pkg/neotest/chain/chain.go
index 7eaa5bd9c..db45ba75f 100644
--- a/pkg/neotest/chain/chain.go
+++ b/pkg/neotest/chain/chain.go
@@ -108,14 +108,24 @@ func init() {
 // NewSingle creates new blockchain instance with a single validator and
 // setups cleanup functions.
 func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) {
+	return NewSingleWithCustomConfig(t, nil)
+}
+
+// NewSingleWithCustomConfig creates new blockchain instance with custom protocol
+// configuration and a single validator. It also setups cleanup functions.
+func NewSingleWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer) {
 	protoCfg := config.ProtocolConfiguration{
 		Magic:              netmode.UnitTestNet,
+		MaxTraceableBlocks: 1000, // We don't need a lot of traceable blocks for tests.
 		SecondsPerBlock:    1,
 		StandbyCommittee:   []string{hex.EncodeToString(committeeAcc.PrivateKey().PublicKey().Bytes())},
 		ValidatorsCount:    1,
 		VerifyBlocks:       true,
 		VerifyTransactions: true,
 	}
+	if f != nil {
+		f(&protoCfg)
+	}
 
 	st := storage.NewMemoryStore()
 	log := zaptest.NewLogger(t)
@@ -129,6 +139,13 @@ func NewSingle(t *testing.T) (*core.Blockchain, neotest.Signer) {
 // NewMulti creates new blockchain instance with 4 validators and 6 committee members.
 // Second return value is for validator signer, third -- for committee.
 func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) {
+	return NewMultiWithCustomConfig(t, nil)
+}
+
+// NewMultiWithCustomConfig creates new blockchain instance with custom protocol
+// configuration, 4 validators and 6 committee members. Second return value is
+// for validator signer, third -- for committee.
+func NewMultiWithCustomConfig(t *testing.T, f func(*config.ProtocolConfiguration)) (*core.Blockchain, neotest.Signer, neotest.Signer) {
 	protoCfg := config.ProtocolConfiguration{
 		Magic:              netmode.UnitTestNet,
 		SecondsPerBlock:    1,
@@ -137,6 +154,9 @@ func NewMulti(t *testing.T) (*core.Blockchain, neotest.Signer, neotest.Signer) {
 		VerifyBlocks:       true,
 		VerifyTransactions: true,
 	}
+	if f != nil {
+		f(&protoCfg)
+	}
 
 	st := storage.NewMemoryStore()
 	log := zaptest.NewLogger(t)
diff --git a/pkg/neotest/client.go b/pkg/neotest/client.go
index 646f58143..89ac48cf7 100644
--- a/pkg/neotest/client.go
+++ b/pkg/neotest/client.go
@@ -9,6 +9,7 @@ import (
 	"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/stretchr/testify/require"
 )
 
 // ContractInvoker is a client for specific contract.
@@ -65,6 +66,20 @@ func (c *ContractInvoker) Invoke(t *testing.T, result interface{}, method string
 	return tx.Hash()
 }
 
+// InvokeAndCheck invokes method with args, persists transaction and checks the result
+// using provided function. Returns transaction hash.
+func (c *ContractInvoker) InvokeAndCheck(t *testing.T, checkResult func(t *testing.T, stack []stackitem.Item), method string, args ...interface{}) 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, vm.HaltState, aer[0].VMState, aer[0].FaultException)
+	if checkResult != nil {
+		checkResult(t, aer[0].Stack)
+	}
+	return tx.Hash()
+}
+
 // InvokeWithFeeFail is like InvokeFail but sets custom system fee for the transaction.
 func (c *ContractInvoker) InvokeWithFeeFail(t *testing.T, message string, sysFee int64, method string, args ...interface{}) util.Uint256 {
 	tx := c.PrepareInvokeNoSign(t, method, args...)