与 pytest 测试框架集成

请注意,Twister 与 pytest 的集成仍在开发中,尚不支持所有平台类型。如果发现问题或有改进建议,请创建 GitHub 问题或增强请求。

简介

Pytest 是 Python 框架,使小型、易读的测试易于编写,也能扩展以支持应用和库的复杂功能测试 (参见 pytest 文档)。Python 拥有丰富的免费库,也便于编写脚本。pytest 通过插件和 fixture 提升扩展性及复用性。pytest-twister-harness 插件将 pytest 与 Twister 集成,使 Zephyr 社区在保留 Twister 为主框架的同时使用 pytest 功能。

与 Twister 集成

默认无需额外操作即可在 Twister 中使用 pytest。插件在 Zephyr 代码树内开发;为实现免安装运行,Twister 先将插件路径加入 PYTHONPATH,再在 pytest 命令中添加 -p twister_harness.plugin。若希望使用已安装版本,应给 Twister 添加 --allow-installed-plugin。

基于 pytest 的套件与其他测试一样,通过 tests.yaml 文件发现。其中 harness 键决定 Twister 如何处理测试。使用 harness: pytest 时,套件发现、并行化、构建和报告等流程保持不变,仅执行阶段不同。下图展示集成的简化概览。

../../_images/twister_and_pytest.svg

使用 harness: pytest 时,Twister 以子进程调用 pytest,将测试执行交给它。构建目录、设备等参数通过 YAML 配置传入。pytest 完成后,Twister 查找其报告 results.xml,并据此设置测试结果。

如何创建 pytest 测试

包含 pytest 测试、应用源代码和 Twister YAML 配置的目录示例如下:

test_foo/
├─── pytest/
│    └─── test_foo.py
├─── src/
│    └─── main.c
├─── CMakeList.txt
├─── prj.conf
└─── testcase.yaml

pytest 测试示例见 samples/subsys/testsuite/pytest/shell/pytest/test_shell.py。Twister 根据 testcase.yaml 配置构建 src 中的应用;若 YAML 中有 harness: pytest,则在独立子进程调用 pytest。配置示例如下:

tests:
   some.foo.test:
      harness: pytest
      tags: foo

默认在与二进制源代码目录同级的 pytest 目录查找测试。可用 YAML 的 harness_config 下的 pytest_root 指向其他文件、目录或子测试,详见 此处。

pytest 按默认的 discovery rules 扫描指定位置查找测试。

传入附加参数

有两种方式向 pytest 子进程传入附加参数:

  1. 在 YAML 的 harness_config 下使用 pytest_args,详见 此处。

  2. 通过 Twister 命令行的 --pytest-args 参数。这尤其适合从套件中选择特定用例,例如:

    $ west 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'
    

    命令行参数会补充 YAML 参数。同一参数同时出现在两处时,命令行优先。

Fixture

dut

提供代表被测设备的 DeviceAdapter 对象,是 pytest 适配器插件的核心 fixture。启动 DUT,包括初始化日志、烧录和连接串口等,均需要它。它按请求类型(native、qemu、hardware 等)返回准备好的设备。所有设备共享相同 API,便于编写不依赖设备类型的测试。作用域由 harness_config 下的 pytest_dut_scope 决定,详见 此处。

from twister_harness import DeviceAdapter

def test_sample(dut: DeviceAdapter):
   dut.readlines_until(regex='Hello world')

shell

提供 Shell 对象及与 shell 应用交互的方法。它调用 wait_for_promt,等待 DUT 就绪后才开始场景。shell fixture 调用 dut,因此可使用其全部方法。shell 还添加针对 shell 交互优化的方法,可代替 dut 用于测试。作用域由 harness_config 下的 pytest_dut_scope 决定,详见 此处。

from twister_harness import Shell

def test_shell(shell: Shell):
   shell.exec_command('help')

mcumgr

封装远程设备管理命令行工具 mcumgr 的示例 fixture。MCUmgr 详情见 MCUmgr。

备注

此 fixture 要求系统 PATH 中可找到 mcumgr。

此 fixture 仅封装 MCUmgr 的部分功能。使用 mcumgr fixture 的测试示例如下:

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.

unlaunched_dut

类似 dut,但不初始化设备。需要更细致地控制构建过程时可使用它,初始化设备由测试负责。

from twister_harness import DeviceAdapter

