summaryrefslogtreecommitdiffstats
path: root/lib/common/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 06:53:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-17 06:53:20 +0000
commite5a812082ae033afb1eed82c0f2df3d0f6bdc93f (patch)
treea6716c9275b4b413f6c9194798b34b91affb3cc7 /lib/common/tests
parentInitial commit. (diff)
downloadpacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.tar.xz
pacemaker-e5a812082ae033afb1eed82c0f2df3d0f6bdc93f.zip
Adding upstream version 2.1.6.upstream/2.1.6
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/common/tests')
-rw-r--r--lib/common/tests/Makefile.am32
-rw-r--r--lib/common/tests/acl/Makefile.am21
-rw-r--r--lib/common/tests/acl/pcmk__is_user_in_group_test.c38
-rw-r--r--lib/common/tests/acl/pcmk_acl_required_test.c26
-rw-r--r--lib/common/tests/acl/xml_acl_denied_test.c61
-rw-r--r--lib/common/tests/acl/xml_acl_enabled_test.c61
-rw-r--r--lib/common/tests/agents/Makefile.am20
-rw-r--r--lib/common/tests/agents/crm_generate_ra_key_test.c48
-rw-r--r--lib/common/tests/agents/crm_parse_agent_spec_test.c87
-rw-r--r--lib/common/tests/agents/pcmk__effective_rc_test.c36
-rw-r--r--lib/common/tests/agents/pcmk_get_ra_caps_test.c63
-rw-r--r--lib/common/tests/agents/pcmk_stonith_param_test.c50
-rw-r--r--lib/common/tests/cmdline/Makefile.am17
-rw-r--r--lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c156
-rw-r--r--lib/common/tests/cmdline/pcmk__quote_cmdline_test.c56
-rw-r--r--lib/common/tests/flags/Makefile.am20
-rw-r--r--lib/common/tests/flags/pcmk__clear_flags_as_test.c41
-rw-r--r--lib/common/tests/flags/pcmk__set_flags_as_test.c25
-rw-r--r--lib/common/tests/flags/pcmk_all_flags_set_test.c33
-rw-r--r--lib/common/tests/flags/pcmk_any_flags_set_test.c26
-rw-r--r--lib/common/tests/health/Makefile.am17
-rw-r--r--lib/common/tests/health/pcmk__parse_health_strategy_test.c56
-rw-r--r--lib/common/tests/health/pcmk__validate_health_strategy_test.c38
-rw-r--r--lib/common/tests/io/Makefile.am18
-rw-r--r--lib/common/tests/io/pcmk__full_path_test.c52
-rw-r--r--lib/common/tests/io/pcmk__get_tmpdir_test.c68
-rw-r--r--lib/common/tests/iso8601/Makefile.am16
-rw-r--r--lib/common/tests/iso8601/pcmk__readable_interval_test.c27
-rw-r--r--lib/common/tests/lists/Makefile.am20
-rw-r--r--lib/common/tests/lists/pcmk__list_of_1_test.c45
-rw-r--r--lib/common/tests/lists/pcmk__list_of_multiple_test.c45
-rw-r--r--lib/common/tests/lists/pcmk__subtract_lists_test.c144
-rw-r--r--lib/common/tests/nvpair/Makefile.am18
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c50
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c59
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c31
-rw-r--r--lib/common/tests/operations/Makefile.am22
-rw-r--r--lib/common/tests/operations/copy_in_properties_test.c62
-rw-r--r--lib/common/tests/operations/expand_plus_plus_test.c256
-rw-r--r--lib/common/tests/operations/fix_plus_plus_recursive_test.c47
-rw-r--r--lib/common/tests/operations/parse_op_key_test.c275
-rw-r--r--lib/common/tests/operations/pcmk_is_probe_test.c25
-rw-r--r--lib/common/tests/operations/pcmk_xe_is_probe_test.c43
-rw-r--r--lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c150
-rw-r--r--lib/common/tests/options/Makefile.am19
-rw-r--r--lib/common/tests/options/pcmk__env_option_enabled_test.c101
-rw-r--r--lib/common/tests/options/pcmk__env_option_test.c134
-rw-r--r--lib/common/tests/options/pcmk__set_env_option_test.c154
-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
-rw-r--r--lib/common/tests/procfs/Makefile.am18
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c42
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c41
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_pid2path_test.c92
-rw-r--r--lib/common/tests/results/Makefile.am16
-rw-r--r--lib/common/tests/results/pcmk__results_test.c61
-rw-r--r--lib/common/tests/scores/Makefile.am19
-rw-r--r--lib/common/tests/scores/char2score_test.c75
-rw-r--r--lib/common/tests/scores/pcmk__add_scores_test.c74
-rw-r--r--lib/common/tests/scores/pcmk_readable_score_test.c33
-rw-r--r--lib/common/tests/strings/Makefile.am41
-rw-r--r--lib/common/tests/strings/crm_get_msec_test.c50
-rw-r--r--lib/common/tests/strings/crm_is_true_test.c57
-rw-r--r--lib/common/tests/strings/crm_str_to_boolean_test.c92
-rw-r--r--lib/common/tests/strings/pcmk__add_word_test.c93
-rw-r--r--lib/common/tests/strings/pcmk__btoa_test.c22
-rw-r--r--lib/common/tests/strings/pcmk__char_in_any_str_test.c46
-rw-r--r--lib/common/tests/strings/pcmk__compress_test.c58
-rw-r--r--lib/common/tests/strings/pcmk__ends_with_test.c57
-rw-r--r--lib/common/tests/strings/pcmk__g_strcat_test.c73
-rw-r--r--lib/common/tests/strings/pcmk__guint_from_hash_test.c76
-rw-r--r--lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c79
-rw-r--r--lib/common/tests/strings/pcmk__parse_ll_range_test.c117
-rw-r--r--lib/common/tests/strings/pcmk__s_test.c29
-rw-r--r--lib/common/tests/strings/pcmk__scan_double_test.c158
-rw-r--r--lib/common/tests/strings/pcmk__scan_min_int_test.c60
-rw-r--r--lib/common/tests/strings/pcmk__scan_port_test.c59
-rw-r--r--lib/common/tests/strings/pcmk__starts_with_test.c35
-rw-r--r--lib/common/tests/strings/pcmk__str_any_of_test.c48
-rw-r--r--lib/common/tests/strings/pcmk__str_in_list_test.c107
-rw-r--r--lib/common/tests/strings/pcmk__str_table_dup_test.c59
-rw-r--r--lib/common/tests/strings/pcmk__str_update_test.c78
-rw-r--r--lib/common/tests/strings/pcmk__strcmp_test.c80
-rw-r--r--lib/common/tests/strings/pcmk__strikey_table_test.c40
-rw-r--r--lib/common/tests/strings/pcmk__strkey_table_test.c40
-rw-r--r--lib/common/tests/strings/pcmk__trim_test.c72
-rw-r--r--lib/common/tests/utils/Makefile.am28
-rw-r--r--lib/common/tests/utils/compare_version_test.c55
-rw-r--r--lib/common/tests/utils/crm_meta_name_test.c41
-rw-r--r--lib/common/tests/utils/crm_meta_value_test.c56
-rw-r--r--lib/common/tests/utils/crm_user_lookup_test.c127
-rw-r--r--lib/common/tests/utils/pcmk__getpid_s_test.c38
-rw-r--r--lib/common/tests/utils/pcmk_daemon_user_test.c83
-rw-r--r--lib/common/tests/utils/pcmk_hostname_test.c56
-rw-r--r--lib/common/tests/utils/pcmk_str_is_infinity_test.c57
-rw-r--r--lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c54
-rw-r--r--lib/common/tests/xml/Makefile.am17
-rw-r--r--lib/common/tests/xml/pcmk__xe_foreach_child_test.c215
-rw-r--r--lib/common/tests/xml/pcmk__xe_match_test.c106
-rw-r--r--lib/common/tests/xpath/Makefile.am16
-rw-r--r--lib/common/tests/xpath/pcmk__xpath_node_id_test.c59
109 files changed, 7116 insertions, 0 deletions
diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am
new file mode 100644
index 0000000..b147309
--- /dev/null
+++ b/lib/common/tests/Makefile.am
@@ -0,0 +1,32 @@
+#
+# Copyright 2020-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.
+#
+
+SUBDIRS = \
+ acl \
+ agents \
+ cmdline \
+ flags \
+ health \
+ io \
+ iso8601 \
+ lists \
+ nvpair \
+ operations \
+ options \
+ output \
+ results \
+ scores \
+ strings \
+ utils \
+ xml \
+ xpath
+
+if SUPPORT_PROCFS
+SUBDIRS += procfs
+endif
diff --git a/lib/common/tests/acl/Makefile.am b/lib/common/tests/acl/Makefile.am
new file mode 100644
index 0000000..50408f9
--- /dev/null
+++ b/lib/common/tests/acl/Makefile.am
@@ -0,0 +1,21 @@
+#
+# 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__is_user_in_group_test \
+ pcmk_acl_required_test \
+ xml_acl_denied_test \
+ xml_acl_enabled_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/acl/pcmk__is_user_in_group_test.c b/lib/common/tests/acl/pcmk__is_user_in_group_test.c
new file mode 100644
index 0000000..917d92e
--- /dev/null
+++ b/lib/common/tests/acl/pcmk__is_user_in_group_test.c
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020-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/acl.h>
+
+#include "../../crmcommon_private.h"
+#include "mock_private.h"
+
+static void
+is_pcmk__is_user_in_group(void **state)
+{
+ pcmk__mock_grent = true;
+
+ // null user
+ assert_false(pcmk__is_user_in_group(NULL, "grp0"));
+ // null group
+ assert_false(pcmk__is_user_in_group("user0", NULL));
+ // nonexistent group
+ assert_false(pcmk__is_user_in_group("user0", "nonexistent_group"));
+ // user is in group
+ assert_true(pcmk__is_user_in_group("user0", "grp0"));
+ // user is not in group
+ assert_false(pcmk__is_user_in_group("user2", "grp0"));
+
+ pcmk__mock_grent = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(is_pcmk__is_user_in_group))
diff --git a/lib/common/tests/acl/pcmk_acl_required_test.c b/lib/common/tests/acl/pcmk_acl_required_test.c
new file mode 100644
index 0000000..bd5b922
--- /dev/null
+++ b/lib/common/tests/acl/pcmk_acl_required_test.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020-2021 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/acl.h>
+
+static void
+is_pcmk_acl_required(void **state)
+{
+ assert_false(pcmk_acl_required(NULL));
+ assert_false(pcmk_acl_required(""));
+ assert_true(pcmk_acl_required("123"));
+ assert_false(pcmk_acl_required(CRM_DAEMON_USER));
+ assert_false(pcmk_acl_required("root"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(is_pcmk_acl_required))
diff --git a/lib/common/tests/acl/xml_acl_denied_test.c b/lib/common/tests/acl/xml_acl_denied_test.c
new file mode 100644
index 0000000..faf2a39
--- /dev/null
+++ b/lib/common/tests/acl/xml_acl_denied_test.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020-2021 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/acl.h>
+
+#include "../../crmcommon_private.h"
+
+static void
+is_xml_acl_denied_without_node(void **state)
+{
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ assert_false(xml_acl_denied(test_xml));
+
+ test_xml->doc->_private = NULL;
+ assert_false(xml_acl_denied(test_xml));
+
+ test_xml->doc = NULL;
+ assert_false(xml_acl_denied(test_xml));
+
+ test_xml = NULL;
+ assert_false(xml_acl_denied(test_xml));
+}
+
+static void
+is_xml_acl_denied_with_node(void **state)
+{
+ xml_doc_private_t *docpriv;
+
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+
+ // allocate memory for _private, which is NULL by default
+ test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t));
+
+ assert_false(xml_acl_denied(test_xml));
+
+ // cast _private from void* to xml_doc_private_t*
+ docpriv = test_xml->doc->_private;
+
+ // enable an irrelevant flag
+ docpriv->flags |= pcmk__xf_acl_enabled;
+
+ assert_false(xml_acl_denied(test_xml));
+
+ // enable pcmk__xf_acl_denied
+ docpriv->flags |= pcmk__xf_acl_denied;
+
+ assert_true(xml_acl_denied(test_xml));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(is_xml_acl_denied_without_node),
+ cmocka_unit_test(is_xml_acl_denied_with_node))
diff --git a/lib/common/tests/acl/xml_acl_enabled_test.c b/lib/common/tests/acl/xml_acl_enabled_test.c
new file mode 100644
index 0000000..28665f4
--- /dev/null
+++ b/lib/common/tests/acl/xml_acl_enabled_test.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020-2021 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/acl.h>
+
+#include "../../crmcommon_private.h"
+
+static void
+is_xml_acl_enabled_without_node(void **state)
+{
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ assert_false(xml_acl_enabled(test_xml));
+
+ test_xml->doc->_private = NULL;
+ assert_false(xml_acl_enabled(test_xml));
+
+ test_xml->doc = NULL;
+ assert_false(xml_acl_enabled(test_xml));
+
+ test_xml = NULL;
+ assert_false(xml_acl_enabled(test_xml));
+}
+
+static void
+is_xml_acl_enabled_with_node(void **state)
+{
+ xml_doc_private_t *docpriv;
+
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+
+ // allocate memory for _private, which is NULL by default
+ test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t));
+
+ assert_false(xml_acl_enabled(test_xml));
+
+ // cast _private from void* to xml_doc_private_t*
+ docpriv = test_xml->doc->_private;
+
+ // enable an irrelevant flag
+ docpriv->flags |= pcmk__xf_acl_denied;
+
+ assert_false(xml_acl_enabled(test_xml));
+
+ // enable pcmk__xf_acl_enabled
+ docpriv->flags |= pcmk__xf_acl_enabled;
+
+ assert_true(xml_acl_enabled(test_xml));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(is_xml_acl_enabled_without_node),
+ cmocka_unit_test(is_xml_acl_enabled_with_node))
diff --git a/lib/common/tests/agents/Makefile.am b/lib/common/tests/agents/Makefile.am
new file mode 100644
index 0000000..7a54b7d
--- /dev/null
+++ b/lib/common/tests/agents/Makefile.am
@@ -0,0 +1,20 @@
+#
+# Copyright 2020-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 = crm_generate_ra_key_test \
+ crm_parse_agent_spec_test \
+ pcmk__effective_rc_test \
+ pcmk_get_ra_caps_test \
+ pcmk_stonith_param_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/agents/crm_generate_ra_key_test.c b/lib/common/tests/agents/crm_generate_ra_key_test.c
new file mode 100644
index 0000000..f71c1c2
--- /dev/null
+++ b/lib/common/tests/agents/crm_generate_ra_key_test.c
@@ -0,0 +1,48 @@
+/*
+ * 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/agents.h>
+
+static void
+all_params_null(void **state) {
+ assert_null(crm_generate_ra_key(NULL, NULL, NULL));
+}
+
+static void
+some_params_null(void **state) {
+ char *retval;
+
+ assert_null(crm_generate_ra_key("std", "prov", NULL));
+
+ retval = crm_generate_ra_key("std", NULL, "ty");
+ assert_string_equal(retval, "std:ty");
+ free(retval);
+
+ assert_null(crm_generate_ra_key(NULL, "prov", "ty"));
+ assert_null(crm_generate_ra_key("std", NULL, NULL));
+ assert_null(crm_generate_ra_key(NULL, "prov", NULL));
+ assert_null(crm_generate_ra_key(NULL, NULL, "ty"));
+}
+
+static void
+no_params_null(void **state) {
+ char *retval;
+
+ retval = crm_generate_ra_key("std", "prov", "ty");
+ assert_string_equal(retval, "std:prov:ty");
+ free(retval);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(all_params_null),
+ cmocka_unit_test(some_params_null),
+ cmocka_unit_test(no_params_null))
diff --git a/lib/common/tests/agents/crm_parse_agent_spec_test.c b/lib/common/tests/agents/crm_parse_agent_spec_test.c
new file mode 100644
index 0000000..cfd75f0
--- /dev/null
+++ b/lib/common/tests/agents/crm_parse_agent_spec_test.c
@@ -0,0 +1,87 @@
+/*
+ * 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/agents.h>
+
+static void
+all_params_null(void **state) {
+ assert_int_equal(crm_parse_agent_spec(NULL, NULL, NULL, NULL), -EINVAL);
+ assert_int_equal(crm_parse_agent_spec("", NULL, NULL, NULL), -EINVAL);
+ assert_int_equal(crm_parse_agent_spec(":", NULL, NULL, NULL), -EINVAL);
+ assert_int_equal(crm_parse_agent_spec("::", NULL, NULL, NULL), -EINVAL);
+}
+
+static void
+no_prov_or_type(void **state) {
+ assert_int_equal(crm_parse_agent_spec("ocf", NULL, NULL, NULL), -EINVAL);
+ assert_int_equal(crm_parse_agent_spec("ocf:", NULL, NULL, NULL), -EINVAL);
+ assert_int_equal(crm_parse_agent_spec("ocf::", NULL, NULL, NULL), -EINVAL);
+}
+
+static void
+no_type(void **state) {
+ assert_int_equal(crm_parse_agent_spec("ocf:pacemaker:", NULL, NULL, NULL), -EINVAL);
+}
+
+static void
+get_std_and_ty(void **state) {
+ char *std = NULL;
+ char *prov = NULL;
+ char *ty = NULL;
+
+ assert_int_equal(crm_parse_agent_spec("stonith:fence_xvm", &std, &prov, &ty), pcmk_ok);
+ assert_string_equal(std, "stonith");
+ assert_null(prov);
+ assert_string_equal(ty, "fence_xvm");
+
+ free(std);
+ free(ty);
+}
+
+static void
+get_all_values(void **state) {
+ char *std = NULL;
+ char *prov = NULL;
+ char *ty = NULL;
+
+ assert_int_equal(crm_parse_agent_spec("ocf:pacemaker:ping", &std, &prov, &ty), pcmk_ok);
+ assert_string_equal(std, "ocf");
+ assert_string_equal(prov, "pacemaker");
+ assert_string_equal(ty, "ping");
+
+ free(std);
+ free(prov);
+ free(ty);
+}
+
+static void
+get_systemd_values(void **state) {
+ char *std = NULL;
+ char *prov = NULL;
+ char *ty = NULL;
+
+ assert_int_equal(crm_parse_agent_spec("systemd:UNIT@A:B", &std, &prov, &ty), pcmk_ok);
+ assert_string_equal(std, "systemd");
+ assert_null(prov);
+ assert_string_equal(ty, "UNIT@A:B");
+
+ free(std);
+ free(ty);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(all_params_null),
+ cmocka_unit_test(no_prov_or_type),
+ cmocka_unit_test(no_type),
+ cmocka_unit_test(get_std_and_ty),
+ cmocka_unit_test(get_all_values),
+ cmocka_unit_test(get_systemd_values))
diff --git a/lib/common/tests/agents/pcmk__effective_rc_test.c b/lib/common/tests/agents/pcmk__effective_rc_test.c
new file mode 100644
index 0000000..c9bad97
--- /dev/null
+++ b/lib/common/tests/agents/pcmk__effective_rc_test.c
@@ -0,0 +1,36 @@
+/*
+ * 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/agents.h>
+
+static void
+pcmk__effective_rc_test(void **state) {
+ /* All other PCMK_OCF_* values after UNKNOWN are deprecated and no longer used,
+ * so probably not worth testing them.
+ */
+ assert_int_equal(PCMK_OCF_OK, pcmk__effective_rc(PCMK_OCF_OK));
+ assert_int_equal(PCMK_OCF_OK, pcmk__effective_rc(PCMK_OCF_DEGRADED));
+ assert_int_equal(PCMK_OCF_RUNNING_PROMOTED, pcmk__effective_rc(PCMK_OCF_DEGRADED_PROMOTED));
+ assert_int_equal(PCMK_OCF_UNKNOWN, pcmk__effective_rc(PCMK_OCF_UNKNOWN));
+
+ /* There's nothing that says pcmk__effective_rc is restricted to PCMK_OCF_*
+ * values. That's just how it's used. Let's check some values outside
+ * that range just to be sure.
+ */
+ assert_int_equal(-1, pcmk__effective_rc(-1));
+ assert_int_equal(255, pcmk__effective_rc(255));
+ assert_int_equal(INT_MAX, pcmk__effective_rc(INT_MAX));
+ assert_int_equal(INT_MIN, pcmk__effective_rc(INT_MIN));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(pcmk__effective_rc_test))
diff --git a/lib/common/tests/agents/pcmk_get_ra_caps_test.c b/lib/common/tests/agents/pcmk_get_ra_caps_test.c
new file mode 100644
index 0000000..178dce5
--- /dev/null
+++ b/lib/common/tests/agents/pcmk_get_ra_caps_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/agents.h>
+
+static void
+ocf_standard(void **state) {
+ uint32_t expected = pcmk_ra_cap_provider | pcmk_ra_cap_params |
+ pcmk_ra_cap_unique | pcmk_ra_cap_promotable;
+
+ assert_int_equal(pcmk_get_ra_caps("ocf"), expected);
+ assert_int_equal(pcmk_get_ra_caps("OCF"), expected);
+}
+
+static void
+stonith_standard(void **state) {
+ uint32_t expected = pcmk_ra_cap_params | pcmk_ra_cap_unique |
+ pcmk_ra_cap_stdin | pcmk_ra_cap_fence_params;
+
+ assert_int_equal(pcmk_get_ra_caps("stonith"), expected);
+ assert_int_equal(pcmk_get_ra_caps("StOnItH"), expected);
+}
+
+static void
+service_standard(void **state) {
+ assert_int_equal(pcmk_get_ra_caps("systemd"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("SYSTEMD"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("service"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("SeRvIcE"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("lsb"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("LSB"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("upstart"), pcmk_ra_cap_status);
+ assert_int_equal(pcmk_get_ra_caps("uPsTaRt"), pcmk_ra_cap_status);
+}
+
+static void
+nagios_standard(void **state) {
+ assert_int_equal(pcmk_get_ra_caps("nagios"), pcmk_ra_cap_params);
+ assert_int_equal(pcmk_get_ra_caps("NAGios"), pcmk_ra_cap_params);
+}
+
+static void
+unknown_standard(void **state) {
+ assert_int_equal(pcmk_get_ra_caps("blahblah"), pcmk_ra_cap_none);
+ assert_int_equal(pcmk_get_ra_caps(""), pcmk_ra_cap_none);
+ assert_int_equal(pcmk_get_ra_caps(NULL), pcmk_ra_cap_none);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(ocf_standard),
+ cmocka_unit_test(stonith_standard),
+ cmocka_unit_test(service_standard),
+ cmocka_unit_test(nagios_standard),
+ cmocka_unit_test(unknown_standard))
diff --git a/lib/common/tests/agents/pcmk_stonith_param_test.c b/lib/common/tests/agents/pcmk_stonith_param_test.c
new file mode 100644
index 0000000..fad431e
--- /dev/null
+++ b/lib/common/tests/agents/pcmk_stonith_param_test.c
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2020-2021 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/agents.h>
+
+static void
+is_stonith_param(void **state)
+{
+ assert_false(pcmk_stonith_param(NULL));
+ assert_false(pcmk_stonith_param(""));
+ assert_false(pcmk_stonith_param("unrecognized"));
+ assert_false(pcmk_stonith_param("pcmk_unrecognized"));
+ assert_false(pcmk_stonith_param("x" PCMK_STONITH_ACTION_LIMIT));
+ assert_false(pcmk_stonith_param(PCMK_STONITH_ACTION_LIMIT "x"));
+
+ assert_true(pcmk_stonith_param(PCMK_STONITH_ACTION_LIMIT));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_DELAY_BASE));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_DELAY_MAX));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_ARGUMENT));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_CHECK));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_LIST));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_HOST_MAP));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_PROVIDES));
+ assert_true(pcmk_stonith_param(PCMK_STONITH_STONITH_TIMEOUT));
+}
+
+static void
+is_stonith_action_param(void **state)
+{
+ /* Currently, the function accepts any string not containing underbars as
+ * the action name, so we do not need to verify particular action names.
+ */
+ assert_false(pcmk_stonith_param("pcmk_on_unrecognized"));
+ assert_true(pcmk_stonith_param("pcmk_on_action"));
+ assert_true(pcmk_stonith_param("pcmk_on_timeout"));
+ assert_true(pcmk_stonith_param("pcmk_on_retries"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(is_stonith_param),
+ cmocka_unit_test(is_stonith_action_param))
diff --git a/lib/common/tests/cmdline/Makefile.am b/lib/common/tests/cmdline/Makefile.am
new file mode 100644
index 0000000..d781ed5
--- /dev/null
+++ b/lib/common/tests/cmdline/Makefile.am
@@ -0,0 +1,17 @@
+#
+# Copyright 2020-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__cmdline_preproc_test \
+ pcmk__quote_cmdline_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c
new file mode 100644
index 0000000..863fbb9
--- /dev/null
+++ b/lib/common/tests/cmdline/pcmk__cmdline_preproc_test.c
@@ -0,0 +1,156 @@
+/*
+ * Copyright 2020-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/cmdline_internal.h>
+
+#include <glib.h>
+#include <stdint.h>
+
+#define LISTS_EQ(a, b) { \
+ assert_int_equal(g_strv_length((gchar **) (a)), g_strv_length((gchar **) (b))); \
+ for (int i = 0; i < g_strv_length((a)); i++) { \
+ assert_string_equal((a)[i], (b)[i]); \
+ } \
+}
+
+static void
+empty_input(void **state) {
+ assert_null(pcmk__cmdline_preproc(NULL, ""));
+}
+
+static void
+no_specials(void **state) {
+ const char *argv[] = { "crm_mon", "-a", "-b", "-c", "-d", "-1", NULL };
+ const gchar *expected[] = { "crm_mon", "-a", "-b", "-c", "-d", "-1", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL);
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+
+ processed = pcmk__cmdline_preproc((char **) argv, "");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+single_dash(void **state) {
+ const char *argv[] = { "crm_mon", "-", NULL };
+ const gchar *expected[] = { "crm_mon", "-", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL);
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+double_dash(void **state) {
+ const char *argv[] = { "crm_mon", "-a", "--", "-bc", NULL };
+ const gchar *expected[] = { "crm_mon", "-a", "--", "-bc", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL);
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+special_args(void **state) {
+ const char *argv[] = { "crm_mon", "-aX", "-Fval", NULL };
+ const gchar *expected[] = { "crm_mon", "-a", "X", "-F", "val", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, "aF");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+special_arg_at_end(void **state) {
+ const char *argv[] = { "crm_mon", "-a", NULL };
+ const gchar *expected[] = { "crm_mon", "-a", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, "a");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+long_arg(void **state) {
+ const char *argv[] = { "crm_mon", "--blah=foo", NULL };
+ const gchar *expected[] = { "crm_mon", "--blah=foo", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL);
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+negative_score(void **state) {
+ const char *argv[] = { "crm_mon", "-v", "-1000", NULL };
+ const gchar *expected[] = { "crm_mon", "-v", "-1000", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, "v");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+negative_score_2(void **state) {
+ const char *argv[] = { "crm_mon", "-1i3", NULL };
+ const gchar *expected[] = { "crm_mon", "-1", "-i", "-3", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, NULL);
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+string_arg_with_dash(void **state) {
+ const char *argv[] = { "crm_mon", "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL };
+ const gchar *expected[] = { "crm_mon", "-n", "crm_mon_options", "-v", "--opt1 --opt2", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, "v");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+string_arg_with_dash_2(void **state) {
+ const char *argv[] = { "crm_mon", "-n", "crm_mon_options", "-v", "-1i3", NULL };
+ const gchar *expected[] = { "crm_mon", "-n", "crm_mon_options", "-v", "-1i3", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, "v");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+static void
+string_arg_with_dash_3(void **state) {
+ const char *argv[] = { "crm_mon", "-abc", "-1i3", NULL };
+ const gchar *expected[] = { "crm_mon", "-a", "-b", "-c", "-1i3", NULL };
+
+ gchar **processed = pcmk__cmdline_preproc((char **) argv, "c");
+ LISTS_EQ(processed, expected);
+ g_strfreev(processed);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(no_specials),
+ cmocka_unit_test(single_dash),
+ cmocka_unit_test(double_dash),
+ cmocka_unit_test(special_args),
+ cmocka_unit_test(special_arg_at_end),
+ cmocka_unit_test(long_arg),
+ cmocka_unit_test(negative_score),
+ cmocka_unit_test(negative_score_2),
+ cmocka_unit_test(string_arg_with_dash),
+ cmocka_unit_test(string_arg_with_dash_2),
+ cmocka_unit_test(string_arg_with_dash_3))
diff --git a/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c
new file mode 100644
index 0000000..42bd8ca
--- /dev/null
+++ b/lib/common/tests/cmdline/pcmk__quote_cmdline_test.c
@@ -0,0 +1,56 @@
+/*
+ * 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/cmdline_internal.h>
+
+#include <glib.h>
+
+static void
+empty_input(void **state) {
+ assert_null(pcmk__quote_cmdline(NULL));
+}
+
+static void
+no_spaces(void **state) {
+ const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello", "--output-as=xml", NULL };
+ const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v hello --output-as=xml";
+
+ gchar *processed = pcmk__quote_cmdline((gchar **) argv);
+ assert_string_equal(processed, expected);
+ g_free(processed);
+}
+
+static void
+spaces_no_quote(void **state) {
+ const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "hello world", "--output-as=xml", NULL };
+ const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'hello world' --output-as=xml";
+
+ gchar *processed = pcmk__quote_cmdline((gchar **) argv);
+ assert_string_equal(processed, expected);
+ g_free(processed);
+}
+
+static void
+spaces_with_quote(void **state) {
+ const char *argv[] = { "crm_resource", "-r", "rsc1", "--meta", "-p", "comment", "-v", "here's johnny", "--output-as=xml", NULL };
+ const gchar *expected = "crm_resource -r rsc1 --meta -p comment -v 'here\\\'s johnny' --output-as=xml";
+
+ gchar *processed = pcmk__quote_cmdline((gchar **) argv);
+ assert_string_equal(processed, expected);
+ g_free(processed);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(no_spaces),
+ cmocka_unit_test(spaces_no_quote),
+ cmocka_unit_test(spaces_with_quote))
diff --git a/lib/common/tests/flags/Makefile.am b/lib/common/tests/flags/Makefile.am
new file mode 100644
index 0000000..16d8ffb
--- /dev/null
+++ b/lib/common/tests/flags/Makefile.am
@@ -0,0 +1,20 @@
+#
+# Copyright 2020-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__clear_flags_as_test \
+ pcmk__set_flags_as_test \
+ pcmk_all_flags_set_test \
+ pcmk_any_flags_set_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/flags/pcmk__clear_flags_as_test.c b/lib/common/tests/flags/pcmk__clear_flags_as_test.c
new file mode 100644
index 0000000..07dbe28
--- /dev/null
+++ b/lib/common/tests/flags/pcmk__clear_flags_as_test.c
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+clear_none(void **state) {
+ assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0x00f, NULL), 0x0f0);
+ assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0xf0f, NULL), 0x0f0);
+}
+
+static void
+clear_some(void **state) {
+ assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0x020, NULL), 0x0d0);
+ assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0x030, NULL), 0x0c0);
+}
+
+static void
+clear_all(void **state) {
+ assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0x0f0, NULL), 0x000);
+ assert_int_equal(pcmk__clear_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0xfff, NULL), 0x000);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(clear_none),
+ cmocka_unit_test(clear_some),
+ cmocka_unit_test(clear_all))
diff --git a/lib/common/tests/flags/pcmk__set_flags_as_test.c b/lib/common/tests/flags/pcmk__set_flags_as_test.c
new file mode 100644
index 0000000..cd14c85
--- /dev/null
+++ b/lib/common/tests/flags/pcmk__set_flags_as_test.c
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+set_flags(void **state) {
+ assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0x00f, NULL), 0x0ff);
+ assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0xf0f, NULL), 0xfff);
+ assert_int_equal(pcmk__set_flags_as(__func__, __LINE__, LOG_TRACE, "Test",
+ "test", 0x0f0, 0xfff, NULL), 0xfff);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(set_flags))
diff --git a/lib/common/tests/flags/pcmk_all_flags_set_test.c b/lib/common/tests/flags/pcmk_all_flags_set_test.c
new file mode 100644
index 0000000..512ccce
--- /dev/null
+++ b/lib/common/tests/flags/pcmk_all_flags_set_test.c
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+all_set(void **state) {
+ assert_false(pcmk_all_flags_set(0x000, 0x003));
+ assert_true(pcmk_all_flags_set(0x00f, 0x003));
+ assert_false(pcmk_all_flags_set(0x00f, 0x010));
+ assert_false(pcmk_all_flags_set(0x00f, 0x011));
+ assert_true(pcmk_all_flags_set(0x000, 0x000));
+ assert_true(pcmk_all_flags_set(0x00f, 0x000));
+}
+
+static void
+one_is_set(void **state) {
+ // pcmk_is_set() is a simple macro alias for pcmk_all_flags_set()
+ assert_true(pcmk_is_set(0x00f, 0x001));
+ assert_false(pcmk_is_set(0x00f, 0x010));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(all_set),
+ cmocka_unit_test(one_is_set))
diff --git a/lib/common/tests/flags/pcmk_any_flags_set_test.c b/lib/common/tests/flags/pcmk_any_flags_set_test.c
new file mode 100644
index 0000000..dc3aabc
--- /dev/null
+++ b/lib/common/tests/flags/pcmk_any_flags_set_test.c
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+any_set(void **state) {
+ assert_false(pcmk_any_flags_set(0x000, 0x000));
+ assert_false(pcmk_any_flags_set(0x000, 0x001));
+ assert_true(pcmk_any_flags_set(0x00f, 0x001));
+ assert_false(pcmk_any_flags_set(0x00f, 0x010));
+ assert_true(pcmk_any_flags_set(0x00f, 0x011));
+ assert_false(pcmk_any_flags_set(0x000, 0x000));
+ assert_false(pcmk_any_flags_set(0x00f, 0x000));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(any_set))
diff --git a/lib/common/tests/health/Makefile.am b/lib/common/tests/health/Makefile.am
new file mode 100644
index 0000000..ad2a2da
--- /dev/null
+++ b/lib/common/tests/health/Makefile.am
@@ -0,0 +1,17 @@
+#
+# 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 $(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__parse_health_strategy_test \
+ pcmk__validate_health_strategy_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/health/pcmk__parse_health_strategy_test.c b/lib/common/tests/health/pcmk__parse_health_strategy_test.c
new file mode 100644
index 0000000..28cc702
--- /dev/null
+++ b/lib/common/tests/health/pcmk__parse_health_strategy_test.c
@@ -0,0 +1,56 @@
+/*
+ * 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>
+
+static void
+valid(void **state) {
+ assert_int_equal(pcmk__parse_health_strategy(NULL),
+ pcmk__health_strategy_none);
+
+ assert_int_equal(pcmk__parse_health_strategy("none"),
+ pcmk__health_strategy_none);
+
+ assert_int_equal(pcmk__parse_health_strategy("NONE"),
+ pcmk__health_strategy_none);
+
+ assert_int_equal(pcmk__parse_health_strategy("None"),
+ pcmk__health_strategy_none);
+
+ assert_int_equal(pcmk__parse_health_strategy("nOnE"),
+ pcmk__health_strategy_none);
+
+ assert_int_equal(pcmk__parse_health_strategy("migrate-on-red"),
+ pcmk__health_strategy_no_red);
+
+ assert_int_equal(pcmk__parse_health_strategy("only-green"),
+ pcmk__health_strategy_only_green);
+
+ assert_int_equal(pcmk__parse_health_strategy("progressive"),
+ pcmk__health_strategy_progressive);
+
+ assert_int_equal(pcmk__parse_health_strategy("custom"),
+ pcmk__health_strategy_custom);
+}
+
+static void
+invalid(void **state) {
+ assert_int_equal(pcmk__parse_health_strategy("foo"),
+ pcmk__health_strategy_none);
+ assert_int_equal(pcmk__parse_health_strategy("custom1"),
+ pcmk__health_strategy_none);
+ assert_int_equal(pcmk__parse_health_strategy("not-only-green-here"),
+ pcmk__health_strategy_none);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(valid),
+ cmocka_unit_test(invalid))
diff --git a/lib/common/tests/health/pcmk__validate_health_strategy_test.c b/lib/common/tests/health/pcmk__validate_health_strategy_test.c
new file mode 100644
index 0000000..c7c60aa
--- /dev/null
+++ b/lib/common/tests/health/pcmk__validate_health_strategy_test.c
@@ -0,0 +1,38 @@
+/*
+ * 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>
+
+// Test functions
+
+static void
+valid_strategy(void **state) {
+ assert_true(pcmk__validate_health_strategy("none"));
+ assert_true(pcmk__validate_health_strategy("None"));
+ assert_true(pcmk__validate_health_strategy("NONE"));
+ assert_true(pcmk__validate_health_strategy("NoNe"));
+ assert_true(pcmk__validate_health_strategy("migrate-on-red"));
+ assert_true(pcmk__validate_health_strategy("only-green"));
+ assert_true(pcmk__validate_health_strategy("progressive"));
+ assert_true(pcmk__validate_health_strategy("custom"));
+}
+
+static void
+invalid_strategy(void **state) {
+ assert_false(pcmk__validate_health_strategy(NULL));
+ assert_false(pcmk__validate_health_strategy(""));
+ assert_false(pcmk__validate_health_strategy("none to speak of"));
+ assert_false(pcmk__validate_health_strategy("customized"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(valid_strategy),
+ cmocka_unit_test(invalid_strategy))
diff --git a/lib/common/tests/io/Makefile.am b/lib/common/tests/io/Makefile.am
new file mode 100644
index 0000000..c26482c
--- /dev/null
+++ b/lib/common/tests/io/Makefile.am
@@ -0,0 +1,18 @@
+#
+# Copyright 2020-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__full_path_test \
+ pcmk__get_tmpdir_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/io/pcmk__full_path_test.c b/lib/common/tests/io/pcmk__full_path_test.c
new file mode 100644
index 0000000..dbbd71b
--- /dev/null
+++ b/lib/common/tests/io/pcmk__full_path_test.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2020-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 "mock_private.h"
+
+static void
+function_asserts(void **state)
+{
+ pcmk__assert_asserts(pcmk__full_path(NULL, "/dir"));
+ pcmk__assert_asserts(pcmk__full_path("file", NULL));
+
+ pcmk__assert_asserts(
+ {
+ pcmk__mock_strdup = true; // strdup() will return NULL
+ expect_string(__wrap_strdup, s, "/full/path");
+ pcmk__full_path("/full/path", "/dir");
+ pcmk__mock_strdup = false; // Use real strdup()
+ }
+ );
+}
+
+static void
+full_path(void **state)
+{
+ char *path = NULL;
+
+ path = pcmk__full_path("file", "/dir");
+ assert_int_equal(strcmp(path, "/dir/file"), 0);
+ free(path);
+
+ path = pcmk__full_path("/full/path", "/dir");
+ assert_int_equal(strcmp(path, "/full/path"), 0);
+ free(path);
+
+ path = pcmk__full_path("../relative/path", "/dir");
+ assert_int_equal(strcmp(path, "/dir/../relative/path"), 0);
+ free(path);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(function_asserts),
+ cmocka_unit_test(full_path))
diff --git a/lib/common/tests/io/pcmk__get_tmpdir_test.c b/lib/common/tests/io/pcmk__get_tmpdir_test.c
new file mode 100644
index 0000000..bc17513
--- /dev/null
+++ b/lib/common/tests/io/pcmk__get_tmpdir_test.c
@@ -0,0 +1,68 @@
+/*
+ * 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 <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include "mock_private.h"
+
+static void
+getenv_returns_invalid(void **state)
+{
+ const char *result;
+
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "TMPDIR");
+ will_return(__wrap_getenv, NULL); // getenv("TMPDIR") return value
+ result = pcmk__get_tmpdir();
+ assert_string_equal(result, "/tmp");
+
+ expect_string(__wrap_getenv, name, "TMPDIR");
+ will_return(__wrap_getenv, ""); // getenv("TMPDIR") return value
+ result = pcmk__get_tmpdir();
+ assert_string_equal(result, "/tmp");
+
+ expect_string(__wrap_getenv, name, "TMPDIR");
+ will_return(__wrap_getenv, "subpath"); // getenv("TMPDIR") return value
+ result = pcmk__get_tmpdir();
+ assert_string_equal(result, "/tmp");
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+getenv_returns_valid(void **state)
+{
+ const char *result;
+
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "TMPDIR");
+ will_return(__wrap_getenv, "/var/tmp"); // getenv("TMPDIR") return value
+ result = pcmk__get_tmpdir();
+ assert_string_equal(result, "/var/tmp");
+
+ expect_string(__wrap_getenv, name, "TMPDIR");
+ will_return(__wrap_getenv, "/"); // getenv("TMPDIR") return value
+ result = pcmk__get_tmpdir();
+ assert_string_equal(result, "/");
+
+ expect_string(__wrap_getenv, name, "TMPDIR");
+ will_return(__wrap_getenv, "/tmp/abcd.1234"); // getenv("TMPDIR") return value
+ result = pcmk__get_tmpdir();
+ assert_string_equal(result, "/tmp/abcd.1234");
+
+ pcmk__mock_getenv = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(getenv_returns_invalid),
+ cmocka_unit_test(getenv_returns_valid))
diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am
new file mode 100644
index 0000000..5187aec
--- /dev/null
+++ b/lib/common/tests/iso8601/Makefile.am
@@ -0,0 +1,16 @@
+#
+# Copyright 2020-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__readable_interval_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/iso8601/pcmk__readable_interval_test.c b/lib/common/tests/iso8601/pcmk__readable_interval_test.c
new file mode 100644
index 0000000..43b5541
--- /dev/null
+++ b/lib/common/tests/iso8601/pcmk__readable_interval_test.c
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2021 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 <limits.h>
+
+static void
+readable_interval(void **state)
+{
+ assert_string_equal(pcmk__readable_interval(0), "0s");
+ assert_string_equal(pcmk__readable_interval(30000), "30s");
+ assert_string_equal(pcmk__readable_interval(150000), "2m30s");
+ assert_string_equal(pcmk__readable_interval(3333), "3.333s");
+ assert_string_equal(pcmk__readable_interval(UINT_MAX), "49d17h2m47.295s");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(readable_interval))
diff --git a/lib/common/tests/lists/Makefile.am b/lib/common/tests/lists/Makefile.am
new file mode 100644
index 0000000..ae0c0b6
--- /dev/null
+++ b/lib/common/tests/lists/Makefile.am
@@ -0,0 +1,20 @@
+#
+# 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 $(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__list_of_1_test \
+ pcmk__list_of_multiple_test \
+ pcmk__subtract_lists_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/lists/pcmk__list_of_1_test.c b/lib/common/tests/lists/pcmk__list_of_1_test.c
new file mode 100644
index 0000000..a3e0bbc
--- /dev/null
+++ b/lib/common/tests/lists/pcmk__list_of_1_test.c
@@ -0,0 +1,45 @@
+/*
+ * 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 <glib.h>
+
+static void
+empty_list(void **state) {
+ assert_false(pcmk__list_of_1(NULL));
+}
+
+static void
+singleton_list(void **state) {
+ GList *lst = NULL;
+
+ lst = g_list_append(lst, strdup("abc"));
+ assert_true(pcmk__list_of_1(lst));
+
+ g_list_free_full(lst, free);
+}
+
+static void
+longer_list(void **state) {
+ GList *lst = NULL;
+
+ lst = g_list_append(lst, strdup("abc"));
+ lst = g_list_append(lst, strdup("xyz"));
+ assert_false(pcmk__list_of_1(lst));
+
+ g_list_free_full(lst, free);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_list),
+ cmocka_unit_test(singleton_list),
+ cmocka_unit_test(longer_list))
diff --git a/lib/common/tests/lists/pcmk__list_of_multiple_test.c b/lib/common/tests/lists/pcmk__list_of_multiple_test.c
new file mode 100644
index 0000000..0966037
--- /dev/null
+++ b/lib/common/tests/lists/pcmk__list_of_multiple_test.c
@@ -0,0 +1,45 @@
+/*
+ * 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 <glib.h>
+
+static void
+empty_list(void **state) {
+ assert_false(pcmk__list_of_multiple(NULL));
+}
+
+static void
+singleton_list(void **state) {
+ GList *lst = NULL;
+
+ lst = g_list_append(lst, strdup("abc"));
+ assert_false(pcmk__list_of_multiple(lst));
+
+ g_list_free_full(lst, free);
+}
+
+static void
+longer_list(void **state) {
+ GList *lst = NULL;
+
+ lst = g_list_append(lst, strdup("abc"));
+ lst = g_list_append(lst, strdup("xyz"));
+ assert_true(pcmk__list_of_multiple(lst));
+
+ g_list_free_full(lst, free);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_list),
+ cmocka_unit_test(singleton_list),
+ cmocka_unit_test(longer_list))
diff --git a/lib/common/tests/lists/pcmk__subtract_lists_test.c b/lib/common/tests/lists/pcmk__subtract_lists_test.c
new file mode 100644
index 0000000..1198e2b
--- /dev/null
+++ b/lib/common/tests/lists/pcmk__subtract_lists_test.c
@@ -0,0 +1,144 @@
+/*
+ * 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/lists_internal.h>
+
+#include <glib.h>
+
+static void
+different_lists(void **state)
+{
+ GList *from = NULL;
+ GList *items = NULL;
+ GList *result = NULL;
+
+ from = g_list_append(from, strdup("abc"));
+ from = g_list_append(from, strdup("def"));
+ from = g_list_append(from, strdup("ghi"));
+
+ items = g_list_append(items, strdup("123"));
+ items = g_list_append(items, strdup("456"));
+
+ result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp);
+
+ assert_int_equal(g_list_length(result), 3);
+ assert_string_equal(g_list_nth_data(result, 0), "abc");
+ assert_string_equal(g_list_nth_data(result, 1), "def");
+ assert_string_equal(g_list_nth_data(result, 2), "ghi");
+
+ g_list_free(result);
+ g_list_free_full(from, free);
+ g_list_free_full(items, free);
+}
+
+static void
+remove_first_item(void **state)
+{
+ GList *from = NULL;
+ GList *items = NULL;
+ GList *result = NULL;
+
+ from = g_list_append(from, strdup("abc"));
+ from = g_list_append(from, strdup("def"));
+ from = g_list_append(from, strdup("ghi"));
+
+ items = g_list_append(items, strdup("abc"));
+
+ result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp);
+
+ assert_int_equal(g_list_length(result), 2);
+ assert_string_equal(g_list_nth_data(result, 0), "def");
+ assert_string_equal(g_list_nth_data(result, 1), "ghi");
+
+ g_list_free(result);
+ g_list_free_full(from, free);
+ g_list_free_full(items, free);
+}
+
+static void
+remove_middle_item(void **state)
+{
+ GList *from = NULL;
+ GList *items = NULL;
+ GList *result = NULL;
+
+ from = g_list_append(from, strdup("abc"));
+ from = g_list_append(from, strdup("def"));
+ from = g_list_append(from, strdup("ghi"));
+
+ items = g_list_append(items, strdup("def"));
+
+ result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp);
+
+ assert_int_equal(g_list_length(result), 2);
+ assert_string_equal(g_list_nth_data(result, 0), "abc");
+ assert_string_equal(g_list_nth_data(result, 1), "ghi");
+
+ g_list_free(result);
+ g_list_free_full(from, free);
+ g_list_free_full(items, free);
+}
+
+static void
+remove_last_item(void **state)
+{
+ GList *from = NULL;
+ GList *items = NULL;
+ GList *result = NULL;
+
+ from = g_list_append(from, strdup("abc"));
+ from = g_list_append(from, strdup("def"));
+ from = g_list_append(from, strdup("ghi"));
+
+ items = g_list_append(items, strdup("ghi"));
+
+ result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp);
+
+ assert_int_equal(g_list_length(result), 2);
+ assert_string_equal(g_list_nth_data(result, 0), "abc");
+ assert_string_equal(g_list_nth_data(result, 1), "def");
+
+ g_list_free(result);
+ g_list_free_full(from, free);
+ g_list_free_full(items, free);
+}
+
+static void
+remove_all_items(void **state)
+{
+ GList *from = NULL;
+ GList *items = NULL;
+ GList *result = NULL;
+
+ from = g_list_append(from, strdup("abc"));
+ from = g_list_append(from, strdup("def"));
+ from = g_list_append(from, strdup("ghi"));
+
+ items = g_list_append(items, strdup("abc"));
+ items = g_list_append(items, strdup("def"));
+ items = g_list_append(items, strdup("ghi"));
+
+ result = pcmk__subtract_lists(from, items, (GCompareFunc) strcmp);
+
+ assert_int_equal(g_list_length(result), 0);
+
+ g_list_free(result);
+ g_list_free_full(from, free);
+ g_list_free_full(items, free);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(different_lists),
+ cmocka_unit_test(remove_first_item),
+ cmocka_unit_test(remove_middle_item),
+ cmocka_unit_test(remove_last_item),
+ cmocka_unit_test(remove_all_items))
diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am
new file mode 100644
index 0000000..7acaba3
--- /dev/null
+++ b/lib/common/tests/nvpair/Makefile.am
@@ -0,0 +1,18 @@
+#
+# 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__xe_attr_is_true_test \
+ pcmk__xe_get_bool_attr_test \
+ pcmk__xe_set_bool_attr_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
new file mode 100644
index 0000000..3723707
--- /dev/null
+++ b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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/xml_internal.h>
+
+static void
+empty_input(void **state)
+{
+ xmlNode *node = string2xml("<node/>");
+
+ assert_false(pcmk__xe_attr_is_true(NULL, NULL));
+ assert_false(pcmk__xe_attr_is_true(NULL, "whatever"));
+ assert_false(pcmk__xe_attr_is_true(node, NULL));
+
+ free_xml(node);
+}
+
+static void
+attr_missing(void **state)
+{
+ xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+
+ assert_false(pcmk__xe_attr_is_true(node, "c"));
+ free_xml(node);
+}
+
+static void
+attr_present(void **state)
+{
+ xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+
+ assert_true(pcmk__xe_attr_is_true(node, "a"));
+ assert_false(pcmk__xe_attr_is_true(node, "b"));
+
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(attr_missing),
+ cmocka_unit_test(attr_present))
diff --git a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
new file mode 100644
index 0000000..500d8a6
--- /dev/null
+++ b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2021-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/xml_internal.h>
+
+static void
+empty_input(void **state)
+{
+ xmlNode *node = string2xml("<node/>");
+ bool value;
+
+ assert_int_equal(pcmk__xe_get_bool_attr(NULL, NULL, &value), ENODATA);
+ assert_int_equal(pcmk__xe_get_bool_attr(NULL, "whatever", &value), ENODATA);
+ assert_int_equal(pcmk__xe_get_bool_attr(node, NULL, &value), EINVAL);
+ assert_int_equal(pcmk__xe_get_bool_attr(node, "whatever", NULL), EINVAL);
+
+ free_xml(node);
+}
+
+static void
+attr_missing(void **state)
+{
+ xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ bool value;
+
+ assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), ENODATA);
+ free_xml(node);
+}
+
+static void
+attr_present(void **state)
+{
+ xmlNode *node = string2xml("<node a=\"true\" b=\"false\" c=\"blah\"/>");
+ bool value;
+
+ value = false;
+ assert_int_equal(pcmk__xe_get_bool_attr(node, "a", &value), pcmk_rc_ok);
+ assert_true(value);
+ value = true;
+ assert_int_equal(pcmk__xe_get_bool_attr(node, "b", &value), pcmk_rc_ok);
+ assert_false(value);
+ assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), pcmk_rc_bad_input);
+
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(attr_missing),
+ cmocka_unit_test(attr_present))
diff --git a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
new file mode 100644
index 0000000..e1fb9c4
--- /dev/null
+++ b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2021 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/xml_internal.h>
+#include <crm/msg_xml.h>
+
+static void
+set_attr(void **state)
+{
+ xmlNode *node = string2xml("<node/>");
+
+ pcmk__xe_set_bool_attr(node, "a", true);
+ pcmk__xe_set_bool_attr(node, "b", false);
+
+ assert_string_equal(crm_element_value(node, "a"), XML_BOOLEAN_TRUE);
+ assert_string_equal(crm_element_value(node, "b"), XML_BOOLEAN_FALSE);
+
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(set_attr))
diff --git a/lib/common/tests/operations/Makefile.am b/lib/common/tests/operations/Makefile.am
new file mode 100644
index 0000000..4687e1b
--- /dev/null
+++ b/lib/common/tests/operations/Makefile.am
@@ -0,0 +1,22 @@
+#
+# Copyright 2020-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 = copy_in_properties_test \
+ expand_plus_plus_test \
+ fix_plus_plus_recursive_test \
+ parse_op_key_test \
+ pcmk_is_probe_test \
+ pcmk_xe_is_probe_test \
+ pcmk_xe_mask_probe_failure_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/operations/copy_in_properties_test.c b/lib/common/tests/operations/copy_in_properties_test.c
new file mode 100644
index 0000000..7882551
--- /dev/null
+++ b/lib/common/tests/operations/copy_in_properties_test.c
@@ -0,0 +1,62 @@
+ /*
+ * 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 <glib.h>
+
+static void
+target_is_NULL(void **state)
+{
+ xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1");
+ xmlNode *test_xml_2 = NULL;
+
+ pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL);
+
+ copy_in_properties(test_xml_2, test_xml_1);
+
+ assert_ptr_equal(test_xml_2, NULL);
+}
+
+static void
+src_is_NULL(void **state)
+{
+ xmlNode *test_xml_1 = NULL;
+ xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2");
+
+ copy_in_properties(test_xml_2, test_xml_1);
+
+ assert_ptr_equal(test_xml_2->properties, NULL);
+}
+
+static void
+copying_is_successful(void **state)
+{
+ const char *xml_1_value;
+ const char *xml_2_value;
+
+ xmlNode *test_xml_1 = create_xml_node(NULL, "test_xml_1");
+ xmlNode *test_xml_2 = create_xml_node(NULL, "test_xml_2");
+
+ pcmk__xe_set_props(test_xml_1, "test_prop", "test_value", NULL);
+
+ copy_in_properties(test_xml_2, test_xml_1);
+
+ xml_1_value = crm_element_value(test_xml_1, "test_prop");
+ xml_2_value = crm_element_value(test_xml_2, "test_prop");
+
+ assert_string_equal(xml_1_value, xml_2_value);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(target_is_NULL),
+ cmocka_unit_test(src_is_NULL),
+ cmocka_unit_test(copying_is_successful))
diff --git a/lib/common/tests/operations/expand_plus_plus_test.c b/lib/common/tests/operations/expand_plus_plus_test.c
new file mode 100644
index 0000000..41471f9
--- /dev/null
+++ b/lib/common/tests/operations/expand_plus_plus_test.c
@@ -0,0 +1,256 @@
+/*
+ * 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 <glib.h>
+
+static void
+value_is_name_plus_plus(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+value_is_name_plus_equals_integer(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=2");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "7");
+}
+
+// NULL input
+
+static void
+target_is_NULL(void **state)
+{
+
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(NULL, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+static void
+name_is_NULL(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, NULL, "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+static void
+value_is_NULL(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", NULL);
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+// the value input doesn't start with the name input
+
+static void
+value_is_wrong_name(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "Y++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "Y++");
+}
+
+static void
+value_is_only_an_integer(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "2");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "2");
+}
+
+// non-integers
+
+static void
+variable_is_initialized_to_be_NULL(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", NULL);
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "X++");
+}
+
+static void
+variable_is_initialized_to_be_non_numeric(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "hello");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "1");
+}
+
+static void
+variable_is_initialized_to_be_non_numeric_2(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "hello");
+ expand_plus_plus(test_xml, "X", "X+=2");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "2");
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5.01");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5.50");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5.99");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+value_is_non_numeric(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=hello");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "5");
+}
+
+static void
+value_is_numeric_and_decimal_point_containing(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=2.01");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "7");
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_2(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=1.50");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_3(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=1.99");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "6");
+}
+
+// undefined input
+
+static void
+name_is_undefined(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "Y", "5");
+ expand_plus_plus(test_xml, "X", "X++");
+ new_value = crm_element_value(test_xml, "X");
+ assert_string_equal(new_value, "X++");
+}
+
+// large input
+
+static void
+assignment_result_is_too_large(void **state)
+{
+ const char *new_value;
+ xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ crm_xml_add(test_xml, "X", "5");
+ expand_plus_plus(test_xml, "X", "X+=100000000000");
+ new_value = crm_element_value(test_xml, "X");
+ printf("assignment result is too large %s\n", new_value);
+ assert_string_equal(new_value, "1000000");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(value_is_name_plus_plus),
+ cmocka_unit_test(value_is_name_plus_equals_integer),
+ cmocka_unit_test(target_is_NULL),
+ cmocka_unit_test(name_is_NULL),
+ cmocka_unit_test(value_is_NULL),
+ cmocka_unit_test(value_is_wrong_name),
+ cmocka_unit_test(value_is_only_an_integer),
+ cmocka_unit_test(variable_is_initialized_to_be_NULL),
+ cmocka_unit_test(variable_is_initialized_to_be_non_numeric),
+ cmocka_unit_test(variable_is_initialized_to_be_non_numeric_2),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_2),
+ cmocka_unit_test(variable_is_initialized_to_be_numeric_and_decimal_point_containing_3),
+ cmocka_unit_test(value_is_non_numeric),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing_2),
+ cmocka_unit_test(value_is_numeric_and_decimal_point_containing_3),
+ cmocka_unit_test(name_is_undefined),
+ cmocka_unit_test(assignment_result_is_too_large))
diff --git a/lib/common/tests/operations/fix_plus_plus_recursive_test.c b/lib/common/tests/operations/fix_plus_plus_recursive_test.c
new file mode 100644
index 0000000..b3c7cc2
--- /dev/null
+++ b/lib/common/tests/operations/fix_plus_plus_recursive_test.c
@@ -0,0 +1,47 @@
+/*
+ * 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 <glib.h>
+
+static void
+element_nodes(void **state)
+{
+ const char *new_value_root;
+ const char *new_value_child;
+ const char *new_value_grandchild;
+
+ xmlNode *test_xml_root = create_xml_node(NULL, "test_xml_root");
+ xmlNode *test_xml_child = create_xml_node(test_xml_root, "test_xml_child");
+ xmlNode *test_xml_grandchild = create_xml_node(test_xml_child, "test_xml_grandchild");
+ xmlNode *test_xml_text = pcmk_create_xml_text_node(test_xml_root, "text_xml_text", "content");
+ xmlNode *test_xml_comment = string2xml("<!-- a comment -->");
+
+ crm_xml_add(test_xml_root, "X", "5");
+ crm_xml_add(test_xml_child, "X", "X++");
+ crm_xml_add(test_xml_grandchild, "X", "X+=2");
+ crm_xml_add(test_xml_text, "X", "X++");
+
+ fix_plus_plus_recursive(test_xml_root);
+ fix_plus_plus_recursive(test_xml_comment);
+
+ new_value_root = crm_element_value(test_xml_root, "X");
+ new_value_child = crm_element_value(test_xml_child, "X");
+ new_value_grandchild = crm_element_value(test_xml_grandchild, "X");
+
+ assert_string_equal(new_value_root, "5");
+ assert_string_equal(new_value_child, "1");
+ assert_string_equal(new_value_grandchild, "2");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(element_nodes))
diff --git a/lib/common/tests/operations/parse_op_key_test.c b/lib/common/tests/operations/parse_op_key_test.c
new file mode 100644
index 0000000..1b1bfff
--- /dev/null
+++ b/lib/common/tests/operations/parse_op_key_test.c
@@ -0,0 +1,275 @@
+/*
+ * Copyright 2020-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 <glib.h>
+
+static void
+basic(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("Fencing_monitor_60000", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "Fencing");
+ assert_string_equal(ty, "monitor");
+ assert_int_equal(ms, 60000);
+ free(rsc);
+ free(ty);
+
+ // Single-character resource name
+ assert_true(parse_op_key("R_monitor_100000", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "R");
+ assert_string_equal(ty, "monitor");
+ assert_int_equal(ms, 100000);
+ free(rsc);
+ free(ty);
+
+ // Single-character action name
+ assert_true(parse_op_key("R_A_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "R");
+ assert_string_equal(ty, "A");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+}
+
+static void
+rsc_just_underbars(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("__monitor_1000", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "_");
+ assert_string_equal(ty, "monitor");
+ assert_int_equal(ms, 1000);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("___migrate_from_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "__");
+ assert_string_equal(ty, "migrate_from");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("____pre_notify_stop_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "___");
+ assert_string_equal(ty, "pre_notify_stop");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+}
+
+static void
+colon_in_rsc(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("ClusterIP:0_start_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "ClusterIP:0");
+ assert_string_equal(ty, "start");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("imagestoreclone:1_post_notify_stop_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "imagestoreclone:1");
+ assert_string_equal(ty, "post_notify_stop");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+}
+
+static void
+dashes_in_rsc(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("httpd-bundle-0_monitor_30000", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "httpd-bundle-0");
+ assert_string_equal(ty, "monitor");
+ assert_int_equal(ms, 30000);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("httpd-bundle-ip-192.168.122.132_start_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "httpd-bundle-ip-192.168.122.132");
+ assert_string_equal(ty, "start");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+}
+
+static void
+migrate_to_from(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("vm_migrate_from_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "vm");
+ assert_string_equal(ty, "migrate_from");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("vm_migrate_to_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "vm");
+ assert_string_equal(ty, "migrate_to");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("vm_idcc_devel_migrate_to_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "vm_idcc_devel");
+ assert_string_equal(ty, "migrate_to");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+}
+
+static void
+pre_post(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("rsc_drbd_7788:1_post_notify_start_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "rsc_drbd_7788:1");
+ assert_string_equal(ty, "post_notify_start");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("rabbitmq-bundle-clone_pre_notify_stop_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "rabbitmq-bundle-clone");
+ assert_string_equal(ty, "pre_notify_stop");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("post_notify_start_0", &rsc, &ty, &ms));
+ assert_string_equal(rsc, "post_notify");
+ assert_string_equal(ty, "start");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+
+ assert_true(parse_op_key("r_confirmed-post_notify_start_0",
+ &rsc, &ty, &ms));
+ assert_string_equal(rsc, "r");
+ assert_string_equal(ty, "confirmed-post_notify_start");
+ assert_int_equal(ms, 0);
+ free(rsc);
+ free(ty);
+}
+
+static void
+skip_rsc(void **state)
+{
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("Fencing_monitor_60000", NULL, &ty, &ms));
+ assert_string_equal(ty, "monitor");
+ assert_int_equal(ms, 60000);
+ free(ty);
+}
+
+static void
+skip_ty(void **state)
+{
+ char *rsc = NULL;
+ guint ms = 0;
+
+ assert_true(parse_op_key("Fencing_monitor_60000", &rsc, NULL, &ms));
+ assert_string_equal(rsc, "Fencing");
+ assert_int_equal(ms, 60000);
+ free(rsc);
+}
+
+static void
+skip_ms(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+
+ assert_true(parse_op_key("Fencing_monitor_60000", &rsc, &ty, NULL));
+ assert_string_equal(rsc, "Fencing");
+ assert_string_equal(ty, "monitor");
+ free(rsc);
+ free(ty);
+}
+
+static void
+empty_input(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_false(parse_op_key("", &rsc, &ty, &ms));
+ assert_null(rsc);
+ assert_null(ty);
+ assert_int_equal(ms, 0);
+
+ assert_false(parse_op_key(NULL, &rsc, &ty, &ms));
+ assert_null(rsc);
+ assert_null(ty);
+ assert_int_equal(ms, 0);
+}
+
+static void
+malformed_input(void **state)
+{
+ char *rsc = NULL;
+ char *ty = NULL;
+ guint ms = 0;
+
+ assert_false(parse_op_key("httpd-bundle-0", &rsc, &ty, &ms));
+ assert_null(rsc);
+ assert_null(ty);
+ assert_int_equal(ms, 0);
+
+ assert_false(parse_op_key("httpd-bundle-0_monitor", &rsc, &ty, &ms));
+ assert_null(rsc);
+ assert_null(ty);
+ assert_int_equal(ms, 0);
+
+ assert_false(parse_op_key("httpd-bundle-0_30000", &rsc, &ty, &ms));
+ assert_null(rsc);
+ assert_null(ty);
+ assert_int_equal(ms, 0);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(basic),
+ cmocka_unit_test(rsc_just_underbars),
+ cmocka_unit_test(colon_in_rsc),
+ cmocka_unit_test(dashes_in_rsc),
+ cmocka_unit_test(migrate_to_from),
+ cmocka_unit_test(pre_post),
+ cmocka_unit_test(skip_rsc),
+ cmocka_unit_test(skip_ty),
+ cmocka_unit_test(skip_ms),
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(malformed_input))
diff --git a/lib/common/tests/operations/pcmk_is_probe_test.c b/lib/common/tests/operations/pcmk_is_probe_test.c
new file mode 100644
index 0000000..4a65e3f
--- /dev/null
+++ b/lib/common/tests/operations/pcmk_is_probe_test.c
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2021 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>
+
+static void
+is_probe_test(void **state)
+{
+ assert_false(pcmk_is_probe(NULL, 0));
+ assert_false(pcmk_is_probe("", 0));
+ assert_false(pcmk_is_probe("blahblah", 0));
+ assert_false(pcmk_is_probe("monitor", 1));
+ assert_true(pcmk_is_probe("monitor", 0));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(is_probe_test))
diff --git a/lib/common/tests/operations/pcmk_xe_is_probe_test.c b/lib/common/tests/operations/pcmk_xe_is_probe_test.c
new file mode 100644
index 0000000..62b21d9
--- /dev/null
+++ b/lib/common/tests/operations/pcmk_xe_is_probe_test.c
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2021 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>
+
+static void
+op_is_probe_test(void **state)
+{
+ xmlNode *node = NULL;
+
+ assert_false(pcmk_xe_is_probe(NULL));
+
+ node = string2xml("<lrm_rsc_op/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation_key=\"blah\" interval=\"30s\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"30s\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"start\" interval=\"0\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\"/>");
+ assert_true(pcmk_xe_is_probe(node));
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(op_is_probe_test))
diff --git a/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c
new file mode 100644
index 0000000..9e38019
--- /dev/null
+++ b/lib/common/tests/operations/pcmk_xe_mask_probe_failure_test.c
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2021 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>
+
+static void
+op_is_not_probe_test(void **state) {
+ xmlNode *node = NULL;
+
+ /* Not worth testing this thoroughly since it's just a duplicate of whether
+ * pcmk_op_is_probe works or not.
+ */
+
+ node = string2xml("<lrm_rsc_op operation=\"start\" interval=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+}
+
+static void
+op_does_not_have_right_values_test(void **state) {
+ xmlNode *node = NULL;
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+}
+
+static void
+check_values_test(void **state) {
+ xmlNode *node = NULL;
+
+ /* PCMK_EXEC_NOT_SUPPORTED */
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"3\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"3\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ /* PCMK_EXEC_DONE */
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"0\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"0\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ /* PCMK_EXEC_NOT_INSTALLED */
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"7\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"7\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR */
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"4\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"4\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"4\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"4\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"4\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR_HARD */
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"5\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"5\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"5\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"5\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"5\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR_FATAL */
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"0\" op-status=\"6\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"2\" op-status=\"6\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"5\" op-status=\"6\"/>");
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"6\" op-status=\"6\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ node = string2xml("<lrm_rsc_op operation=\"monitor\" interval=\"0\" rc-code=\"7\" op-status=\"6\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(op_is_not_probe_test),
+ cmocka_unit_test(op_does_not_have_right_values_test),
+ cmocka_unit_test(check_values_test))
diff --git a/lib/common/tests/options/Makefile.am b/lib/common/tests/options/Makefile.am
new file mode 100644
index 0000000..9a5fa98
--- /dev/null
+++ b/lib/common/tests/options/Makefile.am
@@ -0,0 +1,19 @@
+#
+# 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 $(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__env_option_test \
+ pcmk__set_env_option_test \
+ pcmk__env_option_enabled_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/options/pcmk__env_option_enabled_test.c b/lib/common/tests/options/pcmk__env_option_enabled_test.c
new file mode 100644
index 0000000..b7d5d25
--- /dev/null
+++ b/lib/common/tests/options/pcmk__env_option_enabled_test.c
@@ -0,0 +1,101 @@
+/*
+ * 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 "mock_private.h"
+
+static void
+disabled_null_value(void **state)
+{
+ // Return false if option value not found (NULL accomplishes this)
+ assert_false(pcmk__env_option_enabled(NULL, NULL));
+ assert_false(pcmk__env_option_enabled("pacemaker-execd", NULL));
+}
+
+static void
+enabled_true_value(void **state)
+{
+ // Return true if option value is true, with or without daemon name
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "true");
+ assert_true(pcmk__env_option_enabled(NULL, "env_var"));
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "true");
+ assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+disabled_false_value(void **state)
+{
+ // Return false if option value is false (no daemon list)
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "false");
+ assert_false(pcmk__env_option_enabled(NULL, "env_var"));
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "false");
+ assert_false(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+enabled_daemon_in_list(void **state)
+{
+ // Return true if daemon is in the option's value
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "pacemaker-execd");
+ assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "pacemaker-execd,pacemaker-fenced");
+ assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "pacemaker-controld,pacemaker-execd");
+ assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv,
+ "pacemaker-controld,pacemaker-execd,pacemaker-fenced");
+ assert_true(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+disabled_daemon_not_in_list(void **state)
+{
+ // Return false if value is not true and daemon is not in the option's value
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "pacemaker-controld,pacemaker-fenced");
+ assert_false(pcmk__env_option_enabled("pacemaker-execd", "env_var"));
+
+ pcmk__mock_getenv = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(disabled_null_value),
+ cmocka_unit_test(enabled_true_value),
+ cmocka_unit_test(disabled_false_value),
+ cmocka_unit_test(enabled_daemon_in_list),
+ cmocka_unit_test(disabled_daemon_not_in_list))
diff --git a/lib/common/tests/options/pcmk__env_option_test.c b/lib/common/tests/options/pcmk__env_option_test.c
new file mode 100644
index 0000000..2b85b68
--- /dev/null
+++ b/lib/common/tests/options/pcmk__env_option_test.c
@@ -0,0 +1,134 @@
+/*
+ * 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 "mock_private.h"
+
+static void
+empty_input_string(void **state)
+{
+ pcmk__mock_getenv = true;
+
+ // getenv() not called
+ assert_null(pcmk__env_option(NULL));
+ assert_null(pcmk__env_option(""));
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+input_too_long_for_both(void **state)
+{
+ /* pcmk__env_option() prepends "PCMK_" before lookup. If the option name is
+ * too long for the buffer or isn't found in the env, then it prepends "HA_"
+ * and tries again. A string of length (NAME_MAX - 3) will set us just over
+ * just over the edge for both tries.
+ */
+ char long_opt[NAME_MAX - 2];
+
+ for (int i = 0; i < NAME_MAX - 3; i++) {
+ long_opt[i] = 'a';
+ }
+ long_opt[NAME_MAX - 3] = '\0';
+
+ pcmk__mock_getenv = true;
+
+ // getenv() not called
+ assert_null(pcmk__env_option(long_opt));
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+input_too_long_for_pcmk(void **state)
+{
+ /* If an input is too long for "PCMK_<option>", make sure we fall through
+ * to try "HA_<option>".
+ *
+ * pcmk__env_option() prepends "PCMK_" first. A string of length
+ * (NAME_MAX - 5) will set us just over the edge, still short enough for
+ * "HA_<option>" to fit.
+ */
+ char long_opt[NAME_MAX - 4];
+ char buf[NAME_MAX];
+
+ for (int i = 0; i < NAME_MAX - 5; i++) {
+ long_opt[i] = 'a';
+ }
+ long_opt[NAME_MAX - 5] = '\0';
+
+ pcmk__mock_getenv = true;
+
+ /* NULL/non-NULL retval doesn't really matter here; just testing that we
+ * call getenv() for "HA_" prefix after too long for "PCMK_".
+ */
+ snprintf(buf, NAME_MAX, "HA_%s", long_opt);
+ expect_string(__wrap_getenv, name, buf);
+ will_return(__wrap_getenv, "value");
+ assert_string_equal(pcmk__env_option(long_opt), "value");
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+value_not_found(void **state)
+{
+ // Value not found using PCMK_ or HA_ prefix. Should return NULL.
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, NULL);
+
+ expect_string(__wrap_getenv, name, "HA_env_var");
+ will_return(__wrap_getenv, NULL);
+
+ assert_null(pcmk__env_option("env_var"));
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+value_found_pcmk(void **state)
+{
+ // Value found using PCMK_. Should return value and skip HA_ lookup.
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, "value");
+ assert_string_equal(pcmk__env_option("env_var"), "value");
+
+ pcmk__mock_getenv = false;
+}
+
+static void
+value_found_ha(void **state)
+{
+ // Value not found using PCMK_. Move on to HA_ lookup, find, and return.
+ pcmk__mock_getenv = true;
+
+ expect_string(__wrap_getenv, name, "PCMK_env_var");
+ will_return(__wrap_getenv, NULL);
+
+ expect_string(__wrap_getenv, name, "HA_env_var");
+ will_return(__wrap_getenv, "value");
+
+ assert_string_equal(pcmk__env_option("env_var"), "value");
+
+ pcmk__mock_getenv = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_string),
+ cmocka_unit_test(input_too_long_for_both),
+ cmocka_unit_test(input_too_long_for_pcmk),
+ cmocka_unit_test(value_not_found),
+ cmocka_unit_test(value_found_pcmk),
+ cmocka_unit_test(value_found_ha))
diff --git a/lib/common/tests/options/pcmk__set_env_option_test.c b/lib/common/tests/options/pcmk__set_env_option_test.c
new file mode 100644
index 0000000..753bf74
--- /dev/null
+++ b/lib/common/tests/options/pcmk__set_env_option_test.c
@@ -0,0 +1,154 @@
+/*
+ * 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 "mock_private.h"
+
+static void
+bad_input_string(void **state)
+{
+ // Bad setenv()/unsetenv() input: NULL, empty, or containing '='
+
+ // Never call setenv()
+ pcmk__mock_setenv = true;
+
+ pcmk__set_env_option(NULL, "new_value");
+ pcmk__set_env_option("", "new_value");
+ pcmk__set_env_option("name=val", "new_value");
+
+ pcmk__mock_setenv = false;
+
+ // Never call unsetenv()
+ pcmk__mock_unsetenv = true;
+
+ pcmk__set_env_option(NULL, NULL);
+ pcmk__set_env_option("", NULL);
+ pcmk__set_env_option("name=val", NULL);
+
+ pcmk__mock_unsetenv = false;
+}
+
+static void
+input_too_long_for_both(void **state)
+{
+ /* pcmk__set_env_option() wants to set "PCMK_<option>" and "HA_<option>". If
+ * "PCMK_<option>" is too long for the buffer, it simply moves on to
+ * "HA_<option>". A string of length (NAME_MAX - 3) will set us just over
+ * the edge for both tries.
+ */
+ char long_opt[NAME_MAX - 2];
+
+ for (int i = 0; i < NAME_MAX - 3; i++) {
+ long_opt[i] = 'a';
+ }
+ long_opt[NAME_MAX - 3] = '\0';
+
+ // Never call setenv() or unsetenv()
+ pcmk__mock_setenv = true;
+ pcmk__set_env_option(long_opt, "new_value");
+ pcmk__mock_setenv = false;
+
+ pcmk__mock_unsetenv = true;
+ pcmk__set_env_option(long_opt, NULL);
+ pcmk__mock_unsetenv = false;
+}
+
+static void
+input_too_long_for_pcmk(void **state)
+{
+ /* If an input is too long to set "PCMK_<option>", make sure we fall through
+ * to try to set "HA_<option>".
+ *
+ * A string of length (NAME_MAX - 5) will set us just over the edge for
+ * "PCMK_<option>", while still short enough for "HA_<option>" to fit.
+ */
+ char long_opt[NAME_MAX - 4];
+ char buf[NAME_MAX];
+
+ for (int i = 0; i < NAME_MAX - 5; i++) {
+ long_opt[i] = 'a';
+ }
+ long_opt[NAME_MAX - 5] = '\0';
+
+ snprintf(buf, NAME_MAX, "HA_%s", long_opt);
+
+ // Call setenv() for "HA_" only
+ pcmk__mock_setenv = true;
+
+ expect_string(__wrap_setenv, name, buf);
+ expect_string(__wrap_setenv, value, "new_value");
+ expect_value(__wrap_setenv, overwrite, 1);
+ will_return(__wrap_setenv, 0);
+ pcmk__set_env_option(long_opt, "new_value");
+
+ pcmk__mock_setenv = false;
+
+ // Call unsetenv() for "HA_" only
+ pcmk__mock_unsetenv = true;
+
+ expect_string(__wrap_unsetenv, name, buf);
+ will_return(__wrap_unsetenv, 0);
+ pcmk__set_env_option(long_opt, NULL);
+
+ pcmk__mock_unsetenv = false;
+}
+
+static void
+valid_inputs_set(void **state)
+{
+ // Make sure we set "PCMK_<option>" and "HA_<option>"
+ pcmk__mock_setenv = true;
+
+ expect_string(__wrap_setenv, name, "PCMK_env_var");
+ expect_string(__wrap_setenv, value, "new_value");
+ expect_value(__wrap_setenv, overwrite, 1);
+ will_return(__wrap_setenv, 0);
+ expect_string(__wrap_setenv, name, "HA_env_var");
+ expect_string(__wrap_setenv, value, "new_value");
+ expect_value(__wrap_setenv, overwrite, 1);
+ will_return(__wrap_setenv, 0);
+ pcmk__set_env_option("env_var", "new_value");
+
+ // Empty string is also a valid value
+ expect_string(__wrap_setenv, name, "PCMK_env_var");
+ expect_string(__wrap_setenv, value, "");
+ expect_value(__wrap_setenv, overwrite, 1);
+ will_return(__wrap_setenv, 0);
+ expect_string(__wrap_setenv, name, "HA_env_var");
+ expect_string(__wrap_setenv, value, "");
+ expect_value(__wrap_setenv, overwrite, 1);
+ will_return(__wrap_setenv, 0);
+ pcmk__set_env_option("env_var", "");
+
+ pcmk__mock_setenv = false;
+}
+
+static void
+valid_inputs_unset(void **state)
+{
+ // Make sure we unset "PCMK_<option>" and "HA_<option>"
+ pcmk__mock_unsetenv = true;
+
+ expect_string(__wrap_unsetenv, name, "PCMK_env_var");
+ will_return(__wrap_unsetenv, 0);
+ expect_string(__wrap_unsetenv, name, "HA_env_var");
+ will_return(__wrap_unsetenv, 0);
+ pcmk__set_env_option("env_var", NULL);
+
+ pcmk__mock_unsetenv = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input_string),
+ cmocka_unit_test(input_too_long_for_both),
+ cmocka_unit_test(input_too_long_for_pcmk),
+ cmocka_unit_test(valid_inputs_set),
+ cmocka_unit_test(valid_inputs_unset))
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))
diff --git a/lib/common/tests/procfs/Makefile.am b/lib/common/tests/procfs/Makefile.am
new file mode 100644
index 0000000..75511f5
--- /dev/null
+++ b/lib/common/tests/procfs/Makefile.am
@@ -0,0 +1,18 @@
+#
+# 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 $(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__procfs_has_pids_false_test \
+ pcmk__procfs_has_pids_true_test \
+ pcmk__procfs_pid2path_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c b/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c
new file mode 100644
index 0000000..4601aac
--- /dev/null
+++ b/lib/common/tests/procfs/pcmk__procfs_has_pids_false_test.c
@@ -0,0 +1,42 @@
+/*
+ * 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 "mock_private.h"
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+
+static void
+no_pids(void **state)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, PATH_MAX, "/proc/%u/exe", getpid());
+
+ // Set readlink() errno and link contents (for /proc/PID/exe)
+ pcmk__mock_readlink = true;
+
+ expect_string(__wrap_readlink, path, path);
+ expect_any(__wrap_readlink, buf);
+ expect_value(__wrap_readlink, bufsize, PATH_MAX - 1);
+ will_return(__wrap_readlink, ENOENT);
+ will_return(__wrap_readlink, NULL);
+
+ assert_false(pcmk__procfs_has_pids());
+
+ pcmk__mock_readlink = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(no_pids))
diff --git a/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c b/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c
new file mode 100644
index 0000000..758e3b9
--- /dev/null
+++ b/lib/common/tests/procfs/pcmk__procfs_has_pids_true_test.c
@@ -0,0 +1,41 @@
+/*
+ * 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 "mock_private.h"
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+static void
+has_pids(void **state)
+{
+ char path[PATH_MAX];
+
+ snprintf(path, PATH_MAX, "/proc/%u/exe", getpid());
+
+ // Set readlink() errno and link contents (for /proc/PID/exe)
+ pcmk__mock_readlink = true;
+
+ expect_string(__wrap_readlink, path, path);
+ expect_any(__wrap_readlink, buf);
+ expect_value(__wrap_readlink, bufsize, PATH_MAX - 1);
+ will_return(__wrap_readlink, 0);
+ will_return(__wrap_readlink, "/ok");
+
+ assert_true(pcmk__procfs_has_pids());
+
+ pcmk__mock_readlink = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL, cmocka_unit_test(has_pids))
diff --git a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
new file mode 100644
index 0000000..2bae541
--- /dev/null
+++ b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
@@ -0,0 +1,92 @@
+/*
+ * 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 "mock_private.h"
+
+#include <unistd.h>
+#include <string.h>
+#include <errno.h>
+
+static void
+no_exe_file(void **state)
+{
+ size_t len = PATH_MAX;
+ char *path = calloc(len, sizeof(char));
+
+ // Set readlink() errno and link contents
+ pcmk__mock_readlink = true;
+
+ expect_string(__wrap_readlink, path, "/proc/1000/exe");
+ expect_value(__wrap_readlink, buf, path);
+ expect_value(__wrap_readlink, bufsize, len - 1);
+ will_return(__wrap_readlink, ENOENT);
+ will_return(__wrap_readlink, NULL);
+
+ assert_int_equal(pcmk__procfs_pid2path(1000, path, len), ENOENT);
+
+ pcmk__mock_readlink = false;
+
+ free(path);
+}
+
+static void
+contents_too_long(void **state)
+{
+ size_t len = 10;
+ char *path = calloc(len, sizeof(char));
+
+ // Set readlink() errno and link contents
+ pcmk__mock_readlink = true;
+
+ expect_string(__wrap_readlink, path, "/proc/1000/exe");
+ expect_value(__wrap_readlink, buf, path);
+ expect_value(__wrap_readlink, bufsize, len - 1);
+ will_return(__wrap_readlink, 0);
+ will_return(__wrap_readlink, "/more/than/10/characters");
+
+ assert_int_equal(pcmk__procfs_pid2path(1000, path, len),
+ ENAMETOOLONG);
+
+ pcmk__mock_readlink = false;
+
+ free(path);
+}
+
+static void
+contents_ok(void **state)
+{
+ size_t len = PATH_MAX;
+ char *path = calloc(len, sizeof(char));
+
+ // Set readlink() errno and link contents
+ pcmk__mock_readlink = true;
+
+ expect_string(__wrap_readlink, path, "/proc/1000/exe");
+ expect_value(__wrap_readlink, buf, path);
+ expect_value(__wrap_readlink, bufsize, len - 1);
+ will_return(__wrap_readlink, 0);
+ will_return(__wrap_readlink, "/ok");
+
+ assert_int_equal(pcmk__procfs_pid2path((pid_t) 1000, path, len),
+ pcmk_rc_ok);
+ assert_string_equal(path, "/ok");
+
+ pcmk__mock_readlink = false;
+
+ free(path);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(no_exe_file),
+ cmocka_unit_test(contents_too_long),
+ cmocka_unit_test(contents_ok))
diff --git a/lib/common/tests/results/Makefile.am b/lib/common/tests/results/Makefile.am
new file mode 100644
index 0000000..8d51d12
--- /dev/null
+++ b/lib/common/tests/results/Makefile.am
@@ -0,0 +1,16 @@
+#
+# 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__results_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/results/pcmk__results_test.c b/lib/common/tests/results/pcmk__results_test.c
new file mode 100644
index 0000000..53665d1
--- /dev/null
+++ b/lib/common/tests/results/pcmk__results_test.c
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2020-2021 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 <glib.h>
+#include <bzlib.h>
+
+static void
+test_for_pcmk_rc_name(void **state) {
+ assert_string_equal(pcmk_rc_name(pcmk_rc_error-1), "pcmk_rc_unknown_format");
+ assert_string_equal(pcmk_rc_name(pcmk_rc_ok), "pcmk_rc_ok");
+ assert_string_equal(pcmk_rc_name(pcmk_rc_ok), "pcmk_rc_ok");
+ assert_string_equal(pcmk_rc_name(-7777777), "Unknown");
+}
+
+static void
+test_for_pcmk_rc_str(void **state) {
+ assert_string_equal(pcmk_rc_str(pcmk_rc_error-1), "Unknown output format");
+ assert_string_equal(pcmk_rc_str(pcmk_rc_ok), "OK");
+ assert_string_equal(pcmk_rc_str(-1), "Error");
+}
+
+static void
+test_for_crm_exit_name(void **state) {
+ assert_string_equal(crm_exit_name(CRM_EX_OK), "CRM_EX_OK");
+}
+
+static void
+test_for_crm_exit_str(void **state) {
+ assert_string_equal(crm_exit_str(CRM_EX_OK), "OK");
+ assert_string_equal(crm_exit_str(129), "Interrupted by signal");
+ assert_string_equal(crm_exit_str(-7777777), "Unknown exit status");
+}
+
+static void
+test_for_pcmk_rc2exitc(void **state) {
+ assert_int_equal(pcmk_rc2exitc(pcmk_rc_ok), CRM_EX_OK);
+ assert_int_equal(pcmk_rc2exitc(-7777777), CRM_EX_ERROR);
+}
+
+static void
+test_for_bz2_strerror(void **state) {
+ assert_string_equal(bz2_strerror(BZ_STREAM_END), "Ok");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(test_for_pcmk_rc_name),
+ cmocka_unit_test(test_for_pcmk_rc_str),
+ cmocka_unit_test(test_for_crm_exit_name),
+ cmocka_unit_test(test_for_crm_exit_str),
+ cmocka_unit_test(test_for_pcmk_rc2exitc),
+ cmocka_unit_test(test_for_bz2_strerror))
diff --git a/lib/common/tests/scores/Makefile.am b/lib/common/tests/scores/Makefile.am
new file mode 100644
index 0000000..66ca073
--- /dev/null
+++ b/lib/common/tests/scores/Makefile.am
@@ -0,0 +1,19 @@
+#
+# Copyright 2020-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 = \
+ char2score_test \
+ pcmk__add_scores_test \
+ pcmk_readable_score_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/scores/char2score_test.c b/lib/common/tests/scores/char2score_test.c
new file mode 100644
index 0000000..fbba12a
--- /dev/null
+++ b/lib/common/tests/scores/char2score_test.c
@@ -0,0 +1,75 @@
+/*
+ * 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>
+
+extern int pcmk__score_red;
+extern int pcmk__score_green;
+extern int pcmk__score_yellow;
+
+static void
+empty_input(void **state)
+{
+ assert_int_equal(char2score(NULL), 0);
+}
+
+static void
+bad_input(void **state)
+{
+ assert_int_equal(char2score("PQRST"), 0);
+ assert_int_equal(char2score("3.141592"), 3);
+ assert_int_equal(char2score("0xf00d"), 0);
+}
+
+static void
+special_values(void **state)
+{
+ assert_int_equal(char2score("-INFINITY"), -CRM_SCORE_INFINITY);
+ assert_int_equal(char2score("INFINITY"), CRM_SCORE_INFINITY);
+ assert_int_equal(char2score("+INFINITY"), CRM_SCORE_INFINITY);
+
+ pcmk__score_red = 10;
+ pcmk__score_green = 20;
+ pcmk__score_yellow = 30;
+
+ assert_int_equal(char2score("red"), pcmk__score_red);
+ assert_int_equal(char2score("green"), pcmk__score_green);
+ assert_int_equal(char2score("yellow"), pcmk__score_yellow);
+
+ assert_int_equal(char2score("ReD"), pcmk__score_red);
+ assert_int_equal(char2score("GrEeN"), pcmk__score_green);
+ assert_int_equal(char2score("yElLoW"), pcmk__score_yellow);
+}
+
+/* These ridiculous macros turn an integer constant into a string constant. */
+#define A(x) #x
+#define B(x) A(x)
+
+static void
+outside_limits(void **state)
+{
+ assert_int_equal(char2score(B(CRM_SCORE_INFINITY) "00"), CRM_SCORE_INFINITY);
+ assert_int_equal(char2score("-" B(CRM_SCORE_INFINITY) "00"), -CRM_SCORE_INFINITY);
+}
+
+static void
+inside_limits(void **state)
+{
+ assert_int_equal(char2score("1234"), 1234);
+ assert_int_equal(char2score("-1234"), -1234);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(special_values),
+ cmocka_unit_test(outside_limits),
+ cmocka_unit_test(inside_limits))
diff --git a/lib/common/tests/scores/pcmk__add_scores_test.c b/lib/common/tests/scores/pcmk__add_scores_test.c
new file mode 100644
index 0000000..85ac232
--- /dev/null
+++ b/lib/common/tests/scores/pcmk__add_scores_test.c
@@ -0,0 +1,74 @@
+/*
+ * 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>
+
+static void
+score1_minus_inf(void **state)
+{
+ assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, -1), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, 0), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, 1), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY, CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+}
+
+static void
+score2_minus_inf(void **state)
+{
+ assert_int_equal(pcmk__add_scores(-1, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(0, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(1, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, -CRM_SCORE_INFINITY), -CRM_SCORE_INFINITY);
+}
+
+static void
+score1_pos_inf(void **state)
+{
+ assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, -1), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, 0), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(CRM_SCORE_INFINITY, 1), CRM_SCORE_INFINITY);
+}
+
+static void
+score2_pos_inf(void **state)
+{
+ assert_int_equal(pcmk__add_scores(-1, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(0, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(1, CRM_SCORE_INFINITY), CRM_SCORE_INFINITY);
+}
+
+static void
+result_infinite(void **state)
+{
+ assert_int_equal(pcmk__add_scores(INT_MAX, INT_MAX), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(INT_MIN, INT_MIN), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(2000000, 50), CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-4000000, 50), -CRM_SCORE_INFINITY);
+}
+
+static void
+result_finite(void **state)
+{
+ assert_int_equal(pcmk__add_scores(0, 0), 0);
+ assert_int_equal(pcmk__add_scores(0, 100), 100);
+ assert_int_equal(pcmk__add_scores(200, 0), 200);
+ assert_int_equal(pcmk__add_scores(200, -50), 150);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(score1_minus_inf),
+ cmocka_unit_test(score2_minus_inf),
+ cmocka_unit_test(score1_pos_inf),
+ cmocka_unit_test(score2_pos_inf),
+ cmocka_unit_test(result_infinite),
+ cmocka_unit_test(result_finite))
diff --git a/lib/common/tests/scores/pcmk_readable_score_test.c b/lib/common/tests/scores/pcmk_readable_score_test.c
new file mode 100644
index 0000000..ae24159
--- /dev/null
+++ b/lib/common/tests/scores/pcmk_readable_score_test.c
@@ -0,0 +1,33 @@
+/*
+ * 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>
+
+static void
+outside_limits(void **state)
+{
+ assert_string_equal(pcmk_readable_score(CRM_SCORE_INFINITY * 2),
+ CRM_INFINITY_S);
+ assert_string_equal(pcmk_readable_score(-CRM_SCORE_INFINITY * 2),
+ CRM_MINUS_INFINITY_S);
+}
+
+static void
+inside_limits(void **state)
+{
+ assert_string_equal(pcmk_readable_score(0), "0");
+ assert_string_equal(pcmk_readable_score(1024), "1024");
+ assert_string_equal(pcmk_readable_score(-1024), "-1024");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(outside_limits),
+ cmocka_unit_test(inside_limits))
diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am
new file mode 100644
index 0000000..9abb8e9
--- /dev/null
+++ b/lib/common/tests/strings/Makefile.am
@@ -0,0 +1,41 @@
+#
+# Copyright 2020-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 = \
+ crm_get_msec_test \
+ crm_is_true_test \
+ crm_str_to_boolean_test \
+ pcmk__add_word_test \
+ pcmk__btoa_test \
+ pcmk__char_in_any_str_test \
+ pcmk__compress_test \
+ pcmk__ends_with_test \
+ pcmk__g_strcat_test \
+ pcmk__guint_from_hash_test \
+ pcmk__numeric_strcasecmp_test \
+ pcmk__parse_ll_range_test \
+ pcmk__s_test \
+ pcmk__scan_double_test \
+ pcmk__scan_min_int_test \
+ pcmk__scan_port_test \
+ pcmk__starts_with_test \
+ pcmk__str_any_of_test \
+ pcmk__str_in_list_test \
+ pcmk__str_table_dup_test \
+ pcmk__str_update_test \
+ pcmk__strcmp_test \
+ pcmk__strkey_table_test \
+ pcmk__strikey_table_test \
+ pcmk__trim_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/strings/crm_get_msec_test.c b/lib/common/tests/strings/crm_get_msec_test.c
new file mode 100644
index 0000000..5da548b
--- /dev/null
+++ b/lib/common/tests/strings/crm_get_msec_test.c
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2021 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>
+
+static void
+bad_input(void **state) {
+ assert_int_equal(crm_get_msec(NULL), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec(" "), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec("abcxyz"), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec("100xs"), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec(" 100 xs "), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec("-100ms"), PCMK__PARSE_INT_DEFAULT);
+}
+
+static void
+good_input(void **state) {
+ assert_int_equal(crm_get_msec("100"), 100000);
+ assert_int_equal(crm_get_msec(" 100 "), 100000);
+ assert_int_equal(crm_get_msec("\t100\n"), 100000);
+
+ assert_int_equal(crm_get_msec("100ms"), 100);
+ assert_int_equal(crm_get_msec("100 MSEC"), 100);
+ assert_int_equal(crm_get_msec("1000US"), 1);
+ assert_int_equal(crm_get_msec("1000usec"), 1);
+ assert_int_equal(crm_get_msec("12s"), 12000);
+ assert_int_equal(crm_get_msec("12 sec"), 12000);
+ assert_int_equal(crm_get_msec("1m"), 60000);
+ assert_int_equal(crm_get_msec("13 min"), 780000);
+ assert_int_equal(crm_get_msec("2\th"), 7200000);
+ assert_int_equal(crm_get_msec("1 hr"), 3600000);
+}
+
+static void
+overflow(void **state) {
+ assert_int_equal(crm_get_msec("9223372036854775807s"), LLONG_MAX);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(good_input),
+ cmocka_unit_test(overflow))
diff --git a/lib/common/tests/strings/crm_is_true_test.c b/lib/common/tests/strings/crm_is_true_test.c
new file mode 100644
index 0000000..2a9e31c
--- /dev/null
+++ b/lib/common/tests/strings/crm_is_true_test.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 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>
+
+static void
+bad_input(void **state) {
+ assert_false(crm_is_true(NULL));
+}
+
+static void
+is_true(void **state) {
+ assert_true(crm_is_true("true"));
+ assert_true(crm_is_true("TrUe"));
+ assert_true(crm_is_true("on"));
+ assert_true(crm_is_true("ON"));
+ assert_true(crm_is_true("yes"));
+ assert_true(crm_is_true("yES"));
+ assert_true(crm_is_true("y"));
+ assert_true(crm_is_true("Y"));
+ assert_true(crm_is_true("1"));
+}
+
+static void
+is_false(void **state) {
+ assert_false(crm_is_true("false"));
+ assert_false(crm_is_true("fAlSe"));
+ assert_false(crm_is_true("off"));
+ assert_false(crm_is_true("OFF"));
+ assert_false(crm_is_true("no"));
+ assert_false(crm_is_true("No"));
+ assert_false(crm_is_true("n"));
+ assert_false(crm_is_true("N"));
+ assert_false(crm_is_true("0"));
+
+ assert_false(crm_is_true(""));
+ assert_false(crm_is_true("blahblah"));
+
+ assert_false(crm_is_true("truedat"));
+ assert_false(crm_is_true("onnn"));
+ assert_false(crm_is_true("yep"));
+ assert_false(crm_is_true("Y!"));
+ assert_false(crm_is_true("100"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(is_true),
+ cmocka_unit_test(is_false))
diff --git a/lib/common/tests/strings/crm_str_to_boolean_test.c b/lib/common/tests/strings/crm_str_to_boolean_test.c
new file mode 100644
index 0000000..3bd2e5d
--- /dev/null
+++ b/lib/common/tests/strings/crm_str_to_boolean_test.c
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2021 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>
+
+static void
+bad_input(void **state) {
+ assert_int_equal(crm_str_to_boolean(NULL, NULL), -1);
+ assert_int_equal(crm_str_to_boolean("", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("blahblah", NULL), -1);
+}
+
+static void
+is_true(void **state) {
+ int ret;
+
+ assert_int_equal(crm_str_to_boolean("true", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("TrUe", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("on", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("ON", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("yes", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("yES", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("y", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("Y", &ret), 1);
+ assert_true(ret);
+ assert_int_equal(crm_str_to_boolean("1", &ret), 1);
+ assert_true(ret);
+}
+
+static void
+is_not_true(void **state) {
+ assert_int_equal(crm_str_to_boolean("truedat", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("onnn", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("yep", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("Y!", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("100", NULL), -1);
+}
+
+static void
+is_false(void **state) {
+ int ret;
+
+ assert_int_equal(crm_str_to_boolean("false", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("fAlSe", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("off", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("OFF", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("no", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("No", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("n", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("N", &ret), 1);
+ assert_false(ret);
+ assert_int_equal(crm_str_to_boolean("0", &ret), 1);
+ assert_false(ret);
+}
+
+static void
+is_not_false(void **state) {
+ assert_int_equal(crm_str_to_boolean("falseee", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("of", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("nope", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("N!", NULL), -1);
+ assert_int_equal(crm_str_to_boolean("000", NULL), -1);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(is_true),
+ cmocka_unit_test(is_not_true),
+ cmocka_unit_test(is_false),
+ cmocka_unit_test(is_not_false))
diff --git a/lib/common/tests/strings/pcmk__add_word_test.c b/lib/common/tests/strings/pcmk__add_word_test.c
new file mode 100644
index 0000000..16a749e
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__add_word_test.c
@@ -0,0 +1,93 @@
+/*
+ * Copyright 2020-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>
+
+static void
+add_words(void **state)
+{
+ GString *list = NULL;
+
+ pcmk__add_word(&list, 16, "hello");
+ pcmk__add_word(&list, 16, "world");
+ assert_int_equal(strcmp((const char *) list->str, "hello world"), 0);
+ g_string_free(list, TRUE);
+}
+
+static void
+add_with_no_len(void **state)
+{
+ GString *list = NULL;
+
+ pcmk__add_word(&list, 0, "hello");
+ pcmk__add_word(&list, 0, "world");
+ assert_int_equal(strcmp((const char *) list->str, "hello world"), 0);
+ g_string_free(list, TRUE);
+}
+
+static void
+add_nothing(void **state)
+{
+ GString *list = NULL;
+
+ pcmk__add_word(&list, 0, "hello");
+ pcmk__add_word(&list, 0, NULL);
+ pcmk__add_word(&list, 0, "");
+ assert_int_equal(strcmp((const char *) list->str, "hello"), 0);
+ g_string_free(list, TRUE);
+}
+
+static void
+add_with_null(void **state)
+{
+ GString *list = NULL;
+
+ pcmk__add_separated_word(&list, 32, "hello", NULL);
+ pcmk__add_separated_word(&list, 32, "world", NULL);
+ pcmk__add_separated_word(&list, 32, "I am a unit test", NULL);
+ assert_int_equal(strcmp((const char *) list->str,
+ "hello world I am a unit test"), 0);
+ g_string_free(list, TRUE);
+}
+
+static void
+add_with_comma(void **state)
+{
+ GString *list = NULL;
+
+ pcmk__add_separated_word(&list, 32, "hello", ",");
+ pcmk__add_separated_word(&list, 32, "world", ",");
+ pcmk__add_separated_word(&list, 32, "I am a unit test", ",");
+ assert_int_equal(strcmp((const char *) list->str,
+ "hello,world,I am a unit test"), 0);
+ g_string_free(list, TRUE);
+}
+
+static void
+add_with_comma_and_space(void **state)
+{
+ GString *list = NULL;
+
+ pcmk__add_separated_word(&list, 32, "hello", ", ");
+ pcmk__add_separated_word(&list, 32, "world", ", ");
+ pcmk__add_separated_word(&list, 32, "I am a unit test", ", ");
+ assert_int_equal(strcmp((const char *) list->str,
+ "hello, world, I am a unit test"), 0);
+ g_string_free(list, TRUE);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(add_words),
+ cmocka_unit_test(add_with_no_len),
+ cmocka_unit_test(add_nothing),
+ cmocka_unit_test(add_with_null),
+ cmocka_unit_test(add_with_comma),
+ cmocka_unit_test(add_with_comma_and_space))
diff --git a/lib/common/tests/strings/pcmk__btoa_test.c b/lib/common/tests/strings/pcmk__btoa_test.c
new file mode 100644
index 0000000..f7dee9e
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__btoa_test.c
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+btoa(void **state) {
+ assert_string_equal(pcmk__btoa(false), "false");
+ assert_string_equal(pcmk__btoa(true), "true");
+ assert_string_equal(pcmk__btoa(1 == 0), "false");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(btoa))
diff --git a/lib/common/tests/strings/pcmk__char_in_any_str_test.c b/lib/common/tests/strings/pcmk__char_in_any_str_test.c
new file mode 100644
index 0000000..e70dfb4
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__char_in_any_str_test.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+empty_list(void **state)
+{
+ assert_false(pcmk__char_in_any_str('x', NULL));
+ assert_false(pcmk__char_in_any_str('\0', NULL));
+}
+
+static void
+null_char(void **state)
+{
+ assert_true(pcmk__char_in_any_str('\0', "xxx", "yyy", NULL));
+ assert_true(pcmk__char_in_any_str('\0', "", NULL));
+}
+
+static void
+in_list(void **state)
+{
+ assert_true(pcmk__char_in_any_str('x', "aaa", "bbb", "xxx", NULL));
+}
+
+static void
+not_in_list(void **state)
+{
+ assert_false(pcmk__char_in_any_str('x', "aaa", "bbb", NULL));
+ assert_false(pcmk__char_in_any_str('A', "aaa", "bbb", NULL));
+ assert_false(pcmk__char_in_any_str('x', "", NULL));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_list),
+ cmocka_unit_test(null_char),
+ cmocka_unit_test(in_list),
+ cmocka_unit_test(not_in_list))
diff --git a/lib/common/tests/strings/pcmk__compress_test.c b/lib/common/tests/strings/pcmk__compress_test.c
new file mode 100644
index 0000000..7480937
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__compress_test.c
@@ -0,0 +1,58 @@
+/*
+ * 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 "mock_private.h"
+
+#define SIMPLE_DATA "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
+
+const char *SIMPLE_COMPRESSED = "BZh41AY&SYO\x1ai";
+
+static void
+simple_compress(void **state)
+{
+ char *result = calloc(1024, sizeof(char));
+ unsigned int len;
+
+ assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len), pcmk_rc_ok);
+ assert_memory_equal(result, SIMPLE_COMPRESSED, 13);
+}
+
+static void
+max_too_small(void **state)
+{
+ char *result = calloc(1024, sizeof(char));
+ unsigned int len;
+
+ assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 10, &result, &len), pcmk_rc_error);
+}
+
+static void
+calloc_fails(void **state) {
+ char *result = calloc(1024, sizeof(char));
+ unsigned int len;
+
+ pcmk__assert_asserts(
+ {
+ pcmk__mock_calloc = true; // calloc() will return NULL
+ expect_value(__wrap_calloc, nmemb, (size_t) ((40 * 1.01) + 601));
+ expect_value(__wrap_calloc, size, sizeof(char));
+ pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len);
+ pcmk__mock_calloc = false; // Use the real calloc()
+ }
+ );
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(simple_compress),
+ cmocka_unit_test(max_too_small),
+ cmocka_unit_test(calloc_fails))
diff --git a/lib/common/tests/strings/pcmk__ends_with_test.c b/lib/common/tests/strings/pcmk__ends_with_test.c
new file mode 100644
index 0000000..7503571
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__ends_with_test.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2021 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>
+
+static void
+bad_input(void **state) {
+ assert_false(pcmk__ends_with(NULL, "xyz"));
+
+ assert_true(pcmk__ends_with(NULL, NULL));
+ assert_true(pcmk__ends_with(NULL, ""));
+ assert_true(pcmk__ends_with("", NULL));
+ assert_true(pcmk__ends_with("", ""));
+ assert_true(pcmk__ends_with("abc", NULL));
+ assert_true(pcmk__ends_with("abc", ""));
+}
+
+static void
+ends_with(void **state) {
+ assert_true(pcmk__ends_with("abc", "abc"));
+ assert_true(pcmk__ends_with("abc", "bc"));
+ assert_true(pcmk__ends_with("abc", "c"));
+ assert_true(pcmk__ends_with("abcbc", "bc"));
+
+ assert_false(pcmk__ends_with("abc", "def"));
+ assert_false(pcmk__ends_with("abc", "defg"));
+ assert_false(pcmk__ends_with("abc", "bcd"));
+ assert_false(pcmk__ends_with("abc", "ab"));
+
+ assert_false(pcmk__ends_with("abc", "BC"));
+}
+
+static void
+ends_with_ext(void **state) {
+ assert_true(pcmk__ends_with_ext("ab.c", ".c"));
+ assert_true(pcmk__ends_with_ext("ab.cb.c", ".c"));
+
+ assert_false(pcmk__ends_with_ext("ab.c", ".def"));
+ assert_false(pcmk__ends_with_ext("ab.c", ".defg"));
+ assert_false(pcmk__ends_with_ext("ab.c", ".cd"));
+ assert_false(pcmk__ends_with_ext("ab.c", "ab"));
+
+ assert_false(pcmk__ends_with_ext("ab.c", ".C"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(ends_with),
+ cmocka_unit_test(ends_with_ext))
diff --git a/lib/common/tests/strings/pcmk__g_strcat_test.c b/lib/common/tests/strings/pcmk__g_strcat_test.c
new file mode 100644
index 0000000..2116f0e
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__g_strcat_test.c
@@ -0,0 +1,73 @@
+/*
+ * Copyright 2020-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>
+
+static void
+add_to_null(void **state)
+{
+ pcmk__assert_asserts(pcmk__g_strcat(NULL, NULL));
+ pcmk__assert_asserts(pcmk__g_strcat(NULL, "hello", NULL));
+}
+
+static void
+add_nothing(void **state)
+{
+ GString *buf = g_string_new(NULL);
+
+ // Start with empty string
+ pcmk__g_strcat(buf, NULL);
+ assert_string_equal((const char *) buf->str, "");
+
+ pcmk__g_strcat(buf, "", NULL);
+ assert_string_equal((const char *) buf->str, "");
+
+ // Start with populated string
+ g_string_append(buf, "hello");
+ pcmk__g_strcat(buf, NULL);
+ assert_string_equal((const char *) buf->str, "hello");
+
+ pcmk__g_strcat(buf, "", NULL);
+ assert_string_equal((const char *) buf->str, "hello");
+ g_string_free(buf, TRUE);
+}
+
+static void
+add_words(void **state)
+{
+ GString *buf = g_string_new(NULL);
+
+ // Verify a call with multiple words
+ pcmk__g_strcat(buf, "hello", " ", NULL);
+ assert_string_equal((const char *) buf->str, "hello ");
+
+ // Verify that a second call doesn't overwrite the first one
+ pcmk__g_strcat(buf, "world", NULL);
+ assert_string_equal((const char *) buf->str, "hello world");
+ g_string_free(buf, TRUE);
+}
+
+static void
+stop_early(void **state)
+{
+ GString *buf = g_string_new(NULL);
+
+ // NULL anywhere after buf in the arg list should cause a return
+ pcmk__g_strcat(buf, "hello", NULL, " world", NULL);
+ assert_string_equal((const char *) buf->str, "hello");
+ g_string_free(buf, TRUE);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(add_to_null),
+ cmocka_unit_test(add_nothing),
+ cmocka_unit_test(add_words),
+ cmocka_unit_test(stop_early))
diff --git a/lib/common/tests/strings/pcmk__guint_from_hash_test.c b/lib/common/tests/strings/pcmk__guint_from_hash_test.c
new file mode 100644
index 0000000..e2b4762
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__guint_from_hash_test.c
@@ -0,0 +1,76 @@
+/*
+ * 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 <glib.h>
+
+static void
+null_args(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+ guint result;
+
+ assert_int_equal(pcmk__guint_from_hash(NULL, "abc", 123, &result), EINVAL);
+ assert_int_equal(pcmk__guint_from_hash(tbl, NULL, 123, &result), EINVAL);
+
+ g_hash_table_destroy(tbl);
+}
+
+static void
+missing_key(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+ guint result;
+
+ assert_int_equal(pcmk__guint_from_hash(tbl, "abc", 123, &result), pcmk_rc_ok);
+ assert_int_equal(result, 123);
+
+ g_hash_table_destroy(tbl);
+}
+
+static void
+standard_usage(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+ guint result;
+
+ g_hash_table_insert(tbl, strdup("abc"), strdup("123"));
+
+ assert_int_equal(pcmk__guint_from_hash(tbl, "abc", 456, &result), pcmk_rc_ok);
+ assert_int_equal(result, 123);
+
+ g_hash_table_destroy(tbl);
+}
+
+static void
+conversion_errors(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+ guint result;
+
+ g_hash_table_insert(tbl, strdup("negative"), strdup("-3"));
+ g_hash_table_insert(tbl, strdup("toobig"), strdup("20000000000000000"));
+
+ assert_int_equal(pcmk__guint_from_hash(tbl, "negative", 456, &result), ERANGE);
+ assert_int_equal(result, 456);
+
+ assert_int_equal(pcmk__guint_from_hash(tbl, "toobig", 456, &result), ERANGE);
+ assert_int_equal(result, 456);
+
+ g_hash_table_destroy(tbl);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_args),
+ cmocka_unit_test(missing_key),
+ cmocka_unit_test(standard_usage),
+ cmocka_unit_test(conversion_errors))
diff --git a/lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c b/lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c
new file mode 100644
index 0000000..df7b11c
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__numeric_strcasecmp_test.c
@@ -0,0 +1,79 @@
+/*
+ * 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>
+
+static void
+null_ptr(void **state)
+{
+ pcmk__assert_asserts(pcmk__numeric_strcasecmp(NULL, NULL));
+ pcmk__assert_asserts(pcmk__numeric_strcasecmp("a", NULL));
+ pcmk__assert_asserts(pcmk__numeric_strcasecmp(NULL, "a"));
+}
+
+static void
+no_numbers(void **state)
+{
+ /* All comparisons are done case-insensitively. */
+ assert_int_equal(pcmk__numeric_strcasecmp("abcd", "efgh"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("abcd", "abcd"), 0);
+ assert_int_equal(pcmk__numeric_strcasecmp("efgh", "abcd"), 1);
+
+ assert_int_equal(pcmk__numeric_strcasecmp("AbCd", "eFgH"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("ABCD", "abcd"), 0);
+ assert_int_equal(pcmk__numeric_strcasecmp("EFgh", "ABcd"), 1);
+}
+
+static void
+trailing_numbers(void **state)
+{
+ assert_int_equal(pcmk__numeric_strcasecmp("node1", "node2"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node1", "node1"), 0);
+ assert_int_equal(pcmk__numeric_strcasecmp("node2", "node1"), 1);
+
+ assert_int_equal(pcmk__numeric_strcasecmp("node1", "node10"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node10", "node10"), 0);
+ assert_int_equal(pcmk__numeric_strcasecmp("node10", "node1"), 1);
+
+ assert_int_equal(pcmk__numeric_strcasecmp("node10", "remotenode9"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("remotenode9", "node10"), 1);
+
+ /* Longer numbers sort higher than shorter numbers. */
+ assert_int_equal(pcmk__numeric_strcasecmp("node001", "node1"), 1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node1", "node001"), -1);
+}
+
+static void
+middle_numbers(void **state)
+{
+ assert_int_equal(pcmk__numeric_strcasecmp("node1abc", "node1def"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node1def", "node1abc"), 1);
+
+ assert_int_equal(pcmk__numeric_strcasecmp("node1abc", "node2abc"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node2abc", "node1abc"), 1);
+}
+
+static void
+unequal_lengths(void **state)
+{
+ assert_int_equal(pcmk__numeric_strcasecmp("node-ab", "node-abc"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node-abc", "node-ab"), 1);
+
+ assert_int_equal(pcmk__numeric_strcasecmp("node1ab", "node1abc"), -1);
+ assert_int_equal(pcmk__numeric_strcasecmp("node1abc", "node1ab"), 1);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_ptr),
+ cmocka_unit_test(no_numbers),
+ cmocka_unit_test(trailing_numbers),
+ cmocka_unit_test(middle_numbers),
+ cmocka_unit_test(unequal_lengths))
diff --git a/lib/common/tests/strings/pcmk__parse_ll_range_test.c b/lib/common/tests/strings/pcmk__parse_ll_range_test.c
new file mode 100644
index 0000000..7656ad7
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__parse_ll_range_test.c
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2020-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/common/results.h"
+#include <crm_internal.h>
+
+#include <crm/common/unittest_internal.h>
+
+static void
+empty_input_string(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range(NULL, &start, &end), ENODATA);
+ assert_int_equal(pcmk__parse_ll_range("", &start, &end), ENODATA);
+}
+
+static void
+null_input_variables(void **state)
+{
+ long long start, end;
+
+ pcmk__assert_asserts(pcmk__parse_ll_range("1234", NULL, &end));
+ pcmk__assert_asserts(pcmk__parse_ll_range("1234", &start, NULL));
+}
+
+static void
+missing_separator(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("1234", &start, &end), pcmk_rc_ok);
+ assert_int_equal(start, 1234);
+ assert_int_equal(end, 1234);
+}
+
+static void
+only_separator(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("-", &start, &end), pcmk_rc_bad_input);
+ assert_int_equal(start, PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(end, PCMK__PARSE_INT_DEFAULT);
+}
+
+static void
+no_range_end(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("2000-", &start, &end), pcmk_rc_ok);
+ assert_int_equal(start, 2000);
+ assert_int_equal(end, PCMK__PARSE_INT_DEFAULT);
+}
+
+static void
+no_range_start(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("-2020", &start, &end), pcmk_rc_ok);
+ assert_int_equal(start, PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(end, 2020);
+}
+
+static void
+range_start_and_end(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("2000-2020", &start, &end), pcmk_rc_ok);
+ assert_int_equal(start, 2000);
+ assert_int_equal(end, 2020);
+
+ assert_int_equal(pcmk__parse_ll_range("2000-2020-2030", &start, &end), pcmk_rc_bad_input);
+}
+
+static void
+garbage(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("2000x-", &start, &end), pcmk_rc_bad_input);
+ assert_int_equal(start, PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(end, PCMK__PARSE_INT_DEFAULT);
+
+ assert_int_equal(pcmk__parse_ll_range("-x2000", &start, &end), pcmk_rc_bad_input);
+ assert_int_equal(start, PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(end, PCMK__PARSE_INT_DEFAULT);
+}
+
+static void
+strtoll_errors(void **state)
+{
+ long long start, end;
+
+ assert_int_equal(pcmk__parse_ll_range("20000000000000000000-", &start, &end), EOVERFLOW);
+ assert_int_equal(pcmk__parse_ll_range("100-20000000000000000000", &start, &end), EOVERFLOW);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_string),
+ cmocka_unit_test(null_input_variables),
+ cmocka_unit_test(missing_separator),
+ cmocka_unit_test(only_separator),
+ cmocka_unit_test(no_range_end),
+ cmocka_unit_test(no_range_start),
+ cmocka_unit_test(range_start_and_end),
+ cmocka_unit_test(strtoll_errors),
+ cmocka_unit_test(garbage))
diff --git a/lib/common/tests/strings/pcmk__s_test.c b/lib/common/tests/strings/pcmk__s_test.c
new file mode 100644
index 0000000..cdc2551
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__s_test.c
@@ -0,0 +1,29 @@
+/*
+ * 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>
+
+static void
+s_is_null(void **state) {
+ assert_null(pcmk__s(NULL, NULL));
+ assert_string_equal(pcmk__s(NULL, ""), "");
+ assert_string_equal(pcmk__s(NULL, "something"), "something");
+}
+
+static void
+s_is_not_null(void **state) {
+ assert_string_equal(pcmk__s("something", NULL), "something");
+ assert_string_equal(pcmk__s("something", "default"), "something");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(s_is_null),
+ cmocka_unit_test(s_is_not_null))
diff --git a/lib/common/tests/strings/pcmk__scan_double_test.c b/lib/common/tests/strings/pcmk__scan_double_test.c
new file mode 100644
index 0000000..a1a180a
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__scan_double_test.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2004-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 <float.h> // DBL_MAX, etc.
+#include <math.h> // fabs()
+
+// Ensure plenty of characters for %f display
+#define LOCAL_BUF_SIZE 2 * DBL_MAX_10_EXP
+
+/*
+ * assert_float_equal doesn't exist for older versions of cmocka installed on some
+ * of our builders, so define it in terms of regular assert() here in that case.
+ */
+#if HAVE_DECL_ASSERT_FLOAT_EQUAL == 0
+#define assert_float_equal(a, b, epsilon) assert_true(fabs((a) - (b)) < (epsilon))
+#endif
+
+static void
+empty_input_string(void **state)
+{
+ double result;
+
+ // Without default_text
+ assert_int_equal(pcmk__scan_double(NULL, &result, NULL, NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+
+ assert_int_equal(pcmk__scan_double("", &result, NULL, NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+
+ // With default_text
+ assert_int_equal(pcmk__scan_double(NULL, &result, "2.0", NULL), pcmk_rc_ok);
+ assert_float_equal(result, 2.0, DBL_EPSILON);
+
+ assert_int_equal(pcmk__scan_double("", &result, "2.0", NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+}
+
+static void
+bad_input_string(void **state)
+{
+ double result;
+
+ // Without default text
+ assert_int_equal(pcmk__scan_double("asdf", &result, NULL, NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+
+ assert_int_equal(pcmk__scan_double("as2.0", &result, NULL, NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+
+ // With default text (not used)
+ assert_int_equal(pcmk__scan_double("asdf", &result, "2.0", NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+
+ assert_int_equal(pcmk__scan_double("as2.0", &result, "2.0", NULL), EINVAL);
+ assert_float_equal(result, PCMK__PARSE_DBL_DEFAULT, DBL_EPSILON);
+}
+
+static void
+trailing_chars(void **state)
+{
+ double result;
+ char *end_text;
+
+ assert_int_equal(pcmk__scan_double("2.0asdf", &result, NULL, &end_text), pcmk_rc_ok);
+ assert_float_equal(result, 2.0, DBL_EPSILON);
+ assert_string_equal(end_text, "asdf");
+}
+
+static void
+no_result_variable(void **state)
+{
+ pcmk__assert_asserts(pcmk__scan_double("asdf", NULL, NULL, NULL));
+}
+
+static void
+typical_case(void **state)
+{
+ char str[LOCAL_BUF_SIZE];
+ double result;
+
+ assert_int_equal(pcmk__scan_double("0.0", &result, NULL, NULL), pcmk_rc_ok);
+ assert_float_equal(result, 0.0, DBL_EPSILON);
+
+ assert_int_equal(pcmk__scan_double("1.0", &result, NULL, NULL), pcmk_rc_ok);
+ assert_float_equal(result, 1.0, DBL_EPSILON);
+
+ assert_int_equal(pcmk__scan_double("-1.0", &result, NULL, NULL), pcmk_rc_ok);
+ assert_float_equal(result, -1.0, DBL_EPSILON);
+
+ snprintf(str, LOCAL_BUF_SIZE, "%f", DBL_MAX);
+ assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_ok);
+ assert_float_equal(result, DBL_MAX, DBL_EPSILON);
+
+ snprintf(str, LOCAL_BUF_SIZE, "%f", -DBL_MAX);
+ assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_ok);
+ assert_float_equal(result, -DBL_MAX, DBL_EPSILON);
+}
+
+static void
+double_overflow(void **state)
+{
+ char str[LOCAL_BUF_SIZE];
+ double result;
+
+ /*
+ * 1e(DBL_MAX_10_EXP + 1) produces an inf value
+ * Can't use assert_float_equal() because (inf - inf) == NaN
+ */
+ snprintf(str, LOCAL_BUF_SIZE, "1e%d", DBL_MAX_10_EXP + 1);
+ assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), EOVERFLOW);
+ assert_true(result > DBL_MAX);
+
+ snprintf(str, LOCAL_BUF_SIZE, "-1e%d", DBL_MAX_10_EXP + 1);
+ assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), EOVERFLOW);
+ assert_true(result < -DBL_MAX);
+}
+
+static void
+double_underflow(void **state)
+{
+ char str[LOCAL_BUF_SIZE];
+ double result;
+
+ /*
+ * 1e(DBL_MIN_10_EXP - 1) produces a denormalized value (between 0
+ * and DBL_MIN)
+ *
+ * C99/C11: result will be **no greater than** DBL_MIN
+ */
+ snprintf(str, LOCAL_BUF_SIZE, "1e%d", DBL_MIN_10_EXP - 1);
+ assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_underflow);
+ assert_true(result >= 0.0);
+ assert_true(result <= DBL_MIN);
+
+ snprintf(str, LOCAL_BUF_SIZE, "-1e%d", DBL_MIN_10_EXP - 1);
+ assert_int_equal(pcmk__scan_double(str, &result, NULL, NULL), pcmk_rc_underflow);
+ assert_true(result <= 0.0);
+ assert_true(result >= -DBL_MIN);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_string),
+ cmocka_unit_test(bad_input_string),
+ cmocka_unit_test(trailing_chars),
+ cmocka_unit_test(no_result_variable),
+ cmocka_unit_test(typical_case),
+ cmocka_unit_test(double_overflow),
+ cmocka_unit_test(double_underflow))
diff --git a/lib/common/tests/strings/pcmk__scan_min_int_test.c b/lib/common/tests/strings/pcmk__scan_min_int_test.c
new file mode 100644
index 0000000..90c3e87
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__scan_min_int_test.c
@@ -0,0 +1,60 @@
+/*
+ * 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>
+
+static void
+empty_input_string(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_min_int("", &result, 1), EINVAL);
+ assert_int_equal(result, 1);
+
+ assert_int_equal(pcmk__scan_min_int(NULL, &result, 1), pcmk_rc_ok);
+ assert_int_equal(result, 1);
+}
+
+static void
+input_below_minimum(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_min_int("100", &result, 1024), pcmk_rc_ok);
+ assert_int_equal(result, 1024);
+}
+
+static void
+input_above_maximum(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_min_int("20000000000000000", &result, 100), EOVERFLOW);
+ assert_int_equal(result, INT_MAX);
+}
+
+static void
+input_just_right(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_min_int("1024", &result, 1024), pcmk_rc_ok);
+ assert_int_equal(result, 1024);
+
+ assert_int_equal(pcmk__scan_min_int("2048", &result, 1024), pcmk_rc_ok);
+ assert_int_equal(result, 2048);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_string),
+ cmocka_unit_test(input_below_minimum),
+ cmocka_unit_test(input_above_maximum),
+ cmocka_unit_test(input_just_right))
diff --git a/lib/common/tests/strings/pcmk__scan_port_test.c b/lib/common/tests/strings/pcmk__scan_port_test.c
new file mode 100644
index 0000000..cf927e4
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__scan_port_test.c
@@ -0,0 +1,59 @@
+/*
+ * 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>
+
+static void
+empty_input_string(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_port("", &result), EINVAL);
+ assert_int_equal(result, -1);
+}
+
+static void
+bad_input_string(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_port("abc", &result), EINVAL);
+ assert_int_equal(result, -1);
+}
+
+static void
+out_of_range(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_port("-1", &result), pcmk_rc_before_range);
+ assert_int_equal(result, -1);
+ assert_int_equal(pcmk__scan_port("65536", &result), pcmk_rc_after_range);
+ assert_int_equal(result, -1);
+}
+
+static void
+typical_case(void **state)
+{
+ int result;
+
+ assert_int_equal(pcmk__scan_port("0", &result), pcmk_rc_ok);
+ assert_int_equal(result, 0);
+
+ assert_int_equal(pcmk__scan_port("80", &result), pcmk_rc_ok);
+ assert_int_equal(result, 80);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_string),
+ cmocka_unit_test(bad_input_string),
+ cmocka_unit_test(out_of_range),
+ cmocka_unit_test(typical_case))
diff --git a/lib/common/tests/strings/pcmk__starts_with_test.c b/lib/common/tests/strings/pcmk__starts_with_test.c
new file mode 100644
index 0000000..5429417
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__starts_with_test.c
@@ -0,0 +1,35 @@
+/*
+ * Copyright 2021 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>
+
+static void
+bad_input(void **state) {
+ assert_false(pcmk__starts_with(NULL, "x"));
+ assert_false(pcmk__starts_with("abc", NULL));
+}
+
+static void
+starts_with(void **state) {
+ assert_true(pcmk__starts_with("abc", "a"));
+ assert_true(pcmk__starts_with("abc", "ab"));
+ assert_true(pcmk__starts_with("abc", "abc"));
+
+ assert_false(pcmk__starts_with("abc", "A"));
+ assert_false(pcmk__starts_with("abc", "bc"));
+
+ assert_false(pcmk__starts_with("", "x"));
+ assert_true(pcmk__starts_with("xyz", ""));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(starts_with))
diff --git a/lib/common/tests/strings/pcmk__str_any_of_test.c b/lib/common/tests/strings/pcmk__str_any_of_test.c
new file mode 100644
index 0000000..bd4ae2c
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__str_any_of_test.c
@@ -0,0 +1,48 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+empty_input_list(void **state) {
+ assert_false(pcmk__strcase_any_of("xxx", NULL));
+ assert_false(pcmk__str_any_of("xxx", NULL));
+ assert_false(pcmk__strcase_any_of("", NULL));
+ assert_false(pcmk__str_any_of("", NULL));
+}
+
+static void
+empty_string(void **state) {
+ assert_false(pcmk__strcase_any_of("", "xxx", "yyy", NULL));
+ assert_false(pcmk__str_any_of("", "xxx", "yyy", NULL));
+ assert_false(pcmk__strcase_any_of(NULL, "xxx", "yyy", NULL));
+ assert_false(pcmk__str_any_of(NULL, "xxx", "yyy", NULL));
+}
+
+static void
+in_list(void **state) {
+ assert_true(pcmk__strcase_any_of("xxx", "aaa", "bbb", "xxx", NULL));
+ assert_true(pcmk__str_any_of("xxx", "aaa", "bbb", "xxx", NULL));
+ assert_true(pcmk__strcase_any_of("XXX", "aaa", "bbb", "xxx", NULL));
+}
+
+static void
+not_in_list(void **state) {
+ assert_false(pcmk__strcase_any_of("xxx", "aaa", "bbb", NULL));
+ assert_false(pcmk__str_any_of("xxx", "aaa", "bbb", NULL));
+ assert_false(pcmk__str_any_of("AAA", "aaa", "bbb", NULL));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_list),
+ cmocka_unit_test(empty_string),
+ cmocka_unit_test(in_list),
+ cmocka_unit_test(not_in_list))
diff --git a/lib/common/tests/strings/pcmk__str_in_list_test.c b/lib/common/tests/strings/pcmk__str_in_list_test.c
new file mode 100644
index 0000000..cff536a
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__str_in_list_test.c
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2021 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 <glib.h>
+
+static void
+empty_input_list(void **state) {
+ assert_false(pcmk__str_in_list(NULL, NULL, pcmk__str_none));
+ assert_false(pcmk__str_in_list(NULL, NULL, pcmk__str_null_matches));
+ assert_false(pcmk__str_in_list("xxx", NULL, pcmk__str_none));
+ assert_false(pcmk__str_in_list("", NULL, pcmk__str_none));
+}
+
+static void
+empty_string(void **state) {
+ GList *list = NULL;
+
+ list = g_list_prepend(list, (gpointer) "xxx");
+
+ assert_false(pcmk__str_in_list(NULL, list, pcmk__str_none));
+ assert_true(pcmk__str_in_list(NULL, list, pcmk__str_null_matches));
+ assert_false(pcmk__str_in_list("", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list("", list, pcmk__str_null_matches));
+
+ g_list_free(list);
+}
+
+static void
+star_matches(void **state) {
+ GList *list = NULL;
+
+ list = g_list_prepend(list, (gpointer) "*");
+ list = g_list_append(list, (gpointer) "more");
+
+ assert_true(pcmk__str_in_list("xxx", list, pcmk__str_star_matches));
+ assert_true(pcmk__str_in_list("yyy", list, pcmk__str_star_matches));
+ assert_true(pcmk__str_in_list("XXX", list, pcmk__str_star_matches|pcmk__str_casei));
+ assert_true(pcmk__str_in_list("", list, pcmk__str_star_matches));
+
+ g_list_free(list);
+}
+
+static void
+star_doesnt_match(void **state) {
+ GList *list = NULL;
+
+ list = g_list_prepend(list, (gpointer) "*");
+
+ assert_false(pcmk__str_in_list("xxx", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list("yyy", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list("XXX", list, pcmk__str_casei));
+ assert_false(pcmk__str_in_list("", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list(NULL, list, pcmk__str_star_matches));
+
+ g_list_free(list);
+}
+
+static void
+in_list(void **state) {
+ GList *list = NULL;
+
+ list = g_list_prepend(list, (gpointer) "xxx");
+ list = g_list_prepend(list, (gpointer) "yyy");
+ list = g_list_prepend(list, (gpointer) "zzz");
+
+ assert_true(pcmk__str_in_list("xxx", list, pcmk__str_none));
+ assert_true(pcmk__str_in_list("XXX", list, pcmk__str_casei));
+ assert_true(pcmk__str_in_list("yyy", list, pcmk__str_none));
+ assert_true(pcmk__str_in_list("YYY", list, pcmk__str_casei));
+ assert_true(pcmk__str_in_list("zzz", list, pcmk__str_none));
+ assert_true(pcmk__str_in_list("ZZZ", list, pcmk__str_casei));
+
+ g_list_free(list);
+}
+
+static void
+not_in_list(void **state) {
+ GList *list = NULL;
+
+ list = g_list_prepend(list, (gpointer) "xxx");
+ list = g_list_prepend(list, (gpointer) "yyy");
+
+ assert_false(pcmk__str_in_list("xx", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list("XXX", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list("zzz", list, pcmk__str_none));
+ assert_false(pcmk__str_in_list("zzz", list, pcmk__str_casei));
+
+ g_list_free(list);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input_list),
+ cmocka_unit_test(empty_string),
+ cmocka_unit_test(star_matches),
+ cmocka_unit_test(star_doesnt_match),
+ cmocka_unit_test(in_list),
+ cmocka_unit_test(not_in_list))
diff --git a/lib/common/tests/strings/pcmk__str_table_dup_test.c b/lib/common/tests/strings/pcmk__str_table_dup_test.c
new file mode 100644
index 0000000..754bde6
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__str_table_dup_test.c
@@ -0,0 +1,59 @@
+/*
+ * 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 <glib.h>
+
+static void
+null_input_table(void **state)
+{
+ assert_null(pcmk__str_table_dup(NULL));
+}
+
+static void
+empty_input_table(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+ GHashTable *copy = NULL;
+
+ copy = pcmk__str_table_dup(tbl);
+ assert_int_equal(g_hash_table_size(copy), 0);
+
+ g_hash_table_destroy(tbl);
+ g_hash_table_destroy(copy);
+}
+
+static void
+regular_input_table(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+ GHashTable *copy = NULL;
+
+ g_hash_table_insert(tbl, strdup("abc"), strdup("123"));
+ g_hash_table_insert(tbl, strdup("def"), strdup("456"));
+ g_hash_table_insert(tbl, strdup("ghi"), strdup("789"));
+
+ copy = pcmk__str_table_dup(tbl);
+ assert_int_equal(g_hash_table_size(copy), 3);
+
+ assert_string_equal(g_hash_table_lookup(tbl, "abc"), "123");
+ assert_string_equal(g_hash_table_lookup(tbl, "def"), "456");
+ assert_string_equal(g_hash_table_lookup(tbl, "ghi"), "789");
+
+ g_hash_table_destroy(tbl);
+ g_hash_table_destroy(copy);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_input_table),
+ cmocka_unit_test(empty_input_table),
+ cmocka_unit_test(regular_input_table))
diff --git a/lib/common/tests/strings/pcmk__str_update_test.c b/lib/common/tests/strings/pcmk__str_update_test.c
new file mode 100644
index 0000000..571031d
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__str_update_test.c
@@ -0,0 +1,78 @@
+/*
+ * 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 "mock_private.h"
+
+static void
+update_null(void **state) {
+ char *str = NULL;
+
+ // These just make sure they don't crash
+ pcmk__str_update(NULL, NULL);
+ pcmk__str_update(NULL, "value");
+
+ // Update an already NULL string to NULL
+ pcmk__str_update(&str, NULL);
+ assert_null(str);
+
+ // Update an already allocated string to NULL
+ str = strdup("hello");
+ pcmk__str_update(&str, NULL);
+ assert_null(str);
+}
+
+static void
+update_same(void **state) {
+ char *str = NULL;
+ char *saved = NULL;
+
+ str = strdup("hello");
+ saved = str;
+ pcmk__str_update(&str, "hello");
+ assert_ptr_equal(saved, str); // No free and reallocation
+ free(str);
+}
+
+static void
+update_different(void **state) {
+ char *str = NULL;
+
+ str = strdup("hello");
+ pcmk__str_update(&str, "world");
+ assert_string_equal(str, "world");
+ free(str);
+}
+
+static void
+strdup_fails(void **state) {
+ char *str = NULL;
+
+ str = strdup("hello");
+
+ pcmk__assert_asserts(
+ {
+ pcmk__mock_strdup = true; // strdup() will return NULL
+ expect_string(__wrap_strdup, s, "world");
+ pcmk__str_update(&str, "world");
+ pcmk__mock_strdup = false; // Use the real strdup()
+ }
+ );
+
+ free(str);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(update_null),
+ cmocka_unit_test(update_same),
+ cmocka_unit_test(update_different),
+ cmocka_unit_test(strdup_fails))
diff --git a/lib/common/tests/strings/pcmk__strcmp_test.c b/lib/common/tests/strings/pcmk__strcmp_test.c
new file mode 100644
index 0000000..a709f18
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__strcmp_test.c
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+same_pointer(void **state) {
+ const char *s1 = "abcd";
+ const char *s2 = "wxyz";
+
+ assert_int_equal(pcmk__strcmp(s1, s1, pcmk__str_none), 0);
+ assert_true(pcmk__str_eq(s1, s1, pcmk__str_none));
+ assert_int_not_equal(pcmk__strcmp(s1, s2, pcmk__str_none), 0);
+ assert_false(pcmk__str_eq(s1, s2, pcmk__str_none));
+ assert_int_equal(pcmk__strcmp(NULL, NULL, pcmk__str_none), 0);
+}
+
+static void
+one_is_null(void **state) {
+ const char *s1 = "abcd";
+
+ assert_int_equal(pcmk__strcmp(s1, NULL, pcmk__str_null_matches), 0);
+ assert_true(pcmk__str_eq(s1, NULL, pcmk__str_null_matches));
+ assert_int_equal(pcmk__strcmp(NULL, s1, pcmk__str_null_matches), 0);
+ assert_true(pcmk__strcmp(s1, NULL, pcmk__str_none) > 0);
+ assert_false(pcmk__str_eq(s1, NULL, pcmk__str_none));
+ assert_true(pcmk__strcmp(NULL, s1, pcmk__str_none) < 0);
+}
+
+static void
+case_matters(void **state) {
+ const char *s1 = "abcd";
+ const char *s2 = "ABCD";
+
+ assert_true(pcmk__strcmp(s1, s2, pcmk__str_none) > 0);
+ assert_false(pcmk__str_eq(s1, s2, pcmk__str_none));
+ assert_true(pcmk__strcmp(s2, s1, pcmk__str_none) < 0);
+}
+
+static void
+case_insensitive(void **state) {
+ const char *s1 = "abcd";
+ const char *s2 = "ABCD";
+
+ assert_int_equal(pcmk__strcmp(s1, s2, pcmk__str_casei), 0);
+ assert_true(pcmk__str_eq(s1, s2, pcmk__str_casei));
+}
+
+static void
+regex(void **state) {
+ const char *s1 = "abcd";
+ const char *s2 = "ABCD";
+
+ assert_true(pcmk__strcmp(NULL, "a..d", pcmk__str_regex) > 0);
+ assert_true(pcmk__strcmp(s1, NULL, pcmk__str_regex) > 0);
+ assert_int_equal(pcmk__strcmp(s1, "a..d", pcmk__str_regex), 0);
+ assert_true(pcmk__str_eq(s1, "a..d", pcmk__str_regex));
+ assert_int_not_equal(pcmk__strcmp(s1, "xxyy", pcmk__str_regex), 0);
+ assert_false(pcmk__str_eq(s1, "xxyy", pcmk__str_regex));
+ assert_int_equal(pcmk__strcmp(s2, "a..d", pcmk__str_regex|pcmk__str_casei), 0);
+ assert_true(pcmk__str_eq(s2, "a..d", pcmk__str_regex|pcmk__str_casei));
+ assert_int_not_equal(pcmk__strcmp(s2, "a..d", pcmk__str_regex), 0);
+ assert_false(pcmk__str_eq(s2, "a..d", pcmk__str_regex));
+ assert_true(pcmk__strcmp(s2, "*ab", pcmk__str_regex) > 0);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(same_pointer),
+ cmocka_unit_test(one_is_null),
+ cmocka_unit_test(case_matters),
+ cmocka_unit_test(case_insensitive),
+ cmocka_unit_test(regex))
diff --git a/lib/common/tests/strings/pcmk__strikey_table_test.c b/lib/common/tests/strings/pcmk__strikey_table_test.c
new file mode 100644
index 0000000..d5f8635
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__strikey_table_test.c
@@ -0,0 +1,40 @@
+/*
+ * 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 <glib.h>
+
+static void
+store_strs(void **state)
+{
+ GHashTable *tbl = NULL;
+
+ tbl = pcmk__strikey_table(free, free);
+ assert_non_null(tbl);
+
+ assert_true(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-abc")));
+ assert_int_equal(g_hash_table_size(tbl), 1);
+ assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-abc");
+
+ assert_false(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-def")));
+ assert_int_equal(g_hash_table_size(tbl), 1);
+ assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-def");
+
+ assert_false(g_hash_table_insert(tbl, strdup("key-ABC"), strdup("val-ABC")));
+ assert_int_equal(g_hash_table_size(tbl), 1);
+ assert_string_equal(g_hash_table_lookup(tbl, "key-ABC"), "val-ABC");
+
+ g_hash_table_destroy(tbl);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(store_strs))
diff --git a/lib/common/tests/strings/pcmk__strkey_table_test.c b/lib/common/tests/strings/pcmk__strkey_table_test.c
new file mode 100644
index 0000000..ac6d92e
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__strkey_table_test.c
@@ -0,0 +1,40 @@
+/*
+ * 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 <glib.h>
+
+static void
+store_strs(void **state)
+{
+ GHashTable *tbl = NULL;
+
+ tbl = pcmk__strkey_table(free, free);
+ assert_non_null(tbl);
+
+ assert_true(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-abc")));
+ assert_int_equal(g_hash_table_size(tbl), 1);
+ assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-abc");
+
+ assert_false(g_hash_table_insert(tbl, strdup("key-abc"), strdup("val-def")));
+ assert_int_equal(g_hash_table_size(tbl), 1);
+ assert_string_equal(g_hash_table_lookup(tbl, "key-abc"), "val-def");
+
+ assert_true(g_hash_table_insert(tbl, strdup("key-ABC"), strdup("val-abc")));
+ assert_int_equal(g_hash_table_size(tbl), 2);
+ assert_string_equal(g_hash_table_lookup(tbl, "key-ABC"), "val-abc");
+
+ g_hash_table_destroy(tbl);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(store_strs))
diff --git a/lib/common/tests/strings/pcmk__trim_test.c b/lib/common/tests/strings/pcmk__trim_test.c
new file mode 100644
index 0000000..56bdd17
--- /dev/null
+++ b/lib/common/tests/strings/pcmk__trim_test.c
@@ -0,0 +1,72 @@
+/*
+ * 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 <string.h>
+
+static void
+empty_input(void **state)
+{
+ char *s = strdup("");
+
+ assert_null(pcmk__trim(NULL));
+ assert_string_equal(pcmk__trim(s), "");
+
+ free(s);
+}
+
+static void
+leading_newline(void **state)
+{
+ char *s = strdup("\nabcd");
+
+ assert_string_equal(pcmk__trim(s), "\nabcd");
+ free(s);
+}
+
+static void
+middle_newline(void **state)
+{
+ char *s = strdup("ab\ncd");
+
+ assert_string_equal(pcmk__trim(s), "ab\ncd");
+ free(s);
+}
+
+static void
+trailing_newline(void **state)
+{
+ char *s = strdup("abcd\n\n");
+
+ assert_string_equal(pcmk__trim(s), "abcd");
+ free(s);
+
+ s = strdup("abcd\n ");
+ assert_string_equal(pcmk__trim(s), "abcd\n ");
+ free(s);
+}
+
+static void
+other_whitespace(void **state)
+{
+ char *s = strdup(" ab\t\ncd \t");
+
+ assert_string_equal(pcmk__trim(s), " ab\t\ncd \t");
+ free(s);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(leading_newline),
+ cmocka_unit_test(middle_newline),
+ cmocka_unit_test(trailing_newline),
+ cmocka_unit_test(other_whitespace))
diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am
new file mode 100644
index 0000000..edccf09
--- /dev/null
+++ b/lib/common/tests/utils/Makefile.am
@@ -0,0 +1,28 @@
+#
+# Copyright 2020-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 = \
+ compare_version_test \
+ crm_meta_name_test \
+ crm_meta_value_test \
+ crm_user_lookup_test \
+ pcmk_daemon_user_test \
+ pcmk_str_is_infinity_test \
+ pcmk_str_is_minus_infinity_test \
+ pcmk__getpid_s_test
+
+if WRAPPABLE_UNAME
+check_PROGRAMS += pcmk_hostname_test
+endif
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c
new file mode 100644
index 0000000..35ebb63
--- /dev/null
+++ b/lib/common/tests/utils/compare_version_test.c
@@ -0,0 +1,55 @@
+/*
+ * 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>
+
+static void
+empty_params(void **state)
+{
+ assert_int_equal(compare_version(NULL, NULL), 0);
+ assert_int_equal(compare_version(NULL, "abc"), -1);
+ assert_int_equal(compare_version(NULL, "1.0.1"), -1);
+ assert_int_equal(compare_version("abc", NULL), 1);
+ assert_int_equal(compare_version("1.0.1", NULL), 1);
+}
+
+static void
+equal_versions(void **state)
+{
+ assert_int_equal(compare_version("0.4.7", "0.4.7"), 0);
+ assert_int_equal(compare_version("1.0", "1.0"), 0);
+}
+
+static void
+unequal_versions(void **state)
+{
+ assert_int_equal(compare_version("0.4.7", "0.4.8"), -1);
+ assert_int_equal(compare_version("0.4.8", "0.4.7"), 1);
+
+ assert_int_equal(compare_version("0.2.3", "0.3"), -1);
+ assert_int_equal(compare_version("0.3", "0.2.3"), 1);
+
+ assert_int_equal(compare_version("0.99", "1.0"), -1);
+ assert_int_equal(compare_version("1.0", "0.99"), 1);
+}
+
+static void
+shorter_versions(void **state)
+{
+ assert_int_equal(compare_version("1.0", "1.0.1"), -1);
+ assert_int_equal(compare_version("1.0.1", "1.0"), 1);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_params),
+ cmocka_unit_test(equal_versions),
+ cmocka_unit_test(unequal_versions),
+ cmocka_unit_test(shorter_versions))
diff --git a/lib/common/tests/utils/crm_meta_name_test.c b/lib/common/tests/utils/crm_meta_name_test.c
new file mode 100644
index 0000000..06fecc5
--- /dev/null
+++ b/lib/common/tests/utils/crm_meta_name_test.c
@@ -0,0 +1,41 @@
+/*
+ * 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/msg_xml.h>
+
+static void
+empty_params(void **state)
+{
+ assert_null(crm_meta_name(NULL));
+}
+
+static void
+standard_usage(void **state)
+{
+ char *s = NULL;
+
+ s = crm_meta_name(XML_RSC_ATTR_NOTIFY);
+ assert_string_equal(s, "CRM_meta_notify");
+ free(s);
+
+ s = crm_meta_name(XML_RSC_ATTR_STICKINESS);
+ assert_string_equal(s, "CRM_meta_resource_stickiness");
+ free(s);
+
+ s = crm_meta_name("blah");
+ assert_string_equal(s, "CRM_meta_blah");
+ free(s);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_params),
+ cmocka_unit_test(standard_usage))
diff --git a/lib/common/tests/utils/crm_meta_value_test.c b/lib/common/tests/utils/crm_meta_value_test.c
new file mode 100644
index 0000000..0bde5c6
--- /dev/null
+++ b/lib/common/tests/utils/crm_meta_value_test.c
@@ -0,0 +1,56 @@
+/*
+ * 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/msg_xml.h>
+
+#include <glib.h>
+
+static void
+empty_params(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+
+ assert_null(crm_meta_value(NULL, NULL));
+ assert_null(crm_meta_value(tbl, NULL));
+
+ g_hash_table_destroy(tbl);
+}
+
+static void
+key_not_in_table(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+
+ assert_null(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY));
+ assert_null(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS));
+
+ g_hash_table_destroy(tbl);
+}
+
+static void
+key_in_table(void **state)
+{
+ GHashTable *tbl = pcmk__strkey_table(free, free);
+
+ g_hash_table_insert(tbl, crm_meta_name(XML_RSC_ATTR_NOTIFY), strdup("1"));
+ g_hash_table_insert(tbl, crm_meta_name(XML_RSC_ATTR_STICKINESS), strdup("2"));
+
+ assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_NOTIFY), "1");
+ assert_string_equal(crm_meta_value(tbl, XML_RSC_ATTR_STICKINESS), "2");
+
+ g_hash_table_destroy(tbl);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_params),
+ cmocka_unit_test(key_not_in_table),
+ cmocka_unit_test(key_in_table))
diff --git a/lib/common/tests/utils/crm_user_lookup_test.c b/lib/common/tests/utils/crm_user_lookup_test.c
new file mode 100644
index 0000000..5842ec5
--- /dev/null
+++ b/lib/common/tests/utils/crm_user_lookup_test.c
@@ -0,0 +1,127 @@
+/*
+ * 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 "crmcommon_private.h"
+#include "mock_private.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+
+static void
+calloc_fails(void **state)
+{
+ uid_t uid;
+ gid_t gid;
+
+ pcmk__mock_calloc = true; // calloc() will return NULL
+
+ expect_value(__wrap_calloc, nmemb, 1);
+ expect_value(__wrap_calloc, size, PCMK__PW_BUFFER_LEN);
+ assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -ENOMEM);
+
+ pcmk__mock_calloc = false; // Use real calloc()
+}
+
+static void
+getpwnam_r_fails(void **state)
+{
+ uid_t uid;
+ gid_t gid;
+
+ // Set getpwnam_r() return value and result parameter
+ pcmk__mock_getpwnam_r = true;
+
+ expect_string(__wrap_getpwnam_r, name, "hauser");
+ expect_any(__wrap_getpwnam_r, pwd);
+ expect_any(__wrap_getpwnam_r, buf);
+ expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN);
+ expect_any(__wrap_getpwnam_r, result);
+ will_return(__wrap_getpwnam_r, EIO);
+ will_return(__wrap_getpwnam_r, NULL);
+
+ assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EIO);
+
+ pcmk__mock_getpwnam_r = false;
+}
+
+static void
+no_matching_pwent(void **state)
+{
+ uid_t uid;
+ gid_t gid;
+
+ // Set getpwnam_r() return value and result parameter
+ pcmk__mock_getpwnam_r = true;
+
+ expect_string(__wrap_getpwnam_r, name, "hauser");
+ expect_any(__wrap_getpwnam_r, pwd);
+ expect_any(__wrap_getpwnam_r, buf);
+ expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN);
+ expect_any(__wrap_getpwnam_r, result);
+ will_return(__wrap_getpwnam_r, 0);
+ will_return(__wrap_getpwnam_r, NULL);
+
+ assert_int_equal(crm_user_lookup("hauser", &uid, &gid), -EINVAL);
+
+ pcmk__mock_getpwnam_r = false;
+}
+
+static void
+entry_found(void **state)
+{
+ uid_t uid;
+ gid_t gid;
+
+ /* We don't care about any of the other fields of the password entry, so just
+ * leave them blank.
+ */
+ struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 };
+
+ /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */
+
+ // Set getpwnam_r() return value and result parameter
+ pcmk__mock_getpwnam_r = true;
+
+ expect_string(__wrap_getpwnam_r, name, "hauser");
+ expect_any(__wrap_getpwnam_r, pwd);
+ expect_any(__wrap_getpwnam_r, buf);
+ expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN);
+ expect_any(__wrap_getpwnam_r, result);
+ will_return(__wrap_getpwnam_r, 0);
+ will_return(__wrap_getpwnam_r, &returned_ent);
+
+ assert_int_equal(crm_user_lookup("hauser", NULL, NULL), 0);
+
+ /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */
+
+ // Set getpwnam_r() return value and result parameter
+ expect_string(__wrap_getpwnam_r, name, "hauser");
+ expect_any(__wrap_getpwnam_r, pwd);
+ expect_any(__wrap_getpwnam_r, buf);
+ expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN);
+ expect_any(__wrap_getpwnam_r, result);
+ will_return(__wrap_getpwnam_r, 0);
+ will_return(__wrap_getpwnam_r, &returned_ent);
+
+ assert_int_equal(crm_user_lookup("hauser", &uid, &gid), 0);
+ assert_int_equal(uid, 1000);
+ assert_int_equal(gid, 1000);
+
+ pcmk__mock_getpwnam_r = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(calloc_fails),
+ cmocka_unit_test(getpwnam_r_fails),
+ cmocka_unit_test(no_matching_pwent),
+ cmocka_unit_test(entry_found))
diff --git a/lib/common/tests/utils/pcmk__getpid_s_test.c b/lib/common/tests/utils/pcmk__getpid_s_test.c
new file mode 100644
index 0000000..20ba36a
--- /dev/null
+++ b/lib/common/tests/utils/pcmk__getpid_s_test.c
@@ -0,0 +1,38 @@
+/*
+ * 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 "mock_private.h"
+
+#include <sys/types.h>
+#include <unistd.h>
+
+static void
+pcmk__getpid_s_test(void **state)
+{
+ char *retval;
+
+ // Set getpid() return value
+ pcmk__mock_getpid = true;
+ will_return(__wrap_getpid, 1234);
+
+ retval = pcmk__getpid_s();
+ assert_non_null(retval);
+ assert_string_equal("1234", retval);
+
+ free(retval);
+
+ pcmk__mock_getpid = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(pcmk__getpid_s_test))
diff --git a/lib/common/tests/utils/pcmk_daemon_user_test.c b/lib/common/tests/utils/pcmk_daemon_user_test.c
new file mode 100644
index 0000000..a63ca73
--- /dev/null
+++ b/lib/common/tests/utils/pcmk_daemon_user_test.c
@@ -0,0 +1,83 @@
+/*
+ * 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 "crmcommon_private.h"
+#include "mock_private.h"
+
+#include <pwd.h>
+#include <sys/types.h>
+
+static void
+no_matching_pwent(void **state)
+{
+ uid_t uid;
+ gid_t gid;
+
+ // Set getpwnam_r() return value and result parameter
+ pcmk__mock_getpwnam_r = true;
+
+ expect_string(__wrap_getpwnam_r, name, "hacluster");
+ expect_any(__wrap_getpwnam_r, pwd);
+ expect_any(__wrap_getpwnam_r, buf);
+ expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN);
+ expect_any(__wrap_getpwnam_r, result);
+ will_return(__wrap_getpwnam_r, ENOENT);
+ will_return(__wrap_getpwnam_r, NULL);
+
+ assert_int_equal(pcmk_daemon_user(&uid, &gid), -ENOENT);
+
+ pcmk__mock_getpwnam_r = false;
+}
+
+static void
+entry_found(void **state)
+{
+ uid_t uid;
+ gid_t gid;
+
+ /* We don't care about any of the other fields of the password entry, so just
+ * leave them blank.
+ */
+ struct passwd returned_ent = { .pw_uid = 1000, .pw_gid = 1000 };
+
+ /* Test getpwnam_r returning a valid passwd entry, but we don't pass uid or gid. */
+
+ // Set getpwnam_r() return value and result parameter
+ pcmk__mock_getpwnam_r = true;
+
+ expect_string(__wrap_getpwnam_r, name, "hacluster");
+ expect_any(__wrap_getpwnam_r, pwd);
+ expect_any(__wrap_getpwnam_r, buf);
+ expect_value(__wrap_getpwnam_r, buflen, PCMK__PW_BUFFER_LEN);
+ expect_any(__wrap_getpwnam_r, result);
+ will_return(__wrap_getpwnam_r, 0);
+ will_return(__wrap_getpwnam_r, &returned_ent);
+
+ assert_int_equal(pcmk_daemon_user(NULL, NULL), 0);
+
+ /* Test getpwnam_r returning a valid passwd entry, and we do pass uid and gid. */
+
+ /* We don't need to call will_return() again because pcmk_daemon_user()
+ * will have cached the uid/gid from the previous call and won't make
+ * another call to getpwnam_r().
+ */
+ assert_int_equal(pcmk_daemon_user(&uid, &gid), 0);
+ assert_int_equal(uid, 1000);
+ assert_int_equal(gid, 1000);
+
+ pcmk__mock_getpwnam_r = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(no_matching_pwent),
+ cmocka_unit_test(entry_found))
diff --git a/lib/common/tests/utils/pcmk_hostname_test.c b/lib/common/tests/utils/pcmk_hostname_test.c
new file mode 100644
index 0000000..7329486
--- /dev/null
+++ b/lib/common/tests/utils/pcmk_hostname_test.c
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2021 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 "mock_private.h"
+
+#include <sys/utsname.h>
+
+static void
+uname_succeeded_test(void **state)
+{
+ char *retval;
+
+ // Set uname() return value and buf parameter node name
+ pcmk__mock_uname = true;
+
+ expect_any(__wrap_uname, buf);
+ will_return(__wrap_uname, 0);
+ will_return(__wrap_uname, "somename");
+
+ retval = pcmk_hostname();
+ assert_non_null(retval);
+ assert_string_equal("somename", retval);
+
+ free(retval);
+
+ pcmk__mock_uname = false;
+}
+
+static void
+uname_failed_test(void **state)
+{
+ // Set uname() return value and buf parameter node name
+ pcmk__mock_uname = true;
+
+ expect_any(__wrap_uname, buf);
+ will_return(__wrap_uname, -1);
+ will_return(__wrap_uname, NULL);
+
+ assert_null(pcmk_hostname());
+
+ pcmk__mock_uname = false;
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(uname_succeeded_test),
+ cmocka_unit_test(uname_failed_test))
diff --git a/lib/common/tests/utils/pcmk_str_is_infinity_test.c b/lib/common/tests/utils/pcmk_str_is_infinity_test.c
new file mode 100644
index 0000000..fff58ab
--- /dev/null
+++ b/lib/common/tests/utils/pcmk_str_is_infinity_test.c
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+uppercase_str_passes(void **state)
+{
+ assert_true(pcmk_str_is_infinity("INFINITY"));
+ assert_true(pcmk_str_is_infinity("+INFINITY"));
+}
+
+static void
+mixed_case_str_fails(void **state)
+{
+ assert_false(pcmk_str_is_infinity("infinity"));
+ assert_false(pcmk_str_is_infinity("+infinity"));
+ assert_false(pcmk_str_is_infinity("Infinity"));
+ assert_false(pcmk_str_is_infinity("+Infinity"));
+}
+
+static void
+added_whitespace_fails(void **state)
+{
+ assert_false(pcmk_str_is_infinity(" INFINITY"));
+ assert_false(pcmk_str_is_infinity("INFINITY "));
+ assert_false(pcmk_str_is_infinity(" INFINITY "));
+ assert_false(pcmk_str_is_infinity("+ INFINITY"));
+}
+
+static void
+empty_str_fails(void **state)
+{
+ assert_false(pcmk_str_is_infinity(NULL));
+ assert_false(pcmk_str_is_infinity(""));
+}
+
+static void
+minus_infinity_fails(void **state)
+{
+ assert_false(pcmk_str_is_infinity("-INFINITY"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(uppercase_str_passes),
+ cmocka_unit_test(mixed_case_str_fails),
+ cmocka_unit_test(added_whitespace_fails),
+ cmocka_unit_test(empty_str_fails),
+ cmocka_unit_test(minus_infinity_fails))
diff --git a/lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c b/lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c
new file mode 100644
index 0000000..477b055
--- /dev/null
+++ b/lib/common/tests/utils/pcmk_str_is_minus_infinity_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2020-2021 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>
+
+static void
+uppercase_str_passes(void **state)
+{
+ assert_true(pcmk_str_is_minus_infinity("-INFINITY"));
+}
+
+static void
+mixed_case_str_fails(void **state)
+{
+ assert_false(pcmk_str_is_minus_infinity("-infinity"));
+ assert_false(pcmk_str_is_minus_infinity("-Infinity"));
+}
+
+static void
+added_whitespace_fails(void **state)
+{
+ assert_false(pcmk_str_is_minus_infinity(" -INFINITY"));
+ assert_false(pcmk_str_is_minus_infinity("-INFINITY "));
+ assert_false(pcmk_str_is_minus_infinity(" -INFINITY "));
+ assert_false(pcmk_str_is_minus_infinity("- INFINITY"));
+}
+
+static void
+empty_str_fails(void **state)
+{
+ assert_false(pcmk_str_is_minus_infinity(NULL));
+ assert_false(pcmk_str_is_minus_infinity(""));
+}
+
+static void
+infinity_fails(void **state)
+{
+ assert_false(pcmk_str_is_minus_infinity("INFINITY"));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(uppercase_str_passes),
+ cmocka_unit_test(mixed_case_str_fails),
+ cmocka_unit_test(added_whitespace_fails),
+ cmocka_unit_test(empty_str_fails),
+ cmocka_unit_test(infinity_fails))
diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am
new file mode 100644
index 0000000..0ccdcc3
--- /dev/null
+++ b/lib/common/tests/xml/Makefile.am
@@ -0,0 +1,17 @@
+#
+# 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 $(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__xe_foreach_child_test \
+ pcmk__xe_match_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
new file mode 100644
index 0000000..9bcba87
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
@@ -0,0 +1,215 @@
+/*
+ * 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/xml_internal.h>
+
+static int compare_name_handler(xmlNode *xml, void *userdata) {
+ function_called();
+ assert_string_equal((char *) userdata, crm_element_name(xml));
+ return pcmk_rc_ok;
+}
+
+const char *str1 =
+ "<xml>\n"
+ " <!-- This is a level 1 node -->\n"
+ " <level1>\n"
+ " content\n"
+ " </level1>\n"
+ " <!-- This is a level 1 node -->\n"
+ " <level1>\n"
+ " content\n"
+ " </level1>\n"
+ " <!-- This is a level 1 node -->\n"
+ " <level1>\n"
+ " content\n"
+ " </level1>\n"
+ "</xml>";
+
+static void
+bad_input(void **state) {
+ xmlNode *xml = string2xml(str1);
+
+ pcmk__assert_asserts(pcmk__xe_foreach_child(xml, NULL, NULL, NULL));
+
+ free_xml(xml);
+}
+
+static void
+name_given_test(void **state) {
+ xmlNode *xml = string2xml(str1);
+
+ /* The handler should be called once for every <level1> node. */
+ expect_function_call(compare_name_handler);
+ expect_function_call(compare_name_handler);
+ expect_function_call(compare_name_handler);
+
+ pcmk__xe_foreach_child(xml, "level1", compare_name_handler, (void *) "level1");
+ free_xml(xml);
+}
+
+static void
+no_name_given_test(void **state) {
+ xmlNode *xml = string2xml(str1);
+
+ /* The handler should be called once for every <level1> node. */
+ expect_function_call(compare_name_handler);
+ expect_function_call(compare_name_handler);
+ expect_function_call(compare_name_handler);
+
+ pcmk__xe_foreach_child(xml, NULL, compare_name_handler, (void *) "level1");
+ free_xml(xml);
+}
+
+static void
+name_doesnt_exist_test(void **state) {
+ xmlNode *xml = string2xml(str1);
+ pcmk__xe_foreach_child(xml, "xxx", compare_name_handler, NULL);
+ free_xml(xml);
+}
+
+const char *str2 =
+ "<xml>\n"
+ " <level1>\n"
+ " <!-- Inside a level 1 node -->\n"
+ " <level2>\n"
+ " <!-- Inside a level 2 node -->\n"
+ " </level2>\n"
+ " </level1>\n"
+ " <level1>\n"
+ " <!-- Inside a level 1 node -->\n"
+ " <level2>\n"
+ " <!-- Inside a level 2 node -->\n"
+ " <level3>\n"
+ " <!-- Inside a level 3 node -->\n"
+ " </level3>\n"
+ " </level2>\n"
+ " <level2>\n"
+ " <!-- Inside a level 2 node -->\n"
+ " </level2>\n"
+ " </level1>\n"
+ "</xml>";
+
+static void
+multiple_levels_test(void **state) {
+ xmlNode *xml = string2xml(str2);
+
+ /* The handler should be called once for every <level1> node. */
+ expect_function_call(compare_name_handler);
+ expect_function_call(compare_name_handler);
+
+ pcmk__xe_foreach_child(xml, "level1", compare_name_handler, (void *) "level1");
+ free_xml(xml);
+}
+
+static void
+multiple_levels_no_name_test(void **state) {
+ xmlNode *xml = string2xml(str2);
+
+ /* The handler should be called once for every <level1> node. */
+ expect_function_call(compare_name_handler);
+ expect_function_call(compare_name_handler);
+
+ pcmk__xe_foreach_child(xml, NULL, compare_name_handler, (void *) "level1");
+ free_xml(xml);
+}
+
+const char *str3 =
+ "<xml>\n"
+ " <!-- This is node #1 -->\n"
+ " <node1>\n"
+ " content\n"
+ " </node1>\n"
+ " <!-- This is node #2 -->\n"
+ " <node2>\n"
+ " content\n"
+ " </node2>\n"
+ " <!-- This is node #3 -->\n"
+ " <node3>\n"
+ " content\n"
+ " </node3>\n"
+ "</xml>";
+
+static int any_of_handler(xmlNode *xml, void *userdata) {
+ function_called();
+ assert_true(pcmk__str_any_of(crm_element_name(xml), "node1", "node2", "node3", NULL));
+ return pcmk_rc_ok;
+}
+
+static void
+any_of_test(void **state) {
+ xmlNode *xml = string2xml(str3);
+
+ /* The handler should be called once for every <nodeX> node. */
+ expect_function_call(any_of_handler);
+ expect_function_call(any_of_handler);
+ expect_function_call(any_of_handler);
+
+ pcmk__xe_foreach_child(xml, NULL, any_of_handler, NULL);
+ free_xml(xml);
+}
+
+static int stops_on_first_handler(xmlNode *xml, void *userdata) {
+ function_called();
+
+ if (pcmk__str_eq(crm_element_name(xml), "node1", pcmk__str_none)) {
+ return pcmk_rc_error;
+ } else {
+ return pcmk_rc_ok;
+ }
+}
+
+static int stops_on_second_handler(xmlNode *xml, void *userdata) {
+ function_called();
+
+ if (pcmk__str_eq(crm_element_name(xml), "node2", pcmk__str_none)) {
+ return pcmk_rc_error;
+ } else {
+ return pcmk_rc_ok;
+ }
+}
+
+static int stops_on_third_handler(xmlNode *xml, void *userdata) {
+ function_called();
+
+ if (pcmk__str_eq(crm_element_name(xml), "node3", pcmk__str_none)) {
+ return pcmk_rc_error;
+ } else {
+ return pcmk_rc_ok;
+ }
+}
+
+static void
+one_of_test(void **state) {
+ xmlNode *xml = string2xml(str3);
+
+ /* The handler should be called once. */
+ expect_function_call(stops_on_first_handler);
+ assert_int_equal(pcmk__xe_foreach_child(xml, "node1", stops_on_first_handler, NULL), pcmk_rc_error);
+
+ expect_function_call(stops_on_second_handler);
+ assert_int_equal(pcmk__xe_foreach_child(xml, "node2", stops_on_second_handler, NULL), pcmk_rc_error);
+
+ expect_function_call(stops_on_third_handler);
+ assert_int_equal(pcmk__xe_foreach_child(xml, "node3", stops_on_third_handler, NULL), pcmk_rc_error);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(name_given_test),
+ cmocka_unit_test(no_name_given_test),
+ cmocka_unit_test(name_doesnt_exist_test),
+ cmocka_unit_test(multiple_levels_test),
+ cmocka_unit_test(multiple_levels_no_name_test),
+ cmocka_unit_test(any_of_test),
+ cmocka_unit_test(one_of_test))
diff --git a/lib/common/tests/xml/pcmk__xe_match_test.c b/lib/common/tests/xml/pcmk__xe_match_test.c
new file mode 100644
index 0000000..be2c949
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xe_match_test.c
@@ -0,0 +1,106 @@
+/*
+ * 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/msg_xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+const char *str1 =
+ "<xml>\n"
+ " <!-- This is an A node -->\n"
+ " <nodeA attrA=\"123\" " XML_ATTR_ID "=\"1\">\n"
+ " content\n"
+ " </nodeA>\n"
+ " <!-- This is an A node -->\n"
+ " <nodeA attrA=\"456\" " XML_ATTR_ID "=\"2\">\n"
+ " content\n"
+ " </nodeA>\n"
+ " <!-- This is an A node -->\n"
+ " <nodeA attrB=\"XYZ\" " XML_ATTR_ID "=\"3\">\n"
+ " content\n"
+ " </nodeA>\n"
+ " <!-- This is a B node -->\n"
+ " <nodeB attrA=\"123\" " XML_ATTR_ID "=\"4\">\n"
+ " content\n"
+ " </nodeA>\n"
+ " <!-- This is a B node -->\n"
+ " <nodeB attrB=\"ABC\" " XML_ATTR_ID "=\"5\">\n"
+ " content\n"
+ " </nodeA>\n"
+ "</xml>";
+
+static void
+bad_input(void **state) {
+ xmlNode *xml = string2xml(str1);
+
+ assert_null(pcmk__xe_match(NULL, NULL, NULL, NULL));
+ assert_null(pcmk__xe_match(NULL, NULL, NULL, "attrX"));
+
+ free_xml(xml);
+}
+
+static void
+not_found(void **state) {
+ xmlNode *xml = string2xml(str1);
+
+ /* No node with an attrX attribute */
+ assert_null(pcmk__xe_match(xml, NULL, "attrX", NULL));
+ /* No nodeX node */
+ assert_null(pcmk__xe_match(xml, "nodeX", NULL, NULL));
+ /* No nodeA node with attrX */
+ assert_null(pcmk__xe_match(xml, "nodeA", "attrX", NULL));
+ /* No nodeA node with attrA=XYZ */
+ assert_null(pcmk__xe_match(xml, "nodeA", "attrA", "XYZ"));
+
+ free_xml(xml);
+}
+
+static void
+find_attrB(void **state) {
+ xmlNode *xml = string2xml(str1);
+ xmlNode *result = NULL;
+
+ /* Find the first node with attrB */
+ result = pcmk__xe_match(xml, NULL, "attrB", NULL);
+ assert_non_null(result);
+ assert_string_equal(crm_element_value(result, "id"), "3");
+
+ /* Find the first nodeB with attrB */
+ result = pcmk__xe_match(xml, "nodeB", "attrB", NULL);
+ assert_non_null(result);
+ assert_string_equal(crm_element_value(result, "id"), "5");
+
+ free_xml(xml);
+}
+
+static void
+find_attrA_matching(void **state) {
+ xmlNode *xml = string2xml(str1);
+ xmlNode *result = NULL;
+
+ /* Find attrA=456 */
+ result = pcmk__xe_match(xml, NULL, "attrA", "456");
+ assert_non_null(result);
+ assert_string_equal(crm_element_value(result, "id"), "2");
+
+ /* Find a nodeB with attrA=123 */
+ result = pcmk__xe_match(xml, "nodeB", "attrA", "123");
+ assert_non_null(result);
+ assert_string_equal(crm_element_value(result, "id"), "4");
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(not_found),
+ cmocka_unit_test(find_attrB),
+ cmocka_unit_test(find_attrA_matching));
diff --git a/lib/common/tests/xpath/Makefile.am b/lib/common/tests/xpath/Makefile.am
new file mode 100644
index 0000000..94abeee
--- /dev/null
+++ b/lib/common/tests/xpath/Makefile.am
@@ -0,0 +1,16 @@
+#
+# 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__xpath_node_id_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
new file mode 100644
index 0000000..3922b34
--- /dev/null
+++ b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
@@ -0,0 +1,59 @@
+/*
+ * 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 <crm_internal.h>
+
+#include <crm/msg_xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+static void
+empty_input(void **state) {
+ assert_null(pcmk__xpath_node_id(NULL, "lrm"));
+ assert_null(pcmk__xpath_node_id("", "lrm"));
+ assert_null(pcmk__xpath_node_id("/blah/blah", NULL));
+ assert_null(pcmk__xpath_node_id("/blah/blah", ""));
+ assert_null(pcmk__xpath_node_id(NULL, NULL));
+}
+
+static void
+no_quotes(void **state) {
+ const char *xpath = "/some/xpath/lrm[@" XML_ATTR_ID "=xyz]";
+ pcmk__assert_asserts(pcmk__xpath_node_id(xpath, "lrm"));
+}
+
+static void
+not_present(void **state) {
+ const char *xpath = "/some/xpath/string[@" XML_ATTR_ID "='xyz']";
+ assert_null(pcmk__xpath_node_id(xpath, "lrm"));
+
+ xpath = "/some/xpath/containing[@" XML_ATTR_ID "='lrm']";
+ assert_null(pcmk__xpath_node_id(xpath, "lrm"));
+}
+
+static void
+present(void **state) {
+ char *s = NULL;
+ const char *xpath = "/some/xpath/containing/lrm[@" XML_ATTR_ID "='xyz']";
+
+ s = pcmk__xpath_node_id(xpath, "lrm");
+ assert_int_equal(strcmp(s, "xyz"), 0);
+ free(s);
+
+ xpath = "/some/other/lrm[@" XML_ATTR_ID "='xyz']/xpath";
+ s = pcmk__xpath_node_id(xpath, "lrm");
+ assert_int_equal(strcmp(s, "xyz"), 0);
+ free(s);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_input),
+ cmocka_unit_test(no_quotes),
+ cmocka_unit_test(not_present),
+ cmocka_unit_test(present))