]> git.ipfire.org Git - thirdparty/asterisk.git/commitdiff
Backport unit test API to 1.4.
authorRussell Bryant <russell@russellbryant.com>
Mon, 28 Jun 2010 18:34:18 +0000 (18:34 +0000)
committerRussell Bryant <russell@russellbryant.com>
Mon, 28 Jun 2010 18:34:18 +0000 (18:34 +0000)
Review: https://reviewboard.asterisk.org/r/750/

git-svn-id: https://origsvn.digium.com/svn/asterisk/branches/1.4@272878 65c4cc65-6c06-0410-ace0-fbb531ad65f3

Makefile
build_tools/cflags-devmode.xml
include/asterisk.h
include/asterisk/linkedlists.h
include/asterisk/test.h [new file with mode: 0644]
main/Makefile
main/asterisk.c
main/test.c [new file with mode: 0644]
tests/Makefile [new file with mode: 0644]
tests/test_skel.c [new file with mode: 0644]

index a34c9f86e74187ee525cbd72d922901141e5a1a5..148ff43daa2894e7a861d5fc408ae3810cdd8912 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -265,7 +265,7 @@ endif
 
 _ASTCFLAGS+=$(BUSYDETECT)$(OPTIONS)
 
-MOD_SUBDIRS:=res channels pbx apps codecs formats cdr funcs main
+MOD_SUBDIRS:=res channels pbx apps codecs formats cdr funcs tests main
 OTHER_SUBDIRS:=utils agi
 SUBDIRS:=$(OTHER_SUBDIRS) $(MOD_SUBDIRS)
 SUBDIRS_INSTALL:=$(SUBDIRS:%=%-install)
index 9ce9a68d51f62dc16f42a93de5d37170a4a5a17f..6e7e645c9b7a9cde1a7bd38b9428012405f15913 100644 (file)
@@ -12,4 +12,6 @@
                </member>
                <member name="MTX_PROFILE" displayname="Enable Code Profiling Using TSC Counters">
                </member>
+               <member name="TEST_FRAMEWORK" displayname="Enable Test Framework API">
+               </member>
        </category>
index 1f6c2c723d18a17db0e29d05f899398acdcfe969..e54b9b6a80d4a9b0db769303aca97086b9a10e47 100644 (file)
@@ -108,6 +108,7 @@ void threadstorage_init(void);                      /*!< Provided by threadstorage.c */
 int astobj2_init(void);                                /*! Provided by astobj2.c */
 void ast_autoservice_init(void);    /*!< Provided by autoservice.c */
 int ast_fd_init(void);                         /*!< Provided by astfd.c */
+int ast_test_init(void);                        /*!< Provided by test.c */
 
 /* Many headers need 'ast_channel' to be defined */
 struct ast_channel;
