Browse Source
789733891
[tests] Introduce TestNode (John Newbery)
Pull request description:
Continues #10082
TestNode is a class responsible for all state related to a bitcoind node
under test. It stores local state, is responsible for tracking the
bitcoind process and delegates unrecognised messages to the RPC
connection.
This commit changes start_nodes and stop_nodes to start and stop the
bitcoind nodes in parallel, making test setup and teardown much faster.
On my vm, this changeset reduces total test_runner runtime for the base set of tests
(including building the cache) from 263s to 195s (a 25% speedup). Note that the time
reported by test_runner does not include time spent building the cache:
*with TestNode*:
```
→ date +"%T" ; ./test_runner.py -q ; date +"%T"
12:48:04
..................................................................................................................................................................................................................................................................................................................................
TEST | STATUS | DURATION
abandonconflict.py | ✓ Passed | 12 s
bip68-112-113-p2p.py | ✓ Passed | 19 s
blockchain.py | ✓ Passed | 8 s
bumpfee.py | ✓ Passed | 13 s
decodescript.py | ✓ Passed | 3 s
disablewallet.py | ✓ Passed | 3 s
disconnect_ban.py | ✓ Passed | 6 s
fundrawtransaction.py | ✓ Passed | 37 s
getchaintips.py | ✓ Passed | 4 s
httpbasics.py | ✓ Passed | 3 s
import-rescan.py | ✓ Passed | 4 s
importmulti.py | ✓ Passed | 6 s
importprunedfunds.py | ✓ Passed | 3 s
invalidblockrequest.py | ✓ Passed | 4 s
invalidtxrequest.py | ✓ Passed | 4 s
keypool.py | ✓ Passed | 7 s
listsinceblock.py | ✓ Passed | 4 s
listtransactions.py | ✓ Passed | 33 s
mempool_limit.py | ✓ Passed | 4 s
mempool_persist.py | ✓ Passed | 15 s
mempool_reorg.py | ✓ Passed | 4 s
mempool_resurrect_test.py | ✓ Passed | 3 s
mempool_spendcoinbase.py | ✓ Passed | 3 s
merkle_blocks.py | ✓ Passed | 3 s
multi_rpc.py | ✓ Passed | 4 s
net.py | ✓ Passed | 3 s
nulldummy.py | ✓ Passed | 3 s
p2p-compactblocks.py | ✓ Passed | 28 s
p2p-fullblocktest.py | ✓ Passed | 126 s
p2p-leaktests.py | ✓ Passed | 8 s
p2p-mempool.py | ✓ Passed | 3 s
p2p-segwit.py | ✓ Passed | 59 s
p2p-versionbits-warning.py | ✓ Passed | 8 s
preciousblock.py | ✓ Passed | 3 s
prioritise_transaction.py | ✓ Passed | 5 s
proxy_test.py | ✓ Passed | 3 s
rawtransactions.py | ✓ Passed | 9 s
receivedby.py | ✓ Passed | 19 s
reindex.py | ✓ Passed | 12 s
rest.py | ✓ Passed | 9 s
rpcnamedargs.py | ✓ Passed | 3 s
segwit.py | ✓ Passed | 7 s
sendheaders.py | ✓ Passed | 24 s
signmessages.py | ✓ Passed | 3 s
signrawtransactions.py | ✓ Passed | 3 s
txn_clone.py | ✓ Passed | 4 s
txn_doublespend.py --mineblock | ✓ Passed | 4 s
uptime.py | ✓ Passed | 3 s
wallet-accounts.py | ✓ Passed | 3 s
wallet-dump.py | ✓ Passed | 7 s
wallet-encryption.py | ✓ Passed | 8 s
wallet-hd.py | ✓ Passed | 15 s
wallet.py | ✓ Passed | 31 s
walletbackup.py | ✓ Passed | 104 s
zapwallettxes.py | ✓ Passed | 9 s
zmq_test.py | ○ Skipped | 0 s
ALL | ✓ Passed | 735 s (accumulated)
Runtime: 189 s
12:51:19
```
*master*:
```
→ date +"%T" ; ./test_runner.py -q ; date +"%T"
12:40:13
..........................................................................................................................................................................................................................................................................................................................................................................................................................................
TEST | STATUS | DURATION
abandonconflict.py | ✓ Passed | 15 s
bip68-112-113-p2p.py | ✓ Passed | 19 s
blockchain.py | ✓ Passed | 8 s
bumpfee.py | ✓ Passed | 20 s
decodescript.py | ✓ Passed | 3 s
disablewallet.py | ✓ Passed | 3 s
disconnect_ban.py | ✓ Passed | 8 s
fundrawtransaction.py | ✓ Passed | 36 s
getchaintips.py | ✓ Passed | 11 s
httpbasics.py | ✓ Passed | 7 s
import-rescan.py | ✓ Passed | 16 s
importmulti.py | ✓ Passed | 10 s
importprunedfunds.py | ✓ Passed | 5 s
invalidblockrequest.py | ✓ Passed | 4 s
invalidtxrequest.py | ✓ Passed | 3 s
keypool.py | ✓ Passed | 7 s
listsinceblock.py | ✓ Passed | 11 s
listtransactions.py | ✓ Passed | 37 s
mempool_limit.py | ✓ Passed | 4 s
mempool_persist.py | ✓ Passed | 23 s
mempool_reorg.py | ✓ Passed | 7 s
mempool_resurrect_test.py | ✓ Passed | 3 s
mempool_spendcoinbase.py | ✓ Passed | 3 s
merkle_blocks.py | ✓ Passed | 10 s
multi_rpc.py | ✓ Passed | 6 s
net.py | ✓ Passed | 6 s
nulldummy.py | ✓ Passed | 3 s
p2p-compactblocks.py | ✓ Passed | 30 s
p2p-fullblocktest.py | ✓ Passed | 126 s
p2p-leaktests.py | ✓ Passed | 8 s
p2p-mempool.py | ✓ Passed | 3 s
p2p-segwit.py | ✓ Passed | 62 s
p2p-versionbits-warning.py | ✓ Passed | 8 s
preciousblock.py | ✓ Passed | 8 s
prioritise_transaction.py | ✓ Passed | 7 s
proxy_test.py | ✓ Passed | 10 s
rawtransactions.py | ✓ Passed | 15 s
receivedby.py | ✓ Passed | 28 s
reindex.py | ✓ Passed | 12 s
rest.py | ✓ Passed | 12 s
rpcnamedargs.py | ✓ Passed | 3 s
segwit.py | ✓ Passed | 12 s
sendheaders.py | ✓ Passed | 26 s
signmessages.py | ✓ Passed | 3 s
signrawtransactions.py | ✓ Passed | 3 s
txn_clone.py | ✓ Passed | 10 s
txn_doublespend.py --mineblock | ✓ Passed | 10 s
uptime.py | ✓ Passed | 3 s
wallet-accounts.py | ✓ Passed | 3 s
wallet-dump.py | ✓ Passed | 6 s
wallet-encryption.py | ✓ Passed | 8 s
wallet-hd.py | ✓ Passed | 18 s
wallet.py | ✓ Passed | 69 s
walletbackup.py | ✓ Passed | 130 s
zapwallettxes.py | ✓ Passed | 15 s
zmq_test.py | ○ Skipped | 0 s
ALL | ✓ Passed | 936 s (accumulated)
Runtime: 242 s
12:44:36
```
Tree-SHA512: 6dfc4c11fd0caf7de6954c93679cf22c3df0acc6f432e616d1151062a61f456faa8ae2fe670b427868af55bb564802df84c8fd76e90b4b338750dbc23f46ad88
0.16
MarcoFalke
7 years ago
12 changed files with 193 additions and 97 deletions
@ -0,0 +1,134 @@
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3 |
||||
# Copyright (c) 2017 The Bitcoin Core developers |
||||
# Distributed under the MIT software license, see the accompanying |
||||
# file COPYING or http://www.opensource.org/licenses/mit-license.php. |
||||
"""Class for bitcoind node under test""" |
||||
|
||||
import errno |
||||
import http.client |
||||
import logging |
||||
import os |
||||
import subprocess |
||||
import time |
||||
|
||||
from .util import ( |
||||
assert_equal, |
||||
get_rpc_proxy, |
||||
rpc_url, |
||||
) |
||||
from .authproxy import JSONRPCException |
||||
|
||||
class TestNode(): |
||||
"""A class for representing a bitcoind node under test. |
||||
|
||||
This class contains: |
||||
|
||||
- state about the node (whether it's running, etc) |
||||
- a Python subprocess.Popen object representing the running process |
||||
- an RPC connection to the node |
||||
|
||||
To make things easier for the test writer, a bit of magic is happening under the covers. |
||||
Any unrecognised messages will be dispatched to the RPC connection.""" |
||||
|
||||
def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mocktime, coverage_dir): |
||||
self.index = i |
||||
self.datadir = os.path.join(dirname, "node" + str(i)) |
||||
self.rpchost = rpchost |
||||
self.rpc_timeout = timewait |
||||
if binary is None: |
||||
self.binary = os.getenv("BITCOIND", "bitcoind") |
||||
else: |
||||
self.binary = binary |
||||
self.stderr = stderr |
||||
self.coverage_dir = coverage_dir |
||||
# Most callers will just need to add extra args to the standard list below. For those callers that need more flexibity, they can just set the args property directly. |
||||
self.extra_args = extra_args |
||||
self.args = [self.binary, "-datadir=" + self.datadir, "-server", "-keypool=1", "-discover=0", "-rest", "-logtimemicros", "-debug", "-debugexclude=libevent", "-debugexclude=leveldb", "-mocktime=" + str(mocktime), "-uacomment=testnode%d" % i] |
||||
|
||||
self.running = False |
||||
self.process = None |
||||
self.rpc_connected = False |
||||
self.rpc = None |
||||
self.url = None |
||||
self.log = logging.getLogger('TestFramework.node%d' % i) |
||||
|
||||
def __getattr__(self, *args, **kwargs): |
||||
"""Dispatches any unrecognised messages to the RPC connection.""" |
||||
assert self.rpc_connected and self.rpc is not None, "Error: no RPC connection" |
||||
return self.rpc.__getattr__(*args, **kwargs) |
||||
|
||||
def start(self): |
||||
"""Start the node.""" |
||||
self.process = subprocess.Popen(self.args + self.extra_args, stderr=self.stderr) |
||||
self.running = True |
||||
self.log.debug("bitcoind started, waiting for RPC to come up") |
||||
|
||||
def wait_for_rpc_connection(self): |
||||
"""Sets up an RPC connection to the bitcoind process. Returns False if unable to connect.""" |
||||
|
||||
# Wait for up to 10 seconds for the RPC server to respond |
||||
for _ in range(40): |
||||
assert not self.process.poll(), "bitcoind exited with status %i during initialization" % self.process.returncode |
||||
try: |
||||
self.rpc = get_rpc_proxy(rpc_url(self.datadir, self.index, self.rpchost), self.index, coveragedir=self.coverage_dir) |
||||
self.rpc.getblockcount() |
||||
# If the call to getblockcount() succeeds then the RPC connection is up |
||||
self.rpc_connected = True |
||||
self.url = self.rpc.url |
||||
self.log.debug("RPC successfully started") |
||||
return |
||||
except IOError as e: |
||||
if e.errno != errno.ECONNREFUSED: # Port not yet open? |
||||
raise # unknown IO error |
||||
except JSONRPCException as e: # Initialization phase |
||||
if e.error['code'] != -28: # RPC in warmup? |
||||
raise # unknown JSON RPC exception |
||||
except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting |
||||
if "No RPC credentials" not in str(e): |
||||
raise |
||||
time.sleep(0.25) |
||||
raise AssertionError("Unable to connect to bitcoind") |
||||
|
||||
def get_wallet_rpc(self, wallet_name): |
||||
assert self.rpc_connected |
||||
assert self.rpc |
||||
wallet_path = "wallet/%s" % wallet_name |
||||
return self.rpc / wallet_path |
||||
|
||||
def stop_node(self): |
||||
"""Stop the node.""" |
||||
if not self.running: |
||||
return |
||||
self.log.debug("Stopping node") |
||||
try: |
||||
self.stop() |
||||
except http.client.CannotSendRequest: |
||||
self.log.exception("Unable to stop node.") |
||||
|
||||
def is_node_stopped(self): |
||||
"""Checks whether the node has stopped. |
||||
|
||||
Returns True if the node has stopped. False otherwise. |
||||
This method is responsible for freeing resources (self.process).""" |
||||
if not self.running: |
||||
return True |
||||
return_code = self.process.poll() |
||||
if return_code is not None: |
||||
# process has stopped. Assert that it didn't return an error code. |
||||
assert_equal(return_code, 0) |
||||
self.running = False |
||||
self.process = None |
||||
self.log.debug("Node stopped") |
||||
return True |
||||
return False |
||||
|
||||
def node_encrypt_wallet(self, passphrase): |
||||
""""Encrypts the wallet. |
||||
|
||||
This causes bitcoind to shutdown, so this method takes |
||||
care of cleaning up resources.""" |
||||
self.encryptwallet(passphrase) |
||||
while not self.is_node_stopped(): |
||||
time.sleep(0.1) |
||||
self.rpc = None |
||||
self.rpc_connected = False |
Loading…
Reference in new issue