.. _pytest-integration:
pytest Integration
==================
For building a large testsuite, it makes sense to leverage existing frameworks
for driving the tests and generating reports. One popular framework for Python
is `pytest `_. This guide shows how you can use tbot to
run hardware tests in pytest:
General Concept
---------------
The idea is to make the tbot context available as a fixture to pytest
testcases. These can then request machines as they need to interact with the
hardware.
To not make the test-runs excruciatingly slow, machines should be "kept alive"
between individual tests. This means, for example, that the board should not
be powered off after each test just for the next test to power it on again.
There is challenge here, though: When a test fails, the board probably
*should* be powercycled anyway, to make sure the following test will not be
affected by the previous test failure.
tbot's :py:class:`tbot.Context` has a mechanism to allow implementing exactly
that. You will see how it works below:
``conftest.py``
---------------
``conftest.py`` is pytest's configuration file. We need to define a fixture
for tbot's context here. We also need to provide a mechanism to allow
selecting the tbot configuration. This can be done by adding custom
command-line parameters and loading the configuration when pytest runs its
`configuration hook
`_.
All in all, this is a good skeleton to start from:
.. code-block:: python
import pytest
import tbot
from tbot import newbot
def pytest_addoption(parser, pluginmanager):
parser.addoption("--tbot-config", action="append", default=[], dest="tbot_configs")
parser.addoption("--tbot-flag", action="append", default=[], dest="tbot_flags")
def pytest_configure(config):
# Only register configuration when nobody else did so already.
if not tbot.ctx.is_active():
# Register flags
for tbot_flag in config.option.tbot_flags:
tbot.flags.add(tbot_flag)
# Register configurations
for tbot_config in config.option.tbot_configs:
newbot.load_config(tbot_config, tbot.ctx)
@pytest.fixture(scope="session", autouse=True)
def tbot_ctx(pytestconfig):
with tbot.ctx:
# Configure the context for keep_alive (so machines can be reused
# between tests). reset_on_error_by_default will make sure test
# failures lead to a powercycle of the DUT anyway.
with tbot.ctx.reconfigure(keep_alive=True, reset_on_error_by_default=True):
# Tweak the standard output logging options
with tbot.log.with_verbosity(tbot.log.Verbosity.STDOUT, nesting=1):
yield tbot.ctx
Testcases
---------
After writing the pytest config, you can start writing testcases. You should
probably read the `pytest Getting Started
`_ guide first if you
are not familiar with pytest.
Testcases can just use ``tbot.ctx`` like usual as the fixture will be activated
automatically (due to ``autouse=True``). Here is an example:
.. code-block:: python
# test_examples.py
import tbot
def test_encabulator():
with tbot.ctx.request(tbot.role.BoardLinux) as lnx:
lnx.exec0("systemctl", "status", "turbo-encabulator.service")
Now you are ready to run it! You have to translate your ``newbot`` commandline
parameters to ``pytest`` like this:
.. code-block:: shell-session
$ # This newbot commandline...
$ newbot -c config.my_lab -c config.board1 -f foo ...
$ # becomes this pytest commandline:
$ pytest --tbot-config config.my_lab --tbot-config config.board1 --tbot-flag foo ...
You can call pytest with ``-vs`` to see the tbot logging output during the
tests.
``keep_alive`` Notes
--------------------
As mentioned above, we use the ``keep_alive`` context mode to speed up the
test-run. This comes with a number of gotchas, though. You need to design
your testcases accordingly so the ``keep_alive`` mode does not lead to
problems.
- A testcase must leave all machines in the same state that it found them in.
If this is not possible, for example when running a crash test, the relevant
machines should be requested with ``exclusive=True`` to make sure the machine
is powercycled before the next testcase accesses it.
- Testcases by default must assume that the machine was already active before
they got it. If this is not wanted, the relevant machines should be
requested with ``reset=True`` to enforce a powercycle before the testcase
accesses the machine.
- If some machines may prevent requesting some other machine (like
``BoardLinux`` prevents ``BoardUBoot``), testcases requiring the prevented
one should use :py:meth:`~tbot.Context.teardown_if_alive` to deactivate the
offending machine first.
Here are a few examples of such testcases:
.. code-block:: python
import time
import tbot
from tbot.machine import linux
def test_watchdog_timeout():
with tbot.ctx.request(tbot.role.BoardLinux, exclusive=True) as lnx:
wdt = lnx.fsroot / "dev" / "watchdog0"
lnx.exec0("echo", "1", linux.RedirStdout(wdt))
# And now we expect the U-Boot header within 60 seconds
ch = lnx.ch.take()
with tbot.log.EventIO(
["board", "wdt-timeout"],
tbot.log.c("Waiting for the watchdog reset... ").bold,
verbosity=tbot.log.Verbosity.QUIET,
) as ev, ch.with_stream(ev):
ev.verbosity = tbot.log.Verbosity.STDOUT
ev.prefix = " <> "
ch.expect("U-Boot 2022.", timeout=60)
def test_uboot_can_echo():
tbot.ctx.teardown_if_alive(tbot.role.BoardLinux)
with tbot.ctx.request(tbot.role.BoardUBoot) as ub:
ub.exec0("echo", "Hello World")