Merge pull request #673 from Foundation-Devices/SFT-7945-add-pytest-to-devshell
What changed, and why it matters
This commit is a routine development tooling and cleanup change. It adds the Pytest testing framework to the project's Nix development shell, fixes a test runner path, adds a GitHub Actions workflow to run simulator tests automatically, and removes an unused translation system along with its associated tests. There is no indication this change fixes a security vulnerability or introduces a security-relevant behavior.
No security action required. Treat as normal development/maintenance commit. Reviewers may optionally verify the new CI workflow runs successfully and that removing translations does not break any user-facing strings still referenced elsewhere.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The merge commit d8ee1164085d53d6db3b7aad4966077cea4a171a is titled ‘SFT-7945: Add Pytest to Nix Devshell’. The diff shows: (1) flake.nix additions of python3Packages.pytest and related Python packages (imageio, opencv4, pillow, pysdl2) to the build devshell; (2) Justfile correction of the –simulatordir path using justfile_directory(); (3) a new .github/workflows/simulator-tests.yaml CI job that runs ‘just test’ under xvfb inside nix develop; (4) removal of the translations module (translations/init.py, en.py, tags.py) from the manifest and deletion of those files; (5) removal of unused ‘from translations import t, T’ imports across several flows and main.py; (6) removal of test_translations.py and unit/settings.py tests; (7) updates to simulator.py fixture for robust process lifecycle management and a simulator sflash.py change to use a separate test_spi_flash.bin path when –unit-test is passed. No cryptographic, authentication, authorization, or secret-handling code is modified. No vulnerability disclosure or security advisory is referenced.
Changed components
Nix development shell (flake.nix)Justfile test targetGitHub Actions CI (.github/workflows/simulator-tests.yaml)Simulator test fixture (ports/stm32/boards/Passport/modules/tests/fixtures/simulator.py)Simulator SPI flash module (simulator/sim_modules/sflash.py)Removed translation subsystem (ports/stm32/boards/Passport/modules/translations/)Inspect captured patch +101 / −258
### .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?
### .github/workflows/simulator-tests.yaml
@@ -0,0 +1,15 @@
+# SPDX-FileCopyrightText: © 2026 Foundation Devices, Inc. <hello@foundation.xyz>
+# 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
### 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")
### 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": {
### 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
]);
### 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:
### 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',
### 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
### 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):
### 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):
### 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
### 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
### 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
### 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):
### 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):
### 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:')
### 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':
### ports/stm32/boards/Passport/modules/tests/test_translations.py
@@ -1,51 +0,0 @@
-# SPDX-FileCopyrightText: © 2021 Foundation Devices, Inc. <hello@foundation.xyz>
-#
-# 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
### 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'
### 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))
### ports/stm32/boards/Passport/modules/tests/unit/settings.py
@@ -1,25 +0,0 @@
-# SPDX-FileCopyrightText: 2026 Foundation Devices, Inc. <hello@foundation.xyz>
-#
-# 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')
### ports/stm32/boards/Passport/modules/translations/__init__.py
@@ -1,53 +0,0 @@
-# SPDX-FileCopyrightText: © 2021 Foundation Devices, Inc. <hello@foundation.xyz>
-#
-# 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 '<UNKNOWN TEXT>'
-
- 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
### ports/stm32/boards/Passport/modules/translations/en.py
@@ -1,42 +0,0 @@
-# SPDX-FileCopyrightText: © 2022 Foundation Devices, Inc. <hello@foundation.xyz>
-# 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''',
-}
### ports/stm32/boards/Passport/modules/translations/tags.py
@@ -1,42 +0,0 @@
-# SPDX-FileCopyrightText: © 2022 Foundation Devices, Inc. <hello@foundation.xyz>
-# 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',
-)
### simulator/sim_modules/sflash.py
@@ -2,10 +2,12 @@
# SPDX-FileCopyrightText: © 2021 Foundation Devices, Inc. <hello@foundationdevices.com>
# 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:Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.