55"""Base class for RPC testing."""
66
77from collections import deque
8+ import errno
89from enum import Enum
10+ import http .client
911import logging
1012import optparse
1113import os
1618import time
1719import traceback
1820
21+ from .authproxy import JSONRPCException
22+ from . import coverage
1923from .util import (
20- PortSeed ,
2124 MAX_NODES ,
22- bitcoind_processes ,
25+ PortSeed ,
26+ assert_equal ,
2327 check_json_precision ,
2428 connect_nodes_bi ,
2529 disable_mocktime ,
2630 disconnect_nodes ,
27- enable_coverage ,
2831 enable_mocktime ,
2932 get_mocktime ,
3033 get_rpc_proxy ,
3437 p2p_port ,
3538 rpc_url ,
3639 set_node_times ,
37- _start_node ,
38- _start_nodes ,
39- _stop_node ,
40- _stop_nodes ,
4140 sync_blocks ,
4241 sync_mempools ,
43- wait_for_bitcoind_start ,
4442)
45- from .authproxy import JSONRPCException
4643
4744class TestStatus (Enum ):
4845 PASSED = 1
@@ -53,6 +50,8 @@ class TestStatus(Enum):
5350TEST_EXIT_FAILED = 1
5451TEST_EXIT_SKIPPED = 77
5552
53+ BITCOIND_PROC_WAIT_TIMEOUT = 60
54+
5655class BitcoinTestFramework (object ):
5756 """Base class for a bitcoin test script.
5857
@@ -72,7 +71,8 @@ class BitcoinTestFramework(object):
7271 def __init__ (self ):
7372 self .num_nodes = 4
7473 self .setup_clean_chain = False
75- self .nodes = None
74+ self .nodes = []
75+ self .bitcoind_processes = {}
7676
7777 def add_options (self , parser ):
7878 pass
@@ -98,7 +98,7 @@ def setup_nodes(self):
9898 extra_args = None
9999 if hasattr (self , "extra_args" ):
100100 extra_args = self .extra_args
101- self .nodes = _start_nodes (self .num_nodes , self .options .tmpdir , extra_args )
101+ self .nodes = self . start_nodes (self .num_nodes , self .options .tmpdir , extra_args )
102102
103103 def run_test (self ):
104104 raise NotImplementedError
@@ -130,9 +130,6 @@ def main(self):
130130 self .add_options (parser )
131131 (self .options , self .args ) = parser .parse_args ()
132132
133- if self .options .coveragedir :
134- enable_coverage (self .options .coveragedir )
135-
136133 PortSeed .n = self .options .port_seed
137134
138135 os .environ ['PATH' ] = self .options .srcdir + ":" + self .options .srcdir + "/qt:" + os .environ ['PATH' ]
@@ -209,16 +206,88 @@ def main(self):
209206 # Public helper methods. These can be accessed by the subclass test scripts.
210207
211208 def start_node (self , i , dirname , extra_args = None , rpchost = None , timewait = None , binary = None , stderr = None ):
212- return _start_node (i , dirname , extra_args , rpchost , timewait , binary , stderr )
209+ """Start a bitcoind and return RPC connection to it"""
210+
211+ datadir = os .path .join (dirname , "node" + str (i ))
212+ if binary is None :
213+ binary = os .getenv ("BITCOIND" , "bitcoind" )
214+ args = [binary , "-datadir=" + datadir , "-server" , "-keypool=1" , "-discover=0" , "-rest" , "-logtimemicros" , "-debug" , "-debugexclude=libevent" , "-debugexclude=leveldb" , "-mocktime=" + str (get_mocktime ()), "-uacomment=testnode%d" % i ]
215+ if extra_args is not None :
216+ args .extend (extra_args )
217+ self .bitcoind_processes [i ] = subprocess .Popen (args , stderr = stderr )
218+ self .log .debug ("initialize_chain: bitcoind started, waiting for RPC to come up" )
219+ self ._wait_for_bitcoind_start (self .bitcoind_processes [i ], datadir , i , rpchost )
220+ self .log .debug ("initialize_chain: RPC successfully started" )
221+ proxy = get_rpc_proxy (rpc_url (datadir , i , rpchost ), i , timeout = timewait )
222+
223+ if self .options .coveragedir :
224+ coverage .write_all_rpc_commands (self .options .coveragedir , proxy )
225+
226+ return proxy
213227
214228 def start_nodes (self , num_nodes , dirname , extra_args = None , rpchost = None , timewait = None , binary = None ):
215- return _start_nodes (num_nodes , dirname , extra_args , rpchost , timewait , binary )
229+ """Start multiple bitcoinds, return RPC connections to them"""
230+
231+ if extra_args is None :
232+ extra_args = [None ] * num_nodes
233+ if binary is None :
234+ binary = [None ] * num_nodes
235+ assert_equal (len (extra_args ), num_nodes )
236+ assert_equal (len (binary ), num_nodes )
237+ rpcs = []
238+ try :
239+ for i in range (num_nodes ):
240+ rpcs .append (self .start_node (i , dirname , extra_args [i ], rpchost , timewait = timewait , binary = binary [i ]))
241+ except :
242+ # If one node failed to start, stop the others
243+ # TODO: abusing self.nodes in this way is a little hacky.
244+ # Eventually we should do a better job of tracking nodes
245+ self .nodes .extend (rpcs )
246+ self .stop_nodes ()
247+ self .nodes = []
248+ raise
249+ return rpcs
250+
251+ def stop_node (self , i ):
252+ """Stop a bitcoind test node"""
216253
217- def stop_node (self , num_node ):
218- _stop_node (self .nodes [num_node ], num_node )
254+ self .log .debug ("Stopping node %d" % i )
255+ try :
256+ self .nodes [i ].stop ()
257+ except http .client .CannotSendRequest as e :
258+ self .log .exception ("Unable to stop node" )
259+ return_code = self .bitcoind_processes [i ].wait (timeout = BITCOIND_PROC_WAIT_TIMEOUT )
260+ assert_equal (return_code , 0 )
261+ del self .bitcoind_processes [i ]
219262
220263 def stop_nodes (self ):
221- _stop_nodes (self .nodes )
264+ """Stop multiple bitcoind test nodes"""
265+
266+ for i in range (len (self .nodes )):
267+ self .stop_node (i )
268+ assert not self .bitcoind_processes .values () # All connections must be gone now
269+
270+ def assert_start_raises_init_error (self , i , dirname , extra_args = None , expected_msg = None ):
271+ with tempfile .SpooledTemporaryFile (max_size = 2 ** 16 ) as log_stderr :
272+ try :
273+ self .start_node (i , dirname , extra_args , stderr = log_stderr )
274+ self .stop_node (i )
275+ except Exception as e :
276+ assert 'bitcoind exited' in str (e ) # node must have shutdown
277+ if expected_msg is not None :
278+ log_stderr .seek (0 )
279+ stderr = log_stderr .read ().decode ('utf-8' )
280+ if expected_msg not in stderr :
281+ raise AssertionError ("Expected error \" " + expected_msg + "\" not found in:\n " + stderr )
282+ else :
283+ if expected_msg is None :
284+ assert_msg = "bitcoind should have exited with an error"
285+ else :
286+ assert_msg = "bitcoind should have exited with expected error " + expected_msg
287+ raise AssertionError (assert_msg )
288+
289+ def wait_for_node_exit (self , i , timeout ):
290+ self .bitcoind_processes [i ].wait (timeout )
222291
223292 def split_network (self ):
224293 """
@@ -300,9 +369,9 @@ def _initialize_chain(self, test_dir, num_nodes, cachedir):
300369 args = [os .getenv ("BITCOIND" , "bitcoind" ), "-server" , "-keypool=1" , "-datadir=" + datadir , "-discover=0" ]
301370 if i > 0 :
302371 args .append ("-connect=127.0.0.1:" + str (p2p_port (0 )))
303- bitcoind_processes [i ] = subprocess .Popen (args )
372+ self . bitcoind_processes [i ] = subprocess .Popen (args )
304373 self .log .debug ("initialize_chain: bitcoind started, waiting for RPC to come up" )
305- wait_for_bitcoind_start ( bitcoind_processes [i ], datadir , i )
374+ self . _wait_for_bitcoind_start ( self . bitcoind_processes [i ], datadir , i )
306375 self .log .debug ("initialize_chain: RPC successfully started" )
307376
308377 self .nodes = []
@@ -355,6 +424,30 @@ def _initialize_chain_clean(self, test_dir, num_nodes):
355424 for i in range (num_nodes ):
356425 initialize_datadir (test_dir , i )
357426
427+ def _wait_for_bitcoind_start (self , process , datadir , i , rpchost = None ):
428+ """Wait for bitcoind to start.
429+
430+ This means that RPC is accessible and fully initialized.
431+ Raise an exception if bitcoind exits during initialization."""
432+ while True :
433+ if process .poll () is not None :
434+ raise Exception ('bitcoind exited with status %i during initialization' % process .returncode )
435+ try :
436+ # Check if .cookie file to be created
437+ rpc = get_rpc_proxy (rpc_url (datadir , i , rpchost ), i , coveragedir = self .options .coveragedir )
438+ rpc .getblockcount ()
439+ break # break out of loop on success
440+ except IOError as e :
441+ if e .errno != errno .ECONNREFUSED : # Port not yet open?
442+ raise # unknown IO error
443+ except JSONRPCException as e : # Initialization phase
444+ if e .error ['code' ] != - 28 : # RPC in warmup?
445+ raise # unknown JSON RPC exception
446+ except ValueError as e : # cookie file not found and no rpcuser or rpcassword. bitcoind still starting
447+ if "No RPC credentials" not in str (e ):
448+ raise
449+ time .sleep (0.25 )
450+
358451class ComparisonTestFramework (BitcoinTestFramework ):
359452 """Test framework for doing p2p comparison testing
360453
0 commit comments