diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 00000000..495f8693 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1 @@ +d7e18cc4a7695d5446bfde246c91b91b092c0084 diff --git a/.github/workflows/fast-checks.yml b/.github/workflows/fast-checks.yml index a5f7b40d..a2ef05db 100644 --- a/.github/workflows/fast-checks.yml +++ b/.github/workflows/fast-checks.yml @@ -20,9 +20,9 @@ jobs: steps: - name: Clone uses: actions/checkout@v4 - - run: pip install flake8 flake8-pyproject - - name: Flake8 lint Python code - run: flake8 src/ + - run: pip install ruff + - name: ruff check + run: ruff check yapf: name: Formatting @@ -30,12 +30,9 @@ jobs: steps: - name: Clone uses: actions/checkout@v4 - - run: pip install yapf toml - - name: Yapf source formatting - run: | - yapf src/ --recursive -d - yapf tests/ --recursive -d - yapf template/ --recursive -d + - run: pip install ruff toml + - name: Ruff source formatting + run: ruff format --check mypy: name: Type checking diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..110ab302 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,35 @@ +# To install hooks, run: +# pre-commit install --hook-type pre-commit +# pre-commit install --hook-type commit-msg + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-toml + - id: check-added-large-files + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.8 + hooks: + - id: ruff-format + args: ["--diff"] + - id: ruff-check + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.14.1 + hooks: + - id: mypy + additional_dependencies: + - types-toml + - types-requests + + - repo: https://github.com/Mateusz-Grzelinski/actionlint-py + rev: v1.7.7.24 + hooks: + - id: actionlint + types_or: [yaml] + args: [-shellcheck='' -pyflakes=''] \ No newline at end of file diff --git a/doc/conf.py b/doc/conf.py index c2261ca3..1c6e4eed 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -4,22 +4,22 @@ # list see the documentation: # https://www.sphinx-doc.org/en/master/usage/configuration.html +import os +import sys +from importlib.metadata import version as get_version + # -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -# -# import os -# import sys -# sys.path.insert(0, os.path.abspath('.')) # -- Project information ----------------------------------------------------- -project = 'Ragger' -copyright = '2022, bow' -author = 'bow' +project = "Ragger" +copyright = "2022, bow" +author = "bow" # -- General configuration --------------------------------------------------- @@ -30,9 +30,9 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'sphinx.ext.autodoc', - 'sphinx_copybutton', - 'sphinxcontrib.images', + "sphinx.ext.autodoc", + "sphinx_copybutton", + "sphinxcontrib.images", ] images_config = { @@ -40,32 +40,32 @@ } # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] -html_sidebars = { '**': ['globaltoc.html', 'relations.html', 'sourcelink.html', 'searchbox.html'] } +html_sidebars = { + "**": ["globaltoc.html", "relations.html", "sourcelink.html", "searchbox.html"] +} # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path. -exclude_patterns = ['build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["build", "Thumbs.db", ".DS_Store"] -import os -import sys +sys.path.insert(0, os.path.abspath("../src/")) -sys.path.insert(0, os.path.abspath('../src/')) - -from importlib.metadata import version as get_version -release = get_version('ragger') -version = '.'.join(release.split('.')[:2]) +release = get_version("ragger") +version = ".".join(release.split(".")[:2]) ## Autodoc conf ## + # Do not skip __init__ methods by default def skip(app, what, name, obj, would_skip, options): if name == "__init__": return False return would_skip + # Remove every module documentation string. # Prevents to integrate the Licence when using automodule. # It is possible to limit the impacted module by filtering with the 'name' @@ -75,20 +75,23 @@ def remove_module_docstring(app, what, name, obj, options, lines): if what == "module": del lines[:] + ## Setup ## + def setup(app): app.connect("autodoc-process-docstring", remove_module_docstring) app.connect("autodoc-skip-member", skip) + # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -html_theme = 'sphinx_rtd_theme' +html_theme = "sphinx_rtd_theme" # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] diff --git a/src/ragger/__init__.py b/src/ragger/__init__.py index 38d6352e..b099b46c 100644 --- a/src/ragger/__init__.py +++ b/src/ragger/__init__.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + try: from ragger.__version__ import __version__ # noqa except ImportError: diff --git a/src/ragger/backend/__init__.py b/src/ragger/backend/__init__.py index b9309b97..efd01e8d 100644 --- a/src/ragger/backend/__init__.py +++ b/src/ragger/backend/__init__.py @@ -1,23 +1,26 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from .interface import BackendInterface, RaisePolicy from .stub import StubBackend -ERROR_MSG = ("This backend needs {}. Please install this package (run `pip install ragger[{}]` or " - "check this address: '{}')") +ERROR_MSG = ( + "This backend needs {}. Please install this package (run `pip install ragger[{}]` or " + "check this address: '{}')" +) try: from .speculos import SpeculosBackend @@ -27,7 +30,10 @@ def SpeculosBackend(*args, **kwargs): # type: ignore raise ImportError( - ERROR_MSG.format("Speculos", "speculos", "https://github.com/LedgerHQ/speculos/")) + ERROR_MSG.format( + "Speculos", "speculos", "https://github.com/LedgerHQ/speculos/" + ) + ) try: @@ -38,7 +44,10 @@ def SpeculosBackend(*args, **kwargs): # type: ignore def LedgerCommBackend(*args, **kwargs): # type: ignore raise ImportError( - ERROR_MSG.format("LedgerComm", "ledgercomm", "https://github.com/LedgerHQ/ledgercomm/")) + ERROR_MSG.format( + "LedgerComm", "ledgercomm", "https://github.com/LedgerHQ/ledgercomm/" + ) + ) try: @@ -49,11 +58,17 @@ def LedgerCommBackend(*args, **kwargs): # type: ignore def LedgerWalletBackend(*args, **kwargs): # type: ignore raise ImportError( - ERROR_MSG.format("LedgerWallet", "ledgerwallet", - "https://github.com/LedgerHQ/ledgerctl/")) + ERROR_MSG.format( + "LedgerWallet", "ledgerwallet", "https://github.com/LedgerHQ/ledgerctl/" + ) + ) __all__ = [ - "SpeculosBackend", "LedgerCommBackend", "LedgerWalletBackend", "BackendInterface", - "RaisePolicy", "StubBackend" + "SpeculosBackend", + "LedgerCommBackend", + "LedgerWalletBackend", + "BackendInterface", + "RaisePolicy", + "StubBackend", ] diff --git a/src/ragger/backend/interface.py b/src/ragger/backend/interface.py index 5764ce29..be3c4269 100644 --- a/src/ragger/backend/interface.py +++ b/src/ragger/backend/interface.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from abc import ABC, abstractmethod from contextlib import contextmanager from enum import Enum, auto @@ -54,11 +55,12 @@ def __str__(self) -> str: class BackendInterface(ABC): - - def __init__(self, - device: Device, - log_apdu_file: Optional[Path] = None, - whitelisted_status: Iterable = ()): + def __init__( + self, + device: Device, + log_apdu_file: Optional[Path] = None, + whitelisted_status: Iterable = (), + ): """Initializes the Backend :param device: Which Device will be managed @@ -110,8 +112,12 @@ def __enter__(self) -> "BackendInterface": raise NotImplementedError @abstractmethod - def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType]): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ): raise NotImplementedError def handle_usb_reset(self) -> None: @@ -129,13 +135,21 @@ def is_raise_required(self, rapdu: RAPDU) -> bool: :return: If the given status is considered valid or not :rtype: bool """ - return ((self.raise_policy == RaisePolicy.RAISE_ALL) - or ((self.raise_policy == RaisePolicy.RAISE_ALL_BUT_0x9000) and - (rapdu.status != StatusWords.SWO_SUCCESS)) - or ((self.raise_policy == RaisePolicy.RAISE_CUSTOM) and - (rapdu.status not in self.whitelisted_status))) - - def send(self, cla: int, ins: int, p1: int = 0, p2: int = 0, data: bytes = b"") -> None: + return ( + (self.raise_policy == RaisePolicy.RAISE_ALL) + or ( + (self.raise_policy == RaisePolicy.RAISE_ALL_BUT_0x9000) + and (rapdu.status != StatusWords.SWO_SUCCESS) + ) + or ( + (self.raise_policy == RaisePolicy.RAISE_CUSTOM) + and (rapdu.status not in self.whitelisted_status) + ) + ) + + def send( + self, cla: int, ins: int, p1: int = 0, p2: int = 0, data: bytes = b"" + ) -> None: """ Formats then sends an APDU to the backend. @@ -188,13 +202,15 @@ def receive(self) -> RAPDU: """ raise NotImplementedError - def exchange(self, - cla: int, - ins: int, - p1: int = 0, - p2: int = 0, - data: bytes = b"", - tick_timeout: int = 5 * 60 * 10) -> RAPDU: + def exchange( + self, + cla: int, + ins: int, + p1: int = 0, + p2: int = 0, + data: bytes = b"", + tick_timeout: int = 5 * 60 * 10, + ) -> RAPDU: """ Formats and sends an APDU to the backend, then receives its response. @@ -218,7 +234,9 @@ def exchange(self, :return: The APDU response :rtype: RAPDU """ - return self.exchange_raw(pack_APDU(cla, ins, p1, p2, data), tick_timeout=tick_timeout) + return self.exchange_raw( + pack_APDU(cla, ins, p1, p2, data), tick_timeout=tick_timeout + ) @abstractmethod def exchange_raw(self, data: bytes = b"", tick_timeout: int = 5 * 60 * 10) -> RAPDU: @@ -241,12 +259,9 @@ def exchange_raw(self, data: bytes = b"", tick_timeout: int = 5 * 60 * 10) -> RA raise NotImplementedError @contextmanager - def exchange_async(self, - cla: int, - ins: int, - p1: int = 0, - p2: int = 0, - data: bytes = b"") -> Generator[None, None, None]: + def exchange_async( + self, cla: int, ins: int, p1: int = 0, p2: int = 0, data: bytes = b"" + ) -> Generator[None, None, None]: """ Formats and sends an APDU to the backend, then gives the control back to the caller. @@ -280,7 +295,9 @@ def exchange_async(self, @contextmanager @abstractmethod - def exchange_async_raw(self, data: bytes = b"") -> Generator[Union[bool, None], None, None]: + def exchange_async_raw( + self, data: bytes = b"" + ) -> Generator[Union[bool, None], None, None]: """ Sends the given APDU to the backend, then gives the control back to the caller. @@ -376,11 +393,9 @@ def finger_touch(self, x: int = 0, y: int = 0, delay: float = 0.5) -> None: raise NotImplementedError @abstractmethod - def finger_swipe(self, - x: int = 0, - y: int = 0, - direction: str = "left", - delay: float = 0.5) -> None: + def finger_swipe( + self, x: int = 0, y: int = 0, direction: str = "left", delay: float = 0.5 + ) -> None: """ Performs a finger swipe on the device screen. @@ -407,11 +422,13 @@ def finger_swipe(self, raise NotImplementedError @abstractmethod - def compare_screen_with_snapshot(self, - golden_snap_path: Path, - crop: Optional[Crop] = None, - tmp_snap_path: Optional[Path] = None, - golden_run: bool = False) -> bool: + def compare_screen_with_snapshot( + self, + golden_snap_path: Path, + crop: Optional[Crop] = None, + tmp_snap_path: Optional[Path] = None, + golden_run: bool = False, + ) -> bool: """ Compare the current device screen with the provided snapshot. diff --git a/src/ragger/backend/ledgercomm.py b/src/ragger/backend/ledgercomm.py index 256b2757..feaa2df4 100644 --- a/src/ragger/backend/ledgercomm.py +++ b/src/ragger/backend/ledgercomm.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from contextlib import contextmanager from pathlib import Path from time import sleep @@ -28,7 +29,7 @@ def raise_policy_enforcer(function): - def decoration(self: 'LedgerCommBackend', *args, **kwargs) -> RAPDU: + def decoration(self: "LedgerCommBackend", *args, **kwargs) -> RAPDU: rapdu: RAPDU = function(self, *args, **kwargs) self.apdu_logger.info("<= %s%4x", rapdu.data.hex(), rapdu.status) @@ -42,39 +43,40 @@ def decoration(self: 'LedgerCommBackend', *args, **kwargs) -> RAPDU: class LedgerCommBackend(PhysicalBackend): - - def __init__(self, - device: Device, - *args, - host: str = "127.0.0.1", - port: int = 9999, - interface: str = 'hid', - log_apdu_file: Optional[Path] = None, - with_gui: bool = False, - **kwargs): - super().__init__(device, *args, log_apdu_file=log_apdu_file, with_gui=with_gui, **kwargs) + def __init__( + self, + device: Device, + *args, + host: str = "127.0.0.1", + port: int = 9999, + interface: str = "hid", + log_apdu_file: Optional[Path] = None, + with_gui: bool = False, + **kwargs, + ): + super().__init__( + device, *args, log_apdu_file=log_apdu_file, with_gui=with_gui, **kwargs + ) self._host = host self._port = port self._client: Optional[Transport] = None - kwargs['interface'] = interface + kwargs["interface"] = interface self._args = (args, kwargs) def __enter__(self) -> "LedgerCommBackend": self.logger.info(f"Starting {self.__class__.__name__} stream") try: - self._client = Transport(server=self._host, - port=self._port, - *self._args[0], - **self._args[1]) + self._client = Transport( + server=self._host, port=self._port, *self._args[0], **self._args[1] + ) except Exception: # Give some time for the USB stack to power up and to be enumerated # Might be needed in successive tests where app is exited at the end of the test sleep(1) - self._client = Transport(server=self._host, - port=self._port, - *self._args[0], - **self._args[1]) + self._client = Transport( + server=self._host, port=self._port, *self._args[0], **self._args[1] + ) return self def __exit__(self, *args): diff --git a/src/ragger/backend/ledgerwallet.py b/src/ragger/backend/ledgerwallet.py index c20f3ed6..0705280e 100644 --- a/src/ragger/backend/ledgerwallet.py +++ b/src/ragger/backend/ledgerwallet.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from contextlib import contextmanager from time import sleep from typing import Generator, Optional @@ -28,7 +29,7 @@ def raise_policy_enforcer(function): - def decoration(self: 'LedgerWalletBackend', *args, **kwargs) -> RAPDU: + def decoration(self: "LedgerWalletBackend", *args, **kwargs) -> RAPDU: # Catch backend raise try: rapdu: RAPDU = function(self, *args, **kwargs) @@ -46,7 +47,6 @@ def decoration(self: 'LedgerWalletBackend', *args, **kwargs) -> RAPDU: class LedgerWalletBackend(PhysicalBackend): - def __init__(self, device: Device, *args, with_gui: bool = False, **kwargs): super().__init__(device, *args, with_gui=with_gui, **kwargs) self._client: Optional[LedgerClient] = None diff --git a/src/ragger/backend/physical_backend.py b/src/ragger/backend/physical_backend.py index fbf01d28..2e75de09 100644 --- a/src/ragger/backend/physical_backend.py +++ b/src/ragger/backend/physical_backend.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from ledgered.devices import Device from pathlib import Path from types import TracebackType @@ -26,16 +27,19 @@ class PhysicalBackend(BackendInterface): - def __init__(self, device: Device, *args, with_gui: bool = False, **kwargs): super().__init__(device, *args, **kwargs) - self._ui: Optional[RaggerGUI] = RaggerGUI(device=device.name) if with_gui else None + self._ui: Optional[RaggerGUI] = ( + RaggerGUI(device=device.name) if with_gui else None + ) self._last_valid_snap_path: Optional[Path] = None - def __exit__(self, - exc_type: Optional[Type[BaseException]] = None, - exc_val: Optional[BaseException] = None, - exc_tb: Optional[TracebackType] = None): + def __exit__( + self, + exc_type: Optional[Type[BaseException]] = None, + exc_val: Optional[BaseException] = None, + exc_tb: Optional[TracebackType] = None, + ): if self._ui is not None: self._ui.kill() @@ -43,8 +47,9 @@ def init_gui(self) -> None: """ Initialize the GUI if needed. """ - assert self._ui is not None, \ + assert self._ui is not None, ( "This method should only be called if the backend manages an GUI" + ) if not self._ui.is_alive(): self._ui.start() @@ -72,21 +77,21 @@ def finger_touch(self, x: int = 0, y: int = 0, delay: float = 0.5) -> None: self.init_gui() self._ui.ask_for_touch_action(x, y) - def finger_swipe(self, - x: int = 0, - y: int = 0, - direction: str = "left", - delay: float = 0.5) -> None: + def finger_swipe( + self, x: int = 0, y: int = 0, direction: str = "left", delay: float = 0.5 + ) -> None: if self._ui is None: return self.init_gui() self._ui.ask_for_swipe_action(x, y, direction) - def compare_screen_with_snapshot(self, - golden_snap_path: Path, - crop: Optional[Crop] = None, - tmp_snap_path: Optional[Path] = None, - golden_run: bool = False) -> bool: + def compare_screen_with_snapshot( + self, + golden_snap_path: Path, + crop: Optional[Crop] = None, + tmp_snap_path: Optional[Path] = None, + golden_run: bool = False, + ) -> bool: # If the file has no size, it's because we are within a NamedTemporaryFile # We do nothing and return False to exit the while loop of @@ -118,7 +123,8 @@ def compare_screen_with_text(self, text: str) -> bool: except ImportError as error: raise ImportError( "This feature needs at least one physical backend. " - "Please install ragger[ledgercomm] or ragger[ledgerwallet]") from error + "Please install ragger[ledgercomm] or ragger[ledgerwallet]" + ) from error if self._ui is None: return True diff --git a/src/ragger/backend/speculos.py b/src/ragger/backend/speculos.py index 9c289a8b..16f0864d 100644 --- a/src/ragger/backend/speculos.py +++ b/src/ragger/backend/speculos.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + import select import socket from contextlib import contextmanager @@ -28,7 +29,12 @@ from ledgered import binary from ledgered.devices import Device -from speculos.client import SpeculosClient, screenshot_equal, ApduResponse, ApduException +from speculos.client import ( + SpeculosClient, + screenshot_equal, + ApduResponse, + ApduException, +) from speculos.mcu.seproxyhal import TICKER_DELAY from ragger.error import StatusWords, ExceptionRAPDU @@ -42,7 +48,7 @@ def _is_port_in_use(port: int) -> bool: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - return s.connect_ex(('localhost', port)) == 0 + return s.connect_ex(("localhost", port)) == 0 def _get_unused_port_from(starting_port: int) -> int: @@ -53,7 +59,7 @@ def _get_unused_port_from(starting_port: int) -> int: def raise_policy_enforcer(function): - def decoration(self: 'SpeculosBackend', *args, **kwargs) -> RAPDU: + def decoration(self: "SpeculosBackend", *args, **kwargs) -> RAPDU: # Catch backend raise try: rapdu: RAPDU = function(self, *args, **kwargs) @@ -71,23 +77,25 @@ def decoration(self: 'SpeculosBackend', *args, **kwargs) -> RAPDU: class SpeculosBackend(BackendInterface): - _DEFAULT_API_PORT = 5000 - _ARGS_KEY = 'args' - _ARGS_API_PORT_KEY = '--api-port' - _ARGS_APDU_PORT_KEY = '--apdu-port' - - def __init__(self, - application: Path, - device: Device, - log_apdu_file: Optional[Path] = None, - **kwargs): + _ARGS_KEY = "args" + _ARGS_API_PORT_KEY = "--api-port" + _ARGS_APDU_PORT_KEY = "--apdu-port" + + def __init__( + self, + application: Path, + device: Device, + log_apdu_file: Optional[Path] = None, + **kwargs, + ): super().__init__(device=device, log_apdu_file=log_apdu_file) # crafting Speculos arguments args = ["--model", device.name] speculos_args: List = kwargs.get(self._ARGS_KEY, list()) - assert isinstance(speculos_args, list), \ + assert isinstance(speculos_args, list), ( f"'{self._ARGS_KEY}' ({speculos_args}) keyword argument must be a list of arguments" + ) # Inferring the API port if self._ARGS_API_PORT_KEY in speculos_args: index = speculos_args.index(self._ARGS_API_PORT_KEY) @@ -111,14 +119,16 @@ def __init__(self, # test just for the unit-test to pass. # In real life, the application is the path to the elf file, always present bin_data = binary.LedgerBinaryApp(application) - self.sdk_graphics = GraphicalLibrary.from_string(bin_data.sections.sdk_graphics) + self.sdk_graphics = GraphicalLibrary.from_string( + bin_data.sections.sdk_graphics + ) self.logger.info("Speculos binary: '%s'", application) self.logger.info("SDK Library: '%s'", self.sdk_graphics) self.logger.info("Speculos options: '%s'", " ".join(kwargs[self._ARGS_KEY])) - self._client: SpeculosClient = SpeculosClient(app=str(application), - api_url=self.url, - **kwargs) + self._client: SpeculosClient = SpeculosClient( + app=str(application), api_url=self.url, **kwargs + ) self._pending: Optional[ApduResponse] = None self._pending_async_response: Optional[ApduResponse] = None self._last_screenshot: Optional[BytesIO] = None @@ -140,12 +150,18 @@ def apdu_timeout(self, value: float) -> None: def _check_async_error(self) -> None: """Check for async APDU errors and raise if present.""" - if self._pending_async_response is not None and self._last_async_response is None: + if ( + self._pending_async_response is not None + and self._last_async_response is None + ): if has_data_available(self._pending_async_response, timeout=0): - self.logger.info("[Ragger] Early async data available, retrieving it now.") + self.logger.info( + "[Ragger] Early async data available, retrieving it now." + ) # This will raise ExceptionRAPDU immediately if status != 9000 self._last_async_response = self._get_last_async_response( - self._pending_async_response) + self._pending_async_response + ) def _retrieve_client_screen_content(self) -> dict: raw_content = self._client.get_current_screen_content() @@ -184,9 +200,10 @@ def __enter__(self) -> "SpeculosBackend": while not self._retrieve_client_screen_content()["events"]: # Send a ticker event and let the app process it sleep(0.1) - if (time() - start > 20.0): + if time() - start > 20.0: raise TimeoutError( - "Timeout waiting for screen content upon Ragger Speculos Instance start") + "Timeout waiting for screen content upon Ragger Speculos Instance start" + ) self._last_screenshot = BytesIO(self._client.get_screenshot()) @@ -214,8 +231,10 @@ def receive(self) -> RAPDU: @raise_policy_enforcer def exchange_raw(self, data: bytes = b"", tick_timeout: int = 5 * 60 * 10) -> RAPDU: self.apdu_logger.info("=> %s", data.hex()) - return RAPDU(StatusWords.SWO_SUCCESS, - self._client._apdu_exchange(data, tick_timeout=tick_timeout)) + return RAPDU( + StatusWords.SWO_SUCCESS, + self._client._apdu_exchange(data, tick_timeout=tick_timeout), + ) @raise_policy_enforcer def _get_last_async_response(self, response) -> RAPDU: @@ -226,11 +245,9 @@ def exchange_async_raw(self, data: bytes = b"") -> Generator[bool, None, None]: self.apdu_logger.info("=> %s", data.hex()) # Reset state for this new async exchange self._last_async_response = None - with self._client.apdu_exchange_nowait(cla=data[0], - ins=data[1], - p1=data[2], - p2=data[3], - data=data[5:]) as response: + with self._client.apdu_exchange_nowait( + cla=data[0], ins=data[1], p1=data[2], p2=data[3], data=data[5:] + ) as response: self._pending_async_response = response try: yield has_data_available(response, timeout=self.apdu_timeout) @@ -253,11 +270,9 @@ def both_click(self) -> None: def finger_touch(self, x: int = 0, y: int = 0, delay: float = 0.1) -> None: self._client.finger_touch(x, y, delay=delay) - def finger_swipe(self, - x: int = 0, - y: int = 0, - direction: str = "left", - delay: float = 0.1) -> None: + def finger_swipe( + self, x: int = 0, y: int = 0, direction: str = "left", delay: float = 0.1 + ) -> None: self._client.finger_swipe(x, y, direction=direction, delay=delay) def _save_screen_snapshot(self, snap: BytesIO, path: Path) -> None: @@ -265,11 +280,13 @@ def _save_screen_snapshot(self, snap: BytesIO, path: Path) -> None: img = Image.open(snap) img.save(path) - def compare_screen_with_snapshot(self, - golden_snap_path: Path, - crop: Optional[Crop] = None, - tmp_snap_path: Optional[Path] = None, - golden_run: bool = False) -> bool: + def compare_screen_with_snapshot( + self, + golden_snap_path: Path, + crop: Optional[Crop] = None, + tmp_snap_path: Optional[Path] = None, + golden_run: bool = False, + ) -> bool: snap = BytesIO(self._client.get_screenshot()) # Save snap in tmp folder. @@ -282,12 +299,14 @@ def compare_screen_with_snapshot(self, self._save_screen_snapshot(snap, golden_snap_path) if crop is not None: - return screenshot_equal(f"{golden_snap_path}", - snap, - left=crop.left, - upper=crop.upper, - right=crop.right, - lower=crop.lower) + return screenshot_equal( + f"{golden_snap_path}", + snap, + left=crop.left, + upper=crop.upper, + right=crop.right, + lower=crop.lower, + ) else: return screenshot_equal(f"{golden_snap_path}", snap) @@ -304,10 +323,9 @@ def compare_screen_with_text(self, text: str) -> bool: return True return False - def _wait_for_text_on_screen_or_not(self, - should_be_on_screen: bool, - text: str, - timeout: float = 10.0) -> None: + def _wait_for_text_on_screen_or_not( + self, should_be_on_screen: bool, text: str, timeout: float = 10.0 + ) -> None: endtime = time() + timeout # Only manual ticks sent by compare_screen_with_text in this function because # we don't want a desync between screen and events @@ -371,32 +389,49 @@ def clean_args(cls: Type[T], speculos_args: List) -> None: speculos_args.pop(index) @classmethod - def batch(cls: Type[T], - application: Path, - device: Device, - number: int, - *args, - different_seeds: bool = True, - different_rng: bool = True, - different_private: bool = True, - different_attestation: bool = False, - **kwargs) -> List["SpeculosBackend"]: + def batch( + cls: Type[T], + application: Path, + device: Device, + number: int, + *args, + different_seeds: bool = True, + different_rng: bool = True, + different_private: bool = True, + different_attestation: bool = False, + **kwargs, + ) -> List["SpeculosBackend"]: logger = get_default_logger() - logger.info("Request to spawn %d Speculos instances of '%s'", number, application) + logger.info( + "Request to spawn %d Speculos instances of '%s'", number, application + ) test_port = STARTING_RANGE result: List["SpeculosBackend"] = list() while len(result) < number: tmp_kwargs = deepcopy(kwargs) api_port = _get_unused_port_from(test_port) apdu_port = _get_unused_port_from(api_port + 1) - logger.info("Instance %d ports: %d (API) and %s (APDU)", - len(result) + 1, api_port, apdu_port) + logger.info( + "Instance %d ports: %d (API) and %s (APDU)", + len(result) + 1, + api_port, + apdu_port, + ) test_port = apdu_port + 1 - additional_args = [cls._ARGS_API_PORT_KEY, str(api_port), "--apdu-port", str(apdu_port)] + additional_args = [ + cls._ARGS_API_PORT_KEY, + str(api_port), + "--apdu-port", + str(apdu_port), + ] if different_seeds: - additional_args.extend(["--seed", Mnemonic("english").generate(strength=256)]) + additional_args.extend( + ["--seed", Mnemonic("english").generate(strength=256)] + ) if different_rng: - additional_args.extend(["--deterministic-rng", f"{apdu_port}{api_port}"]) + additional_args.extend( + ["--deterministic-rng", f"{apdu_port}{api_port}"] + ) if different_private: additional_args.extend(["--user-private-key", urandom(32).hex()]) if different_attestation: diff --git a/src/ragger/backend/stub.py b/src/ragger/backend/stub.py index 7fa64556..cf7cb7e3 100644 --- a/src/ragger/backend/stub.py +++ b/src/ragger/backend/stub.py @@ -1,18 +1,19 @@ """ - Copyright 2023 Ledger SAS +Copyright 2023 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from contextlib import contextmanager from pathlib import Path from types import TracebackType @@ -35,8 +36,12 @@ class StubBackend(BackendInterface): def __enter__(self): pass - def __exit__(self, exc_type: Optional[Type[BaseException]], exc_val: Optional[BaseException], - exc_tb: Optional[TracebackType]): + def __exit__( + self, + exc_type: Optional[Type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ): pass def handle_usb_reset(self) -> None: @@ -67,18 +72,18 @@ def both_click(self) -> None: def finger_touch(self, x: int = 0, y: int = 0, delay: float = 0.5) -> None: pass - def finger_swipe(self, - x: int = 0, - y: int = 0, - direction: str = "left", - delay: float = 0.5) -> None: + def finger_swipe( + self, x: int = 0, y: int = 0, direction: str = "left", delay: float = 0.5 + ) -> None: pass - def compare_screen_with_snapshot(self, - golden_snap_path: Path, - crop: Optional[Crop] = None, - tmp_snap_path: Optional[Path] = None, - golden_run: bool = False) -> bool: + def compare_screen_with_snapshot( + self, + golden_snap_path: Path, + crop: Optional[Crop] = None, + tmp_snap_path: Optional[Path] = None, + golden_run: bool = False, + ) -> bool: return True def wait_for_home_screen(self, timeout: float = 10.0) -> None: diff --git a/src/ragger/bip/__init__.py b/src/ragger/bip/__init__.py index 8987d1d3..f8f05828 100644 --- a/src/ragger/bip/__init__.py +++ b/src/ragger/bip/__init__.py @@ -1,19 +1,24 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ -from .path import BtcDerivationPathFormat, pack_derivation_path, bitcoin_pack_derivation_path + +from .path import ( + BtcDerivationPathFormat, + pack_derivation_path, + bitcoin_pack_derivation_path, +) from .seed import CurveChoice, calculate_public_key_and_chaincode __all__ = [ diff --git a/src/ragger/bip/path.py b/src/ragger/bip/path.py index 01bede8c..6679f124 100644 --- a/src/ragger/bip/path.py +++ b/src/ragger/bip/path.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from bip_utils import Bip32Utils from enum import IntEnum @@ -31,18 +32,22 @@ def pack_derivation_path(derivation_path: str) -> bytes: if split[0] != "m": raise ValueError("Error master expected") - path_bytes: bytes = (len(split) - 1).to_bytes(1, byteorder='big') + path_bytes: bytes = (len(split) - 1).to_bytes(1, byteorder="big") for value in split[1:]: if value == "": raise ValueError(f'Error missing value in split list "{split}"') - if value.endswith('\''): - path_bytes += Bip32Utils.HardenIndex(int(value[:-1])).to_bytes(4, byteorder='big') + if value.endswith("'"): + path_bytes += Bip32Utils.HardenIndex(int(value[:-1])).to_bytes( + 4, byteorder="big" + ) else: - path_bytes += int(value).to_bytes(4, byteorder='big') + path_bytes += int(value).to_bytes(4, byteorder="big") return path_bytes -def bitcoin_pack_derivation_path(format: BtcDerivationPathFormat, derivation_path: str) -> bytes: +def bitcoin_pack_derivation_path( + format: BtcDerivationPathFormat, derivation_path: str +) -> bytes: if not isinstance(format, BtcDerivationPathFormat): raise ValueError(f'"{format}" must be a BtcDerivationPathFormat enum') return format.to_bytes(1, "big") + pack_derivation_path(derivation_path) diff --git a/src/ragger/bip/seed.py b/src/ragger/bip/seed.py index 20379c93..f7774200 100644 --- a/src/ragger/bip/seed.py +++ b/src/ragger/bip/seed.py @@ -27,13 +27,16 @@ class CurveChoice(Enum): SPECULOS_MNEMONIC = ( "glory promote mansion idle axis finger extra february uncover one trip resource " - "lawn turtle enact monster seven myth punch hobby comfort wild raise skin") + "lawn turtle enact monster seven myth punch hobby comfort wild raise skin" +) -def calculate_public_key_and_chaincode(curve: CurveChoice, - path: str, - mnemonic: Sequence[str] = SPECULOS_MNEMONIC, - compress_public_key: bool = False) -> Tuple[str, str]: +def calculate_public_key_and_chaincode( + curve: CurveChoice, + path: str, + mnemonic: Sequence[str] = SPECULOS_MNEMONIC, + compress_public_key: bool = False, +) -> Tuple[str, str]: if not isinstance(curve, CurveChoice): raise ValueError(f'"{curve}" must be a CurveChoice enum') diff --git a/src/ragger/conftest/base_conftest.py b/src/ragger/conftest/base_conftest.py index 62d5eb47..3c9ac51c 100644 --- a/src/ragger/conftest/base_conftest.py +++ b/src/ragger/conftest/base_conftest.py @@ -9,12 +9,30 @@ from typing import Generator, List, Optional from unittest.mock import MagicMock -from ragger.backend import BackendInterface, SpeculosBackend, LedgerCommBackend, LedgerWalletBackend +from ragger.backend import ( + BackendInterface, + SpeculosBackend, + LedgerCommBackend, + LedgerWalletBackend, +) from ragger.firmware import Firmware from ragger.logger import init_loggers, standalone_conf_logger -from ragger.navigator import Navigator, NanoNavigator, TouchNavigator, NavigateWithScenario -from ragger.utils import find_project_root_dir, find_library_application, find_application -from ragger.utils.misc import get_current_app_name_and_version, exit_current_app, open_app_from_dashboard +from ragger.navigator import ( + Navigator, + NanoNavigator, + TouchNavigator, + NavigateWithScenario, +) +from ragger.utils import ( + find_project_root_dir, + find_library_application, + find_application, +) +from ragger.utils.misc import ( + get_current_app_name_and_version, + exit_current_app, + open_app_from_dashboard, +) from ragger.error import MissingElfError from . import configuration as conf @@ -27,41 +45,55 @@ def pytest_addoption(parser): parser.addoption("--device", choices=DEVICES, required=True) parser.addoption("--backend", choices=BACKENDS, default="speculos") - parser.addoption("--no-nav", action="store_true", default=False, help="Disable the navigation") - parser.addoption("--display", - action="store_true", - default=False, - help="Pops up a Qt interface displaying either the emulated device (Speculos " - "backend) or the expected screens and actions (physical backend)") - parser.addoption("--golden_run", - action="store_true", - default=False, - help="Do not compare the snapshots during testing, but instead save the live " - "ones. Will only work with 'speculos' as the backend") - parser.addoption("--pki_prod", - action="store_true", - default=False, - help="Have Speculos accept prod PKI certificates instead of test") - parser.addoption("--log_apdu_file", - action="store", - default=None, - nargs="?", - const="apdu.log", - help="Log the APDU in a file. If no pattern provided, uses 'apdu_xxx.log'.") + parser.addoption( + "--no-nav", action="store_true", default=False, help="Disable the navigation" + ) + parser.addoption( + "--display", + action="store_true", + default=False, + help="Pops up a Qt interface displaying either the emulated device (Speculos " + "backend) or the expected screens and actions (physical backend)", + ) + parser.addoption( + "--golden_run", + action="store_true", + default=False, + help="Do not compare the snapshots during testing, but instead save the live " + "ones. Will only work with 'speculos' as the backend", + ) + parser.addoption( + "--pki_prod", + action="store_true", + default=False, + help="Have Speculos accept prod PKI certificates instead of test", + ) + parser.addoption( + "--log_apdu_file", + action="store", + default=None, + nargs="?", + const="apdu.log", + help="Log the APDU in a file. If no pattern provided, uses 'apdu_xxx.log'.", + ) parser.addoption("--seed", action="store", default=None, help="Set a custom seed") - parser.addoption("--ignore-missing-binaries", - action="store_true", - default=False, - help="Skip tests instead of failing when application binaries are missing") + parser.addoption( + "--ignore-missing-binaries", + action="store_true", + default=False, + help="Skip tests instead of failing when application binaries are missing", + ) # Always allow "default" even if application conftest does not define it allowed_setups = conf.OPTIONAL.ALLOWED_SETUPS if "default" not in allowed_setups: allowed_setups.insert(0, "default") - parser.addoption("--setup", - action="store", - default="default", - help="Specify the setup fixture (e.g., 'prod_build')", - choices=allowed_setups) + parser.addoption( + "--setup", + action="store", + default="default", + help="Specify the setup fixture (e.g., 'prod_build')", + choices=allowed_setups, + ) @pytest.fixture(scope="session") @@ -147,13 +179,13 @@ def full_test_name(request) -> str: # Get the name of current pytest test test_name = request.node.name - if '[' in test_name: + if "[" in test_name: # Split all parameters - base_name, params = test_name.rsplit('[', 1) - params = params.rstrip(']') + base_name, params = test_name.rsplit("[", 1) + params = params.rstrip("]") # Split parameters by '-' and filter out device names - param_list = [p for p in params.split('-') if p not in DEVICES] + param_list = [p for p in params.split("-") if p not in DEVICES] # Rebuild test name with remaining parameters if param_list: @@ -162,13 +194,15 @@ def full_test_name(request) -> str: test_name = base_name # Clean up for filename friendliness by replacing special characters with underscores - translation_table = str.maketrans({ - '[': '_', - ']': '', - '-': '_', - '/': '_', - "'": '', - }) + translation_table = str.maketrans( + { + "[": "_", + "]": "", + "-": "_", + "/": "_", + "'": "", + } + ) clean_name = test_name.translate(translation_table) return clean_name @@ -211,9 +245,12 @@ def pytest_generate_tests(metafunc): # Enable firmware for requested devices for fw in Devices(): - if device == fw.name or device == "all" or (device == "all_nano" - and fw.is_nano) or (device == "all_eink" - and not fw.is_nano): + if ( + device == fw.name + or device == "all" + or (device == "all_nano" and fw.is_nano) + or (device == "all_eink" and not fw.is_nano) + ): device_list.append(fw) ids.append(fw.name) @@ -231,9 +268,12 @@ def pytest_generate_tests(metafunc): # Enable firmware for requested devices for fw in Firmware: - if device == fw.name or device == "all" or (device == "all_nano" - and fw.is_nano) or (device == "all_eink" - and not fw.is_nano): + if ( + device == fw.name + or device == "all" + or (device == "all_nano" and fw.is_nano) + or (device == "all_eink" and not fw.is_nano) + ): firmware_list.append(fw) ids.append(fw.name) @@ -243,14 +283,16 @@ def pytest_generate_tests(metafunc): metafunc.parametrize("firmware", firmware_list, ids=ids, scope="session") -def prepare_speculos_args(root_pytest_dir: Path, - device: Device, - display: bool, - pki_prod: bool, - cli_user_seed: str, - additional_args: List[str], - verbose_speculos: bool = False, - ignore_missing_binaries: bool = False): +def prepare_speculos_args( + root_pytest_dir: Path, + device: Device, + display: bool, + pki_prod: bool, + cli_user_seed: str, + additional_args: List[str], + verbose_speculos: bool = False, + ignore_missing_binaries: bool = False, +): speculos_args = additional_args.copy() if display: @@ -275,7 +317,9 @@ def prepare_speculos_args(root_pytest_dir: Path, # project_root_dir / conf.OPTIONAL.MAIN_APP_DIR. There should be only one subfolder in the path. main_app_path = None if conf.OPTIONAL.MAIN_APP_DIR is not None: - app_dir_content = list((project_root_dir / conf.OPTIONAL.MAIN_APP_DIR).iterdir()) + app_dir_content = list( + (project_root_dir / conf.OPTIONAL.MAIN_APP_DIR).iterdir() + ) app_dir_subdirectories = [child for child in app_dir_content if child.is_dir()] if len(app_dir_subdirectories) != 1: raise ValueError( @@ -284,20 +328,28 @@ def prepare_speculos_args(root_pytest_dir: Path, main_app_path = find_application(app_dir_subdirectories[0], device_name, "c") # This repo holds the library, not the standalone app: search in build_directory - lib_path = find_application(project_root_dir / manifest.app.build_directory, device_name, - manifest.app.sdk) + lib_path = find_application( + project_root_dir / manifest.app.build_directory, + device_name, + manifest.app.sdk, + ) speculos_args.append(f"-l{lib_path}") # If the app is standalone, the main app should be located in project_root_dir / manifest.app.build_directory else: - main_app_path = find_application(project_root_dir / manifest.app.build_directory, - device_name, manifest.app.sdk) + main_app_path = find_application( + project_root_dir / manifest.app.build_directory, + device_name, + manifest.app.sdk, + ) # Legacy lib method, remove once exchange is ported if len(conf.OPTIONAL.SIDELOADED_APPS) != 0: # We are testing a a standalone app that needs libraries: search in SIDELOADED_APPS_DIR if conf.OPTIONAL.SIDELOADED_APPS_DIR is None: - raise ValueError("Configuration \"SIDELOADED_APPS_DIR\" is mandatory if " - "\"SIDELOADED_APPS\" is used") + raise ValueError( + 'Configuration "SIDELOADED_APPS_DIR" is mandatory if ' + '"SIDELOADED_APPS" is used' + ) libs_dir = Path(project_root_dir / conf.OPTIONAL.SIDELOADED_APPS_DIR) # Add "-l Appname:filepath" to Speculos command line for every required lib app for coin_name, lib_name in conf.OPTIONAL.SIDELOADED_APPS.items(): @@ -306,8 +358,10 @@ def prepare_speculos_args(root_pytest_dir: Path, speculos_args.append(f"-l{lib_name}:{lib_path}") except MissingElfError as e: if ignore_missing_binaries: - warnings.warn(f"Could not find sideloaded app library for '{lib_name}': {e}", - UserWarning) + warnings.warn( + f"Could not find sideloaded app library for '{lib_name}': {e}", + UserWarning, + ) else: raise else: @@ -316,17 +370,24 @@ def prepare_speculos_args(root_pytest_dir: Path, if conf.OPTIONAL.SIDELOADED_APPS_DIR is not None: sideloaded_dir = project_root_dir / conf.OPTIONAL.SIDELOADED_APPS_DIR subdirs = sorted( - filter(lambda d: (sideloaded_dir / d).is_dir(), os.listdir(sideloaded_dir))) + filter( + lambda d: (sideloaded_dir / d).is_dir(), os.listdir(sideloaded_dir) + ) + ) for subdir in subdirs: try: # Currently only C apps are used as additional binaries by ragger (Ethereum and Exchange) # TODO: add support for Rust SDK libraries if needed - lib_path = find_application(sideloaded_dir / subdir, device_name, "c") + lib_path = find_application( + sideloaded_dir / subdir, device_name, "c" + ) speculos_args.append(f"-l{lib_path}") except MissingElfError as e: if ignore_missing_binaries: - warnings.warn(f"Could not find sideloaded app binary for '{subdir}': {e}", - UserWarning) + warnings.warn( + f"Could not find sideloaded app binary for '{subdir}': {e}", + UserWarning, + ) else: raise @@ -343,53 +404,82 @@ def prepare_speculos_args(root_pytest_dir: Path, # Depending on the "--backend" option value, a different backend is # instantiated, and the tests will either run on Speculos or on a physical # device depending on the backend -def create_backend(root_pytest_dir: Path, - backend_name: str, - device: Device, - display: bool, - pki_prod: bool, - log_apdu_file: Optional[Path], - cli_user_seed: str, - additional_speculos_arguments: List[str], - verbose_speculos: bool = False, - ignore_missing_binaries: bool = False) -> BackendInterface: +def create_backend( + root_pytest_dir: Path, + backend_name: str, + device: Device, + display: bool, + pki_prod: bool, + log_apdu_file: Optional[Path], + cli_user_seed: str, + additional_speculos_arguments: List[str], + verbose_speculos: bool = False, + ignore_missing_binaries: bool = False, +) -> BackendInterface: if backend_name.lower() == "ledgercomm": - return LedgerCommBackend(device=device, - interface="hid", - log_apdu_file=log_apdu_file, - with_gui=display) + return LedgerCommBackend( + device=device, + interface="hid", + log_apdu_file=log_apdu_file, + with_gui=display, + ) elif backend_name.lower() == "ledgerwallet": - return LedgerWalletBackend(device=device, log_apdu_file=log_apdu_file, with_gui=display) + return LedgerWalletBackend( + device=device, log_apdu_file=log_apdu_file, with_gui=display + ) elif backend_name.lower() == "speculos": - main_app_path, speculos_args = prepare_speculos_args(root_pytest_dir, device, display, - pki_prod, cli_user_seed, - additional_speculos_arguments, - verbose_speculos, - ignore_missing_binaries) - return SpeculosBackend(main_app_path, - device=device, - log_apdu_file=log_apdu_file, - **speculos_args) + main_app_path, speculos_args = prepare_speculos_args( + root_pytest_dir, + device, + display, + pki_prod, + cli_user_seed, + additional_speculos_arguments, + verbose_speculos, + ignore_missing_binaries, + ) + return SpeculosBackend( + main_app_path, device=device, log_apdu_file=log_apdu_file, **speculos_args + ) else: - raise ValueError(f"Backend '{backend_name}' is unknown. Valid backends are: {BACKENDS}") + raise ValueError( + f"Backend '{backend_name}' is unknown. Valid backends are: {BACKENDS}" + ) # Backend scope can be configured by the user # fixture skip_tests_for_unsupported_devices is a dependency because we want to skip the test # before trying to find the binary @pytest.fixture(scope=conf.OPTIONAL.BACKEND_SCOPE) -def backend(skip_tests_for_unsupported_devices, root_pytest_dir: Path, backend_name: str, - device: Device, display: bool, pki_prod: bool, log_apdu_file: Optional[Path], - cli_user_seed: str, additional_speculos_arguments: List[str], verbose_speculos: bool, - ignore_missing_binaries: bool) -> Generator[BackendInterface, None, None]: +def backend( + skip_tests_for_unsupported_devices, + root_pytest_dir: Path, + backend_name: str, + device: Device, + display: bool, + pki_prod: bool, + log_apdu_file: Optional[Path], + cli_user_seed: str, + additional_speculos_arguments: List[str], + verbose_speculos: bool, + ignore_missing_binaries: bool, +) -> Generator[BackendInterface, None, None]: # to separate the test name and its following logs print("") backend_instance = None try: - backend_instance = create_backend(root_pytest_dir, backend_name, device, display, pki_prod, - log_apdu_file, cli_user_seed, - additional_speculos_arguments, verbose_speculos, - ignore_missing_binaries) + backend_instance = create_backend( + root_pytest_dir, + backend_name, + device, + display, + pki_prod, + log_apdu_file, + cli_user_seed, + additional_speculos_arguments, + verbose_speculos, + ignore_missing_binaries, + ) except MissingElfError as e: pytest.fail(f"Missing ELF: {e}") @@ -410,8 +500,13 @@ def backend(skip_tests_for_unsupported_devices, root_pytest_dir: Path, backend_n @pytest.fixture(scope=conf.OPTIONAL.BACKEND_SCOPE) -def navigator(backend: BackendInterface, device: Device, golden_run: bool, display: bool, - navigation: bool): +def navigator( + backend: BackendInterface, + device: Device, + golden_run: bool, + display: bool, + navigation: bool, +): if not navigation: return MagicMock() @@ -422,14 +517,21 @@ def navigator(backend: BackendInterface, device: Device, golden_run: bool, displ @pytest.fixture(scope="function") -def scenario_navigator(backend: BackendInterface, navigator: Navigator, device: Device, - test_name: str, default_screenshot_path: Path): - return NavigateWithScenario(backend, navigator, device, test_name, default_screenshot_path) +def scenario_navigator( + backend: BackendInterface, + navigator: Navigator, + device: Device, + test_name: str, + default_screenshot_path: Path, +): + return NavigateWithScenario( + backend, navigator, device, test_name, default_screenshot_path + ) @pytest.fixture(autouse=True) def use_only_on_backend(request, backend_name): - marker = request.node.get_closest_marker('use_on_backend') + marker = request.node.get_closest_marker("use_on_backend") if marker: current_backend = marker.args[0] if current_backend != backend_name: @@ -442,11 +544,14 @@ def use_only_on_backend(request, backend_name): def pytest_collection_modifyitems(config, items): current = config.getoption("--setup") for item in items: - marker = item.get_closest_marker('needs_setup') + marker = item.get_closest_marker("needs_setup") needed = marker.args[0] if marker else "default" if needed != current: item.add_marker( - pytest.mark.skip(reason=f"Test requires setup '{needed}' but setup is '{current}'")) + pytest.mark.skip( + reason=f"Test requires setup '{needed}' but setup is '{current}'" + ) + ) # Fixture like function that will configure the ragger log level diff --git a/src/ragger/conftest/configuration.py b/src/ragger/conftest/configuration.py index bf90e87e..de991e8d 100644 --- a/src/ragger/conftest/configuration.py +++ b/src/ragger/conftest/configuration.py @@ -17,7 +17,6 @@ class OptionalOptions: # Use this parameter if you want physical Ragger backends (LedgerWallet and LedgerComm) to start # your application from the Dashboard at test start. APP_NAME=str(), - # If not None, the application being tested with Ragger should be loaded as a library and not as # a standalone application. This parameter points to the repository holding the "main app", i.e # the application started from the Dashboard, which will then use the "local app" as a library. @@ -29,10 +28,8 @@ class OptionalOptions: # Speculos will then start "tests/.test_dependencies/ethereum/build//bin/app.elf" # There must be exactly one application cloned inside this directory. MAIN_APP_DIR=None, - # Deprecated SIDELOADED_APPS=dict(), - # Relative path to the directory that will store library applications needed for the test. # They will be sideloaded by Speculos and can then be called by the main application. This # emulates the applications being installed on the device alongside the main application. @@ -40,19 +37,16 @@ class OptionalOptions: # example: configuration.OPTIONAL.SIDELOADED_APPS_DIR = "tests/.test_dependencies/libraries" # Speculos will then sideload "tests/.test_dependencies/libraries/*/build//bin/app.elf" SIDELOADED_APPS_DIR=None, - # As the backend instantiation may take some time, Ragger supports multiple backend scopes. # You can choose to share the backend instance between {session / module / class / function} # When using "session" all your tests will share a single backend instance (faster) # When using "function" each test will have its independent backend instance (no collusion) BACKEND_SCOPE="class", - # Use this parameter if you want speculos to use a custom seed instead of the default one. # This would result in speculos being launched with --seed # If a seed is provided through the "--seed" pytest command line option, it will override this one. # /!\ DO NOT USE SEEDS WITH REAL FUNDS /!\ CUSTOM_SEED=str(), - # /!\ DEPRECATED /!\ # Use this parameter if you want ragger to handle running different test suites depending on setup # Useful when some tests need certain build options and other tests need other build options, or a diff --git a/src/ragger/error.py b/src/ragger/error.py index aa589de5..f40861b9 100644 --- a/src/ragger/error.py +++ b/src/ragger/error.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from dataclasses import dataclass from enum import IntEnum diff --git a/src/ragger/firmware/__init__.py b/src/ragger/firmware/__init__.py index 9cfa19a0..6445d2c7 100644 --- a/src/ragger/firmware/__init__.py +++ b/src/ragger/firmware/__init__.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from .structs import DEPRECATION_MESSAGE, Firmware __all__ = ["DEPRECATION_MESSAGE", "Firmware"] diff --git a/src/ragger/firmware/structs.py b/src/ragger/firmware/structs.py index bf480291..7568a8f3 100644 --- a/src/ragger/firmware/structs.py +++ b/src/ragger/firmware/structs.py @@ -1,25 +1,28 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from enum import IntEnum from ledgered.devices import Devices, DeviceType from warnings import warn -DEPRECATION_MESSAGE = "`ragger.firmware.Firmware` is deprecated, use `ledgered.devices.Devices` " \ +DEPRECATION_MESSAGE = ( + "`ragger.firmware.Firmware` is deprecated, use `ledgered.devices.Devices` " "or `ledgered.devices.DeviceType` instead" +) class Firmware(IntEnum): diff --git a/src/ragger/firmware/touch/__init__.py b/src/ragger/firmware/touch/__init__.py index 95c44316..4b4dc896 100644 --- a/src/ragger/firmware/touch/__init__.py +++ b/src/ragger/firmware/touch/__init__.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from .screen import MetaScreen, FullScreen __all__ = ["MetaScreen", "FullScreen"] diff --git a/src/ragger/firmware/touch/element.py b/src/ragger/firmware/touch/element.py index 650486d9..f6858a62 100644 --- a/src/ragger/firmware/touch/element.py +++ b/src/ragger/firmware/touch/element.py @@ -1,18 +1,19 @@ """ - Copyright 2024 Ledger SAS +Copyright 2024 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from ledgered.devices import Device from ragger.backend import BackendInterface @@ -20,7 +21,6 @@ class Element: - def __init__(self, client: BackendInterface, device: Device): self._client = client self._device = device @@ -39,7 +39,6 @@ def positions(self): class Center(Element): - def swipe_left(self): self.client.finger_swipe(*self.positions, direction="left") diff --git a/src/ragger/firmware/touch/layouts.py b/src/ragger/firmware/touch/layouts.py index 01704e8b..f44b5658 100644 --- a/src/ragger/firmware/touch/layouts.py +++ b/src/ragger/firmware/touch/layouts.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + import logging from .element import Element @@ -22,14 +23,12 @@ class ChoiceList(Element): - def choose(self, index: int): assert 1 <= index <= 6, "Choice index must be in [1, 6]" self.client.finger_touch(*self.positions[index]) class Suggestions(Element): - def choose(self, index: int): assert 1 <= index <= 4, "Suggestion index must be in [1, 4]" self.client.finger_touch(*self.positions[index]) @@ -37,10 +36,11 @@ def choose(self, index: int): # Keyboards class _GenericKeyboard(Element): - def write(self, word: str): for letter in word.lower(): - logging.info("Writing letter '%s', position '%s'", letter, self.positions[letter]) + logging.info( + "Writing letter '%s', position '%s'", letter, self.positions[letter] + ) self.client.finger_touch(*self.positions[letter]) def back(self): @@ -52,19 +52,16 @@ class LetterOnlyKeyboard(_GenericKeyboard): class _FullKeyboard(_GenericKeyboard): - def change_layout(self): self.client.finger_touch(*self.positions["change_layout"]) class FullKeyboardLetters(_FullKeyboard): - def change_case(self): self.client.finger_touch(*self.positions["change_case"]) class _FullKeyboardSpecialCharacters(_FullKeyboard): - def more_specials(self): self.client.finger_touch(*self.positions["more_specials"]) @@ -78,7 +75,6 @@ class FullKeyboardSpecialCharacters2(_FullKeyboardSpecialCharacters): class _TappableElement(Element): - def tap(self): self.client.finger_touch(*self.positions) diff --git a/src/ragger/firmware/touch/positions.py b/src/ragger/firmware/touch/positions.py index d5477705..a753815e 100644 --- a/src/ragger/firmware/touch/positions.py +++ b/src/ragger/firmware/touch/positions.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from dataclasses import astuple, dataclass from ledgered.devices import DeviceType, Devices, Resolution @@ -443,7 +444,7 @@ def __iter__(self): ")": Position(240, 470), "&": Position(280, 470), "@": Position(320, 470), - "\"": Position(360, 470), + '"': Position(360, 470), # third line "more_specials": Position(50, 525), ".": Position(120, 525), @@ -477,7 +478,7 @@ def __iter__(self): ")": Position(288, 420), "&": Position(336, 420), "@": Position(384, 420), - "\"": Position(432, 420), + '"': Position(432, 420), # third line "more_specials": Position(60, 490), ".": Position(144, 490), @@ -511,7 +512,7 @@ def __iter__(self): ")": Position(180, 270), "&": Position(210, 270), "@": Position(240, 270), - "\"": Position(270, 270), + '"': Position(270, 270), # third line "more_specials": Position(35, 320), ".": Position(90, 320), @@ -545,7 +546,7 @@ def __iter__(self): ")": Position(180, 270), "&": Position(210, 270), "@": Position(240, 270), - "\"": Position(270, 270), + '"': Position(270, 270), # third line "more_specials": Position(35, 320), ".": Position(90, 320), @@ -581,7 +582,7 @@ def __iter__(self): ">": Position(240, 470), "$": Position(280, 470), "`": Position(320, 470), - "\"": Position(360, 470), + '"': Position(360, 470), # third line "more_specials": Position(50, 525), ".": Position(120, 525), @@ -615,7 +616,7 @@ def __iter__(self): "<": Position(288, 420), "$": Position(336, 420), "`": Position(384, 420), - "\"": Position(432, 420), + '"': Position(432, 420), # third line "more_specials": Position(60, 490), ".": Position(144, 490), @@ -649,7 +650,7 @@ def __iter__(self): "<": Position(180, 270), "$": Position(210, 270), "`": Position(240, 270), - "\"": Position(270, 270), + '"': Position(270, 270), # third line "more_specials": Position(35, 320), ".": Position(90, 320), @@ -683,7 +684,7 @@ def __iter__(self): "<": Position(180, 270), "$": Position(210, 270), "`": Position(240, 270), - "\"": Position(270, 270), + '"': Position(270, 270), # third line "more_specials": Position(35, 320), ".": Position(90, 320), @@ -701,85 +702,85 @@ def __iter__(self): DeviceType.STAX: STAX_CENTER, DeviceType.FLEX: FLEX_CENTER, DeviceType.APEX_P: APEX_P_CENTER, - DeviceType.APEX_M: APEX_M_CENTER + DeviceType.APEX_M: APEX_M_CENTER, }, "RightHeader": { DeviceType.STAX: STAX_BUTTON_UPPER_RIGHT, DeviceType.FLEX: FLEX_BUTTON_UPPER_RIGHT, DeviceType.APEX_P: APEX_P_BUTTON_UPPER_RIGHT, - DeviceType.APEX_M: APEX_M_BUTTON_UPPER_RIGHT + DeviceType.APEX_M: APEX_M_BUTTON_UPPER_RIGHT, }, "LeftHeader": { DeviceType.STAX: STAX_BUTTON_UPPER_LEFT, DeviceType.FLEX: FLEX_BUTTON_UPPER_LEFT, DeviceType.APEX_P: APEX_P_BUTTON_UPPER_LEFT, - DeviceType.APEX_M: APEX_M_BUTTON_UPPER_LEFT + DeviceType.APEX_M: APEX_M_BUTTON_UPPER_LEFT, }, "CenteredFooter": { DeviceType.STAX: STAX_BUTTON_LOWER_MIDDLE, DeviceType.FLEX: FLEX_BUTTON_LOWER_MIDDLE, DeviceType.APEX_P: APEX_P_BUTTON_LOWER_MIDDLE, - DeviceType.APEX_M: APEX_M_BUTTON_LOWER_MIDDLE + DeviceType.APEX_M: APEX_M_BUTTON_LOWER_MIDDLE, }, "LeftFooter": { DeviceType.STAX: STAX_BUTTON_LOWER_LEFT, DeviceType.FLEX: FLEX_BUTTON_LOWER_LEFT, DeviceType.APEX_P: APEX_P_BUTTON_LOWER_LEFT, - DeviceType.APEX_M: APEX_M_BUTTON_LOWER_LEFT + DeviceType.APEX_M: APEX_M_BUTTON_LOWER_LEFT, }, "CancelFooter": { DeviceType.STAX: STAX_BUTTON_LOWER_LEFT, DeviceType.FLEX: FLEX_BUTTON_LOWER_LEFT, DeviceType.APEX_P: APEX_P_BUTTON_LOWER_LEFT, - DeviceType.APEX_M: APEX_M_BUTTON_LOWER_LEFT + DeviceType.APEX_M: APEX_M_BUTTON_LOWER_LEFT, }, "UseCaseHome": { DeviceType.STAX: { "info": STAX_BUTTON_UPPER_RIGHT, "settings": STAX_BUTTON_UPPER_RIGHT, - "quit": STAX_BUTTON_LOWER_MIDDLE + "quit": STAX_BUTTON_LOWER_MIDDLE, }, DeviceType.FLEX: { "info": FLEX_BUTTON_UPPER_RIGHT, "settings": FLEX_BUTTON_UPPER_RIGHT, - "quit": FLEX_BUTTON_LOWER_MIDDLE + "quit": FLEX_BUTTON_LOWER_MIDDLE, }, DeviceType.APEX_P: { "info": APEX_P_BUTTON_UPPER_RIGHT, "settings": APEX_P_BUTTON_UPPER_RIGHT, - "quit": APEX_P_BUTTON_LOWER_MIDDLE + "quit": APEX_P_BUTTON_LOWER_MIDDLE, }, DeviceType.APEX_M: { "info": APEX_M_BUTTON_UPPER_RIGHT, "settings": APEX_M_BUTTON_UPPER_RIGHT, - "quit": APEX_M_BUTTON_LOWER_MIDDLE - } + "quit": APEX_M_BUTTON_LOWER_MIDDLE, + }, }, "UseCaseHomeExt": { DeviceType.STAX: { "info": STAX_BUTTON_UPPER_RIGHT, "settings": STAX_BUTTON_UPPER_RIGHT, "action": STAX_BUTTON_ABOVE_LOWER_MIDDLE, - "quit": STAX_BUTTON_LOWER_MIDDLE + "quit": STAX_BUTTON_LOWER_MIDDLE, }, DeviceType.FLEX: { "info": FLEX_BUTTON_UPPER_RIGHT, "settings": FLEX_BUTTON_UPPER_RIGHT, "action": FLEX_BUTTON_ABOVE_LOWER_MIDDLE, - "quit": FLEX_BUTTON_LOWER_MIDDLE + "quit": FLEX_BUTTON_LOWER_MIDDLE, }, DeviceType.APEX_P: { "info": APEX_P_BUTTON_UPPER_RIGHT, "settings": APEX_P_BUTTON_UPPER_RIGHT, "action": APEX_P_BUTTON_ABOVE_LOWER_MIDDLE, - "quit": APEX_P_BUTTON_LOWER_MIDDLE + "quit": APEX_P_BUTTON_LOWER_MIDDLE, }, DeviceType.APEX_M: { "info": APEX_M_BUTTON_UPPER_RIGHT, "settings": APEX_M_BUTTON_UPPER_RIGHT, "action": APEX_M_BUTTON_ABOVE_LOWER_MIDDLE, - "quit": APEX_M_BUTTON_LOWER_MIDDLE - } + "quit": APEX_M_BUTTON_LOWER_MIDDLE, + }, }, "UseCaseSettings": { DeviceType.STAX: { @@ -805,7 +806,7 @@ def __iter__(self): "multi_page_exit": APEX_M_BUTTON_UPPER_LEFT, "previous": APEX_M_BUTTON_LOWER_MIDDLE_RIGHT, "next": APEX_M_BUTTON_LOWER_RIGHT, - } + }, }, "UseCaseSubSettings": { DeviceType.STAX: { @@ -827,7 +828,7 @@ def __iter__(self): "exit": APEX_M_BUTTON_UPPER_LEFT, "previous": APEX_M_BUTTON_LOWER_LEFT, "next": APEX_M_BUTTON_LOWER_RIGHT, - } + }, }, "UseCaseChoice": { DeviceType.STAX: { @@ -845,7 +846,7 @@ def __iter__(self): DeviceType.APEX_M: { "confirm": APEX_M_BUTTON_ABOVE_LOWER_MIDDLE, "reject": APEX_M_BUTTON_LOWER_LEFT, - } + }, }, "UseCaseStatus": { DeviceType.STAX: { @@ -859,7 +860,7 @@ def __iter__(self): }, DeviceType.APEX_M: { "dismiss": APEX_M_CENTER, - } + }, }, "UseCaseReview": { DeviceType.STAX: { @@ -885,7 +886,7 @@ def __iter__(self): "previous": APEX_M_BUTTON_LOWER_MIDDLE, "confirm": APEX_M_BUTTON_ABOVE_LOWER_MIDDLE, "reject": APEX_M_BUTTON_LOWER_LEFT, - } + }, }, "UseCaseViewDetails": { DeviceType.STAX: { @@ -907,7 +908,7 @@ def __iter__(self): "exit": APEX_M_BUTTON_LOWER_LEFT, "previous": APEX_M_BUTTON_LOWER_MIDDLE, "next": APEX_M_BUTTON_LOWER_RIGHT, - } + }, }, "UseCaseAddressConfirmation": { DeviceType.STAX: { @@ -933,6 +934,6 @@ def __iter__(self): "exit_qr": APEX_M_BUTTON_LOWER_MIDDLE, "confirm": APEX_M_BUTTON_ABOVE_LOWER_MIDDLE, "cancel": APEX_M_BUTTON_LOWER_LEFT, - } + }, }, } diff --git a/src/ragger/firmware/touch/screen.py b/src/ragger/firmware/touch/screen.py index 63cf3ccb..05344b1f 100644 --- a/src/ragger/firmware/touch/screen.py +++ b/src/ragger/firmware/touch/screen.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from typing import Dict, Tuple from ledgered.devices import Device @@ -20,13 +21,36 @@ from .element import Center -from .layouts import CancelFooter, CenteredFooter, ChoiceList, ExitFooter, ExitHeader, \ - FullKeyboardLetters, FullKeyboardSpecialCharacters1, FullKeyboardSpecialCharacters2, \ - InfoFooter, InfoHeader, LeftHeader, LetterOnlyKeyboard, NavigationHeader, RightHeader, \ - SettingsFooter, Suggestions, TappableCenter - -from .use_cases import UseCaseHome, UseCaseSettings, UseCaseSubSettings, UseCaseChoice, \ - UseCaseStatus, UseCaseReview, UseCaseViewDetails, UseCaseAddressConfirmation +from .layouts import ( + CancelFooter, + CenteredFooter, + ChoiceList, + ExitFooter, + ExitHeader, + FullKeyboardLetters, + FullKeyboardSpecialCharacters1, + FullKeyboardSpecialCharacters2, + InfoFooter, + InfoHeader, + LeftHeader, + LetterOnlyKeyboard, + NavigationHeader, + RightHeader, + SettingsFooter, + Suggestions, + TappableCenter, +) + +from .use_cases import ( + UseCaseHome, + UseCaseSettings, + UseCaseSubSettings, + UseCaseChoice, + UseCaseStatus, + UseCaseReview, + UseCaseViewDetails, + UseCaseAddressConfirmation, +) ELEMENT_PREFIX = "element_" LAYOUT_PREFIX = "layout_" @@ -83,15 +107,18 @@ class Screen(metaclass=MetaScreen): def __new__(cls, name: str, parents: Tuple, namespace: Dict): elements = { key.split(ELEMENT_PREFIX)[1]: namespace.pop(key) - for key in list(namespace.keys()) if key.startswith(ELEMENT_PREFIX) + for key in list(namespace.keys()) + if key.startswith(ELEMENT_PREFIX) } layouts = { key.split(LAYOUT_PREFIX)[1]: namespace.pop(key) - for key in list(namespace.keys()) if key.startswith(LAYOUT_PREFIX) + for key in list(namespace.keys()) + if key.startswith(LAYOUT_PREFIX) } use_cases = { key.split(USE_CASE_PREFIX)[1]: namespace.pop(key) - for key in list(namespace.keys()) if key.startswith(USE_CASE_PREFIX) + for key in list(namespace.keys()) + if key.startswith(USE_CASE_PREFIX) } original_init = namespace.pop("__init__", lambda *args, **kwargs: None) diff --git a/src/ragger/firmware/touch/use_cases.py b/src/ragger/firmware/touch/use_cases.py index f524bd23..e4f87c01 100644 --- a/src/ragger/firmware/touch/use_cases.py +++ b/src/ragger/firmware/touch/use_cases.py @@ -1,23 +1,23 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from .element import Element class UseCaseHome(Element): - def info(self): self.client.finger_touch(*self.positions["info"]) @@ -29,13 +29,11 @@ def quit(self): class UseCaseHomeExt(UseCaseHome): - def action(self): self.client.finger_touch(*self.positions["action"]) class UseCaseSettings(Element): - def single_page_exit(self): self.client.finger_touch(*self.positions["single_page_exit"]) @@ -50,7 +48,6 @@ def next(self): class UseCaseSubSettings(Element): - def exit(self): self.client.finger_touch(*self.positions["exit"]) @@ -62,7 +59,6 @@ def next(self): class UseCaseChoice(Element): - def confirm(self): self.client.finger_touch(*self.positions["confirm"]) @@ -71,13 +67,11 @@ def reject(self): class UseCaseStatus(Element): - def dismiss(self): self.client.finger_touch(*self.positions["dismiss"]) class UseCaseReview(Element): - def tap(self): self.client.finger_touch(*self.positions["tap"]) @@ -93,7 +87,6 @@ def confirm(self): class UseCaseViewDetails(Element): - def exit(self): self.client.finger_touch(*self.positions["exit"]) @@ -105,7 +98,6 @@ def next(self): class UseCaseAddressConfirmation(Element): - def tap(self): self.client.finger_touch(*self.positions["tap"]) diff --git a/src/ragger/gui/__init__.py b/src/ragger/gui/__init__.py index 35bfd66f..3190eb44 100644 --- a/src/ragger/gui/__init__.py +++ b/src/ragger/gui/__init__.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + try: from .process import RaggerGUI except ImportError as e: @@ -22,7 +23,8 @@ def RaggerGUI(*args, **kwatgs): # type: ignore raise ImportError( - "This feature needs PyQt6. Please install this package (run `pip install pyqt6`)") + "This feature needs PyQt6. Please install this package (run `pip install pyqt6`)" + ) __all__ = ["RaggerGUI"] diff --git a/src/ragger/gui/interface.py b/src/ragger/gui/interface.py index bd3938b5..385c5da4 100644 --- a/src/ragger/gui/interface.py +++ b/src/ragger/gui/interface.py @@ -1,23 +1,32 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from functools import partial from pathlib import Path from PyQt6.QtCore import QRect, Qt, QVariantAnimation -from PyQt6.QtWidgets import QApplication, QWidget, QMainWindow, QLabel, QPushButton, QSizePolicy, \ - QGridLayout, QGraphicsOpacityEffect +from PyQt6.QtWidgets import ( + QApplication, + QWidget, + QMainWindow, + QLabel, + QPushButton, + QSizePolicy, + QGridLayout, + QGraphicsOpacityEffect, +) from PyQt6.QtGui import QAction, QGuiApplication, QIcon, QPixmap, QFont, QKeyEvent from typing import Callable @@ -37,7 +46,6 @@ class RaggerMainWindow(QMainWindow): - def __init__(self, device: str): super().__init__() self._devicebody: QLabel @@ -52,32 +60,37 @@ def __init__(self, device: str): self.logger.info("Initiated") def _init_UI(self): - exitAct = QAction(QIcon('exit.png'), '&Exit', self) - exitAct.setShortcut('Ctrl+Q') - exitAct.setStatusTip('Exit application') + exitAct = QAction(QIcon("exit.png"), "&Exit", self) + exitAct.setShortcut("Ctrl+Q") + exitAct.setStatusTip("Exit application") exitAct.triggered.connect(QApplication.quit) menubar = self.menuBar() - fileMenu = menubar.addMenu('&Menu') + fileMenu = menubar.addMenu("&Menu") fileMenu.addAction(exitAct) self.resize(WIDTH, HEIGHT) qr = self.frameGeometry() cp = QGuiApplication.primaryScreen().availableGeometry().center() qr.moveCenter(cp) self.move(qr.topLeft()) - self.setWindowTitle('Ragger - Ledger Nano app automation framework') - self.setWindowIcon(QIcon('/home/lpascal/repos/tools/ragger/doc/images/ragger.png')) + self.setWindowTitle("Ragger - Ledger Nano app automation framework") + self.setWindowIcon( + QIcon("/home/lpascal/repos/tools/ragger/doc/images/ragger.png") + ) self._init_gui_widgets() self.show() def _bigger(self, screenshot: Path) -> QPixmap: - return QPixmap(str(screenshot.resolve()))\ - .scaled(SCREENSHOT_MAX_WIDTH, SCREENSHOT_MAX_HEIGHT, Qt.KeepAspectRatio) + return QPixmap(str(screenshot.resolve())).scaled( + SCREENSHOT_MAX_WIDTH, SCREENSHOT_MAX_HEIGHT, Qt.KeepAspectRatio + ) def _init_screenshot(self) -> None: self._screenshot = QLabel(self._central_widget) self._screenshot.setScaledContents(False) self._screenshot.setObjectName("screenshot") - self._screenshot.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self._screenshot.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) self._screenshot.setAlignment(Qt.AlignmentFlag.AlignVCenter) dict_margin = { "nanos": 75, @@ -86,35 +99,45 @@ def _init_screenshot(self) -> None: "stax": 65, "flex": 65, "apex_p": 65, - "apex_m": 65 + "apex_m": 65, } margin = dict_margin[self._device] self._screenshot.setStyleSheet(f"QLabel {{margin-left: {margin}px;}}") def _init_action_hint(self) -> None: self._actionhint = QLabel(self._central_widget) - self._actionhint.setGeometry(QRect(0, 0, SCREENSHOT_MAX_WIDTH, SCREENSHOT_MAX_HEIGHT)) + self._actionhint.setGeometry( + QRect(0, 0, SCREENSHOT_MAX_WIDTH, SCREENSHOT_MAX_HEIGHT) + ) self._actionhint.setScaledContents(False) self._actionhint.setObjectName("action_hint") - self._actionhint.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self._actionhint.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) self._actionhint.setAlignment(Qt.AlignmentFlag.AlignCenter) custom_font = QFont() custom_font.setWeight(30) self._actionhint.setFont(custom_font) self._actionhint.setText("") - margin_top = self._devicebody.height() + self._actionhint.fontInfo().pixelSize() + 10 + margin_top = ( + self._devicebody.height() + self._actionhint.fontInfo().pixelSize() + 10 + ) self._actionhint.setStyleSheet(f"QLabel {{margin-top: {margin_top}px;}}") self._actionhint.show() def _show_button(self, widget: QWidget, show: bool, x: int = 0, y: int = 0): if widget.objectName in [ - self._lb.objectName, self._rb.objectName, self._touch.objectName, - self._swipe_left.objectName, self._swipe_right.objectName + self._lb.objectName, + self._rb.objectName, + self._touch.objectName, + self._swipe_left.objectName, + self._swipe_right.objectName, ]: if show: if widget.objectName in [ - self._touch.objectName, self._swipe_left.objectName, - self._swipe_right.objectName + self._touch.objectName, + self._swipe_left.objectName, + self._swipe_right.objectName, ]: margin_left = 65 - widget.width() // 2 margin_top = 25 # Tip of finger @@ -134,14 +157,19 @@ def _init_device_body(self) -> None: self._devicebody.setPixmap(bodypix) self._devicebody.setMinimumHeight(bodypix.height()) - self._devicebody.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding) + self._devicebody.setSizePolicy( + QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding + ) self._devicebody.setAlignment(Qt.AlignmentFlag.AlignCenter) self._lb = QLabel(self._central_widget) self._lb.setScaledContents(False) self._lb.setObjectName("left_button") self._lb.setPixmap( - QPixmap(str(Path(__file__).parent / "assets" / f"{self._device}_leftbutton.png"))) + QPixmap( + str(Path(__file__).parent / "assets" / f"{self._device}_leftbutton.png") + ) + ) self._lb.setAlignment(Qt.AlignmentFlag.AlignCenter) self._lb.hide() @@ -149,7 +177,12 @@ def _init_device_body(self) -> None: self._rb.setScaledContents(False) self._rb.setObjectName("right_button") self._rb.setPixmap( - QPixmap(str(Path(__file__).parent / "assets" / f"{self._device}_rightbutton.png"))) + QPixmap( + str( + Path(__file__).parent / "assets" / f"{self._device}_rightbutton.png" + ) + ) + ) self._rb.setAlignment(Qt.AlignmentFlag.AlignCenter) self._rb.hide() @@ -164,17 +197,24 @@ def _init_device_body(self) -> None: self._swipe_left = QLabel(self._central_widget) self._swipe_left.setScaledContents(False) self._swipe_left.setObjectName("swipe_left") - swipe_left_pix = QPixmap(str(Path(__file__).parent / "assets/swipe_left_action.png")) - self._swipe_left.setGeometry(QRect(0, 0, swipe_left_pix.width(), swipe_left_pix.height())) + swipe_left_pix = QPixmap( + str(Path(__file__).parent / "assets/swipe_left_action.png") + ) + self._swipe_left.setGeometry( + QRect(0, 0, swipe_left_pix.width(), swipe_left_pix.height()) + ) self._swipe_left.setPixmap(swipe_left_pix) self._swipe_left.hide() self._swipe_right = QLabel(self._central_widget) self._swipe_right.setScaledContents(False) self._swipe_right.setObjectName("swipe_right") - swipe_right_pix = QPixmap(str(Path(__file__).parent / "assets/swipe_right_action.png")) - self._swipe_right.setGeometry(QRect(0, 0, swipe_right_pix.width(), - swipe_right_pix.height())) + swipe_right_pix = QPixmap( + str(Path(__file__).parent / "assets/swipe_right_action.png") + ) + self._swipe_right.setGeometry( + QRect(0, 0, swipe_right_pix.width(), swipe_right_pix.height()) + ) self._swipe_right.setPixmap(swipe_right_pix) self._swipe_right.hide() @@ -206,9 +246,9 @@ def _init_device_buttons_effect(self) -> None: self.animation = QVariantAnimation(self) self.animation.setDuration(1500) - self.animation.setStartValue(0.) + self.animation.setStartValue(0.0) self.animation.setKeyValueAt(0.5, 1.0) - self.animation.setEndValue(0.) + self.animation.setEndValue(0.0) self.animation.setLoopCount(-1) self.animation.valueChanged.connect(self._update_buttons_opacity) self.animation.start() @@ -219,16 +259,20 @@ def _update_buttons_opacity(self, opacity): def _init_validation_buttons(self) -> None: self._yes = QPushButton(self._central_widget) - self._yes.setGeometry(QRect(0, SCREENSHOT_MAX_HEIGHT, WIDTH // 2, BUTTON_HEIGHT)) + self._yes.setGeometry( + QRect(0, SCREENSHOT_MAX_HEIGHT, WIDTH // 2, BUTTON_HEIGHT) + ) self._yes.setObjectName("valid_button") self._no = QPushButton(self._central_widget) - self._no.setGeometry(QRect(WIDTH // 2, SCREENSHOT_MAX_HEIGHT, WIDTH // 2, BUTTON_HEIGHT)) + self._no.setGeometry( + QRect(WIDTH // 2, SCREENSHOT_MAX_HEIGHT, WIDTH // 2, BUTTON_HEIGHT) + ) self._no.setObjectName("invalid_button") def keyPressEvent(self, event: QKeyEvent): - if event.text().lower() == 'y': + if event.text().lower() == "y": self._yes.click() - elif event.text().lower() == 'n': + elif event.text().lower() == "n": self._no.click() def _init_gui_widgets(self) -> None: diff --git a/src/ragger/gui/process.py b/src/ragger/gui/process.py index 2197dac0..0835329f 100644 --- a/src/ragger/gui/process.py +++ b/src/ragger/gui/process.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + import sys from multiprocessing import Process, Queue from pathlib import Path @@ -27,7 +28,7 @@ NAVIGATION_ACTIONS = { NavInsID.RIGHT_CLICK: "right button", NavInsID.LEFT_CLICK: "left button", - NavInsID.BOTH_CLICK: "both buttons" + NavInsID.BOTH_CLICK: "both buttons", } @@ -85,7 +86,6 @@ def __del__(self): class RaggerGUI(Process): - def __init__(self, device: str, args=None): super().__init__(name="RaggerGUI") self.thread: QThread diff --git a/src/ragger/logger.py b/src/ragger/logger.py index 7b23dafe..17fff20f 100644 --- a/src/ragger/logger.py +++ b/src/ragger/logger.py @@ -1,25 +1,25 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ import atexit import logging from pathlib import Path -SHORT_FORMAT = '[%(levelname)s] %(name)s - %(message)s' -LONG_FORMAT = '[%(asctime)s][%(levelname)s] %(name)s - %(message)s' +SHORT_FORMAT = "[%(levelname)s] %(name)s - %(message)s" +LONG_FORMAT = "[%(asctime)s][%(levelname)s] %(name)s - %(message)s" def get_default_logger(): @@ -69,8 +69,8 @@ def set_apdu_logger_file(log_apdu_file: Path): if isinstance(handler, logging.FileHandler): apdu_logger.removeHandler(handler) - apdu_handler = logging.FileHandler(filename=log_apdu_file, mode='w', delay=True) - apdu_handler.setFormatter(logging.Formatter('%(message)s')) + apdu_handler = logging.FileHandler(filename=log_apdu_file, mode="w", delay=True) + apdu_handler.setFormatter(logging.Formatter("%(message)s")) apdu_logger.addHandler(apdu_handler) def cleanup(): diff --git a/src/ragger/navigator/__init__.py b/src/ragger/navigator/__init__.py index 0a43c6d7..e1f9ce63 100644 --- a/src/ragger/navigator/__init__.py +++ b/src/ragger/navigator/__init__.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from .instruction import BaseNavInsID, NavInsID, NavIns from .navigator import Navigator from .touch_navigator import TouchNavigator @@ -20,6 +21,11 @@ from .navigation_scenario import NavigateWithScenario __all__ = [ - "BaseNavInsID", "NavInsID", "NavIns", "Navigator", "TouchNavigator", "NanoNavigator", - "NavigateWithScenario" + "BaseNavInsID", + "NavInsID", + "NavIns", + "Navigator", + "TouchNavigator", + "NanoNavigator", + "NavigateWithScenario", ] diff --git a/src/ragger/navigator/instruction.py b/src/ragger/navigator/instruction.py index aa6ff914..de9acc52 100644 --- a/src/ragger/navigator/instruction.py +++ b/src/ragger/navigator/instruction.py @@ -7,6 +7,7 @@ class BaseNavInsID(Enum): Base NavInsID class, allowing to define NavInsID specific to one's application while being compatible with all Navigator methods. """ + pass @@ -14,6 +15,7 @@ class NavInsID(BaseNavInsID): """ Pre-defined instruction ID to navigate into a device UI. """ + WAIT = auto() # Navigation instructions that embedded a call to @@ -82,7 +84,6 @@ class NavInsID(BaseNavInsID): class NavIns: - def __init__(self, id: BaseNavInsID, args=(), kwargs: Dict[str, Any] = {}): self.id = id self.args = args diff --git a/src/ragger/navigator/nano_navigator.py b/src/ragger/navigator/nano_navigator.py index 605282d1..62eb6ad8 100644 --- a/src/ragger/navigator/nano_navigator.py +++ b/src/ragger/navigator/nano_navigator.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from time import sleep from typing import Callable, Dict from ledgered.devices import Device @@ -22,10 +23,13 @@ class NanoNavigator(Navigator): - - def __init__(self, backend: BackendInterface, device: Device, golden_run: bool = False): + def __init__( + self, backend: BackendInterface, device: Device, golden_run: bool = False + ): if device.touchable: - raise ValueError(f"'{self.__class__.__name__}' does not work on touchable devices") + raise ValueError( + f"'{self.__class__.__name__}' does not work on touchable devices" + ) callbacks: Dict[BaseNavInsID, Callable] = { NavInsID.WAIT: sleep, NavInsID.WAIT_FOR_SCREEN_CHANGE: backend.wait_for_screen_change, @@ -34,6 +38,6 @@ def __init__(self, backend: BackendInterface, device: Device, golden_run: bool = NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN: backend.wait_for_text_not_on_screen, NavInsID.RIGHT_CLICK: backend.right_click, NavInsID.LEFT_CLICK: backend.left_click, - NavInsID.BOTH_CLICK: backend.both_click + NavInsID.BOTH_CLICK: backend.both_click, } super().__init__(backend, device, callbacks, golden_run) diff --git a/src/ragger/navigator/navigation_scenario.py b/src/ragger/navigator/navigation_scenario.py index d060d7a2..823ac367 100644 --- a/src/ragger/navigator/navigation_scenario.py +++ b/src/ragger/navigator/navigation_scenario.py @@ -20,13 +20,15 @@ class NavigationScenarioData: pattern: str = "" post_validation_spinner: Optional[str] = None - def __init__(self, - device: Device, - backend: BackendInterface, - use_case: UseCase, - approve: bool, - nb_warnings: int = 1, - post_validation_spinner: Optional[str] = None): + def __init__( + self, + device: Device, + backend: BackendInterface, + use_case: UseCase, + approve: bool, + nb_warnings: int = 1, + post_validation_spinner: Optional[str] = None, + ): if device.is_nano: self.navigation = NavInsID.RIGHT_CLICK @@ -35,21 +37,31 @@ def __init__(self, if backend.sdk_graphics == GraphicalLibrary.BAGL: self.dismiss_warning = [NavInsID.RIGHT_CLICK] * nb_warnings # Legacy navigation scenario when running an App compiled with bagl sdk library - self.pattern = r"^(Accept risk|Accept|Approve|Sign|Confirm)$" if approve else r"^(Cancel|Reject)$" + self.pattern = ( + r"^(Accept risk|Accept|Approve|Sign|Confirm)$" + if approve + else r"^(Cancel|Reject)$" + ) else: self.dismiss_warning = [] self.dismiss_warning += [NavInsID.RIGHT_CLICK] * (nb_warnings - 1) self.dismiss_warning += [NavInsID.BOTH_CLICK] # navigation scenario when running an App compiled with nbgl sdk library if use_case == UseCase.ADDRESS_CONFIRMATION: - self.pattern = r"^(Accept risk|Accept|Approve|Sign|Confirm)$" if approve else r"^(Cancel|Reject)$" + self.pattern = ( + r"^(Accept risk|Accept|Approve|Sign|Confirm)$" + if approve + else r"^(Cancel|Reject)$" + ) elif use_case == UseCase.TX_REVIEW: if approve: # Matches: # - "Accept risk and " # - "Accept risk and sign [transaction/message/operation]" # - "Sign [transaction/message/operation]" - blind_sign_pattern = r"(Accept risk and (sign (transaction|message|operation))?)" + blind_sign_pattern = ( + r"(Accept risk and (sign (transaction|message|operation))?)" + ) clear_sign_pattern = r"(Sign (transaction|message|operation))" self.pattern = rf"^({blind_sign_pattern}|{clear_sign_pattern})$" else: @@ -78,7 +90,8 @@ def __init__(self, self.validation = [NavInsID.USE_CASE_REVIEW_CONFIRM] else: self.validation = [ - NavInsID.USE_CASE_REVIEW_REJECT, NavInsID.USE_CASE_CHOICE_CONFIRM + NavInsID.USE_CASE_REVIEW_REJECT, + NavInsID.USE_CASE_CHOICE_CONFIRM, ] self.pattern = "^Hold to sign$" @@ -97,21 +110,28 @@ def __init__(self, class NavigateWithScenario: - - def __init__(self, backend: BackendInterface, navigator: Navigator, device: Device, - test_name: str, screenshot_path: Path): + def __init__( + self, + backend: BackendInterface, + navigator: Navigator, + device: Device, + test_name: str, + screenshot_path: Path, + ): self.navigator = navigator self.device = device self.backend = backend self.test_name = test_name self.screenshot_path = screenshot_path - def _navigate_with_scenario(self, - scenario: NavigationScenarioData, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True): + def _navigate_with_scenario( + self, + scenario: NavigationScenarioData, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + ): if custom_screen_text is not None: scenario.pattern = custom_screen_text @@ -125,13 +145,15 @@ def _navigate_with_scenario(self, text=scenario.pattern, path=path if path else self.screenshot_path, test_case_name=test_name if test_name else self.test_name, - screen_change_after_last_instruction=screen_change_after_last_instruction) + screen_change_after_last_instruction=screen_change_after_last_instruction, + ) else: self.navigator.navigate_until_text( navigate_instruction=scenario.navigation, validation_instructions=scenario.validation, text=scenario.pattern, - screen_change_after_last_instruction=screen_change_after_last_instruction) + screen_change_after_last_instruction=screen_change_after_last_instruction, + ) if scenario.post_validation_spinner is not None: # If the navigation ended with a spinner, we did not assert the post validation screen in @@ -139,94 +161,130 @@ def _navigate_with_scenario(self, # We perform a manual text_on_screen check instead self.backend.wait_for_text_on_screen(scenario.post_validation_spinner) - def _navigate_warning(self, - scenario: NavigationScenarioData, - test_name: Optional[str] = None, - do_comparison: bool = True, - warning_path: str = "warning"): + def _navigate_warning( + self, + scenario: NavigationScenarioData, + test_name: Optional[str] = None, + do_comparison: bool = True, + warning_path: str = "warning", + ): if do_comparison: self.navigator.navigate_and_compare( self.screenshot_path, f"{test_name if test_name else self.test_name}/{warning_path}", scenario.dismiss_warning, - screen_change_after_last_instruction=False) + screen_change_after_last_instruction=False, + ) else: - self.navigator.navigate(scenario.dismiss_warning, - screen_change_after_last_instruction=False) - - def review_approve(self, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True): - scenario = NavigationScenarioData(self.device, self.backend, UseCase.TX_REVIEW, True) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) - - def review_approve_with_warning(self, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True, - warning_path: str = "warning", - nb_warnings: int = 1): - scenario = NavigationScenarioData(self.device, - self.backend, - UseCase.TX_REVIEW, - True, - nb_warnings=nb_warnings) + self.navigator.navigate( + scenario.dismiss_warning, screen_change_after_last_instruction=False + ) + + def review_approve( + self, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + ): + scenario = NavigationScenarioData( + self.device, self.backend, UseCase.TX_REVIEW, True + ) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) + + def review_approve_with_warning( + self, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + warning_path: str = "warning", + nb_warnings: int = 1, + ): + scenario = NavigationScenarioData( + self.device, self.backend, UseCase.TX_REVIEW, True, nb_warnings=nb_warnings + ) self._navigate_warning(scenario, test_name, do_comparison, warning_path) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) - - def review_approve_with_spinner(self, - spinner_text: str, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True): - scenario = NavigationScenarioData(self.device, - self.backend, - UseCase.TX_REVIEW, - True, - post_validation_spinner=spinner_text) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) - - def review_reject(self, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True): - scenario = NavigationScenarioData(self.device, self.backend, UseCase.TX_REVIEW, False) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) - - def review_reject_with_warning(self, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True, - warning_path: str = "warning", - nb_warnings: int = 1): - scenario = NavigationScenarioData(self.device, - self.backend, - UseCase.TX_REVIEW, - False, - nb_warnings=nb_warnings) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) + + def review_approve_with_spinner( + self, + spinner_text: str, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + ): + scenario = NavigationScenarioData( + self.device, + self.backend, + UseCase.TX_REVIEW, + True, + post_validation_spinner=spinner_text, + ) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) + + def review_reject( + self, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + ): + scenario = NavigationScenarioData( + self.device, self.backend, UseCase.TX_REVIEW, False + ) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) + + def review_reject_with_warning( + self, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + warning_path: str = "warning", + nb_warnings: int = 1, + ): + scenario = NavigationScenarioData( + self.device, self.backend, UseCase.TX_REVIEW, False, nb_warnings=nb_warnings + ) self._navigate_warning(scenario, test_name, do_comparison, warning_path) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) - - def address_review_approve(self, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True): - scenario = NavigationScenarioData(self.device, self.backend, UseCase.ADDRESS_CONFIRMATION, - True) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) - - def address_review_reject(self, - path: Optional[Path] = None, - test_name: Optional[str] = None, - custom_screen_text: Optional[str] = None, - do_comparison: bool = True): - scenario = NavigationScenarioData(self.device, self.backend, UseCase.ADDRESS_CONFIRMATION, - False) - self._navigate_with_scenario(scenario, path, test_name, custom_screen_text, do_comparison) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) + + def address_review_approve( + self, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + ): + scenario = NavigationScenarioData( + self.device, self.backend, UseCase.ADDRESS_CONFIRMATION, True + ) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) + + def address_review_reject( + self, + path: Optional[Path] = None, + test_name: Optional[str] = None, + custom_screen_text: Optional[str] = None, + do_comparison: bool = True, + ): + scenario = NavigationScenarioData( + self.device, self.backend, UseCase.ADDRESS_CONFIRMATION, False + ) + self._navigate_with_scenario( + scenario, path, test_name, custom_screen_text, do_comparison + ) diff --git a/src/ragger/navigator/navigator.py b/src/ragger/navigator/navigator.py index f841e175..c3f16378 100644 --- a/src/ragger/navigator/navigator.py +++ b/src/ragger/navigator/navigator.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from abc import ABC from pathlib import Path from tempfile import NamedTemporaryFile @@ -30,16 +31,17 @@ class Navigator(ABC): - GOLDEN_INSTRUCTION_SLEEP_MULTIPLIER_FIRST = 2 GOLDEN_INSTRUCTION_SLEEP_MULTIPLIER_MIDDLE = 5 GOLDEN_INSTRUCTION_SLEEP_MULTIPLIER_LAST = 2 - def __init__(self, - backend: BackendInterface, - device: Device, - callbacks: Dict[BaseNavInsID, Callable], - golden_run: bool = False): + def __init__( + self, + backend: BackendInterface, + device: Device, + callbacks: Dict[BaseNavInsID, Callable], + golden_run: bool = False, + ): """Initializes the Backend :param backend: Which Backend will be managed @@ -56,28 +58,31 @@ def __init__(self, self._callbacks = callbacks self._golden_run = golden_run - def _get_snaps_dir_path(self, path: Path, test_case_name: Union[Path, str], - is_golden: bool) -> Path: + def _get_snaps_dir_path( + self, path: Path, test_case_name: Union[Path, str], is_golden: bool + ) -> Path: if is_golden: subdir = "snapshots" else: subdir = "snapshots-tmp" return path / subdir / self._device.name / test_case_name - def _check_snaps_dir_path(self, path: Path, test_case_name: Union[Path, str], - is_golden: bool) -> Path: + def _check_snaps_dir_path( + self, path: Path, test_case_name: Union[Path, str], is_golden: bool + ) -> Path: dir_path = self._get_snaps_dir_path(path, test_case_name, is_golden) if not dir_path.is_dir(): if self._golden_run: dir_path.mkdir(parents=True) else: - raise ValueError(f"Golden snapshots directory ({dir_path}) does not exist.") + raise ValueError( + f"Golden snapshots directory ({dir_path}) does not exist." + ) return dir_path - def _init_snaps_temp_dir(self, - path: Path, - test_case_name: Union[Path, str], - start_idx: int = 0) -> Path: + def _init_snaps_temp_dir( + self, path: Path, test_case_name: Union[Path, str], start_idx: int = 0 + ) -> Path: snaps_tmp_path = self._get_snaps_dir_path(path, test_case_name, False) if snaps_tmp_path.exists(): for file in snaps_tmp_path.iterdir(): @@ -97,15 +102,19 @@ def _init_snaps_temp_dir(self, def _get_snap_path(self, path: Path, index: int) -> Path: return path / f"{str(index).zfill(5)}.png" - def _compare_snap_with_timeout(self, - path: Path, - timeout_s: float = 5.0, - crop: Optional[Crop] = None, - tmp_snap_path: Optional[Path] = None) -> bool: + def _compare_snap_with_timeout( + self, + path: Path, + timeout_s: float = 5.0, + crop: Optional[Crop] = None, + tmp_snap_path: Optional[Path] = None, + ) -> bool: start = time() now = start while not (now - start > timeout_s): - if self._backend.compare_screen_with_snapshot(path, crop, tmp_snap_path=tmp_snap_path): + if self._backend.compare_screen_with_snapshot( + path, crop, tmp_snap_path=tmp_snap_path + ): return True now = time() return False @@ -115,10 +124,12 @@ def _compare_snap(self, snaps_tmp_path: Path, snaps_golden_path: Path, index: in tmp = self._get_snap_path(snaps_tmp_path, index) assert self._backend.compare_screen_with_snapshot( - golden, tmp_snap_path=tmp, - golden_run=self._golden_run), f"Screen does not match golden '{tmp}'" + golden, tmp_snap_path=tmp, golden_run=self._golden_run + ), f"Screen does not match golden '{tmp}'" - def add_callback(self, ins_id: BaseNavInsID, callback: Callable, override: bool = True) -> None: + def add_callback( + self, ins_id: BaseNavInsID, callback: Callable, override: bool = True + ) -> None: """ Register a new callback. @@ -137,21 +148,27 @@ def add_callback(self, ins_id: BaseNavInsID, callback: Callable, override: bool :rtype: NoneType """ if not override and ins_id in self._callbacks: - raise KeyError(f"Navigation instruction ID '{ins_id}' already exists in the " - "registered callbacks") + raise KeyError( + f"Navigation instruction ID '{ins_id}' already exists in the " + "registered callbacks" + ) self._callbacks[ins_id] = callback - def _run_instruction(self, - instruction: InstructionType, - timeout: float = 10.0, - wait_for_screen_change: bool = True, - path: Optional[Path] = None, - test_case_name: Optional[Union[Path, str]] = None, - snap_idx: int = 0) -> None: + def _run_instruction( + self, + instruction: InstructionType, + timeout: float = 10.0, + wait_for_screen_change: bool = True, + path: Optional[Path] = None, + test_case_name: Optional[Union[Path, str]] = None, + snap_idx: int = 0, + ) -> None: if isinstance(instruction, BaseNavInsID): instruction = NavIns(instruction) if instruction.id not in self._callbacks: - raise NotImplementedError(f"No callback registered for instruction ID {instruction.id}") + raise NotImplementedError( + f"No callback registered for instruction ID {instruction.id}" + ) if instruction.id == NavInsID.USE_CASE_REVIEW_CONFIRM: # Specific handling due to the fact that the screen is updated multiple @@ -163,12 +180,12 @@ def _run_instruction(self, # That's why we are first backuping the screen content in a temp file. # This screen content is then used to check if the screen changed enough, # e.g. with cropping the progress bar from the screen. - with NamedTemporaryFile(suffix='.png') as tmp: + with NamedTemporaryFile(suffix=".png") as tmp: tmp_file = Path(tmp.name) # Backup screen content before instruction in tmp file - self._backend.compare_screen_with_snapshot(tmp_file, - tmp_snap_path=tmp_file, - golden_run=True) + self._backend.compare_screen_with_snapshot( + tmp_file, tmp_snap_path=tmp_file, golden_run=True + ) # Call instruction callback self._callbacks[instruction.id](*instruction.args, **instruction.kwargs) @@ -181,7 +198,9 @@ def _run_instruction(self, endtime = time() + timeout while True: self._backend.wait_for_screen_change(endtime - time()) - if not self._backend.compare_screen_with_snapshot(tmp_file, cropping): + if not self._backend.compare_screen_with_snapshot( + tmp_file, cropping + ): break else: @@ -191,8 +210,10 @@ def _run_instruction(self, # Wait for screen change unless explicitly specify otherwise if wait_for_screen_change: if instruction.id in [ - NavInsID.WAIT_FOR_SCREEN_CHANGE, NavInsID.WAIT_FOR_HOME_SCREEN, - NavInsID.WAIT_FOR_TEXT_ON_SCREEN, NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN + NavInsID.WAIT_FOR_SCREEN_CHANGE, + NavInsID.WAIT_FOR_HOME_SCREEN, + NavInsID.WAIT_FOR_TEXT_ON_SCREEN, + NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN, ]: # Function wait_for_screen_change() is already called during # instruction callback execution above. @@ -204,21 +225,25 @@ def _run_instruction(self, if path and test_case_name: if snap_idx == 0: snaps_tmp_path = self._init_snaps_temp_dir(path, test_case_name) - snaps_golden_path = self._check_snaps_dir_path(path, test_case_name, True) + snaps_golden_path = self._check_snaps_dir_path( + path, test_case_name, True + ) else: snaps_tmp_path = self._get_snaps_dir_path(path, test_case_name, False) snaps_golden_path = self._get_snaps_dir_path(path, test_case_name, True) self._compare_snap(snaps_tmp_path, snaps_golden_path, snap_idx) - def navigate_and_compare(self, - path: Optional[Path], - test_case_name: Optional[Union[Path, str]], - instructions: Sequence[InstructionType], - timeout: float = 10.0, - screen_change_before_first_instruction: bool = True, - screen_change_after_last_instruction: bool = True, - snap_start_idx: int = 0) -> None: + def navigate_and_compare( + self, + path: Optional[Path], + test_case_name: Optional[Union[Path, str]], + instructions: Sequence[InstructionType], + timeout: float = 10.0, + screen_change_before_first_instruction: bool = True, + screen_change_after_last_instruction: bool = True, + snap_start_idx: int = 0, + ) -> None: """ Navigate on the device according to a set of navigation instructions provided then compare each step snapshot with "golden images". @@ -257,12 +282,14 @@ def navigate_and_compare(self, # - when called to finish the execution of a navigate_until_text() call. # - compare the initial screen content with the golden reference if path and # test_case_name are valid. - self._run_instruction(NavIns(NavInsID.WAIT, (0, )), - timeout, - wait_for_screen_change=screen_change_before_first_instruction, - path=path, - test_case_name=test_case_name, - snap_idx=snap_start_idx) + self._run_instruction( + NavIns(NavInsID.WAIT, (0,)), + timeout, + wait_for_screen_change=screen_change_before_first_instruction, + path=path, + test_case_name=test_case_name, + snap_idx=snap_start_idx, + ) for idx, instruction in enumerate(instructions, start=1): if idx != len(instructions) or screen_change_after_last_instruction: @@ -271,28 +298,34 @@ def navigate_and_compare(self, # - last instruction but with screen_change_after_last_instruction=True # => wait_for_screen_change() # => screenshot comparison if path and test_case_name are valid - self._run_instruction(instruction, - timeout, - wait_for_screen_change=True, - path=path, - test_case_name=test_case_name, - snap_idx=snap_start_idx + idx) + self._run_instruction( + instruction, + timeout, + wait_for_screen_change=True, + path=path, + test_case_name=test_case_name, + snap_idx=snap_start_idx + idx, + ) else: # Last instruction case with screen_change_after_last_instruction=False # => no wait_for_screen_change() # => no screenshot comparison - self._run_instruction(instruction, - timeout, - wait_for_screen_change=False, - snap_idx=snap_start_idx + idx) + self._run_instruction( + instruction, + timeout, + wait_for_screen_change=False, + snap_idx=snap_start_idx + idx, + ) self._backend.resume_ticker() - def navigate(self, - instructions: Sequence[InstructionType], - timeout: float = 10.0, - screen_change_before_first_instruction: bool = True, - screen_change_after_last_instruction: bool = True) -> None: + def navigate( + self, + instructions: Sequence[InstructionType], + timeout: float = 10.0, + screen_change_before_first_instruction: bool = True, + screen_change_after_last_instruction: bool = True, + ) -> None: """ Navigate on the device according to a set of navigation instructions provided. @@ -322,19 +355,22 @@ def navigate(self, instructions=instructions, timeout=timeout, screen_change_before_first_instruction=screen_change_before_first_instruction, - screen_change_after_last_instruction=screen_change_after_last_instruction) - - def navigate_until_snap(self, - navigate_instruction: InstructionType, - validation_instruction: InstructionType, - path: Path, - test_case_name: Union[Path, str], - start_img_name: str, - last_img_name: str, - take_snaps: bool = True, - timeout: int = 30, - crop_first: Optional[Crop] = None, - crop_last: Optional[Crop] = None) -> int: + screen_change_after_last_instruction=screen_change_after_last_instruction, + ) + + def navigate_until_snap( + self, + navigate_instruction: InstructionType, + validation_instruction: InstructionType, + path: Path, + test_case_name: Union[Path, str], + start_img_name: str, + last_img_name: str, + take_snaps: bool = True, + timeout: int = 30, + crop_first: Optional[Crop] = None, + crop_last: Optional[Crop] = None, + ) -> int: """ Navigate until snapshot is found. @@ -395,10 +431,9 @@ def navigate_until_snap(self, # Check if the first snapshot is found before going in the navigation loop. # It saves time in non-nominal cases where the navigation flow does not start. - if self._compare_snap_with_timeout(first_golden_snap, - timeout_s=2, - crop=crop_first, - tmp_snap_path=tmp_snap_path): + if self._compare_snap_with_timeout( + first_golden_snap, timeout_s=2, crop=crop_first, tmp_snap_path=tmp_snap_path + ): start = time() # Navigate until the last snapshot specified in argument is found. while True: @@ -406,19 +441,23 @@ def navigate_until_snap(self, # Take snapshots if required. tmp_snap_path = self._get_snap_path(snaps_tmp_path, img_idx) - if self._compare_snap_with_timeout(last_golden_snap, - timeout_s=0.5, - crop=crop_last, - tmp_snap_path=tmp_snap_path): + if self._compare_snap_with_timeout( + last_golden_snap, + timeout_s=0.5, + crop=crop_last, + tmp_snap_path=tmp_snap_path, + ): break now = time() # Global navigation loop timeout in case the snapshot is never found. - if (now - start > timeout): + if now - start > timeout: raise TimeoutError(f"Timeout waiting for snap {last_golden_snap}") # Go to the next screen. - self._run_instruction(navigate_instruction, wait_for_screen_change=False) + self._run_instruction( + navigate_instruction, wait_for_screen_change=False + ) img_idx += 1 # Validation action when last snapshot is found. @@ -426,9 +465,11 @@ def navigate_until_snap(self, # Make sure there is a screen update after the final action. start = time() - while self._compare_snap_with_timeout(last_golden_snap, timeout_s=0.5, crop=crop_last): + while self._compare_snap_with_timeout( + last_golden_snap, timeout_s=0.5, crop=crop_last + ): now = time() - if (now - start > LAST_SCREEN_UPDATE_TIMEOUT): + if now - start > LAST_SCREEN_UPDATE_TIMEOUT: raise TimeoutError( f"Timeout waiting for screen change after last snapshot : {last_golden_snap}" ) @@ -436,16 +477,18 @@ def navigate_until_snap(self, raise ValueError(f"Could not find first snapshot {first_golden_snap}") return img_idx - def navigate_until_text_and_compare(self, - navigate_instruction: InstructionType, - validation_instructions: Sequence[InstructionType], - text: str, - path: Optional[Path] = None, - test_case_name: Optional[Union[Path, str]] = None, - timeout: int = 300, - screen_change_before_first_instruction: bool = True, - screen_change_after_last_instruction: bool = True, - snap_start_idx: int = 0) -> None: + def navigate_until_text_and_compare( + self, + navigate_instruction: InstructionType, + validation_instructions: Sequence[InstructionType], + text: str, + path: Optional[Path] = None, + test_case_name: Optional[Union[Path, str]] = None, + timeout: int = 300, + screen_change_before_first_instruction: bool = True, + screen_change_after_last_instruction: bool = True, + snap_start_idx: int = 0, + ) -> None: """ Navigate until some text is found on the screen content displayed then compare each step snapshot with "golden images". @@ -499,12 +542,14 @@ def navigate_until_text_and_compare(self, # screen already displays the first review page. # - compare the initial screen content with the golden reference if path and # test_case_name are valid. - self._run_instruction(NavIns(NavInsID.WAIT, (0, )), - timeout, - wait_for_screen_change=screen_change_before_first_instruction, - path=path, - test_case_name=test_case_name, - snap_idx=idx) + self._run_instruction( + NavIns(NavInsID.WAIT, (0,)), + timeout, + wait_for_screen_change=screen_change_before_first_instruction, + path=path, + test_case_name=test_case_name, + snap_idx=idx, + ) # Navigate until the text specified in argument is found. while True: @@ -514,17 +559,19 @@ def navigate_until_text_and_compare(self, else: # Global navigation loop timeout in case the text is never found. remaining = timeout - (time() - start) - if (remaining < 0): + if remaining < 0: raise TimeoutError(f"Timeout waiting for text {text}") # Go to the next screen. idx += 1 - self._run_instruction(navigate_instruction, - remaining, - wait_for_screen_change=True, - path=path, - test_case_name=test_case_name, - snap_idx=idx) + self._run_instruction( + navigate_instruction, + remaining, + wait_for_screen_change=True, + path=path, + test_case_name=test_case_name, + snap_idx=idx, + ) # Perform navigation validation instructions in an "navigate_and_compare" way. if validation_instructions: @@ -536,18 +583,21 @@ def navigate_until_text_and_compare(self, timeout=remaining, screen_change_before_first_instruction=False, screen_change_after_last_instruction=screen_change_after_last_instruction, - snap_start_idx=idx) + snap_start_idx=idx, + ) self._backend.resume_ticker() - def navigate_until_text(self, - navigate_instruction: InstructionType, - validation_instructions: Sequence[InstructionType], - text: str, - timeout: int = 300, - screen_change_before_first_instruction: bool = True, - screen_change_after_last_instruction: bool = True, - snap_start_idx: int = 0) -> None: + def navigate_until_text( + self, + navigate_instruction: InstructionType, + validation_instructions: Sequence[InstructionType], + text: str, + timeout: int = 300, + screen_change_before_first_instruction: bool = True, + screen_change_after_last_instruction: bool = True, + snap_start_idx: int = 0, + ) -> None: """ Navigate until some text is found on the screen content displayed. @@ -580,12 +630,14 @@ def navigate_until_text(self, :return: None :rtype: NoneType """ - self.navigate_until_text_and_compare(navigate_instruction, - validation_instructions, - text, - None, - None, - timeout, - screen_change_before_first_instruction, - screen_change_after_last_instruction, - snap_start_idx=snap_start_idx) + self.navigate_until_text_and_compare( + navigate_instruction, + validation_instructions, + text, + None, + None, + timeout, + screen_change_before_first_instruction, + screen_change_after_last_instruction, + snap_start_idx=snap_start_idx, + ) diff --git a/src/ragger/navigator/touch_navigator.py b/src/ragger/navigator/touch_navigator.py index c0f4a20c..d4668623 100644 --- a/src/ragger/navigator/touch_navigator.py +++ b/src/ragger/navigator/touch_navigator.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from time import sleep from typing import Callable, Dict from ledgered.devices import Device @@ -24,10 +25,13 @@ class TouchNavigator(Navigator): - - def __init__(self, backend: BackendInterface, device: Device, golden_run: bool = False): + def __init__( + self, backend: BackendInterface, device: Device, golden_run: bool = False + ): if not device.touchable: - raise ValueError(f"'{self.__class__.__name__}' only works with touchable devices") + raise ValueError( + f"'{self.__class__.__name__}' only works with touchable devices" + ) screen = FullScreen(backend, device) callbacks: Dict[BaseNavInsID, Callable] = { NavInsID.WAIT: sleep, diff --git a/src/ragger/utils/__init__.py b/src/ragger/utils/__init__.py index d868ee30..0788e7d7 100644 --- a/src/ragger/utils/__init__.py +++ b/src/ragger/utils/__init__.py @@ -1,24 +1,32 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from .structs import RAPDU, Crop from .packing import pack_APDU from .misc import find_library_application, prefix_with_len, find_project_root_dir from .misc import create_currency_config, split_message, find_application __all__ = [ - "find_library_application", "create_currency_config", "Crop", "pack_APDU", "prefix_with_len", - "RAPDU", "split_message", "find_project_root_dir", "find_application" + "find_library_application", + "create_currency_config", + "Crop", + "pack_APDU", + "prefix_with_len", + "RAPDU", + "split_message", + "find_project_root_dir", + "find_application", ] diff --git a/src/ragger/utils/misc.py b/src/ragger/utils/misc.py index f793baa9..d8986096 100644 --- a/src/ragger/utils/misc.py +++ b/src/ragger/utils/misc.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + import toml from typing import Optional, Tuple, List from pathlib import Path @@ -101,7 +102,10 @@ def _is_root(path_to_check: Path) -> bool: def find_project_root_dir(origin: Path) -> Path: project_root_dir = origin - while not _is_root(project_root_dir) and not (project_root_dir / ".git").resolve().is_dir(): + while ( + not _is_root(project_root_dir) + and not (project_root_dir / ".git").resolve().is_dir() + ): project_root_dir = project_root_dir.parent if _is_root(project_root_dir): raise ValueError("Could not find project top directory") @@ -112,9 +116,11 @@ def prefix_with_len(to_prefix: bytes) -> bytes: return len(to_prefix).to_bytes(1, byteorder="big") + to_prefix -def create_currency_config(main_ticker: str, - application_name: str, - sub_coin_config: Optional[Tuple[str, int]] = None) -> bytes: +def create_currency_config( + main_ticker: str, + application_name: str, + sub_coin_config: Optional[Tuple[str, int]] = None, +) -> bytes: sub_config: bytes = b"" if sub_coin_config is not None: sub_config = prefix_with_len(sub_coin_config[0].encode()) @@ -126,7 +132,7 @@ def create_currency_config(main_ticker: str, def split_message(message: bytes, max_size: int) -> List[bytes]: - return [message[x:x + max_size] for x in range(0, len(message), max_size)] + return [message[x : x + max_size] for x in range(0, len(message), max_size)] def get_current_app_name_and_version(backend): @@ -135,7 +141,8 @@ def get_current_app_name_and_version(backend): cla=0xB0, # specific CLA for BOLOS ins=0x01, # specific INS for get_app_and_version p1=0, - p2=0).data + p2=0, + ).data offset = 0 format_id = response[offset] @@ -144,18 +151,18 @@ def get_current_app_name_and_version(backend): app_name_len = response[offset] offset += 1 - app_name = response[offset:offset + app_name_len].decode("ascii") + app_name = response[offset : offset + app_name_len].decode("ascii") offset += app_name_len version_len = response[offset] offset += 1 - version = response[offset:offset + version_len].decode("ascii") + version = response[offset : offset + version_len].decode("ascii") offset += version_len if app_name != "BOLOS": flags_len = response[offset] offset += 1 - _ = response[offset:offset + flags_len] + _ = response[offset : offset + flags_len] offset += flags_len assert offset == len(response) @@ -172,7 +179,8 @@ def exit_current_app(backend): cla=0xB0, # specific CLA for BOLOS ins=0xA7, # specific INS for INS_APP_EXIT p1=0, - p2=0) + p2=0, + ) def open_app_from_dashboard(backend, app_name: str): @@ -182,7 +190,8 @@ def open_app_from_dashboard(backend, app_name: str): ins=0xD8, # specific INS for INS_OPEN_APP p1=0, p2=0, - data=app_name.encode()) + data=app_name.encode(), + ) except ExceptionRAPDU as e: if e.status == ERROR_DENIED_BY_USER: raise ValueError("Open app consent denied by the user") diff --git a/src/ragger/utils/packing.py b/src/ragger/utils/packing.py index 7fc26fe9..45373728 100644 --- a/src/ragger/utils/packing.py +++ b/src/ragger/utils/packing.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from struct import pack diff --git a/src/ragger/utils/structs.py b/src/ragger/utils/structs.py index 1f71eb04..1d859ae1 100644 --- a/src/ragger/utils/structs.py +++ b/src/ragger/utils/structs.py @@ -1,18 +1,19 @@ """ - Copyright 2022 Ledger SAS +Copyright 2022 Ledger SAS - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at - http://www.apache.org/licenses/LICENSE-2.0 + http://www.apache.org/licenses/LICENSE-2.0 - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. """ + from dataclasses import dataclass @@ -37,12 +38,13 @@ class RAPDU: - ``data`` (``bytes``): the rest of the response (the entire payload without the two last bytes) """ + status: int data: bytes def __str__(self): - return f'[0x{self.status:02x}] {self.data.hex() if self.data else ""}' + return f"[0x{self.status:02x}] {self.data.hex() if self.data else ''}" @property def raw(self): - return self.data + self.status.to_bytes(2, 'big') + return self.data + self.status.to_bytes(2, "big") diff --git a/template/conftest.py b/template/conftest.py index 909ec8bf..94bd49f4 100644 --- a/template/conftest.py +++ b/template/conftest.py @@ -1,4 +1,4 @@ -from ragger.conftest import configuration +# from ragger.conftest import configuration ########################### ### CONFIGURATION START ### @@ -12,4 +12,4 @@ ######################### # Pull all features from the base ragger conftest using the overridden configuration -pytest_plugins = ("ragger.conftest.base_conftest", ) +pytest_plugins = ("ragger.conftest.base_conftest",) diff --git a/tests/functional/backend/test_speculos.py b/tests/functional/backend/test_speculos.py index 34b9f144..2ae1badd 100644 --- a/tests/functional/backend/test_speculos.py +++ b/tests/functional/backend/test_speculos.py @@ -34,7 +34,9 @@ def test_something(self): - else means the response has a APDUStatus.ERROR status (arbitrarily set to 0x8000) """ - def check_rapdu(self, rapdu: RAPDU, expected: Optional[bytes] = None, status: int = 0x9000): + def check_rapdu( + self, rapdu: RAPDU, expected: Optional[bytes] = None, status: int = 0x9000 + ): self.assertEqual(rapdu.status, status) if expected is None: return @@ -57,9 +59,11 @@ def test_exchange_raw_error(self): with self.backend: self.backend.raise_policy = RaisePolicy.RAISE_NOTHING rapdu = self.backend.exchange_raw(bytes.fromhex("01000000")) - self.check_rapdu(rapdu, - expected=bytes.fromhex(EndPoint.APDU), - status=APDUStatus.ERROR) + self.check_rapdu( + rapdu, + expected=bytes.fromhex(EndPoint.APDU), + status=APDUStatus.ERROR, + ) def test_exchange_raw_raises(self): with patch("speculos.client.subprocess"): @@ -117,9 +121,11 @@ def test_exchange_async_raw_error(self): self.assertIsNone(self.backend.last_async_response) rapdu = self.backend.last_async_response self.assertIsNotNone(rapdu) - self.check_rapdu(rapdu, - expected=bytes.fromhex(EndPoint.APDU), - status=APDUStatus.ERROR) + self.check_rapdu( + rapdu, + expected=bytes.fromhex(EndPoint.APDU), + status=APDUStatus.ERROR, + ) def test_exchange_async_raw_raises(self): with patch("speculos.client.subprocess"): @@ -164,7 +170,9 @@ def test_async_error_raised_during_navigation(self): # (wait_for_screen_change calls _check_async_error) self.backend.wait_for_screen_change(timeout=0.5) # This line should be unreachable - if reached, the error wasn't raised during navigation - assert False, "Expected ExceptionRAPDU was not raised during navigation" # pragma: no cover + assert False, ( + "Expected ExceptionRAPDU was not raised during navigation" + ) # pragma: no cover self.assertEqual(error.exception.status, APDUStatus.ERROR) # Perform a second async exchange with a SUCCESS APDU to ensure state is properly reset @@ -173,4 +181,6 @@ def test_async_error_raised_during_navigation(self): with self.backend.exchange_async_raw(bytes.fromhex("00000000")): self.backend.wait_for_screen_change(timeout=1) # Verify that the response was successfully retrieved despite the timeout - self.assertEqual(self.backend.last_async_response.status, APDUStatus.SUCCESS) + self.assertEqual( + self.backend.last_async_response.status, APDUStatus.SUCCESS + ) diff --git a/tests/functional/conftest.py b/tests/functional/conftest.py index ee6f770a..e424386a 100644 --- a/tests/functional/conftest.py +++ b/tests/functional/conftest.py @@ -14,4 +14,4 @@ ######################### # Pull all features from the base ragger conftest using the overridden configuration -pytest_plugins = ("ragger.conftest.base_conftest", ) +pytest_plugins = ("ragger.conftest.base_conftest",) diff --git a/tests/functional/navigator/test_navigator.py b/tests/functional/navigator/test_navigator.py index 85ef79bd..5004564f 100644 --- a/tests/functional/navigator/test_navigator.py +++ b/tests/functional/navigator/test_navigator.py @@ -49,14 +49,15 @@ def test_navigate_and_compare(self): NavIns(NavInsID.BOTH_CLICK), NavIns(NavInsID.RIGHT_CLICK), NavIns(NavInsID.BOTH_CLICK), - NavIns(NavInsID.WAIT, (2, )) + NavIns(NavInsID.WAIT, (2,)), ] self.navigator.navigate_and_compare( ROOT_SCREENSHOT_PATH, "test_navigate_and_compare", instructions, screen_change_before_first_instruction=False, - screen_change_after_last_instruction=False) + screen_change_after_last_instruction=False, + ) def test_navigate_and_compare_no_golden(self): with patch("speculos.client.subprocess"): @@ -68,10 +69,13 @@ def test_navigate_and_compare_no_golden(self): ROOT_SCREENSHOT_PATH, "test_navigate_and_compare_no_golden", instructions, - screen_change_before_first_instruction=False) + screen_change_before_first_instruction=False, + ) self.assertIn("No such file or directory", str(error.exception)) - self.assertIn("test_navigate_and_compare_no_golden/00001.png", - str(error.exception)) + self.assertIn( + "test_navigate_and_compare_no_golden/00001.png", + str(error.exception), + ) def test_navigate_and_compare_wrong_golden(self): with patch("speculos.client.subprocess"): @@ -83,7 +87,8 @@ def test_navigate_and_compare_wrong_golden(self): ROOT_SCREENSHOT_PATH, "test_navigate_and_compare_wrong_golden", instructions, - screen_change_before_first_instruction=False) + screen_change_before_first_instruction=False, + ) self.assertIn("Screen does not match golden", str(error.exception)) self.assertIn("00001.png", str(error.exception)) @@ -91,10 +96,14 @@ def test_navigate_until_snap(self): with patch("speculos.client.subprocess"): with SpeculosServerStub(): with self.backend: - ret = self.navigator.navigate_until_snap(NavIns(NavInsID.RIGHT_CLICK), - NavIns(NavInsID.BOTH_CLICK), - ROOT_SCREENSHOT_PATH, "generic", - "00000.png", "00002.png") + ret = self.navigator.navigate_until_snap( + NavIns(NavInsID.RIGHT_CLICK), + NavIns(NavInsID.BOTH_CLICK), + ROOT_SCREENSHOT_PATH, + "generic", + "00000.png", + "00002.png", + ) self.assertEqual(ret, 2) def test_navigate_fail_cannot_find_first_snap(self): @@ -102,10 +111,14 @@ def test_navigate_fail_cannot_find_first_snap(self): with SpeculosServerStub(): with self.backend: with self.assertRaises(ValueError) as error: - self.navigator.navigate_until_snap(NavIns(NavInsID.RIGHT_CLICK), - NavIns(NavInsID.BOTH_CLICK), - ROOT_SCREENSHOT_PATH, "generic", - "00003.png", "00002.png") + self.navigator.navigate_until_snap( + NavIns(NavInsID.RIGHT_CLICK), + NavIns(NavInsID.BOTH_CLICK), + ROOT_SCREENSHOT_PATH, + "generic", + "00003.png", + "00002.png", + ) self.assertIn("Could not find first snapshot", str(error.exception)) def test_navigate_fail_cannot_find_last_snap(self): @@ -113,23 +126,27 @@ def test_navigate_fail_cannot_find_last_snap(self): with SpeculosServerStub(): with self.backend: with self.assertRaises(TimeoutError) as error: - self.navigator.navigate_until_snap(NavIns(NavInsID.RIGHT_CLICK), - NavIns(NavInsID.BOTH_CLICK), - ROOT_SCREENSHOT_PATH, - "generic", - "00000.png", - "00004.png", - timeout=5) + self.navigator.navigate_until_snap( + NavIns(NavInsID.RIGHT_CLICK), + NavIns(NavInsID.BOTH_CLICK), + ROOT_SCREENSHOT_PATH, + "generic", + "00000.png", + "00004.png", + timeout=5, + ) self.assertIn("Timeout waiting for snap", str(error.exception)) def test_navigate_until_text(self): with patch("speculos.client.subprocess"): with SpeculosServerStub(): with self.backend: - self.navigator.navigate_until_text(NavIns(NavInsID.RIGHT_CLICK), - [NavIns(NavInsID.BOTH_CLICK)], - "About", - screen_change_before_first_instruction=False) + self.navigator.navigate_until_text( + NavIns(NavInsID.RIGHT_CLICK), + [NavIns(NavInsID.BOTH_CLICK)], + "About", + screen_change_before_first_instruction=False, + ) def test_navigate_until_text_screen_change_timeout(self): with patch("speculos.client.subprocess"): @@ -137,22 +154,28 @@ def test_navigate_until_text_screen_change_timeout(self): with self.backend: with self.assertRaises(TimeoutError) as error: self.navigator.navigate_until_text( - NavIns(NavInsID.BOTH_CLICK), [NavIns(NavInsID.BOTH_CLICK)], + NavIns(NavInsID.BOTH_CLICK), + [NavIns(NavInsID.BOTH_CLICK)], "WILL NOT BE FOUND", timeout=5, - screen_change_before_first_instruction=False) - self.assertIn("Timeout waiting for screen change", str(error.exception)) + screen_change_before_first_instruction=False, + ) + self.assertIn( + "Timeout waiting for screen change", str(error.exception) + ) def test_navigate_until_text_and_compare(self): with patch("speculos.client.subprocess"): with SpeculosServerStub(): with self.backend: self.navigator.navigate_until_text_and_compare( - NavIns(NavInsID.RIGHT_CLICK), [NavIns(NavInsID.BOTH_CLICK)], + NavIns(NavInsID.RIGHT_CLICK), + [NavIns(NavInsID.BOTH_CLICK)], "About", ROOT_SCREENSHOT_PATH, "test_navigate_until_text_and_compare", - screen_change_before_first_instruction=False) + screen_change_before_first_instruction=False, + ) def test_navigate_until_text_and_compare_no_golden(self): with patch("speculos.client.subprocess"): @@ -160,14 +183,18 @@ def test_navigate_until_text_and_compare_no_golden(self): with self.backend: with self.assertRaises(FileNotFoundError) as error: self.navigator.navigate_until_text_and_compare( - NavIns(NavInsID.RIGHT_CLICK), [NavIns(NavInsID.BOTH_CLICK)], + NavIns(NavInsID.RIGHT_CLICK), + [NavIns(NavInsID.BOTH_CLICK)], "About", ROOT_SCREENSHOT_PATH, "test_navigate_and_compare_no_golden", - screen_change_before_first_instruction=False) + screen_change_before_first_instruction=False, + ) self.assertIn("No such file or directory", str(error.exception)) - self.assertIn("test_navigate_and_compare_no_golden/00001.png", - str(error.exception)) + self.assertIn( + "test_navigate_and_compare_no_golden/00001.png", + str(error.exception), + ) def test_navigate_until_text_and_compare_wrong_golden(self): with patch("speculos.client.subprocess"): @@ -175,10 +202,12 @@ def test_navigate_until_text_and_compare_wrong_golden(self): with self.backend: with self.assertRaises(AssertionError) as error: self.navigator.navigate_until_text_and_compare( - NavIns(NavInsID.RIGHT_CLICK), [NavIns(NavInsID.BOTH_CLICK)], + NavIns(NavInsID.RIGHT_CLICK), + [NavIns(NavInsID.BOTH_CLICK)], "About", ROOT_SCREENSHOT_PATH, "test_navigate_and_compare_wrong_golden", - screen_change_before_first_instruction=False) + screen_change_before_first_instruction=False, + ) self.assertIn("Screen does not match golden", str(error.exception)) self.assertIn("00001.png", str(error.exception)) diff --git a/tests/functional/test_boilerplate.py b/tests/functional/test_boilerplate.py index 3c72d894..338355a4 100644 --- a/tests/functional/test_boilerplate.py +++ b/tests/functional/test_boilerplate.py @@ -16,7 +16,7 @@ def test_error_returns_not_raises(backend): backend.raise_policy = RaisePolicy.RAISE_NOTHING result = backend.exchange(0x01, 0x00) assert isinstance(result, RAPDU) - assert result.status == 0x6e00 + assert result.status == 0x6E00 assert not result.data @@ -24,14 +24,14 @@ def test_error_raises_not_returns(backend): try: backend.exchange(0x01, 0x00) except ExceptionRAPDU as e: - assert e.status == 0x6e00 + assert e.status == 0x6E00 assert not e.data @pytest.mark.use_on_backend("speculos") def test_quit_app(backend, device, navigator): if not device.touchable: - right_clicks = {'nanos': 3, 'nanox': 3, 'nanosp': 3} + right_clicks = {"nanos": 3, "nanox": 3, "nanosp": 3} backend.get_current_screen_content() for _ in range(right_clicks[device.name]): @@ -51,9 +51,11 @@ def test_quit_app(backend, device, navigator): with pytest.raises(ConnectionError): # clicking on "Quit", Speculos then stops and raises - navigator.navigate([NavInsID.USE_CASE_HOME_QUIT], - screen_change_before_first_instruction=False, - screen_change_after_last_instruction=False) + navigator.navigate( + [NavInsID.USE_CASE_HOME_QUIT], + screen_change_before_first_instruction=False, + screen_change_after_last_instruction=False, + ) time.sleep(1) # Then a new dummy touch should raise a ConnectionError @@ -67,10 +69,12 @@ def test_waiting_screen(backend, device, navigator): prep_tx_apdu = bytes.fromhex("e006008015058000002c80000001800000000000000000000000") - sign_tx_apdu = bytes.fromhex("e0060100310000000000000001de0b29" - "5669a9fd93d5f28d9ec85e40f4cb697b" - "ae000000000000029a0c466f72207520" - "457468446576") + sign_tx_apdu = bytes.fromhex( + "e0060100310000000000000001de0b29" + "5669a9fd93d5f28d9ec85e40f4cb697b" + "ae000000000000029a0c466f72207520" + "457468446576" + ) # Test multiple way to wait for the return for the Home screen after a review. @@ -79,49 +83,79 @@ def test_waiting_screen(backend, device, navigator): with backend.exchange_async_raw(sign_tx_apdu): navigator.navigate_until_text_and_compare( NavInsID.USE_CASE_REVIEW_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], "Hold to sign", - ROOT_SCREENSHOT_PATH, "waiting_screen") + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.USE_CASE_STATUS_DISMISS], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + "waiting_screen", + ) # Using WAIT_FOR_HOME_SCREEN instruction backend.exchange_raw(prep_tx_apdu) with backend.exchange_async_raw(sign_tx_apdu): navigator.navigate_until_text_and_compare( NavInsID.USE_CASE_REVIEW_NEXT, - [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.WAIT_FOR_HOME_SCREEN], "Hold to sign", - ROOT_SCREENSHOT_PATH, "waiting_screen") + [NavInsID.USE_CASE_REVIEW_CONFIRM, NavInsID.WAIT_FOR_HOME_SCREEN], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + "waiting_screen", + ) # Using WAIT_FOR_TEXT_ON_SCREEN instruction backend.exchange_raw(prep_tx_apdu) with backend.exchange_async_raw(sign_tx_apdu): - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_NEXT, [ - NavInsID.USE_CASE_REVIEW_CONFIRM, - NavIns(NavInsID.WAIT_FOR_TEXT_ON_SCREEN, ("This app enables signing", )) - ], "Hold to sign", ROOT_SCREENSHOT_PATH, "waiting_screen") + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_REVIEW_NEXT, + [ + NavInsID.USE_CASE_REVIEW_CONFIRM, + NavIns(NavInsID.WAIT_FOR_TEXT_ON_SCREEN, ("This app enables signing",)), + ], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + "waiting_screen", + ) # Using WAIT_FOR_TEXT_NOT_ON_SCREEN instruction backend.exchange_raw(prep_tx_apdu) with backend.exchange_async_raw(sign_tx_apdu): - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_NEXT, [ - NavInsID.USE_CASE_REVIEW_CONFIRM, - NavIns(NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN, ("Transaction", )) - ], "Hold to sign", ROOT_SCREENSHOT_PATH, "waiting_screen") + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_REVIEW_NEXT, + [ + NavInsID.USE_CASE_REVIEW_CONFIRM, + NavIns(NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN, ("Transaction",)), + ], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + "waiting_screen", + ) # Verify the error flow of WAIT_FOR_TEXT_ON_SCREEN instruction backend.exchange_raw(prep_tx_apdu) with backend.exchange_async_raw(sign_tx_apdu): with pytest.raises(TimeoutError) as error: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_NEXT, [ - NavInsID.USE_CASE_REVIEW_CONFIRM, - NavIns(NavInsID.WAIT_FOR_TEXT_ON_SCREEN, ("WILL NOT BE FOUND", )) - ], "Hold to sign", ROOT_SCREENSHOT_PATH, "waiting_screen") + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_REVIEW_NEXT, + [ + NavInsID.USE_CASE_REVIEW_CONFIRM, + NavIns(NavInsID.WAIT_FOR_TEXT_ON_SCREEN, ("WILL NOT BE FOUND",)), + ], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + "waiting_screen", + ) assert "Timeout waiting for screen change" in str(error.value) # Verify the error flow of WAIT_FOR_TEXT_ON_SCREEN instruction backend.exchange_raw(prep_tx_apdu) with backend.exchange_async_raw(sign_tx_apdu): with pytest.raises(TimeoutError) as error: - navigator.navigate_until_text_and_compare(NavInsID.USE_CASE_REVIEW_NEXT, [ - NavInsID.USE_CASE_REVIEW_CONFIRM, - NavIns(NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN, ("T", )) - ], "Hold to sign", ROOT_SCREENSHOT_PATH, "waiting_screen") + navigator.navigate_until_text_and_compare( + NavInsID.USE_CASE_REVIEW_NEXT, + [ + NavInsID.USE_CASE_REVIEW_CONFIRM, + NavIns(NavInsID.WAIT_FOR_TEXT_NOT_ON_SCREEN, ("T",)), + ], + "Hold to sign", + ROOT_SCREENSHOT_PATH, + "waiting_screen", + ) assert "Timeout waiting for screen change" in str(error.value) diff --git a/tests/stubs.py b/tests/stubs.py index b3a1f578..4ed6f6d5 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -22,17 +22,18 @@ class EndPoint: class Events: back = [{"text": "Back", "x": 51, "y": 19}] - info = [{ - "text": "Boilerplate App", - "x": 20, - "y": 3 - }, { - "text": "(c) 2020 Ledger", - "x": 26, - "y": 17 - }] - home = [{"text": "Boilerplate", "x": 41, "y": 3}, {"text": "is ready", "x": 41, "y": 17}] - version = [{"text": "Version", "x": 43, "y": 3}, {"text": "1.0.1", "x": 52, "y": 17}] + info = [ + {"text": "Boilerplate App", "x": 20, "y": 3}, + {"text": "(c) 2020 Ledger", "x": 26, "y": 17}, + ] + home = [ + {"text": "Boilerplate", "x": 41, "y": 3}, + {"text": "is ready", "x": 41, "y": 17}, + ] + version = [ + {"text": "Version", "x": 43, "y": 3}, + {"text": "1.0.1", "x": 52, "y": 17}, + ] about = [{"text": "About", "x": 47, "y": 19}] indexed = [home, version, about, info, back] @@ -52,7 +53,6 @@ def apdu(*args): class Actions: - def __init__(self): self.idx = 0 @@ -84,8 +84,11 @@ def events(self, *args): return {"events": Events.indexed[self.idx]}, 200 def screenshot(self, *args): - path = Path( - __file__).parent.resolve() / "snapshots/nanos/generic" / f"{str(self.idx).zfill(5)}.png" + path = ( + Path(__file__).parent.resolve() + / "snapshots/nanos/generic" + / f"{str(self.idx).zfill(5)}.png" + ) img_temp = Image.open(path) iobytes = BytesIO() img_temp.save(iobytes, format="PNG") @@ -96,18 +99,27 @@ def ticker(self, *args): class SpeculosServerStub: - def __init__(self): actions = Actions() - self.app = Flask('stub') + self.app = Flask("stub") self.app.add_url_rule("/", view_func=root) self.app.add_url_rule("/apdu", methods=["GET", "POST"], view_func=apdu) - self.app.add_url_rule("/button/right", methods=["GET", "POST"], view_func=actions.button) - self.app.add_url_rule("/button/left", methods=["GET", "POST"], view_func=actions.button) - self.app.add_url_rule("/button/both", methods=["GET", "POST"], view_func=actions.button) + self.app.add_url_rule( + "/button/right", methods=["GET", "POST"], view_func=actions.button + ) + self.app.add_url_rule( + "/button/left", methods=["GET", "POST"], view_func=actions.button + ) + self.app.add_url_rule( + "/button/both", methods=["GET", "POST"], view_func=actions.button + ) self.app.add_url_rule("/events", view_func=actions.events) - self.app.add_url_rule("/screenshot", methods=["GET"], view_func=actions.screenshot) - self.app.add_url_rule("/ticker", methods=["GET", "POST"], view_func=actions.ticker) + self.app.add_url_rule( + "/screenshot", methods=["GET"], view_func=actions.screenshot + ) + self.app.add_url_rule( + "/ticker", methods=["GET", "POST"], view_func=actions.ticker + ) self.process = None def __enter__(self): diff --git a/tests/unit/backend/test_interface.py b/tests/unit/backend/test_interface.py index afbafbf3..7739c024 100644 --- a/tests/unit/backend/test_interface.py +++ b/tests/unit/backend/test_interface.py @@ -11,7 +11,6 @@ class DummyBackend(BackendInterface): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.mock = MagicMock() @@ -84,10 +83,12 @@ def send_tick(self) -> None: class TestBackendInterface(TestCase): - def setUp(self): self.device = Devices.get_by_type(DeviceType.NANOS) - self.errors = (ExceptionRAPDU(0x8888, "ERROR1"), ExceptionRAPDU(0x7777, "ERROR2")) + self.errors = ( + ExceptionRAPDU(0x8888, "ERROR1"), + ExceptionRAPDU(0x7777, "ERROR2"), + ) self.valid_statuses = (0x9000, 0x9001, 0x9002) self.backend = DummyBackend(device=self.device) @@ -104,7 +105,7 @@ def test_send(self): self.assertFalse(self.backend.mock.send_raw.called) self.backend.send(cla, ins, p1, p2) self.assertTrue(self.backend.mock.send_raw.called) - self.assertEqual(self.backend.mock.send_raw.call_args, ((expected, ), )) + self.assertEqual(self.backend.mock.send_raw.call_args, ((expected,),)) def test_exchange(self): cla, ins, p1, p2 = 1, 2, 3, 4 @@ -112,7 +113,9 @@ def test_exchange(self): self.assertFalse(self.backend.mock.send_raw.called) result = self.backend.exchange(cla, ins, p1, p2) self.assertTrue(self.backend.mock.exchange_raw.called) - self.assertEqual(self.backend.mock.exchange_raw.call_args, ((expected, 5 * 60 * 10), )) + self.assertEqual( + self.backend.mock.exchange_raw.call_args, ((expected, 5 * 60 * 10),) + ) self.assertEqual(result, self.backend.mock.exchange_raw()) def test_exchange_async(self): @@ -122,19 +125,18 @@ def test_exchange_async(self): with self.backend.exchange_async(cla, ins, p1, p2): pass self.assertTrue(self.backend.mock.exchange_async_raw.called) - self.assertEqual(self.backend.mock.exchange_async_raw.call_args, ((expected, ), )) + self.assertEqual(self.backend.mock.exchange_async_raw.call_args, ((expected,),)) class TestBackendInterfaceLogging(TestCase): - def test_log_apdu(self): self.device = Devices.get_by_type(DeviceType.NANOS) with tempfile.TemporaryDirectory() as td: test_file = (Path(td) / "test_log_file.log").resolve() self.backend = DummyBackend(device=self.device, log_apdu_file=test_file) ref_lines = ["Test logging", "hello world", "Lorem Ipsum"] - for l in ref_lines: - self.backend.apdu_logger.info(l) - with open(test_file, mode='r') as fp: - read_lines = [l.strip() for l in fp.readlines()] + for line in ref_lines: + self.backend.apdu_logger.info(line) + with open(test_file, mode="r") as fp: + read_lines = [line.strip() for line in fp.readlines()] self.assertEqual(read_lines, ref_lines) diff --git a/tests/unit/backend/test_ledgerwallet.py b/tests/unit/backend/test_ledgerwallet.py index b326c4c7..07f3956f 100644 --- a/tests/unit/backend/test_ledgerwallet.py +++ b/tests/unit/backend/test_ledgerwallet.py @@ -10,12 +10,13 @@ class TestLedgerWalletBackend(TestCase): - def setUp(self): self.device = MagicMock() self.backend = LedgerWalletBackend(Devices.get_by_type(DeviceType.NANOS)) - def check_rapdu(self, rapdu: RAPDU, status: int = 0x9000, payload: Optional[bytes] = None): + def check_rapdu( + self, rapdu: RAPDU, status: int = 0x9000, payload: Optional[bytes] = None + ): self.assertIsInstance(rapdu, RAPDU) self.assertEqual(rapdu.status, status) self.assertEqual(rapdu.data, payload) diff --git a/tests/unit/backend/test_physical_backend.py b/tests/unit/backend/test_physical_backend.py index d2b616a4..a277767e 100644 --- a/tests/unit/backend/test_physical_backend.py +++ b/tests/unit/backend/test_physical_backend.py @@ -12,7 +12,6 @@ class StubPhysicalBackend(PhysicalBackend): - def __enter__(self): pass @@ -31,7 +30,6 @@ def exchange_async_raw(self, data: bytes = b"") -> Generator[None, None, None]: class TestPhysicalBackend(TestCase): - def setUp(self): self.device = Devices.get_by_type(DeviceType.NANOS) self.backend = StubPhysicalBackend(self.device, with_gui=True) @@ -59,20 +57,27 @@ def test_init_gui_with_gui(self): def test_navigation_methods_no_gui_None(self): backend = StubPhysicalBackend(self.device) for method in [ - backend.right_click, backend.left_click, backend.both_click, backend.finger_touch + backend.right_click, + backend.left_click, + backend.both_click, + backend.finger_touch, ]: self.assertIsNone(method()) def test_click_methods_with_gui(self): - for (method, expected_arg) in [(self.backend.right_click, NavInsID.RIGHT_CLICK), - (self.backend.left_click, NavInsID.LEFT_CLICK), - (self.backend.both_click, NavInsID.BOTH_CLICK)]: + for method, expected_arg in [ + (self.backend.right_click, NavInsID.RIGHT_CLICK), + (self.backend.left_click, NavInsID.LEFT_CLICK), + (self.backend.both_click, NavInsID.BOTH_CLICK), + ]: # mocking the underlying called method self.backend._ui.ask_for_click_action = MagicMock() self.assertIsNone(method()) self.assertTrue(self.backend._ui.ask_for_click_action.called) - self.assertEqual(self.backend._ui.ask_for_click_action.call_args, ((expected_arg, ), )) + self.assertEqual( + self.backend._ui.ask_for_click_action.call_args, ((expected_arg,),) + ) def test_finger_touch_with_gui(self): x, y = 3, 7 @@ -81,7 +86,7 @@ def test_finger_touch_with_gui(self): self.assertIsNone(self.backend.finger_touch(x, y)) self.assertTrue(self.backend._ui.ask_for_touch_action.called) - self.assertEqual(self.backend._ui.ask_for_touch_action.call_args, ((x, y), )) + self.assertEqual(self.backend._ui.ask_for_touch_action.call_args, ((x, y),)) def test_compare_methods_no_gui_bool(self): backend = StubPhysicalBackend(self.device) @@ -124,9 +129,13 @@ def test_compare_screen_with_text_with_gui_last_valid_snap_path_exists(self): self.backend._last_valid_snap_path = path self.assertTrue(self.backend.compare_screen_with_text(text)) - self.assertFalse(self.backend.compare_screen_with_text("this text does not exist here")) + self.assertFalse( + self.backend.compare_screen_with_text("this text does not exist here") + ) - def test_compare_screen_with_text_with_gui_last_valid_snap_path_does_not_exist(self): + def test_compare_screen_with_text_with_gui_last_valid_snap_path_does_not_exist( + self, + ): # mocking the underlying called method oracle = MagicMock() oracle.return_value = True @@ -135,7 +144,7 @@ def test_compare_screen_with_text_with_gui_last_valid_snap_path_does_not_exist(s text = "sometext" self.assertTrue(self.backend.compare_screen_with_text(text)) self.assertTrue(oracle.called) - self.assertEqual(oracle.call_args, ((text, ), )) + self.assertEqual(oracle.call_args, ((text,),)) def test_wait_for_screen_change(self): self.assertIsNone(self.backend.wait_for_screen_change()) diff --git a/tests/unit/backend/test_speculos.py b/tests/unit/backend/test_speculos.py index fa2a3cc6..d9e26649 100644 --- a/tests/unit/backend/test_speculos.py +++ b/tests/unit/backend/test_speculos.py @@ -15,7 +15,6 @@ def get_next_in_list(the_list: List, elt: str) -> str: class TestSpeculosBackend(TestCase): - maxDiff = None def setUp(self): @@ -28,11 +27,11 @@ def test___init__ok(self): def test___init__ports_ok(self): api_port, apdu_port = 4567, 9876 - b1 = SpeculosBackend(APPNAME, - self.nanos, - args=["--api-port", - str(api_port), "--apdu-port", - str(apdu_port)]) + b1 = SpeculosBackend( + APPNAME, + self.nanos, + args=["--api-port", str(api_port), "--apdu-port", str(apdu_port)], + ) self.assertEqual(b1._api_port, api_port) self.assertEqual(b1._apdu_port, apdu_port) @@ -65,8 +64,9 @@ def test_context_manager(self): self.assertTrue(backend._client.__enter__.called) self.assertEqual(backend, yielded) self.assertEqual(backend._last_screenshot, backend._home_screenshot) - self.assertEqual(backend._last_screenshot.getvalue(), - BytesIO(expected_image).getvalue()) + self.assertEqual( + backend._last_screenshot.getvalue(), BytesIO(expected_image).getvalue() + ) self.assertFalse(backend._client.__exit__.called) self.assertTrue(backend._client.__exit__.called) @@ -81,7 +81,8 @@ def test_batch_ok(self): self.nanos, client_number, different_attestation=True, - args=['--apdu-port', arg_apdu_port, '--api-port', arg_api_port]) + args=["--apdu-port", arg_apdu_port, "--api-port", arg_api_port], + ) self.assertEqual(len(clients), client_number) self.assertEqual(patched_client.call_count, client_number) @@ -98,12 +99,18 @@ def test_batch_ok(self): args, kwargs = all_client_args[index] self.assertEqual(args, ()) self.assertEqual(kwargs["app"], APPNAME) - self.assertEqual(kwargs["api_url"], f"http://127.0.0.1:{client._api_port}") + self.assertEqual( + kwargs["api_url"], f"http://127.0.0.1:{client._api_port}" + ) speculos_args = kwargs["args"] client_seeds.add(get_next_in_list(speculos_args, "--seed")) client_rngs.add(get_next_in_list(speculos_args, "--deterministic-rng")) - client_priv_keys.add(get_next_in_list(speculos_args, "--user-private-key")) - client_attestations.add(get_next_in_list(speculos_args, "--attestation-key")) + client_priv_keys.add( + get_next_in_list(speculos_args, "--user-private-key") + ) + client_attestations.add( + get_next_in_list(speculos_args, "--attestation-key") + ) api_port = int(get_next_in_list(speculos_args, "--api-port")) client_api_ports.add(client._api_port) apdu_port = int(get_next_in_list(speculos_args, "--apdu-port")) diff --git a/tests/unit/backend/test_stub.py b/tests/unit/backend/test_stub.py index fa209fbc..4e512d3a 100644 --- a/tests/unit/backend/test_stub.py +++ b/tests/unit/backend/test_stub.py @@ -6,7 +6,6 @@ class TestStubBackend(TestCase): - def setUp(self): self.stub = StubBackend(None) @@ -14,12 +13,24 @@ def test_can_instantiate(self): self.assertIsInstance(self.stub, BackendInterface) def test_emtpy_methods(self): - for func in (self.stub.handle_usb_reset, self.stub.send_raw, self.stub.right_click, - self.stub.left_click, self.stub.both_click, self.stub.finger_touch, - self.stub.wait_for_screen_change, self.stub.get_current_screen_content, - self.stub.pause_ticker, self.stub.resume_ticker, self.stub.send_tick): + for func in ( + self.stub.handle_usb_reset, + self.stub.send_raw, + self.stub.right_click, + self.stub.left_click, + self.stub.both_click, + self.stub.finger_touch, + self.stub.wait_for_screen_change, + self.stub.get_current_screen_content, + self.stub.pause_ticker, + self.stub.resume_ticker, + self.stub.send_tick, + ): self.assertIsNone(func()) for func in (self.stub.receive, self.stub.exchange_raw): self.assertEqual(func(), RAPDU(0x9000, b"")) - for func in (self.stub.compare_screen_with_snapshot, self.stub.compare_screen_with_text): + for func in ( + self.stub.compare_screen_with_snapshot, + self.stub.compare_screen_with_text, + ): self.assertTrue(func(None)) diff --git a/tests/unit/bip/test_path.py b/tests/unit/bip/test_path.py index 98528042..dd988e77 100644 --- a/tests/unit/bip/test_path.py +++ b/tests/unit/bip/test_path.py @@ -8,18 +8,21 @@ class TestPath(TestCase): - def _test_level_n(self, n: int): for variant in [0, 255, 81845, 887587, MAX_VALUE]: for hardening in [False, True]: - hardening_char = '\'' if hardening else '' + hardening_char = "'" if hardening else "" previous_levels = "0/" * (n - 1) - path = f'm/{previous_levels}{variant}{hardening_char}' + path = f"m/{previous_levels}{variant}{hardening_char}" packed = p.pack_derivation_path(path) self.assertEqual(packed[0], n) self.assertEqual(len(packed), 1 + n * 4) - last_value = int.from_bytes(packed[len(packed) - 4:len(packed)], byteorder="big") - self.assertEqual(last_value, variant | (HARDENED_INDEX if hardening else 0)) + last_value = int.from_bytes( + packed[len(packed) - 4 : len(packed)], byteorder="big" + ) + self.assertEqual( + last_value, variant | (HARDENED_INDEX if hardening else 0) + ) def test_errors(self): with self.assertRaises(ValueError): @@ -70,7 +73,9 @@ def test_bitcoin_pack_derivation_path(self): base = b"\x03\x80\x00\x00\x2c\x80\x00\x00\x00\x00\x00\x00\x00" prefix = [b"\x00", b"\x01", b"\x02", b"\x03", b"\x04"] for i, format in enumerate(p.BtcDerivationPathFormat): - self.assertEqual(prefix[i] + base, p.bitcoin_pack_derivation_path(format, "m/44'/0'/0")) + self.assertEqual( + prefix[i] + base, p.bitcoin_pack_derivation_path(format, "m/44'/0'/0") + ) def test_bitcoin_pack_derivation_path_nok(self): with self.assertRaises(ValueError): diff --git a/tests/unit/bip/test_seed.py b/tests/unit/bip/test_seed.py index d0a3aa91..24ec18e8 100644 --- a/tests/unit/bip/test_seed.py +++ b/tests/unit/bip/test_seed.py @@ -1,5 +1,6 @@ from unittest import TestCase from typing import Tuple + # from ragger.bip.seed import Seed from ragger.bip import calculate_public_key_and_chaincode, CurveChoice from bip_utils import Bip32KeyError diff --git a/tests/unit/conftests/test_base_conftest.py b/tests/unit/conftests/test_base_conftest.py index 49031513..fe74e801 100644 --- a/tests/unit/conftests/test_base_conftest.py +++ b/tests/unit/conftests/test_base_conftest.py @@ -14,12 +14,14 @@ def prepare_base_dir(directory: Path) -> Tuple[Path, Path]: (directory / ".git").mkdir() (directory / "build" / "stax" / "bin").mkdir(parents=True, exist_ok=True) - (directory / "deps" / "dep" / "build" / "stax" / "bin").mkdir(parents=True, exist_ok=True) - dep_path = (directory / "deps" / "dep" / "build" / "stax" / "bin" / "app.elf") + (directory / "deps" / "dep" / "build" / "stax" / "bin").mkdir( + parents=True, exist_ok=True + ) + dep_path = directory / "deps" / "dep" / "build" / "stax" / "bin" / "app.elf" dep_path.touch() - token_file_path = (directory / "deps" / ".ethereum_application_build_goes_there") + token_file_path = directory / "deps" / ".ethereum_application_build_goes_there" token_file_path.touch() - app_path = (directory / "build" / "stax" / "bin" / "app.elf") + app_path = directory / "build" / "stax" / "bin" / "app.elf" app_path.touch() return app_path, dep_path @@ -40,7 +42,6 @@ def from_path(*args): class TestBaseConftest(TestCase): - def setUp(self): self.seed = "some seed" self.stax = Devices.get_by_type(DeviceType.STAX) @@ -49,8 +50,9 @@ def test_prepare_speculos_args_simplest(self): with temporary_directory() as temp_dir: app_path, _ = prepare_base_dir(temp_dir) with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): - result_app, result_args = bc.prepare_speculos_args(temp_dir, self.stax, False, - False, self.seed, []) + result_app, result_args = bc.prepare_speculos_args( + temp_dir, self.stax, False, False, self.seed, [] + ) self.assertEqual(result_app, app_path) self.assertEqual(result_args, {"args": ["--seed", self.seed]}) @@ -58,8 +60,9 @@ def test_prepare_speculos_args_with_prod_pki(self): with temporary_directory() as temp_dir: app_path, _ = prepare_base_dir(temp_dir) with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): - result_app, result_args = bc.prepare_speculos_args(temp_dir, self.stax, False, True, - self.seed, []) + result_app, result_args = bc.prepare_speculos_args( + temp_dir, self.stax, False, True, self.seed, [] + ) self.assertEqual(result_app, app_path) self.assertEqual(result_args, {"args": ["-p", "--seed", self.seed]}) @@ -68,8 +71,9 @@ def test_prepare_speculos_args_with_custom_args(self): with temporary_directory() as temp_dir: app_path, _ = prepare_base_dir(temp_dir) with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): - result_app, result_args = bc.prepare_speculos_args(temp_dir, self.stax, False, - False, self.seed, [arg]) + result_app, result_args = bc.prepare_speculos_args( + temp_dir, self.stax, False, False, self.seed, [arg] + ) self.assertEqual(result_app, app_path) self.assertEqual(result_args, {"args": [arg, "--seed", self.seed]}) @@ -77,20 +81,28 @@ def test_prepare_speculos_args_simple_with_gui(self): with temporary_directory() as temp_dir: app_path, _ = prepare_base_dir(temp_dir) with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): - result_app, result_args = bc.prepare_speculos_args(temp_dir, self.stax, True, False, - self.seed, []) + result_app, result_args = bc.prepare_speculos_args( + temp_dir, self.stax, True, False, self.seed, [] + ) self.assertEqual(result_app, app_path) - self.assertEqual(result_args, {"args": ["--display", "qt", "--seed", self.seed]}) + self.assertEqual( + result_args, {"args": ["--display", "qt", "--seed", self.seed]} + ) def test_prepare_speculos_args_main_as_library(self): with temporary_directory() as temp_dir: app_path, dep_path = prepare_base_dir(temp_dir) - with patch("ragger.conftest.base_conftest.conf.OPTIONAL.MAIN_APP_DIR", "./deps"): - with patch("ragger.conftest.base_conftest.Manifest", ManifestMock) as manifest: + with patch( + "ragger.conftest.base_conftest.conf.OPTIONAL.MAIN_APP_DIR", "./deps" + ): + with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): result_app, result_args = bc.prepare_speculos_args( - temp_dir, self.stax, False, False, self.seed, []) + temp_dir, self.stax, False, False, self.seed, [] + ) self.assertEqual(result_app, dep_path) - self.assertEqual(result_args, {"args": [f"-l{app_path}", "--seed", self.seed]}) + self.assertEqual( + result_args, {"args": [f"-l{app_path}", "--seed", self.seed]} + ) def test_prepare_speculos_args_sideloaded_apps_ok(self): with temporary_directory() as temp_dir: @@ -105,15 +117,19 @@ def test_prepare_speculos_args_sideloaded_apps_ok(self): lib1_exe.touch() lib2_exe.touch() - with patch("ragger.conftest.base_conftest.conf.OPTIONAL.SIDELOADED_APPS_DIR", - sideloaded_apps_dir): - + with patch( + "ragger.conftest.base_conftest.conf.OPTIONAL.SIDELOADED_APPS_DIR", + sideloaded_apps_dir, + ): with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): result_app, result_args = bc.prepare_speculos_args( - temp_dir, self.stax, False, False, self.seed, []) + temp_dir, self.stax, False, False, self.seed, [] + ) self.assertEqual(result_app, app_path) - self.assertEqual(result_args, - {"args": [f"-l{lib1_exe}", f"-l{lib2_exe}", "--seed", self.seed]}) + self.assertEqual( + result_args, + {"args": [f"-l{lib1_exe}", f"-l{lib2_exe}", "--seed", self.seed]}, + ) def test_create_backend_nok(self): with self.assertRaises(ValueError): @@ -124,18 +140,28 @@ def test_create_backend_speculos(self): with temporary_directory() as temp_dir: prepare_base_dir(temp_dir) with patch("ragger.conftest.base_conftest.Manifest", ManifestMock): - result = bc.create_backend(temp_dir, "Speculos", self.stax, False, False, None, - self.seed, []) + result = bc.create_backend( + temp_dir, + "Speculos", + self.stax, + False, + False, + None, + self.seed, + [], + ) self.assertEqual(result, backend()) def test_create_backend_ledgercomm(self): with patch("ragger.conftest.base_conftest.LedgerWalletBackend") as backend: - result = bc.create_backend(None, "ledgerWALLET", self.stax, False, False, None, - self.seed, []) + result = bc.create_backend( + None, "ledgerWALLET", self.stax, False, False, None, self.seed, [] + ) self.assertEqual(result, backend()) def test_create_backend_ledgerwallet(self): with patch("ragger.conftest.base_conftest.LedgerCommBackend") as backend: - result = bc.create_backend(None, "LedgerComm", self.stax, False, False, None, self.seed, - []) + result = bc.create_backend( + None, "LedgerComm", self.stax, False, False, None, self.seed, [] + ) self.assertEqual(result, backend()) diff --git a/tests/unit/firmware/test_structs_Firmware.py b/tests/unit/firmware/test_structs_Firmware.py index f44ec90f..cdce599f 100644 --- a/tests/unit/firmware/test_structs_Firmware.py +++ b/tests/unit/firmware/test_structs_Firmware.py @@ -4,7 +4,6 @@ class TestFirmware(TestCase): - def test_firmware_not_existing_version(self): with self.assertRaises(ValueError): Firmware(10) diff --git a/tests/unit/firmware/touch/test_element.py b/tests/unit/firmware/touch/test_element.py index 1994b752..a994601f 100644 --- a/tests/unit/firmware/touch/test_element.py +++ b/tests/unit/firmware/touch/test_element.py @@ -5,12 +5,13 @@ class TestElement(TestCase): - def test___init__(self): client = MagicMock() device = MagicMock() positions = MagicMock() - with patch("ragger.firmware.touch.element.POSITIONS", {Element.__name__: positions}): + with patch( + "ragger.firmware.touch.element.POSITIONS", {Element.__name__: positions} + ): element = Element(client, device) self.assertEqual(element.device, device) self.assertEqual(element.client, client) diff --git a/tests/unit/firmware/touch/test_screen_FullScreen.py b/tests/unit/firmware/touch/test_screen_FullScreen.py index ecddbbf2..f3dac8ed 100644 --- a/tests/unit/firmware/touch/test_screen_FullScreen.py +++ b/tests/unit/firmware/touch/test_screen_FullScreen.py @@ -7,7 +7,6 @@ class TestFullScreen(TestCase): - def setUp(self): self.backend = MagicMock() self.device = Devices.get_by_type(DeviceType.STAX) @@ -22,8 +21,14 @@ def test_non_variable_layouts(self): (self.screen.info_header, POSITIONS["RightHeader"][self.device.type]), (self.screen.left_header, POSITIONS["LeftHeader"][self.device.type]), (self.screen.navigation_header, POSITIONS["LeftHeader"][self.device.type]), - (self.screen.tappable_center, POSITIONS["TappableCenter"][self.device.type]), - (self.screen.centered_footer, POSITIONS["CenteredFooter"][self.device.type]), + ( + self.screen.tappable_center, + POSITIONS["TappableCenter"][self.device.type], + ), + ( + self.screen.centered_footer, + POSITIONS["CenteredFooter"][self.device.type], + ), (self.screen.cancel_footer, POSITIONS["CancelFooter"][self.device.type]), (self.screen.exit_footer, POSITIONS["CancelFooter"][self.device.type]), (self.screen.info_footer, POSITIONS["CancelFooter"][self.device.type]), @@ -31,12 +36,12 @@ def test_non_variable_layouts(self): ] call_number = 0 self.assertEqual(self.backend.finger_touch.call_count, call_number) - for (layout, position) in layout_positions: + for layout, position in layout_positions: # each of this layout.tap() call_number += 1 self.assertEqual(self.backend.finger_touch.call_count, call_number) - self.assertEqual(self.backend.finger_touch.call_args, ((*position, ), )) + self.assertEqual(self.backend.finger_touch.call_args, ((*position,),)) def test_choosing_layouts(self): layout_index_positions = [ @@ -45,46 +50,68 @@ def test_choosing_layouts(self): ] call_number = 0 self.assertEqual(self.backend.finger_touch.call_count, call_number) - for (layout, index, position) in layout_index_positions: + for layout, index, position in layout_index_positions: # each of this layout.choose(index) call_number += 1 self.assertEqual(self.backend.finger_touch.call_count, call_number) - self.assertEqual(self.backend.finger_touch.call_args, ((*position[index], ), )) + self.assertEqual( + self.backend.finger_touch.call_args, ((*position[index],),) + ) def test_keyboards_common_functions(self): layouts_word_positions = [ - (self.screen.letter_only_keyboard, "basicword", - POSITIONS["LetterOnlyKeyboard"][self.device.type]), - (self.screen.full_keyboard_letters, "still basic", - POSITIONS["FullKeyboardLetters"][self.device.type]), - (self.screen.full_keyboard_special_characters_1, "12)&@'.", - POSITIONS["FullKeyboardSpecialCharacters1"][self.device.type]), - (self.screen.full_keyboard_special_characters_2, "[$?~+*|", - POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type]), + ( + self.screen.letter_only_keyboard, + "basicword", + POSITIONS["LetterOnlyKeyboard"][self.device.type], + ), + ( + self.screen.full_keyboard_letters, + "still basic", + POSITIONS["FullKeyboardLetters"][self.device.type], + ), + ( + self.screen.full_keyboard_special_characters_1, + "12)&@'.", + POSITIONS["FullKeyboardSpecialCharacters1"][self.device.type], + ), + ( + self.screen.full_keyboard_special_characters_2, + "[$?~+*|", + POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type], + ), ] self.assertEqual(self.backend.finger_touch.call_count, 0) - for (layout, word, positions) in layouts_word_positions: - + for layout, word, positions in layouts_word_positions: layout.write(word) - argument_list = [((*positions[letter], ), ) for letter in word] + argument_list = [((*positions[letter],),) for letter in word] call_number = len(word) self.assertEqual(self.backend.finger_touch.call_count, call_number) self.assertEqual(self.backend.finger_touch.call_args_list, argument_list) layout.back() self.assertEqual(self.backend.finger_touch.call_count, call_number + 1) - self.assertEqual(self.backend.finger_touch.call_args, ((*positions["back"], ), )) + self.assertEqual( + self.backend.finger_touch.call_args, ((*positions["back"],),) + ) self.backend.finger_touch.reset_mock() def test_keyboards_change_layout(self): layouts_positions = [ - (self.screen.full_keyboard_letters, POSITIONS["FullKeyboardLetters"][self.device.type]), - (self.screen.full_keyboard_special_characters_1, - POSITIONS["FullKeyboardSpecialCharacters1"][self.device.type]), - (self.screen.full_keyboard_special_characters_2, - POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type]), + ( + self.screen.full_keyboard_letters, + POSITIONS["FullKeyboardLetters"][self.device.type], + ), + ( + self.screen.full_keyboard_special_characters_1, + POSITIONS["FullKeyboardSpecialCharacters1"][self.device.type], + ), + ( + self.screen.full_keyboard_special_characters_2, + POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type], + ), ] call_number = 0 self.assertEqual(self.backend.finger_touch.call_count, call_number) @@ -92,22 +119,29 @@ def test_keyboards_change_layout(self): layout.change_layout() call_number += 1 self.assertEqual(self.backend.finger_touch.call_count, call_number) - self.assertEqual(self.backend.finger_touch.call_args, - ((*positions["change_layout"], ), )) + self.assertEqual( + self.backend.finger_touch.call_args, ((*positions["change_layout"],),) + ) def test_keyboards_change_case(self): self.assertEqual(self.backend.finger_touch.call_count, 0) self.screen.full_keyboard_letters.change_case() self.assertEqual(self.backend.finger_touch.call_count, 1) - self.assertEqual(self.backend.finger_touch.call_args, - ((*POSITIONS["FullKeyboardLetters"][self.device.type]["change_case"], ), )) + self.assertEqual( + self.backend.finger_touch.call_args, + ((*POSITIONS["FullKeyboardLetters"][self.device.type]["change_case"],),), + ) def test_keyboards_change_special_characters(self): layouts_positions = [ - (self.screen.full_keyboard_special_characters_1, - POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type]), - (self.screen.full_keyboard_special_characters_2, - POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type]), + ( + self.screen.full_keyboard_special_characters_1, + POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type], + ), + ( + self.screen.full_keyboard_special_characters_2, + POSITIONS["FullKeyboardSpecialCharacters2"][self.device.type], + ), ] call_number = 0 self.assertEqual(self.backend.finger_touch.call_count, call_number) @@ -115,5 +149,6 @@ def test_keyboards_change_special_characters(self): layout.more_specials() call_number += 1 self.assertEqual(self.backend.finger_touch.call_count, call_number) - self.assertEqual(self.backend.finger_touch.call_args, - ((*positions["more_specials"], ), )) + self.assertEqual( + self.backend.finger_touch.call_args, ((*positions["more_specials"],),) + ) diff --git a/tests/unit/firmware/touch/test_screen_MetaScreen.py b/tests/unit/firmware/touch/test_screen_MetaScreen.py index b29b33ca..dc9153a0 100644 --- a/tests/unit/firmware/touch/test_screen_MetaScreen.py +++ b/tests/unit/firmware/touch/test_screen_MetaScreen.py @@ -5,7 +5,6 @@ class TestMetaScreen(TestCase): - def setUp(self): self.layout = MagicMock() @@ -24,7 +23,7 @@ def test___init__(self): args = (client, device, "some") test = self.cls(*args) self.assertEqual(self.layout.call_count, 1) - self.assertEqual(self.layout.call_args, ((client, device), )) + self.assertEqual(self.layout.call_args, ((client, device),)) self.assertEqual(test.one, self.layout()) self.assertEqual(test.some, args[-1]) self.assertIsNone(test.other) diff --git a/tests/unit/navigator/test_nano_navigator.py b/tests/unit/navigator/test_nano_navigator.py index ded0e817..734361ae 100644 --- a/tests/unit/navigator/test_nano_navigator.py +++ b/tests/unit/navigator/test_nano_navigator.py @@ -7,10 +7,11 @@ class TestNanoNavigator(TestCase): - def test___init__ok(self): for backend_cls in [ - partial(SpeculosBackend, "some app"), LedgerCommBackend, LedgerWalletBackend + partial(SpeculosBackend, "some app"), + LedgerCommBackend, + LedgerWalletBackend, ]: backend = backend_cls(Devices.get_by_type(DeviceType.NANOS)) NanoNavigator(backend, Devices.get_by_type(DeviceType.NANOS)) diff --git a/tests/unit/navigator/test_navigation_scenario.py b/tests/unit/navigator/test_navigation_scenario.py index 2f6b92f2..fba2894d 100644 --- a/tests/unit/navigator/test_navigation_scenario.py +++ b/tests/unit/navigator/test_navigation_scenario.py @@ -8,15 +8,15 @@ class TestNavigationScenario(TestCase): - def setUp(self): self.directory = TemporaryDirectory() self.backend = MagicMock() self.device = Devices.get_by_type(DeviceType.NANOS) self.callbacks = dict() self.navigator = MagicMock() - self.navigate_with_scenario = NavigateWithScenario(self.backend, self.navigator, - self.device, "test_name", self.directory) + self.navigate_with_scenario = NavigateWithScenario( + self.backend, self.navigator, self.device, "test_name", self.directory + ) def tearDown(self): self.directory.cleanup() @@ -42,9 +42,12 @@ def test_review_approve_with_spinner_nano(self): call_kwargs = self.navigator.navigate_until_text_and_compare.call_args self.assertFalse( call_kwargs.kwargs.get( - 'screen_change_after_last_instruction', - call_kwargs[1].get('screen_change_after_last_instruction', True) - if len(call_kwargs) > 1 else True)) + "screen_change_after_last_instruction", + call_kwargs[1].get("screen_change_after_last_instruction", True) + if len(call_kwargs) > 1 + else True, + ) + ) # The backend should have been called with the spinner text self.backend.wait_for_text_on_screen.assert_called_once_with(spinner_text) @@ -53,7 +56,9 @@ def test_review_approve_with_spinner_no_comparison(self): """Test that review_approve_with_spinner with do_comparison=False uses navigate_until_text with screen_change_after_last_instruction=False and calls wait_for_text_on_screen.""" spinner_text = "Signing..." - self.navigate_with_scenario.review_approve_with_spinner(spinner_text, do_comparison=False) + self.navigate_with_scenario.review_approve_with_spinner( + spinner_text, do_comparison=False + ) # navigate_until_text should have been called (not navigate_until_text_and_compare) self.navigator.navigate_until_text_and_compare.assert_not_called() @@ -61,9 +66,12 @@ def test_review_approve_with_spinner_no_comparison(self): call_kwargs = self.navigator.navigate_until_text.call_args self.assertFalse( call_kwargs.kwargs.get( - 'screen_change_after_last_instruction', - call_kwargs[1].get('screen_change_after_last_instruction', True) - if len(call_kwargs) > 1 else True)) + "screen_change_after_last_instruction", + call_kwargs[1].get("screen_change_after_last_instruction", True) + if len(call_kwargs) > 1 + else True, + ) + ) # The backend should have been called with the spinner text self.backend.wait_for_text_on_screen.assert_called_once_with(spinner_text) @@ -71,8 +79,9 @@ def test_review_approve_with_spinner_no_comparison(self): def test_review_approve_with_spinner_touchable(self): """Test spinner behavior on a touchable device (Stax).""" device = Devices.get_by_type(DeviceType.STAX) - navigate_with_scenario = NavigateWithScenario(self.backend, self.navigator, device, - "test_name", self.directory) + navigate_with_scenario = NavigateWithScenario( + self.backend, self.navigator, device, "test_name", self.directory + ) spinner_text = "Please wait..." navigate_with_scenario.review_approve_with_spinner(spinner_text) @@ -81,9 +90,12 @@ def test_review_approve_with_spinner_touchable(self): call_kwargs = self.navigator.navigate_until_text_and_compare.call_args self.assertFalse( call_kwargs.kwargs.get( - 'screen_change_after_last_instruction', - call_kwargs[1].get('screen_change_after_last_instruction', True) - if len(call_kwargs) > 1 else True)) + "screen_change_after_last_instruction", + call_kwargs[1].get("screen_change_after_last_instruction", True) + if len(call_kwargs) > 1 + else True, + ) + ) # The backend should have been called with the spinner text self.backend.wait_for_text_on_screen.assert_called_once_with(spinner_text) diff --git a/tests/unit/navigator/test_navigator.py b/tests/unit/navigator/test_navigator.py index cddba273..2c10436f 100644 --- a/tests/unit/navigator/test_navigator.py +++ b/tests/unit/navigator/test_navigator.py @@ -9,7 +9,6 @@ class TestNavigator(TestCase): - def setUp(self): self.directory = TemporaryDirectory() self.backend = MagicMock() @@ -37,7 +36,9 @@ def test__get_snaps_dir_path(self): def test__checks_snaps_dir_path_ok_creates_dir(self): name = "some_name" expected = self.pathdir / "snapshots" / self.device.name / name - navigator = Navigator(self.backend, self.device, self.callbacks, golden_run=True) + navigator = Navigator( + self.backend, self.device, self.callbacks, golden_run=True + ) self.assertFalse(expected.exists()) result = navigator._check_snaps_dir_path(self.pathdir, name, True) self.assertEqual(result, expected) @@ -46,7 +47,9 @@ def test__checks_snaps_dir_path_ok_creates_dir(self): def test__checks_snaps_dir_path_ok_dir_exists(self): name = "some_name" expected = self.pathdir / "snapshots" / self.device.name / name - navigator = Navigator(self.backend, self.device, self.callbacks, golden_run=True) + navigator = Navigator( + self.backend, self.device, self.callbacks, golden_run=True + ) expected.mkdir(parents=True) self.assertTrue(expected.exists()) result = navigator._check_snaps_dir_path(self.pathdir, name, True) @@ -79,7 +82,9 @@ def test___init_snaps_temp_dir_ok_unlink_files(self): (expected / filename).touch() self.assertTrue((expected / filename).exists()) if start_idx: - result = self.navigator._init_snaps_temp_dir(self.pathdir, name, start_idx) + result = self.navigator._init_snaps_temp_dir( + self.pathdir, name, start_idx + ) else: result = self.navigator._init_snaps_temp_dir(self.pathdir, name) self.assertEqual(result, expected) @@ -90,24 +95,30 @@ def test___init_snaps_temp_dir_ok_unlink_files(self): def test__get_snap_path(self): path = Path("not important") testset = {1: "00001", 11: "00011", 111: "00111", 1111: "01111", 11111: "11111"} - for (index, name) in testset.items(): + for index, name in testset.items(): name += ".png" self.assertEqual(self.navigator._get_snap_path(path, index), path / name) def test__compare_snap_with_timeout_ok(self): self.navigator._backend.compare_screen_with_snapshot.side_effect = [False, True] self.assertTrue(self.navigator._compare_snap_with_timeout("not important", 1)) - self.assertEqual(self.navigator._backend.compare_screen_with_snapshot.call_count, 2) + self.assertEqual( + self.navigator._backend.compare_screen_with_snapshot.call_count, 2 + ) def test__compare_snap_with_timeout_ok_no_timeout(self): self.navigator._backend.compare_screen_with_snapshot.return_value = True self.assertTrue(self.navigator._compare_snap_with_timeout("not important", 0)) - self.assertEqual(self.navigator._backend.compare_screen_with_snapshot.call_count, 1) + self.assertEqual( + self.navigator._backend.compare_screen_with_snapshot.call_count, 1 + ) def test__compare_snap_with_timeout_nok(self): self.navigator._backend.compare_screen_with_snapshot.return_value = False self.assertFalse(self.navigator._compare_snap_with_timeout("not important", 0)) - self.assertEqual(self.navigator._backend.compare_screen_with_snapshot.call_count, 1) + self.assertEqual( + self.navigator._backend.compare_screen_with_snapshot.call_count, 1 + ) def test_compare_snap_ok(self): self.navigator._backend.compare_screen_with_snapshot.return_value = True @@ -137,7 +148,7 @@ def test__run_instructions_NavInsID(self): self.navigator._callbacks = {NavInsID.WAIT: cb_wait} self.assertIsNone(self.navigator._run_instruction(NavInsID.WAIT)) self.assertEqual(cb_wait.call_count, 1) - self.assertEqual(cb_wait.call_args, ((), )) + self.assertEqual(cb_wait.call_args, ((),)) def test__run_instructions_custom_instruction(self): @@ -148,7 +159,7 @@ class TestInsID(BaseNavInsID): self.navigator._callbacks = {TestInsID.WAIT: cb_wait} self.assertIsNone(self.navigator._run_instruction(TestInsID.WAIT)) self.assertEqual(cb_wait.call_count, 1) - self.assertEqual(cb_wait.call_args, ((), )) + self.assertEqual(cb_wait.call_args, ((),)) def test_navigate_nok_raises(self): with self.assertRaises(NotImplementedError): @@ -156,25 +167,32 @@ def test_navigate_nok_raises(self): def test_navigate_ok(self): cb_wait, cb1, cb2 = MagicMock(), MagicMock(), MagicMock() - ni1, ni2, ni3 = NavIns(1, (1, ), {'1': 1}), NavIns(2, (2, ), {'2': 2}), NavInsID.WAIT + ni1, ni2, ni3 = ( + NavIns(1, (1,), {"1": 1}), + NavIns(2, (2,), {"2": 2}), + NavInsID.WAIT, + ) self.navigator._callbacks = {NavInsID.WAIT: cb_wait, ni1.id: cb1, ni2.id: cb2} self.navigator.navigate([ni1, ni2, ni3]) self.assertEqual(cb_wait.call_count, 2) - self.assertEqual(cb_wait.call_args, ((), )) + self.assertEqual(cb_wait.call_args, ((),)) for cb, ni in [(cb1, ni1), (cb2, ni2)]: self.assertEqual(cb.call_count, 1) self.assertEqual(cb.call_args, (ni.args, ni.kwargs)) def test_navigate_and_compare_ok(self): cb_wait, cb1, cb2 = MagicMock(), MagicMock(), MagicMock() - ni1, ni2 = NavIns(1, (1, ), {'1': 1}), NavIns(2, (2, ), {'2': 2}) + ni1, ni2 = NavIns(1, (1,), {"1": 1}), NavIns(2, (2,), {"2": 2}) self.navigator._callbacks = {NavInsID.WAIT: cb_wait, ni1.id: cb1, ni2.id: cb2} self.navigator._backend = MagicMock(spec=SpeculosBackend) self.navigator._compare_snap = MagicMock() - self.navigator.navigate_and_compare(self.pathdir, - self.pathdir, [ni1, ni2], - screen_change_before_first_instruction=True, - screen_change_after_last_instruction=True) + self.navigator.navigate_and_compare( + self.pathdir, + self.pathdir, + [ni1, ni2], + screen_change_before_first_instruction=True, + screen_change_after_last_instruction=True, + ) # backend wait_for_screen_change function called 3 times self.assertEqual(self.navigator._backend.wait_for_screen_change.call_count, 3) @@ -190,54 +208,78 @@ def test_navigate_and_compare_ok(self): def test_navigate_until_text_and_compare_ok_no_snapshots(self): self.navigator._backend = MagicMock(spec=SpeculosBackend) - self.navigator._backend.compare_screen_with_text.side_effect = [False, False, True] + self.navigator._backend.compare_screen_with_text.side_effect = [ + False, + False, + True, + ] self.navigator._run_instruction = MagicMock() text = "some triggering text" cb_wait, cb1, cb2 = MagicMock(), MagicMock(), MagicMock() - ni1, ni2 = NavIns(1, (1, ), {'1': 1}), NavIns(2, (2, ), {'2': 2}) + ni1, ni2 = NavIns(1, (1,), {"1": 1}), NavIns(2, (2,), {"2": 2}) self.navigator._callbacks = {NavInsID.WAIT: cb_wait, ni1.id: cb1, ni2.id: cb2} self.navigator._compare_snap = MagicMock() - self.assertIsNone(self.navigator.navigate_until_text_and_compare(ni1, [ni2], text)) + self.assertIsNone( + self.navigator.navigate_until_text_and_compare(ni1, [ni2], text) + ) # no snapshot to check, so no call self.assertFalse(self.navigator._compare_snap.called) # backend compare function called 3 times with the text self.assertEqual(self.navigator._backend.compare_screen_with_text.call_count, 3) - self.assertEqual(self.navigator._backend.compare_screen_with_text.call_args_list, - [((text, ), )] * 3) + self.assertEqual( + self.navigator._backend.compare_screen_with_text.call_args_list, + [((text,),)] * 3, + ) # backend compare function return 2 time False, then True # so 2 calls with the navigate instruction, and the final one with the validation instruction self.assertEqual(self.navigator._run_instruction.call_count, 5) - self.assertEqual(self.navigator._run_instruction.call_args_list[0][0][0].id, NavInsID.WAIT) + self.assertEqual( + self.navigator._run_instruction.call_args_list[0][0][0].id, NavInsID.WAIT + ) self.assertEqual(self.navigator._run_instruction.call_args_list[1][0][0], ni1) self.assertEqual(self.navigator._run_instruction.call_args_list[2][0][0], ni1) - self.assertEqual(self.navigator._run_instruction.call_args_list[3][0][0].id, NavInsID.WAIT) + self.assertEqual( + self.navigator._run_instruction.call_args_list[3][0][0].id, NavInsID.WAIT + ) self.assertEqual(self.navigator._run_instruction.call_args_list[4][0][0], ni2) def test_navigate_until_text_and_compare_ok_with_snapshots(self): self.navigator._backend = MagicMock(spec=SpeculosBackend) - self.navigator._backend.compare_screen_with_text.side_effect = [False, False, True] + self.navigator._backend.compare_screen_with_text.side_effect = [ + False, + False, + True, + ] self.navigator._run_instruction = MagicMock() text = "some triggering text" cb_wait, cb1, cb2 = MagicMock(), MagicMock(), MagicMock() - ni1, ni2 = NavIns(1, (1, ), {'1': 1}), NavIns(2, (2, ), {'2': 2}) + ni1, ni2 = NavIns(1, (1,), {"1": 1}), NavIns(2, (2,), {"2": 2}) self.navigator._callbacks = {NavInsID.WAIT: cb_wait, ni1.id: cb1, ni2.id: cb2} self.navigator._compare_snap = MagicMock() self.assertIsNone( - self.navigator.navigate_until_text_and_compare(ni1, [ni2], text, self.pathdir, - self.pathdir)) + self.navigator.navigate_until_text_and_compare( + ni1, [ni2], text, self.pathdir, self.pathdir + ) + ) # backend compare function called 3 times with the text self.assertEqual(self.navigator._backend.compare_screen_with_text.call_count, 3) - self.assertEqual(self.navigator._backend.compare_screen_with_text.call_args_list, - [((text, ), )] * 3) + self.assertEqual( + self.navigator._backend.compare_screen_with_text.call_args_list, + [((text,),)] * 3, + ) # backend compare function return 2 time False, then True # so 2 calls with the navigate instruction, and the final one with the validation instruction self.assertEqual(self.navigator._run_instruction.call_count, 5) - self.assertEqual(self.navigator._run_instruction.call_args_list[0][0][0].id, NavInsID.WAIT) + self.assertEqual( + self.navigator._run_instruction.call_args_list[0][0][0].id, NavInsID.WAIT + ) self.assertEqual(self.navigator._run_instruction.call_args_list[1][0][0], ni1) self.assertEqual(self.navigator._run_instruction.call_args_list[2][0][0], ni1) - self.assertEqual(self.navigator._run_instruction.call_args_list[3][0][0].id, NavInsID.WAIT) + self.assertEqual( + self.navigator._run_instruction.call_args_list[3][0][0].id, NavInsID.WAIT + ) self.assertEqual(self.navigator._run_instruction.call_args_list[4][0][0], ni2) def test_navigate_until_text_and_compare_nok_timeout(self): @@ -245,19 +287,23 @@ def test_navigate_until_text_and_compare_nok_timeout(self): self.navigator._backend.compare_screen_with_text.return_value = False self.navigator.navigate = MagicMock() cb_wait, cb = MagicMock(), MagicMock() - ni = NavIns(1, (1, ), {'1': 1}) + ni = NavIns(1, (1,), {"1": 1}) self.navigator._callbacks = {NavInsID.WAIT: cb_wait, ni.id: cb} self.navigator._compare_snap = MagicMock() with self.assertRaises(TimeoutError): - self.navigator.navigate_until_text_and_compare(ni, [], "not important", timeout=0) + self.navigator.navigate_until_text_and_compare( + ni, [], "not important", timeout=0 + ) def test_navigate_until_snap_not_speculos(self): self.navigator._backend = MagicMock(spec=LedgerCommBackend) self.assertEqual( 0, - self.navigator.navigate_until_snap(NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", - "")) + self.navigator.navigate_until_snap( + NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", "" + ), + ) def test_navigate_until_snap_ok(self): self.navigator._backend = MagicMock(spec=SpeculosBackend) @@ -273,8 +319,10 @@ def test_navigate_until_snap_ok(self): self.navigator._compare_snap_with_timeout.side_effect = snapshot_comparisons self.assertEqual( expected_idx, - self.navigator.navigate_until_snap(NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", - "")) + self.navigator.navigate_until_snap( + NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", "" + ), + ) snapshot_comparisons = (True, False, False, True, False) # comparing first snapshot: True @@ -285,8 +333,10 @@ def test_navigate_until_snap_ok(self): self.navigator._compare_snap_with_timeout.side_effect = snapshot_comparisons self.assertEqual( expected_idx, - self.navigator.navigate_until_snap(NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", - "")) + self.navigator.navigate_until_snap( + NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", "" + ), + ) def test_navigate_until_snap_nok_timeout(self): self.navigator._backend = MagicMock(spec=SpeculosBackend) @@ -296,10 +346,6 @@ def test_navigate_until_snap_nok_timeout(self): self.navigator._compare_snap_with_timeout.return_value = True with patch("ragger.navigator.navigator.LAST_SCREEN_UPDATE_TIMEOUT", 0): with self.assertRaises(TimeoutError): - self.navigator.navigate_until_snap(NavInsID.WAIT, - NavInsID.WAIT, - Path(), - Path(), - "", - "", - timeout=0) + self.navigator.navigate_until_snap( + NavInsID.WAIT, NavInsID.WAIT, Path(), Path(), "", "", timeout=0 + ) diff --git a/tests/unit/navigator/test_touch_navigator.py b/tests/unit/navigator/test_touch_navigator.py index 70ed97bf..48401f65 100644 --- a/tests/unit/navigator/test_touch_navigator.py +++ b/tests/unit/navigator/test_touch_navigator.py @@ -7,10 +7,11 @@ class TestTouchNavigator(TestCase): - def test___init__ok(self): for backend_cls in [ - partial(SpeculosBackend, "some app"), LedgerCommBackend, LedgerWalletBackend + partial(SpeculosBackend, "some app"), + LedgerCommBackend, + LedgerWalletBackend, ]: backend = backend_cls(Devices.get_by_type(DeviceType.STAX)) TouchNavigator(backend, Devices.get_by_type(DeviceType.STAX)) diff --git a/tests/unit/test_error_ApplicationError.py b/tests/unit/test_error_ApplicationError.py index 61f59520..5eaf91d7 100644 --- a/tests/unit/test_error_ApplicationError.py +++ b/tests/unit/test_error_ApplicationError.py @@ -4,7 +4,6 @@ class TestExceptionRAPDU(TestCase): - def test___str__(self): status, data = 99, b"data" error = ExceptionRAPDU(status, data) diff --git a/tests/unit/utils/test_misc.py b/tests/unit/utils/test_misc.py index 59d99bfb..12c4c78e 100644 --- a/tests/unit/utils/test_misc.py +++ b/tests/unit/utils/test_misc.py @@ -11,11 +11,10 @@ class TestMisc(TestCase): - def test_find_application_ok_c(self): device, sdk = "device", "sdk" with temporary_directory() as dir_path: - tmp_dir = (dir_path / "build" / device / "bin") + tmp_dir = dir_path / "build" / device / "bin" tmp_dir.mkdir(parents=True, exist_ok=True) expected = tmp_dir / "app.elf" expected.touch() @@ -28,7 +27,7 @@ def test_find_application_ok_rust(self): cmd = ["cargo", "new", appname] subprocess.check_output(cmd, cwd=dir_path) app_path = dir_path / appname - tmp_dir = (app_path / "target" / device / "release") + tmp_dir = app_path / "target" / device / "release" tmp_dir.mkdir(parents=True, exist_ok=True) expected = tmp_dir / appname expected.touch() @@ -44,7 +43,7 @@ def test_find_application_nok_not_dir(self): def test_find_application_nok_not_file(self): device, sdk = "device", "sdk" with temporary_directory() as dir_path: - tmp_dir = (dir_path / "build" / device / "bin") + tmp_dir = dir_path / "build" / device / "bin" tmp_dir.mkdir(parents=True, exist_ok=True) expected = tmp_dir / "app.elf" with self.assertRaises(MissingElfError) as error: @@ -60,7 +59,8 @@ def test_find_project_root_dir_ok(self): os.mkdir(nested_dir) self.assertEqual( Path(dir_path).resolve(), - Path(misc.find_project_root_dir(nested_dir)).resolve()) + Path(misc.find_project_root_dir(nested_dir)).resolve(), + ) def test_find_project_root_dir_nok(self): with temporary_directory() as dir_path: @@ -85,8 +85,16 @@ def test_create_currency_config_full(self): ticker = "ticker" # size 6 name = "name" # size 4 subconfig = ("subconfig", 13) # size 9 + 1 - expected = b"\x06" + ticker.encode() + b"\x04" + name.encode() + b"\x0b" \ - + b"\x09" + subconfig[0].encode() + subconfig[1].to_bytes(1, byteorder="big") + expected = ( + b"\x06" + + ticker.encode() + + b"\x04" + + name.encode() + + b"\x0b" + + b"\x09" + + subconfig[0].encode() + + subconfig[1].to_bytes(1, byteorder="big") + ) self.assertEqual(expected, misc.create_currency_config(ticker, name, subconfig)) def test_split_message(self): @@ -101,10 +109,14 @@ def test_get_current_app_name_and_version_ok(self): # # # - backend.exchange().data = bytes.fromhex("01") \ - + len(name.encode()).to_bytes(1, "big") + name.encode() \ - + len(version.encode()).to_bytes(1, "big") + version.encode() \ + backend.exchange().data = ( + bytes.fromhex("01") + + len(name.encode()).to_bytes(1, "big") + + name.encode() + + len(version.encode()).to_bytes(1, "big") + + version.encode() + bytes.fromhex("0112") + ) result_name, result_version = misc.get_current_app_name_and_version(backend) self.assertEqual(name, result_name) self.assertEqual(version, result_version) diff --git a/tests/unit/utils/test_packing.py b/tests/unit/utils/test_packing.py index 1ddf3afb..502c3439 100644 --- a/tests/unit/utils/test_packing.py +++ b/tests/unit/utils/test_packing.py @@ -4,7 +4,6 @@ class TestPacking(TestCase): - def test_pack_APDU(self): cla, ins, p1, p2, data = 1, 2, 3, 4, b"data" expected = bytes.fromhex("0102030404") + data diff --git a/tests/unit/utils/test_path.py b/tests/unit/utils/test_path.py index dce508ca..e5a617b4 100644 --- a/tests/unit/utils/test_path.py +++ b/tests/unit/utils/test_path.py @@ -4,7 +4,6 @@ class TestStructsRAPDU(TestCase): - def test___str__data(self): status = 19 data = "012345" diff --git a/tests/unit/utils/test_structs.py b/tests/unit/utils/test_structs.py index 787e64e9..273bebad 100644 --- a/tests/unit/utils/test_structs.py +++ b/tests/unit/utils/test_structs.py @@ -4,9 +4,8 @@ class TestRAPDU(TestCase): - def test_raw(self): status = 0x9000 - data = bytes.fromhex('0123456789abcdef') - expected = data + bytes.fromhex('9000') + data = bytes.fromhex("0123456789abcdef") + expected = data + bytes.fromhex("9000") self.assertEqual(RAPDU(status, data).raw, expected)