diff --git a/include/crm/common/unittest_internal.h b/include/crm/common/unittest_internal.h index 13378a8962..c7d22c22d9 100644 --- a/include/crm/common/unittest_internal.h +++ b/include/crm/common/unittest_internal.h @@ -1,142 +1,159 @@ /* * 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 Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include +#include + #ifndef CRM_COMMON_UNITTEST_INTERNAL__H #define CRM_COMMON_UNITTEST_INTERNAL__H /* internal unit testing related utilities */ #if (PCMK__WITH_COVERAGE == 1) /* This function isn't exposed anywhere. The following prototype was taken from * /usr/lib/gcc/x86_64-redhat-linux/??/include/gcov.h */ extern void __gcov_dump(void); #else #define __gcov_dump() #endif +/*! + * \internal + * \brief Assert that the XML output from an API function is valid + * + * \param[in] xml The XML output of some public pacemaker API function + * + * Run the given XML through xmllint and attempt to validate it against the + * api-result.rng schema file. Assert if validation fails. + * + * \note PCMK_schema_directory needs to be set to the directory containing + * the built schema files before calling this function. Typically, + * this will be done in Makefile.am. + */ +void pcmk__assert_validates(xmlNode *xml); + /*! * \internal * \brief Assert that a statement aborts through CRM_ASSERT(). * * \param[in] stmt Statement to execute; can be an expression. * * A cmocka-like assert macro for use in unit testing. This one verifies that a * statement aborts through CRM_ASSERT(), erroring out if that is not the case. * * This macro works by running the statement in a forked child process with core * dumps disabled (CRM_ASSERT() calls \c abort(), which will write out a core * dump). The parent waits for the child to exit and checks why. If the child * received a \c SIGABRT, the test passes. For all other cases, the test fails. * * \note If cmocka's expect_*() or will_return() macros are called along with * pcmk__assert_asserts(), they must be called within a block that is * passed as the \c stmt argument. That way, the values are added only to * the child's queue. Otherwise, values added to the parent's queue will * never be popped, and the test will fail. */ #define pcmk__assert_asserts(stmt) \ do { \ pid_t p = fork(); \ if (p == 0) { \ struct rlimit cores = { 0, 0 }; \ setrlimit(RLIMIT_CORE, &cores); \ stmt; \ __gcov_dump(); \ _exit(0); \ } else if (p > 0) { \ int wstatus = 0; \ if (waitpid(p, &wstatus, 0) == -1) { \ fail_msg("waitpid failed"); \ } \ if (!(WIFSIGNALED(wstatus) && WTERMSIG(wstatus) == SIGABRT)) { \ fail_msg("statement terminated in child without asserting"); \ } \ } else { \ fail_msg("unable to fork for assert test"); \ } \ } while (0); /*! * \internal * \brief Assert that a statement aborts * * This is exactly the same as pcmk__assert_asserts (CRM_ASSERT() is implemented * with abort()), but given a different name for clarity. */ #define pcmk__assert_aborts(stmt) pcmk__assert_asserts(stmt) /*! * \internal * \brief Assert that a statement exits with the expected exit status. * * \param[in] stmt Statement to execute; can be an expression. * \param[in] rc The expected exit status. * * This functions just like \c pcmk__assert_asserts, except that it tests for * an expected exit status. Abnormal termination or incorrect exit status is * treated as a failure of the test. * * In the event that stmt does not exit at all, the special code \c CRM_EX_NONE * will be returned. It is expected that this code is not used anywhere, thus * always causing an error. */ #define pcmk__assert_exits(rc, stmt) \ do { \ pid_t p = fork(); \ if (p == 0) { \ struct rlimit cores = { 0, 0 }; \ setrlimit(RLIMIT_CORE, &cores); \ stmt; \ __gcov_dump(); \ _exit(CRM_EX_NONE); \ } else if (p > 0) { \ int wstatus = 0; \ if (waitpid(p, &wstatus, 0) == -1) { \ fail_msg("waitpid failed"); \ } \ if (!WIFEXITED(wstatus)) { \ fail_msg("statement terminated abnormally"); \ } else if (WEXITSTATUS(wstatus) != rc) { \ fail_msg("statement exited with %d, not expected %d", WEXITSTATUS(wstatus), rc); \ } \ } else { \ fail_msg("unable to fork for assert test"); \ } \ } while (0); /* Generate the main function of most unit test files. Typically, group_setup * and group_teardown will be NULL. The rest of the arguments are a list of * calls to cmocka_unit_test or cmocka_unit_test_setup_teardown to run the * individual unit tests. */ #define PCMK__UNIT_TEST(group_setup, group_teardown, ...) \ int \ main(int argc, char **argv) \ { \ const struct CMUnitTest t[] = { \ __VA_ARGS__ \ }; \ cmocka_set_message_output(CM_OUTPUT_TAP); \ return cmocka_run_group_tests(t, group_setup, group_teardown); \ } #endif /* CRM_COMMON_UNITTEST_INTERNAL__H */ diff --git a/lib/common/Makefile.am b/lib/common/Makefile.am index d68589d067..91456888d0 100644 --- a/lib/common/Makefile.am +++ b/lib/common/Makefile.am @@ -1,140 +1,141 @@ # # Copyright 2004-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/common.mk AM_CPPFLAGS += -I$(top_builddir)/lib/gnu \ -I$(top_srcdir)/lib/gnu ## libraries lib_LTLIBRARIES = libcrmcommon.la check_LTLIBRARIES = libcrmcommon_test.la # Disable -Wcast-qual if used, because we do some hacky casting, # and because libxml2 has some signatures that should be const but aren't # for backward compatibility reasons. # s390 needs -fPIC # s390-suse-linux/bin/ld: .libs/ipc.o: relocation R_390_PC32DBL against `__stack_chk_fail@@GLIBC_2.4' can not be used when making a shared object; recompile with -fPIC CFLAGS = $(CFLAGS_COPY:-Wcast-qual=) -fPIC # Without "." here, check-recursive will run through the subdirectories first # and then run "make check" here. This will fail, because there's things in # the subdirectories that need check_LTLIBRARIES built first. Adding "." here # changes the order so the subdirectories are processed afterwards. SUBDIRS = . tests noinst_HEADERS = crmcommon_private.h \ mock_private.h libcrmcommon_la_LDFLAGS = -version-info 46:0:12 libcrmcommon_la_CFLAGS = $(CFLAGS_HARDENED_LIB) libcrmcommon_la_LDFLAGS += $(LDFLAGS_HARDENED_LIB) libcrmcommon_la_LIBADD = @LIBADD_DL@ \ $(top_builddir)/lib/gnu/libgnu.la # If configured with --with-profiling or --with-coverage, BUILD_PROFILING will # be set and -fno-builtin will be added to the CFLAGS. However, libcrmcommon # uses the fabs() function which is normally supplied by gcc as one of its # builtins. Therefore we need to explicitly link against libm here or the # tests won't link. if BUILD_PROFILING libcrmcommon_la_LIBADD += -lm endif ## Library sources (*must* use += format for bumplibs) libcrmcommon_la_SOURCES = libcrmcommon_la_SOURCES += acl.c libcrmcommon_la_SOURCES += actions.c libcrmcommon_la_SOURCES += agents.c libcrmcommon_la_SOURCES += alerts.c libcrmcommon_la_SOURCES += attrs.c libcrmcommon_la_SOURCES += cib.c if BUILD_CIBSECRETS libcrmcommon_la_SOURCES += cib_secrets.c endif libcrmcommon_la_SOURCES += cmdline.c libcrmcommon_la_SOURCES += digest.c libcrmcommon_la_SOURCES += health.c libcrmcommon_la_SOURCES += io.c libcrmcommon_la_SOURCES += ipc_attrd.c libcrmcommon_la_SOURCES += ipc_client.c libcrmcommon_la_SOURCES += ipc_common.c libcrmcommon_la_SOURCES += ipc_controld.c libcrmcommon_la_SOURCES += ipc_pacemakerd.c libcrmcommon_la_SOURCES += ipc_schedulerd.c libcrmcommon_la_SOURCES += ipc_server.c libcrmcommon_la_SOURCES += iso8601.c libcrmcommon_la_SOURCES += lists.c libcrmcommon_la_SOURCES += logging.c libcrmcommon_la_SOURCES += mainloop.c libcrmcommon_la_SOURCES += messages.c libcrmcommon_la_SOURCES += nodes.c libcrmcommon_la_SOURCES += nvpair.c libcrmcommon_la_SOURCES += options.c libcrmcommon_la_SOURCES += output.c libcrmcommon_la_SOURCES += output_html.c libcrmcommon_la_SOURCES += output_log.c libcrmcommon_la_SOURCES += output_none.c libcrmcommon_la_SOURCES += output_text.c libcrmcommon_la_SOURCES += output_xml.c libcrmcommon_la_SOURCES += patchset.c libcrmcommon_la_SOURCES += patchset_display.c libcrmcommon_la_SOURCES += pid.c libcrmcommon_la_SOURCES += probes.c libcrmcommon_la_SOURCES += procfs.c libcrmcommon_la_SOURCES += remote.c libcrmcommon_la_SOURCES += resources.c libcrmcommon_la_SOURCES += results.c libcrmcommon_la_SOURCES += roles.c libcrmcommon_la_SOURCES += rules.c libcrmcommon_la_SOURCES += scheduler.c libcrmcommon_la_SOURCES += schemas.c libcrmcommon_la_SOURCES += scores.c libcrmcommon_la_SOURCES += strings.c libcrmcommon_la_SOURCES += utils.c libcrmcommon_la_SOURCES += watchdog.c libcrmcommon_la_SOURCES += xml.c libcrmcommon_la_SOURCES += xml_attr.c libcrmcommon_la_SOURCES += xml_display.c libcrmcommon_la_SOURCES += xpath.c # # libcrmcommon_test is used only with unit tests, so we can mock system calls. # See mock.c for details. # include $(top_srcdir)/mk/tap.mk libcrmcommon_test_la_SOURCES = $(libcrmcommon_la_SOURCES) libcrmcommon_test_la_SOURCES += mock.c +libcrmcommon_test_la_SOURCES += unittest.c libcrmcommon_test_la_LDFLAGS = $(libcrmcommon_la_LDFLAGS) \ -rpath $(libdir) \ $(LDFLAGS_WRAP) # If GCC emits a builtin function in place of something we've mocked up, that will # get used instead of the mocked version which leads to unexpected test results. So # disable all builtins. Older versions of GCC (at least, on RHEL7) will still emit # replacement code for strdup (and possibly other functions) unless -fno-inline is # also added. libcrmcommon_test_la_CFLAGS = $(libcrmcommon_la_CFLAGS) \ -DPCMK__UNIT_TESTING \ -fno-builtin \ -fno-inline # If -fno-builtin is used, -lm also needs to be added. See the comment at # BUILD_PROFILING above. libcrmcommon_test_la_LIBADD = $(libcrmcommon_la_LIBADD) if BUILD_COVERAGE libcrmcommon_test_la_LIBADD += -lgcov endif libcrmcommon_test_la_LIBADD += -lcmocka libcrmcommon_test_la_LIBADD += -lm nodist_libcrmcommon_test_la_SOURCES = $(nodist_libcrmcommon_la_SOURCES) diff --git a/lib/common/output_xml.c b/lib/common/output_xml.c index b740c4783c..2d329cd434 100644 --- a/lib/common/output_xml.c +++ b/lib/common/output_xml.c @@ -1,560 +1,561 @@ /* * Copyright 2019-2024 the Pacemaker project contributors * * The version control history for this file may have further details. * * This source code is licensed under the GNU Lesser General Public License * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. */ #include #include #include #include #include #include #include #include #include #include #include // pcmk__xml2fd static gboolean legacy_xml = FALSE; static gboolean simple_list = FALSE; GOptionEntry pcmk__xml_output_entries[] = { { "xml-legacy", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &legacy_xml, NULL, NULL }, { "xml-simple-list", 0, G_OPTION_FLAG_HIDDEN, G_OPTION_ARG_NONE, &simple_list, NULL, NULL }, { NULL } }; typedef struct subst_s { const char *from; const char *to; } subst_t; static const subst_t substitutions[] = { { "Active Resources", PCMK_XE_RESOURCES, }, { "Assignment Scores", PCMK_XE_ALLOCATIONS, }, { "Assignment Scores and Utilization Information", PCMK_XE_ALLOCATIONS_UTILIZATIONS, }, { "Cluster Summary", PCMK_XE_SUMMARY, }, { "Current cluster status", PCMK_XE_CLUSTER_STATUS, }, { "Executing Cluster Transition", PCMK_XE_TRANSITION, }, { "Failed Resource Actions", PCMK_XE_FAILURES, }, { "Fencing History", PCMK_XE_FENCE_HISTORY, }, { "Full List of Resources", PCMK_XE_RESOURCES, }, { "Inactive Resources", PCMK_XE_RESOURCES, }, { "Migration Summary", PCMK_XE_NODE_HISTORY, }, { "Negative Location Constraints", PCMK_XE_BANS, }, { "Node Attributes", PCMK_XE_NODE_ATTRIBUTES, }, { "Operations", PCMK_XE_NODE_HISTORY, }, { "Resource Config", PCMK_XE_RESOURCE_CONFIG, }, { "Resource Operations", PCMK_XE_OPERATIONS, }, { "Revised Cluster Status", PCMK_XE_REVISED_CLUSTER_STATUS, }, { "Timings", PCMK_XE_TIMINGS, }, { "Transition Summary", PCMK_XE_ACTIONS, }, { "Utilization Information", PCMK_XE_UTILIZATIONS, }, { NULL, NULL } }; /* The first several elements of this struct must be the same as the first * several elements of private_data_s in lib/common/output_html.c. That * struct gets passed to a bunch of the pcmk__output_xml_* functions which * assume an XML private_data_s. Keeping them laid out the same means this * still works. */ typedef struct private_data_s { /* Begin members that must match the HTML version */ xmlNode *root; GQueue *parent_q; GSList *errors; /* End members that must match the HTML version */ bool legacy_xml; } private_data_t; static void xml_free_priv(pcmk__output_t *out) { private_data_t *priv = NULL; if (out == NULL || out->priv == NULL) { return; } priv = out->priv; free_xml(priv->root); g_queue_free(priv->parent_q); g_slist_free(priv->errors); free(priv); out->priv = NULL; } static bool xml_init(pcmk__output_t *out) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL); /* If xml_init was previously called on this output struct, just return. */ if (out->priv != NULL) { return true; } else { out->priv = calloc(1, sizeof(private_data_t)); if (out->priv == NULL) { return false; } priv = out->priv; } if (legacy_xml) { priv->root = create_xml_node(NULL, PCMK_XE_CRM_MON); crm_xml_add(priv->root, PCMK_XA_VERSION, PACEMAKER_VERSION); } else { priv->root = create_xml_node(NULL, PCMK_XE_PACEMAKER_RESULT); crm_xml_add(priv->root, PCMK_XA_API_VERSION, PCMK__API_VERSION); - crm_xml_add(priv->root, PCMK_XA_REQUEST, out->request); + crm_xml_add(priv->root, PCMK_XA_REQUEST, + pcmk__s(out->request, "libpacemaker")); } priv->parent_q = g_queue_new(); priv->errors = NULL; g_queue_push_tail(priv->parent_q, priv->root); /* Copy this from the file-level variable. This means that it is only settable * as a command line option, and that pcmk__output_new must be called after all * command line processing is completed. */ priv->legacy_xml = legacy_xml; return true; } static void add_error_node(gpointer data, gpointer user_data) { char *str = (char *) data; xmlNodePtr node = (xmlNodePtr) user_data; pcmk_create_xml_text_node(node, PCMK_XE_ERROR, str); } static void xml_finish(pcmk__output_t *out, crm_exit_t exit_status, bool print, void **copy_dest) { private_data_t *priv = NULL; xmlNodePtr node; CRM_ASSERT(out != NULL); priv = out->priv; /* If root is NULL, xml_init failed and we are being called from pcmk__output_free * in the pcmk__output_new path. */ if (priv == NULL || priv->root == NULL) { return; } if (legacy_xml) { GSList *node = priv->errors; if (exit_status != CRM_EX_OK) { fprintf(stderr, "%s\n", crm_exit_str(exit_status)); } while (node != NULL) { fprintf(stderr, "%s\n", (char *) node->data); node = node->next; } } else { char *rc_as_str = pcmk__itoa(exit_status); node = create_xml_node(priv->root, PCMK_XE_STATUS); pcmk__xe_set_props(node, PCMK_XA_CODE, rc_as_str, PCMK_XA_MESSAGE, crm_exit_str(exit_status), NULL); if (g_slist_length(priv->errors) > 0) { xmlNodePtr errors_node = create_xml_node(node, PCMK_XE_ERRORS); g_slist_foreach(priv->errors, add_error_node, (gpointer) errors_node); } free(rc_as_str); } if (print) { pcmk__xml2fd(fileno(out->dest), priv->root); } if (copy_dest != NULL) { *copy_dest = copy_xml(priv->root); } } static void xml_reset(pcmk__output_t *out) { CRM_ASSERT(out != NULL); out->dest = freopen(NULL, "w", out->dest); CRM_ASSERT(out->dest != NULL); xml_free_priv(out); xml_init(out); } static void xml_subprocess_output(pcmk__output_t *out, int exit_status, const char *proc_stdout, const char *proc_stderr) { xmlNodePtr node, child_node; char *rc_as_str = NULL; CRM_ASSERT(out != NULL); rc_as_str = pcmk__itoa(exit_status); node = pcmk__output_xml_create_parent(out, PCMK_XE_COMMAND, PCMK_XA_CODE, rc_as_str, NULL); if (proc_stdout != NULL) { child_node = pcmk_create_xml_text_node(node, PCMK_XE_OUTPUT, proc_stdout); crm_xml_add(child_node, PCMK_XA_SOURCE, "stdout"); } if (proc_stderr != NULL) { child_node = pcmk_create_xml_text_node(node, PCMK_XE_OUTPUT, proc_stderr); crm_xml_add(child_node, PCMK_XA_SOURCE, "stderr"); } free(rc_as_str); } static void xml_version(pcmk__output_t *out, bool extended) { const char *author = "Andrew Beekhof and the Pacemaker project " "contributors"; CRM_ASSERT(out != NULL); pcmk__output_create_xml_node(out, PCMK_XE_VERSION, PCMK_XA_PROGRAM, "Pacemaker", PCMK_XA_VERSION, PACEMAKER_VERSION, PCMK_XA_AUTHOR, author, PCMK_XA_BUILD, BUILD_VERSION, PCMK_XA_FEATURES, CRM_FEATURES, NULL); } G_GNUC_PRINTF(2, 3) static void xml_err(pcmk__output_t *out, const char *format, ...) { private_data_t *priv = NULL; int len = 0; char *buf = NULL; va_list ap; CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len > 0); va_end(ap); priv->errors = g_slist_append(priv->errors, buf); } G_GNUC_PRINTF(2, 3) static int xml_info(pcmk__output_t *out, const char *format, ...) { return pcmk_rc_no_output; } static void xml_output_xml(pcmk__output_t *out, const char *name, const char *buf) { xmlNodePtr parent = NULL; xmlNodePtr cdata_node = NULL; CRM_ASSERT(out != NULL); parent = pcmk__output_create_xml_node(out, name, NULL); if (parent == NULL) { return; } cdata_node = xmlNewCDataBlock(parent->doc, (pcmkXmlStr) buf, strlen(buf)); xmlAddChild(parent, cdata_node); } G_GNUC_PRINTF(4, 5) static void xml_begin_list(pcmk__output_t *out, const char *singular_noun, const char *plural_noun, const char *format, ...) { va_list ap; char *name = NULL; char *buf = NULL; int len; CRM_ASSERT(out != NULL); va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len >= 0); va_end(ap); for (const subst_t *s = substitutions; s->from != NULL; s++) { if (strcmp(s->from, buf) == 0) { name = g_strdup(s->to); break; } } if (name == NULL) { name = g_ascii_strdown(buf, -1); } if (legacy_xml || simple_list) { pcmk__output_xml_create_parent(out, name, NULL); } else { pcmk__output_xml_create_parent(out, PCMK_XE_LIST, PCMK_XA_NAME, name, NULL); } g_free(name); free(buf); } G_GNUC_PRINTF(3, 4) static void xml_list_item(pcmk__output_t *out, const char *name, const char *format, ...) { xmlNodePtr item_node = NULL; va_list ap; char *buf = NULL; int len; CRM_ASSERT(out != NULL); va_start(ap, format); len = vasprintf(&buf, format, ap); CRM_ASSERT(len >= 0); va_end(ap); item_node = pcmk__output_create_xml_text_node(out, PCMK_XE_ITEM, buf); if (name != NULL) { crm_xml_add(item_node, PCMK_XA_NAME, name); } free(buf); } static void xml_increment_list(pcmk__output_t *out) { /* This function intentially left blank */ } static void xml_end_list(pcmk__output_t *out) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); priv = out->priv; if (priv->legacy_xml || simple_list) { g_queue_pop_tail(priv->parent_q); } else { char *buf = NULL; xmlNodePtr node; node = g_queue_pop_tail(priv->parent_q); buf = crm_strdup_printf("%lu", xmlChildElementCount(node)); crm_xml_add(node, PCMK_XA_COUNT, buf); free(buf); } } static bool xml_is_quiet(pcmk__output_t *out) { return false; } static void xml_spacer(pcmk__output_t *out) { /* This function intentionally left blank */ } static void xml_progress(pcmk__output_t *out, bool end) { /* This function intentionally left blank */ } pcmk__output_t * pcmk__mk_xml_output(char **argv) { pcmk__output_t *retval = calloc(1, sizeof(pcmk__output_t)); if (retval == NULL) { return NULL; } retval->fmt_name = "xml"; retval->request = pcmk__quote_cmdline(argv); retval->init = xml_init; retval->free_priv = xml_free_priv; retval->finish = xml_finish; retval->reset = xml_reset; retval->register_message = pcmk__register_message; retval->message = pcmk__call_message; retval->subprocess_output = xml_subprocess_output; retval->version = xml_version; retval->info = xml_info; retval->transient = xml_info; retval->err = xml_err; retval->output_xml = xml_output_xml; retval->begin_list = xml_begin_list; retval->list_item = xml_list_item; retval->increment_list = xml_increment_list; retval->end_list = xml_end_list; retval->is_quiet = xml_is_quiet; retval->spacer = xml_spacer; retval->progress = xml_progress; retval->prompt = pcmk__text_prompt; return retval; } xmlNodePtr pcmk__output_xml_create_parent(pcmk__output_t *out, const char *name, ...) { va_list args; xmlNodePtr node = NULL; CRM_ASSERT(out != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); node = pcmk__output_create_xml_node(out, name, NULL); va_start(args, name); pcmk__xe_set_propv(node, args); va_end(args); pcmk__output_xml_push_parent(out, node); return node; } void pcmk__output_xml_add_node_copy(pcmk__output_t *out, xmlNodePtr node) { private_data_t *priv = NULL; xmlNodePtr parent = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); CRM_ASSERT(node != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); priv = out->priv; parent = g_queue_peek_tail(priv->parent_q); // Shouldn't happen unless the caller popped priv->root CRM_CHECK(parent != NULL, return); add_node_copy(parent, node); } xmlNodePtr pcmk__output_create_xml_node(pcmk__output_t *out, const char *name, ...) { xmlNodePtr node = NULL; private_data_t *priv = NULL; va_list args; CRM_ASSERT(out != NULL && out->priv != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); priv = out->priv; node = create_xml_node(g_queue_peek_tail(priv->parent_q), name); va_start(args, name); pcmk__xe_set_propv(node, args); va_end(args); return node; } xmlNodePtr pcmk__output_create_xml_text_node(pcmk__output_t *out, const char *name, const char *content) { xmlNodePtr node = NULL; CRM_ASSERT(out != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); node = pcmk__output_create_xml_node(out, name, NULL); pcmk__xe_set_content(node, content); return node; } void pcmk__output_xml_push_parent(pcmk__output_t *out, xmlNodePtr parent) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); CRM_ASSERT(parent != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); priv = out->priv; g_queue_push_tail(priv->parent_q, parent); } void pcmk__output_xml_pop_parent(pcmk__output_t *out) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return); priv = out->priv; CRM_ASSERT(g_queue_get_length(priv->parent_q) > 0); g_queue_pop_tail(priv->parent_q); } xmlNodePtr pcmk__output_xml_peek_parent(pcmk__output_t *out) { private_data_t *priv = NULL; CRM_ASSERT(out != NULL && out->priv != NULL); CRM_CHECK(pcmk__str_any_of(out->fmt_name, "xml", "html", NULL), return NULL); priv = out->priv; /* If queue is empty NULL will be returned */ return g_queue_peek_tail(priv->parent_q); } diff --git a/lib/common/unittest.c b/lib/common/unittest.c new file mode 100644 index 0000000000..423932cef9 --- /dev/null +++ b/lib/common/unittest.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 Lesser General Public License + * version 2.1 or later (LGPLv2.1+) WITHOUT ANY WARRANTY. + */ + +#include + +#include + +#include +#include + +void +pcmk__assert_validates(xmlNode *xml) +{ + const char *schema_dir = NULL; + char *cmd = NULL; + gchar *out = NULL; + gchar *err = NULL; + gint status; + GError *gerr = NULL; + char *xmllint_input = crm_strdup_printf("%s/test-xmllint.XXXXXX", + pcmk__get_tmpdir()); + int fd; + int rc; + + fd = mkstemp(xmllint_input); + if (fd < 0) { + fail_msg("Could not create temp file: %s", strerror(errno)); + } + + rc = pcmk__xml2fd(fd, xml); + if (rc != pcmk_rc_ok) { + unlink(xmllint_input); + fail_msg("Could not write temp file: %s", pcmk_rc_str(rc)); + } + + close(fd); + + /* This should be set as part of AM_TESTS_ENVIRONMENT in Makefile.am. */ + schema_dir = getenv("PCMK_schema_directory"); + if (schema_dir == NULL) { + unlink(xmllint_input); + fail_msg("PCMK_schema_directory is not set in test environment"); + } + + cmd = crm_strdup_printf("xmllint --relaxng %s/api/api-result.rng %s", + schema_dir, xmllint_input); + + if (!g_spawn_command_line_sync(cmd, &out, &err, &status, &gerr)) { + unlink(xmllint_input); + fail_msg("Error occurred when performing validation: %s", gerr->message); + } + + if (WIFEXITED(status) && WEXITSTATUS(status) != 0) { + unlink(xmllint_input); + fail_msg("XML validation failed: %s\n%s\n", out, err); + } + + free(cmd); + g_free(out); + g_free(err); + unlink(xmllint_input); + free(xmllint_input); +} diff --git a/lib/pacemaker/tests/pcmk_resource/pcmk_resource_delete_test.c b/lib/pacemaker/tests/pcmk_resource/pcmk_resource_delete_test.c index bdb6053c16..9cc1c2eb4c 100644 --- a/lib/pacemaker/tests/pcmk_resource/pcmk_resource_delete_test.c +++ b/lib/pacemaker/tests/pcmk_resource/pcmk_resource_delete_test.c @@ -1,185 +1,191 @@ /* * 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 #include #include #include #include static char *cib_path = NULL; static int setup_group(void **state) { /* This needs to be run before we attempt to read in a CIB or it will fail * to validate. There's no harm in doing this before all tests. */ crm_xml_init(); return 0; } static void cib_not_connected(void **state) { xmlNode *xml = NULL; /* Without any special setup, cib_new() in pcmk_resource_delete will use the * native CIB which means IPC calls. But there's nothing listening for those * calls, so signon() will return ENOTCONN. Check that we handle that. */ assert_int_equal(pcmk_resource_delete(&xml, "rsc", "primitive"), ENOTCONN); + pcmk__assert_validates(xml); free_xml(xml); } static int setup_test(void **state) { char *in_path = crm_strdup_printf("%s/crm_mon.xml", getenv("PCMK_CTS_CLI_DIR")); char *contents = NULL; int fd; /* Copy the CIB over to a temp location so we can modify it. */ cib_path = crm_strdup_printf("%s/test-cib.XXXXXX", pcmk__get_tmpdir()); fd = mkstemp(cib_path); if (fd < 0) { free(cib_path); return -1; } if (pcmk__file_contents(in_path, &contents) != pcmk_rc_ok) { free(cib_path); close(fd); return -1; } if (pcmk__write_sync(fd, contents) != pcmk_rc_ok) { free(cib_path); free(in_path); free(contents); close(fd); return -1; } setenv("CIB_file", cib_path, 1); return 0; } static int teardown_test(void **state) { unlink(cib_path); free(cib_path); cib_path = NULL; unsetenv("CIB_file"); return 0; } static void bad_input(void **state) { xmlNode *xml = NULL; /* There is a primitive resource named "Fencing", so we're just checking * that it returns EINVAL if both parameters aren't given. */ assert_int_equal(pcmk_resource_delete(&xml, "Fencing", NULL), EINVAL); + pcmk__assert_validates(xml); free_xml(xml); xml = NULL; assert_int_equal(pcmk_resource_delete(&xml, NULL, "primitive"), EINVAL); + pcmk__assert_validates(xml); free_xml(xml); } static xmlNode * find_rsc(const char *rsc) { GString *xpath = g_string_sized_new(1024); xmlNode *xml_search = NULL; cib_t *cib = NULL; cib = cib_new(); cib->cmds->signon(cib, crm_system_name, cib_command); pcmk__g_strcat(xpath, pcmk_cib_xpath_for(PCMK_XE_RESOURCES), "//*[@" PCMK_XA_ID "=\"", rsc, "\"]", NULL); cib->cmds->query(cib, (const char *) xpath->str, &xml_search, cib_xpath|cib_scope_local); g_string_free(xpath, TRUE); cib__clean_up_connection(&cib); return xml_search; } static void incorrect_type(void **state) { xmlNode *xml = NULL; xmlNode *result = NULL; /* cib_process_delete returns pcmk_ok even if given the wrong type so * we have to do an xpath query of the CIB to make sure it's still * there. */ assert_int_equal(pcmk_resource_delete(&xml, "Fencing", "clone"), pcmk_rc_ok); + pcmk__assert_validates(xml); free_xml(xml); result = find_rsc("Fencing"); assert_non_null(result); free_xml(result); } static void correct_type(void **state) { xmlNode *xml = NULL; xmlNode *result = NULL; assert_int_equal(pcmk_resource_delete(&xml, "Fencing", "primitive"), pcmk_rc_ok); + pcmk__assert_validates(xml); free_xml(xml); result = find_rsc("Fencing"); assert_null(result); free_xml(result); } static void unknown_resource(void **state) { xmlNode *xml = NULL; /* cib_process_delete returns pcmk_ok even if asked to delete something * that doesn't exist. */ assert_int_equal(pcmk_resource_delete(&xml, "no_such_resource", "primitive"), pcmk_rc_ok); + pcmk__assert_validates(xml); free_xml(xml); } /* There are two kinds of tests in this file: * * (1) Those that test what happens if the CIB is not set up correctly, and * (2) Those that test what happens when run against a CIB. * * Therefore, we need two kinds of setup/teardown functions. We only do * minimal overall setup for the entire group, and then setup the CIB for * those tests that need it. */ PCMK__UNIT_TEST(setup_group, NULL, cmocka_unit_test(cib_not_connected), cmocka_unit_test_setup_teardown(bad_input, setup_test, teardown_test), cmocka_unit_test_setup_teardown(incorrect_type, setup_test, teardown_test), cmocka_unit_test_setup_teardown(correct_type, setup_test, teardown_test), cmocka_unit_test_setup_teardown(unknown_resource, setup_test, teardown_test))