def test_sample(unlaunched_dut: DeviceAdapter):
   unlaunched_dut.launch()
   unlaunched_dut.readlines_until(regex='Hello world')

多 DUT 场景的 fixture

多 DUT fixture 对应单 DUT 的 unlaunched_dut、dut、shell,但操作设备列表。支持硬件和 native_sim,不支持 QEMU。要求 twister_pytest_config.yaml 包含 duts 条目。Twister 自动生成此文件并调用 pytest;仿真目标会自动创建占位 DUT,无需硬件映射。不通过 Twister 手动重跑 pytest 时复用同一配置,见常见问题。配置详情见 多 DUT 测试支持。

unlaunched_duts

类似 unlaunched_dut,但返回 DeviceAdapter 对象列表,每个预留 DUT 一个,日志文件已初始化,设备尚未启动。

from twister_harness import DeviceAdapter

def test_sample(unlaunched_duts: list[DeviceAdapter]):
   for dut in unlaunched_duts:
      dut.launch()
   unlaunched_duts[0].readlines_until(regex='Hello world')

duts

类似 dut,但返回已启动的 DeviceAdapter 对象列表。所有设备并行烧录以缩短准备时间。若需顺序烧录,在 conftest.py 中覆盖此 fixture。

from twister_harness import DeviceAdapter

def test_sample(duts: list[DeviceAdapter]):
   assert len(duts) > 1
   duts[0].readlines_until(regex='Hello world')
   duts[1].readlines_until(regex='Hello world')

shells

类似 shell,但返回 Shell 对象列表,每个预留 DUT 一个,均已等待 shell 提示符。

from twister_harness import Shell

def test_sample(shells: list[Shell]):
   assert len(shells) > 1
   shells[0].exec_command('help')
   shells[1].exec_command('help')

类

DeviceAdapter

class twister_harness.DeviceAdapter(device_config: DeviceConfig)

This class defines a common interface for all device types (hardware, simulator, QEMU) used in tests to gathering device output and send data to it.

launch() → None

Launch the test application on the target device.

This method performs the complete device initialization sequence:

  • Close any previously running application (cleanup)

  • Generate the execution command if not already set

  • Add any extra test arguments to the command

  • Start reader threads to capture device output

  • Launch the application (flash for hardware, execute for simulators)

The launch process varies by device type:

  • Hardware devices: Flash the application and establish serial communication. May connect before or after flashing depending on device configuration.

  • QEMU/simulators: Start subprocess and establish FIFO/pipe communication

  • Native simulators: Execute binary and connect via process pipes

close() → None

Disconnect, close device and close reader threads.

readline(connection_index: int = 0, **kwargs) → str

Read a single line from device output.

参数:
  • connection_index -- Connection port to read from (0=main UART, 1=second core, etc.)

  • kwargs -- Additional keyword arguments (timeout, print_output)

返回:

Single line of text without trailing newlines

readlines(connection_index: int = 0, **kwargs) → list[str]

Read all available output lines produced by device from internal buffer.

readlines_until(connection_index: int = 0, **kwargs) → list[str]

Read lines from device output until a specific condition is met.

This method provides flexible ways to collect device output: wait for a specific pattern, collect a fixed number of lines, or get all currently available lines.

参数:
  • connection_index -- Index of the connection/port to read from (default: 0). For hardware devices: 0 = main UART, 1 = second core UART, etc. For QEMU and native: always use 0 (only one connection available)

  • kwargs --

    Keyword arguments passed to underlying connection including:

    • regex - Regular expression pattern to search for

    • num_of_lines - Exact number of lines to read

    • timeout - Maximum time in seconds to wait (uses base_timeout if not provided)

    • print_output - Whether to log each line as it's read (default: True)

返回:

List of output lines without trailing newlines

抛出:

AssertionError -- If timeout expires before condition is met

write(data: bytes, connection_index: int = 0) → None

Write data bytes to the target device.

Sends raw bytes to the device through the appropriate communication channel. The underlying transport mechanism varies by device type: Hardware devices write to serial/UART port, QEMU devices write to FIFO queue, and Native simulators write to process stdin pipes.

参数:
  • data -- Raw bytes to send to the device

  • connection_index -- Index of the connection/port to write to (default: 0)

Shell

class twister_harness.Shell(device: DeviceAdapter, prompt: str = 'uart:~$', timeout: float | None = None)

