import logging
import optparse
import os
import sys
import pdb
import shutil
import tempfile
import traceback
from os.path import basename
import string
from sys import argv
from typing import List, Optional
from test_framework.test_node import TestNode
from .util import (
initialize_chain_clean,
start_node,
start_nodes,
connect_nodes_bi,
stop_node,
sync_blocks,
sync_mempools,
stop_nodes,
wait_bitcoind_exit,
check_json_precision,
UtilOptions,
)
from .authproxy import JSONRPCException
class BitcoinTestFramework:
drop_to_pdb = os.getenv("DROP_TO_PDB", "")
setup_clean_chain = True
num_nodes = 1
extra_args: Optional[List[List[str]]] = None
nodes: List[TestNode] = []
options = None
args = None
def __init__(self):
super().__init__()
self.set_test_params()
def set_test_params(self):
pass
async def run_test(self):
raise NotImplementedError("Test needs to override this method")
def add_options(self, parser):
pass
async def setup_chain(self):
logging.info("Initializing test directory %s", self.options.tmpdir)
assert self.setup_clean_chain
await initialize_chain_clean(
self.options.tmpdir,
self.num_nodes,
)
async def setup_nodes(self):
return await start_nodes(
self.num_nodes, self.options.tmpdir, extra_args=self.extra_args
)
async def setup_network(self):
self.nodes = await self.setup_nodes()
if self.num_nodes == 1:
return
if self.num_nodes == 2:
await connect_nodes_bi(self.nodes, 0, 1)
await self.sync_all_nodes()
return
if self.num_nodes != 4:
raise Exception(f"Default setup_network for {self.num_nodes} nodes NYI")
await connect_nodes_bi(self.nodes, 0, 1)
await connect_nodes_bi(self.nodes, 1, 2)
await connect_nodes_bi(self.nodes, 2, 3)
await self.sync_all_nodes()
def stop_node(self, i: int):
node = self.nodes[i]
stop_node(node)
wait_bitcoind_exit(node, 60)
def stop_nodes(self):
return stop_nodes(self.nodes)
def start_node(self, i, extra_args=None):
return start_node(i, self.options.tmpdir, extra_args)
async def restart_node(self, i, extra_args=None):
self.stop_node(i)
self.nodes[i] = await self.start_node(i, extra_args)
async def sync_all_nodes(self):
await sync_blocks(self.nodes)
await sync_mempools(self.nodes)
async def sync_blocks(self):
await sync_blocks(self.nodes)
async def main(self):
parser = optparse.OptionParser(usage="%prog [options]")
parser.add_option(
"--nocleanup",
dest="nocleanup",
default=False,
action="store_true",
help="Leave nexads and test.* datadir on exit or error",
)
parser.add_option(
"--noshutdown",
dest="noshutdown",
default=False,
action="store_true",
help="Don't stop nexads after the test execution",
)
testname = "".join(
filter(lambda x: x in string.ascii_lowercase, basename(argv[0]))
)
default_tempdir = tempfile.mkdtemp(prefix="test_" + testname + "_")
parser.add_option(
"--tmppfx",
dest="tmppfx",
default=None,
help="Directory custom prefix for data directories, if specified, overrides tmpdir",
)
parser.add_option(
"--tmpdir",
dest="tmpdir",
default=default_tempdir,
help="Root directory for data directories.",
)
parser.add_option(
"--tracerpc",
dest="trace_rpc",
default=False,
action="store_true",
help="Print out all RPC calls as they are made",
)
parser.add_option(
"--no-ipv6-rpc-listen",
dest="no_ipv6_rpc_listen",
default=False,
action="store_true",
help="Switch off listening on the IPv6 ::1 localhost RPC port. "
"This is meant to deal with travis which is currently not supporting IPv6 sockets.",
)
parser.add_option(
"--gitlab",
dest="gitlab",
default=False,
action="store_true",
help="Changes root directory for gitlab artifact exporting. overrides tmpdir and tmppfx",
)
self.add_options(parser)
self.options, self.args = parser.parse_args()
if self.options.gitlab is True:
basedir = os.path.normpath(
os.path.dirname(os.path.realpath(__file__)) + "/../../qa_tests"
)
if not os.path.exists(basedir):
try:
os.mkdir(path=basedir, mode=0o700)
except FileExistsError:
pass
self.options.tmpdir = tempfile.mkdtemp(
prefix="test_" + testname + "_", dir=basedir
)
UtilOptions.no_ipv6_rpc_listen = self.options.no_ipv6_rpc_listen
if self.options.trace_rpc:
logging.basicConfig(level=logging.DEBUG, stream=sys.stdout)
check_json_precision()
success = False
try:
try:
os.makedirs(self.options.tmpdir, exist_ok=False)
except FileExistsError:
assert self.options.tmpdir.count(os.sep) >= 2
for n in range(
0, 8
): d = self.options.tmpdir + os.sep + f"node{n}"
try:
shutil.rmtree(d)
except FileNotFoundError:
pass
await self.setup_chain()
await self.setup_network()
await self.run_test()
success = True
except JSONRPCException as e:
logging.error("JSONRPC error: %s", e.error["message"])
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
if self.drop_to_pdb:
pdb.post_mortem(tb)
except AssertionError as e:
logging.error("Assertion failed: %s", str(e))
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
if self.drop_to_pdb:
pdb.post_mortem(tb)
except KeyError as e:
logging.error("key not found: %s", str(e))
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
if self.drop_to_pdb:
pdb.post_mortem(tb)
except Exception as e:
logging.error("Unexpected exception caught during testing: %s", repr(e))
_, _, tb = sys.exc_info()
traceback.print_tb(tb)
if self.drop_to_pdb:
pdb.post_mortem(tb)
except KeyboardInterrupt as e:
logging.error("Exiting after %s", repr(e))
if not self.options.noshutdown:
logging.info("Stopping nodes")
if hasattr(self, "nodes"): stop_nodes(self.nodes)
else:
logging.warning("Note: nexads were not stopped and may still be running")
if not self.options.nocleanup and not self.options.noshutdown and success:
logging.info("Cleaning up")
shutil.rmtree(self.options.tmpdir)
else:
logging.info("Not cleaning up dir %s", self.options.tmpdir)
if success:
logging.info("Test successful")
else:
raise Exception("Test failed")