如何捕捉对 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 函数是弱符号,因此您可以创建自己的函数副本以捕获调用它的情况。此外,您可以在测试代码中使用 setjmplongjmp 来检测对退出的正确调用:

例如:

#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