Helper class that provides methods used to interact with shell application.

exec_command(command: str, timeout: float | None = None, print_output: bool = True, *, get_full_output: bool = False, full_output_timeout: float | None = None) → list[str]

Send shell command to a device and return response. Passed command is extended by double enter sings - first one to execute this command on a device, second one to receive next prompt what is a signal that execution was finished. Method returns printout of the executed command.

参数:
  • get_full_output -- If True, after the shell prompt is seen, keep reading lines until full_output_timeout elapses (wall clock). Use this when output (e.g. decoded logs) continues to arrive after the prompt.

  • full_output_timeout -- Seconds to spend draining additional lines after the prompt. When get_full_output is True and this is omitted, timeout (or base_timeout) is used.

wait_for_prompt(timeout: float | None = None) → bool

Send every 0.5 second "enter" command to the device until shell prompt statement will occur (return True) or timeout will be exceeded (return False).

get_filtered_output(command_lines: list[str]) → list[str]

Filter out prompts and log messages

Take the output of exec_command, which can contain log messages and command prompts, and filter them to obtain only the command output.

Example:
>>> # equivalent to `lines = shell.exec_command("kernel version")`
>>> lines = [
>>>    'uart:~$',                    # filter prompts
>>>    'Zephyr version 3.6.0',       # keep this line
>>>    'uart:~$ <dbg> debug message' # filter log messages
>>> ]
>>> filtered_output = shell.get_filtered_output(output)
>>> filtered_output
['Zephyr version 3.6.0']
参数:

command_lines -- List of strings i.e. the output of exec_command.

返回:

A list of strings containing, excluding prompts and log messages.

Zephyr 项目中的 pytest 测试示例

常见问题

如何在每次 pytest 会话中只烧录/运行一次应用?

dut fixture 负责烧录和运行应用,默认作用域为 function。可在 YAML 的 harness_config 下添加 pytest_dut_scope 更改:

harness: pytest
harness_config:
   pytest_dut_scope: session

更多信息见 此处。

如何仅运行 Python 文件中的某一测试?

有多种方式。可在 YAML 的 harness_config 下添加 pytest_root,列出要运行的测试:

harness: pytest
harness_config:
   pytest_root:
      - "pytest/test_shell.py::test_shell_print_help"

也可使用 pytest 的 -k 选项选择测试,关键字过滤详情见 here。在 YAML 的 pytest_args 中添加 -k 过滤条件即可:

harness: pytest
harness_config:
   pytest_args:
      - "-k test_shell_print_help"

或将其加入 Twister 命令,覆盖 YAML 参数:

$ west twister ... --pytest-args='-k test_shell_print_help'

如何在测试中获取所用设备类型?

可从代表 DeviceAdapter 对象的 dut fixture 获取:

device_type: str = dut.device_config.type
if device_type == 'hardware':
   ...
elif device_type == 'native':
   ...

如何在本地重跑 pytest 测试,而不让 Twister 重新构建应用?

再次运行 Twister 并添加 --test-only 即可。也可用最高详细级别 -vv 运行 Twister,再从以 Running pytest command: ... 开头的日志中复制启动 pytest 的命令。

首先通过 Twister 运行场景:

$ west twister -vv -ll debug -T samples/subsys/testsuite/pytest/shell \
-s sample.pytest.shell --device-testing -p nrf54l15dk/nrf54l15/cpuapp --device-serial \
/dev/ttyACM1 --west-flash=--erase

Twister 在构建目录自动生成 twister_pytest_config.yaml,包含设备配置等全部测试参数。

若尚未设置,导出所需 PYTHONPATH 环境变量:

$ export PYTHONPATH=$ZEPHYR_BASE/scripts/pylib/pytest-twister-harness/src

最后运行 pytest 命令:

$ pytest -s -v -p twister_harness.plugin \
--twister-config=twister-out/.../sample.pytest.shell/twister_pytest_config.yaml \
samples/subsys/testsuite/pytest/shell/pytest

能否并行运行 pytest 测试?

pytest-harness-plugin 最初并未针对并行运行 pytest 测试设计,尤其是硬件测试。它假定 Twister 负责测试并行化及可用任务和硬件资源管理。如果因某些原因要自行并行执行,例如使用 pytest-xdist plugin,需要自行承担相关风险。

限制

  • 插件尚不支持所有平台类型。