feat(core/python): create testing module in trezorlib
What changed, and why it matters
This commit is a routine code reorganization: it moves existing test-only helper code from the project's internal test directory into the public `trezorlib` Python package as a new `testing` module. The moved code includes utilities for handling emulated devices during tests, translation blob helpers, and Bitcoin compact-size encoding. There is no change to the firmware that runs on Trezor devices, no change to production cryptography, and no security fix or vulnerability introduced.
No security action required. Treat as a normal refactoring/repackaging commit. Reviewers may optionally verify that no production package accidentally depends on the new testing-only module.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The diff creates python/src/trezorlib/testing/ containing __init__.py, common.py, device_handler.py, and translations.py, which are essentially relocated copies of previously internal test helpers (tests/device_handler.py, tests/translations.py, and parts of tests/common.py). It then updates import statements across the test suite to reference the new trezorlib.testing module. The logic is unchanged; only module paths and minor type annotations/docstrings differ. No runtime firmware, protocol, or cryptographic code is modified.
Changed components
python/src/trezorlib/testing/__init__.pypython/src/trezorlib/testing/common.pypython/src/trezorlib/testing/device_handler.pypython/src/trezorlib/testing/translations.pytests/click_tests/*tests/device_tests/*tests/input_flows.pytests/input_flows_helpers.pytests/persistence_tests/test_shamir_persistence.pytests/upgrade_tests/test_firmware_upgrades.pyInspect captured patch +814 / −467
diff --git a/python/.changelog.d/6994.added b/python/.changelog.d/6994.added
new file mode 100644
index 00000000..da4adf45
--- /dev/null
+++ b/python/.changelog.d/6994.added
@@ -0,0 +1 @@
+Add testing module to trezorlib.
diff --git a/python/src/trezorlib/testing/__init__.py b/python/src/trezorlib/testing/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/python/src/trezorlib/testing/common.py b/python/src/trezorlib/testing/common.py
new file mode 100644
index 00000000..5c883e32
--- /dev/null
+++ b/python/src/trezorlib/testing/common.py
@@ -0,0 +1,93 @@
+from typing import TYPE_CHECKING, Generator, Union
+
+from .. import messages
+from ..debuglink import LayoutType
+
+if TYPE_CHECKING:
+
+ from ..debuglink import DebugLink
+
+
+BRGeneratorType = Generator[None, messages.ButtonRequest, None]
+
+PRIVATE_KEYS_DEV = [byte * 32 for byte in (b"\xdd", b"\xde", b"\xdf")]
+
+
+def compact_size(n: int) -> bytes:
+ """
+ Encode an integer using Bitcoin's compact size format.
+
+ Args:
+ n (int): The integer to encode.
+
+ Returns:
+ bytes: The encoded integer.
+
+ Raises:
+ ValueError: If n is not in the range 0..2^64-1.
+ """
+ if n < 0 or n > 0xFFFF_FFFF_FFFF_FFFF:
+ raise ValueError("compact_size supports integers in range 0..2^64-1")
+ if n < 253:
+ return n.to_bytes(1, "little")
+ elif n < 0x1_0000:
+ return bytes([253]) + n.to_bytes(2, "little")
+ elif n < 0x1_0000_0000:
+ return bytes([254]) + n.to_bytes(4, "little")
+ else:
+ return bytes([255]) + n.to_bytes(8, "little")
+
+
+def get_text_possible_pagination(debug: "DebugLink", br: messages.ButtonRequest) -> str:
+ """
+ Read all text content from the device, handling possible pagination.
+
+ Args:
+ debug (DebugLink): The debug link to interact with the device.
+ br (ButtonRequest): The button request containing pagination info.
+
+ Returns:
+ str: The concatenated text from all pages.
+ """
+ text = debug.read_layout().text_content()
+ if br.pages is not None:
+ for _ in range(br.pages - 1):
+ if debug.layout_type is LayoutType.Eckhart:
+ debug.click(debug.screen_buttons.ok())
+ else:
+ debug.swipe_up()
+ text += " "
+ text += debug.read_layout().text_content()
+ return text
+
+
+def swipe_if_necessary(
+ debug: "DebugLink", br_code: Union[messages.ButtonRequestType, None] = None
+) -> BRGeneratorType:
+ """
+ Generator that swipes through pages if necessary, based on button request code.
+
+ Args:
+ debug (DebugLink): The debug link to interact with the device.
+ br_code (ButtonRequestType or None): Expected button request code.
+
+ Yields:
+ ButtonRequest: The button request to process.
+ """
+ br = yield
+ if br_code is not None:
+ assert br.code == br_code
+ swipe_till_the_end(debug, br)
+
+
+def swipe_till_the_end(debug: "DebugLink", br: messages.ButtonRequest) -> None:
+ """
+ Swipe through all pages until the end.
+
+ Args:
+ debug (DebugLink): The debug link to interact with the device.
+ br (ButtonRequest): The button request containing pagination info.
+ """
+ if br.pages is not None:
+ for _ in range(br.pages - 1):
+ debug.swipe_up()
diff --git a/python/src/trezorlib/testing/device_handler.py b/python/src/trezorlib/testing/device_handler.py
new file mode 100644
index 00000000..cb3970f0
--- /dev/null
+++ b/python/src/trezorlib/testing/device_handler.py
@@ -0,0 +1,269 @@
+from __future__ import annotations
+
+import typing as t
+from concurrent.futures import ThreadPoolExecutor
+
+import typing_extensions as tx
+
+from ..messages import DebugWaitType
+from ..transport import udp
+
+if t.TYPE_CHECKING:
+ from .._internal.emulator import Emulator
+ from ..client import Session
+ from ..debuglink import DebugLink
+ from ..debuglink import TrezorTestContext as Client
+ from ..messages import Features
+
+ P = tx.ParamSpec("P")
+
+
+udp.SOCKET_TIMEOUT = 0.1
+
+
+class NullUI:
+ """
+ A mock UI class for testing purposes.
+
+ Provides static methods that do nothing or raise errors, simulating
+ a UI interface for use in tests where UI interaction is not expected.
+ """
+
+ @staticmethod
+ def clear(*args: t.Any, **kwargs: t.Any) -> None:
+ """
+ Mock clear method. Does nothing.
+ """
+
+ @staticmethod
+ def button_request(code: t.Any) -> None:
+ """
+ Mock button request handler. Does nothing.
+ """
+
+ @staticmethod
+ def get_pin(code: t.Any = None) -> None:
+ """
+ Mock PIN entry handler.
+
+ Raises:
+ NotImplementedError: Always, as PIN entry should not be used with NullUI.
+ """
+ raise NotImplementedError("NullUI should not be used with T1")
+
+
+class BackgroundDeviceHandler:
+ """
+ Handles background operations and UI simulation for device testing.
+
+ Manages asynchronous device interactions, session handling, and UI events
+ in a testing environment. Ensures that device tasks are properly started,
+ awaited, and finalized.
+ """
+
+ _pool = ThreadPoolExecutor()
+
+ def __init__(self, client: "Client", nowait: bool = False) -> None:
+ """
+ Initialize the handler with a client.
+
+ Args:
+ client: The client object to interact with.
+ nowait: If True, do not wait for tasks to finish before returning.
+ """
+ self._configure_client(client)
+ self.task = None
+ self.nowait = nowait
+
+ def _configure_client(self, client: "Client") -> None:
+ """
+ Configure the client for background handling and set up the mock UI.
+
+ Args:
+ client: The client object to configure.
+ """
+ self.client = client
+ self.client.ui = NullUI() # pyright: ignore [reportAttributeAccessIssue]
+ self.client.app.button_callback = self.client.ui.button_request
+ self.client.debug.input_wait_type = DebugWaitType.CURRENT_LAYOUT
+
+ def get_session(self, *args: t.Any, **kwargs: t.Any) -> None:
+ """
+ Start a background task to get a session, waiting for a layout change.
+
+ Raises:
+ RuntimeError: If a previous task is still running.
+ """
+ if self.task is not None:
+ raise RuntimeError("Wait for previous task first")
+
+ with self.debuglink().wait_for_layout_change():
+ self.task = self._pool.submit(self.client.get_session, *args, **kwargs)
+
+ def run_with_session(
+ self,
+ function: t.Callable[tx.Concatenate["Session", P], t.Any],
+ seedless: bool = False,
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> None:
+ """
+ Run a function that interacts with a device session in the background.
+
+ Ensures the UI is updated before returning.
+
+ Args:
+ function: The function to run with the session.
+ seedless: If True, use a seedless session.
+ *args: Additional positional arguments for the function.
+ **kwargs: Additional keyword arguments for the function.
+
+ Raises:
+ RuntimeError: If a previous task is still running.
+ """
+ if self.task is not None:
+ raise RuntimeError("Wait for previous task first")
+
+ def task_function(*args: t.Any, **kwargs: t.Any) -> t.Any:
+ if seedless:
+ session = self.client.get_seedless_session()
+ else:
+ session = self.client.get_session()
+ return function(session, *args, **kwargs)
+
+ # wait for the first UI change triggered by the task running in the background
+ with self.debuglink().wait_for_layout_change():
+ self.task = self._pool.submit(task_function, *args, **kwargs)
+
+ def run_with_provided_session(
+ self,
+ session: "Session",
+ function: t.Callable[tx.Concatenate["Session", P], t.Any],
+ *args: P.args,
+ **kwargs: P.kwargs,
+ ) -> None:
+ """
+ Run a function with a provided session in the background.
+
+ Ensures the UI is updated before returning.
+
+ Args:
+ session: The session to use.
+ function: The function to run.
+ *args: Additional positional arguments for the function.
+ **kwargs: Additional keyword arguments for the function.
+
+ Raises:
+ RuntimeError: If a previous task is still running.
+ """
+ if self.task is not None:
+ raise RuntimeError("Wait for previous task first")
+
+ # wait for the first UI change triggered by the task running in the background
+ with self.debuglink().wait_for_layout_change():
+ self.task = self._pool.submit(function, session, *args, **kwargs)
+
+ def kill_task(self) -> None:
+ """
+ Kill the currently running background task, if any.
+
+ Closes the client transport and waits for the task to finish.
+ """
+ if self.task is not None:
+ # Force close the transport, which should raise an exception in a client
+ # waiting on IO. Does not work over Bridge, because bridge doesn't have
+ # a close() method.
+ self.client.transport.close()
+ try:
+ self.task.result(timeout=1)
+ except Exception:
+ pass
+ self.task = None
+
+ def restart(self, emulator: "Emulator") -> None:
+ """
+ Restart the emulator and reconfigure the client.
+
+ Args:
+ emulator: The emulator to restart.
+ """
+ # TODO handle actual restart as well
+ self.kill_task()
+ emulator.restart()
+ self._configure_client(emulator.client)
+
+ def result(self, timeout: float | None = None) -> t.Any:
+ """
+ Get the result of the background task.
+
+ Args:
+ timeout: Optional timeout in seconds.
+
+ Returns:
+ The result of the task.
+
+ Raises:
+ RuntimeError: If no task is running.
+ """
+ if self.task is None:
+ raise RuntimeError("No task running")
+ try:
+ return self.task.result(timeout=timeout)
+ finally:
+ self.task = None
+
+ def features(self) -> "Features":
+ """
+ Refresh and return the client's features.
+
+ Returns:
+ Features: The client's features.
+
+ Raises:
+ RuntimeError: If a task is running.
+ """
+ if self.task is not None:
+ raise RuntimeError("Cannot query features while task is running")
+ self.client.refresh_features()
+ return self.client.features
+
+ def debuglink(self) -> "DebugLink":
+ """
+ Get the debuglink for the client.
+
+ Returns:
+ DebugLink: The client's debuglink.
+ """
+ return self.client.debug
+
+ def check_finalize(self) -> bool:
+ """
+ Ensure all tasks are finalized.
+
+ Returns:
+ bool: True if no task was running, False if a task was killed.
+ """
+ if self.task is not None:
+ self.kill_task()
+ return False
+ return True
+
+ def __enter__(self) -> "BackgroundDeviceHandler":
+ """
+ Enter the context manager.
+
+ Returns:
+ BackgroundDeviceHandler: self
+ """
+ return self
+
+ def __exit__(self, exc_type: t.Any, exc_value: t.Any, traceback: t.Any) -> None:
+ """
+ Exit the context manager, ensuring all tasks are finalized.
+
+ Raises:
+ RuntimeError: If exiting while a task is unfinished.
+ """
+ finalized_ok = self.check_finalize()
+ if exc_type is None and not finalized_ok:
+ raise RuntimeError("Exit while task is unfinished")
diff --git a/python/src/trezorlib/testing/translations.py b/python/src/trezorlib/testing/translations.py
new file mode 100644
index 00000000..9234ec15
--- /dev/null
+++ b/python/src/trezorlib/testing/translations.py
@@ -0,0 +1,369 @@
+from __future__ import annotations
+
+import json
+import os
+import re
+import threading
+import typing as t
+import warnings
+from hashlib import sha256
+from pathlib import Path
+
+from .. import cosi, device, models
+from .._internal import translations
+from ..debuglink import DebugSession, LayoutType
+from . import common
+
+HERE = Path(__file__).resolve().parent
+ROOT = HERE.parent.parent.parent.parent
+
+
+def get_translations_dir() -> Path:
+ env_dir = os.environ.get("TREZOR_TRANSLATIONS_DIR")
+ if env_dir:
+ env_dir = Path(env_dir)
+ if env_dir.is_dir():
+ return env_dir
+ raise FileNotFoundError(
+ f"TREZOR_TRANSLATIONS_DIR is set to '{env_dir}' but it is not a valid directory."
+ )
+ else:
+ # Legacy: translations living in the firmware repo
+ legacy = ROOT / "core" / "translations"
+ if legacy.is_dir():
+ return legacy
+
+ raise FileNotFoundError(
+ "Translations directory not found. Set TREZOR_TRANSLATIONS_DIR environment variable."
+ )
+
+
+TRANSLATIONS_DIR = get_translations_dir()
+FONTS_DIR = TRANSLATIONS_DIR / "fonts"
+ORDER_FILE = TRANSLATIONS_DIR / "order.json"
+
+LANGUAGES = [file.stem for file in TRANSLATIONS_DIR.glob("??.json")]
+
+_CURRENT_TRANSLATION = threading.local()
+
+
+def prepare_blob(
+ lang_or_def: translations.JsonDef | Path | str,
+ model: models.TrezorModel,
+ version: translations.VersionTuple | tuple[int, int, int] | None = None,
+) -> translations.TranslationsBlob:
+ """
+ Prepare a translation blob for a given language and device model.
+
+ Args:
+ lang_or_def: Language identifier, JSON definition, or path to JSON file.
+ model: Trezor device model.
+ version: Optional version tuple.
+
+ Returns:
+ TranslationsBlob: The prepared translation blob.
+ """
+ order = translations.order_from_json(json.loads(ORDER_FILE.read_text()))
+ if isinstance(lang_or_def, str):
+ lang_or_def = get_lang_json(lang_or_def)
+ if isinstance(lang_or_def, Path):
+ lang_or_def = t.cast(translations.JsonDef, json.loads(lang_or_def.read_text()))
+
+ # generate raw blob
+ if version is None:
+ version = translations.version_from_json(lang_or_def["header"]["version"])
+ elif len(version) == 3:
+ # version coming from client object does not have build item
+ version = *version, 0
+ return translations.blob_from_defs(lang_or_def, order, model, version, FONTS_DIR)
+
+
+def sign_blob(blob: translations.TranslationsBlob) -> bytes:
+ """
+ Sign a translation blob using the developer private keys.
+
+ Args:
+ blob: The translation blob to sign.
+
+ Returns:
+ bytes: The signed blob as bytes.
+ """
+ # build 0-item Merkle proof
+ digest = sha256(b"\x00" + blob.header_bytes).digest()
+ signature = cosi.sign_with_privkeys(digest, common.PRIVATE_KEYS_DEV)
+ blob.proof = translations.Proof(
+ merkle_proof=[],
+ sigmask=0b111,
+ signature=signature,
+ )
+ return blob.build()
+
+
+def build_and_sign_blob(
+ lang_or_def: translations.JsonDef | Path | str,
+ session: DebugSession,
+) -> bytes:
+ """
+ Prepare and sign a translation blob for a given language and session.
+
+ Args:
+ lang_or_def: Language identifier, JSON definition, or path to JSON file.
+ session: DebugSession object.
+
+ Returns:
+ bytes: The signed translation blob.
+ """
+ blob = prepare_blob(lang_or_def, session.model, session.version)
+ return sign_blob(blob)
+
+
+def set_language(session: DebugSession, lang: str, *, force: bool = False) -> None:
+ """
+ Set the device language for a given session.
+
+ Args:
+ session: DebugSession object.
+ lang: Language code (e.g., 'en', 'cs').
+ force: If True, force setting the language even if already set.
+ """
+ if lang.startswith("en"):
+ language_data = b""
+ else:
+ language_data = build_and_sign_blob(lang, session)
+ with session.test_ctx:
+ language = session.features.language
+ if language is None or not language.startswith(lang) or force:
+ device.change_language(session, language_data)
+ _CURRENT_TRANSLATION.LAYOUT = session.layout_type
+ _CURRENT_TRANSLATION.TR = TRANSLATIONS[lang]
+
+
+def check_language(session: DebugSession, lang: str) -> None:
+ """
+ Assert that the device language matches the expected language.
+
+ Args:
+ session: DebugSession object.
+ lang: Expected language code.
+
+ Raises:
+ RuntimeError: If the device language does not match.
+ """
+ with session.test_ctx:
+ language = session.features.language
+ assert isinstance(language, str)
+ if not language.startswith(lang):
+ raise RuntimeError(
+ f"Incompatible language on device: expected '{lang}', got '{language}'"
+ )
+ _CURRENT_TRANSLATION.LAYOUT = session.layout_type
+ _CURRENT_TRANSLATION.TR = TRANSLATIONS[lang]
+
+
+def get_language() -> str:
+ """
+ Get the current language code.
+
+ Returns:
+ str: The current language code.
+ """
+ for lang in LANGUAGES:
+ if _CURRENT_TRANSLATION.TR == TRANSLATIONS[lang]:
+ return lang
+ return "en"
+
+
+def get_lang_json(lang: str) -> translations.JsonDef:
+ """
+ Load the JSON definition for a given language.
+
+ Args:
+ lang: Language code.
+
+ Returns:
+ JsonDef: The loaded JSON definition.
+
+ Raises:
+ AssertionError: If the language is not available.
+ """
+ assert lang in LANGUAGES
+ lang_json = json.loads((TRANSLATIONS_DIR / f"{lang}.json").read_text())
+ if (fonts_safe3 := lang_json.get("fonts", {}).get("##Safe3")) is not None:
+ lang_json["fonts"]["T2B1"] = fonts_safe3
+ lang_json["fonts"]["T3B1"] = fonts_safe3
+ return lang_json
+
+
+class Translation:
+ """
+ Represents a translation for a specific language.
+
+ Provides methods for translating keys, formatting, and generating regex patterns.
+ """
+
+ FORMAT_STR_RE = re.compile(r"\\{\d+\\}")
+
+ def __init__(self, lang: str) -> None:
+ """
+ Initialize a Translation object.
+
+ Args:
+ lang: Language code.
+ """
+ self.lang = lang
+ self.lang_json = get_lang_json(lang)
+
+ @property
+ def translations(self) -> dict[str, str | dict[str, str]]:
+ """
+ Get the translations dictionary.
+
+ Returns:
+ dict[str, str | dict[str, str]]: The translations dictionary.
+ """
+ return self.lang_json["translations"]
+
+ def _translate_raw(self, key: str, _stacklevel: int = 0) -> str:
+ tr = self.translations.get(key)
+ # Emulate firmware behaviour: English strings are used instead of missing & empty strings
+ if tr:
+ # Handle layout-specific translations
+ if isinstance(tr, dict) and hasattr(_CURRENT_TRANSLATION, "LAYOUT"):
+ # Try to get translation for current layout
+ layout_name = _CURRENT_TRANSLATION.LAYOUT.name
+ if layout_name in tr:
+ return tr[layout_name]
+ # Fall back to any available translation if no match for current layout
+ return next(iter(tr.values()))
+ elif isinstance(tr, str):
+ return tr
+ else:
+ raise ValueError(f"Invalid translation value for key '{key}'")
+ if self.lang != "en":
+ # check if the key exists in English first
+ retval = TRANSLATIONS["en"]._translate_raw(key)
+ # if not, a KeyError was raised so we fall through.
+ # otherwise, warn that the key is untranslated in target language.
+ warnings.warn(
+ f"Translation key '{key}' not found in '{self.lang}' translation file",
+ stacklevel=_stacklevel + 2,
+ )
+ return retval
+ raise KeyError(key)
+
+ def translate(self, key: str, _stacklevel: int = 0) -> str:
+ """
+ Get the translated string for a key.
+
+ Args:
+ key: Translation key.
+ _stacklevel: Internal stacklevel for warnings.
+
+ Returns:
+ str: The translated string.
+ """
+ return self._translate_raw(key, _stacklevel=_stacklevel + 1).strip()
+
+ def as_regexp(self, key: str, _stacklevel: int = 0) -> re.Pattern:
+ """
+ Get a regular expression pattern for a translation key.
+
+ Args:
+ key: Translation key.
+ _stacklevel: Internal stacklevel for warnings.
+
+ Returns:
+ re.Pattern: The compiled regular expression.
+ """
+ tr = self.translate(key, _stacklevel=_stacklevel + 1)
+ re_safe = re.escape(tr)
+ return re.compile(self.FORMAT_STR_RE.sub(r".*?", re_safe))
+
+ def format(self, key: str, *args: t.Any, **kwargs: t.Any) -> str:
+ """
+ Format a translation string with arguments.
+
+ Args:
+ key: Translation key.
+ *args: Positional arguments for formatting.
+ **kwargs: Keyword arguments for formatting.
+
+ Returns:
+ str: The formatted translation string.
+
+ Raises:
+ ValueError: If formatting fails.
+ """
+ tr = self.translate(key)
+ try:
+ return tr.format(*args, **kwargs)
+ except (KeyError, IndexError) as e:
+ raise ValueError(
+ f"Failed to format translation '{key}' with args={args}, kwargs={kwargs}: {e}"
+ ) from e
+
+
+TRANSLATIONS = {lang: Translation(lang) for lang in LANGUAGES}
+_CURRENT_TRANSLATION.TR = TRANSLATIONS["en"]
+_CURRENT_TRANSLATION.LAYOUT = LayoutType.Bolt
+
+
+def translate(key: str, _stacklevel: int = 0) -> str:
+ """
+ Translate a key using the current translation.
+
+ Args:
+ key: Translation key.
+ _stacklevel: Internal stacklevel for warnings.
+
+ Returns:
+ str: The translated string.
+ """
+ return _CURRENT_TRANSLATION.TR.translate(key, _stacklevel=_stacklevel + 1)
+
+
+def regexp(key: str) -> re.Pattern:
+ """
+ Get a regular expression pattern for a translation key.
+
+ Args:
+ key: Translation key.
+
+ Returns:
+ re.Pattern: The compiled regular expression.
+ """
+ return _CURRENT_TRANSLATION.TR.as_regexp(key, _stacklevel=1)
+
+
+def format(key: str, *args: t.Any, **kwargs: t.Any) -> str:
+ """
+ Format a translation string with arguments.
+
+ Args:
+ key: Translation key.
+ *args: Positional arguments for formatting.
+ **kwargs: Keyword arguments for formatting.
+
+ Returns:
+ str: The formatted translation string.
+ """
+ return _CURRENT_TRANSLATION.TR.format(key, *args, **kwargs)
+
+
+def __getattr__(key: str) -> str:
+ """
+ Allow attribute-style access to translations.
+
+ Args:
+ key: Translation key.
+
+ Returns:
+ str: The translated string.
+
+ Raises:
+ AttributeError: If the translation key is not found.
+ """
+ try:
+ return translate(key, _stacklevel=1)
+ except KeyError as e:
+ raise AttributeError(f"Translation key '{key}' not found") from e
diff --git a/tests/click_tests/common.py b/tests/click_tests/common.py
index e9c0e4a9..492f5850 100644
--- a/tests/click_tests/common.py
+++ b/tests/click_tests/common.py
@@ -5,8 +5,7 @@ import typing as t
from enum import Enum
from trezorlib.debuglink import LayoutType
-
-from .. import translations as TR
+from trezorlib.testing import translations as TR
if t.TYPE_CHECKING:
from trezorlib.debuglink import DebugLink, LayoutContent
diff --git a/tests/click_tests/device_menu/common.py b/tests/click_tests/device_menu/common.py
index d70a7e39..4b2cfa7c 100644
--- a/tests/click_tests/device_menu/common.py
+++ b/tests/click_tests/device_menu/common.py
@@ -18,14 +18,12 @@ from enum import Enum, auto
from typing import TYPE_CHECKING, Callable
from trezorlib.messages import BackupAvailability
-
-from ... import translations as TR
+from trezorlib.testing import translations as TR
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import Features
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
PIN4 = "1234"
diff --git a/tests/click_tests/device_menu/test_auto_lock.py b/tests/click_tests/device_menu/test_auto_lock.py
index ef432a72..a61171b5 100644
--- a/tests/click_tests/device_menu/test_auto_lock.py
+++ b/tests/click_tests/device_menu/test_auto_lock.py
@@ -18,7 +18,8 @@ from typing import TYPE_CHECKING
import pytest
-from ... import translations as TR
+from trezorlib.testing import translations as TR
+
from ..test_pin import PIN4
from .common import (
Menu,
@@ -33,8 +34,7 @@ from .common import (
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import Features
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_check_backup.py b/tests/click_tests/device_menu/test_check_backup.py
index 20f8d524..efbe4213 100644
--- a/tests/click_tests/device_menu/test_check_backup.py
+++ b/tests/click_tests/device_menu/test_check_backup.py
@@ -19,8 +19,8 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import messages
+from trezorlib.testing import translations as TR
-from ... import translations as TR
from ...common import MNEMONIC12
from .common import (
Menu,
@@ -34,8 +34,7 @@ from .common import (
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import Features
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_device_settings.py b/tests/click_tests/device_menu/test_device_settings.py
index 8f683e7e..017b300f 100644
--- a/tests/click_tests/device_menu/test_device_settings.py
+++ b/tests/click_tests/device_menu/test_device_settings.py
@@ -18,14 +18,14 @@ from typing import TYPE_CHECKING
import pytest
-from ... import translations as TR
+from trezorlib.testing import translations as TR
+
from .common import Menu, assert_device_screen, close_device_menu, open_device_menu
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import Features
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_label.py b/tests/click_tests/device_menu/test_label.py
index 1a70d012..91120bb8 100644
--- a/tests/click_tests/device_menu/test_label.py
+++ b/tests/click_tests/device_menu/test_label.py
@@ -18,7 +18,8 @@ from typing import TYPE_CHECKING
import pytest
-from ... import translations as TR
+from trezorlib.testing import translations as TR
+
from ..common import KeyboardCategory, delete_char, go_to_category, press_char
from .common import (
Menu,
@@ -31,8 +32,7 @@ from .common import (
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import Features
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_notifications.py b/tests/click_tests/device_menu/test_notifications.py
index addfb235..3c7602f4 100644
--- a/tests/click_tests/device_menu/test_notifications.py
+++ b/tests/click_tests/device_menu/test_notifications.py
@@ -19,8 +19,8 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import messages
+from trezorlib.testing import translations as TR
-from ... import translations as TR
from ..test_pin import PIN4, _assert_pin_entry, _enter_two_times
from .common import (
Menu,
@@ -33,7 +33,7 @@ from .common import (
if TYPE_CHECKING:
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_pin.py b/tests/click_tests/device_menu/test_pin.py
index 1a538aa9..af2163d4 100644
--- a/tests/click_tests/device_menu/test_pin.py
+++ b/tests/click_tests/device_menu/test_pin.py
@@ -19,7 +19,8 @@ from typing import TYPE_CHECKING
import pytest
-from ... import translations as TR
+from trezorlib.testing import translations as TR
+
from ..common import go_next
from ..test_pin import (
PIN1,
@@ -43,8 +44,7 @@ from .common import (
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import Features
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_traverse_menu.py b/tests/click_tests/device_menu/test_traverse_menu.py
index dc40571b..4f0b615e 100644
--- a/tests/click_tests/device_menu/test_traverse_menu.py
+++ b/tests/click_tests/device_menu/test_traverse_menu.py
@@ -21,7 +21,7 @@ import pytest
from .common import PIN4, Menu, close_device_menu, enter_pin, open_device_menu
if TYPE_CHECKING:
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_wipe_code.py b/tests/click_tests/device_menu/test_wipe_code.py
index 0b68e319..33e21032 100644
--- a/tests/click_tests/device_menu/test_wipe_code.py
+++ b/tests/click_tests/device_menu/test_wipe_code.py
@@ -18,7 +18,8 @@ from typing import TYPE_CHECKING
import pytest
-from ... import translations as TR
+from trezorlib.testing import translations as TR
+
from ..common import go_next
from ..test_pin import (
PIN1,
@@ -42,8 +43,7 @@ from .test_pin import Situation, prepare_pin_dialogue
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
-
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/device_menu/test_wipe_device.py b/tests/click_tests/device_menu/test_wipe_device.py
index 7f7ffbca..4bf87458 100644
--- a/tests/click_tests/device_menu/test_wipe_device.py
+++ b/tests/click_tests/device_menu/test_wipe_device.py
@@ -18,11 +18,12 @@ from typing import TYPE_CHECKING
import pytest
-from ... import translations as TR
+from trezorlib.testing import translations as TR
+
from .common import PIN4, Menu, enter_pin, open_device_menu
if TYPE_CHECKING:
- from ...device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
pytestmark = [pytest.mark.models("eckhart")]
diff --git a/tests/click_tests/recovery.py b/tests/click_tests/recovery.py
index 7504524d..4f4f6cbe 100644
--- a/tests/click_tests/recovery.py
+++ b/tests/click_tests/recovery.py
@@ -1,8 +1,8 @@
from typing import TYPE_CHECKING
from trezorlib.debuglink import LayoutType
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..recovery_helpers import navigate_to_keyboard
from .common import go_next
diff --git a/tests/click_tests/reset.py b/tests/click_tests/reset.py
index 4f3b139e..86d9ae62 100644
--- a/tests/click_tests/reset.py
+++ b/tests/click_tests/reset.py
@@ -4,8 +4,7 @@ from typing import TYPE_CHECKING
from shamir_mnemonic import shamir # type: ignore
from trezorlib.debuglink import LayoutType
-
-from .. import translations as TR
+from trezorlib.testing import translations as TR
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
diff --git a/tests/click_tests/test_autolock.py b/tests/click_tests/test_autolock.py
index 689459c2..cc79b279 100644
--- a/tests/click_tests/test_autolock.py
+++ b/tests/click_tests/test_autolock.py
@@ -24,10 +24,10 @@ from trezorlib import btc, device, exceptions, messages
from trezorlib.client import PassphraseSetting
from trezorlib.debuglink import DebugLink, LayoutType
from trezorlib.protobuf import MessageType
+from trezorlib.testing import translations as TR
from trezorlib.tools import parse_path
from .. import common
-from .. import translations as TR
from ..device_tests.bitcoin.coinjoin_req import make_coinjoin_request
from ..tx_cache import TxCache
from . import recovery
@@ -35,8 +35,7 @@ from .common import go_next, unlock_gesture
if TYPE_CHECKING:
from trezorlib.debuglink import LayoutContent
-
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
TX_CACHE_MAINNET = TxCache("Bitcoin")
TX_CACHE_TESTNET = TxCache("Testnet")
diff --git a/tests/click_tests/test_backup_slip39_custom.py b/tests/click_tests/test_backup_slip39_custom.py
index e380c29f..e874485c 100644
--- a/tests/click_tests/test_backup_slip39_custom.py
+++ b/tests/click_tests/test_backup_slip39_custom.py
@@ -20,13 +20,13 @@ import pytest
from trezorlib import device, messages
from trezorlib.debuglink import LayoutType
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy
from . import reset
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_lock.py b/tests/click_tests/test_lock.py
index 38fb91e1..bacd4bca 100644
--- a/tests/click_tests/test_lock.py
+++ b/tests/click_tests/test_lock.py
@@ -23,7 +23,7 @@ from trezorlib import messages, models
from trezorlib.debuglink import LayoutType
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
PIN4 = "1234"
diff --git a/tests/click_tests/test_passphrase_bde.py b/tests/click_tests/test_passphrase_bde.py
index 58e20fd4..50327a62 100644
--- a/tests/click_tests/test_passphrase_bde.py
+++ b/tests/click_tests/test_passphrase_bde.py
@@ -36,8 +36,7 @@ from .common import ( # KEYBOARD_CATEGORY,
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
-
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("t2t1", "delizia", "eckhart")
diff --git a/tests/click_tests/test_pin.py b/tests/click_tests/test_pin.py
index e13d4f4b..89085a07 100644
--- a/tests/click_tests/test_pin.py
+++ b/tests/click_tests/test_pin.py
@@ -23,14 +23,13 @@ import pytest
from trezorlib import device, exceptions
from trezorlib.debuglink import DisplayStyle, LayoutType
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from .common import go_next, navigate_to_action_and_press
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
-
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_recovery.py b/tests/click_tests/test_recovery.py
index be3136f9..f83f71e0 100644
--- a/tests/click_tests/test_recovery.py
+++ b/tests/click_tests/test_recovery.py
@@ -29,8 +29,7 @@ from .test_autolock import PIN4, set_autolock_delay, unlock_dry_run
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
-
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_repeated_backup.py b/tests/click_tests/test_repeated_backup.py
index f7a0ef23..e71fa169 100644
--- a/tests/click_tests/test_repeated_backup.py
+++ b/tests/click_tests/test_repeated_backup.py
@@ -19,14 +19,14 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import device, exceptions, messages
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..common import MOCK_GET_ENTROPY, LayoutType
from . import recovery, reset
from .common import go_next
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_reset_bip39.py b/tests/click_tests/test_reset_bip39.py
index be257aaf..cfce2503 100644
--- a/tests/click_tests/test_reset_bip39.py
+++ b/tests/click_tests/test_reset_bip39.py
@@ -19,14 +19,14 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import device, messages
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..common import MOCK_GET_ENTROPY
from . import reset
from .common import LayoutType, go_next
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_reset_slip39_advanced.py b/tests/click_tests/test_reset_slip39_advanced.py
index 12521bf8..d2f65196 100644
--- a/tests/click_tests/test_reset_slip39_advanced.py
+++ b/tests/click_tests/test_reset_slip39_advanced.py
@@ -19,13 +19,13 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import device, messages
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy
from . import reset
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_reset_slip39_basic.py b/tests/click_tests/test_reset_slip39_basic.py
index d8f48767..3a156298 100644
--- a/tests/click_tests/test_reset_slip39_basic.py
+++ b/tests/click_tests/test_reset_slip39_basic.py
@@ -19,13 +19,13 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import device, messages
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, LayoutType, generate_entropy
from . import reset
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
pytestmark = pytest.mark.models("core")
diff --git a/tests/click_tests/test_tutorial_caesar.py b/tests/click_tests/test_tutorial_caesar.py
index aa03518a..103a6cb9 100644
--- a/tests/click_tests/test_tutorial_caesar.py
+++ b/tests/click_tests/test_tutorial_caesar.py
@@ -21,13 +21,11 @@ import pytest
from trezorlib import device
from trezorlib.exceptions import Cancelled
-
-from .. import translations as TR
+from trezorlib.testing import translations as TR
if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
-
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Safe family only
diff --git a/tests/click_tests/test_tutorial_delizia.py b/tests/click_tests/test_tutorial_delizia.py
index 3bcd2df6..49695528 100644
--- a/tests/click_tests/test_tutorial_delizia.py
+++ b/tests/click_tests/test_tutorial_delizia.py
@@ -19,11 +19,10 @@ from typing import TYPE_CHECKING
import pytest
from trezorlib import device
-
-from .. import translations as TR
+from trezorlib.testing import translations as TR
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 5 only
diff --git a/tests/click_tests/test_tutorial_eckhart.py b/tests/click_tests/test_tutorial_eckhart.py
index 37f61c23..513e801a 100644
--- a/tests/click_tests/test_tutorial_eckhart.py
+++ b/tests/click_tests/test_tutorial_eckhart.py
@@ -20,11 +20,10 @@ import pytest
from trezorlib import device, exceptions
from trezorlib.debuglink import DebugLink
-
-from .. import translations as TR
+from trezorlib.testing import translations as TR
if TYPE_CHECKING:
- from ..device_handler import BackgroundDeviceHandler
+ from trezorlib.testing.device_handler import BackgroundDeviceHandler
# Trezor Safe 7 only
diff --git a/tests/common.py b/tests/common.py
index be9ab1f8..a532a025 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -36,10 +36,6 @@ if TYPE_CHECKING:
from trezorlib.debuglink import DebugLink
from trezorlib.messages import ButtonRequest
-PRIVATE_KEYS_DEV = [byte * 32 for byte in (b"\xdd", b"\xde", b"\xdf")]
-
-BRGeneratorType = Generator[None, messages.ButtonRequest, None]
-
# fmt: off
# 1 2 3 4 5 6 7 8 9 10 11 12
@@ -386,44 +382,5 @@ def get_test_address(session: "Session") -> str:
return btc.get_address(session, "Testnet", TEST_ADDRESS_N)
-def compact_size(n: int) -> bytes:
- if n < 253:
- return n.to_bytes(1, "little")
- elif n < 0x1_0000:
- return bytes([253]) + n.to_bytes(2, "little")
- elif n < 0x1_0000_0000:
- return bytes([254]) + n.to_bytes(4, "little")
- else:
- return bytes([255]) + n.to_bytes(8, "little")
-
-
-def get_text_possible_pagination(debug: "DebugLink", br: messages.ButtonRequest) -> str:
- text = debug.read_layout().text_content()
- if br.pages is not None:
- for _ in range(br.pages - 1):
- if debug.layout_type is LayoutType.Eckhart:
- debug.click(debug.screen_buttons.ok())
- else:
- debug.swipe_up()
- text += " "
- text += debug.read_layout().text_content()
- return text
-
-
-def swipe_if_necessary(
- debug: "DebugLink", br_code: messages.ButtonRequestType | None = None
-) -> BRGeneratorType:
- br = yield
- if br_code is not None:
- assert br.code == br_code
- swipe_till_the_end(debug, br)
-
-
-def swipe_till_the_end(debug: "DebugLink", br: messages.ButtonRequest) -> None:
- if br.pages is not None:
- for _ in range(br.pages - 1):
- debug.swipe_up()
-
-
def is_core(session: Client | Session) -> bool:
return session.model is not models.T1B1
diff --git a/tests/conftest.py b/tests/conftest.py
index c6dea67b..b7128491 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -39,8 +39,10 @@ from trezorlib.transport.ble import BleTransport
# so that we see details of failed asserts from this module
pytest.register_assert_rewrite("tests.common")
-from . import translations, ui_tests
-from .device_handler import BackgroundDeviceHandler
+from trezorlib.testing import translations
+from trezorlib.testing.device_handler import BackgroundDeviceHandler
+
+from . import ui_tests
from .emulators import EmulatorWrapper
if t.TYPE_CHECKING:
diff --git a/tests/definitions.py b/tests/definitions.py
index a18996cd..a83350ab 100644
--- a/tests/definitions.py
+++ b/tests/definitions.py
@@ -5,8 +5,7 @@ import typing as t
from hashlib import sha256
from trezorlib import cosi, definitions, messages, protobuf
-
-from .common import PRIVATE_KEYS_DEV
+from trezorlib.testing.common import PRIVATE_KEYS_DEV
def make_eth_network(
diff --git a/tests/device_handler.py b/tests/device_handler.py
deleted file mode 100644
index 3f25f9af..00000000
--- a/tests/device_handler.py
+++ /dev/null
@@ -1,149 +0,0 @@
-from __future__ import annotations
-
-import typing as t
-from concurrent.futures import ThreadPoolExecutor
-
-import typing_extensions as tx
-
-from trezorlib.messages import DebugWaitType
-from trezorlib.transport import udp
-
-if t.TYPE_CHECKING:
- from trezorlib._internal.emulator import Emulator
- from trezorlib.client import Session
- from trezorlib.debuglink import DebugLink
- from trezorlib.debuglink import TrezorTestContext as Client
- from trezorlib.messages import Features
-
- P = tx.ParamSpec("P")
-
-
-udp.SOCKET_TIMEOUT = 0.1
-
-
-class NullUI:
- @staticmethod
- def clear(*args, **kwargs):
- pass
-
- @staticmethod
- def button_request(code):
- pass
-
- @staticmethod
- def get_pin(code=None):
- raise NotImplementedError("NullUI should not be used with T1")
-
-
-class BackgroundDeviceHandler:
- _pool = ThreadPoolExecutor()
-
- def __init__(self, client: "Client", nowait: bool = False) -> None:
- self._configure_client(client)
- self.task = None
- self.nowait = nowait
-
- def _configure_client(self, client: "Client") -> None:
- self.client = client
- self.client.ui = NullUI # type: ignore [NullUI is OK UI]
- self.client.app.button_callback = self.client.ui.button_request
- self.client.debug.input_wait_type = DebugWaitType.CURRENT_LAYOUT
-
- def get_session(self, *args, **kwargs):
- if self.task is not None:
- raise RuntimeError("Wait for previous task first")
-
- with self.debuglink().wait_for_layout_change():
- self.task = self._pool.submit(self.client.get_session, *args, **kwargs)
-
- def run_with_session(
- self,
- function: t.Callable[tx.Concatenate["Session", P], t.Any],
- seedless: bool = False,
- *args: P.args,
- **kwargs: P.kwargs,
- ) -> None:
- """Runs some function that interacts with a device.
-
- Makes sure the UI is updated before returning.
- """
- if self.task is not None:
- raise RuntimeError("Wait for previous task first")
-
- def task_function(*args, **kwargs):
- if seedless:
- session = self.client.get_seedless_session()
- else:
- session = self.client.get_session()
- return function(session, *args, **kwargs)
-
- # wait for the first UI change triggered by the task running in the background
- with self.debuglink().wait_for_layout_change():
- self.task = self._pool.submit(task_function, *args, **kwargs)
-
- def run_with_provided_session(
- self,
- session,
- function: t.Callable[tx.Concatenate["Session", P], t.Any],
- *args: P.args,
- **kwargs: P.kwargs,
- ) -> None:
- """Runs some function that interacts with a device.
-
- Makes sure the UI is updated before returning.
- """
- if self.task is not None:
- raise RuntimeError("Wait for previous task first")
-
- # wait for the first UI change triggered by the task running in the background
- with self.debuglink().wait_for_layout_change():
- self.task = self._pool.submit(function, session, *args, **kwargs)
-
- def kill_task(self) -> None:
- if self.task is not None:
- # Force close the transport, which should raise an exception in a client
- # waiting on IO. Does not work over Bridge, because bridge doesn't have
- # a close() method.
- self.client.transport.close()
- try:
- self.task.result(timeout=1)
- except Exception:
- pass
- self.task = None
-
- def restart(self, emulator: "Emulator") -> None:
- # TODO handle actual restart as well
- self.kill_task()
- emulator.restart()
- self._configure_client(emulator.client) # type: ignore [client cannot be None]
-
- def result(self, timeout: float | None = None) -> t.Any:
- if self.task is None:
- raise RuntimeError("No task running")
- try:
- return self.task.result(timeout=timeout)
- finally:
- self.task = None
-
- def features(self) -> "Features":
- if self.task is not None:
- raise RuntimeError("Cannot query features while task is running")
- self.client.refresh_features()
- return self.client.features
-
- def debuglink(self) -> "DebugLink":
- return self.client.debug
-
- def check_finalize(self) -> bool:
- if self.task is not None:
- self.kill_task()
- return False
- return True
-
- def __enter__(self) -> "BackgroundDeviceHandler":
- return self
-
- def __exit__(self, exc_type, exc_value, traceback) -> None:
- finalized_ok = self.check_finalize()
- if exc_type is None and not finalized_ok:
- raise RuntimeError("Exit while task is unfinished")
diff --git a/tests/device_tests/evolu/common.py b/tests/device_tests/evolu/common.py
index 38de6d69..141c854d 100644
--- a/tests/device_tests/evolu/common.py
+++ b/tests/device_tests/evolu/common.py
@@ -8,10 +8,9 @@ from trezorlib import evolu
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.messages import EvoluDelegatedIdentityKey, ThpCredentialResponse
+from trezorlib.testing.common import compact_size
from trezorlib.thp import curve25519
-from ...common import compact_size
-
TEST_randomness = os.urandom(32)
TEST_host_static_private_key = curve25519.get_private_key(TEST_randomness)
TEST_host_static_public_key = curve25519.get_public_key(TEST_host_static_private_key)
diff --git a/tests/device_tests/evolu/test_sign_registration.py b/tests/device_tests/evolu/test_sign_registration.py
index c5bcfa2d..2471d334 100644
--- a/tests/device_tests/evolu/test_sign_registration.py
+++ b/tests/device_tests/evolu/test_sign_registration.py
@@ -4,8 +4,8 @@ from ecdsa import NIST256p, SigningKey, VerifyingKey
from trezorlib import evolu
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.exceptions import TrezorFailure
+from trezorlib.testing.common import compact_size
-from ...common import compact_size
from ..certificate import check_signature_optiga
from .common import get_delegated_identity_key, get_invalid_proof, get_proof
diff --git a/tests/device_tests/misc/test_msg_enablelabeling.py b/tests/device_tests/misc/test_msg_enablelabeling.py
index cd883e7a..8b2f4d0a 100644
--- a/tests/device_tests/misc/test_msg_enablelabeling.py
+++ b/tests/device_tests/misc/test_msg_enablelabeling.py
@@ -18,8 +18,8 @@ import pytest
from trezorlib import misc
from trezorlib.debuglink import TrezorTestContext as Client
+from trezorlib.testing import translations as TR
-from ... import translations as TR
from ...common import MNEMONIC12
diff --git a/tests/device_tests/payment_req.py b/tests/device_tests/payment_req.py
index e1c14435..6b33735a 100644
--- a/tests/device_tests/payment_req.py
+++ b/tests/device_tests/payment_req.py
@@ -5,8 +5,7 @@ from ecdsa import NIST256p, SigningKey
from trezorlib import messages
from trezorlib.client import Session
-
-from ..common import compact_size
+from trezorlib.testing.common import compact_size
SLIP44_ID_UNDEFINED = 0xFFFF_FFFF
diff --git a/tests/device_tests/reset_recovery/test_recovery_restart.py b/tests/device_tests/reset_recovery/test_recovery_restart.py
index b0e7dc9a..22b2e390 100644
--- a/tests/device_tests/reset_recovery/test_recovery_restart.py
+++ b/tests/device_tests/reset_recovery/test_recovery_restart.py
@@ -16,12 +16,12 @@
import pytest
-from tests import translations as TR
from trezorlib import device
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import LayoutType
from trezorlib.exceptions import Cancelled
from trezorlib.messages import BackupMethod, Capability, RecoveryStatus
+from trezorlib.testing import translations as TR
from ...input_flows import RecoveryFlow
diff --git a/tests/device_tests/reset_recovery/test_reset_bip39_t1.py b/tests/device_tests/reset_recovery/test_reset_bip39_t1.py
index 44ad3c61..934c9e16 100644
--- a/tests/device_tests/reset_recovery/test_reset_bip39_t1.py
+++ b/tests/device_tests/reset_recovery/test_reset_bip39_t1.py
@@ -25,14 +25,10 @@ from trezorlib.btc import get_public_node
from trezorlib.debuglink import DebugLink
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import TrezorTestContext
+from trezorlib.testing.common import BRGeneratorType
from trezorlib.tools import parse_path
-from ...common import (
- EXTERNAL_ENTROPY,
- MOCK_GET_ENTROPY,
- BRGeneratorType,
- generate_entropy,
-)
+from ...common import EXTERNAL_ENTROPY, MOCK_GET_ENTROPY, generate_entropy
pytestmark = pytest.mark.models("legacy")
diff --git a/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py b/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
index a1e11bcc..3e23b5ff 100644
--- a/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
+++ b/tests/device_tests/reset_recovery/test_reset_recovery_bip39.py
@@ -20,11 +20,11 @@ from trezorlib import btc, device, messages
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.messages import BackupMethod, BackupType
+from trezorlib.testing.translations import set_language
from trezorlib.tools import parse_path
from ...common import MOCK_GET_ENTROPY
from ...input_flows import InputFlowBip39Recovery, InputFlowBip39ResetBackup
-from ...translations import set_language
@pytest.mark.models("core")
diff --git a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
index cd8fe808..7e9f17e8 100644
--- a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
+++ b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_advanced.py
@@ -20,6 +20,7 @@ from trezorlib import btc, device, messages
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.messages import BackupMethod, BackupType
+from trezorlib.testing.translations import set_language
from trezorlib.tools import parse_path
from ...common import MOCK_GET_ENTROPY
@@ -27,7 +28,6 @@ from ...input_flows import (
InputFlowSlip39AdvancedRecovery,
InputFlowSlip39AdvancedResetRecovery,
)
-from ...translations import set_language
@pytest.mark.models("core")
diff --git a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
index aad2e0cc..f817d108 100644
--- a/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
+++ b/tests/device_tests/reset_recovery/test_reset_recovery_slip39_basic.py
@@ -23,6 +23,7 @@ from trezorlib import btc, device, messages
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.messages import BackupMethod, BackupType
+from trezorlib.testing.translations import set_language
from trezorlib.tools import parse_path
from ...common import MOCK_GET_ENTROPY
@@ -30,7 +31,6 @@ from ...input_flows import (
InputFlowSlip39BasicRecovery,
InputFlowSlip39BasicResetRecovery,
)
-from ...translations import set_language
@pytest.mark.models("core")
diff --git a/tests/device_tests/test_authenticate_device.py b/tests/device_tests/test_authenticate_device.py
index 5d1e2d94..a5c8ac9e 100644
--- a/tests/device_tests/test_authenticate_device.py
+++ b/tests/device_tests/test_authenticate_device.py
@@ -2,8 +2,8 @@ import pytest
from trezorlib import device, exceptions, messages
from trezorlib.debuglink import DebugSession as Session
+from trezorlib.testing.common import compact_size
-from ..common import compact_size
from .certificate import check_signature_optiga, check_signature_tropic
# The tests below require Optiga (and some require Tropic)
diff --git a/tests/device_tests/test_language.py b/tests/device_tests/test_language.py
index f3d463b1..589b02dc 100644
--- a/tests/device_tests/test_language.py
+++ b/tests/device_tests/test_language.py
@@ -26,8 +26,7 @@ from trezorlib._internal import translations
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.debuglink import message_filters
-
-from ..translations import (
+from trezorlib.testing.translations import (
LANGUAGES,
build_and_sign_blob,
get_lang_json,
diff --git a/tests/device_tests/test_repeated_backup.py b/tests/device_tests/test_repeated_backup.py
index fecdf4c7..f1a70923 100644
--- a/tests/device_tests/test_repeated_backup.py
+++ b/tests/device_tests/test_repeated_backup.py
@@ -20,8 +20,8 @@ import pytest
from trezorlib import device, exceptions, messages
from trezorlib.debuglink import DebugSession as Session
from trezorlib.exceptions import Cancelled, TrezorFailure
+from trezorlib.testing import translations as TR
-from .. import translations as TR
from ..common import (
MNEMONIC_SLIP39_SINGLE_EXT_20,
TEST_ADDRESS_N,
diff --git a/tests/device_tests/test_sdcard.py b/tests/device_tests/test_sdcard.py
index 34409efc..23843ec2 100644
--- a/tests/device_tests/test_sdcard.py
+++ b/tests/device_tests/test_sdcard.py
@@ -26,7 +26,7 @@ from trezorlib.messages import SdProtectOperationType as Op
B = messages.ButtonRequestType
-from .. import translations as TR
+from trezorlib.testing import translations as TR
PIN = "1234"
diff --git a/tests/device_tests/test_session_id_and_passphrase.py b/tests/device_tests/test_session_id_and_passphrase.py
index 77caace4..b5cd6da5 100644
--- a/tests/device_tests/test_session_id_and_passphrase.py
+++ b/tests/device_tests/test_session_id_and_passphrase.py
@@ -25,10 +25,9 @@ from trezorlib.debuglink import LayoutType, TrezorTestContext
from trezorlib.exceptions import TrezorFailure
from trezorlib.messages import FailureType, SafetyCheckLevel
from trezorlib.protocol_v1 import SessionV1, TrezorClientV1
+from trezorlib.testing import translations as TR
from trezorlib.tools import parse_path
-from .. import translations as TR
-
pytestmark = pytest.mark.protocol("v1")
XPUB_PASSPHRASES = {
diff --git a/tests/input_flows.py b/tests/input_flows.py
index 910bd923..b0a624fb 100644
--- a/tests/input_flows.py
+++ b/tests/input_flows.py
@@ -22,17 +22,19 @@ from trezorlib.debuglink import DebugLink, DebugSession, LayoutContent, LayoutTy
from trezorlib.debuglink import TrezorTestContext as Client
from trezorlib.debuglink import multipage_content
from trezorlib.exceptions import TrezorFailure
+from trezorlib.testing import translations as TR
+from trezorlib.testing.common import (
+ BRGeneratorType,
+ get_text_possible_pagination,
+ swipe_if_necessary,
+)
-from . import translations as TR
from .common import (
- BRGeneratorType,
check_pin_backoff_time,
click_info_button_bolt,
click_info_button_delizia_eckhart,
click_through,
- get_text_possible_pagination,
read_and_confirm_mnemonic,
- swipe_if_necessary,
)
from .input_flows_helpers import (
BackupFlow,
diff --git a/tests/input_flows_helpers.py b/tests/input_flows_helpers.py
index c42981e0..1aa2492d 100644
--- a/tests/input_flows_helpers.py
+++ b/tests/input_flows_helpers.py
@@ -3,9 +3,8 @@ import typing as t
from trezorlib import messages
from trezorlib.debuglink import DebugLink, LayoutType
from trezorlib.debuglink import TrezorTestContext as Client
-
-from . import translations as TR
-from .common import BRGeneratorType, get_text_possible_pagination
+from trezorlib.testing import translations as TR
+from trezorlib.testing.common import BRGeneratorType, get_text_possible_pagination
B = messages.ButtonRequestType
diff --git a/tests/persistence_tests/test_shamir_persistence.py b/tests/persistence_tests/test_shamir_persistence.py
index 3d75838b..fe0dab7c 100644
--- a/tests/persistence_tests/test_shamir_persistence.py
+++ b/tests/persistence_tests/test_shamir_persistence.py
@@ -19,11 +19,11 @@ import pytest
from trezorlib import device, messages
from trezorlib.debuglink import DebugLink, LayoutType
from trezorlib.messages import RecoveryStatus
+from trezorlib.testing import translations as TR
+from trezorlib.testing.device_handler import BackgroundDeviceHandler
-from .. import translations as TR
from ..click_tests import common, recovery
from ..common import MNEMONIC_SLIP39_ADVANCED_20, MNEMONIC_SLIP39_BASIC_20_3of6
-from ..device_handler import BackgroundDeviceHandler
from ..emulators import Emulator
from ..upgrade_tests import core_only
diff --git a/tests/translations.py b/tests/translations.py
deleted file mode 100644
index 53bfc081..00000000
--- a/tests/translations.py
+++ /dev/null
@@ -1,175 +0,0 @@
-from __future__ import annotations
-
-import json
-import re
-import threading
-import typing as t
-import warnings
-from hashlib import sha256
-from pathlib import Path
-
-from trezorlib import cosi, device, models
-from trezorlib._internal import translations
-from trezorlib.debuglink import DebugSession, LayoutType
-
-from . import common
-
-HERE = Path(__file__).resolve().parent
-ROOT = HERE.parent
-
-TRANSLATIONS_DIR = ROOT / "core" / "translations"
-FONTS_DIR = TRANSLATIONS_DIR / "fonts"
-ORDER_FILE = TRANSLATIONS_DIR / "order.json"
-
-LANGUAGES = [file.stem for file in TRANSLATIONS_DIR.glob("??.json")]
-
-_CURRENT_TRANSLATION = threading.local()
-
-
-def prepare_blob(
- lang_or_def: translations.JsonDef | Path | str,
- model: models.TrezorModel,
- version: translations.VersionTuple | tuple[int, int, int] | None = None,
-) -> translations.TranslationsBlob:
- order = translations.order_from_json(json.loads(ORDER_FILE.read_text()))
- if isinstance(lang_or_def, str):
- lang_or_def = get_lang_json(lang_or_def)
- if isinstance(lang_or_def, Path):
- lang_or_def = t.cast(translations.JsonDef, json.loads(lang_or_def.read_text()))
-
- # generate raw blob
- if version is None:
- version = translations.version_from_json(lang_or_def["header"]["version"])
- elif len(version) == 3:
- # version coming from client object does not have build item
- version = *version, 0
- return translations.blob_from_defs(lang_or_def, order, model, version, FONTS_DIR)
-
-
-def sign_blob(blob: translations.TranslationsBlob) -> bytes:
- # build 0-item Merkle proof
- digest = sha256(b"\x00" + blob.header_bytes).digest()
- signature = cosi.sign_with_privkeys(digest, common.PRIVATE_KEYS_DEV)
- blob.proof = translations.Proof(
- merkle_proof=[],
- sigmask=0b111,
- signature=signature,
- )
- return blob.build()
-
-
-def build_and_sign_blob(
- lang_or_def: translations.JsonDef | Path | str,
- session: DebugSession,
-) -> bytes:
- blob = prepare_blob(lang_or_def, session.model, session.version)
- return sign_blob(blob)
-
-
-def set_language(session: DebugSession, lang: str, *, force: bool = False):
- if lang.startswith("en"):
- language_data = b""
- else:
- language_data = build_and_sign_blob(lang, session)
- with session.test_ctx:
- if not session.features.language.startswith(lang) or force:
- device.change_language(session, language_data) # type: ignore
- _CURRENT_TRANSLATION.LAYOUT = session.layout_type
- _CURRENT_TRANSLATION.TR = TRANSLATIONS[lang]
-
-
-def get_language() -> str:
- for lang in LANGUAGES:
- if _CURRENT_TRANSLATION.TR == TRANSLATIONS[lang]:
- return lang
- return "en"
-
-
-def get_lang_json(lang: str) -> translations.JsonDef:
- assert lang in LANGUAGES
- lang_json = json.loads((TRANSLATIONS_DIR / f"{lang}.json").read_text())
- if (fonts_safe3 := lang_json.get("fonts", {}).get("##Safe3")) is not None:
- lang_json["fonts"]["T2B1"] = fonts_safe3
- lang_json["fonts"]["T3B1"] = fonts_safe3
- return lang_json
-
-
-class Translation:
- FORMAT_STR_RE = re.compile(r"\\{\d+\\}")
-
- def __init__(self, lang: str) -> None:
- self.lang = lang
- self.lang_json = get_lang_json(lang)
-
- @property
- def translations(self) -> dict[str, str | dict[str, str]]:
- return self.lang_json["translations"]
-
- def _translate_raw(self, key: str, _stacklevel: int = 0) -> str:
- tr = self.translations.get(key)
- # Emulate firmware behaviour: English strings are used instead of missing & empty strings
- if tr:
- # Handle layout-specific translations
- if isinstance(tr, dict) and hasattr(_CURRENT_TRANSLATION, "LAYOUT"):
- # Try to get translation for current layout
- layout_name = _CURRENT_TRANSLATION.LAYOUT.name
- if layout_name in tr:
- return tr[layout_name]
- # Fall back to any available translation if no match for current layout
- return next(iter(tr.values()))
- elif isinstance(tr, str):
- return tr
- else:
- raise ValueError(f"Invalid translation value for key '{key}'")
- if self.lang != "en":
- # check if the key exists in English first
- retval = TRANSLATIONS["en"]._translate_raw(key)
- # if not, a KeyError was raised so we fall through.
- # otherwise, warn that the key is untranslated in target language.
- warnings.warn(
- f"Translation key '{key}' not found in '{self.lang}' translation file",
- stacklevel=_stacklevel + 2,
- )
- return retval
- raise KeyError(key)
-
- def translate(self, key: str, _stacklevel: int = 0) -> str:
- return self._translate_raw(key, _stacklevel=_stacklevel + 1).strip()
-
- def as_regexp(self, key: str, _stacklevel: int = 0) -> re.Pattern:
- tr = self.translate(key, _stacklevel=_stacklevel + 1)
- re_safe = re.escape(tr)
- return re.compile(self.FORMAT_STR_RE.sub(r".*?", re_safe))
-
- def format(self, key: str, *args, **kwargs) -> str:
- tr = self.translate(key)
- try:
- return tr.format(*args, **kwargs)
- except (KeyError, IndexError) as e:
- raise ValueError(
- f"Failed to format translation '{key}' with args={args}, kwargs={kwargs}: {e}"
- ) from e
-
-
-TRANSLATIONS = {lang: Translation(lang) for lang in LANGUAGES}
-_CURRENT_TRANSLATION.TR = TRANSLATIONS["en"]
-_CURRENT_TRANSLATION.LAYOUT = LayoutType.Bolt
-
-
-def translate(key: str, _stacklevel: int = 0) -> str:
- return _CURRENT_TRANSLATION.TR.translate(key, _stacklevel=_stacklevel + 1)
-
-
-def regexp(key: str) -> re.Pattern:
- return _CURRENT_TRANSLATION.TR.as_regexp(key, _stacklevel=1)
-
-
-def format(key: str, *args, **kwargs) -> str:
- return _CURRENT_TRANSLATION.TR.format(key, *args, **kwargs)
-
-
-def __getattr__(key: str) -> str:
- try:
- return translate(key, _stacklevel=1)
- except KeyError as e:
- raise AttributeError(f"Translation key '{key}' not found") from e
diff --git a/tests/upgrade_tests/test_firmware_upgrades.py b/tests/upgrade_tests/test_firmware_upgrades.py
index d7bd3174..132b434d 100644
--- a/tests/upgrade_tests/test_firmware_upgrades.py
+++ b/tests/upgrade_tests/test_firmware_upgrades.py
@@ -33,11 +33,11 @@ from trezorlib.messages import (
RecoveryStatus,
Success,
)
+from trezorlib.testing.device_handler import BackgroundDeviceHandler
from trezorlib.tools import H_, parse_path
from ..click_tests import recovery
from ..common import MNEMONIC_SLIP39_BASIC_20_3of6, MNEMONIC_SLIP39_BASIC_20_3of6_SECRET
-from ..device_handler import BackgroundDeviceHandler
from ..emulators import EmulatorWrapper
from ..input_flows import InputFlowSlip39BasicBackup
from . import ALL_TAGS, for_all, for_tags, recovery_old, version_from_tag
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.