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

Posted 17岁boy想当攻城狮

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Zephyr单元测试框架:ztest/twister的使用和介绍相关的知识,希望对你有一定的参考价值。

目录

简介

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>文件中

注意事项

  1. 若要使用ztest框架需要在prj.conf中将CONFIG_ZTEST设置为y
  2. main函数应该命名为:test_main(强制性的)
  3. 测试函数都应以"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值
  • – 要比较的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值
  • – 要比较的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指针
  • – 要比较的b指针
  • msg – a和b指针地址不相等时产生断言信息,支持可变参数

函数作用

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

示例

zassert_equal_ptr(a, b, "False assertion");

zassert_within

函数原型

#define zassert_within(a, b, d, msg, ...)

参数介绍

  • – 要判断的a值
  • – 要判断的b值
  • – 最大数
  • 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\\n",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\\n", 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\\n", 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.yamlCase测试配置文件(仅供twister使用)
main.c测试代码
prj.conf项目配置文件

CMakeList.txt

CMakeList会被Zephyr的编译脚本调用,最终链接生成.bin文件,下面是一个简单的CMake示例:

cmake_minimum_required(VERSION 3.20.0)      # 设置编译最低版本
find_package(Zephyr REQUIRED HINTS $ENVZEPHYR_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官方是这么说的:

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

也就是说你的命名理应是:属于系统中哪部分(如果是属于内核则:kernel,驱动则Driver).测试属性.测试目的

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

driver.i2c.read

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

可选子键:

键值

格式/示例

作用

build_onlytrue | false只编译不运行,这个选项如果为true的情况下,测试脚本只会将对其进行编译,但不会对其进行test,一般这个选项用于测试某些项目能否在指定平台上编译通过
platform_allowboard name指定测试的平台,如果没有真实硬件board,可以使用qemu_*来作为虚拟测试环境
tagstags | tags1 tags2 ...分类,这个主要是将测试集进行分类,如:当有多个测试集的时候我只想测试某一类的测试集,就可以指定tags来测试,多个标签使用空格分开
skiptrue | false测试时无条件跳过这个case
slowtrue | false在调用自动化测试脚本时除非传入--enable-slow才会运行并测试这个case,否则只编译这个case不会去运行与测试
extra_argsargs | 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_alltrue | false如果为true,则在目前zephyr支持的所有平台上进行case测试
depends_onadc | adc i2c ...当前case在测试平台下依赖哪些功能,如果这个平台上没有这些功能则会测试失败,如需要指定平台上有adc driver,如果没有则默认失败
min_ramint对测试平台的ram最小内存为多少,单位是kb
min_flashint测试平台上最低运行内存为多少,比如你的程序是运行在flash里的,那么flash的内存不能低于这个数,单位是kb
timeoutint在QEMU中运行测试的时间长度,然后自动终止测试(默认为60秒)
arch_allowarm | arm x86 ...只能在指定平台上运行
arch_exclude

arm | arm x86 ...

不能在指定平台上运行
platform_allowarm | 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_excludearm | arm x86 ...这条case不能在这些平台上运行,如果指定了这些平台则构建阶段就会报错

特殊子健:

键值

格式

作用

harnessstring成功运行这条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:一行一行匹配,\\n为结尾

multi_line:多行匹配,\\0为结尾

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里来验证一下。

键值

格式

作用

filterexpression过滤器,用来设置一些参数属性过滤一掉一些包含这些属性的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。

注意事项

  1. Twister需要src文件参与单元测试的函数命名全部以“test_”开头
  2. 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 ARCHarch过滤器,如果未指定则测试所有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选项

以上是关于Zephyr单元测试框架:ztest/twister的使用和介绍的主要内容,如果未能解决你的问题,请参考以下文章

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

Zephyr驱动程序框架简介

第六章:单元测试框架unittest

JUnitJava 单元测试框架 | 学习笔记

单元测试框架-Unittest

如何找出软件挂起的原因? (qemu + zephyr + tfm 的问题)

(c)2006-2024 SYSTEM All Rights Reserved IT常识