ragger 1.41.1__tar.gz → 1.42.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ragger-1.41.1 → ragger-1.42.0}/CHANGELOG.md +12 -0
- {ragger-1.41.1/src/ragger.egg-info → ragger-1.42.0}/PKG-INFO +1 -1
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/__version__.py +3 -3
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/speculos.py +27 -2
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/conftest/base_conftest.py +54 -19
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/error.py +8 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/utils/misc.py +3 -3
- {ragger-1.41.1 → ragger-1.42.0/src/ragger.egg-info}/PKG-INFO +1 -1
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/backend/test_speculos.py +27 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/utils/test_misc.py +2 -2
- {ragger-1.41.1 → ragger-1.42.0}/.codeql-config.yaml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.gitattributes +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.github/workflows/build_and_tests.yml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.github/workflows/codeql-analysis.yml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.github/workflows/documentation.yml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.github/workflows/fast-checks.yml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.github/workflows/force-rebase.yml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/.gitignore +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/LICENSE +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/MANIFEST.in +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/README.md +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/Makefile +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/_static/layout.css +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/_templates/layout.html +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/architecture.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/conf.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/faq.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/glossary.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/navigate.draw +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/navigate.svg +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/ragger.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/stax_infos.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/stax_welcome.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/usage.draw +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/images/usage.svg +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/index.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/installation.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/rationale.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/source.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/tutorial.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/tutorial_conftest.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/tutorial_installation.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/doc/tutorial_screen.rst +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/ledger_app.toml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/pyproject.toml +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/setup.cfg +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/interface.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/ledgercomm.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/ledgerwallet.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/physical_backend.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/backend/stub.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/bip/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/bip/path.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/bip/seed.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/conftest/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/conftest/configuration.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/structs.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/touch/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/touch/element.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/touch/layouts.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/touch/positions.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/touch/screen.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/firmware/touch/use_cases.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanos_body.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanos_leftbutton.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanos_rightbutton.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanosp_body.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanosp_leftbutton.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanosp_rightbutton.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanox_body.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanox_leftbutton.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/nanox_rightbutton.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/stax_body.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/swipe_left_action.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/swipe_right_action.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/assets/touch_action.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/interface.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/gui/process.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/logger.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/navigator/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/navigator/instruction.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/navigator/nano_navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/navigator/navigation_scenario.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/navigator/navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/navigator/touch_navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/py.typed +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/utils/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/utils/packing.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger/utils/structs.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger.egg-info/SOURCES.txt +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger.egg-info/dependency_links.txt +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger.egg-info/requires.txt +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/src/ragger.egg-info/top_level.txt +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/template/.dispatch_to_your_test_folder +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/template/conftest.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/template/usage.md +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/backend/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/conftest.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/navigator/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/navigator/test_navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/functional/test_boilerplate.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/pytest.ini +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/apex_p/waiting_screen/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/apex_p/waiting_screen/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/apex_p/waiting_screen/00002.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/apex_p/waiting_screen/00003.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/apex_p/waiting_screen/00004.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/flex/waiting_screen/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/flex/waiting_screen/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/flex/waiting_screen/00002.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/flex/waiting_screen/00003.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/flex/waiting_screen/00004.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/generic/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/generic/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/generic/00002.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/generic/00003.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/generic/00004.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00002.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00003.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00004.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00005.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00006.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare/00007.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare_no_golden/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare_wrong_golden/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare_wrong_golden/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00002.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00003.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/stax/waiting_screen/00000.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/stax/waiting_screen/00001.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/stax/waiting_screen/00002.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/stax/waiting_screen/00003.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/stax/waiting_screen/00004.png +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/stubs.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/test_interface.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/test_ledgercomm.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/test_ledgerwallet.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/test_physical_backend.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/test_speculos.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/backend/test_stub.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/bip/test_path.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/bip/test_seed.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/conftests/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/conftests/test_base_conftest.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/firmware/test_structs_Firmware.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/firmware/touch/test_element.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/firmware/touch/test_screen_FullScreen.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/firmware/touch/test_screen_MetaScreen.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/helpers.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/navigator/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/navigator/test_nano_navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/navigator/test_navigation_scenario.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/navigator/test_navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/navigator/test_touch_navigator.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/test_error_ApplicationError.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/utils/__init__.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/utils/test_packing.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/utils/test_path.py +0 -0
- {ragger-1.41.1 → ragger-1.42.0}/tests/unit/utils/test_structs.py +0 -0
|
@@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.42.0] - 2026-01-29
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Detect error APDUs during navigation to have proper error APDUs exceptions instead of timeouts
|
|
13
|
+
|
|
14
|
+
## [1.41.2] - 2026-01-08
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Only fail the test where elf file is missing
|
|
19
|
+
|
|
8
20
|
## [1.41.1] - 2025-12-12
|
|
9
21
|
|
|
10
22
|
### Fixed
|
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '1.
|
|
32
|
-
__version_tuple__ = version_tuple = (1,
|
|
31
|
+
__version__ = version = '1.42.0'
|
|
32
|
+
__version_tuple__ = version_tuple = (1, 42, 0)
|
|
33
33
|
|
|
34
|
-
__commit_id__ = commit_id = '
|
|
34
|
+
__commit_id__ = commit_id = 'g16d6f9e71'
|
|
@@ -120,6 +120,7 @@ class SpeculosBackend(BackendInterface):
|
|
|
120
120
|
api_url=self.url,
|
|
121
121
|
**kwargs)
|
|
122
122
|
self._pending: Optional[ApduResponse] = None
|
|
123
|
+
self._pending_async_response: Optional[ApduResponse] = None
|
|
123
124
|
self._last_screenshot: Optional[BytesIO] = None
|
|
124
125
|
self._home_screenshot: Optional[BytesIO] = None
|
|
125
126
|
self._ticker_paused_count = 0
|
|
@@ -137,6 +138,15 @@ class SpeculosBackend(BackendInterface):
|
|
|
137
138
|
def apdu_timeout(self, value: float) -> None:
|
|
138
139
|
self._apdu_timeout = value
|
|
139
140
|
|
|
141
|
+
def _check_async_error(self) -> None:
|
|
142
|
+
"""Check for async APDU errors and raise if present."""
|
|
143
|
+
if self._pending_async_response is not None and self._last_async_response is None:
|
|
144
|
+
if has_data_available(self._pending_async_response, timeout=0):
|
|
145
|
+
self.logger.info("[Ragger] Early async data available, retrieving it now.")
|
|
146
|
+
# This will raise ExceptionRAPDU immediately if status != 9000
|
|
147
|
+
self._last_async_response = self._get_last_async_response(
|
|
148
|
+
self._pending_async_response)
|
|
149
|
+
|
|
140
150
|
def _retrieve_client_screen_content(self) -> dict:
|
|
141
151
|
raw_content = self._client.get_current_screen_content()
|
|
142
152
|
# Keep only text events
|
|
@@ -214,13 +224,22 @@ class SpeculosBackend(BackendInterface):
|
|
|
214
224
|
@contextmanager
|
|
215
225
|
def exchange_async_raw(self, data: bytes = b"") -> Generator[bool, None, None]:
|
|
216
226
|
self.apdu_logger.info("=> %s", data.hex())
|
|
227
|
+
# Reset state for this new async exchange
|
|
228
|
+
self._last_async_response = None
|
|
217
229
|
with self._client.apdu_exchange_nowait(cla=data[0],
|
|
218
230
|
ins=data[1],
|
|
219
231
|
p1=data[2],
|
|
220
232
|
p2=data[3],
|
|
221
233
|
data=data[5:]) as response:
|
|
222
|
-
|
|
223
|
-
|
|
234
|
+
self._pending_async_response = response
|
|
235
|
+
try:
|
|
236
|
+
yield has_data_available(response, timeout=self.apdu_timeout)
|
|
237
|
+
# Only retrieve if not already retrieved by _check_async_error during navigation
|
|
238
|
+
if self._last_async_response is None:
|
|
239
|
+
self._last_async_response = self._get_last_async_response(response)
|
|
240
|
+
finally:
|
|
241
|
+
# Clear pending async response flag in all cases
|
|
242
|
+
self._pending_async_response = None
|
|
224
243
|
|
|
225
244
|
def right_click(self) -> None:
|
|
226
245
|
self._client.press_and_release("right")
|
|
@@ -315,6 +334,12 @@ class SpeculosBackend(BackendInterface):
|
|
|
315
334
|
if not screenshot_equal(screenshot, self._last_screenshot):
|
|
316
335
|
break
|
|
317
336
|
|
|
337
|
+
# Check for async APDU errors before sending a tick. This ensures that if the
|
|
338
|
+
# application has already refused the APDU (e.g., due to an error), we detect and raise
|
|
339
|
+
# the error immediately instead of waiting for a screen change that will never occur.
|
|
340
|
+
# This makes navigation robust and prevents hanging.
|
|
341
|
+
self._check_async_error()
|
|
342
|
+
|
|
318
343
|
# Send a ticker event and let the app process it
|
|
319
344
|
self.send_tick()
|
|
320
345
|
screenshot = BytesIO(self._client.get_screenshot())
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import pytest
|
|
3
3
|
import logging
|
|
4
|
+
import warnings
|
|
4
5
|
from dataclasses import fields
|
|
5
6
|
from ledgered.devices import Device, Devices
|
|
6
7
|
from ledgered.manifest import Manifest
|
|
@@ -14,6 +15,7 @@ from ragger.logger import init_loggers, standalone_conf_logger
|
|
|
14
15
|
from ragger.navigator import Navigator, NanoNavigator, TouchNavigator, NavigateWithScenario
|
|
15
16
|
from ragger.utils import find_project_root_dir, find_library_application, find_application
|
|
16
17
|
from ragger.utils.misc import get_current_app_name_and_version, exit_current_app, open_app_from_dashboard
|
|
18
|
+
from ragger.error import MissingElfError
|
|
17
19
|
|
|
18
20
|
from . import configuration as conf
|
|
19
21
|
|
|
@@ -47,6 +49,10 @@ def pytest_addoption(parser):
|
|
|
47
49
|
const="apdu.log",
|
|
48
50
|
help="Log the APDU in a file. If no pattern provided, uses 'apdu_xxx.log'.")
|
|
49
51
|
parser.addoption("--seed", action="store", default=None, help="Set a custom seed")
|
|
52
|
+
parser.addoption("--ignore-missing-binaries",
|
|
53
|
+
action="store_true",
|
|
54
|
+
default=False,
|
|
55
|
+
help="Skip tests instead of failing when application binaries are missing")
|
|
50
56
|
# Always allow "default" even if application conftest does not define it
|
|
51
57
|
allowed_setups = conf.OPTIONAL.ALLOWED_SETUPS
|
|
52
58
|
if "default" not in allowed_setups:
|
|
@@ -102,6 +108,11 @@ def cli_user_seed(pytestconfig):
|
|
|
102
108
|
return pytestconfig.getoption("seed")
|
|
103
109
|
|
|
104
110
|
|
|
111
|
+
@pytest.fixture(scope="session")
|
|
112
|
+
def ignore_missing_binaries(pytestconfig):
|
|
113
|
+
return pytestconfig.getoption("ignore_missing_binaries")
|
|
114
|
+
|
|
115
|
+
|
|
105
116
|
@pytest.fixture(scope="session")
|
|
106
117
|
def pki_prod(pytestconfig):
|
|
107
118
|
return pytestconfig.getoption("pki_prod")
|
|
@@ -238,7 +249,8 @@ def prepare_speculos_args(root_pytest_dir: Path,
|
|
|
238
249
|
pki_prod: bool,
|
|
239
250
|
cli_user_seed: str,
|
|
240
251
|
additional_args: List[str],
|
|
241
|
-
verbose_speculos: bool = False
|
|
252
|
+
verbose_speculos: bool = False,
|
|
253
|
+
ignore_missing_binaries: bool = False):
|
|
242
254
|
speculos_args = additional_args.copy()
|
|
243
255
|
|
|
244
256
|
if display:
|
|
@@ -270,16 +282,14 @@ def prepare_speculos_args(root_pytest_dir: Path,
|
|
|
270
282
|
f"Expected a single folder in {manifest.app.build_directory}, found {len(app_dir_subdirectories)}"
|
|
271
283
|
)
|
|
272
284
|
main_app_path = find_application(app_dir_subdirectories[0], device_name, "c")
|
|
273
|
-
# If the app is standalone, the main app should be located in project_root_dir / manifest.app.build_directory
|
|
274
|
-
else:
|
|
275
|
-
main_app_path = find_application(project_root_dir / manifest.app.build_directory,
|
|
276
|
-
device_name, manifest.app.sdk)
|
|
277
285
|
|
|
278
|
-
# If this repository does not hold the main app, then we need to load this repository's application as a library
|
|
279
|
-
if conf.OPTIONAL.MAIN_APP_DIR is not None:
|
|
280
286
|
# This repo holds the library, not the standalone app: search in root_dir/build
|
|
281
287
|
lib_path = find_application(project_root_dir, device_name, manifest.app.sdk)
|
|
282
288
|
speculos_args.append(f"-l{lib_path}")
|
|
289
|
+
# If the app is standalone, the main app should be located in project_root_dir / manifest.app.build_directory
|
|
290
|
+
else:
|
|
291
|
+
main_app_path = find_application(project_root_dir / manifest.app.build_directory,
|
|
292
|
+
device_name, manifest.app.sdk)
|
|
283
293
|
|
|
284
294
|
# Legacy lib method, remove once exchange is ported
|
|
285
295
|
if len(conf.OPTIONAL.SIDELOADED_APPS) != 0:
|
|
@@ -290,8 +300,15 @@ def prepare_speculos_args(root_pytest_dir: Path,
|
|
|
290
300
|
libs_dir = Path(project_root_dir / conf.OPTIONAL.SIDELOADED_APPS_DIR)
|
|
291
301
|
# Add "-l Appname:filepath" to Speculos command line for every required lib app
|
|
292
302
|
for coin_name, lib_name in conf.OPTIONAL.SIDELOADED_APPS.items():
|
|
293
|
-
|
|
294
|
-
|
|
303
|
+
try:
|
|
304
|
+
lib_path = find_library_application(libs_dir, coin_name, device_name)
|
|
305
|
+
speculos_args.append(f"-l{lib_name}:{lib_path}")
|
|
306
|
+
except MissingElfError as e:
|
|
307
|
+
if ignore_missing_binaries:
|
|
308
|
+
warnings.warn(f"Could not find sideloaded app library for '{lib_name}': {e}",
|
|
309
|
+
UserWarning)
|
|
310
|
+
else:
|
|
311
|
+
raise
|
|
295
312
|
else:
|
|
296
313
|
# Keep this method instead
|
|
297
314
|
# Find all external libraries that have to be sideloaded
|
|
@@ -300,10 +317,17 @@ def prepare_speculos_args(root_pytest_dir: Path,
|
|
|
300
317
|
subdirs = sorted(
|
|
301
318
|
filter(lambda d: (sideloaded_dir / d).is_dir(), os.listdir(sideloaded_dir)))
|
|
302
319
|
for subdir in subdirs:
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
320
|
+
try:
|
|
321
|
+
# Currently only C apps are used as additional binaries by ragger (Ethereum and Exchange)
|
|
322
|
+
# TODO: add support for Rust SDK libraries if needed
|
|
323
|
+
lib_path = find_application(sideloaded_dir / subdir, device_name, "c")
|
|
324
|
+
speculos_args.append(f"-l{lib_path}")
|
|
325
|
+
except MissingElfError as e:
|
|
326
|
+
if ignore_missing_binaries:
|
|
327
|
+
warnings.warn(f"Could not find sideloaded app binary for '{subdir}': {e}",
|
|
328
|
+
UserWarning)
|
|
329
|
+
else:
|
|
330
|
+
raise
|
|
307
331
|
|
|
308
332
|
# Check if custom user seed has been provided through CLI or optional configuration.
|
|
309
333
|
# CLI user seed has priority over the optional configuration seed.
|
|
@@ -326,7 +350,8 @@ def create_backend(root_pytest_dir: Path,
|
|
|
326
350
|
log_apdu_file: Optional[Path],
|
|
327
351
|
cli_user_seed: str,
|
|
328
352
|
additional_speculos_arguments: List[str],
|
|
329
|
-
verbose_speculos: bool = False
|
|
353
|
+
verbose_speculos: bool = False,
|
|
354
|
+
ignore_missing_binaries: bool = False) -> BackendInterface:
|
|
330
355
|
if backend_name.lower() == "ledgercomm":
|
|
331
356
|
return LedgerCommBackend(device=device,
|
|
332
357
|
interface="hid",
|
|
@@ -338,7 +363,8 @@ def create_backend(root_pytest_dir: Path,
|
|
|
338
363
|
main_app_path, speculos_args = prepare_speculos_args(root_pytest_dir, device, display,
|
|
339
364
|
pki_prod, cli_user_seed,
|
|
340
365
|
additional_speculos_arguments,
|
|
341
|
-
verbose_speculos
|
|
366
|
+
verbose_speculos,
|
|
367
|
+
ignore_missing_binaries)
|
|
342
368
|
return SpeculosBackend(main_app_path,
|
|
343
369
|
device=device,
|
|
344
370
|
log_apdu_file=log_apdu_file,
|
|
@@ -353,12 +379,21 @@ def create_backend(root_pytest_dir: Path,
|
|
|
353
379
|
@pytest.fixture(scope=conf.OPTIONAL.BACKEND_SCOPE)
|
|
354
380
|
def backend(skip_tests_for_unsupported_devices, root_pytest_dir: Path, backend_name: str,
|
|
355
381
|
device: Device, display: bool, pki_prod: bool, log_apdu_file: Optional[Path],
|
|
356
|
-
cli_user_seed: str, additional_speculos_arguments: List[str],
|
|
357
|
-
|
|
382
|
+
cli_user_seed: str, additional_speculos_arguments: List[str], verbose_speculos: bool,
|
|
383
|
+
ignore_missing_binaries: bool) -> Generator[BackendInterface, None, None]:
|
|
358
384
|
# to separate the test name and its following logs
|
|
359
385
|
print("")
|
|
360
|
-
|
|
361
|
-
|
|
386
|
+
backend_instance = None
|
|
387
|
+
try:
|
|
388
|
+
backend_instance = create_backend(root_pytest_dir, backend_name, device, display, pki_prod,
|
|
389
|
+
log_apdu_file, cli_user_seed,
|
|
390
|
+
additional_speculos_arguments, verbose_speculos,
|
|
391
|
+
ignore_missing_binaries)
|
|
392
|
+
except MissingElfError as e:
|
|
393
|
+
pytest.fail(f"Missing ELF: {e}")
|
|
394
|
+
|
|
395
|
+
assert backend_instance is not None, "Backend instance should be initialized"
|
|
396
|
+
with backend_instance as b:
|
|
362
397
|
if backend_name.lower() != "speculos" and conf.OPTIONAL.APP_NAME:
|
|
363
398
|
# Make sure the app is restarted as this is what is requested by the fixture scope
|
|
364
399
|
app_name, version = get_current_app_name_and_version(b)
|
|
@@ -173,3 +173,11 @@ class ExceptionRAPDU(Exception):
|
|
|
173
173
|
|
|
174
174
|
def __str__(self):
|
|
175
175
|
return f"Error [0x{self.status:x}] {str(self.data)}"
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
class MissingElfError(Exception):
|
|
179
|
+
"""Exception raised when an expected ELF binary is missing."""
|
|
180
|
+
|
|
181
|
+
def __init__(self, path: str):
|
|
182
|
+
super().__init__(f"File '{path}' missing. Did you compile for this target?")
|
|
183
|
+
self.path = path
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import toml
|
|
17
17
|
from typing import Optional, Tuple, List
|
|
18
18
|
from pathlib import Path
|
|
19
|
-
from ragger.error import ExceptionRAPDU
|
|
19
|
+
from ragger.error import ExceptionRAPDU, MissingElfError
|
|
20
20
|
import subprocess
|
|
21
21
|
import json
|
|
22
22
|
|
|
@@ -48,7 +48,7 @@ def find_library_application(base_dir: Path, name: str, device: str) -> Path:
|
|
|
48
48
|
raise AssertionError(f"{base_dir} is not a directory")
|
|
49
49
|
lib = Path(base_dir / (name + "_" + device + ".elf")).resolve()
|
|
50
50
|
if not lib.is_file():
|
|
51
|
-
raise
|
|
51
|
+
raise MissingElfError(str(lib))
|
|
52
52
|
return lib
|
|
53
53
|
|
|
54
54
|
|
|
@@ -91,7 +91,7 @@ def find_application(base_dir: Path, device: str, sdk: str) -> Path:
|
|
|
91
91
|
else:
|
|
92
92
|
app = app / "build" / device / "bin" / "app.elf"
|
|
93
93
|
if not app.is_file():
|
|
94
|
-
raise
|
|
94
|
+
raise MissingElfError(str(app))
|
|
95
95
|
return app
|
|
96
96
|
|
|
97
97
|
|
|
@@ -147,3 +147,30 @@ class TestbackendSpeculos(TestCase):
|
|
|
147
147
|
self.backend.right_click()
|
|
148
148
|
self.backend.left_click()
|
|
149
149
|
self.backend.both_click()
|
|
150
|
+
|
|
151
|
+
def test_async_error_raised_during_navigation(self):
|
|
152
|
+
"""
|
|
153
|
+
Ensure that an async APDU error is raised immediately during navigation (e.g., while waiting for a screen change),
|
|
154
|
+
not just at context exit. This simulates an error response being available before navigation completes.
|
|
155
|
+
Also verifies that sequential async exchanges properly reset state and retrieve fresh responses.
|
|
156
|
+
"""
|
|
157
|
+
with patch("speculos.client.subprocess"):
|
|
158
|
+
with SpeculosServerStub():
|
|
159
|
+
with self.backend:
|
|
160
|
+
# Start an async exchange with an error APDU (not 0x9000)
|
|
161
|
+
with self.assertRaises(ExceptionRAPDU) as error:
|
|
162
|
+
with self.backend.exchange_async_raw(bytes.fromhex("01000000")):
|
|
163
|
+
# Simulate navigation that would trigger the async error check
|
|
164
|
+
# (wait_for_screen_change calls _check_async_error)
|
|
165
|
+
self.backend.wait_for_screen_change(timeout=0.5)
|
|
166
|
+
# This line should be unreachable - if reached, the error wasn't raised during navigation
|
|
167
|
+
assert False, "Expected ExceptionRAPDU was not raised during navigation" # pragma: no cover
|
|
168
|
+
self.assertEqual(error.exception.status, APDUStatus.ERROR)
|
|
169
|
+
|
|
170
|
+
# Perform a second async exchange with a SUCCESS APDU to ensure state is properly reset
|
|
171
|
+
# If state wasn't reset, this could incorrectly raise due to the previous error response
|
|
172
|
+
with self.assertRaises(TimeoutError):
|
|
173
|
+
with self.backend.exchange_async_raw(bytes.fromhex("00000000")):
|
|
174
|
+
self.backend.wait_for_screen_change(timeout=1)
|
|
175
|
+
# Verify that the response was successfully retrieved despite the timeout
|
|
176
|
+
self.assertEqual(self.backend.last_async_response.status, APDUStatus.SUCCESS)
|
|
@@ -4,7 +4,7 @@ from pathlib import Path
|
|
|
4
4
|
from unittest import TestCase
|
|
5
5
|
from unittest.mock import MagicMock
|
|
6
6
|
|
|
7
|
-
from ragger.error import ExceptionRAPDU
|
|
7
|
+
from ragger.error import ExceptionRAPDU, MissingElfError
|
|
8
8
|
from ragger.utils import misc
|
|
9
9
|
|
|
10
10
|
from ..helpers import temporary_directory
|
|
@@ -47,7 +47,7 @@ class TestMisc(TestCase):
|
|
|
47
47
|
tmp_dir = (dir_path / "build" / device / "bin")
|
|
48
48
|
tmp_dir.mkdir(parents=True, exist_ok=True)
|
|
49
49
|
expected = tmp_dir / "app.elf"
|
|
50
|
-
with self.assertRaises(
|
|
50
|
+
with self.assertRaises(MissingElfError) as error:
|
|
51
51
|
misc.find_application(dir_path, device, sdk)
|
|
52
52
|
self.assertIn(str(expected), str(error.exception))
|
|
53
53
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_and_compare_no_golden/00000.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00000.png
RENAMED
|
File without changes
|
{ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00001.png
RENAMED
|
File without changes
|
{ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00002.png
RENAMED
|
File without changes
|
{ragger-1.41.1 → ragger-1.42.0}/tests/snapshots/nanos/test_navigate_until_text_and_compare/00003.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|