2021-10-23 11:28:03 +00:00
|
|
|
package neotest
|
|
|
|
|
|
|
|
import (
|
|
|
|
"encoding/json"
|
2021-11-16 14:40:01 +00:00
|
|
|
"fmt"
|
2021-12-03 15:45:55 +00:00
|
|
|
"math/big"
|
2021-10-23 11:28:03 +00:00
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/config/netmode"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/block"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/blockchainer"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/fee"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/core/native"
|
|
|
|
"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/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/emit"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/vm/stackitem"
|
|
|
|
"github.com/nspcc-dev/neo-go/pkg/wallet"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
)
|
|
|
|
|
|
|
|
// Executor is a wrapper over chain state.
|
|
|
|
type Executor struct {
|
|
|
|
Chain blockchainer.Blockchainer
|
2021-11-03 12:22:08 +00:00
|
|
|
Validator Signer
|
2021-11-03 10:44:46 +00:00
|
|
|
Committee Signer
|
2021-10-23 11:28:03 +00:00
|
|
|
CommitteeHash util.Uint160
|
|
|
|
Contracts map[string]*Contract
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewExecutor creates new executor instance from provided blockchain and committee.
|
2021-11-03 12:22:08 +00:00
|
|
|
func NewExecutor(t *testing.T, bc blockchainer.Blockchainer, validator, committee Signer) *Executor {
|
2021-11-03 12:28:29 +00:00
|
|
|
checkMultiSigner(t, validator)
|
|
|
|
checkMultiSigner(t, committee)
|
2021-10-23 11:28:03 +00:00
|
|
|
|
|
|
|
return &Executor{
|
|
|
|
Chain: bc,
|
2021-11-03 12:22:08 +00:00
|
|
|
Validator: validator,
|
2021-10-23 11:28:03 +00:00
|
|
|
Committee: committee,
|
2021-11-03 10:44:46 +00:00
|
|
|
CommitteeHash: committee.ScriptHash(),
|
2021-10-23 11:28:03 +00:00
|
|
|
Contracts: make(map[string]*Contract),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TopBlock returns block with the highest index.
|
|
|
|
func (e *Executor) TopBlock(t *testing.T) *block.Block {
|
|
|
|
b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(int(e.Chain.BlockHeight())))
|
|
|
|
require.NoError(t, err)
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
// NativeHash returns native contract hash by name.
|
|
|
|
func (e *Executor) NativeHash(t *testing.T, name string) util.Uint160 {
|
|
|
|
h, err := e.Chain.GetNativeContractScriptHash(name)
|
|
|
|
require.NoError(t, err)
|
|
|
|
return h
|
|
|
|
}
|
|
|
|
|
2021-12-03 15:45:55 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2021-10-23 11:28:03 +00:00
|
|
|
// 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()
|
|
|
|
emit.AppCall(w.BinWriter, hash, method, callflag.All, args...)
|
|
|
|
require.NoError(t, w.Err)
|
|
|
|
|
|
|
|
script := w.Bytes()
|
|
|
|
tx := transaction.New(script, 0)
|
2021-11-11 17:00:40 +00:00
|
|
|
tx.Nonce = Nonce()
|
2021-10-23 11:28:03 +00:00
|
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
|
|
return tx
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewTx creates new transaction which invokes contract method.
|
|
|
|
// Transaction is signed with signer.
|
2021-11-03 10:44:46 +00:00
|
|
|
func (e *Executor) NewTx(t *testing.T, signers []Signer,
|
2021-10-23 11:28:03 +00:00
|
|
|
hash util.Uint160, method string, args ...interface{}) *transaction.Transaction {
|
|
|
|
tx := e.NewUnsignedTx(t, hash, method, args...)
|
2021-11-03 10:44:46 +00:00
|
|
|
return e.SignTx(t, tx, -1, signers...)
|
2021-10-23 11:28:03 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// SignTx signs a transaction using provided signers.
|
2021-11-03 10:44:46 +00:00
|
|
|
func (e *Executor) SignTx(t *testing.T, tx *transaction.Transaction, sysFee int64, signers ...Signer) *transaction.Transaction {
|
|
|
|
for _, acc := range signers {
|
2021-10-23 11:28:03 +00:00
|
|
|
tx.Signers = append(tx.Signers, transaction.Signer{
|
2021-11-03 10:44:46 +00:00
|
|
|
Account: acc.ScriptHash(),
|
2021-10-23 11:28:03 +00:00
|
|
|
Scopes: transaction.Global,
|
|
|
|
})
|
|
|
|
}
|
2021-11-03 10:44:46 +00:00
|
|
|
addNetworkFee(e.Chain, tx, signers...)
|
|
|
|
addSystemFee(e.Chain, tx, sysFee)
|
2021-10-23 11:28:03 +00:00
|
|
|
|
2021-11-03 10:44:46 +00:00
|
|
|
for _, acc := range signers {
|
|
|
|
require.NoError(t, acc.SignTx(e.Chain.GetConfig().Magic, tx))
|
|
|
|
}
|
2021-10-23 11:28:03 +00:00
|
|
|
return tx
|
|
|
|
}
|
|
|
|
|
2021-12-09 14:58:20 +00:00
|
|
|
// NewAccount returns 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.T, expectedGASBalance ...int64) Signer {
|
2021-10-23 11:28:03 +00:00
|
|
|
acc, err := wallet.NewAccount()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
2021-12-09 14:58:20 +00:00
|
|
|
amount := int64(100_0000_0000)
|
|
|
|
if len(expectedGASBalance) != 0 {
|
|
|
|
amount = expectedGASBalance[0]
|
|
|
|
}
|
|
|
|
tx := e.NewTx(t, []Signer{e.Validator},
|
2021-10-23 11:28:03 +00:00
|
|
|
e.NativeHash(t, nativenames.Gas), "transfer",
|
2021-12-09 14:58:20 +00:00
|
|
|
e.Validator.ScriptHash(), acc.Contract.ScriptHash(), amount, nil)
|
2021-10-23 11:28:03 +00:00
|
|
|
e.AddNewBlock(t, tx)
|
|
|
|
e.CheckHalt(t, tx.Hash())
|
2021-11-03 10:44:46 +00:00
|
|
|
return NewSingleSigner(acc)
|
2021-10-23 11:28:03 +00:00
|
|
|
}
|
|
|
|
|
2021-11-16 14:35:39 +00:00
|
|
|
// DeployContract compiles and deploys contract to bc. It also checks that
|
|
|
|
// precalculated contract hash matches the actual one.
|
2021-10-23 11:28:03 +00:00
|
|
|
// data is an optional argument to `_deploy`.
|
|
|
|
// Returns hash of the deploy transaction.
|
|
|
|
func (e *Executor) DeployContract(t *testing.T, c *Contract, data interface{}) util.Uint256 {
|
|
|
|
tx := e.NewDeployTx(t, e.Chain, c, data)
|
|
|
|
e.AddNewBlock(t, tx)
|
|
|
|
e.CheckHalt(t, tx.Hash())
|
2021-11-16 14:35:39 +00:00
|
|
|
|
|
|
|
// Check that 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()),
|
|
|
|
}),
|
|
|
|
})
|
|
|
|
|
2021-10-23 11:28:03 +00:00
|
|
|
return tx.Hash()
|
|
|
|
}
|
|
|
|
|
2021-11-18 09:23:22 +00:00
|
|
|
// DeployContractCheckFAULT compiles and deploys contract to bc. It checks that deploy
|
|
|
|
// transaction FAULTed with the specified error.
|
|
|
|
func (e *Executor) DeployContractCheckFAULT(t *testing.T, c *Contract, data interface{}, errMessage string) {
|
|
|
|
tx := e.NewDeployTx(t, e.Chain, c, data)
|
|
|
|
e.AddNewBlock(t, tx)
|
|
|
|
e.CheckFault(t, tx.Hash(), errMessage)
|
|
|
|
}
|
|
|
|
|
2021-11-18 09:14:53 +00:00
|
|
|
// InvokeScript adds transaction with the specified script to the chain and
|
|
|
|
// returns its hash. It does no faults check.
|
|
|
|
func (e *Executor) InvokeScript(t *testing.T, script []byte, signers []Signer) util.Uint256 {
|
|
|
|
tx := transaction.New(script, 0)
|
|
|
|
tx.Nonce = Nonce()
|
|
|
|
tx.ValidUntilBlock = e.Chain.BlockHeight() + 1
|
|
|
|
e.SignTx(t, tx, -1, signers...)
|
|
|
|
e.AddNewBlock(t, tx)
|
|
|
|
return tx.Hash()
|
|
|
|
}
|
|
|
|
|
|
|
|
// InvokeScriptCheckHALT adds transaction with the specified script to the chain
|
|
|
|
// and checks it's HALTed with the specified items on stack.
|
|
|
|
func (e *Executor) InvokeScriptCheckHALT(t *testing.T, script []byte, signers []Signer, stack ...stackitem.Item) {
|
|
|
|
hash := e.InvokeScript(t, script, signers)
|
|
|
|
e.CheckHalt(t, hash, stack...)
|
|
|
|
}
|
|
|
|
|
|
|
|
// InvokeScriptCheckFAULT adds transaction with the specified script to the
|
|
|
|
// chain and checks it's FAULTed with the specified error.
|
|
|
|
func (e *Executor) InvokeScriptCheckFAULT(t *testing.T, script []byte, signers []Signer, errMessage string) {
|
|
|
|
hash := e.InvokeScript(t, script, signers)
|
|
|
|
e.CheckFault(t, hash, errMessage)
|
|
|
|
}
|
|
|
|
|
2021-10-23 11:28:03 +00:00
|
|
|
// CheckHalt checks that transaction persisted with HALT state.
|
|
|
|
func (e *Executor) CheckHalt(t *testing.T, h util.Uint256, stack ...stackitem.Item) *state.AppExecResult {
|
|
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, vm.HaltState, aer[0].VMState, aer[0].FaultException)
|
|
|
|
if len(stack) != 0 {
|
|
|
|
require.Equal(t, stack, aer[0].Stack)
|
|
|
|
}
|
|
|
|
return &aer[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
// CheckFault checks that transaction persisted with FAULT state.
|
|
|
|
// Raised exception is also checked to contain s as a substring.
|
|
|
|
func (e *Executor) CheckFault(t *testing.T, h util.Uint256, s string) {
|
|
|
|
aer, err := e.Chain.GetAppExecResults(h, trigger.Application)
|
|
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, vm.FaultState, aer[0].VMState)
|
|
|
|
require.True(t, strings.Contains(aer[0].FaultException, s),
|
|
|
|
"expected: %s, got: %s", s, aer[0].FaultException)
|
|
|
|
}
|
|
|
|
|
2021-11-16 14:40:01 +00:00
|
|
|
// CheckTxNotificationEvent checks that specified event was emitted at the specified position
|
|
|
|
// during transaction script execution. Negative index corresponds to backwards enumeration.
|
|
|
|
func (e *Executor) CheckTxNotificationEvent(t *testing.T, 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])
|
|
|
|
}
|
|
|
|
|
2021-12-03 15:45:55 +00:00
|
|
|
// 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()))
|
|
|
|
}
|
|
|
|
|
2021-10-23 11:28:03 +00:00
|
|
|
// 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)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
neb, err := c.NEF.Bytes()
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
buf := io.NewBufBinWriter()
|
|
|
|
emit.AppCall(buf.BinWriter, bc.ManagementContractHash(), "deploy", callflag.All, neb, rawManifest, data)
|
|
|
|
require.NoError(t, buf.Err)
|
|
|
|
|
|
|
|
tx := transaction.New(buf.Bytes(), 100*native.GASFactor)
|
2021-11-11 17:00:40 +00:00
|
|
|
tx.Nonce = Nonce()
|
2021-10-23 11:28:03 +00:00
|
|
|
tx.ValidUntilBlock = bc.BlockHeight() + 1
|
|
|
|
tx.Signers = []transaction.Signer{{
|
2021-12-09 14:58:20 +00:00
|
|
|
Account: e.Validator.ScriptHash(),
|
2021-10-23 11:28:03 +00:00
|
|
|
Scopes: transaction.Global,
|
|
|
|
}}
|
2021-12-09 14:58:20 +00:00
|
|
|
addNetworkFee(bc, tx, e.Validator)
|
|
|
|
require.NoError(t, e.Validator.SignTx(netmode.UnitTestNet, tx))
|
2021-10-23 11:28:03 +00:00
|
|
|
return tx
|
|
|
|
}
|
|
|
|
|
|
|
|
func addSystemFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, sysFee int64) {
|
|
|
|
if sysFee >= 0 {
|
|
|
|
tx.SystemFee = sysFee
|
|
|
|
return
|
|
|
|
}
|
|
|
|
v, _ := TestInvoke(bc, tx) // ignore error to support failing transactions
|
|
|
|
tx.SystemFee = v.GasConsumed()
|
|
|
|
}
|
|
|
|
|
2021-11-03 10:44:46 +00:00
|
|
|
func addNetworkFee(bc blockchainer.Blockchainer, tx *transaction.Transaction, signers ...Signer) {
|
2022-01-11 21:58:03 +00:00
|
|
|
baseFee := bc.GetBaseExecFee()
|
2021-10-23 11:28:03 +00:00
|
|
|
size := io.GetVarSize(tx)
|
2021-11-03 10:44:46 +00:00
|
|
|
for _, sgr := range signers {
|
|
|
|
netFee, sizeDelta := fee.Calculate(baseFee, sgr.Script())
|
|
|
|
tx.NetworkFee += netFee
|
|
|
|
size += sizeDelta
|
|
|
|
}
|
2021-10-23 11:28:03 +00:00
|
|
|
tx.NetworkFee += int64(size) * bc.FeePerByte()
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewUnsignedBlock creates new unsigned block from txs.
|
|
|
|
func (e *Executor) NewUnsignedBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block {
|
|
|
|
lastBlock := e.TopBlock(t)
|
|
|
|
b := &block.Block{
|
|
|
|
Header: block.Header{
|
2021-11-03 12:22:08 +00:00
|
|
|
NextConsensus: e.Validator.ScriptHash(),
|
2021-10-23 11:28:03 +00:00
|
|
|
Script: transaction.Witness{
|
2021-11-03 12:22:08 +00:00
|
|
|
VerificationScript: e.Validator.Script(),
|
2021-10-23 11:28:03 +00:00
|
|
|
},
|
|
|
|
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 provided transactions and adds it on bc.
|
|
|
|
func (e *Executor) AddNewBlock(t *testing.T, txs ...*transaction.Transaction) *block.Block {
|
|
|
|
b := e.NewUnsignedBlock(t, txs...)
|
|
|
|
e.SignBlock(b)
|
|
|
|
require.NoError(t, e.Chain.AddBlock(b))
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
2021-12-03 15:45:55 +00:00
|
|
|
// 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-10-23 11:28:03 +00:00
|
|
|
// SignBlock add validators signature to b.
|
|
|
|
func (e *Executor) SignBlock(b *block.Block) *block.Block {
|
2021-11-03 12:22:08 +00:00
|
|
|
invoc := e.Validator.SignHashable(uint32(e.Chain.GetConfig().Magic), b)
|
2021-11-03 10:44:46 +00:00
|
|
|
b.Script.InvocationScript = invoc
|
2021-10-23 11:28:03 +00:00
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
2021-10-23 12:36:26 +00:00
|
|
|
// AddBlockCheckHalt is a convenient wrapper over AddBlock and CheckHalt.
|
|
|
|
func (e *Executor) AddBlockCheckHalt(t *testing.T, txs ...*transaction.Transaction) *block.Block {
|
2021-10-23 11:28:03 +00:00
|
|
|
b := e.AddNewBlock(t, txs...)
|
|
|
|
for _, tx := range txs {
|
|
|
|
e.CheckHalt(t, tx.Hash())
|
|
|
|
}
|
|
|
|
return b
|
|
|
|
}
|
|
|
|
|
|
|
|
// TestInvoke creates a test VM with dummy block and executes transaction in it.
|
|
|
|
func TestInvoke(bc blockchainer.Blockchainer, tx *transaction.Transaction) (*vm.VM, error) {
|
|
|
|
lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
b := &block.Block{
|
|
|
|
Header: block.Header{
|
|
|
|
Index: bc.BlockHeight() + 1,
|
|
|
|
Timestamp: lastBlock.Timestamp + 1,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
// `GetTestVM` as well as `Run` can use transaction hash which will set cached value.
|
|
|
|
// This is unwanted behaviour so we explicitly copy transaction to perform execution.
|
|
|
|
ttx := *tx
|
2022-01-12 22:20:08 +00:00
|
|
|
ic := bc.GetTestVM(trigger.Application, &ttx, b)
|
|
|
|
defer ic.Finalize()
|
2021-10-23 11:28:03 +00:00
|
|
|
|
2022-01-12 22:20:08 +00:00
|
|
|
ic.VM.LoadWithFlags(tx.Script, callflag.All)
|
|
|
|
err = ic.VM.Run()
|
|
|
|
return ic.VM, err
|
2021-10-23 11:28:03 +00:00
|
|
|
}
|
2021-12-03 15:45:55 +00:00
|
|
|
|
|
|
|
// 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]
|
|
|
|
}
|