diff --git a/external/ckcc-protocol b/external/ckcc-protocol index 0e686dbd..f87d30f2 160000 --- a/external/ckcc-protocol +++ b/external/ckcc-protocol @@ -1 +1 @@ -Subproject commit 0e686dbda686f76c4d3e8069558b2a31f9d1c2b1 +Subproject commit f87d30f220cb6334eb3c4ace93c1b62e04942022 diff --git a/testing/api.py b/testing/api.py index 942e168c..918ec517 100644 --- a/testing/api.py +++ b/testing/api.py @@ -51,7 +51,7 @@ class Bitcoind: [ self.bitcoind_path, # needed for newest master - # TODO legacy wallet will be deprecated in 26 + # TODO legacy wallet will be deprecated in 29 "-deprecatedrpc=create_bdb", "-regtest", f"-datadir={self.datadir}", @@ -59,6 +59,7 @@ class Bitcoind: "-fallbackfee=0.0002", "-server=1", "-keypool=1", + "-listen=0" f"-port={self.p2p_port}", f"-rpcport={self.rpc_port}" ] diff --git a/testing/clone_tests.py b/testing/clone_tests.py index 5f423ec0..31ab03a1 100644 --- a/testing/clone_tests.py +++ b/testing/clone_tests.py @@ -5,7 +5,7 @@ from charcodes import KEY_ENTER from core_fixtures import _pick_menu_item, _cap_story, _press_select from core_fixtures import _need_keypress, _cap_menu, _sim_exec from run_sim_tests import ColdcardSimulator, clean_sim_data -from ckcc_protocol.client import ColdcardDevice, CKCC_SIMULATOR_PATH +from ckcc_protocol.client import ColdcardDevice def _clone(source, target): @@ -18,7 +18,7 @@ def _clone(source, target): clean_sim_data() # remove all from previous sim_target = ColdcardSimulator(args=[target_sim_arg, "-l"]) sim_target.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, target_is_Q, "Import Existing") _pick_menu_item(device, target_is_Q, "Clone Coldcard") time.sleep(.1) @@ -36,7 +36,7 @@ def _clone(source, target): sim_source = ColdcardSimulator(args=[source_sim_arg, "--ms", "--p2wsh", "--set", "nfc=1", "--set", "vidsk=1"]) sim_source.start(start_wait=6) - device_source = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device_source = ColdcardDevice(is_simulator=True) _pick_menu_item(device_source, source_is_Q, "Advanced/Tools") time.sleep(.1) _pick_menu_item(device_source, source_is_Q, "Backup") @@ -89,7 +89,7 @@ def _clone(source, target): # TARGET again. Killed now - restart and verify settings sim_target = ColdcardSimulator(args=[target_sim_arg]) sim_target.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, target_is_Q, "Settings") _pick_menu_item(device, target_is_Q, "Multisig Wallets") time.sleep(.1) diff --git a/testing/conftest.py b/testing/conftest.py index 730e4417..aaecc2ef 100644 --- a/testing/conftest.py +++ b/testing/conftest.py @@ -13,13 +13,15 @@ from api import bitcoind_d_sim_watch, finalize_v2_v0_convert from binascii import b2a_hex, a2b_hex from constants import * from charcodes import * -from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen +from core_fixtures import _need_keypress, _sim_exec, _cap_story, _cap_menu, _cap_screen, _sim_eval from core_fixtures import _press_select, _pick_menu_item, _enter_complex, _dev_hw_label # lock down randomness random.seed(42) +# needs to be run from /testing directory +os.environ["SRC_ROOT"] = os.path.join(os.getcwd().rsplit("/", 1)[0]) if sys.platform == 'darwin': # BUGFIX: my ARM-based MacOS system uses rosetta to run Python in x86 mode # and so I needed this? @@ -37,6 +39,7 @@ def pytest_addoption(parser): default=False, help="operator must press keys on real CC") parser.addoption("--mk", default=4, help="Assume mark N hardware") + parser.addoption("--sim-socket", "-S", type=str, help="Simulator .socket path", default=None) parser.addoption("--duress", action="store_true", default=False, help="assume logged-in with duress PIN") @@ -79,48 +82,42 @@ def simulator(request): raise pytest.skip('need simulator for this test, have real device') try: - return ColdcardDevice(sn=SIM_PATH) - except: + return ColdcardDevice(sn=request.config.getoption("--sim-socket"), is_simulator=True) + except Exception as e: print("Simulator is required for this test") raise pytest.fail('missing simulator') -@pytest.fixture(scope='module') +@pytest.fixture def sim_exec(dev): - # run code in the simulator's interpretor + # run code in the simulator's interpreter # - can work on real product too, if "debug build" is used. f = functools.partial(_sim_exec, dev) return f -@pytest.fixture(scope='module') +@pytest.fixture def sim_eval(dev): # eval an expression in the simulator's interpretor # - can work on real product too, if "debug build" is used. + f = functools.partial(_sim_eval, dev) + return f - def doit(cmd, timeout=None): - return dev.send_recv(b'EVAL' + cmd.encode('utf-8'), timeout=timeout).decode('utf-8') - - return doit - -@pytest.fixture(scope='module') -def sim_execfile(simulator): +@pytest.fixture +def sim_execfile(simulator, src_root_dir): # run a whole file in the simulator's interpretor # - requires shared filesystem - import os - def doit(fname, timeout=None): - fn = os.path.realpath(fname) - hook = 'execfile("%s")' % fn + hook = 'execfile("%s")' % (src_root_dir + "/testing/" + fname) return simulator.send_recv(b'EXEC' + hook.encode('utf-8'), timeout=timeout).decode('utf-8') return doit -@pytest.fixture(scope='module') +@pytest.fixture def is_simulator(dev): def doit(): return hasattr(dev.dev, 'pipe') return doit -@pytest.fixture(scope='module') +@pytest.fixture def send_ux_abort(simulator): def doit(): @@ -138,7 +135,7 @@ def OK(is_q1): def X(is_q1): return "CANCEL" if is_q1 else "X" -@pytest.fixture(scope='module') +@pytest.fixture def need_keypress(dev, request): def doit(k, timeout=1000): if request.config.getoption("--manual"): @@ -152,7 +149,7 @@ def need_keypress(dev, request): return doit -@pytest.fixture(scope='module') +@pytest.fixture def enter_number(need_keypress, press_select): def doit(number): number = str(number) if not isinstance(number, str) else number @@ -170,7 +167,7 @@ def enter_complex(dev, is_q1): f = functools.partial(_enter_complex, dev, is_q1) return f -@pytest.fixture(scope='module') +@pytest.fixture def enter_hex(need_keypress, enter_text, is_q1): def doit(hex_str): if is_q1: @@ -185,7 +182,7 @@ def enter_hex(need_keypress, enter_text, is_q1): return doit -@pytest.fixture(scope='module') +@pytest.fixture def enter_pin(enter_number, press_select, cap_screen, is_q1): def doit(pin): assert '-' in pin @@ -207,7 +204,7 @@ def enter_pin(enter_number, press_select, cap_screen, is_q1): return doit -@pytest.fixture(scope='module') +@pytest.fixture def do_keypresses(need_keypress): # do a series of keypresses, any kind def doit(value): @@ -217,7 +214,7 @@ def do_keypresses(need_keypress): return doit -@pytest.fixture(scope='module') +@pytest.fixture def enter_text(need_keypress, is_q1): # enter a text value, might be a number or string ... on Q can be multiline def doit(value, multiline=False): @@ -260,14 +257,14 @@ def master_xpub(dev): return r -@pytest.fixture(scope='module') +@pytest.fixture def unit_test(sim_execfile): def doit(filename): rv = sim_execfile(filename) if rv: pytest.fail(rv) return doit -@pytest.fixture(scope='module') +@pytest.fixture def get_settings(sim_execfile): # get all settings def doit(): @@ -278,7 +275,7 @@ def get_settings(sim_execfile): return doit -@pytest.fixture(scope='module') +@pytest.fixture def get_setting(sim_execfile, sim_exec): # get an individual setting def doit(name, default=None): @@ -290,15 +287,15 @@ def get_setting(sim_execfile, sim_exec): return doit -@pytest.fixture(scope='module') +@pytest.fixture def addr_vs_path(master_xpub): - from bip32 import BIP32Node - from ckcc_protocol.constants import AF_CLASSIC, AFC_PUBKEY, AF_P2WPKH, AFC_SCRIPT - from ckcc_protocol.constants import AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH - from bech32 import bech32_decode, convertbits, Encoding - from hashlib import sha256 - def doit(given_addr, path=None, addr_fmt=None, script=None, testnet=True): + from bip32 import BIP32Node + from ckcc_protocol.constants import AF_CLASSIC, AFC_PUBKEY, AF_P2WPKH, AFC_SCRIPT + from ckcc_protocol.constants import AF_P2WPKH_P2SH, AF_P2SH, AF_P2WSH, AF_P2WSH_P2SH + from bech32 import bech32_decode, convertbits, Encoding + from hashlib import sha256 + if not script: try: # prefer using xpub if we can @@ -369,13 +366,13 @@ def capture_enabled(sim_eval): # - could be xfail or xskip here assert sim_eval("'sim_display' in sys.modules") == 'True' -@pytest.fixture(scope='module') +@pytest.fixture def cap_menu(dev): "Return menu items as a list" f = functools.partial(_cap_menu, dev) return f -@pytest.fixture(scope='module') +@pytest.fixture def is_ftux_screen(sim_exec): "are we presenting a view from ftux.py??" def doit(): @@ -402,12 +399,12 @@ def expect_ftux(cap_menu, cap_story, press_select, is_ftux_screen): return doit -@pytest.fixture(scope='module') +@pytest.fixture def cap_screen(dev): f = functools.partial(_cap_screen, dev) return f -@pytest.fixture(scope='module') +@pytest.fixture def cap_text_box(cap_screen): # provides text inside a lined box on the screen right now - Q1 only def doit(): @@ -423,15 +420,15 @@ def cap_text_box(cap_screen): return doit -@pytest.fixture(scope='module') +@pytest.fixture def cap_story(dev): # returns (title, body) of whatever story is being actively shown f = functools.partial(_cap_story, dev) return f -@pytest.fixture(scope='module') -def cap_image(request, sim_exec, is_q1, is_headless): +@pytest.fixture +def cap_image(request, sim_exec, is_q1, is_headless, sim_root_dir): def flip(raw): reorg = bytearray(128*64) @@ -451,7 +448,7 @@ def cap_image(request, sim_exec, is_q1, is_headless): if is_headless: raise pytest.skip("headless mode: QR tests disabled") # trigger simulator to capture a snapshot into a named file, read it. - fn = os.path.realpath(f'./debug/snap-{random.randint(int(1E6), int(9E6))}.png') + fn = os.path.realpath(f'{sim_root_dir}/debug/snap-{random.randint(int(1E6), int(9E6))}.png') try: sim_exec(f"from glob import dis; dis.dis.save_snapshot({fn!r})") for _ in range(20): @@ -480,7 +477,7 @@ RV.write(b2a_hex(dis.dis.buffer))''')) QR_HISTORY = [] @pytest.fixture(scope='session') -def qr_quality_check(): +def qr_quality_check(sim_root_dir): # Use this with cap_screen_qr print("QR codes will be captured and shown at end of run.") yield None @@ -529,13 +526,13 @@ def qr_quality_check(): #rv = rv.resize(tuple(c*4 for c in rv.size), resample=Image.NEAREST) - rv.save('debug/all-qrs.png') + rv.save(f'{sim_root_dir}/debug/all-qrs.png') rv.show() -@pytest.fixture(scope='module') -def cap_screen_qr(cap_image): +@pytest.fixture +def cap_screen_qr(cap_image, sim_root_dir): def doit(no_history=False): # NOTE: version=4 QR is pixel doubled to be 66x66 with 2 missing lines at bottom # LATER: not doing that anymore; v={1,2,3} doubled, all higher 1:1 pixels (tiny) @@ -570,7 +567,7 @@ def cap_screen_qr(cap_image): w, h = orig_img.size # 320x240 img = orig_img.crop( (0, 0, w, h-5) ).convert('L') - img.save('debug/last-qr.png') + img.save(f'{sim_root_dir}/debug/last-qr.png') #img.show() # Above usually works @ zoom=1, but not always! @@ -618,8 +615,8 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1): # - skips first line, which on Q shows the index number sometimes # - insists on some spaces full = cap_screen() + full_split = full.split("\n") if is_q1: - full_split = full.split("\n") if is_change: for i, (c, line) in enumerate(zip("XXXXCHANGE", full_split)): if i > 3: @@ -640,14 +637,18 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1): for c, line in zip("XXXXXXBACK", full_split): assert not line.endswith(c) - txt = ''.join(l for l in full.split() if len(l)>4).replace('~', '') + txt = ''.join(l for l in full_split if len(l)>4).replace('~', '') + if txt: + # just index remained + int(txt) + txt = None else: if is_change: assert "CHANGE BACK" in full elif is_change is False: assert "CHANGE BACK" not in full - txt = ''.join(full.split("\n")).replace('CHANGE BACK', '') + txt = ''.join(full_split).replace('CHANGE BACK', '') if txt: assert txt == qr @@ -662,7 +663,7 @@ def verify_qr_address(cap_screen_qr, cap_screen, is_q1): return doit -@pytest.fixture(scope='module') +@pytest.fixture def get_pp_sofar(sim_exec): # get entry value for bip39 passphrase def doit(): @@ -672,7 +673,7 @@ def get_pp_sofar(sim_exec): return doit -@pytest.fixture(scope='module') +@pytest.fixture def get_secrets(sim_execfile): # returns big dict based on what we'd normally put into a backup file. def doit(): @@ -692,48 +693,48 @@ def get_secrets(sim_execfile): return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_select(dev, has_qwerty): f = functools.partial(_press_select, dev, has_qwerty) return f -@pytest.fixture(scope='module') +@pytest.fixture def press_cancel(need_keypress, has_qwerty): def doit(**kws): need_keypress(KEY_CANCEL if has_qwerty else 'x', **kws) return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_delete(need_keypress, has_qwerty): def doit(**kws): need_keypress(KEY_DELETE if has_qwerty else 'x', **kws) return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_nfc(need_keypress, has_qwerty): def doit(num=3, **kws): need_keypress(KEY_NFC if has_qwerty else str(num), **kws) return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_up(need_keypress, has_qwerty): def doit(**kws): need_keypress(KEY_UP if has_qwerty else "5", **kws) return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_down(need_keypress, has_qwerty): def doit(**kws): need_keypress(KEY_DOWN if has_qwerty else "8", **kws) return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_left(need_keypress, has_qwerty): def doit(**kws): need_keypress(KEY_LEFT if has_qwerty else "7", **kws) return doit -@pytest.fixture(scope='module') +@pytest.fixture def press_right(need_keypress, has_qwerty): def doit(**kws): need_keypress(KEY_RIGHT if has_qwerty else "9", **kws) @@ -775,24 +776,37 @@ def goto_home(cap_menu, press_cancel, press_select, pick_menu_item, cap_screen): return doit -@pytest.fixture(scope="module") +@pytest.fixture def pick_menu_item(dev, has_qwerty): f = functools.partial(_pick_menu_item, dev, has_qwerty) return f -@pytest.fixture(scope='module') -def virtdisk_path(request, is_simulator, needs_virtdisk): +@pytest.fixture(scope='session') +def src_root_dir(): + return os.environ.get("SRC_ROOT") + +@pytest.fixture(scope='session') +def sim_root_dir(dev, request, src_root_dir): + if request.config.getoption("--dev"): + return os.path.join(src_root_dir, "unix/work") + + cmd = f"import ckcc; RV.write(ckcc.get_sim_root_dirs()[0])" + rv = _sim_exec(dev, cmd) + return rv + +@pytest.fixture +def virtdisk_path(request, is_simulator, needs_virtdisk, sim_root_dir): # get a path to indicated filename on emulated/shared dir def doit(fn): - # could use: ckcc.get_sim_root_dirs() here if is_simulator(): get_setting = request.getfixturevalue('get_setting') if not get_setting('vidsk', False): raise pytest.xfail('virtdisk disabled') - assert os.path.isdir('../unix/work/VirtDisk') - return '../unix/work/VirtDisk/' + fn + + return sim_root_dir + '/VirtDisk/' + fn elif sys.platform == 'darwin': + # TODO if not request.config.getoption("--manual"): raise pytest.fail('must use --manual CLI option') @@ -803,7 +817,7 @@ def virtdisk_path(request, is_simulator, needs_virtdisk): return doit -@pytest.fixture(scope='module') +@pytest.fixture def virtdisk_wipe(dev, needs_virtdisk, virtdisk_path): def doit(): for fn in glob.glob(virtdisk_path('*')): @@ -815,13 +829,12 @@ def virtdisk_wipe(dev, needs_virtdisk, virtdisk_path): return doit -@pytest.fixture(scope='module') -def microsd_path(simulator): +@pytest.fixture +def microsd_path(simulator, sim_root_dir): # open a file from the simulated microsd def doit(fn): - # could use: ckcc.get_sim_root_dirs() here - return '../unix/work/MicroSD/' + fn + return sim_root_dir + '/MicroSD/' + fn return doit @@ -836,7 +849,7 @@ def microsd_wipe(microsd_path): os.remove(dir + fname) return doit -@pytest.fixture(scope='module') +@pytest.fixture def open_microsd(simulator, microsd_path): # open a file from the simulated microsd @@ -846,13 +859,12 @@ def open_microsd(simulator, microsd_path): return doit -@pytest.fixture(scope='module') -def settings_path(simulator): +@pytest.fixture +def settings_path(simulator, sim_root_dir): # open a file from the simulated microsd def doit(fn): - # could use: ckcc.get_sim_root_dirs() here - return '../unix/work/settings/' + fn + return sim_root_dir + '/settings/' + fn return doit @@ -864,7 +876,7 @@ def settings_slots(settings_path): if fn.endswith(".aes")] return doit -@pytest.fixture(scope="function") +@pytest.fixture def set_master_key(sim_exec, sim_execfile, simulator, reset_seed_words): # load simulator w/ a specific bip32 master key @@ -888,7 +900,7 @@ def set_master_key(sim_exec, sim_execfile, simulator, reset_seed_words): # - actually need seed words for all tests reset_seed_words() -@pytest.fixture(scope="function") +@pytest.fixture def set_xfp(sim_exec): # set the XFP, without really knowing the private keys # - won't be able to sign, but should accept PSBT for signing @@ -906,7 +918,7 @@ def set_xfp(sim_exec): sim_exec('from main import settings; settings.set("xfp", 0x%x);' % simulator_fixed_xfp) -@pytest.fixture(scope="function") +@pytest.fixture def set_encoded_secret(sim_exec, sim_execfile, simulator, reset_seed_words): # load simulator w/ a specific secret @@ -932,21 +944,21 @@ def set_encoded_secret(sim_exec, sim_execfile, simulator, reset_seed_words): # - actually need seed words for all tests reset_seed_words() -@pytest.fixture(scope="function") +@pytest.fixture def use_mainnet(settings_set): def doit(): settings_set('chain', 'BTC') yield doit settings_set('chain', 'XTN') -@pytest.fixture(scope="function") +@pytest.fixture def use_testnet(settings_set): def doit(do_testnet=True): settings_set('chain', 'XTN' if do_testnet else 'BTC') yield doit settings_set('chain', 'XTN') -@pytest.fixture(scope="function") +@pytest.fixture def use_regtest(request, settings_set): if request.config.getoption("--manual"): def xrt_warn(): @@ -960,7 +972,7 @@ def use_regtest(request, settings_set): settings_set('chain', 'XTN') -@pytest.fixture(scope="function") +@pytest.fixture def set_seed_words(change_seed_words, reset_seed_words): def doit(w): return change_seed_words(w) @@ -971,7 +983,7 @@ def set_seed_words(change_seed_words, reset_seed_words): reset_seed_words() -@pytest.fixture(scope="function") +@pytest.fixture def change_seed_words(sim_exec, sim_execfile, simulator): # load simulator w/ a specific bip32 master key @@ -989,7 +1001,7 @@ def change_seed_words(sim_exec, sim_execfile, simulator): return doit -@pytest.fixture() +@pytest.fixture def reset_seed_words(change_seed_words): # load simulator w/ a specific bip39 seed phrase @@ -1004,7 +1016,7 @@ def reset_seed_words(change_seed_words): return doit -@pytest.fixture() +@pytest.fixture def settings_set(sim_exec): def doit(key, val, prelogin=False): @@ -1014,7 +1026,7 @@ def settings_set(sim_exec): return doit -@pytest.fixture() +@pytest.fixture def settings_get(sim_exec): def doit(key, def_val=None, prelogin=False): @@ -1026,7 +1038,7 @@ def settings_get(sim_exec): return doit -@pytest.fixture() +@pytest.fixture def master_settings_get(sim_exec): def doit(key): @@ -1037,7 +1049,7 @@ def master_settings_get(sim_exec): return doit -@pytest.fixture() +@pytest.fixture def settings_remove(sim_exec): def doit(key): @@ -1046,19 +1058,14 @@ def settings_remove(sim_exec): return doit -@pytest.fixture(scope='module') -def repl(request): - return request.getfixturevalue('mk4_repl') - - -@pytest.fixture(scope='module') -def mk4_repl(sim_eval, sim_exec): +@pytest.fixture(scope='session') +def repl(dev, request): # Provide an interactive connection to the REPL, using the debug build USB commands class Mk4USBRepl: def eval(self, cmd, max_time=3): # send a command, wait for it to finish - resp = sim_eval(cmd) + resp = _sim_eval(dev, cmd) print(f"eval: {cmd} => {resp}") if 'Traceback' in resp: raise RuntimeError(resp) @@ -1066,7 +1073,7 @@ def mk4_repl(sim_eval, sim_exec): def exec(self, cmd, proc_time=1, raw=False): # send a (one line) command and read the one-line response - resp = sim_exec(cmd) + resp = _sim_exec(dev, cmd) print(f"exec: {cmd} => {resp}") if raw: return resp return eval(resp) if resp else None @@ -1167,7 +1174,7 @@ def old_mk_repl(dev=None): return USBRepl() -@pytest.fixture() +@pytest.fixture def decode_with_bitcoind(bitcoind): def doit(raw_txn): @@ -1181,7 +1188,7 @@ def decode_with_bitcoind(bitcoind): return doit -@pytest.fixture() +@pytest.fixture def decode_psbt_with_bitcoind(bitcoind): def doit(raw_psbt): @@ -1196,7 +1203,7 @@ def decode_psbt_with_bitcoind(bitcoind): return doit -@pytest.fixture() +@pytest.fixture def check_against_bitcoind(bitcoind, use_regtest, sim_exec, sim_execfile): def doit(hex_txn, fee, num_warn=0, change_outs=None, dests=[]): @@ -1577,7 +1584,7 @@ def has_qwerty(is_q1): return is_q1 @pytest.fixture(scope='module') -def rf_interface(needs_nfc, sim_exec): +def rf_interface(needs_nfc, dev): # provide a read/write connection over NFC # - requires pyscard module and desktop NFC-V reader which doesn't exist raise pytest.xfail('broken NFC-V challenges') @@ -1629,15 +1636,15 @@ def rf_interface(needs_nfc, sim_exec): pass # get the CC into NFC tap mode (but no UX) - sim_exec('glob.NFC.set_rf_disable(0)') + _sim_exec(dev, 'glob.NFC.set_rf_disable(0)') time.sleep(3) yield RFHandler() - sim_exec('glob.NFC.set_rf_disable(1)') + _sim_exec(dev, 'glob.NFC.set_rf_disable(1)') -@pytest.fixture() +@pytest.fixture def nfc_read(request, needs_nfc): # READ data from NFC chip # - perfer to do over NFC reader, but can work over USB too @@ -1655,7 +1662,7 @@ def nfc_read(request, needs_nfc): except: return doit_usb -@pytest.fixture() +@pytest.fixture def nfc_read_url(nfc_read, press_cancel): # gives URL from ndef @@ -1673,7 +1680,7 @@ def nfc_read_url(nfc_read, press_cancel): return doit -@pytest.fixture() +@pytest.fixture def nfc_write(request, needs_nfc, is_q1): # WRITE data into NFC "chip" def doit_usb(ccfile): @@ -1690,26 +1697,26 @@ def nfc_write(request, needs_nfc, is_q1): except: return doit_usb -@pytest.fixture() +@pytest.fixture def enable_nfc(needs_nfc, sim_exec, settings_set): def doit(): settings_set('nfc', 1) sim_exec('import nfc; nfc.NFCHandler.startup()') return doit -@pytest.fixture() +@pytest.fixture def nfc_disabled(settings_get): def doit(): return not bool(settings_get('nfc', 0)) return doit -@pytest.fixture() +@pytest.fixture def vdisk_disabled(settings_get): def doit(): return not bool(settings_get('vidsk', 0)) return doit -@pytest.fixture() +@pytest.fixture def scan_a_qr(sim_exec, is_q1): # simulate a QR being scanned # XXX limitation: our USB protocol can't send a v40 QR, limit is more like 30 or so @@ -1742,14 +1749,14 @@ def ccfile_wrap(recs): return rv -@pytest.fixture() +@pytest.fixture def nfc_write_text(nfc_write): def doit(text): msg = b''.join(ndef.message_encoder([ndef.TextRecord(text), ])) return nfc_write(ccfile_wrap(msg)) return doit -@pytest.fixture() +@pytest.fixture def nfc_read_json(nfc_read): def doit(): import json @@ -1761,7 +1768,7 @@ def nfc_read_json(nfc_read): return doit -@pytest.fixture() +@pytest.fixture def nfc_read_text(nfc_read): def doit(): got = list(ndef.message_decoder(nfc_read())) @@ -1771,7 +1778,7 @@ def nfc_read_text(nfc_read): return got.text return doit -@pytest.fixture() +@pytest.fixture def nfc_read_txn(nfc_read, press_select): def doit(txid=None, contents=None): if contents is None: @@ -1807,7 +1814,7 @@ def nfc_read_txn(nfc_read, press_select): return doit -@pytest.fixture() +@pytest.fixture def nfc_block4rf(sim_eval): # wait until RF is enabled and something to read (doesn't read it tho) def doit(timeout=15): diff --git a/testing/constants.py b/testing/constants.py index e509a614..713b05c8 100644 --- a/testing/constants.py +++ b/testing/constants.py @@ -1,8 +1,6 @@ # (c) Copyright 2020 by Coinkite Inc. This file is covered by license found in COPYING-CC. # -SIM_PATH = '/tmp/ckcc-simulator.sock' - # Simulator normally powers up with this 'wallet' simulator_fixed_tprv = "tprv8ZgxMBicQKsPeXJHL3vPPgTAEqQ5P2FD9qDeCQT4Cp1EMY5QkwMPWFxHdxHrxZhhcVRJ2m7BNWTz9Xre68y7mX5vCdMJ5qXMUfnrZ2si2X4" simulator_fixed_tpub = "tpubD6NzVbkrYhZ4XzL5Dhayo67Gorv1YMS7j8pRUvVMd5odC2LBPLAygka9p7748JtSq82FNGPppFEz5xxZUdasBRCqJqXvUHq6xpnsMcYJzeh" diff --git a/testing/core_fixtures.py b/testing/core_fixtures.py index c7a8689f..e8381a67 100644 --- a/testing/core_fixtures.py +++ b/testing/core_fixtures.py @@ -17,6 +17,11 @@ def _sim_exec(device, cmd, binary=False, timeout=60000): # print(f'sim_exec: {cmd!r} -> {s!r}') return s.decode('utf-8') if not isinstance(s, str) else s +def _sim_eval(device, cmd, binary=False, timeout=None): + s = device.send_recv(b'EVAL' + cmd.encode('utf-8'), timeout=timeout) + if binary: return s + return s.decode('utf-8') + def _cap_story(device): cmd = "RV.write('\0'.join(sim_display.story or []))" rv = _sim_exec(device, cmd) diff --git a/testing/debug/.gitignore b/testing/debug/.gitignore deleted file mode 100644 index c81791f8..00000000 --- a/testing/debug/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -*.psbt -*.txn -*.txt -*.json -*.png diff --git a/testing/devtest/check_decode.py b/testing/devtest/check_decode.py index 91b3ece8..6d14ba90 100644 --- a/testing/devtest/check_decode.py +++ b/testing/devtest/check_decode.py @@ -34,7 +34,7 @@ for fn in ['had_witness', 'num_inputs', 'num_outputs', if 'destinations' in expect: for (val, addr), (idx, txo) in zip(expect['destinations'], p.output_iter()): assert val == txo.nValue - txt = active_request.render_output(txo) + txt, _ = active_request.render_output(txo) # normalize from display format address = txt.split("\n")[-2] assert address[0] == "\x02" diff --git a/testing/login_settings_tests.py b/testing/login_settings_tests.py index 708edd8f..341b93f1 100644 --- a/testing/login_settings_tests.py +++ b/testing/login_settings_tests.py @@ -11,7 +11,7 @@ import pytest, time, pdb from core_fixtures import _pick_menu_item, _cap_menu, _cap_story, _cap_screen from core_fixtures import _need_keypress, _enter_complex, _press_select -from ckcc_protocol.client import ColdcardDevice, CKCC_SIMULATOR_PATH +from ckcc_protocol.client import ColdcardDevice from run_sim_tests import ColdcardSimulator, clean_sim_data @@ -138,7 +138,7 @@ def test_set_nickname(nick, request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1"] if is_Q else []) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, is_Q, "Settings") _pick_menu_item(device, is_Q, "Login Settings") @@ -150,7 +150,7 @@ def test_set_nickname(nick, request): sim = ColdcardSimulator(args= ["--q1" if is_Q else "", "--early-usb"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) scr = _cap_screen(device) target = "".join(scr.strip().split("\n")) @@ -167,7 +167,7 @@ def test_randomize_pin_keys(request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1"] if is_Q else []) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, is_Q, "Settings") _pick_menu_item(device, is_Q, "Login Settings") @@ -177,7 +177,7 @@ def test_randomize_pin_keys(request): sim.stop() # power off sim = ColdcardSimulator(args=["--q1" if is_Q else "", "--pin", "22-22", "--early-usb"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _login(device, is_Q, "22-22", scrambled=True) time.sleep(3) m = _cap_menu(device) @@ -190,7 +190,7 @@ def test_login_countdown(lcdwn, request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1"] if is_Q else []) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, is_Q, "Settings") _pick_menu_item(device, is_Q, "Login Settings") @@ -200,7 +200,7 @@ def test_login_countdown(lcdwn, request): sim.stop() # power off sim = ColdcardSimulator(args=["--q1" if is_Q else "", "--pin", "22-22", "--early-usb"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) secs = int(lcdwn.strip().split()[0]) _login(device, is_Q, "22-22") time.sleep(.15) @@ -223,7 +223,7 @@ def test_kill_key(kbtn, when, request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1"] if is_Q else []) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, is_Q, "Settings") _pick_menu_item(device, is_Q, "Login Settings") @@ -233,7 +233,7 @@ def test_kill_key(kbtn, when, request): sim.stop() # power off sim = ColdcardSimulator(args= ["--q1" if is_Q else "", "--pin", "22-22", "--early-usb"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) if is_Q: possible_kbtn = [chr(65 + i) for i in range(26)] + [i for i in '\',./'] @@ -278,7 +278,7 @@ def test_terms_ok(request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--early-usb", "-w", "--q1" if is_Q else ""]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) time.sleep(.1) _, story = _cap_story(device) @@ -317,7 +317,7 @@ def test_terms_ok(request): sim.stop() # power off sim = ColdcardSimulator(args=["-l", "--q1" if is_Q else "", "--early-usb", "--pin", "22-22"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _login(device, is_Q, "22-22") time.sleep(3) m = _cap_menu(device) @@ -331,7 +331,7 @@ def test_wrong_pin_input(request, brick): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--early-usb", "--q1" if is_Q else "", "--pin", "22-22"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) time.sleep(.1) num_attmeptss = 13 for ii, i in enumerate(range(31, 43), start=1): @@ -394,7 +394,7 @@ def test_login_integration(request, nick, randomize, login_ctdwn, kill_btn, kill clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1"] if is_Q else []) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, is_Q, "Settings") _pick_menu_item(device, is_Q, "Login Settings") @@ -416,7 +416,7 @@ def test_login_integration(request, nick, randomize, login_ctdwn, kill_btn, kill sim.stop() # power off sim = ColdcardSimulator(args=["--q1" if is_Q else "", "--pin", "22-22", "--early-usb"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) if nick: scr = _cap_screen(device) @@ -484,7 +484,7 @@ def test_calc_login(request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, is_Q, "Settings") _pick_menu_item(device, is_Q, "Login Settings") @@ -494,7 +494,7 @@ def test_calc_login(request): sim.stop() # power off sim = ColdcardSimulator(args=["--q1", "--pin", "22-22", "--early-usb"]) sim.start(start_wait=6) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) scr = _cap_screen(device) assert 'ECC Calculator' in scr diff --git a/testing/run_sim_tests.py b/testing/run_sim_tests.py index 25bf88b6..2bcf28b3 100644 --- a/testing/run_sim_tests.py +++ b/testing/run_sim_tests.py @@ -30,16 +30,82 @@ python run_sim_tests.py --collect veryslow # just print all python run_sim_tests.py --collect manual # just print all manual tests to stdout Make sure to run manual test if you want to state that your changes passed all the tests. + +Testing on multiple simulators in parallel + +python run_sim_tests.py --q1 --multiproc # to run all Q tests in parallel (default num-proc=14 simulators) +python run_sim_tests.py --multiproc --num-proc 6 # to run all Mk4 tests in parallel max 6 simulators at once +python run_sim_tests.py -m test_addr.py -m test_bbqr.py --multiproc # just desired test +python run_sim_tests.py --q1 -m test_sign.py --multiproc # just desired test +python run_sim_tests --multiproc --turbo # turbo causes both Mk4 & Q tests to run simultaneously (turbo doubles num-procs) +python run_sim_tests --multiproc --turbo # all Mk4 & Q tests run in 60 minutes total!! +python run_sim_tests --multiproc --turbo -m test_addr.py -m test_ux.py # will spawn 4 simulators: one Q and one Mk4 for address tests & one Q and one Mk4 for ux tests + +Console output has some useful info: +* when job is started it will print its PID +* when job is done you'll get elapsed time from start (test duration) +* when all is done - complete test session duration + +``` +$ python run_sim_tests.py -m test_addr.py -m test_drv_entro.py -m test_usb.py --multiproc --turbo +started: Mk4 test_addr.py 38824 +started: Q test_addr.py 38935 +started: Mk4 test_drv_entro.py 39042 +started: Q test_drv_entro.py 39150 +started: Mk4 test_usb.py 39257 +started: Q test_usb.py 39364 +done: Mk4 test_usb.py 0:00:06.043072 +done: Q test_usb.py 0:00:06.081147 +done: Mk4 test_addr.py 0:00:51.141250 +done: Q test_addr.py 0:01:03.185571 +done: Mk4 test_drv_entro.py 0:03:24.234521 +done: Q test_drv_entro.py 0:03:30.278795 + + +elapsed: 0:03:50.308146 +``` + +After jobs are finished, or even during execution you can inspect `/tmp/cc-simulators` directory: +* contains simulator work directories named as of specific simulator +* log directories where pytest output is piped + * mk4_logs + * q1_logs + +``` +$ pwd +/tmp/cc-simulators +$ ls +38824 38935 39042 39150 39257 39364 mk4_logs q1_logs +$ ls 39042/* +39042/debug: +last-qr.png + +39042/MicroSD: +drv-hex-idx0-2.txt drv-pw-idx0.txt drv-words-idx0-2.txt drv-words-idx0.txt +drv-hex-idx0.txt drv-wif-idx0.txt drv-words-idx0-3.txt drv-xprv-idx0.txt + +39042/settings: + +39042/VirtDisk: +README.md +$ ls mk4_logs/ +test_addr.py.log test_drv_entro.py.log test_usb.py.log +``` + +To parse only failures use below cmd in {mk4,q1}_logs directory: +``` +for f in $(ls); do x=`grep -n "short test summary info" $f | grep -Eo '^[^:]+'`; if [ -n "$x" ];then tail -n +"$x" $f | grep -E '^FAILED|^ERROR';fi ;done +``` """ -import os, time, glob, json, pytest, atexit, signal, argparse, subprocess, contextlib +import os, time, glob, json, pytest, atexit, signal, argparse, subprocess, contextlib, shutil +from datetime import timedelta from typing import List - from pytest import ExitCode SIM_INIT_WAIT = 2 # 2 seconds, can be tweaked via cmdline arguments ( -w 6 ) - +DEFAULT_PYTEST_MARKS = "not onetime and not veryslow and not manual" @contextlib.contextmanager def pushd(new_dir): @@ -50,13 +116,17 @@ def pushd(new_dir): finally: os.chdir(previous_dir) +def clean_directory(pth): + for root, dirs, files in os.walk(pth): + for f in files: + os.unlink(os.path.join(root, f)) + for d in dirs: + shutil.rmtree(os.path.join(root, d)) -def remove_client_sockets(): +def remove_all_client_sockets(): with pushd("/tmp"): for fn in glob.glob("ckcc-client*.sock"): os.remove(fn) - print("Removed all client sockets") - def remove_cautious(fpath: str) -> None: if os.path.basename(fpath) in ["README.md", ".gitignore"]: @@ -99,7 +169,7 @@ def is_ok(ec: ExitCode) -> bool: def _run_pytest_tests(test_module: str, pytest_marks: str, pytest_k: str, pdb: bool, - failed_first: bool, psbt2=False, is_Q=False, headless=False) -> ExitCode: + failed_first: bool, psbt2=False, is_Q=False, headless=False, sim_socket=None) -> ExitCode: cmd_list = [ "--cache-clear", "-m", pytest_marks, "--sim", test_module if test_module is not None else "" @@ -116,42 +186,50 @@ def _run_pytest_tests(test_module: str, pytest_marks: str, pytest_k: str, pdb: b cmd_list.insert(0, "--Q") # only changes behavior in login_settings_test if headless: cmd_list.append("--headless") + if sim_socket: + cmd_list.append("--sim-socket") + cmd_list.append(sim_socket) return pytest.main(cmd_list) -def _run_coldcard_tests(test_module: str, simulator_args: List[str], pytest_marks: str, +def _run_coldcard_tests(test_module: str, simulator_args: List[str], pytest_k: str, pdb: bool, failed_first: bool, psbt2=False, - is_Q=False, headless=False) -> ExitCode: + is_Q=False, headless=False, pytest_marks: str = DEFAULT_PYTEST_MARKS, + sim_segregate=False) -> ExitCode: + sock_path = None if simulator_args is not None: - sim = ColdcardSimulator(args=simulator_args, headless=headless) + sim = ColdcardSimulator(args=simulator_args, headless=headless, segregate=sim_segregate) sim.start() time.sleep(1) + sock_path = sim.socket exit_code = _run_pytest_tests(test_module, pytest_marks, pytest_k, pdb, - failed_first, psbt2, is_Q, headless) + failed_first, psbt2, is_Q, headless, sock_path) if simulator_args is not None: sim.stop() time.sleep(1) clean_sim_data() + remove_all_client_sockets() + return exit_code def run_coldcard_tests(test_module=None, simulator_args=None, pytest_k=None, pdb=False, failed_first=False, psbt2=False, is_Q=False, headless=False, - pytest_marks="not onetime and not veryslow and not manual"): + pytest_marks=DEFAULT_PYTEST_MARKS): failed = [] - exit_code = _run_coldcard_tests(test_module, simulator_args, pytest_marks, pytest_k, - pdb, failed_first, psbt2, is_Q, headless) + exit_code = _run_coldcard_tests(test_module, simulator_args, pytest_k, + pdb, failed_first, psbt2, is_Q, headless, pytest_marks) if not is_ok(exit_code): # no success, no nothing - give failed another try, each alone with its own simulator last_failed = get_last_failed() print("Running failed from last run", last_failed) exit_codes = [] for failed_test in last_failed: - exit_code_2 = _run_coldcard_tests(failed_test, simulator_args, pytest_marks, + exit_code_2 = _run_coldcard_tests(failed_test, simulator_args, pytest_k, pdb, failed_first, psbt2, is_Q, - headless) + headless, pytest_marks) exit_codes.append(exit_code_2) if not is_ok(exit_code_2): failed.append(failed_test) @@ -173,11 +251,12 @@ class PytestCollectMarked: class ColdcardSimulator: - def __init__(self, path=None, args=None, headless=False): + def __init__(self,args=None, headless=False, segregate=False): self.proc = None self.args = args - self.path = "/tmp/ckcc-simulator.sock" if path is None else path self.headless = headless + self.segregate = segregate + self.socket = "/tmp/ckcc-simulator.sock" def start(self, start_wait=None): # here we are in testing directory @@ -188,14 +267,20 @@ class ColdcardSimulator: cmd_list.extend(self.args) if self.headless: cmd_list.append("--headless") + if self.segregate: + cmd_list.append("--segregate") self.proc = subprocess.Popen( cmd_list, # this needs to be in firmware/unix - expected to be run from firmware/testing cwd="../unix", - preexec_fn=os.setsid + preexec_fn=os.setsid, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, ) time.sleep(start_wait or SIM_INIT_WAIT) + if self.segregate: + self.socket = "/tmp/ckcc-simulator-%d.sock" % self.proc.pid atexit.register(self.stop) def stop(self): @@ -205,7 +290,6 @@ class ColdcardSimulator: os.waitpid(os.getpgid(self.proc.pid), 0) atexit.unregister(self.stop) - remove_client_sockets() def main(): @@ -233,6 +317,12 @@ def main(): help="only run tests which match the given substring expression") parser.add_argument("--headless", action="store_true", default=False, help="run simulator instance in headless mode") + parser.add_argument("--multiproc", action="store_true", default=False, + help="Run tests & simulators in parallel") + parser.add_argument("--num-proc", type=int, default=14, + help="How many executors/simulators to run in parallel in --multiproc mode") + parser.add_argument("--turbo", action="store_true", default=False, + help="Both Mk4 and Q at the same time") args = parser.parse_args() if args.sim_init_wait: @@ -258,47 +348,159 @@ def main(): if args.module is None: test_modules = [] elif len(args.module) == 1 and args.module[0].lower() == "all": - test_modules = sorted(glob.glob("test_*.py")) + test_modules = glob.glob("test_*.py") assert test_modules, "please run in ../testing subdir" else: for fn in args.module: if not os.path.exists(fn): raise RuntimeError(f"{fn} does not exist") - test_modules = sorted(args.module) + test_modules = args.module - result = [] - for test_module in test_modules: - test_args = DEFAULT_SIMULATOR_ARGS - if test_module in ["test_rng.py", "test_pincodes.py", "test_rolls.py"]: - # test_pincodes.py can only be run against real device - # test_rng.py not needed when using simulator - # test_rolls.py should be run alone as it does not need simulator - print("Skipped", test_module) - continue + # test_pincodes.py can only be run against real device + # test_rng.py not needed when using simulator + # test_rolls.py should be run alone as it does not need simulator + # set diff + test_modules = set(test_modules) - {"test_rng.py", "test_pincodes.py", "test_rolls.py"} - print("Started", test_module) + module_args = [] + for test_module in sorted(list(test_modules)): + sim_args = DEFAULT_SIMULATOR_ARGS if test_module in ["test_bsms.py", "test_address_explorer.py", "test_export.py", "test_multisig.py", "test_ux.py"]: - test_args = DEFAULT_SIMULATOR_ARGS + ["--set", "vidsk=1"] + sim_args = DEFAULT_SIMULATOR_ARGS + ["--set", "vidsk=1"] if test_module == "test_vdisk.py": - test_args = ["--eject"] + DEFAULT_SIMULATOR_ARGS + ["--set", "vidsk=1"] + sim_args = ["--eject"] + DEFAULT_SIMULATOR_ARGS + ["--set", "vidsk=1"] if test_module == "test_bip39pw.py": - test_args = [] + sim_args = [] if test_module in ["test_unit.py", "test_se2.py", "test_backup.py", "test_teleport.py"]: # test_nvram_mk4 needs to run without --eff # se2 duress wallet activated as ephemeral seed requires proper `settings.load` - test_args = ["--set", "nfc=1"] + sim_args = ["--set", "nfc=1"] if test_module in ["test_ephemeral.py", "test_notes.py", "test_ccc.py"]: # proper `settings.load` _ virtual disk - test_args = ["--set", "nfc=1", "--set", "vidsk=1"] + sim_args = ["--set", "nfc=1", "--set", "vidsk=1"] - if args.q1 and '--q1' not in test_args: - test_args.append('--q1') + if args.q1 and '--q1' not in sim_args: + sim_args.append('--q1') - ec, failed_tests = run_coldcard_tests(test_module, simulator_args=test_args, - pytest_k=args.pytest_k, pdb=args.pdb, - failed_first=args.ff, psbt2=args.psbt2, - headless=args.headless) + module_args.append((test_module, sim_args, args.pytest_k, args.pdb, + args.ff, args.psbt2, args.q1, args.headless)) + + if args.multiproc: + start_time = time.time() + def add_to_queue(module_name, simulator_args, queue): + if module_name == "test_multisig.py": + # split takes too much time + queue.append((0, [module_name, simulator_args, "not tutorial and not airgapped and not ms_address and not descriptor_export", ""])) + queue.append((0, [module_name, simulator_args, "airgapped", "-sep1"])) + queue.append((0, [module_name, simulator_args, "tutorial", "-sep2"])) + queue.append((0, [module_name, simulator_args, "ms_address", "-sep3"])) + queue.append((0, [module_name, simulator_args, "descriptor_export", "-sep4"])) + + elif module_name == "test_seed_xor.py": + # split takes too much time + queue.append((0, [module_name, simulator_args, "test_import_xor", "-sep1"])) + queue.append((0, [module_name, simulator_args, "not test_import_xor", ""])) + + elif module_name in ["test_export.py", "test_ephemeral.py", "test_sign.py", "test_msg.py", + "test_backup.py"]: + # higher priority + queue.append((1, [module_name, simulator_args, None, ""])) + + else: + # standard priority + queue.append((2, [module_name, simulator_args, None, ""])) + + # will clear everything there from previous runs + tmp_dir = "/tmp/cc-simulators" + clean_directory(tmp_dir) # clean it + mk4_log_dir = f"{tmp_dir}/mk4_logs" + q1_log_dir = f"{tmp_dir}/q1_logs" + os.makedirs(mk4_log_dir, exist_ok=True) + os.makedirs(q1_log_dir, exist_ok=True) + + q = [] # build priority queue + for mod_name, sim_args, *_ in module_args: + if args.turbo: + if "--q1" in sim_args: + add_to_queue(mod_name, sim_args, q) + add_to_queue(mod_name, [i for i in sim_args if i == "--q1"], q) + else: + add_to_queue(mod_name, sim_args, q) + add_to_queue(mod_name, sim_args + ["--q1"], q) + + else: + add_to_queue(mod_name, sim_args, q) + + # sort queue by priority, highest priority elements at the end + q = [i[1] for i in sorted(q, reverse=True)] + + num_proc = args.num_proc + if args.turbo: + # double num-proc + num_proc *= 2 + + procs = [] + while True: + # create as many processes as allowed by --num-proc (default=14) + if q and (len(procs) < num_proc): + # start simulators first + q_chunks = [] + for _ in range (num_proc - len(procs)): + try: + mn, sim_args, k, mod_add = q.pop() # remove element + except IndexError: + # priority queue is empty + break + sim = ColdcardSimulator(sim_args, segregate=True) + sim.start(start_wait=0) + ld = q1_log_dir if "--q1" in sim_args else mk4_log_dir + q_chunks.append((sim, mn, mod_add, k, ld)) + + time.sleep(5) + for sim, mn, mod_add, k, log_dir in q_chunks: + assert sim.socket + out_log_path = f"{log_dir}/%s.log" % (mn + mod_add) + out_fd = open(out_log_path, "w") + cmd_list = ["pytest", "--cache-clear", "-m", DEFAULT_PYTEST_MARKS, "--sim", + mn, "--sim-socket", sim.socket] + if k: + cmd_list.extend(["-k", k]) + p = subprocess.Popen(cmd_list, preexec_fn=os.setsid, stdout=out_fd, stderr=out_fd) + mark = "Q" if "q1" in log_dir else "Mk4" + procs.append((mn+mod_add, p, out_fd, sim, mark, time.time())) + print(f'started: {mark:<6}{mn+mod_add:<30}{sim.socket.split("-")[-1].split(".")[0]:<10}') + + if not procs and not q: + # done + break + + i = 0 + while i < len(procs): + mn, p, out_fd, sim, mark, st = procs[i] + if p.poll() is None: + # still running + i += 1 + continue + else: + # done + p.communicate() + out_fd.close() + sim.stop() + del procs[i] + print(f"done: {mark:<6}{mn:<30}{str(timedelta(seconds=time.time()-st)):<15}") + + time.sleep(3) + + # multiprocess done + print(f"\n\nelapsed: {str(timedelta(seconds=time.time()-start_time))}") + return + + result = [] + for arguments in module_args: + test_module = arguments[0] + print("Started", test_module) + ec, failed_tests = run_coldcard_tests(*arguments) result.append((test_module, ec, failed_tests)) print("Done", test_module) print(80 * "=") @@ -363,5 +565,8 @@ def main(): if __name__ == "__main__": main() - + # sim = ColdcardSimulator(args=["--eff", "--segregate"]) + # sim.start() + # import pdb;pdb.set_trace() + # x = 5 # EOF diff --git a/testing/seedless_tests.py b/testing/seedless_tests.py index 05555e5d..4920bb84 100644 --- a/testing/seedless_tests.py +++ b/testing/seedless_tests.py @@ -3,9 +3,9 @@ import pytest, pdb, time, random, os from charcodes import KEY_CANCEL from core_fixtures import _pick_menu_item, _press_select -from core_fixtures import _need_keypress, _cap_screen, _sim_exec +from core_fixtures import _need_keypress, _sim_exec from run_sim_tests import ColdcardSimulator, clean_sim_data -from ckcc_protocol.client import ColdcardDevice, CKCC_SIMULATOR_PATH +from ckcc_protocol.client import ColdcardDevice def test_status_bar_rewrite_after_restore_master(request): @@ -13,7 +13,7 @@ def test_status_bar_rewrite_after_restore_master(request): clean_sim_data() # remove all from previous sim = ColdcardSimulator(args=["--q1", "-l"]) sim.start(start_wait=3) - device = ColdcardDevice(sn=CKCC_SIMULATOR_PATH) + device = ColdcardDevice(is_simulator=True) _pick_menu_item(device, True, "Advanced/Tools") _pick_menu_item(device, True, "Temporary Seed") diff --git a/testing/test_backup.py b/testing/test_backup.py index f4a3172c..a81af598 100644 --- a/testing/test_backup.py +++ b/testing/test_backup.py @@ -570,13 +570,14 @@ def test_seed_vault_backup_frozen(reset_seed_words, settings_set, repl, build_te assert target in bk -def test_clone_start(reset_seed_words, pick_menu_item, cap_story, goto_home): - sd_dir = "../unix/work/MicroSD" +def test_clone_start(reset_seed_words, pick_menu_item, cap_story, goto_home, src_root_dir, + sim_root_dir): + sd_dir = f"{sim_root_dir}/MicroSD" num_7z = len([i for i in os.listdir(sd_dir) if i.endswith(".7z")]) fname = "ccbk-start.json" reset_seed_words() goto_home() - shutil.copy(f"data/{fname}", sd_dir) + shutil.copy(f"{src_root_dir}/testing/data/{fname}", sd_dir) pick_menu_item("Advanced/Tools") pick_menu_item("Backup") pick_menu_item("Clone Coldcard") @@ -593,18 +594,23 @@ def test_clone_start(reset_seed_words, pick_menu_item, cap_story, goto_home): def test_bkpw_override(reset_seed_words, override_bkpw, goto_home, pick_menu_item, cap_story, press_select, garbage_collector, microsd_path, - restore_backup_cs): + restore_backup_cs, is_q1): reset_seed_words() # clean slate old_pw = None test_cases = [ - "arm prob slot merc hub fiel wing aver tale undo diar boos army cabl mous teac drif risk frow achi poet ecol boss grit", - " ".join(12 * ["elevator"]), - " ".join(12 * ["fever"]), 32 * "a", - (16 * "0") + " " + (16 *"1"), - 64 * "Q", (26 * "?") + "!@#$%^&*()", ] + if is_q1: + # not needed on mk4 - even tho works (takes too much time) + # Mk4 display is not suitable for these type of passwords anyways + test_cases += [ + "arm prob slot merc hub fiel wing aver tale undo diar boos army cabl mous teac drif risk frow achi poet ecol boss grit", + " ".join(12 * ["elevator"]), + " ".join(12 * ["fever"]), + 64 * "Q", + ] + fnames = [] for pw in test_cases: override_bkpw(pw, old_pw) diff --git a/testing/test_bbqr.py b/testing/test_bbqr.py index c84494dc..0b4ebe29 100644 --- a/testing/test_bbqr.py +++ b/testing/test_bbqr.py @@ -219,14 +219,14 @@ def test_show_bbqr_sizes(size, cap_screen_qr, sim_exec, render_bbqr): assert ft == 'U' @pytest.mark.parametrize('src', [ 'rng', 'gpu', 'bigger'] ) -def test_show_bbqr_contents(src, cap_screen_qr, sim_exec, render_bbqr, load_shared_mod): +def test_show_bbqr_contents(src, cap_screen_qr, sim_exec, render_bbqr, load_shared_mod, src_root_dir): args = dict(msg=f'Test {src}', file_type='B') if src == 'rng': args['data'] = expect = prandom(500) # limited by simulated USB path elif src in { 'gpu', 'bigger' }: args['setup'] = 'from gpu_binary import BINARY' - cc_gpu_bin = load_shared_mod('cc_gpu_bin', '../shared/gpu_binary.py') + cc_gpu_bin = load_shared_mod('cc_gpu_bin', f'{src_root_dir}/shared/gpu_binary.py') if src == 'gpu': args['str_expr'] = 'BINARY' expect = cc_gpu_bin.BINARY @@ -254,7 +254,7 @@ def test_bbqr_psbt(size, encoding, max_ver, partial, segwit, scan_a_qr, readback cap_screen_qr, render_bbqr, goto_home, use_regtest, cap_story, decode_psbt_with_bitcoind, decode_with_bitcoind, fake_txn, dev, start_sign, end_sign, press_cancel, press_select, need_keypress, - base64str, try_sign_bbqr, signing_artifacts_reexport): + base64str, try_sign_bbqr, signing_artifacts_reexport, sim_root_dir): num_in = size num_out = size*10 @@ -274,7 +274,8 @@ def test_bbqr_psbt(size, encoding, max_ver, partial, segwit, scan_a_qr, readback if base64str: psbt = base64.b64encode(psbt).decode() - open('debug/last.psbt', 'w' if base64str else 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'w' if base64str else 'wb') as f: + f.write(psbt) _, file_type, rb = try_sign_bbqr(psbt, type_code="U" if base64str else "P", max_version=max_ver, encoding=encoding) diff --git a/testing/test_bip39pw.py b/testing/test_bip39pw.py index 84f44c88..3d59a17c 100644 --- a/testing/test_bip39pw.py +++ b/testing/test_bip39pw.py @@ -50,7 +50,7 @@ def test_b9p_basic(pw, set_bip39_pw): set_bip39_pw(pw) -@pytest.fixture() +@pytest.fixture def set_bip39_pw(dev, need_keypress, reset_seed_words, cap_story, sim_execfile, press_select): diff --git a/testing/test_ccc.py b/testing/test_ccc.py index 406663e4..e6151d39 100644 --- a/testing/test_ccc.py +++ b/testing/test_ccc.py @@ -284,13 +284,13 @@ def setup_ccc(goto_home, pick_menu_item, cap_story, press_select, pass_word_quiz press_select() pick_menu_item(vel_mi) - pick_menu_item(vel) # actually a full menu item if vel == "Unlimited": target = 0 else: target = int(vel.split()[0]) - time.sleep(.2) + pick_menu_item(vel) # actually a full menu item + time.sleep(.3) assert settings_get("ccc")["pol"]["vel"] == target if whitelist: @@ -304,10 +304,14 @@ def setup_ccc(goto_home, pick_menu_item, cap_story, press_select, pass_word_quiz pick_menu_item("Scan QR") for i, addr in enumerate(whitelist, start=1): scan_a_qr(addr) - time.sleep(.5) - scr = cap_screen() - assert f"Got {i} so far" in scr - assert "ENTER to apply" in scr + + for _ in range(10): + scr = cap_screen() + if (f"Got {i} so far" in scr) and ("ENTER to apply" in scr): + break + time.sleep(.2) + else: + assert False, "updating whitelist failed" press_select() else: @@ -385,7 +389,7 @@ def enter_enabled_ccc(goto_home, pick_menu_item, cap_story, press_select, is_q1, @pytest.fixture def ccc_ms_setup(microsd_path, virtdisk_path, scan_a_qr, is_q1, cap_menu, pick_menu_item, cap_story, press_select, need_keypress, enter_number, press_cancel, - garbage_collector): + garbage_collector, cap_screen): def doit(N=3, b_words=12, way="sd", addr_fmt=AF_P2WSH, ftype="cc", bbqr=True): @@ -449,56 +453,72 @@ def ccc_ms_setup(microsd_path, virtdisk_path, scan_a_qr, is_q1, cap_menu, pick_m time.sleep(.1) title, story = cap_story() - if is_q1: - assert title == "QR or SD Card?" - if way in ("sd", "vdisk"): + + if way in ("sd", "vdisk"): + if is_q1: + assert "ENTER to use SD card" in story + press_select() + + if addr_fmt == AF_P2WSH: press_select() else: - need_keypress(KEY_QR) - time.sleep(.1) - title, story = cap_story() - assert title == "Address Format" - assert "Press ENTER for default address format (P2WSH" in story - assert "press (1) for P2SH-P2WSH" in story - if addr_fmt == AF_P2WSH: - press_select() - else: - need_keypress("1") + need_keypress("1") + else: + assert way == "qr" + if not is_q1: + raise pytest.skip("mk4 no qr") - for d, dd in res: - if ftype == "cc": - conts = json.dumps(dd) - tc = "J" - else: - deriv = dd[f"{label}_deriv"].replace("m/", "") - conts = f"[{dd['xfp']}/{deriv}]{dd[label]}" - tc = "U" - - if bbqr: - _, parts = split_qrs(conts, tc, max_version=20) - for p in parts: - scan_a_qr(p) - time.sleep(.1) - else: - scan_a_qr(conts) - time.sleep(.1) - - time.sleep(.5) - - press_cancel() # after we're done scanning keys, exit QR animation to proceed - - # casual on-device multisig create - if way != "qr": + assert title == "QR or SD Card?" + need_keypress(KEY_QR) + time.sleep(.1) + title, story = cap_story() + assert title == "Address Format" + assert "Press ENTER for default address format (P2WSH" in story + assert "press (1) for P2SH-P2WSH" in story if addr_fmt == AF_P2WSH: press_select() else: need_keypress("1") + for i, (d, dd) in enumerate(res, start=1): + if ftype == "cc": + conts = json.dumps(dd) + tc = "J" + else: + deriv = dd[f"{label}_deriv"].replace("m/", "") + conts = f"[{dd['xfp']}/{deriv}]{dd[label]}" + tc = "U" + + if bbqr: + _, parts = split_qrs(conts, tc, max_version=20) + for p in parts: + scan_a_qr(p) + time.sleep(.25) + else: + scan_a_qr(conts) + + for _ in range(10): + time.sleep(.2) + scr = cap_screen() + if ("Number of keys scanned: %d" % i) in scr: + break + else: + assert False, f"failed to scan ms xpubs ({i})" + + press_cancel() # after we're done scanning keys, exit QR animation to proceed + + time.sleep(.1) # CCC C key account number enter_number("0") - time.sleep(.1) - title, story = cap_story() - assert "Create new multisig wallet" in story + for _ in range(5): + time.sleep(.1) + title, story = cap_story() + if "Create new multisig wallet" in story: + break + else: + press_cancel() + assert False, "failed to create ms wallet" + assert f"Policy: 2 of {N}" in story if is_q1: assert "Coldcard Co-sign" in story diff --git a/testing/test_hsm.py b/testing/test_hsm.py index f2dabc58..0f2d3592 100644 --- a/testing/test_hsm.py +++ b/testing/test_hsm.py @@ -122,7 +122,7 @@ def enable_hsm_commands(dev, sim_exec, only_mk4): sim_exec(cmd) -@pytest.fixture(scope='function') +@pytest.fixture def hsm_reset(dev, sim_exec): # filename for the policy file, as stored on simulated CC @@ -476,10 +476,11 @@ def wait_til_signed(dev): return result @pytest.fixture -def attempt_psbt(hsm_status, start_sign, dev): +def attempt_psbt(hsm_status, start_sign, dev, sim_root_dir): def doit(psbt, refuse=None, remote_error=None): - open('debug/attempt.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/attempt.psbt', 'wb') as f: + f.write(psbt) start_sign(psbt) try: @@ -780,7 +781,7 @@ def test_multiple_signings(dev, quick_start_hsm, is_simulator, def test_multiple_signings_multisig(cc_first, M_N, dev, quick_start_hsm, is_simulator, attempt_psbt, fake_txn, load_hsm_users, auth_user, bitcoind, - request): + request, sim_root_dir): # signs 400 different PSBTs in loop beaing one leg of multisig # CC must be on regtest if testing with real thing af = "bech32" @@ -850,7 +851,9 @@ def test_multiple_signings_multisig(cc_first, M_N, dev, quick_start_hsm, # uploading only external to CC file_len, sha = dev.upload_file(desc_ext.encode('ascii')) - open('debug/last-config.txt', 'wt').write(desc_ext) + with open(f'{sim_root_dir}/debug/last-config.txt', 'wt') as f: + f.write(desc_ext) + dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha), timeout=30000) time.sleep(.2) diff --git a/testing/test_multisig.py b/testing/test_multisig.py index 075541a0..3b4c5e13 100644 --- a/testing/test_multisig.py +++ b/testing/test_multisig.py @@ -45,7 +45,7 @@ def str2ipath(s): yield here -@pytest.fixture(scope='function') +@pytest.fixture def has_ms_checks(request, sim_exec): # Add this fixture to any test that should FAIL if ms checks are disabled # - in other words, tests that test the checks which are disabled. @@ -64,7 +64,7 @@ def has_ms_checks(request, sim_exec): return danger_mode -@pytest.fixture() +@pytest.fixture def bitcoind_p2sh(bitcoind): # Use bitcoind to generate a p2sh addres based on public keys. @@ -142,12 +142,13 @@ def make_multisig(dev, sim_execfile): return doit @pytest.fixture -def offer_ms_import(cap_story, dev): +def offer_ms_import(cap_story, dev, sim_root_dir): def doit(config, allow_non_ascii=False): # upload the file, trigger import file_len, sha = dev.upload_file(config.encode('utf-8' if allow_non_ascii else 'ascii')) - open('debug/last-config.txt', 'wt').write(config) + with open(f'{sim_root_dir}/debug/last-config.txt', 'wt') as f: + f.write(config) dev.send_recv(CCProtocolPacker.multisig_enroll(file_len, sha)) @@ -249,12 +250,12 @@ def import_multisig(request, is_q1, need_keypress, offer_ms_import): @pytest.fixture def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select, is_q1, request, need_keypress, import_multisig, - settings_set): + settings_set, sim_root_dir): def doit(M, N, addr_fmt=None, name=None, unique=0, accept=False, common=None, keys=None, do_import=True, derivs=None, descriptor=False, int_ext_desc=False, dev_key=False, way=None, bip67=True, - force_unsort_ms=True, netcode="XTN"): + force_unsort_ms=True, netcode="XTN", return_desc=False): # param: bip67 if false, only usable together with descriptor=True if not bip67: assert descriptor, "needs descriptor=True" @@ -308,7 +309,8 @@ def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select, xfp2str(xfp), dd.hwif(as_private=False)) #print(config) - open('debug/last-ms.txt', 'wt').write(config) + with open(f'{sim_root_dir}/debug/last-ms.txt', 'wt') as f: + f.write(config) title, story = import_multisig(data=config, way=way) @@ -331,6 +333,9 @@ def import_ms_wallet(dev, make_multisig, offer_ms_import, press_select, xor ^= xfp assert dev.send_recv(CCProtocolPacker.multisig_check(M, N, xor)) == 1 + if return_desc and descriptor: + return config + return keys return doit @@ -560,7 +565,7 @@ def test_import_ranges(m_of_n, use_regtest, addr_fmt, clear_ms, import_ms_wallet @pytest.mark.ms_danger def test_violate_bip67(clear_ms, use_regtest, import_ms_wallet, test_ms_show_addr, has_ms_checks, - fake_ms_txn, try_sign): + fake_ms_txn, try_sign, sim_root_dir): # detect when pubkeys are not in order in the redeem script clear_ms() M, N = 1, 15 @@ -578,7 +583,7 @@ def test_violate_bip67(clear_ms, use_regtest, import_ms_wallet, change_outputs=[1], violate_script_key_order=True) - with open('debug/last.psbt', 'wb') as f: + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: f.write(psbt) with pytest.raises(Exception) as e: @@ -588,7 +593,8 @@ def test_violate_bip67(clear_ms, use_regtest, import_ms_wallet, @pytest.mark.parametrize("has_change", [True, False]) def test_violate_import_order_multi(has_change, clear_ms, import_ms_wallet, - fake_ms_txn, try_sign, test_ms_show_addr): + fake_ms_txn, try_sign, test_ms_show_addr, + sim_root_dir): clear_ms() M, N = 3, 5 keys = import_ms_wallet(M, N, accept=True, descriptor=True, bip67=False) @@ -601,7 +607,7 @@ def test_violate_import_order_multi(has_change, clear_ms, import_ms_wallet, change_outputs=[1] if has_change else [], bip67=False, violate_script_key_order=True) - with open('debug/last.psbt', 'wb') as f: + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: f.write(psbt) with pytest.raises(Exception) as e: @@ -1167,8 +1173,6 @@ def test_import_dup_xfp_fails(m_of_n, use_regtest, addr_fmt, clear_ms, @pytest.mark.parametrize('desc', ["multi", "sortedmulti"]) def test_ms_cli(dev, addr_fmt, clear_ms, import_ms_wallet, addr_vs_path, desc): # exercise the p2sh command of ckcc:cli ... hard to do manually. - from subprocess import check_output - M, N = 2, 3 clear_ms() bip67, descriptor = (False, True) if desc == "multi" else (True, False) @@ -1180,41 +1184,17 @@ def test_ms_cli(dev, addr_fmt, clear_ms, import_ms_wallet, addr_vs_path, desc): scr, pubkeys, xfp_paths = make_redeem(M, keys, pmapper, bip67=bip67) - def decode_path(p): - return '/'.join(str(i) if i < 0x80000000 else "%d'"%(i& 0x7fffffff) for i in p) + addr = dev.send_recv(CCProtocolPacker.show_p2sh_address( + scr[0] - 80, xfp_paths, scr, addr_fmt=addr_fmt), timeout=None + ) - if 1: - args = ['ckcc'] - if dev.is_simulator: - args += ['-x'] + addr_vs_path(addr, addr_fmt=addr_fmt, script=scr) - args += ['p2sh', '-q'] - - if addr_fmt == AF_P2WSH: - args += ['-s'] - elif addr_fmt == AF_P2WSH_P2SH: - args += ['-s', '-w'] - - args += [B2A(scr)] - args += [xfp2str(x)+'/'+decode_path(path) for x,*path in xfp_paths] - - import shlex - print('CMD: ' + (' '.join(shlex.quote(i) for i in args))) - - addr = check_output(args, encoding='ascii').strip() - - print(addr) - addr_vs_path(addr, addr_fmt=addr_fmt, script=scr) - - # test case for make_ms_address really. - expect_addr, _, scr2, _ = make_ms_address(M, keys, path_mapper=pmapper, - addr_fmt=addr_fmt, bip67=bip67) - assert expect_addr == addr - assert scr2 == scr - - # need to re-start our connection once ckcc has talked to simulator - dev.start_encryption() - dev.check_mitm() + # test case for make_ms_address really. + expect_addr, _, scr2, _ = make_ms_address(M, keys, path_mapper=pmapper, + addr_fmt=addr_fmt, bip67=bip67) + assert expect_addr == addr + assert scr2 == scr clear_ms() @@ -1446,7 +1426,7 @@ def fake_ms_txn(pytestconfig): @pytest.mark.parametrize('desc', ["multi", "sortedmulti"]) def test_ms_sign_simple(M_N, num_ins, dev, addr_fmt, clear_ms, incl_xpubs, import_ms_wallet, addr_vs_path, fake_ms_txn, try_sign, try_sign_microsd, transport, - has_change, settings_set, desc): + has_change, settings_set, desc, sim_root_dir): M, N = M_N num_outs = num_ins-1 descriptor, bip67 = (True, False) if desc == "multi" else (False, True) @@ -1470,7 +1450,8 @@ def test_ms_sign_simple(M_N, num_ins, dev, addr_fmt, clear_ms, incl_xpubs, impor outstyles=ADDR_STYLES_MS, change_outputs=[1] if has_change else [], bip67=bip67) - open('debug/last.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: + f.write(psbt) if transport == 'sd': try_sign_microsd(psbt, encoding=('binary', 'hex', 'base64')[random.randint(0,2)]) @@ -1484,7 +1465,7 @@ def test_ms_sign_simple(M_N, num_ins, dev, addr_fmt, clear_ms, incl_xpubs, impor @pytest.mark.parametrize('segwit', [True, False]) @pytest.mark.parametrize('incl_xpubs', [ True, False ]) def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms, - fake_ms_txn, try_sign, incl_xpubs, bitcoind): + fake_ms_txn, try_sign, incl_xpubs, bitcoind, sim_root_dir): # IMPORTANT: wont work if you start simulator with --ms flag. Use no args @@ -1502,11 +1483,13 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev psbt = fake_ms_txn(num_ins, num_outs, M, keys, segwit_in=segwit, incl_xpubs=incl_xpubs, outstyles=all_out_styles, change_outputs=list(range(1,num_outs))) - open(f'debug/myself-before.psbt', 'w').write(b64encode(psbt).decode()) + with open(f'{sim_root_dir}/debug/myself-before.psbt', 'w') as f: + f.write(b64encode(psbt).decode()) for idx in range(M): select_wallet(idx) _, updated = try_sign(psbt, accept_ms_import=incl_xpubs) - open(f'debug/myself-after.psbt', 'w').write(b64encode(updated).decode()) + with open(f'{sim_root_dir}/debug/myself-after.psbt', 'w') as f: + f.write(b64encode(updated).decode()) assert updated != psbt aft = BasicPSBT().parse(updated) @@ -1522,9 +1505,12 @@ def test_ms_sign_myself(M, use_regtest, make_myself_wallet, segwit, num_ins, dev assert all(inp['next'] in {'finalizer','updater'} for inp in anal['inputs']), "other issue: %r" % anal except: # XXX seems to be a bug in analyzepsbt function ... not fully studied - pprint(anal, stream=open('debug/analyzed.txt', 'wt')) + with open(f'{sim_root_dir}/debug/analyzed.txt', 'wt') as f: + pprint(anal, stream=f) + decode = bitcoind.rpc.decodepsbt(b64encode(psbt).decode('ascii')) - pprint(decode, stream=open('debug/decoded.txt', 'wt')) + with open(f'{sim_root_dir}/debug/decoded.txt', 'wt') as f: + pprint(decode, stream=f) if M==N or segwit: # as observed, bug not trigged, so raise if it *does* happen @@ -1749,7 +1735,7 @@ def test_make_airgapped(addr_fmt, acct_num, M_N, goto_home, cap_story, pick_menu @pytest.mark.parametrize('addr_style', ["legacy", "p2sh-segwit", "bech32"]) @pytest.mark.parametrize('cc_sign_first', [True, False]) def test_bitcoind_cosigning(cc_sign_first, dev, bitcoind, import_ms_wallet, clear_ms, try_sign, - press_cancel, addr_style, use_regtest, is_q1): + press_cancel, addr_style, use_regtest, is_q1, sim_root_dir): # Make a P2SH wallet with local bitcoind as a co-signer (and simulator) # - send an receive various # - following text of @@ -1851,7 +1837,8 @@ def test_bitcoind_cosigning(cc_sign_first, dev, bitcoind, import_ms_wallet, clea # assert resp['changepos'] == -1 psbt = b64decode(resp['psbt']) - open('debug/funded.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/funded.psbt', 'wb') as f: + f.write(psbt) # patch up the PSBT a little ... bitcoind doesn't know the path for the CC's key ex = BasicPSBT().parse(psbt) @@ -1863,11 +1850,13 @@ def test_bitcoind_cosigning(cc_sign_first, dev, bitcoind, import_ms_wallet, clea psbt = ex.as_bytes() - open('debug/patched.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/patched.psbt', 'wb') as f: + f.write(psbt) _, updated = try_sign(psbt, finalize=False) - open('debug/cc-updated.psbt', 'wb').write(updated) + with open(f'{sim_root_dir}/debug/cc-updated.psbt', 'wb') as f: + f.write(updated) if cc_sign_first: # cc signed first - bitcoind is now second @@ -1879,7 +1868,9 @@ def test_bitcoind_cosigning(cc_sign_first, dev, bitcoind, import_ms_wallet, clea # finalize and send rr = bitcoind.supply_wallet.finalizepsbt(both_signed, True) - open('debug/bc-final-txn.txn', 'wt').write(rr['hex']) + with open(f'{sim_root_dir}/debug/bc-final-txn.txn', 'wt') as f: + f.write(rr['hex']) + assert rr['complete'] tx_hex = rr["hex"] res = bitcoind.supply_wallet.testmempoolaccept([tx_hex]) @@ -1894,8 +1885,9 @@ def test_bitcoind_cosigning(cc_sign_first, dev, bitcoind, import_ms_wallet, clea @pytest.mark.parametrize('out_style', ['p2wsh']) @pytest.mark.parametrize('bitrot', list(range(0,6)) + [98, 99, 100] + list(range(-5, 0))) @pytest.mark.ms_danger -def test_ms_sign_bitrot(num_ins, dev, addr_fmt, clear_ms, incl_xpubs, import_ms_wallet, addr_vs_path, - fake_ms_txn, start_sign, end_sign, out_style, cap_story, bitrot, has_ms_checks): +def test_ms_sign_bitrot(num_ins, dev, addr_fmt, clear_ms, incl_xpubs, import_ms_wallet, + addr_vs_path, fake_ms_txn, start_sign, end_sign, out_style, cap_story, + bitrot, has_ms_checks, sim_root_dir): M = 1 N = 3 num_outs = 2 @@ -1926,7 +1918,8 @@ def test_ms_sign_bitrot(num_ins, dev, addr_fmt, clear_ms, incl_xpubs, import_ms_ assert len(track) == 1 - open('debug/last.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: + f.write(psbt) start_sign(psbt) with pytest.raises(Exception) as ee: @@ -1947,7 +1940,8 @@ def test_ms_sign_bitrot(num_ins, dev, addr_fmt, clear_ms, incl_xpubs, import_ms_ @pytest.mark.parametrize('pk_num', range(4)) @pytest.mark.parametrize('case', ['pubkey', 'path']) def test_ms_change_fraud(case, pk_num, num_ins, dev, addr_fmt, clear_ms, incl_xpubs, make_multisig, - addr_vs_path, fake_ms_txn, start_sign, end_sign, out_style, cap_story): + addr_vs_path, fake_ms_txn, start_sign, end_sign, out_style, cap_story, + sim_root_dir): M = 1 N = 3 @@ -1982,7 +1976,8 @@ def test_ms_change_fraud(case, pk_num, num_ins, dev, addr_fmt, clear_ms, incl_xp hack_change_out=lambda idx: dict(tweak_pubkeys= lambda data: tweak(case, pk_num, data))) - open('debug/last.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: + f.write(psbt) with pytest.raises(Exception) as ee: start_sign(psbt) @@ -1999,7 +1994,7 @@ def test_ms_change_fraud(case, pk_num, num_ins, dev, addr_fmt, clear_ms, incl_xp @pytest.mark.parametrize('repeat', range(2) ) -def test_iss6743(repeat, set_seed_words, sim_execfile, try_sign): +def test_iss6743(repeat, set_seed_words, sim_execfile, try_sign, sim_root_dir): # from SomberNight psbt_b4 = bytes.fromhex('70736274ff0100520200000001bde05be36069e2e0fe44793c68ad8244bb1a52cc37f152e0fa5b75e40169d7f70000000000fdffffff018b1e000000000000160014ed5180f05c7b1dc980732602c50cda40530e00ad4de11c004f01024289ef0000000000000000007dd565da7ee1cf05c516e89a608968fed4a2450633a00c7b922df66b27afd2e1033a0a4fa4b0a997738ac2f142a395c1f02afcb31d7ffd46a90a0c927a4c411fd704094ef7844f01024289ef0431fcbdcc8000000112d4aaea7292e7870c7eeb3565fa1c1fa8f957fa7c4c24b411d5b4f5710d359a023e63d1e54063525bea286ccb2a0ad7b14560aa31ec4be826afa883141dfe1d53145c9e228d300000800100008000000080010000804f01024289ef04e44b38f1800000014a1960f3a3c86ba355a16a66a548cfb62eeb25663311f7cd662a192896f3777e038cc595159a395e4ec35e477c9523a1512f873e74d303fb03fc9a1503b1ba45271434652fae30000080010000800000008001000080000100df02000000000101d1a321707660769c7f8604d04c9ae2db58cf1ec7a01f4f285cdcbb25ce14bdfd0300000000fdffffff02401f00000000000017a914bfd0b8471a3706c1e17870a4d39b0354bcea57b687c864000000000000160014e608b171d63ec24d9fa252d5c1e45624b14e44700247304402205133eb96df167b895f657cce31c6882840a403013682d9d4651aed2730a7dad502202aaacc045d85d9c711af0c84e7f355cc18bf2f8e6d91774d42ba24de8418a39e012103a58d8eb325abb412eaf927cf11d8b7641c4a468ce412057e47892ca2d13ed6144de11c000104220020a3c65c4e376d82fb3ca45596feee5b08313ad64f38590c1b08bc530d1c0bbfea010569522102ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da621034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef6381921039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f53ae220602ab84641359fa22461b8461515231da63c196614cd22b26e556ed878e30db4da60c094ef78400000000030000002206034211ab0f75c3a307a2f6bf6f09a9e05d3c8edd0ba7a2ac31f432d1045ef638191c5c9e228d3000008001000080000000800100008000000000030000002206039690cf74941da5db291fa8be7348abe3807786732d969eac5d27e0afa909a55f1c34652fae3000008001000080000000800100008000000000030000000000') # pre 3.2.0 result @@ -2033,7 +2028,8 @@ def test_iss6743(repeat, set_seed_words, sim_execfile, try_sign): assert out_psbt != psbt_wrong assert BasicPSBT().parse(out_psbt) == BasicPSBT().parse(psbt_right) - open('debug/i6.psbt', 'wt').write(out_psbt.hex()) + with open(f'{sim_root_dir}/debug/i6.psbt', 'wt') as f: + f.write(out_psbt.hex()) @pytest.mark.parametrize('N', [ 3, 15]) @pytest.mark.parametrize('xderiv', [ None, 'any', 'unknown', '*', '', 'none']) @@ -2128,7 +2124,8 @@ def test_ms_import_many_derivs(M, N, way, make_multisig, clear_ms, offer_ms_impo @pytest.mark.ms_danger @pytest.mark.parametrize('descriptor', [True, False]) -def test_danger_warning(request, descriptor, clear_ms, import_ms_wallet, cap_story, fake_ms_txn, start_sign, sim_exec): +def test_danger_warning(request, descriptor, clear_ms, import_ms_wallet, cap_story, fake_ms_txn, + start_sign, sim_exec, sim_root_dir): # note: cant use has_ms_checks fixture here danger_mode = (request.config.getoption('--ms-danger')) sim_exec(f'from multisig import MultisigWallet; MultisigWallet.disable_checks={danger_mode}') @@ -2138,7 +2135,8 @@ def test_danger_warning(request, descriptor, clear_ms, import_ms_wallet, cap_sto keys = import_ms_wallet(M, N, accept=1, descriptor=descriptor, addr_fmt="p2wsh") psbt = fake_ms_txn(1, 1, M, keys, incl_xpubs=True) - open('debug/last.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: + f.write(psbt) start_sign(psbt) title, story = cap_story() @@ -2306,13 +2304,17 @@ def test_dup_ms_wallet_bug(goto_home, pick_menu_item, press_select, import_ms_wa @pytest.mark.parametrize('int_ext_desc', [True, False]) @pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) @pytest.mark.parametrize('desc', ["multi", "sortedmulti"]) -def test_import_desciptor(M_N, addr_fmt, int_ext_desc, way, import_ms_wallet, goto_home, pick_menu_item, - press_select, clear_ms, cap_story, microsd_path, virtdisk_path, - nfc_read_text, load_export, is_q1, desc): +def test_import_descriptor(M_N, addr_fmt, int_ext_desc, way, import_ms_wallet, goto_home, pick_menu_item, + press_select, clear_ms, cap_story, microsd_path, virtdisk_path, + nfc_read_text, load_export, is_q1, desc): clear_ms() M, N = M_N - import_ms_wallet(M, N, addr_fmt=addr_fmt, accept=1, descriptor=True, - int_ext_desc=int_ext_desc, bip67=False if desc == "multi" else True) + desc_import = import_ms_wallet( + M, N, addr_fmt=addr_fmt, accept=True, descriptor=True, + int_ext_desc=int_ext_desc, bip67=False if desc == "multi" else True, + return_desc=True + ) + desc_import = desc_import.strip() goto_home() pick_menu_item('Settings') @@ -2322,8 +2324,7 @@ def test_import_desciptor(M_N, addr_fmt, int_ext_desc, way, import_ms_wallet, go pick_menu_item('Export') contents = load_export(way, label="Descriptor multisig setup", is_json=False) desc_export = contents.strip() - with open("debug/last-ms.txt", "r") as f: - desc_import = f.read().strip() + normalized = parse_desc_str(desc_export) # as new format is not widely supported we only allow to import it - no export yet if int_ext_desc: @@ -2342,7 +2343,7 @@ def test_import_desciptor(M_N, addr_fmt, int_ext_desc, way, import_ms_wallet, go @pytest.mark.parametrize("start_idx", [2147483540, MAX_BIP32_IDX, 0]) @pytest.mark.parametrize('M_N', [(2, 2), (3, 5), (15, 15)]) @pytest.mark.parametrize('addr_fmt', [AF_P2WSH, AF_P2SH, AF_P2WSH_P2SH]) -@pytest.mark.parametrize('way', ["sd", "vdisk", "nfc"]) +@pytest.mark.parametrize('way', ["sd", "nfc"]) # vdisk def test_bitcoind_ms_address(change, M_N, addr_fmt, clear_ms, goto_home, need_keypress, pick_menu_item, cap_menu, cap_story, make_multisig, import_ms_wallet, microsd_path, bitcoind_d_wallet_w_sk, use_regtest, load_export, way, @@ -3042,14 +3043,13 @@ def test_ms_wallet_ordering(clear_ms, import_ms_wallet, try_sign_microsd, fake_m psbt = fake_ms_txn(5, 5, 3, keys3, outstyles=all_out_styles, segwit_in=True, incl_xpubs=True) - open('debug/last.psbt', 'wb').write(psbt) - try_sign_microsd(psbt, encoding='base64') @pytest.mark.parametrize("descriptor", [True, False]) @pytest.mark.parametrize("m_n", [(2, 3), (3, 5), (5, 10)]) -def test_ms_xpub_ordering(descriptor, m_n, clear_ms, make_multisig, import_ms_wallet, try_sign_microsd, fake_ms_txn): +def test_ms_xpub_ordering(descriptor, m_n, clear_ms, make_multisig, import_ms_wallet, + try_sign_microsd, fake_ms_txn): clear_ms() M, N = m_n all_out_styles = list(unmap_addr_fmt.keys()) @@ -3063,13 +3063,11 @@ def test_ms_xpub_ordering(descriptor, m_n, clear_ms, make_multisig, import_ms_wa addr_fmt="p2wsh", descriptor=descriptor) psbt = fake_ms_txn(5, 5, M, opt, outstyles=all_out_styles, segwit_in=True, incl_xpubs=True) - open('debug/last.psbt', 'wb').write(psbt) try_sign_microsd(psbt, encoding='base64') for opt_1 in all_options: # create PSBT with original keys order psbt = fake_ms_txn(5, 5, M, opt_1, outstyles=all_out_styles, segwit_in=True, incl_xpubs=True) - open('debug/last.psbt', 'wb').write(psbt) try_sign_microsd(psbt, encoding='base64') diff --git a/testing/test_nfc.py b/testing/test_nfc.py index 029ccf08..5c3f49c6 100644 --- a/testing/test_nfc.py +++ b/testing/test_nfc.py @@ -15,7 +15,7 @@ from charcodes import KEY_NFC, KEY_QR @pytest.mark.parametrize('case', range(6)) -def test_ndef(case, load_shared_mod): +def test_ndef(case, load_shared_mod, src_root_dir): # NDEF unit tests -- runs in cpython def get_body(efile): @@ -36,7 +36,7 @@ def test_ndef(case, load_shared_mod): def decode(msg): return list(ndef.message_decoder(get_body(msg))) - cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py') + cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py') n = cc_ndef.ndefMaker() if case == 0: @@ -101,13 +101,13 @@ def test_ndef(case, load_shared_mod): 'short', 'long', ]) -def test_ndef_ccfile(ccfile, load_shared_mod): +def test_ndef_ccfile(ccfile, load_shared_mod, src_root_dir): # NDEF unit tests def decode(body): return list(ndef.message_decoder(body)) - cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py') + cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py') txt_msg = None if ccfile == 'rx': @@ -149,7 +149,8 @@ def test_ndef_ccfile(ccfile, load_shared_mod): @pytest.fixture def try_sign_nfc(cap_story, pick_menu_item, goto_home, need_keypress, sim_exec, nfc_read, nfc_write, nfc_block4rf, press_select, - press_cancel, press_nfc, nfc_read_txn, ndef_parse_txn_psbt): + press_cancel, press_nfc, nfc_read_txn, ndef_parse_txn_psbt, + sim_root_dir): # like "try_sign" but use NFC to send/receive PSBT/results @@ -182,7 +183,7 @@ def try_sign_nfc(cap_story, pick_menu_item, goto_home, need_keypress, ndef.TextRecord('some text'), ] - with open('debug/nfc-sent.psbt', 'wb') as f: + with open(f'{sim_root_dir}/debug/nfc-sent.psbt', 'wb') as f: f.write(ip) # wrap in a CCFile @@ -274,7 +275,7 @@ def try_sign_nfc(cap_story, pick_menu_item, goto_home, need_keypress, sim_exec('from pyb import SDCard; SDCard.ejected = False') @pytest.fixture -def ndef_parse_txn_psbt(press_cancel): +def ndef_parse_txn_psbt(press_cancel, sim_root_dir): def doit(contents, txid=None, orig=None, expect_finalized=True): # from NFC data read, what did we get? got_txid = None @@ -310,12 +311,14 @@ def ndef_parse_txn_psbt(press_cancel): assert got_txid == txid assert expect_finalized result = got_txn - open("debug/nfc-result.txn", 'wb').write(result) + with open(f"{sim_root_dir}/debug/nfc-result.txn", 'wb') as f: + f.write(result) else: assert not expect_finalized result = got_psbt - open("debug/nfc-result.psbt", 'wb').write(result) + with open(f"{sim_root_dir}/debug/nfc-result.psbt", 'wb') as f: + f.write(result) # read back final product if got_txn: @@ -434,9 +437,9 @@ def test_rf_uid(rf_interface, cap_story, goto_home, pick_menu_item): print(uid) -def test_ndef_roundtrip(load_shared_mod): +def test_ndef_roundtrip(load_shared_mod, src_root_dir): # specific failing case - cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py') + cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py') r = open('data/ms-import.ndef', 'rb').read() @@ -605,10 +608,11 @@ def test_share_by_pushtx(goto_home, cap_story, pick_menu_item, settings_set, ]) def test_nfc_share_files(fname, mode, ftype, nfc_read_json, nfc_read_text, need_keypress, goto_home, pick_menu_item, is_q1, - cap_menu, nfc_read, nfc_block4rf, press_select): + cap_menu, nfc_read, nfc_block4rf, press_select, + src_root_dir, sim_root_dir): goto_home() - fpath = "data/" + fname - shutil.copy2(fpath, '../unix/work/MicroSD') + fpath = f"{src_root_dir}/testing/data/" + fname + shutil.copy2(fpath, f'{sim_root_dir}/MicroSD') pick_menu_item("Advanced/Tools") pick_menu_item("File Management") pick_menu_item("NFC File Share") @@ -659,6 +663,6 @@ def test_nfc_share_files(fname, mode, ftype, nfc_read_json, nfc_read_text, res = json.loads(res) assert res == contents - os.remove('../unix/work/MicroSD/' + fname) + os.remove(f'{sim_root_dir}/MicroSD/' + fname) # EOF diff --git a/testing/test_ownership.py b/testing/test_ownership.py index 9f4203b1..0b9553e2 100644 --- a/testing/test_ownership.py +++ b/testing/test_ownership.py @@ -159,7 +159,8 @@ def test_ux(valid, testnet, method, sim_exec, wipe_cache, make_myself_wallet, use_testnet, goto_home, pick_menu_item, press_cancel, press_select, settings_set, is_q1, nfc_write, need_keypress, cap_screen, cap_story, load_shared_mod, scan_a_qr, skip_if_useless_way, - sign_msg_from_address, multisig, import_ms_wallet, clear_ms, verify_qr_address + sign_msg_from_address, multisig, import_ms_wallet, clear_ms, verify_qr_address, + src_root_dir, sim_root_dir ): skip_if_useless_way(method) addr_fmt = AF_CLASSIC @@ -201,7 +202,7 @@ def test_ux(valid, testnet, method, elif method == 'nfc': - cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py') + cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py') n = cc_ndef.ndefMaker() n.add_text(addr) ccfile = n.bytes() @@ -211,7 +212,8 @@ def test_ux(valid, testnet, method, pick_menu_item('Advanced/Tools') pick_menu_item('NFC Tools') pick_menu_item('Verify Address') - open('debug/nfc-addr.ndef', 'wb').write(ccfile) + with open(f'{sim_root_dir}/debug/nfc-addr.ndef', 'wb') as f: + f.write(ccfile) nfc_write(ccfile) #press_select() @@ -255,7 +257,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo pick_menu_item, need_keypress, sim_exec, clear_ms, import_ms_wallet, press_select, goto_home, nfc_write, load_shared_mod, load_export_and_verify_signature, - cap_story, is_q1): + cap_story, is_q1, src_root_dir, sim_root_dir): goto_home() wipe_cache() settings_set('accts', []) @@ -290,7 +292,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo if idx == 200: addr = addr - cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py') + cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py') n = cc_ndef.ndefMaker() n.add_text(addr) ccfile = n.bytes() @@ -300,7 +302,9 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo pick_menu_item('Advanced/Tools') pick_menu_item('NFC Tools') pick_menu_item('Verify Address') - open('debug/nfc-addr.ndef', 'wb').write(ccfile) + with open(f'{sim_root_dir}/debug/nfc-addr.ndef', 'wb') as f: + f.write(ccfile) + nfc_write(ccfile) time.sleep(1) @@ -317,7 +321,7 @@ def test_address_explorer_saver(af, wipe_cache, settings_set, goto_address_explo def test_regtest_addr_on_mainnet(goto_home, is_q1, pick_menu_item, scan_a_qr, nfc_write, cap_story, - need_keypress, load_shared_mod, use_mainnet): + need_keypress, load_shared_mod, use_mainnet, src_root_dir, sim_root_dir): # testing bug in chains.possible_address_fmt # allowed regtest addresses to be allowed on main chain goto_home() @@ -335,7 +339,7 @@ def test_regtest_addr_on_mainnet(goto_home, is_q1, pick_menu_item, scan_a_qr, nf need_keypress('1') else: - cc_ndef = load_shared_mod('cc_ndef', '../shared/ndef.py') + cc_ndef = load_shared_mod('cc_ndef', f'{src_root_dir}/shared/ndef.py') n = cc_ndef.ndefMaker() n.add_text(addr) ccfile = n.bytes() @@ -344,7 +348,8 @@ def test_regtest_addr_on_mainnet(goto_home, is_q1, pick_menu_item, scan_a_qr, nf pick_menu_item('Advanced/Tools') pick_menu_item('NFC Tools') pick_menu_item('Verify Address') - open('debug/nfc-addr.ndef', 'wb').write(ccfile) + with open(f'{sim_root_dir}/debug/nfc-addr.ndef', 'wb') as f: + f.write(ccfile) nfc_write(ccfile) # press_select() diff --git a/testing/test_paper.py b/testing/test_paper.py index 0dc2e0db..aa435c69 100644 --- a/testing/test_paper.py +++ b/testing/test_paper.py @@ -18,7 +18,7 @@ from ckcc_protocol.constants import * @pytest.mark.parametrize('netcode', ["XTN", "BTC"]) def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home, cap_story, need_keypress, microsd_path, verify_detached_signature_file, settings_set, - press_select): + press_select, src_root_dir): # test UX and operation of the 'bitcoin core' wallet export mx = "Don't make PDF" @@ -47,7 +47,7 @@ def test_generate(mode, pdf, netcode, dev, cap_menu, pick_menu_item, goto_home, if pdf: assert mx in cap_menu() - shutil.copy('../docs/paperwallet.pdf', microsd_path('paperwallet.pdf')) + shutil.copy(f'{src_root_dir}/docs/paperwallet.pdf', microsd_path('paperwallet.pdf')) pick_menu_item(mx) time.sleep(0.2) diff --git a/testing/test_pwsave.py b/testing/test_pwsave.py index 3f98d051..2e158af6 100644 --- a/testing/test_pwsave.py +++ b/testing/test_pwsave.py @@ -6,7 +6,12 @@ import pytest, time, os, shutil from binascii import a2b_hex from constants import simulator_fixed_tprv -SIM_FNAME = '../unix/work/MicroSD/.tmp.tmp' + +@pytest.fixture +def simulator_db_file(sim_root_dir): + def doit(): + return sim_root_dir + "/MicroSD/.tmp.tmp" + return doit @pytest.fixture def set_pw_phrase(pick_menu_item, word_menu_entry): @@ -31,8 +36,9 @@ def set_pw_phrase(pick_menu_item, word_menu_entry): 'ab'*25, ]) def test_first_time(pws, need_keypress, cap_story, pick_menu_item, enter_complex, - cap_menu, go_to_passphrase, reset_seed_words, press_select): - try: os.unlink(SIM_FNAME) + cap_menu, go_to_passphrase, reset_seed_words, press_select, + simulator_db_file): + try: os.unlink(simulator_db_file()) except: pass pws = pws.split() @@ -85,7 +91,7 @@ def test_first_time(pws, need_keypress, cap_story, pick_menu_item, enter_complex reset_seed_words() -def test_crypto_unittest(sim_eval, sim_exec, simulator): +def test_crypto_unittest(sim_exec, simulator, simulator_db_file): # unit test for AES key generation from SDCard and master secret card = sim_exec('import files; from h import b2a_hex; cs = files.CardSlot().__enter__(); RV.write(b2a_hex(cs.get_id_hash())); cs.__exit__()') @@ -121,7 +127,7 @@ p=PassphraseSaver(); p._calc_key(cs); RV.write(b2a_hex(p.key)); cs.__exit__()''' # check that key works for decrypt and that the file was actually encrypted - with open(SIM_FNAME, 'rb') as fd: + with open(simulator_db_file(), 'rb') as fd: raw = fd.read() import pyaes @@ -137,7 +143,7 @@ p=PassphraseSaver(); p._calc_key(cs); RV.write(b2a_hex(p.key)); cs.__exit__()''' assert j[0]['xfp'] def test_delete_one_by_one(go_to_passphrase, pick_menu_item, cap_menu, - cap_story, press_select): + cap_story, press_select, src_root_dir, sim_root_dir): # delete it one by one # when all deleted - we must be back in Passphrase # menu without Restore Saved option visible @@ -145,7 +151,7 @@ def test_delete_one_by_one(go_to_passphrase, pick_menu_item, cap_menu, time.sleep(.1) m = cap_menu() if 'Restore Saved' not in m: - shutil.copy2('data/pwsave.tmp', '../unix/work/MicroSD/.tmp.tmp') + shutil.copy2(f'{src_root_dir}/testing/data/pwsave.tmp', f'{sim_root_dir}/MicroSD/.tmp.tmp') go_to_passphrase() pick_menu_item('Restore Saved') m = cap_menu() diff --git a/testing/test_se2.py b/testing/test_se2.py index 092ea215..b0c88cae 100644 --- a/testing/test_se2.py +++ b/testing/test_se2.py @@ -65,7 +65,7 @@ def decode_slot(data): assert len(data) == 128 return SlotInfo(*struct.unpack(TRICK_FMT, data)) -@pytest.fixture(scope='function') +@pytest.fixture def se2_gate(sim_exec): # not-so-low-level method: include auth data for main PIN def doit(method_num, obj=None, buf=None): @@ -252,7 +252,7 @@ def test_ux_trick_menus(goto_trick_menu, pick_menu_item, cap_menu, # all clear now -@pytest.fixture(scope='function') +@pytest.fixture def new_trick_pin(goto_trick_menu, pick_menu_item, cap_menu, press_select, cap_story, enter_pin, se2_gate, is_simulator, is_q1): # using menus and UX, setup a new trick PIN diff --git a/testing/test_seed_xor.py b/testing/test_seed_xor.py index c6a9febd..ec823905 100644 --- a/testing/test_seed_xor.py +++ b/testing/test_seed_xor.py @@ -3,7 +3,7 @@ # test Seed XOR features # -import pytest, time, itertools +import pytest, time, itertools, random from mnemonic import Mnemonic from constants import simulator_fixed_words from xor import prepare_test_pairs, xor @@ -54,7 +54,7 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story, word_menu_entry, verify_ephemeral_secret_ui, confirm_tmp_seed, seed_vault_enable, press_select, scan_a_qr, is_q1, cap_screen_qr, cap_screen, OK): - def doit(parts, expect, incl_self=False, save_to_vault=False, + def doit(parts, expect, incl_self=False, save_to_vault=None, is_master_tmp_fail=False, way=None): if expect is None: parts, expect = prepare_test_pairs(*parts) @@ -66,6 +66,9 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story, elif incl_self is False: set_seed_words(proper[num_words]) + if save_to_vault is None: + save_to_vault = random.getrandbits(1) + seed_vault_enable(save_to_vault) time.sleep(.2) @@ -171,7 +174,6 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story, @pytest.mark.parametrize('way', ["qr", "seedqr", "classic"]) @pytest.mark.parametrize('incl_self', [False, True]) -@pytest.mark.parametrize('seed_vault', [False, True]) @pytest.mark.parametrize('parts, expect', [ # 24words - 3 parts (['romance wink lottery autumn shop bring dawn tongue range crater truth ability miss spice fitness easy legal release recall obey exchange recycle dragon room', @@ -191,10 +193,10 @@ def restore_seed_xor(set_seed_words, goto_home, pick_menu_item, cap_story, # random generated *random_test_cases() ]) -def test_import_xor(seed_vault, incl_self, parts, expect, restore_seed_xor, way, is_q1): +def test_import_xor(incl_self, parts, expect, restore_seed_xor, way, is_q1): if not is_q1 and "qr" in way: raise pytest.skip("Q only") - restore_seed_xor(parts, expect, incl_self, seed_vault, way=way) + restore_seed_xor(parts, expect, incl_self, way=way) @pytest.mark.parametrize('incl_self', [False, True]) diff --git a/testing/test_sign.py b/testing/test_sign.py index fa46430c..3f595af0 100644 --- a/testing/test_sign.py +++ b/testing/test_sign.py @@ -119,14 +119,14 @@ def xxx_test_sign_truncated(dev): 'data/worked-combined.psbt', 'data/worked-7.psbt', ]) -def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec): +def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec, src_root_dir, sim_root_dir): # unit test: parsing by the psbt proxy object - sim_exec('import main; main.FILENAME = %r; ' % ('../../testing/'+fn)) + sim_exec('import main; main.FILENAME = %r; ' % (f'{src_root_dir}/testing/'+fn)) rv = sim_execfile('devtest/unit_psbt.py') assert not rv, rv - rb = '../unix/work/readback.psbt' + rb = f'{sim_root_dir}/readback.psbt' oo = BasicPSBT().parse(open(fn, 'rb').read()) rb = BasicPSBT().parse(open(rb, 'rb').read()) @@ -134,7 +134,7 @@ def test_psbt_proxy_parsing(fn, sim_execfile, sim_exec): @pytest.mark.unfinalized def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign, - press_select, press_cancel): + press_select, press_cancel, sim_root_dir): # measure time to sign a larger txn if is_mark4: # Mk4: expect @@ -151,7 +151,9 @@ def test_speed_test(dev, fake_txn, is_mark3, is_mark4, start_sign, end_sign, psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=True) - open('debug/speed.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/speed.psbt', 'wb') as f: + f.write(psbt) + dt = time.time() start_sign(psbt, finalize=False) @@ -193,7 +195,7 @@ if 0: @pytest.mark.veryslow @pytest.mark.parametrize('segwit', [True, False]) def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, - start_sign, end_sign, dev, segwit, accept = True): + start_sign, end_sign, dev, segwit, sim_root_dir): # try a bunch of different bigger sized txns # - important to test on real device, due to it's limited memory @@ -212,7 +214,8 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit, outstyles=ADDR_STYLES) - open('debug/last.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: + f.write(psbt) start_sign(psbt, finalize=True) @@ -225,9 +228,10 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, except: cap_story = None - signed = end_sign(accept, finalize=True) + signed = end_sign(accept=True, finalize=True) - open('debug/signed.txn', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/signed.txn', 'wb') as f: + f.write(signed) decoded = decode_with_bitcoind(signed) @@ -263,12 +267,14 @@ def test_io_size(request, use_regtest, decode_with_bitcoind, fake_txn, @pytest.mark.bitcoind @pytest.mark.parametrize('num_ins', [ 2, 7, 15 ]) @pytest.mark.parametrize('segwit', [True, False]) -def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit, decode_with_bitcoind): +def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit, + decode_with_bitcoind, sim_root_dir): # create a TXN using actual addresses that are correct for DUT xp = dev.master_xpub psbt = fake_txn(num_ins, 1, xp, segwit_in=segwit) - open('debug/real-%d.psbt' % num_ins, 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/real-%d.psbt' % num_ins, 'wb') as f: + f.write(psbt) _, txn = try_sign(psbt, accept=True, finalize=True) @@ -288,7 +294,7 @@ def test_real_signing(fake_txn, use_regtest, try_sign, dev, num_ins, segwit, dec @pytest.mark.parametrize('num_dests', [ 1, 10, 25 ]) @pytest.mark.bitcoind def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, - start_sign, end_sign, we_finalize, num_dests): + start_sign, end_sign, we_finalize, num_dests, sim_root_dir): wallet_xfp = match_key use_regtest() @@ -344,7 +350,8 @@ def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, fee = resp['fee'] chg_pos = resp['changepos'] - open('debug/vs.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/vs.psbt', 'wb') as f: + f.write(psbt) # check some basics mine = BasicPSBT().parse(psbt) @@ -366,14 +373,17 @@ def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, assert mine.version == 2 signed = end_sign(accept=True, finalize=we_finalize) - open('debug/vs-signed.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/vs-signed.psbt', 'wb') as f: + f.write(signed) if not we_finalize: b4 = BasicPSBT().parse(psbt) aft = BasicPSBT().parse(signed) assert b4 != aft, "signing didn't change anything?" - open('debug/signed.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/signed.psbt', 'wb') as f: + f.write(signed) + resp = bitcoind.supply_wallet.finalizepsbt(str(b64encode(signed), 'ascii'), True) #combined_psbt = b64decode(resp['psbt']) @@ -385,7 +395,8 @@ def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, # assert resp['complete'] print("Final txn: %r" % network) - open('debug/finalized-by-btcd.txn', 'wb').write(network) + with open(f'{sim_root_dir}/debug/finalized-by-btcd.txn', 'wb') as f: + f.write(network) # try to send it txed = bitcoind.supply_wallet.sendrawtransaction(B2A(network)) @@ -394,7 +405,8 @@ def test_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, else: assert signed[0:4] != b'psbt', "expecting raw bitcoin txn" #print("Final txn: %s" % B2A(signed)) - open('debug/finalized-by-cc.txn', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/finalized-by-cc.txn', 'wb') as f: + f.write(signed) txed = bitcoind.supply_wallet.sendrawtransaction(B2A(signed)) print("Final txn hash: %r" % txed) @@ -426,7 +438,7 @@ def test_sign_example(set_master_key, sim_execfile, start_sign, end_sign): @pytest.mark.bitcoind @pytest.mark.unfinalized -def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind): +def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind, sim_root_dir): # Check we can finalize p2sh_p2wpkh inputs right. # TODO fix this @@ -442,7 +454,8 @@ def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind start_sign(psbt, finalize=True) signed = end_sign(accept=True) #signed = end_sign(None) - open('debug/p2sh-signed.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/p2sh-signed.psbt', 'wb') as f: + f.write(signed) #print('my finalization: ' + B2A(signed)) @@ -450,7 +463,9 @@ def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind signed_psbt = end_sign(accept=True) # use bitcoind to combine - open('debug/signed.psbt', 'wb').write(signed_psbt) + with open(f'{sim_root_dir}/debug/signed.psbt', 'wb') as f: + f.write(signed_psbt) + resp = bitcoind.rpc.finalizepsbt(str(b64encode(signed_psbt), 'ascii'), True) assert resp['complete'] == True, "bitcoind wasn't able to finalize it" @@ -463,7 +478,8 @@ def test_sign_p2sh_p2wpkh(match_key, use_regtest, start_sign, end_sign, bitcoind @pytest.mark.bitcoind @pytest.mark.unfinalized def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign, end_sign, - decode_psbt_with_bitcoind, offer_ms_import, press_select, clear_ms): + decode_psbt_with_bitcoind, offer_ms_import, press_select, clear_ms, + sim_root_dir): # Use the private key given in BIP 174 and do similar signing # as the examples. @@ -504,7 +520,8 @@ def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign start_sign(psbt) part_signed = end_sign(True) - open('debug/ex-signed-part.psbt', 'wb').write(part_signed) + with open(f'{sim_root_dir}/debug/ex-signed-part.psbt', 'wb') as f: + f.write(part_signed) b4 = BasicPSBT().parse(psbt) aft = BasicPSBT().parse(part_signed) @@ -514,7 +531,9 @@ def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign start_sign(part_signed, finalize=False) signed = end_sign(True, finalize=False) - open('debug/ex-signed.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/ex-signed.psbt', 'wb') as f: + f.write(signed) + aft2 = BasicPSBT().parse(signed) decode = decode_psbt_with_bitcoind(signed) @@ -542,7 +561,8 @@ def test_sign_p2sh_example(set_master_key, use_regtest, sim_execfile, start_sign @pytest.mark.bitcoind -def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind, cap_story): +def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind, cap_story, + sim_root_dir): # is change shown/hidden at right times. no fraud checks # NOTE: out#1 is change: @@ -562,7 +582,8 @@ def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind, check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[1,]) signed = end_sign(True) - open('debug/chg-signed.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/chg-signed.psbt', 'wb') as f: + f.write(signed) # modify it: remove bip32 path b4.outputs[1].bip32_paths = {} @@ -581,14 +602,17 @@ def test_change_case(start_sign, use_regtest, end_sign, check_against_bitcoind, check_against_bitcoind(B2A(b4.txn), Decimal('0.00000294'), change_outs=[]) signed2 = end_sign(True) - open('debug/chg-signed2.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/chg-signed2.psbt', 'wb') as f: + f.write(signed) + aft = BasicPSBT().parse(signed) aft2 = BasicPSBT().parse(signed2) assert aft.txn == aft2.txn @pytest.mark.parametrize('case', [ 1, 2]) @pytest.mark.bitcoind -def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_against_bitcoind, cap_story): +def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_against_bitcoind, + cap_story, sim_root_dir): # fraud: BIP-32 path of output doesn't lead to pubkey indicated # NOTE: out#1 is change: @@ -612,7 +636,8 @@ def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_agains b4.serialize(fd) mod_psbt = fd.getvalue() - open('debug/mod-%d.psbt' % case, 'wb').write(mod_psbt) + with open(f'{sim_root_dir}/debug/mod-%d.psbt' % case, 'wb') as f: + f.write(mod_psbt) if case == 1: start_sign(mod_psbt) @@ -631,7 +656,8 @@ def test_change_fraud_path(start_sign, use_regtest, end_sign, case, check_agains end_sign(True) @pytest.mark.bitcoind -def test_change_fraud_addr(start_sign, end_sign, use_regtest, check_against_bitcoind, cap_story): +def test_change_fraud_addr(start_sign, end_sign, use_regtest, check_against_bitcoind, cap_story, + sim_root_dir): # fraud: BIP-32 path of output doesn't match TXO address # NOTE: out#1 is change: #chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' @@ -653,7 +679,8 @@ def test_change_fraud_addr(start_sign, end_sign, use_regtest, check_against_bitc b4.serialize(fd) mod_psbt = fd.getvalue() - open('debug/mod-addr.psbt', 'wb').write(mod_psbt) + with open(f'{sim_root_dir}/debug/mod-addr.psbt', 'wb') as f: + f.write(mod_psbt) start_sign(mod_psbt) with pytest.raises(CCProtoError) as ee: @@ -664,13 +691,15 @@ def test_change_fraud_addr(start_sign, end_sign, use_regtest, check_against_bitc @pytest.mark.parametrize('case', ['p2sh-p2wpkh', 'p2wpkh', 'p2sh', 'p2sh-p2pkh']) @pytest.mark.bitcoind def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_regtest, - cap_story, case): + cap_story, case, sim_root_dir): # not fraud: output address encoded in various equiv forms use_regtest() # NOTE: out#1 is change: #chg_addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' - psbt = open('data/example-change.psbt', 'rb').read() + with open('data/example-change.psbt', 'rb') as f: + psbt = f.read() + b4 = BasicPSBT().parse(psbt) t = CTransaction() @@ -720,7 +749,8 @@ def test_change_p2sh_p2wpkh(start_sign, end_sign, check_against_bitcoind, use_re b4.serialize(fd) mod_psbt = fd.getvalue() - open('debug/mod-%s.psbt' % case, 'wb').write(mod_psbt) + with open(f'{sim_root_dir}/debug/mod-%s.psbt' % case, 'wb') as f: + f.write(mod_psbt) start_sign(mod_psbt) @@ -817,7 +847,8 @@ def test_sign_multisig_partial_fail(start_sign, end_sign): assert 'None of the keys involved' in str(ee) @pytest.mark.unfinalized -def test_sign_wutxo(start_sign, set_seed_words, end_sign, cap_story, sim_exec, sim_execfile): +def test_sign_wutxo(start_sign, set_seed_words, end_sign, cap_story, sim_exec, sim_execfile, + sim_root_dir): # Example from SomberNight: we can sign it, but signature won't be accepted by # network because the PSBT lies about the UTXO amount and tries to give away to miners, @@ -852,11 +883,13 @@ def test_sign_wutxo(start_sign, set_seed_words, end_sign, cap_story, sim_exec, s signed = end_sign(True, finalize=fin) - open('debug/sn-signed.'+ ('txn' if fin else 'psbt'), 'wt').write(B2A(signed)) + with open(f'{sim_root_dir}/debug/sn-signed.'+ ('txn' if fin else 'psbt'), 'wt') as f: + f.write(B2A(signed)) @pytest.mark.parametrize('fee_max', [ 10, 25, 50]) @pytest.mark.parametrize('under', [ False, True]) -def test_network_fee_amts(fee_max, under, fake_txn, try_sign, start_sign, dev, settings_set, sim_exec, cap_story): +def test_network_fee_amts(fee_max, under, fake_txn, try_sign, start_sign, dev, settings_set, + sim_exec, cap_story, sim_root_dir): settings_set('fee_limit', fee_max) @@ -866,7 +899,8 @@ def test_network_fee_amts(fee_max, under, fake_txn, try_sign, start_sign, dev, s psbt = fake_txn(1, 1, dev.master_xpub, fee=None, outvals=[outval]) - open('debug/fee.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/fee.psbt', 'wb') as f: + f.write(psbt) if not under: with pytest.raises(CCProtoError) as ee: @@ -885,7 +919,8 @@ def test_network_fee_amts(fee_max, under, fake_txn, try_sign, start_sign, dev, s settings_set('fee_limit', 10) -def test_network_fee_unlimited(fake_txn, start_sign, end_sign, dev, settings_set, cap_story): +def test_network_fee_unlimited(fake_txn, start_sign, end_sign, dev, settings_set, cap_story, + sim_root_dir): settings_set('fee_limit', -1) @@ -894,7 +929,8 @@ def test_network_fee_unlimited(fake_txn, start_sign, end_sign, dev, settings_set psbt = fake_txn(1, 1, dev.master_xpub, fee=None, outvals=[outval]) - open('debug/fee-un.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/fee-un.psbt', 'wb') as f: + f.write(psbt) # should be able to sign, but get warning start_sign(psbt, False) @@ -917,15 +953,17 @@ def test_network_fee_unlimited(fake_txn, start_sign, end_sign, dev, settings_set @pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE) @pytest.mark.parametrize('visualized', [0, STXN_VISUALIZE, STXN_VISUALIZE|STXN_SIGNED]) def test_change_outs(fake_txn, start_sign, end_sign, cap_story, dev, num_outs, master_xpub, - act_outs, segwit, out_style, visualized, add_xpub, num_ins=3): + act_outs, segwit, out_style, visualized, add_xpub, sim_root_dir): # create a TXN which has change outputs, which shouldn't be shown to user, and also not fail. xp = dev.master_xpub + num_ins = 3 couts = num_outs if act_outs == -1 else num_ins-act_outs psbt = fake_txn(num_ins, num_outs, xp, segwit_in=segwit, outstyles=[out_style], change_outputs=range(couts), add_xpub=add_xpub) - open('debug/change.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/change.psbt', 'wb') as f: + f.write(psbt) # should be able to sign, but get warning if not visualized: @@ -1001,7 +1039,8 @@ def KEEP_test_random_psbt(try_sign, sim_exec, fname="data/ .psbt"): @pytest.mark.bitcoind @pytest.mark.unfinalized @pytest.mark.parametrize('num_dests', [ 1, 10, 25 ]) -def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, start_sign, end_sign, num_dests): +def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind, bitcoind, + start_sign, end_sign, num_dests, sim_root_dir): # Compare how we finalize vs bitcoind ... should be exactly the same txn wallet_xfp = match_key # has to be after match key @@ -1030,7 +1069,8 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind fee = resp['fee'] chg_pos = resp['changepos'] - open('debug/vs.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/vs.psbt', 'wb') as f: + f.write(psbt) # check some basics mine = BasicPSBT().parse(psbt) @@ -1053,13 +1093,15 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind signed_final = end_sign(accept=True, finalize=True) assert signed_final[0:4] != b'psbt', "expecting raw bitcoin txn" - open('debug/finalized-by-ckcc.txn', 'wt').write(B2A(signed_final)) + with open(f'{sim_root_dir}/debug/finalized-by-ckcc.txn', 'wt') as f: + f.write(B2A(signed_final)) # Sign again, but don't finalize it. start_sign(psbt, finalize=False) signed = end_sign(accept=True) - open('debug/vs-signed-unfin.psbt', 'wb').write(signed) + with open(f'{sim_root_dir}/debug/vs-signed-unfin.psbt', 'wb') as f: + f.write(signed) # Use bitcoind to finalize it this time. resp = bitcoind.supply_wallet.finalizepsbt(str(b64encode(signed), 'ascii'), True) @@ -1069,7 +1111,8 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind # assert resp['complete'] #print("Final txn: %r" % network) - open('debug/finalized-by-btcd.txn', 'wt').write(B2A(network)) + with open(f'{sim_root_dir}/debug/finalized-by-btcd.txn', 'wt') as f: + f.write(B2A(network)) assert network == signed_final, "Finalized differently" @@ -1091,7 +1134,7 @@ def test_finalization_vs_bitcoind(match_key, use_regtest, check_against_bitcoind ("44'/1'/0'/3000/5", '2nd last component'), ("44'/1'/0'/3/5", '2nd last component'), ]) -def test_change_troublesome(dev, start_sign, cap_story, try_path, expect): +def test_change_troublesome(dev, start_sign, cap_story, try_path, expect, sim_root_dir): # NOTE: out#1 is change: # addr = 'mvBGHpVtTyjmcfSsy6f715nbTGvwgbgbwo' # path = (m=4369050F)/44'/1'/0'/1/5 @@ -1115,7 +1158,8 @@ def test_change_troublesome(dev, start_sign, cap_story, try_path, expect): b4.serialize(fd) mod_psbt = fd.getvalue() - open('debug/troublesome.psbt', 'wb').write(mod_psbt) + with open(f'{sim_root_dir}/debug/troublesome.psbt', 'wb') as f: + f.write(mod_psbt) start_sign(mod_psbt) time.sleep(0.1) @@ -1200,8 +1244,6 @@ def spend_outputs(funding_psbt, finalized_txn, tweaker=None): with BytesIO() as rv: nn.serialize(rv) raw = rv.getvalue() - - open('debug/spend_outs.psbt', 'wb').write(raw) return nn, raw @@ -1231,7 +1273,7 @@ def txid_from_export_prompt(cap_story, cap_screen_qr, cap_screen, need_keypress) @pytest.mark.parametrize('segwit_in', [False, True]) def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, settings_set, settings_get, cap_story, sim_exec, hist_count, - txid_from_export_prompt, press_cancel): + txid_from_export_prompt, press_cancel, sim_root_dir): # cleanup prev runs, if very first time thru sim_exec('import history; history.OutptValueCache.clear()') @@ -1243,7 +1285,8 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set outstyles=(['p2wpkh']*num_utxo) + ['p2wpkh-p2sh', 'p2pkh']) _, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False) - open('debug/funding.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/funding.psbt', 'wb') as f: + f.write(psbt) num_inp_utxo = (1 if segwit_in else 0) @@ -1266,6 +1309,8 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set assert all_utxo == hist_b4+num_utxo+num_inp_utxo # build a new PSBT based on those change outputs psbt2, raw = spend_outputs(psbt, txn) + with open(f'{sim_root_dir}/debug/spend_outs.psbt', 'wb') as f: + f.write(raw) # try to sign that ... should work fine try_sign(raw, accept=True, finalize=True) @@ -1281,6 +1326,8 @@ def test_bip143_attack_data_capture(num_utxo, segwit_in, try_sign, fake_txn, set spendables[0][1].nValue += amt psbt3, raw = spend_outputs(psbt, txn, tweaker=value_tweak) + with open(f'{sim_root_dir}/debug/spend_outs.psbt', 'wb') as f: + f.write(raw) with pytest.raises(CCProtoError) as ee: orig, result = try_sign(raw, accept=True, finalize=True) @@ -1352,7 +1399,8 @@ def test_sdcard_signing(encoding, num_outs, del_after, partial, try_sign_microsd @pytest.mark.unfinalized @pytest.mark.parametrize('num_ins', [2,3,8]) @pytest.mark.parametrize('num_outs', [1,2,8]) -def test_payjoin_signing(num_ins, num_outs, fake_txn, try_sign, start_sign, end_sign, cap_story): +def test_payjoin_signing(num_ins, num_outs, fake_txn, try_sign, start_sign, end_sign, + cap_story, sim_root_dir): # Try to simulate a PSBT that might be involved in a Payjoin (BIP-78 txn) @@ -1362,7 +1410,8 @@ def test_payjoin_signing(num_ins, num_outs, fake_txn, try_sign, start_sign, end_ psbt = fake_txn(num_ins, num_outs, segwit_in=True, psbt_hacker=hack) - open('debug/payjoin.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/payjoin.psbt', 'wb') as f: + f.write(psbt) ip = start_sign(psbt, finalize=False) time.sleep(.1) @@ -1414,7 +1463,7 @@ def test_wrong_xfp(fake_txn, try_sign, segwit): assert 'found 12345678' in str(ee) @pytest.mark.parametrize('segwit', [False, True]) -def test_wrong_xfp_multi(fake_txn, try_sign, segwit): +def test_wrong_xfp_multi(fake_txn, try_sign, segwit, sim_root_dir): # A PSBT which is unsigned and doesn't involve our XFP value # - but multiple wrong XFP values @@ -1431,7 +1480,9 @@ def test_wrong_xfp_multi(fake_txn, try_sign, segwit): psbt = fake_txn(7, 2, segwit_in=segwit, psbt_hacker=hack) - open('debug/wrong-xfp.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/wrong-xfp.psbt', 'wb') as f: + f.write(psbt) + with pytest.raises(CCProtoError) as ee: orig, result = try_sign(psbt, accept=True) @@ -1445,7 +1496,8 @@ def test_wrong_xfp_multi(fake_txn, try_sign, segwit): @pytest.mark.parametrize('out_style', ADDR_STYLES_SINGLE) @pytest.mark.parametrize('segwit', [False, True]) @pytest.mark.parametrize('outval', ['.5', '.788888', '0.92640866']) -def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, dev): +def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, dev, + sim_root_dir): # check how we render the value of outputs # - works on simulator and connected USB real-device xp = dev.master_xpub @@ -1454,7 +1506,8 @@ def test_render_outs(out_style, segwit, outval, fake_txn, start_sign, end_sign, psbt = fake_txn(1, 2, dev.master_xpub, segwit_in=segwit, outvals=[oi, int(1E8-oi)], outstyles=[out_style], change_outputs=[1]) - open('debug/render.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/render.psbt', 'wb') as f: + f.write(psbt) # should be able to sign, but get warning @@ -1501,7 +1554,8 @@ def test_negative_fee(dev, fake_txn, try_sign): ( 5, 'mXTN'), ( 2, 'bits'), ( 0, 'sats')]) -def test_value_render(dev, units, fake_txn, start_sign, cap_story, settings_set, settings_remove): +def test_value_render(dev, units, fake_txn, start_sign, cap_story, settings_set, + settings_remove, sim_root_dir): # Check we are rendering values in right units. decimal, units = units @@ -1517,7 +1571,8 @@ def test_value_render(dev, units, fake_txn, start_sign, cap_story, settings_set, need = sum(outputs) psbt = fake_txn(1, len(outputs), dev.master_xpub, segwit_in=True, outvals=outputs, invals=[need]) - open('debug/values.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/values.psbt', 'wb') as f: + f.write(psbt) ip = start_sign(psbt, finalize=False) time.sleep(.1) @@ -1542,12 +1597,13 @@ def test_value_render(dev, units, fake_txn, start_sign, cap_story, settings_set, @pytest.mark.parametrize('num_out', [1,2,3]) @pytest.mark.parametrize('segwit', [True, False]) def test_qr_txn(num_in, num_out, segwit, fake_txn, try_sign, dev, cap_screen_qr, - qr_quality_check, cap_story, need_keypress, is_q1, press_cancel): + qr_quality_check, cap_story, need_keypress, is_q1, press_cancel, + sim_root_dir): psbt = fake_txn(num_in, num_out, dev.master_xpub, segwit_in=segwit) _, txn = try_sign(psbt, accept=True, finalize=True, exit_export_loop=False) - with open('debug/last.txn', 'wb') as f: + with open(f'{sim_root_dir}/debug/last.txn', 'wb') as f: f.write(txn) title, story = cap_story() @@ -1833,7 +1889,7 @@ def test_op_return_signing(op_return_data, dev, fake_txn, bitcoind_d_sim_watch, ({b"x" * 64: b"y" * 128}, {b"q" * 64: b"p" * 128}, {b"w" * 90: b"z" * 256}), ({b"x" * 32: b"y" * 256}, {b"q" * 32: b"p" * 256, b"f" * 15: 32 * b"\x01"}, {b"w": b"z"}), ]) -def test_unknow_values_in_psbt(unknowns, dev, start_sign, end_sign, fake_txn): +def test_unknow_values_in_psbt(unknowns, dev, start_sign, end_sign, fake_txn, sim_root_dir): unknown_global, unknown_ins, unknown_outs = unknowns def hack(psbt): psbt.unknown = unknown_global @@ -1843,7 +1899,8 @@ def test_unknow_values_in_psbt(unknowns, dev, start_sign, end_sign, fake_txn): o.unknown = unknown_outs psbt = fake_txn(5, 5, dev.master_xpub, segwit_in=True, psbt_hacker=hack) - open('debug/last.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'wb') as f: + f.write(psbt) psbt_o = BasicPSBT().parse(psbt) assert psbt_o.unknown == unknown_global for inp in psbt_o.inputs: @@ -1860,7 +1917,7 @@ def test_unknow_values_in_psbt(unknowns, dev, start_sign, end_sign, fake_txn): for out in res.outputs: assert out.unknown == unknown_outs -def test_read_write_prop_attestation_keys(try_sign, fake_txn): +def test_read_write_prop_attestation_keys(try_sign, fake_txn, sim_root_dir): from psbt import ser_prop_key, PSBT_PROP_CK_ID def attach_attest_to_outs(psbt): for idx, o in enumerate(psbt.outputs): @@ -1869,7 +1926,8 @@ def test_read_write_prop_attestation_keys(try_sign, fake_txn): o.proprietary[key] = value psbt = fake_txn(2, 2, psbt_hacker=attach_attest_to_outs) - open('debug/propkeys.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/propkeys.psbt', 'wb') as f: + f.write(psbt) orig, signed = try_sign(psbt) res = BasicPSBT().parse(signed) @@ -1910,7 +1968,7 @@ def test_duplicate_unknow_values_in_psbt(dev, start_sign, end_sign, fake_txn): @pytest.fixture def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev, bitcoind, bitcoind_d_dev_watch, settings_set, - finalize_v2_v0_convert, pytestconfig): + finalize_v2_v0_convert, pytestconfig, sim_root_dir): def doit(addr_fmt, sighash, num_inputs=2, num_outputs=2, consolidation=False, sh_checks=False, psbt_v2=None, tx_check=True): @@ -1984,7 +2042,7 @@ def _test_single_sig_sighash(cap_story, press_select, start_sign, end_sign, dev, psbt_sh = x.as_b64_str() # make useful reference psbt along the way - with open(f'debug/sighash-{sighash[0] if len(sighash) == 1 else "MIX"}.psbt'\ + with open(f'{sim_root_dir}/debug/sighash-{sighash[0] if len(sighash) == 1 else "MIX"}.psbt'\ .replace('|', '-'), 'wt') as f: f.write(psbt_sh) @@ -2847,7 +2905,7 @@ def random_nLockTime_test_cases(num=10): *random_nLockTime_test_cases() ]) def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest, - bitcoind_d_sim_watch, nLockTime): + bitcoind_d_sim_watch, nLockTime, sim_root_dir): # - works on simulator and connected USB real-device nLockTime, expect_ux = nLockTime num_ins = 10 @@ -2883,7 +2941,8 @@ def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest, ) psbt = base64.b64decode(psbt_resp.get("psbt")) - open('debug/locktimes.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/locktimes.psbt', 'wb') as f: + f.write(psbt) # should be able to sign, but get warning @@ -2905,7 +2964,7 @@ def test_timelocks_visualize(start_sign, end_sign, dev, bitcoind, use_regtest, def test_base64_psbt_qr(in_out, partial, segwit, scan_a_qr, readback_bbqr, goto_home, use_regtest, cap_story, fake_txn, dev, decode_psbt_with_bitcoind, decode_with_bitcoind, - press_cancel, press_select, need_keypress): + press_cancel, press_select, need_keypress, sim_root_dir): def hack(psbt): if partial: # change first input to not be ours @@ -2923,7 +2982,8 @@ def test_base64_psbt_qr(in_out, partial, segwit, scan_a_qr, readback_bbqr, psbt = base64.b64encode(psbt).decode() - open('debug/last.psbt', 'w').write(psbt) + with open(f'{sim_root_dir}/debug/last.psbt', 'w') as f: + f.write(psbt) goto_home() need_keypress(KEY_QR) @@ -3104,8 +3164,7 @@ def test_txout_explorer_op_return(finalize, data, fake_txn, start_sign, cap_stor assert s == dd0 assert e == dd1 qr = qr_list[i - 20] - assert qr.startswith(s) - assert qr.endswith(e) + assert qr == "" press_cancel() # exit txn out explorer end_sign(finalize=finalize) @@ -3180,7 +3239,7 @@ def test_zero_value_outputs(num_outs, change, fake_txn, start_sign, end_sign, re psbt = fake_txn(1, num_outs, outvals=num_outs*[0], change_outputs=change_outs, input_amount=1) start_sign(psbt, False, stxn_flags=STXN_VISUALIZE) story = end_sign(accept=None, expect_txn=False).decode() - assert f"Zero Value: Non-standard zero value outputs: {num_outs}" in story + assert f"Zero Value: Non-standard zero value output(s)" in story assert "1 input" in story assert f"{num_outs} output{'' if num_outs == 1 else 's'}" in story assert 'Network fee 0.00000001 XTN' in story diff --git a/testing/test_teleport.py b/testing/test_teleport.py index cf03f8e2..a986a1e1 100644 --- a/testing/test_teleport.py +++ b/testing/test_teleport.py @@ -20,7 +20,7 @@ def THIS_FILE_requires_q1(is_q1, is_headless): if not is_q1 or is_headless: raise pytest.skip('Q1 only (not headless)') -@pytest.fixture() +@pytest.fixture def rx_start(grab_payload, goto_home, pick_menu_item): def doit(**kws): goto_home() @@ -31,7 +31,7 @@ def rx_start(grab_payload, goto_home, pick_menu_item): return doit -@pytest.fixture() +@pytest.fixture def main_do_over(unit_test, settings_get, settings_set): # reset all contents, including master secret ... except ktrx # - so you can test backup-restore onto blank unit @@ -42,7 +42,7 @@ def main_do_over(unit_test, settings_get, settings_set): return doit -@pytest.fixture() +@pytest.fixture def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_story, nfc_block4rf, cap_screen_qr, readback_bbqr): # started the process; capture pw/code and QR contents, verify NFC works @@ -108,7 +108,7 @@ def grab_payload(press_select, need_keypress, press_cancel, nfc_read_url, cap_s return doit -@pytest.fixture() +@pytest.fixture def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr, enter_complex, cap_screen, goto_home, split_scan_bbqr): # finish the teleport by doing QR and getting data def doit(data, pw, expect_fail=False, expect_xfp=None): @@ -126,10 +126,13 @@ def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr, if expect_fail: time.sleep(.200) return - for retries in range(20): + + for _ in range(10): scr = cap_screen() if 'Teleport Password' in scr: break - time.sleep(.200) + time.sleep(.2) + else: + assert False, "Teleport Password not in screen" if expect_xfp: assert xfp2str(expect_xfp) in scr @@ -140,7 +143,7 @@ def rx_complete(press_select, need_keypress, press_cancel, cap_story, scan_a_qr, return doit -@pytest.fixture() +@pytest.fixture def tx_start(press_select, need_keypress, press_cancel, goto_home, pick_menu_item, cap_story, scan_a_qr, enter_complex, cap_screen): # start the Tx process, capturing password and leaving you are picker menu @@ -150,13 +153,15 @@ def tx_start(press_select, need_keypress, press_cancel, goto_home, pick_menu_ite time.sleep(.250) # required scan_a_qr(rx_qr) - time.sleep(.250) # required - scr = cap_screen() - if expect_fail: - assert expect_fail in scr - return - - assert 'Teleport Password (number)' in scr + for _ in range(10): + scr = cap_screen() + if expect_fail and expect_fail in scr: + return + elif 'Teleport Password (number)' in scr: + break + time.sleep(.2) + else: + assert False, "Teleport Password not in screen" enter_complex(rx_code) time.sleep(.150) # required @@ -416,10 +421,10 @@ def test_tx_wrong_pub(rx_start, tx_start, cap_menu, enter_complex, pick_menu_ite @pytest.mark.parametrize('segwit', [True]) @pytest.mark.parametrize('incl_xpubs', [ False ]) def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, dev, clear_ms, - fake_ms_txn, try_sign, incl_xpubs, bitcoind, cap_story, need_keypress, + fake_ms_txn, try_sign, incl_xpubs, bitcoind, cap_story, need_keypress, cap_menu, pick_menu_item, grab_payload, rx_complete, press_select, ndef_parse_txn_psbt, press_nfc, nfc_read, settings_get, settings_set, - txid_from_export_prompt): + txid_from_export_prompt, sim_root_dir): # IMPORTANT: won't work if you start simulator with --ms flag. Use no args all_out_styles = list(unmap_addr_fmt.keys()) @@ -436,13 +441,15 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d psbt = fake_ms_txn(num_ins, num_outs, M, keys, segwit_in=segwit, incl_xpubs=incl_xpubs, outstyles=all_out_styles, change_outputs=list(range(1,num_outs))) - open(f'debug/myself-before.psbt', 'wb').write(psbt) + with open(f'{sim_root_dir}/debug/myself-before.psbt', 'wb') as f: + f.write(psbt) cur_wallet = 0 my_xfp = select_wallet(cur_wallet) _, updated = try_sign(psbt, accept_ms_import=incl_xpubs, exit_export_loop=False) - open(f'debug/myself-after-1.psbt', 'wb').write(updated) + with open(f'{sim_root_dir}/debug/myself-after-1.psbt', 'wb') as f: + f.write(updated) assert updated != psbt title, body = cap_story() @@ -484,7 +491,8 @@ def test_teleport_ms_sign(M, use_regtest, make_myself_wallet, segwit, num_ins, d assert len(pw) == 8 nn = xfp2str(next_xfp) - open(f'debug/next_qr_{nn}.txt', 'wt').write(f'{nn}\n\n{pw}\n\n{data}') + with open(f'{sim_root_dir}/debug/next_qr_{nn}.txt', 'wt') as f: + f.write(f'{nn}\n\n{pw}\n\n{data}') time.sleep(.1) title, story = cap_story() @@ -628,8 +636,7 @@ def test_teleport_real_ms(dev, fake_ms_txn): # py.test test_teleport.py --dev --manual -k test_teleport_real_ms # from bip32 import BIP32Node - from struct import unpack - from ckcc_protocol.protocol import CCProtocolPacker, CCProtoError + from ckcc_protocol.protocol import CCProtocolPacker M = N = 2 diff --git a/testing/test_upgrades.py b/testing/test_upgrades.py index 95a9c2bf..b89eac03 100644 --- a/testing/test_upgrades.py +++ b/testing/test_upgrades.py @@ -25,13 +25,13 @@ def upload_file(dev): return doit @pytest.fixture -def make_firmware(): - def doit(hw_compat, fname='../stm32/firmware-signed.bin', outname='tmp-firmware.bin'): +def make_firmware(src_root_dir): + def doit(hw_compat, fname=f'{src_root_dir}/stm32/firmware-signed.bin', outname='tmp-firmware.bin'): # os.system(f'signit sign 3.0.99 --keydir ../stm32/keys -r {fname} -o {outname} --hw-compat=0x{hw_compat:02x}') p = subprocess.run( [ 'signit', 'sign', '3.0.99', - '--keydir', '../stm32/keys', + '--keydir', f'{src_root_dir}/stm32/keys', '-r', f'{fname}', '-o', f'{outname}', f'--hw-compat={hw_compat}' @@ -50,7 +50,7 @@ def make_firmware(): return doit @pytest.fixture -def upgrade_by_sd(open_microsd, cap_story, pick_menu_item, goto_home, press_select, microsd_path, sim_exec): +def upgrade_by_sd(open_microsd, cap_story, pick_menu_item, goto_home, press_select, microsd_path, sim_exec, src_root_dir): # send a firmware file over the microSD card @@ -64,7 +64,7 @@ def upgrade_by_sd(open_microsd, cap_story, pick_menu_item, goto_home, press_sele # create DFU file (wrapper) open(f'{fname}.bin', 'wb').write(data) dfu = microsd_path('tmp-firmware.dfu') - cmd = f'../external/micropython/tools/dfu.py -b 0x08008000:{fname}.bin {dfu}' + cmd = f'{src_root_dir}/external/micropython/tools/dfu.py -b 0x08008000:{fname}.bin {dfu}' print(cmd) os.system(cmd) diff --git a/testing/test_ux.py b/testing/test_ux.py index d69dea3e..a221c030 100644 --- a/testing/test_ux.py +++ b/testing/test_ux.py @@ -826,7 +826,8 @@ def test_sign_file_from_list_files(f_len, goto_home, cap_story, pick_menu_item, def test_bip39_pw_signing_xfp_ux(pick_menu_item, press_select, cap_story, enter_complex, - reset_seed_words, cap_menu, go_to_passphrase): + reset_seed_words, cap_menu, go_to_passphrase, microsd_wipe): + microsd_wipe() # need to wipe all PSBT on SD card so we do not proceed to signing go_to_passphrase() enter_complex("21coinkite21", apply=True) time.sleep(0.3) @@ -834,6 +835,7 @@ def test_bip39_pw_signing_xfp_ux(pick_menu_item, press_select, cap_story, enter_ assert title == "[0C9DC99D]" assert 'Above is the master key fingerprint of the new wallet' in story press_select() # confirm passphrase + time.sleep(0.1) m = cap_menu() assert m[0] == "[0C9DC99D]" pick_menu_item("Ready To Sign") @@ -973,8 +975,8 @@ def test_custom_pushtx_url(goto_home, pick_menu_item, press_select, enter_comple ("coldcard-export.json", "J"), ("coldcard-export.sig", "U"), ]) -def test_bbqr_share_files(fname, ftype, readback_bbqr, need_keypress, - goto_home, pick_menu_item, is_q1, cap_menu): +def test_bbqr_share_files(fname, ftype, readback_bbqr, need_keypress, src_root_dir, + goto_home, pick_menu_item, is_q1, cap_menu, sim_root_dir): goto_home() if not is_q1: pick_menu_item("Advanced/Tools") @@ -982,8 +984,8 @@ def test_bbqr_share_files(fname, ftype, readback_bbqr, need_keypress, assert "BBQr File Share" not in cap_menu() return - fpath = "data/" + fname - shutil.copy2(fpath, '../unix/work/MicroSD') + fpath = f"{src_root_dir}/testing/data/" + fname + shutil.copy2(fpath, f'{sim_root_dir}/MicroSD') pick_menu_item("Advanced/Tools") pick_menu_item("File Management") pick_menu_item("BBQr File Share") @@ -995,14 +997,15 @@ def test_bbqr_share_files(fname, ftype, readback_bbqr, need_keypress, res = f.read() assert res == rb - os.remove('../unix/work/MicroSD/' + fname) + os.remove(f'{sim_root_dir}/MicroSD/' + fname) @pytest.mark.parametrize("fname", [ "ccbk-start.json", "devils-txn.txn", "payjoin.psbt", # base64 string in file ]) -def test_qr_share_files(fname, pick_menu_item, goto_home, is_q1, cap_menu, cap_screen_qr): +def test_qr_share_files(fname, pick_menu_item, goto_home, is_q1, cap_menu, cap_screen_qr, + src_root_dir, sim_root_dir): goto_home() if not is_q1: pick_menu_item("Advanced/Tools") @@ -1010,8 +1013,8 @@ def test_qr_share_files(fname, pick_menu_item, goto_home, is_q1, cap_menu, cap_s assert "QR File Share" not in cap_menu() return - fpath = "data/" + fname - shutil.copy2(fpath, '../unix/work/MicroSD') + fpath = f"{src_root_dir}/testing/data/" + fname + shutil.copy2(fpath, f'{sim_root_dir}/MicroSD') pick_menu_item("Advanced/Tools") pick_menu_item("File Management") pick_menu_item("QR File Share") @@ -1022,7 +1025,7 @@ def test_qr_share_files(fname, pick_menu_item, goto_home, is_q1, cap_menu, cap_s res = f.read() assert res == qr.decode() - os.remove('../unix/work/MicroSD/' + fname) + os.remove(f'{sim_root_dir}/MicroSD/' + fname) @pytest.mark.onetime diff --git a/testing/test_vdisk.py b/testing/test_vdisk.py index 66fc5bb9..b6db2d3d 100644 --- a/testing/test_vdisk.py +++ b/testing/test_vdisk.py @@ -30,7 +30,7 @@ def test_vd_basics(dev, virtdisk_path, is_simulator): @pytest.fixture def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, press_cancel, - pick_menu_item, goto_home): + pick_menu_item, goto_home, sim_root_dir): # like "try_sign" but use Virtual Disk to send/receive PSBT/results # - on real dev, need user to manually say yes ... alot @@ -142,10 +142,9 @@ def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, pre assert expect_finalize assert got_txn assert got_txid == txid == result_txid - with open("debug/vd-result.txn", 'wb') as f: + with open(f"{sim_root_dir}/debug/vd-result.txn", 'wb') as f: f.write(got_txid) - # check output encoding matches input (for PSBT only) if got_psbt: if encoding == 'hex': @@ -170,7 +169,7 @@ def try_sign_virtdisk(press_select, virtdisk_path, cap_story, virtdisk_wipe, pre if got_psbt: assert got_psbt[0:5] == b'psbt\xff' - with open("debug/vd-result.psbt", 'wb') as f: + with open(f"{sim_root_dir}/debug/vd-result.psbt", 'wb') as f: f.write(got_psbt) from psbt import BasicPSBT diff --git a/testing/txn.py b/testing/txn.py index fc34e2ac..73f592d2 100644 --- a/testing/txn.py +++ b/testing/txn.py @@ -14,7 +14,7 @@ from serialize import uint256_from_str from ctransaction import CTransaction, COutPoint, CTxIn, CTxOut -@pytest.fixture() +@pytest.fixture def fake_txn(dev, pytestconfig): # make various size txn's ... completely fake and pointless values # - but has UTXO's to match needs diff --git a/unix/README.md b/unix/README.md index 44d0ce25..481102dc 100644 --- a/unix/README.md +++ b/unix/README.md @@ -60,6 +60,7 @@ wallet (on testnet, always with the same seed). But there are other options: - `--scan` => (Q) use attached serial port connected to a QR scanner module (not simulation) - `--battery` => (Q) assume the USB cable is NOT connected (ie. on battery power) - `--early-usb` => start simulated USB interface even before user is login (useful for login testing) +- `--segregate` => scroll down to `Running simulators in parallel` section See `variant/sim_settings.py` for the details of settings-related options. @@ -78,7 +79,7 @@ See `variant/sim_settings.py` for the details of settings-related options. ## Requirements -- uses good olde `xterm` for console input and output +- uses good old `xterm` for console input and output - this directory has additional `requirements.txt` (a superset of other requirements of the project) - run "brew install sdl2" before/after doing python requirements - run "make setup" then "make" @@ -99,4 +100,39 @@ See `variant/sim_settings.py` for the details of settings-related options. - linux supported (only tested on debian based Ubuntu 20.04), please check main README.md - Windows can work under WSL but is not supported by our team. Follow instructions on +# Running simulators in parallel +Normally, when simulator is spwned with `./simulator.py --eff --q1` (or similar) +the default socket file is produced (`/tmp/ckcc-simulator.sock`) for simulator to be able to emulate various types of comms. +Each time new simulator is spwned (while som older still running) the socket file gets claimed by the most recently opened simulator. +You can continue to use older simulator manually, but it is no longer possible to communicate via the socket file. +Besides shared socket file, all simulators share some working directories (`work` & previously `testing/debug`, currently `work/debug`). + +To enable full parallel operation on multiple simulators use `--segregate` simulator flag that: +* creates unique simulator socket file `/tmp/ckcc-simulator-.sock` for every simulator spwned with the flag +* creates separate simulator work directory in `/tmp/cc-simulators/` (every simulator has its own debug dir in work dir) + +Spawn two simulators: + +```shell +./simulator.py --eff --segregate # Mk4 +./simulator.py --eff --segregate --q1 # Q +``` + +Two directories were created inside `/tmp/cc-simulators` and two socket files in `/tmp` with same PID in names as directory names created. +To operate above simulators new `--socket`/`-c` flag needs to be used with client: `ckcc -c /tmp/ckcc-simulator-35156.sock ...` + +```shell +ckcc -c /tmp/ckcc-simulator-35156.sock addr -s +ckcc -c /tmp/ckcc-simulator-35291.sock addr -s +``` + +Simulator socket path is dumped to STDOUT after simulator is started: +```shell +Coldcard Simulator: Commands (over simulated window): + - Control-Q to quit + - ^Z to snapshot screen. + - ^S/^E to start/end movie recording + - ^N to capture NFC data (tap it) + - socket: /tmp/ckcc-simulator-35291.sock +``` diff --git a/unix/linux_addr.patch b/unix/linux_addr.patch index c7174eb5..d3223388 100644 --- a/unix/linux_addr.patch +++ b/unix/linux_addr.patch @@ -1,18 +1,16 @@ diff --git a/unix/variant/pyb.py b/unix/variant/pyb.py -index d22bb1b..fe8e7ca 100644 +index 5e108da3..5a15fc9c 100644 --- a/unix/variant/pyb.py +++ b/unix/variant/pyb.py -@@ -36,10 +36,10 @@ class USB_HID: - import usocket as socket +@@ -40,9 +40,9 @@ class USB_HID: + sfp_b = SOCKET_FILE_PATH.encode() self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) # If on linux, try commenting the following line -- addr = bytes([len(self.fn)+2, socket.AF_UNIX] + list(self.fn)) -+ # addr = bytes([len(self.fn)+2, socket.AF_UNIX] + list(self.fn)) +- addr = bytes([len(sfp_b)+2, socket.AF_UNIX] + list(sfp_b)) ++ #addr = bytes([len(sfp_b)+2, socket.AF_UNIX] + list(sfp_b)) # If on linux, try uncommenting the following two lines -- #import struct -- #addr = struct.pack('H108s', socket.AF_UNIX, self.fn) -+ import struct -+ addr = struct.pack('H108s', socket.AF_UNIX, self.fn) +- # addr = struct.pack('H108s', socket.AF_UNIX, sfp_b) ++ addr = struct.pack('H108s', socket.AF_UNIX, sfp_b) while 1: try: self.pipe.bind(addr) diff --git a/unix/sim_boot.py b/unix/sim_boot.py index b39b6128..ef6efb4c 100644 --- a/unix/sim_boot.py +++ b/unix/sim_boot.py @@ -6,6 +6,11 @@ import machine, pyb, sys +socket_path = sys.argv.pop() # last arg must be a socket path - remove +assert ("ckcc-simulator" in socket_path) and (".sock" in socket_path) +pyb.SOCKET_FILE_PATH = socket_path +print("socket:", pyb.SOCKET_FILE_PATH) + if '--metal' in sys.argv: # next in argv will be two open file descriptors to use for serial I/O to a real Coldcard import bare_metal diff --git a/unix/simulator.py b/unix/simulator.py index 016bb307..af7e94fa 100755 --- a/unix/simulator.py +++ b/unix/simulator.py @@ -14,12 +14,10 @@ # Limitations: # - USB light not fully implemented, because happens at irq level on real product # -import os, sys, signal, time, pdb, tempfile, struct, zlib -import subprocess, asyncio +import os, sys, signal, time, pdb, tempfile, struct, zlib, subprocess, shutil from dataclasses import dataclass import sdl2.ext -import PIL -from PIL import Image, ImageSequence, ImageOps +from PIL import Image, ImageOps from select import select import fcntl from bare import BareMetal @@ -31,6 +29,7 @@ UNIX_SOCKET_PATH = '/tmp/ckcc-simulator.sock' current_led_state = 0x0 + def activate_file(filename): # see if sys.platform == "win32": @@ -745,6 +744,14 @@ def handle_q1_key_events(event, numpad_tx, data_tx): def start(): is_q1 = ('--q1' in sys.argv) + segregate = ("--segregate" in sys.argv) + pid = os.getpid() + # for compatibility with old clients + # UNIX_SOCKET_PATH is always used if not segregate + socket_path = UNIX_SOCKET_PATH + if segregate: + socket_path = '/tmp/ckcc-simulator-%d.sock' % pid + if "--headless" in sys.argv: sys.argv.remove("--headless") is_headless = True @@ -760,6 +767,7 @@ def start(): - ^S/^E to start/end movie recording - ^N to capture NFC data (tap it)''' ) + print(" - socket: %s" % socket_path) if is_q1: print('''\ Q1 specials: @@ -818,10 +826,10 @@ Q1 specials: # manage unix socket cleanup for client def sock_cleanup(): import os - fp = UNIX_SOCKET_PATH + fp = socket_path if os.path.exists(fp): os.remove(fp) - sock_cleanup() + import atexit atexit.register(sock_cleanup) @@ -849,11 +857,30 @@ Q1 specials: scan_args = [ '--scan', str(port.fileno()) ] sys.argv.remove('--scan') - os.chdir('./work') - cc_cmd = ['../coldcard-mpy', - '-X', 'heapsize=9m', - '-i', '../sim_boot.py'] + [str(i) for i in pass_fds] \ - + metal_args + scan_args + sys.argv[1:] + # unix + cwd = os.getcwd() + # abs paths + cc_mpy = os.path.join(cwd, "coldcard-mpy") + sim_boot = os.path.join(cwd, "sim_boot.py") + + if segregate: + os.makedirs("/tmp/cc-simulators", exist_ok=True) + os.chdir("/tmp/cc-simulators") + # our new work /tmp/cc-simulators/ + os.mkdir(str(pid)) + os.chdir(str(pid)) + os.mkdir("MicroSD") + os.mkdir("settings") + os.mkdir("VirtDisk") + os.mkdir("debug") + # needed for VirtDisk test + shutil.copy(os.path.join(cwd, "work", "VirtDisk", "README.md"), + os.path.join(os.getcwd(), "VirtDisk", "README.md")) + else: + os.chdir('./work') + + cc_cmd = [cc_mpy, '-X', 'heapsize=9m', '-i', sim_boot] + [str(i) for i in pass_fds] \ + + metal_args + scan_args + sys.argv[1:] + [socket_path] if is_headless: pass_fds.remove("-1") diff --git a/unix/variant/ckcc.py b/unix/variant/ckcc.py index e32cb0b4..99b46f3f 100644 --- a/unix/variant/ckcc.py +++ b/unix/variant/ckcc.py @@ -198,10 +198,9 @@ def is_simulator(): def is_debug_build(): return True - def get_sim_root_dirs(): # return a single path and list of files to pretend to find there - import ffilib, os + import ffilib libc = ffilib.libc() b = bytearray(500) diff --git a/unix/variant/pyb.py b/unix/variant/pyb.py index 4c06ad67..5e108da3 100644 --- a/unix/variant/pyb.py +++ b/unix/variant/pyb.py @@ -2,9 +2,10 @@ # import utime as time import uerrno as errno -import sys +import usocket as socket +import sys, struct, os -from machine import Pin +SOCKET_FILE_PATH = None class USB_VCP: @staticmethod @@ -28,22 +29,20 @@ def usb_mode(nm=UNSET, **kws): return _umode class USB_HID: - fn = b'/tmp/ckcc-simulator.sock' - def __init__(self): self.pipe = None self.last_from = None self._open() def _open(self): - import sys - import usocket as socket + + assert SOCKET_FILE_PATH # has to be set in sim_boot.py by caller + sfp_b = SOCKET_FILE_PATH.encode() self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) # If on linux, try commenting the following line - addr = bytes([len(self.fn)+2, socket.AF_UNIX] + list(self.fn)) + addr = bytes([len(sfp_b)+2, socket.AF_UNIX] + list(sfp_b)) # If on linux, try uncommenting the following two lines - #import struct - #addr = struct.pack('H108s', socket.AF_UNIX, self.fn) + # addr = struct.pack('H108s', socket.AF_UNIX, sfp_b) while 1: try: self.pipe.bind(addr) @@ -51,8 +50,7 @@ class USB_HID: except OSError as exc: if exc.args[0] == errno.EADDRINUSE: # handle restart after first run - import os - os.remove(self.fn) + os.remove(SOCKET_FILE_PATH) continue def recv(self, buf, timeout=0): diff --git a/unix/work/debug/.gitignore b/unix/work/debug/.gitignore new file mode 100644 index 00000000..5146bd3b --- /dev/null +++ b/unix/work/debug/.gitignore @@ -0,0 +1,11 @@ +*.7z +*.txt +*.json +*.psbt +*.csv +*.pdf +*.dfu +*.txn +*.sig +*.png +.tmp.tmp \ No newline at end of file diff --git a/testing/debug/README.md b/unix/work/debug/README.md similarity index 100% rename from testing/debug/README.md rename to unix/work/debug/README.md