opts.py: init sequence, opt checking cleanups/improvements

Testing:

  $ test/test.py opts
This commit is contained in:
The MMGen Project 2020-03-12 17:10:02 +00:00
commit 987dafd353
Signed by: mmgen
GPG key ID: 3F8B1861E32B7DA2
7 changed files with 473 additions and 239 deletions

View file

@ -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'])

View file

@ -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)

View file

@ -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])

View file

@ -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)

71
test/misc/opts.py Executable file
View file

@ -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)))

View file

@ -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}),

114
test/test_py_d/ts_opts.py Executable file
View file

@ -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 <mmgen@tuta.io>
#
# 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)