4 执行自动化运行时测试

OpenEmbedded 构建系统为镜像提供了一系列自动化测试,用于验证运行时功能。 这些测试可以在 QEMU 或实际目标硬件上运行。测试使用 Python 的 unittest 模块编写,大多数会通过 SSH 在目标系统上执行命令。本节介绍如何配置测试环境、 运行现有测试,以及编写和添加自己的测试。

关于 Yocto 项目提供的测试和质量保证基础设施,参见 Yocto 项目参考手册的 测试与质量保证 一节。

4 启用测试

根据计划使用 QEMU 还是实际硬件运行测试,启用步骤有所不同。 以下各小节分别介绍这两种测试方式。

4 在 QEMU 上启用运行时测试

运行测试前,需要完成以下工作:

  • 配置网络操作,避免 sudo 交互: 可以选择以下方式之一:

    • 在 /etc/sudoers 中为用户添加 NOPASSWD,作用于所有命令, 或仅作用于 runqemu-ifup。必须指定完整路径,因为使用多个源码仓库克隆时, 路径可能不同。

      备注

      某些发行版还需要注释掉 /etc/sudoers 中的 "Defaults requiretty"。

    • 手动为系统配置 tap 接口。

    • 以 root 身份运行 scripts/runqemu-gen-tapdevs 脚本,生成一组 tap 设备。 Autobuilder 一类环境通常采用这种方式。

      备注

      • 通过 sudo 调用此脚本时,务必使用绝对路径。

      • 确保主机安装了 iptables 软件包。

      • 运行该脚本需要 qemu-helper-native 软件包配方。 使用以下命令构建该软件包::

        $ bitbake qemu-helper-native
        
  • 设置 DISPLAY 变量: 需要设置该变量,以便使用 X 服务器 (例如,为无显示器的机器启动 vncserver)。

  • 确保主机防火墙接受来自 192.168.7.0/24 的连接: 某些测试,尤其是 DNF 测试,会在随机的高端口上启动 HTTP 服务器, 向目标设备提供文件。DNF 模块提供 ${WORKDIR}/oe-rootfs-repo 的访问, 以便运行 DNF 软件源命令。因此,主机防火墙必须接受来自 192.168.7.0/24 的连接,这是 runqemu 为 tap 设备使用的默认 IP 地址范围。

  • 确保主机安装了正确的软件包: 根据主机发行版,需要安装:

    • Ubuntu 和 Debian:sysstat 和 iproute2

    • openSUSE:sysstat 和 iproute2

    • Fedora:sysstat 和 iproute

    • CentOS:sysstat 和 iproute

开始运行测试后,会依次执行以下操作:

  1. 将根文件系统的副本写入 ${WORKDIR}/testimage。

  2. 使用标准 runqemu 脚本在 QEMU 中启动镜像。

  3. 默认等待最多 500 秒,使启动过程进入登录提示符。 可以在 configuration file 中设置 TEST_QEMUBOOT_TIMEOUT 修改超时时间。

  4. 启动完成并出现登录提示符后,开始运行测试。 完整启动日志写入 ${WORKDIR}/testimage/qemu_boot_log。

  5. 按 TEST_SUITES 中的顺序加载各测试模块。 通过 SSH 执行的命令的完整输出位于 ${WORKDIR}/testimgage/ssh_target_log。

  6. 如果没有失败,测试任务成功结束。 unittest 的输出位于任务日志 ${WORKDIR}/temp/log.do_testimage。

4 在硬件上启用运行时测试

OpenEmbedded 构建系统可以在真实硬件上运行测试;对于某些设备, 还可以在测试前将待测镜像部署到设备上。

为了自动部署,初始设置时需要在硬件上安装一次“控制器镜像”。 此后,每次运行测试时执行以下操作:

  1. 启动控制器镜像,并用它将待测镜像写入第二个分区。

  2. 使用你提供的外部脚本重新启动设备。

  3. 设备启动进入待测镜像。

运行测试时,无论镜像是否自动部署,设备都应以预先确定的 IP 地址接入网络。 可以在镜像中写入静态 IP,也可以让镜像使用 DHCP, 并由测试网络中的 DHCP 服务器根据设备 MAC 地址分配已知 IP。

