您现在的位置是:首页 >技术交流 >Zephyr单元测试框架:ztest的使用和介绍网站首页技术交流

Zephyr单元测试框架:ztest的使用和介绍

lbaihao 2023-05-20 04:00:02
简介Zephyr单元测试框架:ztest的使用和介绍

目录

简介

Ztest

简介

注意事项

宏函数

ZTEST

ZTEST_USER

ZTEST_RULE

常用宏函数封装

ztest_test_suite

ztest_unit_test

ztest_run_test_suite

测试函数

ztest_test_fail

ztest_test_pass

ztest_test_skip

unit_test_noop

断言函数

zassert

zassert_unreachable

zassert_true

zassert_false

zassert_ok

zassert_is_null

zassert_not_null

zassert_equal

zassert_not_equal

zassert_equal_ptr

zassert_within

zassert_mem_equal

函数测试框架

ztest_check_expected_value

ztest_expect_value

ztest_get_return_value

ztest_returns_value

ztest_get_return_value_ptr

ztest_expect_data

ztest_check_expected_data

ztest_return_data

ztest_copy_return_data

函数测试框架原理

单元测试框架的基本组成

CMakeList.txt

testcase.yaml/sample.yaml

键值介绍

prj.conf

src目录

Twister

简介

注意事项

命令行

测试结果输出目录与结构

Qemu模拟测试

开发板测试

准备工作

在stm32f746g_disco上进行测试

samples/subsys/testsuite/

简介
Zephyr为开发者们提供了一套简单的测试框架:Ztest,用于测试开发者们开发的Core,Ztest提供了断言以及一些基础的测试API,类似C语言的断言功能。

开发者们可以用这套框架编写Test Case,可以编写自动化脚本来测试自己的Drier或其它API接口是否能够正常工作,同时Zephyr也提供了自动化测试的脚本:twister,它是使用Python编写,用于批量或指定运行Test Cose,并且它能根据Ztest输出结果生成一些诊断信息。

Ztest
简介
Ztest是zephyr提供的Ci测试化框架,同时框架提供了大量的宏函数和API来做测试开发,这些宏函数和API位于:<ztest.h>文件中

注意事项
若要使用ztest框架需要在prj.conf中将CONFIG_ZTEST设置为y
main函数应该命名为:test_main(强制性的)
测试函数都应以"test_"开头(这不是强制性的)
宏函数
ZTEST
宏函数原型

#define ZTEST(suite, fn)
参数介绍:

suite – 要创建的测试单元名

fn – 要测试的调用函数

函数作用

创建并注册一个新的单元测试,并将fn参数里的函数附加到声明的单元测试中,同一个Test Case里只能创建一次

ZTEST_USER
宏函数原型

#define ZTEST_USER(suite, fn)
参数介绍

suite – 要创建的测试单元名

fn – 要测试的调用函数

函数作用

此宏的行为与ZTEST完全相同,唯一不同的是这个宏函数会跟“CONFIG_USERSPACE”属性挂钩,如果在prj.conf中将“CONFIG_USERSPACE”开启则创建的测试单元会在用户空间,并且在测试时也是在用户空间进行测试,会在用户空间中开辟一个线程来调用test函数,否则则是由zephyr内核空间完成。

ZTEST_RULE
宏函数原型

#define ZTEST_RULE(name, before_each_fn, after_each_fn)
参数介绍

name – 测试单元名

before_each_fn – 每次测试前要调用的函数,可以为空

after_each_fn – 每次测试后要调用的函数,可以为空

函数作用

这个函数设置指定测试单元在每次测试前和测试后调用的函数,同时会传递回调函数两个参数:当前测试指针,当前测试日志指针

回调函数

typedef void (*ztest_rule_cb)(const struct ztest_unit_test *test, void *data);
参数介绍

test – 指向当前单元测试上下文指针
data – 当前日志数据指针
常用宏函数封装
ztest_test_suite
宏函数原型

#define ztest_test_suite(suite,...)
参数介绍:

suite – 要创建的单元测试
... – 测试函数需要使用ztest_unit_test添加
函数作用

创建或添加套件到单元测试,它依赖于ZTEST宏

ztest_unit_test
宏函数原型

#define ztest_unit_test(unit)
参数介绍:

unit – 要添加的unit
函数作用

将函数转化为单元测试函数,只支持无参函数

ztest_run_test_suite
宏函数原型

#define ztest_run_test_suite(suite)
参数介绍:

suite – 要运行的单元测试
函数作用

运行测指定单元测试

测试函数
测试函数只能在你的test函数里调用,它用来设置并反馈给zephyr的ztest框架,告知当前测试函数的一些状态

ztest_test_fail
函数原型

void ztest_test_fail(void);
函数作用

当前测试函数失败

ztest_test_pass
函数原型

void ztest_test_pass(void);
函数作用

当前test测试成功,通常情况下不需要调用这个函数,一般直接在你的test函数里return就可以了,只要没有调用ztest的一些失败函数或没有产生断言就视当前test函数为正确的,如果遇到一些事件如当前case其实是成功的,但是某些原因总会导致断言失败,可以主动调用这个函数,在k_sys_fatal_error_handler函数中调用。

ztest_test_skip
函数原型

void ztest_test_skip(void);
函数作用

跳过当前测试函数,忽略

unit_test_noop
函数原型

static inline void unit_test_noop(void)
函数作用

什么都不做,当前test函数为成功的

断言函数
当断言失败时,它将打印当前文件、行和函数,以及失败原因和可选消息。如果config选项:CONFIG_ZTEST_ASSERT_VERBOSE为0,则断言将只打印文件和行号,从而减少测试的二进制大小。

zassert
函数原型

#define zassert(cond, default_msg, msg, ...)
参数介绍

cond – 表达式
default_msg – 表达式为True时打印的信息
msg – 表达式为False时打印的信息,支持可变参数
函数作用

断言函数

zassert_unreachable
函数原型

