From 3a15e62bbe937382d441447b8f53f6c101baa56e Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Tue, 10 Aug 2021 19:38:21 +0000 Subject: [PATCH] Daemon: reimplement flags and opts using ClassFlags,ClassOpts --- mmgen/daemon.py | 82 ++++++++----------------- test/start-coin-daemons.py | 3 +- test/unit_tests_d/ut_daemon.py | 107 +++++++++++++++++++++++++++++++++ test/unit_tests_d/ut_rpc.py | 2 +- 4 files changed, 135 insertions(+), 59 deletions(-) create mode 100755 test/unit_tests_d/ut_daemon.py diff --git a/mmgen/daemon.py b/mmgen/daemon.py index e4065cd0..e3120cd9 100755 --- a/mmgen/daemon.py +++ b/mmgen/daemon.py @@ -25,6 +25,7 @@ from subprocess import run,PIPE,CompletedProcess from collections import namedtuple from .exception import * from .common import * +from .flags import * _dd = namedtuple('daemon_data',['coind_name','coind_version','coind_version_str']) # latest tested version _cd = namedtuple('coins_data',['coin_name','daemon_ids']) @@ -43,16 +44,18 @@ class Daemon(Lockable): private_port = None avail_opts = () avail_flags = () # like opts, but can be added or removed after instantiation - _reset_ok = ('debug','wait','_flags') + _reset_ok = ('debug','wait') + + def __init__(self,opts=None,flags=None): - def __init__(self): - self.opts = [] - self._flags = [] self.platform = g.platform if self.platform == 'win': self.use_pidfile = False self.use_threads = True + self.opt = ClassOpts(self,opts) + self.flag = ClassFlags(self,flags) + def exec_cmd_thread(self,cmd): import threading tname = ('exec_cmd','exec_cmd_win_console')[self.platform == 'win' and self.new_console_mswin] @@ -70,7 +73,7 @@ class Daemon(Lockable): p.wait() def exec_cmd(self,cmd,is_daemon=False): - out = None if (is_daemon and 'no_daemonize' in self.opts) else PIPE + out = (PIPE,None)[is_daemon and self.opt.no_daemonize] try: cp = run(cmd,check=False,stdout=out,stderr=out) except Exception as e: @@ -87,7 +90,7 @@ class Daemon(Lockable): if self.debug: msg('\nExecuting: {}'.format(' '.join(cmd))) - if self.use_threads and (is_daemon and not 'no_daemonize' in self.opts): + if self.use_threads and is_daemon and not self.opt.no_daemonize: ret = self.exec_cmd_thread(cmd) else: ret = self.exec_cmd(cmd,is_daemon) @@ -208,28 +211,10 @@ class Daemon(Lockable): else: die(2,f'Wait for state {req_state!r} timeout exceeded for {self.desc} (port {self.bind_port})') - @property - def flags(self): - return self._flags - - def add_flag(self,val): - if val not in self.avail_flags: - m = '{!r}: unrecognized flag (available options: {})' - die(1,m.format(val,self.avail_flags)) - if val in self._flags: - die(1,'Flag {!r} already set'.format(val)) - self._flags.append(val) - - def remove_flag(self,val): - if val not in self.avail_flags: - m = '{!r}: unrecognized flag (available options: {})' - die(1,m.format(val,self.avail_flags)) - if val not in self._flags: - die(1,'Flag {!r} not set, so cannot be removed'.format(val)) - self._flags.remove(val) - class RPCDaemon(Daemon): + avail_opts = ('no_daemonize',) + def __init__(self): super().__init__() self.desc = '{} {} {}RPC daemon'.format( @@ -305,7 +290,7 @@ class MoneroWalletDaemon(RPCDaemon): [f'--daemon-port={self.daemon_port}', not self.daemon_addr], [f'--proxy={self.proxy}', self.proxy], [f'--pidfile={self.pidfile}', self.platform == 'linux'], - ['--detach', not 'no_daemonize' in self.opts], + ['--detach', not self.opt.no_daemonize], ['--stagenet', self.network == 'testnet'], ) @@ -337,10 +322,10 @@ class CoinDaemon(Daemon): def __new__(cls, network_id = None, - test_suite = False, - flags = None, proto = None, opts = None, + flags = None, + test_suite = False, port_shift = None, p2p_port = None, datadir = None, @@ -378,10 +363,10 @@ class CoinDaemon(Daemon): def __init__(self, network_id = None, - test_suite = False, - flags = None, proto = None, opts = None, + flags = None, + test_suite = False, port_shift = None, p2p_port = None, datadir = None, @@ -389,7 +374,7 @@ class CoinDaemon(Daemon): self.test_suite = test_suite - super().__init__() + super().__init__(opts=opts,flags=flags) self._set_ok += ('shared_args','usr_coind_args') self.shared_args = [] @@ -403,22 +388,6 @@ class CoinDaemon(Daemon): getattr(self.proto.network_names,self.network), 'test suite ' if test_suite else '' ) - if opts: - if type(opts) not in (list,tuple): - die(1,f'{opts!r}: illegal value for opts (must be list or tuple)') - for o in opts: - if o not in CoinDaemon.avail_opts: - die(1,f'{o!r}: unrecognized option') - elif o not in self.avail_opts: - die(1,f'{o!r}: option not supported for {self.coind_name} daemon') - self.opts = list(opts) - - if flags: - if type(flags) not in (list,tuple): - die(1,f'{flags!r}: illegal value for flags (must be list or tuple)') - for flag in flags: - self.add_flag(flag) - # user-set values take precedence self.datadir = os.path.abspath(datadir or g.daemon_data_dir or self.init_datadir()) self.non_dfl_datadir = bool(datadir or g.daemon_data_dir or test_suite) @@ -475,7 +444,7 @@ class CoinDaemon(Daemon): def pre_start(self): os.makedirs(self.datadir,exist_ok=True) - if self.cfg_file and not 'keep_cfg_file' in self.flags: + if self.cfg_file and not self.flag.keep_cfg_file: open('{}/{}'.format(self.datadir,self.cfg_file),'w').write(self.cfg_file_hdr) if self.use_pidfile and os.path.exists(self.pidfile): @@ -536,7 +505,7 @@ class bitcoin_core_daemon(CoinDaemon): ['--rpcallowip=127.0.0.1'], [f'--rpcbind=127.0.0.1:{self.rpc_port}'], ['--pid='+self.pidfile, self.use_pidfile], - ['--daemon', self.platform == 'linux' and not 'no_daemonize' in self.opts], + ['--daemon', self.platform == 'linux' and not self.opt.no_daemonize], ['--fallbackfee=0.0002', self.coin == 'BTC' and self.network == 'regtest'], ['--usecashaddr=0', self.coin == 'BCH'], ['--mempoolreplacement=1', self.coin == 'LTC'], @@ -628,8 +597,8 @@ class monero_daemon(CoinDaemon): ['--no-igd'], [f'--data-dir={self.datadir}', self.non_dfl_datadir], [f'--pidfile={self.pidfile}', self.platform == 'linux'], - ['--detach', not 'no_daemonize' in self.opts], - ['--offline', not 'online' in self.opts], + ['--detach', not self.opt.no_daemonize], + ['--offline', not self.opt.online], ) @property @@ -645,7 +614,7 @@ class ethereum_daemon(CoinDaemon): def __init__(self,*args,**kwargs): - if not hasattr(ethereum_daemon,'all_daemons'): + if not hasattr(self,'all_daemons'): ethereum_daemon.all_daemons = get_subclasses(ethereum_daemon,names=True) self.port_offset = ( @@ -679,7 +648,7 @@ class openethereum_daemon(ethereum_daemon): def init_subclass(self): - ld = self.platform == 'linux' and not 'no_daemonize' in self.opts + ld = self.platform == 'linux' and not self.opt.no_daemonize self.coind_args = list_gen( ['--no-ws'], @@ -719,7 +688,7 @@ class geth_daemon(ethereum_daemon): ['--http.api=eth,web3,txpool'], [f'--http.port={self.rpc_port}'], [f'--port={self.p2p_port}', self.p2p_port], # geth binds p2p port even with --maxpeers=0 - ['--maxpeers=0', not 'online' in self.opts], + ['--maxpeers=0', not self.opt.online], [f'--datadir={self.datadir}', self.non_dfl_datadir], ['--goerli', self.network=='testnet'], ['--dev', self.network=='regtest'], @@ -727,7 +696,6 @@ class geth_daemon(ethereum_daemon): # https://github.com/ledgerwatch/erigon class erigon_daemon(geth_daemon): - avail_opts = ('online',) daemon_data = _dd('Erigon', 2021007005, '2021.07.5') version_pat = r'erigon/(\d+)\.(\d+)\.(\d+)' exec_fn = 'erigon' @@ -741,7 +709,7 @@ class erigon_daemon(geth_daemon): self.coind_args = list_gen( ['--verbosity=0'], [f'--port={self.p2p_port}', self.p2p_port], - ['--maxpeers=0', not 'online' in self.opts], + ['--maxpeers=0', not self.opt.online], [f'--private.api.addr=127.0.0.1:{self.private_port}'], [f'--datadir={self.datadir}', self.non_dfl_datadir], ['--chain=dev', self.network=='regtest'], diff --git a/test/start-coin-daemons.py b/test/start-coin-daemons.py index fb7d6038..a097d69b 100755 --- a/test/start-coin-daemons.py +++ b/test/start-coin-daemons.py @@ -21,6 +21,7 @@ opts_data = { -s, --get-state Get the state of the daemon(s) and exit -t, --testing Testing mode. Print commands but don't execute them -q, --quiet Produce quieter output +-u, --usermode Run the daemon in user (non test-suite) mode -v, --verbose Produce more verbose output -W, --no-wait Don't wait for daemons to change state before exiting """, @@ -42,7 +43,7 @@ def run(network_id=None,proto=None,daemon_id=None): d = CoinDaemon( network_id = network_id, proto = proto, - test_suite = True, + test_suite = not opt.usermode, opts = ['no_daemonize'] if opt.no_daemonize else None, port_shift = int(opt.port_shift or 0), datadir = opt.datadir, diff --git a/test/unit_tests_d/ut_daemon.py b/test/unit_tests_d/ut_daemon.py new file mode 100755 index 00000000..15c6ce85 --- /dev/null +++ b/test/unit_tests_d/ut_daemon.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +""" +test/unit_tests_d/ut_daemon.py: unit test for the MMGen suite's Daemon class +""" + +from subprocess import run,DEVNULL +from mmgen.common import * +from mmgen.exception import * +from mmgen.daemon import * +from mmgen.protocol import init_proto + +class unit_test(object): + + def run_test(self,name,ut): + + def test_flags(): + d = CoinDaemon('eth') + vmsg(f'Available opts: {fmt_list(d.avail_opts,fmt="bare")}') + vmsg(f'Available flags: {fmt_list(d.avail_flags,fmt="bare")}') + vals = namedtuple('vals',['online','no_daemonize','keep_cfg_file']) + + def gen(): + for opts,flags,val in ( + (None,None, vals(False,False,False)), + (None,['keep_cfg_file'], vals(False,False,True)), + (['online'],['keep_cfg_file'], vals(True,False,True)), + (['online','no_daemonize'],['keep_cfg_file'], vals(True,True,True)), + ): + d = CoinDaemon('eth',opts=opts,flags=flags) + assert d.flag.keep_cfg_file == val.keep_cfg_file + assert d.opt.online == val.online + assert d.opt.no_daemonize == val.no_daemonize + d.flag.keep_cfg_file = not val.keep_cfg_file + d.flag.keep_cfg_file = val.keep_cfg_file + yield d + + return tuple(gen()) + + def test_flags_err(d): + + def bad1(): d[0].flag.foo = False + def bad2(): d[0].opt.foo = False + def bad3(): d[0].opt.no_daemonize = True + def bad4(): d[0].flag.keep_cfg_file = 'x' + def bad5(): d[0].opt.no_daemonize = 'x' + def bad6(): d[0].flag.keep_cfg_file = False + def bad7(): d[1].flag.keep_cfg_file = True + + ut.process_bad_data(( + ('flag (1)', 'ClassFlagsError', 'unrecognized flag', bad1 ), + ('opt (1)', 'ClassFlagsError', 'unrecognized opt', bad2 ), + ('opt (2)', 'AttributeError', 'is read-only', bad3 ), + ('flag (2)', 'AssertionError', 'not boolean', bad4 ), + ('opt (3)', 'AttributeError', 'is read-only', bad5 ), + ('flag (3)', 'ClassFlagsError', 'not set', bad6 ), + ('flag (4)', 'ClassFlagsError', 'already set', bad7 ), + )) + + def test_cmds(op): + network_ids = CoinDaemon.get_network_ids() + for test_suite in [True,False] if op == 'print' else [True]: + vmsg(orange(f'Start commands (op={op}, test_suite={test_suite}):')) + for coin,data in CoinDaemon.coins.items(): + for daemon_id in data.daemon_ids: + for network in globals()[daemon_id+'_daemon'].networks: + d = CoinDaemon( + proto=init_proto(coin=coin,network=network), + daemon_id = daemon_id, + test_suite = test_suite ) + if op == 'print': + for cmd in d.start_cmds: + vmsg(' '.join(cmd)) + else: + if run(['which',d.exec_fn],stdout=DEVNULL,stderr=DEVNULL).returncode: + if op == 'start': + qmsg(yellow(f'Warning: {d.exec_fn} not found in executable path')) + else: + if opt.quiet: + msg_r('.') + getattr(d,op)(silent=opt.quiet) + + qmsg_r('Testing flags and opts...') + vmsg('') + daemons = test_flags() + qmsg('OK') + + qmsg_r('Testing error handling for flags and opts...') + vmsg('') + test_flags_err(daemons) + qmsg('OK') + + qmsg_r('Testing start commands for configured daemons...') + vmsg('') + test_cmds('print') + qmsg('OK') + + msg_r('Starting all configured daemons available on system...') + qmsg('') + test_cmds('start') + msg('OK') + + msg_r('Stopping all configured daemons available on system...') + qmsg('') + test_cmds('stop') + msg('OK') + + return True diff --git a/test/unit_tests_d/ut_rpc.py b/test/unit_tests_d/ut_rpc.py index 79a6aff0..f9ea1c28 100755 --- a/test/unit_tests_d/ut_rpc.py +++ b/test/unit_tests_d/ut_rpc.py @@ -18,7 +18,7 @@ def cfg_file_auth_test(proto,d): cf = os.path.join(d.datadir,d.cfg_file) open(cf,'a').write('\nrpcuser = ut_rpc\nrpcpassword = ut_rpc_passw0rd\n') - d.add_flag('keep_cfg_file') + d.flag.keep_cfg_file = True d.start() async def do():