要在硬件上运行测试,需要为 TEST_TARGET 设置适当的值。 对于 QEMU,无需修改,默认值就是 "qemu"。硬件测试可使用以下选项:

  • "simpleremote": 如果目标系统已经运行待测镜像且可以通过网络访问, 选择 "simpleremote"。它既可用于真实硬件,也可用于单独启动的 QEMU 或其他虚拟机管理器中运行的镜像。

  • "SystemdbootTarget": 如果硬件是基于 EFI 的机器,使用 systemd-boot 引导加载程序,并已安装 core-image-testmaster 或类似镜像, 则选择 "SystemdbootTarget"。待测硬件还必须接入启用了 DHCP 的网络, 并在每次重启后获得相同的 IP 地址。

    该选项还涉及其他要求和注意事项,详见后面的 选择 SystemdbootTarget 一节。

  • "BeagleBoneTarget": 如果在 BeagleBone "Black" 或最初的 "White" 硬件上部署镜像并运行测试,选择 "BeagleBoneTarget"。使用方式见 meta-yocto-bsp/lib/oeqa/controllers/beaglebonetarget.py 文件顶部的注释。

  • "GrubTarget": 如果在使用 GRUB 引导的通用 PC 上部署镜像并运行测试, 选择 "GrubTarget"。使用方式见 meta-yocto-bsp/lib/oeqa/controllers/grubtarget.py 文件顶部的注释。

  • "your-target": 如果要在 BSP 层中的自定义机器上部署镜像并测试, 可以创建自定义目标。需要在层的 lib/oeqa/controllers/ 下添加 定义目标类的 Python 模块,并提供空的 __init__.py。 示例见 meta-yocto-bsp/lib/oeqa/controllers/ 中的文件。

4 选择 SystemdbootTarget

如果没有将 TEST_TARGET 设为 "SystemdbootTarget", 可以跳过本节,直接阅读 运行测试。

如果设置了该值,还需要按以下步骤对控制器镜像进行一次性配置:

  1. 设置 EFI_PROVIDER: 确保 EFI_PROVIDER 如下::

    EFI_PROVIDER = "systemd-boot"
    
  2. 构建控制器镜像: 构建 core-image-testmaster 镜像。 此配方是“控制器”镜像的示例,可以像其他配方一样进行定制。

    镜像配方必须满足以下要求:

    • 继承 core-image,以便安装内核模块。

    • 安装常规 Linux 工具,而非 BusyBox 版本, 例如 bash、coreutils、tar、gzip 和 kmod。

    • 使用带自定义安装程序的 Initramfs 镜像。 普通可安装镜像通常只创建一个根文件系统分区,而这里使用的安装程序 会创建特定分区布局。并非所有板级支持包(BSP)都能使用安装程序。 在这些情况下,需要在目标上手动创建以下布局:

      • 第一个分区挂载于 /boot,标签为 "boot"。

      • 安装此镜像的主根文件系统分区,挂载于 /。

      • 另一个标签为 "testrootfs" 的分区,用于部署待测镜像。

  3. 安装镜像: 将刚构建的镜像安装到目标系统上。

将 TEST_TARGET 设为 "SystemdbootTarget" 后,最后还需要设置待测镜像:

  1. 配置 local.conf: 确保 build/conf/local.conf 中包含以下语句::

    IMAGE_FSTYPES += "tar.gz"
    IMAGE_CLASSES += "testimage"
    TEST_TARGET = "SystemdbootTarget"
    TEST_TARGET_IP = "192.168.2.3"
    
  2. 构建待测镜像: 使用 BitBake 构建镜像::

    $ bitbake core-image-sato
    

4 电源控制

除 "simpleremote" 外,大多数硬件目标支持电源控制:

  • 可以组合使用 TEST_POWERCONTROL_CMD 和 TEST_POWERCONTROL_EXTRA_ARGS,定义在主机上运行的电源控制命令。 测试代码向该命令传入一个参数:off、on 或 cycle(先关再开)。 以下示例可以放入 build/conf/local.conf::

    TEST_POWERCONTROL_CMD = "powercontrol.exp test 10.11.12.1 nuc1"
    

    在此示例中,expect 脚本执行以下操作:

    ssh test@10.11.12.1 "pyctl nuc1 arg"
    

    随后运行一个 Python 脚本,控制标签为 nuc1 的设备的电源。

    备注

    需要根据实际环境定制 TEST_POWERCONTROL_CMD 和 TEST_POWERCONTROL_EXTRA_ARGS。唯一要求是命令接受 "on"、"off" 和 "cycle" 作为最后一个参数。

  • 如果未定义电源控制命令,则通过 SSH 连接设备,使用常规 reboot 命令重启。 只要机器确实能够重启(即 SSH 测试没有失败),这种方式就可用。 它适用于配置简单、通常只有一块开发板,并能接受偶尔手动干预的场景。

如果没有能够自动控制电源的硬件,但仍想尝试自动化硬件测试, 可以使用 dialog-power-control 脚本,通过对话框提示你执行所需的电源操作。 该脚本需要安装 KDialog 或 Zenity。使用时设置 TEST_POWERCONTROL_CMD::

TEST_POWERCONTROL_CMD = "${COREBASE}/scripts/contrib/dialog-power-control"

