1.. _integration_with_pytest:
2
3Integration with pytest test framework
4######################################
5
6*Please mind that integration of twister with pytest is still work in progress. Not every platform
7type is supported in pytest (yet). If you find any issue with the integration or have an idea for
8an improvement, please, let us know about it and open a GitHub issue/enhancement.*
9
10Introduction
11************
12
13Pytest is a python framework that *“makes it easy to write small, readable tests, and can scale to
14support complex functional testing for applications and libraries”* (`<https://docs.pytest.org/en/7.3.x/>`_).
15Python is known for its free libraries and ease of using it for scripting. In addition, pytest
16utilizes the concept of plugins and fixtures, increasing its expendability and reusability.
17A pytest plugin `pytest-twister-harness` was introduced to provide an integration between pytest
18and twister, allowing Zephyr’s community to utilize pytest functionality with keeping twister as
19the main framework.
20
21Integration with twister
22************************
23
24By default, there is nothing to be done to enable pytest support in twister. The plugin is
25developed as a part of Zephyr’s tree. To enable install-less operation, twister first extends
26``PYTHONPATH`` with path to this plugin, and then during pytest call, it appends the command with
27``-p twister_harness.plugin`` argument. If one prefers to use the installed version of the plugin,
28they must add ``--allow-installed-plugin`` flag to twister’s call.
29
30Pytest-based test suites are discovered the same way as other twister tests, i.e., by a presence
31of test/sample.yaml. Inside, a keyword ``harness`` tells twister how to handle a given test.
32In the case of ``harness: pytest``, most of twister workflow (test suites discovery,
33parallelization, building and reporting) remains the same as for other harnesses. The change
34happens during the execution step. The below picture presents a simplified overview of the
35integration.
36
37.. figure:: figures/twister_and_pytest.svg
38   :figclass: align-center
39
40
41If ``harness: pytest`` is used, twister delegates the test execution to pytest, by calling it as
42a subprocess. Required parameters (such as build directory, device to be used, etc.) are passed
43through a CLI command. When pytest is done, twister looks for a pytest report (results.xml) and
44sets the test result accordingly.
45
46How to create a pytest test
47***************************
48
49An example folder containing a pytest test, application source code and Twister configuration .yaml
50file can look like the following:
51
52.. code-block:: none
53
54   test_foo/
55   ├─── pytest/
56   │    └─── test_foo.py
57   ├─── src/
58   │    └─── main.c
59   ├─── CMakeList.txt
60   ├─── prj.conf
61   └─── testcase.yaml
62
63An example of a pytest test is given at
64:zephyr_file:`samples/subsys/testsuite/pytest/shell/pytest/test_shell.py`. Using the configuration
65provided in the ``testcase.yaml`` file, Twister builds the application from ``src`` and then, if the
66.yaml file contains a ``harness: pytest`` entry, it calls pytest in a separate subprocess. A sample
67configuration file may look like this:
68
69.. code-block:: yaml
70
71   tests:
72      some.foo.test:
73         harness: pytest
74         tags: foo
75
76By default, pytest tries to look for tests in a ``pytest`` directory located next to a directory
77with binary sources. A keyword ``pytest_root`` placed under ``harness_config`` section in .yaml file
78can be used to point to other files, directories or subtests (more info :ref:`here <pytest_root>`).
79
80Pytest scans the given locations looking for tests, following its default
81`discovery rules <https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#conventions-for-python-test-discovery>`_.
82
83Passing extra arguments
84=======================
85
86There are two ways for passing extra arguments to the called pytest subprocess:
87
88#. From .yaml file, using ``pytest_args`` placed under ``harness_config`` section - more info
89   :ref:`here <pytest_args>`.
90#. Through Twister command line interface as ``--pytest-args`` argument. This can be particularly
91   useful when one wants to select a specific testcase from a test suite. For instance, one can use
92   a command:
93
94   .. code-block:: console
95
96      $ ./scripts/twister --platform native_sim -T samples/subsys/testsuite/pytest/shell \
97      -s samples/subsys/testsuite/pytest/shell/sample.pytest.shell \
98      --pytest-args='-k test_shell_print_version'
99
100
101Fixtures
102********
103
104dut
105===
106
107Give access to a `DeviceAdapter`_ type object, that represents Device Under Test. This fixture is
108the core of pytest harness plugin. It is required to launch DUT (initialize logging, flash device,
109connect serial etc). This fixture yields a device prepared according to the requested type
110(``native``, ``qemu``, ``hardware``, etc.). All types of devices share the same API. This allows for
111writing tests which are device-type-agnostic. Scope of this fixture is determined by the
112``pytest_dut_scope`` keyword placed under ``harness_config`` section (more info
113:ref:`here <pytest_dut_scope>`).
114
115
116.. code-block:: python
117
118   from twister_harness import DeviceAdapter
119
120   def test_sample(dut: DeviceAdapter):
121      dut.readlines_until('Hello world')
122
123shell
124=====
125
126Provide a `Shell <shell_class_>`_ class object with methods used to interact with shell application.
127It calls ``wait_for_promt`` method, to not start scenario until DUT is ready. The shell fixture
128calls ``dut`` fixture, hence has access to all its methods. The ``shell`` fixture adds methods
129optimized for interactions with a shell. It can be used instead of ``dut`` for tests. Scope of this
130fixture is determined by the ``pytest_dut_scope`` keyword placed under ``harness_config`` section
131(more info :ref:`here <pytest_dut_scope>`).
132
133.. code-block:: python
134
135   from twister_harness import Shell
136
137   def test_shell(shell: Shell):
138      shell.exec_command('help')
139
140mcumgr
141======
142
143Sample fixture to wrap ``mcumgr`` command-line tool used to manage remote devices. More information
144about MCUmgr can be found here :ref:`mcu_mgr`.
145
146.. note::
147   This fixture requires the ``mcumgr`` available in the system PATH
148
149Only selected functionality of MCUmgr is wrapped by this fixture. For example, here is a test with
150a fixture ``mcumgr``
151
152.. code-block:: python
153
154   from twister_harness import DeviceAdapter, Shell, McuMgr
155
156   def test_upgrade(dut: DeviceAdapter, shell: Shell, mcumgr: McuMgr):
157      # free the serial port for mcumgr
158      dut.disconnect()
159      # upload the signed image
160      mcumgr.image_upload('path/to/zephyr.signed.bin')
161      # obtain the hash of uploaded image from the device
162      second_hash = mcumgr.get_hash_to_test()
163      # test a new upgrade image
164      mcumgr.image_test(second_hash)
165      # reset the device remotely
166      mcumgr.reset_device()
167      # continue test scenario, check version etc.
168
169Classes
170*******
171
172DeviceAdapter
173=============
174
175.. autoclass:: twister_harness.DeviceAdapter
176
177   .. automethod:: launch
178
179   .. automethod:: connect
180
181   .. automethod:: readline
182
183   .. automethod:: readlines
184
185   .. automethod:: readlines_until
186
187   .. automethod:: write
188
189   .. automethod:: disconnect
190
191   .. automethod:: close
192
193.. _shell_class:
194
195Shell
196=====
197
198.. autoclass:: twister_harness.Shell
199
200   .. automethod:: exec_command
201
202   .. automethod:: wait_for_prompt
203
204
205Examples of pytest tests in the Zephyr project
206**********************************************
207
208* :zephyr:code-sample:`pytest_shell`
209* MCUmgr tests - :zephyr_file:`tests/boot/with_mcumgr`
210* LwM2M tests - :zephyr_file:`tests/net/lib/lwm2m/interop`
211* GDB stub tests - :zephyr_file:`tests/subsys/debug/gdbstub`
212
213
214FAQ
215***
216
217How to flash/run application only once per pytest session?
218==========================================================
219
220   ``dut`` is a fixture responsible for flashing/running application. By default, its scope is set
221   as ``function``. This can be changed by adding to .yaml file ``pytest_dut_scope`` keyword placed
222   under ``harness_config`` section:
223
224   .. code-block:: yaml
225
226      harness: pytest
227      harness_config:
228         pytest_dut_scope: session
229
230   More info can be found :ref:`here <pytest_dut_scope>`.
231
232How to run only one particular test from a python file?
233=======================================================
234
235   This can be achieved in several ways. In .yaml file it can be added using a ``pytest_root`` entry
236   placed under ``harness_config`` with list of tests which should be run:
237
238   .. code-block:: yaml
239
240      harness: pytest
241      harness_config:
242         pytest_root:
243            - "pytest/test_shell.py::test_shell_print_help"
244
245   Particular tests can be also chosen by pytest ``-k`` option (more info about pytest keyword
246   filter can be found
247   `here <https://docs.pytest.org/en/latest/example/markers.html#using-k-expr-to-select-tests-based-on-their-name>`_
248   ). It can be applied by adding ``-k`` filter in ``pytest_args`` in .yaml file:
249
250   .. code-block:: yaml
251
252      harness: pytest
253      harness_config:
254         pytest_args:
255            - "-k test_shell_print_help"
256
257   or by adding it to Twister command overriding parameters from the .yaml file:
258
259   .. code-block:: console
260
261      $ ./scripts/twister ... --pytest-args='-k test_shell_print_help'
262
263How to get information about used device type in test?
264======================================================
265
266   This can be taken from ``dut`` fixture (which represents `DeviceAdapter`_ object):
267
268   .. code-block:: python
269
270      device_type: str = dut.device_config.type
271      if device_type == 'hardware':
272         ...
273      elif device_type == 'native':
274         ...
275
276How to rerun locally pytest tests without rebuilding application by Twister?
277============================================================================
278
279   This can be achieved by running Twister once again with ``--test-only`` argument added to Twister
280   command. Another way is running Twister with highest verbosity level (``-vv``) and then
281   copy-pasting from logs command dedicated for spawning pytest (log started by ``Running pytest
282   command: ...``).
283
284Is this possible to run pytest tests in parallel?
285=================================================
286
287   Basically ``pytest-harness-plugin`` wasn't written with intention of running pytest tests in
288   parallel. Especially those one dedicated for hardware. There was assumption that parallelization
289   of tests is made by Twister, and it is responsible for managing available sources (jobs and
290   hardwares). If anyone is interested in doing this for some reasons (for example via
291   `pytest-xdist plugin <https://pytest-xdist.readthedocs.io/en/stable/>`_) they do so at their own
292   risk.
293
294
295Limitations
296***********
297
298* Not every platform type is supported in the plugin (yet).
299