Implemented g_test_trap_fork() API.
authorTim Janik <timj@src.gnome.org>
Tue, 20 Nov 2007 15:00:25 +0000 (15:00 +0000)
committerTim Janik <timj@src.gnome.org>
Tue, 20 Nov 2007 15:00:25 +0000 (15:00 +0000)
* gtestframework.h: added g_assert_cmphex(). reworked g_test_trap*() API.

* gtestframework.c: implemented g_test_trap_fork() API.

svn path=/trunk/; revision=5879

glib/gtestframework.c
glib/gtestframework.h

index 9bfb4e3..64225da 100644 (file)
  */
 #include "config.h"
 #include "gtestframework.h"
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <fcntl.h>
 #include <string.h>
 #include <stdlib.h>
+#include <unistd.h>
+#include <errno.h>
+#include <signal.h>
+#ifdef HAVE_SYS_SELECT_H
+#include <sys/select.h>
+#endif /* HAVE_SYS_SELECT_H */
 
 /* --- structures --- */
 struct GTestCase
@@ -37,7 +46,11 @@ struct GTestSuite
   GSList *cases;
 };
 
+/* --- prototypes --- */
+static void                     test_trap_clear         (void);
+
 /* --- variables --- */
+static int         test_stdmsg = 1;
 static gboolean    test_mode_quick = TRUE;
 static gboolean    test_mode_perf = FALSE;
 static gboolean    test_mode_fatal = TRUE;
@@ -51,6 +64,10 @@ static gchar      *test_run_name = "";
 static GSList     *test_paths = NULL;
 static GTestSuite *test_suite_root = NULL;
 static GSList     *test_run_free_queue = NULL;
+static int         test_trap_last_status = 0;
+static int         test_trap_last_pid = 0;
+static char       *test_trap_last_stdout = NULL;
+static char       *test_trap_last_stderr = NULL;
 
 /* --- functions --- */
 static void
@@ -270,14 +287,15 @@ test_case_run (GTestCase *tc)
       if (tc->fixture_setup)
         tc->fixture_setup (fixture);
       tc->fixture_test (fixture);
-      if (tc->fixture_teardown)
-        tc->fixture_teardown (fixture);
+      test_trap_clear();
       while (test_run_free_queue)
         {
           gpointer freeme = test_run_free_queue->data;
           test_run_free_queue = g_slist_delete_link (test_run_free_queue, test_run_free_queue);
           g_free (freeme);
         }
+      if (tc->fixture_teardown)
+        tc->fixture_teardown (fixture);
       g_free (fixture);
       if (!test_run_quiet)
         g_print ("OK\n");
