使用 Python 进行功能测试

tests/functional 包含 Python 功能测试。 它们通常是较高层测试,可能访问外部资源,并与各种客户机操作系统交互。

测试应采用 Python unittest 风格,通过标准输入输出使用 TAP 协议。 tests/functional/qemu_test 提供 QemuBaseTest、QemuUserTest、QemuSystemTest 等类及辅助函数,帮助满足这些要求,例如替换 stdout 对象,将普通测试输出重定向到 stderr。

如果不使用 QemuBaseTest 派生类,或测试会启动子进程, 必须确保 stdio 不出现与 TAP 不兼容的输出。 可以为每行加上 "# ",将其标为 TAP 注释, 也可以捕获子进程 stdout,或将其重定向到 stderr。

基于 qemu_test.QemuSystemTest 的测试可以方便地:

  • 定制 self.vm 这一 QEMUMachine 实例的命令行参数。

  • 与 QEMU 监视器交互,发送 QMP 命令并检查结果。

  • 通过便利的控制台设备与客户机系统交互,验证命令行参数或 QMP 命令的效果。

  • 下载并缓存远程数据,例如固件和内核镜像。

运行测试

执行以下命令运行功能测试:

make check-functional

也可只运行特定目标的测试,例如 x86_64:

make check-functional-x86_64

若不使用 Meson 测试运行器,可以通过环境变量指定模拟器程序,然后直接执行测试文件。

假设当前目录是源码根目录,构建目录为 ./build::

$ export QEMU_TEST_QEMU_BINARY=qemu-system-x86_64

运行文件中的全部测试::

$ ./build/run tests/functional/x86_64/test_virtio_version.py

运行某个测试类::

$ ./build/run tests/functional/x86_64/test_virtio_version.py VirtioVersionCheck

运行单个测试::

$ ./build/run tests/functional/x86_64/test_virtio_version.py VirtioVersionCheck.test_modern_only_devs

也支持按名称过滤::

$ ./build/run tests/functional/x86_64/test_virtio_version.py -k modern

框架会自动清理测试产生的临时文件。 调试失败时,可设置 QEMU_TEST_KEEP_SCRATCH=1 保留它们。 下次不设置该变量运行测试时,这些文件会被删除。

日志

每个测试的日志保存在构建目录的以下子目录::

<builddir>/tests/functional/<arch>/<fileid>.<classid>.<testname>/

通常有三个文件:

  • base.log:测试代码通过 self.log.info()、self.log.debug() 等写入的通用日志。

  • console.log:客户机串口控制台输出。

  • default.log:QEMU 输出。如果测试为虚拟机选择了其他标识符, 例如同时运行多个虚拟机,此文件名称也会不同。

编写测试入门

tests/functional/qemu_test 提供 qemu_test 模块及 QemuSystemTest 类。 简单示例如下:

#!/usr/bin/env python3

from qemu_test import QemuSystemTest

class Version(QemuSystemTest):

    def test_qmp_human_info_version(self):
        self.vm.launch()
        res = self.vm.cmd('human-monitor-command',
                          command_line='info version')
        self.assertRegex(res, r'^(\d+\.\d+\.\d)')

if __name__ == '__main__':
    QemuSystemTest.main()

脚本开头提供 shebang、赋予可执行权限并调用 QemuSystemTest.main() 后, 可脱离运行器独立执行。 通过测试运行器执行时,main() 也负责按正确方式调用测试, 例如提供 Meson 要求的 TAP 输出。

qemu_test.QemuSystemTest 基类

此类具有以下特点。

首先,它为每个测试准备 self.vm QEMUMachine 实例。 由于许多测试需要调整命令行,调用 self.vm.launch() 启动实例由测试作者负责。

基类也支持多个实例。self.get_vm() 返回 QEMUMachine, 接受创建实例所需的参数,以及可选 name。 通过 name 可以识别特定机器,并在不同测试方法中重复获取同一实例。 示例如下:

from qemu_test import QemuSystemTest

class MultipleMachines(QemuSystemTest):
    def test_multiple_machines(self):
        first_machine = self.get_vm()
        second_machine = self.get_vm()
        self.get_vm(name='third_machine').launch()

        first_machine.launch()
        second_machine.launch()

        first_res = first_machine.cmd(
            'human-monitor-command',
            command_line='info version')

        second_res = second_machine.cmd(
            'human-monitor-command',
            command_line='info version')

        third_res = self.get_vm(name='third_machine').cmd(
            'human-monitor-command',
            command_line='info version')

        self.assertEqual(first_res, second_res, third_res)

测试清理阶段,QemuSystemTest 负责关闭全部实例。

QEMUMachine

此 API 已广泛用于 Python iotests、device-crash-test 及其他脚本。 它包装 QEMU 程序执行,提供:

  • 设置 QEMU 命令行参数的能力。

  • 已就绪的 QMP 连接和接口,可发送命令、检查结果及异步事件。

  • 更简洁、直观地设置常用参数的便利方法。

