]> git.ipfire.org Git - thirdparty/dovecot/core.git/commitdiff
stats: Add initial unit tests
authorAki Tuomi <aki.tuomi@open-xchange.com>
Mon, 9 Dec 2019 13:17:48 +0000 (15:17 +0200)
committeraki.tuomi <aki.tuomi@open-xchange.com>
Fri, 20 Dec 2019 08:11:27 +0000 (08:11 +0000)
src/stats/Makefile.am
src/stats/test-client-reader.c [new file with mode: 0644]
src/stats/test-client-writer.c [new file with mode: 0644]
src/stats/test-stats-common.c [new file with mode: 0644]
src/stats/test-stats-common.h [new file with mode: 0644]
src/stats/test-stats-metrics.c [new file with mode: 0644]

index d01a6c685ec72d32a6450e9fa3ca9cf1c473abe3..b036b7a72b03dc049a9a34c4d912552f6851646a 100644 (file)
@@ -2,23 +2,32 @@ pkglibexecdir = $(libexecdir)/dovecot
 
 pkglibexec_PROGRAMS = stats
 
+noinst_LTLIBRARIES = libstats_local.la
+
 AM_CPPFLAGS = \
        -I$(top_srcdir)/src/lib \
        -I$(top_srcdir)/src/lib-settings \
        -I$(top_srcdir)/src/lib-master \
        -I$(top_srcdir)/src/lib-http \
        -I$(top_srcdir)/src/lib-ssl-iostream \
+       -I$(top_srcdir)/src/lib-test \
        $(BINARY_CFLAGS)
 
-stats_LDADD = $(LIBDOVECOT) \
+stats_LDADD = \
+       $(noinst_LTLIBRARIES) \
+       $(LIBDOVECOT) \
        $(DOVECOT_SSL_LIBS) \
        $(BINARY_LDFLAGS)
 
 stats_DEPENDENCIES = \
+       $(noinst_LTLIBRARIES) \
        $(DOVECOT_SSL_LIBS) \
        $(LIBDOVECOT_DEPS)
 
 stats_SOURCES = \
+       main.c
+
+libstats_local_la_SOURCES = \
        client-reader.c \
        client-writer.c \
        event-exporter-fmt.c \
@@ -28,7 +37,6 @@ stats_SOURCES = \
        event-exporter-transport-drop.c \
        event-exporter-transport-http-post.c \
        event-exporter-transport-log.c \
-       main.c \
        stats-event-category.c \
        stats-metrics.c \
        stats-settings.c
@@ -39,4 +47,44 @@ noinst_HEADERS = \
        event-exporter.h \
        stats-event-category.h \
        stats-metrics.h \