@@ -398,6 +416,7 @@ g_assertion_message_cmpnum (const char     *domain,
   switch (numtype)
     {
     case 'i':   s = g_strdup_printf ("assertion failed (%s): (%.0Lf %s %.0Lf)", expr, arg1, cmp, arg2); break;
+    case 'x':   s = g_strdup_printf ("assertion failed (%s): (0x%08Lx %s 0x%08Lx)", expr, (guint64) arg1, cmp, (guint64) arg2); break;
     case 'f':   s = g_strdup_printf ("assertion failed (%s): (%.9Lg %s %.9Lg)", expr, arg1, cmp, arg2); break;
       /* ideally use: floats=%.7g double=%.17g */
     }
@@ -437,3 +456,300 @@ g_strcmp0 (const char     *str1,
     return str1 != str2;
   return strcmp (str1, str2);
 }
+
+static int /* 0 on success */
+kill_child (int  pid,
+            int *status,
+            int  patience)
+{
+  int wr;
+  if (patience >= 3)    /* try graceful reap */
+    {
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+    }
+  if (patience >= 2)    /* try SIGHUP */
+    {
+      kill (pid, SIGHUP);
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+      g_usleep (20 * 1000); /* give it some scheduling/shutdown time */
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+      g_usleep (50 * 1000); /* give it some scheduling/shutdown time */
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+      g_usleep (100 * 1000); /* give it some scheduling/shutdown time */
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+    }
+  if (patience >= 1)    /* try SIGTERM */
+    {
+      kill (pid, SIGTERM);
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+      g_usleep (200 * 1000); /* give it some scheduling/shutdown time */
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+      g_usleep (400 * 1000); /* give it some scheduling/shutdown time */
+      if (waitpid (pid, status, WNOHANG) > 0)
+        return 0;
+    }
+  /* finish it off */
+  kill (pid, SIGKILL);
+  do
+    wr = waitpid (pid, status, 0);
+  while (wr < 0 && errno == EINTR);
+  return wr;
+}
+
+static inline int
+g_string_must_read (GString *gstring,
+                    int      fd)
+{
+#define STRING_BUFFER_SIZE     4096
+  char buf[STRING_BUFFER_SIZE];
+  gssize bytes;
+ again:
+  bytes = read (fd, buf, sizeof (buf));
+  if (bytes == 0)
+    return 0; /* EOF, calling this function assumes data is available */
+  else if (bytes > 0)
+    {
+      g_string_append_len (gstring, buf, bytes);
+      return 1;
+    }
+  else if (bytes < 0 && errno == EINTR)
+    goto again;
+  else /* bytes < 0 */
+    {
+      g_warning ("failed to read() from child process (%d): %s", test_trap_last_pid, g_strerror (errno));
+      return 1; /* ignore error after warning */
+    }
+}
+
+static inline void
+g_string_write_out (GString *gstring,
+                    int      outfd,
+                    int     *stringpos)
+{
+  if (*stringpos < gstring->len)
+    {
+      int r;
+      do
+        r = write (outfd, gstring->str + *stringpos, gstring->len - *stringpos);
+      while (r < 0 && errno == EINTR);
+      *stringpos += MAX (r, 0);
+    }
+}
+
+static int
+sane_dup2 (int fd1,
+           int fd2)
+{
+  int ret;
+  do
+    ret = dup2 (fd1, fd2);
+  while (ret < 0 && errno == EINTR);
+  return ret;
+}
+
+static void
+test_trap_clear (void)
+{
+  test_trap_last_status = 0;
+  test_trap_last_pid = 0;
+  g_free (test_trap_last_stdout);
+  test_trap_last_stdout = NULL;
+  g_free (test_trap_last_stderr);
+  test_trap_last_stderr = NULL;
+}
+
+static guint64
+test_time_stamp (void)
+{
+  GTimeVal tv;
+  guint64 stamp;
+  g_get_current_time (&tv);
+  stamp = tv.tv_sec;
+  stamp = stamp * 1000000 + tv.tv_usec;
+  return stamp;
+}
+
+gboolean
+g_test_trap_fork (guint64        usec_timeout,
+                  GTestTrapFlags test_trap_flags)
+{
+  int stdout_pipe[2] = { -1, -1 };
+  int stderr_pipe[2] = { -1, -1 };
+  int stdtst_pipe[2] = { -1, -1 };
+  test_trap_clear();
+  if (pipe (stdout_pipe) < 0 || pipe (stderr_pipe) < 0 || pipe (stdtst_pipe) < 0)
+    g_error ("failed to create pipes to fork test program: %s", g_strerror (errno));
+  signal (SIGCHLD, SIG_DFL);
+  test_trap_last_pid = fork ();
+  if (test_trap_last_pid < 0)
+    g_error ("failed to fork test program: %s", g_strerror (errno));
+  if (test_trap_last_pid == 0)  /* child */
+    {
+      int fd0 = -1;
+      close (stdout_pipe[0]);
+      close (stderr_pipe[0]);
+      close (stdtst_pipe[0]);
+      if (!(test_trap_flags & G_TEST_TRAP_INHERIT_STDIN))
+        fd0 = open ("/dev/null", O_RDONLY);
+      if (sane_dup2 (stdout_pipe[1], 1) < 0 || sane_dup2 (stderr_pipe[1], 2) < 0 || (fd0 >= 0 && sane_dup2 (fd0, 0) < 0))
+        g_error ("failed to dup2() in forked test program: %s", g_strerror (errno));
+      if (fd0 >= 3)
+        close (fd0);
+      if (stdout_pipe[1] >= 3)
+        close (stdout_pipe[1]);
+      if (stderr_pipe[1] >= 3)
+        close (stderr_pipe[1]);
+      test_stdmsg = stdtst_pipe[1];
+      return TRUE;
+    }
+  else                          /* parent */
+    {
+      GString *sout = g_string_new (NULL);
+      GString *serr = g_string_new (NULL);
+      GString *stst = g_string_new (NULL);
+      guint64 sstamp;
+      int soutpos = 0, serrpos = 0, ststpos = 0, wr, need_wait = TRUE;
+      close (stdout_pipe[1]);
+      close (stderr_pipe[1]);
+      close (stdtst_pipe[1]);
+      sstamp = test_time_stamp();
+      /* read data until we get EOF on all pipes */
+      while (stdout_pipe[0] >= 0 || stderr_pipe[0] >= 0 || stdtst_pipe[0] > 0)
+        {
+          fd_set fds;
+          struct timeval tv;
+          FD_ZERO (&fds);
+          if (stdout_pipe[0] >= 0)
+            FD_SET (stdout_pipe[0], &fds);
+          if (stderr_pipe[0] >= 0)
+            FD_SET (stderr_pipe[0], &fds);
+          if (stdtst_pipe[0] >= 0)
+            FD_SET (stdtst_pipe[0], &fds);
+          tv.tv_sec = 0;
+          tv.tv_usec = MIN (usec_timeout ? usec_timeout : 1000000, 100 * 1000); // sleep at most 0.5 seconds to catch clock skews, etc.
+          int ret = select (MAX (MAX (stdout_pipe[0], stderr_pipe[0]), stdtst_pipe[0]) + 1, &fds, NULL, NULL, &tv);
+          if (ret < 0 && errno != EINTR)
+            {
+              g_warning ("Unexpected error in select() while reading from child process (%d): %s", test_trap_last_pid, g_strerror (errno));
+              break;
+            }
+          if (stdout_pipe[0] >= 0 && FD_ISSET (stdout_pipe[0], &fds) &&
+              g_string_must_read (sout, stdout_pipe[0]) == 0)
+            {
+              close (stdout_pipe[0]);
+              stdout_pipe[0] = -1;
+            }
+          if (stderr_pipe[0] >= 0 && FD_ISSET (stderr_pipe[0], &fds) &&
+              g_string_must_read (serr, stderr_pipe[0]) == 0)
+            {
+              close (stderr_pipe[0]);
+              stderr_pipe[0] = -1;
+            }
+          if (stdtst_pipe[0] >= 0 && FD_ISSET (stdtst_pipe[0], &fds) &&
+              g_string_must_read (stst, stdtst_pipe[0]) == 0)
+            {
+              close (stdtst_pipe[0]);
+              stdtst_pipe[0] = -1;
+            }
+          if (!(test_trap_flags & G_TEST_TRAP_SILENCE_STDOUT))
+            g_string_write_out (sout, 1, &soutpos);
+          if (!(test_trap_flags & G_TEST_TRAP_SILENCE_STDERR))
+            g_string_write_out (serr, 2, &serrpos);
+          if (TRUE) // FIXME: needs capturing into log file
+            g_string_write_out (stst, 1, &ststpos);
+          if (usec_timeout)
+            {
+              guint64 nstamp = test_time_stamp();
+              int status = 0;
+              sstamp = MIN (sstamp, nstamp); // guard against backwards clock skews
+              if (usec_timeout < nstamp - sstamp)
+                {
+                  /* timeout reached, need to abort the child now */
+                  kill_child (test_trap_last_pid, &status, 3);
+                  test_trap_last_status = 1024; /* timeout */
+                  if (0 && WIFSIGNALED (status))
+                    g_printerr ("%s: child timed out and received: %s\n", G_STRFUNC, g_strsignal (WTERMSIG (status)));
+                  need_wait = FALSE;
+                  break;
+                }
+            }
+        }
+      close (stdout_pipe[0]);
+      close (stderr_pipe[0]);
+      close (stdtst_pipe[0]);
+      if (need_wait)
+        {
+          int status = 0;
+          do
+            wr = waitpid (test_trap_last_pid, &status, 0);
+          while (wr < 0 && errno == EINTR);
+          if (WIFEXITED (status)) /* normal exit */
+            test_trap_last_status = WEXITSTATUS (status); /* 0..255 */
+          else if (WIFSIGNALED (status))
+            test_trap_last_status = (WTERMSIG (status) << 12); /* signalled */
+          else /* WCOREDUMP (status) */
+            test_trap_last_status = 512; /* coredump */
+        }
+      test_trap_last_stdout = g_string_free (sout, FALSE);
+      test_trap_last_stderr = g_string_free (serr, FALSE);
+      g_string_free (stst, TRUE);
+      return FALSE;
+    }
+}
+
+gboolean
+g_test_trap_has_passed (void)
+{
+  return test_trap_last_status == 0; /* exit_status == 0 && !signal && !coredump */
+}
+
+gboolean
+g_test_trap_reached_timeout (void)
+{
+  return 0 != (test_trap_last_status & 1024); /* timeout flag */
+}
+
+void
+g_test_trap_assertions (const char     *domain,
+                        const char     *file,
+                        int             line,
+                        const char     *func,
+                        gboolean        must_pass,
+                        gboolean        must_fail,
+                        const char     *stdout_pattern,
+                        const char     *stderr_pattern)
+{
+  if (test_trap_last_pid == 0)
+    g_error ("child process failed to exit after g_test_trap_fork() and before g_test_trap_assert*()");
+  if (must_pass && !g_test_trap_has_passed())
+    {
+      char *msg = g_strdup_printf ("child process (%d) of test trap failed unexpectedly", test_trap_last_pid);
+      g_assertion_message (domain, file, line, func, msg);
+      g_free (msg);
+    }
+  if (must_fail && g_test_trap_has_passed())
+    {
+      char *msg = g_strdup_printf ("child process (%d) did not fail as expected", test_trap_last_pid);
+      g_assertion_message (domain, file, line, func, msg);
+      g_free (msg);
+    }
+  if (stdout_pattern && !g_pattern_match_simple (stdout_pattern, test_trap_last_stdout))
+    {
+      char *msg = g_strdup_printf ("stdout of child process (%d) failed to match: %s", test_trap_last_pid, stdout_pattern);
+      g_assertion_message (domain, file, line, func, msg);
+      g_free (msg);
+    }
+  if (stderr_pattern && !g_pattern_match_simple (stderr_pattern, test_trap_last_stderr))
+    {
+      char *msg = g_strdup_printf ("stderr of child process (%d) failed to match: %s", test_trap_last_pid, stderr_pattern);
+      g_assertion_message (domain, file, line, func, msg);
+      g_free (msg);
+    }
+}
index 12fb4d3..69687a9 100644 (file)
@@ -33,6 +33,9 @@ typedef struct GTestSuite GTestSuite;
 #define g_assert_cmpint(n1, cmp, n2)    do { if (n1 cmp n2) ; else \
                                           g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
                                             #n1 " " #cmp " " #n2, n1, #cmp, n2, 'i'); } while (0)
+#define g_assert_cmphex(n1, cmp, n2)    do { if (n1 cmp n2) ; else \
+                                          g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
+                                            #n1 " " #cmp " " #n2, n1, #cmp, n2, 'x'); } while (0)
 #define g_assert_cmpfloat(n1,cmp,n2)    do { if (n1 cmp n2) ; else \
                                           g_assertion_message_cmpnum (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \
                                             #n1 " " #cmp " " #n2, n1, #cmp, n2, 'f'); } while (0)
@@ -49,13 +52,6 @@ void    g_test_maximized_result         (double          maximized_quantity,
                                          const char     *format,
                                          ...) G_GNUC_PRINTF (2, 3);
 
-/* guards used around forked tests */
-guint   g_test_trap_fork                ();
-void    g_test_trap_assert_passed       (void);
-void    g_test_trap_assert_failed       (void);
-void    g_test_trap_assert_stdout       (const char     *stdout_pattern);
-void    g_test_trap_assert_stderr       (const char     *stderr_pattern);
-
 /* initialize testing framework */
 void    g_test_init                     (int            *argc,
                                          char         ***argv,
@@ -83,25 +79,48 @@ double  g_test_timer_last               (void); // repeat last elapsed() result
 void    g_test_queue_free               (gpointer gfree_pointer);
 void    g_test_queue_unref              (gpointer gobjectunref_pointer);
 
+/* test traps are guards used around forked tests */
+typedef enum {
+  G_TEST_TRAP_SILENCE_STDOUT    = 1 << 7,
+  G_TEST_TRAP_SILENCE_STDERR    = 1 << 8,
+  G_TEST_TRAP_INHERIT_STDIN     = 1 << 9,
+} GTestTrapFlags;
+gboolean g_test_trap_fork               (guint64              usec_timeout,
+                                         GTestTrapFlags       test_trap_flags);
+gboolean g_test_trap_has_passed         (void);
+gboolean g_test_trap_reached_timeout    (void);
+#define  g_test_trap_assert_passed()            g_test_trap_assertions (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, 1, 0, 0, 0)
+#define  g_test_trap_assert_failed()            g_test_trap_assertions (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, 0, 1, 0, 0)
+#define  g_test_trap_assert_stdout(soutpattern) g_test_trap_assertions (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, 0, 0, soutpattern, 0)
+#define  g_test_trap_assert_stderr(serrpattern) g_test_trap_assertions (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, 0, 0, 0, serrpattern)
+
 /* provide seed-able random numbers for tests */
-long double     g_test_rand_range       (long double range_start,
-                                         long double range_end);
+double   g_test_rand_range              (double          range_start,
+                                         double          range_end);
 
 /* semi-internal API */
-GTestCase*      g_test_create_case      (const char     *test_name,
+GTestCase*    g_test_create_case        (const char     *test_name,
                                          gsize           data_size,
                                          void          (*data_setup) (void),
                                          void          (*data_test) (void),
                                          void          (*data_teardown) (void));
-GTestSuite*     g_test_create_suite     (const char     *suite_name);
-GTestSuite*     g_test_get_root         (void);
-void            g_test_suite_add        (GTestSuite     *suite,
+GTestSuite*   g_test_create_suite       (const char     *suite_name);
+GTestSuite*   g_test_get_root           (void);
+void          g_test_suite_add          (GTestSuite     *suite,
                                          GTestCase      *test_case);
-void            g_test_suite_add_suite  (GTestSuite     *suite,
+void          g_test_suite_add_suite    (GTestSuite     *suite,
                                          GTestSuite     *nestedsuite);
-int             g_test_run_suite        (GTestSuite     *suite);
+int           g_test_run_suite          (GTestSuite     *suite);
 
 /* internal ABI */
+void    g_test_trap_assertions          (const char     *domain,
+                                         const char     *file,
+                                         int             line,
+                                         const char     *func,
+                                         gboolean        must_pass,
+                                         gboolean        must_fail,
+                                         const char     *stdout_pattern,
+                                         const char     *stderr_pattern);
 void    g_assertion_message             (const char     *domain,
                                          const char     *file,
                                          int             line,