github.com/nspcc-dev/neo-go@v0.105.2-0.20240517133400-6be757af3eba/internal/testcli/executor.go (about)

     1  /*
     2  Package testcli contains auxiliary code to test CLI commands.
     3  
     4  All testdata assets for it are contained in the cli directory and paths here
     5  use `../` prefix to reference them because the package itself is used from
     6  cli/* subpackages.
     7  */
     8  package testcli
     9  
    10  import (
    11  	"bytes"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"math"
    16  	"path/filepath"
    17  	"strings"
    18  	"sync"
    19  	"testing"
    20  	"time"
    21  
    22  	"github.com/nspcc-dev/neo-go/cli/app"
    23  	"github.com/nspcc-dev/neo-go/cli/input"
    24  	"github.com/nspcc-dev/neo-go/pkg/config"
    25  	"github.com/nspcc-dev/neo-go/pkg/consensus"
    26  	"github.com/nspcc-dev/neo-go/pkg/core"
    27  	"github.com/nspcc-dev/neo-go/pkg/core/storage"
    28  	"github.com/nspcc-dev/neo-go/pkg/core/transaction"
    29  	"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
    30  	"github.com/nspcc-dev/neo-go/pkg/encoding/address"
    31  	"github.com/nspcc-dev/neo-go/pkg/network"
    32  	"github.com/nspcc-dev/neo-go/pkg/services/rpcsrv"
    33  	"github.com/nspcc-dev/neo-go/pkg/smartcontract/trigger"
    34  	"github.com/nspcc-dev/neo-go/pkg/util"
    35  	"github.com/nspcc-dev/neo-go/pkg/vm/vmstate"
    36  	"github.com/stretchr/testify/require"
    37  	"github.com/urfave/cli"
    38  	"go.uber.org/zap"
    39  	"go.uber.org/zap/zaptest"
    40  	"golang.org/x/term"
    41  )
    42  
    43  const (
    44  	ValidatorWIF  = "KxyjQ8eUa4FHt3Gvioyt1Wz29cTUrE4eTqX3yFSk1YFCsPL8uNsY"
    45  	ValidatorAddr = "NfgHwwTi3wHAS8aFAN243C5vGbkYDpqLHP"
    46  	MultisigAddr  = "NVTiAjNgagDkTr5HTzDmQP9kPwPHN5BgVq"
    47  
    48  	TestWalletPath    = "../testdata/testwallet.json"
    49  	TestWalletAccount = "Nfyz4KcsgYepRJw1W5C2uKCi6QWKf7v6gG"
    50  
    51  	TestWalletMultiPath     = "../testdata/testwallet_multi.json"
    52  	TestWalletMultiAccount1 = "NgHcPxgEKZQV4QBedzyASJrgiANhJqBVLw"
    53  	TestWalletMultiAccount2 = "NLvHRfKAifjio2z9HiwLo9ZnpRPHUbAHgH"
    54  	TestWalletMultiAccount3 = "NcDfG8foJx79XSihcDDrx1df7cHAoJBfXj"
    55  
    56  	ValidatorWallet = "../testdata/wallet1_solo.json"
    57  	ValidatorPass   = "one"
    58  )
    59  
    60  var (
    61  	ValidatorHash, _ = address.StringToUint160(ValidatorAddr)
    62  	ValidatorPriv, _ = keys.NewPrivateKeyFromWIF(ValidatorWIF)
    63  
    64  	TestWalletMultiAccount1Hash, _ = address.StringToUint160(TestWalletMultiAccount1)
    65  	TestWalletMultiAccount2Hash, _ = address.StringToUint160(TestWalletMultiAccount2)
    66  	TestWalletMultiAccount3Hash, _ = address.StringToUint160(TestWalletMultiAccount3)
    67  )
    68  
    69  // Executor represents context for a test instance.
    70  // It can be safely used in multiple tests, but not in parallel.
    71  type Executor struct {
    72  	// CLI is a cli application to test.
    73  	CLI *cli.App
    74  	// Chain is a blockchain instance (can be empty).
    75  	Chain *core.Blockchain
    76  	// RPC is an RPC server to query (can be empty).
    77  	RPC *rpcsrv.Server
    78  	// NetSrv is a network server (can be empty).
    79  	NetSrv *network.Server
    80  	// Out contains command output.
    81  	Out *ConcurrentBuffer
    82  	// Err contains command errors.
    83  	Err *bytes.Buffer
    84  	// In contains command input.
    85  	In *bytes.Buffer
    86  }
    87  
    88  // ConcurrentBuffer is a wrapper over Buffer with mutex.
    89  type ConcurrentBuffer struct {
    90  	lock sync.RWMutex
    91  	buf  *bytes.Buffer
    92  }
    93  
    94  // NewConcurrentBuffer returns new ConcurrentBuffer with underlying buffer initialized.
    95  func NewConcurrentBuffer() *ConcurrentBuffer {
    96  	return &ConcurrentBuffer{
    97  		buf: bytes.NewBuffer(nil),
    98  	}
    99  }
   100  
   101  // Write is a concurrent wrapper over the corresponding method of bytes.Buffer.
   102  func (w *ConcurrentBuffer) Write(p []byte) (int, error) {
   103  	w.lock.Lock()
   104  	defer w.lock.Unlock()
   105  
   106  	return w.buf.Write(p)
   107  }
   108  
   109  // ReadString is a concurrent wrapper over the corresponding method of bytes.Buffer.
   110  func (w *ConcurrentBuffer) ReadString(delim byte) (string, error) {
   111  	w.lock.RLock()
   112  	defer w.lock.RUnlock()
   113  
   114  	return w.buf.ReadString(delim)
   115  }
   116  
   117  // Bytes is a concurrent wrapper over the corresponding method of bytes.Buffer.
   118  func (w *ConcurrentBuffer) Bytes() []byte {
   119  	w.lock.RLock()
   120  	defer w.lock.RUnlock()
   121  
   122  	return w.buf.Bytes()
   123  }
   124  
   125  // String is a concurrent wrapper over the corresponding method of bytes.Buffer.
   126  func (w *ConcurrentBuffer) String() string {
   127  	w.lock.RLock()
   128  	defer w.lock.RUnlock()
   129  
   130  	return w.buf.String()
   131  }
   132  
   133  // Reset is a concurrent wrapper over the corresponding method of bytes.Buffer.
   134  func (w *ConcurrentBuffer) Reset() {
   135  	w.lock.Lock()
   136  	defer w.lock.Unlock()
   137  
   138  	w.buf.Reset()
   139  }
   140  
   141  func NewTestChain(t *testing.T, f func(*config.Config), run bool) (*core.Blockchain, *rpcsrv.Server, *network.Server) {
   142  	configPath := "../../config/protocol.unit_testnet.single.yml"
   143  	cfg, err := config.LoadFile(configPath)
   144  	require.NoError(t, err, "could not load config")
   145  	if f != nil {
   146  		f(&cfg)
   147  	}
   148  
   149  	memoryStore := storage.NewMemoryStore()
   150  	logger := zaptest.NewLogger(t)
   151  	chain, err := core.NewBlockchain(memoryStore, cfg.Blockchain(), logger)
   152  	require.NoError(t, err, "could not create chain")
   153  
   154  	if run {
   155  		go chain.Run()
   156  	}
   157  
   158  	serverConfig, err := network.NewServerConfig(cfg)
   159  	require.NoError(t, err)
   160  	serverConfig.UserAgent = fmt.Sprintf(config.UserAgentFormat, "0.98.3-test")
   161  	netSrv, err := network.NewServer(serverConfig, chain, chain.GetStateSyncModule(), zap.NewNop())
   162  	require.NoError(t, err)
   163  	cons, err := consensus.NewService(consensus.Config{
   164  		Logger:                zap.NewNop(),
   165  		Broadcast:             netSrv.BroadcastExtensible,
   166  		Chain:                 chain,
   167  		BlockQueue:            netSrv.GetBlockQueue(),
   168  		ProtocolConfiguration: cfg.ProtocolConfiguration,
   169  		RequestTx:             netSrv.RequestTx,
   170  		StopTxFlow:            netSrv.StopTxFlow,
   171  		Wallet:                cfg.ApplicationConfiguration.Consensus.UnlockWallet,
   172  		TimePerBlock:          serverConfig.TimePerBlock,
   173  	})
   174  	require.NoError(t, err)
   175  	netSrv.AddConsensusService(cons, cons.OnPayload, cons.OnTransaction)
   176  	netSrv.Start()
   177  	errCh := make(chan error, 2)
   178  	rpcServer := rpcsrv.New(chain, cfg.ApplicationConfiguration.RPC, netSrv, nil, logger, errCh)
   179  	rpcServer.Start()
   180  
   181  	return chain, &rpcServer, netSrv
   182  }
   183  
   184  func NewExecutor(t *testing.T, needChain bool) *Executor {
   185  	return NewExecutorWithConfig(t, needChain, true, nil)
   186  }
   187  
   188  func NewExecutorSuspended(t *testing.T) *Executor {
   189  	return NewExecutorWithConfig(t, true, false, nil)
   190  }
   191  
   192  func NewExecutorWithConfig(t *testing.T, needChain, runChain bool, f func(*config.Config)) *Executor {
   193  	e := &Executor{
   194  		CLI: app.New(),
   195  		Out: NewConcurrentBuffer(),
   196  		Err: bytes.NewBuffer(nil),
   197  		In:  bytes.NewBuffer(nil),
   198  	}
   199  	e.CLI.Writer = e.Out
   200  	e.CLI.ErrWriter = e.Err
   201  	if needChain {
   202  		e.Chain, e.RPC, e.NetSrv = NewTestChain(t, f, runChain)
   203  	}
   204  	t.Cleanup(func() {
   205  		e.Close(t)
   206  	})
   207  	return e
   208  }
   209  
   210  func (e *Executor) Close(t *testing.T) {
   211  	input.Terminal = nil
   212  	if e.RPC != nil {
   213  		e.RPC.Shutdown()
   214  	}
   215  	if e.NetSrv != nil {
   216  		e.NetSrv.Shutdown()
   217  	}
   218  	if e.Chain != nil {
   219  		e.Chain.Close()
   220  	}
   221  }
   222  
   223  // GetTransaction returns tx with hash h after it has persisted.
   224  // If it is in mempool, we can just wait for the next block, otherwise
   225  // it must be already in chain. 1 second is time per block in a unittest chain.
   226  func (e *Executor) GetTransaction(t *testing.T, h util.Uint256) (*transaction.Transaction, uint32) {
   227  	var tx *transaction.Transaction
   228  	var height uint32
   229  	require.Eventually(t, func() bool {
   230  		var err error
   231  		tx, height, err = e.Chain.GetTransaction(h)
   232  		return err == nil && height != math.MaxUint32
   233  	}, time.Second*2, time.Millisecond*100, "too long time waiting for block")
   234  	return tx, height
   235  }
   236  
   237  func (e *Executor) GetNextLine(t *testing.T) string {
   238  	line, err := e.Out.ReadString('\n')
   239  	require.NoError(t, err)
   240  	return strings.TrimSuffix(line, "\n")
   241  }
   242  
   243  func (e *Executor) CheckNextLine(t *testing.T, expected string) {
   244  	line := e.GetNextLine(t)
   245  	e.CheckLine(t, line, expected)
   246  }
   247  
   248  func (e *Executor) CheckLine(t *testing.T, line, expected string) {
   249  	require.Regexp(t, expected, line)
   250  }
   251  
   252  func (e *Executor) CheckEOF(t *testing.T) {
   253  	_, err := e.Out.ReadString('\n')
   254  	require.True(t, errors.Is(err, io.EOF))
   255  }
   256  
   257  func setExitFunc() <-chan int {
   258  	ch := make(chan int, 1)
   259  	cli.OsExiter = func(code int) {
   260  		ch <- code
   261  	}
   262  	return ch
   263  }
   264  
   265  func checkExit(t *testing.T, ch <-chan int, code int) {
   266  	select {
   267  	case c := <-ch:
   268  		require.Equal(t, code, c)
   269  	default:
   270  		if code != 0 {
   271  			require.Fail(t, "no exit was called")
   272  		}
   273  	}
   274  }
   275  
   276  // RunWithError runs command and checks that is exits with error.
   277  func (e *Executor) RunWithError(t *testing.T, args ...string) {
   278  	ch := setExitFunc()
   279  	require.Error(t, e.run(args...))
   280  	checkExit(t, ch, 1)
   281  }
   282  
   283  // Run runs command and checks that there were no errors.
   284  func (e *Executor) Run(t *testing.T, args ...string) {
   285  	ch := setExitFunc()
   286  	require.NoError(t, e.run(args...))
   287  	checkExit(t, ch, 0)
   288  }
   289  
   290  // RunUnchecked runs command and ensures that proper exit code is set (0 if no error is returned, 1 is an error is returned).
   291  // The resulting error is returned (if so).
   292  func (e *Executor) RunUnchecked(t *testing.T, args ...string) error {
   293  	ch := setExitFunc()
   294  	err := e.run(args...)
   295  	if err != nil {
   296  		checkExit(t, ch, 1)
   297  	} else {
   298  		checkExit(t, ch, 0)
   299  	}
   300  	return err
   301  }
   302  
   303  func (e *Executor) run(args ...string) error {
   304  	e.Out.Reset()
   305  	e.Err.Reset()
   306  	input.Terminal = term.NewTerminal(input.ReadWriter{
   307  		Reader: e.In,
   308  		Writer: io.Discard,
   309  	}, "")
   310  	err := e.CLI.Run(args)
   311  	input.Terminal = nil
   312  	e.In.Reset()
   313  	return err
   314  }
   315  
   316  func (e *Executor) CheckTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) {
   317  	line, err := e.Out.ReadString('\n')
   318  	require.NoError(t, err)
   319  
   320  	line = strings.TrimSpace(line)
   321  	if len(prefix) > 0 {
   322  		line = strings.TrimPrefix(line, prefix[0])
   323  	}
   324  	h, err := util.Uint256DecodeStringLE(line)
   325  	require.NoError(t, err, "can't decode tx hash: %s", line)
   326  
   327  	tx, height := e.GetTransaction(t, h)
   328  	aer, err := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
   329  	require.NoError(t, err)
   330  	require.Equal(t, 1, len(aer))
   331  	require.Equal(t, vmstate.Halt, aer[0].VMState)
   332  	return tx, height
   333  }
   334  
   335  func (e *Executor) CheckAwaitableTxPersisted(t *testing.T, prefix ...string) (*transaction.Transaction, uint32) {
   336  	tx, vub := e.CheckTxPersisted(t, prefix...)
   337  	e.CheckNextLine(t, "OnChain:\ttrue")
   338  	e.CheckNextLine(t, "VMState:\tHALT")
   339  	return tx, vub
   340  }
   341  
   342  func GenerateKeys(t *testing.T, n int) ([]*keys.PrivateKey, keys.PublicKeys) {
   343  	privs := make([]*keys.PrivateKey, n)
   344  	pubs := make(keys.PublicKeys, n)
   345  	for i := range privs {
   346  		var err error
   347  		privs[i], err = keys.NewPrivateKey()
   348  		require.NoError(t, err)
   349  		pubs[i] = privs[i].PublicKey()
   350  	}
   351  	return privs, pubs
   352  }
   353  
   354  func (e *Executor) CheckTxTestInvokeOutput(t *testing.T, scriptSize int) {
   355  	e.CheckNextLine(t, `Hash:\s+`)
   356  	e.CheckNextLine(t, `OnChain:\s+false`)
   357  	e.CheckNextLine(t, `ValidUntil:\s+\d+`)
   358  	e.CheckNextLine(t, `Signer:\s+\w+`)
   359  	e.CheckNextLine(t, `SystemFee:\s+(\d|\.)+`)
   360  	e.CheckNextLine(t, `NetworkFee:\s+(\d|\.)+`)
   361  	e.CheckNextLine(t, `Script:\s+\w+`)
   362  	e.CheckScriptDump(t, scriptSize)
   363  }
   364  
   365  func (e *Executor) CheckScriptDump(t *testing.T, scriptSize int) {
   366  	e.CheckNextLine(t, `INDEX\s+`)
   367  	for i := 0; i < scriptSize; i++ {
   368  		e.CheckNextLine(t, `\d+\s+\w+`)
   369  	}
   370  }
   371  
   372  func DeployContract(t *testing.T, e *Executor, inPath, configPath, wallet, address, pass string) util.Uint160 {
   373  	tmpDir := t.TempDir()
   374  	nefName := filepath.Join(tmpDir, "contract.nef")
   375  	manifestName := filepath.Join(tmpDir, "contract.manifest.json")
   376  	e.Run(t, "neo-go", "contract", "compile",
   377  		"--in", inPath,
   378  		"--config", configPath,
   379  		"--out", nefName, "--manifest", manifestName)
   380  	e.In.WriteString(pass + "\r")
   381  	e.Run(t, "neo-go", "contract", "deploy",
   382  		"--rpc-endpoint", "http://"+e.RPC.Addresses()[0],
   383  		"--wallet", wallet, "--address", address,
   384  		"--force",
   385  		"--in", nefName, "--manifest", manifestName)
   386  	e.CheckTxPersisted(t, "Sent invocation transaction ")
   387  	line, err := e.Out.ReadString('\n')
   388  	require.NoError(t, err)
   389  	line = strings.TrimSpace(strings.TrimPrefix(line, "Contract: "))
   390  	h, err := util.Uint160DecodeStringLE(line)
   391  	require.NoError(t, err)
   392  	return h
   393  }