From e8f26b1e305e56fabbaeccf6eddb05c8236cdfb7 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Wed, 11 Dec 2024 10:38:13 -0600 Subject: [PATCH] Screenshot generator fixes, refactor * New `ScreenshotConfig` utility class. * Proper reset & isolation when looping to generate screenshots for multiple locales. * Bugfix for unpredictable race condition when rendering screenshots. * Improved screenshots due to the above. --- src/seedsigner/controller.py | 10 + src/seedsigner/gui/toast.py | 14 +- src/seedsigner/views/seed_views.py | 14 +- tests/screenshot_generator/generator.py | 470 ++++++++++++------------ tests/screenshot_generator/utils.py | 36 +- 5 files changed, 300 insertions(+), 244 deletions(-) diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index a963e13f..7505408c 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -147,6 +147,16 @@ class Controller(Singleton): else: # Instantiate the one and only Controller instance return cls.configure_instance() + + + @classmethod + def reset_instance(cls): + """ + Currently used by the screenshot generator, but could potentially be used to + wipe and reset the state of the device. + """ + cls._instance = None + cls.configure_instance() @classmethod diff --git a/src/seedsigner/gui/toast.py b/src/seedsigner/gui/toast.py index b9e53fbe..baefa16b 100644 --- a/src/seedsigner/gui/toast.py +++ b/src/seedsigner/gui/toast.py @@ -189,12 +189,16 @@ class BaseToastOverlayManagerThread(BaseThread): class RemoveSDCardToastManagerThread(BaseToastOverlayManagerThread): - def __init__(self, activation_delay=3): - # Note: activation_delay is configurable so the screenshot generator can get the - # toast to immediately render. + def __init__(self, activation_delay: int = 3, duration: int = 1e6): + """ + * activation_delay: configurable so the screenshot generator can get the + toast to immediately render. + * duration: default value is essentially forever. Overrideable for the + screenshot generator. + """ super().__init__( - activation_delay=activation_delay, # seconds - duration=1e6, # seconds ("forever") + activation_delay=activation_delay, + duration=duration, ) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 0832a3f8..a02e8b4c 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -77,11 +77,8 @@ class SeedSelectSeedView(View): TYPE_ELECTRUM = ButtonOption("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) - def __init__(self, flow: str = None): - from seedsigner.controller import Controller + def __init__(self, flow: str): super().__init__() - if not flow: - flow = Controller.FLOW__VERIFY_SINGLESIG_ADDR self.flow = flow @@ -572,6 +569,7 @@ class SeedOptionsView(View): button_data = [] if self.controller.unverified_address: + # TODO: Verify that an addr verification flow can actually reach this code addr = self.controller.unverified_address["address"][:7] self.VERIFY_ADDRESS.button_label += f" {addr}" button_data.append(self.VERIFY_ADDRESS) @@ -1701,7 +1699,7 @@ class AddressVerificationStartView(View): if self.controller.unverified_address["script_type"] == SettingsConstants.LEGACY_P2PKH: # Legacy P2PKH addresses are always singlesig sig_type = SettingsConstants.SINGLE_SIG - destination = Destination(SeedSelectSeedView, skip_current_view=True) + destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True) if self.controller.unverified_address["script_type"] == SettingsConstants.NESTED_SEGWIT: # No way to differentiate single sig from multisig @@ -1721,7 +1719,7 @@ class AddressVerificationStartView(View): else: sig_type = SettingsConstants.SINGLE_SIG - destination = Destination(SeedSelectSeedView, skip_current_view=True) + destination = Destination(SeedSelectSeedView, view_args=dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), skip_current_view=True) elif self.controller.unverified_address["script_type"] == SettingsConstants.TAPROOT: # TODO: add Taproot support @@ -2158,10 +2156,6 @@ class SeedSignMessageConfirmMessageView(View): super().__init__() self.page_num = page_num # Note: zero-indexed numbering! - self.seed_num = self.controller.sign_message_data.get("seed_num") - if self.seed_num is None: - raise Exception("Routing error: seed_num hasn't been set") - def run(self): from seedsigner.gui.screens.seed_screens import SeedSignMessageConfirmMessageScreen diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index e41551a4..bf71d3df 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -37,7 +37,7 @@ from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, NotYe from seedsigner.views.screensaver import OpeningSplashView from seedsigner.views.view import ErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView, View -from .utils import ScreenshotComplete, ScreenshotRenderer +from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer import warnings; warnings.warn = lambda *args, **kwargs: None @@ -58,6 +58,51 @@ def test_generate_all(locale, target_locale): +"""************************************************************************************** + Set up global test data that will be re-used across a variety of screenshots and for + all locales. +**************************************************************************************""" +BASE64_PSBT_1 = """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""" +mnemonic_12b = ["abandon"] * 11 + ["about"] +seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + +def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes): + data = (compact.to_bytes(OPCODES.OP_RETURN) + + compact.to_bytes(OPCODES.OP_PUSHDATA1) + + compact.to_bytes(len(raw_payload_data)) + + raw_payload_data) + script = Script(data) + output = OutputScope() + output.script_pubkey = script + output.value = 0 + psbt.outputs.append(output) + return psbt.to_string() + +# Prep a PSBT with a human-readable OP_RETURN +raw_payload_data = "Chancellor on the brink of third bailout for banks".encode() +psbt = PSBT.from_base64(BASE64_PSBT_1) + +# Simplify the output side +output = psbt.outputs[-1] +psbt.outputs.clear() +psbt.outputs.append(output) +assert len(psbt.outputs) == 1 +BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data) + +# Prep a PSBT with a (repeatably) random 80-byte OP_RETURN +random.seed(6102) +BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80)) + +mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split() +mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() +seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) +seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) +seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) + +MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx""" + + + def generate_screenshots(locale): """ The `Renderer` class is mocked so that calls in the normal code are ignored @@ -75,69 +120,21 @@ def generate_screenshots(locale): Renderer.configure_instance = Mock() Renderer.get_instance = Mock(return_value=screenshot_renderer) - controller = Controller.get_instance() - BASE64_PSBT_1 = """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""" - mnemonic_12b = ["abandon"] * 11 + ["about"] - seed_12b = Seed(mnemonic=mnemonic_12b, wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - - def add_op_return_to_psbt(psbt: PSBT, raw_payload_data: bytes): - data = (compact.to_bytes(OPCODES.OP_RETURN) + - compact.to_bytes(OPCODES.OP_PUSHDATA1) + - compact.to_bytes(len(raw_payload_data)) + - raw_payload_data) - script = Script(data) - output = OutputScope() - output.script_pubkey = script - output.value = 0 - psbt.outputs.append(output) - return psbt.to_string() - - # Prep a PSBT with a human-readable OP_RETURN - raw_payload_data = "Chancellor on the brink of third bailout for banks".encode() - psbt = PSBT.from_base64(BASE64_PSBT_1) - - # Simplify the output side - output = psbt.outputs[-1] - psbt.outputs.clear() - psbt.outputs.append(output) - assert len(psbt.outputs) == 1 - BASE64_PSBT_WITH_OP_RETURN_TEXT = add_op_return_to_psbt(psbt, raw_payload_data) - - # Prep a PSBT with a (repeatably) random 80-byte OP_RETURN - random.seed(6102) - BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES = add_op_return_to_psbt(PSBT.from_base64(BASE64_PSBT_1), random.randbytes(80)) - - - def setup_screenshots(locale: str) -> dict: - # Set up some test data that we'll need in the `Controller` for certain Views + def setup_screenshots(locale: str) -> dict[str, list[ScreenshotConfig]]: + """ Set up some test data that we'll need in the `Controller` for certain Views """ + # Must reset the Controller so each locale gets a fresh start + Controller.reset_instance() controller = Controller.get_instance() controller.settings.set_value(SettingsConstants.SETTING__SIG_TYPES, [attr for attr, name in SettingsConstants.ALL_SIG_TYPES]) controller.settings.set_value(SettingsConstants.SETTING__SCRIPT_TYPES, [attr for attr, name in SettingsConstants.ALL_SCRIPT_TYPES]) - mnemonic_12 = "forum undo fragile fade shy sign arrest garment culture tube off merit".split() - mnemonic_24 = "attack pizza motion avocado network gather crop fresh patrol unusual wild holiday candy pony ranch winter theme error hybrid van cereal salon goddess expire".split() - seed_12 = Seed(mnemonic=mnemonic_12, passphrase="cap*BRACKET3stove", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - seed_24 = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) - seed_24_w_passphrase = Seed(mnemonic=mnemonic_24, passphrase="some-PASS*phrase9", wordlist_language_code=SettingsConstants.WORDLIST_LANGUAGE__ENGLISH) controller.storage.seeds.append(seed_12) controller.storage.seeds.append(seed_12b) controller.storage.seeds.append(seed_24) controller.storage.set_pending_seed(seed_24_w_passphrase) - # Seed: Verify Address data - controller.unverified_address = dict( - # These are all totally fake data - address="bc1q6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek", - network=SettingsConstants.MAINNET, - sig_type=SettingsConstants.SINGLE_SIG, - script_type=SettingsConstants.NATIVE_SEGWIT, - derivation_path = "m/84h/0h/0h", - verified_index=5, - verified_index_is_change=False - ) - # Pending mnemonic for ToolsCalcFinalWordShowFinalWordView controller.storage.init_pending_mnemonic(num_words=12) for i, word in enumerate(mnemonic_12[:11]): @@ -150,10 +147,6 @@ def generate_screenshots(locale): controller.psbt = decoder.get_psbt() controller.psbt_seed = seed_12b - # Multisig wallet descriptor for the multisig in the above PSBT - MULTISIG_WALLET_DESCRIPTOR = """wsh(sortedmulti(1,[22bde1a9/48h/1h/0h/2h]tpubDFfsBrmpj226ZYiRszYi2qK6iGvh2vkkghfGB2YiRUVY4rqqedHCFEgw12FwDkm7rUoVtq9wLTKc6BN2sxswvQeQgp7m8st4FP8WtP8go76/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*))#3jhtf6yx""" - controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR) - # Message signing data derivation_path = "m/84h/0h/0h/0/0" controller.sign_message_data = { @@ -165,17 +158,18 @@ def generate_screenshots(locale): # Automatically populate all Settings options Views settings_views_list = [] - settings_views_list.append(settings_views.SettingsMenuView) - settings_views_list.append(( - settings_views.SettingsMenuView, - dict( - visibility=SettingsConstants.VISIBILITY__ADVANCED, - selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS, - initial_scroll=240, # Just guessing how many pixels to scroll down - ), - "SettingsMenuView__Advanced" - )) - + settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView)) + settings_views_list.append( + ScreenshotConfig( + settings_views.SettingsMenuView, + dict( + visibility=SettingsConstants.VISIBILITY__ADVANCED, + selected_attr=SettingsConstants.SETTING__ELECTRUM_SEEDS, + initial_scroll=240, # Just guessing how many pixels to scroll down + ), + screenshot_name="SettingsMenuView__Advanced" + ) + ) # so we get a choice for transcribe seed qr format controller.settings.set_value( @@ -186,135 +180,183 @@ def generate_screenshots(locale): if settings_entry.visibility == SettingsConstants.VISIBILITY__HIDDEN: continue - settings_views_list.append((settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) + settings_views_list.append(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}")) settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" settingsqr_data_not_persistent = f"settings::v1 name=Mode_Ephemeral persistent=D coords=spa,spd denom=thr network=M qr_density=M xpub_export=E sigs=ss scripts=nat xpub_details=E passphrase=E camera=0 compact_seedqr=E bip85=D priv_warn=E dire_warn=E partners=E locale={locale}" + + # Set up screenshot-specific callbacks to inject data before the View is run and + # reset data after the View is run. + def load_basic_psbt_cb(): + decoder = DecodeQR() + decoder.add_data(BASE64_PSBT_1) + controller.psbt = decoder.get_psbt() + controller.psbt_seed = seed_12b + controller.multisig_wallet_descriptor = None + + + def load_multisig_wallet_descriptor_cb(): + controller.multisig_wallet_descriptor = embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR) + + + def load_address_verification_data_cb(): + controller.unverified_address = dict( + # These are all totally fake data + address="bc1q6p00wazu4nnqac29fvky6vhjnnhku5u2g9njss62rvy7e0yuperq86f5ek", + network=SettingsConstants.MAINNET, + sig_type=SettingsConstants.SINGLE_SIG, + script_type=SettingsConstants.NATIVE_SEGWIT, + derivation_path = "m/84h/0h/0h", + verified_index=5, + verified_index_is_change=False + ) + + + def PSBTSelectSeedView_cb_before(): + # Have to ensure this is cleared out in order to get the seed selection screen + controller.psbt_seed = None + + + def PSBTOverviewView_op_return_cb_before(): + controller.psbt_seed = seed_12b + decoder = DecodeQR() + decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT) + controller.psbt = decoder.get_psbt() + controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b) + + + def PSBTOpReturnView_raw_hex_data_cb_before(): + decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES) + controller.psbt = decoder.get_psbt() + controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b) + + screenshot_sections = { "Main Menu Views": [ - (OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=True)), - (OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=False), "OpeningSplashView_no_partner_logos"), - MainMenuView, - (MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_removed', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED)), - (MainMenuView, {}, 'MainMenuView_SDCardStateChangeToast_inserted', SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED)), - (MainMenuView, {}, 'MainMenuView_RemoveSDCardToast', RemoveSDCardToastManagerThread(activation_delay=0)), - PowerOptionsView, - RestartView, - PowerOffView, + ScreenshotConfig(OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=True)), + ScreenshotConfig(OpeningSplashView, dict(is_screenshot_renderer=True, force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos"), + ScreenshotConfig(MainMenuView), + ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_removed', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__REMOVED, activation_delay=0, duration=0)), + ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SDCardStateChangeToast_inserted', toast_thread=SDCardStateChangeToastManagerThread(action=MicroSD.ACTION__INSERTED, activation_delay=0, duration=0)), + ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_RemoveSDCardToast', toast_thread=RemoveSDCardToastManagerThread(activation_delay=0, duration=0)), + ScreenshotConfig(PowerOptionsView), + ScreenshotConfig(RestartView), + ScreenshotConfig(PowerOffView), ], "Seed Views": [ - seed_views.SeedsMenuView, - seed_views.LoadSeedView, - seed_views.SeedMnemonicEntryView, - seed_views.SeedMnemonicInvalidView, - seed_views.SeedFinalizeView, - (seed_views.SeedAddPassphraseView, {}, "SeedAddPassphraseView_lowercase"), - (seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__UPPERCASE_BUTTON_TEXT), "SeedAddPassphraseView_uppercase"), - (seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__DIGITS_BUTTON_TEXT), "SeedAddPassphraseView_digits"), - (seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_1_BUTTON_TEXT), "SeedAddPassphraseView_symbols_1"), - (seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_2_BUTTON_TEXT), "SeedAddPassphraseView_symbols_2"), - seed_views.SeedAddPassphraseExitDialogView, - seed_views.SeedReviewPassphraseView, + ScreenshotConfig(seed_views.SeedsMenuView), + ScreenshotConfig(seed_views.LoadSeedView), + ScreenshotConfig(seed_views.SeedMnemonicEntryView), + ScreenshotConfig(seed_views.SeedMnemonicInvalidView), + ScreenshotConfig(seed_views.SeedFinalizeView), + ScreenshotConfig(seed_views.SeedAddPassphraseView, screenshot_name="SeedAddPassphraseView_lowercase"), + ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__UPPERCASE_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_uppercase"), + ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__DIGITS_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_digits"), + ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_1_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_1"), + ScreenshotConfig(seed_views.SeedAddPassphraseView, dict(initial_keyboard=SeedAddPassphraseScreen.KEYBOARD__SYMBOLS_2_BUTTON_TEXT), screenshot_name="SeedAddPassphraseView_symbols_2"), + ScreenshotConfig(seed_views.SeedAddPassphraseExitDialogView), + ScreenshotConfig(seed_views.SeedReviewPassphraseView), - (seed_views.SeedOptionsView, dict(seed_num=0)), - (seed_views.SeedBackupView, dict(seed_num=0)), - (seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)), - (seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")), - (seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")), - (seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")), - (seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")), - (seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")), - #SeedExportXpubQRDisplayView, - (seed_views.SeedWordsWarningView, dict(seed_num=0)), - (seed_views.SeedWordsView, dict(seed_num=0)), - (seed_views.SeedWordsView, dict(seed_num=0, page_index=2), "SeedWordsView_2"), - (seed_views.SeedBIP85ApplicationModeView, dict(seed_num=0)), - (seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)), - (seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)), - (seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)), - (seed_views.SeedWordsBackupTestView, dict(seed_num=0, rand_seed=6102)), - (seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="satoshi")), - (seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)), - (seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)), - (seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)), - (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), "SeedTranscribeSeedQRWholeQRView_12_Compact"), - (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_12_Standard"), - (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), "SeedTranscribeSeedQRWholeQRView_24_Compact"), - (seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), "SeedTranscribeSeedQRWholeQRView_24_Standard"), - (seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_block_x=1, initial_block_y=1), "SeedTranscribeSeedQRZoomedInView_12_Compact"), - (seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), "SeedTranscribeSeedQRZoomedInView_12_Standard"), + ScreenshotConfig(seed_views.SeedOptionsView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedBackupView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed_num=0, sig_type="msig")), + ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed_num=0, sig_type="ss", script_type="")), + ScreenshotConfig(seed_views.SeedExportXpubCoordinatorView, dict(seed_num=0, sig_type="ss", script_type="nat")), + ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed_num=0, sig_type="msig", script_type="nes", coordinator="spd", custom_derivation="")), + ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed_num=0, sig_type="ss", script_type="nat", coordinator="bw", custom_derivation="")), + #ScreenshotConfig(SeedExportXpubQRDisplayView), + ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedWordsView, dict(seed_num=0, page_index=2), screenshot_name="SeedWordsView_2"), + ScreenshotConfig(seed_views.SeedBIP85ApplicationModeView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedBIP85SelectChildIndexView, dict(seed_num=0, num_words=24)), + ScreenshotConfig(seed_views.SeedBIP85InvalidChildIndexView, dict(seed_num=0, num_words=12)), + ScreenshotConfig(seed_views.SeedWordsBackupTestPromptView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedWordsBackupTestView, dict(seed_num=0, rand_seed=6102)), + ScreenshotConfig(seed_views.SeedWordsBackupTestMistakeView, dict(seed_num=0, cur_index=7, wrong_word="satoshi")), + ScreenshotConfig(seed_views.SeedWordsBackupTestSuccessView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRFormatView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRWarningView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Compact"), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Standard"), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Compact"), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed_num=2, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Standard"), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_block_x=1, initial_block_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed_num=0, seedqr_format=QRType.SEED__SEEDQR, initial_block_x=2, initial_block_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"), - (seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed_num=0)), # Screenshot can't render live preview screens - # (seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)), + # ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed_num=0)), - seed_views.SeedSelectSeedView, - seed_views.AddressVerificationSigTypeView, - (seed_views.SeedAddressVerificationView, dict(seed_num=0)), - (seed_views.SeedAddressVerificationSuccessView, dict(seed_num=0)), + ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__VERIFY_SINGLESIG_ADDR), screenshot_name="SeedSelectSeedView_address_verification"), + ScreenshotConfig(seed_views.AddressVerificationSigTypeView), + ScreenshotConfig(seed_views.SeedAddressVerificationView, dict(seed_num=0), run_before=load_address_verification_data_cb), + ScreenshotConfig(seed_views.SeedAddressVerificationSuccessView, dict(seed_num=0)), # Relies on callback above - seed_views.LoadMultisigWalletDescriptorView, - seed_views.MultisigWalletDescriptorView, - (seed_views.SeedDiscardView, dict(seed_num=0)), + ScreenshotConfig(seed_views.LoadMultisigWalletDescriptorView), + ScreenshotConfig(seed_views.MultisigWalletDescriptorView, run_before=load_multisig_wallet_descriptor_cb), + ScreenshotConfig(seed_views.SeedDiscardView, dict(seed_num=0)), - seed_views.SeedSignMessageConfirmMessageView, - seed_views.SeedSignMessageConfirmAddressView, + ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__SIGN_MESSAGE), screenshot_name="SeedSelectSeedView_sign_message"), + ScreenshotConfig(seed_views.SeedSignMessageConfirmMessageView), + ScreenshotConfig(seed_views.SeedSignMessageConfirmAddressView), - seed_views.SeedElectrumMnemonicStartView, + ScreenshotConfig(seed_views.SeedElectrumMnemonicStartView), ], "PSBT Views": [ - psbt_views.PSBTSelectSeedView, # this will fail, be rerun below - psbt_views.PSBTOverviewView, - psbt_views.PSBTUnsupportedScriptTypeWarningView, - psbt_views.PSBTNoChangeWarningView, - psbt_views.PSBTMathView, - (psbt_views.PSBTAddressDetailsView, dict(address_num=0)), + ScreenshotConfig(psbt_views.PSBTSelectSeedView, run_before=PSBTSelectSeedView_cb_before), + ScreenshotConfig(psbt_views.PSBTOverviewView, run_before=load_basic_psbt_cb), + ScreenshotConfig(psbt_views.PSBTUnsupportedScriptTypeWarningView), + ScreenshotConfig(psbt_views.PSBTNoChangeWarningView), + ScreenshotConfig(psbt_views.PSBTMathView), + ScreenshotConfig(psbt_views.PSBTAddressDetailsView, dict(address_num=0)), - (NotYetImplementedView, {}, "PSBTChangeDetailsView_multisig_unverified"), # Must manually re-run this below - (psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), "PSBTChangeDetailsView_multisig_verified"), - (NotYetImplementedView, {}, "PSBTOverviewView_op_return"), # Placeholder - (NotYetImplementedView, {}, "PSBTOpReturnView_text"), # Placeholder - (NotYetImplementedView, {}, "PSBTOpReturnView_raw_hex_data"), # Placeholder - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_change"), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), "PSBTAddressVerificationFailedView_singlesig_selftransfer"), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_change"), - (psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), "PSBTAddressVerificationFailedView_multisig_selftransfer"), - psbt_views.PSBTFinalizeView, - #PSBTSignedQRDisplayView - psbt_views.PSBTSigningErrorView, + ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_unverified", run_before=load_basic_psbt_cb), + ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_verified", run_before=load_multisig_wallet_descriptor_cb), + ScreenshotConfig(psbt_views.PSBTOverviewView, screenshot_name="PSBTOverviewView_op_return", run_before=PSBTOverviewView_op_return_cb_before), + ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_text"), # Relies on callback above + ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_raw_hex_data", run_before=PSBTOpReturnView_raw_hex_data_cb_before), + ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=False), screenshot_name="PSBTAddressVerificationFailedView_singlesig_change"), + ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=False), screenshot_name="PSBTAddressVerificationFailedView_singlesig_selftransfer"), + ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=True, is_multisig=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_change"), + ScreenshotConfig(psbt_views.PSBTAddressVerificationFailedView, dict(is_change=False, is_multisig=True), screenshot_name="PSBTAddressVerificationFailedView_multisig_selftransfer"), + ScreenshotConfig(psbt_views.PSBTFinalizeView), + #ScreenshotConfig(PSBTSignedQRDisplayViewScreenshotConfig), + ScreenshotConfig(psbt_views.PSBTSigningErrorView), ], "Tools Views": [ - tools_views.ToolsMenuView, - #ToolsImageEntropyLivePreviewView - #ToolsImageEntropyFinalImageView - tools_views.ToolsImageEntropyMnemonicLengthView, - tools_views.ToolsDiceEntropyMnemonicLengthView, - (tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)), - tools_views.ToolsCalcFinalWordNumWordsView, - tools_views.ToolsCalcFinalWordFinalizePromptView, - tools_views.ToolsCalcFinalWordCoinFlipsView, - (tools_views.ToolsCalcFinalWordShowFinalWordView, {}, "ToolsCalcFinalWordShowFinalWordView_pick_word"), - (tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), "ToolsCalcFinalWordShowFinalWordView_coin_flips"), - tools_views.ToolsCalcFinalWordDoneView, - tools_views.ToolsAddressExplorerSelectSourceView, - tools_views.ToolsAddressExplorerAddressTypeView, - tools_views.ToolsAddressExplorerAddressListView, - # tools_views.ToolsAddressExplorerAddressView, + ScreenshotConfig(tools_views.ToolsMenuView), + #ScreenshotConfig(ToolsImageEntropyLivePreviewView), + #ScreenshotConfig(ToolsImageEntropyFinalImageView), + ScreenshotConfig(tools_views.ToolsImageEntropyMnemonicLengthView), + ScreenshotConfig(tools_views.ToolsDiceEntropyMnemonicLengthView), + ScreenshotConfig(tools_views.ToolsDiceEntropyEntryView, dict(total_rolls=50)), + ScreenshotConfig(tools_views.ToolsCalcFinalWordNumWordsView), + ScreenshotConfig(tools_views.ToolsCalcFinalWordFinalizePromptView), + ScreenshotConfig(tools_views.ToolsCalcFinalWordCoinFlipsView), + ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, screenshot_name="ToolsCalcFinalWordShowFinalWordView_pick_word"), + ScreenshotConfig(tools_views.ToolsCalcFinalWordShowFinalWordView, dict(coin_flips="0010101"), screenshot_name="ToolsCalcFinalWordShowFinalWordView_coin_flips"), + ScreenshotConfig(tools_views.ToolsCalcFinalWordDoneView), + ScreenshotConfig(tools_views.ToolsAddressExplorerSelectSourceView), + ScreenshotConfig(tools_views.ToolsAddressExplorerAddressTypeView), + ScreenshotConfig(tools_views.ToolsAddressExplorerAddressListView), + # ScreenshotConfig(tools_views.ToolsAddressExplorerAddressView), ], "Settings Views": settings_views_list + [ - settings_views.IOTestView, - settings_views.DonateView, - (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), "SettingsIngestSettingsQRView_persistent"), - (settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), "SettingsIngestSettingsQRView_not_persistent"), + ScreenshotConfig(settings_views.IOTestView), + ScreenshotConfig(settings_views.DonateView), + ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_persistent), screenshot_name="SettingsIngestSettingsQRView_persistent"), + ScreenshotConfig(settings_views.SettingsIngestSettingsQRView, dict(data=settingsqr_data_not_persistent), screenshot_name="SettingsIngestSettingsQRView_not_persistent"), ], "Misc Error Views": [ - NotYetImplementedView, - (UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])), - (NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")), - (OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), - (ErrorView, dict( + ScreenshotConfig(NotYetImplementedView), + ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])), + ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")), + ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)), + ScreenshotConfig(ErrorView, dict( title="Error", status_headline="Unknown QR Type", text="QRCode is invalid or is a data format not yet supported.", @@ -326,15 +368,22 @@ def generate_screenshots(locale): return screenshot_sections - def screencap_view(view_cls: View, view_args: dict = {}, view_name: str = None, toast_thread: BaseToastOverlayManagerThread = None): - if not view_name: - view_name = view_cls.__name__ - screenshot_renderer.set_screenshot_filename(f"{view_name}.png") + def screencap_view(screenshot_config: ScreenshotConfig): + # Block until we have exclusive access to the screenshot renderer. Without this + # we were occasionally running into confusing race conditions where the next + # screenshot would begin rendering over the previous one. Claiming the lock + # guarantees that the previous screenshot has been fully rendered and saved. + screenshot_renderer.lock.acquire() + screenshot_renderer.set_screenshot_filename(f"{screenshot_config.screenshot_name}.png") + screenshot_renderer.lock.release() + controller = Controller.get_instance() + toast_thread = screenshot_config.toast_thread try: - print(f"Running {view_name}") + print(f"Running {screenshot_config.screenshot_name}") try: - view_cls(**view_args).run() + screenshot_config.run_callback_before() + screenshot_config.View_cls(**screenshot_config.view_kwargs).run() except ScreenshotComplete: # The target View has run and its Screen has rendered what it needs to if toast_thread is not None: @@ -342,20 +391,29 @@ def generate_screenshots(locale): controller.activate_toast(toast_thread) while controller.toast_notification_thread.is_alive(): # Give the Toast a moment to complete its work + time.sleep(0.01) + + # TODO: Necessary now that the lock is in place? + # Whenever possible, clean up toast thread HERE before killing the + # main thread with ScreenshotComplete. + toast_thread.stop() + toast_thread.join() raise ScreenshotComplete() except ScreenshotComplete: # Slightly hacky way to exit ScreenshotRenderer as expected - print(f"Completed {view_name}") + print(f"Completed {screenshot_config.screenshot_name}") except Exception as e: # Something else went wrong from traceback import print_exc print_exc() raise e finally: - if toast_thread: + if toast_thread and toast_thread.is_alive(): toast_thread.stop() - time.sleep(0.1) #jdlcdl + toast_thread.join() + + screenshot_config.run_callback_after() # Parse the main `l10n/messages.pot` for overall stats @@ -370,7 +428,6 @@ def generate_screenshots(locale): locale, display_name = locale_tuple_list[0] Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale) - screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale)) locale_readme = f"""# SeedSigner Screenshots: {display_name}\n""" @@ -396,57 +453,14 @@ def generate_screenshots(locale): locale_readme += f"## {section_name}\n\n" locale_readme += """""" locale_readme += f"""
""" - for screenshot in screenshot_list: - if type(screenshot) == tuple: - toast_thread = None - if len(screenshot) == 2: - view_cls, view_args = screenshot - view_name = view_cls.__name__ - elif len(screenshot) == 3: - view_cls, view_args, view_name = screenshot - elif len(screenshot) == 4: - view_cls, view_args, view_name, toast_thread = screenshot - else: - view_cls = screenshot - view_args = {} - view_name = view_cls.__name__ - toast_thread = None - - screencap_view(view_cls, view_args, view_name, toast_thread=toast_thread) + for screenshot_config in screenshot_list: + screencap_view(screenshot_config) locale_readme += """ """ - locale_readme += f"""""" + locale_readme += f"""""" locale_readme += """
{view_name}

