/* * Copyright (C) 2014 Red Hat, Inc. * SPDX-License-Identifier: LGPL-2.1-or-later */ #include "config.h" #include "cockpittest.h" #include "common/cockpitconf.h" #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 skippable; 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-skippable 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->skippable) { 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. * * 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); cockpit_test_setenv ("GIO_USE_VFS", "local"); cockpit_test_setenv ("GSETTINGS_BACKEND", "memory"); cockpit_test_setenv ("GIO_USE_PROXY_RESOLVER", "dummy"); g_assert (g_snprintf (path, sizeof (path), "%s:%s", BUILDDIR, g_getenv ("PATH")) < sizeof (path)); cockpit_test_setenv ("PATH", path); /* 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); // 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 skippable, 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->skippable = optional ? TRUE : skippable; 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); } } 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; JsonParser *parser; JsonNode *node; JsonNode *exnode; 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); } parser = json_parser_new (); if (!json_parser_load_from_data (parser, expect, -1, &error)) { g_assertion_message_error (domain, file, line, func, "error", error, 0, 0); g_object_unref (parser); json_node_free (node); return; } exnode = json_parser_get_root (parser); g_assert (exnode); if (!json_node_equal (exnode, node)) { JsonGenerator *generator = json_generator_new (); json_generator_set_root (generator, node); gchar *escaped = json_generator_to_data (generator, NULL); g_object_unref (generator); gchar *msg = g_strdup_printf ("%s != %s", escaped, expect); g_assertion_message (domain, file, line, func, msg); g_free (escaped); g_free (msg); } g_object_unref (parser); json_node_free (node); } void _cockpit_assert_gvariant_eq_msg (const char *domain, const char *file, int line, const char *func, GVariant *actual, const gchar *expected) { GVariant *expected_variant = g_variant_parse (NULL, expected, NULL, NULL, NULL); if (!g_variant_equal (actual, expected_variant)) { gchar *actual_string = g_variant_print (actual, TRUE); gchar *msg = g_strdup_printf ("%s != %s", actual_string, expected); g_assertion_message (domain, file, line, func, msg); g_free (msg); g_free (actual_string); } g_variant_unref (expected_variant); } 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); } 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"); cockpit_test_setenv ("G_DEBUG", "fatal-criticals"); } void cockpit_test_reset_warnings (void) { if (orig_g_debug != NULL) { cockpit_test_setenv ("G_DEBUG", orig_g_debug); orig_g_debug = NULL; } } void cockpit_test_setenv (const char *variable, const char *value) { if (!g_setenv (variable, value, TRUE)) g_error ("couldn't set $%s", variable); } gboolean cockpit_test_skip_slow (void) { if (g_getenv ("COCKPIT_SKIP_SLOW_TESTS")) { g_test_skip ("Skipping slow tests"); return TRUE; } return FALSE; } void cockpit_assertion_message_error_matches (const char *domain, const char *file, int line, const char *func, const char *expr, const GError *error, GQuark error_domain, int error_code, const char *message_pattern) { /* loosely based on g_assertion_message_error() */ g_autoptr(GString) gstring = g_string_new ("assertion failed "); g_string_append_printf (gstring, "%s =~ GError(", expr); if (error_domain) g_string_append_printf (gstring, "domain=%s", g_quark_to_string (error_domain)); else g_string_append (gstring, "domain=any"); g_string_append (gstring, ", "); if (error_code != -1) g_string_append_printf (gstring, "code=%d", error_code); else g_string_append (gstring, "code=any"); g_string_append (gstring, ", "); if (message_pattern) g_string_append_printf (gstring, "message=~'%s'", message_pattern); else g_string_append (gstring, "message=any"); g_string_append (gstring, ")): "); if (error) g_string_append_printf (gstring, "%s (%s, %d)", error->message, g_quark_to_string (error->domain), error->code); else g_string_append_printf (gstring, "%s is NULL", expr); g_assertion_message (domain, file, line, func, gstring->str); }