#define zassert_unreachable(msg, ...)
参数介绍

msg – 输出信息,支持可变参数
函数作用

这个函数没有表达式参数,当发生异常时可以调用这个函数主动产生断言

示例

zassert_unreachable("An error has occurred here...");
zassert_true
函数原型

#define zassert_true(cond, msg, ...)
参数介绍

cond – 表达式
msg – 输出的信息,支持可变参数
函数作用

这个函数只有在表达式为True时才会产生断言

示例

zassert_true(a == b, "pass a == %d", b)
zassert_false
函数原型

#define zassert_false(cond, msg, ...)
参数介绍

cond – 表达式
msg – 输出的信息,支持可变参数
函数作用

这个函数只有在表达式为False时才会产生断言

示例

zassert_false(a == b, "pass a != %d", b)
zassert_ok
函数原型

#define zassert_ok(cond, msg, ...)
参数介绍

cond – 表达式
msg – 输出的信息,支持可变参数
函数作用

这个函数会检查cond表达式是否为0,无论如何要求cond的值一定等于0,如果不为0则断言,支持可变参数

示例

zassert_ok(init(a,b,c), "Error init");
zassert_is_null
函数原型

#define zassert_is_null(ptt, msg, ...)
参数介绍

ptr – 指针
msg – 输出的信息,支持可变参数
函数作用

这个函数检查指针是否为NULL,如果是,则断言

示例

zassert_is_null(pIw2Cw, "Error pIw2Cw is NULL");
zassert_not_null
函数原型

#define zassert_not_null(ptt, msg, ...)
参数介绍

ptr – 指针
msg – 输出的信息,支持可变参数
函数作用

这个函数要确认指针是否不是NULL,如果不是,则断言

示例

zassert_not_null(pIw2Cw, "Error pIw2Cw not NULL");
zassert_equal
函数原型

#define zassert_equal(a, b, msg, ...)
参数介绍

a – 要比较的a值
b – 要比较的b值
msg – a和b值不相等时产生断言信息,支持可变参数
函数作用

判断a、b两个值是否相等,不相等时产生断言,a和b只会进行比较,它俩的值不会被改变,也不会进行任何类型转换

示例

zassert_equal(a, b, "Error a and b equal");
zassert_not_equal
函数原型

#define zassert_not_equal(a, b, msg, ...)
参数介绍

a – 要比较的a值
b – 要比较的b值
msg – a和b值相等时产生断言信息,支持可变参数
函数作用

判断a、b两个值是否不相等,只有在相等时才会产生断言,a和b只会进行比较,它俩的值不会被改变,也不会进行任何类型转换

示例

zassert_not_equal(a, b, "Error a and b not equal");
zassert_equal_ptr
函数原型

#define zassert_equal_ptr(a, b, msg, ...)
参数介绍

a – 要比较的a指针
b – 要比较的b指针
msg – a和b指针地址不相等时产生断言信息,支持可变参数
函数作用

判断a、b两个指针地址是否相同,比较前会将两个指针转换为void*,只有在不相同时才会产生断言

示例

zassert_equal_ptr(a, b, "False assertion");
zassert_within
函数原型

#define zassert_within(a, b, d, msg, ...)
参数介绍

a – 要判断的a值
b – 要判断的b值
d – 最大数
msg – a和b不在d之内时产生断言信息,支持可变参数
函数作用

判断a、b两个值都在d之内,也就是小于d,如果不在d之内则产生断言

示例

zassert_within(3, 4, 10,"3 and 4 are within 10");
zassert_mem_equal
函数原型

#define zassert_mem_equal(buf, exp, size, msg, ...)
参数介绍

buf – 缓冲区1
exp – 缓冲区2
size – 缓冲区大小,两个缓冲区应具有一样大小
msg – buf和exp缓冲区里的内容不相等时产生断言信息,支持可变参数
函数作用

判断buf和exp两个缓冲区里的内容是否一致,如果不相等则产生断言,它内部调用的是zassert_mem_equal__

示例

zassert_mem_equal(buff1, buff2, 10,"Error data");
函数测试框架
该框架是用于模拟函数,来判断参数以及返回值是否正确,用于模拟一个函数的输入输出,当一个函数出现问题时可以用这套框架来诊断,它会模拟输入一套值,并调用你的函数,当输入参数和输出值不对时会立刻产生断言,你需要在prj.conf中将CONFIG_ZTEST_MOCKING设置为y,它是一整套函数测试框架,预先设置好值然后调用函数当值产生不一致时则会产生断言

ztest_check_expected_value
函数原型

#define ztest_check_expected_value(value)
参数介绍

par – 检查的参数
函数作用

检查参数是否与预期设置一致,配合ztest_expect_value使用

示例

void test(a){
 
    ztest_check_expected_value(a);
 
}
ztest_expect_value
函数原型

#define ztest_expect_value(func, param, value)
参数介绍

func – 要测试的函数
param – 要设置的参数名
value – 预期值
函数作用

设置预期值,该值会在内部以uintptr_t格式存储

示例

void test(a,b){
 
    ztest_check_expected_value(a);
    ztest_check_expected_value(b); //这里产生断言,因为预期ztest_expect_value设置的为3,实际调用时传递进来的是4
 
}
void par_test(){
     
    ztest_expect_value(test, a, 2);
    ztest_expect_value(test, b, 3);
    test(2,4);
 
}
ztest_get_return_value
函数原型

int ztest_get_return_value()
参数介绍

NULL
函数作用

获取当前函数返回值,需要首先使用ztest_returns_value设置返回值

ztest_returns_value
函数原型

#define ztest_returns_value(func,value);
参数介绍

func – 要测试的函数
param – 要设置的参数名
函数作用

获取函数返回值,用来判断函数返回值是否与预期一致,结合上面两个函数,下面代码就是简单的函数测试框架,用于测试一个函数的输入参数与返回值,一旦又任意一个与预期设置的值不同就会断言