index a41ba857b6c9c1d07f0b708b59ecfef55ba697bd..6810deea81a1f5cd98df1ebca45d1c561ff9b0a9 100644 (file)
@@ -449,6 +449,7 @@ struct {                                                            \
   \li AST_LIST_INSERT_AFTER()
   \li AST_LIST_INSERT_HEAD()
   \li AST_LIST_INSERT_TAIL()
+  \li AST_LIST_INSERT_SORTALPHA()
 */
 #define AST_LIST_TRAVERSE(head,var,field)                              \
        for((var) = (head)->first; (var); (var) = (var)->field.next)
@@ -680,6 +681,35 @@ struct {                                                           \
 
 #define AST_RWLIST_INSERT_TAIL AST_LIST_INSERT_TAIL
 
+/*!
+ * \brief Inserts a list entry into a alphabetically sorted list
+ * \param head Pointer to the list head structure
+ * \param elm Pointer to the entry to be inserted
+ * \param field Name of the list entry field (declared using AST_LIST_ENTRY())
+ * \param sortfield Name of the field on which the list is sorted
+ */
+#define AST_LIST_INSERT_SORTALPHA(head, elm, field, sortfield) do { \
+       if (!(head)->first) {                                           \
+               (head)->first = (elm);                                      \
+               (head)->last = (elm);                                       \
+       } else {                                                        \
+               typeof((head)->first) cur = (head)->first, prev = NULL;     \
+               while (cur && strcmp(cur->sortfield, elm->sortfield) < 0) { \
+                       prev = cur;                                             \
+                       cur = cur->field.next;                                  \
+               }                                                           \
+               if (!prev) {                                                \
+                       AST_LIST_INSERT_HEAD(head, elm, field);                 \
+               } else if (!cur) {                                          \
+                       AST_LIST_INSERT_TAIL(head, elm, field);                 \
+               } else {                                                    \
+                       AST_LIST_INSERT_AFTER(head, prev, elm, field);          \
+               }                                                           \
+       }                                                               \
+} while (0)
+
+#define AST_RWLIST_INSERT_SORTALPHA    AST_LIST_INSERT_SORTALPHA
+
 /*!
   \brief Appends a whole list to the tail of a list.
   \param head This is a pointer to the list head structure
diff --git a/include/asterisk/test.h b/include/asterisk/test.h
new file mode 100644 (file)
index 0000000..f97df80
--- /dev/null
@@ -0,0 +1,215 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2009-2010, Digium, Inc.
+ *
+ * David Vossel <dvossel@digium.com>
+ * Russell Bryant <russell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*!
+ * \file
+ * \brief Test Framework API
+ *
+ * For an overview on how to use the test API, see \ref AstUnitTestAPI
+ *
+ * \author David Vossel <dvossel@digium.com>
+ * \author Russell Bryant <russell@digium.com>
+ */
+
+#ifndef _AST_TEST_H_
+#define _AST_TEST_H_
+
+#ifdef TEST_FRAMEWORK
+#include "asterisk/cli.h"
+#include "asterisk/strings.h"
+#endif
+
+/*! 
+
+\page AstUnitTestAPI Asterisk Unit Test API
+
+\section UnitTestAPIUsage How to Use the Unit Test API
+
+\subsection DefineTest Define a Test
+
+   Create a callback function for the test using the AST_TEST_DEFINE macro.
+
+   Each defined test has three arguments avaliable to it's test code.
+       \param struct ast_test_info *info
+       \param enum ast_test_command cmd
+       \param struct ast_test *test
+
+   While these arguments are not visible they are passed to every test function
+   defined using the AST_TEST_DEFINE macro.
+
+   Below is an example of how to define and write a test function.
+
+\code
+   AST_TEST_DEFINE(sample_test_cb) \\The name of the callback function
+   {                               \\The the function's body 
+      switch (cmd) {
+      case TEST_INIT:
+          info->name = "sample_test";
+          info->category = "main/test/";
+          info->summary = "sample test for example purpose";
+          info->description = "This demonstrates how to initialize a test function";
+
+          return AST_TEST_NOT_RUN;
+      case TEST_EXECUTE:
+          break;
+      }
+      \test code
+      .
+      .
+      .
+      if (fail) {                 \\ the following is just some example logic
+          ast_test_status_update(test, "an error occured because...");
+          res = AST_RESULT_FAIL;
+      } else {
+          res = AST_RESULT_PASS
+      }
+      return res;                 \\ result must be of type enum ast_test_result_state
+   }
+\endcode
+
+      Details of the test execution, especially failure details, should be provided
+      by using the ast_test_status_update() function.
+
+\subsection RegisterTest Register a Test 
+
+   Register the test using the AST_TEST_REGISTER macro.
+
+   AST_TEST_REGISTER uses the callback function to retrieve all the information
+   pertaining to a test, so the callback function is the only argument required
+   for registering a test.
+
+   AST_TEST_REGISTER(sample_test_cb);    \\ Test callback function defined by AST_TEST_DEFINE
+
+   Tests are unregestered by using the AST_TEST_UNREGISTER macro.
+
+   AST_TEST_UNREGISTER(sample_test_cb);  \\ Remove a registered test by callback function
+
+\subsection ExecuteTest Execute a Test
+
+   Execute and generate test results via CLI commands
+
+   CLI Examples:
+\code
+   'test show registered all'  will show every registered test.
+   'test execute all'          will execute every registered test.
+   'test show results all'     will show detailed results for ever executed test
+   'test generate results xml' will generate a test report in xml format
+   'test generate results txt' will generate a test report in txt format
+\endcode
+*/
+
+/*! Macros used for defining and registering a test */
+#ifdef TEST_FRAMEWORK
+
+#define AST_TEST_DEFINE(hdr) static enum ast_test_result_state hdr(struct ast_test_info *info, enum ast_test_command cmd, struct ast_test *test)
+#define AST_TEST_REGISTER(cb) ast_test_register(cb)
+#define AST_TEST_UNREGISTER(cb) ast_test_unregister(cb)
+
+#else
+
+#define AST_TEST_DEFINE(hdr) static enum ast_test_result_state attribute_unused hdr(struct ast_test_info *info, enum ast_test_command cmd, struct ast_test *test)
+#define AST_TEST_REGISTER(cb)
+#define AST_TEST_UNREGISTER(cb)
+#define ast_test_status_update(a,b,c...)
+
+#endif
+
+enum ast_test_result_state {
+       AST_TEST_NOT_RUN,
+       AST_TEST_PASS,
+       AST_TEST_FAIL,
+};
+
+enum ast_test_command {
+       TEST_INIT,
+       TEST_EXECUTE,
+};
+
+/*!
+ * \brief An Asterisk unit test.
+ *
+ * This is an opaque type.
+ */
+struct ast_test;
+
+/*!
+ * \brief Contains all the initialization information required to store a new test definition
+ */
+struct ast_test_info {
+       /*! \brief name of test, unique to category */
+       const char *name;
+       /*! \brief test category */
+       const char *category;
+       /*! \brief optional short summary of test */
+       const char *summary;
+       /*! \brief optional brief detailed description of test */
+       const char *description;
+};
+
+#ifdef TEST_FRAMEWORK
+/*!
+ * \brief Generic test callback function
+ *
+ * \param error buffer string for failure results
+ *
+ * \retval AST_TEST_PASS for pass
+ * \retval AST_TEST_FAIL for failure
+ */
+typedef enum ast_test_result_state (ast_test_cb_t)(struct ast_test_info *info,
+               enum ast_test_command cmd, struct ast_test *test);
+
+/*!
+ * \brief unregisters a test with the test framework
+ *
+ * \param test callback function (required)
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_test_unregister(ast_test_cb_t *cb);
+
+/*!
+ * \brief registers a test with the test framework
+ *
+ * \param test callback function (required)
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_test_register(ast_test_cb_t *cb);
+
+/*!
+ * \brief update test's status during testing.
+ *
+ * \param test currently executing test
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int __ast_test_status_update(const char *file, const char *func, int line,
+               struct ast_test *test, const char *fmt, ...)
+               __attribute__((format(printf, 5, 6)));
+
+/*!
+ * \ref __ast_test_status_update()
+ */
+#define ast_test_status_update(t, f, ...) __ast_test_status_update(__FILE__, __PRETTY_FUNCTION__, __LINE__, (t), (f), ## __VA_ARGS__)
+
+#endif /* TEST_FRAMEWORK */
+#endif /* _AST_TEST_H */
index f72ac0780c6cd932c67935819c0e29136b236553..e55731f17c809cfbb45918be1f7b6c116de26194 100644 (file)
@@ -27,7 +27,7 @@ OBJS= io.o sched.o logger.o frame.o loader.o config.o channel.o \
        netsock.o slinfactory.o ast_expr2.o ast_expr2f.o \
        cryptostub.o sha1.o http.o fixedjitterbuf.o abstract_jb.o \
        strcompat.o threadstorage.o dial.o astobj2.o global_datastores.o \
-       audiohook.o poll.o
+       audiohook.o poll.o test.o
 
 # we need to link in the objects statically, not as a library, because
 # otherwise modules will not have them available if none of the static
index db8271735ddf04340ec76b6cd6bc8e29afcc6212..1a8ddebd061cd8e512f329fa1ff1079f3df4d12d 100644 (file)
@@ -3056,6 +3056,13 @@ int main(int argc, char *argv[])
        }
 #endif
 
