C 如何捕获对exit()的调用(用于单元测试)

C 如何捕获对exit()的调用(用于单元测试),c,unit-testing,exit,C,Unit Testing,Exit,我正在为一个个人项目编写一个动态数组,并尝试对所有函数进行单元测试。我正试图为util\u dyn\u array\u check\u index()编写一个单元测试,但在这样做时遇到了问题,因为我做出了设计决策,如果索引超出范围,则调用exit(-1)。我想在单元测试中检查它是否在提供无效索引时调用exit()。但如果我给它一个无效的索引,它就会退出我的测试程序。是否可能以某种方式捕捉到对exit()的调用被抛出,或者在我的测试程序中重新定义exit(),以防止它结束测试 从中,我研究了ate

我正在为一个个人项目编写一个动态数组,并尝试对所有函数进行单元测试。我正试图为
util\u dyn\u array\u check\u index()
编写一个单元测试,但在这样做时遇到了问题,因为我做出了设计决策,如果索引超出范围,则调用
exit(-1)
。我想在单元测试中检查它是否在提供无效索引时调用
exit()
。但如果我给它一个无效的索引,它就会退出我的测试程序。是否可能以某种方式捕捉到对
exit()
的调用被抛出,或者在我的测试程序中重新定义
exit()
,以防止它结束测试

从中,我研究了
atexit()
,但这看起来并没有停止退出,只是在退出之前执行一个或多个用户定义的函数。这对我不起作用,因为在这之后我还有其他测试要运行。我最后的想法是,我可以将
util\u dyn\u array\u check\u 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;
下面是我希望测试的框架(为了清晰起见,省略了我用来让编写测试变得更好的一些宏魔术)

bool test\u dyn\u array\u check\u index(){
util_dyn_数组向量=util_dyn_数组新(sizeof(int),16);
对于(int i=0;i<16;i++){
util_dyn_array_push(&vector,(void*)&i);
}
对于(int i=0;i<16;i++){
//如果什么都没发生,那就是成功了
util_dyn_数组_check_索引(&vector,i);
}
//以某种方式检查它是否调用exit,而不让它使我的程序崩溃
{
util_dyn_数组_check_索引(&vector,16);
}
返回true;
}

显然,我可以更改我的代码以返回
bool
或写入
errno
,但我更希望它退出,因为它通常是一个不可恢复的错误。

在标准C中定义另一个
exit()
(如果包含头,则作为函数和宏)。不过,在许多环境中,您可能都能侥幸逃脱

话虽如此,在这里做这样的事情毫无意义,因为我们谈论的是
exit()
。该函数不希望在此之后继续执行,因此几乎迫使您将其替换为
longjmp()
,或者使用文本替换将其转换为
返回类型(假设为
void
返回类型)。在这两种情况下,都意味着您需要假设函数没有使事物处于中断状态(例如持有某些资源)。这是一个很大的假设,但如果您的单元测试框架要绑定到这个特定的项目,那么这可能是一个合理的解决方法


我建议您在测试框架中添加对在自己的流程中运行测试的支持,而不是试图修改被测试函数的行为。除了能够测试这些东西之外,还有很多好处。例如,您可以免费并行运行测试,并隔离测试之间的许多副作用。

如果是单元测试,您不需要实际捕获对
exit()
的调用

在解决方案之前,我建议重新设计您的库。你有几个选择:

  • util\u dyn\u array
    包含一个回调,如果遇到越界模式,将调用该回调,并将其默认为
    exit(1)
    (而不是
    exit(-1)
    ,当从shell调用程序时,该回调无法正常工作)
  • 拥有一个全局越界处理程序(默认为
    exit(1)
    ),并允许程序在运行时通过调用类似
    set\u oob\u处理程序(new\u handler)
    的东西来更改处理程序
  • 做集成测试而不是单元测试。正如这里的许多人所建议的,如果库可以退出或崩溃,这将进入集成领域(与调用进程/OS)
我的解决方案:

main.c

#include <stdio.h>

void func(void);

int main(int argc, char **argv)
{
    printf("starting\n");
    func();
    printf("ending\n");
}
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

(请注意,这是在Linux上测试的,我没有Mac可供测试,因此可能需要对makefile进行较小的修改)。

退出函数是一个弱符号,因此您可以创建自己的函数副本,以捕捉调用它的情况。此外,您可以在测试代码中使用
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;

}    
#包括“文件到测试.c”
应为静态int_代码;//被测试函数传递给exit的期望值
静态int应_退出;//1如果应该调用exit
静态int完成;//设置为1以防止存根行为并实际退出
静态jmp_buf jump_env;
静态int-rslt;
#定义测试断言(x)(rslt=rslt&&(x))
//存根函数
无效退出(内部代码)
{
如果(!完成)
{
test_assert(应该_exit==1);
测试\断言(预期\代码==代码);
longjmp(jump_env,1);
}
其他的
{
_出口(代码);
}
}
布尔测试\u动态\u数组\u检查\u索引(){
int jmprval;
完成=0;
rslt=1;
util_dyn_数组向量=util_dyn_数组新(sizeof(int),16);
对于(int i=0;i<16;i++){
util_dyn_array_push(&vector,(void*)&i);
}
对于(int i=0;i<16;i++){
//如果什么都没发生,那就是成功了
应该退出=0;
如果(!(jmp_rval=setjmp(jump_env)))
{
util_dyn_数组_check_索引(&vector,i);
}
test_assert(jmp_rval==0);
}
//应呼叫出口(-1)
{
应退出=1;
预期的_代码=2;
如果(!(jmp_rval=setjmp(jump_env)))
{
util_dyn_数组_check_索引(&vector,16);
}
test_assert(jmp_rval==1);
}
完成=1
# 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
#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;

}    
#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));
    }
}
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.
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 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