qa: Only allow disconnecting all NodeConns
[bitcoinplatinum.git] / test / functional / test_framework / test_node.py
blob73018ee5da79431a66de918ce4c8d434e4fabaff
1 #!/usr/bin/env python3
2 # Copyright (c) 2017 The Bitcoin Core developers
3 # Distributed under the MIT software license, see the accompanying
4 # file COPYING or http://www.opensource.org/licenses/mit-license.php.
5 """Class for bitcoind node under test"""
7 import decimal
8 import errno
9 import http.client
10 import json
11 import logging
12 import os
13 import subprocess
14 import time
16 from .authproxy import JSONRPCException
17 from .mininode import NodeConn
18 from .util import (
19 assert_equal,
20 get_rpc_proxy,
21 rpc_url,
22 wait_until,
23 p2p_port,
26 BITCOIND_PROC_WAIT_TIMEOUT = 60
28 class TestNode():
29 """A class for representing a bitcoind node under test.
31 This class contains:
33 - state about the node (whether it's running, etc)
34 - a Python subprocess.Popen object representing the running process
35 - an RPC connection to the node
36 - one or more P2P connections to the node
39 To make things easier for the test writer, any unrecognised messages will
40 be dispatched to the RPC connection."""
42 def __init__(self, i, dirname, extra_args, rpchost, timewait, binary, stderr, mocktime, coverage_dir):
43 self.index = i
44 self.datadir = os.path.join(dirname, "node" + str(i))
45 self.rpchost = rpchost
46 if timewait:
47 self.rpc_timeout = timewait
48 else:
49 # Wait for up to 60 seconds for the RPC server to respond
50 self.rpc_timeout = 60
51 if binary is None:
52 self.binary = os.getenv("BITCOIND", "bitcoind")
53 else:
54 self.binary = binary
55 self.stderr = stderr
56 self.coverage_dir = coverage_dir
57 # 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.
58 self.extra_args = extra_args
59 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]
61 self.cli = TestNodeCLI(os.getenv("BITCOINCLI", "bitcoin-cli"), self.datadir)
63 self.running = False
64 self.process = None
65 self.rpc_connected = False
66 self.rpc = None
67 self.url = None
68 self.log = logging.getLogger('TestFramework.node%d' % i)
70 self.p2ps = []
72 def __getattr__(self, name):
73 """Dispatches any unrecognised messages to the RPC connection."""
74 assert self.rpc_connected and self.rpc is not None, "Error: no RPC connection"
75 return getattr(self.rpc, name)
77 def start(self, extra_args=None, stderr=None):
78 """Start the node."""
79 if extra_args is None:
80 extra_args = self.extra_args
81 if stderr is None:
82 stderr = self.stderr
83 self.process = subprocess.Popen(self.args + extra_args, stderr=stderr)
84 self.running = True
85 self.log.debug("bitcoind started, waiting for RPC to come up")
87 def wait_for_rpc_connection(self):
88 """Sets up an RPC connection to the bitcoind process. Returns False if unable to connect."""
89 # Poll at a rate of four times per second
90 poll_per_s = 4
91 for _ in range(poll_per_s * self.rpc_timeout):
92 assert self.process.poll() is None, "bitcoind exited with status %i during initialization" % self.process.returncode
93 try:
94 self.rpc = get_rpc_proxy(rpc_url(self.datadir, self.index, self.rpchost), self.index, timeout=self.rpc_timeout, coveragedir=self.coverage_dir)
95 self.rpc.getblockcount()
96 # If the call to getblockcount() succeeds then the RPC connection is up
97 self.rpc_connected = True
98 self.url = self.rpc.url
99 self.log.debug("RPC successfully started")
100 return
101 except IOError as e:
102 if e.errno != errno.ECONNREFUSED: # Port not yet open?
103 raise # unknown IO error
104 except JSONRPCException as e: # Initialization phase
105 if e.error['code'] != -28: # RPC in warmup?
106 raise # unknown JSON RPC exception
107 except ValueError as e: # cookie file not found and no rpcuser or rpcassword. bitcoind still starting
108 if "No RPC credentials" not in str(e):
109 raise
110 time.sleep(1.0 / poll_per_s)
111 raise AssertionError("Unable to connect to bitcoind")
113 def get_wallet_rpc(self, wallet_name):
114 assert self.rpc_connected
115 assert self.rpc
116 wallet_path = "wallet/%s" % wallet_name
117 return self.rpc / wallet_path
119 def stop_node(self):
120 """Stop the node."""
121 if not self.running:
122 return
123 self.log.debug("Stopping node")
124 try:
125 self.stop()
126 except http.client.CannotSendRequest:
127 self.log.exception("Unable to stop node.")
128 del self.p2ps[:]
130 def is_node_stopped(self):
131 """Checks whether the node has stopped.
133 Returns True if the node has stopped. False otherwise.
134 This method is responsible for freeing resources (self.process)."""
135 if not self.running:
136 return True
137 return_code = self.process.poll()
138 if return_code is None:
139 return False
141 # process has stopped. Assert that it didn't return an error code.
142 assert_equal(return_code, 0)
143 self.running = False
144 self.process = None
145 self.rpc_connected = False
146 self.rpc = None
147 self.log.debug("Node stopped")
148 return True
150 def wait_until_stopped(self, timeout=BITCOIND_PROC_WAIT_TIMEOUT):
151 wait_until(self.is_node_stopped, timeout=timeout)
153 def node_encrypt_wallet(self, passphrase):
154 """"Encrypts the wallet.
156 This causes bitcoind to shutdown, so this method takes
157 care of cleaning up resources."""
158 self.encryptwallet(passphrase)
159 self.wait_until_stopped()
161 def add_p2p_connection(self, p2p_conn, **kwargs):
162 """Add a p2p connection to the node.
164 This method adds the p2p connection to the self.p2ps list and also
165 returns the connection to the caller."""
166 if 'dstport' not in kwargs:
167 kwargs['dstport'] = p2p_port(self.index)
168 if 'dstaddr' not in kwargs:
169 kwargs['dstaddr'] = '127.0.0.1'
170 self.p2ps.append(p2p_conn)
171 kwargs.update({'rpc': self.rpc, 'callback': p2p_conn})
172 p2p_conn.add_connection(NodeConn(**kwargs))
174 return p2p_conn
176 @property
177 def p2p(self):
178 """Return the first p2p connection
180 Convenience property - most tests only use a single p2p connection to each
181 node, so this saves having to write node.p2ps[0] many times."""
182 assert self.p2ps, "No p2p connection"
183 return self.p2ps[0]
185 def disconnect_p2ps(self):
186 """Close all p2p connections to the node."""
187 for p in self.p2ps:
188 # Connection could have already been closed by other end.
189 if p.connection is not None:
190 p.connection.disconnect_node()
191 self.p2ps = []
194 class TestNodeCLI():
195 """Interface to bitcoin-cli for an individual node"""
197 def __init__(self, binary, datadir):
198 self.args = []
199 self.binary = binary
200 self.datadir = datadir
201 self.input = None
203 def __call__(self, *args, input=None):
204 # TestNodeCLI is callable with bitcoin-cli command-line args
205 self.args = [str(arg) for arg in args]
206 self.input = input
207 return self
209 def __getattr__(self, command):
210 def dispatcher(*args, **kwargs):
211 return self.send_cli(command, *args, **kwargs)
212 return dispatcher
214 def send_cli(self, command, *args, **kwargs):
215 """Run bitcoin-cli command. Deserializes returned string as python object."""
217 pos_args = [str(arg) for arg in args]
218 named_args = [str(key) + "=" + str(value) for (key, value) in kwargs.items()]
219 assert not (pos_args and named_args), "Cannot use positional arguments and named arguments in the same bitcoin-cli call"
220 p_args = [self.binary, "-datadir=" + self.datadir] + self.args
221 if named_args:
222 p_args += ["-named"]
223 p_args += [command] + pos_args + named_args
224 process = subprocess.Popen(p_args, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, universal_newlines=True)
225 cli_stdout, cli_stderr = process.communicate(input=self.input)
226 returncode = process.poll()
227 if returncode:
228 # Ignore cli_stdout, raise with cli_stderr
229 raise subprocess.CalledProcessError(returncode, self.binary, output=cli_stderr)
230 return json.loads(cli_stdout, parse_float=decimal.Decimal)