diff --git a/lib/common/tests/schemas/Makefile.am b/lib/common/tests/schemas/Makefile.am index 8854eb264c..8269683754 100644 --- a/lib/common/tests/schemas/Makefile.am +++ b/lib/common/tests/schemas/Makefile.am @@ -1,57 +1,60 @@ # # 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 $(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. -check_PROGRAMS = pcmk__build_schema_xml_node_test \ +check_PROGRAMS = crm_schema_init_test \ + get_schema_name_test \ + get_schema_version_test \ + pcmk__build_schema_xml_node_test \ pcmk__schema_files_later_than_test \ - pcmk__xml_find_x_0_schema_index_test + pcmk__find_x_0_schema_index_test TESTS = $(check_PROGRAMS) $(TESTS): setup-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. .PHONY: setup-schema-dir setup-schema-dir: $(MKDIR_P) schemas ( cd schemas ; \ ln -sf $(abs_top_builddir)/xml/pacemaker-next.rng . ; \ ln -sf $(abs_top_builddir)/xml/upgrade-*.xsl . ; \ for f in $(shell ls -1v $(abs_top_builddir)/xml/pacemaker-[0-9]*.rng | head -15); do \ ln -sf $$f $$(basename $$f); \ done ; \ for f in $(shell ls -1 $(top_srcdir)/xml/*.rng | grep -v pacemaker); 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 0000000000..899edf1faf --- /dev/null +++ b/lib/common/tests/schemas/crm_schema_init_test.c @@ -0,0 +1,138 @@ +/* + * 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 + +#include +#include + +#include +#include +#include + +/* crm_schema_init is best tested by writing tests for all the other public + * functions it calls. However, it's useful to have a test to make sure + * incorporating a second directory of schema files (like may be seen on + * Pacemaker Remote nodes) works. + */ + +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 +extra_schema_files(void **state) { + crm_schema_init(); + + pcmk__log_known_schemas(); + + /* 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_string_equal("pacemaker-1.0", get_schema_name(0)); + assert_string_equal("pacemaker-1.2", get_schema_name(1)); + assert_string_equal("pacemaker-2.0", get_schema_name(3)); + assert_string_equal("pacemaker-3.0", get_schema_name(14)); + assert_string_equal("pacemaker-3.1", get_schema_name(15)); + assert_string_equal("pacemaker-3.2", get_schema_name(16)); + + /* This will one day be removed */ + assert_string_equal("pacemaker-next", get_schema_name(17)); + + assert_string_equal("none", get_schema_name(18)); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(extra_schema_files)); diff --git a/lib/common/tests/schemas/get_schema_name_test.c b/lib/common/tests/schemas/get_schema_name_test.c new file mode 100644 index 0000000000..d5d4447dd7 --- /dev/null +++ b/lib/common/tests/schemas/get_schema_name_test.c @@ -0,0 +1,47 @@ +/* + * 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 + +#include +#include +#include + +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 +bad_input(void **state) { + assert_string_equal("unknown", get_schema_name(-1)); + assert_string_equal("unknown", get_schema_name(47000)); +} + +static void +typical_usage(void **state) { + assert_string_equal("pacemaker-1.0", get_schema_name(0)); + assert_string_equal("pacemaker-1.2", get_schema_name(1)); + assert_string_equal("pacemaker-2.0", get_schema_name(3)); + assert_string_equal("pacemaker-2.5", get_schema_name(8)); + assert_string_equal("pacemaker-3.0", get_schema_name(14)); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(bad_input), + cmocka_unit_test(typical_usage)); diff --git a/lib/common/tests/schemas/get_schema_version_test.c b/lib/common/tests/schemas/get_schema_version_test.c new file mode 100644 index 0000000000..0d503aa792 --- /dev/null +++ b/lib/common/tests/schemas/get_schema_version_test.c @@ -0,0 +1,53 @@ +/* + * 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 + +#include +#include +#include + +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 +bad_input(void **state) { + assert_int_equal(16, get_schema_version(NULL)); + assert_int_equal(-1, get_schema_version("")); + assert_int_equal(-1, get_schema_version("blahblah")); + assert_int_equal(-1, get_schema_version("pacemaker-2.47")); + assert_int_equal(-1, get_schema_version("pacemaker-47.0")); +} + +static void +typical_usage(void **state) { + assert_int_equal(0, get_schema_version("pacemaker-1.0")); + assert_int_equal(0, get_schema_version("PACEMAKER-1.0")); + assert_int_equal(1, get_schema_version("pacemaker-1.2")); + assert_int_equal(3, get_schema_version("pacemaker-2.0")); + assert_int_equal(3, get_schema_version("pAcEmAkEr-2.0")); + assert_int_equal(8, get_schema_version("pacemaker-2.5")); + assert_int_equal(14, get_schema_version("pacemaker-3.0")); + assert_int_equal(14, get_schema_version("paceMAKER-3.0")); +} + +PCMK__UNIT_TEST(setup, teardown, + cmocka_unit_test(bad_input), + cmocka_unit_test(typical_usage)); diff --git a/lib/common/tests/schemas/pcmk__xml_find_x_0_schema_index_test.c b/lib/common/tests/schemas/pcmk__find_x_0_schema_index_test.c similarity index 100% rename from lib/common/tests/schemas/pcmk__xml_find_x_0_schema_index_test.c rename to lib/common/tests/schemas/pcmk__find_x_0_schema_index_test.c diff --git a/lib/common/tests/xml/Makefile.am b/lib/common/tests/xml/Makefile.am index 465c950702..08485dab19 100644 --- a/lib/common/tests/xml/Makefile.am +++ b/lib/common/tests/xml/Makefile.am @@ -1,17 +1,18 @@ # # Copyright 2022-2023 the Pacemaker project contributors # # The version control history for this file may have further details. # # This source code is licensed under the GNU General Public License version 2 # or later (GPLv2+) WITHOUT ANY WARRANTY. # include $(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 \ +check_PROGRAMS = crm_xml_init_test \ + pcmk__xe_foreach_child_test \ pcmk__xe_match_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 0000000000..89f4bb3de7 --- /dev/null +++ b/lib/common/tests/xml/crm_xml_init_test.c @@ -0,0 +1,228 @@ +/* + * 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 + +#include +#include +#include + +#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((pcmkXmlStr) "1.0"); + + /* 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((pcmkXmlStr) "1.0"); + 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((pcmkXmlStr) "1.0"); + xmlNodePtr node = xmlNewDocNode(doc, NULL, (pcmkXmlStr) "test", NULL); + xmlAttrPtr attr = xmlNewProp(node, (pcmkXmlStr) "name", (pcmkXmlStr) "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((pcmkXmlStr) "1.0"); + 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((pcmkXmlStr) "1.0"); + 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((pcmkXmlStr) "1.0"); + xmlDtdPtr dtd = xmlNewDtd(doc, (pcmkXmlStr) "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((pcmkXmlStr) "1.0"); + 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));