Skip to content

Commit

Permalink
[tests] Improving unit test coverage
Browse files Browse the repository at this point in the history
  • Loading branch information
lpascal-ledger committed Jan 31, 2024
1 parent 2199149 commit 0e5a4f9
Show file tree
Hide file tree
Showing 8 changed files with 171 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/ragger/navigator/nano_navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
class NanoNavigator(Navigator):

def __init__(self, backend: BackendInterface, firmware: Firmware, golden_run: bool = False):
if firmware == Firmware.STAX:
raise ValueError(f"'{self.__class__.__name__}' does not work on Stax")
callbacks: Dict[NavInsID, Callable] = {
NavInsID.WAIT: sleep,
NavInsID.WAIT_FOR_SCREEN_CHANGE: backend.wait_for_screen_change,
Expand Down
5 changes: 3 additions & 2 deletions src/ragger/navigator/navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@

from .instruction import NavIns, NavInsID

LAST_SCREEN_UPDATE_TIMEOUT = 2


class Navigator(ABC):

Expand Down Expand Up @@ -418,10 +420,9 @@ def navigate_until_snap(self,

# Make sure there is a screen update after the final action.
start = time()
last_screen_update_timeout = 2
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}"
)
Expand Down
2 changes: 2 additions & 0 deletions src/ragger/navigator/stax_navigator.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
class StaxNavigator(Navigator):

