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(regex='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(regex='Hello world') 186 187Classes 188******* 189 190DeviceAdapter 191============= 192 193.. autoclass:: twister_harness.DeviceAdapter 194 195 .. automethod:: launch 196 197 .. automethod:: close 198 199 .. automethod:: readline 200 201 .. automethod:: readlines 202 203 .. automethod:: readlines_until 204 205 .. automethod:: write 206 207.. _shell_class: 208 209Shell 210===== 211 212.. autoclass:: twister_harness.Shell 213 214 .. automethod:: exec_command 215 216 .. automethod:: wait_for_prompt 217 218 .. automethod:: get_filtered_output 219 220 221Examples of pytest tests in the Zephyr project 222********************************************** 223 224* :zephyr:code-sample:`pytest_shell` 225* MCUmgr tests - :zephyr_file:`tests/boot/with_mcumgr` 226* LwM2M tests - :zephyr_file:`tests/net/lib/lwm2m/interop` 227* GDB stub tests - :zephyr_file:`tests/subsys/debug/gdbstub` 228 229 230FAQ 231*** 232 233How to flash/run application only once per pytest session? 234========================================================== 235 236 ``dut`` is a fixture responsible for flashing/running application. By default, its scope is set 237 as ``function``. This can be changed by adding to .yaml file ``pytest_dut_scope`` keyword placed 238 under ``harness_config`` section: 239 240 .. code-block:: yaml 241 242 harness: pytest 243 harness_config: 244 pytest_dut_scope: session 245 246 More info can be found :ref:`here <pytest_dut_scope>`. 247 248How to run only one particular test from a python file? 249======================================================= 250 251 This can be achieved in several ways. In .yaml file it can be added using a ``pytest_root`` entry 252 placed under ``harness_config`` with list of tests which should be run: 253 254 .. code-block:: yaml 255 256 harness: pytest 257 harness_config: 258 pytest_root: 259 - "pytest/test_shell.py::test_shell_print_help" 260 261 Particular tests can be also chosen by pytest ``-k`` option (more info about pytest keyword 262 filter can be found 263 `here <https://docs.pytest.org/en/latest/example/markers.html#using-k-expr-to-select-tests-based-on-their-name>`_ 264 ). It can be applied by adding ``-k`` filter in ``pytest_args`` in .yaml file: 265 266 .. code-block:: yaml 267 268 harness: pytest 269 harness_config: 270 pytest_args: 271 - "-k test_shell_print_help" 272 273 or by adding it to Twister command overriding parameters from the .yaml file: 274 275 .. code-block:: console 276 277 $ ./scripts/twister ... --pytest-args='-k test_shell_print_help' 278 279How to get information about used device type in test? 280====================================================== 281 282 This can be taken from ``dut`` fixture (which represents `DeviceAdapter`_ object): 283 284 .. code-block:: python 285 286 device_type: str = dut.device_config.type 287 if device_type == 'hardware': 288 ... 289 elif device_type == 'native': 290 ... 291 292How to rerun locally pytest tests without rebuilding application by Twister? 293============================================================================ 294 295 This can be achieved by running Twister once again with ``--test-only`` argument added to Twister 296 command. Another way is running Twister with highest verbosity level (``-vv``) and then 297 copy-pasting from logs command dedicated for spawning pytest (log started by ``Running pytest 298 command: ...``). 299 300Is this possible to run pytest tests in parallel? 301================================================= 302 303 Basically ``pytest-harness-plugin`` wasn't written with intention of running pytest tests in 304 parallel. Especially those one dedicated for hardware. There was assumption that parallelization 305 of tests is made by Twister, and it is responsible for managing available sources (jobs and 306 hardwares). If anyone is interested in doing this for some reasons (for example via 307 `pytest-xdist plugin <https://pytest-xdist.readthedocs.io/en/stable/>`_) they do so at their own 308 risk. 309 310 311Limitations 312*********** 313 314* Not every platform type is supported in the plugin (yet). 315