Part 1
요약·해설
pytest-twister-harness의 자동 plugin 연결, testcase.yaml 발견, dut·shell·mcumgr fixture, API와 session scope·test 선택·재실행·병렬화 조건을 설명합니다.
Twister · Pytest · results.xml
dut · shell · mcumgr
scope · filter · rerun
Part 2
접을 수 있는 영어 원문 전체
영어 원문 전체 펼치기
D80381A199AB5CD8F16DEEB55FF58B0D0C6D4F734A7235620020C5AE2A57EB77
.. _integration_with_pytest:
Integration with pytest test framework
######################################
*Please mind that integration of twister with pytest is still work in progress. Not every platform
type is supported in pytest (yet). If you find any issue with the integration or have an idea for
an improvement, please, let us know about it and open a GitHub issue/enhancement.*
Introduction
************
Pytest is a python framework that *“makes it easy to write small, readable tests, and can scale to
support complex functional testing for applications and libraries”* (`<https://docs.pytest.org/en/7.3.x/>`_).
Python is known for its free libraries and ease of using it for scripting. In addition, pytest
utilizes the concept of plugins and fixtures, increasing its expendability and reusability.
A pytest plugin `pytest-twister-harness` was introduced to provide an integration between pytest
and twister, allowing Zephyr’s community to utilize pytest functionality with keeping twister as
the main framework.
Integration with twister
************************
By default, there is nothing to be done to enable pytest support in twister. The plugin is
developed as a part of Zephyr’s tree. To enable install-less operation, twister first extends
``PYTHONPATH`` with path to this plugin, and then during pytest call, it appends the command with
``-p twister_harness.plugin`` argument. If one prefers to use the installed version of the plugin,
they must add ``--allow-installed-plugin`` flag to twister’s call.
Pytest-based test suites are discovered the same way as other twister tests, i.e., by a presence
of test/sample.yaml. Inside, a keyword ``harness`` tells twister how to handle a given test.
In the case of ``harness: pytest``, most of twister workflow (test suites discovery,
parallelization, building and reporting) remains the same as for other harnesses. The change
happens during the execution step. The below picture presents a simplified overview of the
integration.
.. figure:: ../../../../develop/test/figures/twister_and_pytest.svg
:figclass: align-center
If ``harness: pytest`` is used, twister delegates the test execution to pytest, by calling it as
a subprocess. Required parameters (such as build directory, device to be used, etc.) are passed
through a CLI command. When pytest is done, twister looks for a pytest report (results.xml) and
sets the test result accordingly.
How to create a pytest test
***************************
An example folder containing a pytest test, application source code and Twister configuration .yaml
file can look like the following:
.. code-block:: none
test_foo/
├─── pytest/
│ └─── test_foo.py
├─── src/
│ └─── main.c
├─── CMakeList.txt
├─── prj.conf
└─── testcase.yaml
An example of a pytest test is given at
:zephyr_file:`samples/subsys/testsuite/pytest/shell/pytest/test_shell.py`. Using the configuration
provided in the ``testcase.yaml`` file, Twister builds the application from ``src`` and then, if the
.yaml file contains a ``harness: pytest`` entry, it calls pytest in a separate subprocess. A sample
configuration file may look like this:
.. code-block:: yaml
tests:
some.foo.test:
harness: pytest
tags: foo
By default, pytest tries to look for tests in a ``pytest`` directory located next to a directory
with binary sources. A keyword ``pytest_root`` placed under ``harness_config`` section in .yaml file
can be used to point to other files, directories or subtests (more info :ref:`here <pytest_root>`).
Pytest scans the given locations looking for tests, following its default
`discovery rules <https://docs.pytest.org/en/7.1.x/explanation/goodpractices.html#conventions-for-python-test-discovery>`_.
Passing extra arguments
=======================
There are two ways for passing extra arguments to the called pytest subprocess:
#. From .yaml file, using ``pytest_args`` placed under ``harness_config`` section - more info
:ref:`here <pytest_args>`.
#. Through Twister command line interface as ``--pytest-args`` argument. This can be particularly
useful when one wants to select a specific testcase from a test suite. For instance, one can use
a command:
.. code-block:: console
$ ./scripts/twister --platform native_sim -T samples/subsys/testsuite/pytest/shell \
-s samples/subsys/testsuite/pytest/shell/sample.pytest.shell \
--pytest-args='-k test_shell_print_version'
Fixtures
********
dut
===
Give access to a `DeviceAdapter`_ type object, that represents Device Under Test. This fixture is
the core of pytest harness plugin. It is required to launch DUT (initialize logging, flash device,
connect serial etc). This fixture yields a device prepared according to the requested type
(``native``, ``qemu``, ``hardware``, etc.). All types of devices share the same API. This allows for
writing tests which are device-type-agnostic. Scope of this fixture is determined by the
``pytest_dut_scope`` keyword placed under ``harness_config`` section (more info
:ref:`here <pytest_dut_scope>`).
.. code-block:: python
from twister_harness import DeviceAdapter
def test_sample(dut: DeviceAdapter):
dut.readlines_until('Hello world')
shell
=====
Provide a `Shell <shell_class_>`_ class object with methods used to interact with shell application.
It calls ``wait_for_promt`` method, to not start scenario until DUT is ready. The shell fixture
calls ``dut`` fixture, hence has access to all its methods. The ``shell`` fixture adds methods
optimized for interactions with a shell. It can be used instead of ``dut`` for tests. Scope of this
fixture is determined by the ``pytest_dut_scope`` keyword placed under ``harness_config`` section
(more info :ref:`here <pytest_dut_scope>`).
.. code-block:: python
from twister_harness import Shell
def test_shell(shell: Shell):
shell.exec_command('help')
mcumgr
======
Sample fixture to wrap ``mcumgr`` command-line tool used to manage remote devices. More information
about MCUmgr can be found here :ref:`mcu_mgr`.
.. note::
This fixture requires the ``mcumgr`` available in the system PATH
Only selected functionality of MCUmgr is wrapped by this fixture. For example, here is a test with
a fixture ``mcumgr``
.. code-block:: python
from twister_harness import DeviceAdapter, Shell, McuMgr
def test_upgrade(dut: DeviceAdapter, shell: Shell, mcumgr: McuMgr):
# free the serial port for mcumgr
dut.disconnect()
# upload the signed image
mcumgr.image_upload('path/to/zephyr.signed.bin')
# obtain the hash of uploaded image from the device
second_hash = mcumgr.get_hash_to_test()
# test a new upgrade image
mcumgr.image_test(second_hash)
# reset the device remotely
mcumgr.reset_device()
# continue test scenario, check version etc.
Classes
*******
DeviceAdapter
=============
.. autoclass:: twister_harness.DeviceAdapter
.. automethod:: launch
.. automethod:: connect
.. automethod:: readline
.. automethod:: readlines
.. automethod:: readlines_until
.. automethod:: write
.. automethod:: disconnect
.. automethod:: close
.. _shell_class:
Shell
=====
.. autoclass:: twister_harness.Shell
.. automethod:: exec_command
.. automethod:: wait_for_prompt
Examples of pytest tests in the Zephyr project
**********************************************
* :zephyr:code-sample:`pytest_shell`
* MCUmgr tests - :zephyr_file:`tests/boot/with_mcumgr`
* LwM2M tests - :zephyr_file:`tests/net/lib/lwm2m/interop`
* GDB stub tests - :zephyr_file:`tests/subsys/debug/gdbstub`
FAQ
***
How to flash/run application only once per pytest session?
==========================================================
``dut`` is a fixture responsible for flashing/running application. By default, its scope is set
as ``function``. This can be changed by adding to .yaml file ``pytest_dut_scope`` keyword placed
under ``harness_config`` section:
.. code-block:: yaml
harness: pytest
harness_config:
pytest_dut_scope: session
More info can be found :ref:`here <pytest_dut_scope>`.
How to run only one particular test from a python file?
=======================================================
This can be achieved in several ways. In .yaml file it can be added using a ``pytest_root`` entry
placed under ``harness_config`` with list of tests which should be run:
.. code-block:: yaml
harness: pytest
harness_config:
pytest_root:
- "pytest/test_shell.py::test_shell_print_help"
Particular tests can be also chosen by pytest ``-k`` option (more info about pytest keyword
filter can be found
`here <https://docs.pytest.org/en/latest/example/markers.html#using-k-expr-to-select-tests-based-on-their-name>`_
). It can be applied by adding ``-k`` filter in ``pytest_args`` in .yaml file:
.. code-block:: yaml
harness: pytest
harness_config:
pytest_args:
- "-k test_shell_print_help"
or by adding it to Twister command overriding parameters from the .yaml file:
.. code-block:: console
$ ./scripts/twister ... --pytest-args='-k test_shell_print_help'
How to get information about used device type in test?
======================================================
This can be taken from ``dut`` fixture (which represents `DeviceAdapter`_ object):
.. code-block:: python
device_type: str = dut.device_config.type
if device_type == 'hardware':
...
elif device_type == 'native':
...
How to rerun locally pytest tests without rebuilding application by Twister?
============================================================================
This can be achieved by running Twister once again with ``--test-only`` argument added to Twister
command. Another way is running Twister with highest verbosity level (``-vv``) and then
copy-pasting from logs command dedicated for spawning pytest (log started by ``Running pytest
command: ...``).
Is this possible to run pytest tests in parallel?
=================================================
Basically ``pytest-harness-plugin`` wasn't written with intention of running pytest tests in
parallel. Especially those one dedicated for hardware. There was assumption that parallelization
of tests is made by Twister, and it is responsible for managing available sources (jobs and
hardwares). If anyone is interested in doing this for some reasons (for example via
`pytest-xdist plugin <https://pytest-xdist.readthedocs.io/en/stable/>`_) they do so at their own
risk.
Limitations
***********
* Not every platform type is supported in the plugin (yet).
Part 3
한국어 전문 번역
Pytest test framework와 통합
소개
Pytest는 작고 읽기 쉬운 test를 간단히 작성할 수 있고, application과 library의 복잡한 functional test까지 확장할 수 있는 Python framework입니다.
Python은 자유롭게 사용할 수 있는 library가 많고 scripting이 쉽습니다. Pytest는 plugin과 fixture 개념을 사용해 확장성과 재사용성도 높입니다.
pytest-twister-harness plugin은 Pytest와 Twister를 연결합니다. Zephyr community가 Twister를 main framework로 계속 사용하면서 Pytest 기능을 활용할 수 있게 합니다.
Twister와의 통합
기본 설정에서는 Twister의 Pytest 지원을 따로 활성화할 필요가 없습니다. Plugin은 Zephyr source tree의 일부입니다. 설치하지 않고 실행할 수 있도록 Twister가 먼저 plugin path를 PYTHONPATH에 추가하고, Pytest를 호출할 때 command에 -p twister_harness.plugin argument를 붙입니다.
설치된 plugin version을 사용하려면 Twister command에 --allow-installed-plugin flag를 추가해야 합니다.
Pytest 기반 test suite도 다른 Twister test와 같은 방식으로 testcase.yaml 또는 sample.yaml 존재 여부를 통해 발견합니다. YAML의 harness keyword가 test 처리 방식을 Twister에 알려 줍니다.
harness: pytest이면 test suite 발견, 병렬화, build, report 등 Twister workflow 대부분은 다른 harness와 같습니다. 달라지는 지점은 실행 단계입니다.
harness: pytest를 사용하면 Twister가 Pytest를 subprocess로 호출해 test 실행을 위임합니다. Build directory, 사용할 device 같은 필수 parameter는 CLI command로 전달합니다. Pytest가 끝나면 Twister는 results.xml report를 찾아 그 내용에 맞게 test 결과를 설정합니다.
즉 Twister는 어떤 test를 어느 platform에서 build·배치할지와 최종 결과 취합을 계속 책임지고, Pytest는 넘겨받은 build와 device에서 Python test function을 실행합니다. 두 framework를 중복 실행하는 구조가 아니라 실행 단계만 분담하는 구조입니다.
Pytest test 만들기
Pytest test, application source code, Twister configuration YAML을 포함한 folder는 다음과 같이 구성할 수 있습니다.
test_foo/
├─── pytest/
│ └─── test_foo.py
├─── src/
│ └─── main.c
├─── CMakeList.txt
├─── prj.conf
└─── testcase.yaml실제 예제는 samples/subsys/testsuite/pytest/shell/pytest/test_shell.py에서 볼 수 있습니다.
testcase.yaml configuration에 따라 Twister가 src의 application을 build합니다. YAML에 harness: pytest가 있으면 별도 subprocess에서 Pytest를 호출합니다. Configuration 예:
tests:
some.foo.test:
harness: pytest
tags: foo기본적으로 Pytest는 binary source directory 옆의 pytest directory에서 test를 찾습니다. 다른 file, directory 또는 subtest를 지정하려면 YAML의 harness_config 아래에 pytest_root를 둡니다.
Pytest는 지정한 위치를 scan하며 기본 test 발견 규칙을 적용합니다.
추가 argument 전달
호출되는 Pytest subprocess에 추가 argument를 전달하는 방법은 두 가지입니다.
- YAML의
harness_config아래에pytest_args를 둡니다. - Twister command line에
--pytest-argsargument를 사용합니다. Test suite에서 특정 testcase만 선택할 때 특히 유용합니다.
예를 들어 다음 command는 이름이 일치하는 test만 선택합니다.
$ ./scripts/twister --platform native_sim -T samples/subsys/testsuite/pytest/shell \
-s samples/subsys/testsuite/pytest/shell/sample.pytest.shell \
--pytest-args='-k test_shell_print_version'Fixture
dut
dut fixture는 DUT(Device Under Test)를 나타내는 DeviceAdapter object를 제공합니다. Pytest harness plugin의 핵심 fixture이며 logging 초기화, device flash, serial 연결 등을 포함해 DUT를 실행하는 데 필요합니다.
native, qemu, hardware 등 요청한 type에 맞게 준비된 device를 yield합니다. 모든 device type이 같은 API를 공유하므로 test code가 device type에 종속되지 않게 작성할 수 있습니다.
Fixture scope는 YAML harness_config 아래의 pytest_dut_scope keyword로 정합니다.
from twister_harness import DeviceAdapter
def test_sample(dut: DeviceAdapter):
dut.readlines_until('Hello world')shell
shell fixture는 shell application과 상호 작용할 method를 가진 Shell object를 제공합니다. DUT가 준비되기 전에 scenario를 시작하지 않도록 prompt를 기다립니다. 원문의 이 설명에는 wait_for_promt로 표기되어 있지만 실제 class API 이름은 wait_for_prompt입니다.
shell은 dut fixture를 호출하므로 모든 DUT method에 접근할 수 있고, shell 상호 작용에 최적화된 method를 더합니다. Test에서 dut 대신 사용할 수 있으며 scope 역시 pytest_dut_scope로 정합니다.
from twister_harness import Shell
def test_shell(shell: Shell):
shell.exec_command('help')mcumgr
mcumgr fixture는 remote device를 관리하는 mcumgr command-line tool을 감쌉니다. MCUmgr의 배경과 protocol은 MCUmgr 문서를 참조하십시오.
이 fixture는 MCUmgr 기능 전체가 아니라 선택된 일부만 감쌉니다. 아래 test는 serial port를 해제하고 signed image를 upload한 뒤 hash를 얻어 image를 test하고 device를 원격 reset합니다.
from twister_harness import DeviceAdapter, Shell, McuMgr
def test_upgrade(dut: DeviceAdapter, shell: Shell, mcumgr: McuMgr):
# free the serial port for mcumgr
dut.disconnect()
# upload the signed image
mcumgr.image_upload('path/to/zephyr.signed.bin')
# obtain the hash of uploaded image from the device
second_hash = mcumgr.get_hash_to_test()
# test a new upgrade image
mcumgr.image_test(second_hash)
# reset the device remotely
mcumgr.reset_device()
# continue test scenario, check version etc.Class
DeviceAdapter
twister_harness.DeviceAdapter는 test에서 사용하는 hardware, simulator, QEMU를 하나의 API로 다루는 공통 interface입니다. 원문 autoclass와 automethod가 생성한 API 설명은 다음과 같습니다.
| API | 동작 |
|---|---|
DeviceAdapter(device_config: DeviceConfig) | Hardware, simulator, QEMU에서 device 출력을 수집하고 data를 보내는 공통 interface입니다. |
launch() -> None | 기존 application을 닫고 test application을 flash·실행한 뒤 device output을 수집하는 내부 reader thread를 시작합니다. |
connect(retry_s: int = 0) -> None | Device에 연결해 output 수집을 허용합니다. |
readline(timeout=None, print_output=True) -> str | Device output 한 줄을 읽습니다. Timeout이 없으면 base_timeout을 사용합니다. |
readlines(print_output=True) -> list[str] | 내부 buffer에 준비된 모든 output line을 읽습니다. |
readlines_until(regex=None, num_of_lines=None, timeout=None, print_output=True) -> list[str] | 내부 buffer의 device output을 조건에 맞을 때까지 읽습니다. regex를 주면 읽은 line에서 정규식이 발견될 때까지, num_of_lines를 주면 지정한 line 수에 도달할 때까지 읽으며 두 경우 모두 timeout이 상한입니다. 두 조건이 모두 없으면 지금까지 수집한 line을 즉시 반환합니다. Timeout을 생략하면 base_timeout을 사용합니다. |
write(data: bytes) -> None | Device에 byte data를 씁니다. |
disconnect() -> None | Device 연결을 끊어 output 수집을 막습니다. |
close() -> None | Device 연결과 device 자체를 닫고 reader thread를 종료합니다. |
Shell
twister_harness.Shell은 DeviceAdapter 위에서 shell command와 prompt 처리를 단순화합니다.
| API | 동작 |
|---|---|
Shell(device, prompt='uart:~$', timeout=None) | Shell application과 상호 작용하는 helper class입니다. |
exec_command(command, timeout=None, print_output=True) -> list[str] | 명령과 두 번의 Enter를 보내 실행한 뒤 다음 prompt가 나타날 때까지 기다리고 출력 결과를 반환합니다. |
wait_for_prompt(timeout=None) -> bool | 0.5초마다 Enter를 보내 prompt가 나타나면 True, timeout을 넘으면 False를 반환합니다. |
Zephyr project의 Pytest test 예제
- Pytest shell code sample
- MCUmgr test:
tests/boot/with_mcumgr - LwM2M test:
tests/net/lib/lwm2m/interop - GDB stub test:
tests/subsys/debug/gdbstub
FAQ
Pytest session마다 application을 한 번만 flash·실행하려면?
dut fixture가 application flash와 실행을 담당합니다. 기본 scope는 function입니다. YAML의 harness_config 아래에 pytest_dut_scope를 추가해 session으로 바꿀 수 있습니다.
harness: pytest
harness_config:
pytest_dut_scope: sessionPython file에서 특정 test 하나만 실행하려면?
pytest_root에 실행할 test 목록을 넣을 수 있습니다.
harness: pytest
harness_config:
pytest_root:
- "pytest/test_shell.py::test_shell_print_help"Pytest -k option으로도 특정 test를 고를 수 있습니다. 자세한 keyword filter 규칙은 Pytest marker 문서를 참조하십시오. YAML의 pytest_args에 filter를 넣는 예:
harness: pytest
harness_config:
pytest_args:
- "-k test_shell_print_help"또는 Twister command에서 YAML parameter를 덮어씁니다.
$ ./scripts/twister ... --pytest-args='-k test_shell_print_help'Test에서 사용 중인 device type을 확인하려면?
DeviceAdapter object를 나타내는 dut fixture의 device_config.type을 읽습니다.
device_type: str = dut.device_config.type
if device_type == 'hardware':
...
elif device_type == 'native':
...Twister가 application을 다시 build하지 않고 Pytest test만 로컬 재실행하려면?
Twister command에 --test-only를 추가해 다시 실행합니다. 다른 방법은 최고 verbosity인 -vv로 Twister를 실행하고 log에서 Running pytest command: ...로 시작하는 Pytest 전용 command를 복사해 직접 실행하는 것입니다.
Pytest test를 병렬 실행할 수 있나?
pytest-harness-plugin은 Pytest test 병렬 실행, 특히 hardware 대상 병렬 실행을 의도해 작성되지 않았습니다. Test 병렬화와 사용 가능한 job·hardware 관리는 Twister가 담당한다는 전제입니다.
pytest-xdist 같은 plugin으로 별도 병렬화를 시도할 수는 있지만, 사용자가 위험을 감수해야 합니다.
제한 사항
- 아직 plugin이 모든 platform type을 지원하지는 않습니다.
Source
출처
영어 원문에는 문단, 문서 지시문, 표, 코드, 코드 심벌, 경로를 그대로 보존했습니다.