如果测试函没有返回值则视为失败,会产生断言,调用此函数之前应调用ztest_returns_value

示例

#include <ztest.h>
 
static void expect_two_parameters(int a, int b)
{
    ztest_check_expected_value(a);  //检查a值是否与ztest_expect_value(expect_two_parameters, a, 2);预期设置的一致
    ztest_check_expected_value(b);  //检查b值是否与ztest_expect_value(expect_two_parameters, b, 3);预期设置的一致
 }
 
static void parameter_tests(void)
{
    ztest_expect_value(expect_two_parameters, a, 2);    //注册函数到框架里,预期a应为2
    ztest_expect_value(expect_two_parameters, b, 3);    //注册函数到框架里,预期b应为3
    expect_two_parameters(2, 3);    //调用
}
 
static int returns_int(void)
{
    return ztest_get_return_value();    //返回ztest_returns_value设置的返回值
  }
 
static void return_value_tests(void)
{
    ztest_returns_value(returns_int, 5);//设置返回值为5
    zassert_equal(returns_int(), 5, NULL);//判断返回值
}
 
void test_main(void)
{
 ztest_test_suite(mock_framework_tests,
        ztest_unit_test(parameter_tests),
        ztest_unit_test(return_value_tests)
    );
 
    ztest_run_test_suite(mock_framework_tests);
}
ztest_get_return_value_ptr
函数原型

void* ztest_get_return_value_ptr()
参数介绍

NULL
函数作用

获取当前函数返回值指针,需要首先使用ztest_returns_value设置返回值,返回的是void*类型

ztest_expect_data
函数原型

#define ztest_expect_data(func, param, data)
参数介绍

func – 要测试的函数
param – 要设置的参数名
data – 数据指针,要设置的数据
函数作用

设置参数,这里的参数可以是char*或其它ptr指针数据流

ztest_check_expected_data
函数原型

#define ztest_check_expected_data(param, length)
参数介绍

param – 要检查的参数
length – 数据长度
函数作用

检查参数,与ztest_expect_data预期设置的是否一致

ztest_return_data
函数原型

#define ztest_return_data(func, param, data)
参数介绍

func – 要设置的函数
param – 保存名
data – 返回的数据,这里是一个指针,指向data的数据(uint_t *格式的指针),保存的是地址,不是值
函数作用

保存一份与param名称一致的数据存储到内存当中,需要注意的是这里的return,指的不是函数里的return,指的是它可以存储一份data数据,数据流

ztest_copy_return_data
函数原型

#define ztest_copy_return_data(param, length)
参数介绍

param – 参数指针
lenght – 参数缓冲区长度
函数作用

将param参数名的数据copy出来,并将它从内存中删除

示例

void test_func(uint_t a,uint_t b){
     
    uint_t test_data = 0;
    ztest_copy_return_data( test_data,sizeof(uint_t));
    printk("%d ",test_data);   //这里是12
}
 
void test_return_data(){
 
   static uint_t test_data = 12;
   ztest_return_data(test_func, test_data,&test_data);
     
 
}
函数测试框架原理
这里以之前的代码做示例:

#include <ztest.h>
 
static void expect_two_parameters(int a, int b)
{
    ztest_check_expected_value(a);  //检查a值是否与ztest_expect_value(expect_two_parameters, a, 2);预期设置的一致
    ztest_check_expected_value(b);  //检查b值是否与ztest_expect_value(expect_two_parameters, b, 3);预期设置的一致
 }
 
static void parameter_tests(void)
{
    ztest_expect_value(expect_two_parameters, a, 2);    //注册函数到框架里,预期a应为2
    ztest_expect_value(expect_two_parameters, b, 3);    //注册函数到框架里,预期b应为3
    expect_two_parameters(2, 3);    //调用
}
这里有一个问题,就是zephyr的ztest框架里的ztest_check_expected_value函数怎么知道我们调用了expect_two_parameters函数?

这个很简单,首先当我们调用ztest_expect_value时,它对参数做了转换,用c语言的#符号,这个符号的作用就是将参数名转化为字符串,然后它在插入到内存里,用这个作为标记

我们将ztest_expect_value拆开看下:

#define ztest_expect_value(func, param, value)                                
        z_ztest_expect_value(STRINGIFY(func), STRINGIFY(param),                
                             (uintptr_t)(value))
可以看到它将func用STRINGIFY宏函数包含起来了,而STRINGIF宏函数展开就是#define STRINGIF(x) #x,param也一样,那么保存到内存里以后就知道了要判断的函数以及参数名,最后就是value用uintptr_t存储起来了是个指针,并指向这个地址空间

我们可以看下z_ztest_expect_value的实际调用:

void z_ztest_expect_value(const char *fn, const char *name, uintptr_t val)
{
        insert_value(&parameter_list, fn, name, val);
}
它调用了insert_value来插入到parameter_list里,parameter_list是一个全局的链表,动态链表

可以看到它的两个参数都是char*,由于#符号的作用,函数名和参数名都变成了字符串保存起来了

把insert_value拆开就可以看到它存储了name和func的名称:

static void insert_value(struct parameter *param, const char *fn,
                         const char *name, uintptr_t val)
{
        struct parameter *value;
 
        value = alloc_parameter();
        value->fn = fn;
        value->name = name;
        value->value = val;
 
        /* Seek to end of linked list to ensure correct discovery order in find_and_delete_value */
        while (param->next) {
                param = param->next;
        }
 
        /* Append to end of linked list */
        value->next = param->next;
        param->next = value;
}

最后当我们调用ztest_check_expected_value的时候可以展开看一下:

#define ztest_check_expected_value(param)                                      
        z_ztest_check_expected_value(__func__, STRINGIFY(param),              
                                     (uintptr_t)(param))
重点看:__func__,这个宏是GCC自带的宏,表示当前函数名,这样就可以知道当前调用的函数是哪个了,所以ztest_check_expected_value就给了一个参数,然后将param转化为字符串,在对param进行指针操作

