0ffa24932b
New --await flag is an option to synchronize on transaction execution for CLI commands. Closes #3244 Signed-off-by: Ekaterina Pavlova <ekt@morphbits.io>
370 lines
10 KiB
Go
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
|
|
}
|