From 1bf3c2e4c1e1121c5e1c6b7c824fddee207c8b5c Mon Sep 17 00:00:00 2001 From: bennylp Date: Wed, 3 Jul 2024 08:01:23 +0700 Subject: [PATCH 1/7] Merged unit-test and argparse features from unittest-framework branch --- pjlib/build/Makefile | 4 +- pjlib/build/pjlib.vcproj | 120 ++++ pjlib/build/pjlib.vcxproj | 3 + pjlib/build/pjlib.vcxproj.filters | 9 + pjlib/build/pjlib_test.vcproj | 112 ++++ pjlib/build/pjlib_test.vcxproj | 1 + pjlib/build/pjlib_test.vcxproj.filters | 3 + pjlib/include/pj/argparse.h | 198 ++++++ pjlib/include/pj/unittest.h | 734 ++++++++++++++++++++++ pjlib/include/pjlib.h | 4 + pjlib/src/pj/unittest.c | 821 +++++++++++++++++++++++++ pjlib/src/pjlib-test/test_util.h | 283 +++++++++ pjlib/src/pjlib-test/unittest_test.c | 667 ++++++++++++++++++++ 13 files changed, 2957 insertions(+), 2 deletions(-) create mode 100644 pjlib/include/pj/argparse.h create mode 100644 pjlib/include/pj/unittest.h create mode 100644 pjlib/src/pj/unittest.c create mode 100644 pjlib/src/pjlib-test/test_util.h create mode 100644 pjlib/src/pjlib-test/unittest_test.c diff --git a/pjlib/build/Makefile b/pjlib/build/Makefile index 7d16780435..d13951b042 100644 --- a/pjlib/build/Makefile +++ b/pjlib/build/Makefile @@ -36,7 +36,7 @@ export PJLIB_OBJS += $(OS_OBJS) $(M_OBJS) $(CC_OBJS) $(HOST_OBJS) \ os_info.o pool.o pool_buf.o pool_caching.o pool_dbg.o rand.o \ rbtree.o sock_common.o sock_qos_common.o \ ssl_sock_common.o ssl_sock_ossl.o ssl_sock_gtls.o ssl_sock_dump.o \ - ssl_sock_darwin.o string.o timer.o types.o + ssl_sock_darwin.o string.o timer.o types.o unittest.o export PJLIB_CFLAGS += $(_CFLAGS) export PJLIB_CXXFLAGS += $(_CXXFLAGS) export PJLIB_LDFLAGS += $(_LDFLAGS) @@ -52,7 +52,7 @@ export TEST_OBJS += activesock.o atomic.o echo_clt.o errno.o exception.o \ select.o sleep.o sock.o sock_perf.o ssl_sock.o \ string.o test.o thread.o timer.o timestamp.o \ udp_echo_srv_sync.o udp_echo_srv_ioqueue.o \ - util.o + unittest_test.o util.o export TEST_CFLAGS += $(_CFLAGS) export TEST_CXXFLAGS += $(_CXXFLAGS) export TEST_LDFLAGS += $(PJLIB_LDLIB) $(_LDFLAGS) diff --git a/pjlib/build/pjlib.vcproj b/pjlib/build/pjlib.vcproj index e3043794c2..bc4e2b2c75 100644 --- a/pjlib/build/pjlib.vcproj +++ b/pjlib/build/pjlib.vcproj @@ -8420,6 +8420,118 @@ /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -12533,6 +12645,10 @@ RelativePath="..\include\pj\addr_resolv.h" > + + @@ -12681,6 +12797,10 @@ RelativePath="..\include\pj\unicode.h" > + + diff --git a/pjlib/build/pjlib.vcxproj b/pjlib/build/pjlib.vcxproj index 90ffe174f7..ad50bbf69e 100644 --- a/pjlib/build/pjlib.vcxproj +++ b/pjlib/build/pjlib.vcxproj @@ -1029,12 +1029,14 @@ + + @@ -1100,6 +1102,7 @@ + diff --git a/pjlib/build/pjlib.vcxproj.filters b/pjlib/build/pjlib.vcxproj.filters index f4b5682a5d..edfa1069ac 100644 --- a/pjlib/build/pjlib.vcxproj.filters +++ b/pjlib/build/pjlib.vcxproj.filters @@ -173,6 +173,9 @@ Source Files + + Source Files + Source Files\Other Targets @@ -232,6 +235,9 @@ Header Files + + Header Files + Header Files @@ -337,6 +343,9 @@ Header Files + + Header Files + Header Files\compat diff --git a/pjlib/build/pjlib_test.vcproj b/pjlib/build/pjlib_test.vcproj index 6876328118..1196a15823 100644 --- a/pjlib/build/pjlib_test.vcproj +++ b/pjlib/build/pjlib_test.vcproj @@ -7334,6 +7334,118 @@ /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/pjlib/build/pjlib_test.vcxproj b/pjlib/build/pjlib_test.vcxproj index 69d0bef86c..0801881867 100644 --- a/pjlib/build/pjlib_test.vcxproj +++ b/pjlib/build/pjlib_test.vcxproj @@ -795,6 +795,7 @@ + diff --git a/pjlib/build/pjlib_test.vcxproj.filters b/pjlib/build/pjlib_test.vcxproj.filters index 181327a4cc..64451af637 100644 --- a/pjlib/build/pjlib_test.vcxproj.filters +++ b/pjlib/build/pjlib_test.vcxproj.filters @@ -114,6 +114,9 @@ Source Files + + Source Files + Source Files diff --git a/pjlib/include/pj/argparse.h b/pjlib/include/pj/argparse.h new file mode 100644 index 0000000000..192e873e03 --- /dev/null +++ b/pjlib/include/pj/argparse.h @@ -0,0 +1,198 @@ +/* + * 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_ARGPARSE_H__ +#define __PJ_ARGPARSE_H__ + +/** + * @file argparse.h + * @brief Command line argument parser + */ +#include +#include +#include + +PJ_BEGIN_DECL + +/** + * Define function to display parsing error. + */ +#ifndef PJ_ARGPARSE_ERROR +# include +# define PJ_ARGPARSE_ERROR(fmt, arg) printf(fmt "\n", arg) +#endif + + +/** + * @defgroup PJ_ARGPARSE Command line argument parser + * @ingroup PJ_MISC + * @{ + * + * This module provides header only utilities to parse command line arguments. + * This is mostly used by PJSIP test and sample apps. Note that there is + * getopt() implementation in PJLIB-UTIL (but it's in PJLIB-UTIL, so it can't + * be used by PJLIB) + */ + +/** + * Peek the next possible option from argv. An argument is considered an + * option if it starts with "-" and followed by at least another letter that + * is not digit or starts with "--" and followed by a letter. + * + * @param argv The argv, which must be null terminated. + * + * @return next option or NULL. + */ +static char* pj_argparse_peek_next_option(char *const argv[]) +{ + while (*argv) { + const char *arg = *argv; + if ((*arg=='-' && *(arg+1) && !pj_isdigit(*(arg+1))) || + (*arg=='-' && *(arg+1)=='-' && *(arg+2))) + { + return *argv; + } + ++argv; + } + return NULL; +} + +/** + * Check that an option exists, without modifying argv. + * + * @param opt The option to find, e.g. "-h", "--help" + * @param argv The argv, which must be null terminated. + * + * @return PJ_TRUE if the option exists, else PJ_FALSE. + */ +static pj_bool_t pj_argparse_exists(const char *opt, char *const argv[]) +{ + int i; + for (i=1; argv[i]; ++i) { + if (pj_ansi_strcmp(argv[i], opt)==0) + return PJ_TRUE; + } + return PJ_FALSE; +} + +/** + * Check for an option and if it exists, returns PJ_TRUE remove that option + * from argc/argv. + * + * @param opt The option to find, e.g. "-h", "--help" + * @param argc Pointer to argc. + * @param argv Null terminated argv. + * + * @return PJ_TRUE if the option exists, else PJ_FALSE. + */ +static pj_bool_t pj_argparse_get_bool(const char *opt, int *argc, char *argv[]) +{ + int i; + for (i=1; argv[i]; ++i) { + if (pj_ansi_strcmp(argv[i], opt)==0) { + pj_memmove(&argv[i], &argv[i+1], ((*argc)-i)*sizeof(char*)); + (*argc)--; + return PJ_TRUE; + } + } + return PJ_FALSE; +} + +/** + * Check for an option and if it exists, get the value and remove both + * the option the the value from argc/argv. Note that the function only + * supports whitespace as separator between option and value (i.e. equal + * sign is not supported). + * + * @param opt The option to find, e.g. "-t", "--type" + * @param argc Pointer to argc. + * @param argv Null terminated argv. + * @param ptr_value Pointer to receive the value. + * + * @return PJ_SUCCESS if the option exists and value is found or if the + * option does not exist + * PJ_EINVAL if the option exits but value is not found, + */ +static pj_status_t pj_argparse_get_str( const char *opt, int *argc, + char *argv[], char **ptr_value) +{ + int i; + for (i=1; argv[i]; ++i) { + if (pj_ansi_strcmp(argv[i], opt)==0) { + pj_memmove(&argv[i], &argv[i+1], ((*argc)-i)*sizeof(char*)); + (*argc)--; + + if (argv[i]) { + char *val = argv[i]; + pj_memmove(&argv[i], &argv[i+1], ((*argc)-i)*sizeof(char*)); + (*argc)--; + *ptr_value = val; + return PJ_SUCCESS; + } else { + PJ_ARGPARSE_ERROR("Error: missing value for %s argument", + opt); + return PJ_EINVAL; + } + } + } + return PJ_SUCCESS; +} + +/** + * Check for an option and if it exists, get the integer value and remove both + * the option the the value from argc/argv. Note that the function only + * supports whitespace as separator between option and value (i.e. equal + * sign is not supported) + * + * @param opt The option to find, e.g. "-h", "--help" + * @param argc Pointer to argc. + * @param argv Null terminated argv. + * @param ptr_value Pointer to receive the value. + * + * @return PJ_SUCCESS if the option exists and value is found or if the + * option does not exist + * PJ_EINVAL if the option exits but value is not found, + */ +static pj_status_t pj_argparse_get_int( char *opt, int *argc, char *argv[], + int *ptr_value) +{ + char *endptr, *sval=NULL; + long val; + pj_status_t status = pj_argparse_get_str(opt, argc, argv, &sval); + if (status!=PJ_SUCCESS || !sval) + return status; + + val = strtol(sval, &endptr, 10); + if (*endptr) { + PJ_ARGPARSE_ERROR("Error: invalid value for %s argument", + opt); + return PJ_EINVAL; + } + + *ptr_value = (int)val; + return PJ_SUCCESS; +} + +/** + * @} + */ + +PJ_END_DECL + + +#endif /* __PJ_ARGPARSE_H__ */ + diff --git a/pjlib/include/pj/unittest.h b/pjlib/include/pj/unittest.h new file mode 100644 index 0000000000..739a9c6098 --- /dev/null +++ b/pjlib/include/pj/unittest.h @@ -0,0 +1,734 @@ +/* + * 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)) { \ + 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__ */ + diff --git a/pjlib/include/pjlib.h b/pjlib/include/pjlib.h index dfb9e53f24..4bf7dd272d 100644 --- a/pjlib/include/pjlib.h +++ b/pjlib/include/pjlib.h @@ -25,6 +25,9 @@ * @brief Include all PJLIB header files. */ +/* argparse is a utility for test apps, not really a feature of pjlib. */ +/*#include */ + #include #include #include @@ -55,6 +58,7 @@ #include #include #include +#include #include diff --git a/pjlib/src/pj/unittest.c b/pjlib/src/pj/unittest.c new file mode 100644 index 0000000000..916bd72097 --- /dev/null +++ b/pjlib/src/pj/unittest.c @@ -0,0 +1,821 @@ +/* + * 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 + */ +#include +#include +#include +#include +#include +#include +#include + +#define THIS_FILE "unittest.c" +#define INVALID_TLS_ID -1 + +static long tls_id = INVALID_TLS_ID; + +/* When basic runner is used, current test is saved in this global var */ +static pj_test_case *tc_main_thread; + +/* Forward decls. */ +static void unittest_log_callback(int level, const char *data, int len); +static int get_completion_line( const pj_test_case *tc, const char *end_line, + char *log_buf, unsigned buf_size); + +/* atexit() callback to free TLS */ +static void unittest_shutdown(void) +{ + if (tls_id != INVALID_TLS_ID) { + pj_thread_local_free(tls_id); + tls_id = INVALID_TLS_ID; + } +} + +/* initialize unittest subsystem. can be called many times. */ +static pj_status_t unittest_init(void) +{ +#if PJ_HAS_THREADS + if (tls_id == INVALID_TLS_ID) { + pj_status_t status; + status = pj_thread_local_alloc(&tls_id); + if (status != PJ_SUCCESS) { + tls_id = INVALID_TLS_ID; + return status; + } + + pj_atexit(&unittest_shutdown); + } +#endif + return PJ_SUCCESS; +} + +/* Initialize param with default values */ +PJ_DEF(void) pj_test_case_param_default( pj_test_case_param *prm) +{ + pj_bzero(prm, sizeof(*prm)); + prm->log_level = 6; +} + +/* Initialize test case */ +PJ_DEF(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) +{ + PJ_ASSERT_ON_FAIL( ((flags & (PJ_TEST_KEEP_FIRST|PJ_TEST_KEEP_LAST)) != + (PJ_TEST_KEEP_FIRST|PJ_TEST_KEEP_LAST)), {} ); + pj_bzero(tc, sizeof(*tc)); + + /* Parameters */ + if (prm) { + pj_memcpy(&tc->prm, prm, sizeof(*prm)); + } else { + pj_test_case_param_default(&tc->prm); + } + pj_ansi_strxcpy(tc->obj_name, obj_name, sizeof(tc->obj_name)); + tc->flags = flags; + tc->test_func = test_func; + tc->arg = arg; + pj_fifobuf_init(&tc->fb, fifobuf_buf, buf_size); + + /* Run-time state */ + tc->result = PJ_EPENDING; + pj_list_init(&tc->logs); +} + +/* Init test suite */ +PJ_DEF(void) pj_test_suite_init(pj_test_suite *suite) +{ + pj_bzero(suite, sizeof(*suite)); + pj_list_init(&suite->tests); +} + +/* Add test case */ +PJ_DEF(void) pj_test_suite_add_case(pj_test_suite *suite, pj_test_case *tc) +{ + pj_list_push_back(&suite->tests, tc); +} + +/* Shuffle */ +PJ_DEF(void) pj_test_suite_shuffle(pj_test_suite *suite, int seed) +{ + pj_test_case src, *tc; + unsigned total, movable; + + if (seed >= 0) + pj_srand(seed); + + /* Move tests to new list */ + pj_list_init(&src); + pj_list_merge_last(&src, &suite->tests); + + /* Move KEEP_FIRST tests first */ + for (tc=src.next; tc!=&src; ) { + pj_test_case *next = tc->next; + if (tc->flags & PJ_TEST_KEEP_FIRST) { + pj_list_erase(tc); + pj_list_push_back(&suite->tests, tc); + } + + tc = next; + } + + /* Count non-KEEP_LAST tests */ + for (total=0, movable=0, tc=src.next; tc!=&src; tc=tc->next) { + ++total; + if ((tc->flags & PJ_TEST_KEEP_LAST)==0) + ++movable; + } + + /* Shuffle non KEEP_LAST tests */ + while (movable > 0) { + int step = pj_rand() % total; + if (step < 0) + continue; + + for (tc=src.next; step>0; tc=tc->next, --step) + ; + + pj_assert(tc!=&src); + if (tc->flags & PJ_TEST_KEEP_LAST) + continue; + + pj_list_erase(tc); + pj_list_push_back(&suite->tests, tc); + --movable; + --total; + } + + /* Move KEEP_LAST tests */ + for (tc=src.next; tc!=&src; ) { + pj_test_case *next = tc->next; + pj_assert(tc->flags & PJ_TEST_KEEP_LAST); + pj_list_erase(tc); + pj_list_push_back(&suite->tests, tc); + tc = next; + } +} + +/* Initialize text runner param with default values */ +PJ_DEF(void) pj_test_runner_param_default(pj_test_runner_param *prm) +{ + pj_bzero(prm, sizeof(*prm)); +#if PJ_HAS_THREADS + prm->nthreads = 1; +#endif +} + +/* Main API to start running a test runner */ +PJ_DEF(void) pj_test_run(pj_test_runner *runner, pj_test_suite *suite) +{ + pj_test_case *tc; + + /* Redirect logging to our custom callback */ + runner->orig_log_writer = pj_log_get_log_func(); + pj_log_set_log_func(&unittest_log_callback); + + /* Initialize suite and test cases */ + runner->suite = suite; + runner->ntests = (unsigned)pj_list_size(&suite->tests); + runner->nruns = 0; + + for (tc=suite->tests.next; tc!=&suite->tests; + tc=tc->next) + { + tc->result = PJ_EPENDING; + tc->runner = NULL; /* WIll be assigned runner when is run */ + } + + /* Call the run method to perform runner specific loop */ + pj_get_timestamp(&suite->start_time); + runner->main(runner); + pj_get_timestamp(&suite->end_time); + + /* Restore logging */ + pj_log_set_log_func(runner->orig_log_writer); +} + +/* Check if we are under test */ +PJ_DEF(pj_bool_t) pj_test_is_under_test(void) +{ + return pj_log_get_log_func()==&unittest_log_callback; +} + +/* Calculate statistics */ +PJ_DEF(void) pj_test_get_stat( const pj_test_suite *suite, pj_test_stat *stat) +{ + const pj_test_case *tc; + + pj_bzero(stat, sizeof(*stat)); + stat->duration = pj_elapsed_time(&suite->start_time, &suite->end_time); + stat->ntests = (unsigned)pj_list_size(&suite->tests); + + for (tc=suite->tests.next; tc!=&suite->tests; tc=tc->next) { + if (tc->result != PJ_EPENDING) { + stat->nruns++; + if (tc->result != PJ_SUCCESS) { + if (stat->nfailed < PJ_ARRAY_SIZE(stat->failed_names)) { + stat->failed_names[stat->nfailed] = tc->obj_name; + } + stat->nfailed++; + } + } + } +} + +/* Display statistics */ +PJ_DEF(void) pj_test_display_stat(const pj_test_stat *stat, + const char *test_name, + const char *log_sender) +{ + PJ_LOG(3,(log_sender, "Unit test statistics for %s:", test_name)); + PJ_LOG(3,(log_sender, " Total number of tests: %d", stat->ntests)); + PJ_LOG(3,(log_sender, " Number of test run: %d", stat->nruns)); + PJ_LOG(3,(log_sender, " Number of failed test: %d", stat->nfailed)); + PJ_LOG(3,(log_sender, " Total duration: %dm%d.%03ds", + (int)stat->duration.sec/60, (int)stat->duration.sec%60, + (int)stat->duration.msec)); +} + +/* Get name with argument info if any, e.g. "ice_test (arg: 1)" */ +static const char *get_test_case_info(const pj_test_case *tc, + char *buf, unsigned size) +{ + char arg_info[64]; + if (tc->flags & PJ_TEST_FUNC_NO_ARG) { + arg_info[0] = '\0'; + } else { + char arg_val[40]; + /* treat argument as integer */ + pj_ansi_snprintf(arg_val, sizeof(arg_val), "%ld", (long)tc->arg); + + /* if arg value is too long (e.g. it's a pointer!), then just show + * a portion of it */ + if (pj_ansi_strlen(arg_val) > 6) { + pj_ansi_strxcat(arg_val+6, "...", sizeof(arg_val)); + } + + pj_ansi_snprintf(arg_info, sizeof(arg_info), " (arg: %s)", arg_val); + } + pj_ansi_snprintf(buf, size, "%s%s", tc->obj_name, arg_info); + return buf; +} + +/* Dump previously saved log messages */ +PJ_DEF(void) pj_test_display_log_messages(const pj_test_suite *suite, + unsigned flags) +{ + const pj_test_case *tc = suite->tests.next; + pj_log_func *log_writer = pj_log_get_log_func(); + char tcname[64]; + const char *title; + + if ((flags & PJ_TEST_ALL_TESTS)==PJ_TEST_ALL_TESTS) + title = "all"; + else if ((flags & PJ_TEST_ALL_TESTS)==PJ_TEST_FAILED_TESTS) + title = "failed"; + else if ((flags & PJ_TEST_ALL_TESTS)==PJ_TEST_SUCCESSFUL_TESTS) + title = "successful"; + else + title = "unknown"; + + while (tc != &suite->tests) { + const pj_test_log_item *log_item = tc->logs.next; + + if ((tc->result == PJ_EPENDING) || + ((flags & PJ_TEST_ALL_TESTS)==PJ_TEST_FAILED_TESTS && + tc->result==0) || + ((flags & PJ_TEST_ALL_TESTS)==PJ_TEST_SUCCESSFUL_TESTS && + tc->result!=0)) + { + /* Test doesn't meet criteria */ + tc = tc->next; + continue; + } + + if (log_item != &tc->logs) { + if (title && (flags & PJ_TEST_NO_HEADER_FOOTER)==0) { + PJ_LOG(3,(THIS_FILE, + "------------ Displaying %s test logs: ------------", + title)); + title = NULL; + } + + PJ_LOG(3,(THIS_FILE, "------------ Logs for %s [rc:%d]: ------------", + get_test_case_info(tc, tcname, sizeof(tcname)), + tc->result)); + + do { + log_writer(log_item->level, log_item->msg, log_item->len); + log_item = log_item->next; + } while (log_item != &tc->logs); + } + tc = tc->next; + } + + if (!title) { + PJ_LOG(3,(THIS_FILE, + "--------------------------------------------------------")); + } +} + +/* Destroy runner */ +PJ_DEF(void) pj_test_runner_destroy(pj_test_runner *runner) +{ + runner->destroy(runner); +} + +/**************************** Common for runners ****************************/ + +/* Set the current test case being run by a thread. The logging callback + * needs this info. + */ +static void set_current_test_case(pj_test_case *tc) +{ + if (tls_id == INVALID_TLS_ID) + tc_main_thread = tc; + else + pj_thread_local_set(tls_id, tc); +} + + +/* Get the current test case being run by a thread. The logging callback + * needs this info. + */ +static pj_test_case *get_current_test_case() +{ + if (tls_id == INVALID_TLS_ID) + return tc_main_thread; + else + return (pj_test_case*) pj_thread_local_get(tls_id); +} + +/* Logging callback */ +static void unittest_log_callback(int level, const char *data, int len) +{ + pj_test_case *tc = get_current_test_case(); + unsigned req_size, free_size; + pj_bool_t truncated; + pj_test_log_item *log_item; + + if (len < 1) + return; + + if (tc==NULL) { + /* We are being called by thread that is not part of unit-test. + * Call the original log writer, hoping that the thread did not + * change the writer before this.. (note: this can only be solved + * by setting pj_log_set/get_log_func() to be thread specific.) + */ + pj_log_write(level, data, len); + return; + } + + /* Filter out unwanted log */ + if (level > tc->prm.log_level) + return; + + /* If the test case wants to display the original log as they are called, + * then write it using the original logging writer now. + */ + if (tc->flags & PJ_TEST_LOG_NO_CACHE) { + tc->runner->orig_log_writer(level, data, len); + return; + } + + /* If fifobuf is not configured on this test case, there's nothing + * we can do. We assume tester doesn't want logging. + */ + if (pj_fifobuf_capacity(&tc->fb)==0) + return; + + /* Required size is the message length plus sizeof(pj_test_log_item). + * This should be enough to save the message INCLUDING the null + * character (because of msg[1] in pj_test_log_item) + */ + req_size = len + sizeof(pj_test_log_item); + + /* Free the buffer until it's enough to save the message. */ + while ((free_size = pj_fifobuf_available_size(&tc->fb)) < req_size && + !pj_list_empty(&tc->logs)) + { + pj_test_log_item *first = tc->logs.next; + + /* Free the oldest */ + pj_list_erase(first); + pj_fifobuf_free(&tc->fb, first); + } + + if (free_size < sizeof(pj_test_log_item) + 10) { + /* Tester has set the fifobuf's size too small */ + return; + } + + if (free_size < req_size) { + /* Truncate message */ + len = free_size - sizeof(pj_test_log_item); + req_size = free_size; + truncated = PJ_TRUE; + } else { + truncated = PJ_FALSE; + } + + log_item = (pj_test_log_item*)pj_fifobuf_alloc(&tc->fb, req_size); + PJ_ASSERT_ON_FAIL(log_item, return); + log_item->level = level; + log_item->len = len; + pj_memcpy(log_item->msg, data, len+1); + if (truncated) + log_item->msg[len-1] = '\n'; + pj_list_push_back(&tc->logs, log_item); +} + +/* Create test case completion line, i.e. the one that looks like: + * [2/24] pool_test [OK] + */ +static int get_completion_line( const pj_test_case *tc, const char *end_line, + char *log_buf, unsigned buf_size) +{ + char tcname[64]; + char res_buf[64]; + pj_time_val elapsed; + int log_len; + + elapsed = pj_elapsed_time(&tc->start_time, &tc->end_time); + + if (tc->result==0) { + pj_ansi_snprintf(res_buf, sizeof(res_buf), "[OK] [%d.%03ds]", + (int)elapsed.sec, (int)elapsed.msec); + } else if (tc->result==PJ_EPENDING) { + pj_ansi_strxcpy(res_buf, "pending", sizeof(res_buf)); + } else { + pj_ansi_snprintf(res_buf, sizeof(res_buf), "[Err: %d] [%d.%03ds]", + tc->result, (int)elapsed.sec, (int)elapsed.msec); + } + + log_len = pj_ansi_snprintf(log_buf, buf_size, "%-32s %s%s\n", + get_test_case_info(tc, tcname, sizeof(tcname)), + res_buf, end_line); + + if (log_len < 1 || log_len >= sizeof(log_buf)) + log_len = (int)pj_ansi_strlen(log_buf); + + return log_len; +} + +/* This is the main function to run a single test case. It may + * be used by the basic runner, which has no threads (=no TLS), + * no fifobuf, no pool, or by multiple threads. + */ +static void run_test_case(pj_test_runner *runner, int tid, pj_test_case *tc) +{ + char tcname[64]; + + if (runner->prm.verbosity >= 1) { + const char *exclusivity = (tc->flags & PJ_TEST_EXCLUSIVE) ? + " (exclusive)" : ""; + get_test_case_info(tc, tcname, sizeof(tcname)); + PJ_LOG(3,(THIS_FILE, "Thread %d starts running %s%s", + tid, tcname, exclusivity)); + } + + /* Set the test case being worked on by this thread */ + set_current_test_case(tc); + + tc->runner = runner; + pj_get_timestamp(&tc->start_time); + + /* Call the test case's function */ + if (tc->flags & PJ_TEST_FUNC_NO_ARG) { + /* Function without argument */ + typedef int (*func_t)(void); + func_t func = (func_t)tc->test_func; + tc->result = func(); + } else { + tc->result = tc->test_func(tc->arg); + } + + if (tc->result == PJ_EPENDING) + tc->result = -12345; + + if (tc->result && runner->prm.stop_on_error) + runner->stopping = PJ_TRUE; + + pj_get_timestamp(&tc->end_time); + runner->on_test_complete(runner, tc); + + /* Reset the test case being worked on by this thread */ + set_current_test_case(NULL); + + if (runner->prm.verbosity >= 1) { + PJ_LOG(3,(THIS_FILE, "Thread %d done running %s (rc: %d)", + tid, tcname, tc->result)); + } +} + +static pj_test_case *get_first_running(pj_test_case *tests) +{ + pj_test_case *tc; + for (tc=tests->next; tc!=tests; tc=tc->next) { + if (tc->runner && tc->result==PJ_EPENDING) + return tc; + } + return NULL; +} + +static pj_test_case *get_next_to_run(pj_test_case *tests) +{ + pj_test_case *tc; + for (tc=tests->next; tc!=tests; tc=tc->next) { + if (tc->runner==NULL) { + assert(tc->result==PJ_EPENDING); + return tc; + } + } + return NULL; +} + +/******************************* Basic Runner *******************************/ + +/* This is the "main()" function for basic runner. It just runs the tests + * sequentially + */ +static void basic_runner_main(pj_test_runner *runner) +{ + pj_test_case *tc; + for (tc = runner->suite->tests.next; + tc != &runner->suite->tests && !runner->stopping; + tc = tc->next) + { + run_test_case(runner, 0, tc); + } +} + +/* Basic runner's callback when a test case completes. */ +static void basic_on_test_complete(pj_test_runner *runner, pj_test_case *tc) +{ + char line[80]; + int len; + + runner->nruns++; + + len = pj_ansi_snprintf( line, sizeof(line), "[%2d/%d] ", + runner->nruns, runner->ntests); + if (len < 1 || len >= sizeof(line)) + len = (int)pj_ansi_strlen(line); + + len += get_completion_line(tc, "", line+len, sizeof(line)-len); + tc->runner->orig_log_writer(3, line, len); +} + +/* Destroy for basic runner */ +static void basic_runner_destroy(pj_test_runner *runner) +{ + /* Nothing to do for basic runner */ + PJ_UNUSED_ARG(runner); +} + +/* Initialize a basic runner. */ +PJ_DEF(void) pj_test_init_basic_runner(pj_test_runner *runner, + const pj_test_runner_param *prm) +{ + pj_bzero(runner, sizeof(*runner)); + if (prm) + pj_memcpy(&runner->prm, prm, sizeof(*prm)); + else + pj_test_runner_param_default(&runner->prm); + runner->main = &basic_runner_main; + runner->destroy = &basic_runner_destroy; + runner->on_test_complete = &basic_on_test_complete; +} + +/******************************* Text Runner *******************************/ + +typedef struct text_runner_t +{ + pj_test_runner base; + pj_mutex_t *mutex; + pj_thread_t **threads; +} text_runner_t; + +/* This is called by thread(s) to get the next test case to run. + * + * Returns: + * - PJ_SUCCESS if we successfully returns next test case (tc) + * - PJ_EPENDING if there is tc left but we must wait for completion of + * exclusive tc + * - PJ_ENOTFOUND if there is no further tests, which in this case the + * thread can exit. + * - other error is not anticipated, and will cause thread to exit. + */ +static pj_status_t text_runner_get_next_test_case(text_runner_t *runner, + pj_test_case **p_test_case) +{ + pj_test_suite *suite; + pj_test_case *cur, *next; + pj_status_t status; + + *p_test_case = NULL; + pj_mutex_lock(runner->mutex); + + suite = runner->base.suite; + + if (runner->base.stopping) { + pj_mutex_unlock(runner->mutex); + return PJ_ENOTFOUND; + } + + cur = get_first_running(&suite->tests); + next = get_next_to_run(&suite->tests); + + if (cur == NULL) { + if (next==NULL) { + status = PJ_ENOTFOUND; + } else { + *p_test_case = next; + /* Mark as running so it won't get picked up by other threads + * when we exit this function + */ + next->runner = &runner->base; + status = PJ_SUCCESS; + } + } else { + /* Test is still running. */ + if ((cur->flags & PJ_TEST_EXCLUSIVE)==0 && next != NULL && + (next->flags & PJ_TEST_EXCLUSIVE)==0) + { + /* Allowed other test to run because test also allows + * parallel test + */ + *p_test_case = next; + /* Mark as running so it won't get picked up by other threads + * when we exit this function + */ + next->runner = &runner->base; + status = PJ_SUCCESS; + } else { + if (next==NULL) { + /* The current case is the last one. The calling thread + * can quit now. + */ + status = PJ_ENOTFOUND; + } else { + /* Current test case or next test do not allow parallel run */ + status = PJ_EPENDING; + } + } + } + + pj_mutex_unlock(runner->mutex); + return status; +} + +typedef struct thread_param_t +{ + text_runner_t *runner; + unsigned tid; +} thread_param_t; + +/* Thread loop */ +static int text_runner_thread_proc(void *arg) +{ + thread_param_t *prm = (thread_param_t*)arg; + text_runner_t *runner = prm->runner; + unsigned tid = prm->tid; + + for (;;) { + pj_test_case *tc; + pj_status_t status; + + status = text_runner_get_next_test_case(runner, &tc); + if (status==PJ_SUCCESS) { + run_test_case(&runner->base, tid, tc); + } else if (status==PJ_EPENDING) { + /* Yeah sleep, but the "correct" solution is probably an order of + * magnitute more complicated, so this is good I think. + */ + pj_thread_sleep(PJ_TEST_THREAD_WAIT_MSEC); + } else { + break; + } + } + + return 0; +} + +/* This is the "main()" function for text runner. */ +static void text_runner_main(pj_test_runner *base) +{ + text_runner_t *runner = (text_runner_t*)base; + thread_param_t tprm = { runner, 0 }; + unsigned i; + + for (i=0; iprm.nthreads; ++i) { + pj_thread_resume(runner->threads[i]); + } + + /* The main thread behaves like another worker thread */ + text_runner_thread_proc(&tprm); + + for (i=0; iprm.nthreads; ++i) { + pj_thread_join(runner->threads[i]); + } +} + +/* text runner's callback when a test case completes. */ +static void text_runner_on_test_complete(pj_test_runner *base, + pj_test_case *tc) +{ + text_runner_t *runner = (text_runner_t*)base; + pj_mutex_lock(runner->mutex); + basic_on_test_complete(base, tc); + pj_mutex_unlock(runner->mutex); +} + +/* text runner destructor */ +static void text_runner_destroy(pj_test_runner *base) +{ + text_runner_t *runner = (text_runner_t*)base; + unsigned i; + + for (i=0; iprm.nthreads; ++i) { + pj_thread_destroy(runner->threads[i]); + } + if (runner->mutex) + pj_mutex_destroy(runner->mutex); + +} + +/* Create text runner */ +PJ_DEF(pj_status_t) pj_test_create_text_runner( + pj_pool_t *pool, + const pj_test_runner_param *prm, + pj_test_runner **p_runner) +{ + text_runner_t *runner; + unsigned i; + pj_status_t status; + + *p_runner = NULL; + + status = unittest_init(); + if (status != PJ_SUCCESS) + return status; + + runner = PJ_POOL_ZALLOC_T(pool, text_runner_t); + runner->base.main = text_runner_main; + runner->base.destroy = text_runner_destroy; + runner->base.on_test_complete = &text_runner_on_test_complete; + + status = pj_mutex_create(pool, "unittest%p", PJ_MUTEX_RECURSE, + &runner->mutex); + if (status != PJ_SUCCESS) + goto on_error; + + if (prm) { + pj_memcpy(&runner->base.prm, prm, sizeof(*prm)); + } else { + pj_test_runner_param_default(&runner->base.prm); + } + runner->base.prm.nthreads = 0; + runner->threads = (pj_thread_t**) pj_pool_calloc(pool, prm->nthreads, + sizeof(pj_thread_t*)); + for (i=0; inthreads; ++i) { + thread_param_t *tprm = PJ_POOL_ZALLOC_T(pool, thread_param_t); + tprm->runner = runner; + tprm->tid = i+1; + status = pj_thread_create(pool, "unittest%p", + text_runner_thread_proc, tprm, + 0, PJ_THREAD_SUSPENDED, + &runner->threads[i]); + if (status != PJ_SUCCESS) + goto on_error; + runner->base.prm.nthreads++; + } + + *p_runner = (pj_test_runner*)runner; + return PJ_SUCCESS; + +on_error: + text_runner_destroy(&runner->base); + return status; +} + diff --git a/pjlib/src/pjlib-test/test_util.h b/pjlib/src/pjlib-test/test_util.h new file mode 100644 index 0000000000..91ab0e32d9 --- /dev/null +++ b/pjlib/src/pjlib-test/test_util.h @@ -0,0 +1,283 @@ +#include +#include +#include +#include +#include +#include +#include + +/* Overrideable max tests */ +#ifndef UT_MAX_TESTS +# define UT_MAX_TESTS 16 +#endif + +/* Overrideable log max buffer size */ +#ifndef UT_LOG_BUF_SIZE +# define UT_LOG_BUF_SIZE 1000 +#endif + +/* Usually app won't supply THIS_FILE when including test_util.h, so + * create a temporary one + */ +#ifndef THIS_FILE +# define THIS_FILE "test.c" +# define UNDEF_THIS_FILE 1 +#endif + +/* Unit testing app */ +typedef struct ut_app_t +{ + pj_bool_t prm_config; + pj_test_select_tests prm_logging_policy; + int prm_nthreads; + int prm_list_test; + pj_bool_t prm_stop_on_error; + pj_bool_t prm_shuffle; + int prm_seed; + unsigned flags; + unsigned verbosity; + + pj_pool_t *pool; + pj_test_suite suite; + pj_test_runner *runner; + + int ntests; + pj_test_case test_cases[UT_MAX_TESTS]; +} ut_app_t; + +/* Call this in main.c before parsing arguments */ +static void ut_app_init0(ut_app_t *ut_app) +{ + pj_bzero(ut_app, sizeof(*ut_app)); + ut_app->prm_logging_policy = PJ_TEST_FAILED_TESTS; + ut_app->prm_nthreads = -1; + ut_app->flags = 0; +} + +/* Call this in test.c before adding test cases */ +static pj_status_t ut_app_init1(ut_app_t *ut_app, pj_pool_factory *mem) +{ + ut_app->pool = pj_pool_create(mem, THIS_FILE, 4000, 4000, NULL); + PJ_TEST_NOT_NULL(ut_app->pool, NULL, return PJ_ENOMEM); + pj_test_suite_init(&ut_app->suite); + return PJ_SUCCESS; +} + +/* Don't forget to call this */ +static void ut_app_destroy(ut_app_t *ut_app) +{ + pj_pool_release(ut_app->pool); + ut_app->pool = NULL; +} + +typedef int (*ut_func)(void*); + +/* This is for adding test func that has no arg */ +#define UT_ADD_TEST(ut_app, test_func, flags) \ + ut_add_test(ut_app, (ut_func)test_func, 0, \ + #test_func, flags | PJ_TEST_FUNC_NO_ARG, argc, argv) + + +/* This is for adding test func that HAS arg */ +#define UT_ADD_TEST1(ut_app, test_func, arg, flags) \ + ut_add_test(ut_app, (ut_func)test_func, arg, #test_func, flags, argc, argv) + + +/* Check if a test is specified/requested in cmdline */ +static pj_bool_t ut_test_included(const char *name, int argc, char *argv[]) +{ + if (argc <= 1) + return PJ_TRUE; + + ++argv; + while (*argv) { + if (pj_ansi_strcmp(name, *argv)==0) + return PJ_TRUE; + ++argv; + } + return PJ_FALSE; +} + +/* Add test case */ +static pj_status_t ut_add_test(ut_app_t *ut_app, int (*test_func)(void*), + void *arg, const char *test_name, + unsigned flags, int argc, char *argv[]) +{ + char *log_buf; + pj_test_case *tc; + + if (ut_app->ntests >= UT_MAX_TESTS) { + PJ_LOG(1,(THIS_FILE, "Too many tests for adding %s", test_name)); + return PJ_ETOOMANY; + } + + if (!ut_test_included(test_name, argc, argv)) { + return PJ_ENOTFOUND; + } + + log_buf = (char*)pj_pool_alloc(ut_app->pool, UT_LOG_BUF_SIZE); + tc = &ut_app->test_cases[ut_app->ntests]; + flags |= ut_app->flags; + pj_test_case_init(tc, test_name, flags, (int (*)(void*))test_func, arg, + log_buf, UT_LOG_BUF_SIZE, NULL); + + pj_test_suite_add_case( &ut_app->suite, tc); + ++ut_app->ntests; + + return PJ_SUCCESS; +} + +static void ut_list_tests(ut_app_t *ut_app, const char *title) +{ + unsigned d = pj_log_get_decor(); + const pj_test_case *tc, *prev=NULL; + + pj_log_set_decor(d ^ PJ_LOG_HAS_NEWLINE); + PJ_LOG(3,(THIS_FILE, "%ld %s:", pj_list_size(&ut_app->suite.tests), + title)); + pj_log_set_decor(0); + for (tc=ut_app->suite.tests.next; tc!=&ut_app->suite.tests; tc=tc->next) { + if (!prev || pj_ansi_strcmp(tc->obj_name, prev->obj_name)) + PJ_LOG(3,(THIS_FILE, " %s", tc->obj_name)); + prev = tc; + } + PJ_LOG(3,(THIS_FILE, "\n")); + pj_log_set_decor(d); +} + +static pj_status_t ut_run_tests(ut_app_t *ut_app, const char *title, + int argc, char *argv[]) +{ + pj_test_runner_param runner_prm; + pj_test_runner_param_default(&runner_prm); + pj_test_runner *runner; + pj_test_stat stat; + pj_status_t status; + + if (ut_app->prm_list_test) { + ut_list_tests(ut_app, title); + return PJ_SUCCESS; + } + + if (argc > 1) { + int i; + for (i=1; isuite.tests.next; tc!=&ut_app->suite.tests; + tc=tc->next) + { + if (pj_ansi_strcmp(argv[i], tc->obj_name)==0) + break; + } + if (tc==&ut_app->suite.tests) { + PJ_LOG(2,(THIS_FILE, "Test \"%s\" is not found in %s", + argv[i], title)); + } + } + } + + if (ut_app->ntests <= 0) + return PJ_SUCCESS; + + pj_test_runner_param_default(&runner_prm); + runner_prm.stop_on_error = ut_app->prm_stop_on_error; + if (ut_app->prm_nthreads >= 0) + runner_prm.nthreads = ut_app->prm_nthreads; + runner_prm.verbosity = ut_app->verbosity; + status = pj_test_create_text_runner(ut_app->pool, &runner_prm, &runner); + PJ_TEST_SUCCESS(status, "error creating text runner", return status); + + PJ_LOG(3,(THIS_FILE, + "Performing %d %s with %d worker thread%s", + ut_app->ntests, title, runner_prm.nthreads, + runner_prm.nthreads>1?"s":"")); + + if (ut_app->prm_shuffle) { + PJ_LOG(3,(THIS_FILE, "Shuffling tests, random seed=%d", + ut_app->prm_seed)); + pj_test_suite_shuffle(&ut_app->suite, ut_app->prm_seed); + } + + pj_test_run(runner, &ut_app->suite); + pj_test_runner_destroy(runner); + pj_test_display_log_messages(&ut_app->suite, ut_app->prm_logging_policy); + pj_test_get_stat(&ut_app->suite, &stat); + pj_test_display_stat(&stat, title, THIS_FILE); + + return stat.nfailed ? PJ_EBUG : PJ_SUCCESS; +} + +static void ut_usage() +{ + puts(" -c, --config Show configuration macros"); + puts(" -l 0,1,2,3 0: Don't show logging after tests"); + puts(" 1: Show logs of only failed tests (default)"); + puts(" 2: Show logs of only successful tests"); + puts(" 3: Show logs of all tests"); + puts(" --log-no-cache Do not cache logging"); + printf(" -w N Set N worker threads (0: disable. Default: %d)\n", PJ_HAS_THREADS); + puts(" -L, --list List the tests and exit"); + puts(" --stop-err Stop testing on error"); + puts(" --shuffle Shuffle the test order"); + puts(" --seed N Set shuffle random seed (must be >= 0)"); + puts(" -v, --verbose Show info when starting/stopping tests"); +} + + +static pj_status_t ut_parse_args(ut_app_t *ut_app, int *argc, char *argv[]) +{ + int itmp = -1; + pj_status_t status; + + ut_app->prm_config = pj_argparse_get_bool("-c", argc, argv) || + pj_argparse_get_bool("--config", argc, argv); + ut_app->prm_list_test = pj_argparse_get_bool("-L", argc, argv) || + pj_argparse_get_bool("--list", argc, argv); + ut_app->prm_stop_on_error = pj_argparse_get_bool("--stop-err", argc, argv); + ut_app->prm_shuffle = pj_argparse_get_bool("--shuffle", argc, argv); + if (pj_argparse_get_bool("--log-no-cache", argc, argv)) { + ut_app->flags |= PJ_TEST_LOG_NO_CACHE; + } + + if (pj_argparse_exists("-l", argv)) { + status = pj_argparse_get_int("-l", argc, argv, &itmp); + if (status==PJ_SUCCESS && itmp>=0 && itmp<=3) { + ut_app->prm_logging_policy = (pj_test_select_tests)itmp; + } else { + puts("Error: invalid value for -l option"); + return PJ_EINVAL; + } + } + + if (pj_argparse_exists("-w", argv)) { + status = pj_argparse_get_int("-w", argc, argv, &itmp); + if (status==PJ_SUCCESS && itmp>=0 && itmp<50) { + ut_app->prm_nthreads = itmp; + } else { + puts("Error: invalid/missing value for -w option"); + return PJ_EINVAL; + } + } + + if (ut_app->prm_shuffle) { + pj_time_val tv; + + status = pj_gettimeofday(&tv); + if (status != PJ_SUCCESS) + return status; + + ut_app->prm_seed = (int)(tv.msec); + status = pj_argparse_get_int("--seed", argc, argv, &ut_app->prm_seed); + if (status != PJ_SUCCESS) + return status; + } + + ut_app->verbosity = pj_argparse_get_bool("-v", argc, argv) || + pj_argparse_get_bool("--verbose", argc, argv); + + return PJ_SUCCESS; +} + +#ifdef UNDEF_THIS_FILE +# undef THIS_FILE +#endif diff --git a/pjlib/src/pjlib-test/unittest_test.c b/pjlib/src/pjlib-test/unittest_test.c new file mode 100644 index 0000000000..f4653a0862 --- /dev/null +++ b/pjlib/src/pjlib-test/unittest_test.c @@ -0,0 +1,667 @@ +/* + * 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 + */ +#include "test.h" +#include + + +#define THIS_FILE "unittest_test.c" + +static char log_buffer[1024], *log_buffer_ptr = log_buffer; +static pj_log_func *old_log_func; + +static void reset_log_buffer() +{ + log_buffer_ptr = log_buffer; + *log_buffer_ptr = '\0'; +} + +#if 0 +static void print_log_buffer(char *title) +{ + printf("------ log buffer %s: ------\n", title); + printf("%s", log_buffer); + printf("%s\n", "------ end buffer: ------"); +} +#else +#define print_log_buffer(title) +#endif + +/* This log callback appends the log to the log_buffer */ +static void log_callback(int level, const char *data, int len) +{ + unsigned max_len = (log_buffer+sizeof(log_buffer))-log_buffer_ptr-1; + + /* make sure len is correct */ + len = strlen(data); + if (len > max_len) + len = max_len; + + pj_ansi_strxcpy(log_buffer_ptr, data, max_len); + log_buffer_ptr += len; +} + +static void start_capture_log() +{ + old_log_func = pj_log_get_log_func(); + pj_log_set_log_func(&log_callback); + reset_log_buffer(); +} + +static void end_capture_log() +{ + pj_log_set_log_func(old_log_func); +} + +static int test_true(int is_true, const char *reason) +{ + PJ_TEST_TRUE(is_true, reason, return -100); + return 0; +} + +static int test_eq(int value0, int value1, const char *reason) +{ + PJ_TEST_EQ(value0, value1, reason, return -200); + return 0; +} + +static int test_success(pj_status_t status, const char *reason) +{ + PJ_TEST_SUCCESS(status, reason, return -300); + return 0; +} + +/* + * Test various assertion tests + */ +static int assertion_tests() +{ + pj_str_t s0, s1; + int ret; + + /* Unary PJ_TEST_TRUE successful */ + ret = test_true(1, NULL); + if (ret != 0) return ret; + + /* PJ_TEST_TRUE successful, reason is specified (but not used) */ + ret = test_true(1, "logic error"); + if (ret != 0) return ret; + + /* PJ_TEST_TRUE fails, without reason */ + reset_log_buffer(); + ret = test_true(0, NULL); + if (ret != -100) return -110; + + /* Check log message, should be something like: + 09:47:09.692 Test "is_true" fails in unittest_test.c:44 + */ + if (pj_ansi_strlen(log_buffer) < 10) return -120; + if (!pj_ansi_strstr(log_buffer, "is_true")) return -121; + if (!pj_ansi_strstr(log_buffer, THIS_FILE)) return -122; + + /* PJ_TEST_TRUE fails, reason is specified */ + reset_log_buffer(); + ret = test_true(0, "logic error"); + if (ret != -100) return -110; + + /* Check log message, should be something like: + 09:47:37.145 Test "is_true" fails in unittest_test.c:44 (logic error) + */ + if (pj_ansi_strlen(log_buffer) < 10) return -130; + if (!pj_ansi_strstr(log_buffer, "is_true")) return -131; + if (!pj_ansi_strstr(log_buffer, THIS_FILE)) return -132; + if (!pj_ansi_strstr(log_buffer, " (logic error)")) return -132; + + /* Binary PJ_TEST_EQ successful */ + ret = test_eq(999, 999, NULL); + if (ret != 0) return ret; + + ret = test_eq(999, 999, "not used"); + if (ret != 0) return ret; + + /* Binary comparison PJ_TEST_EQ fails, reason not given */ + reset_log_buffer(); + ret = test_eq(998, 999, NULL); + if (ret != -200) return -210; + + /* Check log message, should be something like: + 09:47:56.315 Test "value0" (998) == "value1" (999) fails in unittest_test.c:50 + */ + if (pj_ansi_strlen(log_buffer) < 10) return -220; + if (!pj_ansi_strstr(log_buffer, "value0")) return -221; + if (!pj_ansi_strstr(log_buffer, "value1")) return -222; + if (!pj_ansi_strstr(log_buffer, "998")) return -223; + if (!pj_ansi_strstr(log_buffer, "999")) return -224; + if (!pj_ansi_strstr(log_buffer, THIS_FILE)) return -225; + + /* Binary comparison PJ_TEST_EQ fails, reason is given */ + reset_log_buffer(); + ret = test_eq(998, 999, "values are different"); + if (ret != -200) return -250; + + /* Check log message, should be something like: + 09:51:37.866 Test "value0" (998) == "value1" (999) fails in unittest_test.c:50 (values are different) + */ + if (pj_ansi_strlen(log_buffer) < 10) return -260; + if (!pj_ansi_strstr(log_buffer, "value0")) return -261; + if (!pj_ansi_strstr(log_buffer, "value1")) return -262; + if (!pj_ansi_strstr(log_buffer, "998")) return -263; + if (!pj_ansi_strstr(log_buffer, "999")) return -264; + if (!pj_ansi_strstr(log_buffer, THIS_FILE)) return -265; + if (!pj_ansi_strstr(log_buffer, " (values are different)")) return -266; + + /* PJ_TEST_SUCCESS successful scenario */ + ret = test_success(PJ_SUCCESS, NULL); + if (ret != 0) return ret; + + /* PJ_TEST_SUCCESS successful, reason is specified (but not used) */ + ret = test_success(PJ_SUCCESS, "logic error"); + if (ret != 0) return ret; + + /* PJ_TEST_SUCCESS fails, without reason */ + reset_log_buffer(); + ret = test_success(PJ_EPENDING, NULL); + if (ret != -300) return -310; + + /* Check log message, should be something like: + 09:52:22.654 "status" fails in unittest_test.c:56, status=70002 (Pending operation (PJ_EPENDING)) + */ + if (pj_ansi_strlen(log_buffer) < 10) return -320; + if (!pj_ansi_strstr(log_buffer, "Pending operation")) return -321; + if (!pj_ansi_strstr(log_buffer, THIS_FILE)) return -322; + + /* PJ_TEST_SUCCESS fails, reason given */ + reset_log_buffer(); + ret = test_success(PJ_EPENDING, "should be immediate"); + if (ret != -300) return -350; + + /* Check log message, should be something like: + 09:52:49.717 "status" fails in unittest_test.c:56, status=70002 (Pending operation (PJ_EPENDING)) (should be immediate) + */ + if (pj_ansi_strlen(log_buffer) < 10) return -350; + if (!pj_ansi_strstr(log_buffer, "Pending operation")) return -351; + if (!pj_ansi_strstr(log_buffer, THIS_FILE)) return -352; + if (!pj_ansi_strstr(log_buffer, " (should be immediate)")) return -353; + + /* String tests */ + PJ_TEST_STREQ(pj_cstr(&s0, "123456"), pj_cstr(&s1, "123456"), NULL, + return -400); + PJ_TEST_STRICMP(pj_cstr(&s0, "123456"), pj_cstr(&s1, "123456"), ==, 0, + NULL, return -405); + + ret = -1; + PJ_TEST_STREQ(pj_cstr(&s0, "123456"), pj_cstr(&s1, "135"), NULL, + ret=0); + if (ret) + PJ_TEST_EQ(ret, 0, "PJ_TEST_STREQ was expected to fail", return -410); + + PJ_TEST_STRNEQ(pj_cstr(&s0, "123456"), pj_cstr(&s1, "000000"), NULL, + return -420); + + ret = -1; + PJ_TEST_STRNEQ(pj_cstr(&s0, "123456"), pj_cstr(&s1, "123456"), NULL, + ret=0); + if (ret) + PJ_TEST_EQ(ret, 0, "PJ_TEST_STRNEQ was expected to fail", return -410); + + return 0; +} + + +enum test_flags +{ + /* value 0-31 is reserved for test id */ + TEST_LOG_DETAIL = 32, + TEST_LOG_INFO = 64, + TEST_LOG_ALL = (TEST_LOG_DETAIL | TEST_LOG_INFO), + + TEST_RETURN_ERROR = 256, +}; + +/** Dummy test */ +static int func_to_test(void *arg) +{ + unsigned flags = (unsigned)(long)arg; + unsigned test_id = (flags & 31); + + /* Note that for simplicity, make the length of log messages the same + * (otherwise freeing one oldest log may not be enough to fit in the + * later log) + */ + if (flags & TEST_LOG_DETAIL) { + PJ_LOG(4,(THIS_FILE, "Entering func_to_test(%d).....", test_id)); + } + + if (flags & TEST_LOG_INFO) { + PJ_LOG(3,(THIS_FILE, "Performing func_to_test(%d)...", test_id)); + } + + if (flags & TEST_RETURN_ERROR) { + PJ_LOG(1,(THIS_FILE, "Some error in func_to_test(%d)", test_id)); + } + + /* Simulate some work and additional sleep to ensure tests + * completes in correct order + */ + pj_thread_sleep(100+test_id*100); + + return (flags & TEST_RETURN_ERROR) ? -123 : 0; +} + +enum { + /* approx len of each log msg in func_to_test() + * Note that logging adds decor e.g. time, plus overhead in fifobuf. + */ + MSG_LEN = 45 + 4 + sizeof(pj_test_log_item), +}; + +/** + * Simple demonstration on how to use the unittest framework. + * Here we use the unittest framework to test the unittest framework itself. + * We test both the basic and text runner. + */ +static int usage_test(pj_pool_t *pool, pj_bool_t basic, pj_bool_t parallel, + unsigned log_size) +{ + enum { + TEST_CASE_LOG_SIZE = 256, + }; + char test_title[80]; + pj_test_suite suite; + unsigned flags; + char buffer0[TEST_CASE_LOG_SIZE], buffer1[TEST_CASE_LOG_SIZE]; + pj_test_case test_case0, test_case1; + pj_test_runner *runner; + pj_test_runner basic_runner; + pj_test_stat stat; + + /* to differentiate different invocations of this function */ + pj_ansi_snprintf(test_title, sizeof(test_title), + "basic=%d, parallel=%d, log_size=%d", + basic, parallel, log_size); + //PJ_LOG(3,(THIS_FILE, "Unittest usage_test: %s", test_title)); + + PJ_TEST_LTE(log_size, TEST_CASE_LOG_SIZE, test_title, return -1); + + /* Init test suite */ + pj_test_suite_init(&suite); + + if (basic) + flags = 0; + else + flags = parallel? 0 : PJ_TEST_EXCLUSIVE; + + /* Add test case 0. This test case writes some logs and returns + * success. + */ + + pj_test_case_init(&test_case0, "successful test", flags, &func_to_test, + (void*)(long)(0+TEST_LOG_ALL), + buffer0, log_size, NULL); + pj_test_suite_add_case(&suite, &test_case0); + + /* Add test case 1. This test case simulates error. It writes + * error to log and returns non-zero error. + */ + pj_test_case_init(&test_case1, "failure test", flags, &func_to_test, + (void*)(long)(1+TEST_LOG_ALL+TEST_RETURN_ERROR), + buffer1, log_size, NULL); + pj_test_suite_add_case(&suite, &test_case1); + + /* Create runner */ + if (basic) { + runner = &basic_runner; + pj_test_init_basic_runner(runner, NULL); + } else { + pj_test_runner_param prm; + pj_test_runner_param_default(&prm); + prm.nthreads = 4; /* more threads than we need, for testing */ + PJ_TEST_SUCCESS(pj_test_create_text_runner(pool, &prm, &runner), + test_title, return -10); + } + + /* Run runner */ + pj_test_run(runner, &suite); + + /* Runner can be safely destroyed now */ + pj_test_runner_destroy(runner); + + /* test the statistics returned by pj_test_get_stat() */ + pj_bzero(&stat, sizeof(stat)); + pj_test_get_stat(&suite, &stat); + PJ_TEST_EQ(stat.ntests, 2, test_title, return -100); + PJ_TEST_EQ(stat.nruns, 2, test_title, return -110); + PJ_TEST_EQ(stat.nfailed, 1, test_title, return -120); + PJ_TEST_EQ(stat.failed_names[0], test_case1.obj_name, test_title, return -130); + PJ_TEST_EQ(strcmp(stat.failed_names[0], "failure test"), 0, + test_title, return -135); + PJ_TEST_GTE( PJ_TIME_VAL_MSEC(stat.duration), 200, test_title, return -140); + + /* test logging. + * Since gave the test cases buffer to store log messages, we can dump + * the logs now and check the contents. + */ + start_capture_log(); + /* Dumping all test logs. both test 0 and 1 must be present */ + pj_test_display_log_messages(&suite, PJ_TEST_ALL_TESTS | + PJ_TEST_NO_HEADER_FOOTER); + print_log_buffer(test_title); + end_capture_log(); + if (log_size >= MSG_LEN) { + /* We should only have space for the last log */ + PJ_TEST_NOT_NULL(strstr(log_buffer, "Some error in func_to_test(1)"), + test_title, return -201); + PJ_TEST_NOT_NULL(strstr(log_buffer, "Performing func_to_test(0)"), + test_title, return -202); + } else if (log_size < 10) { + /* buffer is too small, writing log will be rejected */ + PJ_TEST_EQ(strstr(log_buffer, "Some error"), NULL, + test_title, return -203); + } else { + /* buffer is small, message will be truncated */ + PJ_TEST_NOT_NULL(strstr(log_buffer, "Some error in"), + test_title, return -204); + } + + if (log_size >= 2*MSG_LEN) { + /* We should have space for two last log messages */ + PJ_TEST_NOT_NULL(strstr(log_buffer, "Performing func_to_test(1)"), + test_title, return -205); + PJ_TEST_NOT_NULL(strstr(log_buffer, "Entering func_to_test(0)"), + test_title, return -206); + } + if (log_size >= 3*MSG_LEN) { + /* We should have space for three last log messages */ + PJ_TEST_NOT_NULL(strstr(log_buffer, "Entering func_to_test(1)"), + test_title, return -207); + } + + /* Dumping only failed test. Only test 1 must be present */ + start_capture_log(); + pj_test_display_log_messages(&suite, PJ_TEST_FAILED_TESTS | + PJ_TEST_NO_HEADER_FOOTER); + print_log_buffer(test_title); + end_capture_log(); + if (log_size >= MSG_LEN) { + PJ_TEST_NOT_NULL(strstr(log_buffer, "Some error in func_to_test(1)"), + test_title, return -211); + } else if (log_size < 10) { + /* buffer is too small, writing log will be rejected */ + PJ_TEST_EQ(strstr(log_buffer, "Some error"), NULL, + test_title, return -212); + } else { + /* buffer is small, message will be truncated */ + PJ_TEST_NOT_NULL(strstr(log_buffer, "Some error in"), + test_title, return -213); + } + if (log_size >= 2*MSG_LEN) { + PJ_TEST_NOT_NULL(strstr(log_buffer, "Performing func_to_test(1)"), + test_title, return -214); + } + if (log_size >= 3*MSG_LEN) { + PJ_TEST_NOT_NULL(strstr(log_buffer, "Entering func_to_test(1)"), + test_title, return -215); + } + PJ_TEST_EQ(strstr(log_buffer, "Entering func_to_test(0)"), NULL, + test_title, return -216); + + /* Dumping only successful test. Only test 0 must be present */ + start_capture_log(); + pj_test_display_log_messages(&suite, PJ_TEST_SUCCESSFUL_TESTS | + PJ_TEST_NO_HEADER_FOOTER); + print_log_buffer(test_title); + end_capture_log(); + if (log_size >= MSG_LEN) { + PJ_TEST_NOT_NULL(strstr(log_buffer, "Performing func_to_test(0)"), + test_title, return -221); + } + if (log_size >= 2*MSG_LEN) { + PJ_TEST_NOT_NULL(strstr(log_buffer, "Entering func_to_test(0)"), + test_title, return -222); + } + PJ_TEST_EQ(strstr(log_buffer, "Entering func_to_test(1)"), NULL, + test_title, return -223); + + return 0; +} + +static int shuffle_test() +{ + enum { N=16, REPEAT=16 }; + pj_test_suite suite; + char test_names[N][16]; + pj_test_case tcs[N], *tc; + unsigned i, repeat, flags[N]; + + for (i=0; iarg, 2, seed_info, return -40); + tc = tc->next; + PJ_TEST_EQ((long)tc->arg, 5, seed_info, return -41); + tc = tc->next; + PJ_TEST_TRUE((long)tc->arg==0 || (long)tc->arg==3, seed_info, + return -42); + tc = tc->next; + PJ_TEST_TRUE((long)tc->arg==0 || (long)tc->arg==3, seed_info, + return -43); + tc = tc->next; + PJ_TEST_EQ((long)tc->arg, 1, seed_info, return -44); + tc = tc->next; + PJ_TEST_EQ((long)tc->arg, 4, seed_info, return -45); + } + + return 0; +} + +static int log_msg_sizes[] = { + 1*MSG_LEN, /* log buffer enough for 1 message */ + 2*MSG_LEN, /* log buffer enough for 2 messages */ + 3*MSG_LEN, /* log buffer enough for 3 message */ + 0, /* no log buffer */ + 64, /* log will be truncated */ +}; + +int unittest_basic_test(void) +{ + int ret, log_level = pj_log_get_level(); + unsigned j; + + if (pj_test_is_under_test()) { + PJ_LOG(1,(THIS_FILE, "Cannot run unittest_test under unit-test!")); + return -1; + } + + /* We wants to get detailed logging */ + pj_log_set_level(4); + + start_capture_log(); + ret = assertion_tests(); + end_capture_log(); + if (ret) goto on_return; + + for (j=0; jsleep); + pj_ansi_strxcat(parallel_msg, prm->id, sizeof(parallel_msg)); + return 0; +} + +/* + * Test that PJ_TEST_EXCLUSIVE flag (or lack of) works. + */ +int unittest_parallel_test() +{ + enum { + MAX_TESTS = 11, + LOG_SIZE = 128, + MS = PJ_TEST_THREAD_WAIT_MSEC, + }; + pj_pool_t *pool; + pj_test_suite suite; + char buffers[MAX_TESTS][LOG_SIZE]; + pj_test_case test_cases[MAX_TESTS]; + pj_test_runner_param prm; + struct parallel_param_t parallel_params[MAX_TESTS] = { + {PJ_TEST_EXCLUSIVE, MS+2*MS, "a"}, + {PJ_TEST_EXCLUSIVE, MS+1*MS, "b"}, /* b have to wait for a */ + {0, MS+7*MS, "c"}, /* c have to wait for b */ + {0, MS+1*MS, "d"}, /* d have to wait for b */ + {0, MS+4*MS, "e"}, /* e have to wait for b */ + {PJ_TEST_EXCLUSIVE, MS+2*MS, "f"}, /* f have to wait for c, d, e */ + {PJ_TEST_EXCLUSIVE, MS+0*MS, "g"}, /* g have to wait for f */ + {PJ_TEST_EXCLUSIVE, MS+5*MS, "h"}, /* h have to wait for g */ + {0, MS+4*MS, "i"}, /* i will finish last */ + {0, MS+2*MS, "j"}, /* i will finish second last */ + {0, MS+0*MS, "k"}, /* i will finish third last */ + }; + const char *correct_msg = "abdecfghkji"; + pj_str_t stmp0, stmp1; + pj_test_runner *runner; + int i; + + pj_test_suite_init(&suite); + PJ_TEST_NOT_NULL((pool=pj_pool_create( mem, NULL, 4000, 4000, NULL)), + NULL, return -1); + + for (i=0; i Date: Wed, 3 Jul 2024 11:33:29 +0700 Subject: [PATCH 2/7] Further merge from unittest-framework branch and tested on Visial Studio 2015 --- pjlib/include/pj/fifobuf.h | 107 +++++++++- pjlib/include/pj/unittest.h | 2 +- pjlib/src/pj/fifobuf.c | 53 +++-- pjlib/src/pj/unittest.c | 5 +- pjlib/src/pjlib-test/fifobuf.c | 100 +++++++--- pjlib/src/pjlib-test/test.c | 6 + pjlib/src/pjlib-test/test.h | 6 +- pjlib/src/pjlib-test/test_util.h | 283 --------------------------- pjlib/src/pjlib-test/unittest_test.c | 4 +- 9 files changed, 233 insertions(+), 333 deletions(-) delete mode 100644 pjlib/src/pjlib-test/test_util.h diff --git a/pjlib/include/pj/fifobuf.h b/pjlib/include/pj/fifobuf.h index 26b8fb776e..c457960afe 100644 --- a/pjlib/include/pj/fifobuf.h +++ b/pjlib/include/pj/fifobuf.h @@ -19,25 +19,112 @@ #ifndef __PJ_FIFOBUF_H__ #define __PJ_FIFOBUF_H__ +/** + * @file fifobuf.h + * @brief Circular buffer + */ +/** + * @defgroup PJ_FIFOBUF Circular buffer + * @ingroup PJ_DS + * @{ + */ + #include PJ_BEGIN_DECL -typedef struct pj_fifobuf_t pj_fifobuf_t; -struct pj_fifobuf_t + +/** + * FIFO buffer or circular buffer. + */ +typedef struct pj_fifobuf_t { - char *first, *last; - char *ubegin, *uend; + /** The start of the buffer */ + char *first; + + /** The end of the buffer */ + char *last; + + /** The start of empty area in the buffer */ + char *ubegin; + + /** The end of empty area in the buffer */ + char *uend; + + /** Full flag when ubegin==uend */ int full; -}; -PJ_DECL(void) pj_fifobuf_init (pj_fifobuf_t *fb, void *buffer, unsigned size); -PJ_DECL(unsigned) pj_fifobuf_max_size (pj_fifobuf_t *fb); -PJ_DECL(void*) pj_fifobuf_alloc (pj_fifobuf_t *fb, unsigned size); -PJ_DECL(pj_status_t) pj_fifobuf_unalloc (pj_fifobuf_t *fb, void *buf); -PJ_DECL(pj_status_t) pj_fifobuf_free (pj_fifobuf_t *fb, void *buf); +} pj_fifobuf_t; + + +/** + * Initialize the circular buffer by giving it a buffer and size. + * + * @param fb The fifobuf/circular buffer + * @param buffer Buffer to be used to allocate/free chunks of memory from by + * the circular buffer. + * @param size The size of the buffer. + */ +PJ_DECL(void) pj_fifobuf_init(pj_fifobuf_t *fb, void *buffer, unsigned size); + +/** + * Returns the capacity (initial size) of the buffer. + * + * @param fb The fifobuf/circular buffer + * + * @return Capacity in bytes. + */ +PJ_DECL(unsigned) pj_fifobuf_capacity(pj_fifobuf_t *fb); + +/** + * Returns maximum buffer size that can be allocated from the circular buffer. + * + * @param fb The fifobuf/circular buffer + * + * @return Free size in bytes + */ +PJ_DECL(unsigned) pj_fifobuf_available_size(pj_fifobuf_t *fb); + +/** + * Allocate a buffer from the circular buffer. + * + * @param fb The fifobuf/circular buffer + * @param size Size to allocate + * + * @return Allocated buffer or NULL if the buffer cannot be allocated. + */ +PJ_DECL(void*) pj_fifobuf_alloc(pj_fifobuf_t *fb, unsigned size); + +/** + * Free up space used by the last allocated buffer. For example, if you + * allocated ptr0, ptr1, and ptr2, this function is used to free ptr2. + * + * @param fb The fifobuf/circular buffer + * @param buf The buffer to be freed. This is the pointer returned by + * pj_fifobuf_alloc() + * + * @return PJ_SUCCESS or the appropriate error. + */ +PJ_DECL(pj_status_t) pj_fifobuf_unalloc(pj_fifobuf_t *fb, void *buf); + +/** + * Free up space used by the earliest allocated buffer. For example, if you + * allocated ptr0, ptr1, and ptr2, this function is used to free ptr0. + * + * @param fb The fifobuf/circular buffer + * @param buf The buffer to be freed. This is the pointer returned by + * pj_fifobuf_alloc() + * + * @return PJ_SUCCESS or the appropriate error. + */ +PJ_DECL(pj_status_t) pj_fifobuf_free(pj_fifobuf_t *fb, void *buf); + PJ_END_DECL +/** + * @} // PJ_FIFOBUF group + */ + #endif /* __PJ_FIFOBUF_H__ */ diff --git a/pjlib/include/pj/unittest.h b/pjlib/include/pj/unittest.h index 739a9c6098..c11b252ee5 100644 --- a/pjlib/include/pj/unittest.h +++ b/pjlib/include/pj/unittest.h @@ -49,7 +49,7 @@ PJ_BEGIN_DECL */ #define PJ_TEST_NON_ZERO(expr, err_reason, err_action) \ { \ - if (!(expr)) { \ + if ((expr)==0) { \ const char *tmp_reason_ = err_reason; \ const char *sep0_ = (tmp_reason_ ? " (": ""); \ const char *sep1_ = (tmp_reason_ ? ")": ""); \ diff --git a/pjlib/src/pj/fifobuf.c b/pjlib/src/pj/fifobuf.c index 464f3114cc..5acd3220bc 100644 --- a/pjlib/src/pj/fifobuf.c +++ b/pjlib/src/pj/fifobuf.c @@ -20,11 +20,25 @@ #include #include #include +#include #define THIS_FILE "fifobuf" #define SZ sizeof(unsigned) +/* put and get size at arbitrary, possibly unaligned location */ +PJ_INLINE(void) put_size(void *ptr, unsigned size) +{ + pj_memcpy(ptr, &size, sizeof(size)); +} + +PJ_INLINE(unsigned) get_size(const void *ptr) +{ + unsigned size; + pj_memcpy(&size, ptr, sizeof(size)); + return size; +} + PJ_DEF(void) pj_fifobuf_init (pj_fifobuf_t *fifobuf, void *buffer, unsigned size) { PJ_CHECK_STACK(); @@ -36,23 +50,38 @@ PJ_DEF(void) pj_fifobuf_init (pj_fifobuf_t *fifobuf, void *buffer, unsigned size fifobuf->first = (char*)buffer; fifobuf->last = fifobuf->first + size; fifobuf->ubegin = fifobuf->uend = fifobuf->first; - fifobuf->full = 0; + fifobuf->full = (fifobuf->last==fifobuf->first); } -PJ_DEF(unsigned) pj_fifobuf_max_size (pj_fifobuf_t *fifobuf) +PJ_DEF(unsigned) pj_fifobuf_capacity (pj_fifobuf_t *fifobuf) { - unsigned s1, s2; + unsigned cap = (unsigned)(fifobuf->last - fifobuf->first); + return (cap > 0) ? cap-SZ : 0; +} +PJ_DEF(unsigned) pj_fifobuf_available_size (pj_fifobuf_t *fifobuf) +{ PJ_CHECK_STACK(); + if (fifobuf->full) + return 0; + if (fifobuf->uend >= fifobuf->ubegin) { - s1 = (unsigned)(fifobuf->last - fifobuf->uend); - s2 = (unsigned)(fifobuf->ubegin - fifobuf->first); + unsigned s; + unsigned s1 = (unsigned)(fifobuf->last - fifobuf->uend); + unsigned s2 = (unsigned)(fifobuf->ubegin - fifobuf->first); + if (s1 <= SZ) + s = s2; + else if (s2 <= SZ) + s = s1; + else + s = s1=SZ) ? s-SZ : 0; } else { - s1 = s2 = (unsigned)(fifobuf->ubegin - fifobuf->uend); + unsigned s = (unsigned)(fifobuf->ubegin - fifobuf->uend); + return (s>=SZ) ? s-SZ : 0; } - - return s1uend = fifobuf->first; if (fifobuf->uend == fifobuf->ubegin) fifobuf->full = 1; - *(unsigned*)ptr = size+SZ; + put_size(ptr, size+SZ); ptr += SZ; PJ_LOG(6, (THIS_FILE, @@ -97,7 +126,7 @@ PJ_DEF(void*) pj_fifobuf_alloc (pj_fifobuf_t *fifobuf, unsigned size) fifobuf->uend = start + size + SZ; if (fifobuf->uend == fifobuf->ubegin) fifobuf->full = 1; - *(unsigned*)ptr = size+SZ; + put_size(ptr, size+SZ); ptr += SZ; PJ_LOG(6, (THIS_FILE, @@ -121,7 +150,7 @@ PJ_DEF(pj_status_t) pj_fifobuf_unalloc (pj_fifobuf_t *fifobuf, void *buf) PJ_CHECK_STACK(); ptr -= SZ; - sz = *(unsigned*)ptr; + sz = get_size(ptr); endptr = fifobuf->uend; if (endptr == fifobuf->first) @@ -162,7 +191,7 @@ PJ_DEF(pj_status_t) pj_fifobuf_free (pj_fifobuf_t *fifobuf, void *buf) } end = (fifobuf->uend > fifobuf->ubegin) ? fifobuf->uend : fifobuf->last; - sz = *(unsigned*)ptr; + sz = get_size(ptr); if (ptr+sz > end) { pj_assert(!"Invalid size!"); return -1; diff --git a/pjlib/src/pj/unittest.c b/pjlib/src/pj/unittest.c index 916bd72097..d8ac75132d 100644 --- a/pjlib/src/pj/unittest.c +++ b/pjlib/src/pj/unittest.c @@ -725,9 +725,12 @@ static int text_runner_thread_proc(void *arg) static void text_runner_main(pj_test_runner *base) { text_runner_t *runner = (text_runner_t*)base; - thread_param_t tprm = { runner, 0 }; + thread_param_t tprm ; unsigned i; + tprm.runner = runner; + tprm.tid = 0; + for (i=0; iprm.nthreads; ++i) { pj_thread_resume(runner->threads[i]); } diff --git a/pjlib/src/pjlib-test/fifobuf.c b/pjlib/src/pjlib-test/fifobuf.c index fb38460aa4..cf1bb9eef8 100644 --- a/pjlib/src/pjlib-test/fifobuf.c +++ b/pjlib/src/pjlib-test/fifobuf.c @@ -18,6 +18,7 @@ */ #include "test.h" + /* To prevent warning about "translation unit is empty" * when this test is disabled. */ @@ -27,29 +28,87 @@ int dummy_fifobuf_test; #include +#define THIS_FILE "fifobuf.c" + +static int fifobuf_size_test() +{ + enum { SIZE = 32, SZ=sizeof(unsigned) }; + char before[8]; + char buffer[SIZE]; + char after[8]; + char zero[8]; + void *p0, *p1; + pj_fifobuf_t fifo; + + pj_bzero(before, sizeof(before)); + pj_bzero(after, sizeof(after)); + pj_bzero(zero, sizeof(zero)); + + pj_fifobuf_init (&fifo, buffer, sizeof(buffer)); + + PJ_TEST_EQ(pj_fifobuf_capacity(&fifo), SIZE-SZ, NULL, return -11); + PJ_TEST_EQ(pj_fifobuf_available_size(&fifo), SIZE-SZ, NULL, return -12); + + p0 = pj_fifobuf_alloc(&fifo, 16); + p1 = pj_fifobuf_alloc(&fifo, 4); + PJ_UNUSED_ARG(p1); + + pj_fifobuf_free(&fifo, p0); + + PJ_TEST_EQ( pj_fifobuf_available_size(&fifo), 16, NULL, return -14); + PJ_TEST_EQ( pj_memcmp(before, zero, sizeof(zero)), 0, NULL, return -18); + PJ_TEST_EQ( pj_memcmp(after, zero, sizeof(zero)), 0, NULL, return -19); + return 0; +} + int fifobuf_test() { enum { SIZE = 1024, MAX_ENTRIES = 128, - MIN_SIZE = 4, MAX_SIZE = 64, + MIN_SIZE = 4, MAX_SIZE = 64, SZ = sizeof(unsigned), LOOP=10000 }; pj_pool_t *pool; pj_fifobuf_t fifo; - unsigned available = SIZE; + pj_size_t available = SIZE; void *entries[MAX_ENTRIES]; void *buffer; int i; + i = fifobuf_size_test(); + if (i != 0) + return i; + pool = pj_pool_create(mem, NULL, SIZE+256, 0, NULL); - if (!pool) - return -10; + PJ_TEST_NOT_NULL(pool, NULL, return -10); buffer = pj_pool_alloc(pool, SIZE); - if (!buffer) - return -20; + PJ_TEST_NOT_NULL(buffer, NULL, return -20); pj_fifobuf_init (&fifo, buffer, SIZE); - // Test 1 + /* Capacity and maximum alloc */ + PJ_TEST_EQ(pj_fifobuf_capacity(&fifo), SIZE-SZ, NULL, return -21); + PJ_TEST_EQ(pj_fifobuf_available_size(&fifo), SIZE-SZ, NULL, return -22); + + entries[0] = pj_fifobuf_alloc(&fifo, SIZE-SZ); + PJ_TEST_NOT_NULL(entries[0], NULL, return -23); + + pj_fifobuf_free(&fifo, entries[0]); + PJ_TEST_EQ(pj_fifobuf_capacity(&fifo), SIZE-SZ, NULL, return -21); + PJ_TEST_EQ(pj_fifobuf_available_size(&fifo), SIZE-SZ, NULL, return -22); + + /* Alignment test */ + for (i=0; i<30; ++i) { + entries[i] = pj_fifobuf_alloc(&fifo, i+1); + PJ_TEST_NOT_NULL(entries[i], NULL, return -50); + //fifobuf is no longer aligned. + //PJ_TEST_EQ(((pj_size_t)entries[i]) % sizeof(unsigned), 0, -60, + // "alignment error"); + } + for (i=0; i<30; ++i) { + PJ_TEST_SUCCESS( pj_fifobuf_free(&fifo, entries[i]), NULL, return -70); + } + + /* alloc() and free() */ for (i=0; i=MIN_SIZE+4 && count < MAX_ENTRIES;) { + available = pj_fifobuf_available_size(&fifo); + for (count=0; available>MIN_SIZE+SZ+MAX_SIZE/2 && count < MAX_ENTRIES;) { int size = MIN_SIZE+(pj_rand() % MAX_SIZE); entries[count] = pj_fifobuf_alloc (&fifo, size); if (entries[count]) { - available -= (size+4); + available = pj_fifobuf_available_size(&fifo); ++count; } } for (j=0; j -#include -#include -#include -#include -#include -#include - -/* Overrideable max tests */ -#ifndef UT_MAX_TESTS -# define UT_MAX_TESTS 16 -#endif - -/* Overrideable log max buffer size */ -#ifndef UT_LOG_BUF_SIZE -# define UT_LOG_BUF_SIZE 1000 -#endif - -/* Usually app won't supply THIS_FILE when including test_util.h, so - * create a temporary one - */ -#ifndef THIS_FILE -# define THIS_FILE "test.c" -# define UNDEF_THIS_FILE 1 -#endif - -/* Unit testing app */ -typedef struct ut_app_t -{ - pj_bool_t prm_config; - pj_test_select_tests prm_logging_policy; - int prm_nthreads; - int prm_list_test; - pj_bool_t prm_stop_on_error; - pj_bool_t prm_shuffle; - int prm_seed; - unsigned flags; - unsigned verbosity; - - pj_pool_t *pool; - pj_test_suite suite; - pj_test_runner *runner; - - int ntests; - pj_test_case test_cases[UT_MAX_TESTS]; -} ut_app_t; - -/* Call this in main.c before parsing arguments */ -static void ut_app_init0(ut_app_t *ut_app) -{ - pj_bzero(ut_app, sizeof(*ut_app)); - ut_app->prm_logging_policy = PJ_TEST_FAILED_TESTS; - ut_app->prm_nthreads = -1; - ut_app->flags = 0; -} - -/* Call this in test.c before adding test cases */ -static pj_status_t ut_app_init1(ut_app_t *ut_app, pj_pool_factory *mem) -{ - ut_app->pool = pj_pool_create(mem, THIS_FILE, 4000, 4000, NULL); - PJ_TEST_NOT_NULL(ut_app->pool, NULL, return PJ_ENOMEM); - pj_test_suite_init(&ut_app->suite); - return PJ_SUCCESS; -} - -/* Don't forget to call this */ -static void ut_app_destroy(ut_app_t *ut_app) -{ - pj_pool_release(ut_app->pool); - ut_app->pool = NULL; -} - -typedef int (*ut_func)(void*); - -/* This is for adding test func that has no arg */ -#define UT_ADD_TEST(ut_app, test_func, flags) \ - ut_add_test(ut_app, (ut_func)test_func, 0, \ - #test_func, flags | PJ_TEST_FUNC_NO_ARG, argc, argv) - - -/* This is for adding test func that HAS arg */ -#define UT_ADD_TEST1(ut_app, test_func, arg, flags) \ - ut_add_test(ut_app, (ut_func)test_func, arg, #test_func, flags, argc, argv) - - -/* Check if a test is specified/requested in cmdline */ -static pj_bool_t ut_test_included(const char *name, int argc, char *argv[]) -{ - if (argc <= 1) - return PJ_TRUE; - - ++argv; - while (*argv) { - if (pj_ansi_strcmp(name, *argv)==0) - return PJ_TRUE; - ++argv; - } - return PJ_FALSE; -} - -/* Add test case */ -static pj_status_t ut_add_test(ut_app_t *ut_app, int (*test_func)(void*), - void *arg, const char *test_name, - unsigned flags, int argc, char *argv[]) -{ - char *log_buf; - pj_test_case *tc; - - if (ut_app->ntests >= UT_MAX_TESTS) { - PJ_LOG(1,(THIS_FILE, "Too many tests for adding %s", test_name)); - return PJ_ETOOMANY; - } - - if (!ut_test_included(test_name, argc, argv)) { - return PJ_ENOTFOUND; - } - - log_buf = (char*)pj_pool_alloc(ut_app->pool, UT_LOG_BUF_SIZE); - tc = &ut_app->test_cases[ut_app->ntests]; - flags |= ut_app->flags; - pj_test_case_init(tc, test_name, flags, (int (*)(void*))test_func, arg, - log_buf, UT_LOG_BUF_SIZE, NULL); - - pj_test_suite_add_case( &ut_app->suite, tc); - ++ut_app->ntests; - - return PJ_SUCCESS; -} - -static void ut_list_tests(ut_app_t *ut_app, const char *title) -{ - unsigned d = pj_log_get_decor(); - const pj_test_case *tc, *prev=NULL; - - pj_log_set_decor(d ^ PJ_LOG_HAS_NEWLINE); - PJ_LOG(3,(THIS_FILE, "%ld %s:", pj_list_size(&ut_app->suite.tests), - title)); - pj_log_set_decor(0); - for (tc=ut_app->suite.tests.next; tc!=&ut_app->suite.tests; tc=tc->next) { - if (!prev || pj_ansi_strcmp(tc->obj_name, prev->obj_name)) - PJ_LOG(3,(THIS_FILE, " %s", tc->obj_name)); - prev = tc; - } - PJ_LOG(3,(THIS_FILE, "\n")); - pj_log_set_decor(d); -} - -static pj_status_t ut_run_tests(ut_app_t *ut_app, const char *title, - int argc, char *argv[]) -{ - pj_test_runner_param runner_prm; - pj_test_runner_param_default(&runner_prm); - pj_test_runner *runner; - pj_test_stat stat; - pj_status_t status; - - if (ut_app->prm_list_test) { - ut_list_tests(ut_app, title); - return PJ_SUCCESS; - } - - if (argc > 1) { - int i; - for (i=1; isuite.tests.next; tc!=&ut_app->suite.tests; - tc=tc->next) - { - if (pj_ansi_strcmp(argv[i], tc->obj_name)==0) - break; - } - if (tc==&ut_app->suite.tests) { - PJ_LOG(2,(THIS_FILE, "Test \"%s\" is not found in %s", - argv[i], title)); - } - } - } - - if (ut_app->ntests <= 0) - return PJ_SUCCESS; - - pj_test_runner_param_default(&runner_prm); - runner_prm.stop_on_error = ut_app->prm_stop_on_error; - if (ut_app->prm_nthreads >= 0) - runner_prm.nthreads = ut_app->prm_nthreads; - runner_prm.verbosity = ut_app->verbosity; - status = pj_test_create_text_runner(ut_app->pool, &runner_prm, &runner); - PJ_TEST_SUCCESS(status, "error creating text runner", return status); - - PJ_LOG(3,(THIS_FILE, - "Performing %d %s with %d worker thread%s", - ut_app->ntests, title, runner_prm.nthreads, - runner_prm.nthreads>1?"s":"")); - - if (ut_app->prm_shuffle) { - PJ_LOG(3,(THIS_FILE, "Shuffling tests, random seed=%d", - ut_app->prm_seed)); - pj_test_suite_shuffle(&ut_app->suite, ut_app->prm_seed); - } - - pj_test_run(runner, &ut_app->suite); - pj_test_runner_destroy(runner); - pj_test_display_log_messages(&ut_app->suite, ut_app->prm_logging_policy); - pj_test_get_stat(&ut_app->suite, &stat); - pj_test_display_stat(&stat, title, THIS_FILE); - - return stat.nfailed ? PJ_EBUG : PJ_SUCCESS; -} - -static void ut_usage() -{ - puts(" -c, --config Show configuration macros"); - puts(" -l 0,1,2,3 0: Don't show logging after tests"); - puts(" 1: Show logs of only failed tests (default)"); - puts(" 2: Show logs of only successful tests"); - puts(" 3: Show logs of all tests"); - puts(" --log-no-cache Do not cache logging"); - printf(" -w N Set N worker threads (0: disable. Default: %d)\n", PJ_HAS_THREADS); - puts(" -L, --list List the tests and exit"); - puts(" --stop-err Stop testing on error"); - puts(" --shuffle Shuffle the test order"); - puts(" --seed N Set shuffle random seed (must be >= 0)"); - puts(" -v, --verbose Show info when starting/stopping tests"); -} - - -static pj_status_t ut_parse_args(ut_app_t *ut_app, int *argc, char *argv[]) -{ - int itmp = -1; - pj_status_t status; - - ut_app->prm_config = pj_argparse_get_bool("-c", argc, argv) || - pj_argparse_get_bool("--config", argc, argv); - ut_app->prm_list_test = pj_argparse_get_bool("-L", argc, argv) || - pj_argparse_get_bool("--list", argc, argv); - ut_app->prm_stop_on_error = pj_argparse_get_bool("--stop-err", argc, argv); - ut_app->prm_shuffle = pj_argparse_get_bool("--shuffle", argc, argv); - if (pj_argparse_get_bool("--log-no-cache", argc, argv)) { - ut_app->flags |= PJ_TEST_LOG_NO_CACHE; - } - - if (pj_argparse_exists("-l", argv)) { - status = pj_argparse_get_int("-l", argc, argv, &itmp); - if (status==PJ_SUCCESS && itmp>=0 && itmp<=3) { - ut_app->prm_logging_policy = (pj_test_select_tests)itmp; - } else { - puts("Error: invalid value for -l option"); - return PJ_EINVAL; - } - } - - if (pj_argparse_exists("-w", argv)) { - status = pj_argparse_get_int("-w", argc, argv, &itmp); - if (status==PJ_SUCCESS && itmp>=0 && itmp<50) { - ut_app->prm_nthreads = itmp; - } else { - puts("Error: invalid/missing value for -w option"); - return PJ_EINVAL; - } - } - - if (ut_app->prm_shuffle) { - pj_time_val tv; - - status = pj_gettimeofday(&tv); - if (status != PJ_SUCCESS) - return status; - - ut_app->prm_seed = (int)(tv.msec); - status = pj_argparse_get_int("--seed", argc, argv, &ut_app->prm_seed); - if (status != PJ_SUCCESS) - return status; - } - - ut_app->verbosity = pj_argparse_get_bool("-v", argc, argv) || - pj_argparse_get_bool("--verbose", argc, argv); - - return PJ_SUCCESS; -} - -#ifdef UNDEF_THIS_FILE -# undef THIS_FILE -#endif diff --git a/pjlib/src/pjlib-test/unittest_test.c b/pjlib/src/pjlib-test/unittest_test.c index f4653a0862..65c10a4881 100644 --- a/pjlib/src/pjlib-test/unittest_test.c +++ b/pjlib/src/pjlib-test/unittest_test.c @@ -44,7 +44,9 @@ static void print_log_buffer(char *title) /* This log callback appends the log to the log_buffer */ static void log_callback(int level, const char *data, int len) { - unsigned max_len = (log_buffer+sizeof(log_buffer))-log_buffer_ptr-1; + int max_len = (log_buffer+sizeof(log_buffer))-log_buffer_ptr-1; + + PJ_UNUSED_ARG(level); /* make sure len is correct */ len = strlen(data); From 7e2e50c6cac9572c6a86b9990856a81d365aa1dc Mon Sep 17 00:00:00 2001 From: bennylp Date: Wed, 3 Jul 2024 11:38:12 +0700 Subject: [PATCH 3/7] Fix accidental typo in test.h --- pjlib/src/pjlib-test/test.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pjlib/src/pjlib-test/test.h b/pjlib/src/pjlib-test/test.h index f8b5c16384..fe42897e94 100644 --- a/pjlib/src/pjlib-test/test.h +++ b/pjlib/src/pjlib-test/test.h @@ -51,8 +51,8 @@ #define INCLUDE_FIFOBUF_TEST GROUP_DATA_STRUCTURE #define INCLUDE_RBTREE_TEST GROUP_DATA_STRUCTURE #define INCLUDE_TIMER_TEST GROUP_DATA_STRUCTURE -#define INCLUDE_TIMER_TEST GROUP_DATA_STRUCTURE -#define INCLUDE_UNITTEST_TEST GROUP_OS +#define INCLUDE_ATOMIC_TEST GROUP_DATA_STRUCTURE +#define INCLUDE_UNITTEST_TEST GROUP_DATA_STRUCTUREc #define INCLUDE_MUTEX_TEST (PJ_HAS_THREADS && GROUP_OS) #define INCLUDE_SLEEP_TEST GROUP_OS #define INCLUDE_OS_TEST GROUP_OS From 385fd04e5276bdecd7f38d1e166642134e2d9462 Mon Sep 17 00:00:00 2001 From: bennylp Date: Wed, 3 Jul 2024 11:40:31 +0700 Subject: [PATCH 4/7] Another typo fix to test.h --- pjlib/src/pjlib-test/test.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pjlib/src/pjlib-test/test.h b/pjlib/src/pjlib-test/test.h index fe42897e94..226a71bf84 100644 --- a/pjlib/src/pjlib-test/test.h +++ b/pjlib/src/pjlib-test/test.h @@ -51,8 +51,8 @@ #define INCLUDE_FIFOBUF_TEST GROUP_DATA_STRUCTURE #define INCLUDE_RBTREE_TEST GROUP_DATA_STRUCTURE #define INCLUDE_TIMER_TEST GROUP_DATA_STRUCTURE -#define INCLUDE_ATOMIC_TEST GROUP_DATA_STRUCTURE #define INCLUDE_UNITTEST_TEST GROUP_DATA_STRUCTUREc +#define INCLUDE_ATOMIC_TEST GROUP_OS #define INCLUDE_MUTEX_TEST (PJ_HAS_THREADS && GROUP_OS) #define INCLUDE_SLEEP_TEST GROUP_OS #define INCLUDE_OS_TEST GROUP_OS From e243b1ee72fc33c33c18f204673b988e44c3be58 Mon Sep 17 00:00:00 2001 From: bennylp Date: Wed, 3 Jul 2024 14:00:25 +0700 Subject: [PATCH 5/7] Remove argparse (into separate PR) --- pjlib/build/pjlib.vcxproj | 3 +- pjlib/build/pjlib.vcxproj.filters | 3 - pjlib/include/pj/argparse.h | 198 ------------------------------ pjlib/include/pjlib.h | 3 - 4 files changed, 1 insertion(+), 206 deletions(-) delete mode 100644 pjlib/include/pj/argparse.h diff --git a/pjlib/build/pjlib.vcxproj b/pjlib/build/pjlib.vcxproj index ad50bbf69e..5a58c29aa5 100644 --- a/pjlib/build/pjlib.vcxproj +++ b/pjlib/build/pjlib.vcxproj @@ -1,4 +1,4 @@ - + @@ -1036,7 +1036,6 @@ - diff --git a/pjlib/build/pjlib.vcxproj.filters b/pjlib/build/pjlib.vcxproj.filters index edfa1069ac..3a823e52ab 100644 --- a/pjlib/build/pjlib.vcxproj.filters +++ b/pjlib/build/pjlib.vcxproj.filters @@ -235,9 +235,6 @@ Header Files - - Header Files - Header Files diff --git a/pjlib/include/pj/argparse.h b/pjlib/include/pj/argparse.h deleted file mode 100644 index 192e873e03..0000000000 --- a/pjlib/include/pj/argparse.h +++ /dev/null @@ -1,198 +0,0 @@ -/* - * 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_ARGPARSE_H__ -#define __PJ_ARGPARSE_H__ - -/** - * @file argparse.h - * @brief Command line argument parser - */ -#include -#include -#include - -PJ_BEGIN_DECL - -/** - * Define function to display parsing error. - */ -#ifndef PJ_ARGPARSE_ERROR -# include -# define PJ_ARGPARSE_ERROR(fmt, arg) printf(fmt "\n", arg) -#endif - - -/** - * @defgroup PJ_ARGPARSE Command line argument parser - * @ingroup PJ_MISC - * @{ - * - * This module provides header only utilities to parse command line arguments. - * This is mostly used by PJSIP test and sample apps. Note that there is - * getopt() implementation in PJLIB-UTIL (but it's in PJLIB-UTIL, so it can't - * be used by PJLIB) - */ - -/** - * Peek the next possible option from argv. An argument is considered an - * option if it starts with "-" and followed by at least another letter that - * is not digit or starts with "--" and followed by a letter. - * - * @param argv The argv, which must be null terminated. - * - * @return next option or NULL. - */ -static char* pj_argparse_peek_next_option(char *const argv[]) -{ - while (*argv) { - const char *arg = *argv; - if ((*arg=='-' && *(arg+1) && !pj_isdigit(*(arg+1))) || - (*arg=='-' && *(arg+1)=='-' && *(arg+2))) - { - return *argv; - } - ++argv; - } - return NULL; -} - -/** - * Check that an option exists, without modifying argv. - * - * @param opt The option to find, e.g. "-h", "--help" - * @param argv The argv, which must be null terminated. - * - * @return PJ_TRUE if the option exists, else PJ_FALSE. - */ -static pj_bool_t pj_argparse_exists(const char *opt, char *const argv[]) -{ - int i; - for (i=1; argv[i]; ++i) { - if (pj_ansi_strcmp(argv[i], opt)==0) - return PJ_TRUE; - } - return PJ_FALSE; -} - -/** - * Check for an option and if it exists, returns PJ_TRUE remove that option - * from argc/argv. - * - * @param opt The option to find, e.g. "-h", "--help" - * @param argc Pointer to argc. - * @param argv Null terminated argv. - * - * @return PJ_TRUE if the option exists, else PJ_FALSE. - */ -static pj_bool_t pj_argparse_get_bool(const char *opt, int *argc, char *argv[]) -{ - int i; - for (i=1; argv[i]; ++i) { - if (pj_ansi_strcmp(argv[i], opt)==0) { - pj_memmove(&argv[i], &argv[i+1], ((*argc)-i)*sizeof(char*)); - (*argc)--; - return PJ_TRUE; - } - } - return PJ_FALSE; -} - -/** - * Check for an option and if it exists, get the value and remove both - * the option the the value from argc/argv. Note that the function only - * supports whitespace as separator between option and value (i.e. equal - * sign is not supported). - * - * @param opt The option to find, e.g. "-t", "--type" - * @param argc Pointer to argc. - * @param argv Null terminated argv. - * @param ptr_value Pointer to receive the value. - * - * @return PJ_SUCCESS if the option exists and value is found or if the - * option does not exist - * PJ_EINVAL if the option exits but value is not found, - */ -static pj_status_t pj_argparse_get_str( const char *opt, int *argc, - char *argv[], char **ptr_value) -{ - int i; - for (i=1; argv[i]; ++i) { - if (pj_ansi_strcmp(argv[i], opt)==0) { - pj_memmove(&argv[i], &argv[i+1], ((*argc)-i)*sizeof(char*)); - (*argc)--; - - if (argv[i]) { - char *val = argv[i]; - pj_memmove(&argv[i], &argv[i+1], ((*argc)-i)*sizeof(char*)); - (*argc)--; - *ptr_value = val; - return PJ_SUCCESS; - } else { - PJ_ARGPARSE_ERROR("Error: missing value for %s argument", - opt); - return PJ_EINVAL; - } - } - } - return PJ_SUCCESS; -} - -/** - * Check for an option and if it exists, get the integer value and remove both - * the option the the value from argc/argv. Note that the function only - * supports whitespace as separator between option and value (i.e. equal - * sign is not supported) - * - * @param opt The option to find, e.g. "-h", "--help" - * @param argc Pointer to argc. - * @param argv Null terminated argv. - * @param ptr_value Pointer to receive the value. - * - * @return PJ_SUCCESS if the option exists and value is found or if the - * option does not exist - * PJ_EINVAL if the option exits but value is not found, - */ -static pj_status_t pj_argparse_get_int( char *opt, int *argc, char *argv[], - int *ptr_value) -{ - char *endptr, *sval=NULL; - long val; - pj_status_t status = pj_argparse_get_str(opt, argc, argv, &sval); - if (status!=PJ_SUCCESS || !sval) - return status; - - val = strtol(sval, &endptr, 10); - if (*endptr) { - PJ_ARGPARSE_ERROR("Error: invalid value for %s argument", - opt); - return PJ_EINVAL; - } - - *ptr_value = (int)val; - return PJ_SUCCESS; -} - -/** - * @} - */ - -PJ_END_DECL - - -#endif /* __PJ_ARGPARSE_H__ */ - diff --git a/pjlib/include/pjlib.h b/pjlib/include/pjlib.h index 4bf7dd272d..1bc591be51 100644 --- a/pjlib/include/pjlib.h +++ b/pjlib/include/pjlib.h @@ -25,9 +25,6 @@ * @brief Include all PJLIB header files. */ -/* argparse is a utility for test apps, not really a feature of pjlib. */ -/*#include */ - #include #include #include From 45ad16830968bbc44ef7f54a991fcee05bad25bf Mon Sep 17 00:00:00 2001 From: bennylp Date: Wed, 3 Jul 2024 14:02:56 +0700 Subject: [PATCH 6/7] Remove argparse from old VC project --- pjlib/build/pjlib.vcproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/pjlib/build/pjlib.vcproj b/pjlib/build/pjlib.vcproj index bc4e2b2c75..afc3441a5c 100644 --- a/pjlib/build/pjlib.vcproj +++ b/pjlib/build/pjlib.vcproj @@ -12645,10 +12645,6 @@ RelativePath="..\include\pj\addr_resolv.h" > - - From 7bd99ecf79b5e4d0cd97c8cde3984dcc8292714e Mon Sep 17 00:00:00 2001 From: bennylp Date: Tue, 9 Jul 2024 11:09:01 +0700 Subject: [PATCH 7/7] Remove pj_fifobuf_unalloc(), improve doc, improve error codes --- pjlib/include/pj/fifobuf.h | 54 +++++++------- pjlib/src/pj/fifobuf.c | 60 +++++++-------- pjlib/src/pjlib-test/fifobuf.c | 130 +++++++++++++++++++++++---------- 3 files changed, 142 insertions(+), 102 deletions(-) diff --git a/pjlib/include/pj/fifobuf.h b/pjlib/include/pj/fifobuf.h index c457960afe..31190a2c65 100644 --- a/pjlib/include/pj/fifobuf.h +++ b/pjlib/include/pj/fifobuf.h @@ -35,7 +35,12 @@ PJ_BEGIN_DECL /** - * FIFO buffer or circular buffer. + * A FIFO buffer provides chunks of memory to the application with first in + * first out policy (or more correctly, first out first in). The fifobuf is + * created by providing it with a fixed buffer. After that, application may + * request chunks of memory from this buffer. When the app is done with a + * chunk of memory, it must return that chunk back to the fifobuf, with the + * requirement that the oldest allocated chunk must be returned first. */ typedef struct pj_fifobuf_t { @@ -45,10 +50,10 @@ typedef struct pj_fifobuf_t /** The end of the buffer */ char *last; - /** The start of empty area in the buffer */ + /** The start of used area in the buffer */ char *ubegin; - /** The end of empty area in the buffer */ + /** The end of used area in the buffer */ char *uend; /** Full flag when ubegin==uend */ @@ -58,11 +63,11 @@ typedef struct pj_fifobuf_t /** - * Initialize the circular buffer by giving it a buffer and size. + * Initialize the fifobuf by giving it a buffer and size. * - * @param fb The fifobuf/circular buffer + * @param fb The fifobuf * @param buffer Buffer to be used to allocate/free chunks of memory from by - * the circular buffer. + * the fifo buffer. * @param size The size of the buffer. */ PJ_DECL(void) pj_fifobuf_init(pj_fifobuf_t *fb, void *buffer, unsigned size); @@ -70,49 +75,40 @@ PJ_DECL(void) pj_fifobuf_init(pj_fifobuf_t *fb, void *buffer, unsigned size); /** * Returns the capacity (initial size) of the buffer. * - * @param fb The fifobuf/circular buffer + * @param fb The fifobuf * * @return Capacity in bytes. */ PJ_DECL(unsigned) pj_fifobuf_capacity(pj_fifobuf_t *fb); /** - * Returns maximum buffer size that can be allocated from the circular buffer. + * Returns maximum size of memory chunk that can be allocated from the buffer. * - * @param fb The fifobuf/circular buffer + * @param fb The fifobuf * - * @return Free size in bytes + * @return Size in bytes */ PJ_DECL(unsigned) pj_fifobuf_available_size(pj_fifobuf_t *fb); /** - * Allocate a buffer from the circular buffer. + * Allocate a chunk of memory from the fifobuf. * - * @param fb The fifobuf/circular buffer + * @param fb The fifobuf * @param size Size to allocate * - * @return Allocated buffer or NULL if the buffer cannot be allocated. + * @return Allocated buffer or NULL if the buffer cannot be allocated */ PJ_DECL(void*) pj_fifobuf_alloc(pj_fifobuf_t *fb, unsigned size); /** - * Free up space used by the last allocated buffer. For example, if you - * allocated ptr0, ptr1, and ptr2, this function is used to free ptr2. - * - * @param fb The fifobuf/circular buffer - * @param buf The buffer to be freed. This is the pointer returned by - * pj_fifobuf_alloc() + * Return the space used by the earliest allocated memory chunk back to the + * fifobuf. For example, if app previously allocated ptr0, ptr1, and ptr2 + * (in that order), then pj_fifobuf_free() can only be called with ptr0 as + * parameter. Subsequent pj_fifobuf_free() must be called with ptr1, and + * the next one with ptr2, and so on. * - * @return PJ_SUCCESS or the appropriate error. - */ -PJ_DECL(pj_status_t) pj_fifobuf_unalloc(pj_fifobuf_t *fb, void *buf); - -/** - * Free up space used by the earliest allocated buffer. For example, if you - * allocated ptr0, ptr1, and ptr2, this function is used to free ptr0. - * - * @param fb The fifobuf/circular buffer - * @param buf The buffer to be freed. This is the pointer returned by + * @param fb The fifobuf + * @param buf Pointer to memory chunk previously returned by * pj_fifobuf_alloc() * * @return PJ_SUCCESS or the appropriate error. diff --git a/pjlib/src/pj/fifobuf.c b/pjlib/src/pj/fifobuf.c index 5acd3220bc..53e5787c10 100644 --- a/pjlib/src/pj/fifobuf.c +++ b/pjlib/src/pj/fifobuf.c @@ -17,6 +17,7 @@ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ #include +#include #include #include #include @@ -100,6 +101,16 @@ PJ_DEF(void*) pj_fifobuf_alloc (pj_fifobuf_t *fifobuf, unsigned size) /* try to allocate from the end part of the fifo */ if (fifobuf->uend >= fifobuf->ubegin) { + /* If we got here, then first <= ubegin <= uend <= last, and + * the the buffer layout is like this: + * + * <--free0---> <--- used --> <-free1-> + * | |#############| | + * ^ ^ ^ ^ + * first ubegin uend last + * + * where the size of free0, used, and/or free1 may be zero. + */ available = (unsigned)(fifobuf->last - fifobuf->uend); if (available >= size+SZ) { char *ptr = fifobuf->uend; @@ -112,13 +123,22 @@ PJ_DEF(void*) pj_fifobuf_alloc (pj_fifobuf_t *fifobuf, unsigned size) ptr += SZ; PJ_LOG(6, (THIS_FILE, - "fifobuf_alloc fifobuf=%p, size=%d: returning %p, p1=%p, p2=%p", + "fifobuf_alloc fifobuf=%p, size=%d: returning %p, p1=%p, p2=%p", fifobuf, size, ptr, fifobuf->ubegin, fifobuf->uend)); return ptr; } } - /* try to allocate from the start part of the fifo */ + /* If we got here, then either there is not enough space in free1 above, + * or the the buffer layout is like this: + * + * <-used0-> <--free--> <- used1 -> + * |#########| |###########| + * ^ ^ ^ ^ + * first uend ubegin last + * + * where the size of used0, used1, and/or free may be zero. + */ start = (fifobuf->uend <= fifobuf->ubegin) ? fifobuf->uend : fifobuf->first; available = (unsigned)(fifobuf->ubegin - start); if (available >= size+SZ) { @@ -141,36 +161,6 @@ PJ_DEF(void*) pj_fifobuf_alloc (pj_fifobuf_t *fifobuf, unsigned size) return NULL; } -PJ_DEF(pj_status_t) pj_fifobuf_unalloc (pj_fifobuf_t *fifobuf, void *buf) -{ - char *ptr = (char*)buf; - char *endptr; - unsigned sz; - - PJ_CHECK_STACK(); - - ptr -= SZ; - sz = get_size(ptr); - - endptr = fifobuf->uend; - if (endptr == fifobuf->first) - endptr = fifobuf->last; - - if (ptr+sz != endptr) { - pj_assert(!"Invalid pointer to undo alloc"); - return -1; - } - - fifobuf->uend = ptr; - fifobuf->full = 0; - - PJ_LOG(6, (THIS_FILE, - "fifobuf_unalloc fifobuf=%p, ptr=%p, size=%d, p1=%p, p2=%p", - fifobuf, buf, sz, fifobuf->ubegin, fifobuf->uend)); - - return 0; -} - PJ_DEF(pj_status_t) pj_fifobuf_free (pj_fifobuf_t *fifobuf, void *buf) { char *ptr = (char*)buf; @@ -182,19 +172,19 @@ PJ_DEF(pj_status_t) pj_fifobuf_free (pj_fifobuf_t *fifobuf, void *buf) ptr -= SZ; if (ptr < fifobuf->first || ptr >= fifobuf->last) { pj_assert(!"Invalid pointer to free"); - return -1; + return PJ_EINVAL; } if (ptr != fifobuf->ubegin && ptr != fifobuf->first) { pj_assert(!"Invalid free() sequence!"); - return -1; + return PJ_EINVAL; } end = (fifobuf->uend > fifobuf->ubegin) ? fifobuf->uend : fifobuf->last; sz = get_size(ptr); if (ptr+sz > end) { pj_assert(!"Invalid size!"); - return -1; + return PJ_EINVAL; } fifobuf->ubegin = ptr + sz; diff --git a/pjlib/src/pjlib-test/fifobuf.c b/pjlib/src/pjlib-test/fifobuf.c index cf1bb9eef8..1d42bf936c 100644 --- a/pjlib/src/pjlib-test/fifobuf.c +++ b/pjlib/src/pjlib-test/fifobuf.c @@ -30,9 +30,13 @@ int dummy_fifobuf_test; #define THIS_FILE "fifobuf.c" +enum { + SZ = sizeof(unsigned), +}; + static int fifobuf_size_test() { - enum { SIZE = 32, SZ=sizeof(unsigned) }; + enum { SIZE = 32 }; char before[8]; char buffer[SIZE]; char after[8]; @@ -61,10 +65,71 @@ static int fifobuf_size_test() return 0; } -int fifobuf_test() +static int fifobuf_rolling_test() +{ + enum { + REPEAT=2048, + N=100, + MIN_SIZE = sizeof(pj_list), + MAX_SIZE = 64, + SIZE=(MIN_SIZE+MAX_SIZE)/2*N, + }; + pj_list chunks; + char buffer[SIZE]; + pj_fifobuf_t fifo; + unsigned rep; + + pj_fifobuf_init(&fifo, buffer, sizeof(buffer)); + pj_list_init(&chunks); + + PJ_TEST_EQ(pj_fifobuf_capacity(&fifo), SIZE-SZ, NULL, return -300); + PJ_TEST_EQ(pj_fifobuf_available_size(&fifo), SIZE-SZ, NULL, return -310); + + pj_srand(0); + + /* Repeat the test */ + for (rep=0; rep n) { + chunk = chunks.next; + pj_list_erase(chunk); + pj_fifobuf_free(&fifo, chunk); + } + } + + while (pj_list_size(&chunks)) { + pj_list *chunk = chunks.next; + pj_list_erase(chunk); + pj_fifobuf_free(&fifo, chunk); + } + + PJ_TEST_EQ(pj_fifobuf_capacity(&fifo), SIZE-SZ, NULL, return -350); + PJ_TEST_EQ(pj_fifobuf_available_size(&fifo), SIZE-SZ, NULL, return -360); + + return 0; +} + +static int fifobuf_misc_test() { enum { SIZE = 1024, MAX_ENTRIES = 128, - MIN_SIZE = 4, MAX_SIZE = 64, SZ = sizeof(unsigned), + MIN_SIZE = 4, MAX_SIZE = 64, LOOP=10000 }; pj_pool_t *pool; pj_fifobuf_t fifo; @@ -73,10 +138,6 @@ int fifobuf_test() void *buffer; int i; - i = fifobuf_size_test(); - if (i != 0) - return i; - pool = pj_pool_create(mem, NULL, SIZE+256, 0, NULL); PJ_TEST_NOT_NULL(pool, NULL, return -10); @@ -108,6 +169,9 @@ int fifobuf_test() PJ_TEST_SUCCESS( pj_fifobuf_free(&fifo, entries[i]), NULL, return -70); } + PJ_TEST_EQ(pj_fifobuf_capacity(&fifo), SIZE-SZ, NULL, return -31); + PJ_TEST_EQ(pj_fifobuf_available_size(&fifo), SIZE-SZ, NULL, return -32); + /* alloc() and free() */ for (i=0; iMIN_SIZE+SZ+MAX_SIZE/2 && count < MAX_ENTRIES;) { - int size = MIN_SIZE+(pj_rand() % MAX_SIZE); - entries[count] = pj_fifobuf_alloc (&fifo, size); - if (entries[count]) { - available = pj_fifobuf_available_size(&fifo); - ++count; - } - } - for (j=0; j