void z_ztest_check_expected_value(const char *fn, const char *name,
                                  uintptr_t val)
 
{
        struct parameter *param;
        uintptr_t expected;
 
        param = find_and_delete_value(&parameter_list, fn, name);
        if (!param) {
                PRINT("Failed to find parameter %s for %s ", name, fn);
                ztest_test_fail();
        }
 
        expected = param->value;
        free_parameter(param);
 
        if (expected != val) {
                /* We need to cast these values since the toolchain doesn't
                 * provide inttypes.h
                 */
                PRINT("%s:%s received wrong value: Got %lu, expected %lu ", fn,
                      name, (unsigned long)val, (unsigned long)expected);
                ztest_test_fail();
        }
}
注意find_and_delete_value,它找到以后就会删除掉对应的索引,所以检测只能应用于一次

找到了以后在将两个值进行一下比较就知道传递进来的参数值是否一致了

最后注意这里:

ztest_expect_value(expect_two_parameters, a, 2);
由于最后一个参数值应该是指针,当我们传递进去"2"时,编译器隐式将2转化为了一个int 2,最后指针指向这个地址,由于在函数里parameter_tests调用,且expect_two_parameters也是在这个函数里发生调用的,只要parameter_tests函数没有返回,这些栈就有效

单元测试框架的基本组成
一个Test Core基本由下面几个文件组成:

文件名

作用

文件名

作用

CMakeList.txt    构建脚本
testcase.yaml/sample.yaml    Case测试配置文件(仅供twister使用)
main.c    测试代码
prj.conf    项目配置文件
CMakeList.txt
CMakeList会被Zephyr的编译脚本调用,最终链接生成.bin文件,下面是一个简单的CMake示例:

cmake_minimum_required(VERSION 3.20.0)      # 设置编译最低版本
find_package(Zephyr REQUIRED HINTS $ENV{ZEPHYR_BASE})   #将Zephyr的依赖SDK包含进来
project(integration)    # 设置工程名称
 
