a10b1ad32d
With Start() and Shutdown() taking no parameters and returning no values.
308 lines
8.2 KiB
Go
308 lines
8.2 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"io"
|
|
"math"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"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/rpc/server"
|
|
"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/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 *server.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, *server.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.ProtocolConfiguration, logger)
|
|
require.NoError(t, err, "could not create chain")
|
|
|
|
if run {
|
|
go chain.Run()
|
|
}
|
|
|
|
serverConfig := network.NewServerConfig(cfg)
|
|
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,
|
|
ProtocolConfiguration: chain.GetConfig(),
|
|
RequestTx: netSrv.RequestTx,
|
|
Wallet: serverConfig.Wallet,
|
|
TimePerBlock: serverConfig.TimePerBlock,
|
|
})
|
|
require.NoError(t, err)
|
|
netSrv.AddExtensibleHPService(cons, consensus.Category, cons.OnPayload, cons.OnTransaction)
|
|
go netSrv.Start(make(chan error, 1))
|
|
errCh := make(chan error, 2)
|
|
rpcServer := server.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: newApp(),
|
|
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, vm.HaltState, aer[0].VMState)
|
|
return tx, height
|
|
}
|
|
|
|
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
|
|
}
|