{screenshot_config.screenshot_name}

\n""" locale_readme += "
" - # many screens don't work, leaving a missing image, re-run here for now - controller.psbt_seed = None - screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, "psbt_views")) - - decoder = DecodeQR() - decoder.add_data(BASE64_PSBT_1) - controller.psbt = decoder.get_psbt() - controller.psbt_seed = seed_12b - controller.multisig_wallet_descriptor = None - screencap_view(psbt_views.PSBTChangeDetailsView, view_name='PSBTChangeDetailsView_multisig_unverified', view_args=dict(change_address_num=0)) - - controller.psbt_seed = None - screencap_view(psbt_views.PSBTSelectSeedView, view_name='PSBTSelectSeedView') - - # Render OP_RETURN screens for real - controller.psbt_seed = seed_12b - decoder = DecodeQR() - decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_TEXT) - controller.psbt = decoder.get_psbt() - controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b) - screencap_view(psbt_views.PSBTOverviewView, view_name='PSBTOverviewView_op_return') - screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_text") - - decoder.add_data(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES) - controller.psbt = decoder.get_psbt() - controller.psbt_parser = PSBTParser(p=controller.psbt, seed=seed_12b) - screencap_view(psbt_views.PSBTOpReturnView, view_name="PSBTOpReturnView_raw_hex_data") - with open(os.path.join(screenshot_root, locale, "README.md"), 'w') as readme_file: readme_file.write(locale_readme) diff --git a/tests/screenshot_generator/utils.py b/tests/screenshot_generator/utils.py index 26d232a4..d767bdf2 100644 --- a/tests/screenshot_generator/utils.py +++ b/tests/screenshot_generator/utils.py @@ -1,12 +1,19 @@ import os + +from dataclasses import dataclass from PIL import Image, ImageDraw + from seedsigner.gui.renderer import Renderer +from seedsigner.gui.toast import BaseToastOverlayManagerThread +from seedsigner.views.view import View + class ScreenshotComplete(Exception): pass + class ScreenshotRenderer(Renderer): screenshot_path: str = None screenshot_filename: str = None @@ -35,7 +42,7 @@ class ScreenshotRenderer(Renderer): self.screenshot_path = path - def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False): + def show_image(self, image=None, alpha_overlay=None, is_background_thread: bool = False): if is_background_thread: return @@ -51,3 +58,30 @@ class ScreenshotRenderer(Renderer): self.canvas.save(os.path.join(self.screenshot_path, self.screenshot_filename)) raise ScreenshotComplete() + + +@dataclass +class ScreenshotConfig: + View_cls: View + view_kwargs: dict = None + screenshot_name: str = None + toast_thread: BaseToastOverlayManagerThread = None + run_before: callable = None + run_after: callable = None + + + def __post_init__(self): + if not self.view_kwargs: + self.view_kwargs = {} + if not self.screenshot_name: + self.screenshot_name = self.View_cls.__name__ + + + def run_callback_before(self): + if self.run_before: + self.run_before() + + + def run_callback_after(self): + if self.run_after: + self.run_after()