Browse Source

rewrite test/gentest.py utility

- support for testing all configured generators in one go has also been added
  (via the 'all' subparameter)
The MMGen Project 3 years ago
parent
commit
b43d827b50
2 changed files with 270 additions and 220 deletions
  1. 269 219
      test/gentest.py
  2. 1 1
      test/test-release.sh

+ 269 - 219
test/gentest.py

@@ -30,68 +30,88 @@ sys.path.insert(0,overlay_setup(repo_root))
 from mmgen.common import *
 from test.include.common import getrand
 
+results_file = 'gentest.out.json'
+
 rounds = 100
 opts_data = {
 	'text': {
 		'desc': 'Test key/address generation of the MMGen suite in various ways',
-		'usage':'[options] [spec] [rounds | dump file]',
+		'usage':'[options] <spec> <rounds | dump file>',
 		'options': """
--h, --help       Print this help message
--a, --all        Test all coins supported by specified external tool
+-h, --help         Print this help message
+--, --longhelp     Print help message for long options (common options)
+-a, --all-coins    Test all coins supported by specified external tool
 -k, --use-internal-keccak-module Force use of the internal keccak module
---, --longhelp   Print help message for long options (common options)
--q, --quiet      Produce quieter output
--t, --type=t     Specify address type (e.g. 'compressed','segwit','zcash_z','bech32')
--v, --verbose    Produce more verbose output
+-q, --quiet        Produce quieter output
+-s, --save-results Save output of external tool in Compare test to
+                   {rf!r}
+-t, --type=t       Specify address type (e.g. 'compressed','segwit',
+                   'zcash_z','bech32')
+-v, --verbose      Produce more verbose output
 """,
 	'notes': """
 TEST TYPES:
 
-   A/B:     {prog} A:B [rounds]  (compare key generators A and B)
-   Speed:   {prog} A [rounds]    (test speed of key generator A)
-   Compare: {prog} A <dump file> (compare generator A to wallet dump)
+  Compare: {prog} A:B <rounds>  (compare address generators A and B)
+  Speed:   {prog} A <rounds>    (test speed of generator A)
+  Dump:    {prog} A <dump file> (compare generator A to wallet dump)
+
+  where:
+
+      A and B are keygen backend numbers ('1' being the default); or
+      B is the name of an external tool (see below) or 'ext'.
 
-     where A and B are one of:
-       '1' - native Python ECDSA library (slow), or
-       '2' - bitcoincore.org's libsecp256k1 library (default);
-     or:
-       B is name of an external tool (see below) or 'ext'.
-       If B is 'ext', the external tool will be chosen automatically.
+      If B is 'ext', the external tool will be chosen automatically.
+
+  For the Compare test, A may be 'all' to test all backends for the current
+  coin/address type combination.
 
 EXAMPLES:
 
-  Compare addresses generated by native Python ECDSA library and libsecp256k1,
-  100 rounds:
+  Compare addresses generated by 'libsecp256k1' and 'python-ecdsa' backends,
+  with 100 random rounds plus private-key edge cases:
   $ {prog} 1:2 100
 
-  Compare mmgen-secp256k1 Segwit address generation to pycoin library for all
-  supported coins, 100 rounds:
-  $ {prog} --all --type=segwit 2:pycoin 100
+  Compare Segwit addresses from default 'libsecp256k1' backend to 'pycoin'
+  library for all supported coins, 100 rounds + edge cases:
+  $ {prog} --all-coins --type=segwit 1:pycoin 100
+
+  Compare addresses from 'python-ecdsa' backend to output of 'keyconv' tool
+  for all supported coins, 100 rounds + edge cases:
+  $ {prog} --all-coins --type=compressed 2:keyconv 100
 
-  Compare mmgen-secp256k1 address generation to keyconv tool for all
-  supported coins, 100 rounds:
-  $ {prog} --all --type=compressed 2:keyconv 100
+  Compare bech32 addrs from 'libsecp256k1' backend to Bitcoin Core wallet
+  dump:
+  $ {prog} --type=bech32 1 bech32wallet.dump
 
-  Compare mmgen-secp256k1 XMR address generation to configured external tool,
-  10 rounds:
-  $ {prog} --coin=xmr 2:ext 10
+  Compare addresses from Monero 'ed25519ll' backend to output of default
+  external tool, 10 rounds + edge cases:
+  $ {prog} --coin=xmr 3:ext 10
 
-  Test speed of mmgen-secp256k1 address generation, 10,000 rounds:
-  $ {prog} 2 10000
+  Test the speed of default Monero 'nacl' backend, 10,000 rounds:
+  $ test/gentest.py --coin=xmr 1 10000
 
-  Compare mmgen-secp256k1-generated bech32 addrs to coin daemon wallet dump:
-  $ {prog} --type=bech32 2 bech32wallet.dump
+  Same for Zcash:
+  $ test/gentest.py --coin=zec --type=zcash_z 1 10000
 
-Supported external tools:
+  Test all configured Monero backends against 'moneropy' library, 3 rounds
+  + edge cases:
+  $ test/gentest.py --coin=xmr all:moneropy 3
+
+  Test 'nacl' and 'ed25519ll_djbec' backends against each other, 10,000 rounds
+  + edge cases:
+  $ test/gentest.py --coin=xmr 1:2 10000
+
+SUPPORTED EXTERNAL TOOLS:
 
   + ethkey (for ETH,ETC)
-	https://github.com/openethereum/openethereum
+    https://github.com/openethereum/openethereum
     (build with 'cargo build -p ethkey-cli --release')
 
-  + zcash-mini (for Zcash Z-addresses)
+  + zcash-mini (for Zcash-Z addresses and view keys)
     https://github.com/FiloSottile/zcash-mini
 
-  + moneropy (for Monero addresses)
+  + moneropy (for Monero addresses and view keys)
     https://github.com/bigreddmachine/MoneroPy
 
   + pycoin (for supported coins)
@@ -103,6 +123,9 @@ Supported external tools:
 """
 	},
 	'code': {
+		'options': lambda s: s.format(
+			rf=results_file,
+		),
 		'notes': lambda s: s.format(
 			prog='test/gentest.py',
 			pnm=g.proj_name,
@@ -110,36 +133,33 @@ Supported external tools:
 	}
 }
 
-sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
-
-cmd_args = opts.init(opts_data,add_opts=['exact_output'])
-
-if not 1 <= len(cmd_args) <= 2:
-	opts.usage()
-
-from mmgen.protocol import init_proto_from_opts
-proto = init_proto_from_opts()
+gtr = namedtuple('gen_tool_result',['wif','addr','vk'])
 
-from subprocess import run,PIPE,DEVNULL
 def get_cmd_output(cmd,input=None):
 	return run(cmd,input=input,stdout=PIPE,stderr=DEVNULL).stdout.decode().splitlines()
 
-from collections import namedtuple
-gtr = namedtuple('gen_tool_result',['wif','addr','vk'])
+saved_results = {}
 
 class GenTool(object):
 
+	def __init__(self,proto,addr_type):
+		self.proto = proto
+		self.addr_type = addr_type
+		self.data = {}
+
+	def __del__(self):
+		if opt.save_results:
+			key = f'{self.proto.coin}-{self.proto.network}-{self.addr_type.name}-{self.desc}'.lower()
+			saved_results[key] = self.data
+
 	def run_tool(self,sec):
-		vcoin = 'BTC' if proto.coin == 'BCH' else proto.coin
-		return self.run(sec,vcoin)
+		vcoin = 'BTC' if self.proto.coin == 'BCH' else self.proto.coin
+		ret = self.run(sec,vcoin)
+		self.data[sec] = ret._asdict()
+		return ret
 
 class GenToolEthkey(GenTool):
 	desc = 'ethkey'
-	def __init__(self):
-		proto = init_proto('eth')
-		global addr_type
-		addr_type = MMGenAddrType(proto,'E')
-
 	def run(self,sec,vcoin):
 		o = get_cmd_output(['ethkey','info',sec])
 		return gtr(o[0].split()[1],o[-1].split()[1],None)
@@ -152,11 +172,6 @@ class GenToolKeyconv(GenTool):
 
 class GenToolZcash_mini(GenTool):
 	desc = 'zcash-mini'
-	def __init__(self):
-		proto = init_proto('zec')
-		global addr_type
-		addr_type = MMGenAddrType(proto,'Z')
-
 	def run(self,sec,vcoin):
 		o = get_cmd_output(['zcash-mini','-key','-simple'],input=(sec.wif+'\n').encode())
 		return gtr(o[1],o[0],o[-1])
@@ -166,24 +181,26 @@ class GenToolPycoin(GenTool):
 	pycoin/networks/all.py pycoin/networks/legacy_networks.py
 	"""
 	desc = 'pycoin'
-	def __init__(self):
-		m = "Unable to import pycoin.networks.registry.  Is pycoin installed on your system?"
+	def __init__(self,*args,**kwargs):
+		super().__init__(*args,**kwargs)
 		try:
 			from pycoin.networks.registry import network_for_netcode
 		except:
-			raise ImportError(m)
+			raise ImportError('Unable to import pycoin.networks.registry. Is pycoin installed on your system?')
 		self.nfnc = network_for_netcode
 
 	def run(self,sec,vcoin):
-		if proto.testnet:
+		if self.proto.testnet:
 			vcoin = ci.external_tests['testnet']['pycoin'][vcoin]
 		network = self.nfnc(vcoin)
-		key = network.keys.private(secret_exponent=int(sec,16),is_compressed=addr_type.name != 'legacy')
+		key = network.keys.private(
+			secret_exponent = int(sec,16),
+			is_compressed = self.addr_type.name != 'legacy' )
 		if key is None:
 			die(1,f'can’t parse {sec}')
-		if addr_type.name in ('segwit','bech32'):
+		if self.addr_type.name in ('segwit','bech32'):
 			hash160_c = key.hash160(is_compressed=True)
-			if addr_type.name == 'segwit':
+			if self.addr_type.name == 'segwit':
 				p2sh_script = network.contract.for_p2pkh_wit(hash160_c)
 				addr = network.address.for_p2s(p2sh_script)
 			else:
@@ -194,44 +211,42 @@ class GenToolPycoin(GenTool):
 
 class GenToolMoneropy(GenTool):
 	desc = 'moneropy'
-	def __init__(self):
 
-		m = "Unable to import moneropy.  Is moneropy installed on your system?"
+	def __init__(self,*args,**kwargs):
+		super().__init__(*args,**kwargs)
 		try:
 			import moneropy.account
 		except:
-			raise ImportError(m)
-
+			raise ImportError('Unable to import moneropy. Is moneropy installed on your system?')
 		self.mpa = moneropy.account
-		proto = init_proto('xmr')
-
-		global addr_type
-		addr_type = MMGenAddrType(proto,'M')
 
 	def run(self,sec,vcoin):
-		sk_t,vk_t,addr_t = self.mpa.account_from_spend_key(sec) # VERY slow!
-		return gtr(sk_t,addr_t,vk_t)
+		if sec in self.data:
+			return gtr(**self.data[sec])
+		else:
+			sk,vk,addr = self.mpa.account_from_spend_key(sec) # VERY slow!
+			return gtr(sk,addr,vk)
+
+def find_or_check_tool(proto,addr_type,toolname):
 
-def get_tool(arg):
+	ext_progs = list(ci.external_tests[proto.network])
 
-	if arg not in ext_progs + ['ext']:
-		die(1,f'{arg!r}: unsupported tool for network {proto.network}')
+	if toolname not in ext_progs + ['ext']:
+		die(1,f'{toolname!r}: unsupported tool for network {proto.network}')
 
-	if opt.all:
-		if arg == 'ext':
-			die(1,"'--all' must be combined with a specific external testing tool")
-		return arg
+	if opt.all_coins and toolname == 'ext':
+		die(1,"'--all-coins' must be combined with a specific external testing tool")
 	else:
 		tool = ci.get_test_support(
 			proto.coin,
 			addr_type.name,
 			proto.network,
 			verbose = not opt.quiet,
-			tool = arg if arg in ext_progs else None )
-		if not tool:
-			sys.exit(2)
-		if arg in ext_progs and arg != tool:
+			tool = toolname if toolname != 'ext' else None )
+		if tool and toolname in ext_progs and toolname != tool:
 			sys.exit(3)
+		if tool == None:
+			return None
 		return tool
 
 def test_equal(desc,a_val,b_val,in_bytes,sec,wif,a_desc,b_desc):
@@ -251,76 +266,120 @@ def test_equal(desc,a_val,b_val,in_bytes,sec,wif,a_desc,b_desc):
 				w=max(len(e) for e in (a_desc,b_desc)) + 1
 		).rstrip())
 
-def gentool_test(kg_a,kg_b,ag,rounds):
-
-	m = "Comparing address generators '{A}' and '{B}' for {N} {c} ({n}), addrtype {a!r}"
-	e = ci.get_entry(proto.coin,proto.network)
-	qmsg(green(m.format(
-		A = kg_a.desc,
-		B = kg_b.desc,
-		N = proto.network,
-		c = proto.coin,
-		n = e.name if e else '---',
-		a = addr_type.name )))
-
-	global last_t
-	last_t = time.time()
+def do_ab_test(proto,addr_type,kg_b,rounds,backend_num):
 
-	def do_compare_test(n,trounds,in_bytes):
+	def run_ab_inner(n,trounds,in_bytes):
 		global last_t
 		if opt.verbose or time.time() - last_t >= 0.1:
 			qmsg_r(f'\rRound {i+1}/{trounds} ')
 			last_t = time.time()
 		sec = PrivKey(proto,in_bytes,compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
-		a_ph = kg_a.to_pubhex(sec)
-		a_addr = ag.to_addr(a_ph)
+		data = kg_a.to_pubhex(sec)
+		ag = AddrGenerator(proto,addr_type)
+		a_addr = ag.to_addr(data)
+		tinfo = (in_bytes,sec,sec.wif,type(kg_a).__name__,type(kg_b).__name__)
 		a_vk = None
-		tinfo = (in_bytes,sec,sec.wif,kg_a.desc,kg_b.desc)
+
+		def do_msg():
+			vmsg( fs.format( b=in_bytes.hex(), r=sec, k=sec.wif, v=a_vk, a=a_addr ))
+
 		if isinstance(kg_b,GenTool):
-			b = kg_b.run_tool(sec)
-			test_equal('WIF keys',sec.wif,b.wif,*tinfo)
-			test_equal('addresses',a_addr,b.addr,*tinfo)
-			if b.vk:
-				a_vk = ag.to_viewkey(a_ph)
-				test_equal('view keys',a_vk,b.vk,*tinfo)
+			def run_tool():
+				b = kg_b.run_tool(sec)
+				test_equal('WIF keys',sec.wif,b.wif,*tinfo)
+				test_equal('addresses',a_addr,b.addr,*tinfo)
+				if b.vk:
+					test_equal( 'view keys', ag.to_viewkey(data), b.vk, *tinfo )
+				return b.vk
+			a_vk = run_tool()
+			do_msg()
 		else:
-			b_addr = ag.to_addr(kg_b.to_pubhex(sec))
-			test_equal('addresses',a_addr,b_addr,*tinfo)
-		vmsg(fs.format(b=in_bytes.hex(),k=sec.wif,v=a_vk,a=a_addr))
+			test_equal( 'addresses', a_addr, ag.to_addr(kg_b.to_pubhex(sec)), *tinfo )
+			do_msg()
+
 		qmsg_r(f'\rRound {n+1}/{trounds} ')
 
-	fs  = ( '\ninput:    {b}\n%-9s {k}\naddr:     {a}\n',
-			'\ninput:    {b}\n%-9s {k}\nviewkey:  {v}\naddr:     {a}\n')[
-				'viewkey' in addr_type.extra_attrs] % (addr_type.wif_label + ':')
+	kg_a = KeyGenerator(proto,addr_type,backend_num)
+	if type(kg_a) == type(kg_b):
+		rdie(1,'Key generators are the same!')
+
+	e = ci.get_entry(proto.coin,proto.network)
+	qmsg(green("Comparing address generators '{A}' and '{B}' for {N} {c} ({n}), addrtype {a!r}".format(
+		A = type(kg_a).__name__,
+		B = type(kg_b).__name__.replace('GenTool','').replace('_','-').lower(),
+		N = proto.network,
+		c = proto.coin,
+		n = e.name if e else '---',
+		a = addr_type.name )))
+
+	global last_t
+	last_t = time.time()
+
+	fs  = (
+		'\ninput:    {b}' +
+		'\nreduced:  {r}' +
+		'\n{:9} {{k}}'.format(addr_type.wif_label+':') +
+		('\nviewkey:  {v}' if 'viewkey' in addr_type.extra_attrs else '') +
+		'\naddr:     {a}\n' )
+
+	ge = CoinProtocol.Secp256k1.secp256k1_ge
 
 	# test some important private key edge cases:
 	edgecase_sks = (
 		bytes([0x00]*31 + [0x01]), # min
 		bytes([0xff]*32),          # max
-		bytes([0x0f] + [0xff]*31), # same key as above for zcash-z
+		bytes([0x0f] + [0xff]*31), # produces same key as above for zcash-z
+		int.to_bytes(ge + 1, 32, 'big'), # bitcoin will reduce
+		int.to_bytes(ge - 1, 32, 'big'), # bitcoin will not reduce
 		bytes([0x00]*31 + [0xff]), # monero will reduce
 		bytes([0xff]*31 + [0x0f]), # monero will not reduce
+		bytes.fromhex('deadbeef'*8),
 	)
 
 	qmsg(purple('edge cases:'))
 	for i,in_bytes in enumerate(edgecase_sks):
-		do_compare_test(i,len(edgecase_sks),in_bytes)
+		run_ab_inner(i,len(edgecase_sks),in_bytes)
 	qmsg(green('\rOK            ' if opt.verbose else 'OK'))
 
 	qmsg(purple('random input:'))
 	for i in range(rounds):
-		do_compare_test(i,rounds,getrand(32))
+		run_ab_inner(i,rounds,getrand(32))
 	qmsg(green('\rOK            ' if opt.verbose else 'OK'))
 
-def speed_test(kg,ag,rounds):
-	m = "Testing speed of address generator '{}' for coin {}"
-	qmsg(green(m.format(kg.desc,proto.coin)))
+def init_tool(proto,addr_type,toolname):
+	return globals()['GenTool'+capfirst(toolname.replace('-','_'))](proto,addr_type)
+
+def get_backends(proto,foo):
+	return (1,) if isinstance(proto,CoinProtocol.Zcash) else (1,2)
+
+def ab_test(proto,gen_num,rounds,toolname_or_gen2_num):
+
+	addr_type = MMGenAddrType( proto=proto, id_str=opt.type or proto.dfl_mmtype )
+
+	if is_int(toolname_or_gen2_num):
+		assert gen_num != 'all', "'all' must be used only with external tool"
+		tool = KeyGenerator( proto, addr_type, int(toolname_or_gen2_num) )
+	else:
+		toolname = find_or_check_tool( proto, addr_type, toolname_or_gen2_num )
+		if toolname == None:
+			ymsg(f'Warning: skipping tool {toolname_or_gen2_num!r} for {proto.coin} {addr_type.name}')
+			return
+		tool = init_tool( proto, addr_type, toolname )
+
+	if gen_num == 'all': # check all backends against external tool
+		for n in range(len(get_backends(proto,addr_type.pubkey_type))):
+			do_ab_test( proto, addr_type, tool, rounds, n+1 )
+	else:                # check specific backend against external tool or another backend
+		do_ab_test( proto, addr_type, tool, rounds, int(gen_num) )
+
+def speed_test(proto,addr_type,kg,ag,rounds):
+	qmsg(green('Testing speed of address generator {!r} for coin {}'.format(
+		type(kg).__name__,
+		proto.coin )))
 	from struct import pack,unpack
 	seed = getrand(28)
 	qmsg('Incrementing key with each round')
-	qmsg('Starting key: {}'.format(
-		(seed + pack('I',0)).hex()
-	))
+	qmsg('Starting key: {}'.format( (seed + pack('I',0)).hex() ))
 	import time
 	start = last_t = time.time()
 
@@ -328,7 +387,7 @@ def speed_test(kg,ag,rounds):
 		if time.time() - last_t >= 0.1:
 			qmsg_r(f'\rRound {i+1}/{rounds} ')
 			last_t = time.time()
-		sec = PrivKey(proto,seed+pack('I',i),compressed=addr_type.compressed,pubkey_type=addr_type.pubkey_type)
+		sec = PrivKey( proto, seed+pack('I', i), compressed=addr_type.compressed, pubkey_type=addr_type.pubkey_type )
 		addr = ag.to_addr(kg.to_pubhex(sec))
 		vmsg(f'\nkey:  {sec.wif}\naddr: {addr}\n')
 	qmsg(
@@ -337,15 +396,18 @@ def speed_test(kg,ag,rounds):
 		('' if g.test_suite_deterministic else f' in {time.time()-start:.2f} seconds')
 	)
 
-def dump_test(kg,ag,fh):
+def dump_test(proto,kg,ag,filename):
 
-	dump = [[*(e.split()[0] for e in line.split('addr='))] for line in fh.readlines() if 'addr=' in line]
-	if not dump:
-		die(1,f'File {fh.name!r} appears not to be a wallet dump')
-	fh.close()
+	with open(filename) as fp:
+		dump = [[*(e.split()[0] for e in line.split('addr='))] for line in fp.readlines() if 'addr=' in line]
+		if not dump:
+			die(1,f'File {filename!r} appears not to be a wallet dump')
 
-	m = 'Comparing output of address generator {!r} against wallet dump {!r}'
-	qmsg(green(m.format(kg.desc,fh.name)))
+	qmsg(green(
+		"A: generator pair '{}:{}'\nB: wallet dump {!r}".format(
+			type(kg).__name__,
+			type(ag).__name__,
+			filename)))
 
 	for count,(b_wif,b_addr) in enumerate(dump,1):
 		qmsg_r(f'\rKey {count}/{len(dump)} ')
@@ -355,102 +417,90 @@ def dump_test(kg,ag,fh):
 			die(2,f'\nInvalid {proto.network} WIF address in dump file: {b_wif}')
 		a_addr = ag.to_addr(kg.to_pubhex(b_sec))
 		vmsg(f'\nwif: {b_wif}\naddr: {b_addr}\n')
-		tinfo = (bytes.fromhex(b_sec),b_sec,b_wif,kg.desc,fh.name)
+		tinfo = (b_sec,b_sec,b_wif,type(kg).__name__,filename)
 		test_equal('addresses',a_addr,b_addr,*tinfo)
+
 	qmsg(green(('\n','')[bool(opt.verbose)] + 'OK'))
 
-def init_tool(tname):
-	return globals()['GenTool'+capfirst(tname.replace('-','_'))]()
+def get_protos(proto,addr_type,toolname):
 
-def parse_arg1(arg,arg_id):
+	init_genonly_altcoins(testnet=proto.testnet)
 
-	m1 = 'First argument must be a numeric generator ID or two colon-separated generator IDs'
-	m2 = 'Second part of first argument must be a numeric generator ID or one of {}'
+	for coin in ci.external_tests[proto.network][toolname]:
+		if coin.lower() not in CoinProtocol.coins:
+			continue
+		ret = init_proto(coin,testnet=proto.testnet)
+		if addr_type not in ret.mmtypes:
+			continue
+		yield ret
 
-	def check_gen_num(n):
-		if not (1 <= int(n) <= len(g.key_generators)):
-			die(1,f'{n}: invalid generator ID')
-		return int(n)
+def parse_args():
+
+	if len(cmd_args) != 2:
+		opts.usage()
+
+	arg1,arg2 = cmd_args
+	pa = namedtuple('parsed_args',['test','gen_num','rounds','arg'])
+
+	if is_int(arg1) and is_int(arg2):
+		return pa( test='speed', gen_num=arg1, rounds=int(arg2), arg=None )
+
+	if is_int(arg1) and os.access(arg2,os.R_OK):
+		return pa( test='dump', gen_num=arg1, rounds=None, arg=arg2 )
+
+	if not is_int(arg2):
+		die(1,'Second argument must be dump filename or integer rounds specification')
 
-	if arg_id == 'a':
-		if is_int(arg):
-			a_num = check_gen_num(arg)
-			return (KeyGenerator(proto,addr_type,a_num),a_num)
-		else:
-			die(1,m1)
-	elif arg_id == 'b':
-		if is_int(arg):
-			return KeyGenerator(proto,addr_type,check_gen_num(arg))
-		elif arg in ext_progs + ['ext']:
-			return init_tool(get_tool(arg))
-		else:
-			die(1,m2.format(ext_progs))
-
-def parse_arg2():
-	m = 'Second argument must be dump filename or integer rounds specification'
-	if len(cmd_args) == 1:
-		return None
-	arg = cmd_args[1]
-	if is_int(arg) and int(arg) > 0:
-		return int(arg)
 	try:
-		return open(arg)
+		a,b = arg1.split(':')
 	except:
-		die(1,m)
+		die(1,'First argument must be a generator backend number or two colon-separated arguments')
+
+	if not is_int(a) and a != 'all':
+		die(1,"First part of first argument must be a generator backend number or 'all'")
+
+	if is_int(b):
+		if opt.all_coins:
+			die(1,'--all-coins must be used with external tool only')
+	else:
+		proto = init_proto_from_opts()
+		ext_progs = list(ci.external_tests[proto.network]) + ['ext']
+		if b not in ext_progs:
+			die(1,f'Second part of first argument must be a generator backend number or one of {ext_progs}')
+
+	return pa( test='ab', gen_num=a, rounds=int(arg2), arg=b )
+
+def main():
 
-# begin execution
-from mmgen.protocol import init_proto
+	pa = parse_args()
+	proto = init_proto_from_opts()
+	addr_type = MMGenAddrType( proto=proto, id_str=opt.type or proto.dfl_mmtype )
+
+	if pa.test == 'ab':
+		protos = get_protos(proto,addr_type,pa.arg) if opt.all_coins else [proto]
+		for proto in protos:
+			ab_test( proto, pa.gen_num, pa.rounds, toolname_or_gen2_num=pa.arg )
+	else:
+		kg = KeyGenerator( proto, addr_type, pa.gen_num )
+		ag = AddrGenerator( proto, addr_type )
+		if pa.test == 'speed':
+			speed_test( proto, addr_type, kg, ag, pa.rounds )
+		elif pa.test == 'dump':
+			dump_test( proto, kg, ag, filename=pa.arg )
+
+	if saved_results:
+		import json
+		with open(results_file,'w') as fp:
+			fp.write(json.dumps( saved_results, indent=4 ))
+
+from subprocess import run,PIPE,DEVNULL
+from collections import namedtuple
+from mmgen.protocol import init_proto,init_proto_from_opts,CoinProtocol,init_genonly_altcoins
 from mmgen.altcoin import CoinInfo as ci
 from mmgen.key import PrivKey
 from mmgen.addr import KeyGenerator,AddrGenerator,MMGenAddrType
 
-addr_type = MMGenAddrType(
-	proto = proto,
-	id_str = opt.type or proto.dfl_mmtype )
-
-ext_progs = list(ci.external_tests[proto.network])
-
-arg1 = cmd_args[0].split(':')
-
-if len(arg1) == 1:
-	a,a_num = parse_arg1(arg1[0],'a')
-	b = None
-elif len(arg1) == 2:
-	a,a_num = parse_arg1(arg1[0],'a')
-	b = parse_arg1(arg1[1],'b')
-else:
-	opts.usage()
-
-if type(a) == type(b):
-	rdie(1,'Address generators are the same!')
-
-arg2 = parse_arg2()
-
-if not opt.all:
-	ag = AddrGenerator(proto,addr_type)
-
-if not b and type(arg2) == int:
-	speed_test(a,ag,arg2)
-elif not b and hasattr(arg2,'read'):
-	dump_test(a,ag,arg2)
-elif a and b and type(arg2) == int:
-	if opt.all:
-		from mmgen.protocol import CoinProtocol,init_genonly_altcoins
-		init_genonly_altcoins(testnet=proto.testnet)
-		for coin in ci.external_tests[proto.network][b.desc]:
-			if coin.lower() not in CoinProtocol.coins:
-#				ymsg(f'Coin {coin} not configured')
-				continue
-			proto = init_proto(coin)
-			if addr_type not in proto.mmtypes:
-				continue
-			# proto has changed, so reinit kg and ag
-			a = KeyGenerator(proto,addr_type,a_num)
-			ag = AddrGenerator(proto,addr_type)
-			b_chk = ci.get_test_support(proto.coin,addr_type.name,proto.network,tool=b.desc,verbose=not opt.quiet)
-			if b_chk == b.desc:
-				gentool_test(a,b,ag,arg2)
-	else:
-		gentool_test(a,b,ag,arg2)
-else:
-	opts.usage()
+sys.argv = [sys.argv[0]] + ['--skip-cfg-file'] + sys.argv[1:]
+cmd_args = opts.init(opts_data,add_opts=['exact_output'])
+
+main()

+ 1 - 1
test/test-release.sh

@@ -330,7 +330,7 @@ t_alts="
 	m #   moneropy
 	m $gentest_py --all --coin=xmr 2:moneropy $rounds_min # very slow, be patient!
 	z #   zcash-mini
-	z $gentest_py --all 2:zcash-mini $rounds_mid
+	z $gentest_py --all --coin=zec --type=zcash_z 1:zcash-mini $rounds_mid
 "
 
 [ "$MSYS2" ] && t_alts_skip='m z'  # no moneropy (pysha3), zcash-mini (golang)