From 0879e53e7446348ae30eb91379727e8827121df3 Mon Sep 17 00:00:00 2001 From: MMGen Date: Wed, 6 Mar 2019 20:58:59 +0000 Subject: [PATCH] tooltest2.py - add BTC test vectors tooltest.py - bugfixes, remove some commands covered in tooltest2.py mmgen-tool - bugfixes, cleanups, rename some commands, change some command options - all commands taking binary input can now receive it from file or stdin + numerous minor fixes throughout --- data_files/mmgen.cfg | 3 + mmgen/addr.py | 19 +- mmgen/crypto.py | 8 +- mmgen/exception.py | 1 + mmgen/globalvars.py | 3 +- mmgen/main_tool.py | 6 +- mmgen/protocol.py | 12 +- mmgen/seed.py | 38 +- mmgen/tool.py | 159 ++++--- mmgen/util.py | 33 +- scripts/test-release.sh | 33 +- test/common.py | 7 +- test/pexpect.py | 7 +- test/ref/keyaddrfile_password | 1 + test/test_py_d/common.py | 4 - test/test_py_d/ts_ethdev.py | 2 +- test/test_py_d/ts_main.py | 4 +- test/test_py_d/ts_ref.py | 6 +- test/tooltest.py | 94 +--- test/tooltest2.py | 808 +++++++++++++++++++++++++--------- 20 files changed, 818 insertions(+), 430 deletions(-) create mode 100644 test/ref/keyaddrfile_password diff --git a/data_files/mmgen.cfg b/data_files/mmgen.cfg index 27cd2489..6a6a5819 100644 --- a/data_files/mmgen.cfg +++ b/data_files/mmgen.cfg @@ -65,6 +65,9 @@ # Set the maximum transaction file size: # max_tx_file_size 100000 +# Set the maximum input size - applies both to files and standard input: +# max_input_size 1048576 + # Set the Ethereum mainnet name # eth_mainnet_chain_name foundation diff --git a/mmgen/addr.py b/mmgen/addr.py index 3de38487..f33b2d02 100755 --- a/mmgen/addr.py +++ b/mmgen/addr.py @@ -627,15 +627,16 @@ Removed {{}} duplicate WIF key{{}} from keylist (also in {pnm} key-address file ret.append(a) - if self.has_keys and keypress_confirm('Check key-to-address validity?'): - kg = KeyGenerator(self.al_id.mmtype) - ag = AddrGenerator(self.al_id.mmtype) - llen = len(ret) - for n,e in enumerate(ret): - msg_r('\rVerifying keys {}/{}'.format(n+1,llen)) - assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),( - "Key doesn't match address!\n {}\n {}".format(e.sec.wif,e.addr)) - msg(' - done') + if self.has_keys: + if (hasattr(opt,'yes') and opt.yes) or keypress_confirm('Check key-to-address validity?'): + kg = KeyGenerator(self.al_id.mmtype) + ag = AddrGenerator(self.al_id.mmtype) + llen = len(ret) + for n,e in enumerate(ret): + qmsg_r('\rVerifying keys {}/{}'.format(n+1,llen)) + assert e.addr == ag.to_addr(kg.to_pubhex(e.sec)),( + "Key doesn't match address!\n {}\n {}".format(e.sec.wif,e.addr)) + qmsg(' - done') return ret diff --git a/mmgen/crypto.py b/mmgen/crypto.py index 627ff9ca..e768e4d3 100755 --- a/mmgen/crypto.py +++ b/mmgen/crypto.py @@ -182,7 +182,8 @@ def mmgen_encrypt(data,desc='data',hash_preset=''): salt = get_random(_salt_len) iv = get_random(g.aesctr_iv_len) nonce = get_random(_nonce_len) - hp = hash_preset or get_hash_preset_from_user('3',desc) + hp = hash_preset or ( + opt.hash_preset if 'hash_preset' in opt.set_by_user else get_hash_preset_from_user('3',desc)) m = ('user-requested','default')[hp=='3'] vmsg('Encrypting {}'.format(desc)) qmsg("Using {} hash preset of '{}'".format(m,hp)) @@ -192,12 +193,13 @@ def mmgen_encrypt(data,desc='data',hash_preset=''): return salt+iv+enc_d def mmgen_decrypt(data,desc='data',hash_preset=''): + vmsg('Preparing to decrypt {}'.format(desc)) dstart = _salt_len + g.aesctr_iv_len salt = data[:_salt_len] iv = data[_salt_len:dstart] enc_d = data[dstart:] - vmsg('Preparing to decrypt {}'.format(desc)) - hp = hash_preset or get_hash_preset_from_user('3',desc) + hp = hash_preset or ( + opt.hash_preset if 'hash_preset' in opt.set_by_user else get_hash_preset_from_user('3',desc)) m = ('user-requested','default')[hp=='3'] qmsg("Using {} hash preset of '{}'".format(m,hp)) passwd = get_mmgen_passphrase(desc) diff --git a/mmgen/exception.py b/mmgen/exception.py index fba19108..922c465d 100755 --- a/mmgen/exception.py +++ b/mmgen/exception.py @@ -32,6 +32,7 @@ class TokenNotInBlockchain(Exception): mmcode = 2 # 3: yellow hl, 'MMGen Error' + exception + message class RPCFailure(Exception): mmcode = 3 class BadTxSizeEstimate(Exception): mmcode = 3 +class MaxInputSizeExceeded(Exception): mmcode = 3 # 4: red hl, 'MMGen Fatal Error' + exception + message class BadMMGenTxID(Exception): mmcode = 4 diff --git a/mmgen/globalvars.py b/mmgen/globalvars.py index 5c41d287..bcc27358 100755 --- a/mmgen/globalvars.py +++ b/mmgen/globalvars.py @@ -140,7 +140,7 @@ class g(object): 'daemon_data_dir','force_256_color','regtest', 'btc_max_tx_fee','ltc_max_tx_fee','bch_max_tx_fee','eth_max_tx_fee', 'eth_mainnet_chain_name','eth_testnet_chain_name', - 'max_tx_file_size' + 'max_tx_file_size','max_input_size' ) env_opts = ( 'MMGEN_BOGUS_WALLET_DATA', @@ -163,6 +163,7 @@ class g(object): min_screen_width = 80 minconf = 1 max_tx_file_size = 100000 + max_input_size = 1024 * 1024 # Global var sets user opt: global_sets_opt = ['minconf','seed_len','hash_preset','usr_randchars','debug', diff --git a/mmgen/main_tool.py b/mmgen/main_tool.py index db824d7c..aa08dff1 100755 --- a/mmgen/main_tool.py +++ b/mmgen/main_tool.py @@ -38,7 +38,7 @@ def make_cmd_help(): out.append('') out.append('') - cls_funcs = bc.user_commands() + cls_funcs = bc._user_commands() max_w = max(map(len,cls_funcs)) fs = ' {{:{}}} - {{}}'.format(max_w) for func in cls_funcs: @@ -60,6 +60,8 @@ opts_data = lambda: { -d, --outdir= d Specify an alternate directory 'd' for output -h, --help Print this help message --, --longhelp Print help message for long options (common options) +-p, --hash-preset= p Use the scrypt hash parameters defined by preset 'p' + for password hashing (default: '{g.hash_preset}') -P, --passwd-file= f Get passphrase from file 'f'. -q, --quiet Produce quieter output -r, --usr-randchars=n Get 'n' characters of additional randomness from @@ -95,4 +97,4 @@ args,kwargs = tool._process_args(cmd,cmd_args) ret = getattr(tc,cmd)(*args,**kwargs) -tool._process_result(ret,to_screen=True,pager='pager' in kwargs and kwargs['pager']) +tool._process_result(ret,pager='pager' in kwargs and kwargs['pager'],print_result=True) diff --git a/mmgen/protocol.py b/mmgen/protocol.py index a19440d0..5b12e4e5 100755 --- a/mmgen/protocol.py +++ b/mmgen/protocol.py @@ -95,6 +95,7 @@ class BitcoinProtocol(MMGenObject): bech32_hrp = 'bc' sign_mode = 'daemon' secp256k1_ge = 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141 + privkey_len = 32 @classmethod def is_testnet(cls): @@ -124,6 +125,7 @@ class BitcoinProtocol(MMGenObject): @classmethod def hex2wif(cls,hexpriv,pubkey_type,compressed): # PrivKey + assert len(hexpriv) == cls.privkey_len*2, '{} bytes: incorrect private key length!'.format(len(hexpriv)//2) return _b58chk_encode(cls.wif_ver_num[pubkey_type] + hexpriv + (b'',b'01')[bool(compressed)]) @classmethod @@ -405,9 +407,9 @@ class MoneroProtocol(DummyWIF,BitcoinProtocolAddrgen): def b58dec(addr_str): from mmgen.util import baseconv - dec,l = baseconv.tohex,len(addr_str) - a = ''.join([dec(addr_str[i*11:i*11+11],'b58',pad=16) for i in range(l//11)]) - b = dec(addr_str[-(l%11):],'b58',pad=10) + l = len(addr_str) + a = b''.join([baseconv.tohex(addr_str[i*11:i*11+11],'b58',pad=16) for i in range(l//11)]) + b = baseconv.tohex(addr_str[-(l%11):],'b58',pad=10) return a + b from mmgen.util import is_b58_str @@ -417,9 +419,9 @@ class MoneroProtocol(DummyWIF,BitcoinProtocolAddrgen): ret = b58dec(addr) import sha3 chk = sha3.keccak_256(unhexlify(ret)[:-4]).hexdigest()[:8] - assert chk == ret[-8:],'Incorrect checksum' + assert chk.encode() == ret[-8:],'Incorrect checksum' - return { 'hex': ret.encode(), 'format': 'monero' } if return_dict else True + return { 'hex': ret, 'format': 'monero' } if return_dict else True class MoneroTestnetProtocol(MoneroProtocol): addr_ver_num = { 'monero': (b'35','4'), 'monero_sub': (b'3f','8') } # 53,63 diff --git a/mmgen/seed.py b/mmgen/seed.py index f29309f1..15b6c9ca 100755 --- a/mmgen/seed.py +++ b/mmgen/seed.py @@ -34,6 +34,18 @@ def check_usr_seed_len(seed_len): m = "ERROR: requested seed length ({}) doesn't match seed length of source ({})" die(1,m.format((opt.seed_len,seed_len))) +def is_mnemonic(s): + oq_save = opt.quiet + opt.quiet = True + try: + SeedSource(in_data=s,in_fmt='words') + ret = True + except: + ret = False + finally: + opt.quiet = oq_save + return ret + class Seed(MMGenObject): def __init__(self,seed_bin=None): if not seed_bin: @@ -63,7 +75,9 @@ class SeedSource(MMGenObject): class SeedSourceData(MMGenObject): pass - def __new__(cls,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False): + def __new__(cls,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False,in_data=None,in_fmt=None): + + in_fmt = in_fmt or opt.in_fmt def die_on_opt_mismatch(opt,sstype): opt_sstype = cls.fmt_code_to_type(opt) @@ -86,13 +100,13 @@ class SeedSource(MMGenObject): # permit comma in filename fn = ','.join(opt.hidden_incog_input_params.split(',')[:-1]) f = Filename(fn,ftype=IncogWalletHidden) - if opt.in_fmt and not ignore_in_fmt: - die_on_opt_mismatch(opt.in_fmt,f.ftype) + if in_fmt and not ignore_in_fmt: + die_on_opt_mismatch(in_fmt,f.ftype) me = super(cls,cls).__new__(f.ftype) me.infile = f me.op = ('old','pwchg_old')[bool(passchg)] - elif opt.in_fmt: # Input format - sstype = cls.fmt_code_to_type(opt.in_fmt) + elif in_fmt: # Input format + sstype = cls.fmt_code_to_type(in_fmt) me = super(cls,cls).__new__(sstype) me.op = ('old','pwchg_old')[bool(passchg)] else: # Called with no inputs - initialize with random seed @@ -104,10 +118,11 @@ class SeedSource(MMGenObject): return me - def __init__(self,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False): + def __init__(self,fn=None,ss=None,seed=None,ignore_in_fmt=False,passchg=False,in_data=None,in_fmt=None): self.ssdata = self.SeedSourceData() self.msg = {} + self.in_data = in_data for c in reversed(self.__class__.__mro__): if hasattr(c,'_msg'): @@ -116,7 +131,7 @@ class SeedSource(MMGenObject): if hasattr(self,'seed'): self._encrypt() return - elif hasattr(self,'infile') or not g.stdin_tty: + elif hasattr(self,'infile') or self.in_data or not g.stdin_tty: self._deformat_once() self._decrypt_retry() else: @@ -130,8 +145,9 @@ class SeedSource(MMGenObject): def _get_data(self): if hasattr(self,'infile'): - self.fmt_data = get_data_from_file(self.infile.name,self.desc, - binary=self.file_mode=='binary',require_utf8=self.require_utf8_input) + self.fmt_data = get_data_from_file(self.infile.name,self.desc,binary=self.file_mode=='binary') + elif self.in_data: + self.fmt_data = self.in_data else: self.fmt_data = self._get_data_from_user(self.desc) @@ -437,7 +453,7 @@ class Mnemonic (SeedSourceUnenc): hexseed = self.seed.hexdata mn = baseconv.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed)) - ret = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn)).encode() + ret = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn)) # Internal error, so just die on fail compare_or_die(ret,'recomputed seed',hexseed,'original',e='Internal error') @@ -459,7 +475,7 @@ class Mnemonic (SeedSourceUnenc): msg('Invalid mnemonic: word #{} is not in the wordlist'.format(n)) return False - hexseed = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn)).encode() + hexseed = baseconv.tohex(mn,self.wl_id,self._mn2hex_pad(mn)) ret = baseconv.fromhex(hexseed,self.wl_id,self._hex2mn_pad(hexseed)) if len(hexseed) * 4 not in g.seed_lens: diff --git a/mmgen/tool.py b/mmgen/tool.py index b31e1a11..eb8d2130 100755 --- a/mmgen/tool.py +++ b/mmgen/tool.py @@ -43,12 +43,15 @@ def _create_call_sig(cmd,parsed=False): nargs = len(args) - len(dfls) + def get_type_from_ann(arg): + return ann[arg][1:] + (' or STDIN','')[parsed] if ann[arg] == 'sstr' else ann[arg].__name__ + if parsed: - c_args = [(a,'str' if ann[a] == 'sstr' else ann[a].__name__) for a in args[:nargs]] + c_args = [(a,get_type_from_ann(a)) for a in args[:nargs]] c_kwargs = [(a,dfls[n]) for n,a in enumerate(args[nargs:])] return c_args,dict(c_kwargs),'STDIN_OK' if c_args and ann[args[0]] == 'sstr' else flag else: - c_args = ['{} [{}]'.format(a,'str or STDIN' if ann[a] == 'sstr' else ann[a].__name__) for a in args[:nargs]] + c_args = ['{} [{}]'.format(a,get_type_from_ann(a)) for a in args[:nargs]] c_kwargs = ['"{}" [{}={!r}{}]'.format( a, type(dfls[n]).__name__, dfls[n], (' ' + ann[a] if a in ann else '')) @@ -85,14 +88,14 @@ def _usage(cmd=None,exit_val=1): for bc in MMGenToolCmd.__bases__: cls_info = bc.__doc__.strip().split('\n')[0] Msg(' {}{}\n'.format(cls_info[0].upper(),cls_info[1:])) - ucmds = bc.user_commands() + ucmds = bc._user_commands() max_w = max(map(len,ucmds)) for cmd in ucmds: if getattr(MMGenToolCmd,cmd).__doc__: Msg(' {:{w}} {}'.format(cmd,_create_call_sig(cmd),w=max_w)) Msg('') Msg(m2) - elif cmd in MMGenToolCmd.user_commands(): + elif cmd in MMGenToolCmd._user_commands(): msg('USAGE: {} {} {}'.format(g.prog_name,cmd,_create_call_sig(cmd))) else: die(1,"'{}': no such tool command".format(cmd)) @@ -101,6 +104,7 @@ def _usage(cmd=None,exit_val=1): def _process_args(cmd,cmd_args): c_args,c_kwargs,flag = _create_call_sig(cmd,parsed=True) + have_stdin_input = False if flag != 'VAR_ARGS': if len(cmd_args) < len(c_args): @@ -115,7 +119,14 @@ def _process_args(cmd,cmd_args): if sys.stdin.isatty(): raise BadFilename("Standard input is a TTY. Can't use '-' as a filename") else: - u_args[0] = sys.stdin.read().strip() + max_dlen_spec = '10kB' # limit input to 10KB for now + max_dlen = MMGenToolCmdUtil().bytespec(max_dlen_spec) + u_args[0] = os.read(0,max_dlen) +# try: u_args[0] = u_args[0].decode() +# except: pass + have_stdin_input = True + if len(u_args[0]) >= max_dlen: + die(2,'Maximum data input for this command is {}'.format(max_dlen_spec)) if not u_args[0]: die(2,'{}: ERROR: no output from previous command in pipe'.format(cmd)) @@ -144,13 +155,19 @@ def _process_args(cmd,cmd_args): _usage(cmd) def conv_type(arg,arg_name,arg_type): - if arg_type == 'bytes': pdie(arg,arg_name,arg_type) + if arg_type == 'bytes' and type(arg) != bytes: + die(1,"'Binary input data must be supplied via STDIN") + + if have_stdin_input and arg_type == 'str' and type(arg) == bytes: + arg = arg.decode().rstrip('\n') + if arg_type == 'bool': if arg.lower() in ('true','yes','1','on'): arg = True elif arg.lower() in ('false','no','0','off'): arg = False else: msg("'{}': invalid boolean value for keyword argument".format(arg)) _usage(cmd) + try: return __builtins__[arg_type](arg) except: @@ -164,24 +181,31 @@ def _process_args(cmd,cmd_args): return args,kwargs -def _process_result(ret,to_screen=False,pager=False): # returns a string or string subclass - do_ret = not to_screen +def _process_result(ret,pager=False,print_result=False): + """ + Convert result to something suitable for output to screen and return it. + If result is bytes and not convertible to utf8, output as binary using os.write(). + If 'print_result' is True, send the converted result directly to screen or + pager instead of returning it. + """ + def triage_result(o): + return o if not print_result else do_pager(o) if pager else Msg(o) + if issubclass(type(ret),str): - return ret if do_ret else do_pager(ret) if pager else Msg(ret) + return triage_result(ret) + elif issubclass(type(ret),int): + return triage_result(str(ret)) elif type(ret) == tuple: - o = '\n'.join([r.decode() if issubclass(type(r),bytes) else r for r in ret]) - return o if do_ret else do_pager(o) if pager else Msg(o) + return triage_result('\n'.join([r.decode() if issubclass(type(r),bytes) else r for r in ret])) elif issubclass(type(ret),bytes): - if do_ret: - try: return ret.decode() - except: return repr(ret) - else: - try: - o = ret.decode() - do_pager(o) if pager else Msg(o) - except: os.write(1,ret) + try: + o = ret.decode() + return o if not print_result else do_pager(o) if pager else Msg(o) + except: + # don't add NL to binary data if it can't be converted to utf8 + return ret if not print_result else os.write(1,ret) elif ret == True: - if do_ret: return '' + return True elif ret in (False,None): ydie(1,"tool command returned '{}'".format(ret)) else: @@ -202,7 +226,7 @@ dfl_wl_id = 'electrum' class MMGenToolCmdBase(object): @classmethod - def user_commands(cls): + def _user_commands(cls): return [e for e in dir(cls) if e[0] != '_' and getattr(cls,e).__doc__] @@ -220,7 +244,7 @@ class MMGenToolCmdUtil(MMGenToolCmdBase): def bytespec(self,dd_style_byte_specifier:str): "convert a byte specifier such as '1GB' into an integer" - return str(parse_bytespec(dd_style_byte_specifier)) + return parse_bytespec(dd_style_byte_specifier) def randhex(self,nbytes='32'): "print 'n' bytes (default 32) of random data in hex format" @@ -230,18 +254,22 @@ class MMGenToolCmdUtil(MMGenToolCmdBase): "reverse bytes of a hexadecimal string" return hexlify(unhexlify(hexstr.strip())[::-1]) - def hexlify(self,hexstr:'sstr'): - "display string in hexadecimal format" - return hexlify(hexstr.encode()) + def hexlify(self,infile:str): + "convert bytes in file to hexadecimal (use '-' for stdin)" + data = get_data_from_file(infile,dash=True,silent=True,binary=True) + return hexlify(data) + + def unhexlify(self,hexstr:'sstr'): + "convert hexadecimal value to bytes (warning: outputs binary data)" + return unhexlify(hexstr.encode()) def hexdump(self,infile:str,cols=8,line_nums=True): - "encode data into formatted hexadecimal form (file or stdin)" - return pretty_hexdump( - get_data_from_file(infile,dash=True,silent=True,binary=True), - cols=cols,line_nums=line_nums) + "create hexdump of data from file (use '-' for stdin)" + data = get_data_from_file(infile,dash=True,silent=True,binary=True) + return pretty_hexdump(data,cols=cols,line_nums=line_nums).rstrip() def unhexdump(self,infile:str): - "decode formatted hexadecimal data (file or stdin)" + "decode hexdump from file (use '-' for stdin) (warning: outputs binary data)" if g.platform == 'win': import msvcrt msvcrt.setmode(sys.stdout.fileno(),os.O_BINARY) @@ -258,10 +286,10 @@ class MMGenToolCmdUtil(MMGenToolCmdBase): if file_input: b = get_data_from_file(string_or_bytes,binary=True) elif hex_input: b = decode_pretty_hexdump(string_or_bytes) else: b = string_or_bytes - return sha256(sha256(b.encode()).digest()).hexdigest() + return sha256(sha256(b.encode()).digest()).hexdigest().encode() def id6(self,infile:str): - "generate 6-character MMGen ID for a file (or stdin)" + "generate 6-character MMGen ID for a file (use '-' for stdin)" return make_chksum_6( get_data_from_file(infile,dash=True,silent=True,binary=True)) @@ -270,30 +298,30 @@ class MMGenToolCmdUtil(MMGenToolCmdBase): return make_chksum_6(''.join(string.split())) def id8(self,infile:str): - "generate 8-character MMGen ID for a file (or stdin)" + "generate 8-character MMGen ID for a file (use '-' for stdin)" return make_chksum_8( get_data_from_file(infile,dash=True,silent=True,binary=True)) - def b58randenc(self): - "generate a random 32-byte number and convert it to base 58" - r = get_random(32) - return baseconv.b58encode(r,pad=True) + def randb58(self,nbytes=32,pad=True): + "generate random data (default: 32 bytes) and convert it to base 58" + return baseconv.b58encode(get_random(nbytes),pad=pad) - def strtob58(self,string:'sstr',pad=0): - "convert a string to base 58" - return baseconv.fromhex(hexlify(string.encode()),'b58',pad,tostr=True) + def bytestob58(self,infile:str,pad=0): + "convert bytes to base 58 (supply data via STDIN)" + data = get_data_from_file(infile,dash=True,silent=True,binary=True) + return baseconv.fromhex(hexlify(data),'b58',pad=pad,tostr=True) - def b58tostr(self,b58num:'sstr'): - "convert a base 58 number to a string" - return unhexlify(baseconv.tohex(b58num,'b58')) + def b58tobytes(self,b58num:'sstr',pad=0): + "convert a base 58 number to bytes (warning: outputs binary data)" + return unhexlify(baseconv.tohex(b58num,'b58',pad=pad)) def hextob58(self,hexstr:'sstr',pad=0): "convert a hexadecimal number to base 58" - return baseconv.fromhex(hexstr.encode(),'b58',pad,tostr=True) + return baseconv.fromhex(hexstr.encode(),'b58',pad=pad,tostr=True) def b58tohex(self,b58num:'sstr',pad=0): "convert a base 58 number to hexadecimal" - return baseconv.tohex(b58num,'b58',pad) + return baseconv.tohex(b58num,'b58',pad=pad) def hextob58chk(self,hexstr:'sstr'): "convert a hexadecimal number to base58-check encoding" @@ -306,11 +334,11 @@ class MMGenToolCmdUtil(MMGenToolCmdBase): return _b58chk_decode(b58chk_num) def hextob32(self,hexstr:'sstr',pad=0): - "convert a hexadecimal number to base 32" + "convert a hexadecimal number to MMGen's flavor of base 32" return baseconv.fromhex(hexstr.encode(),'b32',pad,tostr=True) def b32tohex(self,b32num:'sstr',pad=0): - "convert a base 32 number to hexadecimal" + "convert an MMGen-flavor base 32 number to hexadecimal" return baseconv.tohex(b32num.upper(),'b32',pad) class MMGenToolCmdCoin(MMGenToolCmdBase): @@ -353,12 +381,14 @@ class MMGenToolCmdCoin(MMGenToolCmdBase): def wif2redeem_script(self,wifkey:'sstr'): # new "convert a WIF private key to a Segwit P2SH-P2WPKH redeem script" + assert opt.type == 'segwit','This command is meaningful only for --type=segwit' init_generators() privhex = PrivKey(wif=wifkey) return ag.to_segwit_redeem_script(kg.to_pubhex(privhex)) def wif2segwit_pair(self,wifkey:'sstr'): "generate both a Segwit P2SH-P2WPKH redeem script and address from WIF" + assert opt.type == 'segwit','This command is meaningful only for --type=segwit' init_generators() pubhex = kg.to_pubhex(PrivKey(wif=wifkey)) addr = ag.to_addr(pubhex) @@ -378,10 +408,14 @@ class MMGenToolCmdCoin(MMGenToolCmdBase): def pubhex2addr(self,pubkeyhex:'sstr'): "convert a hex pubkey to an address" - return self.pubhash2addr(hash160(pubkeyhex.encode()).decode()) + if opt.type == 'segwit': + return g.proto.pubhex2segwitaddr(pubkeyhex.encode()) + else: + return self.pubhash2addr(hash160(pubkeyhex.encode()).decode()) def pubhex2redeem_script(self,pubkeyhex:'sstr'): # new "convert a hex pubkey to a Segwit P2SH-P2WPKH redeem script" + assert opt.type == 'segwit','This command is meaningful only for --type=segwit' return g.proto.pubhex2redeem_script(pubkeyhex) def pubhash2addr(self,pubhashhex:'sstr'): @@ -392,8 +426,8 @@ class MMGenToolCmdCoin(MMGenToolCmdBase): init_generators('at') return g.proto.pubhash2addr(pubhashhex.encode(),at.addr_fmt=='p2sh') - def addr2hexaddr(self,addr:'sstr'): - "convert coin address from base58 to hex format" + def addr2pubhash(self,addr:'sstr'): + "convert coin address to public key hash" return g.proto.verify_addr(addr,CoinAddr.hex_width,return_dict=True)['hex'] class MMGenToolCmdMnemonic(MMGenToolCmdBase): @@ -403,14 +437,11 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase): IMPORTANT NOTE: Though MMGen mnemonics use the Electrum wordlist, they're computed using a different algorithm and are NOT Electrum-compatible! """ - def _do_random_mn(self,nbytes:int,wordlist:str): + def _do_random_mn(self,nbytes:int,wordlist_id:str): + assert nbytes in (16,24,32), 'nbytes must be 16, 24 or 32' hexrand = hexlify(get_random(nbytes)) Vmsg('Seed: {}'.format(hexrand)) - for wl_id in ([wordlist],wordlists)[wordlist=='all']: - if wordlist == 'all': # TODO - Msg('{} mnemonic:'.format(capfirst(wl_id))) - mn = baseconv.fromhex(hexrand,wl_id) - return ' '.join(mn) + return self.hex2mn(hexrand,wordlist_id=wordlist_id) def mn_rand128(self,wordlist=dfl_wl_id): "generate random 128-bit mnemonic" @@ -424,13 +455,19 @@ class MMGenToolCmdMnemonic(MMGenToolCmdBase): "generate random 256-bit mnemonic" return self._do_random_mn(32,wordlist) - def hex2mn(self,hexstr:'sstr',wordlist=dfl_wl_id): + def hex2mn(self,hexstr:'sstr',wordlist_id=dfl_wl_id): "convert a 16, 24 or 32-byte hexadecimal number to a mnemonic" - return ' '.join(baseconv.fromhex(hexstr.encode(),wordlist)) + opt.out_fmt = 'words' + from mmgen.seed import SeedSource + s = SeedSource(seed=unhexlify(hexstr)) + s._format() + return ' '.join(s.ssdata.mnemonic) def mn2hex(self,seed_mnemonic:'sstr',wordlist=dfl_wl_id): "convert a 12, 18 or 24-word mnemonic to a hexadecimal number" - return baseconv.tohex(seed_mnemonic.split(),wordlist) + opt.quiet = True + from mmgen.seed import SeedSource + return SeedSource(in_data=seed_mnemonic,in_fmt='words').seed.hexdata def mn_stats(self,wordlist=dfl_wl_id): "show stats for mnemonic wordlist" @@ -448,11 +485,15 @@ class MMGenToolCmdFile(MMGenToolCmdBase): def addrfile_chksum(self,mmgen_addrfile:str): "compute checksum for MMGen address file" + opt.yes = True + opt.quiet = True from mmgen.addr import AddrList return AddrList(mmgen_addrfile).chksum def keyaddrfile_chksum(self,mmgen_keyaddrfile:str): "compute checksum for MMGen key-address file" + opt.yes = True + opt.quiet = True from mmgen.addr import KeyAddrList return KeyAddrList(mmgen_keyaddrfile).chksum diff --git a/mmgen/util.py b/mmgen/util.py index 28627262..7fc01770 100755 --- a/mmgen/util.py +++ b/mmgen/util.py @@ -309,7 +309,8 @@ class baseconv(object): if pad: assert type(pad) == bool, "'pad' must be boolean type" d = dict(pad_map) - assert len(s) in d, 'Invalid data length for b58{}code(pad=True)'.format(op) + m = 'Invalid data length for b58{}code(pad=True) (must be one of {})' + assert len(s) in d, m.format(op,repr([e[0] for e in pad_map])) return d[len(s)] else: return None @@ -349,7 +350,7 @@ class baseconv(object): deconv = [wl.index(words[::-1][i])*(base**i) for i in range(len(words))] ret = ('{:0{w}x}'.format(sum(deconv),w=pad or 0)) - return ('','0')[len(ret) % 2] + ret + return (('','0')[len(ret) % 2] + ret).encode() # return bytes, for consistency with hexlify() @classmethod def fromhex(cls,hexnum,wl_id,pad=None,tostr=False): @@ -465,11 +466,10 @@ def compare_or_die(val1, desc1, val2, desc2, e='Error'): def open_file_or_exit(filename,mode,silent=False): try: - f = open(filename, mode) + return open(filename, mode) except: op = ('writing','reading')['r' in mode] die(2,("Unable to open file '{}' for {}".format(filename,op),'')[silent]) - return f def check_file_type_and_access(fname,ftype,blkdev_ok=False): @@ -681,7 +681,7 @@ def mmgen_decrypt_file_maybe(fn,desc='',silent=False): have_enc_ext = get_extension(fn) == g.mmenc_ext if have_enc_ext or not is_utf8(d): m = ('Attempting to decrypt','Decrypting')[have_enc_ext] - msg("{} {} '{}'".format(m,desc,fn)) + qmsg("{} {} '{}'".format(m,desc,fn)) from mmgen.crypto import mmgen_decrypt_retry d = mmgen_decrypt_retry(d,desc) return d @@ -699,18 +699,21 @@ def get_data_from_user(desc='data',silent=False): # user input MUST be UTF-8 dmsg('User input: [{}]'.format(data)) return data -def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False,require_utf8=False): - if dash and infile == '-': return sys.stdin.read() +def get_data_from_file(infile,desc='data',dash=False,silent=False,binary=False): + if not opt.quiet and not silent and desc: qmsg("Getting {} from file '{}'".format(desc,infile)) - f = open_file_or_exit(infile,('r','rb')[bool(binary)],silent=silent) - data = f.read() - f.close() - if require_utf8: - try: - if binary: data = data.decode() - else: data.encode() - except: die(1,'{} data must be UTF-8 encoded.'.format(capfirst(desc))) + + mode = ('r','rb')[bool(binary)] + + if dash and infile == '-': + data = os.fdopen(0,mode).read(g.max_input_size+1) + else: + data = open_file_or_exit(infile,mode,silent=silent).read(g.max_input_size+1) + + if len(data) == g.max_input_size + 1: + raise MaxInputSizeExceeded('Too much input data! Max input data size: {} bytes'.format(g.max_input_size)) + return data def pwfile_reuse_warning(): diff --git a/scripts/test-release.sh b/scripts/test-release.sh index e5a98195..4fc09464 100755 --- a/scripts/test-release.sh +++ b/scripts/test-release.sh @@ -331,28 +331,27 @@ f_ltc_rt='Regtest (Bob and Alice) mode tests for LTC completed' i_tool2='Tooltest2' s_tool2="The following tests will run '$tooltest2_py' for all supported coins" t_tool2=( - "$tooltest2_py --quiet --non-coin-dependent" - "$tooltest2_py --quiet --coin=btc --coin-dependent" - "$tooltest2_py --quiet --coin=btc --testnet=1 --coin-dependent" - "$tooltest2_py --quiet --coin=ltc --coin-dependent" - "$tooltest2_py --quiet --coin=ltc --testnet=1 --coin-dependent" - "$tooltest2_py --quiet --coin=bch --coin-dependent" - "$tooltest2_py --quiet --coin=bch --testnet=1 --coin-dependent" - "$tooltest2_py --quiet --coin=zec --coin-dependent" - "$tooltest2_py --quiet --coin=zec --type=zcash_z --coin-dependent" - "$tooltest2_py --quiet --coin=xmr --coin-dependent" - "$tooltest2_py --quiet --coin=dash --coin-dependent" - "$tooltest2_py --quiet --coin=eth --coin-dependent" - "$tooltest2_py --quiet --coin=eth --testnet=1 --coin-dependent" - "$tooltest2_py --quiet --coin=eth --token=mm1 --coin-dependent" - "$tooltest2_py --quiet --coin=eth --token=mm1 --testnet=1 --coin-dependent" - "$tooltest2_py --quiet --coin=etc --coin-dependent") + "$tooltest2_py --quiet" + "$tooltest2_py --quiet --coin=btc" + "$tooltest2_py --quiet --coin=btc --testnet=1" + "$tooltest2_py --quiet --coin=ltc" + "$tooltest2_py --quiet --coin=ltc --testnet=1" + "$tooltest2_py --quiet --coin=bch" + "$tooltest2_py --quiet --coin=bch --testnet=1" + "$tooltest2_py --quiet --coin=zec" + "$tooltest2_py --quiet --coin=zec --type=zcash_z" + "$tooltest2_py --quiet --coin=xmr" + "$tooltest2_py --quiet --coin=dash" + "$tooltest2_py --quiet --coin=eth" + "$tooltest2_py --quiet --coin=eth --testnet=1" + "$tooltest2_py --quiet --coin=eth --token=mm1" + "$tooltest2_py --quiet --coin=eth --token=mm1 --testnet=1" + "$tooltest2_py --quiet --coin=etc") f_tool2='tooltest2 tests completed' i_tool='Tooltest' s_tool="The following tests will run '$tooltest_py' for all supported coins" t_tool=( - "$tooltest_py --coin=btc util" "$tooltest_py --coin=btc cryptocoin" "$tooltest_py --coin=btc mnemonic" "$tooltest_py --coin=ltc cryptocoin" diff --git a/test/common.py b/test/common.py index 74b201ac..0736335c 100755 --- a/test/common.py +++ b/test/common.py @@ -17,9 +17,14 @@ # along with this program. If not, see . """ -common.py: Shared routines for the test suites +common.py: Shared routines and data for the MMGen test suites """ +sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks' + +ref_kafile_pass = 'kafile password' +ref_kafile_hash_preset = '1' + class TestSuiteException(Exception): pass class TestSuiteFatalException(Exception): pass diff --git a/test/pexpect.py b/test/pexpect.py index 156f22b8..3813f8d6 100755 --- a/test/pexpect.py +++ b/test/pexpect.py @@ -68,10 +68,11 @@ class MMGenPexpect(object): self.req_exit_val = 0 self.skip_ok = False - def do_decrypt_ka_data(self,hp,pw,desc='key-address data',check=True): - self.hash_preset(desc,hp) + def do_decrypt_ka_data(self,hp,pw,desc='key-address data',check=True,have_yes_opt=False): +# self.hash_preset(desc,hp) self.passphrase(desc,pw) - self.expect('Check key-to-address validity? (y/N): ',('n','y')[check]) + if not have_yes_opt: + self.expect('Check key-to-address validity? (y/N): ',('n','y')[check]) def view_tx(self,view): self.expect('View.* transaction.*\? .*: ',view,regex=True) diff --git a/test/ref/keyaddrfile_password b/test/ref/keyaddrfile_password new file mode 100644 index 00000000..6ab74c00 --- /dev/null +++ b/test/ref/keyaddrfile_password @@ -0,0 +1 @@ +kafile password diff --git a/test/test_py_d/common.py b/test/test_py_d/common.py index 123cf67f..2e338479 100755 --- a/test/test_py_d/common.py +++ b/test/test_py_d/common.py @@ -62,12 +62,8 @@ ref_bw_hash_preset = '1' ref_bw_file = 'wallet.mmbrain' ref_bw_file_spc = 'wallet-spaced.mmbrain' -ref_kafile_pass = 'kafile password' -ref_kafile_hash_preset = '1' - ref_enc_fn = 'sample-text.mmenc' tool_enc_passwd = "Scrypt it, don't hash it!" -sample_text = 'The Times 03/Jan/2009 Chancellor on brink of second bailout for banks\n' chksum_pat = r'\b[A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4} [A-F0-9]{4}\b' def ok_msg(): diff --git a/test/test_py_d/ts_ethdev.py b/test/test_py_d/ts_ethdev.py index 4d11feed..753577f6 100755 --- a/test/test_py_d/ts_ethdev.py +++ b/test/test_py_d/ts_ethdev.py @@ -362,7 +362,7 @@ class TestSuiteEthdev(TestSuiteBase,TestSuiteShared): return t def txcreate1(self): - # valid_keypresses = 'adrMmeqpvwl' + # valid_keypresses = EthereumTwUnspentOutputs.key_mappings.keys() menu = ['a','d','r','M','D','e','m','m'] # include one invalid keypress, 'D' args = ['98831F3A:E:1,123.456'] return self.txcreate(args=args,menu=menu,acct='1',non_mmgen_inputs=1) diff --git a/test/test_py_d/ts_main.py b/test/test_py_d/ts_main.py index b0ef237d..19d59daf 100755 --- a/test/test_py_d/ts_main.py +++ b/test/test_py_d/ts_main.py @@ -277,7 +277,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): out = [] for d in tx_data.values(): - al = adata.addrlist(d['al_id']) + al = adata.addrlist(al_id=d['al_id']) for n,(idx,coinaddr) in enumerate(al.addrpairs()): lbl = get_label(do_shuffle=True) out.append(self._create_fake_unspent_entry(coinaddr,d['al_id'],idx,lbl,segwit=d['segwit'])) @@ -556,7 +556,7 @@ class TestSuiteMain(TestSuiteBase,TestSuiteShared): args=['-H','{},{}'.format(rf,hincog_offset),'-l',str(hincog_seedlen)]) def txsign_keyaddr(self,keyaddr_file,txfile): - t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,'-M',keyaddr_file,txfile]) + t = self.spawn('mmgen-txsign', ['-d',self.tmpdir,'-p1','-M',keyaddr_file,txfile]) t.license() t.do_decrypt_ka_data(hp='1',pw=self.kapasswd) t.view_tx('n') diff --git a/test/test_py_d/ts_ref.py b/test/test_py_d/ts_ref.py index c384f7eb..e8bc1c35 100755 --- a/test/test_py_d/ts_ref.py +++ b/test/test_py_d/ts_ref.py @@ -108,9 +108,9 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): af = joinpath(ref_dir,(subdir or self.ref_subdir,'')[ftype=='passwd'],af_fn) coin_arg = [] if coin == None else ['--coin='+coin] tool_cmd = ftype.replace('segwit','').replace('bech32','')+'file_chksum' - t = self.spawn('mmgen-tool',coin_arg+[tool_cmd,af]+add_args) + t = self.spawn('mmgen-tool',coin_arg+['-p1',tool_cmd,af]+add_args) if ftype == 'keyaddr': - t.do_decrypt_ka_data(hp=ref_kafile_hash_preset,pw=ref_kafile_pass) + t.do_decrypt_ka_data(hp=ref_kafile_hash_preset,pw=ref_kafile_pass,have_yes_opt=True) rc = self.chk_data[ 'ref_' + ftype + 'file_chksum' + ('_'+coin.lower() if coin else '') + ('_'+mmtype if mmtype else '')] @@ -161,5 +161,5 @@ class TestSuiteRef(TestSuiteBase,TestSuiteShared): t.written_to_file('Decrypted data') dec_txt = read_from_file(dec_file) imsg_r(dec_txt) - cmp_or_die(sample_text,dec_txt) + cmp_or_die(sample_text+'\n',dec_txt) # file adds a newline to sample_text return t diff --git a/test/tooltest.py b/test/tooltest.py index 832598d3..1a0bd590 100755 --- a/test/tooltest.py +++ b/test/tooltest.py @@ -58,30 +58,6 @@ cmd_args = opts.init(opts_data,add_opts=['exact_output','profile']) from collections import OrderedDict cmd_data = OrderedDict([ - ('util', { - 'desc': 'base conversion, hashing and file utilities', - 'cmd_data': OrderedDict([ - ('strtob58', ()), - ('b58tostr', ('strtob58','io')), - ('hextob58', ()), - ('b58tohex', ('hextob58','io')), - ('b58randenc', ()), - ('hextob32', ()), - ('b32tohex', ('hextob32','io')), - ('randhex', ()), - ('id6', ()), - ('id8', ()), - ('str2id6', ()), - ('hash160', ()), - ('hash256', ()), - ('hexreverse', ()), - ('hexlify', ()), - ('hexdump', ()), - ('unhexdump', ('hexdump','io')), - ('rand2file', ()), - ]) - } - ), ('cryptocoin', { 'desc': 'Cryptocoin address/key commands', 'cmd_data': OrderedDict([ @@ -93,9 +69,9 @@ cmd_data = OrderedDict([ ('privhex2pubhex', ('wif2hex','o3')), # segwit only ('pubhex2addr', ('privhex2pubhex','o3')), # segwit only ('hex2wif', ('wif2hex','io2')), # uncomp, comp - ('addr2hexaddr', ('randpair','o4'))] + # uncomp, comp, bech32 + ('addr2pubhash', ('randpair','o4'))] + # uncomp, comp, bech32 ([],[ - ('pubhash2addr', ('addr2hexaddr','io4')) # uncomp, comp, bech32 + ('pubhash2addr', ('addr2pubhash','io4')) # uncomp, comp, bech32 ])[opt.type != 'zcash_z'] + ([],[ ('pubhex2redeem_script', ('privhex2pubhex','o3')), @@ -196,7 +172,7 @@ if opt.list_names: ignore = () from mmgen.tool import MMGenToolCmd uc = sorted( - set(MMGenToolCmd.user_commands()) - + set(MMGenToolCmd._user_commands()) - set(ignore) - set(tested_in['tooltest.py']) - set(tested_in['tooltest2.py']) - @@ -313,47 +289,6 @@ def ok_or_die(val,chk_func,s,skip_ok=False): class MMGenToolTestCmds(object): - # Util - def strtob58(self,name): tu.run_cmd_out(name,getrandstr(16)) - def b58tostr(self,name,f1,f2): tu.run_cmd_chk(name,f1,f2) - def hextob58(self,name): tu.run_cmd_out(name,getrandhex(32)) - def b58tohex(self,name,f1,f2): tu.run_cmd_chk(name,f1,f2,strip_hex=True) - def b58randenc(self,name): - ret = tu.run_cmd_out(name,Return=True) - ok_or_die(ret,is_b58_str,'base 58 string') - def hextob32(self,name): tu.run_cmd_out(name,getrandhex(24)) - def b32tohex(self,name,f1,f2): tu.run_cmd_chk(name,f1,f2,strip_hex=True) - def randhex(self,name): - ret = tu.run_cmd_out(name,Return=True) - ok_or_die(ret,binascii.unhexlify,'hex string') - def id6(self,name): tu.run_cmd_randinput(name) - def id8(self,name): tu.run_cmd_randinput(name) - def str2id6(self,name): - s = getrandstr(120,no_space=True) - s2 = ' {} {} {} {} {} '.format(s[:3],s[3:9],s[9:29],s[29:50],s[50:120]) - ret1 = tu.run_cmd(name,[s],extra_msg='unspaced input'); ok() - ret2 = tu.run_cmd(name,[s2],extra_msg='spaced input') - cmp_or_die(ret1,ret2) - vmsg('Returned: {}'.format(ret1)) - ok() - def hash160(self,name): tu.run_cmd_out(name,getrandhex(16)) - def hash256(self,name): tu.run_cmd_out(name,getrandstr(16)) - def hexreverse(self,name): tu.run_cmd_out(name,getrandhex(24)) - def hexlify(self,name): tu.run_cmd_out(name,getrandstr(24)) - def hexdump(self,name): tu.run_cmd_randinput(name,strip=False) - def unhexdump(self,name,fn1,fn2): - ret = tu.run_cmd(name,[fn2],strip=False,binary=True) - orig = read_from_file(fn1,binary=True) - cmp_or_die(orig,ret) - ok() - def rand2file(self,name): - of = name + '.out' - dlen = 1024 - tu.run_cmd(name,[of,str(1024),'threads=4','silent=1'],strip=False) - d = read_from_tmpfile(cfg,of,binary=True) - cmp_or_die(dlen,len(d)) - ok() - # Cryptocoin def randwif(self,name): for n,k in enumerate(['',maybe_compressed]): @@ -398,7 +333,7 @@ class MMGenToolTestCmds(object): for n,fi,fo,k in ((1,f1,f2,''),(2,f3,f4,maybe_compressed)): ao = ['--type='+k] if k else [] ret = tu.run_cmd_chk(name,fi,fo,add_opts=ao) - def addr2hexaddr(self,name,f1,f2,f3,f4): + def addr2pubhash(self,name,f1,f2,f3,f4): for n,f,m,ao in ( (1,f1,'',[]), (2,f2,'from {}'.format(maybe_compressed),[]), @@ -418,9 +353,12 @@ class MMGenToolTestCmds(object): tu.run_cmd_out(name,addr,add_opts=maybe_type_compressed,fn_idx=3) # what about uncompressed? def pubhex2redeem_script(self,name,f1,f2,f3): # from above addr = read_from_file(f3).strip() - tu.run_cmd_out(name,addr,fn_idx=3) - rs = read_from_tmpfile(cfg,name+'3.out').strip() + tu.run_cmd_out(name,addr,add_opts=maybe_type_segwit,fn_idx=3) + type_save = opt.type + opt.type = 'segwit' + rs = read_from_tmpfile(cfg,'privhex2pubhex3.out').strip() tu.run_cmd_out('pubhex2addr',rs,add_opts=maybe_type_segwit,fn_idx=3,hush=True) + opt.type = type_save addr1 = read_from_tmpfile(cfg,'pubhex2addr3.out').strip() addr2 = read_from_tmpfile(cfg,'randpair3.out').split()[1] cmp_or_die(addr1,addr2) @@ -440,20 +378,22 @@ class MMGenToolTestCmds(object): def pipetest(self,name,f1,f2,f3): wif = read_from_file(f3).split()[0] - cmd = ( '{c} {a} wif2hex {wif} | ' + - '{c} {a} --type=compressed privhex2pubhex - | ' + - '{c} {a} pubhex2redeem_script - | ' + - '{c} {a} --type=segwit pubhex2addr -').format( + cmd = ( '{c} {a} wif2hex {wif}' + + ' | {c} {a} --type=compressed privhex2pubhex -' + + ' | {c} {a} --type=segwit pubhex2redeem_script -' + + ' | {c} {a} hash160 -' + + ' | {c} {a} --type=segwit pubhash2addr -').format( c=' '.join(spawn_cmd), a=' '.join(add_spawn_args), wif=wif) test_msg('command piping') if opt.verbose: sys.stderr.write(green('Executing ') + cyan(cmd) + '\n') - p = subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,shell=True) + p = subprocess.Popen(cmd,stdout=subprocess.PIPE,shell=True) res = p.stdout.read().decode().strip() + p.wait() addr = read_from_tmpfile(cfg,'wif2addr3.out').strip() - cmp_or_die(res,addr) + cmp_or_die(addr,res) ok() # Mnemonic diff --git a/test/tooltest2.py b/test/tooltest2.py index daca98a7..4a539d2e 100755 --- a/test/tooltest2.py +++ b/test/tooltest2.py @@ -36,6 +36,9 @@ os.environ['MMGEN_TEST_SUITE'] = '1' from mmgen.common import * from test.common import * from mmgen.obj import is_wif,is_coin_addr +from mmgen.seed import is_mnemonic + +def is_str(s): return type(s) == str opts_data = lambda: { 'desc': "Simple test suite for the 'mmgen-tool' utility", @@ -43,10 +46,9 @@ opts_data = lambda: { 'options': """ -h, --help Print this help message -C, --coverage Produce code coverage info using trace module --d, --coin-dependent Run only coin-dependent tests --D, --non-coin-dependent Run only non-coin-dependent tests +-d, --die-on-missing Abort if no test data found for given command --, --longhelp Print help message for long options (common options) --l, --list-tests List the tests in this test suite +-l, --list-tests List the test groups in this test suite -L, --list-tested-cmds Output the 'mmgen-tool' commands that are tested by this test suite -n, --names Print command names instead of descriptions -q, --quiet Produce quieter output @@ -63,170 +65,480 @@ If no command is given, the whole suite of tests is run. """ } -tests = ( - ('util', 'base conversion, hashing and file utilities', - ( - ('b58chktohex','conversion from base58chk to hex', [ - ( ['eFGDJPketnz'], 'deadbeef' ), - ( ['5CizhNNRPYpBjrbYX'], 'deadbeefdeadbeef' ), - ( ['5qCHTcgbQwprzjWrb'], 'ffffffffffffffff' ), - ( ['111111114FCKVB'], '0000000000000000' ), - ( ['3QJmnh'], '' ), - ( ['1111111111111111111114oLvT2'], '000000000000000000000000000000000000000000' ), - ]), - ('hextob58chk','conversion from hex to base58chk', [ - ( ['deadbeef'], 'eFGDJPketnz' ), - ( ['deadbeefdeadbeef'], '5CizhNNRPYpBjrbYX' ), - ( ['ffffffffffffffff'], '5qCHTcgbQwprzjWrb' ), - ( ['0000000000000000'], '111111114FCKVB' ), - ( [''], '3QJmnh' ), - ( ['000000000000000000000000000000000000000000'], '1111111111111111111114oLvT2' ), - ]), - ('bytespec',"conversion of 'dd'-style byte specifier to bytes", [ - ( ['1G'], str(1024*1024*1024) ), - ( ['1234G'], str(1234*1024*1024*1024) ), - ( ['1GB'], str(1000*1000*1000) ), - ( ['1234GB'], str(1234*1000*1000*1000) ), - ( ['1.234MB'], str(1234*1000) ), - ( ['1.234567M'], str(int(Decimal('1.234567')*1024*1024)) ), - ( ['1234'], str(1234) ), - ]), - ), - ), - ('wallet', 'MMGen wallet operations', - ( - ('gen_key','generation of single key from wallet', [ - ( ['98831F3A:11','wallet=test/ref/98831F3A.mmwords'], - '5JKLcdYbhP6QQ4BXc9HtjfqJ79FFRXP2SZTKUyEuyXJo9QSFUkv' - ), - ( ['98831F3A:C:11','wallet=test/ref/98831F3A.mmwords'], - 'L2LwXv94XTU2HjCbJPXCFuaHjrjucGipWPWUi1hkM5EykgektyqR' - ), - ( ['98831F3A:B:11','wallet=test/ref/98831F3A.mmwords'], - 'L2K4Y9MWb5oUfKKZtwdgCm6FLZdUiWJDHjh9BYxpEvtfcXt4iM5g' - ), - ( ['98831F3A:S:11','wallet=test/ref/98831F3A.mmwords'], - 'KwmkkfC9GghnJhnKoRXRn5KwGCgXrCmDw6Uv83NzE4kJS5axCR9A' - ), - ]), - ('gen_addr','generation of single address from wallet', [ - ( ['98831F3A:11','wallet=test/ref/98831F3A.mmwords'], - '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm' - ), - ( ['98831F3A:L:11','wallet=test/ref/98831F3A.mmwords'], - '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm' - ), - ( ['98831F3A:C:11','wallet=test/ref/98831F3A.mmwords'], - '1MPsZ7BY9qikqfPxqmrovE8gLDX2rYArZk' - ), - ( ['98831F3A:B:11','wallet=test/ref/98831F3A.mmwords'], - 'bc1qxptlvmwaymaxa7pxkr2u5pn7c0508stcncv7ms' - ), - ( ['98831F3A:S:11','wallet=test/ref/98831F3A.mmwords'], - '3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn' - ), - ]), - ), - ), - ('cryptocoin', 'coin-dependent utilities', - ( - ('randwif','random WIF key', { - 'btc_mainnet': [ ( [], is_wif, ['-r0'] ) ], - 'btc_testnet': [ ( [], is_wif, ['-r0'] ) ], - }), - ('randpair','random key/address pair', { - 'btc_mainnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ], - 'btc_testnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ], - }), - ('wif2addr','WIF-to-address conversion', { - 'btc_mainnet': [ - ( ['5JKLcdYbhP6QQ4BXc9HtjfqJ79FFRXP2SZTKUyEuyXJo9QSFUkv'], - '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm', ['--type=legacy'], 'opt.type="legacy"' ), - ( ['L2LwXv94XTU2HjCbJPXCFuaHjrjucGipWPWUi1hkM5EykgektyqR'], - '1MPsZ7BY9qikqfPxqmrovE8gLDX2rYArZk', ['--type=compressed'], 'opt.type="compressed"' ), - ( ['KwmkkfC9GghnJhnKoRXRn5KwGCgXrCmDw6Uv83NzE4kJS5axCR9A'], - '3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn', ['--type=segwit'], 'opt.type="segwit"' ), - ( ['L2K4Y9MWb5oUfKKZtwdgCm6FLZdUiWJDHjh9BYxpEvtfcXt4iM5g'], - 'bc1qxptlvmwaymaxa7pxkr2u5pn7c0508stcncv7ms', ['--type=bech32'], 'opt.type="bech32"' ), - ], - }), - ), - ), -# TODO: compressed address files are missing -# 'addrfile_compressed_chk': { -# 'btc': ('A33C 4FDE F515 F5BC','6C48 AA57 2056 C8C8'), -# 'ltc': ('3FC0 8F03 C2D6 BD19','4C0A 49B6 2DD1 1BE0'), - ('file', 'Operations with MMGen files', - ( - ('addrfile_chksum','address file checksums', { - 'btc_mainnet': [ - ( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].addrs'], - '6FEF 6FB9 7B13 5D91'), - ( ['test/ref/98831F3A-S[1,31-33,500-501,1010-1011].addrs'], - '06C1 9C87 F25C 4EE6'), - ( ['test/ref/98831F3A-B[1,31-33,500-501,1010-1011].addrs'], - '9D2A D4B6 5117 F02E'), - ], - 'btc_testnet': [ - ( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].testnet.addrs'], - '424E 4326 CFFE 5F51'), - ( ['test/ref/98831F3A-S[1,31-33,500-501,1010-1011].testnet.addrs'], - '072C 8B07 2730 CB7A'), - ( ['test/ref/98831F3A-B[1,31-33,500-501,1010-1011].testnet.addrs'], - '0527 9C39 6C1B E39A'), - ], - 'ltc_mainnet': [ - ( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].addrs'], - 'AD52 C3FE 8924 AAF0'), - ( ['test/ref/litecoin/98831F3A-LTC-S[1,31-33,500-501,1010-1011].addrs'], - '63DF E42A 0827 21C3'), - ( ['test/ref/litecoin/98831F3A-LTC-B[1,31-33,500-501,1010-1011].addrs'], - 'FF1C 7939 5967 AB82'), - ], - 'ltc_testnet': [ - ( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.addrs'], - '4EBE 2E85 E969 1B30'), - ( ['test/ref/litecoin/98831F3A-LTC-S[1,31-33,500-501,1010-1011].testnet.addrs'], - '5DD1 D186 DBE1 59F2'), - ( ['test/ref/litecoin/98831F3A-LTC-B[1,31-33,500-501,1010-1011].testnet.addrs'], - 'ED3D 8AA4 BED4 0B40'), - ], - 'zec_mainnet': [ - ( ['test/ref/zcash/98831F3A-ZEC-C[1,31-33,500-501,1010-1011].addrs'],'903E 7225 DD86 6E01'), ], - 'zec_z_mainnet': [ - ( ['test/ref/zcash/98831F3A-ZEC-Z[1,31-33,500-501,1010-1011].addrs'],'9C7A 72DC 3D4A B3AF'), ], - 'xmr_mainnet': [ - ( ['test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs'],'4369 0253 AC2C 0E38'), ], - 'dash_mainnet': [ - ( ['test/ref/dash/98831F3A-DASH-C[1,31-33,500-501,1010-1011].addrs'],'FBC1 6B6A 0988 4403'), ], - 'eth_mainnet': [ - ( ['test/ref/ethereum/98831F3A-ETH[1,31-33,500-501,1010-1011].addrs'],'E554 076E 7AF6 66A3'), ], - 'etc_mainnet': [ - ( ['test/ref/ethereum_classic/98831F3A-ETC[1,31-33,500-501,1010-1011].addrs'], - 'E97A D796 B495 E8BC'), ], - }), - ('txview','transaction file view', { - 'btc_mainnet': [ ( ['test/ref/0B8D5A[15.31789,14,tl=1320969600].rawtx'], None ), ], - 'btc_testnet': [ ( ['test/ref/0C7115[15.86255,14,tl=1320969600].testnet.rawtx'], None ), ], - 'bch_mainnet': [ ( ['test/ref/460D4D-BCH[10.19764,tl=1320969600].rawtx'], None ), ], - 'bch_testnet': [ ( ['test/ref/359FD5-BCH[6.68868,tl=1320969600].testnet.rawtx'], None ), ], - 'ltc_mainnet': [ ( ['test/ref/litecoin/AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx'], None ), ], - 'ltc_testnet': [ ( ['test/ref/litecoin/A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx'], - None ), ], - 'eth_mainnet': [ ( ['test/ref/ethereum/88FEFD-ETH[23.45495,40000].rawtx'], None ), ], - 'eth_testnet': [ ( ['test/ref/ethereum/B472BD-ETH[23.45495,40000].testnet.rawtx'], None ), ], - 'mm1_mainnet': [ ( ['test/ref/ethereum/5881D2-MM1[1.23456,50000].rawtx'], None ), ], - 'mm1_testnet': [ ( ['test/ref/ethereum/6BDB25-MM1[1.23456,50000].testnet.rawtx'], None ), ], - 'etc_mainnet': [ ( ['test/ref/ethereum_classic/ED3848-ETC[1.2345,40000].rawtx'], None ), ], - }), - ), - ), -) +sample_text_hexdump = ( + '000000: 5468 6520 5469 6d65 7320 3033 2f4a 616e\n' + + '000010: 2f32 3030 3920 4368 616e 6365 6c6c 6f72\n' + + '000020: 206f 6e20 6272 696e 6b20 6f66 2073 6563\n' + + '000030: 6f6e 6420 6261 696c 6f75 7420 666f 7220\n' + + '000040: 6261 6e6b 73' ) -def do_cmd(cdata): - cmd_name,desc,data = cdata - if type(data) == dict: - if opt.non_coin_dependent: return +kafile_opts = ['-p1','-Ptest/ref/keyaddrfile_password'] +kafile_code = ( + "\nopt.hash_preset = '1'" + + "\nopt.set_by_user = ['hash_preset']" + + "\nopt.use_old_ed25519 = None" + + "\nopt.passwd_file = 'test/ref/keyaddrfile_password'" ) + +tests = { + 'Mnemonic': { + 'hex2mn': [ + ( ['deadbeefdeadbeefdeadbeefdeadbeef'], + 'table cast forgive master funny gaze sadness ripple million paint moral match' ), + ( ['deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'], + ('swirl maybe anymore mix scale stray fog use approach page crime rhyme ' + + 'class former strange window snap soon') ), + ( ['deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'], + ('swell type milk figure cheese phone fill black test bloom heard comfort ' + + 'image terrible radio lesson own reply battle goal goodbye need laugh stream') ), + ( ['ffffffffffffffffffffffffffffffff'], + 'yellow yeah show bowl season spider cling defeat poison law shelter reflect' ), + ( ['ffffffffffffffffffffffffffffffffffffffffffffffff'], + ('yeah youth quit fail perhaps drum out person young click skin ' + + 'weird inside silently perfectly together anyone memory') ), + ( ['ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'], + ('wrote affection object cell opinion here laughter stare honest north cost begin ' + + 'murder something yourself effort acid dot doubt game broke tell guilt innocent') ), + ( ['0000000000000000000000000000000000000000000000000000000000000001'], + ('able able able able able able able able able able able able ' + + 'able able able able able able able able able able able about') ), + ], + 'mn2hex': [ + ( ['table cast forgive master funny gaze sadness ripple million paint moral match'], + 'deadbeefdeadbeefdeadbeefdeadbeef' ), + ( ['swirl maybe anymore mix scale stray fog use approach page crime rhyme ' + + 'class former strange window snap soon'], + 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef'), + ( ['swell type milk figure cheese phone fill black test bloom heard comfort ' + + 'image terrible radio lesson own reply battle goal goodbye need laugh stream'], + 'deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef' ), + ( ['yellow yeah show bowl season spider cling defeat poison law shelter reflect'], + 'ffffffffffffffffffffffffffffffff' ), + ( ['yeah youth quit fail perhaps drum out person young click skin ' + + 'weird inside silently perfectly together anyone memory'], + 'ffffffffffffffffffffffffffffffffffffffffffffffff' ) , + ( ['wrote affection object cell opinion here laughter stare honest north cost begin ' + + 'murder something yourself effort acid dot doubt game broke tell guilt innocent'], + 'ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff'), + ( ['able able able able able able able able able able able able ' + + 'able able able able able able able able able able able about'], + '0000000000000000000000000000000000000000000000000000000000000001'), + ], + 'mn_rand128': [ ( [], is_mnemonic, ['-r0']), ( ['wordlist=tirosh'], is_mnemonic, ['-r0']), ], + 'mn_rand192': [ ( [], is_mnemonic, ['-r0']), ( ['wordlist=tirosh'], is_mnemonic, ['-r0']), ], + 'mn_rand256': [ ( [], is_mnemonic, ['-r0']), ( ['wordlist=tirosh'], is_mnemonic, ['-r0']), ], + 'mn_stats': [ ( [], is_str ), ( ['wordlist=tirosh'], is_str ), ], + 'mn_printlist': [ ( [], is_str ), ( ['wordlist=tirosh'], is_str ), ], + }, + 'Util': { + 'hextob32': [ + ( ['deadbeef'], 'DPK3PXP' ), + ( ['deadbeefdeadbeef'], 'N5LN657PK3PXP' ), + ( ['ffffffffffffffff'], 'P777777777777' ), + ( ['0000000000000000'], '' ), + ( ['0000000000000000','pad=10'], 'AAAAAAAAAA' ), + ( ['ff','pad=10'], 'AAAAAAAAH7' ), + ], + 'b32tohex': [ + ( ['DPK3PXP'], 'deadbeef' ), + ( ['N5LN657PK3PXP'], 'deadbeefdeadbeef' ), + ( ['P777777777777'], 'ffffffffffffffff' ), + ( ['','pad=16'], '0000000000000000' ), + ( ['AAAAAAAAAA','pad=16'], '0000000000000000' ), + ( ['AAAAAAAAH7','pad=2'], 'ff' ), + ], + 'hextob58chk': [ + ( ['deadbeef'], 'eFGDJPketnz' ), + ( ['deadbeefdeadbeef'], '5CizhNNRPYpBjrbYX' ), + ( ['ffffffffffffffff'], '5qCHTcgbQwprzjWrb' ), + ( ['0000000000000000'], '111111114FCKVB' ), + ( [''], '3QJmnh' ), + ( ['000000000000000000000000000000000000000000'], '1111111111111111111114oLvT2' ), + ], + 'b58chktohex': [ + ( ['eFGDJPketnz'], 'deadbeef' ), + ( ['5CizhNNRPYpBjrbYX'], 'deadbeefdeadbeef' ), + ( ['5qCHTcgbQwprzjWrb'], 'ffffffffffffffff' ), + ( ['111111114FCKVB'], '0000000000000000' ), + ( ['3QJmnh'], '' ), + ( ['1111111111111111111114oLvT2'], '000000000000000000000000000000000000000000' ), + ], + 'bytestob58': [ + ( [b'\xde\xad\xbe\xef'], '6h8cQN' ), + ( [b'\xde\xad\xbe\xef\xde\xad\xbe\xef'], 'eFGDJURJykA' ), + ( [b'\xff\xff\xff\xff\xff\xff\xff\xff'], 'jpXCZedGfVQ' ), + ( [b'\x00\x00\x00\x00\x00\x00\x00\x00'], '' ), + ( [b'\x00\x00\x00\x00\x00\x00\x00\x00','pad=10'], '1111111111' ), + ( [b'\xff','pad=10'], '111111115Q' ), + ], + 'b58tobytes': [ + ( ['6h8cQN'], b'\xde\xad\xbe\xef' ), + ( ['eFGDJURJykA'], b'\xde\xad\xbe\xef\xde\xad\xbe\xef' ), + ( ['jpXCZedGfVQ'], b'\xff\xff\xff\xff\xff\xff\xff\xff' ), + ( ['','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ), + ( ['1111111111','pad=16'], b'\x00\x00\x00\x00\x00\x00\x00\x00' ), + ( ['111111115Q','pad=2'], b'\xff' ), + ], + 'hextob58': [ + ( ['deadbeef'], '6h8cQN' ), + ( ['deadbeefdeadbeef'], 'eFGDJURJykA' ), + ( ['ffffffffffffffff'], 'jpXCZedGfVQ' ), + ( ['0000000000000000'], '' ), + ( ['0000000000000000','pad=10'], '1111111111' ), + ( ['ff','pad=10'], '111111115Q' ), + ], + 'b58tohex': [ + ( ['6h8cQN'], 'deadbeef' ), + ( ['eFGDJURJykA'], 'deadbeefdeadbeef' ), + ( ['jpXCZedGfVQ'], 'ffffffffffffffff' ), + ( ['','pad=16'], '0000000000000000' ), + ( ['1111111111','pad=16'], '0000000000000000' ), + ( ['111111115Q','pad=2'], 'ff' ), + ], + 'bytespec': [ + ( ['1G'], str(1024*1024*1024) ), + ( ['1234G'], str(1234*1024*1024*1024) ), + ( ['1GB'], str(1000*1000*1000) ), + ( ['1234GB'], str(1234*1000*1000*1000) ), + ( ['1.234MB'], str(1234*1000) ), + ( ['1.234567M'], str(int(Decimal('1.234567')*1024*1024)) ), + ( ['1234'], str(1234) ), + ], + 'hash160': [ # TODO: check that hextob58chk(hash160) = pubhex2addr + ( ['deadbeef'], 'f04df4c4b30d2b7ac6e1ed2445aeb12a9cb4d2ec' ), + ( ['000000000000000000000000000000000000000000'], '2db95e704e2d9b0474acf76182f3f985b7064a8a' ), + ( [''], 'b472a266d0bd89c13706a4132ccfb16f7c3b9fcb' ), + ( ['ffffffffffffffff'], 'f86221f5a1fca059a865c0b7d374dfa9d5f3aeb4' ), + ], + 'hash256': [ + ( ['deadbeef'], 'e107944e77a688feae4c2d4db5951923812dd0f72026a11168104ee1b248f8a9' ), + ( ['000000000000000000000000000000000000000000'], 'fd5181fcd097a334ab340569e5edcd09f702fef7994abab01f4b66e86b32ebbe' ), + ( [''], '5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456' ), + ( ['ffffffffffffffff'], '57b2d2c3455e0f76c61c5237ff04fc9fc0f3fe691e587ea9c951949e1a5e0fed' ), + ], + 'hexdump': [ + ( [sample_text.encode()], sample_text_hexdump ), + ], + 'unhexdump': [ + ( [sample_text_hexdump.encode()], sample_text.encode() ), + ], + 'hexlify': [ + ( [b'foobar'], '666f6f626172' ), + ], + 'unhexlify': [ + ( ['666f6f626172'], 'foobar' ), + ], + 'hexreverse': [ + ( ['deadbeefcafe'], 'fecaefbeadde' ), + ], + 'id6': [ + ( [sample_text.encode()], 'a6d72b' ), + ], + 'id8': [ + ( [sample_text.encode()], '687C09C2' ), + ], + 'str2id6': [ + ( ['74ev zjeq Zw2g DspF RKpE 7H'], '70413d' ), # checked + ], + 'randhex': [ + ( [], {'boolfunc':is_hex_str,'len':64}, ['-r0'] ), + ( ['nbytes=16'], {'boolfunc':is_hex_str,'len':32}, ['-r0'] ), + ( ['nbytes=6'], {'boolfunc':is_hex_str,'len':12}, ['-r0'] ), + ], + 'randb58': [ + ( [], {'boolfunc':is_b58_str,'len':44}, ['-r0'] ), + ( ['nbytes=16'], {'boolfunc':is_b58_str,'len':22}, ['-r0'] ), + ( ['nbytes=12','pad=false'], is_b58_str, ['-r0'] ), + ], + }, + 'Wallet': { + 'gen_key': [ + ( ['98831F3A:11','wallet=test/ref/98831F3A.mmwords'], + '5JKLcdYbhP6QQ4BXc9HtjfqJ79FFRXP2SZTKUyEuyXJo9QSFUkv' + ), + ( ['98831F3A:C:11','wallet=test/ref/98831F3A.mmwords'], + 'L2LwXv94XTU2HjCbJPXCFuaHjrjucGipWPWUi1hkM5EykgektyqR' + ), + ( ['98831F3A:B:11','wallet=test/ref/98831F3A.mmwords'], + 'L2K4Y9MWb5oUfKKZtwdgCm6FLZdUiWJDHjh9BYxpEvtfcXt4iM5g' + ), + ( ['98831F3A:S:11','wallet=test/ref/98831F3A.mmwords'], + 'KwmkkfC9GghnJhnKoRXRn5KwGCgXrCmDw6Uv83NzE4kJS5axCR9A' + ), + ], + 'gen_addr': [ + ( ['98831F3A:11','wallet=test/ref/98831F3A.mmwords'], + '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm' + ), + ( ['98831F3A:L:11','wallet=test/ref/98831F3A.mmwords'], + '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm' + ), + ( ['98831F3A:C:11','wallet=test/ref/98831F3A.mmwords'], + '1MPsZ7BY9qikqfPxqmrovE8gLDX2rYArZk' + ), + ( ['98831F3A:B:11','wallet=test/ref/98831F3A.mmwords'], + 'bc1qxptlvmwaymaxa7pxkr2u5pn7c0508stcncv7ms' + ), + ( ['98831F3A:S:11','wallet=test/ref/98831F3A.mmwords'], + '3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn' + ), + ], + }, + 'Coin': { + 'addr2pubhash': { + 'btc_mainnet': [ + ( ['12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm'], '118089d66b4a5853765e94923abdd5de4616c6e5' ), + ( ['3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn'], '8e34586186551f6320fa3eb2d238a9c61ab8264b' ), + ( ['bc1qxptlvmwaymaxa7pxkr2u5pn7c0508stcncv7ms'], '3057f66ddd26fa6ef826b0d5ca067ec3e8f3c178' ), + ], + }, + 'pubhash2addr': { + 'btc_mainnet': [ + ( ['118089d66b4a5853765e94923abdd5de4616c6e5'], '12bYUGXS8SRArZneQDN9YEEYAtEa59Rykm', + None, 'opt.type="legacy"' ), + ( ['8e34586186551f6320fa3eb2d238a9c61ab8264b'], '3Eevao3DRVXnYym3tdrJDqS3Wc39PQzahn', + ['--type=segwit'], 'opt.type="segwit"' ), + ( ['3057f66ddd26fa6ef826b0d5ca067ec3e8f3c178'], 'bc1qxptlvmwaymaxa7pxkr2u5pn7c0508stcncv7ms', + ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'hex2wif': { + 'btc_mainnet': [ + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX', + None, 'opt.type="legacy"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + 'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm', + ['--type=compressed'], 'opt.type="compressed"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + 'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm', + ['--type=segwit'], 'opt.type="segwit"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + 'KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm', + ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'privhex2addr': { + 'btc_mainnet': [ + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1', + None, 'opt.type="legacy"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF', + ['--type=compressed'], 'opt.type="compressed"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg', + ['--type=segwit'], 'opt.type="segwit"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + 'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c', + ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'privhex2pubhex': { + 'btc_mainnet': [ + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '044281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e972757f3254c322eeaa3cb6bf97cc5ecf8d4387b0df2c0b1e6ee18fe3a6977a7d57a', + None, 'opt.type="legacy"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727', + ['--type=compressed'], 'opt.type="compressed"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727', + ['--type=segwit'], 'opt.type="segwit"' ), + ( ['118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492'], + '024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727', + ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'pubhex2addr': { + 'btc_mainnet': [ + ( ['044281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e972757f3254c322eeaa3cb6bf97cc5ecf8d4387b0df2c0b1e6ee18fe3a6977a7d57a'], + '1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1', + None, 'opt.type="legacy"' ), + ( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'], + '1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF', + ['--type=compressed'], 'opt.type="compressed"' ), + ( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'], + '3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg', + ['--type=segwit'], 'opt.type="segwit"' ), + ( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'], + 'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c', + ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'pubhex2redeem_script': { + 'btc_mainnet': [ + ( ['024281a85c9ce87279e028410b851410d65136304cfbbbeaaa8e2e3931cf4e9727'], + '0014d04134b9ddb7399907657514d846aa495b4e474c', + ['--type=segwit'], 'opt.type="segwit"' ), + ], + }, + 'randpair': { + 'btc_mainnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ], + 'btc_testnet': [ ( [], [is_wif,is_coin_addr], ['-r0'] ) ], + }, + 'randwif': { + 'btc_mainnet': [ ( [], is_wif, ['-r0'] ) ], + 'btc_testnet': [ ( [], is_wif, ['-r0'] ) ], + }, + 'wif2addr': { + 'btc_mainnet': [ + ( ['5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'], + '1C5VPtgq9xQ6AcTgMAR3J6GDrs72HC4pS1', ['--type=legacy'], 'opt.type="legacy"' ), + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + '1Kz9fVSUMshzPejpzW9D95kScgA3rY6QxF', ['--type=compressed'], 'opt.type="compressed"' ), + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + '3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg', ['--type=segwit'], 'opt.type="segwit"' ), + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + 'bc1q6pqnfwwakuuejpm9w52ds342f9d5u36v0qnz7c', ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'wif2hex': { + 'btc_mainnet': [ + ( ['5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'], + '118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492', + None, 'opt.type="legacy"' ), + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + '118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492', + ['--type=compressed'], 'opt.type="compressed"' ), + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + '118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492', + ['--type=segwit'], 'opt.type="segwit"' ), + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + '118089d66b4a5853765e94923abdd5de4616c6e5118089d66b4a5853765e9492', + ['--type=bech32'], 'opt.type="bech32"' ), + ], + }, + 'wif2redeem_script': { + 'btc_mainnet': [ + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + '0014d04134b9ddb7399907657514d846aa495b4e474c', + ['--type=segwit'], 'opt.type="segwit"' ), + ], + }, + 'wif2segwit_pair': { + 'btc_mainnet': [ + ( ['KwojSzt1VvW343mQfWQi3J537siAt5ktL2qbuCg1ZyKR8BLQ6UJm'], + ('0014d04134b9ddb7399907657514d846aa495b4e474c','3AhjTiWHhVJAi1s5CfKMcLzYps12x3gZhg'), + ['--type=segwit'], 'opt.type="segwit"' ), + ], + }, + }, + # TODO: compressed address files are missing + # 'addrfile_compressed_chk': + # 'btc': ('A33C 4FDE F515 F5BC','6C48 AA57 2056 C8C8'), + # 'ltc': ('3FC0 8F03 C2D6 BD19','4C0A 49B6 2DD1 1BE0'), + 'File': { + 'addrfile_chksum': { + 'btc_mainnet': [ + ( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].addrs'], + '6FEF 6FB9 7B13 5D91'), + ( ['test/ref/98831F3A-S[1,31-33,500-501,1010-1011].addrs'], + '06C1 9C87 F25C 4EE6'), + ( ['test/ref/98831F3A-B[1,31-33,500-501,1010-1011].addrs'], + '9D2A D4B6 5117 F02E'), + ], + 'btc_testnet': [ + ( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].testnet.addrs'], + '424E 4326 CFFE 5F51'), + ( ['test/ref/98831F3A-S[1,31-33,500-501,1010-1011].testnet.addrs'], + '072C 8B07 2730 CB7A'), + ( ['test/ref/98831F3A-B[1,31-33,500-501,1010-1011].testnet.addrs'], + '0527 9C39 6C1B E39A'), + ], + 'ltc_mainnet': [ + ( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].addrs'], + 'AD52 C3FE 8924 AAF0'), + ( ['test/ref/litecoin/98831F3A-LTC-S[1,31-33,500-501,1010-1011].addrs'], + '63DF E42A 0827 21C3'), + ( ['test/ref/litecoin/98831F3A-LTC-B[1,31-33,500-501,1010-1011].addrs'], + 'FF1C 7939 5967 AB82'), + ], + 'ltc_testnet': [ + ( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.addrs'], + '4EBE 2E85 E969 1B30'), + ( ['test/ref/litecoin/98831F3A-LTC-S[1,31-33,500-501,1010-1011].testnet.addrs'], + '5DD1 D186 DBE1 59F2'), + ( ['test/ref/litecoin/98831F3A-LTC-B[1,31-33,500-501,1010-1011].testnet.addrs'], + 'ED3D 8AA4 BED4 0B40'), + ], + 'zec_mainnet': [ + ( ['test/ref/zcash/98831F3A-ZEC-C[1,31-33,500-501,1010-1011].addrs'],'903E 7225 DD86 6E01'), ], + 'zec_z_mainnet': [ + ( ['test/ref/zcash/98831F3A-ZEC-Z[1,31-33,500-501,1010-1011].addrs'],'9C7A 72DC 3D4A B3AF'), ], + 'xmr_mainnet': [ + ( ['test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].addrs'],'4369 0253 AC2C 0E38'), ], + 'dash_mainnet': [ + ( ['test/ref/dash/98831F3A-DASH-C[1,31-33,500-501,1010-1011].addrs'],'FBC1 6B6A 0988 4403'), ], + 'eth_mainnet': [ + ( ['test/ref/ethereum/98831F3A-ETH[1,31-33,500-501,1010-1011].addrs'],'E554 076E 7AF6 66A3'), ], + 'etc_mainnet': [ + ( ['test/ref/ethereum_classic/98831F3A-ETC[1,31-33,500-501,1010-1011].addrs'], + 'E97A D796 B495 E8BC'), ], + }, + 'keyaddrfile_chksum': { + 'btc_mainnet': [ + ( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].akeys.mmenc'], + '9F2D D781 1812 8BAD', kafile_opts, kafile_code ), + ], + 'btc_testnet': [ + ( ['test/ref/98831F3A[1,31-33,500-501,1010-1011].testnet.akeys.mmenc'], + '88CC 5120 9A91 22C2', kafile_opts, kafile_code ), + ], + 'ltc_mainnet': [ + ( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].akeys.mmenc'], + 'B804 978A 8796 3ED4', kafile_opts, kafile_code ), + ], + 'ltc_testnet': [ + ( ['test/ref/litecoin/98831F3A-LTC[1,31-33,500-501,1010-1011].testnet.akeys.mmenc'], + '98B5 AC35 F334 0398', kafile_opts, kafile_code ), + ], + 'zec_mainnet': [ + ( ['test/ref/zcash/98831F3A-ZEC-C[1,31-33,500-501,1010-1011].akeys.mmenc'], + 'F05A 5A5C 0C8E 2617', kafile_opts, kafile_code ), ], + 'zec_z_mainnet': [ + ( ['test/ref/zcash/98831F3A-ZEC-Z[1,31-33,500-501,1010-1011].akeys.mmenc'], + '6B87 9B2D 0D8D 8D1E', kafile_opts, kafile_code ), ], + 'xmr_mainnet': [ + ( ['test/ref/monero/98831F3A-XMR-M[1,31-33,500-501,1010-1011].akeys.mmenc'], + 'E0D7 9612 3D67 404A', kafile_opts, kafile_code ), ], + 'dash_mainnet': [ + ( ['test/ref/dash/98831F3A-DASH-C[1,31-33,500-501,1010-1011].akeys.mmenc'], + 'E83D 2C63 FEA2 4142', kafile_opts, kafile_code ), ], + 'eth_mainnet': [ + ( ['test/ref/ethereum/98831F3A-ETH[1,31-33,500-501,1010-1011].akeys.mmenc'], + 'E400 70D9 0AE3 C7C2', kafile_opts, kafile_code ), ], + 'etc_mainnet': [ + ( ['test/ref/ethereum_classic/98831F3A-ETC[1,31-33,500-501,1010-1011].akeys.mmenc'], + 'EF49 967D BD6C FE45', kafile_opts, kafile_code ), ], + }, + 'passwdfile_chksum': { + 'btc_mainnet': [ + ( ['test/ref/98831F3A-фубар@crypto.org-b58-20[1,4,9-11,1100].pws'], + 'A983 DAB9 5514 27FB', kafile_opts, kafile_code ), ], + }, + 'txview': { + 'btc_mainnet': [ ( ['test/ref/0B8D5A[15.31789,14,tl=1320969600].rawtx'], None ), ], + 'btc_testnet': [ ( ['test/ref/0C7115[15.86255,14,tl=1320969600].testnet.rawtx'], None ), ], + 'bch_mainnet': [ ( ['test/ref/460D4D-BCH[10.19764,tl=1320969600].rawtx'], None ), ], + 'bch_testnet': [ ( ['test/ref/359FD5-BCH[6.68868,tl=1320969600].testnet.rawtx'], None ), ], + 'ltc_mainnet': [ ( ['test/ref/litecoin/AF3CDF-LTC[620.76194,1453,tl=1320969600].rawtx'], None ), ], + 'ltc_testnet': [ ( ['test/ref/litecoin/A5A1E0-LTC[1454.64322,1453,tl=1320969600].testnet.rawtx'], + None ), ], + 'eth_mainnet': [ ( ['test/ref/ethereum/88FEFD-ETH[23.45495,40000].rawtx'], None ), ], + 'eth_testnet': [ ( ['test/ref/ethereum/B472BD-ETH[23.45495,40000].testnet.rawtx'], None ), ], + 'mm1_mainnet': [ ( ['test/ref/ethereum/5881D2-MM1[1.23456,50000].rawtx'], None ), ], + 'mm1_testnet': [ ( ['test/ref/ethereum/6BDB25-MM1[1.23456,50000].testnet.rawtx'], None ), ], + 'etc_mainnet': [ ( ['test/ref/ethereum_classic/ED3848-ETC[1.2345,40000].rawtx'], None ), ], + }, + }, +} + +coin_dependent_groups = ('Coin','File') # TODO: do this as attr of each group in tool.py + +def run_test(gid,cmd_name): + data = tests[gid][cmd_name] + # behavior is like test.py: run coin-dependent tests only if g.testnet or g.coin != BTC + if gid in coin_dependent_groups: k = '{}_{}net'.format((g.token.lower() if g.token else g.coin.lower()),('main','test')[g.testnet]) if k in data: data = data[k] @@ -235,78 +547,142 @@ def do_cmd(cdata): msg("-- no data for {} ({}) - skipping".format(cmd_name,k)) return else: - if opt.coin_dependent: return + if g.coin != 'BTC' or g.testnet: return m2 = '' - m = '{} {}{}'.format(cyan('Testing'),cmd_name if opt.names else desc,m2) + m = '{} {}{}'.format(purple('Testing'), cmd_name if opt.names else + extract_docstring(getattr(getattr(tool,'MMGenToolCmd'+gid),cmd_name)),m2) + msg_r(green(m)+'\n' if opt.verbose else m) + + def fork_cmd(cmd_name,args,out,opts,exec_code): + cmd = list(tool_cmd) + (opts or []) + [cmd_name] + args + vmsg('{} {}'.format(green('Executing'),cyan(' '.join(cmd)))) + p = Popen(cmd,stdin=(PIPE if stdin_input else None),stdout=PIPE,stderr=PIPE) + if stdin_input: + p.stdin.write(stdin_input) + p.stdin.close() + cmd_out = p.stdout.read() + try: + cmd_out = cmd_out.decode().strip() + except: + pass + cmd_err = p.stderr.read() + if cmd_err: vmsg(cmd_err.strip().decode()) + if p.wait() != 0: + die(1,'Spawned program exited with error') + + return cmd_out + + def run_func(cmd_name,args,out,opts,exec_code): + vmsg('{}: {}{}'.format(purple('Running'), + ' '.join([cmd_name]+[repr(e) for e in args]), + ' '+exec_code if exec_code else '' )) + if exec_code: exec(exec_code) + aargs,kwargs = tool._process_args(cmd_name,args) + oq_save = opt.quiet + if not opt.verbose: opt.quiet = True + if stdin_input: + fd0,fd1 = os.pipe() + if os.fork(): # parent + os.close(fd1) + stdin_save = os.dup(0) + os.dup2(fd0,0) + cmd_out = getattr(tc,cmd_name)(*aargs,**kwargs) + os.dup2(stdin_save,0) + os.wait() + opt.quiet = oq_save + return cmd_out + else: # child + os.close(fd0) + os.write(fd1,stdin_input) + vmsg('Input: {!r}'.format(stdin_input)) + sys.exit(0) + else: + ret = getattr(tc,cmd_name)(*aargs,**kwargs) + opt.quiet = oq_save + return ret + for d in data: args,out,opts,exec_code = d + tuple([None] * (4-len(d))) + stdin_input = None + if args and type(args[0]) == bytes: + stdin_input = args[0] + args[0] = '-' if opt.fork: - cmd = list(tool_cmd) + (opts or []) + [cmd_name] + args - vmsg('{} {}'.format(green('Executing'),cyan(' '.join(cmd)))) - p = Popen(cmd,stdout=PIPE,stderr=PIPE) - cmd_out = p.stdout.read() - if type(out) != bytes: - cmd_out = cmd_out.strip().decode() - cmd_err = p.stderr.read() - if cmd_err: vmsg(cmd_err.strip().decode()) - if p.wait() != 0: - die(1,'Spawned program exited with error') + cmd_out = fork_cmd(cmd_name,args,out,opts,exec_code) else: - vmsg('{}: {}'.format(purple('Running'),' '.join([cmd_name]+args))) - if exec_code: exec(exec_code) - aargs,kwargs = tool._process_args(cmd_name,args) - oq_save = opt.quiet - if not opt.verbose: opt.quiet = True - cmd_out = tool._process_result(getattr(tc,cmd_name)(*aargs,**kwargs)) - opt.quiet = oq_save + cmd_out = run_func(cmd_name,args,out,opts,exec_code) - if type(out) != bytes: - cmd_out = cmd_out.strip() - vmsg('Output: {}\n'.format(cmd_out)) + vmsg('Output: {}\n'.format(cmd_out if issubclass(type(out),str) else repr(cmd_out))) + + def check_output(cmd_out,out): + if issubclass(type(out),str): out = out.encode() + if issubclass(type(cmd_out),int): cmd_out = str(cmd_out).encode() + if issubclass(type(cmd_out),str): cmd_out = cmd_out.encode() + + if type(out).__name__ == 'function': + assert out(cmd_out.decode()),"{}({}) failed!".format(out.__name__,cmd_out.decode()) + elif type(out) == dict: + for k in out: + if k == 'boolfunc': + assert out[k](cmd_out.decode()),"{}({}) failed!".format(out[k].__name__,cmd_out.decod()) + else: + if not getattr(__builtins__,k)(cmd_out) == out[k]: + die(1,"{}({}) did not return {}!".format(k,cmd_out,out[k])) + elif out is not None: + assert cmd_out == out,"Output ({!r}) doesn't match expected output ({!r})".format(cmd_out,out) + + if type(out) in (list,tuple): + for co,o in zip(cmd_out.split('\n') if opt.fork else cmd_out,out): + check_output(co,o) else: - vmsg('Output: {}\n'.format(repr(cmd_out))) - - if type(out).__name__ == 'function': - assert out(cmd_out),"{}({}) failed!".format(out.__name__,cmd_out) - elif type(out) == list and type(out[0]).__name__ == 'function': - for i in range(len(out)): - s = cmd_out.split('\n')[i] - assert out[i](s),"{}({}) failed!".format(out[i].__name__,s) - elif out is not None: - assert cmd_out == out,"Output ({}) doesn't match expected output ({})".format(cmd_out,out) + check_output(cmd_out,out) if not opt.verbose: msg_r('.') if not opt.verbose: msg('OK') -def do_group(garg): - gid,gdesc,gdata = garg - qmsg(blue("Testing {}".format("command group '{}'".format(gid) if opt.names else gdesc))) - for cdata in gdata: - do_cmd(cdata) +def extract_docstring(obj): + return obj.__doc__.strip().split('\n')[0] + +def do_group(gid): + qmsg(blue("Testing {}".format( + "command group '{}'".format(gid) if opt.names + else extract_docstring(getattr(tool,'MMGenToolCmd'+gid))))) + + for cname in [e for e in dir(getattr(tool,'MMGenToolCmd'+gid)) if e[0] != '_']: + if cname not in tests[gid]: + m = 'No test for command {!r} in group {!r}!'.format(cname,gid) + if opt.die_on_missing: + die(1,m+' Aborting') + else: + msg(m) + continue + run_test(gid,cname) def do_cmd_in_group(cmd): - for g in tests: - for cdata in g[2]: - if cdata[0] == cmd: - do_cmd(cdata) + for gid in tests: + for cname in tests[gid]: + if cname == cmd: + run_test(gid,cname) return True return False def list_tested_cmds(): - for g in tests: - for cdata in g[2]: - Msg(cdata[0]) + for gid in tests: + for cname in [e for e in dir(getattr(tool,'MMGenToolCmd'+gid)) if e[0] != '_']: + Msg(cname) sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:] cmd_args = opts.init(opts_data) +import mmgen.tool as tool + if opt.list_tests: - Msg('Available commands:') - for gid,gdesc,gdata in tests: - Msg(' {:12} - {}'.format(gid,gdesc)) + Msg('Available tests:') + for gid in tests: + Msg(' {:6} - {}'.format(gid,extract_docstring(getattr(tool,'MMGenToolCmd'+gid)))) sys.exit(0) if opt.list_tested_cmds: @@ -338,7 +714,6 @@ if opt.fork: tool_cmd = ('python3') + tool_cmd else: opt.usr_randchars = 0 - import mmgen.tool as tool tc = tool.MMGenToolCmd() start_time = int(time.time()) @@ -348,9 +723,8 @@ try: if len(cmd_args) != 1: die(1,'Only one command may be specified') cmd = cmd_args[0] - group = [e for e in tests if e[0] == cmd] - if group: - do_group(group[0]) + if cmd in tests: + do_group(cmd) else: if not do_cmd_in_group(cmd): die(1,"'{}': not a recognized test or test group".format(cmd))