从一组单元测试中组成一个组合测试套件程序

Compose a combined test-suite program from a collection of unit-tests

我正在构建一个 set of modules with unit-tests,我想自动生成一个组合程序来执行所有测试。

放纵我内心的控制狂,我决定使用 minunit.h 框架并尝试全部手动完成。所以我有两个源文件(还会有更多)

-rw-r--r-- 1 josh None 6192 Aug 21 23:15 io.c
-rw-r--r-- 1 josh None 2341 Aug 22 00:49 st.c

每个人都有一个单元测试嵌入(所以我更容易保持更新,也许这次 使用 它)由 #ifdef TESTMODULE 保护。大纲:

... 
//module code 
... 

#ifdef TESTMODULE 
... 
//unit tests 
... 
int main(){ 
    // call unit tests 
} 
#endif 

然后我可以用一个非常短的文件制作一个测试程序。

$ cat > io_test.c
#define TESTMODULE
#include "io.c"
$ make io_test
cc     io_test.c   -o io_test

我使用相同类型的包含手动编写了一个组合程序,但重新定义了任何全局名称以使其唯一。

# define main io_main 
# define tests_run io_tests_run 
# define all_tests io_all_tests 
# include "io_test.c" 
# undef main 
# undef tests_run 
# undef all_tests 
int io_test(){ 
    printf("running io_test\n"); 
    return io_main(); 
} 

# define main st_main 
# define tests_run st_tests_run 
# define all_tests st_all_tests 
# include "st_test.c" 
# undef main 
# undef tests_run 
# undef all_tests 
int st_test(){ 
    printf("running st_test\n"); 
    return st_main(); 
} 

int main(){ 
    return 
        0  || io_test()  || st_test()  ; 
} 

现在我想从更短的变量 部分列表自动生成它:iost。我可以用 X-macros 完成大部分工作。

#define HASH #
#define UNITS(_) \
_(io) \
_(st) \
/**/

#define gen_unit_function(unit) \
HASH define main unit##_main \
HASH define tests_run unit##_tests_run \
HASH define all_tests unit##_all_tests \
HASH include STR(unit##_test.c) \
HASH undef main \
HASH undef tests_run \
HASH undef all_tests \
int unit##_test(){ \
    printf("running " STR(unit) "_test\n"); \
    return unit##_main(); \
}

#define gen_func_call(unit) \
    || unit##_test()

UNITS(gen_unit_function)
int main(){
    return
        0 UNITS(gen_func_call) ;
}

当然,这永远行不通,因为您无法使用 C 预处理器强制换行。我可以使用什么来解决此限制?

您可以使用功能更强大的宏处理器 m4 生成源代码。

现在m4不会用同样的方法做X-macros了。您不能简单地传递宏名称并在扩展文本中重新评估它。但是 gnu 文档描述(并分发)了一个 foreach 宏,它提供了相同的能力。

divert(`-1') 
# http://www.gnu.org/savannah-checkouts/gnu/m4/manual/m4-1.4.17/html_node/Foreach.html#Foreach 
# foreach(x, (item_1, item_2, ..., item_n), stmt) 
#   parenthesized list, simple version 
define(`foreach', `pushdef(`')_foreach($@)popdef(`')') 
define(`_arg1', `') 
define(`_foreach', `ifelse(`', `()', `', 
    `define(`', _arg1)`'[=10=](`', (shift), `')')') 

define(`UNITS', (io,st))

divert`'dnl 
`#' include <stdio.h> 
foreach(`unit', UNITS, ` 
`#' define main unit`'_main 
`#' define tests_run unit`'_tests_run 
`#' define all_tests unit`'_all_tests 
`#' include "unit`'_test.c" 
`#' undef main 
`#' undef tests_run 
`#' undef all_tests 
int unit`'_test(){ 
    printf("running unit`'_test\n"); 
    return unit`'_main(); 
} 
')dnl 

int main(){ 
    return 
        0 foreach(`unit', UNITS, ` || unit`'_test() ') ; 
} 

然后运行m4 all_tests.m4 > all_tests.c就可以生成程序了。这可以添加到 makefile 中。

all_tests.c:all_tests.m4 *_test.c
        m4 $< >$@

但是进入这个 m4 文件来更新列表会很丑陋,所以利用命名约定并使用 make 生成列表 (io,st)(它知道如何检查文件名和目录等等).

$cat makefile
testprogs= $(notdir $(wildcard ./*_test.c)) 
unitprogs= $(subst _test,,$(testprogs)) 
units= $(basename $(unitprogs)) 

test:all_tests 
        ./all_tests 
all_tests.c:all_tests.m4 makefile $(unitprogs) 
        m4 -D UNITS="$(units)" $< >$@ 

既然 make 喜欢 space 分隔的列表,但是我们的 m4 文件设置为使用逗号分隔的列表,我们需要在使用之前预处理列表,生成这个最终版本。

$cat all_tests.m4
divert(`-1') 
# http://www.gnu.org/savannah-checkouts/gnu/m4/manual/m4-1.4.17/html_node/Foreach.html#Foreach 
# foreach(x, (item_1, item_2, ..., item_n), stmt) 
#   parenthesized list, simple version 
define(`foreach', `pushdef(`')_foreach($@)popdef(`')') 
define(`_arg1', `') 
define(`_foreach', `ifelse(`', `()', `', 
    `define(`', _arg1)`'[=13=](`', (shift), `')')') 

define(`UNITS', (patsubst(UNITS,`\W',`,'))) 

divert`'dnl 
`#' include <stdio.h> 
foreach(`unit', UNITS, ` 
`#' define main unit`'_main 
`#' define tests_run unit`'_tests_run 
`#' define all_tests unit`'_all_tests 
`#' include "unit`'_test.c" 
`#' undef main 
`#' undef tests_run 
`#' undef all_tests 
int unit`'_test(){ 
    printf("running unit`'_test\n"); 
    return unit`'_main(); 
} 
')dnl 

int main(){ 
    return 
        0 foreach(`unit', UNITS, ` || unit`'_test() ') ; 
} 

现在,每当一个新模块准备好进行由 TESTMODULE 保护的单元测试 main() 时,就可以通过编写问题中描述的相同的两行文件将其添加到套件中,适合模式 *_test.c.

#define TESTMODULE 
#include "zz.c"

并且单元测试和 all_tests 都将通过 make 简单地编译。

[此 material 之前已发布到 comp.lang.c。]

另一种选择是用 sed 之类的东西来补充预处理器来截断行。嵌入一​​个 sed 可以搜索的标记,例如 EOL.

#define HASH #
#define UNITS(_) \
_(io) \
_(st) \
/**/

#define gen_unit_function(unit) \
HASH define main unit##_main EOL\
HASH define tests_run unit##_tests_run EOL\
HASH define all_tests unit##_all_tests EOL\
HASH include STR(unit##_test.c) EOL\
HASH undef main EOL\
HASH undef tests_run EOL\
HASH undef all_tests EOL\
int io_test(){ EOL\
    printf("running " STR(unit) "_test\n"); EOL\
    return unit##_main(); EOL\
}

#define gen_func_call(unit) \
    || unit##_test()

UNITS(gen_unit_function)
int main(){
    return
        0 UNITS(gen_func_call) ;
}

然后在cpp之后通过sed

cpp -P test_suite.inc | sed 's/EOL */
/g'