4 串口控制台连接

某些测试目标类需要通过串口控制台与引导加载程序交互, 例如 BeagleBoneTarget 和 GrubTarget。 此时需要通过 TEST_SERIALCONTROL_CMD 指定连接目标串口控制台的命令, 还可以选择设置 TEST_SERIALCONTROL_EXTRA_ARGS。

如果设备连接到本地串口,可以使用串口终端程序; 如果通过远程控制台服务器访问,则可以使用 telnet 或 ssh。 无论哪种方式,该命令只需像普通终端程序一样连接串口控制台, 并将连接转发到标准输入和输出。例如,使用 picocom 以 115200 bps 连接 /dev/ttyUSB0 时,可设置::

TEST_SERIALCONTROL_CMD = "picocom /dev/ttyUSB0 -b 115200"

对于重启时串口设备会消失的本地设备,另有 "serdevtry" 包装脚本可用。 只需在终端命令前加上 ${COREBASE}/scripts/contrib/serdevtry::

TEST_SERIALCONTROL_CMD = "${COREBASE}/scripts/contrib/serdevtry picocom -b 115200 /dev/ttyUSB0"

4 运行测试

可以自动或手动启动测试:

  • 自动运行测试: 要在 OpenEmbedded 构建系统成功创建镜像后自动测试, 先在 Build Directory 的 build/conf/local.conf 中将 TESTIMAGE_AUTO 设为 "1"::

    TESTIMAGE_AUTO = "1"
    

    然后构建镜像。如果构建成功,就会运行测试::

    bitbake core-image-sato
    
  • 手动运行测试: 先编辑 build/conf/local.conf, 全局继承 testimage 类::

    IMAGE_CLASSES += "testimage"
    

    然后使用 BitBake 运行测试::

    bitbake -c testimage image
    

所有测试文件位于 OpenEmbedded-Core (OE-Core) 的 meta/lib/oeqa/runtime/cases 中。测试名称直接对应 Python 模块, 每个模块可以包含多个独立测试。通常按测试领域分组, 例如 systemd 测试位于 meta/lib/oeqa/runtime/cases/systemd.py。

可以向任何层添加测试,只要将其放在正确位置, 并照常在 build/conf/local.conf 中扩展 BBPATH。 测试必须放在 layer/lib/oeqa/runtime/cases 中。

备注

模块名称不得与 meta/lib/oeqa/runtime/cases 中默认测试模块的名称冲突。

在 build/conf/local.conf 中追加或覆盖 TEST_SUITES, 可以改变要运行的测试集合。其中每个名称都代表该镜像必须运行的测试。 显式列出的模块不能跳过,即使测试不适用于该镜像 (例如,对未安装 rpm 的镜像运行 RPM 测试)。 将 "auto" 追加到 TEST_SUITES 后,构建系统会尝试运行所有适用测试, 此时各模块可以自行决定跳过。

TEST_SUITES 中的顺序很重要,会影响测试依赖。 有依赖的测试应放在其依赖项之后。例如,ssh 测试依赖 ping 测试, 因此列表中 "ssh" 必须位于 "ping" 之后。测试类不负责重新排序或处理依赖。

备注

每个模块可以包含多个类,每个类可以包含多个测试方法,均遵循 Python unittest 规则。

运行测试时应注意:

  • 镜像的默认测试定义如下::

    DEFAULT_TEST_SUITES:pn-image = "ping ssh df connman syslog xorg scp vnc date rpm dnf dmesg"
    
  • 使用以下方式向列表添加自己的测试::

    TEST_SUITES:append = " mytest"
    
  • 按以下方式运行指定测试列表::

    TEST_SUITES = "test1 test2 test3"
    

    请记住顺序很重要:依赖其他测试的测试必须放在其后。

4 导出测试

可以导出测试,使其脱离构建系统独立运行。如果需要将测试交给调度器执行, 就必须导出测试。只能导出 TEST_SUITES 中定义的测试。

如果镜像已经构建完成,确保 build/conf/local.conf 中有以下设置::

IMAGE_CLASSES += "testexport"
TEST_TARGET_IP = "IP-address-for-the-test-target"
TEST_SERVER_IP = "IP-address-for-the-test-server"

然后使用以下形式的 BitBake 命令导出测试::

$ bitbake image -c testexport

导出的测试位于 Build Directory 的 tmp/testimage/image 中, 该位置由 TEST_EXPORT_DIR 控制。

现在可以在构建环境之外运行测试::

$ cd tmp/testimage/image
$ ./oe-test runtime

备注

运行测试前,可能需要先在 QEMU 中运行镜像,或将镜像部署到硬件。

下面是一个使用 core-image-sato 镜像并指定 IP 地址的完整示例::

