summaryrefslogtreecommitdiffstats
path: root/lib/common/tests/output
diff options
context:
space:
mode:
Diffstat (limited to 'lib/common/tests/output')
-rw-r--r--lib/common/tests/output/Makefile.am24
-rw-r--r--lib/common/tests/output/pcmk__call_message_test.c156
-rw-r--r--lib/common/tests/output/pcmk__output_and_clear_error_test.c82
-rw-r--r--lib/common/tests/output/pcmk__output_free_test.c84
-rw-r--r--lib/common/tests/output/pcmk__output_new_test.c148
-rw-r--r--lib/common/tests/output/pcmk__register_format_test.c63
-rw-r--r--lib/common/tests/output/pcmk__register_formats_test.c108
-rw-r--r--lib/common/tests/output/pcmk__register_message_test.c107
-rw-r--r--lib/common/tests/output/pcmk__register_messages_test.c191
-rw-r--r--lib/common/tests/output/pcmk__unregister_formats_test.c39
10 files changed, 1002 insertions, 0 deletions
diff --git a/lib/common/tests/output/Makefile.am b/lib/common/tests/output/Makefile.am
new file mode 100644
index 0000000..6ac7b5f
--- /dev/null
+++ b/lib/common/tests/output/Makefile.am
@@ -0,0 +1,24 @@
+#
+# Copyright 2021-2022 the Pacemaker project contributors
+#
+# The version control history for this file may have further details.
+#
+# This source code is licensed under the GNU General Public License version 2
+# or later (GPLv2+) WITHOUT ANY WARRANTY.
+#
+
+include $(top_srcdir)/mk/tap.mk
+include $(top_srcdir)/mk/unittest.mk
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+check_PROGRAMS = pcmk__call_message_test \
+ pcmk__output_and_clear_error_test \
+ pcmk__output_free_test \
+ pcmk__output_new_test \
+ pcmk__register_format_test \
+ pcmk__register_formats_test \
+ pcmk__register_message_test \
+ pcmk__register_messages_test \
+ pcmk__unregister_formats_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/output/pcmk__call_message_test.c b/lib/common/tests/output/pcmk__call_message_test.c
new file mode 100644
index 0000000..824eac7
--- /dev/null
+++ b/lib/common/tests/output/pcmk__call_message_test.c
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+static int
+default_message_fn(pcmk__output_t *out, va_list args) {
+ function_called();
+ return pcmk_rc_ok;
+}
+
+static int
+failed_message_fn(pcmk__output_t *out, va_list args) {
+ function_called();
+ return pcmk_rc_no_output;
+}
+
+static int
+message_fn_1(pcmk__output_t *out, va_list args) {
+ function_called();
+ return pcmk_rc_ok;
+}
+
+static int
+message_fn_2(pcmk__output_t *out, va_list args) {
+ function_called();
+ return pcmk_rc_ok;
+}
+
+static bool
+fake_text_init(pcmk__output_t *out) {
+ return true;
+}
+
+static void
+fake_text_free_priv(pcmk__output_t *out) {
+ /* This function intentionally left blank */
+}
+
+static pcmk__output_t *
+mk_fake_text_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "text";
+ retval->init = fake_text_init;
+ retval->free_priv = fake_text_free_priv;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ return retval;
+}
+
+static int
+setup(void **state) {
+ pcmk__register_format(NULL, "text", mk_fake_text_output, NULL);
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ pcmk__unregister_formats();
+ return 0;
+}
+
+static void
+no_such_message(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+
+ assert_int_equal(out->message(out, "fake"), EINVAL);
+ pcmk__assert_asserts(out->message(out, ""));
+ pcmk__assert_asserts(out->message(out, NULL));
+
+ pcmk__output_free(out);
+}
+
+static void
+message_return_value(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "text", message_fn_1 },
+ { "msg2", "text", message_fn_2 },
+ { "fail", "text", failed_message_fn },
+ { NULL },
+ };
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+ pcmk__register_messages(out, entries);
+
+ expect_function_call(message_fn_1);
+ assert_int_equal(out->message(out, "msg1"), pcmk_rc_ok);
+ expect_function_call(message_fn_2);
+ assert_int_equal(out->message(out, "msg2"), pcmk_rc_ok);
+ expect_function_call(failed_message_fn);
+ assert_int_equal(out->message(out, "fail"), pcmk_rc_no_output);
+
+ pcmk__output_free(out);
+}
+
+static void
+wrong_format(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "xml", message_fn_1 },
+ { NULL },
+ };
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+ pcmk__register_messages(out, entries);
+
+ assert_int_equal(out->message(out, "msg1"), EINVAL);
+
+ pcmk__output_free(out);
+}
+
+static void
+default_called(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "default", default_message_fn },
+ { "msg1", "xml", message_fn_1 },
+ { NULL },
+ };
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+ pcmk__register_messages(out, entries);
+
+ expect_function_call(default_message_fn);
+ assert_int_equal(out->message(out, "msg1"), pcmk_rc_ok);
+
+ pcmk__output_free(out);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test_setup_teardown(no_such_message, setup, teardown),
+ cmocka_unit_test_setup_teardown(message_return_value, setup, teardown),
+ cmocka_unit_test_setup_teardown(wrong_format, setup, teardown),
+ cmocka_unit_test_setup_teardown(default_called, setup, teardown))
diff --git a/lib/common/tests/output/pcmk__output_and_clear_error_test.c b/lib/common/tests/output/pcmk__output_and_clear_error_test.c
new file mode 100644
index 0000000..f54ed8a
--- /dev/null
+++ b/lib/common/tests/output/pcmk__output_and_clear_error_test.c
@@ -0,0 +1,82 @@
+/*
+ * Copyright 2022-2023 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+#include <glib.h>
+
+static bool
+fake_text_init(pcmk__output_t *out) {
+ return true;
+}
+
+static void
+fake_text_free_priv(pcmk__output_t *out) {
+ /* This function intentionally left blank */
+}
+
+G_GNUC_PRINTF(2, 3)
+static void
+fake_text_err(pcmk__output_t *out, const char *format, ...) {
+ function_called();
+}
+
+static pcmk__output_t *
+mk_fake_text_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "text";
+ retval->init = fake_text_init;
+ retval->free_priv = fake_text_free_priv;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ retval->err = fake_text_err;
+
+ return retval;
+}
+
+static int
+setup(void **state) {
+ pcmk__register_format(NULL, "text", mk_fake_text_output, NULL);
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ pcmk__unregister_formats();
+ return 0;
+}
+
+static void
+standard_usage(void **state) {
+ GError *error = NULL;
+ pcmk__output_t *out = NULL;
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+ g_set_error(&error, PCMK__RC_ERROR, pcmk_rc_bad_nvpair,
+ "some error message");
+
+ expect_function_call(fake_text_err);
+ pcmk__output_and_clear_error(&error, out);
+
+ pcmk__output_free(out);
+ assert_null(error);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test_setup_teardown(standard_usage, setup, teardown))
diff --git a/lib/common/tests/output/pcmk__output_free_test.c b/lib/common/tests/output/pcmk__output_free_test.c
new file mode 100644
index 0000000..ef074d1
--- /dev/null
+++ b/lib/common/tests/output/pcmk__output_free_test.c
@@ -0,0 +1,84 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+static int
+null_message_fn(pcmk__output_t *out, va_list args) {
+ return pcmk_rc_ok;
+}
+
+static bool
+fake_text_init(pcmk__output_t *out) {
+ return true;
+}
+
+static void
+fake_text_free_priv(pcmk__output_t *out) {
+ function_called();
+ /* This function intentionally left blank */
+}
+
+static pcmk__output_t *
+mk_fake_text_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "text";
+ retval->init = fake_text_init;
+ retval->free_priv = fake_text_free_priv;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ return retval;
+}
+
+static int
+setup(void **state) {
+ pcmk__register_format(NULL, "text", mk_fake_text_output, NULL);
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ pcmk__unregister_formats();
+ return 0;
+}
+
+static void
+no_messages(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+
+ expect_function_call(fake_text_free_priv);
+ pcmk__output_free(out);
+}
+
+static void
+messages(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+ pcmk__register_message(out, "fake", null_message_fn);
+
+ expect_function_call(fake_text_free_priv);
+ pcmk__output_free(out);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test_setup_teardown(no_messages, setup, teardown),
+ cmocka_unit_test_setup_teardown(messages, setup, teardown))
diff --git a/lib/common/tests/output/pcmk__output_new_test.c b/lib/common/tests/output/pcmk__output_new_test.c
new file mode 100644
index 0000000..de4268c
--- /dev/null
+++ b/lib/common/tests/output/pcmk__output_new_test.c
@@ -0,0 +1,148 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+#include "mock_private.h"
+
+static bool init_succeeds = true;
+
+static bool
+fake_text_init(pcmk__output_t *out) {
+ return init_succeeds;
+}
+
+static void
+fake_text_free_priv(pcmk__output_t *out) {
+ /* This function intentionally left blank */
+}
+
+/* "text" is the default for pcmk__output_new. */
+static pcmk__output_t *
+mk_fake_text_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "text";
+ retval->init = fake_text_init;
+ retval->free_priv = fake_text_free_priv;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ return retval;
+}
+
+static int
+setup(void **state) {
+ pcmk__register_format(NULL, "text", mk_fake_text_output, NULL);
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ pcmk__unregister_formats();
+ return 0;
+}
+
+static void
+empty_formatters(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__assert_asserts(pcmk__output_new(&out, "fake", NULL, NULL));
+}
+
+static void
+invalid_params(void **state) {
+ /* This must be called with the setup/teardown functions so formatters is not NULL. */
+ pcmk__assert_asserts(pcmk__output_new(NULL, "fake", NULL, NULL));
+}
+
+static void
+no_such_format(void **state) {
+ pcmk__output_t *out = NULL;
+
+ assert_int_equal(pcmk__output_new(&out, "fake", NULL, NULL), pcmk_rc_unknown_format);
+}
+
+static void
+create_fails(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__mock_calloc = true; // calloc() will return NULL
+
+ expect_value(__wrap_calloc, nmemb, 1);
+ expect_value(__wrap_calloc, size, sizeof(pcmk__output_t));
+ assert_int_equal(pcmk__output_new(&out, "text", NULL, NULL), ENOMEM);
+
+ pcmk__mock_calloc = false; // Use real calloc()
+}
+
+static void
+fopen_fails(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__mock_fopen = true;
+ expect_string(__wrap_fopen, pathname, "destfile");
+ expect_string(__wrap_fopen, mode, "w");
+ will_return(__wrap_fopen, EPERM);
+
+ assert_int_equal(pcmk__output_new(&out, "text", "destfile", NULL), EPERM);
+
+ pcmk__mock_fopen = false;
+}
+
+static void
+init_fails(void **state) {
+ pcmk__output_t *out = NULL;
+
+ init_succeeds = false;
+ assert_int_equal(pcmk__output_new(&out, "text", NULL, NULL), ENOMEM);
+ init_succeeds = true;
+}
+
+static void
+everything_succeeds(void **state) {
+ pcmk__output_t *out = NULL;
+
+ assert_int_equal(pcmk__output_new(&out, "text", NULL, NULL), pcmk_rc_ok);
+ assert_string_equal(out->fmt_name, "text");
+ assert_ptr_equal(out->dest, stdout);
+ assert_false(out->quiet);
+ assert_non_null(out->messages);
+ assert_string_equal(getenv("OCF_OUTPUT_FORMAT"), "text");
+
+ pcmk__output_free(out);
+}
+
+static void
+no_fmt_name_given(void **state) {
+ pcmk__output_t *out = NULL;
+
+ assert_int_equal(pcmk__output_new(&out, NULL, NULL, NULL), pcmk_rc_ok);
+ assert_string_equal(out->fmt_name, "text");
+
+ pcmk__output_free(out);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_formatters),
+ cmocka_unit_test_setup_teardown(invalid_params, setup, teardown),
+ cmocka_unit_test_setup_teardown(no_such_format, setup, teardown),
+ cmocka_unit_test_setup_teardown(create_fails, setup, teardown),
+ cmocka_unit_test_setup_teardown(init_fails, setup, teardown),
+ cmocka_unit_test_setup_teardown(fopen_fails, setup, teardown),
+ cmocka_unit_test_setup_teardown(everything_succeeds, setup, teardown),
+ cmocka_unit_test_setup_teardown(no_fmt_name_given, setup, teardown))
diff --git a/lib/common/tests/output/pcmk__register_format_test.c b/lib/common/tests/output/pcmk__register_format_test.c
new file mode 100644
index 0000000..bcbde48
--- /dev/null
+++ b/lib/common/tests/output/pcmk__register_format_test.c
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+static pcmk__output_t *
+null_create_fn(char **argv) {
+ return NULL;
+}
+
+static pcmk__output_t *
+null_create_fn_2(char **argv) {
+ return NULL;
+}
+
+static void
+invalid_params(void **state) {
+ pcmk__assert_asserts(pcmk__register_format(NULL, "fake", NULL, NULL));
+ pcmk__assert_asserts(pcmk__register_format(NULL, "", null_create_fn, NULL));
+ pcmk__assert_asserts(pcmk__register_format(NULL, NULL, null_create_fn, NULL));
+}
+
+static void
+add_format(void **state) {
+ GHashTable *formatters = NULL;
+ gpointer value;
+
+ /* For starters, there should be no formatters defined. */
+ assert_null(pcmk__output_formatters());
+
+ /* Add a fake formatter and check that it's the only item in the hash table. */
+ assert_int_equal(pcmk__register_format(NULL, "fake", null_create_fn, NULL), pcmk_rc_ok);
+ formatters = pcmk__output_formatters();
+ assert_int_equal(g_hash_table_size(formatters), 1);
+
+ value = g_hash_table_lookup(formatters, "fake");
+ assert_ptr_equal(value, null_create_fn);
+
+ /* Add a second fake formatter which should overwrite the first one, leaving
+ * only one item in the hash table but pointing at the new function.
+ */
+ assert_int_equal(pcmk__register_format(NULL, "fake", null_create_fn_2, NULL), pcmk_rc_ok);
+ formatters = pcmk__output_formatters();
+ assert_int_equal(g_hash_table_size(formatters), 1);
+
+ value = g_hash_table_lookup(formatters, "fake");
+ assert_ptr_equal(value, null_create_fn_2);
+
+ pcmk__unregister_formats();
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(invalid_params),
+ cmocka_unit_test(add_format))
diff --git a/lib/common/tests/output/pcmk__register_formats_test.c b/lib/common/tests/output/pcmk__register_formats_test.c
new file mode 100644
index 0000000..4be2d78
--- /dev/null
+++ b/lib/common/tests/output/pcmk__register_formats_test.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+static pcmk__output_t *
+null_create_fn(char **argv) {
+ return NULL;
+}
+
+static pcmk__output_t *
+null_create_fn_2(char **argv) {
+ return NULL;
+}
+
+static void
+no_formats(void **state) {
+ pcmk__register_formats(NULL, NULL);
+ assert_null(pcmk__output_formatters());
+}
+
+static void
+invalid_entries(void **state) {
+ /* Here, we can only test that an empty name won't be added. A NULL name is
+ * the marker for the end of the format table.
+ */
+ pcmk__supported_format_t formats[] = {
+ { "", null_create_fn, NULL },
+ { NULL },
+ };
+
+ pcmk__assert_asserts(pcmk__register_formats(NULL, formats));
+}
+
+static void
+valid_entries(void **state) {
+ GHashTable *formatters = NULL;
+
+ pcmk__supported_format_t formats[] = {
+ { "fmt1", null_create_fn, NULL },
+ { "fmt2", null_create_fn_2, NULL },
+ { NULL },
+ };
+
+ pcmk__register_formats(NULL, formats);
+
+ formatters = pcmk__output_formatters();
+ assert_int_equal(g_hash_table_size(formatters), 2);
+ assert_ptr_equal(g_hash_table_lookup(formatters, "fmt1"), null_create_fn);
+ assert_ptr_equal(g_hash_table_lookup(formatters, "fmt2"), null_create_fn_2);
+
+ pcmk__unregister_formats();
+}
+
+static void
+duplicate_keys(void **state) {
+ GHashTable *formatters = NULL;
+
+ pcmk__supported_format_t formats[] = {
+ { "fmt1", null_create_fn, NULL },
+ { "fmt1", null_create_fn_2, NULL },
+ { NULL },
+ };
+
+ pcmk__register_formats(NULL, formats);
+
+ formatters = pcmk__output_formatters();
+ assert_int_equal(g_hash_table_size(formatters), 1);
+ assert_ptr_equal(g_hash_table_lookup(formatters, "fmt1"), null_create_fn_2);
+
+ pcmk__unregister_formats();
+}
+
+static void
+duplicate_values(void **state) {
+ GHashTable *formatters = NULL;
+
+ pcmk__supported_format_t formats[] = {
+ { "fmt1", null_create_fn, NULL },
+ { "fmt2", null_create_fn, NULL },
+ { NULL },
+ };
+
+ pcmk__register_formats(NULL, formats);
+
+ formatters = pcmk__output_formatters();
+ assert_int_equal(g_hash_table_size(formatters), 2);
+ assert_ptr_equal(g_hash_table_lookup(formatters, "fmt1"), null_create_fn);
+ assert_ptr_equal(g_hash_table_lookup(formatters, "fmt2"), null_create_fn);
+
+ pcmk__unregister_formats();
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(no_formats),
+ cmocka_unit_test(invalid_entries),
+ cmocka_unit_test(valid_entries),
+ cmocka_unit_test(duplicate_keys),
+ cmocka_unit_test(duplicate_values))
diff --git a/lib/common/tests/output/pcmk__register_message_test.c b/lib/common/tests/output/pcmk__register_message_test.c
new file mode 100644
index 0000000..4b4a282
--- /dev/null
+++ b/lib/common/tests/output/pcmk__register_message_test.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2022-2023 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+#include "../../crmcommon_private.h"
+
+static int
+null_message_fn(pcmk__output_t *out, va_list args) {
+ return pcmk_rc_ok;
+}
+
+static int
+null_message_fn_2(pcmk__output_t *out, va_list args) {
+ return pcmk_rc_ok;
+}
+
+static bool
+fake_text_init(pcmk__output_t *out) {
+ return true;
+}
+
+static void
+fake_text_free_priv(pcmk__output_t *out) {
+ /* This function intentionally left blank */
+}
+
+static pcmk__output_t *
+mk_fake_text_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "text";
+ retval->init = fake_text_init;
+ retval->free_priv = fake_text_free_priv;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ return retval;
+}
+
+static int
+setup(void **state) {
+ pcmk__register_format(NULL, "text", mk_fake_text_output, NULL);
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ pcmk__unregister_formats();
+ return 0;
+}
+
+static void
+null_params(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__output_new(&out, "text", NULL, NULL);
+
+ pcmk__assert_asserts(pcmk__register_message(NULL, "fake", null_message_fn));
+ pcmk__assert_asserts(pcmk__register_message(out, NULL, null_message_fn));
+ pcmk__assert_asserts(pcmk__register_message(out, "", null_message_fn));
+ pcmk__assert_asserts(pcmk__register_message(out, "fake", NULL));
+
+ pcmk__output_free(out);
+}
+
+static void
+add_message(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ /* For starters, there should be no messages defined. */
+ assert_int_equal(g_hash_table_size(out->messages), 0);
+
+ /* Add a fake function and check that it's the only item in the hash table. */
+ pcmk__register_message(out, "fake", null_message_fn);
+ assert_int_equal(g_hash_table_size(out->messages), 1);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "fake"), null_message_fn);
+
+ /* Add a second fake function which should overwrite the first one, leaving
+ * only one item in the hash table but pointing at the new function.
+ */
+ pcmk__register_message(out, "fake", null_message_fn_2);
+ assert_int_equal(g_hash_table_size(out->messages), 1);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "fake"), null_message_fn_2);
+
+ pcmk__output_free(out);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test_setup_teardown(null_params, setup, teardown),
+ cmocka_unit_test_setup_teardown(add_message, setup, teardown))
diff --git a/lib/common/tests/output/pcmk__register_messages_test.c b/lib/common/tests/output/pcmk__register_messages_test.c
new file mode 100644
index 0000000..3fdd759
--- /dev/null
+++ b/lib/common/tests/output/pcmk__register_messages_test.c
@@ -0,0 +1,191 @@
+/*
+ * Copyright 2022-2023 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+#include "../../crmcommon_private.h"
+
+static int
+null_message_fn(pcmk__output_t *out, va_list args) {
+ return pcmk_rc_ok;
+}
+
+static int
+null_message_fn_2(pcmk__output_t *out, va_list args) {
+ return pcmk_rc_ok;
+}
+
+static bool
+fake_text_init(pcmk__output_t *out) {
+ return true;
+}
+
+static void
+fake_text_free_priv(pcmk__output_t *out) {
+ /* This function intentionally left blank */
+}
+
+static pcmk__output_t *
+mk_fake_text_output(char **argv) {
+ pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t));
+
+ if (retval == NULL) {
+ return NULL;
+ }
+
+ retval->fmt_name = "text";
+ retval->init = fake_text_init;
+ retval->free_priv = fake_text_free_priv;
+
+ retval->register_message = pcmk__register_message;
+ retval->message = pcmk__call_message;
+
+ return retval;
+}
+
+static int
+setup(void **state) {
+ pcmk__register_format(NULL, "text", mk_fake_text_output, NULL);
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ pcmk__unregister_formats();
+ return 0;
+}
+
+static void
+invalid_entries(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ /* We can't test a NULL message_id here because that's the marker for
+ * the end of the table.
+ */
+ { "", "", null_message_fn },
+ { "", NULL, null_message_fn },
+ { "", "text", NULL },
+ { NULL },
+ };
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ pcmk__assert_asserts(pcmk__register_messages(out, entries));
+ assert_int_equal(g_hash_table_size(out->messages), 0);
+
+ pcmk__output_free(out);
+}
+
+static void
+valid_entries(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "text", null_message_fn },
+ { "msg2", "text", null_message_fn_2 },
+ { NULL },
+ };
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ pcmk__register_messages(out, entries);
+ assert_int_equal(g_hash_table_size(out->messages), 2);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg2"), null_message_fn_2);
+
+ pcmk__output_free(out);
+}
+
+static void
+duplicate_message_ids(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "text", null_message_fn },
+ { "msg1", "text", null_message_fn_2 },
+ { NULL },
+ };
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ pcmk__register_messages(out, entries);
+ assert_int_equal(g_hash_table_size(out->messages), 1);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn_2);
+
+ pcmk__output_free(out);
+}
+
+static void
+duplicate_functions(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "text", null_message_fn },
+ { "msg2", "text", null_message_fn },
+ { NULL },
+ };
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ pcmk__register_messages(out, entries);
+ assert_int_equal(g_hash_table_size(out->messages), 2);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg2"), null_message_fn);
+
+ pcmk__output_free(out);
+}
+
+static void
+default_handler(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "default", null_message_fn },
+ { NULL },
+ };
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ pcmk__register_messages(out, entries);
+ assert_int_equal(g_hash_table_size(out->messages), 1);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn);
+
+ pcmk__output_free(out);
+}
+
+static void
+override_default_handler(void **state) {
+ pcmk__output_t *out = NULL;
+
+ pcmk__message_entry_t entries[] = {
+ { "msg1", "default", null_message_fn },
+ { "msg1", "text", null_message_fn_2 },
+ { NULL },
+ };
+
+ pcmk__bare_output_new(&out, "text", NULL, NULL);
+
+ pcmk__register_messages(out, entries);
+ assert_int_equal(g_hash_table_size(out->messages), 1);
+ assert_ptr_equal(g_hash_table_lookup(out->messages, "msg1"), null_message_fn_2);
+
+ pcmk__output_free(out);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test_setup_teardown(invalid_entries, setup, teardown),
+ cmocka_unit_test_setup_teardown(valid_entries, setup, teardown),
+ cmocka_unit_test_setup_teardown(duplicate_message_ids, setup, teardown),
+ cmocka_unit_test_setup_teardown(duplicate_functions, setup, teardown),
+ cmocka_unit_test_setup_teardown(default_handler, setup, teardown),
+ cmocka_unit_test_setup_teardown(override_default_handler, setup, teardown))
diff --git a/lib/common/tests/output/pcmk__unregister_formats_test.c b/lib/common/tests/output/pcmk__unregister_formats_test.c
new file mode 100644
index 0000000..0631c95
--- /dev/null
+++ b/lib/common/tests/output/pcmk__unregister_formats_test.c
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2022 the Pacemaker project contributors
+ *
+ * The version control history for this file may have further details.
+ *
+ * This source code is licensed under the GNU General Public License version 2
+ * or later (GPLv2+) WITHOUT ANY WARRANTY.
+ */
+
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+#include <crm/common/output_internal.h>
+
+static pcmk__output_t *
+null_create_fn(char **argv) {
+ return NULL;
+}
+
+static void
+invalid_params(void **state) {
+ /* This is basically just here to make sure that calling pcmk__unregister_formats
+ * with formatters=NULL doesn't segfault.
+ */
+ pcmk__unregister_formats();
+ assert_null(pcmk__output_formatters());
+}
+
+static void
+non_null_formatters(void **state) {
+ pcmk__register_format(NULL, "fake", null_create_fn, NULL);
+
+ pcmk__unregister_formats();
+ assert_null(pcmk__output_formatters());
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(invalid_params),
+ cmocka_unit_test(non_null_formatters))