从一组单元测试中组成一个组合测试套件程序
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() ;
}
现在我想从更短的变量 部分列表自动生成它:io
和st
。我可以用 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'
我正在构建一个 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() ;
}
现在我想从更短的变量 部分列表自动生成它:io
和st
。我可以用 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'