From 987dafd353633950fec53963ac38a4226d7d564f Mon Sep 17 00:00:00 2001 From: The MMGen Project Date: Thu, 12 Mar 2020 17:10:02 +0000 Subject: [PATCH] opts.py: init sequence, opt checking cleanups/improvements Testing: $ test/test.py opts --- mmgen/globalvars.py | 8 + mmgen/main_split.py | 2 +- mmgen/opts.py | 502 ++++++++++++++++++++------------------ mmgen/share/Opts.py | 4 +- test/misc/opts.py | 71 ++++++ test/test.py | 5 +- test/test_py_d/ts_opts.py | 114 +++++++++ 7 files changed, 470 insertions(+), 236 deletions(-) create mode 100755 test/misc/opts.py create mode 100755 test/test_py_d/ts_opts.py diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index ced1dce1..d7766534 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -210,6 +210,14 @@ class g(object): 'MMGEN_DISABLE_COLOR', 'MMGEN_DISABLE_MSWIN_PW_WARNING', ) + infile_opts = ( + 'keys_from_file', + 'mmgen_keys_from_file', + 'passwd_file', + 'keysforaddrs', + 'comment_file', + 'contract_data', + ) # Auto-typechecked and auto-set opts - incompatible with global_sets_opt and opt_sets_global # First value in list is the default ov = namedtuple('autoset_opt_info',['type','choices']) diff --git a/mmgen/main_split.py b/mmgen/main_split.py index 5e14fafe..80edac95 100755 --- a/mmgen/main_split.py +++ b/mmgen/main_split.py @@ -113,7 +113,7 @@ if opt.tx_fees: for idx,g_coin in ((1,opt.other_coin),(0,g.coin)): init_coin(g_coin) opt.tx_fee = opt.tx_fees.split(',')[idx] - opts.opt_is_tx_fee(opt.tx_fee,'transaction fee') or sys.exit(1) + opts.opt_is_tx_fee('foo',opt.tx_fee,'transaction fee') # raises exception on error rpc_init(reinit=True) diff --git a/mmgen/opts.py b/mmgen/opts.py index 80a9c986..37c034e0 100755 --- a/mmgen/opts.py +++ b/mmgen/opts.py @@ -21,9 +21,11 @@ opts.py: MMGen-specific options processing after generic processing by share.Op """ import sys,os,stat -class opt(object): +class opt_cls(object): pass +opt = opt_cls() +from mmgen.exception import UserOptError from mmgen.globalvars import g import mmgen.share.Opts from mmgen.util import * @@ -36,7 +38,7 @@ def fmt_opt(o): def die_on_incompatible_opts(incompat_list): for group in incompat_list: - bad = [k for k in opt.__dict__ if opt.__dict__[k] and k in group] + bad = [k for k in opt.__dict__ if k in group and getattr(opt,k) != None] if len(bad) > 1: die(1,'Conflicting options: {}'.format(', '.join(map(fmt_opt,bad)))) @@ -48,14 +50,14 @@ def _show_hash_presets(): msg(fs.format(i,*g.hash_presets[i])) msg('N = memory usage (power of two), p = iterations (rounds)') -def opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args): +def opt_preproc_debug(po): d = ( ('Cmdline', ' '.join(sys.argv)), - ('Short opts', short_opts), - ('Long opts', long_opts), - ('Skipped opts', skipped_opts), - ('User-selected opts', uopts), - ('Cmd args', args), + ('Short opts', po.short_opts), + ('Long opts', po.long_opts), + ('Skipped opts', po.skipped_opts), + ('User-selected opts', po.user_opts), + ('Cmd args', po.cmd_args), ) Msg('\n=== opts.py debug ===') for e in d: @@ -75,17 +77,6 @@ def opt_postproc_debug(): Msg(' {:<20}: {}'.format(e, getattr(g,e))) Msg('\n=== end opts.py debug ===\n') -def opt_postproc_initializations(): - g.coin = g.coin.upper() # allow user to use lowercase - g.dcoin = g.coin # the display coin; for ERC20 tokens, g.dcoin is set to the token symbol - -def set_data_dir_root(): - g.data_dir_root = os.path.normpath(os.path.expanduser(opt.data_dir)) if opt.data_dir else \ - os.path.join(g.home_dir,'.'+g.proj_name.lower()) - - # mainnet and testnet share cfg file, as with Core - g.cfg_file = os.path.join(g.data_dir_root,'{}.cfg'.format(g.proj_name.lower())) - def init_term_and_color(): from mmgen.term import set_terminal_vars set_terminal_vars() @@ -136,8 +127,33 @@ def common_opts_code(s): cu_dfl=g.coin, cu_all=' '.join(CoinProtocol.coins) ) +def show_common_opts_diff(): + + def common_opts_data_to_list(): + for l in common_opts_data['text'].splitlines(): + if l.startswith('--,'): + yield l.split()[1].split('=')[0][2:].replace('-','_') + + def do_fmt(set_data): + return fmt_list(['--'+s.replace('_','-') for s in set_data],fmt='col',indent=' ') + + a = set(g.common_opts) + b = set(common_opts_data_to_list()) + + m1 = 'g.common_opts - common_opts_data:\n {}\n' + msg(m1.format(do_fmt(a-b) if a-b else 'None')) + + m2 = 'common_opts_data - g.common_opts (these do not set global var):\n{}\n' + msg(m2.format(do_fmt(b-a))) + + m3 = 'common_opts_data ^ g.common_opts (these set global var):\n{}\n' + msg(m3.format(do_fmt(b.intersection(a)))) + + sys.exit(0) + common_opts_data = { # Most but not all of these set the corresponding global var + # View differences with show_common_opts_diff() 'text': """ --, --accept-defaults Accept defaults at all prompts --, --coin=c Choose coin unit. Default: {cu_dfl}. Options: {cu_all} @@ -168,23 +184,23 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): opts_data['text']['long_options'] = common_opts_data['text'] - uopts,args,short_opts,long_opts,skipped_opts = \ - mmgen.share.Opts.parse_opts(opts_data,opt_filter=opt_filter,parse_only=parse_only) + # po: user_opts cmd_args short_opts long_opts skipped_opts + po = mmgen.share.Opts.parse_opts(opts_data,opt_filter=opt_filter,parse_only=parse_only) if parse_only: - return uopts,args,short_opts,long_opts,skipped_opts + return po if g.debug_opts: - opt_preproc_debug(short_opts,long_opts,skipped_opts,uopts,args) + opt_preproc_debug(po) # Copy parsed opts to opt, setting values to None if not set by user for o in ( - tuple(s.rstrip('=') for s in long_opts) + tuple(s.rstrip('=') for s in po.long_opts) + tuple(add_opts) - + tuple(skipped_opts) + + tuple(po.skipped_opts) + g.required_opts + g.common_opts ): - setattr(opt,o,uopts[o] if o in uopts else None) + setattr(opt,o,po.user_opts[o] if o in po.user_opts else None) # Make this available to usage() global usage_txt @@ -197,17 +213,16 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): command line. Copyright (C){g.Cdates} {g.author} {g.email} """.format(g=g,pn=g.prog_name.upper()),indent=' ').rstrip()) - if os.getenv('MMGEN_DEBUG_ALL'): - for name in g.env_opts: - if name[:11] == 'MMGEN_DEBUG': - os.environ[name] = '1' - # === begin global var initialization === # # NB: user opt --data-dir is actually g.data_dir_root # cfg file is in g.data_dir_root, wallet and other data are in g.data_dir - # We must set g.data_dir_root and g.cfg_file from cmdline before processing cfg file - set_data_dir_root() + # We must set g.data_dir_root from --data-dir before processing cfg file + g.data_dir_root = ( + os.path.normpath(os.path.expanduser(opt.data_dir)) + if opt.data_dir else + os.path.join(g.home_dir,'.'+g.proj_name.lower()) ) + check_or_create_dir(g.data_dir_root) init_term_and_color() @@ -228,12 +243,16 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): if val != None: setattr(g,k,set_for_type(val,getattr(g,k),'--'+k)) - if g.regtest: g.testnet = True # These are equivalent for now + g.coin = g.coin.upper() # allow user to use lowercase + g.dcoin = g.coin # the display coin; for ERC20 tokens, g.dcoin is set to the token symbol + + if g.regtest: # These are equivalent for now + g.testnet = True g.network = 'testnet' if g.testnet else 'mainnet' from mmgen.protocol import init_genonly_altcoins,CoinProtocol - altcoin_trust_level = init_genonly_altcoins(opt.coin or 'btc') + altcoin_trust_level = init_genonly_altcoins(g.coin) # g.testnet is finalized, so we can set g.proto g.proto = CoinProtocol(g.coin,g.testnet) @@ -250,11 +269,11 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): # - if opt is set, convert its type to that of global value opt.set_by_user = [] for k in g.global_sets_opt: - if k in opt.__dict__ and getattr(opt,k) != None: + if hasattr(opt,k) and getattr(opt,k) != None: setattr(opt,k,set_for_type(getattr(opt,k),getattr(g,k),'--'+k)) opt.set_by_user.append(k) else: - setattr(opt,k,g.__dict__[k]) + setattr(opt,k,getattr(g,k)) if opt.show_hash_presets: _show_hash_presets() @@ -263,16 +282,6 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): if opt.verbose: opt.quiet = None - die_on_incompatible_opts(g.incompatible_opts) - - opt_postproc_initializations() - - if opts_data['do_help']: # print help screen only after global vars are initialized - if not 'code' in opts_data: - opts_data['code'] = {} - opts_data['code']['long_options'] = common_opts_data['code'] - mmgen.share.Opts.print_help(opts_data,opt_filter) # exits - if g.bob or g.alice: g.testnet = True g.regtest = True @@ -284,20 +293,23 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): g.rpc_password = MMGenRegtest.rpc_password g.rpc_port = MMGenRegtest(g.coin).d.rpc_port - check_or_create_dir(g.data_dir) # g.data_dir is finalized, so now we can do this + # === end global var initialization === # + + if opts_data['do_help']: # print help screen only after global vars are initialized + if not 'code' in opts_data: + opts_data['code'] = {} + opts_data['code']['long_options'] = common_opts_data['code'] + mmgen.share.Opts.print_help(opts_data,opt_filter) # exits + + die_on_incompatible_opts(g.incompatible_opts) + + check_or_create_dir(g.data_dir) # g.data_dir is finalized, so we can create it # Check user-set opts without modifying them - if not check_opts(uopts): - die(1,'Options checking failed') + check_usr_opts(po.user_opts) - # Check user-set opts against g.autoset_opts, setting opt if unset: - if not check_opts2(uopts): - die(1,'Options checking failed') - - if hasattr(g,'cfg_options_changed'): - ymsg("Warning: config file options have changed! See '{}' for details".format(g.cfg_file+'.sample')) - if not g.test_suite: - my_raw_input('Hit ENTER to continue: ') + # Check all opts against g.autoset_opts, setting if unset + check_and_set_autoset_opts() if g.debug and g.prog_name != 'test.py': opt.verbose,opt.quiet = (True,None) @@ -308,211 +320,237 @@ def init(opts_data,add_opts=[],opt_filter=None,parse_only=False): warn_altcoins(g.coin,altcoin_trust_level) # We don't need this data anymore - del mmgen.share.Opts, opts_data + del mmgen.share.Opts + for k in ('text','notes','code'): + if k in opts_data: + del opts_data[k] - return args + return po.cmd_args + +def opt_is_tx_fee(key,val,desc): # 'key' must remain a placeholder + + # contract data or non-standard startgas: disable fee checking + if hasattr(opt,'contract_data') and opt.contract_data: + return + if hasattr(opt,'tx_gas') and opt.tx_gas: + return -def opt_is_tx_fee(val,desc): from mmgen.tx import MMGenTX tx = MMGenTX(offline=True) - # TODO: size is just a guess; do this check after parsing tx file + # Size of 224 is just a ball-park figure to eliminate the most extreme cases at startup + # This check will be performed again once we know the true size ret = tx.process_fee_spec(val,224,on_fail='return') - # Non-standard startgas: disable fee checking - if hasattr(opt,'contract_data') and opt.contract_data: ret = None - if hasattr(opt,'tx_gas') and opt.tx_gas: ret = None + if ret == False: - msg("'{}': invalid {}\n(not a {} amount or {} specification)".format( + raise UserOptError('{!r}: invalid {}\n(not a {} amount or {} specification)'.format( val,desc,g.coin.upper(),tx.rel_fee_desc)) - elif ret != None and ret > g.proto.max_tx_fee: - msg("'{}': invalid {}\n({} > max_tx_fee ({} {}))".format( + + if ret > g.proto.max_tx_fee: + raise UserOptError('{!r}: invalid {}\n({} > max_tx_fee ({} {}))'.format( val,desc,ret.fmt(fs='1.1'),g.proto.max_tx_fee,g.coin.upper())) - else: - return True - return False -def check_opts2(usr_opts): # Returns false if any check fails - - for key in [e for e in opt.__dict__ if not e.startswith('__')]: - if key in g.autoset_opts: - val = getattr(opt,key) - d = g.autoset_opts[key] - if d[0] == 'nocase_str': - if val == None: - setattr(opt,key,d[1][0]) - elif val.lower() not in d[1]: - m = "{!r}: invalid parameter for option --{} (valid choices: '{}')" - msg(m.format(val,key.replace('_','-'),"', '".join(d[1]))) - return False - - return True - -def check_opts(usr_opts): # Returns false if any check fails +def check_usr_opts(usr_opts): # Raises an exception if any check fails def opt_splits(val,sep,n,desc): - sepword = 'comma' if sep == ',' else 'colon' if sep == ':' else "'{}'".format(sep) - try: l = val.split(sep) + sepword = 'comma' if sep == ',' else 'colon' if sep == ':' else repr(sep) + try: + l = val.split(sep) except: - msg("'{}': invalid {} (not {}-separated list)".format(val,desc,sepword)) - return False + raise UserOptError('{!r}: invalid {} (not {}-separated list)'.format(val,desc,sepword)) - if len(l) == n: return True - else: - msg("'{}': invalid {} ({} {}-separated items required)".format(val,desc,n,sepword)) - return False + if len(l) != n: + raise UserOptError('{!r}: invalid {} ({} {}-separated items required)'.format(val,desc,n,sepword)) - def opt_compares(val,op_str,target,desc,what=''): + def opt_compares(val,op_str,target,desc,desc2=''): import operator as o op_f = { '<':o.lt, '<=':o.le, '>':o.gt, '>=':o.ge, '=':o.eq }[op_str] - if what: what += ' ' if not op_f(val,target): - msg('{}: invalid {} ({}not {} {})'.format(val,desc,what,op_str,target)) - return False - return True + d2 = desc2 + ' ' if desc2 else '' + raise UserOptError('{}: invalid {} ({}not {} {})'.format(val,desc,d2,op_str,target)) def opt_is_int(val,desc): - try: int(val) - except: - msg("'{}': invalid {} (not an integer)".format(val,desc)) - return False - return True + if not is_int(val): + raise UserOptError('{!r}: invalid {} (not an integer)'.format(val,desc)) def opt_is_float(val,desc): - try: float(val) + try: + float(val) except: - msg("'{}': invalid {} (not a floating-point number)".format(val,desc)) - return False - return True + raise UserOptError('{!r}: invalid {} (not a floating-point number)'.format(val,desc)) - def opt_is_in_list(val,lst,desc): - if val not in lst: - q,sep = (('',','),("'","','"))[type(lst[0]) == str] + def opt_is_in_list(val,tlist,desc): + if val not in tlist: + q,sep = (('',','),("'","','"))[type(tlist[0]) == str] fs = '{q}{v}{q}: invalid {w}\nValid choices: {q}{o}{q}' - msg(fs.format(v=val,w=desc,q=q,o=sep.join(map(str,sorted(lst))))) - return False - return True + raise UserOptError(fs.format(v=val,w=desc,q=q,o=sep.join(map(str,sorted(tlist))))) - def opt_unrecognized(key,val,desc): - msg("'{}': unrecognized {} for option '{}'".format(val,desc,fmt_opt(key))) - return False + def opt_unrecognized(key,val,desc='value'): + raise UserOptError('{!r}: unrecognized {} for option {!r}'.format(val,desc,fmt_opt(key))) def opt_display(key,val='',beg='For selected',end=':\n'): s = '{}={}'.format(fmt_opt(key),val) if val else fmt_opt(key) - msg_r("{} option '{}'{}".format(beg,s,end)) + msg_r('{} option {!r}{}'.format(beg,s,end)) - global opt - for key,val in [(k,getattr(opt,k)) for k in usr_opts]: + def chk_in_fmt(key,val,desc): + from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden + sstype = SeedSource.fmt_code_to_type(val) + if not sstype: + opt_unrecognized(key,val) + if key == 'out_fmt': + p = 'hidden_incog_output_params' + if sstype == IncogWalletHidden and not getattr(opt,p): + m1 = 'Hidden incog format output requested. ' + m2 = 'You must supply a file and offset with the {!r} option' + raise UserOptError(m1+m2.format(fmt_opt(p))) + if issubclass(sstype,IncogWallet) and opt.old_incog_fmt: + opt_display(key,val,beg='Selected',end=' ') + opt_display('old_incog_fmt',beg='conflicts with',end=':\n') + raise UserOptError('Export to old incog wallet format unsupported') + elif issubclass(sstype,Brainwallet): + raise UserOptError('Output to brainwallet format unsupported') - desc = "parameter for '{}' option".format(fmt_opt(key)) + chk_out_fmt = chk_in_fmt - # Check for file existence and readability - if key in ('keys_from_file','mmgen_keys_from_file', - 'passwd_file','keysforaddrs','comment_file'): - check_infile(val) # exits on error - continue + def chk_hidden_incog_input_params(key,val,desc): + a = val.rsplit(',',1) # permit comma in filename + if len(a) != 2: + opt_display(key,val) + raise UserOptError('Option requires two comma-separated arguments') - if key == 'outdir': - check_outdir(val) # exits on error -# # NEW - elif key in ('in_fmt','out_fmt'): - from mmgen.seed import SeedSource,IncogWallet,Brainwallet,IncogWalletHidden - sstype = SeedSource.fmt_code_to_type(val) - if not sstype: - return opt_unrecognized(key,val,'format code') - if key == 'out_fmt': - p = 'hidden_incog_output_params' - if sstype == IncogWalletHidden and not getattr(opt,p): - m1 = 'Hidden incog format output requested. ' - m2 = "You must supply a file and offset with the '{}' option" - die(1,m1+m2.format(fmt_opt(p))) - if issubclass(sstype,IncogWallet) and opt.old_incog_fmt: - opt_display(key,val,beg='Selected',end=' ') - opt_display('old_incog_fmt',beg='conflicts with',end=':\n') - die(1,'Export to old incog wallet format unsupported') - elif issubclass(sstype,Brainwallet): - die(1,'Output to brainwallet format unsupported') - elif key in ('hidden_incog_input_params','hidden_incog_output_params'): - a = val.split(',') - if len(a) < 2: - opt_display(key,val) - msg('Option requires two comma-separated arguments') - return False - fn,ofs = ','.join(a[:-1]),a[-1] # permit comma in filename - if not opt_is_int(ofs,desc): return False - if key == 'hidden_incog_input_params': - check_infile(fn,blkdev_ok=True) - key2 = 'in_fmt' - else: - try: os.stat(fn) - except: - b = os.path.dirname(fn) - if b: check_outdir(b) - else: check_outfile(fn,blkdev_ok=True) - key2 = 'out_fmt' - if hasattr(opt,key2): - val2 = getattr(opt,key2) - from mmgen.seed import IncogWalletHidden - if val2 and val2 not in IncogWalletHidden.fmt_codes: - fs = 'Option conflict:\n {}, with\n {}={}' - die(1,fs.format(fmt_opt(key),fmt_opt(key2),val2)) - elif key == 'seed_len': - if not opt_is_int(val,desc): return False - if not opt_is_in_list(int(val),g.seed_lens,desc): return False - elif key == 'hash_preset': - if not opt_is_in_list(val,list(g.hash_presets.keys()),desc): return False - elif key == 'brain_params': - a = val.split(',') - if len(a) != 2: - opt_display(key,val) - msg('Option requires two comma-separated arguments') - return False - d = 'seed length ' + desc - if not opt_is_int(a[0],d): return False - if not opt_is_in_list(int(a[0]),g.seed_lens,d): return False - d = 'hash preset ' + desc - if not opt_is_in_list(a[1],list(g.hash_presets.keys()),d): return False - elif key == 'usr_randchars': - if val == 0: continue - if not opt_is_int(val,desc): return False - if not opt_compares(val,'>=',g.min_urandchars,desc): return False - if not opt_compares(val,'<=',g.max_urandchars,desc): return False - elif key == 'tx_fee': - if not opt_is_tx_fee(val,desc): return False - elif key == 'tx_confs': - if not opt_is_int(val,desc): return False - if not opt_compares(val,'>=',1,desc): return False - elif key == 'vsize_adj': - if not opt_is_float(val,desc): return False - ymsg('Adjusting transaction vsize by a factor of {:1.2f}'.format(float(val))) - elif key == 'key_generator': - if not opt_compares(val,'<=',len(g.key_generators),desc): return False - if not opt_compares(val,'>',0,desc): return False - elif key == 'coin': - from mmgen.protocol import CoinProtocol - if not opt_is_in_list(val.lower(),list(CoinProtocol.coins.keys()),'coin'): return False - elif key == 'rbf': - if not g.proto.cap('rbf'): - msg('--rbf requested, but {} does not support replace-by-fee transactions'.format(g.coin)) - return False - elif key in ('bob','alice'): - m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize." - from mmgen.regtest import MMGenRegtest - try: os.stat(os.path.join(MMGenRegtest(g.coin).d.datadir,'regtest','debug.log')) - except: die(1,m.format(g.proj_name.lower())) - elif key == 'locktime': - if not opt_is_int(val,desc): return False - if not opt_compares(int(val),'>',0,desc): return False - elif key == 'token': - if not 'token' in g.proto.caps: - msg("Coin '{}' does not support the --token option".format(g.coin)) - return False - elif len(val) == 40 and is_hex_str(val): - pass - elif len(val) > 20 or not all(s.isalnum() for s in val): - msg("u'{}: invalid parameter for --token option".format(val)) - return False - elif key == 'contract_data': - check_infile(val) + fn,offset = a + opt_is_int(offset,desc) + + if key == 'hidden_incog_input_params': + check_infile(fn,blkdev_ok=True) + key2 = 'in_fmt' else: - if g.debug: Msg("check_opts(): No test for opt '{}'".format(key)) + try: os.stat(fn) + except: + b = os.path.dirname(fn) + if b: check_outdir(b) + else: + check_outfile(fn,blkdev_ok=True) + key2 = 'out_fmt' - return True + if hasattr(opt,key2): + val2 = getattr(opt,key2) + from mmgen.seed import IncogWalletHidden + if val2 and val2 not in IncogWalletHidden.fmt_codes: + fs = 'Option conflict:\n {}, with\n {}={}' + raise UserOptError(fs.format(fmt_opt(key),fmt_opt(key2),val2)) + + chk_hidden_incog_output_params = chk_hidden_incog_input_params + + def chk_seed_len(key,val,desc): + opt_is_int(val,desc) + opt_is_in_list(int(val),g.seed_lens,desc) + + def chk_hash_preset(key,val,desc): + opt_is_in_list(val,list(g.hash_presets.keys()),desc) + + def chk_brain_params(key,val,desc): + a = val.split(',') + if len(a) != 2: + opt_display(key,val) + raise UserOptError('Option requires two comma-separated arguments') + opt_is_int(a[0],'seed length '+desc) + opt_is_in_list(int(a[0]),g.seed_lens,'seed length '+desc) + opt_is_in_list(a[1],list(g.hash_presets.keys()),'hash preset '+desc) + + def chk_usr_randchars(key,val,desc): + if val == 0: + return + opt_is_int(val,desc) + opt_compares(val,'>=',g.min_urandchars,desc) + opt_compares(val,'<=',g.max_urandchars,desc) + + def chk_tx_fee(key,val,desc): + opt_is_tx_fee(key,val,desc) + + def chk_tx_confs(key,val,desc): + opt_is_int(val,desc) + opt_compares(val,'>=',1,desc) + + def chk_vsize_adj(key,val,desc): + opt_is_float(val,desc) + ymsg('Adjusting transaction vsize by a factor of {:1.2f}'.format(float(val))) + + def chk_key_generator(key,val,desc): + opt_compares(val,'<=',len(g.key_generators),desc) + opt_compares(val,'>',0,desc) + + def chk_coin(key,val,desc): + from mmgen.protocol import CoinProtocol + opt_is_in_list(val.lower(),list(CoinProtocol.coins.keys()),'coin') + + def chk_rbf(key,val,desc): + if not g.proto.cap('rbf'): + m = '--rbf requested, but {} does not support replace-by-fee transactions' + raise UserOptError(m.format(g.coin)) + + def chk_bob(key,val,desc): + m = "Regtest (Bob and Alice) mode not set up yet. Run '{}-regtest setup' to initialize." + from mmgen.regtest import MMGenRegtest + try: + os.stat(os.path.join(MMGenRegtest(g.coin).d.datadir,'regtest','debug.log')) + except: + raise UserOptError(m.format(g.proj_name.lower())) + + chk_alice = chk_bob + + def chk_locktime(key,val,desc): + opt_is_int(val,desc) + opt_compares(int(val),'>',0,desc) + + def chk_token(key,val,desc): + if not 'token' in g.proto.caps: + raise UserOptError('Coin {!r} does not support the --token option'.format(g.coin)) + if len(val) == 40 and is_hex_str(val): + return + if len(val) > 20 or not all(s.isalnum() for s in val): + raise UserOptError('{!r}: invalid parameter for --token option'.format(val)) + + cfuncs = { k:v for k,v in locals().items() if k.startswith('chk_') } + + for key in usr_opts: + val = getattr(opt,key) + desc = 'parameter for {!r} option'.format(fmt_opt(key)) + + if key in g.infile_opts: + check_infile(val) # file exists and is readable - dies on error + elif key == 'outdir': + check_outdir(val) # dies on error + elif 'chk_'+key in cfuncs: + cfuncs['chk_'+key](key,val,desc) + elif g.debug: + Msg('check_usr_opts(): No test for opt {!r}'.format(key)) + +def check_and_set_autoset_opts(): # Raises exception if any check fails + + def nocase_str(key,val,asd): + if val.lower() in asd.choices: + return True + else: + return 'one of' + + def nocase_pfx(key,val,asd): + cs = [s.startswith(val.lower()) for s in asd.choices] + if cs.count(True) == 1: + return cs.index(True) + else: + return 'unique substring of' + + for key,asd in g.autoset_opts.items(): + if hasattr(opt,key): + val = getattr(opt,key) + if val is None: + setattr(opt,key,asd.choices[0]) + else: + ret = locals()[asd.type](key,val,asd) + if type(ret) is str: + m = '{!r}: invalid parameter for option --{} (not {}: {})' + raise UserOptError(m.format(val,key.replace('_','-'),ret,fmt_list(asd.choices))) + elif ret is True: + setattr(opt,key,val) + else: + setattr(opt,key,asd.choices[ret]) diff --git a/mmgen/share/Opts.py b/mmgen/share/Opts.py index d5c03d83..3fdcf98c 100755 --- a/mmgen/share/Opts.py +++ b/mmgen/share/Opts.py @@ -136,4 +136,6 @@ def parse_opts(opts_data,opt_filter=None,parse_only=False): opts,args = process_opts(opts_data,short_opts,long_opts) - return opts,args,short_opts,long_opts,skipped_opts + from collections import namedtuple + ret = namedtuple('parsed_cmd_opts',['user_opts','cmd_args','short_opts','long_opts','skipped_opts']) + return ret(opts,args,short_opts,long_opts,skipped_opts) diff --git a/test/misc/opts.py b/test/misc/opts.py new file mode 100755 index 00000000..767a2ffb --- /dev/null +++ b/test/misc/opts.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 + +from mmgen.common import * + +opts_data = { + 'sets': [('print_checksum',True,'quiet',True)], + 'text': { + 'desc': 'Opts test', + 'usage':'[args] [opts]', + 'options': """ +-h, --help Print this help message +--, --longhelp Print help message for long options (common options) +-i, --in-fmt= f Input is from wallet format 'f' +-d, --outdir= d Use outdir 'd' +-C, --print-checksum Print a checksum +-E, --fee-estimate-mode=M Specify the network fee estimate mode. +-H, --hidden-incog-input-params=f,o Read hidden incognito data from file + 'f' at offset 'o' (comma-separated) +-K, --key-generator=m Use method 'm' for public key generation + Options: {kgs} (default: {kg}) +-l, --seed-len= l Specify wallet seed length of 'l' bits. +-L, --label= l Specify a label 'l' for output wallet +-m, --keep-label Reuse label of input wallet for output wallet +-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p' +-P, --passwd-file= f Get wallet passphrase from file 'f' +-u, --subseeds= n The number of subseed pairs to scan for +-q, --quiet Be quieter +-v, --verbose Be more verbose +""", + 'notes': """ + + NOTES FOR THIS COMMAND + {nn} +""" + }, + 'code': { + 'options': lambda s: s.format( + kgs=' '.join(['{}:{}'.format(n,k) for n,k in enumerate(g.key_generators,1)]), + kg=g.key_generator, + g=g, + ), + 'notes': lambda s: s.format(nn='a note'), + } +} + +cmd_args = opts.init(opts_data,add_opts=['foo']) + +if cmd_args == ['show_common_opts_diff']: + from mmgen.opts import show_common_opts_diff + show_common_opts_diff() + sys.exit(0) + +for k in ( + 'foo', # added opt + 'print_checksum', # sets 'quiet' + 'quiet','verbose', # required_opts, incompatible_opts + 'fee_estimate_mode', # autoset_opts + 'passwd_file', # infile_opts - check_infile() + 'outdir', # check_outdir() + 'subseeds', # opt_sets_global + 'key_generator', # global_sets_opt + 'hidden_incog_input_params', + ): + msg('{:30} {}'.format('opt.'+k+':',getattr(opt,k))) + +msg('') +for k in ( + 'subseeds', # opt_sets_global + 'key_generator', # global_sets_opt + ): + msg('{:30} {}'.format('g.'+k+':',getattr(opt,k))) diff --git a/test/test.py b/test/test.py index 1734b944..bc4164aa 100755 --- a/test/test.py +++ b/test/test.py @@ -142,7 +142,7 @@ If no command is given, the whole test suite is run. data_dir = os.path.join('test','data_dir' + ('','-α')[bool(os.getenv('MMGEN_DEBUG_UTF8'))]) # we need the values of two opts before running opts.init, so parse without initializing: -_uopts = opts.init(opts_data,parse_only=True)[0] +_uopts = opts.init(opts_data,parse_only=True).user_opts # step 1: delete data_dir symlink in ./test; if not ('resume' in _uopts or 'skip_deps' in _uopts): @@ -161,7 +161,6 @@ sys.argv.insert(1,'--rpc-port={}'.format(CoinDaemon(network_id,test_suite=True). # step 2: opts.init will create new data_dir in ./test (if not 'resume' or 'skip_deps'): usr_args = opts.init(opts_data) - # step 3: move data_dir to /dev/shm and symlink it back to ./test: trash_dir = os.path.join('test','trash') if not ('resume' in _uopts or 'skip_deps' in _uopts): @@ -339,6 +338,7 @@ cfgs = { # addr_idx_lists (except 31,32,33,34) must contain exactly 8 addresses '33': {}, '34': {}, '40': {}, + '41': {}, } def fixup_cfgs(): @@ -463,6 +463,7 @@ def set_restore_term_at_exit(): class CmdGroupMgr(object): cmd_groups_dfl = { + 'opts': ('TestSuiteOpts',{'full_data':True}), 'cfg': ('TestSuiteCfg',{'full_data':True}), 'helpscreens': ('TestSuiteHelp',{'modname':'misc','full_data':True}), 'main': ('TestSuiteMain',{'full_data':True}), diff --git a/test/test_py_d/ts_opts.py b/test/test_py_d/ts_opts.py new file mode 100755 index 00000000..a4a9180a --- /dev/null +++ b/test/test_py_d/ts_opts.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2020 The MMGen Project +# +# Project source code repository: https://github.com/mmgen/mmgen +# Licensed according to the terms of GPL Version 3. See LICENSE for details. + +""" +ts_opts.py: options processing tests for the MMGen test.py test suite +""" + +from test.common import * +from test.test_py_d.ts_base import * + +class TestSuiteOpts(TestSuiteBase): + 'options processing' + networks = ('btc',) + tmpdir_nums = [41] + cmd_group = ( + ('opt_helpscreen', (41,"helpscreen output", [])), + ('opt_noargs', (41,"invocation with no user options or arguments", [])), + ('opt_good', (41,"good opts", [])), + ('opt_bad_infile', (41,"bad infile parameter", [])), + ('opt_bad_outdir', (41,"bad outdir parameter", [])), + ('opt_bad_incompatible', (41,"incompatible opts", [])), + ('opt_bad_autoset', (41,"invalid autoset value", [])), + ('opt_show_diff', (41,"show_common_opts_diff()", [])), + ) + + def spawn_prog(self,args): + return self.spawn('test/misc/opts.py',args,cmd_dir='.') + + def check_vals(self,args,vals): + t = self.spawn_prog(args) + for k,v in vals: + t.expect(r'{}:\s+{}'.format(k,v),regex=True) + t.read() + return t + + def do_run(self,args,expect,exit_val,regex=False): + t = self.spawn_prog(args) + t.expect(expect,regex=regex) + t.read() + t.req_exit_val = exit_val + return t + + def opt_helpscreen(self): + return self.do_run( + ['--help'], + r'OPTS.PY: Opts test.*USAGE:\s+opts.py.*1:python-ecdsa 2:libsecp256k1 \(default: 2\).*' + + r'NOTES FOR THIS.*a note', + 0, + regex=True ) + + def opt_noargs(self): + return self.check_vals( + [], + ( + ('opt.foo', 'None'), # added opt + ('opt.print_checksum', 'None'), # sets 'quiet' + ('opt.quiet', 'False'), # required_opts, incompatible_opts + ('opt.verbose', 'None'), # required_opts, incompatible_opts + ('opt.fee_estimate_mode', 'conservative'), # autoset_opts + ('opt.passwd_file', 'None'), # infile_opts - check_infile() + ('opt.outdir', 'None'), # check_outdir() + ('opt.subseeds', 'None'), # opt_sets_global + ('opt.key_generator', '2'), # global_sets_opt + ('g.subseeds', 'None'), + ('g.key_generator', '2'), + ) + ) + + def opt_good(self): + pf_base = 'testfile' + pf = os.path.join(self.tmpdir,pf_base) + self.write_to_tmpfile(pf_base,'') + return self.check_vals( + [ + '--print-checksum', + '--fee-estimate-mode=E', + '--passwd-file='+pf, + '--outdir='+self.tmpdir, + '--subseeds=200', + '--hidden-incog-input-params={},123'.format(pf), + ], + ( + ('opt.print_checksum', 'True'), + ('opt.quiet', 'True'), # set by print_checksum + ('opt.fee_estimate_mode', 'economical'), + ('opt.passwd_file', pf), + ('opt.outdir', self.tmpdir), + ('opt.subseeds', '200'), + ('opt.hidden_incog_input_params', pf+',123'), + ('g.subseeds', '200'), + ) + ) + + def opt_bad_infile(self): + pf = os.path.join(self.tmpdir,'fubar') + return self.do_run(['--passwd-file='+pf],'not found',1) + + def opt_bad_outdir(self): + bo = self.tmpdir+'_fubar' + return self.do_run(['--outdir='+bo],'not found',1) + + def opt_bad_incompatible(self): + return self.do_run(['--label=Label','--keep-label'],'Conflicting options',1) + + def opt_bad_autoset(self): + return self.do_run(['--fee-estimate-mode=Fubar'],'not unique substring',1) + + def opt_show_diff(self): + return self.do_run(['show_common_opts_diff'],'common_opts_data',0)