neo-go/cli/executor_test.go
Anna Shaleva 9862b40f2c rpc: support InitialGasDistribution response from old Neo-Go nodes
https://github.com/nspcc-dev/neo-go/pull/2435 breaks compatibility
between newer RPC clients and older RPC servers with the following
error:
```
failed to get network magic: json: cannot unmarshal string into Go struct field Protocol.protocol.initialgasdistribution of type int64
```

This behaviour is expected, but we can't allow this radical change.
Thus, the following solution is implemented:
1. RPC server responds with proper non-stringified
   InitialGasDistribution value. The value represents an integral
   of fixed8 multiplied by the decimals.
2. RPC client is able to distinguish older and newer responses. For
   older one the stringified value without decimals part is
   expected. For newer responses the int64 value with decimal part
   is expected.

The cludge will be present in the code for a while until nodes of
version <=0.98.3 become completely absolete.
2022-04-27 19:00:46 +03:00

310 lines
8.3 KiB
Go

package main
import (
"bytes"
"errors"
"fmt"
"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)
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,
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
}