测试框架: UnitTest++
平台: Linux
目标: 在每个源文件中, 以最小的代价, 最少的限制, 任意地对每个函数都能做单元测试, 包括那些以static 故意隐藏起来的代码.
单元测试代码的布署位置: 源代码文件内, 要都依了我, 测试代码跟正常产品代码一样, 不用宏控制其是否被编译. 永远跟产品代码一起. 但这种想法也有相当多人不同意, 原因是会让产品代码不必要地膨胀变大. 退一步, 把所有的单元测试代码都放在一个宏的控制之下. 如 UNIT_TEST
设计: 因为 LD_PRELOAD指定的so 文件会在main 之前运行, 而so在自身加载的同时会运行一个_init的函数, 该函数原型如下:
int _init(void); 正常值是返回0.
注意如果用C++来写这个so模块(我正是这样), 需要用 extern "C"特殊声明这个函数, 否则你会发现该函数莫名其妙地不被运行.
在_init 函数内部, 写替换掉被测试的可执行程序中的main 函数, 所谓替换, 并非是把被测试程序的main函数对应的机器指令换掉, 而仅仅是把 _start 指令中间接调用的main函数的地址用我们的新的main函数地址替换, 这个新的main函数, 就是启动单元测试代码的地方. 该函数的实现非常简单:
static int inject_main()
{
CPPUnitTest(NULL);
return 0;
}
CPPUnitTest是另一个函数, 间接地kick off整个单元测试集的运行, 我目前的实现中, 只简单地传递了NULL, 语意是运行所有的单元测试, 可以传递一个以逗号分隔的测试名字的集合, 只运行这些指定测试.
在_init 文件中对被测试程序main的替换是整个方案中的难点, 首先, 不要求被测试程序带有调试信息, 甚至连符号信息尽可能不要求, 要求越多, 对最终方案的适用性影响就越大, 曾经尝试过的方案包括:
* 用dlsym找到 _start 名
* 想办法反汇编 _start(这个符号本身总是可见的), 找到引用main地址的指令偏移.
* 用 ptrace调用改变这个指令偏移
这些方法要么失败, 要么困难重重. 最终找到的实现办法是这样:
* 用libelf通过解析可执行文件头找到 _start 的地址.
* 找到更好的办法之前, 暂时用硬编码的办法假设 main函数的地址总是在固定偏移(危险!)
* 用mprotect 修改代码段为可写
* 将新的函数地址替换过去
* _init 返回, 被测试程序正常执行, 但它的实际main函数却是我们在 auto_test.so 文件中实现的main函数.
GNU gdb (GDB) 7.1
Copyright (C) 2010 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-pc-linux-gnu".
For bug reporting instructions, please see:
<
Reading symbols from /home/zrf/c/test...done.
Dump of assembler code for function _start:
0x080486d0 <+0>: 31 ed xor ebp,ebp
0x080486d2 <+2>: 5e pop esi
0x080486d3 <+3>: 89 e1 mov ecx,esp
0x080486d5 <+5>: 83 e4 f0 and esp,0xfffffff0
0x080486d8 <+8>: 50 push eax
0x080486d9 <+9>: 54 push esp
0x080486da <+10>: 52 push edx
0x080486db <+11>: 68 70 89 04 08 push 0x8048970
0x080486e0 <+16>: 68 80 89 04 08 push 0x8048980
0x080486e5 <+21>: 51 push ecx
0x080486e6 <+22>: 56 push esi
0x080486e7 <+23>: 68 6e 88 04 08 push 0x804886e
0x080486ec <+28>: e8 7f ff ff ff call 0x8048670 <__libc_start_main@plt>
0x080486f1 <+33>: f4 hlt
0x080486f2 <+34>: 90 nop
0x080486f3 <+35>: 90 nop
End of assembler dump.
上面 +23 处, 就是链接器在生成 _start这块代码时, 把main函数的地址压入栈中, 控制权交给__libc_start_main@plt, 第一个字节是68, 是机器指令码, 后面的4个字节是地址, 所以要修改的地址是
_start + 24
完整的 _init实现为:
extern "C" int _init(void)
{
Elf * e = NULL;
GElf_Ehdr ehdr;
pid_t pid = getpid();
char proc_exe_fname[100] = {0};
char real_exe_file[100] = {0};
snprintf(proc_exe_fname, sizeof(proc_exe_fname)-1, "/proc/%d/exe", pid);
int n_real_path = readlink(proc_exe_fname, real_exe_file, sizeof(real_exe_file) - 1);
if(n_real_path >= sizeof(real_exe_file) - 1)
{
fprintf(stderr, "Executable file name is too long( > %llu chars)",
(long long unsigned) (sizeof(real_exe_file) - 2U) );
exit(-1);
}
int fd = open(proc_exe_fname, O_RDONLY, 0);
if( fd < 0)
{
fprintf(stderr, "Fail to open [%s]", real_exe_file);
exit(-1);
}
struct CloseFD_RAII {
int fd_;
explicit CloseFD_RAII(int fd): fd_(fd) {}
~CloseFD_RAII() { close(fd_); }
} raii_close_fd(fd);
elf_version(EV_CURRENT) ;
e = elf_begin(fd, ELF_C_READ, NULL);
if( e == NULL)
{
fprintf(stderr, "Fail to open [%s] as ELF", real_exe_file);
exit(-1);
}
if(gelf_getehdr(e, &ehdr) == NULL)
{
fprintf(stderr, "Fail to get hdr of [%s]", real_exe_file);
exit(-1);
}
unsigned int * addr = (unsigned int*) ( (unsigned int)ehdr.e_entry + 24 );
int ret = mprotect( (void*) ( (unsigned int)addr / 4096 * 4096), 1024, PROT_READ | PROT_WRITE | PROT_EXEC);
if( ret < 0 )
{
printf("mprotect ret: %d: %s\n", ret, strerror(errno) );
}
unsigned int orig_main = *addr;
*addr = (unsigned int)inject_main;
printf("====== UnitTest++ start...======\n"
"Replace executable [%s]'s main function(%p) with UnitTest main: %p\n"
"\n"
, real_exe_file
, (void *) orig_main, (void *)inject_main );
return 0;
}
测试效果:
TEST(my_linux_test)
{
printf("Hello\n");
}
int main(int argc, char *argv[])
{
printf("prog: %s, function: %s\n", argv[0], __FUNCTION__);
return 0;
}
编译该程序为 test
单独运行
./test时, 其输出自然是程序的名字和函数名main, 但用下面的方法再去运行它时
LD_RELOAD=./auto_test.so ./test
输出变为了
====== UnitTest++ start...======
Replace executable [/home/zrf/c/test]'s main function(0x804886e) with UnitTest main: 0x113c7a
Hello
测试代码被执行, 其中的Hello是测试代码的输出, 后面的XML是测试输出结果的汇总.
上面是测试可执行程序时的情况, 测试其它.so文件呢, 经测试同样可以:
using namespace std;
using namespace UnitTest;
TEST(my_linux_shared_library_UnitTest)
{
char proc_exe[100] = {0};
char real_exe[100] = {0};
snprintf(proc_exe, sizeof(proc_exe) - 1, "/proc/%d/exe", getpid() );
readlink(proc_exe, real_exe, sizeof(real_exe) - 1);
printf("prog: %s, func: %s\n", real_exe, __PRETTY_FUNCTION__ );
}
上面是test_so.cpp
编译后生成了 test_so.so
这样测试它:
LD_PRELOAD=./test_so.so:./auto_test.so /bin/true
输出结果为:
====== UnitTest++ start...======
Replace executable [/bin/true]'s main function(0x8048c10) with UnitTest main: 0xb4fc7a
prog: /bin/true, func: virtual void Testmy_linux_shared_library_UnitTest::RunImpl() const
其中/bin/true 是linux下一个什么事都不干直接返回成功的程序, 但它也有main函数, 虽然可能没有这个符号, 但它是main函数的概念. 所以可以通过这种办法来巧妙地运行一个动态链接库里面的程序.
这里面的一个关键之处是:
UnitTest++ 必需是以动态库的形式被
* auto_test.so
* 被测试程序
同时使用的, 一开始我犯下错误, 用静态链接, 发现此时通过 auto_test.so 中的inject_main运行时没有测试被运行, 原因何在? 就是因为静态链接时, auto_test.so 与 被测试程序中有两套独立的UnitTest++的测试集变量, 而被测试程序中自动注册的那些测试只添加到了它自己那一份测试集链表中, auto_test.so中的却一直是空的.
另一个需要注意的问题是:
__dso_handle 引起的麻烦
在用g++ 链接一个动态链接库, 以及用LD_PRELOAD 强制预载入一个动态链接库时, 它都会引起问题, 在google上找到的一个解决办法是添加下面的定义:
extern "C" void * __dso_handle = NULL;
我在生成 libUnitTest++.so 时, 新加了一个文件: dso.c, 里面只有一句
void * __dso_handle = NULL;
这样就可以正常使用了.
结合以前做过的一个在visual studio中一键运行 UnitTest++ 实现的C++单元测试的插件, 用UnitTest++做单元测试在两大平台都变得方便多了.
TDD for C++, 道不远人
|
文件: | auto_test.tar.gz |
大小: | 90KB |
下载: | 下载 |
|
解压后, UnitTest++ 与 ut是两个平级的目录, 先进入 UnitTest++目录执行make so
会产生 libUnitTest++.so 文件, 再到 ut目录, 执行make, 可以看到实际效果.
其中 UnitTest++ 我略作修改.