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