【问题标题】:How to catch a call to exit() (for unit testing)如何捕捉对 exit() 的调用(用于单元测试)
【发布时间】:2020-10-05 17:24:50
【问题描述】:

我正在为个人项目编写一个动态数组,并尝试对所有功能进行单元测试。我正在尝试为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;

}

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

【问题讨论】:

  • 这通常通过重新设计单元测试调用来处理,以便每个单元测试都作为自己的二进制文件/进程运行,尤其是因为它也会捕获崩溃。
  • 这是有道理的。有什么建议可以在哪里学习如何做到这一点,或者要搜索什么?
  • 请指定您的操作系统和工具链,因为exit() 之后发生的任何事情都超出了标准 C 的范围。例如,在 Unix 上,您可以使用 fork() 在孩子中运行测试过程。
  • 运行 MacOS Catalina、Clang 11.0.3 和 c11
  • 在大多数(全部?)现代 Unix 系统上,您只需定义自己的 exit() 函数,该函数将覆盖标准 C 库中的弱符号。例如。 void exit(int s){ for(;;) printf("tits\n"); }。将其放入 C 文件中,然后将其静态或动态链接到您的代码。这不会覆盖在 main() 返回后调用的隐式 exit(),也不会覆盖 C 库内部的任何 exit() 调用。

标签: c unit-testing exit


【解决方案1】:

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

话虽如此,在这里做这样的事情是没有意义的,因为我们正在谈论exit()。该函数预计不会在此之后继续执行,因此几乎迫使您将其替换为 longjmp() 或使用文本替换将其转换为 return(假设为 void 返回类型)。在这两种情况下,这意味着您需要假设该功能不会使事物处于损坏状态(例如持有某些资源)。这是一个很多假设,但如果您的单元测试框架旨在与此特定项目相关联,这可能是一个合理的出路。

我建议您将在自己的进程中运行测试的支持添加到您的测试框架中,而不是尝试修改测试函数的行为。除了能够测试这些东西之外,还有许多优点。例如,您可以免费并行运行测试并隔离它们之间的许多副作用。

【讨论】:

    【解决方案2】:

    另一种解决方案:使用 #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
    

    【讨论】:

      【解决方案3】:

      在 fork 中运行代码并检查 fork 的退出值。

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

      【讨论】:

      • 这种方法效果非常好。在子进程中关闭 stderr 的提示是我实现的一个很棒的额外掘金。
      【解决方案4】:

      如果是用于单元测试,则无需亲自接听对exit() 的调用。

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

      • 让util_dyn_array 包含一个在遇到越界模式时调用的回调,并将其默认为exit(1)(不是exit(-1),这在从shell 调用程序时效果不佳)。
      • 拥有一个全局越界处理程序(同样默认为exit(1)),并允许程序在运行时通过调用set_oob_handler(new_handler) 之类的方式更改处理程序。
      • 进行集成测试而不是单元测试。正如这里的多人所建议的那样,如果库可以退出或崩溃,这将进入集成领域(与调用进程/操作系统)。

      我的解决方案:

      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 进行少量修改)。

      【讨论】:

      • 修改测试的函数/程序当然可以做任何事情,但这不是问题的解决方案。
      • "做集成测试而不是单元测试......这进入了集成领域" 测试一个函数在无效输入上停止程序并不意味着它是一个集成测试。
      【解决方案5】:

      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 的返回值为1,表示它来自对longjmp 的调用。如果不是longjmp,则不调用,函数返回后setjmp的返回值为0。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-08
        • 2019-05-07
        • 2019-04-20
        • 1970-01-01
        • 2017-07-24
        相关资源
        最近更新 更多