From 3224b9c7d2a2dc1cc96e1cca4d19f671ccee6cb4 Mon Sep 17 00:00:00 2001 From: MMGen Date: Sat, 19 Oct 2019 15:22:30 +0000 Subject: [PATCH] new test: objattrtest.py: test objects' immutable attributes --- test/objattrtest.py | 164 +++++++++++++++++++++++ test/objattrtest_py_d/__init__.py | 0 test/objattrtest_py_d/oat_btc_mainnet.py | 164 +++++++++++++++++++++++ test/objattrtest_py_d/oat_common.py | 53 ++++++++ test/test-release.sh | 4 + 5 files changed, 385 insertions(+) create mode 100755 test/objattrtest.py create mode 100755 test/objattrtest_py_d/__init__.py create mode 100755 test/objattrtest_py_d/oat_btc_mainnet.py create mode 100755 test/objattrtest_py_d/oat_common.py diff --git a/test/objattrtest.py b/test/objattrtest.py new file mode 100755 index 00000000..4971ddea --- /dev/null +++ b/test/objattrtest.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 The MMGen Project +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +""" +test/objattrtest.py: Test immutable attributes of MMGen data objects +""" + +# TODO: test 'typeconv' during instance creation + +import sys,os +pn = os.path.dirname(sys.argv[0]) +os.chdir(os.path.join(pn,os.pardir)) +sys.path.__setitem__(0,os.path.abspath(os.curdir)) + +os.environ['MMGEN_TEST_SUITE'] = '1' + +# Import these _after_ local path's been added to sys.path +from test.objattrtest_py_d.oat_common import * + +opts_data = { + 'sets': [ + ('show_nonstandard_init', True, 'verbose', True), + ('show_descriptor_type', True, 'verbose', True), + ], + 'text': { + 'desc': 'Test immutable attributes of MMGen data objects', + 'usage':'[options] [object]', + 'options': """ +-h, --help Print this help message +--, --longhelp Print help message for long options (common options) +-i, --show-nonstandard-init Display non-standard attribute initialization info +-d, --show-descriptor-type Display the attribute's descriptor type +-v, --verbose Produce more verbose output +""" + } +} + +cmd_args = opts.init(opts_data) +init_coin(g.coin) +from mmgen.tw import * + +pd = namedtuple('permission_bits', ['read_ok','delete_ok','reassign_ok']) + +def parse_permbits(bits): + return pd( + bool(0b001 & bits), # read + bool(0b010 & bits), # delete + bool(0b100 & bits), # reassign + ) + +def get_descriptor_obj(objclass,attrname): + for o in (objclass,objclass.__bases__[0]): # assume there's only one base class + if attrname in o.__dict__: + return o.__dict__[attrname] + rdie(3,'unable to find descriptor {}.{}'.format(objclass.__name__,attrname)) + +def test_attr_perm(obj,attrname,perm_name,perm_value,dobj,attrval_type): + + class SampleObjError(Exception): pass + + pname = perm_name.replace('_ok','') + pstem = pname.rstrip('e') + + try: + if perm_name == 'read_ok': + getattr(obj,attrname) + elif perm_name == 'reassign_ok': + try: + so = sample_objs[attrval_type.__name__] + except: + raise SampleObjError('unable to find sample object of type {!r}'.format(attrval_type.__name__)) + # MMGenListItemAttr allows setting an attribute if its value is None + if type(dobj) == MMGenListItemAttr and getattr(obj,attrname) == None: + setattr(obj,attrname,so) + setattr(obj,attrname,so) + elif perm_name == 'delete_ok': + delattr(obj,attrname) + except SampleObjError as e: + rdie(2,'Test script error ({})'.format(e)) + except Exception as e: + if perm_value == True: + fs = '{!r}: unable to {} attribute {!r}, though {}ing is allowed ({})' + rdie(2,fs.format(type(obj).__name__,pname,attrname,pstem,e)) + else: + if perm_value == False: + fs = '{!r}: attribute {!r} is {n}able, though {n}ing is forbidden' + rdie(2,fs.format(type(obj).__name__,attrname,n=pstem)) + +def test_attr(data,obj,attrname,dobj,bits,attrval_type): + if hasattr(obj,attrname): # TODO + td_attrval_type = data.attrs[attrname][1] + + if attrval_type not in (td_attrval_type,type(None)): + fs = '\nattribute {!r} of {!r} instance has incorrect type {!r} (should be {!r})' + rdie(2,fs.format(attrname,type(obj).__name__,attrval_type.__name__,td_attrval_type.__name__)) + + if hasattr(dobj,'__dict__'): + d = dobj.__dict__ + bits = bits._asdict() + for k in ('reassign_ok','delete_ok'): + if k in d: + if d[k] != bits[k]: + fs = 'init value {iv}={a} for attr {n!r} does not match test data ({iv}={b})' + rdie(2,fs.format(iv=k,n=attrname,a=d[k],b=bits[k])) + if opt.verbose and d[k] == True: + msg_r(' {}={!r}'.format(k,d[k])) + + if opt.show_nonstandard_init: + for k,v in (('typeconv',False),('no_type_check',True),('set_none_ok',True)): + if d[k] == v: + msg_r(' {}={}'.format(k,v)) + +def test_object(test_data,objname): + + if '.' in objname: + on1,on2 = objname.split('.') + cls = getattr(globals()[on1],on2) + else: + cls = globals()[objname] + + fs = 'Testing attribute ' + ('{!r:<15}{dt:13}' if opt.show_descriptor_type else '{!r}') + data = test_data[objname] + obj = cls(*data.args,**data.kwargs) + + for attrname,adata in data.attrs.items(): + dobj = get_descriptor_obj(type(obj),attrname) + if opt.verbose: + msg_r(fs.format(attrname,dt=type(dobj).__name__.replace('MMGen',''))) + bits = parse_permbits(adata[0]) + test_attr(data,obj,attrname,dobj,bits,adata[1]) + for perm_name,perm_value in bits._asdict().items(): + test_attr_perm(obj,attrname,perm_name,perm_value,dobj,adata[1]) + vmsg('') + +def do_loop(): + import importlib + network = ('mainnet','testnet')[bool(g.testnet)] + modname = 'test.objattrtest_py_d.oat_{}_{}'.format(g.coin.lower(),network) + test_data = importlib.import_module(modname).tests + gmsg('Running immutable attribute tests for {} {}'.format(g.coin,network)) + + utests = cmd_args + for obj in test_data: + if utests and obj not in utests: continue + clr = blue if opt.verbose else nocolor + msg(clr('Testing {}'.format(obj))) + test_object(test_data,obj) + +do_loop() diff --git a/test/objattrtest_py_d/__init__.py b/test/objattrtest_py_d/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/test/objattrtest_py_d/oat_btc_mainnet.py b/test/objattrtest_py_d/oat_btc_mainnet.py new file mode 100755 index 00000000..e9b241a8 --- /dev/null +++ b/test/objattrtest_py_d/oat_btc_mainnet.py @@ -0,0 +1,164 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 The MMGen Project + +""" +test.objattrtest_py_d.oat_btc_mainnet: BTC mainnet test vectors for MMGen data +objects +""" + +from .oat_common import * + +sample_objs.update({ + 'PrivKey': PrivKey(seed_bin,compressed=True,pubkey_type='std'), + 'WifKey': WifKey('5HwzecKMWD82ppJK3qMKpC7ohXXAwcyAN5VgdJ9PLFaAzpBG4sX'), + 'CoinAddr': CoinAddr('1111111111111111111114oLvT2'), + 'BTCAmt': BTCAmt('0.01'), + 'MMGenID': MMGenID('F00F00BB:B:1'), + 'TwMMGenID': TwMMGenID('F00F00BB:S:23'), +}) + +tests = { + # addr.py + 'AddrListEntry': atd({ + 'addr': (0b001, CoinAddr), + 'idx': (0b001, AddrIdx), + 'label': (0b101, TwComment), + 'sec': (0b001, PrivKey), +# 'viewkey': (0b001, ViewKey), # TODO +# 'wallet_passwd': (0b001, WalletPassword), # TODO + }, + [], + {} + ), + 'PasswordListEntry': atd({ + 'passwd': (0b001, str), + 'idx': (0b001, AddrIdx), + 'label': (0b101, TwComment), + 'sec': (0b001, PrivKey), + }, + [], + {'passwd':'ΑlphaΩmega', 'idx':1 }, + ), + # obj.py + 'PrivKey': atd({ + 'compressed': (0b001, bool), + 'wif': (0b001, WifKey), + }, + [seed_bin], + {'compressed':True, 'pubkey_type':'std'}, + ), + 'MMGenAddrType': atd({ + 'name': (0b001, str), + 'pubkey_type': (0b001, str), + 'compressed': (0b001, bool), + 'gen_method': (0b001, str), + 'addr_fmt': (0b001, str), + 'wif_label': (0b001, str), + 'extra_attrs': (0b001, tuple), + 'desc': (0b001, str), + }, + ['S'], + {}, + ), + # seed.py + 'SeedBase': atd({ + 'data': (0b001, bytes), + 'sid': (0b001, SeedID), + }, + [seed_bin], + {}, + ), + 'SubSeed': atd({ + 'idx': (0b001, int), + 'nonce': (0b001, int), + 'ss_idx': (0b001, SubSeedIdx), + }, + [sample_objs['SubSeedList'],1,1,'short'], + {}, + ), + 'SeedShareList': atd({ + 'count': (0b001, SeedShareCount), + 'id_str': (0b001, SeedSplitIDString), + }, + [sample_objs['Seed'],sample_objs['SeedShareCount']], + {}, + ), + 'SeedShareLast': atd({ + 'idx': (0b001, SeedShareIdx), + }, + [sample_objs['SeedShareList']], + {}, + ), + 'SeedShareMaster': atd({ + 'idx': (0b001, MasterShareIdx), + 'nonce': (0b001, int), + }, + [sample_objs['SeedShareList'],7,0], + {}, + ), + 'SeedShareMasterJoining': atd({ + 'id_str': (0b001, SeedSplitIDString), + 'count': (0b001, SeedShareCount), + }, + [sample_objs['MasterShareIdx'], sample_objs['Seed'], 'foo', 2], + {}, + ), + # tw.py + 'TwUnspentOutputs.MMGenTwUnspentOutput': atd({ + 'txid': (0b001, CoinTxID), + 'vout': (0b001, int), + 'amt': (0b001, BTCAmt), + 'amt2': (0b001, BTCAmt), + 'label': (0b101, TwComment), + 'twmmid': (0b001, TwMMGenID), + 'addr': (0b001, CoinAddr), + 'confs': (0b001, int), + 'scriptPubKey': (0b001, HexStr), + 'days': (0b001, int), + 'skip': (0b101, str), + }, + [], + { + 'amt':BTCAmt('0.01'), + 'twmmid':'F00F00BB:B:17', + 'addr':'1111111111111111111114oLvT2', + 'confs': 100000, + 'scriptPubKey':'ff', + }, + + ), + # tx.py + 'MMGenTxInput': atd({ + 'vout': (0b001, int), + 'amt': (0b001, BTCAmt), + 'label': (0b101, TwComment), + 'mmid': (0b001, MMGenID), + 'addr': (0b001, CoinAddr), + 'confs': (0b001, int), + 'txid': (0b001, CoinTxID), + 'have_wif': (0b011, bool), + 'scriptPubKey': (0b001, HexStr), + 'sequence': (0b001, int), + }, + [], + { 'amt':BTCAmt('0.01'), 'addr':sample_objs['CoinAddr'] }, + ), + 'MMGenTxOutput': atd({ + 'vout': (0b001, int), + 'amt': (0b001, BTCAmt), + 'label': (0b101, TwComment), + 'mmid': (0b001, MMGenID), + 'addr': (0b001, CoinAddr), + 'confs': (0b001, int), + 'txid': (0b001, CoinTxID), + 'have_wif': (0b011, bool), + 'is_chg': (0b001, bool), + }, + [], + { 'amt':BTCAmt('0.01'), 'addr':sample_objs['CoinAddr'] }, + ), +} + +tests['MMGenPasswordType'] = atd(tests['MMGenAddrType'].attrs, ['P'], {}) diff --git a/test/objattrtest_py_d/oat_common.py b/test/objattrtest_py_d/oat_common.py new file mode 100755 index 00000000..6023c15c --- /dev/null +++ b/test/objattrtest_py_d/oat_common.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# +# mmgen = Multi-Mode GENerator, command-line Bitcoin cold storage solution +# Copyright (C)2013-2019 The MMGen Project + +""" +test.objattrtest_py_d.oat_common: shared data for MMGen data objects tests +""" + +import os +from decimal import Decimal + +from mmgen.obj import * +from mmgen.seed import * +from mmgen.protocol import * +from mmgen.addr import * +from mmgen.tx import * + +from collections import namedtuple +atd = namedtuple('attrtest_entry',['attrs','args','kwargs']) + +seed_bin = os.urandom(32) + +# use the constructors here! otherwise reassignment test might fail when +# reassignment would otherwise succeed +sample_objs = { + 'int': int(1), + 'Decimal': Decimal('0.01'), + 'NoneType': None, + 'bool': bool(True), + 'str': str('foo'), + 'dict': dict({'a':1}), + 'list': list([1]), + 'tuple': tuple((1,2)), + 'bytes': bytes(1), + + 'HexStr': HexStr('ff'), + 'AddrIdx': AddrIdx(1), + 'TwComment': TwComment('αω'), + 'CoinTxID': CoinTxID('aa'*32), + + 'SeedID': SeedID(sid='F00F00BB'), + 'Seed': Seed(seed_bin=seed_bin), + + 'SubSeedList': SubSeedList(Seed(seed_bin=seed_bin)), + 'SubSeedIdx': SubSeedIdx('1S'), + + 'SeedSplitIDString': SeedSplitIDString('alice'), + 'SeedShareList': SeedShareList(Seed(seed_bin=seed_bin),SeedShareCount(2)), + 'SeedShareIdx': SeedShareIdx(1), + 'SeedShareCount': SeedShareCount(2), + 'MasterShareIdx': MasterShareIdx(7), +} diff --git a/test/test-release.sh b/test/test-release.sh index 85667543..bf22e369 100755 --- a/test/test-release.sh +++ b/test/test-release.sh @@ -21,6 +21,7 @@ export MMGEN_NO_LICENSE=1 export PYTHONPATH=. test_py='test/test.py -n' objtest_py='test/objtest.py' +objattrtest_py='test/objattrtest.py' unit_tests_py='test/unit_tests.py --names --quiet' tooltest_py='test/tooltest.py' tooltest2_py='test/tooltest2.py --names' @@ -88,6 +89,7 @@ do python="python3 -m trace --count --file=test/trace.acc --coverdir=test/trace" unit_tests_py="$python $unit_tests_py" objtest_py="$python $objtest_py" + objattrtest_py="$python $objattrtest_py" gentest_py="$python $gentest_py" mmgen_tool="$python $mmgen_tool" mmgen_keygen="$python $mmgen_keygen" ;& @@ -107,6 +109,7 @@ do tooltest_py+=" --verbose" tooltest2_py+=" --verbose" gentest_py+=" --verbose" mmgen_tool+=" --verbose" unit_tests_py="${unit_tests_py/--quiet/--verbose}" + objattrtest_py+=" --verbose" scrambletest_py+=" --verbose" ;; *) exit ;; esac @@ -194,6 +197,7 @@ t_obj=" $objtest_py --coin=ltc $objtest_py --coin=ltc --testnet=1 $objtest_py --coin=eth + $objattrtest_py " f_obj='Data object test complete'