CTEST实现

对于本人来说,单元测试框架要求就是只要有 ASSERT, EQUAL 功能, 用起来顺手就ok。最近发现一个比较酷的 C 单元测试框架 CTEST。 竟然 400 多行代码实现了单元测试框架的基本功能,真的很酷。而实现功能的核心代码就实际上也就只有几十行,后面来看下怎么实现的。

1) 如何使用?

我想 CTESTREADME 解释的会比我更清楚。

2) 实现

CTEST(suite, test) {
    ASSERT_STR("foo", "bar");
}

CTEST 是用一个宏来实现一个测试用例的函数。那么问题来了,框架要怎么知道哪些是需要跑的用例函数呢?

CTEST 实现很简洁并富有技巧,利用预处理和自定义数据段,将这些用例集中到一个自定义的数据段。我们先来看看 CTEST 干了什么?

#define CTEST(sname, tname) __CTEST_INTERNAL(sname, tname, 0)

#define __CTEST_INTERNAL(sname, tname, _skip) \
    void __FNAME(sname, tname)(); \
    __CTEST_STRUCT(sname, tname, _skip, NULL, NULL, NULL) \
    void __FNAME(sname, tname)()

CTEST 这个宏只是调用了 __CTEST_INTERNAL 这个宏,我们加上我们函数展开来看,上面自定义的用例就变成如下 :

void __FNAME(suite, test)(); \
__CTEST_STRUCT(suite, test, _skip, NULL, NULL, NULL) \
 void __FNAME(suite, test)() {
    ASSERT_STR("foo", "bar");
 }

这里又用到 __FNAME__CTEST_STRUCT 这两个宏, __FNAME 实现只是字符串拼接, __CTEST_STRUCT 是定义一个全局的 ctest 结构,这个是实现的关键点.

#define __FNAME(sname, tname) __ctest_##sname##_##tname##_run

#define __CTEST_STRUCT(sname, tname, _skip, __data, __setup, __teardown) \
    struct ctest __TNAME(sname, tname) __Test_Section = { \
        .ssname=#sname, \
        .ttname=#tname, \
        .run = __FNAME(sname, tname), \
        .skip = _skip, \
        .data = __data, \
        .setup = (SetupFunc)__setup,                    \
        .teardown = (TearDownFunc)__teardown,               \
        .magic = __CTEST_MAGIC };

可以看到, CTEST 整个宏的实现,其实就是为了生成一个全局的 ctest 结构,并生成一个对于对应的 run 函数。

一般来说,我们可以粗略的认为,进程内部会有几个段, .text, .data, .bss, .rodata, .comment... 初始化过的全局变量会放到 .data 段。可以通过 gcc 的 __attribute__(("section_name")) 来自定义一个新的数据段。

#ifdef __APPLE__
#define __Test_Section __attribute__ ((unused,section ("__DATA, .ctest")))
#else
#define __Test_Section __attribute__ ((unused,section (".ctest")))
#endif

这样我们通过的 CTEST 定义所有的 ctest 结构数据都会到 .ctest 这个数据段里面来。

image

接下来就看看,CTEST 如何扫描这个段。

    struct ctest* ctest_begin = &__TNAME(suite, test);
    struct ctest* ctest_end = &__TNAME(suite, test);
    // find begin and end of section by comparing magics
    while (1) {
        struct ctest* t = ctest_begin-1;
        if (t->magic != __CTEST_MAGIC) break;
        ctest_begin--;
    }
    while (1) {
        struct ctest* t = ctest_end+1;
        if (t->magic != __CTEST_MAGIC) break;
        ctest_end++;
    }
    ctest_end++;    // end after last one

我们看到CTEST 是通过 &__TNAME(suite, test); 这个宏来当作起点,这个是 CTEST 提前定义好的 ctest 全局变量。

static CTEST(suite, test) { }

还有一个点,为什么从这个起来,往两个方向扫描。 主要是不同操作系统实现数据段存放方式不一样,有的往上增长,有的是往下。