diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 5047966d8..134a50baa 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -55,7 +55,7 @@ jobs: steps: - uses: actions/checkout@v6 - run: sudo apt-get install -y pycodestyle - - run: pycodestyle --statistics --exclude translations ports/stm32/boards/Passport + - run: pycodestyle --statistics ports/stm32/boards/Passport is-foundation-header-up-to-date: name: Is foundation.h header file up to date? diff --git a/.github/workflows/simulator-tests.yaml b/.github/workflows/simulator-tests.yaml new file mode 100644 index 000000000..2860f0a94 --- /dev/null +++ b/.github/workflows/simulator-tests.yaml @@ -0,0 +1,15 @@ +# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. +# SPDX-License-Identifier: GPL-3.0-or-later + +name: Simulator tests +on: [push, pull_request] + +jobs: + simulator-tests-pass: + name: Simulator tests pass? + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: cachix/install-nix-action@v31 + - run: sudo apt-get install -y xvfb + - run: xvfb-run -a nix develop .#build -c just test diff --git a/Justfile b/Justfile index 8c483b1c3..18f1b15a2 100644 --- a/Justfile +++ b/Justfile @@ -72,7 +72,7 @@ sim screen="mono" ext="": # Run unit tests. test: just simulator/build color - cd ports/stm32/boards/Passport/modules/tests; python3 -m pytest . --simulatordir=$(pwd)/simulator + cd ports/stm32/boards/Passport/modules/tests && python3 -m pytest . --simulatordir={{justfile_directory()}}/simulator # Lint the codebase. lint: (run-in-docker "just ports/stm32/lint") (run-in-docker "just extmod/foundation-rust/lint") diff --git a/flake.lock b/flake.lock index ba3674502..3fc349723 100644 --- a/flake.lock +++ b/flake.lock @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771848320, - "narHash": "sha256-0MAd+0mun3K/Ns8JATeHT1sX28faLII5hVLq0L3BdZU=", + "lastModified": 1787736819, + "narHash": "sha256-cV5xEJJK3BvhU8rEd4mC9UsmDi5qscv/kzGPhBRC5WA=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "2fc6539b481e1d2569f25f8799236694180c0993", + "rev": "9fbb54b33e91ee4ca368e35a78e0613c720600b3", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 6abed21b2..304196ac4 100644 --- a/flake.nix +++ b/flake.nix @@ -125,9 +125,15 @@ openssl pkg-config python3 + python3Packages.autopep8 + python3Packages.imageio + python3Packages.opencv4 python3Packages.pip + python3Packages.pillow + python3Packages.pytest + python3Packages.pysdl2 python3Packages.virtualenv - python3Packages.autopep8 + SDL2 reuse rust-cbindgen xterm @@ -145,7 +151,6 @@ fontmiscmisc minicom openocd - SDL2 ]); diff --git a/ports/stm32/Justfile b/ports/stm32/Justfile index 75774d1ea..2dc880195 100644 --- a/ports/stm32/Justfile +++ b/ports/stm32/Justfile @@ -48,7 +48,7 @@ init-openocd: # Lint only the python code of the project lint-py: - pycodestyle --exclude trezor-firmware,unused_modules,graphics.py,translations --statistics boards/Passport + pycodestyle --exclude trezor-firmware,unused_modules,graphics.py --statistics boards/Passport # Lint only the C code of the project lint-c: diff --git a/ports/stm32/boards/Passport/manifest.py b/ports/stm32/boards/Passport/manifest.py index be50eb024..65b9ee386 100644 --- a/ports/stm32/boards/Passport/manifest.py +++ b/ports/stm32/boards/Passport/manifest.py @@ -290,12 +290,6 @@ 'tasks/verify_backup_task.py', 'tasks/verify_firmware_signature_task.py')) -# Translations -freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', - ('translations/__init__.py', - 'translations/en.py', - 'translations/tags.py')) - # UI freeze('$(MPY_DIR)/ports/stm32/boards/Passport/modules', ('ui/__init__.py', diff --git a/ports/stm32/boards/Passport/modules/flows/change_pin_flow.py b/ports/stm32/boards/Passport/modules/flows/change_pin_flow.py index e543f2d9b..8cbc7c90e 100644 --- a/ports/stm32/boards/Passport/modules/flows/change_pin_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/change_pin_flow.py @@ -7,7 +7,6 @@ from pages import PINEntryPage, ErrorPage, SuccessPage from tasks import change_pin_task from utils import spinner_task -from translations import t, T import microns from common import settings from serializations import sha256 diff --git a/ports/stm32/boards/Passport/modules/flows/delete_account_flow.py b/ports/stm32/boards/Passport/modules/flows/delete_account_flow.py index 14222babc..ccf0ee4f8 100644 --- a/ports/stm32/boards/Passport/modules/flows/delete_account_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/delete_account_flow.py @@ -8,7 +8,6 @@ from pages import ErrorPage, SuccessPage, QuestionPage, ErrorPage from tasks import delete_account_task from utils import spinner_task -from translations import t, T class DeleteAccountFlow(Flow): diff --git a/ports/stm32/boards/Passport/modules/flows/delete_multisig_flow.py b/ports/stm32/boards/Passport/modules/flows/delete_multisig_flow.py index 1816a10dd..7bd812009 100644 --- a/ports/stm32/boards/Passport/modules/flows/delete_multisig_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/delete_multisig_flow.py @@ -4,7 +4,6 @@ # delete_multisig_flow.py - Delete the specified multisig config from flows import Flow -from translations import t, T class DeleteMultisigFlow(Flow): diff --git a/ports/stm32/boards/Passport/modules/flows/erase_passport_flow.py b/ports/stm32/boards/Passport/modules/flows/erase_passport_flow.py index 7cf908aee..16cff02c2 100644 --- a/ports/stm32/boards/Passport/modules/flows/erase_passport_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/erase_passport_flow.py @@ -9,7 +9,6 @@ from pages import SuccessPage, QuestionPage, LongQuestionPage from tasks import erase_passport_task from utils import spinner_task -from translations import t, T import microns import passport diff --git a/ports/stm32/boards/Passport/modules/flows/new_account_flow.py b/ports/stm32/boards/Passport/modules/flows/new_account_flow.py index d28fc4b50..b53a6091e 100644 --- a/ports/stm32/boards/Passport/modules/flows/new_account_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/new_account_flow.py @@ -9,7 +9,6 @@ from pages import ErrorPage, SuccessPage, TextInputPage, ErrorPage from tasks import save_new_account_task from utils import get_account_by_name, get_account_by_number, get_accounts_by_xfp, spinner_task -from translations import t, T from wallets.utils import get_next_account_num from common import settings diff --git a/ports/stm32/boards/Passport/modules/flows/new_seed_flow.py b/ports/stm32/boards/Passport/modules/flows/new_seed_flow.py index de83b3a00..23e4505f1 100644 --- a/ports/stm32/boards/Passport/modules/flows/new_seed_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/new_seed_flow.py @@ -7,7 +7,6 @@ from pages import ErrorPage, QuestionPage, SuccessPage, YesNoChooserPage from tasks import new_seed_task, save_seed_task from utils import has_secrets, spinner_task -from translations import t, T import lvgl as lv import microns diff --git a/ports/stm32/boards/Passport/modules/flows/rename_account_flow.py b/ports/stm32/boards/Passport/modules/flows/rename_account_flow.py index fba662769..55f784d59 100644 --- a/ports/stm32/boards/Passport/modules/flows/rename_account_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/rename_account_flow.py @@ -9,7 +9,6 @@ from pages import ErrorPage, SuccessPage, TextInputPage, ErrorPage from tasks import rename_account_task from utils import get_account_by_name, spinner_task -from translations import t, T class RenameAccountFlow(Flow): diff --git a/ports/stm32/boards/Passport/modules/flows/rename_multisig_flow.py b/ports/stm32/boards/Passport/modules/flows/rename_multisig_flow.py index 1727fdc58..28b829677 100644 --- a/ports/stm32/boards/Passport/modules/flows/rename_multisig_flow.py +++ b/ports/stm32/boards/Passport/modules/flows/rename_multisig_flow.py @@ -5,7 +5,6 @@ from flows import Flow import microns -from translations import t, T class RenameMultisigFlow(Flow): diff --git a/ports/stm32/boards/Passport/modules/main.py b/ports/stm32/boards/Passport/modules/main.py index 57b4502f5..89f346fba 100644 --- a/ports/stm32/boards/Passport/modules/main.py +++ b/ports/stm32/boards/Passport/modules/main.py @@ -15,7 +15,6 @@ import gc from utils import mem_info -# from translations import T, t, set_active_language mem_info(label='Start main.py:') diff --git a/ports/stm32/boards/Passport/modules/tests/fixtures/simulator.py b/ports/stm32/boards/Passport/modules/tests/fixtures/simulator.py index 51eaa40ca..049cbffde 100644 --- a/ports/stm32/boards/Passport/modules/tests/fixtures/simulator.py +++ b/ports/stm32/boards/Passport/modules/tests/fixtures/simulator.py @@ -4,52 +4,102 @@ import pytest import os +import signal +import subprocess +import time class SimulatorSocket: UNIX_SOCKET_PATH = b'/tmp/passport-simulator.sock' + TEST_SPI_FLASH_PATH = 'work/test_spi_flash.bin' def __init__(self, simulator_dir): + self.simulator_dir = simulator_dir self.pipe = None - self._open(simulator_dir) - self._connect() + self.process = None + self.socket_path = None + try: + self._open(simulator_dir) + self._connect() + except BaseException: + self.close() + raise def _open(self, simulator_dir): - import subprocess - + self._remove_server_socket() + self._remove_test_spi_flash(simulator_dir) simulator_cmd = simulator_dir + '/simulator.py' self.process = subprocess.Popen([simulator_cmd, 'color', '--unit-test'], cwd=simulator_dir, preexec_fn=os.setsid) + def _remove_server_socket(self): + try: + os.unlink(self.UNIX_SOCKET_PATH) + except FileNotFoundError: + pass + + def _remove_test_spi_flash(self, simulator_dir): + try: + os.unlink(simulator_dir + '/' + self.TEST_SPI_FLASH_PATH) + except FileNotFoundError: + pass + def _connect(self): import socket import tempfile self.pipe = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) + deadline = time.monotonic() + 10 while True: try: self.pipe.connect(self.UNIX_SOCKET_PATH) break - except Exception: - continue + except OSError: + if self.process.poll() is not None: + raise RuntimeError( + 'Simulator exited before opening its socket ' + f'(exit code {self.process.returncode})' + ) + if time.monotonic() >= deadline: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + raise TimeoutError('Simulator did not open its socket within 10 seconds') + time.sleep(0.01) while True: try: - addr = '' with tempfile.NamedTemporaryFile(suffix='.sock', prefix='passport-client.', dir='/tmp', delete=True) as tmpfile: addr = tmpfile.name self.pipe.bind(addr) + self.socket_path = addr break - except OSError: - continue + except OSError as error: + if time.monotonic() >= deadline: + raise TimeoutError('Could not bind the simulator client socket within 10 seconds') from error + time.sleep(0.01) # Close the connection and kill the simulator process. def close(self): - import signal - - self.pipe.close() - os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + if self.pipe is not None: + self.pipe.close() + self.pipe = None + if self.socket_path is not None: + try: + os.unlink(self.socket_path) + except FileNotFoundError: + pass + self.socket_path = None + if self.process is not None: + if self.process.poll() is None: + os.killpg(os.getpgid(self.process.pid), signal.SIGTERM) + try: + self.process.wait(timeout=5) + except subprocess.TimeoutExpired: + os.killpg(os.getpgid(self.process.pid), signal.SIGKILL) + self.process.wait() + self.process = None + self._remove_server_socket() + self._remove_test_spi_flash(self.simulator_dir) # Run `exec()` in the Unix MP simulator. def exec(self, object): @@ -80,7 +130,11 @@ def execute_command(self, cmd, text): # Get a connection to the simulator. @pytest.fixture def simulator(simulatordir): - return SimulatorSocket(simulatordir) + connection = SimulatorSocket(simulatordir) + try: + yield connection + finally: + connection.close() # Execute a file in the simulator using the Unix Micro-Python built-in `exec()` function. @@ -89,7 +143,6 @@ def exec_file(simulator): def doit(filename): from pathlib import Path cmd, return_value = simulator.exec(Path(filename).read_text()) - simulator.close() if cmd == 'excp': pytest.fail('Remote test failed with exception:\n{}'.format(return_value.decode('utf-8', 'strict'))) elif cmd == 'resp': diff --git a/ports/stm32/boards/Passport/modules/tests/test_translations.py b/ports/stm32/boards/Passport/modules/tests/test_translations.py deleted file mode 100644 index 707e6f2b2..000000000 --- a/ports/stm32/boards/Passport/modules/tests/test_translations.py +++ /dev/null @@ -1,51 +0,0 @@ -# SPDX-FileCopyrightText: © 2021 Foundation Devices, Inc. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# Test that the translation module works. - -import sys -import os - -sys.path.insert(1, os.path.join(sys.path[0], '..')) - - -def test_change_active_language(): - from translations import t, T, set_active_language, get_active_language - - assert set_active_language('en') - assert get_active_language() == 'en' - - # The value of these messages will be always these so we can reliably test - # here if a basic Yes/No is translated. - # - # NOTE: If these values are changed on the translations please update this - # test! - assert t(T.DEFAULT_YES_BUTTON_LABEL) == "Yes" - assert t(T.DEFAULT_NO_BUTTON_LABEL) == "No" - - assert set_active_language('es') - assert get_active_language() == 'es' - - assert t(T.DEFAULT_YES_BUTTON_LABEL) == "Sí" - assert t(T.DEFAULT_NO_BUTTON_LABEL) == "No" - - # Restore it. - assert set_active_language('en') - - -def test_fallback_works(): - from translations import t, T, set_active_language - - # Verify that messages that don't need to be translated for some languages - # correctly fall back to the original english translation (this avoid duplicate - # strings). - # - # NOTE: If ever FOUNDATION_CO is translated to spanish update this test. - assert set_active_language('es') - foundation_co_es = t(T.FOUNDATION_CO) - - assert set_active_language('en') - foundation_co_en = t(T.FOUNDATION_CO) - - assert foundation_co_es == foundation_co_en diff --git a/ports/stm32/boards/Passport/modules/tests/test_unit.py b/ports/stm32/boards/Passport/modules/tests/test_unit.py index 5341139f4..5434c0d42 100644 --- a/ports/stm32/boards/Passport/modules/tests/test_unit.py +++ b/ports/stm32/boards/Passport/modules/tests/test_unit.py @@ -28,10 +28,6 @@ def test_seedqr_codec(test): assert test('seedqr_codec.py') == b'OK' -def test_settings(test): - assert test('settings.py') == b'OK' - - def test_ui(test): assert test('ui.py') == b'OK' diff --git a/ports/stm32/boards/Passport/modules/tests/unit/foundation.py b/ports/stm32/boards/Passport/modules/tests/unit/foundation.py index 9ab69a43f..063fa1378 100644 --- a/ports/stm32/boards/Passport/modules/tests/unit/foundation.py +++ b/ports/stm32/boards/Passport/modules/tests/unit/foundation.py @@ -22,8 +22,8 @@ def should_fail(f): should_fail(lambda: foundation.qr.init()) -should_fail(lambda: foundation.qr.init(None, None, None)) -foundation.qr.init(HOR_RES, VER_RES, bytearray(HOR_RES * VER_RES)) +should_fail(lambda: foundation.qr.init(None, None)) +foundation.qr.init(HOR_RES, VER_RES) should_fail(lambda: foundation.convert_rgb565_to_grayscale()) should_fail(lambda: foundation.convert_rgb565_to_grayscale(None, None, None, None)) diff --git a/ports/stm32/boards/Passport/modules/tests/unit/settings.py b/ports/stm32/boards/Passport/modules/tests/unit/settings.py deleted file mode 100644 index 0c53f538c..000000000 --- a/ports/stm32/boards/Passport/modules/tests/unit/settings.py +++ /dev/null @@ -1,25 +0,0 @@ -# SPDX-FileCopyrightText: 2026 Foundation Devices, Inc. -# -# SPDX-License-Identifier: GPL-3.0-or-later - -from settings import DATA_SIZE, Settings - - -class OversizedSettings: - def __init__(self): - self.curr_dict = {'value': 'x' * DATA_SIZE} - - def next_addr(self): - raise RuntimeError('Oversized settings reached flash slot selection') - - -settings = OversizedSettings() - -try: - Settings.save(settings) -except ValueError as exc: - assert str(DATA_SIZE) in str(exc) -else: - raise RuntimeError('Oversized settings should fail before selecting a flash slot') - -return_value.write(b'OK') diff --git a/ports/stm32/boards/Passport/modules/translations/__init__.py b/ports/stm32/boards/Passport/modules/translations/__init__.py deleted file mode 100644 index 3d87fad34..000000000 --- a/ports/stm32/boards/Passport/modules/translations/__init__.py +++ /dev/null @@ -1,53 +0,0 @@ -# SPDX-FileCopyrightText: © 2021 Foundation Devices, Inc. -# -# SPDX-License-Identifier: GPL-3.0-or-later -# -# translations.py -# -# Multi-language text utility functions -# - -from .tags import T as _T -from .en import EN_TRANSLATIONS - -ACTIVE_LANGUAGE = 'en' -TRANSLATIONS = { - 'en': EN_TRANSLATIONS, -} - - -def t(tag, **kwargs): - translations = TRANSLATIONS[ACTIVE_LANGUAGE] - if tag in translations: - str = translations[tag] - elif tag in EN_TRANSLATIONS: - str = EN_TRANSLATIONS[tag] - else: - # Error - return '' - - if kwargs is not None: - return str.format(**kwargs) - else: - return str - - -# Get the global active language. -def get_active_language(): - return ACTIVE_LANGUAGE - - -# Set the global active language. -def set_active_language(language): - global ACTIVE_LANGUAGE - - # Verify that the requsted language is supported - if language in TRANSLATIONS.keys(): - ACTIVE_LANGUAGE = language - return True - - return False - - -# Re-export the tags -T = _T diff --git a/ports/stm32/boards/Passport/modules/translations/en.py b/ports/stm32/boards/Passport/modules/translations/en.py deleted file mode 100644 index 93c11c34b..000000000 --- a/ports/stm32/boards/Passport/modules/translations/en.py +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-FileCopyrightText: © 2022 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -# en.py - String translations for EN language code -# -# AUTOGENERATED FILE! DO NOT EDIT MANUALLY! -# - -from .tags import T - -EN_TRANSLATIONS = { - T.import_pp_intro_card1_heading: '''Pair Passport with Envoy''', - T.import_pp_intro_card1_subheading: '''On Passport, select Pair Wallet > Envoy''', - T.import_pp_intro_card2_subheading: '''If you want to use Envoy for firmware updates only, feel free to skip this step.''', - T.import_pp_intro_cta: '''Get Started''', - T.import_pp_intro_os_clock: '''9:41''', - T.import_pp_intro_right_action: '''Skip''', - T.import_pp_scan_cta: '''Continue''', - T.import_pp_scan_heading: '''Scan the QR code that Passport generates''', - T.import_pp_scan_os_clock: '''9:41''', - T.import_pp_scan_right_action: '''Skip''', - T.import_pp_scan_subheading: '''This QR code contains the information required for Envoy to interact securley with Passport.''', - T.wallet_address_verify_confirm_cta: '''Continue''', - T.wallet_address_verify_confirm_cta1: '''Contact support''', - T.wallet_address_verify_confirm_heading: '''Address Validated?''', - T.wallet_address_verify_confirm_os_clock: '''9:41''', - T.wallet_address_verify_confirm_right_action: '''Skip''', - T.wallet_address_verify_confirm_subheading: '''If you get a success message on Passport, your setup is now complete. - -If Passport could not verify the address, please try again or contact support.''', - T.wallet_address_verify_cta: '''Continue''', - T.wallet_address_verify_heading: '''Scan this QR code with Passport to validate''', - T.wallet_address_verify_os_clock: '''9:41''', - T.wallet_address_verify_right_action: '''Skip''', - T.wallet_address_verify_subheading: '''This is the first receive address controlled by your Passport.''', - T.wallet_pair_success_cta: '''Validate Receiving address''', - T.wallet_pair_success_cta1: '''Continue to home screen''', - T.wallet_pair_success_heading: '''Connection successful''', - T.wallet_pair_success_os_clock: '''9:41''', - T.wallet_pair_success_right_action: '''Skip''', - T.wallet_pair_success_subheading: '''Envoy has the information required to generate addresses and construct transactions for Passport. You can validate this on the following screen or skip straight to home''', -} diff --git a/ports/stm32/boards/Passport/modules/translations/tags.py b/ports/stm32/boards/Passport/modules/translations/tags.py deleted file mode 100644 index 2681e7421..000000000 --- a/ports/stm32/boards/Passport/modules/translations/tags.py +++ /dev/null @@ -1,42 +0,0 @@ -# SPDX-FileCopyrightText: © 2022 Foundation Devices, Inc. -# SPDX-License-Identifier: GPL-3.0-or-later -# -# tags.py -# -# Text tag names used in all translation files. -# -# AUTOGENERATED FILE! DO NOT EDIT MANUALLY! -# - -from Enum import enum - -T = enum( - 'import_pp_intro_card1_heading', - 'import_pp_intro_card1_subheading', - 'import_pp_intro_card2_subheading', - 'import_pp_intro_cta', - 'import_pp_intro_os_clock', - 'import_pp_intro_right_action', - 'import_pp_scan_cta', - 'import_pp_scan_heading', - 'import_pp_scan_os_clock', - 'import_pp_scan_right_action', - 'import_pp_scan_subheading', - 'wallet_address_verify_confirm_cta', - 'wallet_address_verify_confirm_cta1', - 'wallet_address_verify_confirm_heading', - 'wallet_address_verify_confirm_os_clock', - 'wallet_address_verify_confirm_right_action', - 'wallet_address_verify_confirm_subheading', - 'wallet_address_verify_cta', - 'wallet_address_verify_heading', - 'wallet_address_verify_os_clock', - 'wallet_address_verify_right_action', - 'wallet_address_verify_subheading', - 'wallet_pair_success_cta', - 'wallet_pair_success_cta1', - 'wallet_pair_success_heading', - 'wallet_pair_success_os_clock', - 'wallet_pair_success_right_action', - 'wallet_pair_success_subheading', -) diff --git a/simulator/sim_modules/sflash.py b/simulator/sim_modules/sflash.py index 8a15e9cb8..65dcc77bb 100644 --- a/simulator/sim_modules/sflash.py +++ b/simulator/sim_modules/sflash.py @@ -2,10 +2,12 @@ # SPDX-FileCopyrightText: © 2021 Foundation Devices, Inc. # SPDX-License-Identifier: GPL-3.0-only +import sys + from utils import file_exists _SIZE = 1024 * 1024 * 8 -SPI_FLASH_SIM_PATH = 'spi_flash.bin' +SPI_FLASH_SIM_PATH = 'test_spi_flash.bin' if '--unit-test' in sys.argv else 'spi_flash.bin' class SPIFlash: