| the Compartmented Robust Posix C++ Unit Test system | hosted by | 
|---|
You can safely roll your own versions of functions like
      fork() and clock_gettime().
      crpcut make all library calls through dlopen() and
      dlsym(), and will not be affected by your
      versions[1][2]
    
      Sometimes, however, replacing a library function is not what you want,
      but rather to enclose it inside a wrapper that does filtering and
      error injection. You can do that with the help of the macros
      CRPCUT_WRAP_FUNC(lib, func, rv, param_list, param_call) and
      CRPCUT_WRAP_V_FUNC(lib, func, rv, param_list, param_call). Here's
      classic difficult situation, with a process that spawns a child and
      interacts with it:
      
     extern "C"
     {
     #include <unistd.h>
     #include <sys/types.h>
     #include <sys/wait.h>
     #include <stdlib.h>
     }
     
     #include <stdexcept>
     #include <string>
     #include <cassert>
     
     class work
     {
     public:
       class fork_exception : public std::exception  { };
       class pipe_exception : public std::exception  { };
       work() :pid(-1)
       {
         int rv = pipe(fd);
         if (rv == -1) throw pipe_exception();
         rv = ::fork();
         if (rv == -1) throw fork_exception();
         if (rv == 0) { close(fd[0]); fd[0] = -1; do_work(); return; } // child
         close(fd[1]);
         fd[1] = -1;
         pid = rv;
       }
       ~work() {
         if (fd[0] != -1) close(fd[0]);
         if (fd[1] != -1) close(fd[1]);
       }
       std::string get_data() // empty data signals that child is done
       {
         size_t len = 0;
         ::read(fd[0], &len, sizeof(len));
         std::string data(len > 0 ? len : 0, '_');
         if (len > 0)
           {
             ::read(fd[0], &data[0], len);
           }
         return data;
       }
       void wait()
       {
         int status;
         int rv = ::wait(&status);
         assert(rv != 0 || status == pid);
         pid = -1;
       }
     private:
       void do_work() { /* very hard stuff */  }
       int fd[2];
       int pid;
     };
In this case, simple stubs are used to provide preprogrammed behavior, and fall back to the normal library function when nothing preprogrammed exists.
The test program is a bit long, but take some time to read
      it through. For each wrapped library function, there is a
      configuration in the shape of a list. If the list is
      empty, the normal function applies, otherwise the behavior mandated
      by the list-item is carried out. pipe()
      calls the library function, unless explicitly instructed to fail,
      but stores the allocated file descriptors for later use.
      close() always calls the library function,
      but if the file descriptors are allocated with pipe,
      they are verified to be in valid state.
![]()  | Caution | 
|---|---|
| Your versions of the library functions are in effect for all tests in the entire program. Make sure they have a decent default behavior to save yourself from debugging headaches. | 
A short walk-through of the tests:
Only one configuration which ensures that
              fork() fails with
              ENOMEM.
              Verify that work::fork_exception is thrown.
            
Only one configuration which ensures that
              pipe() fails with
              EMFILE.
              Verify that the constructor throws
              work::pipe_exception
            
The complex one, where all library function wrappers
              are strictly controlled. fork() will
              return a pid, thus testing the parent process
              behavior.
              The pipe() wrapper lets the calls
              to the library function through, but it stores the
             allocated file descriptors for use by the
              read(), write()
              and close stubs.
            
The read() and
              write() functions are stubbed
              completely to verify the correct file descriptor, provide
              predefined data from the faked child process, and
              verify that the data written back is correct.
            
Similar to read_one_string above, but
              the second read is set to return -1 and set
              errno to EINTR,
              to indicate that the call was interrupted by a signal.
            
It doesn't set up any configurations for any of the wrapped library functions, so for this test, the normal libc implementations are run unconstrained. It will, in other words, behave as in a real program.
     #include "process-example.hpp"
     #include <crpcut.hpp>
     #include <cerrno>
     #include <list>
     #include <map>
     
     namespace original
     {
       CRPCUT_WRAP_FUNC(libc, fork, pid_t, (void), ())
       CRPCUT_WRAP_FUNC(libc, close, int, (int fd), (fd))
       CRPCUT_WRAP_FUNC(libc, pipe, int, (int fd[2]), (fd))
       CRPCUT_WRAP_FUNC(libc, wait, pid_t, (void *status), (status))
       CRPCUT_WRAP_FUNC(libc, read, ssize_t, (int fd, void *p, size_t n), (fd, p, n))
       CRPCUT_WRAP_FUNC(libc, write, ssize_t, (int fd, const void *p, size_t n), (fd, p, n))
     }
     
     //
     // data structures used to control how the wrappers behave
     //
     
     struct pipe_data
     {
       pipe_data(int retval_, int *rfd_, int *wfd_, int err_)
         : retval(retval_), rfd(rfd_), wfd(wfd_), err(err_) {}
       int  retval;
       int *rfd;
       int *wfd;
       int  err;
     };
     
     std::list<pipe_data> pipe_actions;
     
     struct fork_data
     {
       fork_data(pid_t retval_, int err_)
         : retval(retval_), err(err_) {}
       pid_t retval;
       int   err;
     };
     
     std::list<fork_data> fork_actions;
     
     struct read_data
     {
       read_data(ssize_t retval_, int *efd, const void *data_, int err_)
         : retval(retval_), expected_fd(efd), data(data_), err(err_) {}
       ssize_t     retval;
       int        *expected_fd;
       const void *data;
       int         err;
     };
     
     std::list<read_data> read_actions;
     
     struct write_data
     {
       write_data(ssize_t retval_, int *efd, const void *data, size_t len, int err_)
         : retval(retval_), expected_fd(efd), expected_data(data),
           expected_len(len), err(err_) {}
       ssize_t     retval;
       int        *expected_fd;
       const void *expected_data;
       size_t      expected_len;
       int         err;
     };
     
     std::list<write_data> write_actions;
     
     
     struct wait_data
     {
       wait_data(int retval_, pid_t pid_, int err_)
         : retval(retval_), pid(pid_), err(err_) {}
       int        retval;
       pid_t      pid;
       int        err;
     };
     
     std::list<wait_data> wait_actions;
     
     typedef enum { unknown, is_pipe, is_closed } fdstatus;
     std::map<int, fdstatus> file_descriptors;
     
     
     //
     // wrapper implementations
     //
     
     extern "C" pid_t fork() throw ()
     {
       if (fork_actions.empty()) return original::fork();
       const fork_data &a = fork_actions.front();
       errno = a.err;
       int rv = a.retval;
       fork_actions.pop_front();
       return rv;
     }
     
     extern "C" ssize_t read(int fd, void *p, size_t n)
     {
       if (read_actions.empty()) return original::read(fd, p, n);
       const read_data &a = read_actions.front();
       if (a.expected_fd)
         {
           ASSERT_EQ(fd, *a.expected_fd);
         }
       ssize_t rv = a.retval;
       if (rv != -1)
         {
           ASSERT_GE(n, size_t(rv));
           memcpy(p, a.data, rv);
         }
       errno = a.err;
       read_actions.pop_front();
       return rv;
     }
     
     extern "C" ssize_t write(int fd, const void *p, size_t n)
     {
       if (write_actions.empty()) return original::write(fd, p, n);
       const write_data &a = write_actions.front();
       if (a.expected_fd)
         {
           ASSERT_EQ(fd, *a.expected_fd);
         }
       ssize_t rv = a.retval;
       if (rv >= 0)
         {
           ASSERT_EQ(n, size_t(rv));
           if (::memcmp(a.expected_data, p, n) != 0)
             {
               FAIL << "write() data differs from expected";
             }
         }
       errno = a.err;
       write_actions.pop_front();
       return rv;
     }
     
     extern "C" int pipe(int fd[2]) throw ()
     {
       int rv;
       if (pipe_actions.empty() || pipe_actions.front().retval == 0)
         {
           rv = original::pipe(fd);
           if (rv == 0)
             {
               file_descriptors[fd[0]] = is_pipe;
               file_descriptors[fd[1]] = is_pipe;
               if (!pipe_actions.empty())
                 {
                   *pipe_actions.front().rfd = fd[0];
                   *pipe_actions.front().wfd = fd[1];
                 }
             }
         }
       else
         {
           rv = pipe_actions.front().retval;
           errno = pipe_actions.front().err;
         }
       if (!pipe_actions.empty()) pipe_actions.pop_front();
       return rv;
     }
     
     extern "C" int close(int fd)
     {
       int rv = original::close(fd);
       ASSERT_NE(file_descriptors[fd], is_closed);
       file_descriptors[fd] = is_closed;
       return rv;
     }
     
     extern "C" pid_t wait(void *p)
     {
       if (wait_actions.empty()) return original::wait(p);
       const wait_data &a = wait_actions.front();
       int rv = a.retval;
       errno = a.err;
       *static_cast<int*>(p) = a.pid;
       wait_actions.pop_front();
       return rv;
     }
     
     
     TEST(fork_fails, EXPECT_EXCEPTION(work::fork_exception))
     {
       fork_actions.push_back(fork_data(-1, ENOMEM));
       work obj;
     }
     
     TEST(pipe_fails, EXPECT_EXCEPTION(work::pipe_exception))
     {
       pipe_actions.push_back(pipe_data(-1, 0, 0, EMFILE));
       work obj;
     }
     
     static const char parent_str[] = "hello parent!";
     
     struct read_fixture
     {
       read_fixture()
         :  len(sizeof(parent_str) - 1),
            zero(0)
       {
         pipe_actions.push_back(pipe_data(0, &rfd, &wfd, 0));
         write_actions.push_back(write_data(sizeof(len),  &wfd, &len,
                                            sizeof(len), 0));
         fork_actions.push_back(fork_data(0x3ffff3, 0));
         wait_actions.push_back(wait_data(0, 0x3ffff3, 0));
     
         read_actions.push_back(read_data(sizeof(len),  &rfd, &len, 0));
         read_actions.push_back(read_data(len,          &rfd,  parent_str,  0));
         read_actions.push_back(read_data(sizeof(zero), &rfd, &zero,        0));
       }
       const size_t len;
       const size_t zero;
       int rfd;
       int wfd;
     };
     
     TEST(read_one_string, read_fixture)
     {
       {
         work obj;
         ASSERT_EQ(file_descriptors[wfd], is_closed);
         std::string s = obj.get_data();
         ASSERT_EQ(s, parent_str);
         s = obj.get_data();
         ASSERT_EQ(s, "");
         obj.wait();
       }
       ASSERT_EQ(file_descriptors[rfd], is_closed);
     }
     
     TEST(signal_on_string_read, read_fixture)
     {
       read_actions.insert(++read_actions.begin(),
                           read_data(-1, &rfd, 0, EINTR));
     
       {
         work obj;
         ASSERT_EQ(file_descriptors[wfd], is_closed);
         std::string s = obj.get_data();
         ASSERT_EQ(s, parent_str);
         s = obj.get_data();
         ASSERT_EQ(s, "");
         obj.wait();
       }
       ASSERT_EQ(file_descriptors[rfd], is_closed);
     }
     
     TEST(normal_work)
     {
       work obj;
       std::string s = obj.get_data();
       INFO << "s=\"" << s << "\"";
       ASSERT_EQ(s, "");
       obj.wait();
     }
     
     int main(int argc, char *argv[])
     {
       return crpcut::run(argc, argv);
     }
The result of running the test program is:
     FAILED: signal_on_string_read
     phase="running"  --------------------------------------------------------------
     /home/bjorn/devel/crpcut/doc-src/samples/process-test.cpp:271
     ASSERT_EQ(s, parent_str)
       where s = _____________
             parent_str = hello parent!
     -------------------------------------------------------------------------------
     ===============================================================================
     FAILED: normal_work
     phase="child"  ----------------------------------------------------------------
     s=""
     -------------------------------------------------------------------------------
     ===============================================================================
     Total 5 test cases selected
     UNTESTED : 0
     PASSED   : 3
     FAILED   : 2
The stubbed tests worked out well, except for signal_on_read, but that was pretty obvious from reading the code. But why did the test with the real library functions fail?
There are so many things wrong with this case, it's actually a race for several failures:
The parent process expects to read, but the child process doesn't produce any data, so the parent hangs.
The constructor returns also in the child process, making the
            child process continue in its copy of the test, calling
            obj.get_data()
            and thus read(), which fails
            because the read file descriptor is closed. When
            INFO is called, crpcut discovers that
            it's a runaway child and kills the whole process group. That's
            what the error message is all about.
          
Had the child exited instead of returned from
            the constructor, the parent would've received a
            SIGCHLD. Since it ignores that signal,
            nothing would have been gained. The
            variable len would probably still contain
            zero, which would've made the function return an empty string
            as expected by the test, causing a false pass.
          
Fortunately crpcut is robust against this kind of abuse, so no harm is done. Unfortunately the details of the report aren't too impressive, but they at least hint about the cause of the error.
[1] You can't easily provide your own
        dlopen(), dlsym() or
        dlclose()
        however.
[2] Rolling
        your own malloc(), calloc(),
        realloc(), free(),
        operator new(),
        operator delete(),
        operator new[]() and
        operator delete[]() is not only hard work,
        but largely unnecessary since crpcut provides functions you
        can control. See the chapter on
        “Heap Management”.