FILE(GLOB app_sources src/*.c)  # 将所有的.c文件添加到编译列表里
target_sources(app PRIVATE ${app_sources})  # 目标生成
testcase.yaml/sample.yaml
testcase.yam/sample.yaml文件是用于描述这个case应该在什么样的环境下进行test,一般供twister(Zephyr下使用Python编写的自动化test case脚本,解析testcase.yaml文件来针对当前的case构建不同的环境)调用,一个Test Case环境下理应有一个testcase.yam或者sample.yaml描述当前测试属性,两个文件作用一致,一个项目里只能有一个文件,这个原因是因为向前兼容,在早期的Zephyr之前的版本里使用的是sample.yaml作为Test Case描述文件,后来改成testcase.yaml,但同时为了向前兼容保留了sample.yaml。

一个简单的示例如下:

tests:
  testing.ztest:
    build_only: true
    platform_allow: stm32f746g_disco
    tags: testing
键值介绍
sample

简介:用于描述当前测试Case

可选子键:

键值

格式

作用

name
string    描述当前Test Case项目名称
description
string
描述当前Test Case项目简介
tests

简介:定义Test Case,它的命名规则在Zephyr官方是这么说的:

测试标识符的格式应是一个字符串,不包含任何空格或特殊字符(允许的字符:字母数字和 [_=]),由多个部分组成,以点 (.) 分隔。
每个测试标识符应以一个部分开始,然后是一个用点分隔的子部分。例如,涵盖内核中信号量的测试应以kernel.sempahore.
testcase.yaml 文件中的所有测试标识符都必须是唯一的。例如,覆盖内核中信号量的 testcase.yaml 文件可以具有:
kernel.semaphore: 用于一般信号量测试
kernel.semaphore.stress: 对内核中的信号量进行压力测试。
根据测试的性质,标识符可以至少包含两个部分:
Ztest 测试:ztest 测试套件中的各个测试用例将连接到 testcase.yaml 文件中的标识符,为套件中的每个测试用例生成唯一标识符。
独立测试和示例:此类测试在 testcase.yaml(或 sample.yaml)文件中的测试标识符中至少应包含 3 个部分。名称的最后一部分应表示测试本身。
也就是说你的命名理应是:属于系统中哪部分(如果是属于内核则:kernel,驱动则Driver).测试属性.测试目的

比如我要测试一个i2c的驱动,目的是测试它的可行性,则命名:

driver.i2c.read

当然你可以不用这么命名,这只是zephyr官方建议以这个标准命名你的case

可选子键:

键值

格式/示例

作用

build_only    true | false    只编译不运行,这个选项如果为true的情况下,测试脚本只会将对其进行编译,但不会对其进行test,一般这个选项用于测试某些项目能否在指定平台上编译通过
platform_allow    board name    指定测试的平台,如果没有真实硬件board,可以使用qemu_*来作为虚拟测试环境
tags    tags | tags1 tags2 ...    分类,这个主要是将测试集进行分类,如:当有多个测试集的时候我只想测试某一类的测试集,就可以指定tags来测试,多个标签使用空格分开
skip    true | false    测试时无条件跳过这个case
slow    true | false    在调用自动化测试脚本时除非传入--enable-slow才会运行并测试这个case,否则只编译这个case不会去运行与测试
extra_args    args | args1 args2 ...    在构建case时加入一些参数选项,如:CONF_FILE="prj_br.conf",多个参数用空格分开
extra_configs    
- CONFIG_X=y
这个参数与prj.conf匹配,用于加入额外的config示例:

1

2

3

4

test_async:

    extra_configs:

      - CONFIG_ADC_ASYNC=y

      - CONFIG_PWM=y

build_on_all    true | false    如果为true,则在目前zephyr支持的所有平台上进行case测试
depends_on    adc | adc i2c ...    当前case在测试平台下依赖哪些功能,如果这个平台上没有这些功能则会测试失败,如需要指定平台上有adc driver,如果没有则默认失败
min_ram    int    对测试平台的ram最小内存为多少,单位是kb
min_flash    int    测试平台上最低运行内存为多少,比如你的程序是运行在flash里的,那么flash的内存不能低于这个数,单位是kb
timeout    int    在QEMU中运行测试的时间长度,然后自动终止测试(默认为60秒)
arch_allow    arm | arm x86 ...    只能在指定平台上运行
arch_exclude    
arm | arm x86 ...

不能在指定平台上运行
platform_allow    arm | arm x86 ...    
这个case只能在指定平台上运行,当这个属性上设置平台之后那么twister会读取它,当测试平台不在这个属性列表里的话不会运行,但会编译之后的test里会跳过它运行,同时当使用twister不去指定平台的话则默认选择这个子健的第一个平台

在测试用例中如果不是这个平台上的case会跳过,但不会计算到Error里,同时构建时不会报错,通俗易懂的说就是:如果测试时平台不在这个属性列表里则参与CI测试但不运行,会被跳过,并且这条case不会计算在Error里,也不会计算为Pass

integration_platforms    
- qemu_x86

这个属性与platform_allow类似,它的作用是在测试case时不需要去用-p指定平台,会自动化测试这个属性下的所有平台,并且如果使用-p去指定平台时会在构建阶段就报错

1

2

3

4

test_async:      

    integration_platforms:

        - qemu_x86

        - stm32f4_disco

platform_exclude    arm | arm x86 ...    这条case不能在这些平台上运行,如果指定了这些平台则构建阶段就会报错
特殊子健:

键值

格式

作用

harness    string    成功运行这条case的线索源,搭配harness_config使用
harness是用来从指定输出端口中取到string信息,并从中获取指定字符串来判断这条case是否正确,因为某些case可能是处于loop back或者其它方式来工作的,有一整套流程逻辑,不能单纯判断API的正确性,比如某条case在成功运行时会输出一条:“The operation is completed and the test is correct!”的字符串,那么我们就可以通过这个方式来判断case的结果,harness的值是用来告诉测试框架从哪里取到输出字符串信息。

如:harness: console 这个属性设置代表从控制台输出获取字符串,也就是zephyr里调用printk输出时的指向,这个可以在dts里找到定义,因为zephyr在运行时也会虚拟一个输出源,也就是默认的控制台缓冲区,这个在zephyr的dts里有个属性叫:zephyr,console,通常你可以在dts里看到这样的定义:zephyr,console = &usart3; 这句定义代表console指向usart3,那么当调用printk时zephyr会调用usart driver往usart3里输入字符串。

最后在使用harness_config属性来配置正则表达式信息:

type: <one_line|multi_line> (必须)

one_line:一行一行匹配, 为结尾

multi_line:多行匹配,为结尾

regex: <expression> (必须)

要匹配的字符串,格式如下(字符串允许包含正则表达式):

regex:
        - "Hello word"
        - "Complete"
ordered: <True|False> (不是必须的,默认False)

在正则表达式匹配字符串时,按照顺序匹配还是随机匹配,True为顺序,False为随机

如当获取到字符串输出时,如果为True,则将regex里的字符串列表从第一个开始往下匹配直到全部匹配完,如果为False则随机在regex里随机抽查一个进行匹配,但不会全部匹配

repeat: <integer>(不是必须的,默认为1)

重复匹配字符串的次数,每次匹配时利用重复匹配多少次

fixture: <expression>(不是必须的)

指定测试用例对平台设备的依赖关系,如当前平台需要依赖哪些装置或传感器,如:i2c_hts221、i2c_bem280,如果没有这些设备则视为失败。

每个测试用例只能定义一个fixture。

pytest_root: <pytest directory> (默认./samples/subsys/testsuite/pytest)
用于判断输出结果的,harness可以设置为pytest,当测试结束时会调用pytest来工作,然后根据pytest里的输出来匹配字符串来决定是否正确,详细可以参加:samples/subsys/testsuite/pytest,一般不用动这个,这个是zephyr留给开发者们测试py的,如果你没有任何py代码就不需要去管这个文件,同时它文件夹下也有一个main.c文件,它里面的输出也可以用来做判断

示例:

从终端判断有指定输出算正确,并且要求有i2c_bme280传感器

tests:
  test:
    harness: console
    tags: sensors
    depends_on: i2c
    harness_config:
        type: multi_line
        ordered: false
        regex:
            - "Temperature:(.*)C"
            - "Relative Humidity:(.*)%"
        fixture: i2c_bme280
从pytest里判断

tests:
  test:
    harness: pytest
    tags: sensors
    depends_on: i2c
    harness_config:
        type: multi_line
        ordered: false
        regex:
            - "Hello world"
        fixture: i2c_bme280
你可以在pytest的目录下,src/main.c里看到有一行输出是Hello world,用这段输出来判断Case的完整性,比如你的某条Case会改变硬件的状态,你可以在Case执行完成之后在Pytest里来验证一下。

键值

格式

作用

filter    expression    过滤器,用来设置一些参数属性过滤一掉一些包含这些属性的case
它支持的运算符如下:

():优先级运算符

and:和运算符

or:或运算符

not:非表达式(c语言里的!)

==:等于

!=:不等于

>:大于

<:小于

in:存在于列表中

filter:not ARCH in [“x86”, “arc”]
::字符串

filter:CONFIG_SOC : “stm.*”
简单示例:

tests:
  kernel.memory_protection.mem_map:
    tags: kernel mmu ignore_faults
    filter: CONFIG_MMU and (not CONFIG_X86_64) #这里判断的config=y的为true,否则false
    extra_sections: _TRANSPLANTED_FUNC
    platform_exclude: qemu_x86_64
common

它的子键与tests一致,具体可以参考tests,但是它不能定义测试case,它能定义一些属性,这些属性会适用于tests下的所有case

示例:

common:
    tags: hello
tests:
  test1.testing:
    tags: hello1
  test2.testing:
    tags:hello2
当加上common时,test1和test2都会有一个共同的tags:hello

prj.conf
这个文件用于开启测试一些config,因为要用到ztest框架,所以一定要设置CONFIG_ZTEST=y,你也可以在这里来进行一些配置,开启Zephyr内核的一些功能来满足case条件

如下就是一个简单的prj,conf文件,它开启了I2C与ztest功能的支持

CONFIG_I2C=y
CONFIG_ZTEST=y
src目录
这里存放Test Case的文件,注意仅支持c/c++代码,如下是一个简单的ztest的测试代码:

#include <ztest.h>
 
static void test_assert(void)
{
        zassert_true(1, "1 was false");
        zassert_false(0, "0 was true");
        zassert_is_null(NULL, "NULL was not NULL");
        zassert_not_null("foo", ""foo" was NULL");
        zassert_equal(1, 1, "1 was not equal to 1");
        zassert_equal_ptr(NULL, NULL, "NULL was not equal to NULL");
}
 
void test_main(void)
{
        ztest_test_suite(framework_tests,
                ztest_unit_test(test_assert)
        );
 
        ztest_run_test_suite(framework_tests);
}

Twister
简介
Twister是Zephyr下提供的CI BUILD自动化测试工具,它依赖于testcase.yaml/sample.yaml与Ztest,它会根据执行结果生成对应详细的结果报告,同时它支持qemu以及在真实的硬件环境下进行test case,它是使用Python编写而成,位于$ZEPHYRHOME/scripts/twister。

注意事项
Twister需要src文件参与单元测试的函数命名全部以“test_”开头
Twister默认错误等级最高,编译时警告也会出现错误
命令行
选项

示例

作用

-h/--help    -help    在终端输出帮助文档
--force-toolchain    --force-toolchain    无条件使用设置工具链
-p/--platform      -p PLATFORM, --platform PLATFORM    设置要测试的平台,如果没有设置此选项且testcase.yaml里没有使用platform_allow之类的属性指定的话则默认使用内置平台构建测试,默认使用qemu模拟芯片
-P/--exclude-platform    -P EXCLUDE_PLATFORM, --exclude-platform EXCLUDE_PLATFORM    排除指定平台不进行测试
-a/--arch     -a ARCH, --arch ARCH    arch过滤器,如果未指定则测试所有arch平台
-t/--TAG     -t TAG, --tag TAG    指定标签运行,这个标签与你的testcase.yaml里的tags相关,所有与这个标签相关的case都会被运行
-e/--exclude-tag     -e EXCLUDE_TAG, --exclude-tag EXCLUDE_TAG    不运行指定标签,与这个标签相关都不会参与ci build
--retry-failed      --retry-failed RETRY_FAILED    当case失败时重复验证次数
--retry-interval      --retry-interval RETRY_INTERVAL    当case失败时间隔多少重复验证
-l/--all    -l, --all    在所有平台上进行测试,忽略testcase.yaml里的平台限制参数
-o/--report-dir     -o REPORT_DIR, --report-dir REPORT_DIR    指定输出结果输出目录,输出为csv和JUNIT格式的xml文件
--json-report    --json-report    生成json格式
 --platform-reports     --platform-reports    为每个平台创建单独的报告
--report-name     --report-name REPORT_NAME    使用自定义名称创建报告
 --report-suffix    --report-suffix REPORT_SUFFIX    为所有生成的文件名添加后缀,例如添加版本或提交ID
--report excluded    --report excluded    根据当前范围和覆盖率列出从未运行的所有测试
 --compare-report     --compare-report COMPARE_REPORT    使用此报告文件进行大小比较
 -B/--subset     -B SUBSET, --subset SUBSET    只运行测试的一个子集,1/4表示运行前25%,3/5表示运行总数的第三个五分之一
-N/--ninja    -N, --ninja    使用ninja编译器构建
-y/--dry-run     -y, --dry-run    
创建过滤后的测试用例列表,但不要实际运行它们

--list-tags    --list-tags    列出选定测试中的所有tags
--export-tests    --export-tests FILENAME    
将测试用例元数据导出到CSV格式的文件中,平台名称使用--platform选项

--timestamps    --timestamps    输出信息时附带时间戳
-r/--release    -r, --release    使用此测试运行的结果更新基准数据库。
在标记正式版本时由 CI 运行。
在查找诸如足迹等指标中的增量时,此数据库用作比较的基础
-W/--disable-warnings-as-errors    -W, --disable-warnings-as-errors    将警告条件视为错误
--overflow-as-errors    --overflow-as-errors    将RAM/SRAM溢出视为错误
-v/--verbose    -v, --verbose    输出调试信息,后面的v代表等级,v越多越详细,如:-vvv
-i/--inline-logs     -i, --inline-logs     在测试失败时,将相关日志数据打印到stdout,而不仅仅写入log文件中
--log-file    --log-file FILENAME    输出log到指定文件
-m/--last-metrics     -m, --last-metrics     与上一个版本的指标进行比较,而不是与上一个twister的结果进行比较
 -u/--no-update     -u, --no-update    不要更新,运行最后一次输出结果
 -G/--integration      -G, --integration     集成运行集成测试
--quarantine-list     --quarantine-list FILENAME    隔离指定文件,这个文件不参与构建
--quarantine-verify    --quarantine-verify    使用隔离下的测试场景列表并运行它们来验证它们的当前状态
-b/--build-only    -b, --build-only    仅构建代码,不要在 QEMU 中执行任何代码
--test-only    --test-only    仅运行设备测试使用上一次构建的项目,不要构建新代码
--cmake-only    --cmake-only    只运行 cmake,不要make和运行
--filter    --filter {buildable,runnable}    
过滤要构建和执行的测试。默认情况下,所有内容都已构建,如果测试可运行(仿真或
连接的设备),则运行它。例如,此选项允许仅构建可以实际运行的测试。
Runnable 是 buildable 的一个子集,具体可以参考testcase.yaml里的filter键介绍

-M/--runtime-artifact-cleanup    -M, --runtime-artifact-cleanup    删除通过测试的项目
-j/--jobs    -j JOBS, --jobs JOBS    
用于构建的作业数,默认为 CPU 线程数

当 --build-only、--show-footprint 显示自上次发布以来的占用统计信息和增量时,被因子 2 过度使用。
-H FOOTPRINT_THRESHOLD, --footprint-threshold FOOTPRINT_THRESHOLD
检查测试用例足迹大小时,如果新应用程序大小大于
上一个版本的指定百分比,则警告用户。默认值为 5。0 警告应用程序大小的任何增加

-D/--all-deltas    -D,--all-deltas    显示所有足迹增量,正或负
-O OUTDIR/--outdir OUTDIR    -O OUTDIR, --outdir OUTDIR    日志和二进制文件的输出目录。默认为当前目录中的“twister-out”
-c/--clobber-output    -c, --clobber-output    清理输出目录只会将其删除,而不是默认的重命名策略
-n/--no-clean    -n, --no-clean    在构建之前重新使用 outdir。将导致更快的编译,因为构建将是增量的
-A/--board-root    -A BOARD_ROOT, --board-root BOARD_ROOT    搜索板子配置文件的目录。将处理目录中的所有 .yaml 文件。该
目录在 Zephyr 主树中应该具有相同的结构:boards///
-z/--size SIZE    -z SIZE, --size SIZE    不要运行twister,同时向标准输出生成一份报告,详细说明指定文件名的 RAM/ROM 大小
-S/--enable-slow    -S, --enable-slow    执行在 testcase.yaml 中被标记为'slow'的耗时测试用例
-K/--force-platform    -K, --force-platform    在选定平台上强制测试,即使它们被排除在测试配置 (testcase.yaml)
--disable-unrecognized-section-test    --disable-unrecognized-section-test    跳过“无法识别的部分”测试
-R/--enable-asserts    -R,--enable-asserts    已弃用,为兼容性而保留
--disable-asserts    --disable-asserts    已弃用,为兼容性而保留
-Q/--error-on-deprecations    -Q,--error-on-deprecations    弃用警告错误
--enable-size-report    --enable-size-report    启用对 RAM/ROM 段大小的计算
-x /--extra-args    -x EXTRA_ARGS, --extra-args EXTRA_ARGS    构建测试用例时要定义的额外 CMake 缓存条目。可以多次调用。在传递给 CMake 之前,键值
条目将以 -D 为前缀。例如,“twister -x=USE_CCACHE=0”将转换为
“cmake -DUSE_CCACHE=0”,最终将禁用 ccache。
--emulation-only    --emulation-only     仅构建和运行仿真平台
--device-testing    --device-testing    直接在设备上测试,需要使用 --device-serial 选项指定要使用的串行设备
-X/--fixture    -X FIXTURE, --fixture FIXTURE    指定电路板可能支持的固定装置
--device-serial-baud    串行设备波特率(默认 115200)    串行设备波特率(默认 115200)
--device-serial    --device-serial DEVICE_SERIAL    用于访问电路板的串行设备(例如,/dev/ ttyACM0)
--device-serial-pty    --device-serial-pty DEVICE_SERIAL_PTY    用于控制伪终端的脚本。Twister 认为,当它实际
与脚本交互时,它与终端交互。例如 "twister --device-testing --device-serial-pty
--generate-hardware-map GENERATE_HARDWARE_MAP
探测连接到该平台的串行设备并创建一个硬件映射文件以与 --device-testing
--persistent-hardware一起使用-map
使用--generate-hardware-map,尝试在支持此
功能的平台(目前仅Linux)上为串行设备使用持久名称
--hardware-map    --hardware-map HARDWARE_MAP    从文件加载硬件映射,这将用于测试在文件中列出的硬件
--pre-script PRE_SCRIPT    --pre-script PRE_SCRIPT    指定一个预脚本,这将被执行
-T/--testcase-root    -T CASE_DIR , --testcase-root CASE_DIR    指定一个存放case项目的目录,ci会自动化遍历这个目录下的所有case项目,读取testcase.yaml/sample.yaml进行自动化测试
-s/--test    -s CASE_DIR/TESTCASE_YAML_CASE_NAME , --test CASE_DIR/TESTCASE_YAML_CASE_NAME    指定一个项目里的case,指定的名字是testcase.yaml里tests下的定义
--west-flash    --west-flash="FLASH"    
自定义烧录,当你的设备无法使用west flash下载时可以用这个参数来指定你的自定义下载,示例:

假设我们的开发板当前可用通过pyocd来烧录那么可用使用如下命令:

1

--west-flash="pyocd flash"

如果是你的自定义脚本可以使用如下命令:

1

--west-flash="<Full path>/your sc"

这个方法仅限twister,如果你想主动为board自定义烧录需要修改board目录下的board.cmake文件,比如stm32f746g_disco:board/arm/stm32f746g_disco/board.cmake,如果有多个可以使用--runner指定

--west-runner    --west-runner="RUN"    这个选项是自定义运行,需要在--west-flash启用时这个选项才有效,使用用法与--west-flash一致
测试结果输出目录与结构
如果你没有使用-O OUTDIR/--outdir OUTDIR参数来改变输出目录的情况下那么默认输出是在你调用twister的目录下

输出的目录名为:twister-out

一般进行测试过之后会有如下几个文件与目录:

目录/文件

作用

test_platform    
这是个目录,这个目录名称为当前测试的平台,这个目录下会有针对这个平台上case的详细测试信息

这个目录可能有很多个,也可能只有一个,看你在测试时的平台数量

twister.csv    csv格式的测试信息
twister.log    测试时的log,这里的log不属于case,属于twister脚本自己的log,log里记录了它调用了什么,执行了哪些文件
twister_report.xml    xml格式的报告信息
twister.xml    xml格式的报告信息
比如我执行了一次测试:

./scripts/twister -p qemu_x86 -s samples/subsys/testsuite/integration/testing.ztest
你可以到当前文件夹下的twister-out目录下查看:

qemu_x86  twister.csv  twister.log  twister_report.xml  twister.xml
qemu_x86就是我们刚刚用-p指定的平台,将它展开:

qemu_x86/samples/subsys/testsuite/integration/testing.ztest
可以看到它下面的子目录命名规则与我们测试的case名一致,当有多个case时会在这个目录下生成不同的文件

比如我们打开yaml文件添加一条新的case规则:

tests:
  testing.ztest:
    build_only: false
    integration_platforms:
            - qemu_x86
            - stm32f4_disco
    tags: testing
#新添加一条
  testing.ztest.hello:
    build_only: false
    tags: testing
然后执行,这里用-T执行整个testcase.yaml,如果用-s的话只能指定一条case

./scripts/twister -p qemu_x86 -T samples/subsys/testsuite/integration/
然后进入到刚刚的目录下可以查看到出现了刚刚添加case的目录名

testing.ztest  testing.ztest.hello
我们进入到任意一个case输出目录下可以看到如下几个文件:

app        CMakeCache.txt  cmake_install.cmake  Kconfig   modules  zephyr_modules.txt
build.log  CMakeFiles      handler.log          Makefile  zephyr   zephyr_settings.txt
其它几个文件可以是构建生成的文件,这里我们只需要重点关注三个文件:

build.log:build过程

handler.log:测试过程

device.log:烧录过程(这个文件仅针对物理设备有效,qemu虚拟环境不会生成)

这几个文件就是我们需要的测试信息,你可以打开handler.log看到test信息,里面的输出信息与你的ztest代码一致

Booting from ROM..*** Booting Zephyr OS build v2.7.99-927-g6b4a2d3e4749  ***
Running test suite framework_tests
===================================================================
START - test_assert
 PASS - test_assert in 0.1 seconds
===================================================================
Test suite framework_tests succeeded
===================================================================
PROJECT EXECUTION SUCCESSFUL
需要值得注意的是如果你下次编译时twister-out目录没有删除,那么会生成新的twister-out.*目录,这里的*是下标,会是1,2,3这样的下标的副本目录,这里面存放的也是调试信息,但我们只需要关注twister-out就可以了

Qemu模拟测试
Qemu的模拟测试非常简单,你可以用ls命令,查看zephyr/boards/目录下的名称,搜索关于qemu的目录,如果有则代表可以模拟的硬件环境,如下是一个简单的模拟例子:

在tests/kernel/comm目录下包含了通用测试case,这个项目下的case不依赖一些特殊硬件,都是通用的芯片测试case,可以用它来做测试

如果你不用-p指定平台的话,twister会默认将qemu能模拟的芯片全部模拟测试一遍

./scripts/twister -T tests/kernel/common
或者你可以指定一个模拟x86芯片的测试

./scripts/twister -p qemu_x86 -s samples/subsys/testsuite/integration/testing.ztest
需要值得注意的是qemu无法模拟一些硬件,如i2c,spi,只能模拟芯片,如果你想要测试这些硬件的话需要接入硬件机器进行测试

输出如下:

INFO    - Zephyr version: v2.7.99-927-g6b4a2d3e4749
INFO    - JOBS: 16
INFO    - Using 'zephyr' toolchain.
INFO    - Building initial testcase list...
INFO    - 1 test scenarios (1 configurations) selected, 0 configurations discarded due to filters.
INFO    - Adding tasks to the queue...
INFO    - Added initial list of jobs to queue
INFO    - Total complete:    1/   1  100%  skipped:    0, failed:    0
INFO    - 1 of 1 test configurations passed (100.00%), 0 failed, 0 skipped with 0 warnings in 6.87 seconds
INFO    - In total 1 test cases were executed, 0 skipped on 1 out of total 403 platforms (0.25%)
INFO    - 1 test configurations executed on platforms, 0 test configurations were only built.
INFO    - Saving reports...
INFO    - Writing xunit report /home/zhihao/zephyrproject/zephyr/twister-out/twister.xml...
INFO    - Writing xunit report /home/zhihao/zephyrproject/zephyr/twister-out/twister_report.xml...
INFO    - Run completed

能够具体的看到pass、skip、failed的占用率,还有case的测试信息,具体信息可以在twister-out里找到

开发板测试
准备工作
在进行开发板测试时,你需要确保你的开发板能够被west flash正确烧录,目前west flash使用的是jtag,确保你的开发板接到你的host主机上并且按照了jtag驱动,west flash能够使用,或你的开发板是ST-LINK也能被west识别,如果不能可以使用--west-flash命令来自定义你的下载
你需要确保你的开发板有串口输出并且接到了host主机上,同时这个串口能够被正常读写,并且这个串口要求不能被其它设备占用,因为ztest框架的输出默认是使用printk输出,而printk依赖于dts里的console配置,因为当烧录到硬件代码中之后就在开发板中运行了,twister与开发板交互并读取输出信息的方式就是串口
在stm32f746g_disco上进行测试
这里的--device-testing在选项参数介绍时就说过,如果要在真实的环境下测试需要加这个参数,那么就会进行烧录与测试,同时也需要指定串口,通过串口进行交互读取内容生成报告信息

./scripts/twister -p stm32f746g_disco --device-testing --device-serial /dev/ttyCOM1 -T tests/kernel/common
samples/subsys/testsuite/
这个目录下存放了一些初始化的Test Case,每次你调用twister时它会首先去执行构建这个目录下的所有Test Case,但是不记作你的测试项目里,只是它用于初始化调用,如果这个目录下的Case有任何错误都会使当前的测试失败,它就相当于初始化代码,你可以在这个目录下新建一个Test Case项目用于初始化测试,同时这个目录下的integration/测试Case是最一个简单的Demo示例
 

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。