Separates the act of creating a TestNode object from starting the node. The test_framework now keeps track of its list of TestNodes, and test writers can call start_node() and stop_node() without having to update the self.nodes list.
142 lines
5.6 KiB
Python
Executable file
142 lines
5.6 KiB
Python
Executable file
#!/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
|
|
if timewait:
|
|
self.rpc_timeout = timewait
|
|
else:
|
|
# Wait for up to 60 seconds for the RPC server to respond
|
|
self.rpc_timeout = 60
|
|
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, extra_args=None, stderr=None):
|
|
"""Start the node."""
|
|
if extra_args is None:
|
|
extra_args = self.extra_args
|
|
if stderr is None:
|
|
stderr = self.stderr
|
|
self.process = subprocess.Popen(self.args + extra_args, stderr=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."""
|
|
# Poll at a rate of four times per second
|
|
poll_per_s = 4
|
|
for _ in range(poll_per_s * self.rpc_timeout):
|
|
assert self.process.poll() is None, "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, timeout=self.rpc_timeout, 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(1.0 / poll_per_s)
|
|
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
|