summaryrefslogtreecommitdiffstats
path: root/lib/common/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-03 13:39:28 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-06-03 13:39:28 +0000
commit924f5ea83e48277e014ebf0d19a27187cb93e2f7 (patch)
tree75920a275bba045f6d108204562c218a9a26ea15 /lib/common/tests
parentAdding upstream version 2.1.7. (diff)
downloadpacemaker-upstream.tar.xz
pacemaker-upstream.zip
Adding upstream version 2.1.8~rc1.upstream/2.1.8_rc1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'lib/common/tests')
-rw-r--r--lib/common/tests/Makefile.am8
-rw-r--r--lib/common/tests/acl/xml_acl_denied_test.c10
-rw-r--r--lib/common/tests/acl/xml_acl_enabled_test.c10
-rw-r--r--lib/common/tests/actions/Makefile.am10
-rw-r--r--lib/common/tests/actions/copy_in_properties_test.c62
-rw-r--r--lib/common/tests/actions/expand_plus_plus_test.c256
-rw-r--r--lib/common/tests/actions/fix_plus_plus_recursive_test.c47
-rw-r--r--lib/common/tests/actions/pcmk_xe_is_probe_test.c43
-rw-r--r--lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c150
-rw-r--r--lib/common/tests/health/pcmk__parse_health_strategy_test.c2
-rw-r--r--lib/common/tests/health/pcmk__validate_health_strategy_test.c2
-rw-r--r--lib/common/tests/io/pcmk__full_path_test.c10
-rw-r--r--lib/common/tests/iso8601/Makefile.am6
-rw-r--r--lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c243
-rw-r--r--lib/common/tests/iso8601/pcmk__readable_interval_test.c4
-rw-r--r--lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c80
-rw-r--r--lib/common/tests/nodes/Makefile.am23
-rw-r--r--lib/common/tests/nodes/pcmk__find_node_in_list_test.c53
-rw-r--r--lib/common/tests/nodes/pcmk__xe_add_node_test.c71
-rw-r--r--lib/common/tests/nodes/pcmk_foreach_active_resource_test.c149
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_clean_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_online_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_pending_test.c54
-rw-r--r--lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c54
-rw-r--r--lib/common/tests/nvpair/Makefile.am7
-rw-r--r--lib/common/tests/nvpair/crm_meta_name_test.c (renamed from lib/common/tests/utils/crm_meta_name_test.c)10
-rw-r--r--lib/common/tests/nvpair/crm_meta_value_test.c (renamed from lib/common/tests/utils/crm_meta_value_test.c)18
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c10
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c11
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c108
-rw-r--r--lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c12
-rw-r--r--lib/common/tests/probes/Makefile.am18
-rw-r--r--lib/common/tests/probes/pcmk_is_probe_test.c (renamed from lib/common/tests/actions/pcmk_is_probe_test.c)2
-rw-r--r--lib/common/tests/probes/pcmk_xe_is_probe_test.c54
-rw-r--r--lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c333
-rw-r--r--lib/common/tests/procfs/pcmk__procfs_pid2path_test.c8
-rw-r--r--lib/common/tests/resources/Makefile.am17
-rw-r--r--lib/common/tests/resources/pcmk_resource_id_test.c36
-rw-r--r--lib/common/tests/resources/pcmk_resource_is_managed_test.c46
-rw-r--r--lib/common/tests/rules/Makefile.am29
-rw-r--r--lib/common/tests/rules/pcmk__cmp_by_type_test.c102
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c831
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_condition_test.c197
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_date_expression_test.c684
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_date_spec_test.c231
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_op_expression_test.c207
-rw-r--r--lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c227
-rw-r--r--lib/common/tests/rules/pcmk__parse_combine_test.c52
-rw-r--r--lib/common/tests/rules/pcmk__parse_comparison_test.c72
-rw-r--r--lib/common/tests/rules/pcmk__parse_source_test.c62
-rw-r--r--lib/common/tests/rules/pcmk__parse_type_test.c127
-rw-r--r--lib/common/tests/rules/pcmk__replace_submatches_test.c81
-rw-r--r--lib/common/tests/rules/pcmk__unpack_duration_test.c120
-rw-r--r--lib/common/tests/rules/pcmk_evaluate_rule_test.c379
-rw-r--r--lib/common/tests/scheduler/Makefile.am19
-rw-r--r--lib/common/tests/scheduler/pcmk_get_dc_test.c47
-rw-r--r--lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c34
-rw-r--r--lib/common/tests/scheduler/pcmk_has_quorum_test.c36
-rw-r--r--lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c71
-rw-r--r--lib/common/tests/schemas/Makefile.am88
-rw-r--r--lib/common/tests/schemas/crm_schema_init_test.c152
-rw-r--r--lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c158
-rw-r--r--lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c121
-rw-r--r--lib/common/tests/schemas/pcmk__find_x_0_schema_test.c100
-rw-r--r--lib/common/tests/schemas/pcmk__get_schema_test.c81
-rw-r--r--lib/common/tests/schemas/pcmk__schema_files_later_than_test.c106
-rw-r--r--lib/common/tests/scores/char2score_test.c14
-rw-r--r--lib/common/tests/scores/pcmk__add_scores_test.c69
-rw-r--r--lib/common/tests/scores/pcmk_readable_score_test.c10
-rw-r--r--lib/common/tests/strings/Makefile.am3
-rw-r--r--lib/common/tests/strings/crm_get_msec_test.c30
-rw-r--r--lib/common/tests/strings/crm_str_to_boolean_test.c16
-rw-r--r--lib/common/tests/strings/pcmk__char_in_any_str_test.c46
-rw-r--r--lib/common/tests/strings/pcmk__compress_test.c11
-rw-r--r--lib/common/tests/strings/pcmk__str_update_test.c3
-rw-r--r--lib/common/tests/utils/Makefile.am11
-rw-r--r--lib/common/tests/utils/compare_version_test.c5
-rw-r--r--lib/common/tests/utils/pcmk__realloc_test.c69
-rw-r--r--lib/common/tests/utils/pcmk_hostname_test.c56
-rw-r--r--lib/common/tests/xml/Makefile.am11
-rw-r--r--lib/common/tests/xml/crm_xml_init_test.c230
-rw-r--r--lib/common/tests/xml/pcmk__xe_copy_attrs_test.c188
-rw-r--r--lib/common/tests/xml/pcmk__xe_first_child_test.c (renamed from lib/common/tests/xml/pcmk__xe_match_test.c)52
-rw-r--r--lib/common/tests/xml/pcmk__xe_foreach_child_test.c20
-rw-r--r--lib/common/tests/xml/pcmk__xe_set_score_test.c188
-rw-r--r--lib/common/tests/xml/pcmk__xml_escape_test.c213
-rw-r--r--lib/common/tests/xml/pcmk__xml_needs_escape_test.c337
-rw-r--r--lib/common/tests/xpath/pcmk__xpath_node_id_test.c29
89 files changed, 7396 insertions, 828 deletions
diff --git a/lib/common/tests/Makefile.am b/lib/common/tests/Makefile.am
index c0407e5..bde6605 100644
--- a/lib/common/tests/Makefile.am
+++ b/lib/common/tests/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -17,10 +17,16 @@ SUBDIRS = \
io \
iso8601 \
lists \
+ nodes \
nvpair \
options \
output \
+ probes \
+ resources \
results \
+ rules \
+ scheduler \
+ schemas \
scores \
strings \
utils \
diff --git a/lib/common/tests/acl/xml_acl_denied_test.c b/lib/common/tests/acl/xml_acl_denied_test.c
index faf2a39..7c5457e 100644
--- a/lib/common/tests/acl/xml_acl_denied_test.c
+++ b/lib/common/tests/acl/xml_acl_denied_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2021 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -17,7 +17,7 @@
static void
is_xml_acl_denied_without_node(void **state)
{
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
assert_false(xml_acl_denied(test_xml));
test_xml->doc->_private = NULL;
@@ -35,10 +35,10 @@ is_xml_acl_denied_with_node(void **state)
{
xml_doc_private_t *docpriv;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
// allocate memory for _private, which is NULL by default
- test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t));
+ test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
assert_false(xml_acl_denied(test_xml));
@@ -56,6 +56,6 @@ is_xml_acl_denied_with_node(void **state)
assert_true(xml_acl_denied(test_xml));
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, 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
index 28665f4..97e361e 100644
--- a/lib/common/tests/acl/xml_acl_enabled_test.c
+++ b/lib/common/tests/acl/xml_acl_enabled_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2021 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -17,7 +17,7 @@
static void
is_xml_acl_enabled_without_node(void **state)
{
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
assert_false(xml_acl_enabled(test_xml));
test_xml->doc->_private = NULL;
@@ -35,10 +35,10 @@ is_xml_acl_enabled_with_node(void **state)
{
xml_doc_private_t *docpriv;
- xmlNode *test_xml = create_xml_node(NULL, "test_xml");
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
// allocate memory for _private, which is NULL by default
- test_xml->doc->_private = calloc(1, sizeof(xml_doc_private_t));
+ test_xml->doc->_private = pcmk__assert_alloc(1, sizeof(xml_doc_private_t));
assert_false(xml_acl_enabled(test_xml));
@@ -56,6 +56,6 @@ is_xml_acl_enabled_with_node(void **state)
assert_true(xml_acl_enabled(test_xml));
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, 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/actions/Makefile.am b/lib/common/tests/actions/Makefile.am
index 6890b84..0dfe521 100644
--- a/lib/common/tests/actions/Makefile.am
+++ b/lib/common/tests/actions/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,12 +11,6 @@ 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
+check_PROGRAMS = parse_op_key_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/actions/copy_in_properties_test.c b/lib/common/tests/actions/copy_in_properties_test.c
deleted file mode 100644
index 7882551..0000000
--- a/lib/common/tests/actions/copy_in_properties_test.c
+++ /dev/null
@@ -1,62 +0,0 @@
- /*
- * 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/actions/expand_plus_plus_test.c b/lib/common/tests/actions/expand_plus_plus_test.c
deleted file mode 100644
index 41471f9..0000000
--- a/lib/common/tests/actions/expand_plus_plus_test.c
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * 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/actions/fix_plus_plus_recursive_test.c b/lib/common/tests/actions/fix_plus_plus_recursive_test.c
deleted file mode 100644
index b3c7cc2..0000000
--- a/lib/common/tests/actions/fix_plus_plus_recursive_test.c
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * 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/actions/pcmk_xe_is_probe_test.c b/lib/common/tests/actions/pcmk_xe_is_probe_test.c
deleted file mode 100644
index 62b21d9..0000000
--- a/lib/common/tests/actions/pcmk_xe_is_probe_test.c
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * 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/actions/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c
deleted file mode 100644
index 9e38019..0000000
--- a/lib/common/tests/actions/pcmk_xe_mask_probe_failure_test.c
+++ /dev/null
@@ -1,150 +0,0 @@
-/*
- * 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/health/pcmk__parse_health_strategy_test.c b/lib/common/tests/health/pcmk__parse_health_strategy_test.c
index 28cc702..197ad1f 100644
--- a/lib/common/tests/health/pcmk__parse_health_strategy_test.c
+++ b/lib/common/tests/health/pcmk__parse_health_strategy_test.c
@@ -16,7 +16,7 @@ valid(void **state) {
assert_int_equal(pcmk__parse_health_strategy(NULL),
pcmk__health_strategy_none);
- assert_int_equal(pcmk__parse_health_strategy("none"),
+ assert_int_equal(pcmk__parse_health_strategy(PCMK_VALUE_NONE),
pcmk__health_strategy_none);
assert_int_equal(pcmk__parse_health_strategy("NONE"),
diff --git a/lib/common/tests/health/pcmk__validate_health_strategy_test.c b/lib/common/tests/health/pcmk__validate_health_strategy_test.c
index c7c60aa..e706185 100644
--- a/lib/common/tests/health/pcmk__validate_health_strategy_test.c
+++ b/lib/common/tests/health/pcmk__validate_health_strategy_test.c
@@ -15,7 +15,7 @@
static void
valid_strategy(void **state) {
- assert_true(pcmk__validate_health_strategy("none"));
+ assert_true(pcmk__validate_health_strategy(PCMK_VALUE_NONE));
assert_true(pcmk__validate_health_strategy("None"));
assert_true(pcmk__validate_health_strategy("NONE"));
assert_true(pcmk__validate_health_strategy("NoNe"));
diff --git a/lib/common/tests/io/pcmk__full_path_test.c b/lib/common/tests/io/pcmk__full_path_test.c
index dbbd71b..2f514aa 100644
--- a/lib/common/tests/io/pcmk__full_path_test.c
+++ b/lib/common/tests/io/pcmk__full_path_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2020-2022 the Pacemaker project contributors
+ * Copyright 2020-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -18,8 +18,13 @@ function_asserts(void **state)
{
pcmk__assert_asserts(pcmk__full_path(NULL, "/dir"));
pcmk__assert_asserts(pcmk__full_path("file", NULL));
+}
- pcmk__assert_asserts(
+static void
+function_exits(void **state)
+{
+ pcmk__assert_exits(
+ CRM_EX_OSERR,
{
pcmk__mock_strdup = true; // strdup() will return NULL
expect_string(__wrap_strdup, s, "/full/path");
@@ -49,4 +54,5 @@ full_path(void **state)
PCMK__UNIT_TEST(NULL, NULL,
cmocka_unit_test(function_asserts),
+ cmocka_unit_test(function_exits),
cmocka_unit_test(full_path))
diff --git a/lib/common/tests/iso8601/Makefile.am b/lib/common/tests/iso8601/Makefile.am
index 5187aec..581115a 100644
--- a/lib/common/tests/iso8601/Makefile.am
+++ b/lib/common/tests/iso8601/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2022 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,6 +11,8 @@ 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
+check_PROGRAMS = pcmk__add_time_from_xml_test \
+ pcmk__readable_interval_test \
+ pcmk__set_time_if_earlier_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c
new file mode 100644
index 0000000..60a71c0
--- /dev/null
+++ b/lib/common/tests/iso8601/pcmk__add_time_from_xml_test.c
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2024 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 <libxml/tree.h> // xmlNode
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include <crm/common/iso8601_internal.h>
+#include <crm/common/xml.h>
+#include "../../crmcommon_private.h"
+
+#define ALL_VALID "<duration id=\"duration1\" years=\"1\" months=\"2\" " \
+ "weeks=\"3\" days=\"-1\" hours=\"1\" minutes=\"1\" " \
+ "seconds=\"1\" />"
+
+#define YEARS_INVALID "<duration id=\"duration1\" years=\"not-a-number\" />"
+
+#define YEARS_TOO_BIG "<duration id=\"duration1\" years=\"2222222222\" />"
+
+#define YEARS_TOO_SMALL "<duration id=\"duration1\" years=\"-2222222222\" />"
+
+static void
+null_time_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_years, xml),
+ EINVAL);
+ free_xml(xml);
+}
+
+static void
+null_xml_ok(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, NULL),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+}
+
+static void
+invalid_component(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(NULL, pcmk__time_unknown, xml),
+ EINVAL);
+ free_xml(xml);
+}
+
+static void
+missing_attr(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+ xmlNode *xml = pcmk__xml_parse(YEARS_INVALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+invalid_attr(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+ xmlNode *xml = pcmk__xml_parse(YEARS_INVALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml),
+ pcmk_rc_unpack_error);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+out_of_range_attr(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = pcmk_copy_time(t);
+ xmlNode *xml = NULL;
+
+ xml = pcmk__xml_parse(YEARS_TOO_BIG);
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+ free_xml(xml);
+
+ xml = pcmk__xml_parse(YEARS_TOO_SMALL);
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml), ERANGE);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+ free_xml(xml);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+}
+
+static void
+add_years(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2025-01-01 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_years, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_months(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-03-01 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_months, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_weeks(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-22 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_weeks, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_days(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2023-12-31 15:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_days, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_hours(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-01 16:00:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_hours, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_minutes(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-01 15:01:00");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_minutes, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+add_seconds(void **state)
+{
+ crm_time_t *t = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *reference = crm_time_new("2024-01-01 15:00:01");
+ xmlNode *xml = pcmk__xml_parse(ALL_VALID);
+
+ assert_int_equal(pcmk__add_time_from_xml(t, pcmk__time_seconds, xml),
+ pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_time_invalid),
+ cmocka_unit_test(null_xml_ok),
+ cmocka_unit_test(invalid_component),
+ cmocka_unit_test(missing_attr),
+ cmocka_unit_test(invalid_attr),
+ cmocka_unit_test(out_of_range_attr),
+ cmocka_unit_test(add_years),
+ cmocka_unit_test(add_months),
+ cmocka_unit_test(add_weeks),
+ cmocka_unit_test(add_days),
+ cmocka_unit_test(add_hours),
+ cmocka_unit_test(add_minutes),
+ cmocka_unit_test(add_seconds));
diff --git a/lib/common/tests/iso8601/pcmk__readable_interval_test.c b/lib/common/tests/iso8601/pcmk__readable_interval_test.c
index 43b5541..d354975 100644
--- a/lib/common/tests/iso8601/pcmk__readable_interval_test.c
+++ b/lib/common/tests/iso8601/pcmk__readable_interval_test.c
@@ -17,9 +17,11 @@ static void
readable_interval(void **state)
{
assert_string_equal(pcmk__readable_interval(0), "0s");
+ assert_string_equal(pcmk__readable_interval(503), "503ms");
+ assert_string_equal(pcmk__readable_interval(3333), "3.333s");
assert_string_equal(pcmk__readable_interval(30000), "30s");
+ assert_string_equal(pcmk__readable_interval(61000), "1m1s");
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");
}
diff --git a/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c b/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c
new file mode 100644
index 0000000..c4bf014
--- /dev/null
+++ b/lib/common/tests/iso8601/pcmk__set_time_if_earlier_test.c
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2024 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/iso8601.h>
+#include "../../crmcommon_private.h"
+
+static void
+null_ok(void **state)
+{
+ crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00");
+ crm_time_t *target_copy = pcmk_copy_time(target);
+
+ // Should do nothing (just checking it doesn't assert or crash)
+ pcmk__set_time_if_earlier(NULL, NULL);
+ pcmk__set_time_if_earlier(NULL, target);
+
+ // Shouldn't assert, crash, or change target
+ pcmk__set_time_if_earlier(target, NULL);
+ assert_int_equal(crm_time_compare(target, target_copy), 0);
+
+ crm_time_free(target);
+ crm_time_free(target_copy);
+}
+
+static void
+target_undefined(void **state)
+{
+ crm_time_t *source = crm_time_new("2024-01-01 00:29:59 +01:00");
+ crm_time_t *target = crm_time_new_undefined();
+
+ pcmk__set_time_if_earlier(target, source);
+ assert_int_equal(crm_time_compare(target, source), 0);
+
+ crm_time_free(source);
+ crm_time_free(target);
+}
+
+static void
+source_earlier(void **state)
+{
+ crm_time_t *source = crm_time_new("2024-01-01 00:29:59 +01:00");
+ crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00");
+
+ pcmk__set_time_if_earlier(target, source);
+ assert_int_equal(crm_time_compare(target, source), 0);
+
+ crm_time_free(source);
+ crm_time_free(target);
+}
+
+static void
+source_later(void **state)
+{
+ crm_time_t *source = crm_time_new("2024-01-01 00:31:00 +01:00");
+ crm_time_t *target = crm_time_new("2024-01-01 00:30:00 +01:00");
+ crm_time_t *target_copy = pcmk_copy_time(target);
+
+ pcmk__set_time_if_earlier(target, source);
+ assert_int_equal(crm_time_compare(target, target_copy), 0);
+
+ crm_time_free(source);
+ crm_time_free(target);
+ crm_time_free(target_copy);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_ok),
+ cmocka_unit_test(target_undefined),
+ cmocka_unit_test(source_earlier),
+ cmocka_unit_test(source_later))
diff --git a/lib/common/tests/nodes/Makefile.am b/lib/common/tests/nodes/Makefile.am
new file mode 100644
index 0000000..f52c615
--- /dev/null
+++ b/lib/common/tests/nodes/Makefile.am
@@ -0,0 +1,23 @@
+#
+# Copyright 2024 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__find_node_in_list_test \
+ pcmk_foreach_active_resource_test \
+ pcmk_node_is_clean_test \
+ pcmk_node_is_in_maintenance_test \
+ pcmk_node_is_online_test \
+ pcmk_node_is_pending_test \
+ pcmk_node_is_shutting_down_test \
+ pcmk__xe_add_node_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/nodes/pcmk__find_node_in_list_test.c b/lib/common/tests/nodes/pcmk__find_node_in_list_test.c
new file mode 100644
index 0000000..7726bf5
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk__find_node_in_list_test.c
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2022-2024 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/pengine/internal.h>
+
+static void
+empty_list(void **state)
+{
+ assert_null(pcmk__find_node_in_list(NULL, NULL));
+ assert_null(pcmk__find_node_in_list(NULL, "cluster1"));
+}
+
+static void
+non_null_list(void **state)
+{
+ GList *nodes = NULL;
+
+ pcmk_node_t *a = pcmk__assert_alloc(1, sizeof(pcmk_node_t));
+ pcmk_node_t *b = pcmk__assert_alloc(1, sizeof(pcmk_node_t));
+
+ a->details = pcmk__assert_alloc(1, sizeof(struct pe_node_shared_s));
+ a->details->uname = "cluster1";
+ b->details = pcmk__assert_alloc(1, sizeof(struct pe_node_shared_s));
+ b->details->uname = "cluster2";
+
+ nodes = g_list_append(nodes, a);
+ nodes = g_list_append(nodes, b);
+
+ assert_ptr_equal(a, pcmk__find_node_in_list(nodes, "cluster1"));
+ assert_null(pcmk__find_node_in_list(nodes, "cluster10"));
+ assert_null(pcmk__find_node_in_list(nodes, "nodecluster1"));
+ assert_ptr_equal(b, pcmk__find_node_in_list(nodes, "CLUSTER2"));
+ assert_null(pcmk__find_node_in_list(nodes, "xyz"));
+
+ free(a->details);
+ free(a);
+ free(b->details);
+ free(b);
+ g_list_free(nodes);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(empty_list),
+ cmocka_unit_test(non_null_list))
diff --git a/lib/common/tests/nodes/pcmk__xe_add_node_test.c b/lib/common/tests/nodes/pcmk__xe_add_node_test.c
new file mode 100644
index 0000000..dd77527
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk__xe_add_node_test.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 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/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+static void
+bad_input(void **state) {
+ xmlNode *node = NULL;
+
+ pcmk__assert_asserts(pcmk__xe_add_node(NULL, NULL, 0));
+
+ node = pcmk__xe_create(NULL, "test");
+
+ pcmk__xe_add_node(node, NULL, 0);
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST));
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST_ID));
+
+ pcmk__xe_add_node(node, NULL, -100);
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST));
+ assert_null(xmlHasProp(node, (pcmkXmlStr) PCMK__XA_ATTR_HOST_ID));
+
+ free_xml(node);
+}
+
+static void
+expected_input(void **state) {
+ xmlNode *node = pcmk__xe_create(NULL, "test");
+ int i;
+
+ pcmk__xe_add_node(node, "somenode", 47);
+ assert_string_equal("somenode",
+ crm_element_value(node, PCMK__XA_ATTR_HOST));
+ assert_int_equal(pcmk_rc_ok,
+ crm_element_value_int(node, PCMK__XA_ATTR_HOST_ID, &i));
+ assert_int_equal(i, 47);
+
+ free_xml(node);
+}
+
+static void
+repeated_use(void **state) {
+ xmlNode *node = pcmk__xe_create(NULL, "test");
+ int i;
+
+ /* Later calls override settings from earlier calls. */
+ pcmk__xe_add_node(node, "nodeA", 1);
+ pcmk__xe_add_node(node, "nodeB", 2);
+ pcmk__xe_add_node(node, "nodeC", 3);
+
+ assert_string_equal("nodeC", crm_element_value(node, PCMK__XA_ATTR_HOST));
+ assert_int_equal(pcmk_rc_ok,
+ crm_element_value_int(node, PCMK__XA_ATTR_HOST_ID, &i));
+ assert_int_equal(i, 3);
+
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(bad_input),
+ cmocka_unit_test(expected_input),
+ cmocka_unit_test(repeated_use))
diff --git a/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c b/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c
new file mode 100644
index 0000000..2402789
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_foreach_active_resource_test.c
@@ -0,0 +1,149 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+#include <glib.h> // GList, TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/resources.h>
+#include <crm/common/unittest_internal.h>
+
+static int counter = 1;
+static int return_false = -1;
+
+static char rsc1_id[] = "rsc1";
+static char rsc2_id[] = "rsc2";
+static char rsc3_id[] = "rsc3";
+
+static pcmk_resource_t rsc1 = {
+ .id = rsc1_id,
+};
+static pcmk_resource_t rsc2 = {
+ .id = rsc2_id,
+};
+static pcmk_resource_t rsc3 = {
+ .id = rsc3_id,
+};
+
+static bool
+fn(pcmk_resource_t *rsc, void *user_data)
+{
+ char *expected_id = crm_strdup_printf("rsc%d", counter);
+
+ assert_string_equal(rsc->id, expected_id);
+ free(expected_id);
+
+ return counter++ != return_false;
+}
+
+static void
+null_args(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ counter = 1;
+
+ // These just test that it doesn't crash
+ pcmk_foreach_active_resource(NULL, NULL, NULL);
+ pcmk_foreach_active_resource(&node, NULL, NULL);
+
+ pcmk_foreach_active_resource(NULL, fn, NULL);
+ assert_int_equal(counter, 1);
+}
+
+static void
+list_of_0(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ counter = 1;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 1);
+}
+
+static void
+list_of_1(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc1);
+
+ counter = 1;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 2);
+
+ g_list_free(shared.running_rsc);
+}
+
+static void
+list_of_3(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc1);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc2);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc3);
+
+ counter = 1;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 4);
+
+ g_list_free(shared.running_rsc);
+}
+
+static void
+list_of_3_return_false(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .running_rsc = NULL,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc1);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc2);
+ shared.running_rsc = g_list_append(shared.running_rsc, &rsc3);
+
+ counter = 1;
+ return_false = 2;
+ pcmk_foreach_active_resource(&node, fn, NULL);
+ assert_int_equal(counter, 3);
+
+ g_list_free(shared.running_rsc);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_args),
+ cmocka_unit_test(list_of_0),
+ cmocka_unit_test(list_of_1),
+ cmocka_unit_test(list_of_3),
+ cmocka_unit_test(list_of_3_return_false))
diff --git a/lib/common/tests/nodes/pcmk_node_is_clean_test.c b/lib/common/tests/nodes/pcmk_node_is_clean_test.c
new file mode 100644
index 0000000..0534633
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_clean_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_unclean(void **state)
+{
+ assert_false(pcmk_node_is_clean(NULL));
+}
+
+static void
+node_is_clean(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .unclean = FALSE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_clean(&node));
+}
+
+static void
+node_is_unclean(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .unclean = TRUE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_clean(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_unclean),
+ cmocka_unit_test(node_is_clean),
+ cmocka_unit_test(node_is_unclean))
diff --git a/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c b/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c
new file mode 100644
index 0000000..45a3b6f
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_in_maintenance_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_not_in_maintenance(void **state)
+{
+ assert_false(pcmk_node_is_in_maintenance(NULL));
+}
+
+static void
+node_is_in_maintenance(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .maintenance = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_in_maintenance(&node));
+}
+
+static void
+node_is_not_in_maintenance(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .maintenance = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_in_maintenance(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_not_in_maintenance),
+ cmocka_unit_test(node_is_in_maintenance),
+ cmocka_unit_test(node_is_not_in_maintenance))
diff --git a/lib/common/tests/nodes/pcmk_node_is_online_test.c b/lib/common/tests/nodes/pcmk_node_is_online_test.c
new file mode 100644
index 0000000..d22e3b4
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_online_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_offline(void **state)
+{
+ assert_false(pcmk_node_is_online(NULL));
+}
+
+static void
+node_is_online(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .online = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_online(&node));
+}
+
+static void
+node_is_offline(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .online = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_online(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_offline),
+ cmocka_unit_test(node_is_online),
+ cmocka_unit_test(node_is_offline))
diff --git a/lib/common/tests/nodes/pcmk_node_is_pending_test.c b/lib/common/tests/nodes/pcmk_node_is_pending_test.c
new file mode 100644
index 0000000..9f2abca
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_pending_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_not_pending(void **state)
+{
+ assert_false(pcmk_node_is_pending(NULL));
+}
+
+static void
+node_is_pending(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .pending = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_pending(&node));
+}
+
+static void
+node_is_not_pending(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .pending = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_pending(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_not_pending),
+ cmocka_unit_test(node_is_pending),
+ cmocka_unit_test(node_is_not_pending))
diff --git a/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c b/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c
new file mode 100644
index 0000000..b6054b0
--- /dev/null
+++ b/lib/common/tests/nodes/pcmk_node_is_shutting_down_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+#include <glib.h> // TRUE, FALSE
+
+#include <crm/common/nodes.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_is_not_shutting_down(void **state)
+{
+ assert_false(pcmk_node_is_shutting_down(NULL));
+}
+
+static void
+node_is_shutting_down(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .shutdown = TRUE,
+ };
+
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_true(pcmk_node_is_shutting_down(&node));
+}
+
+static void
+node_is_not_shutting_down(void **state)
+{
+ struct pe_node_shared_s shared = {
+ .shutdown = FALSE,
+ };
+ pcmk_node_t node = {
+ .details = &shared,
+ };
+
+ assert_false(pcmk_node_is_shutting_down(&node));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_is_not_shutting_down),
+ cmocka_unit_test(node_is_shutting_down),
+ cmocka_unit_test(node_is_not_shutting_down))
diff --git a/lib/common/tests/nvpair/Makefile.am b/lib/common/tests/nvpair/Makefile.am
index 7f406bd..9f762d4 100644
--- a/lib/common/tests/nvpair/Makefile.am
+++ b/lib/common/tests/nvpair/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2021-2023 the Pacemaker project contributors
+# Copyright 2021-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,7 +11,10 @@ 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 \
+check_PROGRAMS = crm_meta_name_test \
+ crm_meta_value_test \
+ pcmk__xe_attr_is_true_test \
+ pcmk__xe_get_datetime_test \
pcmk__xe_get_bool_attr_test \
pcmk__xe_set_bool_attr_test
diff --git a/lib/common/tests/utils/crm_meta_name_test.c b/lib/common/tests/nvpair/crm_meta_name_test.c
index 06fecc5..7c6c32b 100644
--- a/lib/common/tests/utils/crm_meta_name_test.c
+++ b/lib/common/tests/nvpair/crm_meta_name_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,12 +10,12 @@
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
static void
empty_params(void **state)
{
- assert_null(crm_meta_name(NULL));
+ pcmk__assert_asserts(crm_meta_name(NULL));
}
static void
@@ -23,11 +23,11 @@ standard_usage(void **state)
{
char *s = NULL;
- s = crm_meta_name(XML_RSC_ATTR_NOTIFY);
+ s = crm_meta_name(PCMK_META_NOTIFY);
assert_string_equal(s, "CRM_meta_notify");
free(s);
- s = crm_meta_name(XML_RSC_ATTR_STICKINESS);
+ s = crm_meta_name(PCMK_META_RESOURCE_STICKINESS);
assert_string_equal(s, "CRM_meta_resource_stickiness");
free(s);
diff --git a/lib/common/tests/utils/crm_meta_value_test.c b/lib/common/tests/nvpair/crm_meta_value_test.c
index 0bde5c6..ffe9619 100644
--- a/lib/common/tests/utils/crm_meta_value_test.c
+++ b/lib/common/tests/nvpair/crm_meta_value_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -10,7 +10,7 @@
#include <crm_internal.h>
#include <crm/common/unittest_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
#include <glib.h>
@@ -30,8 +30,8 @@ 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));
+ assert_null(crm_meta_value(tbl, PCMK_META_NOTIFY));
+ assert_null(crm_meta_value(tbl, PCMK_META_RESOURCE_STICKINESS));
g_hash_table_destroy(tbl);
}
@@ -41,11 +41,13 @@ 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"));
+ g_hash_table_insert(tbl, crm_meta_name(PCMK_META_NOTIFY), strdup("1"));
+ g_hash_table_insert(tbl, crm_meta_name(PCMK_META_RESOURCE_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");
+ assert_string_equal(crm_meta_value(tbl, PCMK_META_NOTIFY), "1");
+ assert_string_equal(crm_meta_value(tbl, PCMK_META_RESOURCE_STICKINESS),
+ "2");
g_hash_table_destroy(tbl);
}
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
index 3723707..84187da 100644
--- a/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
+++ b/lib/common/tests/nvpair/pcmk__xe_attr_is_true_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,7 +15,7 @@
static void
empty_input(void **state)
{
- xmlNode *node = string2xml("<node/>");
+ xmlNode *node = pcmk__xml_parse("<node/>");
assert_false(pcmk__xe_attr_is_true(NULL, NULL));
assert_false(pcmk__xe_attr_is_true(NULL, "whatever"));
@@ -27,7 +27,7 @@ empty_input(void **state)
static void
attr_missing(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>");
assert_false(pcmk__xe_attr_is_true(node, "c"));
free_xml(node);
@@ -36,7 +36,7 @@ attr_missing(void **state)
static void
attr_present(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>");
assert_true(pcmk__xe_attr_is_true(node, "a"));
assert_false(pcmk__xe_attr_is_true(node, "b"));
@@ -44,7 +44,7 @@ attr_present(void **state)
free_xml(node);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, 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
index 500d8a6..4823f6a 100644
--- a/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
+++ b/lib/common/tests/nvpair/pcmk__xe_get_bool_attr_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2023 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -15,7 +15,7 @@
static void
empty_input(void **state)
{
- xmlNode *node = string2xml("<node/>");
+ xmlNode *node = pcmk__xml_parse("<node/>");
bool value;
assert_int_equal(pcmk__xe_get_bool_attr(NULL, NULL, &value), ENODATA);
@@ -29,7 +29,7 @@ empty_input(void **state)
static void
attr_missing(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\"/>");
bool value;
assert_int_equal(pcmk__xe_get_bool_attr(node, "c", &value), ENODATA);
@@ -39,7 +39,8 @@ attr_missing(void **state)
static void
attr_present(void **state)
{
- xmlNode *node = string2xml("<node a=\"true\" b=\"false\" c=\"blah\"/>");
+ xmlNode *node = pcmk__xml_parse("<node a=\"true\" b=\"false\" "
+ "c=\"blah\"/>");
bool value;
value = false;
@@ -53,7 +54,7 @@ attr_present(void **state)
free_xml(node);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, 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_datetime_test.c b/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c
new file mode 100644
index 0000000..6da1e23
--- /dev/null
+++ b/lib/common/tests/nvpair/pcmk__xe_get_datetime_test.c
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2024 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 <errno.h>
+#include <libxml/tree.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include <crm/common/xml.h>
+#include <crm/common/nvpair_internal.h>
+
+#define REFERENCE_ISO8601 "2024-001"
+#define ATTR_PRESENT "start"
+#define ATTR_MISSING "end"
+#define REFERENCE_XML "<date_expression id=\"id1\" " \
+ ATTR_PRESENT "=\"" REFERENCE_ISO8601 "\"" \
+ " operation=\"gt\">"
+#define BAD_XML "<date_expression id=\"id1\" " \
+ ATTR_PRESENT "=\"not_a_time\"" \
+ " operation=\"gt\">"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = NULL;
+
+ assert_int_equal(pcmk__xe_get_datetime(NULL, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(xml, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(xml, NULL, &t), EINVAL);
+ assert_null(t);
+ assert_int_equal(pcmk__xe_get_datetime(NULL, ATTR_PRESENT, NULL), EINVAL);
+ assert_int_equal(pcmk__xe_get_datetime(NULL, ATTR_PRESENT, &t), EINVAL);
+ assert_null(t);
+ assert_int_equal(pcmk__xe_get_datetime(NULL, NULL, &t), EINVAL);
+ assert_null(t);
+
+ free_xml(xml);
+}
+
+static void
+nonnull_time_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), EINVAL);
+
+ crm_time_free(t);
+ free_xml(xml);
+}
+
+static void
+attr_missing(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = NULL;
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_MISSING, &t), pcmk_rc_ok);
+ assert_null(t);
+
+ free_xml(xml);
+}
+
+static void
+attr_valid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(REFERENCE_XML);
+ crm_time_t *t = NULL;
+ crm_time_t *reference = crm_time_new(REFERENCE_ISO8601);
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t), pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(t, reference), 0);
+
+ crm_time_free(t);
+ crm_time_free(reference);
+ free_xml(xml);
+}
+
+static void
+attr_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(BAD_XML);
+ crm_time_t *t = NULL;
+
+ assert_int_equal(pcmk__xe_get_datetime(xml, ATTR_PRESENT, &t),
+ pcmk_rc_unpack_error);
+ assert_null(t);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(nonnull_time_invalid),
+ cmocka_unit_test(attr_missing),
+ cmocka_unit_test(attr_valid),
+ cmocka_unit_test(attr_invalid))
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
index e1fb9c4..dda2878 100644
--- a/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
+++ b/lib/common/tests/nvpair/pcmk__xe_set_bool_attr_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -11,21 +11,21 @@
#include <crm/common/unittest_internal.h>
#include <crm/common/xml_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/xml.h>
static void
set_attr(void **state)
{
- xmlNode *node = string2xml("<node/>");
+ xmlNode *node = pcmk__xml_parse("<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);
+ assert_string_equal(crm_element_value(node, "a"), PCMK_VALUE_TRUE);
+ assert_string_equal(crm_element_value(node, "b"), PCMK_VALUE_FALSE);
free_xml(node);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(set_attr))
diff --git a/lib/common/tests/probes/Makefile.am b/lib/common/tests/probes/Makefile.am
new file mode 100644
index 0000000..f5a3fb4
--- /dev/null
+++ b/lib/common/tests/probes/Makefile.am
@@ -0,0 +1,18 @@
+#
+# Copyright 2024 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_probe_test \
+ pcmk_xe_is_probe_test \
+ pcmk_xe_mask_probe_failure_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/actions/pcmk_is_probe_test.c b/lib/common/tests/probes/pcmk_is_probe_test.c
index 4a65e3f..835aac1 100644
--- a/lib/common/tests/actions/pcmk_is_probe_test.c
+++ b/lib/common/tests/probes/pcmk_is_probe_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
diff --git a/lib/common/tests/probes/pcmk_xe_is_probe_test.c b/lib/common/tests/probes/pcmk_xe_is_probe_test.c
new file mode 100644
index 0000000..e42476a
--- /dev/null
+++ b/lib/common/tests/probes/pcmk_xe_is_probe_test.c
@@ -0,0 +1,54 @@
+/*
+ * Copyright 2021-2024 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 = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP "/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK__XA_OPERATION_KEY "=\"blah\" "
+ PCMK_META_INTERVAL "=\"30s\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"30s\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_START "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_false(pcmk_xe_is_probe(node));
+ free_xml(node);
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_true(pcmk_xe_is_probe(node));
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(op_is_probe_test))
diff --git a/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c b/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c
new file mode 100644
index 0000000..ad378bf
--- /dev/null
+++ b/lib/common/tests/probes/pcmk_xe_mask_probe_failure_test.c
@@ -0,0 +1,333 @@
+/*
+ * Copyright 2021-2024 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 = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_START "\" "
+ PCMK_META_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;
+ char *s = NULL;
+
+ node = pcmk__xml_parse("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION
+ "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\"/>");
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"\"/>",
+ PCMK_OCF_OK);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+}
+
+static void
+check_values_test(void **state) {
+ xmlNode *node = NULL;
+ char *s = NULL;
+
+ /* PCMK_EXEC_NOT_SUPPORTED */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_NOT_SUPPORTED);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_SUPPORTED);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_DONE */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_DONE);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_NOT_INSTALLED */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_NOT_INSTALLED);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_NOT_INSTALLED);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR_HARD */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR_HARD);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ /* PCMK_EXEC_ERROR_FATAL */
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_OK, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_INVALID_PARAM, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_INSTALLED, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_true(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_CONFIGURED, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+
+ s = crm_strdup_printf("<" PCMK__XE_LRM_RSC_OP " "
+ PCMK_XA_OPERATION "=\"" PCMK_ACTION_MONITOR "\" "
+ PCMK_META_INTERVAL "=\"0\" "
+ PCMK__XA_RC_CODE "=\"%d\" "
+ PCMK__XA_OP_STATUS "=\"%d\"/>",
+ PCMK_OCF_NOT_RUNNING, PCMK_EXEC_ERROR_FATAL);
+ node = pcmk__xml_parse(s);
+ assert_false(pcmk_xe_mask_probe_failure(node));
+ free(s);
+ free_xml(node);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, 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/procfs/pcmk__procfs_pid2path_test.c b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
index 2bae541..52fe006 100644
--- a/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
+++ b/lib/common/tests/procfs/pcmk__procfs_pid2path_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -21,7 +21,7 @@ static void
no_exe_file(void **state)
{
size_t len = PATH_MAX;
- char *path = calloc(len, sizeof(char));
+ char *path = pcmk__assert_alloc(len, sizeof(char));
// Set readlink() errno and link contents
pcmk__mock_readlink = true;
@@ -43,7 +43,7 @@ static void
contents_too_long(void **state)
{
size_t len = 10;
- char *path = calloc(len, sizeof(char));
+ char *path = pcmk__assert_alloc(len, sizeof(char));
// Set readlink() errno and link contents
pcmk__mock_readlink = true;
@@ -66,7 +66,7 @@ static void
contents_ok(void **state)
{
size_t len = PATH_MAX;
- char *path = calloc(len, sizeof(char));
+ char *path = pcmk__assert_alloc(len, sizeof(char));
// Set readlink() errno and link contents
pcmk__mock_readlink = true;
diff --git a/lib/common/tests/resources/Makefile.am b/lib/common/tests/resources/Makefile.am
new file mode 100644
index 0000000..91e29a6
--- /dev/null
+++ b/lib/common/tests/resources/Makefile.am
@@ -0,0 +1,17 @@
+#
+# Copyright 2024 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_resource_id_test \
+ pcmk_resource_is_managed_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/resources/pcmk_resource_id_test.c b/lib/common/tests/resources/pcmk_resource_id_test.c
new file mode 100644
index 0000000..5676d2a
--- /dev/null
+++ b/lib/common/tests/resources/pcmk_resource_id_test.c
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+
+#include <crm/common/resources.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_resource(void **state)
+{
+ assert_null(pcmk_resource_id(NULL));
+}
+
+static void
+resource_with_id(void **state)
+{
+ char rsc1_id[] = "rsc1";
+ pcmk_resource_t rsc1 = {
+ .id = rsc1_id,
+ };
+
+ assert_string_equal(pcmk_resource_id(&rsc1), "rsc1");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_resource),
+ cmocka_unit_test(resource_with_id))
diff --git a/lib/common/tests/resources/pcmk_resource_is_managed_test.c b/lib/common/tests/resources/pcmk_resource_is_managed_test.c
new file mode 100644
index 0000000..958bdc2
--- /dev/null
+++ b/lib/common/tests/resources/pcmk_resource_is_managed_test.c
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2024 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 <stdio.h> // NULL
+
+#include <crm/common/resources.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_resource(void **state)
+{
+ assert_false(pcmk_resource_is_managed(NULL));
+}
+
+static void
+resource_is_managed(void **state)
+{
+ pcmk_resource_t rsc1 = {
+ .flags = pcmk_rsc_managed,
+ };
+
+ assert_true(pcmk_resource_is_managed(&rsc1));
+}
+
+static void
+resource_is_not_managed(void **state)
+{
+ pcmk_resource_t rsc1 = {
+ .flags = 0,
+ };
+
+ assert_false(pcmk_resource_is_managed(&rsc1));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_resource),
+ cmocka_unit_test(resource_is_managed),
+ cmocka_unit_test(resource_is_not_managed))
diff --git a/lib/common/tests/rules/Makefile.am b/lib/common/tests/rules/Makefile.am
new file mode 100644
index 0000000..4163037
--- /dev/null
+++ b/lib/common/tests/rules/Makefile.am
@@ -0,0 +1,29 @@
+#
+# Copyright 2020-2024 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__cmp_by_type_test \
+ pcmk__evaluate_attr_expression_test \
+ pcmk__evaluate_date_expression_test \
+ pcmk__evaluate_date_spec_test \
+ pcmk__evaluate_condition_test \
+ pcmk__evaluate_op_expression_test \
+ pcmk__evaluate_rsc_expression_test \
+ pcmk__parse_combine_test \
+ pcmk__parse_comparison_test \
+ pcmk__parse_source_test \
+ pcmk__parse_type_test \
+ pcmk__replace_submatches_test \
+ pcmk__unpack_duration_test \
+ pcmk_evaluate_rule_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/rules/pcmk__cmp_by_type_test.c b/lib/common/tests/rules/pcmk__cmp_by_type_test.c
new file mode 100644
index 0000000..cf468f1
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__cmp_by_type_test.c
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2024 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 <limits.h> // INT_MIN, INT_MAX
+
+#include <crm/common/util.h> // crm_strdup_printf()
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+null_compares_lesser(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type(NULL, NULL, pcmk__type_string), 0);
+ assert_true(pcmk__cmp_by_type("0", NULL, pcmk__type_integer) > 0);
+ assert_true(pcmk__cmp_by_type(NULL, "0", pcmk__type_number) < 0);
+}
+
+static void
+invalid_compares_equal(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("0", "1", pcmk__type_unknown), 0);
+ assert_int_equal(pcmk__cmp_by_type("hi", "bye", pcmk__type_unknown), 0);
+ assert_int_equal(pcmk__cmp_by_type("-1.0", "2.0", pcmk__type_unknown), 0);
+}
+
+static void
+compare_string_type(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("bye", "bye", pcmk__type_string), 0);
+ assert_int_equal(pcmk__cmp_by_type("bye", "BYE", pcmk__type_string), 0);
+ assert_true(pcmk__cmp_by_type("bye", "hello", pcmk__type_string) < 0);
+ assert_true(pcmk__cmp_by_type("bye", "HELLO", pcmk__type_string) < 0);
+ assert_true(pcmk__cmp_by_type("bye", "boo", pcmk__type_string) > 0);
+ assert_true(pcmk__cmp_by_type("bye", "Boo", pcmk__type_string) > 0);
+}
+
+static void
+compare_integer_type(void **state)
+{
+ char *int_min = crm_strdup_printf("%d", INT_MIN);
+ char *int_max = crm_strdup_printf("%d", INT_MAX);
+
+ assert_int_equal(pcmk__cmp_by_type("0", "0", pcmk__type_integer), 0);
+ assert_true(pcmk__cmp_by_type("0", "1", pcmk__type_integer) < 0);
+ assert_true(pcmk__cmp_by_type("1", "0", pcmk__type_integer) > 0);
+ assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_integer) > 0);
+ assert_true(pcmk__cmp_by_type(int_min, int_max, pcmk__type_integer) < 0);
+ assert_true(pcmk__cmp_by_type(int_max, int_min, pcmk__type_integer) > 0);
+ free(int_min);
+ free(int_max);
+
+ // Non-integers compare as strings
+ assert_int_equal(pcmk__cmp_by_type("0", "x", pcmk__type_integer),
+ pcmk__cmp_by_type("0", "x", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "0", pcmk__type_integer),
+ pcmk__cmp_by_type("x", "0", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_integer),
+ pcmk__cmp_by_type("x", "X", pcmk__type_string));
+}
+
+static void
+compare_number_type(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("0", "0.0", pcmk__type_number), 0);
+ assert_true(pcmk__cmp_by_type("0.345", "0.5", pcmk__type_number) < 0);
+ assert_true(pcmk__cmp_by_type("5", "3.1", pcmk__type_number) > 0);
+ assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_number) > 0);
+
+ // Non-numbers compare as strings
+ assert_int_equal(pcmk__cmp_by_type("0.0", "x", pcmk__type_number),
+ pcmk__cmp_by_type("0.0", "x", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "0.0", pcmk__type_number),
+ pcmk__cmp_by_type("x", "0.0", pcmk__type_string));
+ assert_int_equal(pcmk__cmp_by_type("x", "X", pcmk__type_number),
+ pcmk__cmp_by_type("x", "X", pcmk__type_string));
+}
+
+static void
+compare_version_type(void **state)
+{
+ assert_int_equal(pcmk__cmp_by_type("1.0", "1.0", pcmk__type_version), 0);
+ assert_true(pcmk__cmp_by_type("1.0.0", "1.0.1", pcmk__type_version) < 0);
+ assert_true(pcmk__cmp_by_type("5.0", "3.1.15", pcmk__type_version) > 0);
+ assert_true(pcmk__cmp_by_type("3999", "399", pcmk__type_version) > 0);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_compares_lesser),
+ cmocka_unit_test(invalid_compares_equal),
+ cmocka_unit_test(compare_string_type),
+ cmocka_unit_test(compare_integer_type),
+ cmocka_unit_test(compare_number_type),
+ cmocka_unit_test(compare_version_type))
diff --git a/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c
new file mode 100644
index 0000000..d28cb11
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_attr_expression_test.c
@@ -0,0 +1,831 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*
+ * Shared data
+ */
+
+#define MATCHED_STRING "server-north"
+
+static const regmatch_t submatches[] = {
+ { .rm_so = 0, .rm_eo = 12 }, // %0 = Entire string
+ { .rm_so = 7, .rm_eo = 12 }, // %1 = "north"
+};
+
+static pcmk_rule_input_t rule_input = {
+ // These are the only members used to evaluate attribute expressions
+
+ // Used to replace submatches in attribute name
+ .rsc_id = MATCHED_STRING,
+ .rsc_id_submatches = submatches,
+ .rsc_id_nmatches = 2,
+
+ // Used when source is instance attributes
+ .rsc_params = NULL,
+
+ // Used when source is meta-attributes
+ .rsc_meta = NULL,
+
+ // Used to get actual value of node attribute
+ .node_attrs = NULL,
+};
+
+static int
+setup(void **state)
+{
+ rule_input.rsc_params = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.rsc_params, "foo-param", "bar");
+ pcmk__insert_dup(rule_input.rsc_params, "myparam", "different");
+
+ rule_input.rsc_meta = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.rsc_meta, "foo-meta", "bar");
+ pcmk__insert_dup(rule_input.rsc_params, "mymeta", "different");
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, "foo", "bar");
+ pcmk__insert_dup(rule_input.node_attrs, "num", "10");
+ pcmk__insert_dup(rule_input.node_attrs, "ver", "3.5.0");
+ pcmk__insert_dup(rule_input.node_attrs, "prefer-north", "100");
+
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ g_hash_table_destroy(rule_input.rsc_params);
+ g_hash_table_destroy(rule_input.rsc_meta);
+ g_hash_table_destroy(rule_input.node_attrs);
+ return 0;
+}
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value
+ *
+ * \param[in] xml_string Node attribute expression XML as string
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_attr_expression(const char *xml_string, int reference_rc)
+{
+ xmlNode *xml = pcmk__xml_parse(xml_string);
+
+ assert_int_equal(pcmk__evaluate_attr_expression(xml, &rule_input),
+ reference_rc);
+ free_xml(xml);
+}
+
+
+/*
+ * Invalid arguments
+ */
+
+#define EXPR_SOURCE_LITERAL_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_SOURCE_LITERAL_PASSES);
+
+ assert_int_equal(pcmk__evaluate_attr_expression(NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_attr_expression(xml, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_attr_expression(NULL, &rule_input), EINVAL);
+
+ free_xml(xml);
+}
+
+
+/*
+ * Test PCMK_XA_ID
+ */
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_EXPRESSION " " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ assert_attr_expression(EXPR_ID_MISSING, pcmk_rc_ok);
+}
+
+
+/*
+ * Test PCMK_XA_ATTRIBUTE
+ */
+
+#define EXPR_ATTR_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+attr_missing(void **state)
+{
+ assert_attr_expression(EXPR_ATTR_MISSING, pcmk_rc_unpack_error);
+}
+
+#define EXPR_ATTR_SUBMATCH_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='prefer-%1' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+attr_with_submatch_passes(void **state)
+{
+ assert_attr_expression(EXPR_ATTR_SUBMATCH_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_ATTR_SUBMATCH_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='undefined-%1' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+attr_with_submatch_fails(void **state)
+{
+ assert_attr_expression(EXPR_ATTR_SUBMATCH_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_VALUE_SOURCE
+ */
+
+#define EXPR_SOURCE_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+source_missing(void **state)
+{
+ // Defaults to literal
+ assert_attr_expression(EXPR_SOURCE_MISSING, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_INVALID \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='not-a-source' />"
+
+static void
+source_invalid(void **state)
+{
+ // Currently treated as literal
+ assert_attr_expression(EXPR_SOURCE_INVALID, pcmk_rc_ok);
+}
+
+static void
+source_literal_passes(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_LITERAL_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_LITERAL_VALUE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='wrong-value' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+source_literal_value_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_LITERAL_VALUE_FAILS,
+ pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_LITERAL_ATTR_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='not-an-attribute' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+source_literal_attr_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_LITERAL_ATTR_FAILS,
+ pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_PARAM_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='not-a-param' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />"
+
+static void
+source_params_missing(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_PARAM_MISSING, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_PARAM_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='foo-param' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />"
+
+static void
+source_params_passes(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_PARAM_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_PARAM_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='myparam' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_PARAM "' />"
+
+static void
+source_params_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_PARAM_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_META_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='not-a-meta' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />"
+
+static void
+source_meta_missing(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_META_MISSING, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_SOURCE_META_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='foo-meta' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />"
+
+static void
+source_meta_passes(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_META_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_SOURCE_META_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='mymeta' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_META "' />"
+
+static void
+source_meta_fails(void **state)
+{
+ assert_attr_expression(EXPR_SOURCE_META_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_TYPE
+ */
+
+#define EXPR_TYPE_DEFAULT_NUMBER \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2.5' />"
+
+static void
+type_default_number(void **state)
+{
+ // Defaults to number for "gt" if either value contains a decimal point
+ assert_attr_expression(EXPR_TYPE_DEFAULT_NUMBER, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_DEFAULT_INT \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2' />"
+
+static void
+type_default_int(void **state)
+{
+ // Defaults to integer for "gt" if neither value contains a decimal point
+ assert_attr_expression(EXPR_TYPE_DEFAULT_INT, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_STRING_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+type_string_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_STRING_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_STRING_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bat' />"
+
+static void
+type_string_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_STRING_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_TYPE_INTEGER_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+type_integer_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_INTEGER_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_INTEGER_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='11' />"
+
+static void
+type_integer_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_INTEGER_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_TYPE_INTEGER_TRUNCATION \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10.5' />"
+
+static void
+type_integer_truncation(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_INTEGER_TRUNCATION, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_NUMBER_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10.0' />"
+
+static void
+type_number_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_NUMBER_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_NUMBER_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10.1' />"
+
+static void
+type_number_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_NUMBER_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_TYPE_VERSION_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='ver' " \
+ PCMK_XA_VALUE "='3.4.9' />"
+
+static void
+type_version_passes(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_VERSION_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_VERSION_EQUALITY \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='ver' " \
+ PCMK_XA_VALUE "='3.5' />"
+
+static void
+type_version_equality(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_VERSION_EQUALITY, pcmk_rc_ok);
+}
+
+#define EXPR_TYPE_VERSION_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_VERSION "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='ver' " \
+ PCMK_XA_VALUE "='4.0' />"
+
+static void
+type_version_fails(void **state)
+{
+ assert_attr_expression(EXPR_TYPE_VERSION_FAILS, pcmk_rc_before_range);
+}
+
+/*
+ * Test PCMK_XA_OPERATION
+ */
+
+#define EXPR_OP_MISSING \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+op_missing(void **state)
+{
+ assert_attr_expression(EXPR_OP_MISSING, pcmk_rc_unpack_error);
+}
+
+#define EXPR_OP_INVALID \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='not-an-operation' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+op_invalid(void **state)
+{
+ assert_attr_expression(EXPR_OP_INVALID, pcmk_rc_unpack_error);
+}
+
+#define EXPR_OP_LT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='20' />"
+
+static void
+op_lt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_LT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_LT_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2' />"
+
+static void
+op_lt_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_LT_FAILS, pcmk_rc_after_range);
+}
+
+#define EXPR_OP_GT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='2' />"
+
+static void
+op_gt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_GT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_GT_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='20' />"
+
+static void
+op_gt_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_GT_FAILS, pcmk_rc_before_range);
+}
+
+#define EXPR_OP_LTE_LT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='20' />"
+
+static void
+op_lte_lt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_LTE_LT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_LTE_EQ_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+op_lte_eq_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_LTE_EQ_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_LTE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='9' />"
+
+static void
+op_lte_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_LTE_FAILS, pcmk_rc_after_range);
+}
+
+#define EXPR_OP_GTE_GT_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='1' />"
+
+static void
+op_gte_gt_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_GTE_GT_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_GTE_EQ_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+op_gte_eq_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_GTE_EQ_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_GTE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GTE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='11' />"
+
+static void
+op_gte_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_GTE_FAILS, pcmk_rc_before_range);
+}
+
+// This also tests that string is used if values aren't parseable as numbers
+#define EXPR_OP_EQ_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_NUMBER "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+op_eq_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_EQ_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_EQ_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+op_eq_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_EQ_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_OP_NE_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_STRING "' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \
+ PCMK_XA_VALUE "='bat' " \
+ PCMK_XA_VALUE_SOURCE "='" PCMK_VALUE_LITERAL "' />"
+
+static void
+op_ne_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_NE_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_NE_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_TYPE "='" PCMK_VALUE_INTEGER "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NE "' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_VALUE "='10' />"
+
+static void
+op_ne_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_NE_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_OP_DEFINED_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+op_defined_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_DEFINED_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_DEFINED_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='boo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+op_defined_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+#define EXPR_OP_DEFINED_WITH_VALUE \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_VALUE "='bar' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+op_defined_with_value(void **state)
+{
+ // Ill-formed but currently accepted
+ assert_attr_expression(EXPR_OP_DEFINED_WITH_VALUE, pcmk_rc_ok);
+}
+
+#define EXPR_OP_UNDEFINED_PASSES \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='boo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />"
+
+static void
+op_undefined_passes(void **state)
+{
+ assert_attr_expression(EXPR_OP_UNDEFINED_PASSES, pcmk_rc_ok);
+}
+
+#define EXPR_OP_UNDEFINED_FAILS \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_NOT_DEFINED "' />"
+
+static void
+op_undefined_fails(void **state)
+{
+ assert_attr_expression(EXPR_OP_DEFINED_FAILS, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_VALUE
+ */
+
+#define EXPR_VALUE_MISSING_DEFINED_OK \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='num' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DEFINED "' />"
+
+static void
+value_missing_defined_ok(void **state)
+{
+ assert_attr_expression(EXPR_VALUE_MISSING_DEFINED_OK, pcmk_rc_ok);
+}
+
+#define EXPR_VALUE_MISSING_EQ_OK \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='not-an-attr' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' />"
+
+static void
+value_missing_eq_ok(void **state)
+{
+ // Currently treated as NULL reference value
+ assert_attr_expression(EXPR_VALUE_MISSING_EQ_OK, pcmk_rc_ok);
+}
+
+
+#define expr_test(f) cmocka_unit_test_setup_teardown(f, setup, teardown)
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ expr_test(id_missing),
+ expr_test(attr_missing),
+ expr_test(attr_with_submatch_passes),
+ expr_test(attr_with_submatch_fails),
+ expr_test(source_missing),
+ expr_test(source_invalid),
+ expr_test(source_literal_passes),
+ expr_test(source_literal_value_fails),
+ expr_test(source_literal_attr_fails),
+ expr_test(source_params_missing),
+ expr_test(source_params_passes),
+ expr_test(source_params_fails),
+ expr_test(source_meta_missing),
+ expr_test(source_meta_passes),
+ expr_test(source_meta_fails),
+ expr_test(type_default_number),
+ expr_test(type_default_int),
+ expr_test(type_string_passes),
+ expr_test(type_string_fails),
+ expr_test(type_integer_passes),
+ expr_test(type_integer_fails),
+ expr_test(type_integer_truncation),
+ expr_test(type_number_passes),
+ expr_test(type_number_fails),
+ expr_test(type_version_passes),
+ expr_test(type_version_equality),
+ expr_test(type_version_fails),
+ expr_test(op_missing),
+ expr_test(op_invalid),
+ expr_test(op_lt_passes),
+ expr_test(op_lt_fails),
+ expr_test(op_gt_passes),
+ expr_test(op_gt_fails),
+ expr_test(op_lte_lt_passes),
+ expr_test(op_lte_eq_passes),
+ expr_test(op_lte_fails),
+ expr_test(op_gte_gt_passes),
+ expr_test(op_gte_eq_passes),
+ expr_test(op_gte_fails),
+ expr_test(op_eq_passes),
+ expr_test(op_eq_fails),
+ expr_test(op_ne_passes),
+ expr_test(op_ne_fails),
+ expr_test(op_defined_passes),
+ expr_test(op_defined_fails),
+ expr_test(op_defined_with_value),
+ expr_test(op_undefined_passes),
+ expr_test(op_undefined_fails),
+ expr_test(value_missing_defined_ok),
+ expr_test(value_missing_eq_ok))
diff --git a/lib/common/tests/rules/pcmk__evaluate_condition_test.c b/lib/common/tests/rules/pcmk__evaluate_condition_test.c
new file mode 100644
index 0000000..bcb13a0
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_condition_test.c
@@ -0,0 +1,197 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+
+/*
+ * Test invalid arguments
+ */
+
+#define EXPR_ATTRIBUTE \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='foo' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='bar' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__evaluate_condition(NULL, NULL, next_change), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_ATTRIBUTE);
+ assert_int_equal(pcmk__evaluate_condition(xml, NULL, next_change), EINVAL);
+ free_xml(xml);
+
+ assert_int_equal(pcmk__evaluate_condition(NULL, &rule_input, next_change),
+ EINVAL);
+
+ crm_time_free(next_change);
+}
+
+
+#define EXPR_INVALID "<not_an_expression " PCMK_XA_ID "='e' />"
+
+static void
+invalid_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_INVALID);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change),
+ pcmk_rc_unpack_error);
+
+ crm_time_free(next_change);
+ free_xml(xml);
+}
+
+
+/* Each expression type function already has unit tests, so we just need to test
+ * that they are called correctly (essentially, one of each one's own tests).
+ */
+
+static void
+attribute_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_ATTRIBUTE);
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, "foo", "bar");
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ g_hash_table_destroy(rule_input.node_attrs);
+ rule_input.node_attrs = NULL;
+ free_xml(xml);
+}
+
+#define EXPR_LOCATION \
+ "<" PCMK_XE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_ATTRIBUTE "='" CRM_ATTR_UNAME "' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_EQ "' " \
+ PCMK_XA_VALUE "='node1' />"
+
+static void
+location_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_LOCATION);
+
+ rule_input.node_attrs = pcmk__strkey_table(free, free);
+ pcmk__insert_dup(rule_input.node_attrs, CRM_ATTR_UNAME, "node1");
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ g_hash_table_destroy(rule_input.node_attrs);
+ rule_input.node_attrs = NULL;
+ free_xml(xml);
+}
+
+#define EXPR_DATE \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+date_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_DATE);
+ crm_time_t *now = crm_time_new("2024-02-01 11:59:59");
+ crm_time_t *next_change = crm_time_new("2024-02-01 14:00:00");
+ crm_time_t *reference = crm_time_new("2024-02-01 12:00:00");
+
+ rule_input.now = now;
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, next_change),
+ pcmk_rc_before_range);
+ assert_int_equal(crm_time_compare(next_change, reference), 0);
+ rule_input.now = NULL;
+
+ crm_time_free(reference);
+ crm_time_free(next_change);
+ crm_time_free(now);
+}
+
+#define EXPR_RESOURCE \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+resource_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_RESOURCE);
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_OP \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+op_expression(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_OP);
+
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_SUBRULE \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' /> />"
+
+static void
+subrule(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(EXPR_SUBRULE);
+ assert_int_equal(pcmk__evaluate_condition(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(invalid_expression),
+ cmocka_unit_test(attribute_expression),
+ cmocka_unit_test(location_expression),
+ cmocka_unit_test(date_expression),
+ cmocka_unit_test(resource_expression),
+ cmocka_unit_test(op_expression),
+ cmocka_unit_test(subrule))
diff --git a/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c
new file mode 100644
index 0000000..df8dcbf
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_date_expression_test.c
@@ -0,0 +1,684 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value and output argument
+ *
+ * \param[in] xml Date expression XML
+ * \param[in] now_s Time to evaluate expression with (as string)
+ * \param[in] next_change_s If this and \p reference_s are not NULL, initialize
+ * next change time with this time (as string),
+ * and assert that its value after evaluation is the
+ * reference
+ * \param[in] reference_s If not NULL, time (as string) that next change
+ * should be after expression evaluation
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_date_expression(const xmlNode *xml, const char *now_s,
+ const char *next_change_s, const char *reference_s,
+ int reference_rc)
+{
+ crm_time_t *now = NULL;
+ crm_time_t *next_change = NULL;
+ bool check_next_change = (next_change_s != NULL) && (reference_s != NULL);
+
+ if (check_next_change) {
+ next_change = crm_time_new(next_change_s);
+ }
+
+ now = crm_time_new(now_s);
+ assert_int_equal(pcmk__evaluate_date_expression(xml, now, next_change),
+ reference_rc);
+ crm_time_free(now);
+
+ if (check_next_change) {
+ crm_time_t *reference = crm_time_new(reference_s);
+
+ assert_int_equal(crm_time_compare(next_change, reference), 0);
+ crm_time_free(reference);
+ crm_time_free(next_change);
+ }
+}
+
+#define EXPR_LT_VALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID);
+ crm_time_t *t = crm_time_new("2024-02-01");
+
+ assert_int_equal(pcmk__evaluate_date_expression(NULL, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_expression(xml, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_expression(NULL, t, NULL), EINVAL);
+
+ crm_time_free(t);
+ free_xml(xml);
+}
+
+static void
+null_next_change_ok(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range);
+ free_xml(xml);
+}
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_DATE_EXPRESSION " " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_ID_MISSING);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_within_range);
+ free_xml(xml);
+}
+
+#define EXPR_OP_INVALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='not-a-choice' />"
+
+static void
+op_invalid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_OP_INVALID);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_LT_MISSING_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' />"
+
+static void
+lt_missing_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_MISSING_END);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_LT_INVALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_LT "' " \
+ PCMK_XA_END "='not-a-datetime' />"
+
+static void
+lt_invalid_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_INVALID_END);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+static void
+lt_valid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_LT_VALID);
+
+ // Now and next change are both before end
+ assert_date_expression(xml, "2023-01-01 05:00:00", "2024-02-01 10:00:00",
+ "2024-02-01 10:00:00", pcmk_rc_within_range);
+
+ // Now is before end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:00", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 20:00:00",
+ "2024-02-01 20:00:00", pcmk_rc_after_range);
+
+ // Now and next change are both after end
+ assert_date_expression(xml, "2024-03-01 12:00:00", "2024-02-01 20:00:00",
+ "2024-02-01 20:00:00", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_GT_MISSING_START \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' />"
+
+static void
+gt_missing_start(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_MISSING_START);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_GT_INVALID_START \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_START "='not-a-datetime' />"
+
+static void
+gt_invalid_start(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_INVALID_START);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_GT_VALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_GT "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' />"
+
+static void
+gt_valid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_GT_VALID);
+
+ // Now and next change are both before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:01", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:01", pcmk_rc_before_range);
+
+ // Now is one second after start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 18:00:00",
+ "2024-02-01 18:00:00", pcmk_rc_within_range);
+
+ // t is after start, next change is after start
+ assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_within_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_MISSING \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' />"
+
+static void
+range_missing(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_MISSING);
+ crm_time_t *t = crm_time_new("2024-01-01");
+
+ assert_int_equal(pcmk__evaluate_date_expression(xml, t, NULL),
+ pcmk_rc_undetermined);
+
+ crm_time_free(t);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_START_INVALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='not-a-date' " \
+ PCMK_XA_END "='not-a-date' />"
+
+static void
+range_invalid_start_invalid_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_INVALID_END);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_START_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='not-a-date' />"
+
+static void
+range_invalid_start_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_ONLY);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' />"
+
+static void
+range_valid_start_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_ONLY);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 18:00:00", pcmk_rc_within_range);
+
+ // Now and next change are after start
+ assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_within_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_END_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_END "='not-a-date' />"
+
+static void
+range_invalid_end_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_END_ONLY);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_END_ONLY \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+range_valid_end_only(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_END_ONLY);
+
+ // Now and next change are before end
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_within_range);
+
+ // Now is before end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_INVALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='not-a-date' />"
+
+static void
+range_valid_start_invalid_end(void **state)
+{
+ // Currently treated same as start without end
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_END);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 18:00:00", pcmk_rc_within_range);
+
+ // Now and next change are after start
+ assert_date_expression(xml, "2024-03-01 05:03:11", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_within_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_INVALID_START_VALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='not-a-date' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+range_invalid_start_valid_end(void **state)
+{
+ // Currently treated same as end without start
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_INVALID_START_VALID_END);
+
+ // Now and next change are before end
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_within_range);
+
+ // Now is before end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2024-02-01 18:00:00",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2024-04-04 04:04:04",
+ "2024-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_VALID_END \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00' " \
+ PCMK_XA_END "='2024-02-01 15:00:00' />"
+
+static void
+range_valid_start_valid_end(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_END);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_within_range);
+
+ // Now is between start and end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04",
+ "2028-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_INVALID_DURATION \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00'>" \
+ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \
+ PCMK_XA_HOURS "='not-a-number' />" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+range_valid_start_invalid_duration(void **state)
+{
+ // Currently treated same as end equals start
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_INVALID_DURATION);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-02-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is after start
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 18:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is after start
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 12:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after start
+ assert_date_expression(xml, "2024-02-01 12:00:01", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_VALID_DURATION \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00'>" \
+ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \
+ PCMK_XA_HOURS "='3' />" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+range_valid_start_valid_duration(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_VALID_DURATION);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_within_range);
+
+ // Now is between start and end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04",
+ "2028-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_RANGE_VALID_START_DURATION_MISSING_ID \
+ "<" PCMK_XE_DATE_EXPRESSION " " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_IN_RANGE "' " \
+ PCMK_XA_START "='2024-02-01 12:00:00'>" \
+ "<" PCMK_XE_DURATION " " PCMK_XA_ID "='d' " \
+ PCMK_XA_HOURS "='3' />" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+range_valid_start_duration_missing_id(void **state)
+{
+ // Currently acceptable
+ xmlNodePtr xml = NULL;
+
+ xml = pcmk__xml_parse(EXPR_RANGE_VALID_START_DURATION_MISSING_ID);
+
+ // Now and next change are before start
+ assert_date_expression(xml, "2024-01-01 04:30:05", "2024-01-01 11:00:00",
+ "2024-01-01 11:00:00", pcmk_rc_before_range);
+
+ // Now is before start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 11:59:59", "2024-02-01 14:00:00",
+ "2024-02-01 12:00:00", pcmk_rc_before_range);
+
+ // Now is equal to start, next change is between start and end
+ assert_date_expression(xml, "2024-02-01 12:00:00", "2024-02-01 14:30:00",
+ "2024-02-01 14:30:00", pcmk_rc_within_range);
+
+ // Now is between start and end, next change is after end
+ assert_date_expression(xml, "2024-02-01 14:03:11", "2024-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now is equal to end, next change is after end
+ assert_date_expression(xml, "2024-02-01 15:00:00", "2028-04-04 04:04:04",
+ "2024-02-01 15:00:01", pcmk_rc_within_range);
+
+ // Now and next change are after end
+ assert_date_expression(xml, "2024-02-01 15:00:01", "2028-04-04 04:04:04",
+ "2028-04-04 04:04:04", pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_MISSING \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "' />"
+
+static void
+spec_missing(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_undetermined);
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_INVALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \
+ "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \
+ PCMK_XA_MONTHS "='not-a-number'/>" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+spec_invalid(void **state)
+{
+ // Currently treated as date_spec with no ranges (which passes)
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_INVALID);
+
+ assert_date_expression(xml, "2024-01-01", NULL, NULL, pcmk_rc_ok);
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_VALID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \
+ "<" PCMK_XE_DATE_SPEC " " PCMK_XA_ID "='s' " \
+ PCMK_XA_MONTHS "='2'/>" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+spec_valid(void **state)
+{
+ // date_spec does not currently support next_change
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_VALID);
+
+ // Now is just before spec start
+ assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL,
+ pcmk_rc_before_range);
+
+ // Now matches spec start
+ assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok);
+
+ // Now is within spec range
+ assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok);
+
+ // Now matches spec end
+ assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok);
+
+ // Now is just past spec end
+ assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL,
+ pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+#define EXPR_SPEC_MISSING_ID \
+ "<" PCMK_XE_DATE_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_OPERATION "='" PCMK_VALUE_DATE_SPEC "'>" \
+ "<" PCMK_XE_DATE_SPEC " " \
+ PCMK_XA_MONTHS "='2'/>" \
+ "</" PCMK_XE_DATE_EXPRESSION ">"
+
+static void
+spec_missing_id(void **state)
+{
+ // Currently acceptable; date_spec does not currently support next_change
+ xmlNodePtr xml = pcmk__xml_parse(EXPR_SPEC_MISSING_ID);
+
+ // Now is just before spec start
+ assert_date_expression(xml, "2024-01-01 23:59:59", NULL, NULL,
+ pcmk_rc_before_range);
+
+ // Now matches spec start
+ assert_date_expression(xml, "2024-02-01 00:00:00", NULL, NULL, pcmk_rc_ok);
+
+ // Now is within spec range
+ assert_date_expression(xml, "2024-02-22 22:22:22", NULL, NULL, pcmk_rc_ok);
+
+ // Now matches spec end
+ assert_date_expression(xml, "2024-02-29 23:59:59", NULL, NULL, pcmk_rc_ok);
+
+ // Now is just past spec end
+ assert_date_expression(xml, "2024-03-01 00:00:00", NULL, NULL,
+ pcmk_rc_after_range);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(null_next_change_ok),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(op_invalid),
+ cmocka_unit_test(lt_missing_end),
+ cmocka_unit_test(lt_invalid_end),
+ cmocka_unit_test(lt_valid),
+ cmocka_unit_test(gt_missing_start),
+ cmocka_unit_test(gt_invalid_start),
+ cmocka_unit_test(gt_valid),
+ cmocka_unit_test(range_missing),
+ cmocka_unit_test(range_invalid_start_invalid_end),
+ cmocka_unit_test(range_invalid_start_only),
+ cmocka_unit_test(range_valid_start_only),
+ cmocka_unit_test(range_invalid_end_only),
+ cmocka_unit_test(range_valid_end_only),
+ cmocka_unit_test(range_valid_start_invalid_end),
+ cmocka_unit_test(range_invalid_start_valid_end),
+ cmocka_unit_test(range_valid_start_valid_end),
+ cmocka_unit_test(range_valid_start_invalid_duration),
+ cmocka_unit_test(range_valid_start_valid_duration),
+ cmocka_unit_test(range_valid_start_duration_missing_id),
+ cmocka_unit_test(spec_missing),
+ cmocka_unit_test(spec_invalid),
+ cmocka_unit_test(spec_valid),
+ cmocka_unit_test(spec_missing_id))
diff --git a/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c
new file mode 100644
index 0000000..6048adf
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_date_spec_test.c
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2020-2024 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 <errno.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+run_one_test(const char *t, const char *x, int expected)
+{
+ crm_time_t *tm = crm_time_new(t);
+ xmlNodePtr xml = pcmk__xml_parse(x);
+
+ assert_int_equal(pcmk__evaluate_date_spec(xml, tm), expected);
+
+ crm_time_free(tm);
+ free_xml(xml);
+}
+
+static void
+null_invalid(void **state)
+{
+ xmlNodePtr xml = pcmk__xml_parse("<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2019'/>");
+ crm_time_t *tm = crm_time_new(NULL);
+
+ assert_int_equal(pcmk__evaluate_date_spec(NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_spec(xml, NULL), EINVAL);
+ assert_int_equal(pcmk__evaluate_date_spec(NULL, tm), EINVAL);
+
+ crm_time_free(tm);
+ free_xml(xml);
+}
+
+static void
+spec_id_missing(void **state)
+{
+ // Currently acceptable
+ run_one_test("2020-01-01", "<date_spec years='2020'/>", pcmk_rc_ok);
+}
+
+static void
+invalid_range(void **state)
+{
+ // Currently acceptable
+ run_one_test("2020-01-01", "<date_spec years='not-a-year' months='1'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_satisfies_year_spec(void **state)
+{
+ run_one_test("2020-01-01",
+ "<date_spec " PCMK_XA_ID "='spec' years='2020'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_after_year_spec(void **state)
+{
+ run_one_test("2020-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2019'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+time_satisfies_year_range(void **state)
+{
+ run_one_test("2020-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2010-2030'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_before_year_range(void **state)
+{
+ run_one_test("2000-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2010-2030'/>",
+ pcmk_rc_before_range);
+}
+
+static void
+time_after_year_range(void **state)
+{
+ run_one_test("2020-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2010-2015'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+range_without_start_year_passes(void **state)
+{
+ run_one_test("2010-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='-2020'/>",
+ pcmk_rc_ok);
+}
+
+static void
+range_without_end_year_passes(void **state)
+{
+ run_one_test("2010-01-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2000-'/>",
+ pcmk_rc_ok);
+ run_one_test("2000-10-01",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2000-'/>",
+ pcmk_rc_ok);
+}
+
+static void
+yeardays_satisfies(void **state)
+{
+ run_one_test("2020-01-30",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARDAYS "='30'/>",
+ pcmk_rc_ok);
+}
+
+static void
+time_after_yeardays_spec(void **state)
+{
+ run_one_test("2020-02-15",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARDAYS "='40'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+yeardays_feb_29_satisfies(void **state)
+{
+ run_one_test("2016-02-29",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARDAYS "='60'/>",
+ pcmk_rc_ok);
+}
+
+static void
+exact_ymd_satisfies(void **state)
+{
+ run_one_test("2001-12-31",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='12' "
+ PCMK_XA_MONTHDAYS "='31'/>",
+ pcmk_rc_ok);
+}
+
+static void
+range_in_month_satisfies(void **state)
+{
+ run_one_test("2001-06-10",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='6' "
+ PCMK_XA_MONTHDAYS "='1-10'/>",
+ pcmk_rc_ok);
+}
+
+static void
+exact_ymd_after_range(void **state)
+{
+ run_one_test("2001-12-31",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='12' "
+ PCMK_XA_MONTHDAYS "='30'/>",
+ pcmk_rc_after_range);
+}
+
+static void
+time_after_monthdays_range(void **state)
+{
+ run_one_test("2001-06-10",
+ "<" PCMK_XE_DATE_SPEC " "
+ PCMK_XA_ID "='spec' "
+ PCMK_XA_YEARS "='2001' "
+ PCMK_XA_MONTHS "='6' "
+ PCMK_XA_MONTHDAYS "='11-15'/>",
+ pcmk_rc_before_range);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(spec_id_missing),
+ cmocka_unit_test(invalid_range),
+ cmocka_unit_test(time_satisfies_year_spec),
+ cmocka_unit_test(time_after_year_spec),
+ cmocka_unit_test(time_satisfies_year_range),
+ cmocka_unit_test(time_before_year_range),
+ cmocka_unit_test(time_after_year_range),
+ cmocka_unit_test(range_without_start_year_passes),
+ cmocka_unit_test(range_without_end_year_passes),
+ cmocka_unit_test(yeardays_satisfies),
+ cmocka_unit_test(time_after_yeardays_spec),
+ cmocka_unit_test(yeardays_feb_29_satisfies),
+ cmocka_unit_test(exact_ymd_satisfies),
+ cmocka_unit_test(range_in_month_satisfies),
+ cmocka_unit_test(exact_ymd_after_range),
+ cmocka_unit_test(time_after_monthdays_range))
diff --git a/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c
new file mode 100644
index 0000000..d1cb35f
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_op_expression_test.c
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ // These are the only members used to evaluate operation expressions
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value
+ *
+ * \param[in] xml_string Operation expression XML as string
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_op_expression(const char *xml_string, int reference_rc)
+{
+ xmlNode *xml = pcmk__xml_parse(xml_string);
+
+ assert_int_equal(pcmk__evaluate_op_expression(xml, &rule_input),
+ reference_rc);
+ free_xml(xml);
+}
+
+
+/*
+ * Invalid arguments
+ */
+
+#define EXPR_FAIL_BOTH \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_START "' " \
+ PCMK_XA_INTERVAL "='0' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+
+ assert_int_equal(pcmk__evaluate_op_expression(NULL, NULL), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_FAIL_BOTH);
+ assert_int_equal(pcmk__evaluate_op_expression(xml, NULL), EINVAL);
+ free_xml(xml);
+
+ assert_op_expression(NULL, EINVAL);
+}
+
+
+/*
+ * Test PCMK_XA_ID
+ */
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_OP_EXPRESSION " " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+#define EXPR_ID_EMPTY \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ assert_op_expression(EXPR_ID_MISSING, pcmk_rc_ok);
+ assert_op_expression(EXPR_ID_EMPTY, pcmk_rc_ok);
+}
+
+
+/*
+ * Test PCMK_XA_NAME
+ */
+
+#define EXPR_NAME_MISSING \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+name_missing(void **state)
+{
+ assert_op_expression(EXPR_NAME_MISSING, pcmk_rc_unpack_error);
+}
+
+#define EXPR_MATCH_BOTH \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+#define EXPR_EMPTY_NAME \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='' " PCMK_XA_INTERVAL "='10s' />"
+
+static void
+input_name_missing(void **state)
+{
+ rule_input.op_name = NULL;
+ assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_op_unsatisfied);
+ assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied);
+ rule_input.op_name = PCMK_ACTION_MONITOR;
+}
+
+#define EXPR_FAIL_NAME \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_START "' " \
+ PCMK_XA_INTERVAL "='10s' />"
+
+static void
+fail_name(void **state)
+{
+ assert_op_expression(EXPR_FAIL_NAME, pcmk_rc_op_unsatisfied);
+
+ // An empty name is meaningless but accepted, so not an unpack error
+ assert_op_expression(EXPR_EMPTY_NAME, pcmk_rc_op_unsatisfied);
+}
+
+
+/*
+ * Test PCMK_XA_INTERVAL
+ */
+
+#define EXPR_EMPTY_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='' />"
+
+#define EXPR_INVALID_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='not-an-interval' />"
+
+static void
+invalid_interval(void **state)
+{
+ assert_op_expression(EXPR_EMPTY_INTERVAL, pcmk_rc_unpack_error);
+ assert_op_expression(EXPR_INVALID_INTERVAL, pcmk_rc_unpack_error);
+}
+
+#define EXPR_DEFAULT_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' />"
+
+static void
+default_interval(void **state)
+{
+ assert_op_expression(EXPR_DEFAULT_INTERVAL, pcmk_rc_ok);
+}
+
+#define EXPR_FAIL_INTERVAL \
+ "<" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='9s' />"
+
+static void
+fail_interval(void **state)
+{
+ assert_op_expression(EXPR_FAIL_INTERVAL, pcmk_rc_op_unsatisfied);
+}
+
+
+static void
+match_both(void **state)
+{
+ assert_op_expression(EXPR_MATCH_BOTH, pcmk_rc_ok);
+}
+
+static void
+fail_both(void **state)
+{
+ assert_op_expression(EXPR_FAIL_BOTH, pcmk_rc_op_unsatisfied);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(name_missing),
+ cmocka_unit_test(input_name_missing),
+ cmocka_unit_test(fail_name),
+ cmocka_unit_test(invalid_interval),
+ cmocka_unit_test(default_interval),
+ cmocka_unit_test(fail_interval),
+ cmocka_unit_test(match_both),
+ cmocka_unit_test(fail_both))
diff --git a/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
new file mode 100644
index 0000000..c3a164e
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__evaluate_rsc_expression_test.c
@@ -0,0 +1,227 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ // These are the only members used to evaluate resource expressions
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+};
+
+/*!
+ * \internal
+ * \brief Run one test, comparing return value
+ *
+ * \param[in] xml_string Resource expression XML as string
+ * \param[in] reference_rc Assert that evaluation result equals this
+ */
+static void
+assert_rsc_expression(const char *xml_string, int reference_rc)
+{
+ xmlNode *xml = pcmk__xml_parse(xml_string);
+
+ assert_int_equal(pcmk__evaluate_rsc_expression(xml, &rule_input),
+ reference_rc);
+ free_xml(xml);
+}
+
+
+/*
+ * Invalid arguments
+ */
+
+#define EXPR_ALL_MATCH \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+
+ assert_int_equal(pcmk__evaluate_rsc_expression(NULL, NULL), EINVAL);
+
+ xml = pcmk__xml_parse(EXPR_ALL_MATCH);
+ assert_int_equal(pcmk__evaluate_rsc_expression(xml, NULL), EINVAL);
+ free_xml(xml);
+
+ assert_rsc_expression(NULL, EINVAL);
+}
+
+
+/*
+ * Test PCMK_XA_ID
+ */
+
+#define EXPR_ID_MISSING \
+ "<" PCMK_XE_RSC_EXPRESSION " " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+#define EXPR_ID_EMPTY \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ assert_rsc_expression(EXPR_ID_MISSING, pcmk_rc_ok);
+ assert_rsc_expression(EXPR_ID_EMPTY, pcmk_rc_ok);
+}
+
+
+/*
+ * Test standard, provider, and agent
+ */
+
+#define EXPR_FAIL_STANDARD \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_LSB "' />"
+
+#define EXPR_EMPTY_STANDARD \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='' />"
+
+static void
+fail_standard(void **state)
+{
+ assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied);
+
+ rule_input.rsc_standard = NULL;
+ assert_rsc_expression(EXPR_FAIL_STANDARD, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_STANDARD, pcmk_rc_op_unsatisfied);
+ rule_input.rsc_standard = PCMK_RESOURCE_CLASS_OCF;
+}
+
+#define EXPR_FAIL_PROVIDER \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='pacemaker' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+#define EXPR_EMPTY_PROVIDER \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='' " PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+fail_provider(void **state)
+{
+ assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied);
+
+ rule_input.rsc_provider = NULL;
+ assert_rsc_expression(EXPR_FAIL_PROVIDER, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_PROVIDER, pcmk_rc_op_unsatisfied);
+ rule_input.rsc_provider = "heartbeat";
+}
+
+#define EXPR_FAIL_AGENT \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr3' />"
+
+#define EXPR_EMPTY_AGENT \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' " PCMK_XA_TYPE "='' />"
+
+static void
+fail_agent(void **state)
+{
+ assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied);
+
+ rule_input.rsc_agent = NULL;
+ assert_rsc_expression(EXPR_FAIL_AGENT, pcmk_rc_op_unsatisfied);
+ assert_rsc_expression(EXPR_EMPTY_AGENT, pcmk_rc_op_unsatisfied);
+ rule_input.rsc_agent = "IPaddr2";
+}
+
+#define EXPR_NO_STANDARD_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_PROVIDER "='heartbeat' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+no_standard_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_STANDARD_MATCHES, pcmk_rc_ok);
+}
+
+#define EXPR_NO_PROVIDER_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_TYPE "='IPaddr2' />"
+
+static void
+no_provider_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_PROVIDER_MATCHES, pcmk_rc_ok);
+}
+
+#define EXPR_NO_AGENT_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_CLASS "='" PCMK_RESOURCE_CLASS_OCF "' " \
+ PCMK_XA_PROVIDER "='heartbeat' />"
+
+static void
+no_agent_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_AGENT_MATCHES, pcmk_rc_ok);
+}
+
+#define EXPR_NO_CRITERIA_MATCHES \
+ "<" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e' />"
+
+static void
+no_criteria_matches(void **state)
+{
+ assert_rsc_expression(EXPR_NO_CRITERIA_MATCHES, pcmk_rc_ok);
+}
+
+static void
+all_match(void **state)
+{
+ assert_rsc_expression(EXPR_ALL_MATCH, pcmk_rc_ok);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(fail_standard),
+ cmocka_unit_test(fail_provider),
+ cmocka_unit_test(fail_agent),
+ cmocka_unit_test(no_standard_matches),
+ cmocka_unit_test(no_provider_matches),
+ cmocka_unit_test(no_agent_matches),
+ cmocka_unit_test(no_criteria_matches),
+ cmocka_unit_test(all_match))
diff --git a/lib/common/tests/rules/pcmk__parse_combine_test.c b/lib/common/tests/rules/pcmk__parse_combine_test.c
new file mode 100644
index 0000000..afebcf8
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_combine_test.c
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+default_and(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(NULL), pcmk__combine_and);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(""), pcmk__combine_unknown);
+ assert_int_equal(pcmk__parse_combine(" "), pcmk__combine_unknown);
+ assert_int_equal(pcmk__parse_combine("but"), pcmk__combine_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_combine(PCMK_VALUE_AND), pcmk__combine_and);
+ assert_int_equal(pcmk__parse_combine(PCMK_VALUE_OR), pcmk__combine_or);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_combine("And"),
+ pcmk__combine_and);
+
+ assert_int_equal(pcmk__parse_combine("OR"),
+ pcmk__combine_or);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(default_and),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk__parse_comparison_test.c b/lib/common/tests/rules/pcmk__parse_comparison_test.c
new file mode 100644
index 0000000..a995596
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_comparison_test.c
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+null_unknown(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison(NULL), pcmk__comparison_unknown);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison("nope"), pcmk__comparison_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_DEFINED),
+ pcmk__comparison_defined);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NOT_DEFINED),
+ pcmk__comparison_undefined);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_EQ),
+ pcmk__comparison_eq);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_NE),
+ pcmk__comparison_ne);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LT),
+ pcmk__comparison_lt);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_LTE),
+ pcmk__comparison_lte);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GT),
+ pcmk__comparison_gt);
+
+ assert_int_equal(pcmk__parse_comparison(PCMK_VALUE_GTE),
+ pcmk__comparison_gte);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_comparison("DEFINED"),
+ pcmk__comparison_defined);
+
+ assert_int_equal(pcmk__parse_comparison("Not_Defined"),
+ pcmk__comparison_undefined);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_unknown),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk__parse_source_test.c b/lib/common/tests/rules/pcmk__parse_source_test.c
new file mode 100644
index 0000000..9cf9b32
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_source_test.c
@@ -0,0 +1,62 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+default_literal(void **state)
+{
+ assert_int_equal(pcmk__parse_source(NULL), pcmk__source_literal);
+}
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_source(""), pcmk__source_unknown);
+ assert_int_equal(pcmk__parse_source(" "), pcmk__source_unknown);
+ assert_int_equal(pcmk__parse_source("params"), pcmk__source_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_source(PCMK_VALUE_LITERAL),
+ pcmk__source_literal);
+
+ assert_int_equal(pcmk__parse_source(PCMK_VALUE_PARAM),
+ pcmk__source_instance_attrs);
+
+ assert_int_equal(pcmk__parse_source(PCMK_VALUE_META),
+ pcmk__source_meta_attrs);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_source("LITERAL"),
+ pcmk__source_literal);
+
+ assert_int_equal(pcmk__parse_source("Param"),
+ pcmk__source_instance_attrs);
+
+ assert_int_equal(pcmk__parse_source("MeTa"),
+ pcmk__source_meta_attrs);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(default_literal),
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive))
diff --git a/lib/common/tests/rules/pcmk__parse_type_test.c b/lib/common/tests/rules/pcmk__parse_type_test.c
new file mode 100644
index 0000000..96f02c8
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__parse_type_test.c
@@ -0,0 +1,127 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+static void
+invalid(void **state)
+{
+ assert_int_equal(pcmk__parse_type("nope", pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_unknown);
+}
+
+static void
+valid(void **state)
+{
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_STRING,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_INTEGER,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_NUMBER,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(PCMK_VALUE_VERSION,
+ pcmk__comparison_unknown, NULL, NULL),
+ pcmk__type_version);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_int_equal(pcmk__parse_type("STRING", pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type("Integer", pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_integer);
+}
+
+static void
+default_number(void **state)
+{
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1.0", "2.5"),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1.", "2"),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", ".5"),
+ pcmk__type_number);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1.0", "2"),
+ pcmk__type_number);
+}
+
+static void
+default_integer(void **state)
+{
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lt, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_lte, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gt, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", "2"),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, NULL),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, "1", NULL),
+ pcmk__type_integer);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_gte, NULL, "2.5"),
+ pcmk__type_number);
+}
+
+static void
+default_string(void **state)
+{
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_unknown,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_defined,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_undefined,
+ NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_eq, NULL, NULL),
+ pcmk__type_string);
+
+ assert_int_equal(pcmk__parse_type(NULL, pcmk__comparison_ne, NULL, NULL),
+ pcmk__type_string);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(invalid),
+ cmocka_unit_test(valid),
+ cmocka_unit_test(case_insensitive),
+ cmocka_unit_test(default_number),
+ cmocka_unit_test(default_integer),
+ cmocka_unit_test(default_string))
diff --git a/lib/common/tests/rules/pcmk__replace_submatches_test.c b/lib/common/tests/rules/pcmk__replace_submatches_test.c
new file mode 100644
index 0000000..d404fcc
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__replace_submatches_test.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2024 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 <regex.h> // regmatch_t
+
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+// An example matched string with submatches
+static const char *match = "this is a string";
+static const regmatch_t submatches[] = {
+ { .rm_so = 0, .rm_eo = 16 }, // %0 = entire string
+ { .rm_so = 5, .rm_eo = 7 }, // %1 = "is"
+ { .rm_so = 9, .rm_eo = 9 }, // %2 = empty match
+};
+static const int nmatches = 3;
+
+static void
+assert_submatch(const char *string, const char *reference)
+{
+ char *expanded = NULL;
+
+ expanded = pcmk__replace_submatches(string, match, submatches, nmatches);
+ if ((expanded == NULL) || (reference == NULL)) {
+ assert_null(expanded);
+ assert_null(reference);
+ } else {
+ assert_int_equal(strcmp(expanded, reference), 0);
+ }
+ free(expanded);
+}
+
+static void
+no_source(void **state)
+{
+ assert_null(pcmk__replace_submatches(NULL, NULL, NULL, 0));
+ assert_submatch(NULL, NULL);
+ assert_submatch("", NULL);
+}
+
+static void
+source_has_no_variables(void **state)
+{
+ assert_null(pcmk__replace_submatches("this has no submatch variables",
+ match, submatches, nmatches));
+ assert_null(pcmk__replace_submatches("this ends in a %",
+ match, submatches, nmatches));
+ assert_null(pcmk__replace_submatches("%this starts with one",
+ match, submatches, nmatches));
+}
+
+static void
+without_matches(void **state)
+{
+ assert_submatch("this has an empty submatch %2",
+ "this has an empty submatch ");
+ assert_submatch("this has a nonexistent submatch %3",
+ "this has a nonexistent submatch ");
+}
+
+static void
+with_matches(void **state)
+{
+ assert_submatch("%0", match); // %0 matches entire string
+ assert_submatch("this %1", "this is");
+ assert_submatch("%1 this %ok", "is this %ok");
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(no_source),
+ cmocka_unit_test(source_has_no_variables),
+ cmocka_unit_test(without_matches),
+ cmocka_unit_test(with_matches))
diff --git a/lib/common/tests/rules/pcmk__unpack_duration_test.c b/lib/common/tests/rules/pcmk__unpack_duration_test.c
new file mode 100644
index 0000000..e82546c
--- /dev/null
+++ b/lib/common/tests/rules/pcmk__unpack_duration_test.c
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2024 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 <glib.h>
+
+#include <crm/common/unittest_internal.h>
+
+#include <crm/common/iso8601.h>
+#include <crm/common/xml.h>
+#include "../../crmcommon_private.h"
+
+#define MONTHS_TO_SECONDS "months=\"2\" weeks=\"3\" days=\"-1\" " \
+ "hours=\"1\" minutes=\"1\" seconds=\"1\" />"
+
+#define ALL_VALID "<duration id=\"duration1\" years=\"1\" " MONTHS_TO_SECONDS
+
+#define NO_ID "<duration years=\"1\" " MONTHS_TO_SECONDS
+
+#define YEARS_INVALID "<duration id=\"duration1\" years=\"not-a-number\" " \
+ MONTHS_TO_SECONDS
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(ALL_VALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+
+ assert_int_equal(pcmk__unpack_duration(NULL, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(duration, NULL, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(duration, start, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(duration, NULL, &end), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(NULL, start, NULL), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(NULL, start, &end), EINVAL);
+ assert_int_equal(pcmk__unpack_duration(NULL, NULL, &end), EINVAL);
+
+ crm_time_free(start);
+ free_xml(duration);
+}
+
+static void
+nonnull_end_invalid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(ALL_VALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = crm_time_new("2024-01-01 15:00:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end), EINVAL);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ free_xml(duration);
+}
+
+static void
+no_id(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(NO_ID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+ crm_time_t *reference = crm_time_new("2025-03-21 16:01:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end), pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(end, reference), 0);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ crm_time_free(reference);
+ free_xml(duration);
+}
+
+static void
+years_invalid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(YEARS_INVALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+ crm_time_t *reference = crm_time_new("2024-03-21 16:01:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end),
+ pcmk_rc_unpack_error);
+ assert_int_equal(crm_time_compare(end, reference), 0);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ crm_time_free(reference);
+ free_xml(duration);
+}
+
+static void
+all_valid(void **state)
+{
+ xmlNode *duration = pcmk__xml_parse(ALL_VALID);
+ crm_time_t *start = crm_time_new("2024-01-01 15:00:00");
+ crm_time_t *end = NULL;
+ crm_time_t *reference = crm_time_new("2025-03-21 16:01:01");
+
+ assert_int_equal(pcmk__unpack_duration(duration, start, &end), pcmk_rc_ok);
+ assert_int_equal(crm_time_compare(end, reference), 0);
+
+ crm_time_free(start);
+ crm_time_free(end);
+ crm_time_free(reference);
+ free_xml(duration);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(nonnull_end_invalid),
+ cmocka_unit_test(no_id),
+ cmocka_unit_test(years_invalid),
+ cmocka_unit_test(all_valid))
diff --git a/lib/common/tests/rules/pcmk_evaluate_rule_test.c b/lib/common/tests/rules/pcmk_evaluate_rule_test.c
new file mode 100644
index 0000000..6b6f9eb
--- /dev/null
+++ b/lib/common/tests/rules/pcmk_evaluate_rule_test.c
@@ -0,0 +1,379 @@
+/*
+ * Copyright 2024 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 <stdio.h>
+#include <glib.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/rules_internal.h>
+#include <crm/common/unittest_internal.h>
+
+/*
+ * Shared data
+ */
+
+static pcmk_rule_input_t rule_input = {
+ .rsc_standard = PCMK_RESOURCE_CLASS_OCF,
+ .rsc_provider = "heartbeat",
+ .rsc_agent = "IPaddr2",
+ .op_name = PCMK_ACTION_MONITOR,
+ .op_interval_ms = 10000,
+};
+
+
+/*
+ * Test invalid arguments
+ */
+
+#define RULE_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' > " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+null_invalid(void **state)
+{
+ xmlNode *xml = NULL;
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk_evaluate_rule(NULL, NULL, next_change),
+ EINVAL);
+
+ xml = pcmk__xml_parse(RULE_OP);
+ assert_int_equal(pcmk_evaluate_rule(xml, NULL, next_change), EINVAL);
+ free_xml(xml);
+
+ assert_int_equal(pcmk_evaluate_rule(NULL, &rule_input, next_change),
+ EINVAL);
+
+ crm_time_free(next_change);
+}
+
+#define RULE_OP_MISSING_ID \
+ "<" PCMK_XE_RULE "> " \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+id_missing(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_OP_MISSING_ID);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, next_change),
+ pcmk_rc_ok);
+
+ crm_time_free(next_change);
+ free_xml(xml);
+}
+
+#define RULE_IDREF_PARENT "<" PCMK_XE_CIB ">" RULE_OP "</" PCMK_XE_CIB ">"
+
+static void
+good_idref(void **state)
+{
+ xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT);
+ xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ crm_xml_add(rule_xml, PCMK_XA_ID_REF, "r");
+ assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change),
+ pcmk_rc_ok);
+
+ crm_time_free(next_change);
+ free_xml(parent_xml);
+}
+
+static void
+bad_idref(void **state)
+{
+ xmlNode *parent_xml = pcmk__xml_parse(RULE_IDREF_PARENT);
+ xmlNode *rule_xml = pcmk__xe_create(parent_xml, PCMK_XE_RULE);
+ crm_time_t *next_change = crm_time_new_undefined();
+
+ crm_xml_add(rule_xml, PCMK_XA_ID_REF, "x");
+ assert_int_equal(pcmk_evaluate_rule(rule_xml, &rule_input, next_change),
+ pcmk_rc_unpack_error);
+
+ crm_time_free(next_change);
+ free_xml(parent_xml);
+}
+
+#define RULE_EMPTY "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' />"
+
+static void
+empty_default(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_EMPTY_AND \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' />"
+
+static void
+empty_and(void **state)
+{
+ // Currently acceptable
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_AND);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_EMPTY_OR \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' />"
+
+static void
+empty_or(void **state)
+{
+ // Currently treated as unsatisfied
+ xmlNode *xml = pcmk__xml_parse(RULE_EMPTY_OR);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_DEFAULT_BOOLEAN_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+default_boolean_op(void **state)
+{
+ // Defaults to PCMK_VALUE_AND
+ xmlNode *xml = pcmk__xml_parse(RULE_DEFAULT_BOOLEAN_OP);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_INVALID_BOOLEAN_OP \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='not-an-op' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+invalid_boolean_op(void **state)
+{
+ // Currently defaults to PCMK_VALUE_AND
+ xmlNode *xml = pcmk__xml_parse(RULE_INVALID_BOOLEAN_OP);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_PASSES \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPaddr2' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_PASSES);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_LONELY_AND \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPaddr2' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+lonely_and_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_LONELY_AND);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_ONE_FAILS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_one_fails(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_ONE_FAILS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_AND_TWO_FAIL \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_AND "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='9s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+and_two_fail(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_AND_TWO_FAIL);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_ONE_PASSES \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_one_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_ONE_PASSES);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_TWO_PASS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='IPAddr2' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_two_pass(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_TWO_PASS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_LONELY_OR \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='10s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+lonely_or_passes(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_LONELY_OR);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL), pcmk_rc_ok);
+
+ free_xml(xml);
+}
+
+#define RULE_OR_FAILS \
+ "<" PCMK_XE_RULE " " PCMK_XA_ID "='r' " \
+ PCMK_XA_BOOLEAN_OP "='" PCMK_VALUE_OR "' >" \
+ " <" PCMK_XE_RSC_EXPRESSION " " PCMK_XA_ID "='e1' " \
+ PCMK_XA_TYPE "='Dummy' />" \
+ " <" PCMK_XE_OP_EXPRESSION " " PCMK_XA_ID "='e2' " \
+ PCMK_XA_NAME "='" PCMK_ACTION_MONITOR "' " \
+ PCMK_XA_INTERVAL "='20s' />" \
+ "</" PCMK_XE_RULE ">"
+
+static void
+or_fails(void **state)
+{
+ xmlNode *xml = pcmk__xml_parse(RULE_OR_FAILS);
+
+ assert_int_equal(pcmk_evaluate_rule(xml, &rule_input, NULL),
+ pcmk_rc_op_unsatisfied);
+
+ free_xml(xml);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_invalid),
+ cmocka_unit_test(id_missing),
+ cmocka_unit_test(good_idref),
+ cmocka_unit_test(bad_idref),
+ cmocka_unit_test(empty_default),
+ cmocka_unit_test(empty_and),
+ cmocka_unit_test(empty_or),
+ cmocka_unit_test(default_boolean_op),
+ cmocka_unit_test(invalid_boolean_op),
+ cmocka_unit_test(and_passes),
+ cmocka_unit_test(lonely_and_passes),
+ cmocka_unit_test(and_one_fails),
+ cmocka_unit_test(and_two_fail),
+ cmocka_unit_test(or_one_passes),
+ cmocka_unit_test(or_two_pass),
+ cmocka_unit_test(lonely_or_passes),
+ cmocka_unit_test(or_fails))
diff --git a/lib/common/tests/scheduler/Makefile.am b/lib/common/tests/scheduler/Makefile.am
new file mode 100644
index 0000000..6d5f4f8
--- /dev/null
+++ b/lib/common/tests/scheduler/Makefile.am
@@ -0,0 +1,19 @@
+#
+# Copyright 2024 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_get_dc_test \
+ pcmk_get_no_quorum_policy_test \
+ pcmk_has_quorum_test \
+ pcmk_set_scheduler_cib_test
+
+TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/scheduler/pcmk_get_dc_test.c b/lib/common/tests/scheduler/pcmk_get_dc_test.c
new file mode 100644
index 0000000..5d9d459
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_get_dc_test.c
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2024 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/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ assert_null(pcmk_get_dc(NULL));
+}
+
+static void
+null_dc(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .dc_node = NULL,
+ };
+
+ assert_null(pcmk_get_dc(&scheduler));
+}
+
+static void
+valid_dc(void **state)
+{
+ pcmk_node_t dc = {
+ .weight = 1,
+ };
+ pcmk_scheduler_t scheduler = {
+ .dc_node = &dc,
+ };
+
+ assert_ptr_equal(&dc, pcmk_get_dc(&scheduler));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(null_dc),
+ cmocka_unit_test(valid_dc))
diff --git a/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c b/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c
new file mode 100644
index 0000000..61c97e6
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_get_no_quorum_policy_test.c
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2024 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/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ assert_int_equal(pcmk_get_no_quorum_policy(NULL), pcmk_no_quorum_stop);
+}
+
+static void
+valid_no_quorum_policy(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .no_quorum_policy = pcmk_no_quorum_fence,
+ };
+
+ assert_int_equal(pcmk_get_no_quorum_policy(&scheduler),
+ pcmk_no_quorum_fence);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(valid_no_quorum_policy))
diff --git a/lib/common/tests/scheduler/pcmk_has_quorum_test.c b/lib/common/tests/scheduler/pcmk_has_quorum_test.c
new file mode 100644
index 0000000..51903df
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_has_quorum_test.c
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2024 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/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ assert_false(pcmk_has_quorum(NULL));
+}
+
+static void
+valid_scheduler(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .flags = pcmk_sched_quorate,
+ };
+
+ assert_true(pcmk_has_quorum(&scheduler));
+
+ scheduler.flags = pcmk_sched_none;
+ assert_false(pcmk_has_quorum(&scheduler));
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(valid_scheduler))
diff --git a/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c b/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c
new file mode 100644
index 0000000..71e690b
--- /dev/null
+++ b/lib/common/tests/scheduler/pcmk_set_scheduler_cib_test.c
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2024 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/scheduler.h>
+#include <crm/common/unittest_internal.h>
+
+static void
+null_scheduler(void **state)
+{
+ xmlNode *cib = pcmk__xe_create(NULL, "test");
+
+ assert_int_equal(pcmk_set_scheduler_cib(NULL, NULL), EINVAL);
+ assert_int_equal(pcmk_set_scheduler_cib(NULL, cib), EINVAL);
+
+ free_xml(cib);
+}
+
+static void
+null_cib(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .input = NULL,
+ };
+
+ assert_int_equal(pcmk_set_scheduler_cib(&scheduler, NULL), pcmk_rc_ok);
+ assert_null(scheduler.input);
+}
+
+static void
+previous_cib_null(void **state)
+{
+ pcmk_scheduler_t scheduler = {
+ .input = NULL,
+ };
+ xmlNode *cib = pcmk__xe_create(NULL, "test");
+
+ assert_int_equal(pcmk_set_scheduler_cib(&scheduler, cib), pcmk_rc_ok);
+ assert_ptr_equal(scheduler.input, cib);
+
+ free_xml(cib);
+}
+
+static void
+previous_cib_nonnull(void **state)
+{
+ xmlNode *old_cib = pcmk__xe_create(NULL, "old");
+ xmlNode *new_cib = pcmk__xe_create(NULL, "new");
+ pcmk_scheduler_t scheduler = {
+ .input = old_cib,
+ };
+
+ assert_int_equal(pcmk_set_scheduler_cib(&scheduler, new_cib), pcmk_rc_ok);
+ assert_ptr_equal(scheduler.input, new_cib);
+
+ free_xml(old_cib);
+ free_xml(new_cib);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(null_scheduler),
+ cmocka_unit_test(null_cib),
+ cmocka_unit_test(previous_cib_null),
+ cmocka_unit_test(previous_cib_nonnull))
diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am
new file mode 100644
index 0000000..ba0f805
--- /dev/null
+++ b/lib/common/tests/schemas/Makefile.am
@@ -0,0 +1,88 @@
+#
+# Copyright 2023-2024 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
+
+CFLAGS += -DPCMK__TEST_SCHEMA_DIR='"$(abs_builddir)/schemas"'
+
+# Add "_test" to the end of all test program names to simplify .gitignore.
+
+# These tests share a schema subdirectory
+SHARED_SCHEMA_TESTS = pcmk__cmp_schemas_by_name_test \
+ crm_schema_init_test \
+ pcmk__build_schema_xml_node_test \
+ pcmk__get_schema_test \
+ pcmk__schema_files_later_than_test
+
+# This test has its own schema directory
+FIND_X_0_SCHEMA_TEST = pcmk__find_x_0_schema_test
+
+check_PROGRAMS = $(SHARED_SCHEMA_TESTS) $(FIND_X_0_SCHEMA_TEST)
+
+TESTS = $(check_PROGRAMS)
+
+$(SHARED_SCHEMA_TESTS): setup-schema-dir
+
+$(FIND_X_0_SCHEMA_TEST): setup-find_x_0-schema-dir
+
+# Set up a temporary schemas/ directory containing only some of the full set of
+# pacemaker schema files. This lets us know exactly how many schemas are present,
+# allowing us to write tests without having to make changes when new schemas are
+# added.
+#
+# This directory contains the following:
+#
+# * pacemaker-next.rng - Used to verify that this sorts before all versions
+# * upgrade-*.xsl - Required by various schema versions
+# * pacemaker-[0-9]*.rng - We're only pulling in 15 schemas, which is enough
+# to get everything through pacemaker-3.0.rng. This
+# includes 2.10, needed so we can check that versions
+# are compared as numbers instead of strings.
+# * other RNG files - This catches everything except the pacemaker-*rng
+# files. These files are included by the top-level
+# pacemaker-*rng files, so we need them for tests.
+# This will glob more than we need, but the extra ones
+# won't get in the way.
+
+LINK_FILES = $(abs_top_builddir)/xml/pacemaker-next.rng \
+ $(abs_top_builddir)/xml/upgrade-*.xsl
+ROOT_RNGS = $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15)
+INCLUDED_RNGS = $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker-[0-9])
+
+# Most tests share a common, read-only schema directory
+.PHONY: setup-schema-dir
+setup-schema-dir:
+ $(MKDIR_P) schemas
+ ( cd schemas ; \
+ ln -sf $(LINK_FILES) . ; \
+ for f in $(ROOT_RNGS); do \
+ ln -sf $$f $$(basename $$f); \
+ done ; \
+ for f in $(INCLUDED_RNGS); do \
+ ln -sf ../$$f $$(basename $$f); \
+ done )
+
+# pcmk__find_x_0_schema_test moves schema files around, so it needs its
+# own directory, otherwise other tests run in parallel could fail.
+.PHONY: setup-find_x_0-schema-dir
+setup-find_x_0-schema-dir:
+ $(MKDIR_P) schemas/find_x_0
+ ( cd schemas/find_x_0 ; \
+ ln -sf $(LINK_FILES) . ; \
+ for f in $(ROOT_RNGS); do \
+ ln -sf $$f $$(basename $$f); \
+ done ; \
+ for f in $(INCLUDED_RNGS); do \
+ ln -sf ../$$f $$(basename $$f); \
+ done )
+
+.PHONY: clean-local
+clean-local:
+ -rm -rf schemas
diff --git a/lib/common/tests/schemas/crm_schema_init_test.c b/lib/common/tests/schemas/crm_schema_init_test.c
new file mode 100644
index 0000000..8da79cb
--- /dev/null
+++ b/lib/common/tests/schemas/crm_schema_init_test.c
@@ -0,0 +1,152 @@
+/*
+ * Copyright 2023-2024 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 <ftw.h>
+#include <unistd.h>
+
+#include <crm/common/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+#include "crmcommon_private.h"
+
+static char *remote_schema_dir = NULL;
+
+static int
+symlink_schema(const char *tmpdir, const char *target_file, const char *link_file)
+{
+ int rc = 0;
+ char *oldpath = NULL;
+ char *newpath = NULL;
+
+ oldpath = crm_strdup_printf("%s/%s", PCMK__TEST_SCHEMA_DIR, target_file);
+ newpath = crm_strdup_printf("%s/%s", tmpdir, link_file);
+
+ rc = symlink(oldpath, newpath);
+
+ free(oldpath);
+ free(newpath);
+ return rc;
+}
+
+static int
+rm_files(const char *pathname, const struct stat *sbuf, int type, struct FTW *ftwb)
+{
+ return remove(pathname);
+}
+
+static int
+rmtree(const char *dir)
+{
+ return nftw(dir, rm_files, 10, FTW_DEPTH|FTW_MOUNT|FTW_PHYS);
+}
+
+static int
+setup(void **state)
+{
+ char *dir = NULL;
+
+ /* Create a directory to hold additional schema files. These don't need
+ * to be anything special - we can just copy existing schemas but give
+ * them new names.
+ */
+ dir = crm_strdup_printf("%s/test-schemas.XXXXXX", pcmk__get_tmpdir());
+ remote_schema_dir = mkdtemp(dir);
+
+ if (remote_schema_dir == NULL) {
+ free(dir);
+ return -1;
+ }
+
+ /* Add new files to simulate a remote node not being up-to-date. We can't
+ * add a new major version here without also creating an XSL transform, and
+ * we can't add an older version (like 1.1 or 2.11 or something) because
+ * remotes will only ever ask for stuff newer than their newest.
+ */
+ if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.1.rng") != 0) {
+ rmdir(dir);
+ free(dir);
+ return -1;
+ }
+
+ if (symlink_schema(dir, "pacemaker-3.0.rng", "pacemaker-3.2.rng") != 0) {
+ rmdir(dir);
+ free(dir);
+ return -1;
+ }
+
+ setenv("PCMK_remote_schema_directory", remote_schema_dir, 1);
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+
+ /* Do not call crm_schema_init here because that is the function we're
+ * testing. It needs to be called in each unit test. However, we can
+ * call crm_schema_cleanup in teardown().
+ */
+
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ int rc = 0;
+ char *f = NULL;
+
+ crm_schema_cleanup();
+ unsetenv("PCMK_remote_schema_directory");
+ unsetenv("PCMK_schema_directory");
+
+ rc = rmtree(remote_schema_dir);
+
+ free(remote_schema_dir);
+ free(f);
+ return rc;
+}
+
+static void
+assert_schema(const char *schema_name, int schema_index)
+{
+ GList *entry = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ entry = pcmk__get_schema(schema_name);
+ assert_non_null(entry);
+
+ schema = entry->data;
+ assert_non_null(schema);
+
+ assert_int_equal(schema_index, schema->schema_index);
+}
+
+static void
+extra_schema_files(void **state)
+{
+ crm_schema_init();
+
+ /* Just iterate through the list of schemas and make sure everything
+ * (including the new schemas we loaded from a second directory) is in
+ * the right order.
+ */
+ assert_schema("pacemaker-1.0", 0);
+ assert_schema("pacemaker-1.2", 1);
+ assert_schema("pacemaker-2.0", 3);
+ assert_schema("pacemaker-3.0", 14);
+ assert_schema("pacemaker-3.1", 15);
+ assert_schema("pacemaker-3.2", 16);
+
+ // @COMPAT pacemaker-next is deprecated since 2.1.5
+ assert_schema("pacemaker-next", 17);
+
+ // @COMPAT none is deprecated since 2.1.8
+ assert_schema(PCMK_VALUE_NONE, 18);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(extra_schema_files));
diff --git a/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c
new file mode 100644
index 0000000..e4454e2
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__build_schema_xml_node_test.c
@@ -0,0 +1,158 @@
+/*
+ * Copyright 2023-2024 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/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/lists_internal.h>
+
+#include <glib.h>
+
+const char *rngs1[] = { "pacemaker-3.0.rng", "status-1.0.rng", "alerts-2.10.rng",
+ "nvset-2.9.rng", "score.rng", "rule-2.9.rng",
+ "tags-1.3.rng", "acls-2.0.rng", "fencing-2.4.rng",
+ "constraints-3.0.rng", "resources-3.0.rng", "nvset-3.0.rng",
+ "nodes-3.0.rng", "options-3.0.rng", NULL };
+
+const char *rngs2[] = { "pacemaker-2.0.rng", "status-1.0.rng", "tags-1.3.rng",
+ "acls-2.0.rng", "fencing-1.2.rng", "constraints-1.2.rng",
+ "rule.rng", "score.rng", "resources-1.3.rng",
+ "nvset-1.3.rng", "nodes-1.3.rng", "options-1.0.rng",
+ "nvset.rng", "cib-1.2.rng", NULL };
+
+const char *rngs3[] = { "pacemaker-2.1.rng", "constraints-2.1.rng", NULL };
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ pcmk__xml_test_setup_group(state);
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+invalid_name(void **state)
+{
+ GList *already_included = NULL;
+ xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS);
+
+ pcmk__build_schema_xml_node(parent, "pacemaker-9.0", &already_included);
+ assert_null(parent->children);
+ assert_null(already_included);
+ free_xml(parent);
+}
+
+static void
+single_schema(void **state)
+{
+ GList *already_included = NULL;
+ xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS);
+ xmlNode *schema_node = NULL;
+ xmlNode *file_node = NULL;
+ int i = 0;
+
+ pcmk__build_schema_xml_node(parent, "pacemaker-3.0", &already_included);
+
+ assert_non_null(already_included);
+ assert_non_null(parent->children);
+
+ /* Test that the result looks like this:
+ *
+ * <schemas>
+ * <schema version="pacemaker-3.0">
+ * <file path="pacemaker-3.0.rng">CDATA</file>
+ * <file path="status-1.0.rng">CDATA</file>
+ * ...
+ * </schema>
+ * </schemas>
+ */
+ schema_node = pcmk__xe_first_child(parent, NULL, NULL, NULL);
+ assert_string_equal("pacemaker-3.0",
+ crm_element_value(schema_node, PCMK_XA_VERSION));
+
+ file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL);
+ while (file_node != NULL && rngs1[i] != NULL) {
+ assert_string_equal(rngs1[i],
+ crm_element_value(file_node, PCMK_XA_PATH));
+ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE);
+
+ file_node = pcmk__xe_next(file_node);
+ i++;
+ }
+
+ g_list_free_full(already_included, free);
+ free_xml(parent);
+}
+
+static void
+multiple_schemas(void **state)
+{
+ GList *already_included = NULL;
+ xmlNode *parent = pcmk__xe_create(NULL, PCMK__XA_SCHEMAS);
+ xmlNode *schema_node = NULL;
+ xmlNode *file_node = NULL;
+ int i = 0;
+
+ pcmk__build_schema_xml_node(parent, "pacemaker-2.0", &already_included);
+ pcmk__build_schema_xml_node(parent, "pacemaker-2.1", &already_included);
+
+ assert_non_null(already_included);
+ assert_non_null(parent->children);
+
+ /* Like single_schema, but make sure files aren't included multiple times
+ * when the function is called repeatedly.
+ */
+ schema_node = pcmk__xe_first_child(parent, NULL, NULL, NULL);
+ assert_string_equal("pacemaker-2.0",
+ crm_element_value(schema_node, PCMK_XA_VERSION));
+
+ file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL);
+ while (file_node != NULL && rngs2[i] != NULL) {
+ assert_string_equal(rngs2[i],
+ crm_element_value(file_node, PCMK_XA_PATH));
+ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE);
+
+ file_node = pcmk__xe_next(file_node);
+ i++;
+ }
+
+ schema_node = pcmk__xe_next(schema_node);
+ assert_string_equal("pacemaker-2.1",
+ crm_element_value(schema_node, PCMK_XA_VERSION));
+
+ file_node = pcmk__xe_first_child(schema_node, NULL, NULL, NULL);
+ i = 0;
+
+ while (file_node != NULL && rngs3[i] != NULL) {
+ assert_string_equal(rngs3[i],
+ crm_element_value(file_node, PCMK_XA_PATH));
+ assert_int_equal(pcmk__xml_first_child(file_node)->type, XML_CDATA_SECTION_NODE);
+
+ file_node = pcmk__xe_next(file_node);
+ i++;
+ }
+
+ g_list_free_full(already_included, free);
+ free_xml(parent);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(invalid_name),
+ cmocka_unit_test(single_schema),
+ cmocka_unit_test(multiple_schemas))
diff --git a/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
new file mode 100644
index 0000000..19ec743
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__cmp_schemas_by_name_test.c
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2024 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/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+#include "crmcommon_private.h"
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+// NULL schema name defaults to the "none" schema
+// @COMPAT none is deprecated since 2.1.8
+
+static void
+unknown_is_lesser(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1",
+ "pacemaker-0.2") == 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.1",
+ "pacemaker-1.0") < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0",
+ "pacemaker-0.1") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.1", NULL) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-0.0") > 0);
+
+ /* @COMPAT pacemaker-next is deprecated since 2.1.5,
+ * and pacemaker-0.6 and pacemaker-0.7 since 2.1.8
+ */
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-0.6",
+ "pacemaker-next") < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ "pacemaker-0.7") > 0);
+}
+
+// @COMPAT none is deprecated since 2.1.8
+static void
+none_is_greater(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name(NULL, NULL) == 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, PCMK_VALUE_NONE) == 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE, NULL) == 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ PCMK_VALUE_NONE) == 0);
+
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0",
+ PCMK_VALUE_NONE) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ "pacemaker-1.0") > 0);
+
+ // @COMPAT pacemaker-next is deprecated since 2.1.5
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ PCMK_VALUE_NONE) < 0);
+ assert_true(pcmk__cmp_schemas_by_name(PCMK_VALUE_NONE,
+ "pacemaker-next") > 0);
+}
+
+// @COMPAT pacemaker-next is deprecated since 2.1.5
+// @COMPAT none is deprecated since 2.1.8
+static void
+next_is_before_none(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ "pacemaker-next") == 0);
+ assert_true(pcmk__cmp_schemas_by_name(NULL, "pacemaker-next") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next", NULL) < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-3.0",
+ "pacemaker-next") < 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-next",
+ "pacemaker-1.0") > 0);
+}
+
+static void
+known_numeric(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.0",
+ "pacemaker-1.0") == 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2",
+ "pacemaker-1.0") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("pacemaker-1.2",
+ "pacemaker-2.0") < 0);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_true(pcmk__cmp_schemas_by_name("Pacemaker-1.0",
+ "pacemaker-1.0") == 0);
+ assert_true(pcmk__cmp_schemas_by_name("PACEMAKER-1.2",
+ "pacemaker-1.0") > 0);
+ assert_true(pcmk__cmp_schemas_by_name("PaceMaker-1.2",
+ "pacemaker-2.0") < 0);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(unknown_is_lesser),
+ cmocka_unit_test(none_is_greater),
+ cmocka_unit_test(next_is_before_none),
+ cmocka_unit_test(known_numeric),
+ cmocka_unit_test(case_insensitive));
diff --git a/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c
new file mode 100644
index 0000000..25ba0f3
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__find_x_0_schema_test.c
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2023-2024 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 <stdio.h> // NULL, rename()
+#include <stdlib.h> // setenv(), unsetenv()
+#include <glib.h>
+
+#include <crm/common/unittest_internal.h>
+#include "crmcommon_private.h"
+
+#define SCHEMA_PREFIX PCMK__TEST_SCHEMA_DIR "/find_x_0/pacemaker-"
+
+static int
+setup(void **state)
+{
+ // Use a unique schema directory so we can move files around
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR "/find_x_0", 1);
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+assert_schema_0(int schema_index, const char *schema_name)
+{
+ GList *entry = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ entry = pcmk__find_x_0_schema();
+ assert_non_null(entry);
+
+ schema = entry->data;
+ assert_non_null(schema);
+
+ assert_int_equal(schema->schema_index, schema_index);
+ assert_string_equal(schema->name, schema_name);
+}
+
+static void
+last_is_0(void **state)
+{
+ /* This loads all the schemas normally linked for unit testing, so we have
+ * many 1.x and 2.x schemas and a single pacemaker-3.0 schema at index 14.
+ */
+ crm_schema_init();
+ assert_schema_0(14, "pacemaker-3.0");
+ crm_schema_cleanup();
+}
+
+static void
+last_is_not_0(void **state)
+{
+ /* Disable the pacemaker-3.0 schema, so we now should get pacemaker-2.0 at
+ * index 3.
+ */
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng",
+ SCHEMA_PREFIX "3.0.bak"));
+ crm_schema_init();
+ assert_schema_0(3, "pacemaker-2.0");
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak",
+ SCHEMA_PREFIX "3.0.rng"));
+ crm_schema_cleanup();
+}
+
+static void
+schema_0_missing(void **state)
+{
+ /* Disable the pacemaker-3.0 and pacemaker-2.0 schemas, so we now should get
+ * pacemaker-2.1 at index 3.
+ */
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.rng",
+ SCHEMA_PREFIX "3.0.bak"));
+ assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.rng",
+ SCHEMA_PREFIX "2.0.bak"));
+ crm_schema_init();
+ assert_schema_0(3, "pacemaker-2.1");
+ assert_int_equal(0, rename(SCHEMA_PREFIX "2.0.bak",
+ SCHEMA_PREFIX "2.0.rng"));
+ assert_int_equal(0, rename(SCHEMA_PREFIX "3.0.bak",
+ SCHEMA_PREFIX "3.0.rng"));
+ crm_schema_cleanup();
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(last_is_0),
+ cmocka_unit_test(last_is_not_0),
+ cmocka_unit_test(schema_0_missing))
diff --git a/lib/common/tests/schemas/pcmk__get_schema_test.c b/lib/common/tests/schemas/pcmk__get_schema_test.c
new file mode 100644
index 0000000..6513dfc
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__get_schema_test.c
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2023-2024 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/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+#include "crmcommon_private.h"
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+assert_schema(const char *name, int expected_index)
+{
+ GList *schema_entry = NULL;
+ pcmk__schema_t *schema = NULL;
+
+ schema_entry = pcmk__get_schema(name);
+ assert_non_null(schema_entry);
+
+ schema = schema_entry->data;
+ assert_non_null(schema);
+
+ assert_int_equal(schema->schema_index, expected_index);
+}
+
+static void
+unknown_schema(void **state)
+{
+ assert_null(pcmk__get_schema(""));
+ assert_null(pcmk__get_schema("blahblah"));
+ assert_null(pcmk__get_schema("pacemaker-2.47"));
+ assert_null(pcmk__get_schema("pacemaker-47.0"));
+}
+
+static void
+known_schema(void **state)
+{
+ // @COMPAT none is deprecated since 2.1.8
+ assert_schema(NULL, 16); // defaults to "none"
+
+ assert_schema("pacemaker-1.0", 0);
+ assert_schema("pacemaker-1.2", 1);
+ assert_schema("pacemaker-2.0", 3);
+ assert_schema("pacemaker-2.5", 8);
+ assert_schema("pacemaker-3.0", 14);
+}
+
+static void
+case_insensitive(void **state)
+{
+ assert_schema("PACEMAKER-1.0", 0);
+ assert_schema("pAcEmAkEr-2.0", 3);
+ assert_schema("paceMAKER-3.0", 14);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(unknown_schema),
+ cmocka_unit_test(known_schema),
+ cmocka_unit_test(case_insensitive));
diff --git a/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c
new file mode 100644
index 0000000..68744ef
--- /dev/null
+++ b/lib/common/tests/schemas/pcmk__schema_files_later_than_test.c
@@ -0,0 +1,106 @@
+/*
+ * Copyright 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/lists_internal.h>
+
+#include <glib.h>
+
+static int
+setup(void **state)
+{
+ setenv("PCMK_schema_directory", PCMK__TEST_SCHEMA_DIR, 1);
+ crm_schema_init();
+ return 0;
+}
+
+static int
+teardown(void **state)
+{
+ crm_schema_cleanup();
+ unsetenv("PCMK_schema_directory");
+ return 0;
+}
+
+static void
+invalid_name(void **state)
+{
+ assert_null(pcmk__schema_files_later_than("xyz"));
+ assert_null(pcmk__schema_files_later_than("pacemaker-"));
+}
+
+static void
+valid_name(void **state)
+{
+ GList *schemas = NULL;
+
+ schemas = pcmk__schema_files_later_than("pacemaker-1.0");
+ assert_int_equal(g_list_length(schemas), 18);
+ /* There is no "pacemaker-1.1". */
+ assert_string_equal("pacemaker-1.2.rng", g_list_nth_data(schemas, 0));
+ assert_string_equal("upgrade-1.3.xsl", g_list_nth_data(schemas, 1));
+ assert_string_equal("pacemaker-1.3.rng", g_list_nth_data(schemas, 2));
+ assert_string_equal("pacemaker-2.0.rng", g_list_nth_data(schemas, 3));
+ assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 4));
+ assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 5));
+ assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 6));
+ assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 7));
+ assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 8));
+ assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 9));
+ assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 10));
+ assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 11));
+ assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 12));
+ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 13));
+ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 14));
+ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 15));
+ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 16));
+ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 17));
+ g_list_free_full(schemas, free);
+
+ /* Adding .rng to the end of the schema we're requesting is also valid. */
+ schemas = pcmk__schema_files_later_than("pacemaker-2.0.rng");
+ assert_int_equal(g_list_length(schemas), 14);
+ assert_string_equal("pacemaker-2.1.rng", g_list_nth_data(schemas, 0));
+ assert_string_equal("pacemaker-2.2.rng", g_list_nth_data(schemas, 1));
+ assert_string_equal("pacemaker-2.3.rng", g_list_nth_data(schemas, 2));
+ assert_string_equal("pacemaker-2.4.rng", g_list_nth_data(schemas, 3));
+ assert_string_equal("pacemaker-2.5.rng", g_list_nth_data(schemas, 4));
+ assert_string_equal("pacemaker-2.6.rng", g_list_nth_data(schemas, 5));
+ assert_string_equal("pacemaker-2.7.rng", g_list_nth_data(schemas, 6));
+ assert_string_equal("pacemaker-2.8.rng", g_list_nth_data(schemas, 7));
+ assert_string_equal("pacemaker-2.9.rng", g_list_nth_data(schemas, 8));
+ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 9));
+ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 10));
+ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 11));
+ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 12));
+ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 13));
+ g_list_free_full(schemas, free);
+
+ /* Check that "pacemaker-2.10" counts as later than "pacemaker-2.9". */
+ schemas = pcmk__schema_files_later_than("pacemaker-2.9");
+ assert_int_equal(g_list_length(schemas), 5);
+ assert_string_equal("upgrade-2.10-leave.xsl", g_list_nth_data(schemas, 0));
+ assert_string_equal("upgrade-2.10-enter.xsl", g_list_nth_data(schemas, 1));
+ assert_string_equal("upgrade-2.10.xsl", g_list_nth_data(schemas, 2));
+ assert_string_equal("pacemaker-2.10.rng", g_list_nth_data(schemas, 3));
+ assert_string_equal("pacemaker-3.0.rng", g_list_nth_data(schemas, 4));
+ g_list_free_full(schemas, free);
+
+ /* And then something way in the future that will never apply due to our
+ * special schema directory.
+ */
+ schemas = pcmk__schema_files_later_than("pacemaker-9.0");
+ assert_null(schemas);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(invalid_name),
+ cmocka_unit_test(valid_name))
diff --git a/lib/common/tests/scores/char2score_test.c b/lib/common/tests/scores/char2score_test.c
index fbba12a..5d7252f 100644
--- a/lib/common/tests/scores/char2score_test.c
+++ b/lib/common/tests/scores/char2score_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -32,9 +32,9 @@ bad_input(void **state)
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);
+ assert_int_equal(char2score("-INFINITY"), -PCMK_SCORE_INFINITY);
+ assert_int_equal(char2score("INFINITY"), PCMK_SCORE_INFINITY);
+ assert_int_equal(char2score("+INFINITY"), PCMK_SCORE_INFINITY);
pcmk__score_red = 10;
pcmk__score_green = 20;
@@ -56,8 +56,10 @@ special_values(void **state)
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);
+ assert_int_equal(char2score(B(PCMK_SCORE_INFINITY) "00"),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(char2score("-" B(PCMK_SCORE_INFINITY) "00"),
+ -PCMK_SCORE_INFINITY);
}
static void
diff --git a/lib/common/tests/scores/pcmk__add_scores_test.c b/lib/common/tests/scores/pcmk__add_scores_test.c
index 1309659..952cf97 100644
--- a/lib/common/tests/scores/pcmk__add_scores_test.c
+++ b/lib/common/tests/scores/pcmk__add_scores_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -14,48 +14,71 @@
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);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY,
+ -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, -1),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, 0),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY, 1),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY,
+ PCMK_SCORE_INFINITY),
+ -PCMK_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);
+ assert_int_equal(pcmk__add_scores(-1, -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(0, -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(1, -PCMK_SCORE_INFINITY),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY,
+ -PCMK_SCORE_INFINITY),
+ -PCMK_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);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, -1),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, 0),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY, 1),
+ PCMK_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);
+ assert_int_equal(pcmk__add_scores(-1, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(0, PCMK_SCORE_INFINITY),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(1, PCMK_SCORE_INFINITY),
+ PCMK_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(CRM_SCORE_INFINITY/2, CRM_SCORE_INFINITY/2), CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-CRM_SCORE_INFINITY/2, -CRM_SCORE_INFINITY/2), -CRM_SCORE_INFINITY);
- assert_int_equal(pcmk__add_scores(-4000000, 50), -CRM_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(INT_MAX, INT_MAX), PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(INT_MIN, INT_MIN), -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(2000000, 50), PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(PCMK_SCORE_INFINITY/2,
+ PCMK_SCORE_INFINITY/2),
+ PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-PCMK_SCORE_INFINITY/2,
+ -PCMK_SCORE_INFINITY/2),
+ -PCMK_SCORE_INFINITY);
+ assert_int_equal(pcmk__add_scores(-4000000, 50), -PCMK_SCORE_INFINITY);
}
static void
diff --git a/lib/common/tests/scores/pcmk_readable_score_test.c b/lib/common/tests/scores/pcmk_readable_score_test.c
index ae24159..c3d66f6 100644
--- a/lib/common/tests/scores/pcmk_readable_score_test.c
+++ b/lib/common/tests/scores/pcmk_readable_score_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -14,10 +14,10 @@
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);
+ assert_string_equal(pcmk_readable_score(PCMK_SCORE_INFINITY * 2),
+ PCMK_VALUE_INFINITY);
+ assert_string_equal(pcmk_readable_score(-PCMK_SCORE_INFINITY * 2),
+ PCMK_VALUE_MINUS_INFINITY);
}
static void
diff --git a/lib/common/tests/strings/Makefile.am b/lib/common/tests/strings/Makefile.am
index e66af0d..439b6bf 100644
--- a/lib/common/tests/strings/Makefile.am
+++ b/lib/common/tests/strings/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -16,7 +16,6 @@ check_PROGRAMS = crm_get_msec_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 \
diff --git a/lib/common/tests/strings/crm_get_msec_test.c b/lib/common/tests/strings/crm_get_msec_test.c
index 5da548b..14b87cf 100644
--- a/lib/common/tests/strings/crm_get_msec_test.c
+++ b/lib/common/tests/strings/crm_get_msec_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -19,6 +19,11 @@ bad_input(void **state) {
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);
+
+ assert_int_equal(crm_get_msec("3.xs"), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec(" 3. xs "), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec("3.14xs"), PCMK__PARSE_INT_DEFAULT);
+ assert_int_equal(crm_get_msec(" 3.14 xs "), PCMK__PARSE_INT_DEFAULT);
}
static void
@@ -28,6 +33,7 @@ good_input(void **state) {
assert_int_equal(crm_get_msec("\t100\n"), 100000);
assert_int_equal(crm_get_msec("100ms"), 100);
+ assert_int_equal(crm_get_msec(" 100 ms "), 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);
@@ -37,6 +43,28 @@ good_input(void **state) {
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);
+
+ assert_int_equal(crm_get_msec("3."), 3000);
+ assert_int_equal(crm_get_msec(" 3. ms "), 3);
+ assert_int_equal(crm_get_msec("3.14"), 3000);
+ assert_int_equal(crm_get_msec(" 3.14 ms "), 3);
+
+ // Questionable
+ assert_int_equal(crm_get_msec("3.14."), 3000);
+ assert_int_equal(crm_get_msec(" 3.14. ms "), 3);
+ assert_int_equal(crm_get_msec("3.14.159"), 3000);
+ assert_int_equal(crm_get_msec(" 3.14.159 "), 3000);
+ assert_int_equal(crm_get_msec("3.14.159ms"), 3);
+ assert_int_equal(crm_get_msec(" 3.14.159 ms "), 3);
+
+ // Questionable
+ assert_int_equal(crm_get_msec(" 100 mshr "), 100);
+ assert_int_equal(crm_get_msec(" 100 ms hr "), 100);
+ assert_int_equal(crm_get_msec(" 100 sasdf "), 100000);
+ assert_int_equal(crm_get_msec(" 100 s asdf "), 100000);
+ assert_int_equal(crm_get_msec(" 3.14 shour "), 3000);
+ assert_int_equal(crm_get_msec(" 3.14 s hour "), 3000);
+ assert_int_equal(crm_get_msec(" 3.14 ms!@#$ "), 3);
}
static void
diff --git a/lib/common/tests/strings/crm_str_to_boolean_test.c b/lib/common/tests/strings/crm_str_to_boolean_test.c
index 3bd2e5d..1b0ba45 100644
--- a/lib/common/tests/strings/crm_str_to_boolean_test.c
+++ b/lib/common/tests/strings/crm_str_to_boolean_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -40,6 +40,13 @@ is_true(void **state) {
assert_true(ret);
assert_int_equal(crm_str_to_boolean("1", &ret), 1);
assert_true(ret);
+
+ // Ensure it still validates the string with a NULL result argument
+ assert_int_equal(crm_str_to_boolean("true", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("on", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("yes", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("y", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("1", NULL), 1);
}
static void
@@ -73,6 +80,13 @@ is_false(void **state) {
assert_false(ret);
assert_int_equal(crm_str_to_boolean("0", &ret), 1);
assert_false(ret);
+
+ // Ensure it still validates the string with a NULL result argument
+ assert_int_equal(crm_str_to_boolean("false", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("off", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("no", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("n", NULL), 1);
+ assert_int_equal(crm_str_to_boolean("0", NULL), 1);
}
static void
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
deleted file mode 100644
index e70dfb4..0000000
--- a/lib/common/tests/strings/pcmk__char_in_any_str_test.c
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * 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
index 7b59d9d..813bcdb 100644
--- a/lib/common/tests/strings/pcmk__compress_test.c
+++ b/lib/common/tests/strings/pcmk__compress_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -20,7 +20,7 @@ const char *SIMPLE_COMPRESSED = "BZh41AY&SYO\x1ai";
static void
simple_compress(void **state)
{
- char *result = calloc(1024, sizeof(char));
+ char *result = pcmk__assert_alloc(1024, sizeof(char));
unsigned int len;
assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 0, &result, &len), pcmk_rc_ok);
@@ -30,7 +30,7 @@ simple_compress(void **state)
static void
max_too_small(void **state)
{
- char *result = calloc(1024, sizeof(char));
+ char *result = pcmk__assert_alloc(1024, sizeof(char));
unsigned int len;
assert_int_equal(pcmk__compress(SIMPLE_DATA, 40, 10, &result, &len), EFBIG);
@@ -38,10 +38,11 @@ max_too_small(void **state)
static void
calloc_fails(void **state) {
- char *result = calloc(1024, sizeof(char));
+ char *result = pcmk__assert_alloc(1024, sizeof(char));
unsigned int len;
- pcmk__assert_asserts(
+ pcmk__assert_exits(
+ CRM_EX_OSERR,
{
pcmk__mock_calloc = true; // calloc() will return NULL
expect_value(__wrap_calloc, nmemb, (size_t) ((40 * 1.01) + 601));
diff --git a/lib/common/tests/strings/pcmk__str_update_test.c b/lib/common/tests/strings/pcmk__str_update_test.c
index 571031d..4a44fba 100644
--- a/lib/common/tests/strings/pcmk__str_update_test.c
+++ b/lib/common/tests/strings/pcmk__str_update_test.c
@@ -59,7 +59,8 @@ strdup_fails(void **state) {
str = strdup("hello");
- pcmk__assert_asserts(
+ pcmk__assert_exits(
+ CRM_EX_OSERR,
{
pcmk__mock_strdup = true; // strdup() will return NULL
expect_string(__wrap_strdup, s, "world");
diff --git a/lib/common/tests/utils/Makefile.am b/lib/common/tests/utils/Makefile.am
index f028ce4..fb9d5c3 100644
--- a/lib/common/tests/utils/Makefile.am
+++ b/lib/common/tests/utils/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2020-2023 the Pacemaker project contributors
+# Copyright 2020-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -12,8 +12,6 @@ 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 \
@@ -21,10 +19,7 @@ check_PROGRAMS = compare_version_test \
pcmk__fail_attr_name_test \
pcmk__failcount_name_test \
pcmk__getpid_s_test \
- pcmk__lastfailure_name_test
-
-if WRAPPABLE_UNAME
-check_PROGRAMS += pcmk_hostname_test
-endif
+ pcmk__lastfailure_name_test \
+ pcmk__realloc_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/utils/compare_version_test.c b/lib/common/tests/utils/compare_version_test.c
index 35ebb63..d191f4a 100644
--- a/lib/common/tests/utils/compare_version_test.c
+++ b/lib/common/tests/utils/compare_version_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2022-2023 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -46,6 +46,9 @@ 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);
+ assert_int_equal(compare_version("1.0", "1"), 0);
+ assert_int_equal(compare_version("1", "1.2"), -1);
+ assert_int_equal(compare_version("1.2", "1"), 1);
}
PCMK__UNIT_TEST(NULL, NULL,
diff --git a/lib/common/tests/utils/pcmk__realloc_test.c b/lib/common/tests/utils/pcmk__realloc_test.c
new file mode 100644
index 0000000..62d51df
--- /dev/null
+++ b/lib/common/tests/utils/pcmk__realloc_test.c
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024 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_size(void **state)
+{
+ char *ptr = NULL;
+
+ pcmk__assert_asserts(pcmk__realloc(ptr, 0));
+}
+
+static void
+realloc_fails(void **state)
+{
+ char *ptr = NULL;
+
+ pcmk__assert_aborts(
+ {
+ pcmk__mock_realloc = true; // realloc() will return NULL
+ expect_any(__wrap_realloc, ptr);
+ expect_value(__wrap_realloc, size, 1000);
+ pcmk__realloc(ptr, 1000);
+ pcmk__mock_realloc = false; // Use real realloc()
+ }
+ );
+}
+
+static void
+realloc_succeeds(void **state)
+{
+ char *ptr = NULL;
+
+ /* We can't really test that the resulting pointer is the size we asked
+ * for - it might be larger if that's what the memory allocator decides
+ * to do. And anyway, testing realloc isn't really the point. All we
+ * want to do here is make sure the function works when given good input.
+ */
+
+ /* Allocate new memory */
+ ptr = pcmk__realloc(ptr, 1000);
+ assert_non_null(ptr);
+
+ /* Grow previously allocated memory */
+ ptr = pcmk__realloc(ptr, 2000);
+ assert_non_null(ptr);
+
+ /* Shrink previously allocated memory */
+ ptr = pcmk__realloc(ptr, 500);
+ assert_non_null(ptr);
+
+ free(ptr);
+}
+
+PCMK__UNIT_TEST(NULL, NULL,
+ cmocka_unit_test(bad_size),
+ cmocka_unit_test(realloc_fails),
+ cmocka_unit_test(realloc_succeeds))
diff --git a/lib/common/tests/utils/pcmk_hostname_test.c b/lib/common/tests/utils/pcmk_hostname_test.c
deleted file mode 100644
index 7329486..0000000
--- a/lib/common/tests/utils/pcmk_hostname_test.c
+++ /dev/null
@@ -1,56 +0,0 @@
-/*
- * 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/xml/Makefile.am b/lib/common/tests/xml/Makefile.am
index 465c950..9ed1620 100644
--- a/lib/common/tests/xml/Makefile.am
+++ b/lib/common/tests/xml/Makefile.am
@@ -1,5 +1,5 @@
#
-# Copyright 2022-2023 the Pacemaker project contributors
+# Copyright 2022-2024 the Pacemaker project contributors
#
# The version control history for this file may have further details.
#
@@ -11,7 +11,12 @@ 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
+check_PROGRAMS = crm_xml_init_test \
+ pcmk__xe_copy_attrs_test \
+ pcmk__xe_first_child_test \
+ pcmk__xe_foreach_child_test \
+ pcmk__xe_set_score_test \
+ pcmk__xml_escape_test \
+ pcmk__xml_needs_escape_test
TESTS = $(check_PROGRAMS)
diff --git a/lib/common/tests/xml/crm_xml_init_test.c b/lib/common/tests/xml/crm_xml_init_test.c
new file mode 100644
index 0000000..1f78728
--- /dev/null
+++ b/lib/common/tests/xml/crm_xml_init_test.c
@@ -0,0 +1,230 @@
+/*
+ * Copyright 2023-2024 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/xml.h>
+#include <crm/common/unittest_internal.h>
+#include <crm/common/xml_internal.h>
+
+#include "crmcommon_private.h"
+
+/* Copied from lib/common/xml.c */
+#define XML_DOC_PRIVATE_MAGIC 0x81726354UL
+#define XML_NODE_PRIVATE_MAGIC 0x54637281UL
+
+static int
+setup(void **state) {
+ crm_xml_init();
+ return 0;
+}
+
+static int
+teardown(void **state) {
+ crm_xml_cleanup();
+ return 0;
+}
+
+static void
+buffer_scheme_test(void **state) {
+ assert_int_equal(XML_BUFFER_ALLOC_DOUBLEIT, xmlGetBufferAllocationScheme());
+}
+
+/* These functions also serve as unit tests of the static new_private_data
+ * function. We can't test free_private_data because libxml will call that as
+ * part of freeing everything else. By the time we'd get back into a unit test
+ * where we could check that private members are NULL, the structure containing
+ * the private data would have been freed.
+ *
+ * This could probably be tested with a lot of function mocking, but that
+ * doesn't seem worth it.
+ */
+
+static void
+create_document_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+
+ /* Double check things */
+ assert_non_null(doc);
+ assert_int_equal(doc->type, XML_DOCUMENT_NODE);
+
+ /* Check that the private data is initialized correctly */
+ docpriv = doc->_private;
+ assert_non_null(docpriv);
+ assert_int_equal(docpriv->check, XML_DOC_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeDoc(doc);
+}
+
+static void
+create_element_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_ELEMENT_NODE);
+
+ /* Check that the private data is initialized correctly */
+ priv = node->_private;
+ assert_non_null(priv);
+ assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_attr_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL);
+ xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) PCMK_XA_NAME,
+ (pcmkXmlStr) "dummy-value");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(attr);
+ assert_int_equal(attr->type, XML_ATTRIBUTE_NODE);
+
+ /* Check that the private data is initialized correctly */
+ priv = attr->_private;
+ assert_non_null(priv);
+ assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_comment_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocComment(doc, (pcmkXmlStr) "blahblah");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_COMMENT_NODE);
+
+ /* Check that the private data is initialized correctly */
+ priv = node->_private;
+ assert_non_null(priv);
+ assert_int_equal(priv->check, XML_NODE_PRIVATE_MAGIC);
+ assert_true(pcmk_all_flags_set(priv->flags, pcmk__xf_dirty|pcmk__xf_created));
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_text_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewDocText(doc, (pcmkXmlStr) "blahblah");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_TEXT_NODE);
+
+ /* Check that no private data was created */
+ priv = node->_private;
+ assert_null(priv);
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+static void
+create_dtd_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) PCMK_XA_NAME,
+ (pcmkXmlStr) "externalId",
+ (pcmkXmlStr) "systemId");
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(dtd);
+ assert_int_equal(dtd->type, XML_DTD_NODE);
+
+ /* Check that no private data was created */
+ priv = dtd->_private;
+ assert_null(priv);
+
+ /* Clean up */
+ /* If you call xmlFreeDtd before xmlFreeDoc, you get a segfault */
+ xmlFreeDoc(doc);
+}
+
+static void
+create_cdata_node(void **state) {
+ xml_doc_private_t *docpriv = NULL;
+ xml_node_private_t *priv = NULL;
+ xmlDocPtr doc = xmlNewDoc(PCMK__XML_VERSION);
+ xmlNodePtr node = xmlNewCDataBlock(doc, (pcmkXmlStr) "blahblah", 8);
+
+ /* Adding a node to the document marks it as dirty */
+ docpriv = doc->_private;
+ assert_true(pcmk_all_flags_set(docpriv->flags, pcmk__xf_dirty));
+
+ /* Double check things */
+ assert_non_null(node);
+ assert_int_equal(node->type, XML_CDATA_SECTION_NODE);
+
+ /* Check that no private data was created */
+ priv = node->_private;
+ assert_null(priv);
+
+ /* Clean up */
+ xmlFreeNode(node);
+ xmlFreeDoc(doc);
+}
+
+PCMK__UNIT_TEST(setup, teardown,
+ cmocka_unit_test(buffer_scheme_test),
+ cmocka_unit_test(create_document_node),
+ cmocka_unit_test(create_element_node),
+ cmocka_unit_test(create_attr_node),
+ cmocka_unit_test(create_comment_node),
+ cmocka_unit_test(create_text_node),
+ cmocka_unit_test(create_dtd_node),
+ cmocka_unit_test(create_cdata_node));
diff --git a/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c b/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c
new file mode 100644
index 0000000..146317c
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xe_copy_attrs_test.c
@@ -0,0 +1,188 @@
+ /*
+ * Copyright 2022-2024 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)
+{
+ // This test dumps core via CRM_CHECK()
+ xmlNode *xml = pcmk__xe_create(NULL, "test");
+
+ assert_int_equal(pcmk__xe_copy_attrs(NULL, NULL, pcmk__xaf_none), EINVAL);
+ assert_int_equal(pcmk__xe_copy_attrs(NULL, xml, pcmk__xaf_none), EINVAL);
+ assert_int_equal(pcmk__xe_copy_attrs(xml, NULL, pcmk__xaf_none), EINVAL);
+ assert_ptr_equal(xml->properties, NULL);
+
+ free_xml(xml);
+}
+
+static void
+no_source_attrs(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ // Ensure copying from empty source doesn't create target properties
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_ptr_equal(target->properties, NULL);
+
+ // Ensure copying from empty source doesn't delete target attributes
+ crm_xml_add(target, "attr", "value");
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(target, "attr"), "value");
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+copy_one(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "attr", "value");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+copy_multiple(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ pcmk__xe_set_props(src,
+ "attr1", "value1",
+ "attr2", "value2",
+ "attr3", "value3",
+ NULL);
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr1"),
+ crm_element_value(target, "attr1"));
+ assert_string_equal(crm_element_value(src, "attr2"),
+ crm_element_value(target, "attr2"));
+ assert_string_equal(crm_element_value(src, "attr3"),
+ crm_element_value(target, "attr3"));
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+overwrite(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "attr", "src_value");
+ crm_xml_add(target, "attr", "target_value");
+
+ // Overwrite enabled by default
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+no_overwrite(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "attr", "src_value");
+ crm_xml_add(target, "attr", "target_value");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_no_overwrite),
+ pcmk_rc_ok);
+ assert_string_not_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+
+ // no_overwrite doesn't prevent copy if there's no conflict
+ pcmk__xe_remove_attr(target, "attr");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_no_overwrite),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(src, "attr"),
+ crm_element_value(target, "attr"));
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+score_update(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "plus_plus_attr", "plus_plus_attr++");
+ crm_xml_add(src, "plus_two_attr", "plus_two_attr+=2");
+ crm_xml_add(target, "plus_plus_attr", "1");
+ crm_xml_add(target, "plus_two_attr", "1");
+
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_score_update),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(target, "plus_plus_attr"), "2");
+ assert_string_equal(crm_element_value(target, "plus_two_attr"), "3");
+
+ free_xml(src);
+ free_xml(target);
+}
+
+static void
+no_score_update(void **state)
+{
+ xmlNode *src = pcmk__xe_create(NULL, "test");
+ xmlNode *target = pcmk__xe_create(NULL, "test");
+
+ crm_xml_add(src, "plus_plus_attr", "plus_plus_attr++");
+ crm_xml_add(src, "plus_two_attr", "plus_two_attr+=2");
+ crm_xml_add(target, "plus_plus_attr", "1");
+ crm_xml_add(target, "plus_two_attr", "1");
+
+ // Score update disabled by default
+ assert_int_equal(pcmk__xe_copy_attrs(target, src, pcmk__xaf_none),
+ pcmk_rc_ok);
+ assert_string_equal(crm_element_value(target, "plus_plus_attr"),
+ "plus_plus_attr++");
+ assert_string_equal(crm_element_value(target, "plus_two_attr"),
+ "plus_two_attr+=2");
+
+ free_xml(src);
+ free_xml(target);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_args),
+ cmocka_unit_test(no_source_attrs),
+ cmocka_unit_test(copy_one),
+ cmocka_unit_test(copy_multiple),
+ cmocka_unit_test(overwrite),
+ cmocka_unit_test(no_overwrite),
+ cmocka_unit_test(score_update),
+ cmocka_unit_test(no_score_update));
diff --git a/lib/common/tests/xml/pcmk__xe_match_test.c b/lib/common/tests/xml/pcmk__xe_first_child_test.c
index be2c949..64b90b0 100644
--- a/lib/common/tests/xml/pcmk__xe_match_test.c
+++ b/lib/common/tests/xml/pcmk__xe_first_child_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022 the Pacemaker project contributors
+ * Copyright 2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,97 +9,97 @@
#include <crm_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/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"
+ " <nodeA attrA=\"123\" " PCMK_XA_ID "=\"1\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is an A node -->\n"
- " <nodeA attrA=\"456\" " XML_ATTR_ID "=\"2\">\n"
+ " <nodeA attrA=\"456\" " PCMK_XA_ID "=\"2\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is an A node -->\n"
- " <nodeA attrB=\"XYZ\" " XML_ATTR_ID "=\"3\">\n"
+ " <nodeA attrB=\"XYZ\" " PCMK_XA_ID "=\"3\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is a B node -->\n"
- " <nodeB attrA=\"123\" " XML_ATTR_ID "=\"4\">\n"
+ " <nodeB attrA=\"123\" " PCMK_XA_ID "=\"4\">\n"
" content\n"
" </nodeA>\n"
" <!-- This is a B node -->\n"
- " <nodeB attrB=\"ABC\" " XML_ATTR_ID "=\"5\">\n"
+ " <nodeB attrB=\"ABC\" " PCMK_XA_ID "=\"5\">\n"
" content\n"
" </nodeA>\n"
"</xml>";
static void
bad_input(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
- assert_null(pcmk__xe_match(NULL, NULL, NULL, NULL));
- assert_null(pcmk__xe_match(NULL, NULL, NULL, "attrX"));
+ assert_null(pcmk__xe_first_child(NULL, NULL, NULL, NULL));
+ assert_null(pcmk__xe_first_child(NULL, NULL, NULL, "attrX"));
free_xml(xml);
}
static void
not_found(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
/* No node with an attrX attribute */
- assert_null(pcmk__xe_match(xml, NULL, "attrX", NULL));
+ assert_null(pcmk__xe_first_child(xml, NULL, "attrX", NULL));
/* No nodeX node */
- assert_null(pcmk__xe_match(xml, "nodeX", NULL, NULL));
+ assert_null(pcmk__xe_first_child(xml, "nodeX", NULL, NULL));
/* No nodeA node with attrX */
- assert_null(pcmk__xe_match(xml, "nodeA", "attrX", NULL));
+ assert_null(pcmk__xe_first_child(xml, "nodeA", "attrX", NULL));
/* No nodeA node with attrA=XYZ */
- assert_null(pcmk__xe_match(xml, "nodeA", "attrA", "XYZ"));
+ assert_null(pcmk__xe_first_child(xml, "nodeA", "attrA", "XYZ"));
free_xml(xml);
}
static void
find_attrB(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
xmlNode *result = NULL;
/* Find the first node with attrB */
- result = pcmk__xe_match(xml, NULL, "attrB", NULL);
+ result = pcmk__xe_first_child(xml, NULL, "attrB", NULL);
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "3");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "3");
/* Find the first nodeB with attrB */
- result = pcmk__xe_match(xml, "nodeB", "attrB", NULL);
+ result = pcmk__xe_first_child(xml, "nodeB", "attrB", NULL);
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "5");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "5");
free_xml(xml);
}
static void
find_attrA_matching(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
xmlNode *result = NULL;
/* Find attrA=456 */
- result = pcmk__xe_match(xml, NULL, "attrA", "456");
+ result = pcmk__xe_first_child(xml, NULL, "attrA", "456");
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "2");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "2");
/* Find a nodeB with attrA=123 */
- result = pcmk__xe_match(xml, "nodeB", "attrA", "123");
+ result = pcmk__xe_first_child(xml, "nodeB", "attrA", "123");
assert_non_null(result);
- assert_string_equal(crm_element_value(result, "id"), "4");
+ assert_string_equal(crm_element_value(result, PCMK_XA_ID), "4");
free_xml(xml);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(bad_input),
cmocka_unit_test(not_found),
cmocka_unit_test(find_attrB),
diff --git a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
index ffb9171..a833dde 100644
--- a/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
+++ b/lib/common/tests/xml/pcmk__xe_foreach_child_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2022-2023 the Pacemaker project contributors
+ * Copyright 2022-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -36,7 +36,7 @@ const char *str1 =
static void
bad_input(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
pcmk__assert_asserts(pcmk__xe_foreach_child(xml, NULL, NULL, NULL));
@@ -45,7 +45,7 @@ bad_input(void **state) {
static void
name_given_test(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -58,7 +58,7 @@ name_given_test(void **state) {
static void
no_name_given_test(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -71,7 +71,7 @@ no_name_given_test(void **state) {
static void
name_doesnt_exist_test(void **state) {
- xmlNode *xml = string2xml(str1);
+ xmlNode *xml = pcmk__xml_parse(str1);
pcmk__xe_foreach_child(xml, "xxx", compare_name_handler, NULL);
free_xml(xml);
}
@@ -100,7 +100,7 @@ const char *str2 =
static void
multiple_levels_test(void **state) {
- xmlNode *xml = string2xml(str2);
+ xmlNode *xml = pcmk__xml_parse(str2);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -112,7 +112,7 @@ multiple_levels_test(void **state) {
static void
multiple_levels_no_name_test(void **state) {
- xmlNode *xml = string2xml(str2);
+ xmlNode *xml = pcmk__xml_parse(str2);
/* The handler should be called once for every <level1> node. */
expect_function_call(compare_name_handler);
@@ -147,7 +147,7 @@ static int any_of_handler(xmlNode *xml, void *userdata) {
static void
any_of_test(void **state) {
- xmlNode *xml = string2xml(str3);
+ xmlNode *xml = pcmk__xml_parse(str3);
/* The handler should be called once for every <nodeX> node. */
expect_function_call(any_of_handler);
@@ -190,7 +190,7 @@ static int stops_on_third_handler(xmlNode *xml, void *userdata) {
static void
one_of_test(void **state) {
- xmlNode *xml = string2xml(str3);
+ xmlNode *xml = pcmk__xml_parse(str3);
/* The handler should be called once. */
expect_function_call(stops_on_first_handler);
@@ -205,7 +205,7 @@ one_of_test(void **state) {
free_xml(xml);
}
-PCMK__UNIT_TEST(NULL, NULL,
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
cmocka_unit_test(bad_input),
cmocka_unit_test(name_given_test),
cmocka_unit_test(no_name_given_test),
diff --git a/lib/common/tests/xml/pcmk__xe_set_score_test.c b/lib/common/tests/xml/pcmk__xe_set_score_test.c
new file mode 100644
index 0000000..deb85b0
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xe_set_score_test.c
@@ -0,0 +1,188 @@
+/*
+ * Copyright 2022-2024 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 "crmcommon_private.h" // pcmk__xe_set_score()
+
+/*!
+ * \internal
+ * \brief Update an XML attribute value and check it against a reference value
+ *
+ * The attribute name is hard-coded as \c "X".
+ *
+ * \param[in] initial Initial value
+ * \param[in] new Value to set
+ * \param[in] reference_val Expected attribute value after update
+ * \param[in] reference_rc Expected return code from \c pcmk__xe_set_score()
+ */
+static void
+assert_set_score(const char *initial, const char *new,
+ const char *reference_val, int reference_rc)
+{
+ const char *name = "X";
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
+
+ crm_xml_add(test_xml, name, initial);
+ assert_int_equal(pcmk__xe_set_score(test_xml, name, new), reference_rc);
+ assert_string_equal(crm_element_value(test_xml, name), reference_val);
+
+ free_xml(test_xml);
+}
+
+static void
+value_is_name_plus_plus(void **state)
+{
+ assert_set_score("5", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+value_is_name_plus_equals_integer(void **state)
+{
+ assert_set_score("5", "X+=2", "7", pcmk_rc_ok);
+}
+
+// NULL input
+
+static void
+target_is_NULL(void **state)
+{
+ // Dumps core via CRM_CHECK()
+ assert_int_equal(pcmk__xe_set_score(NULL, "X", "X++"), EINVAL);
+}
+
+static void
+name_is_NULL(void **state)
+{
+ xmlNode *test_xml = pcmk__xe_create(NULL, "test_xml");
+
+ crm_xml_add(test_xml, "X", "5");
+
+ // Dumps core via CRM_CHECK()
+ assert_int_equal(pcmk__xe_set_score(test_xml, NULL, "X++"), EINVAL);
+ assert_string_equal(crm_element_value(test_xml, "X"), "5");
+
+ free_xml(test_xml);
+}
+
+static void
+value_is_NULL(void **state)
+{
+ assert_set_score("5", NULL, "5", pcmk_rc_ok);
+}
+
+// the value input doesn't start with the name input
+
+static void
+value_is_wrong_name(void **state)
+{
+ assert_set_score("5", "Y++", "Y++", pcmk_rc_ok);
+}
+
+static void
+value_is_only_an_integer(void **state)
+{
+ assert_set_score("5", "2", "2", pcmk_rc_ok);
+}
+
+// non-integers
+
+static void
+variable_is_initialized_to_be_non_numeric(void **state)
+{
+ assert_set_score("hello", "X++", "1", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_non_numeric_2(void **state)
+{
+ assert_set_score("hello", "X+=2", "2", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing(void **state)
+{
+ assert_set_score("5.01", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_2(void **state)
+{
+ assert_set_score("5.50", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+variable_is_initialized_to_be_numeric_and_decimal_point_containing_3(void **state)
+{
+ assert_set_score("5.99", "X++", "6", pcmk_rc_ok);
+}
+
+static void
+value_is_non_numeric(void **state)
+{
+ assert_set_score("5", "X+=hello", "5", pcmk_rc_ok);
+}
+
+static void
+value_is_numeric_and_decimal_point_containing(void **state)
+{
+ assert_set_score("5", "X+=2.01", "7", pcmk_rc_ok);
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_2(void **state)
+{
+ assert_set_score("5", "X+=1.50", "6", pcmk_rc_ok);
+}
+
+static void
+value_is_numeric_and_decimal_point_containing_3(void **state)
+{
+ assert_set_score("5", "X+=1.99", "6", pcmk_rc_ok);
+}
+
+// undefined input
+
+static void
+name_is_undefined(void **state)
+{
+ assert_set_score(NULL, "X++", "X++", pcmk_rc_ok);
+}
+
+// large input
+
+static void
+assignment_result_is_too_large(void **state)
+{
+ assert_set_score("5", "X+=100000000000", "1000000", pcmk_rc_ok);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, 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_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/xml/pcmk__xml_escape_test.c b/lib/common/tests/xml/pcmk__xml_escape_test.c
new file mode 100644
index 0000000..8c6fd21
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xml_escape_test.c
@@ -0,0 +1,213 @@
+/*
+ * Copyright 2024 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 "crmcommon_private.h"
+
+static void
+assert_escape(const char *str, const char *reference,
+ enum pcmk__xml_escape_type type)
+{
+ gchar *buf = pcmk__xml_escape(str, type);
+
+ assert_string_equal(buf, reference);
+ g_free(buf);
+}
+
+static void
+null_empty(void **state)
+{
+ assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_text));
+ assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_attr));
+ assert_null(pcmk__xml_escape(NULL, pcmk__xml_escape_attr_pretty));
+
+ assert_escape("", "", pcmk__xml_escape_text);
+ assert_escape("", "", pcmk__xml_escape_attr);
+ assert_escape("", "", pcmk__xml_escape_attr_pretty);
+}
+
+static void
+invalid_type(void **state)
+{
+ const enum pcmk__xml_escape_type type = (enum pcmk__xml_escape_type) -1;
+
+ // Easier to ignore invalid type for NULL or empty string
+ assert_null(pcmk__xml_escape(NULL, type));
+ assert_escape("", "", type);
+
+ // Otherwise, assert if we somehow passed an invalid type
+ pcmk__assert_asserts(pcmk__xml_escape("he<>llo", type));
+}
+
+static void
+escape_unchanged(void **state)
+{
+ // No escaped characters (note: this string includes single quote at end)
+ const char *unchanged = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "`~!@#$%^*()-_=+/|\\[]{}?.,'";
+
+ assert_escape(unchanged, unchanged, pcmk__xml_escape_text);
+ assert_escape(unchanged, unchanged, pcmk__xml_escape_attr);
+ assert_escape(unchanged, unchanged, pcmk__xml_escape_attr_pretty);
+}
+
+// Ensure special characters get escaped at start, middle, and end
+
+static void
+escape_left_angle(void **state)
+{
+ const char *l_angle = "<abc<def<";
+ const char *l_angle_esc = PCMK__XML_ENTITY_LT "abc"
+ PCMK__XML_ENTITY_LT "def" PCMK__XML_ENTITY_LT;
+
+ assert_escape(l_angle, l_angle_esc, pcmk__xml_escape_text);
+ assert_escape(l_angle, l_angle_esc, pcmk__xml_escape_attr);
+ assert_escape(l_angle, l_angle, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_right_angle(void **state)
+{
+ const char *r_angle = ">abc>def>";
+ const char *r_angle_esc = PCMK__XML_ENTITY_GT "abc"
+ PCMK__XML_ENTITY_GT "def" PCMK__XML_ENTITY_GT;
+
+ assert_escape(r_angle, r_angle_esc, pcmk__xml_escape_text);
+ assert_escape(r_angle, r_angle_esc, pcmk__xml_escape_attr);
+ assert_escape(r_angle, r_angle, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_ampersand(void **state)
+{
+ const char *ampersand = "&abc&def&";
+ const char *ampersand_esc = PCMK__XML_ENTITY_AMP "abc"
+ PCMK__XML_ENTITY_AMP "def" PCMK__XML_ENTITY_AMP;
+
+ assert_escape(ampersand, ampersand_esc, pcmk__xml_escape_text);
+ assert_escape(ampersand, ampersand_esc, pcmk__xml_escape_attr);
+ assert_escape(ampersand, ampersand, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_double_quote(void **state)
+{
+ const char *double_quote = "\"abc\"def\"";
+ const char *double_quote_esc_ref = PCMK__XML_ENTITY_QUOT "abc"
+ PCMK__XML_ENTITY_QUOT "def"
+ PCMK__XML_ENTITY_QUOT;
+ const char *double_quote_esc_backslash = "\\\"abc\\\"def\\\"";
+
+ assert_escape(double_quote, double_quote, pcmk__xml_escape_text);
+ assert_escape(double_quote, double_quote_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(double_quote, double_quote_esc_backslash,
+ pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_newline(void **state)
+{
+ const char *newline = "\nabc\ndef\n";
+ const char *newline_esc_ref = "&#x0A;abc&#x0A;def&#x0A;";
+ const char *newline_esc_backslash = "\\nabc\\ndef\\n";
+
+ assert_escape(newline, newline, pcmk__xml_escape_text);
+ assert_escape(newline, newline_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(newline, newline_esc_backslash, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_tab(void **state)
+{
+ const char *tab = "\tabc\tdef\t";
+ const char *tab_esc_ref = "&#x09;abc&#x09;def&#x09;";
+ const char *tab_esc_backslash = "\\tabc\\tdef\\t";
+
+ assert_escape(tab, tab, pcmk__xml_escape_text);
+ assert_escape(tab, tab_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(tab, tab_esc_backslash, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_carriage_return(void **state)
+{
+ const char *cr = "\rabc\rdef\r";
+ const char *cr_esc_ref = "&#x0D;abc&#x0D;def&#x0D;";
+ const char *cr_esc_backslash = "\\rabc\\rdef\\r";
+
+ assert_escape(cr, cr_esc_ref, pcmk__xml_escape_text);
+ assert_escape(cr, cr_esc_ref, pcmk__xml_escape_attr);
+ assert_escape(cr, cr_esc_backslash, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_nonprinting(void **state)
+{
+ const char *nonprinting = "\a\x7F\x1B";
+ const char *nonprinting_esc = "&#x07;&#x7F;&#x1B;";
+
+ assert_escape(nonprinting, nonprinting_esc, pcmk__xml_escape_text);
+ assert_escape(nonprinting, nonprinting_esc, pcmk__xml_escape_attr);
+ assert_escape(nonprinting, nonprinting, pcmk__xml_escape_attr_pretty);
+}
+
+static void
+escape_utf8(void **state)
+{
+ /* Non-ASCII UTF-8 characters may be two, three, or four 8-bit bytes wide
+ * and should not be escaped.
+ */
+ const char *chinese = "仅高级使用";
+ const char *two_byte = "abc""\xCF\xA6""d<ef";
+ const char *two_byte_esc = "abc""\xCF\xA6""d" PCMK__XML_ENTITY_LT "ef";
+
+ const char *three_byte = "abc""\xEF\x98\x98""d<ef";
+ const char *three_byte_esc = "abc""\xEF\x98\x98""d"
+ PCMK__XML_ENTITY_LT "ef";
+
+ const char *four_byte = "abc""\xF0\x94\x81\x90""d<ef";
+ const char *four_byte_esc = "abc""\xF0\x94\x81\x90""d"
+ PCMK__XML_ENTITY_LT "ef";
+
+ assert_escape(chinese, chinese, pcmk__xml_escape_text);
+ assert_escape(chinese, chinese, pcmk__xml_escape_attr);
+ assert_escape(chinese, chinese, pcmk__xml_escape_attr_pretty);
+
+ assert_escape(two_byte, two_byte_esc, pcmk__xml_escape_text);
+ assert_escape(two_byte, two_byte_esc, pcmk__xml_escape_attr);
+ assert_escape(two_byte, two_byte, pcmk__xml_escape_attr_pretty);
+
+ assert_escape(three_byte, three_byte_esc, pcmk__xml_escape_text);
+ assert_escape(three_byte, three_byte_esc, pcmk__xml_escape_attr);
+ assert_escape(three_byte, three_byte, pcmk__xml_escape_attr_pretty);
+
+ assert_escape(four_byte, four_byte_esc, pcmk__xml_escape_text);
+ assert_escape(four_byte, four_byte_esc, pcmk__xml_escape_attr);
+ assert_escape(four_byte, four_byte, pcmk__xml_escape_attr_pretty);
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_empty),
+ cmocka_unit_test(invalid_type),
+ cmocka_unit_test(escape_unchanged),
+ cmocka_unit_test(escape_left_angle),
+ cmocka_unit_test(escape_right_angle),
+ cmocka_unit_test(escape_ampersand),
+ cmocka_unit_test(escape_double_quote),
+ cmocka_unit_test(escape_newline),
+ cmocka_unit_test(escape_tab),
+ cmocka_unit_test(escape_carriage_return),
+ cmocka_unit_test(escape_nonprinting),
+ cmocka_unit_test(escape_utf8));
diff --git a/lib/common/tests/xml/pcmk__xml_needs_escape_test.c b/lib/common/tests/xml/pcmk__xml_needs_escape_test.c
new file mode 100644
index 0000000..612f61b
--- /dev/null
+++ b/lib/common/tests/xml/pcmk__xml_needs_escape_test.c
@@ -0,0 +1,337 @@
+/*
+ * Copyright 2024 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 "crmcommon_private.h"
+
+static void
+null_empty(void **state)
+{
+ assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(NULL, pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape("", pcmk__xml_escape_attr_pretty));
+}
+
+static void
+invalid_type(void **state)
+{
+ const enum pcmk__xml_escape_type type = (enum pcmk__xml_escape_type) -1;
+
+ // Easier to ignore invalid type for NULL or empty string
+ assert_false(pcmk__xml_needs_escape(NULL, type));
+ assert_false(pcmk__xml_needs_escape("", type));
+
+ // Otherwise, assert if we somehow passed an invalid type
+ pcmk__assert_asserts(pcmk__xml_needs_escape("he<>llo", type));
+}
+
+static void
+escape_unchanged(void **state)
+{
+ // No escaped characters (note: this string includes single quote at end)
+ const char *unchanged = "abcdefghijklmnopqrstuvwxyz"
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
+ "0123456789"
+ "`~!@#$%^*()-_=+/|\\[]{}?.,'";
+
+ assert_false(pcmk__xml_needs_escape(unchanged, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(unchanged, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(unchanged,
+ pcmk__xml_escape_attr_pretty));
+}
+
+// Ensure special characters get escaped at start, middle, and end
+
+static void
+escape_left_angle(void **state)
+{
+ const char *l_angle_left = "<abcdef";
+ const char *l_angle_mid = "abc<def";
+ const char *l_angle_right = "abcdef<";
+
+ assert_true(pcmk__xml_needs_escape(l_angle_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(l_angle_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(l_angle_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(l_angle_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(l_angle_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(l_angle_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(l_angle_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(l_angle_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(l_angle_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_right_angle(void **state)
+{
+ const char *r_angle_left = ">abcdef";
+ const char *r_angle_mid = "abc>def";
+ const char *r_angle_right = "abcdef>";
+
+ assert_true(pcmk__xml_needs_escape(r_angle_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(r_angle_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(r_angle_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(r_angle_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(r_angle_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(r_angle_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(r_angle_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(r_angle_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(r_angle_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_ampersand(void **state)
+{
+ const char *ampersand_left = "&abcdef";
+ const char *ampersand_mid = "abc&def";
+ const char *ampersand_right = "abcdef&";
+
+ assert_true(pcmk__xml_needs_escape(ampersand_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(ampersand_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(ampersand_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(ampersand_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(ampersand_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(ampersand_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(ampersand_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(ampersand_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(ampersand_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_double_quote(void **state)
+{
+ const char *double_quote_left = "\"abcdef";
+ const char *double_quote_mid = "abc\"def";
+ const char *double_quote_right = "abcdef\"";
+
+ assert_false(pcmk__xml_needs_escape(double_quote_left,
+ pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(double_quote_mid,
+ pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(double_quote_right,
+ pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(double_quote_left,
+ pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(double_quote_mid,
+ pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(double_quote_right,
+ pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(double_quote_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(double_quote_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(double_quote_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_newline(void **state)
+{
+ const char *newline_left = "\nabcdef";
+ const char *newline_mid = "abc\ndef";
+ const char *newline_right = "abcdef\n";
+
+ assert_false(pcmk__xml_needs_escape(newline_left, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(newline_mid, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(newline_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(newline_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(newline_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(newline_right, pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(newline_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(newline_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(newline_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_tab(void **state)
+{
+ const char *tab_left = "\tabcdef";
+ const char *tab_mid = "abc\tdef";
+ const char *tab_right = "abcdef\t";
+
+ assert_false(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(tab_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(tab_right, pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(tab_left, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(tab_mid, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(tab_right,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_carriage_return(void **state)
+{
+ const char *cr_left = "\rabcdef";
+ const char *cr_mid = "abc\rdef";
+ const char *cr_right = "abcdef\r";
+
+ assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_attr));
+
+ assert_true(pcmk__xml_needs_escape(cr_left, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(cr_mid, pcmk__xml_escape_attr_pretty));
+ assert_true(pcmk__xml_needs_escape(cr_right, pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_nonprinting(void **state)
+{
+ const char *alert_left = "\aabcdef";
+ const char *alert_mid = "abc\adef";
+ const char *alert_right = "abcdef\a";
+
+ const char *delete_left = "\x7F""abcdef";
+ const char *delete_mid = "abc\x7F""def";
+ const char *delete_right = "abcdef\x7F";
+
+ const char *nonprinting_all = "\a\x7F\x1B";
+
+ assert_true(pcmk__xml_needs_escape(alert_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(alert_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(alert_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(alert_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(alert_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(alert_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(alert_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(alert_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(alert_right,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(delete_left, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(delete_mid, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(delete_right, pcmk__xml_escape_text));
+
+ assert_true(pcmk__xml_needs_escape(delete_left, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(delete_mid, pcmk__xml_escape_attr));
+ assert_true(pcmk__xml_needs_escape(delete_right, pcmk__xml_escape_attr));
+
+ assert_false(pcmk__xml_needs_escape(delete_left,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(delete_mid,
+ pcmk__xml_escape_attr_pretty));
+ assert_false(pcmk__xml_needs_escape(delete_right,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(nonprinting_all, pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(nonprinting_all, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(nonprinting_all,
+ pcmk__xml_escape_attr_pretty));
+}
+
+static void
+escape_utf8(void **state)
+{
+ /* Non-ASCII UTF-8 characters may be two, three, or four 8-bit bytes wide
+ * and should not be escaped.
+ */
+ const char *chinese = "仅高级使用";
+ const char *two_byte = "abc""\xCF\xA6""def";
+ const char *two_byte_special = "abc""\xCF\xA6""d<ef";
+ const char *three_byte = "abc""\xEF\x98\x98""def";
+ const char *three_byte_special = "abc""\xEF\x98\x98""d<ef";
+ const char *four_byte = "abc""\xF0\x94\x81\x90""def";
+ const char *four_byte_special = "abc""\xF0\x94\x81\x90""d<ef";
+
+ assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(chinese, pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape(two_byte, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(two_byte, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(two_byte,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(two_byte_special,
+ pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(two_byte_special,
+ pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(two_byte_special,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape(three_byte, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(three_byte, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(three_byte,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(three_byte_special,
+ pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(three_byte_special,
+ pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(three_byte_special,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_false(pcmk__xml_needs_escape(four_byte, pcmk__xml_escape_text));
+ assert_false(pcmk__xml_needs_escape(four_byte, pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(four_byte,
+ pcmk__xml_escape_attr_pretty));
+
+ assert_true(pcmk__xml_needs_escape(four_byte_special,
+ pcmk__xml_escape_text));
+ assert_true(pcmk__xml_needs_escape(four_byte_special,
+ pcmk__xml_escape_attr));
+ assert_false(pcmk__xml_needs_escape(four_byte_special,
+ pcmk__xml_escape_attr_pretty));
+}
+
+PCMK__UNIT_TEST(pcmk__xml_test_setup_group, NULL,
+ cmocka_unit_test(null_empty),
+ cmocka_unit_test(invalid_type),
+ cmocka_unit_test(escape_unchanged),
+ cmocka_unit_test(escape_left_angle),
+ cmocka_unit_test(escape_right_angle),
+ cmocka_unit_test(escape_ampersand),
+ cmocka_unit_test(escape_double_quote),
+ cmocka_unit_test(escape_newline),
+ cmocka_unit_test(escape_tab),
+ cmocka_unit_test(escape_carriage_return),
+ cmocka_unit_test(escape_nonprinting),
+ cmocka_unit_test(escape_utf8));
diff --git a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
index 3922b34..86500c2 100644
--- a/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
+++ b/lib/common/tests/xpath/pcmk__xpath_node_id_test.c
@@ -1,5 +1,5 @@
/*
- * Copyright 2021-2022 the Pacemaker project contributors
+ * Copyright 2021-2024 the Pacemaker project contributors
*
* The version control history for this file may have further details.
*
@@ -9,14 +9,14 @@
#include <crm_internal.h>
-#include <crm/msg_xml.h>
+#include <crm/common/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(NULL, PCMK__XE_LRM));
+ assert_null(pcmk__xpath_node_id("", PCMK__XE_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));
@@ -24,30 +24,31 @@ empty_input(void **state) {
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"));
+ const char *xpath = "/some/xpath/" PCMK__XE_LRM "[@" PCMK_XA_ID "=xyz]";
+ pcmk__assert_asserts(pcmk__xpath_node_id(xpath, PCMK__XE_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"));
+ const char *xpath = "/some/xpath/string[@" PCMK_XA_ID "='xyz']";
+ assert_null(pcmk__xpath_node_id(xpath, PCMK__XE_LRM));
- xpath = "/some/xpath/containing[@" XML_ATTR_ID "='lrm']";
- assert_null(pcmk__xpath_node_id(xpath, "lrm"));
+ xpath = "/some/xpath/containing[@" PCMK_XA_ID "='" PCMK__XE_LRM "']";
+ assert_null(pcmk__xpath_node_id(xpath, PCMK__XE_LRM));
}
static void
present(void **state) {
char *s = NULL;
- const char *xpath = "/some/xpath/containing/lrm[@" XML_ATTR_ID "='xyz']";
+ const char *xpath = "/some/xpath/containing"
+ "/" PCMK__XE_LRM "[@" PCMK_XA_ID "='xyz']";
- s = pcmk__xpath_node_id(xpath, "lrm");
+ s = pcmk__xpath_node_id(xpath, PCMK__XE_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");
+ xpath = "/some/other/" PCMK__XE_LRM "[@" PCMK_XA_ID "='xyz']/xpath";
+ s = pcmk__xpath_node_id(xpath, PCMK__XE_LRM);
assert_int_equal(strcmp(s, "xyz"), 0);
free(s);
}