选择 QEMU 程序

self.vm 使用的程序主要取决于 qemu_bin 实例属性。 测试未显式设置时,默认取 QEMU_TEST_QEMU_BINARY 环境变量。

调试挂起的 QEMU

测试异常时,可能需要调试停滞的 QEMU 进程。 虽然主 QMP socket 由 QEMUMachine 占用, 仍可将 QEMU_TEST_QMP_BACKDOOR 设为一个 Unix socket 路径,请求额外的 QMP 监视器。 随后使用 qmp-shell 连接,检查当前状态。

属性参考

QemuBaseTest

以下属性适用于所有 qemu_test.QemuBaseTest 实例。

arch

QEMU 程序的目标架构。

测试也可以按需要使用它,例如选择启动虚拟机所用内核或磁盘镜像的架构。

qemu_bin

保存 QEMU_TEST_QEMU_BINARY 环境变量的值。

QemuUserTest

用于通过用户态模拟器运行可执行程序。

QemuSystemTest

用于通过 qemu-system-* 程序运行测试。

vm

QEMUMachine 实例,初始根据 qemu_bin 配置。

cpu

测试创建的全部 QEMUMachine 实例使用的 CPU 型号。

machine

测试创建的全部实例使用的机器类型。 通过 QemuSystemTest.set_machine() 设置时,可以自动检查该机器是否编入 QEMU, 未编入时跳过测试。

资源处理

许多功能测试从网络下载 Linux 内核、initrd、固件等资源,给框架带来额外要求。

首先,部分用户的网络受限,因此默认 make check 不应运行这些测试。 需要下载的测试只应加入 meson.build 的 thorough 模式; 不需要下载的功能测试可以加入 quick。 make check 只运行 quick 功能测试及其他套件的快速测试。 单独运行 make check-functional 时,thorough 测试也会执行。 要与其他测试一起运行全部功能测试,可使用::

make -j$(nproc) check SPEED=thorough

其次,下载耗时取决于网络带宽,不应占用测试运行的超时预算。 因此框架在运行前预先下载资源,通过 qemu_test.Asset 实现缓存。 在测试类中声明 URL 和 SHA256,例如::

from qemu_test import Asset

ASSET_somename = Asset(
    ('https://www.qemu.org/assets/images/qemu_head_200.png'),
    '34b74cad46ea28a2966c1d04e102510daf1fd73e6582b6b74523940d5da029dd')

测试函数中获取缓存文件名::

def test_function(self):
    file_path = self.ASSET_somename.fetch()

make check-functional 会自动预缓存,但 make check-functional-<target> 等不会。 只下载、不运行测试时,使用::

make precache-functional

默认缓存目录为 ~/.cache/qemu/download,可通过 QEMU_TEST_CACHE_DIR 修改。

设置 QEMU_TEST_REFRESH_CACHE 可强制重新下载,即使缓存仍有效。

跳过测试

框架基于 Python unittest,因此可使用常见装饰器,按条件跳过测试, 例如缺少程序,或运行环境是 CI。 更多信息见:

虽然跳过条件常因测试而异,但一些常见场景形成了通过环境变量启停测试的惯例。

常用变量如下。

QEMU_TEST_ALLOW_LARGE_STORAGE

需要获取或生成大型资源的测试,只有设置 QEMU_TEST_ALLOW_LARGE_STORAGE=1 才运行。

大型的定义并不严格,通常指解压后至少占用 1 GB 磁盘空间。

QEMU_TEST_ALLOW_UNTRUSTED_CODE

有些测试启动的内核或固件被认为不适合在开发者工作站安全运行,因此默认跳过。 通常指源码或构建过程不公开的二进制内容,但判断标准并不完全固定。

设置 QEMU_TEST_ALLOW_UNTRUSTED_CODE=1 后才允许运行这类测试。

QEMU_TEST_FLAKY_TESTS

不稳定的测试默认关闭,包括无法在 GitLab CI 中稳定运行的测试。 它们往往暴露真实问题,只是 CI 的限制使这些问题比开发机上更容易出现。 遇到类似情况时,应报告缺陷,并按下例标记测试:

# See https://gitlab.com/qemu-project/qemu/-/issues/nnnn
@skipUnless(os.getenv('QEMU_TEST_FLAKY_TESTS'), 'Test is unstable on GitLab')
def test(self):
    do_something()

测试不应永久处于这种状态,必须修复,或最终移除。

QEMU_TEST_ALLOW_SLOW

运行时间很长的测试,可能在启用调试选项构建 QEMU 等情况下超时。 为避免默认超时,也节省普通测试的 CPU 时间, 只有设置 QEMU_TEST_ALLOW_SLOW 才运行这些测试。