diff --git a/include/crm/common/unittest_internal.h b/include/crm/common/unittest_internal.h index 51a8686822..ebe634e2ac 100644 --- a/include/crm/common/unittest_internal.h +++ b/include/crm/common/unittest_internal.h @@ -1,172 +1,190 @@ /* * 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 Perform setup for a group of unit tests that will eventually access * a CIB. * * This function is suitable for being passed as the first argument to the * \c PCMK__UNIT_TEST macro. * * \param[in] state The cmocka state object, currently unused by this * function */ int pcmk__cib_test_setup_group(void **state); +/*! + * \internal + * \brief Copy the given CIB file to a temporary file so it can be modified + * as part of doing unit tests, returning the full temporary file or + * \c NULL on error. + * + * This function should be called as part of the process of setting up any + * single unit test that would access and modify a CIB. That is, it should + * be called from whatever function is the second argument to + * cmocka_unit_test_setup_teardown. + * + * \param[in] in_file The filename of the input CIB file, which must + * exist in the \c $PCMK_CTS_CLI_DIR directory. This + * should only be the filename, not the complete + * path. + */ +char *pcmk__cib_test_copy_cib(const char *in_file); + /*! * \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/unittest.c b/lib/common/unittest.c index d590e93662..c39ac831f6 100644 --- a/lib/common/unittest.c +++ b/lib/common/unittest.c @@ -1,79 +1,114 @@ /* * 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); } int pcmk__cib_test_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; } + +char * +pcmk__cib_test_copy_cib(const char *in_file) +{ + char *in_path = crm_strdup_printf("%s/%s", getenv("PCMK_CTS_CLI_DIR"), in_file); + char *out_path = NULL; + char *contents = NULL; + int fd; + + /* Copy the CIB over to a temp location so we can modify it. */ + out_path = crm_strdup_printf("%s/test-cib.XXXXXX", pcmk__get_tmpdir()); + + fd = mkstemp(out_path); + if (fd < 0) { + free(out_path); + return NULL; + } + + if (pcmk__file_contents(in_path, &contents) != pcmk_rc_ok) { + free(out_path); + close(fd); + return NULL; + } + + if (pcmk__write_sync(fd, contents) != pcmk_rc_ok) { + free(out_path); + free(in_path); + free(contents); + close(fd); + return NULL; + } + + setenv("CIB_file", out_path, 1); + return out_path; +} 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 b56a2e869e..f8ae22fefb 100644 --- a/lib/pacemaker/tests/pcmk_resource/pcmk_resource_delete_test.c +++ b/lib/pacemaker/tests/pcmk_resource/pcmk_resource_delete_test.c @@ -1,181 +1,159 @@ /* * 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 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; + cib_path = pcmk__cib_test_copy_cib("crm_mon.xml"); - /* 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); + if (cib_path == NULL) { 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(pcmk__cib_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))