-       stats-settings.h
+       stats-settings.h \
+       test-stats-common.h
+
+test_libs = \
+       $(noinst_LTLIBRARIES) \
+       $(DOVECOT_SSL_LIBS) \
+       $(LIBDOVECOT) \
+       $(BINARY_LDFLAGS)
+
+test_deps = \
+       $(noinst_LTLIBRARIES) \
+       $(DOVECOT_SSL_LIBS) \
+       $(LIBDOVECOT_DEPS)
+
+test_stats_metrics_SOURCES = test-stats-metrics.c test-stats-common.c
+test_stats_metrics_LDADD = $(test_libs)
+test_stats_metrics_DEPENDENCIES = $(test_deps)
+
+test_client_writer_SOURCES = test-client-writer.c test-stats-common.c
+test_client_writer_LDADD = $(test_libs)
+test_client_writer_DEPENDENCIES = $(test_deps)
+
+test_client_reader_SOURCES = test-client-reader.c test-stats-common.c
+test_client_reader_LDADD = $(test_libs)
+test_client_reader_DEPENDENCIES = $(test_deps)
+
+test_programs = test-stats-metrics test-client-writer test-client-reader
+noinst_PROGRAMS = $(test_programs)
+
+check-local:
+       for bin in $(test_programs); do \
+         if ! $(RUN_TEST) ./$$bin; then exit 1; fi; \
+       done
+
+LIBDOVECOT_TEST_DEPS = \
+       ../lib-ssl-iostream/libssl_iostream.la \
+       ../lib-test/libtest.la \
+       ../lib/liblib.la
+LIBDOVECOT_TEST = \
+       $(LIBDOVECOT_TEST_DEPS) \
+       $(MODULE_LIBS)
diff --git a/src/stats/test-client-reader.c b/src/stats/test-client-reader.c
new file mode 100644 (file)
index 0000000..3565708
--- /dev/null
@@ -0,0 +1,143 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "master-service-private.h"
+#include "client-reader.h"
+#include "connection.h"
+#include "ostream.h"
+
+static struct connection_list *conn_list;
+
+struct test_connection {
+       struct connection conn;
+
+       unsigned int row_count;
+};
+
+static void test_reader_server_destroy(struct connection *conn)
+{
+       io_loop_stop(conn->ioloop);
+}
+
+static struct connection_settings client_set = {
+       .service_name_in = "stats-reader-server",
+       .service_name_out = "stats-reader-client",
+       .major_version = 2,
+       .minor_version = 0,
+       .allow_empty_args_input = TRUE,
+       .input_max_size = (size_t)-1,
+       .output_max_size = (size_t)-1,
+       .client = TRUE,
+};
+
+bool test_stats_callback(struct event *event,
+                        enum event_callback_type type ATTR_UNUSED,
+                        struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+                        va_list args ATTR_UNUSED)
+{
+       if (metrics != NULL) {
+               stats_metrics_event(metrics, event, ctx);
+               struct event_filter *filter = stats_metrics_get_event_filter(metrics);
+               return !event_filter_match(filter, event, ctx);
+       }
+       return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"\n";
+
+static int test_reader_server_input_args(struct connection *conn ATTR_UNUSED,
+                                        const char *const *args)
+{
+       if (args[0] == NULL)
+               return -1;
+
+       test_assert_strcmp(args[0], "test");
+       test_assert_strcmp(args[1], "1");
+
+       return 1;
+}
+
+static void test_dump_metrics(void)
+{
+       int fds[2];
+
+       test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+       struct connection *conn = i_new(struct connection, 1);
+
+       struct ioloop *loop = io_loop_create();
+
+       client_reader_create(fds[1], metrics);
+       connection_init_client_fd(conn_list, conn, "stats", fds[0], fds[0]);
+       o_stream_nsend_str(conn->output, "DUMP\tcount\n");
+
+       io_loop_run(loop);
+       connection_deinit(conn);
+       i_free(conn);
+
+       /* allow client-reader to finish up */
+       io_loop_set_running(loop);
+       io_loop_handler_run(loop);
+
+       io_loop_destroy(&loop);
+}
+
+static void test_client_reader(void)
+{
+       const struct connection_vfuncs client_vfuncs = {
+               .input_args = test_reader_server_input_args,
+               .destroy = test_reader_server_destroy,
+       };
+
+       test_begin("client reader");
+
+       /* register some stats */
+       test_init(settings_blob_1);
+
+       client_readers_init();
+       conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+       /* push event in */
+       struct event *event = event_create(NULL);
+       event_add_category(event, &test_category);
+       event_set_name(event, "test");
+       test_event_send(event);
+       event_unref(&event);
+
+       test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+       test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+       /* check output from reader */
+       test_dump_metrics();
+
+       test_deinit();
+
+       client_readers_deinit();
+       connection_list_deinit(&conn_list);
+
+       test_end();
+}
+
+int main(void) {
+       /* fake master service to pretend destroying
+          connections. */
+       struct master_service local_master_service = {
+               .stopping = TRUE,
+               .total_available_count = 100,
+               .service_count_left = 100,
+       };
+       void (*const test_functions[])(void) = {
+               test_client_reader,
+               NULL
+       };
+
+       master_service = &local_master_service;
+
+       int ret = test_run(test_functions);
+
+       return ret;
+}
diff --git a/src/stats/test-client-writer.c b/src/stats/test-client-writer.c
new file mode 100644 (file)
index 0000000..4561eb0
--- /dev/null
@@ -0,0 +1,152 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+#include "master-service-private.h"
+#include "client-writer.h"
+#include "connection.h"
+#include "ostream.h"
+
+static struct event *last_sent_event = NULL;
+static bool recurse_back = FALSE;
+static struct connection_list *conn_list;
+
+static void test_writer_server_destroy(struct connection *conn)
+{
+       io_loop_stop(conn->ioloop);
+}
+
+static int test_writer_server_input_args(struct connection *conn,
+                                        const char *const *args ATTR_UNUSED)
+{
+       /* check filter */
+       test_assert_strcmp(args[0], "FILTER");
+       test_assert_strcmp(args[1], "ntest");
+       /* send commands now */
+       string_t *send_buf = t_str_new(128);
+       o_stream_nsend_str(conn->output, "CATEGORY\ttest\n");
+       str_printfa(send_buf, "BEGIN\t%"PRIu64"\t0\t0\t", last_sent_event->id);
+       event_export(last_sent_event, send_buf);
+       str_append_c(send_buf, '\n');
+       o_stream_nsend(conn->output, str_data(send_buf), str_len(send_buf));
+       str_truncate(send_buf, 0);
+       str_printfa(send_buf, "END\t%"PRIu64"\n", last_sent_event->id);
+       o_stream_nsend(conn->output, str_data(send_buf), str_len(send_buf));
+       /* disconnect immediately */
+       return -1;
+}
+
+static struct connection_settings client_set = {
+       .service_name_in = "stats-server",
+       .service_name_out = "stats-client",
+       .major_version = 3,
+       .minor_version = 0,
+
+       .input_max_size = (size_t)-1,
+       .output_max_size = (size_t)-1,
+       .client = TRUE,
+};
+
+static const struct connection_vfuncs client_vfuncs = {
+       .input_args = test_writer_server_input_args,
+       .destroy = test_writer_server_destroy,
+};
+
+static void test_write_one(struct event *event ATTR_UNUSED)
+{
+       int fds[2];
+
+       test_assert(socketpair(AF_UNIX, SOCK_STREAM, 0, fds) == 0);
+
+       struct connection *conn = i_new(struct connection, 1);
+
+       struct ioloop *loop = io_loop_create();
+
+       client_writer_create(fds[1], metrics);
+       connection_init_client_fd(conn_list, conn, "stats", fds[0], fds[0]);
+
+       last_sent_event = event;
+       io_loop_run(loop);
+       last_sent_event = NULL;
+       connection_deinit(conn);
+       i_free(conn);
+
+       /* client-writer needs two loops to deinit */
+       io_loop_set_running(loop);
+       io_loop_handler_run(loop);
+       io_loop_set_running(loop);
+       io_loop_handler_run(loop);
+
+       io_loop_destroy(&loop);
+}
+
+bool test_stats_callback(struct event *event,
+                        enum event_callback_type type ATTR_UNUSED,
+                        struct failure_context *ctx ATTR_UNUSED,
+                        const char *fmt ATTR_UNUSED,
+                        va_list args ATTR_UNUSED)
+{
+       if (recurse_back)
+               return TRUE;
+
+       recurse_back = TRUE;
+       if (metrics != NULL) {
+               test_write_one(event);
+       }
+       recurse_back = FALSE;
+
+       return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"\n";
+
+static void test_client_writer(void)
+{
+       test_begin("client writer");
+
+       /* register some stats */
+       test_init(settings_blob_1);
+
+       client_writers_init();
+       conn_list = connection_list_init(&client_set, &client_vfuncs);
+
+       /* push event in */
+       struct event *event = event_create(NULL);
+       event_add_category(event, &test_category);
+       event_set_name(event, "test");
+       test_event_send(event);
+       event_unref(&event);
+
+       test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+       test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+       test_deinit();
+
+       client_writers_deinit();
+       connection_list_deinit(&conn_list);
+
+       test_end();
+}
+
+int main(void) {
+       /* fake master service to pretend destroying
+          connections. */
+       struct master_service local_master_service = {
+               .stopping = TRUE,
+               .total_available_count = 100,
+               .service_count_left = 100,
+       };
+       void (*const test_functions[])(void) = {
+               test_client_writer,
+               NULL
+       };
+
+       master_service = &local_master_service;
+
+       int ret = test_run(test_functions);
+
+       return ret;
+}
diff --git a/src/stats/test-stats-common.c b/src/stats/test-stats-common.c
new file mode 100644 (file)
index 0000000..4e55de0
--- /dev/null
@@ -0,0 +1,93 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+
+struct event_category test_category = {
+       .name = "test",
+};
+
+struct event_category child_test_category = {
+       .name = "child",
+       .parent = &test_category,
+};
+
+pool_t test_pool;
+struct stats_metrics *metrics = NULL;
+
+static bool callback_added = FALSE;
+
+static struct stats_settings *read_settings(const char *settings)
+{
+       struct istream *is = test_istream_create(settings);
+       const char *error;
+       struct setting_parser_context *ctx =
+               settings_parser_init(test_pool, &stats_setting_parser_info, 0);
+       if (settings_parse_stream_read(ctx, is) < 0)
+               i_fatal("Failed to parse settings: %s",
+                       settings_parser_get_error(ctx));
+       if (!settings_parser_check(ctx, test_pool, &error))
+               i_fatal("Failed to parse settings: %s",
+                       error);
+       struct stats_settings *set = settings_parser_get(ctx);
+       settings_parser_deinit(&ctx);
+       i_stream_unref(&is);
+       return set;
+}
+
+void test_init(const char *settings_blob)
+{
+       if (!callback_added) {
+               event_register_callback(test_stats_callback);
+               callback_added = TRUE;
+       }
+
+       stats_event_categories_init();
+       test_pool = pool_alloconly_create(MEMPOOL_GROWING"test pool", 2048);
+
+       /* register test categories */
+       stats_event_category_register(test_category.name, NULL);
+       stats_event_category_register(child_test_category.name,
+                                     &test_category);
+       struct stats_settings *set = read_settings(settings_blob);
+       metrics = stats_metrics_init(set);
+}
+
+void test_deinit(void)
+{
+       stats_metrics_deinit(&metrics);
+       stats_event_categories_deinit();
+       pool_unref(&test_pool);
+}
+
+void test_event_send(struct event *event)
+{
+        struct failure_context ctx = {
+                .type = LOG_TYPE_DEBUG,
+        };
+
+        event_send(event, &ctx, "hello");
+}
+
+uint64_t get_stats_dist_field(const char *metric_name, enum stats_dist_field field)
+{
+        struct stats_metrics_iter *iter = stats_metrics_iterate_init(metrics);
+        const struct metric *metric;
+        while((metric = stats_metrics_iterate(iter)) != NULL)
+                if (strcmp(metric->name, metric_name) == 0)
+                        break;
+
+        /* bug in test if not found */
+        i_assert(metric != NULL);
+
+        stats_metrics_iterate_deinit(&iter);
+
+        switch(field) {
+        case STATS_DIST_COUNT:
+                return stats_dist_get_count(metric->duration_stats);
+        case STATS_DIST_SUM:
+                return stats_dist_get_sum(metric->duration_stats);
+        default:
+                i_unreached();
+        }
+}
+
diff --git a/src/stats/test-stats-common.h b/src/stats/test-stats-common.h
new file mode 100644 (file)
index 0000000..06daca3
--- /dev/null
@@ -0,0 +1,38 @@
+#ifndef TEST_STATS_COMMON
+#define TEST_STATS_COMMON 1
+
+#include "lib.h"
+#include "event-filter.h"
+#include "istream.h"
+#include "settings-parser.h"
+#include "str.h"
+#include "test-common.h"
+#include "lib-event-private.h"
+#include "stats-dist.h"
+#include "stats-event-category.h"
+#include "stats-metrics.h"
+
+extern struct event_category test_category;
+extern struct event_category child_test_category;
+extern pool_t test_pool;
+
+extern struct stats_metrics *metrics;
+
+bool test_stats_callback(struct event *event,
+                        enum event_callback_type type ATTR_UNUSED,
+                        struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+                        va_list args ATTR_UNUSED);
+
+void test_init(const char *settings_blob);
+void test_deinit(void);
+
+void test_event_send(struct event *event);
+
+enum stats_dist_field {
+        STATS_DIST_COUNT,
+        STATS_DIST_SUM,
+};
+
+uint64_t get_stats_dist_field(const char *metric_name, enum stats_dist_field field);
+
+#endif
diff --git a/src/stats/test-stats-metrics.c b/src/stats/test-stats-metrics.c
new file mode 100644 (file)
index 0000000..90b935a
--- /dev/null
@@ -0,0 +1,102 @@
+/* Copyright (c) 2019 Dovecot authors, see the included COPYING file */
+
+#include "test-stats-common.h"
+
+bool test_stats_callback(struct event *event,
+                        enum event_callback_type type ATTR_UNUSED,
+                        struct failure_context *ctx, const char *fmt ATTR_UNUSED,
+                        va_list args ATTR_UNUSED)
+{
+       if (metrics != NULL) {
+               stats_metrics_event(metrics, event, ctx);
+               struct event_filter *filter = stats_metrics_get_event_filter(metrics);
+               return !event_filter_match(filter, event, ctx);
+       }
+       return TRUE;
+}
+
+static const char *settings_blob_1 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"\n";
+
+static void test_stats_metrics(void)
+{
+       test_begin("stats metrics (event counting)");
+
+       /* register some stats */
+       test_init(settings_blob_1);
+
+       /* push event in */
+       struct event *event = event_create(NULL);
+       event_add_category(event, &test_category);
+       event_set_name(event, "test");
+       test_event_send(event);
+       event_unref(&event);
+
+       test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+       test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+       test_deinit();
+       test_end();
+}
+
+static const char *settings_blob_2 =
+"metric=test\n"
+"metric/test/name=test\n"
+"metric/test/event_name=test\n"
+"metric/test/filter=\n"
+"metric/test/filter/test_field=value\n"
+"\n";
+
+static void test_stats_metrics_filter(void)
+{
+       test_begin("stats metrics (filter)");
+
+       test_init(settings_blob_2);
+
+       /* check filter */
+       struct event_filter *filter = stats_metrics_get_event_filter(metrics);
+       string_t *str_filter = t_str_new(64);
+       event_filter_export(filter, str_filter);
+       test_assert_strcmp("ntest       ftest_field     value   ",
+                          str_c(str_filter));
+
+       /* send event */
+       struct event *event = event_create(NULL);
+       event_add_category(event, &test_category);
+       event_set_name(event, "test");
+       event_add_str(event, "test_field", "value");
+       test_event_send(event);
+       event_unref(&event);
+
+       test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+       test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+       /* send another event */
+       event = event_create(NULL);
+       event_add_category(event, &test_category);
+       event_set_name(event, "test");
+       event_add_str(event, "test_field", "nother value");
+       e_debug(event, "test");
+       event_unref(&event);
+
+       test_assert(get_stats_dist_field("test", STATS_DIST_COUNT) == 1);
+       test_assert(get_stats_dist_field("test", STATS_DIST_SUM) > 0);
+
+       test_deinit();
+       test_end();
+}
+
+int main(void) {
+       void (*const test_functions[])(void) = {
+               test_stats_metrics,
+               test_stats_metrics_filter,
+               NULL
+       };
+
+       int ret = test_run(test_functions);
+
+       return ret;
+}