Using and understanding the basis of Ragger testing capabilities
Ragger is mostly used as a testing framework for Ledger application. To ease
the integration in such scenario, it includes pre-defined pytest fixtures
and pre-made configuration. This section shows how to use them.
Note
On top of fixtures, options and other helpers brought by Ragger, the
tests written following this page guidance inherit every pytest base
mechanisms, CLI arguments and so on. Refer to its
documentation for further
information.
Fast testing: using the embedded ragger.conftest module
Before creating any file, let’s first create a directory dedicated to the tests.
$ mkdir -p tests
Ragger package contains a ragger.conftest module, including:
a base_conftest.py module, defining
pytestfixtures and CLI arguments,a configuration.py module, allowing to parametrize the previous fixtures.
As implied, Ragger relies on pytest to implement its testing
capabilities. Using these Ragger features hence requires a pytest setup.
In particular, pytest will fetch its configuration from a conftest.py
file, so in this case one needs to create such a file, and import Ragger
capabilities in it. A minimal conftest.py file would be:
1# This line includes the default `Ragger` configuration.
2# It can be modified to suit local needs
3from ragger.conftest import configuration
4
5# This line will be interpreted by `pytest` which will load the code from the
6# given modules, in this case `ragger.conftest.base_conftest`.
7# This module will define several fixtures, parametrized will the fields of
8# `configuration.OPTIONAL` variable.
9pytest_plugins = ("ragger.conftest.base_conftest", )
Now let’s write a very basic test which does not do much, but will at least allow to use our newly generated fixtures:
1# define the CLA of your application here
2CLA = 0xB0
3# define an instruction of your application here
4INS = 0x01
5
6def test_communicate(backend):
7 print(backend.exchange(cla=CLA, ins=INS))
This test will use the backend
fixture in order to run the application into Speculos. For that, we need
to access the application ELF file, which is expected to be stored in a specific
path. This path is in fact exactly where the default Ledger compilation
environment generates the ELF files, which is
<git root directory>/build/<device name>/bin/app.elf.
Let’s say we are going to run the test on nanos only. The file system should
at least be like:
$ tree .
.
├── build
│ └── nanos
│ └── bin
│ └── app.elf
└── tests
├── conftest.py
└── test_first.py
And now to run the tests:
$ pytest --device nanos tests/ -v
========================================= test session starts ===========================================
collected 1 item
tests/test_first.py::test_communicate[nanos 2.1] PASSED [100%]
=========================================== 1 passed in 0.80s ===========================================
What happened?
This very simple setup actually triggered some interesting events:
pytestautomatically loaded theragger.conftest.base_conftestmodule, and generated several fixtures to be used in following tests.one of these fixtures,
backendis configured with several parameters. We did not specified it in the command line, but its type here isSpeculosBackend(the default type).This backend exchanges with an application running into the Speculos emulator. For the fixture to automatically start this emulator, it needs to know what device it should emulates. That is where comes the
--device nanosparameter.The fixture also needs to access the application ELF. That’s why we have we stored it in
build/nanos/bin/app.elf.So when the
backendfixture is created, it knows it needs to start a NanoS simulator in which theapp.elfapplication file will be loaded.pytestfinally discovers and runs thetest_communicatetest.The test receives the
backendfixture, and uses it to exchange with the application running into the emulator. By default, thebackendis configured to raise if the application replies with an error. In our case, the test passed, so the emulated application responded with a success status.
Out-of-the-box pytest Ragger tools
The previous tutorial explained some feature Ragger brings for application
testing. But there is more!
CLI parameters
Ragger defines several parameters usable from the pytest CLI:
Controlling the backend (--backend)
It is possible to change the backend on which the tests should run through a CLI
argument --backend. Available backends are:
--backend speculos, using theSpeculosBackend(the default behavior),--backend ledgercomm, using theLedgerCommBackend,--backend ledgerwallet, using theledgerWalletBackend.
The two later options are physical backends, meaning they will try to connect to the application through the USB ports. So the application should be installed on a physical device, connected on the test computer through USB, and the application being started on the device, else the tests will not run.
Controlling the devices (--device)
Running the tests on specific device is automatically integrated with the
--device argument. Available devices are:
--device nanos,--device nanox,--device nanosp,--device stax,--device flex,--device apex_p,--device apex_m,--device all.
This last option can only work with the SpeculosBackend (as other backends rely on a physical device,
they can only run on the connected one), but is very convenient in a CI to
perform test campaign on all the devices.
Showing the device interface during test execution (--display)
Warning
Capability limited to the SpeculosBackend
With the SpeculosBackend, it is
possible to display the Qt graphical interface of the device, and so to follow
the actions and displayed screen during the test is executed.
This can be enabled with the --display CLI argument.
Controlling the seed (--seed)
Warning
Capability limited to the SpeculosBackend
Warning
Remember not to share your production seed. This option should be used only with testing, disposable seeds.
By default, the SpeculosBackend has
a fixed seed. It is possible to change its value with the --seed CLI argument.
Recording the screens (--golden_run)
Some tests using high-level Navigator methods comparing snapshots can also
turn these methods into a “record mode”: instead of comparing snapshots, they
will store the captured snapshots, with the --golden_run CLI argument.
This is convenient to automatically generate stock of golden snapshots.
Recording the APDUs (--log_apdu_file)
It can be useful to record all the APDU transmitted between the client and the
application during a test. the --log_apdu_file allows to specify a file
path in which every APDU and RAPDU will be recorded.
Measuring the stack consumption (--get-stack-consumption)
Warning
Capability limited to the SpeculosBackend, and requires the application to implement
the dedicated APDU (CLA=0xB0, INS=0x57).
With the --get-stack-consumption CLI argument, Ragger sends an APDU
after each test to measure the application stack consumption. The per-test
values, as well as a summary highlighting the worst case, are printed at the end
of the test session. If the stack consumption cannot be retrieved (for instance
when a test purposely quits Speculos), the test is not failed.
Producing firmware code coverage (--coverage)
Warning
Capability limited to the SpeculosBackend, and requires the application to be built
with debug symbols.
The --coverage CLI argument traces the application inside Speculos/QEMU and,
at the end of the session, emits an lcov firmware coverage file. Two companion
arguments tune the output:
--coverage_outputsets the output lcov file (defaultcoverage.info). With several devices, one file per device is written as<stem>-<device>.info. An HTML report (<stem>_html/) is also rendered ifgenhtmlis installed.--coverage_excludeexcludes matching source paths (repo-relative) from the coverage, for instance a vendored submodule:--coverage_exclude ethereum-plugin-sdk. It is repeatable, and each value may be a comma-separated list.
More details are available in the dedicated coverage chapter.
Fixtures and decorators
Ragger defines several fixtures and decorators to customize how the tests runs
or access runtime information:
Running a test only on a specific backend
Some tests should only run on a specific backend. Ragger defines a
pytest marker allowing to execute test only on the specified backend:
1import pytest
2
3CLA = 0xB0
4INS = 0x01
5
6# this will prevent this test from running,
7# except with the ``--backend ledgercomm`` argument
8@pytest.mark.use_on_backend("ledgercomm")
9def test_communicate(backend):
10 print(backend.exchange(cla=CLA, ins=INS))
$ pytest --device nanos --backend speculos tests/ -v
============================================ test session starts =============================================
collected 1 item
tests/test_first.py::test_communication[nanos 2.1] SKIPPED (skipped on this backend: "ledgercomm") [100%]
============================================= 1 skipped in 0.81s =============================================
Skipping a test on a specific backend
Conversely, the skip_on_backend marker prevents a test from running on a
given backend, while letting it run on all the others:
1import pytest
2
3CLA = 0xB0
4INS = 0x01
5
6# this will run on every backend, except with the ``--backend speculos`` argument
7@pytest.mark.skip_on_backend("speculos")
8def test_communicate(backend):
9 print(backend.exchange(cla=CLA, ins=INS))
Skipping a test on Nano devices
Some tests are only relevant for touchscreen devices (Stax, Flex, …). The
skip_nano marker skips them when running on a Nano device:
1import pytest
2
3# this test will be skipped when running with ``--device nanos``,
4# ``--device nanox`` or ``--device nanosp``
5@pytest.mark.skip_nano
6def test_touch_only(navigator):
7 ...
Getting the value of any CLI argument
Most argument defined by Ragger into pytest can be reached through a
fixture, and used into any test:
--backendis reachable with thebackend_namefixture,--displayis reachable with thedisplayfixture,--golden_runis reachable with thegolden_runfixture,--log_apdu_fileis reachable with thelog_apdu_filefixture,--seedis reachable with thebackend_cli_user_seedfixture,
--device is not immediately reachable through a fixture, but it can be found
with the backend fixture: backend.firmware.device.