IMAGE_CLASSES += "testexport"
TEST_TARGET_IP = "192.168.7.2"
TEST_SERVER_IP = "192.168.7.1"

使用 BitBake 导出测试::

$ bitbake core-image-sato -c testexport

在构建环境之外,按以下方式运行测试::

$ cd tmp/testimage/core-image-sato
$ ./oe-test runtime

4 编写新测试

如前所述,新测试文件必须位于正确位置,构建系统才能找到它们。 对于核心之外的额外功能,测试应添加到提供该功能的层中, 路径为 layer/lib/oeqa/runtime/cases,并照常在该层的 layer.conf 中扩展 BBPATH。需要注意:

  • 文件名必须直接对应测试模块名称。

  • 模块名称不得与现有核心测试冲突。

  • runtime 目录下至少需要存在空的 __init__.py 文件。

创建测试时,可以先复制一个现有模块,例如 oe_syslog.py 或 gcc.py。 测试模块可以使用 meta/lib/oeqa/utils 中的辅助类。

备注

组织 shell 命令时,应保证其行为可靠,并用单一返回码表示成功。 有时还需要解析输出,示例见 df.py 和 date.py 模块。

所有测试类都继承 meta/lib/oetest.py 中的 oeRuntimeTest。 此基类提供的辅助属性和方法将在以下小节说明。

4 类方法

类方法如下:

  • hasPackage(pkg): 如果 pkg 位于镜像已安装软件包列表中,返回 "True"。 该列表基于 do_rootfs 任务生成的清单文件。

  • hasFeature(feature): 如果功能存在于 IMAGE_FEATURES 或 DISTRO_FEATURES 中,返回 "True"。

4 类属性

类属性如下:

  • pscmd: 如果镜像安装了 procps,值为 "ps -ef"; 否则为 "ps"(busybox)。

  • tc: 测试上下文,可用于访问以下属性:

    • d: BitBake 数据存储,可进行 oeRuntimeTest.tc.d.getVar("VIRTUAL-RUNTIME_init_manager") 等操作。

    • testslist 和 testsrequired: 内部使用,测试无需使用。

    • filesdir: meta/lib/oeqa/runtime/files 的绝对路径。 此目录包含要复制到目标上的测试辅助文件,例如用于编译的小型 C 文件。

    • target: 目标控制器对象,用于在特定目标(如 Qemu、SimpleRemote 和 SystemdbootTarget)上部署并启动镜像。测试通常使用以下成员:

      • ip: 目标 IP 地址。

      • server_ip: 主机 IP 地址,通常由 DNF 测试套件使用。

      • run(cmd, timeout=None): 最常用的方法,是 ssh root@host "cmd" 的包装。返回二元组 (status, output), 分别表示 "cmd" 的返回码和输出。可选参数 timeout 表示等待命令返回的秒数。 若为 "None",使用实例默认超时时间 300 秒;若为 "0",则一直等待命令返回。

      • copy_to(localpath, remotepath): scp localpath root@ip:remotepath。

      • copy_from(remotepath, localpath): scp root@host:remotepath localpath。

4 实例属性

唯一的实例属性是 target,与上一节所述的同名类属性相同。 同时提供实例属性和类属性,是为了使测试能在实例方法中使用 self.target.run(cmd),而不必使用 oeRuntimeTest.tc.target.run(cmd)。

4 在不使用包管理器的情况下向待测设备安装软件包

如果测试需要由 BitBake 构建的软件包,可以将其安装到待测设备(DUT), 而不要求设备安装包管理器。但必须具备 SSH 连接,且目标使用 sshcontrol 类。

备注

此方法使用 scp 将文件从主机复制到目标,因此会丢失权限及特殊属性。

通过 JSON 文件定义测试需要的软件包。文件必须与测试定义文件位于同一路径, 名称直接对应测试模块名称,扩展名为 .json。

JSON 文件中的对象以测试名称为键,其值为对象或数组。 该对象或对象数组使用以下字段:

  • "pkg":必需的字符串,指定要安装的软件包名称。

  • "rm":可选布尔值,默认为 "false",指定是否在测试后删除软件包。

  • "extract":可选布尔值,默认为 "false",指定是否需要从软件包格式中提取内容。 设置为 "true" 时,不会自动将软件包安装到待测设备。

以下 JSON 示例让测试 "foo" 安装软件包 "bar", 让测试 "foobar" 安装软件包 "foo" 和 "bar"。 测试完成后,从待测设备删除这些软件包::

{
    "foo": {
        "pkg": "bar"
    },
    "foobar": [
        {
            "pkg": "foo",
            "rm": true
        },
        {
            "pkg": "bar",
            "rm": true
        }
    ]
}