diff options
Diffstat (limited to 'test/common/odp_cunit_common.c')
-rw-r--r-- | test/common/odp_cunit_common.c | 760 |
1 files changed, 760 insertions, 0 deletions
diff --git a/test/common/odp_cunit_common.c b/test/common/odp_cunit_common.c new file mode 100644 index 000000000..e4e678a54 --- /dev/null +++ b/test/common/odp_cunit_common.c @@ -0,0 +1,760 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright (c) 2014-2018 Linaro Limited + * Copyright (c) 2019-2024 Nokia + * Copyright (c) 2021 Marvell + */ + +#ifndef _GNU_SOURCE +#define _GNU_SOURCE +#endif + +#include <stdlib.h> +#include <string.h> +#include <unistd.h> +#include <sys/mman.h> +#include <odp_api.h> +#include "odp_cunit_common.h" +#include <odp/helper/odph_api.h> + +#include <CUnit/TestDB.h> + +#if defined __GNUC__ && (((__GNUC__ == 4) && \ + (__GNUC_MINOR__ >= 4)) || (__GNUC__ > 4)) +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wstrict-prototypes" +#endif +#include <CUnit/Automated.h> +#if defined __GNUC__ && (((__GNUC__ == 4) && \ + (__GNUC_MINOR__ >= 4)) || (__GNUC__ > 4)) +#pragma GCC diagnostic pop +#endif + +/* Globals */ +static int running_in_ci; +static odph_thread_t thread_tbl[ODP_THREAD_COUNT_MAX]; +static int threads_running; +static odp_instance_t instance; +static bool control_thread; +static char *progname; +static int (*thread_func)(void *); + +/* + * global init/term functions which may be registered + * defaults to functions performing odp init/term. + */ +static int tests_global_init(odp_instance_t *inst); +static int tests_global_term(odp_instance_t inst); +static struct { + int (*global_init_ptr)(odp_instance_t *inst); + int (*global_term_ptr)(odp_instance_t inst); +} global_init_term = {tests_global_init, tests_global_term}; + +static odp_suiteinfo_t *global_testsuites; + +#define MAX_STR_LEN 256 +#define MAX_FAILURES 10 + +/* Recorded assertion failure for later CUnit call in the initial thread */ +typedef struct assertion_failure_t { + char cond[MAX_STR_LEN]; + char file[MAX_STR_LEN]; + unsigned int line; + int fatal; +} assertion_failure_t; + +typedef struct thr_global_t { + assertion_failure_t failure[MAX_FAILURES]; + unsigned long num_failures; +} thr_global_t; + +static thr_global_t *thr_global; + +static __thread int initial_thread = 1; /* Are we the initial thread? */ +static __thread jmp_buf longjmp_env; + +void odp_cu_assert(CU_BOOL value, unsigned int line, + const char *condition, const char *file, CU_BOOL fatal) +{ + unsigned long idx; + + if (initial_thread) { + CU_assertImplementation(value, line, condition, file, "", fatal); + return; + } + + /* Assertion ok, just return */ + if (value) + return; + + /* + * Non-initial thread/process cannot call CUnit assert because: + * + * - CU_assertImplementation() is not thread-safe + * - In process mode an assertion failure would be lost because it + * would not be recorded in the memory of the initial process. + * - Fatal asserts in CUnit perform longjmp which cannot be done in + * an other thread or process that did the setjmp. + * + * --> Record the assertion failure in shared memory so that it can be + * processed later in the context of the initial thread/process. + * --> In fatal assert, longjmp within the current thread. + */ + + idx = __atomic_fetch_add(&thr_global->num_failures, 1, __ATOMIC_RELAXED); + + if (idx < MAX_FAILURES) { + assertion_failure_t *a = &thr_global->failure[idx]; + + odph_strcpy(a->cond, condition, sizeof(a->cond)); + odph_strcpy(a->file, file, sizeof(a->file)); + a->line = line; + a->fatal = fatal; + } + + if (fatal) + longjmp(longjmp_env, 1); +} + +static void handle_postponed_asserts(void) +{ + unsigned long num = thr_global->num_failures; + + if (num > MAX_FAILURES) + num = MAX_FAILURES; + + for (unsigned long n = 0; n < num; n++) { + assertion_failure_t *a = &thr_global->failure[n]; + + /* + * Turn fatal failures into non-fatal failures as we are just + * reporting them. Threads that saw fatal failures which + * prevented them from continuing have already been stopped. + */ + CU_assertImplementation(0, a->line, a->cond, a->file, "", CU_FALSE); + } + thr_global->num_failures = 0; +} + +static int threads_init(void) +{ + static int initialized; + + if (initialized) + return 0; + + /* + * Use shared memory mapping for the global structure to make it + * visible in the child processes if running in process mode. + */ + thr_global = mmap(NULL, sizeof(thr_global_t), + PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, + -1, 0); + if (thr_global == MAP_FAILED) + return -1; + + initialized = 1; + return 0; +} + +static int run_thread(void *arg) +{ + int rc; + + /* Make sure this is zero also in process mode "threads" */ + initial_thread = 0; + + if (setjmp(longjmp_env) == 0) { + /* Normal return, proceed to the thread function. */ + rc = (*thread_func)(arg); + } else { + /* + * Return from longjmp done by the thread function. + * We return 0 here since odph_thread_join() does not like + * nonzero exit statuses. + */ + rc = 0; + } + + return rc; +} + +int odp_cunit_thread_create(int num, int func_ptr(void *), void *const arg[], int priv, int sync) +{ + int i, ret; + odp_cpumask_t cpumask; + odph_thread_common_param_t thr_common; + odph_thread_param_t thr_param[num]; + + if (num > ODP_THREAD_COUNT_MAX) { + fprintf(stderr, "error: %s: too many threads: num=%d max=%d\n", __func__, + num, ODP_THREAD_COUNT_MAX); + return -1; + } + + if (threads_running) { + /* thread_tbl is already in use */ + fprintf(stderr, "error: %s: threads already running\n", __func__); + return -1; + } + + thread_func = func_ptr; + + odph_thread_common_param_init(&thr_common); + + if (arg == NULL) + priv = 0; + + for (i = 0; i < num; i++) { + odph_thread_param_init(&thr_param[i]); + + thr_param[i].start = run_thread; + thr_param[i].thr_type = ODP_THREAD_WORKER; + + if (arg) + thr_param[i].arg = arg[i]; + else + thr_param[i].arg = NULL; + + if (priv == 0) + break; + } + + odp_cpumask_default_worker(&cpumask, num); + + thr_common.instance = instance; + thr_common.cpumask = &cpumask; + thr_common.share_param = !priv; + thr_common.sync = sync; + + /* Create and start additional threads */ + ret = odph_thread_create(thread_tbl, &thr_common, thr_param, num); + + if (ret != num) + fprintf(stderr, "error: odph_thread_create() failed.\n"); + + threads_running = (ret > 0); + + return ret; +} + +int odp_cunit_thread_join(int num) +{ + odph_thread_join_result_t res[num]; + + /* Wait for threads to exit */ + if (odph_thread_join_result(thread_tbl, res, num) != num) { + fprintf(stderr, "error: odph_thread_join_result() failed.\n"); + return -1; + } + + threads_running = 0; + thread_func = 0; + + for (int i = 0; i < num; i++) { + if (res[i].is_sig || res[i].ret != 0) { + fprintf(stderr, "error: worker thread failure%s: %d.\n", res[i].is_sig ? + " (signaled)" : "", res[i].ret); + return -1; + } + } + + handle_postponed_asserts(); + + return 0; +} + +static int tests_global_init(odp_instance_t *inst) +{ + odp_init_t init_param; + odph_helper_options_t helper_options; + odp_thread_type_t thr_type; + + if (odph_options(&helper_options)) { + fprintf(stderr, "error: odph_options() failed.\n"); + return -1; + } + + odp_init_param_init(&init_param); + init_param.mem_model = helper_options.mem_model; + + if (0 != odp_init_global(inst, &init_param, NULL)) { + fprintf(stderr, "error: odp_init_global() failed.\n"); + return -1; + } + + thr_type = control_thread ? ODP_THREAD_CONTROL : ODP_THREAD_WORKER; + if (0 != odp_init_local(*inst, thr_type)) { + fprintf(stderr, "error: odp_init_local() failed.\n"); + return -1; + } + if (0 != odp_schedule_config(NULL)) { + fprintf(stderr, "error: odp_schedule_config(NULL) failed.\n"); + return -1; + } + + return 0; +} + +static int tests_global_term(odp_instance_t inst) +{ + if (0 != odp_term_local()) { + fprintf(stderr, "error: odp_term_local() failed.\n"); + return -1; + } + + if (0 != odp_term_global(inst)) { + fprintf(stderr, "error: odp_term_global() failed.\n"); + return -1; + } + + return 0; +} + +/* + * register tests_global_init and tests_global_term functions. + * If some of these functions are not registered, the defaults functions + * (tests_global_init() and tests_global_term()) defined above are used. + * One should use these register functions when defining these hooks. + * Note that passing NULL as function pointer is valid and will simply + * prevent the default (odp init/term) to be done. + */ +void odp_cunit_register_global_init(int (*func_init_ptr)(odp_instance_t *inst)) +{ + global_init_term.global_init_ptr = func_init_ptr; +} + +void odp_cunit_register_global_term(int (*func_term_ptr)(odp_instance_t inst)) +{ + global_init_term.global_term_ptr = func_term_ptr; +} + +static odp_suiteinfo_t *cunit_get_suite_info(const char *suite_name) +{ + odp_suiteinfo_t *sinfo; + + for (sinfo = global_testsuites; sinfo->name; sinfo++) + if (strcmp(sinfo->name, suite_name) == 0) + return sinfo; + + return NULL; +} + +static odp_testinfo_t *cunit_get_test_info(odp_suiteinfo_t *sinfo, + const char *test_name) +{ + odp_testinfo_t *tinfo; + + for (tinfo = sinfo->testinfo_tbl; tinfo->name; tinfo++) + if (strcmp(tinfo->name, test_name) == 0) + return tinfo; + + return NULL; +} + +/* A wrapper for the suite's init function. This is done to allow for a + * potential runtime check to determine whether each test in the suite + * is active (enabled by using ODP_TEST_INFO_CONDITIONAL()). If present, + * the conditional check is run after the suite's init function. + */ +static int _cunit_suite_init(void) +{ + int ret = 0; + CU_pSuite cur_suite = CU_get_current_suite(); + odp_suiteinfo_t *sinfo; + odp_testinfo_t *tinfo; + + /* find the suite currently being run */ + cur_suite = CU_get_current_suite(); + if (!cur_suite) + return -1; + + sinfo = cunit_get_suite_info(cur_suite->pName); + if (!sinfo) + return -1; + + /* execute its init function */ + if (sinfo->init_func) { + ret = sinfo->init_func(); + if (ret) + return ret; + } + + /* run any configured conditional checks and mark inactive tests */ + for (tinfo = sinfo->testinfo_tbl; tinfo->name; tinfo++) { + CU_pTest ptest; + CU_ErrorCode err; + + if (!tinfo->check_active || tinfo->check_active()) + continue; + + /* test is inactive, mark it as such */ + ptest = CU_get_test_by_name(tinfo->name, cur_suite); + if (ptest) + err = CU_set_test_active(ptest, CU_FALSE); + else + err = CUE_NOTEST; + + if (err != CUE_SUCCESS) { + fprintf(stderr, "%s: failed to set test %s inactive\n", + __func__, tinfo->name); + return -1; + } + } + + return ret; +} + +/* Print names of all inactive tests of the suite. This should be called by + * every suite terminate function. Otherwise, inactive tests are not listed in + * test suite results. */ +int odp_cunit_print_inactive(void) +{ + CU_pSuite cur_suite; + CU_pTest ptest; + odp_suiteinfo_t *sinfo; + odp_testinfo_t *tinfo; + int first = 1; + + cur_suite = CU_get_current_suite(); + if (cur_suite == NULL) + return -1; + + sinfo = cunit_get_suite_info(cur_suite->pName); + if (sinfo == NULL) + return -1; + + for (tinfo = sinfo->testinfo_tbl; tinfo->name; tinfo++) { + ptest = CU_get_test_by_name(tinfo->name, cur_suite); + if (ptest == NULL) { + fprintf(stderr, "%s: test not found: %s\n", + __func__, tinfo->name); + return -1; + } + + if (ptest->fActive) + continue; + + if (first) { + printf("\n\nSuite: %s\n", sinfo->name); + printf(" Inactive tests:\n"); + first = 0; + } + + printf(" %s\n", tinfo->name); + } + + return 0; +} + +int odp_cunit_set_inactive(void) +{ + CU_pSuite cur_suite; + CU_pTest ptest; + odp_suiteinfo_t *sinfo; + odp_testinfo_t *tinfo; + + cur_suite = CU_get_current_suite(); + if (cur_suite == NULL) + return -1; + + sinfo = cunit_get_suite_info(cur_suite->pName); + if (sinfo == NULL) + return -1; + + for (tinfo = sinfo->testinfo_tbl; tinfo->name; tinfo++) { + ptest = CU_get_test_by_name(tinfo->name, cur_suite); + if (ptest == NULL) { + fprintf(stderr, "%s: test not found: %s\n", + __func__, tinfo->name); + return -1; + } + CU_set_test_active(ptest, false); + } + + return 0; +} + +static int default_term_func(void) +{ + return odp_cunit_print_inactive(); +} + +static void _cunit_test_setup_func(void) +{ + CU_AllTestsCompleteMessageHandler all_test_comp_handler; + CU_SuiteCompleteMessageHandler suite_comp_handler; + CU_pFailureRecord failrec; + CU_pSuite suite; + + if (!getenv("ODP_CUNIT_FAIL_IMMEDIATE")) + return; + + if (CU_get_number_of_failure_records() == 0) + return; + + /* User wants the suite to fail immediately once a test hits an error */ + suite = CU_get_current_suite(); + failrec = CU_get_failure_list(); + + printf("Force aborting as a previous test failed\n"); + + /* Call the Cleanup functions before aborting */ + suite->pCleanupFunc(); + + suite_comp_handler = CU_get_suite_complete_handler(); + if (suite_comp_handler) + suite_comp_handler(suite, failrec); + + all_test_comp_handler = CU_get_all_test_complete_handler(); + if (all_test_comp_handler) + all_test_comp_handler(failrec); + + exit(EXIT_FAILURE); +} + +/* + * Register suites and tests with CUnit. + * + * Similar to CU_register_suites() but using locally defined wrapper + * types. + */ +static int cunit_register_suites(odp_suiteinfo_t testsuites[]) +{ + odp_suiteinfo_t *sinfo; + odp_testinfo_t *tinfo; + CU_pSuite suite; + CU_pTest test; + CU_CleanupFunc term_func; + + for (sinfo = testsuites; sinfo->name; sinfo++) { + term_func = default_term_func; + if (sinfo->term_func) + term_func = sinfo->term_func; + + suite = CU_add_suite_with_setup_and_teardown(sinfo->name, _cunit_suite_init, + term_func, _cunit_test_setup_func, + NULL); + if (!suite) + return CU_get_error(); + + for (tinfo = sinfo->testinfo_tbl; tinfo->name; tinfo++) { + test = CU_add_test(suite, tinfo->name, + tinfo->test_func); + if (!test) + return CU_get_error(); + } + } + + return 0; +} + +static int cunit_update_test(CU_pSuite suite, + odp_suiteinfo_t *sinfo, + odp_testinfo_t *updated_tinfo) +{ + CU_pTest test = NULL; + CU_ErrorCode err; + odp_testinfo_t *tinfo; + const char *test_name = updated_tinfo->name; + + tinfo = cunit_get_test_info(sinfo, test_name); + if (tinfo) + test = CU_get_test(suite, test_name); + + if (!tinfo || !test) { + fprintf(stderr, "%s: unable to find existing test named %s\n", + __func__, test_name); + return -1; + } + + err = CU_set_test_func(test, updated_tinfo->test_func); + if (err != CUE_SUCCESS) { + fprintf(stderr, "%s: failed to update test func for %s\n", + __func__, test_name); + return -1; + } + + tinfo->check_active = updated_tinfo->check_active; + + return 0; +} + +static int cunit_update_suite(odp_suiteinfo_t *updated_sinfo) +{ + CU_pSuite suite = NULL; + CU_ErrorCode err; + odp_suiteinfo_t *sinfo; + odp_testinfo_t *tinfo; + + /* find previously registered suite with matching name */ + sinfo = cunit_get_suite_info(updated_sinfo->name); + + if (sinfo) { + /* lookup the associated CUnit suite */ + suite = CU_get_suite_by_name(updated_sinfo->name, + CU_get_registry()); + } + + if (!sinfo || !suite) { + fprintf(stderr, "%s: unable to find existing suite named %s\n", + __func__, updated_sinfo->name); + return -1; + } + + sinfo->init_func = updated_sinfo->init_func; + sinfo->term_func = updated_sinfo->term_func; + + err = CU_set_suite_cleanupfunc(suite, updated_sinfo->term_func); + if (err != CUE_SUCCESS) { + fprintf(stderr, "%s: failed to update cleanup func for %s\n", + __func__, updated_sinfo->name); + return -1; + } + + for (tinfo = updated_sinfo->testinfo_tbl; tinfo->name; tinfo++) { + int ret; + + ret = cunit_update_test(suite, sinfo, tinfo); + if (ret != 0) + return ret; + } + + return 0; +} + +/* + * Run tests previously registered via odp_cunit_register() + */ +int odp_cunit_run(void) +{ + int ret; + + printf("\tODP API version: %s\n", odp_version_api_str()); + printf("\tODP implementation name: %s\n", odp_version_impl_name()); + printf("\tODP implementation version: %s\n", odp_version_impl_str()); + + if (getenv("ODP_TEST_OUT_XML")) { + CU_set_output_filename(progname); + CU_automated_run_tests(); + } else { + CU_basic_set_mode(CU_BRM_VERBOSE); + CU_basic_run_tests(); + } + + ret = CU_get_number_of_failure_records(); + + CU_cleanup_registry(); + + /* call test executable termination hook, if any */ + if (global_init_term.global_term_ptr && + ((*global_init_term.global_term_ptr)(instance) != 0)) + return -1; + + return (ret) ? -1 : 0; +} + +/* + * Update suites/tests previously registered via odp_cunit_register(). + * + * Note that this is intended for modifying the properties of already + * registered suites/tests. New suites/tests can only be registered via + * odp_cunit_register(). + */ +int odp_cunit_update(odp_suiteinfo_t testsuites[]) +{ + int ret = 0; + odp_suiteinfo_t *sinfo; + + for (sinfo = testsuites; sinfo->name && ret == 0; sinfo++) + ret = cunit_update_suite(sinfo); + + return ret; +} + +/* + * Register test suites to be run via odp_cunit_run() + */ +int odp_cunit_register(odp_suiteinfo_t testsuites[]) +{ + if (threads_init()) + return -1; + + /* call test executable init hook, if any */ + if (global_init_term.global_init_ptr) { + if ((*global_init_term.global_init_ptr)(&instance) == 0) { + /* After ODP initialization, set main thread's + * CPU affinity to the 1st available control CPU core + */ + int cpu = 0; + odp_cpumask_t cpuset; + + odp_cpumask_zero(&cpuset); + if (odp_cpumask_default_control(&cpuset, 1) == 1) { + cpu = odp_cpumask_first(&cpuset); + odph_odpthread_setaffinity(cpu); + } + } else { + /* ODP initialization failed */ + return -1; + } + } + + CU_set_error_action(CUEA_ABORT); + + CU_initialize_registry(); + global_testsuites = testsuites; + cunit_register_suites(testsuites); + CU_set_fail_on_inactive(CU_FALSE); + + return 0; +} + +/* + * Parse command line options to extract options affecting cunit_common. + * (hence also helpers options as cunit_common uses the helpers) + * Options private to the test calling cunit_common are not parsed here. + */ +int odp_cunit_parse_options(int *argc, char *argv[]) +{ + const char *ctrl_thread_env = getenv("CI_THREAD_TYPE_CONTROL"); + const char *env = getenv("CI"); + + progname = argv[0]; + *argc = odph_parse_options(*argc, argv); + /* Check if we need to use control thread */ + if (ctrl_thread_env && !strcmp(ctrl_thread_env, "true")) + control_thread = true; + + if (env && !strcmp(env, "true")) { + running_in_ci = 1; + ODPH_DBG("\nWARNING: test result can be used for code coverage only.\n" + "CI=true env variable is set!\n"); + } + + return 0; +} + +int odp_cunit_ret(int val) +{ + return running_in_ci ? 0 : val; +} + +int odp_cunit_ci(void) +{ + return running_in_ci; +} + +int odp_cunit_ci_skip(const char *test_name) +{ + const char *ci_skip; + const char *found; + + ci_skip = getenv("CI_SKIP"); + if (ci_skip == NULL) + return 0; + + found = strstr(ci_skip, test_name); + + return found != NULL; +} |