如何捕捉对 exit() 的调用(用于单元测试)
How to catch a call to exit() (for unit testing)
我正在为个人项目编写一个动态数组,并尝试对所有函数进行单元测试。我正在尝试为 util_dyn_array_check_index()
编写单元测试,但在这样做时遇到了问题,因为我的设计决定是在索引越界时调用 exit(-1)
。我想检查我的单元测试,它在提供无效索引时调用 exit()
。但是如果我给它一个无效的索引,它就会退出我的测试程序。是否有可能以某种方式捕获对 exit()
的调用被抛出,或者在我的测试程序中重新定义 exit()
以防止它结束测试?
从 this answer 我调查了 atexit()
,但它看起来并没有停止退出,只是在退出前执行一个或多个用户定义的函数。这对我不起作用,因为在此之后我还有 运行 的其他测试。我最后的想法是我可以让 util_dyn_array_check_index()
成为一个宏而不是一个函数,并在我的测试程序中重新定义 exit()
成为一个不同的函数,但如果可以避免的话我宁愿不把它变成一个宏它。
这是我的代码:
这个结构的细节并不重要,只是为了完整性
//basically a Vec<T>
typedef struct {
//a pointer to the data stored
void * data;
//the width of the elements to be stored in bytes
size_t stride;
//the number of elements stored
size_t len;
//the number of elements able to be stored without reallocating
size_t capacity;
} util_dyn_array;
这是我要测试的功能。
//exits with -1 if index is out of bounds
inline void util_dyn_array_check_index(util_dyn_array * self, size_t index) {
if (index >= self->len) {
exit(-1);
}
return;
}
这是我希望测试的框架(为清楚起见,省略了一些我用来使编写测试更好的宏魔法)。
bool test_dyn_array_check_index() {
util_dyn_array vector = util_dyn_array_new(sizeof(int), 16);
for(int i = 0; i < 16; i++) {
util_dyn_array_push(&vector, (void*)&i);
}
for(int i = 0; i < 16; i++) {
//if nothing happens, its successful
util_dyn_array_check_index(&vector, i);
}
//somehow check that it calls exit without letting it crash my program
{
util_dyn_array_check_index(&vector, 16);
}
return true;
}
显然我可以将我的代码更改为 return a bool
或写入 errno
,但我更希望它退出,因为它通常是一个无法恢复的错误。
在标准 C 中定义另一个 exit()
是 UB(如果包含头文件,则既作为函数又作为宏)。不过,在许多环境中,您很可能能够摆脱它。
话虽如此,在这里做这样的事情是没有意义的,因为我们正在谈论exit()
。该函数不希望在此之后继续执行,因此这几乎迫使您将其替换为 longjmp()
或使用文本替换将其转换为 return
(假设 void
return 类型)。在这两种情况下,这意味着您需要假设函数不会让事物处于损坏状态(比如持有一些资源)。这是一个很多假设,但如果您的单元测试框架打算与这个特定项目相关联,这可能是一个合理的出路。
与其尝试修改被测试函数的行为,我建议您在自己的测试框架中添加对 运行 测试的支持。除了能够测试这些东西之外,还有很多优点。例如,您可以免费并行进行 运行 测试,并在它们之间隔离许多 side-effects。
如果是为了单元测试,你不需要亲自接住对exit()
的调用。
在解决之前,我想建议重新设计你的库。您有几个选择:
- 让
util_dyn_array
包含一个在遇到 out-of-bound 模式时调用的回调,并将其默认为 exit(1)
(不是 exit(-1)
,效果不佳当从 shell). 调用程序时
- 拥有全局 out-of-bound 处理程序(同样默认为
exit(1)
),并允许程序通过调用类似 set_oob_handler(new_handler)
.[=41= 的方式在运行时更改处理程序]
- 进行集成测试而不是单元测试。正如这里多人所建议的那样,如果库可以退出或崩溃,这将进入集成领域(调用 process/OS)。
我的解决方案:
main.c
:
#include <stdio.h>
void func(void);
int main(int argc, char **argv)
{
printf("starting\n");
func();
printf("ending\n");
}
something.c
:
void my_exit(int status)
{
printf("my_exit(%d)\n", status);
#ifdef UNIT_TEST
printf("captured exit(%d)\n", status); // you can even choose to call a global callback here, only in unit tests.
#else
exit(status);
#endif
}
void func(void) {
my_exit(1);
}
makefile
:
# these targets are MUTUALLY EXCLUSIVE!!
release:
cc -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
fortest:
cc -DUNIT_TEST=1 -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
$ make release
cc -g -c -fpic something.c
[...]
$ LD_LIBRARY_PATH=. ./main
starting
my_exit(1)
$ make fortest
cc -DUNIT_TEST=1 -g -c -fpic something.c
[...]
$ LD_LIBRARY_PATH=. ./main
starting
my_exit(1)
captured exit(1)
ending
(请注意,这是在 Linux 上测试的,我没有要测试的 Mac,因此可能需要对 makefile 进行少量修改。
exit
函数是弱符号,因此您可以创建自己的函数副本以捕获调用它的情况。此外,您可以在测试代码中使用 setjmp
和 longjmp
来检测对退出的正确调用:
例如:
#include "file_to_test.c"
static int expected_code; // the expected value a tested function passes to exit
static int should_exit; // 1 if exit should have been called
static int done; // set to 1 to prevent stubbing behavior and actually exit
static jmp_buf jump_env;
static int rslt;
#define test_assert(x) (rslt = rslt && (x))
// stub function
void exit(int code)
{
if (!done)
{
test_assert(should_exit==1);
test_assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}
bool test_dyn_array_check_index() {
int jmp_rval;
done = 0;
rslt = 1;
util_dyn_array vector = util_dyn_array_new(sizeof(int), 16);
for(int i = 0; i < 16; i++) {
util_dyn_array_push(&vector, (void*)&i);
}
for(int i = 0; i < 16; i++) {
//if nothing happens, its successful
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, i);
}
test_assert(jmp_rval==0);
}
// should call exit(-1)
{
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, 16);
}
test_assert(jmp_rval==1);
}
done = 1
return rslt;
}
调用可调用exit
的函数前,调用setjmp
设置跳转点。存根 exit
函数然后检查是否应该调用 exit
以及使用哪个退出代码,然后调用 longjmp
跳回测试。
如果调用了 exit
,则 setjmp
的 return 值为 1,表示它来自对 longjmp
的调用。 If not longjmp
不被调用并且 setjmp
的 return 值在函数 returns.
之后将为 0
运行分叉中的代码并检查分叉的退出值。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/wait.h>
void util_dyn_array_check_index() {
exit(-1);
}
int main(void) {
pid_t pid = fork();
assert(pid >= 0);
if( pid == 0 ) {
util_dyn_array_check_index();
exit(0);
}
else {
int child_status;
wait(&child_status);
printf("util_dyn_array_check_index exited with %d\n", WEXITSTATUS(child_status));
}
}
请注意,退出状态将为 255,因为尽管接受整数 POSIX,但退出状态是无符号的。考虑改用 exit(1)
或定义 ARGUMENT_ERROR_EXIT_STATUS
宏。
请注意,我使用 assert
来检查我的分叉是否失败。这可能是实施检查的更好方法。
inline void util_dyn_array_check_index(util_dyn_array * self, size_t index) {
assert(index < self->len);
}
这将提供有关错误的更多信息,并且可以在生产中将其关闭以提高性能。
Assertion failed: (index < self->len), function util_dyn_array_check_index, file test.c, line 9.
assert
呼叫 abort
。在您的测试中,您将关闭 stderr 以避免断言消息使输出混乱,并检查 WTERMSIG(status) == SIGABRT
.
void util_dyn_array_check_index() {
assert(43 < 42);
}
int main(void) {
pid_t pid = fork();
assert(pid >= 0);
if( pid == 0 ) {
// Suppress the assert output
fclose(stderr);
util_dyn_array_check_index();
exit(0);
}
else {
int status;
wait(&status);
if( WTERMSIG(status) == SIGABRT ) {
puts("Pass");
}
else {
puts("Fail");
}
}
}
另一种解决方案:使用 #ifdef 宏在调试版本中调用不同的函数。
#ifdef DEBUG
#define exit_badindex(...) my_debug_function(__VA_ARGS__)
#else
#define exit_badindex(...) exit(__VA_ARGS__)
#ifndef NDEBUG
#warning NDEBUG undefined in non-DEBUG build: my_debug_function will not be called
#endif
#endif
我正在为个人项目编写一个动态数组,并尝试对所有函数进行单元测试。我正在尝试为 util_dyn_array_check_index()
编写单元测试,但在这样做时遇到了问题,因为我的设计决定是在索引越界时调用 exit(-1)
。我想检查我的单元测试,它在提供无效索引时调用 exit()
。但是如果我给它一个无效的索引,它就会退出我的测试程序。是否有可能以某种方式捕获对 exit()
的调用被抛出,或者在我的测试程序中重新定义 exit()
以防止它结束测试?
从 this answer 我调查了 atexit()
,但它看起来并没有停止退出,只是在退出前执行一个或多个用户定义的函数。这对我不起作用,因为在此之后我还有 运行 的其他测试。我最后的想法是我可以让 util_dyn_array_check_index()
成为一个宏而不是一个函数,并在我的测试程序中重新定义 exit()
成为一个不同的函数,但如果可以避免的话我宁愿不把它变成一个宏它。
这是我的代码:
这个结构的细节并不重要,只是为了完整性
//basically a Vec<T>
typedef struct {
//a pointer to the data stored
void * data;
//the width of the elements to be stored in bytes
size_t stride;
//the number of elements stored
size_t len;
//the number of elements able to be stored without reallocating
size_t capacity;
} util_dyn_array;
这是我要测试的功能。
//exits with -1 if index is out of bounds
inline void util_dyn_array_check_index(util_dyn_array * self, size_t index) {
if (index >= self->len) {
exit(-1);
}
return;
}
这是我希望测试的框架(为清楚起见,省略了一些我用来使编写测试更好的宏魔法)。
bool test_dyn_array_check_index() {
util_dyn_array vector = util_dyn_array_new(sizeof(int), 16);
for(int i = 0; i < 16; i++) {
util_dyn_array_push(&vector, (void*)&i);
}
for(int i = 0; i < 16; i++) {
//if nothing happens, its successful
util_dyn_array_check_index(&vector, i);
}
//somehow check that it calls exit without letting it crash my program
{
util_dyn_array_check_index(&vector, 16);
}
return true;
}
显然我可以将我的代码更改为 return a bool
或写入 errno
,但我更希望它退出,因为它通常是一个无法恢复的错误。
在标准 C 中定义另一个 exit()
是 UB(如果包含头文件,则既作为函数又作为宏)。不过,在许多环境中,您很可能能够摆脱它。
话虽如此,在这里做这样的事情是没有意义的,因为我们正在谈论exit()
。该函数不希望在此之后继续执行,因此这几乎迫使您将其替换为 longjmp()
或使用文本替换将其转换为 return
(假设 void
return 类型)。在这两种情况下,这意味着您需要假设函数不会让事物处于损坏状态(比如持有一些资源)。这是一个很多假设,但如果您的单元测试框架打算与这个特定项目相关联,这可能是一个合理的出路。
与其尝试修改被测试函数的行为,我建议您在自己的测试框架中添加对 运行 测试的支持。除了能够测试这些东西之外,还有很多优点。例如,您可以免费并行进行 运行 测试,并在它们之间隔离许多 side-effects。
如果是为了单元测试,你不需要亲自接住对exit()
的调用。
在解决之前,我想建议重新设计你的库。您有几个选择:
- 让
util_dyn_array
包含一个在遇到 out-of-bound 模式时调用的回调,并将其默认为exit(1)
(不是exit(-1)
,效果不佳当从 shell). 调用程序时
- 拥有全局 out-of-bound 处理程序(同样默认为
exit(1)
),并允许程序通过调用类似set_oob_handler(new_handler)
.[=41= 的方式在运行时更改处理程序] - 进行集成测试而不是单元测试。正如这里多人所建议的那样,如果库可以退出或崩溃,这将进入集成领域(调用 process/OS)。
我的解决方案:
main.c
:
#include <stdio.h>
void func(void);
int main(int argc, char **argv)
{
printf("starting\n");
func();
printf("ending\n");
}
something.c
:
void my_exit(int status)
{
printf("my_exit(%d)\n", status);
#ifdef UNIT_TEST
printf("captured exit(%d)\n", status); // you can even choose to call a global callback here, only in unit tests.
#else
exit(status);
#endif
}
void func(void) {
my_exit(1);
}
makefile
:
# these targets are MUTUALLY EXCLUSIVE!!
release:
cc -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
fortest:
cc -DUNIT_TEST=1 -g -c -fpic something.c
cc -shared -o libsomething.so something.o
cc -g -o main main.c -L. -lsomething
$ make release
cc -g -c -fpic something.c
[...]
$ LD_LIBRARY_PATH=. ./main
starting
my_exit(1)
$ make fortest
cc -DUNIT_TEST=1 -g -c -fpic something.c
[...]
$ LD_LIBRARY_PATH=. ./main
starting
my_exit(1)
captured exit(1)
ending
(请注意,这是在 Linux 上测试的,我没有要测试的 Mac,因此可能需要对 makefile 进行少量修改。
exit
函数是弱符号,因此您可以创建自己的函数副本以捕获调用它的情况。此外,您可以在测试代码中使用 setjmp
和 longjmp
来检测对退出的正确调用:
例如:
#include "file_to_test.c"
static int expected_code; // the expected value a tested function passes to exit
static int should_exit; // 1 if exit should have been called
static int done; // set to 1 to prevent stubbing behavior and actually exit
static jmp_buf jump_env;
static int rslt;
#define test_assert(x) (rslt = rslt && (x))
// stub function
void exit(int code)
{
if (!done)
{
test_assert(should_exit==1);
test_assert(expected_code==code);
longjmp(jump_env, 1);
}
else
{
_exit(code);
}
}
bool test_dyn_array_check_index() {
int jmp_rval;
done = 0;
rslt = 1;
util_dyn_array vector = util_dyn_array_new(sizeof(int), 16);
for(int i = 0; i < 16; i++) {
util_dyn_array_push(&vector, (void*)&i);
}
for(int i = 0; i < 16; i++) {
//if nothing happens, its successful
should_exit = 0;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, i);
}
test_assert(jmp_rval==0);
}
// should call exit(-1)
{
should_exit = 1;
expected_code = 2;
if (!(jmp_rval=setjmp(jump_env)))
{
util_dyn_array_check_index(&vector, 16);
}
test_assert(jmp_rval==1);
}
done = 1
return rslt;
}
调用可调用exit
的函数前,调用setjmp
设置跳转点。存根 exit
函数然后检查是否应该调用 exit
以及使用哪个退出代码,然后调用 longjmp
跳回测试。
如果调用了 exit
,则 setjmp
的 return 值为 1,表示它来自对 longjmp
的调用。 If not longjmp
不被调用并且 setjmp
的 return 值在函数 returns.
运行分叉中的代码并检查分叉的退出值。
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/wait.h>
void util_dyn_array_check_index() {
exit(-1);
}
int main(void) {
pid_t pid = fork();
assert(pid >= 0);
if( pid == 0 ) {
util_dyn_array_check_index();
exit(0);
}
else {
int child_status;
wait(&child_status);
printf("util_dyn_array_check_index exited with %d\n", WEXITSTATUS(child_status));
}
}
请注意,退出状态将为 255,因为尽管接受整数 POSIX,但退出状态是无符号的。考虑改用 exit(1)
或定义 ARGUMENT_ERROR_EXIT_STATUS
宏。
请注意,我使用 assert
来检查我的分叉是否失败。这可能是实施检查的更好方法。
inline void util_dyn_array_check_index(util_dyn_array * self, size_t index) {
assert(index < self->len);
}
这将提供有关错误的更多信息,并且可以在生产中将其关闭以提高性能。
Assertion failed: (index < self->len), function util_dyn_array_check_index, file test.c, line 9.
assert
呼叫 abort
。在您的测试中,您将关闭 stderr 以避免断言消息使输出混乱,并检查 WTERMSIG(status) == SIGABRT
.
void util_dyn_array_check_index() {
assert(43 < 42);
}
int main(void) {
pid_t pid = fork();
assert(pid >= 0);
if( pid == 0 ) {
// Suppress the assert output
fclose(stderr);
util_dyn_array_check_index();
exit(0);
}
else {
int status;
wait(&status);
if( WTERMSIG(status) == SIGABRT ) {
puts("Pass");
}
else {
puts("Fail");
}
}
}
另一种解决方案:使用 #ifdef 宏在调试版本中调用不同的函数。
#ifdef DEBUG
#define exit_badindex(...) my_debug_function(__VA_ARGS__)
#else
#define exit_badindex(...) exit(__VA_ARGS__)
#ifndef NDEBUG
#warning NDEBUG undefined in non-DEBUG build: my_debug_function will not be called
#endif
#endif