/* * This file is part of Cockpit. * * Copyright (C) 2015 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 #include "cockpitmetrics.h" #include "mock-transport.h" #include "cockpitinternalmetrics.h" #include "common/cockpittest.h" #include "common/cockpitjson.h" typedef struct { MockTransport *transport; CockpitMetrics *channel; gchar *problem; gboolean channel_closed; } TestCase; static void on_channel_close (CockpitChannel *channel, const gchar *problem, gpointer user_data) { TestCase *tc = user_data; g_assert (tc->channel_closed == FALSE); tc->problem = g_strdup (problem); tc->channel_closed = TRUE; } static void on_transport_closed (CockpitTransport *transport, const gchar *problem, gpointer user_data) { g_assert_not_reached (); } typedef struct _CockpitMetrics MockMetrics; typedef struct _CockpitMetricsClass MockMetricsClass; GType mock_metrics_get_type (void); G_DEFINE_TYPE (MockMetrics, mock_metrics, COCKPIT_TYPE_METRICS); static void mock_metrics_init (MockMetrics *self) { /* nothing */ } static void mock_metrics_class_init (MockMetricsClass *self) { /* nothing */ } static void setup (TestCase *tc, gconstpointer data) { tc->transport = mock_transport_new (); g_signal_connect (tc->transport, "closed", G_CALLBACK (on_transport_closed), NULL); tc->channel = g_object_new (mock_metrics_get_type (), "transport", tc->transport, "id", "1234", NULL); g_signal_connect (tc->channel, "closed", G_CALLBACK (on_channel_close), tc); /* Switch off compression by default. Compression is done by * comparing two floating point values for exact equality, and we * can't guarantee that we get the same behavior everywhere. */ cockpit_metrics_set_compress (tc->channel, FALSE); } static GBytes * recv_bytes (TestCase *tc) { GBytes *msg; while ((msg = mock_transport_pop_channel (tc->transport, "1234")) == NULL) g_main_context_iteration (NULL, TRUE); return msg; } static JsonObject * recv_object (TestCase *tc) { GBytes *msg = recv_bytes (tc); JsonObject *res = cockpit_json_parse_bytes (msg, NULL); g_assert (res != NULL); return res; } static JsonArray * recv_array (TestCase *tc) { GBytes *msg; GError *error = NULL; JsonArray *array; JsonNode *node; msg = recv_bytes (tc); node = cockpit_json_parse (g_bytes_get_data (msg, NULL), g_bytes_get_size (msg), &error); g_assert_no_error (error); g_assert_cmpint (json_node_get_node_type (node), ==, JSON_NODE_ARRAY); array = json_node_dup_array (node); json_node_free (node); return array; } static void teardown (TestCase *tc, gconstpointer data) { cockpit_assert_expected (); g_object_unref (tc->transport); if (tc->channel) { g_object_add_weak_pointer (G_OBJECT (tc->channel), (gpointer *)&tc->channel); g_object_unref (tc->channel); g_assert (tc->channel == NULL); } g_free (tc->problem); } static void assert_sample_msg (const char *domain, const char *file, int line, const char *func, TestCase *tc, const gchar *json_str) { JsonArray *array = recv_array (tc); _cockpit_assert_json_eq_msg (domain, file, line, func, array, json_str); json_array_unref (array); } #define assert_sample(tc, json) \ (assert_sample_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (tc), (json))) static JsonObject * json_obj_msg (const char *domain, const char *file, int line, const char *func, const gchar *json_str) { GError *error = NULL; JsonObject *res = cockpit_json_parse_object (json_str, -1, &error); if (error) g_assertion_message_error (domain, file, line, func, "error", error, 0, 0); return res; } #define json_obj(json_str) \ (json_obj_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (json_str))) static void send_sample (TestCase *tc, gint64 timestamp, int n, ...) { va_list ap; va_start (ap, n); double **buffer; buffer = cockpit_metrics_get_data_buffer (tc->channel); for (int i = 0; i < n; i++) buffer[i][0] = va_arg (ap, double); cockpit_metrics_send_data (tc->channel, timestamp); cockpit_metrics_flush_data (tc->channel); va_end (ap); } static void send_instance_sample (TestCase *tc, gint64 timestamp, int n, ...) { va_list ap; va_start (ap, n); double **buffer; buffer = cockpit_metrics_get_data_buffer (tc->channel); for (int i = 0; i < n; i++) buffer[0][i] = va_arg (ap, double); cockpit_metrics_send_data (tc->channel, timestamp); cockpit_metrics_flush_data (tc->channel); va_end (ap); } static void test_compression (TestCase *tc, gconstpointer unused) { cockpit_metrics_set_compress (tc->channel, TRUE); JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo' }," " { 'name': 'bar' }" " ]," " 'interval': 1000" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); send_sample (tc, 0, 2, 0.0, 0.0); assert_sample (tc, "[[0,0]]"); send_sample (tc, 1000, 2, 0.0, 0.0); assert_sample (tc, "[[]]"); send_sample (tc, 2000, 2, 0.0, 0.0); assert_sample (tc, "[[]]"); send_sample (tc, 3000, 2, 0.0, 1.0); assert_sample (tc, "[[null, 1]]"); send_sample (tc, 4000, 2, 1.0, 1.0); assert_sample (tc, "[[1]]"); json_object_unref (meta); } static void test_compression_reset (TestCase *tc, gconstpointer unused) { cockpit_metrics_set_compress (tc->channel, TRUE); JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo' }," " { 'name': 'bar' }" " ]," " 'interval': 1000" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); send_sample (tc, 0, 2, 0.0, 0.0); assert_sample (tc, "[[0,0]]"); send_sample (tc, 1000, 2, 0.0, 0.0); assert_sample (tc, "[[]]"); cockpit_metrics_send_meta (tc->channel, meta, TRUE); json_object_unref (recv_object (tc)); send_sample (tc, 2000, 2, 0.0, 0.0); assert_sample (tc, "[[0,0]]"); send_sample (tc, 3000, 2, 0.0, 0.0); assert_sample (tc, "[[]]"); json_object_unref (meta); } static void test_derive_delta (TestCase *tc, gconstpointer unused) { JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo'," " 'derive': 'delta'" " }" " ]," " 'interval': 100" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); send_sample (tc, 0, 1, 0.0); assert_sample (tc, "[[false]]"); send_sample (tc, 100, 1, 10.0); assert_sample (tc, "[[10]]"); send_sample (tc, 200, 1, 20.0); assert_sample (tc, "[[10]]"); send_sample (tc, 300, 1, 40.0); assert_sample (tc, "[[20]]"); send_sample (tc, 400, 1, 30.0); assert_sample (tc, "[[-10]]"); send_sample (tc, 500, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 600, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 700, 1, 30.0); assert_sample (tc, "[[0]]"); cockpit_metrics_send_meta (tc->channel, meta, TRUE); json_object_unref (recv_object (tc)); send_sample (tc, 800, 1, 30.0); assert_sample (tc, "[[false]]"); send_sample (tc, 900, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 1000, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 1100, 1, 40.0); assert_sample (tc, "[[10]]"); send_sample (tc, 1200, 1, 40.0); assert_sample (tc, "[[0]]"); json_object_unref (meta); } static void test_derive_rate_no_interpolate (TestCase *tc, gconstpointer unused) { cockpit_metrics_set_interpolate (tc->channel, FALSE); JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo'," " 'derive': 'rate'" " }" " ]," " 'interval': 100" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); send_sample (tc, 0, 1, 0.0); assert_sample (tc, "[[false]]"); send_sample (tc, 100, 1, 10.0); assert_sample (tc, "[[100]]"); send_sample (tc, 200, 1, 20.0); assert_sample (tc, "[[100]]"); send_sample (tc, 300, 1, 40.0); assert_sample (tc, "[[200]]"); send_sample (tc, 400, 1, 30.0); assert_sample (tc, "[[-100]]"); send_sample (tc, 500, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 600, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 700, 1, 30.0); assert_sample (tc, "[[0]]"); cockpit_metrics_send_meta (tc->channel, meta, TRUE); json_object_unref (recv_object (tc)); send_sample (tc, 800, 1, 30.0); assert_sample (tc, "[[false]]"); send_sample (tc, 900, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 1000, 1, 30.0); assert_sample (tc, "[[0]]"); send_sample (tc, 1200, 1, 40.0); // double interval -> half rate assert_sample (tc, "[[50]]"); send_sample (tc, 1200, 1, 40.0); assert_sample (tc, "[[false]]"); // divide by zero -> NaN -> false send_sample (tc, 1300, 1, 40.0); assert_sample (tc, "[[0]]"); json_object_unref (meta); } /* Very specific functions to be used by test_interpolate for approximate sample assertions. (The only reason why we don't do this for all tests is that it is not fun to generalize this...) */ static gboolean approx_equal (double a, double b) { return a == b || (fabs(a-b)/fmax(a, b) < 0.0001); } static void assert_2_approx_samples_msg (const char *domain, const char *file, int line, const char *func, TestCase *tc, double val1, double val2) { JsonArray *array = recv_array (tc); JsonArray *sub_array; if (json_array_get_length (array) != 1) goto fail; sub_array = json_array_get_array_element (array, 0); if (json_array_get_length (sub_array) != 2) goto fail; if (!approx_equal (json_array_get_double_element (sub_array, 0), val1)) goto fail; if (!approx_equal (json_array_get_double_element (sub_array, 1), val2)) goto fail; goto out; fail: { JsonNode *node; gchar *escaped; gchar *msg; node = json_node_new (JSON_NODE_ARRAY); json_node_set_array (node, array); escaped = cockpit_json_write (node, NULL); msg = g_strdup_printf ("%s does not approximately match [[%g,%g]]", escaped, val1, val2); g_assertion_message (domain, file, line, func, msg); g_free (msg); g_free (escaped); json_node_free (node); } out: json_array_unref (array); } #define assert_2_approx_samples(tc, val1, val2) \ (assert_2_approx_samples_msg (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, (tc), (val1), (val2))) static void test_interpolate (TestCase *tc, gconstpointer unused) { JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo'" " }," " {" " 'name': 'bar'," " 'derive': 'rate'" " }" " ]," " 'interval': 100" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); // rising by 10 for every 100 ms, with non-equally spaced samples send_sample (tc, 0, 2, 0.0, 0.0); assert_sample (tc, "[[0,false]]"); send_sample (tc, 100, 2, 10.0, 10.0); assert_2_approx_samples (tc, 10, 100); send_sample (tc, 250, 2, 25.0, 25.0); assert_2_approx_samples (tc, 20, 100); send_sample (tc, 300, 2, 30.0, 30.0); assert_2_approx_samples (tc, 30, 100); send_sample (tc, 500, 2, 50.0, 50.0); assert_2_approx_samples (tc, 40, 100); send_sample (tc, 500, 2, 50.0, 50.0); assert_2_approx_samples (tc, 50, 100); json_object_unref (meta); } static void test_instances (TestCase *tc, gconstpointer unused) { JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo'," " 'instances': [ 'a', 'b' ]" " }" " ]," " 'interval': 1000" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); send_instance_sample (tc, 0, 2, 0.0, 0.0); assert_sample (tc, "[[[0,0]]]"); send_instance_sample (tc, 1000, 2, 0.0, 0.0); assert_sample (tc, "[[[0,0]]]"); send_instance_sample (tc, 2000, 2, 0.0, 0.0); assert_sample (tc, "[[[0,0]]]"); send_instance_sample (tc, 3000, 2, 0.0, 1.0); assert_sample (tc, "[[[0, 1]]]"); send_instance_sample (tc, 4000, 2, 1.0, 1.0); assert_sample (tc, "[[[1, 1]]]"); json_object_unref (meta); } static void test_dynamic_instances (TestCase *tc, gconstpointer unused) { JsonObject *meta = json_obj ("{ 'metrics': [ { 'name': 'foo'," " 'instances': [ 'a' ]," " 'derive': 'delta'" " }" " ]," " 'interval': 100" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); send_instance_sample (tc, 0, 1, 0.0); assert_sample (tc, "[[[false]]]"); send_instance_sample (tc, 100, 1, 10.0); assert_sample (tc, "[[[10]]]"); send_instance_sample (tc, 200, 1, 20.0); assert_sample (tc, "[[[10]]]"); json_object_unref (meta); meta = json_obj ("{ 'metrics': [ { 'name': 'foo'," " 'instances': [ 'b', 'a' ]," " 'derive': 'delta'" " }" " ]," " 'interval': 100" "}"); cockpit_metrics_send_meta (tc->channel, meta, FALSE); json_object_unref (recv_object (tc)); /* Instance 'a' is now at a different index. The 'delta' derivation should continue to work, but no compression should happen. */ send_instance_sample (tc, 300, 2, 0.0, 30.0); assert_sample (tc, "[[[false,10]]]"); send_instance_sample (tc, 400, 2, 10.0, 20.0); assert_sample (tc, "[[[10,-10]]]"); send_instance_sample (tc, 500, 2, 10.0, 40.0); assert_sample (tc, "[[[0,20]]]"); send_instance_sample (tc, 600, 2, 10.0, 50.0); assert_sample (tc, "[[[0,10]]]"); send_instance_sample (tc, 700, 2, 10.0, 60.0); assert_sample (tc, "[[[0,10]]]"); json_object_unref (meta); } static void on_close_get_problem (CockpitChannel *channel, const gchar *problem, gpointer user_data) { gchar **result = user_data; g_assert (result != NULL); g_assert (*result == NULL); *result = g_strdup (problem ? problem : ""); } static void test_not_supported (void) { MockTransport *transport; CockpitMetrics *channel; gchar *problem = NULL; JsonObject *options; cockpit_expect_message ("*unknown internal metric*"); transport = mock_transport_new (); g_signal_connect (transport, "closed", G_CALLBACK (on_transport_closed), NULL); options = json_obj ("{ 'metrics': [ { 'name': 'invalid.metrics'," " 'instances': [ 'b', 'a' ]," " 'derive': 'delta'" " }" " ]," " 'interval': 100" "}"); channel = g_object_new (cockpit_internal_metrics_get_type (), "transport", transport, "id", "1234", "options", options, NULL); json_object_unref (options); g_signal_connect (channel, "closed", G_CALLBACK (on_close_get_problem), &problem); while (problem == NULL) g_main_context_iteration (NULL, TRUE); g_assert_cmpstr (problem, ==, "not-supported"); g_object_add_weak_pointer (G_OBJECT (channel), (gpointer *)&channel); g_object_unref (channel); g_assert (channel == NULL); g_object_unref (transport); g_free (problem); } int main (int argc, char *argv[]) { cockpit_test_init (&argc, &argv); g_test_add ("/metrics/compression", TestCase, NULL, setup, test_compression, teardown); g_test_add ("/metrics/compression-reset", TestCase, NULL, setup, test_compression_reset, teardown); g_test_add ("/metrics/derive-delta", TestCase, NULL, setup, test_derive_delta, teardown); g_test_add ("/metrics/derive-rate", TestCase, NULL, setup, test_derive_rate_no_interpolate, teardown); g_test_add ("/metrics/interpolate", TestCase, NULL, setup, test_interpolate, teardown); g_test_add ("/metrics/instances", TestCase, NULL, setup, test_instances, teardown); g_test_add ("/metrics/dynamic-instances", TestCase, NULL, setup, test_dynamic_instances, teardown); g_test_add_func ("/metrics/not-supported", test_not_supported); return g_test_run (); }