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 }