ztest是Zephyr系统下的单元测试架构,可依赖硬件或模拟系统完成对代码的功能测试。
系统绑定流程
常规ztest测试架构包含如下层级:
./
├── CMakeLists.txt
├── prj.conf
├── src
│ └── main.c
└── testcase.yaml
- CMakeLists.txt 首先被构建系统处理,它确定了项目的构建规则和源文件组织。
- prj.conf 的配置会在编译时被应用,影响系统功能的启用和配置参数的设置(哪些参数可以使用可具体查看
zephyr/driver/
目录下Kconfig文件,使用语法是CONFIG_<参数名称> =
)。 - testcase.yaml 被测试框架(通常是twister)用来决定如何执行测试,包括在什么平台上运行,如何验证测试结果等。
- main.c 中的代码在以上配置的基础上执行,执行具体的测试逻辑,并通过ztest框架报告测试结果。
基于以上配置,可启用twister或者west执行构建,并直接在终端输出测试结果。
ztest测试可针对软件和硬件,软检测试可不依赖设备树,硬件测试应当指定设备树结构。以uart_pl011驱动测试为例,并结合某个特定的 evk 开发板而言,测试启动流程应为:
source files (.dts, .dtsi)
↓
预处理
↓
设备树编译器(DTC)
↓
build/zephyr/zephyr.dts
在嵌入式系统启动流程中,Kconfig文件和设备树文件都用来描述当前系统的配置信息,区别在于Kconfig侧重于描述软件系统的配置信息,设备树文件侧重于描述硬件系统的配置信息。我们使用
./build.sh uart_pl011_test evk
指令来完成针对evk板卡的构建,因此应当重点关于与evk有关的设备树文件以及相关驱动Kconfig文件。
查看evk.dts
:
/* SPDX-License-Identifier: Apache-2.0 */
/dts-v1/;
#include <evk.dtsi>
/ {
......
chosen {
...
zephyr,console = &uart0;
zephyr,shell-uart = &uart0;
};
......
uart0: uart@C0200000 {
compatible = "arm,pl011";
...
label = "UART_0";
current-speed = <115200>;
};
......
&uart0 {
status = "okay";
current-speed = <115200>;
};
......
在chosen
和uart0
节点中明确指明了当前设备所使用的uart类型——arm,pl011
,我们对uart_pl011的驱动测试也将围绕它展开。
编写main.c
主测试程序,在程序开头获取设备信息并完成初始化:
#if DT_NODE_HAS_STATUS(DT_CHOSEN(zephyr_console), okay)
#define UART_NODE DT_CHOSEN(zephyr_console)
#else
#define UART_NODE DT_NODELABEL(uart0)
#endif
static const struct device *uart_dev;
static void uart_setup(void)
{
uart_dev = DEVICE_DT_GET(UART_NODE);
zassert_not_null(uart_dev, "UART device not found");
zassert_true(device_is_ready(uart_dev), "UART device not ready");
}
DT_NODE_HAS_STATUS()
宏是Zephyr系统配置关键宏,用来检查设备树节点的status属性,当前在设备树中查找名为"zephyr,console
"的chosen
节点。DT_CHOSEN()
宏用来获取设备树中chosen
节点下指定属性对应的节点,可实现系统级设备的配置。DT_NODELABEL()
宏通过节点标签label
获取设备树节点。zephyr,console
是Zephyr系统中的一个特殊的chosen节点属性,用于指定系统的主控制台设备。
以上宏的定义可在include/devicetree.h
或include/device.h
中查看。而现在的程序则实现了对设备信息的获取。
在prj.conf
中声明:
CONFIG_UART_PL011=y
启用uart_pl011驱动程序,告诉构建系统需要编译和链接uart_pl011驱动代码并启动相关的依赖项,激活设备树支持。
而来到uart_pl011.c
驱动程序本身,在其内部同样有如下设备定义:
DEVICE_DT_INST_DEFINE(0,
&pl011_init,
NULL,
&pl011_data_port_0,
&pl011_cfg_port_0, PRE_KERNEL_1,
CONFIG_KERNEL_INIT_PRIORITY_DEVICE,
&pl011_driver_api);
综上所述,为了实现测试程序与uart_pl011驱动的联系,并保证我们的确是在针对evk开发板的uart_pl011驱动进行的测试,我们的程序执行了如下流程:
- 构建系统首先检查prj.conf中的CONFIG_UART_PL011=y,决定编译uart_pl011驱动。
- 设备树编译器解析.dts文件,识别compatible = "arm,pl011"属性,将这个设备与uart_pl011驱动匹配。
- 系统启动时,驱动程序的DEVICE_DT_INST_DEFINE创建设备实例,并注册到Zephyr的设备管理系统。
- 当测试代码main.c通过DEVICE_DT_GET获取设备时,Zephyr的设备管理系统根据设备树信息返回正确的设备实例。
以上流程环环相扣,缺一不可。
在编译过后,系统会自动生成一个新的build文件夹,并生成新的zephyr.dts设备树文件,可在其中查看测试节点:
uart0: uart@C0200000 {
compatible = "arm,pl011";
reg = < 0xc0200000 0x4c >;
clocks = < &sysclk >;
interrupts = < 0xda 0x2 >;
status = "okay";
label = "UART_0";
current-speed = < 0x1c200 >;
};
表明本次测试的确在测试pl011驱动。
测试程序解析
Zephyr2.7.0的ztest测试架构依赖如下构成:
/* 2.7.0版本按照当前引入头文件的方式。3.7.0则采用 #include <zephyr/ztest.h> 的方式 */
#include <ztest.h>
// 声明测试用例
void test_case_1(void) {
zassert_true(1, "Test case 1 failed.");
}
void test_case_2(void) {
zassert_equal(2, 2, "Test case 2 failed.");
}
// 定义 test_main 函数
void test_main(void) {
// 定义一个测试套件并注册测试用例
ztest_test_suite(my_test_suite,
ztest_unit_test(test_case_1),
ztest_unit_test(test_case_2)
);
// 运行测试套件
ztest_run_test_suite(my_test_suite);
}
在开发程序时,每个测试项都可以被单独定义成一个测试函数,我们将待测模块的API引入测试函数中,并结合zassert_*
宏来进行断言检查,用来判断模块是否符合设计需求。
在测试模块结尾,设计test_main
模块用来作为整个测试程序的入口,在这其中:
ztest_test_suite
:用于定义一个测试套件,并将多个测试用例注册到该套件中。ztest_run_test_suite
:运行指定的测试套件。
注意二者层级关系,ztest_test_suite(...ztest_run_test_suite...)
。
Zephyr提供诸多zassert宏用来进行检验,例如:
zassert_true(cond, ...)
断言条件cond
为真。如果为假,则测试失败。zassert_false(cond, ...)
断言条件cond
为假。如果为真,则测试失败。zassert_equal(a, b, ...)
断言值a
等于值b
。如果不相等,则测试失败。zassert_not_equal(a, b, ...)
断言值a
不等于值b
。如果相等,则测试失败。