Twister 黑盒测试

本指南介绍测试文件结构,帮助读者理解现有文件并编写新测试。开发者应修复因自己的改动而失败的测试,并在引入新功能时添加测试,因此所有 Twister 开发者都应了解这些内容。

基础

Twister 黑盒测试使用 Python 和 pytest 库编写,详见 此处。辅助测试数据保留原始格式。测试和数据全部位于 scripts/tests/twister_blackbox,名称以 test_ 开头。

黑盒测试不应依赖 Twister 内部代码,而应像用户一样调用 Twister 并检查结果。

测试文件示例

 1#!/usr/bin/env python3
 2# Copyright (c) 2024 Intel Corporation
 3#
 4# SPDX-License-Identifier: Apache-2.0
 5
 6import importlib
 7import json
 8import os
 9import sys
10from unittest import mock
11
12import pytest
13from conftest import TEST_DATA, ZEPHYR_BASE, test_filename_mock
14from twisterlib.testplan import TestPlan
15
16
17class TestDummy:
18    TESTDATA_X = [
19        ("smoke", 5),
20        ("acceptance", 6),
21    ]
22
23    @classmethod
24    def setup_class(cls):
25        apath = os.path.join(ZEPHYR_BASE, "scripts", "twister")
26        cls.loader = importlib.machinery.SourceFileLoader("__main__", apath)
27        cls.spec = importlib.util.spec_from_loader(cls.loader.name, cls.loader)
28        cls.twister_module = importlib.util.module_from_spec(cls.spec)
29
30    @classmethod
31    def teardown_class(cls):
32        pass
33
34    @pytest.mark.parametrize(
35        "level, expected_tests", TESTDATA_X, ids=["smoke", "acceptance"]
36    )
37    @mock.patch.object(TestPlan, "TEST_DEFINITION_FILENAME", test_filename_mock)
38    def test_level(self, capfd, out_path, level, expected_tests):
39        # Select platforms used for the tests
40        test_platforms = ["qemu_x86", "frdm_k64f"]
41        # Select test root
42        path = os.path.join(TEST_DATA, "tests")
43        config_path = os.path.join(TEST_DATA, "test_config.yaml")
44
45        # Set flags for our Twister command as a list of strs
46        args = (
47            # Flags related to the generic test setup:
48            # * Control the level of detail in stdout/err
49            # * Establish the output directory
50            # * Select Zephyr tests to use
51            # * Control whether to only build or build and run aforementioned tests
52            ["-i", "--outdir", out_path, "-T", path, "-y"]
53            # Flags under test
54            + ["--level", level]
55            # Flags required for the test
56            + ["--test-config", config_path]
57            # Flags related to platform selection
58            + [
59                val
60                for pair in zip(["-p"] * len(test_platforms), test_platforms, strict=False)
61                for val in pair
62            ]
63        )
64
65        # First, provide the args variable as our Twister command line arguments.
66        # Then, catch the exit code in the sys_exit variable.
67        with mock.patch.object(sys, "argv", [sys.argv[0]] + args), pytest.raises(
68            SystemExit
69        ) as sys_exit:
70            # Execute the Twister call itself.
71            self.loader.exec_module(self.twister_module)
72
73        # Check whether the Twister call succeeded
74        assert str(sys_exit.value) == "0"
75
76        # Access to the test file output
77        with open(os.path.join(out_path, "testplan.json")) as f:
78            j = json.load(f)
79        filtered_j = [
80            (ts["platform"], ts["name"], tc["identifier"])
81            for ts in j["testsuites"]
82            for tc in ts["testcases"]
83            if "reason" not in tc
84        ]
85
86        # Read stdout and stderr to out and err variables respectively
87        out, err = capfd.readouterr()
88        # Rewrite the captured buffers to stdout and stderr so the user can still read them
89        sys.stdout.write(out)
90        sys.stderr.write(err)
91
92        # Test-relevant checks
93        assert expected_tests == len(filtered_j)

与命令行比较

上面的测试运行以下命令:

twister -i --outdir $OUTDIR -T $TEST_DATA/tests -y --level $LEVEL
--test-config $TEST_DATA/test_config.yaml -p qemu_x86 -p frdm_k64f

它假定命令行环境已执行 zephyr-env.sh 或 zephyr-env.cmd。

借助 importlib 的 exec_module() [1],此测试可提供通常运行 Twister 时的全部输出。通过 args 变量 [2] 可方便地设置调用标志,并在 out 和 err 变量中检查标准输出和标准错误。

除标准输出外,还可检查通常位于 twister-out 目录中的文件输出。多数情况下会结合 out_path fixture 和 --outdir 标志(第 52 行),将测试生成的文件保存在临时目录。黑盒测试常读取 testplan.json、twister.xml 和 twister.log。

其他功能

装饰器

  • @pytest.mark.usefixtures('clear_log')
    • 允许使用 conftest.py 中的 clear_log fixture。未来此 fixture 将改为 autouse,届时可移除此装饰器。

  • @pytest.mark.parametrize('level, expected_tests', TESTDATA_X, ids=['smoke', 'acceptance'])
    • 这是 pytest 测试参数化示例,详见 here。TESTDATA 通常声明为类字段。

  • @mock.patch.object(TestPlan, 'TEST_DEFINITION_FILENAME', test_filename_mock)
    • 此装饰器允许只使用 test_data 中定义的测试,忽略 tests 目录中的 Zephyr 用例。注意,所有 test_data 测试使用的文件名是 test_data.yaml,不是 testcase.yaml!test_data 中的数据及 mock 库的更多说明见 here。

Fixture

黑盒测试使用 pytest 的 fixture,更多介绍见 here。

添加自己的 fixture 前,应考虑它只用于一个测试文件还是多个文件。

如何……

在同一测试中多次调用 Twister?

有些测试需要先运行一次 Twister。例如,--test-only 通常与前一次 --build-only 调用配合使用。该如何处理?

若直接调用两次 importlib 的 exec_module,会导致日志重复。twister.log 中每行会出现两次,调用三次则出现三次,而非覆盖日志或向末尾追加。

原因是 Twister 文件使用了模块级日志器变量,重新执行模块会使日志器拥有多个处理器。

为解决此问题,应在两次调用之间执行:

capfd.readouterr()   # To remove output from the buffer
                     # Note that if you want output from all runs after each other,
                     # skip this line.
clear_log_in_test()  # To remove log duplication

脚注