neoneo-go/internal/testcli/executor.go
Ekaterina Pavlova 0ffa24932b cli: add await flag for operations with transactions
New --await flag is an option to synchronize on transaction execution
for CLI commands.

Closes #3244

Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
2023-12-29 15:14:16 +03:00

370 lines
10 KiB
Go

/*
Package testcli contains auxiliary code to test CLI commands.
All testdata assets for it are contained in the cli directory and paths here
use `../` prefix to reference them because the package itself is used from
cli/* subpackages.
*/
package testcli
import (
"bytes"
"errors"
"fmt"
"io"
"math"
"path/filepath"
"strings"
"sync"
"testing"
"time"
"github.com/nspcc-dev/neo-go/cli/app"
"github.com/nspcc-dev/neo-go/cli/input"
"github.com/nspcc-dev/neo-go/pkg/config"
"github.com/nspcc-dev/neo-go/pkg/consensus"
"github.com/nspcc-dev/neo-go/pkg/core"
"github.com/nspcc-dev/neo-go/pkg/core/storage"
"github.com/nspcc-dev/neo-go/pkg/core/transaction"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/nspcc-dev/neo-go/pkg/encoding/address"
"github.com/nspcc-dev/neo-go/pkg/network"
"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv"
"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/vmstate"
"github.com/stretchr/testify/require"
"github.com/urfave/cli"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
"golang.org/x/term"
)
const (
ValidatorWIF = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
ValidatorAddr = "NfgHwwTi3wHAS8aFAN243C5vGbkYDpqLHP"
MultisigAddr = "NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq"
TestWalletPath = "../testdata/testwallet.json"
TestWalletAccount = "Nfyz4KcsgYepRJw1W5C2uKCi6QWKf7v6gG"
ValidatorWallet = "../testdata/wallet1_solo.json"
ValidatorPass = "one"
)
var (
ValidatorHash, _ = address.StringToUint160(ValidatorAddr)
ValidatorPriv, _ = keys.NewPrivateKeyFromWIF(ValidatorWIF)
)
// Executor represents context for a test instance.
// It can be safely used in multiple tests, but not in parallel.
type Executor struct {
// CLI is a cli application to test.
CLI *cli.App
// Chain is a blockchain instance (can be empty).
Chain *core.Blockchain
// RPC is an RPC server to query (can be empty).
RPC *rpcsrv.Server
// NetSrv is a network server (can be empty).
NetSrv *network.Server
// Out contains command output.
Out *ConcurrentBuffer
// Err contains command errors.
Err *bytes.Buffer
// In contains command input.
In *bytes.Buffer
}
// ConcurrentBuffer is a wrapper over Buffer with mutex.
type ConcurrentBuffer struct {
lock sync.RWMutex
buf *bytes.Buffer
}
// NewConcurrentBuffer returns new ConcurrentBuffer with underlying buffer initialized.
func NewConcurrentBuffer() *ConcurrentBuffer {
return &ConcurrentBuffer{
buf: bytes.NewBuffer(nil),
}
}
// Write is a concurrent wrapper over the corresponding method of bytes.Buffer.
func (w *ConcurrentBuffer) Write(p []byte) (int, error) {
w.lock.Lock()
defer w.lock.Unlock()
return w.buf.Write(p)
}
// ReadString is a concurrent wrapper over the corresponding method of bytes.Buffer.
func (w *ConcurrentBuffer) ReadString(delim byte) (string, error) {
w.lock.RLock()
defer w.lock.RUnlock()
return w.buf.ReadString(delim)
}
// Bytes is a concurrent wrapper over the corresponding method of bytes.Buffer.
func (w *ConcurrentBuffer) Bytes() []byte {
w.lock.RLock()
defer w.lock.RUnlock()
return w.buf.Bytes()
}
// String is a concurrent wrapper over the corresponding method of bytes.Buffer.
func (w *ConcurrentBuffer) String() string {
w.lock.RLock()
defer w.lock.RUnlock()
return w.buf.String()
}
// Reset is a concurrent wrapper over the corresponding method of bytes.Buffer.
func (w *ConcurrentBuffer) Reset() {
w.lock.Lock()
defer w.lock.Unlock()
w.buf.Reset()
}
func NewTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockchain, *rpcsrv.Server, *network.Server) {
configPath := "../../config/protocol.unit_testnet.single.yml"
cfg, err := config.LoadFile(configPath)
require.NoError(t, err, "could not load config")
if f != nil {
f(&cfg)
}
memoryStore := storage.NewMemoryStore()
logger := zaptest.NewLogger(t)
chain, err := core.NewBlockchain(memoryStore, cfg.Blockchain(), logger)
require.NoError(t, err, "could not create chain")
if run {
go chain.Run()
}
serverConfig, err := network.NewServerConfig(cfg)
require.NoError(t, err)
serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test")
netSrv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), zap.NewNop())
require.NoError(t, err)
cons, err := consensus.NewService(consensus.Config{
Logger: zap.NewNop(),
Broadcast: netSrv.BroadcastExtensible,
Chain: chain,
BlockQueue: netSrv.GetBlockQueue(),
ProtocolConfiguration: cfg.ProtocolConfiguration,
RequestTx: netSrv.RequestTx,
StopTxFlow: netSrv.StopTxFlow,
Wallet: cfg.ApplicationConfiguration.Consensus.UnlockWallet,
TimePerBlock: serverConfig.TimePerBlock,
})
require.NoError(t, err)
netSrv.AddConsensusService(cons, cons.OnPayload, cons.OnTransaction)
go netSrv.Start()
errCh := make(chan error, 2)
rpcServer := rpcsrv.New(chain, cfg.ApplicationConfiguration.RPC, netSrv, nil, logger, errCh)
rpcServer.Start()
return chain, &rpcServer, netSrv
}
func NewExecutor(t *testing.T, needChain bool) *Executor {
return NewExecutorWithConfig(t, needChain, true, nil)
}
func NewExecutorSuspended(t *testing.T) *Executor {
return NewExecutorWithConfig(t, true, false, nil)
}
func NewExecutorWithConfig(t *testing.T, needChain, runChain bool, f func(*config.Config)) *Executor {
e := &Executor{
CLI: app.New(),
Out: NewConcurrentBuffer(),
Err: bytes.NewBuffer(nil),
In: bytes.NewBuffer(nil),
}
e.CLI.Writer = e.Out
e.CLI.ErrWriter = e.Err
if needChain {
e.Chain, e.RPC, e.NetSrv = NewTestChain(t, f, runChain)
}
t.Cleanup(func() {
e.Close(t)
})
return e
}
func (e *Executor) Close(t *testing.T) {
input.Terminal = nil
if e.RPC != nil {
e.RPC.Shutdown()
}
if e.NetSrv != nil {
e.NetSrv.Shutdown()
}
if e.Chain != nil {
e.Chain.Close()
}
}
// GetTransaction returns tx with hash h after it has persisted.
// If it is in mempool, we can just wait for the next block, otherwise
// it must be already in chain. 1 second is time per block in a unittest chain.
func (e *Executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) {
var tx *transaction.Transaction
var height uint32
require.Eventually(t, func() bool {
var err error
tx, height, err = e.Chain.GetTransaction(h)
return err == nil && height != math.MaxUint32
}, time.Second*2, time.Millisecond*100, "too long time waiting for block")
return tx, height
}
func (e *Executor) GetNextLine(t *testing.T) string {
line, err := e.Out.ReadString('\n')
require.NoError(t, err)
return strings.TrimSuffix(line, "\n")
}
func (e *Executor) CheckNextLine(t *testing.T, expected string) {
line := e.GetNextLine(t)
e.CheckLine(t, line, expected)
}
func (e *Executor) CheckLine(t *testing.T, line, expected string) {
require.Regexp(t, expected, line)
}
func (e *Executor) CheckEOF(t *testing.T) {
_, err := e.Out.ReadString('\n')
require.True(t, errors.Is(err, io.EOF))
}
func setExitFunc() <-chan int {
ch := make(chan int, 1)
cli.OsExiter = func(code int) {
ch <- code
}
return ch
}
func checkExit(t *testing.T, ch <-chan int, code int) {
select {
case c := <-ch:
require.Equal(t, code, c)
default:
if code != 0 {
require.Fail(t, "no exit was called")
}
}
}
// RunWithError runs command and checks that is exits with error.
func (e *Executor) RunWithError(t *testing.T, args ...string) {
ch := setExitFunc()
require.Error(t, e.run(args...))
checkExit(t, ch, 1)
}
// Run runs command and checks that there were no errors.
func (e *Executor) Run(t *testing.T, args ...string) {
ch := setExitFunc()
require.NoError(t, e.run(args...))
checkExit(t, ch, 0)
}
func (e *Executor) run(args ...string) error {
e.Out.Reset()
e.Err.Reset()
input.Terminal = term.NewTerminal(input.ReadWriter{
Reader: e.In,
Writer: io.Discard,
}, "")
err := e.CLI.Run(args)
input.Terminal = nil
e.In.Reset()
return err
}
func (e *Executor) CheckTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) {
line, err := e.Out.ReadString('\n')
require.NoError(t, err)
line = strings.TrimSpace(line)
if len(prefix) > 0 {
line = strings.TrimPrefix(line, prefix[0])
}
h, err := util.Uint256DecodeStringLE(line)
require.NoError(t, err, "can't decode tx hash: %s", line)
tx, height := e.GetTransaction(t, h)
aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
require.NoError(t, err)
require.Equal(t, 1, len(aer))
require.Equal(t, vmstate.Halt, aer[0].VMState)
return tx, height
}
func (e *Executor) CheckAwaitableTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) {
tx, vub := e.CheckTxPersisted(t, prefix...)
e.CheckNextLine(t, "OnChain:\ttrue")
e.CheckNextLine(t, "VMState:\tHALT")
return tx, vub
}
func GenerateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) {
privs := make([]*keys.PrivateKey, n)
pubs := make(keys.PublicKeys, n)
for i := range privs {
var err error
privs[i], err = keys.NewPrivateKey()
require.NoError(t, err)
pubs[i] = privs[i].PublicKey()
}
return privs, pubs
}
func (e *Executor) CheckTxTestInvokeOutput(t *testing.T, scriptSize int) {
e.CheckNextLine(t, `Hash:\s+`)
e.CheckNextLine(t, `OnChain:\s+false`)
e.CheckNextLine(t, `ValidUntil:\s+\d+`)
e.CheckNextLine(t, `Signer:\s+\w+`)
e.CheckNextLine(t, `SystemFee:\s+(\d|\.)+`)
e.CheckNextLine(t, `NetworkFee:\s+(\d|\.)+`)
e.CheckNextLine(t, `Script:\s+\w+`)
e.CheckScriptDump(t, scriptSize)
}
func (e *Executor) CheckScriptDump(t *testing.T, scriptSize int) {
e.CheckNextLine(t, `INDEX\s+`)
for i := 0; i < scriptSize; i++ {
e.CheckNextLine(t, `\d+\s+\w+`)
}
}
func DeployContract(t *testing.T, e *Executor, inPath, configPath, wallet, address, pass string) util.Uint160 {
tmpDir := t.TempDir()
nefName := filepath.Join(tmpDir, "contract.nef")
manifestName := filepath.Join(tmpDir, "contract.manifest.json")
e.Run(t, "neo-go", "contract", "compile",
"--in", inPath,
"--config", configPath,
"--out", nefName, "--manifest", manifestName)
e.In.WriteString(pass + "\r")
e.Run(t, "neo-go", "contract", "deploy",
"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
"--wallet", wallet, "--address", address,
"--force",
"--in", nefName, "--manifest", manifestName)
e.CheckTxPersisted(t, "Sent invocation transaction ")
line, err := e.Out.ReadString('\n')
require.NoError(t, err)
line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: "))
h, err := util.Uint160DecodeStringLE(line)
require.NoError(t, err)
return h
}