From: Aki Tuomi Date: Mon, 9 Dec 2019 13:17:48 +0000 (+0200) Subject: stats: Add initial unit tests X-Git-Tag: 2.3.10~175 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=2de7b54348c2633aa5cf20484f175aca09161554;p=thirdparty%2Fdovecot%2Fcore.git stats: Add initial unit tests --- diff --git a/src/stats/Makefile.am b/src/stats/Makefile.am index d01a6c685e..b036b7a72b 100644 --- a/src/stats/Makefile.am +++ b/src/stats/Makefile.am @@ -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 index 0000000000..3565708e97 --- /dev/null +++ b/src/stats/test-client-reader.c @@ -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 index 0000000000..4561eb07e2 --- /dev/null +++ b/src/stats/test-client-writer.c @@ -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 index 0000000000..4e55de0cb2 --- /dev/null +++ b/src/stats/test-stats-common.c @@ -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 index 0000000000..06daca3c40 --- /dev/null +++ b/src/stats/test-stats-common.h @@ -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 index 0000000000..90b935a7e3 --- /dev/null +++ b/src/stats/test-stats-metrics.c @@ -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; +}