mirror of
https://github.com/SeedSigner/seedsigner.git
synced 2026-10-05 15:08:25 +00:00
The screenshot generator fetches the latest release tag at import time via an unauthenticated GitHub API call. GitHub-hosted runners share outbound IPs and the unauthenticated api.github.com limit is 60 req/hr/IP, so the call was intermittently rate-limited (HTTP 403), returned (None, None), and fed None into the mock_version_to_most_recent_release Version mock. That crashed rendering for every locale with "TypeError: 'NoneType' object is not subscriptable" in the OpeningSplash version string (and would likewise hit len(None) on the Version settings screen). Substitute a placeholder version name and timestamp when the fetch can't resolve the latest release, so the "release build" screenshots still render instead of feeding None into the mock. The real fetch is unchanged when it succeeds.
609 lines
40 KiB
Python
609 lines
40 KiB
Python
import embit
|
|
import os
|
|
import pathlib
|
|
import pytest
|
|
import random
|
|
import sys
|
|
import time
|
|
from contextlib import contextmanager
|
|
from dataclasses import dataclass
|
|
from datetime import datetime
|
|
from PIL import ImageFont
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
|
|
from embit import compact
|
|
from embit.psbt import PSBT, OutputScope
|
|
from embit.script import Script
|
|
|
|
from seedsigner.helpers.version import Version, VersionUtils
|
|
|
|
# Prevent importing modules w/Raspi hardware dependencies.
|
|
# These must precede any SeedSigner imports.
|
|
sys.modules['seedsigner.hardware.displays.st7789_mpy'] = MagicMock()
|
|
sys.modules['seedsigner.hardware.displays.ili9341'] = MagicMock()
|
|
sys.modules['seedsigner.views.screensaver.ScreensaverScreen'] = MagicMock()
|
|
sys.modules['RPi'] = MagicMock()
|
|
sys.modules['RPi.GPIO'] = MagicMock()
|
|
sys.modules['seedsigner.hardware.camera.Camera'] = MagicMock()
|
|
sys.modules['seedsigner.hardware.microsd'] = MagicMock()
|
|
|
|
from seedsigner.controller import Controller
|
|
from seedsigner.gui.renderer import Renderer
|
|
from seedsigner.gui.screens.screen import BaseScreen
|
|
from seedsigner.gui.screens.seed_screens import SeedAddPassphraseScreen
|
|
from seedsigner.gui.toast import RemoveSDCardToastManagerThread, SDCardStateChangeToastManagerThread
|
|
from seedsigner.gui.toast import DefaultToast, InfoToast, SuccessToast, WarningToast, ErrorToast, DireWarningToast
|
|
from seedsigner.hardware.microsd import MicroSD
|
|
from seedsigner.helpers import embit_utils
|
|
from seedsigner.models.decode_qr import DecodeQR
|
|
from seedsigner.models.encode_qr import BaseQrEncoder
|
|
from seedsigner.models.psbt_parser import OPCODES, PSBTParser
|
|
from seedsigner.models.qr_type import QRType
|
|
from seedsigner.models.seed import Seed
|
|
from seedsigner.models.settings import Settings
|
|
from seedsigner.models.settings_definition import SettingsConstants, SettingsDefinition
|
|
from seedsigner.views import (MainMenuView, PowerOptionsView, RestartView, RemoveMicroSDWarningView, NotYetImplementedView, UnhandledExceptionView,
|
|
psbt_views, seed_views, settings_views, tools_views, scan_views)
|
|
from seedsigner.views.screensaver import OpeningSplashView
|
|
from seedsigner.views.view import CameraConnectionErrorView, NetworkMismatchErrorView, OptionDisabledView, PowerOffView
|
|
|
|
from .utils import ScreenshotComplete, ScreenshotConfig, ScreenshotRenderer
|
|
|
|
import warnings; warnings.warn = lambda *args, **kwargs: None
|
|
|
|
# Dynamically generate a pytest test run for each locale
|
|
@pytest.mark.parametrize("locale", [x for x, y in SettingsConstants.get_detected_languages()])
|
|
def test_generate_all(locale, target_locale):
|
|
"""
|
|
`target_locale` is a fixture created in conftest.py via the `--locale` command line arg.
|
|
|
|
Optionally skips all other locales.
|
|
"""
|
|
if target_locale and locale != target_locale:
|
|
pytest.skip(f"Skipping {locale}")
|
|
|
|
if not ImageFont.core.HAVE_RAQM:
|
|
# We can't generate pixel-perfect screenshots that match what gets rendered on
|
|
# the device if we don't have libraqm.
|
|
pytest.fail("libraqm is not installed.")
|
|
|
|
generate_screenshots(locale)
|
|
|
|
|
|
|
|
"""**************************************************************************************
|
|
Set up global test data that will be re-used across a variety of screenshots and for
|
|
all locales.
|
|
**************************************************************************************"""
|
|
# Single sig ("abandon" test wallet) tx; 1mil sat input, 1 external output, 1 self-transfer, 1 change output, 400 sat fee
|
|
BASE64_SINGLE_SIG_PSBT = """cHNidP8BAJACAAAAAT8SmJzLhTMNgtn9QOmBmet0nnqqIJpsgpgBN5JWNJCxAQAAAAD9////A5CfBwAAAAAAFgAULzSqHPAKU7BVopGgOn1F8KaYi1KQ0AMAAAAAABYAFGQh2ztS8DzX4kGVKUKQhFPrlNNIkNADAAAAAAAWABRvoBZQCjxqc367Jg4t3KeLqSNFWGYAAABPAQQ1h88DDvSxr4AAAAA8jCA37kwWIdoNNI21EWNwmmItDSg43ebYQZxR9jAcYgO4jg++P2RjN+2TvAwPO4Q/z30lieXsiEdU5kAgJ6iQtBBzxdoKVAAAgAEAAIAAAACAAAEAcQIAAAABF84F9MpvLC1H3Cyews1xoNZ4ch3uJMu8jonehCIqmScAAAAAAP3///8CM6/2KQEAAAAWABQQumvlzzcWsGXBNIOliqXTvr9YxEBCDwAAAAAAFgAU0MSj7wnpl7bpnjl+UY/j5BoRjKFNAAAAAQEfQEIPAAAAAAAWABTQxKPvCemXtumeOX5Rj+PkGhGMoQEDBAEAAAAiBgLnqyU3tdSelwMJquBunknzbOHJ/rvUTsjg0cygtPnDGRhzxdoKVAAAgAEAAIAAAACAAAAAAAAAAAAAIgIDXUnszVTQCZ5DZ2J3x6bUYl1hHaiKXfSb+VF6d5Gnd6UYc8XaClQAAIABAACAAAAAgAEAAAAAAAAAAAAiAgPu7SBaaQIv7UpioCRX82mbGcBr90v4AazG2a6EvBap4RhzxdoKVAAAgAEAAIAAAACAAAAAAAEAAAAA"""
|
|
|
|
BASE64_MULTISIG_PSBT = """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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_MULTISIG_PSBT)
|
|
|
|
# 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_MULTISIG_PSBT), 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"""
|
|
|
|
# Grab the most recent release version info for the "release build" splash screenshots.
|
|
(latest_release_version_name, latest_release_version_timestamp) = VersionUtils._fetch_latest_seedsigner_release_tag()
|
|
if not latest_release_version_name or not latest_release_version_timestamp:
|
|
# The fetch can fail (offline, or the unauthenticated GitHub API rate-limit that
|
|
# shared CI runners regularly hit). Substitute a placeholder so the "release build"
|
|
# screenshots still render, rather than feeding None into the Version mock (which
|
|
# would otherwise crash both the splash screen and the Version settings screen on
|
|
# None slicing / len()).
|
|
print("Could not fetch latest release version from GitHub; using a placeholder")
|
|
latest_release_version_name = latest_release_version_name or "version_not_available"
|
|
latest_release_version_timestamp = latest_release_version_timestamp or datetime(1970, 1, 1)
|
|
|
|
|
|
# Wrap QRDisplayScreen's `render_brightness_tip` in a simple View + Screen so we
|
|
# can call it outside of its child thread and generate a screenshot.
|
|
class SeedExportXpubQR_ScreenBrightnessView(seed_views.SeedExportXpubQRDisplayView):
|
|
@dataclass
|
|
class QRDisplayScreenBrightnessTipScreen(BaseScreen):
|
|
qr_encoder: BaseQrEncoder = None
|
|
|
|
def _render(self):
|
|
from seedsigner.gui.screens.screen import QRDisplayScreen
|
|
image = self.qr_encoder.part_to_image(self.qr_encoder.cur_part(), 240, 240, border=2, background_color="white")
|
|
QRDisplayScreen.QRDisplayThread.render_brightness_tip(None, image)
|
|
self.renderer.show_image(image)
|
|
|
|
def run(self):
|
|
self.run_screen(
|
|
SeedExportXpubQR_ScreenBrightnessView.QRDisplayScreenBrightnessTipScreen,
|
|
qr_encoder=self.qr_encoder, # initialized by SeedExportXpubQRDisplayView
|
|
)
|
|
|
|
|
|
|
|
def generate_screenshots(locale):
|
|
"""
|
|
The `Renderer` class is mocked so that calls in the normal code are ignored
|
|
(necessary to avoid having it trying to wire up hardware dependencies).
|
|
|
|
When the `Renderer` instance is needed, we patch in our own test-only
|
|
`ScreenshotRenderer`.
|
|
"""
|
|
# Prep the ScreenshotRenderer that will be patched over the normal Renderer
|
|
screenshot_root = os.path.join(os.getcwd(), "seedsigner-screenshots")
|
|
ScreenshotRenderer.configure_instance()
|
|
screenshot_renderer: ScreenshotRenderer = ScreenshotRenderer.get_instance()
|
|
|
|
# Replace the core `Singleton` calls so that only our ScreenshotRenderer is used.
|
|
Renderer.configure_instance = Mock()
|
|
Renderer.get_instance = Mock(return_value=screenshot_renderer)
|
|
|
|
|
|
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])
|
|
|
|
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)
|
|
|
|
# Pending mnemonic for ToolsCalcFinalWordShowFinalWordView
|
|
controller.storage.init_pending_mnemonic(num_words=12)
|
|
for i, word in enumerate(mnemonic_12[:11]):
|
|
controller.storage.update_pending_mnemonic(word=word, index=i)
|
|
controller.storage.update_pending_mnemonic(word="satoshi", index=11) # random last word; not supposed to be a valid checksum (yet)
|
|
|
|
# Load a PSBT into memory
|
|
decoder = DecodeQR()
|
|
decoder.add_data(BASE64_MULTISIG_PSBT)
|
|
controller.psbt = decoder.get_psbt()
|
|
controller.psbt_seed = seed_12b
|
|
|
|
# Message signing data
|
|
derivation_path = "m/84h/0h/0h/0/0"
|
|
controller.sign_message_data = {
|
|
"seed": seed_12,
|
|
"derivation_path": derivation_path,
|
|
"message": "I attest that I control this bitcoin address blah blah blah",
|
|
"addr_format": embit_utils.parse_derivation_path(derivation_path)
|
|
}
|
|
|
|
# so we get a choice for transcribe seed qr format
|
|
controller.settings.set_value(
|
|
attr_name=SettingsConstants.SETTING__COMPACT_SEEDQR,
|
|
value=SettingsConstants.OPTION__ENABLED
|
|
)
|
|
|
|
# Automatically populate all Settings options Views
|
|
settings_views_list = []
|
|
def add_settings_entries(visibility = SettingsConstants.VISIBILITY__GENERAL):
|
|
for settings_entry in SettingsDefinition.settings_entries:
|
|
if settings_entry.visibility != visibility:
|
|
continue
|
|
|
|
if settings_entry.attr_name == SettingsConstants.SETTING__LOCALE:
|
|
# Locale selection has its own dedicated View
|
|
settings_views_list.append(ScreenshotConfig(settings_views.LocaleSelectionView))
|
|
else:
|
|
# Generic SettingsEntry selection View
|
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsEntryUpdateSelectionView, dict(attr_name=settings_entry.attr_name), screenshot_name=f"SettingsEntryUpdateSelectionView_{settings_entry.attr_name}"))
|
|
|
|
# Add the top level "General" settings menu and entries
|
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView))
|
|
|
|
# Scroll the general settings to the bottom
|
|
settings_views_list.append(ScreenshotConfig(settings_views.SettingsMenuView, dict(selected_button_option=settings_views.SettingsMenuView.VERSION), screenshot_name="SettingsMenuView_2"))
|
|
add_settings_entries(SettingsConstants.VISIBILITY__GENERAL)
|
|
|
|
# Add the "Advanced" menu...
|
|
settings_views_list.append(
|
|
ScreenshotConfig(
|
|
settings_views.SettingsMenuView,
|
|
dict(
|
|
visibility=SettingsConstants.VISIBILITY__ADVANCED,
|
|
),
|
|
screenshot_name="SettingsMenuView__Advanced"
|
|
)
|
|
)
|
|
|
|
# ...and Advanced entries
|
|
add_settings_entries(SettingsConstants.VISIBILITY__ADVANCED)
|
|
|
|
# Render the nested "Advanced" -> "Hardware" submenu
|
|
settings_views_list.append(
|
|
ScreenshotConfig(
|
|
settings_views.SettingsMenuView,
|
|
dict(visibility=SettingsConstants.VISIBILITY__HARDWARE),
|
|
screenshot_name="SettingsMenuView__Hardware"
|
|
)
|
|
)
|
|
add_settings_entries(SettingsConstants.VISIBILITY__HARDWARE)
|
|
|
|
settingsqr_data_persistent = f"settings::v1 name=English_noob_mode persistent=E xpub_qr=urca,sta denom=thr network=M qr_density=M 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 xpub_qr=urca,sta denom=thr network=M qr_density=M 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 mocks to provide whatever temporary data/state a particular screenshot
|
|
# might need.
|
|
@contextmanager
|
|
def mock_load_psbt(base64_psbt: str, seed: Seed = seed_12b):
|
|
"""
|
|
Reusable utility for other context managers to mock loading a PSBT into the
|
|
Controller.
|
|
"""
|
|
decoder = DecodeQR()
|
|
decoder.add_data(base64_psbt)
|
|
with patch.object(controller, 'psbt', decoder.get_psbt()):
|
|
with patch.object(controller, 'psbt_seed', seed):
|
|
with patch.object(controller, 'psbt_parser', PSBTParser(p=controller.psbt, seed=seed)):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_single_sig_psbt_loaded():
|
|
with mock_load_psbt(BASE64_SINGLE_SIG_PSBT):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_multisig_psbt_loaded():
|
|
with mock_load_psbt(BASE64_MULTISIG_PSBT):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_multisig_wallet_descriptor_loaded():
|
|
with patch.object(controller, 'multisig_wallet_descriptor', embit.descriptor.Descriptor.from_string(MULTISIG_WALLET_DESCRIPTOR)):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_multisig_psbt_and_descriptor_loaded():
|
|
with mock_multisig_psbt_loaded():
|
|
with mock_multisig_wallet_descriptor_loaded():
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_address_verification_data_loaded():
|
|
fake_addr_verification_data = 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
|
|
)
|
|
with patch.object(controller, 'unverified_address', fake_addr_verification_data):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_controller_psbt_seed_empty():
|
|
# Have to ensure this is cleared out in order to get the seed selection screen
|
|
with patch.object(controller, 'psbt_seed', None):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_psbt_with_op_return_loaded():
|
|
with mock_load_psbt(BASE64_PSBT_WITH_OP_RETURN_TEXT):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_psbt_with_op_return_raw_bytes_loaded():
|
|
with mock_load_psbt(BASE64_PSBT_WITH_OP_RETURN_RAW_BYTES):
|
|
yield
|
|
|
|
|
|
@contextmanager
|
|
def mock_version_to_most_recent_release():
|
|
# Patch the Version get_* calls to the most recent release
|
|
with patch.multiple(Version,
|
|
get_version_name=Mock(return_value=latest_release_version_name),
|
|
get_version_fork=Mock(return_value="SeedSigner"),
|
|
get_version_timestamp=Mock(return_value=latest_release_version_timestamp),
|
|
get_short_commit_hash=Mock(return_value="abcd1234") # dummy value should be ignored
|
|
):
|
|
# Also have to mock us into SeedSigner OS
|
|
with patch("seedsigner.models.settings.Settings.HOSTNAME", Settings.SEEDSIGNER_OS):
|
|
yield
|
|
|
|
|
|
screenshot_sections = {
|
|
"Main Menu Views": [
|
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), mock_context_manager=mock_version_to_most_recent_release),
|
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=False), screenshot_name="OpeningSplashView_no_partner_logos", mock_context_manager=mock_version_to_most_recent_release),
|
|
ScreenshotConfig(OpeningSplashView, dict(force_partner_logos=True), screenshot_name="OpeningSplashView_current_git_state"),
|
|
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(RemoveMicroSDWarningView),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DefaultToast', toast_thread=DefaultToast("This is a default text toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_InfoToast', toast_thread=InfoToast("This is an info toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_SuccessToast', toast_thread=SuccessToast("This is a success toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_WarningToast', toast_thread=WarningToast("This is a warning toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_DireWarningToast', toast_thread=DireWarningToast("This is a dire warning toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(MainMenuView, screenshot_name='MainMenuView_ErrorToast', toast_thread=ErrorToast("This is an error toast!", activation_delay=0, duration=0)),
|
|
ScreenshotConfig(PowerOptionsView),
|
|
ScreenshotConfig(RestartView),
|
|
ScreenshotConfig(PowerOffView),
|
|
],
|
|
"Seed Views": [
|
|
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),
|
|
|
|
ScreenshotConfig(seed_views.SeedOptionsView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedBackupView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedExportXpubSigTypeView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedExportXpubScriptTypeView, dict(seed=seed_12, sig_type="msig")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubCustomDerivationView, dict(seed=seed_12, sig_type="ss", script_type="")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubQRFormatView, dict(seed=seed_12, sig_type="ss", script_type="nat")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubWarningView, dict(seed=seed_12, sig_type="msig", script_type="nes", xpub_qr_format="urca", custom_derivation="")),
|
|
ScreenshotConfig(seed_views.SeedExportXpubDetailsView, dict(seed=seed_12, sig_type="ss", script_type="nat", xpub_qr_format="urca", custom_derivation="")),
|
|
ScreenshotConfig(SeedExportXpubQR_ScreenBrightnessView, dict(seed=seed_12, xpub_qr_format="urca", derivation_path="m/84'/0'/0'")),
|
|
|
|
ScreenshotConfig(seed_views.SeedWordsWarningView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedWordsView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedWordsView, dict(seed=seed_12, page_index=2), screenshot_name="SeedWordsView_2"),
|
|
ScreenshotConfig(seed_views.SeedBIP85SelectNumWordsView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedBIP85SelectChildIndexView, dict(seed=seed_12, num_words=24)),
|
|
ScreenshotConfig(seed_views.SeedBIP85InvalidChildIndexView, dict(seed=seed_12, num_words=12)),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestPromptView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestView, dict(seed=seed_12, rand_seed=6102)),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestMistakeView, dict(seed=seed_12, cur_index=7, wrong_word="satoshi")),
|
|
ScreenshotConfig(seed_views.SeedWordsBackupTestSuccessView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRFormatView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWarningView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_12, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=21), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Compact"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_12, seedqr_format=QRType.SEED__SEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_12_Standard"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_24, seedqr_format=QRType.SEED__COMPACTSEEDQR, num_modules=25), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Compact"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRWholeQRView, dict(seed=seed_24, seedqr_format=QRType.SEED__SEEDQR, num_modules=29), screenshot_name="SeedTranscribeSeedQRWholeQRView_24_Standard"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed=seed_12, seedqr_format=QRType.SEED__COMPACTSEEDQR, initial_zone_x=1, initial_zone_y=1), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Compact"),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRZoomedInView, dict(seed=seed_12, seedqr_format=QRType.SEED__SEEDQR, initial_zone_x=2, initial_zone_y=2), screenshot_name="SeedTranscribeSeedQRZoomedInView_12_Standard"),
|
|
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmQRPromptView, dict(seed=seed_12)),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmWrongSeedView),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmInvalidQRView),
|
|
ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmSuccessView, dict(seed=seed_12)),
|
|
|
|
# Screenshot can't render live preview screens
|
|
# ScreenshotConfig(seed_views.SeedTranscribeSeedQRConfirmScanView, dict(seed=seed_12)),
|
|
|
|
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=seed_12), mock_context_manager=mock_address_verification_data_loaded),
|
|
ScreenshotConfig(seed_views.SeedAddressVerificationSuccessView, mock_context_manager=mock_address_verification_data_loaded),
|
|
|
|
ScreenshotConfig(seed_views.LoadMultisigWalletDescriptorView),
|
|
ScreenshotConfig(seed_views.MultisigWalletDescriptorView, mock_context_manager=mock_multisig_wallet_descriptor_loaded),
|
|
ScreenshotConfig(seed_views.SeedDiscardView, dict(seed=seed_12)),
|
|
|
|
ScreenshotConfig(seed_views.SeedSelectSeedView, dict(flow=Controller.FLOW__SIGN_MESSAGE), screenshot_name="SeedSelectSeedView_sign_message"),
|
|
ScreenshotConfig(seed_views.SeedSignMessageConfirmMessageView),
|
|
ScreenshotConfig(seed_views.SeedSignMessageConfirmAddressView),
|
|
|
|
ScreenshotConfig(seed_views.SeedElectrumMnemonicStartView),
|
|
],
|
|
"PSBT Views": [
|
|
ScreenshotConfig(psbt_views.PSBTSelectSeedView, mock_context_manager=mock_controller_psbt_seed_empty),
|
|
ScreenshotConfig(psbt_views.PSBTOverviewView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTUnsupportedScriptTypeWarningView),
|
|
ScreenshotConfig(psbt_views.PSBTNoChangeWarningView),
|
|
ScreenshotConfig(psbt_views.PSBTMathView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTAddressDetailsView, dict(address_num=0), mock_context_manager=mock_multisig_psbt_loaded),
|
|
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_single_sig_change_verified", mock_context_manager=mock_single_sig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=1), screenshot_name="PSBTChangeDetailsView_single_sig_self_transfer_verified", mock_context_manager=mock_single_sig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_unverified", mock_context_manager=mock_multisig_psbt_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTChangeDetailsView, dict(change_address_num=0), screenshot_name="PSBTChangeDetailsView_multisig_verified", mock_context_manager=mock_multisig_psbt_and_descriptor_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTOverviewView, screenshot_name="PSBTOverviewView_op_return", mock_context_manager=mock_psbt_with_op_return_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_text", mock_context_manager=mock_psbt_with_op_return_loaded),
|
|
ScreenshotConfig(psbt_views.PSBTOpReturnView, screenshot_name="PSBTOpReturnView_raw_hex_data", mock_context_manager=mock_psbt_with_op_return_raw_bytes_loaded),
|
|
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, mock_context_manager=mock_multisig_psbt_loaded),
|
|
#ScreenshotConfig(PSBTSignedQRDisplayViewScreenshotConfig),
|
|
ScreenshotConfig(psbt_views.PSBTSigningErrorView, mock_context_manager=mock_multisig_psbt_loaded),
|
|
],
|
|
"Tools Views": [
|
|
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, mock_context_manager=mock_multisig_wallet_descriptor_loaded),
|
|
ScreenshotConfig(tools_views.ToolsAddressExplorerAddressListView),
|
|
# ScreenshotConfig(tools_views.ToolsAddressExplorerAddressView),
|
|
],
|
|
"Settings Views": settings_views_list + [
|
|
ScreenshotConfig(settings_views.IOTestView),
|
|
ScreenshotConfig(settings_views.DonateView),
|
|
ScreenshotConfig(settings_views.VersionView, mock_context_manager=mock_version_to_most_recent_release),
|
|
ScreenshotConfig(settings_views.VersionView, screenshot_name="VersionView_current_git_state"),
|
|
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"),
|
|
ScreenshotConfig(settings_views.SettingsSelectionRequiredWarningView, dict(attr_name=SettingsConstants.SETTING__SCRIPT_TYPES)),
|
|
],
|
|
"Misc Error Views": [
|
|
ScreenshotConfig(NotYetImplementedView),
|
|
ScreenshotConfig(UnhandledExceptionView, dict(error=["IndexError", "line 1, in some_buggy_code.py", "list index out of range"])),
|
|
ScreenshotConfig(CameraConnectionErrorView),
|
|
ScreenshotConfig(NetworkMismatchErrorView, dict(derivation_path="m/84'/1'/0'")),
|
|
ScreenshotConfig(OptionDisabledView, dict(settings_attr=SettingsConstants.SETTING__MESSAGE_SIGNING)),
|
|
ScreenshotConfig(scan_views.ScanInvalidQRTypeView)
|
|
]
|
|
}
|
|
|
|
return screenshot_sections
|
|
|
|
|
|
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.
|
|
with screenshot_renderer.lock:
|
|
screenshot_renderer.set_screenshot_filename(f"{screenshot_config.screenshot_name}.png")
|
|
|
|
controller = Controller.get_instance()
|
|
toast_thread = screenshot_config.toast_thread
|
|
try:
|
|
print(f"Running {screenshot_config.screenshot_name}")
|
|
try:
|
|
cur_count = screenshot_renderer.render_count
|
|
|
|
# Activate the (optional) context manager for this screenshot to activate
|
|
# any specialized mocks.
|
|
with screenshot_config.mock_context_manager():
|
|
# Set up and run the target View
|
|
screenshot_config.View_cls(**screenshot_config.view_kwargs).run()
|
|
|
|
if screenshot_renderer.render_count == cur_count:
|
|
# The View didn't actually render anything
|
|
raise Exception(f"{screenshot_config.screenshot_name} did not render a screenshot. Verify that its `run_screen()` is reachable by the screenshot generator.")
|
|
|
|
except ScreenshotComplete:
|
|
# The target View has run and its Screen has rendered what it needs to
|
|
if toast_thread is not None:
|
|
# Now run the Toast so it can render on top of the current image buffer
|
|
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)
|
|
|
|
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 and toast_thread.is_alive():
|
|
toast_thread.stop()
|
|
toast_thread.join()
|
|
|
|
|
|
# Parse the main `l10n/messages.pot` for overall stats
|
|
messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "l10n", "messages.pot")
|
|
with open(messages_source_path, 'r') as messages_source_file:
|
|
num_source_messages = messages_source_file.read().count("msgid \"") - 1
|
|
|
|
locale_tuple_list = [locale_tuple for locale_tuple in SettingsConstants.get_detected_languages() if locale_tuple[0] == locale]
|
|
if not locale_tuple_list:
|
|
raise Exception(f"Invalid locale: {locale}")
|
|
|
|
locale, display_name = locale_tuple_list[0]
|
|
|
|
Settings.get_instance().set_value(SettingsConstants.SETTING__LOCALE, value=locale)
|
|
|
|
locale_readme = f"""# SeedSigner Screenshots: {display_name}\n"""
|
|
|
|
# Report the translation progress
|
|
if locale != SettingsConstants.LOCALE__ENGLISH:
|
|
try:
|
|
translated_messages_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "src", "seedsigner", "resources", "seedsigner-translations", "l10n", locale, "LC_MESSAGES", "messages.po")
|
|
with open(translated_messages_path, 'r') as translation_file:
|
|
locale_translations = translation_file.read()
|
|
num_locale_translations = locale_translations.count("msgid \"") - locale_translations.count("""msgstr ""\n\n""") - 1
|
|
|
|
if locale != "en":
|
|
locale_readme += f"## Translation progress: {num_locale_translations / num_source_messages:.1%}\n\n"
|
|
locale_readme += "---\n\n"
|
|
except Exception as e:
|
|
from traceback import print_exc
|
|
print_exc()
|
|
|
|
for section_name, screenshot_list in setup_screenshots(locale).items():
|
|
subdir = section_name.lower().replace(" ", "_")
|
|
screenshot_renderer.set_screenshot_path(os.path.join(screenshot_root, locale, subdir))
|
|
locale_readme += "\n\n---\n\n"
|
|
locale_readme += f"## {section_name}\n\n"
|
|
locale_readme += """<table style="border: 0;">"""
|
|
locale_readme += f"""<tr><td align="center">"""
|
|
for screenshot_config in screenshot_list:
|
|
screencap_view(screenshot_config)
|
|
locale_readme += """ <table align="left" style="border: 1px solid gray;">"""
|
|
locale_readme += f"""<tr><td align="center">{screenshot_config.screenshot_name}<br/><br/><img src="{subdir}/{screenshot_config.screenshot_name}.png"></td></tr>"""
|
|
locale_readme += """</table>\n"""
|
|
|
|
locale_readme += "</td></tr></table>"
|
|
|
|
with open(os.path.join(screenshot_root, locale, "README.md"), 'w') as readme_file:
|
|
readme_file.write(locale_readme)
|
|
|
|
print(f"Done with locale: {locale}.")
|
|
|
|
# Write the main README; ensure it writes all locales, not just the one that may
|
|
# have been specified for this run.
|
|
with open(os.path.join("tests", "screenshot_generator", "template.md"), 'r') as readme_template:
|
|
main_readme = readme_template.read()
|
|
|
|
for locale, display_name in SettingsConstants.get_detected_languages():
|
|
main_readme += f"* [{display_name}]({locale}/README.md)\n"
|
|
|
|
with open(os.path.join(screenshot_root, "README.md"), 'w') as readme_file:
|
|
readme_file.write(main_readme)
|
|
|
|
print(f"Screenshots rendered: {screenshot_renderer.render_count}")
|