+#ifdef TEST_FRAMEWORK
+       if (ast_test_init()) {
+               printf("%s", term_quit());
+               exit(1);
+       }
+#endif
+
        ast_makesocket();
        sigemptyset(&sigs);
        sigaddset(&sigs, SIGHUP);
diff --git a/main/test.c b/main/test.c
new file mode 100644 (file)
index 0000000..1fada7f
--- /dev/null
@@ -0,0 +1,899 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2009-2010, Digium, Inc.
+ *
+ * David Vossel <dvossel@digium.com>
+ * Russell Bryant <russell@digium.com>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*!
+ * \file
+ * \brief Unit Test Framework
+ *
+ * \author David Vossel <dvossel@digium.com>
+ * \author Russell Bryant <russell@digium.com>
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$");
+
+#ifdef TEST_FRAMEWORK
+#include "asterisk/test.h"
+#include "asterisk/logger.h"
+#include "asterisk/linkedlists.h"
+#include "asterisk/utils.h"
+#include "asterisk/cli.h"
+#include "asterisk/term.h"
+#include "asterisk/version.h"
+#include "asterisk/paths.h"
+#include "asterisk/time.h"
+#include "asterisk/threadstorage.h"
+
+/*! This array corresponds to the values defined in the ast_test_state enum */
+static const char * const test_result2str[] = {
+       [AST_TEST_NOT_RUN] = "NOT RUN",
+       [AST_TEST_PASS]    = "PASS",
+       [AST_TEST_FAIL]    = "FAIL",
+};
+
+/*! holds all the information pertaining to a single defined test */
+struct ast_test {
+       struct ast_test_info info;        /*!< holds test callback information */
+       /*!
+        * \brief Test defined status output from last execution
+        */
+       struct ast_dynamic_str *status_str;
+       /*!
+        * \brief CLI arguments, if tests being run from the CLI
+        *
+        * If this is set, status updates from the tests will be sent to the
+        * CLI in addition to being saved off in status_str.
+        */
+       int cli_fd;
+       enum ast_test_result_state state; /*!< current test state */
+       unsigned int time;                /*!< time in ms test took */
+       ast_test_cb_t *cb;                /*!< test callback function */
+       AST_LIST_ENTRY(ast_test) entry;
+};
+
+/*! global structure containing both total and last test execution results */
+static struct ast_test_execute_results {
+       unsigned int total_tests;  /*!< total number of tests, regardless if they have been executed or not */
+       unsigned int total_passed; /*!< total number of executed tests passed */
+       unsigned int total_failed; /*!< total number of executed tests failed */
+       unsigned int total_time;   /*!< total time of all executed tests */
+       unsigned int last_passed;  /*!< number of passed tests during last execution */
+       unsigned int last_failed;  /*!< number of failed tests during last execution */
+       unsigned int last_time;    /*!< total time of the last test execution */
+} last_results;
+
+enum test_mode {
+       TEST_ALL = 0,
+       TEST_CATEGORY = 1,
+       TEST_NAME_CATEGORY = 2,
+};
+
+/*! List of registered test definitions */
+static AST_LIST_HEAD_STATIC(tests, ast_test);
+
+static struct ast_test *test_alloc(ast_test_cb_t *cb);
+static struct ast_test *test_free(struct ast_test *test);
+static int test_insert(struct ast_test *test);
+static struct ast_test *test_remove(ast_test_cb_t *cb);
+static int test_cat_cmp(const char *cat1, const char *cat2);
+
+int __ast_test_status_update(const char *file, const char *func, int line,
+               struct ast_test *test, const char *fmt, ...)
+{
+       struct ast_dynamic_str *buf = NULL;
+       va_list ap;
+
+       if (!(buf = ast_dynamic_str_create(128))) {
+               return -1;
+       }
+
+       va_start(ap, fmt);
+       ast_dynamic_str_thread_set_va(&buf, 0, NULL, fmt, ap);
+       va_end(ap);
+
+       if (test->cli_fd > -1) {
+               ast_cli(test->cli_fd, "[%s:%s:%d]: %s",
+                               file, func, line, buf->str);
+       }
+
+       ast_dynamic_str_append(&test->status_str, 0, "[%s:%s:%d]: %s",
+                       file, func, line, buf->str);
+
+       ast_free(buf);
+
+       return 0;
+}
+
+int ast_test_register(ast_test_cb_t *cb)
+{
+       struct ast_test *test;
+
+       if (!cb) {
+               ast_log(LOG_WARNING, "Attempted to register test without all required information\n");
+               return -1;
+       }
+
+       if (!(test = test_alloc(cb))) {
+               return -1;
+       }
+
+       if (test_insert(test)) {
+               test_free(test);
+               return -1;
+       }
+
+       return 0;
+}
+
+int ast_test_unregister(ast_test_cb_t *cb)
+{
+       struct ast_test *test;
+
+       if (!(test = test_remove(cb))) {
+               return -1; /* not found */
+       }
+
+       test_free(test);
+
+       return 0;
+}
+
+/*!
+ * \internal
+ * \brief executes a single test, storing the results in the test->result structure.
+ *
+ * \note The last_results structure which contains global statistics about test execution
+ * must be updated when using this function. See use in test_execute_multiple().
+ */
+static void test_execute(struct ast_test *test)
+{
+       struct timeval begin;
+
+       ast_dynamic_str_set(&test->status_str, 0, "%s", "");
+
+       begin = ast_tvnow();
+       test->state = test->cb(&test->info, TEST_EXECUTE, test);
+       test->time = ast_tvdiff_ms(ast_tvnow(), begin);
+}
+
+static void test_xml_entry(struct ast_test *test, FILE *f)
+{
+       if (!f || !test || test->state == AST_TEST_NOT_RUN) {
+               return;
+       }
+
+       fprintf(f, "\t<testcase time=\"%d.%d\" name=\"%s%s\"%s>\n",
+                       test->time / 1000, test->time % 1000,
+                       test->info.category, test->info.name,
+                       test->state == AST_TEST_PASS ? "/" : "");
+
+       if (test->state == AST_TEST_FAIL) {
+               fprintf(f, "\t\t<failure><![CDATA[\n%s\n\t\t]]></failure>\n",
+                               S_OR(test->status_str->str, "NA"));
+               fprintf(f, "\t</testcase>\n");
+       }
+
+}
+
+static void test_txt_entry(struct ast_test *test, FILE *f)
+{
+       if (!f || !test) {
+               return;
+       }
+
+       fprintf(f, "\nName:              %s\n", test->info.name);
+       fprintf(f,   "Category:          %s\n", test->info.category);
+       fprintf(f,   "Summary:           %s\n", test->info.summary);
+       fprintf(f,   "Description:       %s\n", test->info.description);
+       fprintf(f,   "Result:            %s\n", test_result2str[test->state]);
+       if (test->state != AST_TEST_NOT_RUN) {
+               fprintf(f,   "Time:              %d\n", test->time);
+       }
+       if (test->state == AST_TEST_FAIL) {
+               fprintf(f,   "Error Description: %s\n\n", S_OR(test->status_str->str, "NA"));
+       }
+}
+
+/*!
+ * \internal
+ * \brief Executes registered unit tests
+ *
+ * \param name of test to run (optional)
+ * \param test category to run (optional)
+ * \param cli args for cli test updates (optional)
+ *
+ * \return number of tests executed.
+ *
+ * \note This function has three modes of operation
+ * -# When given a name and category, a matching individual test will execute if found.
+ * -# When given only a category all matching tests within that category will execute.
+ * -# If given no name or category all registered tests will execute.
+ */
+static int test_execute_multiple(const char *name, const char *category, int cli_fd)
+{
+       char result_buf[32] = { 0 };
+       struct ast_test *test = NULL;
+       enum test_mode mode = TEST_ALL; /* 3 modes, 0 = run all, 1 = only by category, 2 = only by name and category */
+       int execute = 0;
+       int res = 0;
+
+       if (!ast_strlen_zero(category)) {
+               if (!ast_strlen_zero(name)) {
+                       mode = TEST_NAME_CATEGORY;
+               } else {
+                       mode = TEST_CATEGORY;
+               }
+       }
+
+       AST_LIST_LOCK(&tests);
+       /* clear previous execution results */
+       memset(&last_results, 0, sizeof(last_results));
+       AST_LIST_TRAVERSE(&tests, test, entry) {
+
+               execute = 0;
+               switch (mode) {
+               case TEST_CATEGORY:
+                       if (!test_cat_cmp(test->info.category, category)) {
+                               execute = 1;
+                       }
+                       break;
+               case TEST_NAME_CATEGORY:
+                       if (!(test_cat_cmp(test->info.category, category)) && !(strcmp(test->info.name, name))) {
+                               execute = 1;
+                       }
+                       break;
+               case TEST_ALL:
+                       execute = 1;
+               }
+
+               if (execute) {
+                       if (cli_fd > -1) {
+                               ast_cli(cli_fd, "START  %s - %s \n", test->info.category, test->info.name);
+                       }
+
+                       /* set the test status update argument. it is ok if cli is NULL */
+                       test->cli_fd = cli_fd;
+
+                       /* execute the test and save results */
+                       test_execute(test);
+
+                       test->cli_fd = -1;
+
+                       /* update execution specific counts here */
+                       last_results.last_time += test->time;
+                       if (test->state == AST_TEST_PASS) {
+                               last_results.last_passed++;
+                       } else if (test->state == AST_TEST_FAIL) {
+                               last_results.last_failed++;
+                       }
+
+                       if (cli_fd > -1) {
+                               term_color(result_buf,
+                                       test_result2str[test->state],
+                                       (test->state == AST_TEST_FAIL) ? COLOR_RED : COLOR_GREEN,
+                                       0,
+                                       sizeof(result_buf));
+                               ast_cli(cli_fd, "END    %s - %s Time: %s%dms Result: %s\n",
+                                       test->info.category,
+                                       test->info.name,
+                                       test->time ? "" : "<",
+                                       test->time ? test->time : 1,
+                                       result_buf);
+                       }
+               }
+
+               /* update total counts as well during this iteration
+                * even if the current test did not execute this time */
+               last_results.total_time += test->time;
+               last_results.total_tests++;
+               if (test->state != AST_TEST_NOT_RUN) {
+                       if (test->state == AST_TEST_PASS) {
+                               last_results.total_passed++;
+                       } else {
+                               last_results.total_failed++;
+                       }
+               }
+       }
+       res = last_results.last_passed + last_results.last_failed;
+       AST_LIST_UNLOCK(&tests);
+
+       return res;
+}
+
+/*!
+ * \internal
+ * \brief Generate test results.
+ *
+ * \param name of test result to generate (optional)
+ * \param test category to generate (optional)
+ * \param path to xml file to generate. (optional)
+ * \param path to txt file to generate, (optional)
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ *
+ * \note This function has three modes of operation.
+ * -# When given both a name and category, results will be generated for that single test.
+ * -# When given only a category, results for every test within the category will be generated.
+ * -# When given no name or category, results for every registered test will be generated.
+ *
+ * In order for the results to be generated, an xml and or txt file path must be provided.
+ */
+static int test_generate_results(const char *name, const char *category, const char *xml_path, const char *txt_path)
+{
+       enum test_mode mode = TEST_ALL;  /* 0 generate all, 1 generate by category only, 2 generate by name and category */
+       FILE *f_xml = NULL, *f_txt = NULL;
+       int res = 0;
+       struct ast_test *test = NULL;
+
+       /* verify at least one output file was given */
+       if (ast_strlen_zero(xml_path) && ast_strlen_zero(txt_path)) {
+               return -1;
+       }
+
+       /* define what mode is to be used */
+       if (!ast_strlen_zero(category)) {
+               if (!ast_strlen_zero(name)) {
+                       mode = TEST_NAME_CATEGORY;
+               } else {
+                       mode = TEST_CATEGORY;
+               }
+       }
+       /* open files for writing */
+       if (!ast_strlen_zero(xml_path)) {
+               if (!(f_xml = fopen(xml_path, "w"))) {
+                       ast_log(LOG_WARNING, "Could not open file %s for xml test results\n", xml_path);
+                       res = -1;
+                       goto done;
+               }
+       }
+       if (!ast_strlen_zero(txt_path)) {
+               if (!(f_txt = fopen(txt_path, "w"))) {
+                       ast_log(LOG_WARNING, "Could not open file %s for text output of test results\n", txt_path);
+                       res = -1;
+                       goto done;
+               }
+       }
+
+       AST_LIST_LOCK(&tests);
+       /* xml header information */
+       if (f_xml) {
+               /*
+                * http://confluence.atlassian.com/display/BAMBOO/JUnit+parsing+in+Bamboo
+                */
+               fprintf(f_xml, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
+               fprintf(f_xml, "<testsuite errors=\"0\" time=\"%d.%d\" tests=\"%d\" "
+                               "name=\"AsteriskUnitTests\">\n",
+                               last_results.total_time / 1000, last_results.total_time % 1000,
+                               last_results.total_tests);
+               fprintf(f_xml, "\t<properties>\n");
+               fprintf(f_xml, "\t\t<property name=\"version\" value=\"%s\"/>\n", ASTERISK_VERSION);
+               fprintf(f_xml, "\t</properties>\n");
+       }
+
+       /* txt header information */
+       if (f_txt) {
+               fprintf(f_txt, "Asterisk Version:         %s\n", ASTERISK_VERSION);
+               fprintf(f_txt, "Asterisk Version Number:  %d\n", ASTERISK_VERSION_NUM);
+               fprintf(f_txt, "Number of Tests:          %d\n", last_results.total_tests);
+               fprintf(f_txt, "Number of Tests Executed: %d\n", (last_results.total_passed + last_results.total_failed));
+               fprintf(f_txt, "Passed Tests:             %d\n", last_results.total_passed);
+               fprintf(f_txt, "Failed Tests:             %d\n", last_results.total_failed);
+               fprintf(f_txt, "Total Execution Time:     %d\n", last_results.total_time);
+       }
+
+       /* export each individual test */
+       AST_LIST_TRAVERSE(&tests, test, entry) {
+               switch (mode) {
+               case TEST_CATEGORY:
+                       if (!test_cat_cmp(test->info.category, category)) {
+                               test_xml_entry(test, f_xml);
+                               test_txt_entry(test, f_txt);
+                       }
+                       break;
+               case TEST_NAME_CATEGORY:
+                       if (!(strcmp(test->info.category, category)) && !(strcmp(test->info.name, name))) {
+                               test_xml_entry(test, f_xml);
+                               test_txt_entry(test, f_txt);
+                       }
+                       break;
+               case TEST_ALL:
+                       test_xml_entry(test, f_xml);
+                       test_txt_entry(test, f_txt);
+               }
+       }
+       AST_LIST_UNLOCK(&tests);
+
+done:
+       if (f_xml) {
+               fprintf(f_xml, "</testsuite>\n");
+               fclose(f_xml);
+       }
+       if (f_txt) {
+               fclose(f_txt);
+       }
+
+       return res;
+}
+
+/*!
+ * \internal
+ * \brief adds test to container sorted first by category then by name
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+static int test_insert(struct ast_test *test)
+{
+       /* This is a slow operation that may need to be optimized in the future
+        * as the test framework expands.  At the moment we are doing string
+        * comparisons on every item within the list to insert in sorted order. */
+
+       AST_LIST_LOCK(&tests);
+       AST_LIST_INSERT_SORTALPHA(&tests, test, entry, info.category);
+       AST_LIST_UNLOCK(&tests);
+
+       return 0;
+}
+
+/*!
+ * \internal
+ * \brief removes test from container
+ *
+ * \return ast_test removed from list on success, or NULL on failure
+ */
+static struct ast_test *test_remove(ast_test_cb_t *cb)
+{
+       struct ast_test *cur = NULL;
+
+       AST_LIST_LOCK(&tests);
+       AST_LIST_TRAVERSE_SAFE_BEGIN(&tests, cur, entry) {
+               if (cur->cb == cb) {
+                       AST_LIST_REMOVE_CURRENT(&tests, entry);
+                       break;
+               }
+       }
+       AST_LIST_TRAVERSE_SAFE_END;
+       AST_LIST_UNLOCK(&tests);
+
+       return cur;
+}
+
+/*!
+ * \brief compares two test categories to determine if cat1 resides in cat2
+ * \internal
+ *
+ * \retval 0 true
+ * \retval non-zero false
+ */
+
+static int test_cat_cmp(const char *cat1, const char *cat2)
+{
+       int len1 = 0;
+       int len2 = 0;
+
+       if (!cat1 || !cat2) {
+               return -1;
+       }
+
+       len1 = strlen(cat1);
+       len2 = strlen(cat2);
+
+       if (len2 > len1) {
+               return -1;
+       }
+
+       return strncmp(cat1, cat2, len2) ? 1 : 0;
+}
+
+/*!
+ * \internal
+ * \brief free an ast_test object and all it's data members
+ */
+static struct ast_test *test_free(struct ast_test *test)
+{
+       if (!test) {
+               return NULL;
+       }
+
+       ast_free(test->status_str);
+       ast_free(test);
+
+       return NULL;
+}
+
+/*!
+ * \internal
+ * \brief allocate an ast_test object.
+ */
+static struct ast_test *test_alloc(ast_test_cb_t *cb)
+{
+       struct ast_test *test;
+
+       if (!cb || !(test = ast_calloc(1, sizeof(*test)))) {
+               return NULL;
+       }
+
+       test->cli_fd = -1;
+       test->cb = cb;
+
+       test->cb(&test->info, TEST_INIT, test);
+
+       if (ast_strlen_zero(test->info.name)) {
+               ast_log(LOG_WARNING, "Test has no name, test registration refused.\n");
+               return test_free(test);
+       }
+
+       if (ast_strlen_zero(test->info.category)) {
+               ast_log(LOG_WARNING, "Test %s has no category, test registration refused.\n",
+                               test->info.name);
+               return test_free(test);
+       }
+
+       if (ast_strlen_zero(test->info.summary)) {
+               ast_log(LOG_WARNING, "Test %s/%s has no summary, test registration refused.\n",
+                               test->info.category, test->info.name);
+               return test_free(test);
+       }
+
+       if (ast_strlen_zero(test->info.description)) {
+               ast_log(LOG_WARNING, "Test %s/%s has no description, test registration refused.\n",
+                               test->info.category, test->info.name);
+               return test_free(test);
+       }
+
+       if (!(test->status_str = ast_dynamic_str_create(128))) {
+               return test_free(test);
+       }
+
+       return test;
+}
+
+static char *complete_test_category(const char *line, const char *word, int pos, int state)
+{
+       int which = 0;
+       int wordlen = strlen(word);
+       char *ret = NULL;
+       struct ast_test *test;
+
+       AST_LIST_LOCK(&tests);
+       AST_LIST_TRAVERSE(&tests, test, entry) {
+               if (!strncasecmp(word, test->info.category, wordlen) && ++which > state) {
+                       ret = ast_strdup(test->info.category);
+                       break;
+               }
+       }
+       AST_LIST_UNLOCK(&tests);
+       return ret;
+}
+
+static char *complete_test_name(const char *line, const char *word, int pos, int state, int cat_pos)
+{
+       int which = 0;
+       int wordlen = strlen(word);
+       char *ret = NULL;
+       struct ast_test *test;
+       char *cat = NULL;
+       char *tmp = ast_strdupa(line);
+       int i;
+
+       for (i = 0; i < cat_pos - 1 && tmp; cat = strsep(&tmp, " "), cat_pos--) {
+               tmp = ast_skip_blanks(tmp);
+       }
+
+       AST_LIST_LOCK(&tests);
+       AST_LIST_TRAVERSE(&tests, test, entry) {
+               if (!test_cat_cmp(test->info.category, cat) && (!strncasecmp(word, test->info.name, wordlen) && ++which > state)) {
+                       ret = ast_strdup(test->info.name);
+                       break;
+               }
+       }
+       AST_LIST_UNLOCK(&tests);
+       return ret;
+}
+
+static char *complete_show_registered(const char *line, const char *word, int pos, int state)
+{
+       static char * const option1[] = { "all", "category", NULL };
+       static char * const option2[] = { "name", NULL };
+
+       if (pos == 3) {
+               return ast_cli_complete(word, option1, state);
+       }
+       if (pos == 4) {
+               return complete_test_category(line, word, pos, state);
+       }
+       if (pos == 5) {
+               return ast_cli_complete(word, option2, state);
+       }
+       if (pos == 6) {
+               return complete_test_name(line, word, pos, state, 6);
+       }
+
+       return NULL;
+}
+
+/* CLI commands */
+static int test_cli_show_registered(int fd, int argc, char *argv[])
+{
+#define FORMAT "%-25.25s %-30.30s %-40.40s %-13.13s\n"
+       struct ast_test *test = NULL;
+       int count = 0;
+
+       if ((argc < 4) || (argc == 6) || (argc > 7) ||
+               ((argc == 4) && strcmp(argv[3], "all")) ||
+               ((argc == 7) && strcmp(argv[5], "name"))) {
+               return RESULT_SHOWUSAGE;
+       }
+       ast_cli(fd, FORMAT, "Category", "Name", "Summary", "Test Result");
+       ast_cli(fd, FORMAT, "--------", "----", "-------", "-----------");
+       AST_LIST_LOCK(&tests);
+       AST_LIST_TRAVERSE(&tests, test, entry) {
+               if ((argc == 4) ||
+                        ((argc == 5) && !test_cat_cmp(test->info.category, argv[4])) ||
+                        ((argc == 7) && !strcmp(test->info.category, argv[4]) && !strcmp(test->info.name, argv[6]))) {
+
+                       ast_cli(fd, FORMAT, test->info.category, test->info.name,
+                                       test->info.summary, test_result2str[test->state]);
+                       count++;
+               }
+       }
+       AST_LIST_UNLOCK(&tests);
+       ast_cli(fd, FORMAT, "--------", "----", "-------", "-----------");
+       ast_cli(fd, "\n%d Registered Tests Matched\n", count);
+
+       return RESULT_SUCCESS;
+}
+
+static char *complete_execute_registered(const char *line, const char *word, int pos, int state)
+{
+       static char * const option1[] = { "all", "category", NULL };
+       static char * const option2[] = { "name", NULL };
+
+       if (pos == 2) {
+               return ast_cli_complete(word, option1, state);
+       }
+       if (pos == 3) {
+               return complete_test_category(line, word, pos, state);
+       }
+       if (pos == 4) {
+               return ast_cli_complete(word, option2, state);
+       }
+       if (pos == 5) {
+               return complete_test_name(line, word, pos, state, 5);
+       }
+
+       return NULL;
+}
+
+static int test_cli_execute_registered(int fd, int argc, char *argv[])
+{
+       if (argc < 3|| argc > 6) {
+               return RESULT_SHOWUSAGE;
+       }
+
+       if ((argc == 3) && !strcmp(argv[2], "all")) { /* run all registered tests */
+               ast_cli(fd, "Running all available tests...\n\n");
+               test_execute_multiple(NULL, NULL, fd);
+       } else if (argc == 4) { /* run only tests within a category */
+               ast_cli(fd, "Running all available tests matching category %s\n\n", argv[3]);
+               test_execute_multiple(NULL, argv[3], fd);
+       } else if (argc == 6) { /* run only a single test matching the category and name */
+               ast_cli(fd, "Running all available tests matching category %s and name %s\n\n", argv[3], argv[5]);
+               test_execute_multiple(argv[5], argv[3], fd);
+       } else {
+               return RESULT_SHOWUSAGE;
+       }
+
+       AST_LIST_LOCK(&tests);
+       if (!(last_results.last_passed + last_results.last_failed)) {
+               ast_cli(fd, "--- No Tests Found! ---\n");
+       }
+       ast_cli(fd, "\n%d Test(s) Executed  %d Passed  %d Failed\n",
+               (last_results.last_passed + last_results.last_failed),
+               last_results.last_passed,
+               last_results.last_failed);
+       AST_LIST_UNLOCK(&tests);
+
+       return RESULT_SUCCESS;
+}
+
+static char *complete_show_results(const char *line, const char *word, int pos, int state)
+{
+       static char * const option1[] = { "all", "failed", "passed", NULL };
+
+       if (pos == 3) {
+               return ast_cli_complete(word, option1, state);
+       }
+
+       return NULL;
+}
+
+static int test_cli_show_results(int fd, int argc, char *argv[])
+{
+#define FORMAT_RES_ALL1 "%s%s %-30.30s %-25.25s %-10.10s\n"
+#define FORMAT_RES_ALL2 "%s%s %-30.30s %-25.25s %s%ums\n"
+       char result_buf[32] = { 0 };
+       struct ast_test *test = NULL;
+       int failed = 0;
+       int passed = 0;
+       int mode;  /* 0 for show all, 1 for show fail, 2 for show passed */
+
+       /* verify input */
+       if (argc != 4) {
+               return RESULT_SHOWUSAGE;
+       } else if (!strcmp(argv[3], "passed")) {
+               mode = 2;
+       } else if (!strcmp(argv[3], "failed")) {
+               mode = 1;
+       } else if (!strcmp(argv[3], "all")) {
+               mode = 0;
+       } else {
+               return RESULT_SHOWUSAGE;
+       }
+
+       ast_cli(fd, FORMAT_RES_ALL1, "Result", "", "Name", "Category", "Time");
+       AST_LIST_LOCK(&tests);
+       AST_LIST_TRAVERSE(&tests, test, entry) {
+               if (test->state == AST_TEST_NOT_RUN) {
+                       continue;
+               }
+               test->state == AST_TEST_FAIL ? failed++ : passed++;
+               if (!mode || ((mode == 1) && (test->state == AST_TEST_FAIL)) || ((mode == 2) && (test->state == AST_TEST_PASS))) {
+                       /* give our results pretty colors */
+                       term_color(result_buf, test_result2str[test->state],
+                               (test->state == AST_TEST_FAIL) ? COLOR_RED : COLOR_GREEN,
+                               0, sizeof(result_buf));
+
+                       ast_cli(fd, FORMAT_RES_ALL2,
+                               result_buf,
+                               "  ",
+                               test->info.name,
+                               test->info.category,
+                               test->time ? " " : "<",
+                               test->time ? test->time : 1);
+               }
+       }
+       AST_LIST_UNLOCK(&tests);
+
+       ast_cli(fd, "%d Test(s) Executed  %d Passed  %d Failed\n", (failed + passed), passed, failed);
+
+       return RESULT_SUCCESS;
+}
+
+static char *complete_generate_results(const char *line, const char *word, int pos, int state)
+{
+       static char * const option[] = { "xml", "txt", NULL };
+
+       if (pos == 3) {
+               return ast_cli_complete(word, option, state);
+       }
+
+       return NULL;
+}
+
+static int test_cli_generate_results(int fd, int argc, char *argv[])
+{
+       const char *file = NULL;
+       const char *type = "";
+       int isxml = 0;
+       int res = 0;
+       struct ast_dynamic_str *buf = NULL;
+       struct timeval time = ast_tvnow();
+
+       /* verify input */
+       if (argc < 4 || argc > 5) {
+               return RESULT_SHOWUSAGE;
+       } else if (!strcmp(argv[3], "xml")) {
+               type = "xml";
+               isxml = 1;
+       } else if (!strcmp(argv[3], "txt")) {
+               type = "txt";
+       } else {
+               return RESULT_SHOWUSAGE;
+       }
+
+       if (argc == 5) {
+               file = argv[4];
+       } else {
+               if (!(buf = ast_dynamic_str_create(256))) {
+                       return RESULT_FAILURE;
+               }
+               ast_dynamic_str_set(&buf, 0, "%s/asterisk_test_results-%ld.%s", ast_config_AST_LOG_DIR, (long) time.tv_sec, type);
+
+               file = buf->str;
+       }
+
+       if (isxml) {
+               res = test_generate_results(NULL, NULL, file, NULL);
+       } else {
+               res = test_generate_results(NULL, NULL, NULL, file);
+       }
+
+       if (!res) {
+               ast_cli(fd, "Results Generated Successfully: %s\n", S_OR(file, ""));
+       } else {
+               ast_cli(fd, "Results Could Not Be Generated: %s\n", S_OR(file, ""));
+       }
+
+       ast_free(buf);
+
+       return RESULT_SUCCESS;
+}
+
+static const char show_registered_help[] = ""
+       "Usage: 'test show registered' can be used in three ways.\n"
+       "       1. 'test show registered all' shows all registered tests\n"
+       "       2. 'test show registered category [test category]' shows all tests in the given\n"
+       "          category.\n"
+       "       3. 'test show registered category [test category] name [test name]' shows all\n"
+       "           tests in a given category matching a given name\n";
+
+static const char execute_registered_help[] = ""
+       "Usage: test execute can be used in three ways.\n"
+       "       1. 'test execute all' runs all registered tests\n"
+       "       2. 'test execute category [test category]' runs all tests in the given\n"
+       "          category.\n"
+       "       3. 'test execute category [test category] name [test name]' runs all\n"
+       "           tests in a given category matching a given name\n";
+
+static const char show_results_help[] = ""
+       "Usage: test show results can be used in three ways\n"
+       "       1. 'test show results all' Displays results for all executed tests.\n"
+       "       2. 'test show results passed' Displays results for all passed tests.\n"
+       "       3. 'test show results failed' Displays results for all failed tests.\n";
+
+static const char generate_results_help[] = ""
+       "Usage: 'test generate results'\n"
+       "       Generates test results in either xml or txt format. An optional \n"
+       "       file path may be provided to specify the location of the xml or\n"
+       "       txt file\n"
+       "       \nExample usage:\n"
+       "       'test generate results xml' this writes to a default file\n"
+       "       'test generate results xml /path/to/file.xml' writes to specified file\n";
+
+static struct ast_cli_entry test_cli[] = {
+       { { "test", "show", "registered", NULL },
+       test_cli_show_registered, "Show registered tests",
+       show_registered_help, complete_show_registered, },
+
+       { { "test", "execute", NULL },
+       test_cli_execute_registered, "Execute registered tests",
+       execute_registered_help, complete_execute_registered, },
+
+       { { "test", "show", "results", NULL },
+       test_cli_show_results, "Show last test results",
+       show_results_help, complete_show_results, },
+
+       { { "test", "generate", "results", NULL },
+       test_cli_generate_results, "Generate test results to a file",
+       generate_results_help, complete_generate_results, },
+};
+#endif /* TEST_FRAMEWORK */
+
+int ast_test_init()
+{
+#ifdef TEST_FRAMEWORK
+       /* Register cli commands */
+       ast_cli_register_multiple(test_cli, ARRAY_LEN(test_cli));
+#endif
+
+       return 0;
+}
diff --git a/tests/Makefile b/tests/Makefile
new file mode 100644 (file)
index 0000000..712b440
--- /dev/null
@@ -0,0 +1,32 @@
+#
+# Asterisk -- A telephony toolkit for Linux.
+# 
+# Makefile for test modules
+#
+# Copyright (C) 1999-2010, Digium, Inc.
+#
+# This program is free software, distributed under the terms of
+# the GNU General Public License
+#
+
+-include ../menuselect.makeopts ../menuselect.makedeps ../makeopts
+
+MENUSELECT_CATEGORY=TESTS
+MENUSELECT_DESCRIPTION=Test Modules
+
+ALL_C_MODS:=$(patsubst %.c,%,$(wildcard test_*.c))
+ALL_CC_MODS:=$(patsubst %.cc,%,$(wildcard test_*.cc))
+
+C_MODS:=$(filter-out $(MENUSELECT_TESTS),$(ALL_C_MODS))
+CC_MODS:=$(filter-out $(MENUSELECT_TESTS),$(ALL_CC_MODS))
+
+LOADABLE_MODS:=$(C_MODS) $(CC_MODS)
+
+ifneq ($(findstring tests,$(MENUSELECT_EMBED)),)
+  EMBEDDED_MODS:=$(LOADABLE_MODS)
+  LOADABLE_MODS:=
+endif
+
+all: _all
+
+include $(ASTTOPDIR)/Makefile.moddir_rules
diff --git a/tests/test_skel.c b/tests/test_skel.c
new file mode 100644 (file)
index 0000000..c1ce5e8
--- /dev/null
@@ -0,0 +1,82 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) <Year>, <Your Name Here>
+ *
+ * <Your Name Here> <<Your Email Here>>
+ *
+ * See http://www.asterisk.org for more information about
+ * the Asterisk project. Please do not directly contact
+ * any of the maintainers of this project for assistance;
+ * the project provides a web site, mailing lists and IRC
+ * channels for your use.
+ *
+ * This program is free software, distributed under the terms of
+ * the GNU General Public License Version 2. See the LICENSE file
+ * at the top of the source tree.
+ */
+
+/*! 
+ * \file
+ * \brief Skeleton Test
+ *
+ * \author\verbatim <Your Name Here> <<Your Email Here>> \endverbatim
+ * 
+ * This is a skeleton for development of an Asterisk test module
+ * \ingroup tests
+ */
+
+/*** MODULEINFO
+       <depend>TEST_FRAMEWORK</depend>
+ ***/
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
+
+#include "asterisk/utils.h"
+#include "asterisk/module.h"
+#include "asterisk/test.h"
+
+AST_TEST_DEFINE(sample_test)
+{
+       void *ptr;
+
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "sample_test";
+               info->category = "main/sample/";
+               info->summary = "sample unit test";
+               info->description =
+                       "This demonstrates what is required to implement "
+                       "a unit test.";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
+
+       ast_test_status_update(test, "Executing sample test...\n");
+
+       if (!(ptr = ast_malloc(8))) {
+               ast_test_status_update(test, "ast_malloc() failed\n");
+               return AST_TEST_FAIL;
+       }
+
+       ast_free(ptr);
+
+       return AST_TEST_PASS;
+}
+
+static int unload_module(void)
+{
+       AST_TEST_UNREGISTER(sample_test);
+       return 0;
+}
+
+static int load_module(void)
+{
+       AST_TEST_REGISTER(sample_test);
+       return AST_MODULE_LOAD_SUCCESS;
+}
+
+AST_MODULE_INFO_STANDARD(ASTERISK_GPL_KEY, "Skeleton (sample) Test");