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..afc3441a5c 100644
--- a/pjlib/build/pjlib.vcproj
+++ b/pjlib/build/pjlib.vcproj
@@ -8420,6 +8420,118 @@
/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -12681,6 +12793,10 @@
RelativePath="..\include\pj\unicode.h"
>
+
+
diff --git a/pjlib/build/pjlib.vcxproj b/pjlib/build/pjlib.vcxproj
index 90ffe174f7..5a58c29aa5 100644
--- a/pjlib/build/pjlib.vcxproj
+++ b/pjlib/build/pjlib.vcxproj
@@ -1,4 +1,4 @@
-
+
@@ -1029,6 +1029,7 @@
+
@@ -1100,6 +1101,7 @@
+
diff --git a/pjlib/build/pjlib.vcxproj.filters b/pjlib/build/pjlib.vcxproj.filters
index f4b5682a5d..3a823e52ab 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
@@ -337,6 +340,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/fifobuf.h b/pjlib/include/pj/fifobuf.h
index 26b8fb776e..31190a2c65 100644
--- a/pjlib/include/pj/fifobuf.h
+++ b/pjlib/include/pj/fifobuf.h
@@ -19,25 +19,108 @@
#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
+
+/**
+ * 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
{
- char *first, *last;
- char *ubegin, *uend;
+ /** The start of the buffer */
+ char *first;
+
+ /** The end of the buffer */
+ char *last;
+
+ /** The start of used area in the buffer */
+ char *ubegin;
+
+ /** The end of used 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 fifobuf by giving it a buffer and size.
+ *
+ * @param fb The fifobuf
+ * @param buffer Buffer to be used to allocate/free chunks of memory from by
+ * the fifo 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
+ *
+ * @return Capacity in bytes.
+ */
+PJ_DECL(unsigned) pj_fifobuf_capacity(pj_fifobuf_t *fb);
+
+/**
+ * Returns maximum size of memory chunk that can be allocated from the buffer.
+ *
+ * @param fb The fifobuf
+ *
+ * @return Size in bytes
+ */
+PJ_DECL(unsigned) pj_fifobuf_available_size(pj_fifobuf_t *fb);
+
+/**
+ * Allocate a chunk of memory from the fifobuf.
+ *
+ * @param fb The fifobuf
+ * @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);
+
+/**
+ * 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.
+ *
+ * @param fb The fifobuf
+ * @param buf Pointer to memory chunk previously 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
new file mode 100644
index 0000000000..c11b252ee5
--- /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)==0) { \
+ const char *tmp_reason_ = err_reason; \
+ const char *sep0_ = (tmp_reason_ ? " (": ""); \
+ const char *sep1_ = (tmp_reason_ ? ")": ""); \
+ if (!tmp_reason_) tmp_reason_=""; \
+ PJ_LOG(1,(THIS_FILE, "Test \"%s\" != 0 fails in " \
+ "%s:%d%s%s%s", \
+ #expr, THIS_FILE,__LINE__,sep0_, \
+ tmp_reason_,sep1_));\
+ err_action; \
+ } \
+ }
+
+/**
+ * Generic check for binary operation. If the check fails, informative error
+ * message will be displayed, and the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param op The operator
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_BINARY_OP(expr0, op, expr1, err_reason, err_action) \
+ { \
+ long tmp_value0_ = (long)(expr0); \
+ long tmp_value1_ = (long)(expr1); \
+ if (!(tmp_value0_ op tmp_value1_)) { \
+ const char *tmp_reason_ = err_reason; \
+ const char *sep0_ = (tmp_reason_ ? " (": ""); \
+ const char *sep1_ = (tmp_reason_ ? ")": ""); \
+ if (!tmp_reason_) tmp_reason_=""; \
+ PJ_LOG(1,(THIS_FILE, "Test \"%s\" (value=%ld) " #op \
+ " \"%s\" (value=%ld) fails in %s:%d%s%s%s", \
+ #expr0, tmp_value0_, #expr1, tmp_value1_, \
+ THIS_FILE, __LINE__, \
+ sep0_, tmp_reason_, sep1_)); \
+ err_action; \
+ } \
+ }
+
+/**
+ * Generic check for (PJ) string comparison operation. If the check fails,
+ * informative error message will be displayed, and the code in err_action
+ * will be executed.
+ *
+ * @param str_op The string operation (e.g. pj_strcmp)
+ * @param ps0 Pointer to first string
+ * @param ps1 Pointer to second string
+ * @param res_op Operator to compare result (e.g. ==)
+ * @param res Expected return value of str_op(&s0, &s1)
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_STR_OP(str_op, ps0, ps1, res_op, res, err_reason, err_action) \
+ { \
+ int result__ = str_op(ps0, ps1); \
+ if (!(result__ res_op res)) { \
+ const char *fn_name = #str_op; \
+ const char *tmp_reason_ = err_reason; \
+ const char *sep0_ = (tmp_reason_ ? " (": ""); \
+ const char *sep1_ = (tmp_reason_ ? ")": ""); \
+ if (!tmp_reason_) tmp_reason_=""; \
+ PJ_LOG(1,(THIS_FILE, "Test %s(\"%.*s\", \"%.*s\")%s%d" \
+ " fails (%s result=%d) in %s:%d%s%s%s", \
+ fn_name, (int)ps0->slen, ps0->ptr, \
+ (int)ps1->slen, ps1->ptr, #res_op, res, \
+ fn_name, result__, \
+ THIS_FILE, __LINE__, \
+ sep0_, tmp_reason_, sep1_)); \
+ err_action; \
+ } \
+ }
+
+/**
+ * Check that an expression is PJ_SUCCESS. If the check fails, error message
+ * explaining the error code will be displayed, and the code in err_action
+ * will be executed.
+ *
+ * @param expr The expression to check
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_SUCCESS(expr, err_reason, err_action) \
+ { \
+ pj_status_t tmp_status_ = (expr); \
+ if (tmp_status_ != PJ_SUCCESS) { \
+ char errbuf[80]; \
+ const char *tmp_reason_ = err_reason; \
+ const char *sep0_ = (tmp_reason_ ? " (": ""); \
+ const char *sep1_ = (tmp_reason_ ? ")": ""); \
+ if (!tmp_reason_) tmp_reason_=""; \
+ pj_strerror(tmp_status_, errbuf, sizeof(errbuf)); \
+ PJ_LOG(1,(THIS_FILE, "\"%s\" fails in %s:%d, " \
+ "status=%d (%s)%s%s%s", \
+ #expr, THIS_FILE, __LINE__, tmp_status_,errbuf,\
+ sep0_, tmp_reason_, sep1_)); \
+ err_action; \
+ } \
+ }
+
+/**
+ * Alias for PJ_TEST_NON_ZERO()
+ */
+#define PJ_TEST_TRUE(expr, err_reason, err_action) \
+ PJ_TEST_NON_ZERO(expr, err_reason, err_action)
+
+/**
+ * Alias for PJ_TEST_NON_ZERO()
+ */
+#define PJ_TEST_NOT_NULL(expr, err_reason, err_action) \
+ PJ_TEST_NON_ZERO(expr, err_reason, err_action)
+
+/**
+ * Check that expr0 equals expr1.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_EQ(expr0, expr1, err_reason, err_action) \
+ PJ_TEST_BINARY_OP(expr0, ==, expr1, err_reason, err_action)
+
+/**
+ * Check that expr0 does not equal expr1.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_NEQ(expr0, expr1, err_reason, err_action) \
+ PJ_TEST_BINARY_OP(expr0, !=, expr1, err_reason, err_action)
+
+/**
+ * Check that expr0 is less than expr1.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_LT(expr0, expr1, err_reason, err_action) \
+ PJ_TEST_BINARY_OP(expr0, <, expr1, err_reason, err_action)
+
+/**
+ * Check that expr0 is less than or equal to expr1.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_LTE(expr0, expr1, err_reason, err_action) \
+ PJ_TEST_BINARY_OP(expr0, <=, expr1, err_reason, err_action)
+
+/**
+ * Check that expr0 is greater than expr1.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_GT(expr0, expr1, err_reason, err_action) \
+ PJ_TEST_BINARY_OP(expr0, >, expr1, err_reason, err_action)
+
+/**
+ * Check that expr0 is greater than or equal to expr1.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param expr0 First expression
+ * @param expr1 Second expression
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_GTE(expr0, expr1, err_reason, err_action) \
+ PJ_TEST_BINARY_OP(expr0, >=, expr1, err_reason, err_action)
+
+
+/**
+ * Check string comparison result.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param ps0 Pointer to first string
+ * @param ps1 Pointer to second string
+ * @param res_op Operator to compare result (e.g. ==, <, >)
+ * @param exp_result Expected result (e.g. zero for equal string)
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_STRCMP(ps0, ps1, res_op, exp_result, err_reason, err_action) \
+ PJ_TEST_STR_OP(pj_strcmp, ps0, ps1, res_op, exp_result, \
+ err_reason, err_action)
+
+/**
+ * Check case-insensitive string comparison result.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param ps0 Pointer to first string
+ * @param ps1 Pointer to second string
+ * @param res_op Operator to compare result (e.g. ==, <, >)
+ * @param exp_result Expected result (e.g. zero for equal)
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_STRICMP(ps0, ps1, res_op, exp_result, err_reason, err_action) \
+ PJ_TEST_STR_OP(pj_stricmp, ps0, ps1, res_op, exp_result, \
+ err_reason, err_action)
+
+/**
+ * Check that two strings are equal.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param ps0 Pointer to first string
+ * @param ps1 Pointer to second string
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_STREQ(ps0, ps1, err_reason, err_action) \
+ PJ_TEST_STRCMP(ps0, ps1, ==, 0, err_reason, err_action)
+
+/**
+ * Check that two strings are not equal.
+ * If the check fails, informative error message will be displayed and
+ * the code in err_action will be executed.
+ *
+ * @param ps0 Pointer to first string
+ * @param ps1 Pointer to second string
+ * @param err_reason NULL or extra text to display when the check fails
+ * @param err_action Action to perform when the check fails
+ */
+#define PJ_TEST_STRNEQ(ps0, ps1, err_reason, err_action) \
+ PJ_TEST_STRCMP(ps0, ps1, !=, 0, err_reason, err_action)
+
+
+/**
+ * Bitwise constants that can be used in test case flags (see
+ * pj_test_case_init()).
+ */
+typedef enum pj_test_case_flag
+{
+ /**
+ * Do not allow other test cases to run while this test case is running.
+ * Note this only makes sense for test runners that support worker
+ * threads. Basic runner will always run test cases serially.
+ */
+ PJ_TEST_EXCLUSIVE = 1,
+
+ /**
+ * Specify that the test function must be called without argument.
+ * This is mainly for backward compatibility with existing PJ test
+ * functions which take no argument.
+ */
+ PJ_TEST_FUNC_NO_ARG = 2,
+
+ /**
+ * Write the original log at the time it is called instead of pooling
+ * the logs to be printed after all tests finish.
+ */
+ PJ_TEST_LOG_NO_CACHE = 4,
+
+ /**
+ * Keep this test case in front of the list when shuffling the test
+ * cases.
+ */
+ PJ_TEST_KEEP_FIRST = 8,
+
+ /**
+ * Keep this test case in last in the list when shuffling the test
+ * cases.
+ */
+ PJ_TEST_KEEP_LAST = 16,
+
+} pj_test_case_flag;
+
+
+/**
+ * An internal structure to represent one logging item that is saved
+ * inside pj_test_case.
+ */
+typedef struct pj_test_log_item
+{
+ PJ_DECL_LIST_MEMBER(struct pj_test_log_item);
+
+ /** level */
+ int level;
+
+ /** len */
+ int len;
+
+ /** The log message. The actual buffer is longer. */
+ char msg[1];
+
+} pj_test_log_item;
+
+
+/** Forward declaration of test runner */
+typedef struct pj_test_runner pj_test_runner;
+
+
+/**
+ * Additional parameters for creating test case. Use
+ * pj_test_case_param_default() to initialize this structure.
+ */
+typedef struct pj_test_case_param
+{
+ /**
+ * Custom log level for this test case, to filter out logs that are more
+ * detail than this level. Default is 6, meaning it will accept all log
+ * levels.
+ */
+ int log_level;
+
+} pj_test_case_param;
+
+
+/**
+ * A test case is unit-test object to perform test against a user defined
+ * function.
+ */
+typedef struct pj_test_case
+{
+ PJ_DECL_LIST_MEMBER(struct pj_test_case);
+
+ /** Test name */
+ char obj_name[PJ_MAX_OBJ_NAME];
+
+ /**
+ * The test function to be called to perform the test. By convention, the
+ * function must return zero for the test to be considered successful,
+ * non-zero on failure, and MUST NEVER return PJ_EPENDING, otherwise the
+ * return value will be silently changed to -12345.
+ */
+ int (*test_func)(void*);
+
+ /** Argument to be passed to the test function */
+ void *arg;
+
+ /** Flags, combination of pj_test_flag constants */
+ unsigned flags;
+
+ /** Circular buffer for logging */
+ pj_fifobuf_t fb;
+
+ /** Parameters */
+ pj_test_case_param prm;
+
+ /**
+ * The return value of the test function. Zero indicates success. Initially
+ * the value is PJ_EPENDING before the test is run.
+ */
+ int result;
+
+ /** List of saved logging messages */
+ pj_test_log_item logs;
+
+ /** Pointer to the runner running this test case */
+ pj_test_runner *runner;
+
+ /** Start time */
+ pj_timestamp start_time;
+
+ /** End time */
+ pj_timestamp end_time;
+
+} pj_test_case;
+
+
+/**
+ * Test suite is a collection of test cases.
+ */
+typedef struct pj_test_suite
+{
+ /** List of tests */
+ pj_test_case tests;
+
+ /** Start time */
+ pj_timestamp start_time;
+
+ /** End time */
+ pj_timestamp end_time;
+
+} pj_test_suite;
+
+
+/**
+ * Test statistics. Collect the statistics after the test runner finishes
+ * with pj_test_get_stat().
+ */
+typedef struct pj_test_stat
+{
+ /** Total duration */
+ pj_time_val duration;
+
+ /** Total number of tests in the test suite */
+ unsigned ntests;
+
+ /** Number of tests run */
+ unsigned nruns;
+
+ /** Number of failed tests */
+ unsigned nfailed;
+
+ /**
+ * Array of failed test names. Be careful that the number elements are
+ * fixed, hence it may not be able to store all failed test names (in case
+ * nfailed is more than the capacity, not all failed test names will be
+ * stored, hence be careful in the loop).
+ */
+ const char *failed_names[32];
+
+} pj_test_stat;
+
+
+/**
+ * Test runner parameters. Use pj_test_runner_param_default() to initialize
+ * this structure.
+ */
+typedef struct pj_test_runner_param
+{
+ /** Stop the test on error (default: false) */
+ pj_bool_t stop_on_error;
+
+ /** Number of worker threads. Set to zero to disable parallel testings.
+ * Only applicable to test text runner. Default is 1 if multithreading
+ * is available.
+ */
+ unsigned nthreads;
+
+ /**
+ * 0: only display test name and result after test completion (default)
+ * 1: display test name test when starting and finishing a test
+ */
+ unsigned verbosity;
+
+} pj_test_runner_param;
+
+
+/**
+ * This structure represents a test runner. Currently there are two types
+ * of test runners, the basic runner and text runner. The basic runner is the
+ * simplest test runner that can be used without pool and threads, and can be
+ * created with pj_test_init_basic_runner(). The text runner is more powerful
+ * since it supports worker threads, and it is mostly suitable for console
+ * based environments. It is created with pj_test_create_text_runner().
+ */
+struct pj_test_runner
+{
+ /** Parameters */
+ pj_test_runner_param prm;
+
+ /** The test suite being run */
+ pj_test_suite *suite;
+
+ /** Saving the original log writer */
+ pj_log_func *orig_log_writer;
+
+ /** Number of tests */
+ unsigned ntests;
+
+ /** Number of completed tests */
+ unsigned nruns;
+
+ /** Stopping */
+ pj_bool_t stopping;
+
+ /** main method */
+ void (*main)(pj_test_runner*);
+
+ /** callback when test case completes. Default is to write to log */
+ void (*on_test_complete)(pj_test_runner*, pj_test_case*);
+
+ /** destroy method */
+ void (*destroy)(pj_test_runner*);
+};
+
+/** Option to select tests (e.g. in pj_test_dump_log_messages()) */
+typedef enum pj_test_select_tests
+{
+ /** Select no test*/
+ PJ_TEST_NO_TEST = 0,
+
+ /** Select only failed tests */
+ PJ_TEST_FAILED_TESTS = 1,
+
+ /** Select only successful tests */
+ PJ_TEST_SUCCESSFUL_TESTS = 2,
+
+ /** Select all tests*/
+ PJ_TEST_ALL_TESTS = 3,
+
+ /** No header/footer separator */
+ PJ_TEST_NO_HEADER_FOOTER = 4,
+
+} pj_test_select_tests;
+
+
+/**
+ * Initialize test suite.
+ *
+ * @param suite The test suite
+ */
+PJ_DECL(void) pj_test_suite_init(pj_test_suite *suite);
+
+/**
+ * Initialize pj_test_case_param with default values. If app only uses
+ * default values in params, alternatively it doesn't need to use param
+ * at all and just specify NULL in pj_test_case_init().
+ *
+ * @param prm The parameter.
+ */
+PJ_DECL(void) pj_test_case_param_default(pj_test_case_param *prm);
+
+/**
+ * Initialize test case.
+ *
+ * @param tc The test case
+ * @param obj_name Name that will appear as test name/title
+ * @param flags Bitwise of pj_test_case_flag to control threading,
+ * function calling, logging, etc.
+ * @param test_func The test function to be called to perform the test.
+ * By convention, the function must return zero for the
+ * test to be considered successful, non-zero on failure,
+ * and MUST NEVER return PJ_EPENDING, otherwise the
+ * return value will be silently changed to -12345.
+ * @param arg Argument to give to the test function
+ * @param fifobuf_buf Buffer for saving the logs, if required.
+ * @param buf_size Size of the buffer for saving the logs.
+ * @param prm Optional additional settings for the test case or NULL
+ */
+PJ_DECL(void) pj_test_case_init(pj_test_case *tc,
+ const char *obj_name,
+ unsigned flags,
+ int (*test_func)(void*),
+ void *arg,
+ void *fifobuf_buf,
+ unsigned buf_size,
+ const pj_test_case_param *prm);
+
+/**
+ * Add test case to test suite. A test case can only be added to one suite.
+ *
+ * @param suite The test suite
+ * @param tc The test case
+ */
+PJ_DECL(void) pj_test_suite_add_case(pj_test_suite *suite, pj_test_case *tc);
+
+/**
+ * Shuffle the tests.
+ *
+ * @param suite The test suite
+ * @param seed Optional random seed to use, only if the value is
+ * greater than or equal to zero. It is recommended
+ * to set this value to make the test reproducible.
+ */
+PJ_DECL(void) pj_test_suite_shuffle(pj_test_suite *suite, int seed);
+
+/**
+ * Initialize parameters with reasonable default values. This usually means
+ * using one worker thread if threading is enabled, and zero worker thread
+ * (i.e. only use the main thread) otherwise.
+ *
+ * @param prm Test runner parameter
+ */
+PJ_DECL(void) pj_test_runner_param_default(pj_test_runner_param *prm);
+
+/**
+ * Initialize a basic test runner. A basic runner can be declared in the stack
+ * and it does not require pool nor multithreading.
+ *
+ * @param runner The runner.
+ * @param prm Runner params, or NULL to accept default values.
+ */
+PJ_DECL(void) pj_test_init_basic_runner(pj_test_runner *runner,
+ const pj_test_runner_param *prm);
+
+/**
+ * Create console based test runner.
+ *
+ * @param pool The pool to use to allocate memory
+ * @param prm Test runner parameter, or NULL for default values.
+ * @param p_runner Pointer to receive the text runner
+ *
+ * @return PJ_SUCCESS or the appropriate error code.
+ */
+PJ_DECL(pj_status_t) pj_test_create_text_runner(
+ pj_pool_t *pool,
+ const pj_test_runner_param *prm,
+ pj_test_runner **p_runner);
+
+/**
+ * Run test suite with the specified runner.
+ *
+ * @param runner The test runner
+ * @param suite The test suite
+ */
+PJ_DECL(void) pj_test_run(pj_test_runner *runner,
+ pj_test_suite *suite);
+
+/**
+ * This is a crude test to detect if thread is currently running under
+ * a test. It is mainly used to prevent nested unit testing.
+ *
+ * @return PJ_TRUE if we are currently running in the context of a test case
+ * being run.
+ */
+PJ_DECL(pj_bool_t) pj_test_is_under_test(void);
+
+/**
+ * Get the test statistics after the run completes. The test suite and
+ * test cases instances must be kept alive in order to get and access the
+ * statistics or log messages.
+ *
+ * @param suite The test suite
+ * @param stat The test statistics result.
+ */
+PJ_DECL(void) pj_test_get_stat(const pj_test_suite *suite, pj_test_stat *stat);
+
+/**
+ * Display statistics to the log.
+ *
+ * @param stat The test statistics result.
+ */
+PJ_DECL(void) pj_test_display_stat(const pj_test_stat *stat,
+ const char *test_name,
+ const char *log_sender);
+
+/**
+ * Display previously saved log messages in the test cases to logging.
+ * Note that log messages emited during test case's run are only saved
+ * when fifobuf of the test case is configured with a suitable buffer.
+ * Also note that the test suite and test cases instances must be kept alive
+ * in order to get and access the statistics or log messages.
+ *
+ * @param suite The test suite
+ * @param flags Select which test logs to display by choosing
+ * from pj_test_select_tests.
+ */
+PJ_DECL(void) pj_test_display_log_messages(const pj_test_suite *suite,
+ unsigned flags);
+
+/**
+ * Destroy the runner. Runner may be destroyed right after it is run,
+ * but the test suite and test cases instances must be kept alive in order
+ * to get the statistics or log messages.
+ *
+ * @param runner The test runner.
+ */
+PJ_DECL(void) pj_test_runner_destroy(pj_test_runner *runner);
+
+
+/**
+ * Macro to control how long worker thread should sleep waiting for next
+ * ready test.
+ */
+#ifndef PJ_TEST_THREAD_WAIT_MSEC
+# define PJ_TEST_THREAD_WAIT_MSEC 100
+#endif
+
+PJ_END_DECL
+
+/**
+ * @} // PJ_UNITTEST group
+ */
+
+#endif /* __PJ_UNITTEST_H__ */
+
diff --git a/pjlib/include/pjlib.h b/pjlib/include/pjlib.h
index dfb9e53f24..1bc591be51 100644
--- a/pjlib/include/pjlib.h
+++ b/pjlib/include/pjlib.h
@@ -55,6 +55,7 @@
#include
#include
#include
+#include
#include
diff --git a/pjlib/src/pj/fifobuf.c b/pjlib/src/pj/fifobuf.c
index 464f3114cc..53e5787c10 100644
--- a/pjlib/src/pj/fifobuf.c
+++ b/pjlib/src/pj/fifobuf.c
@@ -17,14 +17,29 @@
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include
+#include
#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 +51,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->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;
@@ -79,17 +119,26 @@ PJ_DEF(void*) pj_fifobuf_alloc (pj_fifobuf_t *fifobuf, unsigned size)
fifobuf->uend = 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,
- "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) {
@@ -97,7 +146,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,
@@ -112,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 = *(unsigned*)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;
@@ -153,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 = *(unsigned*)ptr;
+ 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/pj/unittest.c b/pjlib/src/pj/unittest.c
new file mode 100644
index 0000000000..d8ac75132d
--- /dev/null
+++ b/pjlib/src/pj/unittest.c
@@ -0,0 +1,824 @@
+/*
+ * 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 ;
+ unsigned i;
+
+ tprm.runner = runner;
+ tprm.tid = 0;
+
+ 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/fifobuf.c b/pjlib/src/pjlib-test/fifobuf.c
index fb38460aa4..1d42bf936c 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,151 @@ int dummy_fifobuf_test;
#include
-int fifobuf_test()
+#define THIS_FILE "fifobuf.c"
+
+enum {
+ SZ = sizeof(unsigned),
+};
+
+static int fifobuf_size_test()
+{
+ enum { SIZE = 32 };
+ 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;
+}
+
+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,
+ MIN_SIZE = 4, MAX_SIZE = 64,
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;
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);
+ }
+
+ 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; i=MIN_SIZE+4 && count < MAX_ENTRIES;) {
- int size = MIN_SIZE+(pj_rand() % MAX_SIZE);
- entries[count] = pj_fifobuf_alloc (&fifo, size);
- if (entries[count]) {
- available -= (size+4);
- ++count;
- }
- }
- for (j=0; j
+
+
+#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)
+{
+ int max_len = (log_buffer+sizeof(log_buffer))-log_buffer_ptr-1;
+
+ PJ_UNUSED_ARG(level);
+
+ /* 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