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