/* 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/v2" "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" TestWalletMultiPath = "../testdata/testwallet_multi.json" TestWalletMultiAccount1 = "NgHcPxgEKZQV4QBedzyASJrgiANhJqBVLw" TestWalletMultiAccount2 = "NLvHRfKAifjio2z9HiwLo9ZnpRPHUbAHgH" TestWalletMultiAccount3 = "NcDfG8foJx79XSihcDDrx1df7cHAoJBfXj" ValidatorWallet = "../testdata/wallet1_solo.json" ValidatorPass = "one" ) var ( ValidatorHash, _ = address.StringToUint160(ValidatorAddr) ValidatorPriv, _ = keys.NewPrivateKeyFromWIF(ValidatorWIF) TestWalletMultiAccount1Hash, _ = address.StringToUint160(TestWalletMultiAccount1) TestWalletMultiAccount2Hash, _ = address.StringToUint160(TestWalletMultiAccount2) TestWalletMultiAccount3Hash, _ = address.StringToUint160(TestWalletMultiAccount3) ) // 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) 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 and exit code 1. func (e *Executor) RunWithError(t *testing.T, args ...string) { ch := setExitFunc() require.Error(t, e.run(args...)) checkExit(t, ch, 1) } // RunWithErrorCheckExit runs command and checks that is exits with error and exit code 1. func (e *Executor) RunWithErrorCheckExit(t *testing.T, msg string, args ...string) { ch := setExitFunc() require.ErrorContains(t, e.run(args...), msg) checkExit(t, ch, 1) } // RunWithErrorCheck runs command and checks that there were errors with the specified message. Exit code is not checked. func (e *Executor) RunWithErrorCheck(t *testing.T, msg string, args ...string) { err := e.run(args...) require.ErrorContains(t, err, msg) } // 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) } // RunUnchecked runs command and ensures that proper exit code is set (0 if no error is returned, 1 is an error is returned). // The resulting error is returned (if so). func (e *Executor) RunUnchecked(t *testing.T, args ...string) error { ch := setExitFunc() err := e.run(args...) if err != nil { checkExit(t, ch, 1) } else { checkExit(t, ch, 0) } return err } 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 range scriptSize { 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 }