diff --git a/glib/gtestframework.c b/glib/gtestframework.c index 9bfb4e3d1..64225dabe 100644 --- a/glib/gtestframework.c +++ b/glib/gtestframework.c @@ -18,8 +18,17 @@ */ #include "config.h" #include "gtestframework.h" +#include +#include +#include #include #include +#include +#include +#include +#ifdef HAVE_SYS_SELECT_H +#include +#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); + } +} diff --git a/glib/gtestframework.h b/glib/gtestframework.h index 12fb4d3ad..69687a99a 100644 --- a/glib/gtestframework.h +++ b/glib/gtestframework.h @@ -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,