def __init__(self, backend: BackendInterface, firmware: Firmware, golden_run: bool = False):
if not firmware == Firmware.STAX:
raise ValueError(f"'{self.__class__.__name__}' only works on Stax")
screen = FullScreen(backend, firmware)
callbacks: Dict[NavInsID, Callable] = {
NavInsID.WAIT: sleep,
Expand Down
14 changes: 10 additions & 4 deletions src/ragger/utils/misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
from pathlib import Path
from ragger.error import ExceptionRAPDU

ERROR_BOLOS_DEVICE_LOCKED = 0x5515
ERROR_DENIED_BY_USER = 0x5501
ERROR_APP_NOT_FOUND = 0x6807

ERROR_MSG_DEVICE_LOCKED = "Your device is locked"


def find_library_application(base_dir: Path, name: str, device: str) -> Path:
"""
Expand Down Expand Up @@ -117,8 +123,8 @@ def get_current_app_name_and_version(backend):

return app_name, version
except ExceptionRAPDU as e:
if e.status == 0x5515:
raise ValueError("Your device is locked")
if e.status == ERROR_BOLOS_DEVICE_LOCKED:
raise ValueError(ERROR_MSG_DEVICE_LOCKED)
raise e


Expand All @@ -139,8 +145,8 @@ def open_app_from_dashboard(backend, app_name: str):
p2=0,
data=app_name.encode())
except ExceptionRAPDU as e:
if e.status == 0x5501:
if e.status == ERROR_DENIED_BY_USER:
raise ValueError("Open app consent denied by the user")
elif e.status == 0x6807:
elif e.status == ERROR_APP_NOT_FOUND:
raise ValueError(f"App '{app_name} is not present")
raise e
18 changes: 18 additions & 0 deletions tests/unit/navigator/test_nano_navigator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from functools import partial
from unittest import TestCase

from ragger.navigator.nano_navigator import NanoNavigator
from ragger.backend import LedgerCommBackend, LedgerWalletBackend, SpeculosBackend
from ragger.firmware import Firmware


class TestNanoNavigator(TestCase):

def test___init__ok(self):
for backend_cls in [partial(SpeculosBackend, "some app"), LedgerCommBackend, LedgerWalletBackend]:
backend = backend_cls(Firmware.NANOS)
navigator = NanoNavigator(backend, Firmware.NANOS)

Check notice

Code scanning / CodeQL

Unused local variable Note test

Variable navigator is not used.

def test___init__nok(self):
with self.assertRaises(ValueError):
NanoNavigator("whatever", Firmware.STAX)
41 changes: 39 additions & 2 deletions tests/unit/navigator/test_navigator.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from pathlib import Path
from tempfile import TemporaryDirectory
from unittest import TestCase
from unittest.mock import MagicMock
from unittest.mock import MagicMock, patch

from ragger.backend import SpeculosBackend
from ragger.backend import SpeculosBackend, LedgerCommBackend
from ragger.firmware import Firmware
from ragger.navigator import Navigator, NavIns, NavInsID

Expand Down Expand Up @@ -219,3 +219,40 @@ def test_navigate_until_text_and_compare_nok_timeout(self):

with self.assertRaises(TimeoutError):
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(), "", ""))

def test_navigate_until_snap_ok(self):
self.navigator._backend = MagicMock(spec=SpeculosBackend)
self.navigator._check_snaps_dir_path = MagicMock()
self.navigator._run_instruction = MagicMock()
self.navigator._compare_snap_with_timeout = MagicMock()
snapshot_comparisons = (True, True, False)
# comparing first snapshot: True
# then comparing snapshots until given: True (i.e first snapshot is the expected one)
# then waiting for a screen change: False (screen changed)
expected_idx = 0
# as there is no snapshot between the first image and the last snapshot, the index is 0
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(), "", ""))

snapshot_comparisons = (True, False, False, True, False)
# comparing first snapshot: True
# then comparing snapshots until given: False, False, True (i.e first two snapshots did not match, but the third is the expected one)
# then waiting for a screen change: False (screen changed)
expected_idx = 2
# as there is 2 snapshots between the first image and the last snapshot, the index is 0
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(), "", ""))

def test_navigate_until_snap_nok_timeout(self):
self.navigator._backend = MagicMock(spec=SpeculosBackend)
self.navigator._check_snaps_dir_path = MagicMock()
self.navigator._run_instruction = MagicMock()
self.navigator._compare_snap_with_timeout = MagicMock()
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)
18 changes: 18 additions & 0 deletions tests/unit/navigator/test_stax_navigator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from functools import partial
from unittest import TestCase

from ragger.navigator.stax_navigator import StaxNavigator
from ragger.backend import LedgerCommBackend, LedgerWalletBackend, SpeculosBackend
from ragger.firmware import Firmware


class TestStaxNavigator(TestCase):

def test___init__ok(self):
for backend_cls in [partial(SpeculosBackend, "some app"), LedgerCommBackend, LedgerWalletBackend]:
backend = backend_cls(Firmware.STAX)
navigator = StaxNavigator(backend, Firmware.STAX)

Check notice

Code scanning / CodeQL

Unused local variable Note test

Variable navigator is not used.

def test___init__nok(self):
with self.assertRaises(ValueError):
StaxNavigator("whatever", Firmware.NANOS)
79 changes: 79 additions & 0 deletions tests/unit/utils/test_misc.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
from pathlib import Path
from tempfile import mkdtemp
from unittest import TestCase
from unittest.mock import MagicMock

from ragger.error import ExceptionRAPDU
from ragger.utils import misc


Expand Down Expand Up @@ -40,6 +43,32 @@ def test_app_path_from_app_name_nok_no_file(self):
with self.assertRaises(AssertionError):
misc.find_library_application(dir_path, "a", "b")

def test_find_main_application_ok(self):
device = "device"
with self.directory() as dir_path:
temp_dir = (dir_path / "build" / device / "bin")
temp_dir.mkdir(parents=True, exist_ok=True)
expected = temp_dir / "app.elf"
expected.touch()
result = misc.find_main_application(dir_path, device)
self.assertEqual(result, expected)

def test_find_main_application_nok_not_dir(self):
directory, device = Path("does not exist"), "device"
with self.assertRaises(AssertionError) as error:
misc.find_main_application(directory, device)
self.assertIn(str(directory), str(error.exception))

def test_find_main_application_nok_not_file(self):
device = "device"
with self.directory() as dir_path:
temp_dir = (dir_path / "build" / device / "bin")
temp_dir.mkdir(parents=True, exist_ok=True)
expected = temp_dir / "app.elf"
with self.assertRaises(AssertionError) as error:
misc.find_main_application(dir_path, device)
self.assertIn(str(expected), str(error.exception))

def test_find_project_root_dir_ok(self):
with self.directory() as dir_path:
os.mkdir(Path(dir_path / ".git").resolve())
Expand Down Expand Up @@ -82,3 +111,53 @@ def test_split_message(self):
message = b"some message to split"
expected = [b"some ", b"messa", b"ge to", b" spli", b"t"]
self.assertEqual(expected, misc.split_message(message, 5))

def test_get_current_app_name_and_version_ok(self):
name, version = "appname", "12.345.6789"
backend = MagicMock()
# format (=1)
# <l1v name>
# <l1v version>
# <l1v flags>
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)

def test_get_current_app_name_and_version_nok(self):
error_status, backend = 0x1234, MagicMock()
backend.exchange.side_effect = ExceptionRAPDU(error_status)
with self.assertRaises(ExceptionRAPDU) as error:
misc.get_current_app_name_and_version(backend)
self.assertEqual(error.exception.status, error_status)
# specific behavior: device locked
backend.exchange.side_effect = ExceptionRAPDU(misc.ERROR_BOLOS_DEVICE_LOCKED)
with self.assertRaises(ValueError) as error:
misc.get_current_app_name_and_version(backend)
self.assertEqual(misc.ERROR_MSG_DEVICE_LOCKED, str(error.exception))

def test_exit_current_app(self):
backend = MagicMock()
self.assertIsNone(misc.exit_current_app(backend))

Check warning

Code scanning / CodeQL

Use of the return value of a procedure Warning test

The result of
exit_current_app
is used even though it is always None.

def test_open_app_from_dashboard_ok(self):
backend = MagicMock()
self.assertIsNone(misc.open_app_from_dashboard(backend, "some app"))

Check warning

Code scanning / CodeQL

Use of the return value of a procedure Warning test

The result of
open_app_from_dashboard
is used even though it is always None.

def test_open_app_from_dashboard_nok(self):
error_status, backend = 0x1234, MagicMock()
backend.exchange.side_effect = ExceptionRAPDU(error_status)
with self.assertRaises(ExceptionRAPDU) as error:
misc.open_app_from_dashboard(backend, "some app")
self.assertEqual(error.exception.status, error_status)
# specific behavior: user refuses
backend.exchange.side_effect = ExceptionRAPDU(misc.ERROR_DENIED_BY_USER)
with self.assertRaises(ValueError) as error:
misc.open_app_from_dashboard(backend, "some app")
# specific behavior: app not installed
backend.exchange.side_effect = ExceptionRAPDU(misc.ERROR_APP_NOT_FOUND)
with self.assertRaises(ValueError) as error:
misc.open_app_from_dashboard(backend, "some app")

0 comments on commit 0e5a4f9

Please sign in to comment.