/*
* This file is part of Cockpit.
*
* Copyright (C) 2014 Red Hat, Inc.
*
* Cockpit is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* Cockpit is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with Cockpit; If not, see .
*/
#include "config.h"
#include "cockpittest.h"
#include "cockpitjson.h"
#include "cockpitconf.h"
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
/*
* HACK: We can't yet use g_test_expect_message() and friends.
* They were pretty broken until GLib 2.40 if you have any debug
* or info messages ... which we do.
*
* https://bugzilla.gnome.org/show_bug.cgi?id=661926
*/
static gboolean cockpit_test_init_was_called = FALSE;
static const gchar *orig_g_debug;
/* In cockpitconf.c */
extern const gchar *cockpit_config_file;
G_LOCK_DEFINE (expected);
typedef struct {
gchar *log_domain;
GLogLevelFlags log_level;
gchar *pattern;
const gchar *file;
int line;
const gchar *func;
gboolean skipable;
gboolean optional;
} ExpectedMessage;
static void
expected_message_free (gpointer data)
{
ExpectedMessage *expected = data;
g_free (expected->log_domain);
g_free (expected->pattern);
g_free (expected);
}
static gint ignore_fatal_count = 0;
static GSList *expected_messages = NULL;
static GLogFunc gtest_default_log_handler = NULL;
static const gchar *
calc_prefix (gint level)
{
switch (level)
{
case G_LOG_LEVEL_ERROR:
return "ERROR";
case G_LOG_LEVEL_CRITICAL:
return "CRITICAL";
case G_LOG_LEVEL_WARNING:
return "WARNING";
case G_LOG_LEVEL_MESSAGE:
return "Message";
case G_LOG_LEVEL_INFO:
return "INFO";
case G_LOG_LEVEL_DEBUG:
return "DEBUG";
default:
return "Unknown";
}
}
static gboolean
expected_fatal_handler (const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer user_data)
{
gboolean ret = TRUE;
if (log_level & G_LOG_FLAG_FATAL)
{
G_LOCK (expected);
if (ignore_fatal_count > 0)
{
ignore_fatal_count--;
ret = FALSE;
}
G_UNLOCK (expected);
}
return ret;
}
static void
expected_message_handler (const gchar *log_domain,
GLogLevelFlags log_level,
const gchar *message,
gpointer user_data)
{
gint level = log_level & G_LOG_LEVEL_MASK;
ExpectedMessage *expected = NULL;
GSList *l = NULL;
gchar *expected_message;
gboolean skip = FALSE;
G_LOCK (expected);
if (level && expected_messages &&
(level & G_LOG_LEVEL_DEBUG) == 0)
{
if (log_level & G_LOG_FLAG_FATAL)
{
ignore_fatal_count = 1;
/* This handler is reset for each test, so set it right before we need it */
g_test_log_set_fatal_handler (expected_fatal_handler, NULL);
}
/* Loop until we find a non-skipable message or have a match */
for (l = expected_messages; l != NULL; l = l->next)
{
expected = l->data;
if (g_strcmp0 (expected->log_domain, log_domain) == 0 &&
((log_level & expected->log_level) == expected->log_level) &&
g_pattern_match_simple (expected->pattern, message))
{
expected_messages = g_slist_delete_link (expected_messages, l);
expected_message_free (expected);
skip = TRUE;
break;
}
else if (!expected->skipable)
{
break;
}
}
}
G_UNLOCK (expected);
if (skip)
return;
gtest_default_log_handler (log_domain, log_level, message, NULL);
if (expected)
{
expected_message = g_strdup_printf ("Got unexpected message: %s instead of %s-%s: %s",
message,
expected->log_domain,
calc_prefix (expected->log_level),
expected->pattern);
g_assertion_message (expected->log_domain, expected->file, expected->line,
expected->func, expected_message);
g_free (expected_message);
}
}
/**
* cockpit_test_init:
*
* Call this instead of g_test_init() to setup a cocpit test.
* Enables use of cockpit_expect_xxx() functions.
*
* Calls g_type_init() if necessary. Sets up cleaner logging
* during testing.
*
* Also calls g_test_init() for you.
*/
void
cockpit_test_init (int *argc,
char ***argv)
{
static gchar path[4096];
gchar *basename;
signal (SIGPIPE, SIG_IGN);
g_setenv ("GIO_USE_VFS", "local", TRUE);
g_setenv ("GSETTINGS_BACKEND", "memory", TRUE);
g_setenv ("GIO_USE_PROXY_RESOLVER", "dummy", TRUE);
g_assert (g_snprintf (path, sizeof (path), "%s:%s", BUILDDIR, g_getenv ("PATH")) < sizeof (path));
g_setenv ("PATH", path, TRUE);
/* For our process (children are handled through $G_DEBUG) */
g_log_set_always_fatal (G_LOG_LEVEL_ERROR | G_LOG_LEVEL_CRITICAL | G_LOG_LEVEL_WARNING);
g_type_init ();
// System cockpit configuration file should not be loaded
cockpit_config_file = NULL;
if (*argc > 0)
{
basename = g_path_get_basename ((*argv)[0]);
g_set_prgname (basename);
g_free (basename);
}
g_test_init (argc, argv, NULL);
/* Chain to the gtest log handler */
gtest_default_log_handler = g_log_set_default_handler (expected_message_handler, NULL);
g_assert (gtest_default_log_handler != NULL);
cockpit_test_init_was_called = TRUE;
}
void
_cockpit_expect_logged_msg (const char *domain,
const gchar *file,
int line,
const gchar *func,
GLogLevelFlags log_level,
const gchar *pattern,
gboolean skipable,
gboolean optional)
{
ExpectedMessage *expected;
g_assert (cockpit_test_init_was_called);
g_return_if_fail (log_level != 0);
g_return_if_fail (pattern != NULL);
g_return_if_fail (~log_level & G_LOG_LEVEL_ERROR);
g_return_if_fail (log_level & G_LOG_LEVEL_MASK);
expected = g_new (ExpectedMessage, 1);
expected->log_domain = g_strdup (domain);
expected->log_level = log_level & G_LOG_LEVEL_MASK;
expected->pattern = g_strdup (pattern);
expected->file = file;
expected->line = line;
expected->func = func;
expected->skipable = optional ? TRUE : skipable;
expected->optional = optional;
G_LOCK (expected);
expected_messages = g_slist_append (expected_messages, expected);
G_UNLOCK (expected);
}
/**
* cockpit_assert_expected:
*
* Assert that all the things we were expecting in a test
* happened. This should be called in a teardown() function
* or after a cockpit_expect_xxx() function.
*/
void
cockpit_assert_expected (void)
{
ExpectedMessage *expected = NULL;
gchar *message = NULL;
GSList *l = NULL;
g_assert (cockpit_test_init_was_called);
G_LOCK (expected);
if (expected_messages)
{
for (l = expected_messages; l != NULL; l = l->next)
{
expected = l->data;
if (!expected->optional)
{
message = g_strdup_printf ("Did not see expected %s-%s: %s",
expected->log_domain,
calc_prefix (expected->log_level),
expected->pattern);
break;
}
}
}
G_UNLOCK (expected);
if (message)
{
g_assertion_message (expected->log_domain, expected->file, expected->line,
expected->func, message);
g_free (message);
}
g_slist_free_full (expected_messages, expected_message_free);
expected_messages = NULL;
ignore_fatal_count = 0;
}
/**
* cockpit_assert_strmatch:
* @str: the string
* @pattern: to match
*
* Checks that @str matches the wildcard style @pattern
*/
void
_cockpit_assert_strmatch_msg (const char *domain,
const char *file,
int line,
const char *func,
const gchar *string,
const gchar *pattern)
{
const gchar *suffix;
gchar *escaped;
gchar *msg;
int len;
if (!string || !g_pattern_match_simple (pattern, string))
{
escaped = g_strescape (pattern, "");
if (!string)
{
msg = g_strdup_printf ("'%s' does not match: (null)", escaped);
}
else
{
suffix = "";
len = strlen (string);
/* To avoid insane output */
if (len > 8192)
{
len = 8192;
suffix = "\n...\n";
}
msg = g_strdup_printf ("'%s' does not match: %.*s%s", escaped, len, string, suffix);
}
g_assertion_message (domain, file, line, func, msg);
g_free (escaped);
g_free (msg);
}
}
/**
* cockpit_test_skip()
*
* Can't call g_test_skip(). It's not available in
* the GLib's we target. Call this instead.
*
* Same caveat applies. Must return from test, this
* doesn't somehow jump out for you.
*/
void
cockpit_test_skip (const gchar *reason)
{
if (g_test_verbose ())
g_print ("GTest: skipping: %s\n", reason);
else
g_print ("SKIP: %s ", reason);
}
void
_cockpit_assert_json_eq_msg (const char *domain,
const char *file,
int line,
const char *func,
gpointer object_or_array,
const gchar *expect)
{
GError *error = NULL;
JsonNode *node;
JsonNode *exnode;
gchar *escaped;
gchar *msg;
if (expect[0] == '[')
{
node = json_node_new (JSON_NODE_ARRAY);
json_node_set_array (node, object_or_array);
}
else
{
node = json_node_new (JSON_NODE_OBJECT);
json_node_set_object (node, object_or_array);
}
exnode = cockpit_json_parse (expect, -1, &error);
if (error)
g_assertion_message_error (domain, file, line, func, "error", error, 0, 0);
g_assert (exnode);
if (!cockpit_json_equal (exnode, node))
{
escaped = cockpit_json_write (node, NULL);
msg = g_strdup_printf ("%s != %s", escaped, expect);
g_assertion_message (domain, file, line, func, msg);
g_free (escaped);
g_free (msg);
}
json_node_free (node);
json_node_free (exnode);
}
static gchar *
test_escape_data (const guchar *data,
gssize n_data)
{
static const char HEXC[] = "0123456789ABCDEF";
GString *result;
gchar c;
gsize i;
guchar j;
if (!data)
return g_strdup ("NULL");
result = g_string_sized_new (n_data * 2 + 1);
for (i = 0; i < n_data; ++i)
{
c = data[i];
if (g_ascii_isprint (c) && !strchr ("\n\r\v", c))
{
g_string_append_c (result, c);
}
else
{
g_string_append (result, "\\x");
j = c >> 4 & 0xf;
g_string_append_c (result, HEXC[j]);
j = c & 0xf;
g_string_append_c (result, HEXC[j]);
}
}
return g_string_free (result, FALSE);
}
void
_cockpit_assert_data_eq_msg (const char *domain,
const char *file,
int line,
const char *func,
gconstpointer data,
gssize len,
gconstpointer expect,
gssize exp_len)
{
char *a1, *a2, *s;
if (!data && !expect)
return;
if (len < 0)
len = strlen (data ?: "");
if (exp_len < 0)
exp_len = strlen (expect ?: "");
if (len == exp_len)
{
if (len == 0)
return;
else if (data && expect && memcmp (data, expect, len) == 0)
return;
}
a1 = test_escape_data (data, len);
a2 = test_escape_data (expect, exp_len);
s = g_strdup_printf ("data is not the same (%s != %s)", a1, a2);
g_free (a1);
g_free (a2);
g_assertion_message (domain, file, line, func, s);
g_free (s);
}
void
_cockpit_assert_bytes_eq_msg (const char *domain,
const char *file,
int line,
const char *func,
GBytes *data,
gconstpointer expect,
gssize exp_len)
{
_cockpit_assert_data_eq_msg (domain, file, line, func,
g_bytes_get_data (data, NULL),
g_bytes_get_size (data),
expect, exp_len);
}
/*
* This gdb code only works if /proc/sys/kernel/yama/ptrace_scope is set to zero
* See: https://wiki.ubuntu.com/SecurityTeam/Roadmap/KernelHardening#ptrace%20Protection
*/
static gboolean stack_trace_done = FALSE;
static void
stack_trace_sigchld (int signum)
{
stack_trace_done = TRUE;
}
static void
stack_trace (char **args)
{
pid_t pid;
int in_fd[2];
int out_fd[2];
fd_set fdset;
fd_set readset;
struct timeval tv;
int sel, idx, state;
char buffer[256];
char c;
stack_trace_done = FALSE;
signal (SIGCHLD, stack_trace_sigchld);
if ((pipe (in_fd) == -1) || (pipe (out_fd) == -1))
{
perror ("unable to open pipe");
_exit (0);
}
pid = fork ();
if (pid == 0)
{
/* Save stderr for printing failure below */
int old_err = dup (2);
int res = fcntl (old_err, F_GETFD);
if (res == -1)
{
perror ("getfd failed");
}
else if (fcntl (old_err, F_SETFD, res | FD_CLOEXEC) == -1)
{
perror ("setfd failed");
}
else
{
if (dup2 (in_fd[0], 0) < 0 || dup2 (out_fd[1], 1) < 0)
{
perror ("dup fds failed");
}
else
{
execvp (args[0], args);
perror ("exec gdb failed");
}
}
_exit (0);
}
else if (pid == (pid_t) -1)
{
perror ("unable to fork");
_exit (0);
}
FD_ZERO (&fdset);
FD_SET (out_fd[0], &fdset);
if (write (in_fd[1], "backtrace\n", 10) != 10 ||
write (in_fd[1], "quit\n", 5) != 5)
{
perror ("unable to send commands to gdb");
_exit (0);
}
idx = 0;
state = 0;
while (1)
{
readset = fdset;
tv.tv_sec = 1;
tv.tv_usec = 0;
sel = select (FD_SETSIZE, &readset, NULL, NULL, &tv);
if (sel == -1)
break;
if ((sel > 0) && (FD_ISSET (out_fd[0], &readset)))
{
if (read (out_fd[0], &c, 1))
{
switch (state)
{
case 0:
if (c == '#')
{
state = 1;
idx = 0;
buffer[idx++] = c;
}
break;
case 1:
buffer[idx++] = c;
if ((c == '\n') || (c == '\r'))
{
buffer[idx] = 0;
fprintf (stderr, "%s", buffer);
state = 0;
idx = 0;
}
break;
default:
break;
}
}
}
else if (stack_trace_done)
break;
}
close (in_fd[0]);
close (in_fd[1]);
close (out_fd[0]);
close (out_fd[1]);
_exit (0);
}
static void
gdb_stack_trace (void)
{
pid_t pid;
gchar buf[16];
gchar *args[4] = { "gdb", "-p", buf, NULL };
int status;
sprintf (buf, "%u", (guint) getpid ());
pid = fork ();
if (pid == 0)
{
stack_trace (args);
_exit (0);
}
else if (pid == (pid_t) -1)
{
perror ("unable to fork gdb");
return;
}
waitpid (pid, &status, 0);
}
void
cockpit_test_signal_backtrace (int sig)
{
void *array[16];
size_t size;
signal (sig, SIG_DFL);
/* Try to trace with gdb first */
gdb_stack_trace ();
/* In case above didn't work, print raw stack trace */
size = backtrace (array, G_N_ELEMENTS (array));
/* print out all the frames to stderr */
fprintf (stderr, "Error: signal %s:\n", strsignal (sig));
backtrace_symbols_fd (array, size, STDERR_FILENO);
raise (sig);
}
GInetAddress *
cockpit_test_find_non_loopback_address (void)
{
GInetAddress *inet = NULL;
struct ifaddrs *ifas, *ifa;
gpointer bytes;
g_assert_cmpint (getifaddrs (&ifas), ==, 0);
for (ifa = ifas; ifa != NULL; ifa = ifa->ifa_next)
{
if (!(ifa->ifa_flags & IFF_UP))
continue;
if (ifa->ifa_addr == NULL)
continue;
if (ifa->ifa_addr->sa_family == AF_INET)
{
bytes = &(((struct sockaddr_in *)ifa->ifa_addr)->sin_addr);
inet = g_inet_address_new_from_bytes (bytes, G_SOCKET_FAMILY_IPV4);
}
else if (ifa->ifa_addr->sa_family == AF_INET6)
{
bytes = &(((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr);
inet = g_inet_address_new_from_bytes (bytes, G_SOCKET_FAMILY_IPV6);
}
if (inet)
{
if (!g_inet_address_get_is_loopback (inet))
break;
g_object_unref (inet);
inet = NULL;
}
}
freeifaddrs (ifas);
return inet;
}
void
cockpit_test_allow_warnings (void)
{
/* make some noise if this gets called twice */
g_return_if_fail (orig_g_debug == NULL);
orig_g_debug = g_getenv ("G_DEBUG");
g_setenv ("G_DEBUG", "fatal-criticals", TRUE);
}
void
cockpit_test_reset_warnings (void)
{
if (orig_g_debug != NULL)
{
g_setenv ("G_DEBUG", orig_g_debug, TRUE);
orig_g_debug = NULL;
}
}