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   The command line arguments will extend those from the .yaml file. If the same argument is
101   present in both places, the one from the command line will take precedence.
102
103Fixtures
104********
105
106dut
107===
108
109Give access to a `DeviceAdapter`_ type object, that represents Device Under Test. This fixture is
110the core of pytest harness plugin. It is required to launch DUT (initialize logging, flash device,
111connect serial etc). This fixture yields a device prepared according to the requested type
112(``native``, ``qemu``, ``hardware``, etc.). All types of devices share the same API. This allows for
113writing tests which are device-type-agnostic. Scope of this fixture is determined by the
114``pytest_dut_scope`` keyword placed under ``harness_config`` section (more info
115:ref:`here <pytest_dut_scope>`).
116
117
118.. code-block:: python
119
120   from twister_harness import DeviceAdapter
121
122   def test_sample(dut: DeviceAdapter):
123      dut.readlines_until('Hello world')
124
125shell
126=====
127
128Provide a `Shell <shell_class_>`_ class object with methods used to interact with shell application.
129It calls ``wait_for_promt`` method, to not start scenario until DUT is ready. The shell fixture
130calls ``dut`` fixture, hence has access to all its methods. The ``shell`` fixture adds methods
131optimized for interactions with a shell. It can be used instead of ``dut`` for tests. Scope of this
132fixture is determined by the ``pytest_dut_scope`` keyword placed under ``harness_config`` section
133(more info :ref:`here <pytest_dut_scope>`).
134
135.. code-block:: python
136
137   from twister_harness import Shell
138
139   def test_shell(shell: Shell):
140      shell.exec_command('help')
141
142mcumgr
143======
144
145Sample fixture to wrap ``mcumgr`` command-line tool used to manage remote devices. More information
146about MCUmgr can be found here :ref:`mcu_mgr`.
147
148.. note::
149   This fixture requires the ``mcumgr`` available in the system PATH
150
151Only selected functionality of MCUmgr is wrapped by this fixture. For example, here is a test with
152a fixture ``mcumgr``
153
154.. code-block:: python
155
156   from twister_harness import DeviceAdapter, Shell, McuMgr
157
158   def test_upgrade(dut: DeviceAdapter, shell: Shell, mcumgr: McuMgr):
159      # free the serial port for mcumgr
160      dut.disconnect()
161      # upload the signed image
162      mcumgr.image_upload('path/to/zephyr.signed.bin')
163      # obtain the hash of uploaded image from the device
164      second_hash = mcumgr.get_hash_to_test()
165      # test a new upgrade image
166      mcumgr.image_test(second_hash)
167      # reset the device remotely
168      mcumgr.reset_device()
169      # continue test scenario, check version etc.
170
171
172unlaunched_dut
173==============
174
175Similar to the ``dut`` fixture, but it does not initialize the device. It can be used when a finer
176control over the build process is needed. It becomes responsibility of the test to initialize the
177device.
178
179.. code-block:: python
180
181   from twister_harness import DeviceAdapter
182
183   def test_sample(unlaunched_dut: DeviceAdapter):
184      unlaunched_dut.launch()
185      unlaunched_dut.readlines_until('Hello world')
186
187Classes
188*******
189
190DeviceAdapter
191=============
192
193.. autoclass:: twister_harness.DeviceAdapter
194
195   .. automethod:: launch
196
197   .. automethod:: connect
198
199   .. automethod:: readline
200
201   .. automethod:: readlines
202
203   .. automethod:: readlines_until
204
205   .. automethod:: write
206
207   .. automethod:: disconnect
208
209   .. automethod:: close
210
211.. _shell_class:
212
213Shell
214=====
215
216.. autoclass:: twister_harness.Shell
217
218   .. automethod:: exec_command
219
220   .. automethod:: wait_for_prompt
221
222   .. automethod:: get_filtered_output
223
224
225Examples of pytest tests in the Zephyr project
226**********************************************
227
228* :zephyr:code-sample:`pytest_shell`
229* MCUmgr tests - :zephyr_file:`tests/boot/with_mcumgr`
230* LwM2M tests - :zephyr_file:`tests/net/lib/lwm2m/interop`
231* GDB stub tests - :zephyr_file:`tests/subsys/debug/gdbstub`
232
233
234FAQ
235***
236
237How to flash/run application only once per pytest session?
238==========================================================
239
240   ``dut`` is a fixture responsible for flashing/running application. By default, its scope is set
241   as ``function``. This can be changed by adding to .yaml file ``pytest_dut_scope`` keyword placed
242   under ``harness_config`` section:
243
244   .. code-block:: yaml
245
246      harness: pytest
247      harness_config:
248         pytest_dut_scope: session
249
250   More info can be found :ref:`here <pytest_dut_scope>`.
251
252How to run only one particular test from a python file?
253=======================================================
254
255   This can be achieved in several ways. In .yaml file it can be added using a ``pytest_root`` entry
256   placed under ``harness_config`` with list of tests which should be run:
257
258   .. code-block:: yaml
259
260      harness: pytest
261      harness_config:
262         pytest_root:
263            - "pytest/test_shell.py::test_shell_print_help"
264
265   Particular tests can be also chosen by pytest ``-k`` option (more info about pytest keyword
266   filter can be found
267   `here <https://docs.pytest.org/en/latest/example/markers.html#using-k-expr-to-select-tests-based-on-their-name>`_
268   ). It can be applied by adding ``-k`` filter in ``pytest_args`` in .yaml file:
269
270   .. code-block:: yaml
271
272      harness: pytest
273      harness_config:
274         pytest_args:
275            - "-k test_shell_print_help"
276
277   or by adding it to Twister command overriding parameters from the .yaml file:
278
279   .. code-block:: console
280
281      $ ./scripts/twister ... --pytest-args='-k test_shell_print_help'
282
283How to get information about used device type in test?
284======================================================
285
286   This can be taken from ``dut`` fixture (which represents `DeviceAdapter`_ object):
287
288   .. code-block:: python
289
290      device_type: str = dut.device_config.type
291      if device_type == 'hardware':
292         ...
293      elif device_type == 'native':
294         ...
295
296How to rerun locally pytest tests without rebuilding application by Twister?
297============================================================================
298
299   This can be achieved by running Twister once again with ``--test-only`` argument added to Twister
300   command. Another way is running Twister with highest verbosity level (``-vv``) and then
301   copy-pasting from logs command dedicated for spawning pytest (log started by ``Running pytest
302   command: ...``).
303
304Is this possible to run pytest tests in parallel?
305=================================================
306
307   Basically ``pytest-harness-plugin`` wasn't written with intention of running pytest tests in
308   parallel. Especially those one dedicated for hardware. There was assumption that parallelization
309   of tests is made by Twister, and it is responsible for managing available sources (jobs and
310   hardwares). If anyone is interested in doing this for some reasons (for example via
311   `pytest-xdist plugin <https://pytest-xdist.readthedocs.io/en/stable/>`_) they do so at their own
312   risk.
313
314
315Limitations
316***********
317
318* Not every platform type is supported in the plugin (yet).
319