/* * Copyright (C) 2008-2024 Teluu Inc. (http://www.teluu.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #ifndef __PJ_UNITTEST_H__ #define __PJ_UNITTEST_H__ /** * @file testing.h * @brief PJLIB unit testing framework */ /** * @defgroup PJ_UNITTEST Unit testing framework * @ingroup PJ_MISC * @{ */ #include #include #include #include PJ_BEGIN_DECL /* * These various PJ_TEST_XXX macros can be used in any programs without * having to use the unit-test framework. */ /** * Check that an expression is non-zero. If the check fails, informative error * message will be displayed, and the code in err_action will be executed. * * @param expr The expression to check * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_NON_ZERO(expr, err_reason, err_action) \ { \ if ((expr)==0) { \ const char *tmp_reason_ = err_reason; \ const char *sep0_ = (tmp_reason_ ? " (": ""); \ const char *sep1_ = (tmp_reason_ ? ")": ""); \ if (!tmp_reason_) tmp_reason_=""; \ PJ_LOG(1,(THIS_FILE, "Test \"%s\" != 0 fails in " \ "%s:%d%s%s%s", \ #expr, THIS_FILE,__LINE__,sep0_, \ tmp_reason_,sep1_));\ err_action; \ } \ } /** * Generic check for binary operation. If the check fails, informative error * message will be displayed, and the code in err_action will be executed. * * @param expr0 First expression * @param op The operator * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_BINARY_OP(expr0, op, expr1, err_reason, err_action) \ { \ long tmp_value0_ = (long)(expr0); \ long tmp_value1_ = (long)(expr1); \ if (!(tmp_value0_ op tmp_value1_)) { \ const char *tmp_reason_ = err_reason; \ const char *sep0_ = (tmp_reason_ ? " (": ""); \ const char *sep1_ = (tmp_reason_ ? ")": ""); \ if (!tmp_reason_) tmp_reason_=""; \ PJ_LOG(1,(THIS_FILE, "Test \"%s\" (value=%ld) " #op \ " \"%s\" (value=%ld) fails in %s:%d%s%s%s", \ #expr0, tmp_value0_, #expr1, tmp_value1_, \ THIS_FILE, __LINE__, \ sep0_, tmp_reason_, sep1_)); \ err_action; \ } \ } /** * Generic check for (PJ) string comparison operation. If the check fails, * informative error message will be displayed, and the code in err_action * will be executed. * * @param str_op The string operation (e.g. pj_strcmp) * @param ps0 Pointer to first string * @param ps1 Pointer to second string * @param res_op Operator to compare result (e.g. ==) * @param res Expected return value of str_op(&s0, &s1) * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_STR_OP(str_op, ps0, ps1, res_op, res, err_reason, err_action) \ { \ int result__ = str_op(ps0, ps1); \ if (!(result__ res_op res)) { \ const char *fn_name = #str_op; \ const char *tmp_reason_ = err_reason; \ const char *sep0_ = (tmp_reason_ ? " (": ""); \ const char *sep1_ = (tmp_reason_ ? ")": ""); \ if (!tmp_reason_) tmp_reason_=""; \ PJ_LOG(1,(THIS_FILE, "Test %s(\"%.*s\", \"%.*s\")%s%d" \ " fails (%s result=%d) in %s:%d%s%s%s", \ fn_name, (int)ps0->slen, ps0->ptr, \ (int)ps1->slen, ps1->ptr, #res_op, res, \ fn_name, result__, \ THIS_FILE, __LINE__, \ sep0_, tmp_reason_, sep1_)); \ err_action; \ } \ } /** * Check that an expression is PJ_SUCCESS. If the check fails, error message * explaining the error code will be displayed, and the code in err_action * will be executed. * * @param expr The expression to check * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_SUCCESS(expr, err_reason, err_action) \ { \ pj_status_t tmp_status_ = (expr); \ if (tmp_status_ != PJ_SUCCESS) { \ char errbuf[80]; \ const char *tmp_reason_ = err_reason; \ const char *sep0_ = (tmp_reason_ ? " (": ""); \ const char *sep1_ = (tmp_reason_ ? ")": ""); \ if (!tmp_reason_) tmp_reason_=""; \ pj_strerror(tmp_status_, errbuf, sizeof(errbuf)); \ PJ_LOG(1,(THIS_FILE, "\"%s\" fails in %s:%d, " \ "status=%d (%s)%s%s%s", \ #expr, THIS_FILE, __LINE__, tmp_status_,errbuf,\ sep0_, tmp_reason_, sep1_)); \ err_action; \ } \ } /** * Alias for PJ_TEST_NON_ZERO() */ #define PJ_TEST_TRUE(expr, err_reason, err_action) \ PJ_TEST_NON_ZERO(expr, err_reason, err_action) /** * Alias for PJ_TEST_NON_ZERO() */ #define PJ_TEST_NOT_NULL(expr, err_reason, err_action) \ PJ_TEST_NON_ZERO(expr, err_reason, err_action) /** * Check that expr0 equals expr1. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param expr0 First expression * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_EQ(expr0, expr1, err_reason, err_action) \ PJ_TEST_BINARY_OP(expr0, ==, expr1, err_reason, err_action) /** * Check that expr0 does not equal expr1. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param expr0 First expression * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_NEQ(expr0, expr1, err_reason, err_action) \ PJ_TEST_BINARY_OP(expr0, !=, expr1, err_reason, err_action) /** * Check that expr0 is less than expr1. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param expr0 First expression * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_LT(expr0, expr1, err_reason, err_action) \ PJ_TEST_BINARY_OP(expr0, <, expr1, err_reason, err_action) /** * Check that expr0 is less than or equal to expr1. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param expr0 First expression * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_LTE(expr0, expr1, err_reason, err_action) \ PJ_TEST_BINARY_OP(expr0, <=, expr1, err_reason, err_action) /** * Check that expr0 is greater than expr1. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param expr0 First expression * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_GT(expr0, expr1, err_reason, err_action) \ PJ_TEST_BINARY_OP(expr0, >, expr1, err_reason, err_action) /** * Check that expr0 is greater than or equal to expr1. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param expr0 First expression * @param expr1 Second expression * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_GTE(expr0, expr1, err_reason, err_action) \ PJ_TEST_BINARY_OP(expr0, >=, expr1, err_reason, err_action) /** * Check string comparison result. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param ps0 Pointer to first string * @param ps1 Pointer to second string * @param res_op Operator to compare result (e.g. ==, <, >) * @param exp_result Expected result (e.g. zero for equal string) * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_STRCMP(ps0, ps1, res_op, exp_result, err_reason, err_action) \ PJ_TEST_STR_OP(pj_strcmp, ps0, ps1, res_op, exp_result, \ err_reason, err_action) /** * Check case-insensitive string comparison result. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param ps0 Pointer to first string * @param ps1 Pointer to second string * @param res_op Operator to compare result (e.g. ==, <, >) * @param exp_result Expected result (e.g. zero for equal) * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_STRICMP(ps0, ps1, res_op, exp_result, err_reason, err_action) \ PJ_TEST_STR_OP(pj_stricmp, ps0, ps1, res_op, exp_result, \ err_reason, err_action) /** * Check that two strings are equal. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param ps0 Pointer to first string * @param ps1 Pointer to second string * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_STREQ(ps0, ps1, err_reason, err_action) \ PJ_TEST_STRCMP(ps0, ps1, ==, 0, err_reason, err_action) /** * Check that two strings are not equal. * If the check fails, informative error message will be displayed and * the code in err_action will be executed. * * @param ps0 Pointer to first string * @param ps1 Pointer to second string * @param err_reason NULL or extra text to display when the check fails * @param err_action Action to perform when the check fails */ #define PJ_TEST_STRNEQ(ps0, ps1, err_reason, err_action) \ PJ_TEST_STRCMP(ps0, ps1, !=, 0, err_reason, err_action) /** * Bitwise constants that can be used in test case flags (see * pj_test_case_init()). */ typedef enum pj_test_case_flag { /** * Do not allow other test cases to run while this test case is running. * Note this only makes sense for test runners that support worker * threads. Basic runner will always run test cases serially. */ PJ_TEST_EXCLUSIVE = 1, /** * Specify that the test function must be called without argument. * This is mainly for backward compatibility with existing PJ test * functions which take no argument. */ PJ_TEST_FUNC_NO_ARG = 2, /** * Write the original log at the time it is called instead of pooling * the logs to be printed after all tests finish. */ PJ_TEST_LOG_NO_CACHE = 4, /** * Keep this test case in front of the list when shuffling the test * cases. */ PJ_TEST_KEEP_FIRST = 8, /** * Keep this test case in last in the list when shuffling the test * cases. */ PJ_TEST_KEEP_LAST = 16, } pj_test_case_flag; /** * An internal structure to represent one logging item that is saved * inside pj_test_case. */ typedef struct pj_test_log_item { PJ_DECL_LIST_MEMBER(struct pj_test_log_item); /** level */ int level; /** len */ int len; /** The log message. The actual buffer is longer. */ char msg[1]; } pj_test_log_item; /** Forward declaration of test runner */ typedef struct pj_test_runner pj_test_runner; /** * Additional parameters for creating test case. Use * pj_test_case_param_default() to initialize this structure. */ typedef struct pj_test_case_param { /** * Custom log level for this test case, to filter out logs that are more * detail than this level. Default is 6, meaning it will accept all log * levels. */ int log_level; } pj_test_case_param; /** * A test case is unit-test object to perform test against a user defined * function. */ typedef struct pj_test_case { PJ_DECL_LIST_MEMBER(struct pj_test_case); /** Test name */ char obj_name[PJ_MAX_OBJ_NAME]; /** * The test function to be called to perform the test. By convention, the * function must return zero for the test to be considered successful, * non-zero on failure, and MUST NEVER return PJ_EPENDING, otherwise the * return value will be silently changed to -12345. */ int (*test_func)(void*); /** Argument to be passed to the test function */ void *arg; /** Flags, combination of pj_test_flag constants */ unsigned flags; /** Circular buffer for logging */ pj_fifobuf_t fb; /** Parameters */ pj_test_case_param prm; /** * The return value of the test function. Zero indicates success. Initially * the value is PJ_EPENDING before the test is run. */ int result; /** List of saved logging messages */ pj_test_log_item logs; /** Pointer to the runner running this test case */ pj_test_runner *runner; /** Start time */ pj_timestamp start_time; /** End time */ pj_timestamp end_time; } pj_test_case; /** * Test suite is a collection of test cases. */ typedef struct pj_test_suite { /** List of tests */ pj_test_case tests; /** Start time */ pj_timestamp start_time; /** End time */ pj_timestamp end_time; } pj_test_suite; /** * Test statistics. Collect the statistics after the test runner finishes * with pj_test_get_stat(). */ typedef struct pj_test_stat { /** Total duration */ pj_time_val duration; /** Total number of tests in the test suite */ unsigned ntests; /** Number of tests run */ unsigned nruns; /** Number of failed tests */ unsigned nfailed; /** * Array of failed test names. Be careful that the number elements are * fixed, hence it may not be able to store all failed test names (in case * nfailed is more than the capacity, not all failed test names will be * stored, hence be careful in the loop). */ const char *failed_names[32]; } pj_test_stat; /** * Test runner parameters. Use pj_test_runner_param_default() to initialize * this structure. */ typedef struct pj_test_runner_param { /** Stop the test on error (default: false) */ pj_bool_t stop_on_error; /** Number of worker threads. Set to zero to disable parallel testings. * Only applicable to test text runner. Default is 1 if multithreading * is available. */ unsigned nthreads; /** * 0: only display test name and result after test completion (default) * 1: display test name test when starting and finishing a test */ unsigned verbosity; } pj_test_runner_param; /** * This structure represents a test runner. Currently there are two types * of test runners, the basic runner and text runner. The basic runner is the * simplest test runner that can be used without pool and threads, and can be * created with pj_test_init_basic_runner(). The text runner is more powerful * since it supports worker threads, and it is mostly suitable for console * based environments. It is created with pj_test_create_text_runner(). */ struct pj_test_runner { /** Parameters */ pj_test_runner_param prm; /** The test suite being run */ pj_test_suite *suite; /** Saving the original log writer */ pj_log_func *orig_log_writer; /** Number of tests */ unsigned ntests; /** Number of completed tests */ unsigned nruns; /** Stopping */ pj_bool_t stopping; /** main method */ void (*main)(pj_test_runner*); /** callback when test case completes. Default is to write to log */ void (*on_test_complete)(pj_test_runner*, pj_test_case*); /** destroy method */ void (*destroy)(pj_test_runner*); }; /** Option to select tests (e.g. in pj_test_dump_log_messages()) */ typedef enum pj_test_select_tests { /** Select no test*/ PJ_TEST_NO_TEST = 0, /** Select only failed tests */ PJ_TEST_FAILED_TESTS = 1, /** Select only successful tests */ PJ_TEST_SUCCESSFUL_TESTS = 2, /** Select all tests*/ PJ_TEST_ALL_TESTS = 3, /** No header/footer separator */ PJ_TEST_NO_HEADER_FOOTER = 4, } pj_test_select_tests; /** * Initialize test suite. * * @param suite The test suite */ PJ_DECL(void) pj_test_suite_init(pj_test_suite *suite); /** * Initialize pj_test_case_param with default values. If app only uses * default values in params, alternatively it doesn't need to use param * at all and just specify NULL in pj_test_case_init(). * * @param prm The parameter. */ PJ_DECL(void) pj_test_case_param_default(pj_test_case_param *prm); /** * Initialize test case. * * @param tc The test case * @param obj_name Name that will appear as test name/title * @param flags Bitwise of pj_test_case_flag to control threading, * function calling, logging, etc. * @param test_func The test function to be called to perform the test. * By convention, the function must return zero for the * test to be considered successful, non-zero on failure, * and MUST NEVER return PJ_EPENDING, otherwise the * return value will be silently changed to -12345. * @param arg Argument to give to the test function * @param fifobuf_buf Buffer for saving the logs, if required. * @param buf_size Size of the buffer for saving the logs. * @param prm Optional additional settings for the test case or NULL */ PJ_DECL(void) pj_test_case_init(pj_test_case *tc, const char *obj_name, unsigned flags, int (*test_func)(void*), void *arg, void *fifobuf_buf, unsigned buf_size, const pj_test_case_param *prm); /** * Add test case to test suite. A test case can only be added to one suite. * * @param suite The test suite * @param tc The test case */ PJ_DECL(void) pj_test_suite_add_case(pj_test_suite *suite, pj_test_case *tc); /** * Shuffle the tests. * * @param suite The test suite * @param seed Optional random seed to use, only if the value is * greater than or equal to zero. It is recommended * to set this value to make the test reproducible. */ PJ_DECL(void) pj_test_suite_shuffle(pj_test_suite *suite, int seed); /** * Initialize parameters with reasonable default values. This usually means * using one worker thread if threading is enabled, and zero worker thread * (i.e. only use the main thread) otherwise. * * @param prm Test runner parameter */ PJ_DECL(void) pj_test_runner_param_default(pj_test_runner_param *prm); /** * Initialize a basic test runner. A basic runner can be declared in the stack * and it does not require pool nor multithreading. * * @param runner The runner. * @param prm Runner params, or NULL to accept default values. */ PJ_DECL(void) pj_test_init_basic_runner(pj_test_runner *runner, const pj_test_runner_param *prm); /** * Create console based test runner. * * @param pool The pool to use to allocate memory * @param prm Test runner parameter, or NULL for default values. * @param p_runner Pointer to receive the text runner * * @return PJ_SUCCESS or the appropriate error code. */ PJ_DECL(pj_status_t) pj_test_create_text_runner( pj_pool_t *pool, const pj_test_runner_param *prm, pj_test_runner **p_runner); /** * Run test suite with the specified runner. * * @param runner The test runner * @param suite The test suite */ PJ_DECL(void) pj_test_run(pj_test_runner *runner, pj_test_suite *suite); /** * This is a crude test to detect if thread is currently running under * a test. It is mainly used to prevent nested unit testing. * * @return PJ_TRUE if we are currently running in the context of a test case * being run. */ PJ_DECL(pj_bool_t) pj_test_is_under_test(void); /** * Get the test statistics after the run completes. The test suite and * test cases instances must be kept alive in order to get and access the * statistics or log messages. * * @param suite The test suite * @param stat The test statistics result. */ PJ_DECL(void) pj_test_get_stat(const pj_test_suite *suite, pj_test_stat *stat); /** * Display statistics to the log. * * @param stat The test statistics result. */ PJ_DECL(void) pj_test_display_stat(const pj_test_stat *stat, const char *test_name, const char *log_sender); /** * Display previously saved log messages in the test cases to logging. * Note that log messages emited during test case's run are only saved * when fifobuf of the test case is configured with a suitable buffer. * Also note that the test suite and test cases instances must be kept alive * in order to get and access the statistics or log messages. * * @param suite The test suite * @param flags Select which test logs to display by choosing * from pj_test_select_tests. */ PJ_DECL(void) pj_test_display_log_messages(const pj_test_suite *suite, unsigned flags); /** * Destroy the runner. Runner may be destroyed right after it is run, * but the test suite and test cases instances must be kept alive in order * to get the statistics or log messages. * * @param runner The test runner. */ PJ_DECL(void) pj_test_runner_destroy(pj_test_runner *runner); /** * Macro to control how long worker thread should sleep waiting for next * ready test. */ #ifndef PJ_TEST_THREAD_WAIT_MSEC # define PJ_TEST_THREAD_WAIT_MSEC 100 #endif PJ_END_DECL /** * @} // PJ_UNITTEST group */ #endif /* __PJ_UNITTEST_H__ */