Unit Test Framework API
authorDavid Vossel <dvossel@digium.com>
Tue, 22 Dec 2009 16:09:11 +0000 (16:09 +0000)
committerDavid Vossel <dvossel@digium.com>
Tue, 22 Dec 2009 16:09:11 +0000 (16:09 +0000)
The Unit Test Framework is a new API that manages registration and
execution of unit tests in Asterisk with the purpose of verifying the
operation of C functions.  The Framework consists of a single test
manager accompanied by a list of registered test functions defined
within the code.  A test is defined, registered, and unregistered
from the framework using a set of macros which allow the test code
to only be compiled within asterisk when the TEST_FRAMEWORK flag is
enabled in menuselect.  This allows the test code to exist in the
same file as the C functions it intends to verify.  Registered tests
may be viewed and executed via a set of new CLI commands.  CLI commands
are also present for generating and exporting test results into xml
and txt formats.

For more information and use cases please refer to the documentation
provided at the beginning of the test.h file.

Review: https://reviewboard.asterisk.org/r/447/

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

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

index 0b0f5e9..37f7a4d 100644 (file)
@@ -20,4 +20,7 @@
                </member>
                <member name="SKINNY_DEVMODE" displayname="Enable Skinny Dev Mode">
                </member>
+               <member name="TEST_FRAMEWORK" displayname="Enable Test Framework API">
+               </member>
+
        </category>
index b6b2659..cb1b618 100644 (file)
@@ -44,7 +44,8 @@ int ast_indications_reload(void);/*!< Provided by indications.c */
 void ast_stun_init(void);               /*!< Provided by stun.c */
 int ast_cel_engine_init(void);         /*!< Provided by cel.c */
 int ast_cel_engine_reload(void);       /*!< Provided by cel.c */
-int ast_ssl_init(void);                 /*!< Porvided by ssl.c */
+int ast_ssl_init(void);                 /*!< Provided by ssl.c */
+int ast_test_init(void);            /*!< Provided by test.c */
 
 /*!
  * \brief Reload asterisk modules.
diff --git a/include/asterisk/test.h b/include/asterisk/test.h
new file mode 100644 (file)
index 0000000..bba40bd
--- /dev/null
@@ -0,0 +1,218 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2009, Digium, Inc.
+ *
+ * David Vossel <dvossel@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>
+ */
+
+#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_args *args
+
+   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_str_set(&args->ast_test_error_str, 0 , "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
+
+   Every callback function is passed an ast_test_args object which contains
+   an ast_str allowing the function to provide an optional short description of
+   what went wrong if the test failed. This is done by writing to
+   args->ast_test_error_str.
+
+\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_args *args)
+#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_args *args)
+#define AST_TEST_REGISTER(cb)
+#define AST_TEST_UNREGISTER(cb)
+
+#endif
+
+enum ast_test_result_state {
+       AST_TEST_NOT_RUN,
+       AST_TEST_PASS,
+       AST_TEST_FAIL,
+};
+
+enum ast_test_command {
+       TEST_INIT,
+       TEST_EXECUTE,
+};
+
+/*!
+ *  This struct is passed to ast_test_status_update() providing a place to push
+ *  the update to. In the future this structure may expand beyond simply being
+ *  a wrapper for cli args to including other status update options as well.
+ */
+struct ast_test_status_args {
+       /*! pointer to cli arg used for updating status */
+       struct ast_cli_args *cli;
+};
+
+/*!
+ * tools made available to the callback function during test execution
+ */
+struct ast_test_args {
+       struct ast_str *ast_test_error_str;  /*! optional error str to describe error result */
+       struct ast_test_status_args status_update;
+};
+
+/*!
+ * Contains all the initilization information required to store a new test definition
+ */
+struct ast_test_info {
+       /*! name of test, unique to category */
+       const char *name;
+       /*! test category */
+       const char *category;
+       /*! optional short summary of test */
+       const char *summary;
+       /*! 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_args *args);
+
+/*!
+ * \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 ast_test_status_args defines everywhere the update should go.
+ *
+ * \retval 0 success
+ * \retval -1 failure
+ */
+int ast_test_status_update(struct ast_test_status_args *args, const char *fmt, ...)
+__attribute__((format(printf, 2, 3)));
+
+#endif /* TEST_FRAMEWORK */
+#endif /* _AST_TEST_H */
index f8f3fa4..5509788 100644 (file)
@@ -140,6 +140,7 @@ int daemon(int, int);  /* defined in libresolv of all places */
 #include "asterisk/buildinfo.h"
 #include "asterisk/xmldoc.h"
 #include "asterisk/poll-compat.h"
+#include "asterisk/test.h"
 
 #include "../defaults.h"
 
@@ -3547,6 +3548,13 @@ int main(int argc, char *argv[])
                exit(1);
        }
 
+#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..3e5f155
--- /dev/null
@@ -0,0 +1,845 @@
+/*
+ * Asterisk -- An open source telephony toolkit.
+ *
+ * Copyright (C) 2009, Digium, Inc.
+ *
+ * David Vossel <dvossel@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>
+ */
+
+#include "asterisk.h"
+
+ASTERISK_FILE_VERSION(__FILE__, "$Revision$");
+
+#include "asterisk/_private.h"
+
+#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"
+
+/*! This array corrisponds 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 */
+       struct ast_test_args args;        /*! function callback arguments */
+       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, reguardless 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 function prototypes */
+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(struct ast_test_status_args *args, const char *fmt, ...)
+{
+       struct ast_str *buf = NULL;
+       va_list ap;
+
+       /* it is not an error if no cli args exist. */
+       if (!args->cli) {
+               return 0;
+       }
+
+       if (!(buf = ast_str_create(128))) {
+               return -1;
+       }
+
+       va_start(ap, fmt);
+       ast_str_set_va(&buf, 0, fmt, ap);
+       va_end(ap);
+
+       ast_cli(args->cli->fd, "%s", ast_str_buffer(buf));
+
+       ast_free(buf);
+       return 0;
+}
+
+int ast_test_register(ast_test_cb_t *cb)
+{
+       struct ast_test *test;
+
+       /* verify data.*/
+       if (!cb) {
+               ast_log(LOG_WARNING, "Attempted to register test without all required information\n");
+               return -1;
+       }
+
+       /* create test object */
+       if (!(test = test_alloc(cb))) {
+               return -1;
+       }
+
+       /* insert into list */
+       if (test_insert(test)) {
+               test_free(test);
+               return -1;
+       }
+
+       return 0;
+}
+
+int ast_test_unregister(ast_test_cb_t *cb)
+{
+       struct ast_test *test;
+
+       /* find test and remove */
+       if (!(test = test_remove(cb))) {
+               return -1; /* not found */
+       }
+
+       /* free test object */
+       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;
+
+       /* clear any previous error results before starting */
+       ast_str_reset(test->args.ast_test_error_str);
+       /* get start time */
+       begin = ast_tvnow();
+       /* the callback gets the pointer to the pointer of the error buf */
+       test->state = test->cb(&test->info, TEST_EXECUTE, &test->args);
+       /* record the total time the test took */
+       test->time = ast_tvdiff_ms(ast_tvnow(), begin);
+       /* clear any status update args that may have been set */
+       memset(&test->args.status_update, 0, sizeof(struct ast_test_status_args));
+}
+
+static void test_xml_entry(struct ast_test *test, FILE *f)
+{
+       if (!f || !test) {
+               return;
+       }
+
+       fprintf(f, "\n<test>\n");
+       fprintf(f, "<name>%s</name>\n", test->info.name);
+       fprintf(f, "<category>%s</category>\n", test->info.category);
+       fprintf(f, "<summary>%s</summary>\n", test->info.summary);
+       fprintf(f, "<description>\n%s\n</description>\n", test->info.description);
+
+       fprintf(f, "<result>\n\t%s\n", test_result2str[test->state]);
+       if (test->state == AST_TEST_FAIL) {
+               fprintf(f, "\t<error>\n\t\t%s\n\t</error>\n", S_OR(ast_str_buffer(test->args.ast_test_error_str), "NA"));
+       }
+       if (test->state != AST_TEST_NOT_RUN) {
+               fprintf(f, "\t<time>\n\t\t%d\n\t</time>\n", test->time);
+       }
+       fprintf(f, "</result>\n");
+
+       fprintf(f, "</test>\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,   "Catagory:          %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_FAIL) {
+               fprintf(f,   "Error Description: %s\n", S_OR(ast_str_buffer(test->args.ast_test_error_str), "NA"));
+       }
+       if (test->state != AST_TEST_NOT_RUN) {
+               fprintf(f,   "Time:              %d\n", test->time);
+       }
+}
+
+/*!
+ * \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, struct ast_cli_args *cli)
+{
+       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 (!(strcmp(test->info.category, category)) && !(strcmp(test->info.name, name))) {
+                               execute = 1;
+                       }
+                       break;
+               case TEST_ALL:
+                       execute = 1;
+               }
+
+               if (execute) {
+                       if (cli) {
+                               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->args.status_update.cli = cli;
+
+                       /* execute the test and save results */
+                       test_execute(test);
+
+                       /* update execution specific counts here */
+                       last_results.last_time += test->time;
+                       if (test->state == AST_TEST_PASS) {
+                               last_results.last_passed++;
+                       } else {
+                               last_results.last_failed++;
+                       }
+
+                       if (cli) {
+                               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: %dms Result: %s %s\n",
+                                       test->info.category,
+                                       test->info.name,
+                                       test->time,
+                                       result_buf,
+                                       ast_str_buffer(test->args.ast_test_error_str));
+                       }
+               }
+
+               /* 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) {
+               fprintf(f_xml, "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n");
+               fprintf(f_xml, "\n<results>\n");
+               fprintf(f_xml, "<version>%s</version>\n", ASTERISK_VERSION);
+               fprintf(f_xml, "<versionnum>%d</versionnum>\n", ASTERISK_VERSION_NUM);
+               fprintf(f_xml, "<numtests>%d</numtests>\n", (last_results.total_tests));
+               fprintf(f_xml, "<executedtests>%d</executedtests>\n", (last_results.total_passed + last_results.total_failed));
+               fprintf(f_xml, "<passedtests>%d</passedtests>\n", last_results.total_passed);
+               fprintf(f_xml, "<failedtests>%d</failedtests>\n", last_results.total_failed);
+               fprintf(f_xml, "<totaltime>%d</totaltime>\n", last_results.total_time);
+               fprintf(f_xml, "</results>\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) {
+               fclose(f_xml);
+       }
+       if (f_txt) {
+               fclose(f_txt);
+       }
+
+       return res;
+}
+
+/*!
+ * \internal
+ * \brief adds test to container sorted first by category then by name
+ *
+ * \return 0 on success, -1 on failure
+ */
+static int test_insert(struct ast_test *test)
+{
+       struct ast_test *cur = NULL;
+       int res = 0;
+       int i = 0;
+       int inserted = 0;
+
+       /* 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_TRAVERSE_SAFE_BEGIN(&tests, cur, entry) {
+               if ((i = strcmp(test->info.category, cur->info.category)) < 0) {
+                       AST_LIST_INSERT_BEFORE_CURRENT(test, entry);
+                       inserted = 1;
+                       break;
+               } else if (!i) {  /* same category, now insert by name within that category*/
+                       if ((i = strcmp(test->info.name, cur->info.name)) < 0) {
+                               AST_LIST_INSERT_BEFORE_CURRENT(test, entry);
+                               inserted = 1;
+                               break;
+                       } else if (!i) {
+                               /* Error, duplicate found */
+                               res = -1;
+                               break;
+                       }
+               }
+       }
+       AST_LIST_TRAVERSE_SAFE_END;
+
+       if (!inserted && !res) {
+               AST_LIST_INSERT_TAIL(&tests, test, entry);
+               inserted = 1;
+       }
+
+       AST_LIST_UNLOCK(&tests);
+
+       return res;
+}
+
+/*!
+ * \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(entry);
+                       break;
+               }
+       }
+       AST_LIST_TRAVERSE_SAFE_END;
+       AST_LIST_UNLOCK(&tests);
+
+       return cur;
+}
+
+/*!
+ * \brief compares two test catagories to determine if cat1 resides in cat2
+ * \internal
+ *
+ * \return 0 if true
+ */
+
+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;
+}
+
+/*!
+ * \brief frees a ast_test object and all it's data members
+ * \internal
+ */
+static struct ast_test *test_free(struct ast_test *test)
+{
+       if (!test) {
+               return NULL;
+       }
+
+       ast_free(test->args.ast_test_error_str);
+       ast_free(test);
+
+       return NULL;
+}
+
+/*!
+ * \internal
+ * \brief allocates 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->cb = cb;
+
+       test->cb(&test->info, TEST_INIT, &test->args);
+
+       if (ast_strlen_zero(test->info.name) ||
+               ast_strlen_zero(test->info.category) ||
+               ast_strlen_zero(test->info.summary) ||
+               ast_strlen_zero(test->info.description) ||
+               !(test->args.ast_test_error_str = ast_str_create(128))) {
+
+               return test_free(test);
+       }
+
+       return test;
+}
+
+/* CLI commands */
+static char *test_cli_show_registered(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+#define FORMAT "%-15s %-20s %-30s %-10s\n"
+       static const char * const option1[] = { "all", "category", NULL };
+       static const char * const option2[] = { "name", NULL };
+       struct ast_test *test = NULL;
+       int count = 0;
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "test show registered";
+
+               e->usage =
+                       "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";
+               return NULL;
+       case CLI_GENERATE:
+               if (a->pos == 3) {
+                       return ast_cli_complete(a->word, option1, a->n);
+               }
+               if (a->pos == 5) {
+                       return ast_cli_complete(a->word, option2, a->n);
+               }
+               return NULL;
+       case CLI_HANDLER:
+               if ((a->argc < 4) || (a->argc == 6) || (a->argc > 7) ||
+                       ((a->argc == 4) && strcmp(a->argv[3], "all")) ||
+                       ((a->argc == 7) && strcmp(a->argv[5], "name"))) {
+                       return CLI_SHOWUSAGE;
+               }
+               ast_cli(a->fd, FORMAT, "Name", "Catagory", "Summary", "Test Result");
+               AST_LIST_LOCK(&tests);
+               AST_LIST_TRAVERSE(&tests, test, entry) {
+                       if ((a->argc == 4) ||
+                                ((a->argc == 5) && !test_cat_cmp(test->info.category, a->argv[4])) ||
+                                ((a->argc == 7) && !strcmp(test->info.category, a->argv[4]) && !strcmp(test->info.name, a->argv[6]))) {
+
+                               ast_cli(a->fd, FORMAT, test->info.name, test->info.category, test->info.summary, test_result2str[test->state]);
+                               count ++;
+                       }
+               }
+               AST_LIST_UNLOCK(&tests);
+               ast_cli(a->fd, "%d Registered Tests Matched\n", count);
+       default:
+               return NULL;
+       }
+
+       return CLI_SUCCESS;
+}
+
+static char *test_cli_execute_registered(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       static const char * const option1[] = { "all", "category", NULL };
+       static const char * const option2[] = { "name", NULL };
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "test execute";
+               e->usage =
+                       "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";
+               return NULL;
+       case CLI_GENERATE:
+               if (a->pos == 2) {
+                       return ast_cli_complete(a->word, option1, a->n);
+               }
+               if (a->pos == 4) {
+                       return ast_cli_complete(a->word, option2, a->n);
+               }
+               return NULL;
+       case CLI_HANDLER:
+
+               if (a->argc < 3|| a->argc > 6) {
+                       return CLI_SHOWUSAGE;
+               }
+
+               if ((a->argc == 3) && !strcmp(a->argv[2], "all")) { /* run all registered tests */
+                       ast_cli(a->fd, "Running all available tests...\n\n");
+                       test_execute_multiple(NULL, NULL, a);
+               } else if (a->argc == 4) { /* run only tests within a category */
+                       ast_cli(a->fd, "Running all available tests matching category %s\n\n", a->argv[3]);
+                       test_execute_multiple(NULL, a->argv[3], a);
+               } else if (a->argc == 6) { /* run only a single test matching the category and name */
+                       ast_cli(a->fd, "Running all available tests matching category %s and name %s\n\n", a->argv[5], a->argv[3]);
+                       test_execute_multiple(a->argv[5], a->argv[3], a);
+               } else {
+                       return CLI_SHOWUSAGE;
+               }
+
+               AST_LIST_LOCK(&tests);
+               if (!(last_results.last_passed + last_results.last_failed)) {
+                       ast_cli(a->fd, "--- No Tests Found! ---\n");
+               }
+               ast_cli(a->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);
+       default:
+               return NULL;
+       }
+
+       return CLI_SUCCESS;
+}
+
+static char *test_cli_show_results(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+#define FORMAT_RES_ALL "%s%s %-15s %-20s %-30s\n"
+       static const char * const option1[] = { "all", "failed", "passed", NULL };
+       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 */
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "test show results";
+               e->usage =
+                       "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";
+               return NULL;
+       case CLI_GENERATE:
+               if (a->pos == 3) {
+                       return ast_cli_complete(a->word, option1, a->n);
+               }
+               return NULL;
+       case CLI_HANDLER:
+
+               /* verify input */
+               if (a->argc != 4) {
+                       return CLI_SHOWUSAGE;
+               } else if (!strcmp(a->argv[3], "passed")) {
+                       mode = 2;
+               } else if (!strcmp(a->argv[3], "failed")) {
+                       mode = 1;
+               } else if (!strcmp(a->argv[3], "all")) {
+                       mode = 0;
+               } else {
+                       return CLI_SHOWUSAGE;
+               }
+
+               ast_cli(a->fd, FORMAT_RES_ALL, "Result", "", "Name", "Catagory", "Error Description");
+               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(a->fd, FORMAT_RES_ALL,
+                                       result_buf,
+                                       "  ",
+                                       test->info.name,
+                                       test->info.category,
+                                       (test->state == AST_TEST_FAIL) ? S_OR(ast_str_buffer(test->args.ast_test_error_str), "Not Avaliable") : "");
+                       }
+               }
+               AST_LIST_UNLOCK(&tests);
+
+               ast_cli(a->fd, "%d Test(s) Executed  %d Passed  %d Failed\n", (failed + passed), passed, failed);
+       default:
+               return NULL;
+       }
+       return CLI_SUCCESS;
+}
+
+static char *test_cli_generate_results(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
+{
+       static const char * const option[] = { "xml", "txt", NULL };
+       const char *file = NULL;
+       const char *type = "";
+       int isxml = 0;
+       int res = 0;
+       struct ast_str *buf = NULL;
+       struct timeval time = ast_tvnow();
+
+       switch (cmd) {
+       case CLI_INIT:
+               e->command = "test generate results";
+               e->usage =
+                       "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";
+               return NULL;
+       case CLI_GENERATE:
+               if (a->pos == 3) {
+                       return ast_cli_complete(a->word, option, a->n);
+               }
+               return NULL;
+       case CLI_HANDLER:
+
+               /* verify input */
+               if (a->argc < 4 || a->argc > 5) {
+                       return CLI_SHOWUSAGE;
+               } else if (!strcmp(a->argv[3], "xml")) {
+                       type = "xml";
+                       isxml = 1;
+               } else if (!strcmp(a->argv[3], "txt")) {
+                       type = "txt";
+               } else {
+                       return CLI_SHOWUSAGE;
+               }
+
+               if (a->argc == 5) {
+                       file = a->argv[4];
+               } else {
+                       if (!(buf = ast_str_create(256))) {
+                               return NULL;
+                       }
+                       ast_str_set(&buf, 0, "%s/asterisk_test_results-%ld.%s", ast_config_AST_LOG_DIR, time.tv_sec, type);
+
+                       file = ast_str_buffer(buf);
+               }
+
+               if (isxml) {
+                       res = test_generate_results(NULL, NULL, file, NULL);
+               } else {
+                       res = test_generate_results(NULL, NULL, NULL, file);
+               }
+
+               if (!res) {
+                       ast_cli(a->fd, "Results Generated Successfully: %s\n", S_OR(file, ""));
+               } else {
+                       ast_cli(a->fd, "Results Could Not Be Generated: %s\n", S_OR(file, ""));
+               }
+
+               ast_free(buf);
+       default:
+               return NULL;
+       }
+
+       return CLI_SUCCESS;
+}
+
+static struct ast_cli_entry test_cli[] = {
+       AST_CLI_DEFINE(test_cli_show_registered,           "show registered tests"),
+       AST_CLI_DEFINE(test_cli_execute_registered,        "execute registered tests"),
+       AST_CLI_DEFINE(test_cli_show_results,              "show last test results"),
+       AST_CLI_DEFINE(test_cli_generate_results,          "generate test results to file"),
+};
+#endif /* TEST_FRAMEWORK */
+
+int ast_test_init()
+{
+#ifdef TEST_FRAMEWORK
+       /* Register cli commands */
+       ast_cli_register_multiple(test_cli, ARRAY_LEN(test_cli));
+
+       /* in the future this function could be used to register functions not
+        * defined within a module */
+#endif
+
+       return 0;
+}
index 54fd2b0..0757f29 100644 (file)
@@ -32,9 +32,9 @@
 ASTERISK_FILE_VERSION(__FILE__, "$Revision$")
 
 #include "asterisk/module.h"
-#include "asterisk/cli.h"
 #include "asterisk/utils.h"
 #include "asterisk/heap.h"
+#include "asterisk/test.h"
 
 struct node {
        long val;
@@ -55,24 +55,32 @@ static int node_cmp(void *_n1, void *_n2)
        }
 }
 
-static int test1(int fd)
+AST_TEST_DEFINE(heap_test_1)
 {
        struct ast_heap *h;
        struct node *obj;
        struct node nodes[3] = {
-               { 1, },
-               { 2, },
-               { 3, },
+               { 1, } ,
+               { 2, } ,
+               { 3, } ,
        };
 
-       if (!(h = ast_heap_create(8, node_cmp, offsetof(struct node, index)))) {
-               return -1;
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "heap_test_1";
+               info->category = "main/heap/";
+               info->summary = "push and pop elements";
+               info->description = "Push a few elements onto a heap and make sure that they come back off in the right order.";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
        }
 
-       /* Pushing 1 2 3, and then popping 3 elements */
+       if (!(h = ast_heap_create(8, node_cmp, offsetof(struct node, index)))) {
+               return AST_TEST_FAIL;
+       }
 
-       ast_cli(fd, "Test #1 - Push a few elements onto a heap and make sure that they "
-                       "come back off in the right order.\n");
+       ast_test_status_update(&args->status_update, "pushing nodes\n");
 
        ast_heap_push(h, &nodes[0]);
 
@@ -82,52 +90,59 @@ static int test1(int fd)
 
        obj = ast_heap_pop(h);
        if (obj->val != 3) {
-               return -2;
+               return AST_TEST_FAIL;
        }
 
+       ast_test_status_update(&args->status_update, "popping nodes\n");
        obj = ast_heap_pop(h);
        if (obj->val != 2) {
-               return -3;
+               return AST_TEST_FAIL;
        }
 
        obj = ast_heap_pop(h);
        if (obj->val != 1) {
-               return -4;
+               return AST_TEST_FAIL;
        }
 
        obj = ast_heap_pop(h);
        if (obj) {
-               return -5;
+               return AST_TEST_FAIL;
        }
 
        h = ast_heap_destroy(h);
 
-       ast_cli(fd, "Test #1 successful.\n");
-
-       return 0;
+       return AST_TEST_PASS;
 }
 
-static int test2(int fd)
+AST_TEST_DEFINE(heap_test_2)
 {
        struct ast_heap *h = NULL;
        static const unsigned int one_million = 1000000;
        struct node *nodes = NULL;
        struct node *node;
        unsigned int i = one_million;
-       long last = LONG_MAX, cur;
-       int res = 0;
+       long last = LONG_MAX;
+       long cur;
+       enum ast_test_result_state res = AST_TEST_PASS;
 
-       ast_cli(fd, "Test #2 - Push a million random elements on to a heap, "
-                       "verify that the heap has been properly constructed, "
-                       "and then ensure that the elements are come back off in the proper order\n");
+       switch (cmd) {
+       case TEST_INIT:
+               info->name = "heap_test_2";
+               info->category = "main/heap/";
+               info->summary = "load test";
+               info->description = "Push a million random elements on to a heap,verify that the heap has been properly constructed, and then ensure that the elements are come back off in the proper order";
+               return AST_TEST_NOT_RUN;
+       case TEST_EXECUTE:
+               break;
+       }
 
        if (!(nodes = ast_malloc(one_million * sizeof(*node)))) {
-               res = -1;
+               res = AST_TEST_FAIL;
                goto return_cleanup;
        }
 
        if (!(h = ast_heap_create(20, node_cmp, offsetof(struct node, index)))) {
-               res = -2;
+               res = AST_TEST_FAIL;
                goto return_cleanup;
        }
 
@@ -137,7 +152,7 @@ static int test2(int fd)
        }
 
        if (ast_heap_verify(h)) {
-               res = -3;
+               res = AST_TEST_FAIL;
                goto return_cleanup;
        }
 
@@ -145,8 +160,8 @@ static int test2(int fd)
        while ((node = ast_heap_pop(h))) {
                cur = node->val;
                if (cur > last) {
-                       ast_cli(fd, "i: %u, cur: %ld, last: %ld\n", i, cur, last);
-                       res = -4;
+                       ast_str_set(&args->ast_test_error_str, 0, "i: %u, cur: %ld, last: %ld\n", i, cur, last);
+                       res = AST_TEST_FAIL;
                        goto return_cleanup;
                }
                last = cur;
@@ -154,13 +169,11 @@ static int test2(int fd)
        }
 
        if (i != one_million) {
-               ast_cli(fd, "Stopped popping off after only getting %u nodes\n", i);
-               res = -5;
+               ast_str_set(&args->ast_test_error_str, 0, "Stopped popping off after only getting %u nodes\n", i);
+               res = AST_TEST_FAIL;
                goto return_cleanup;
        }
 
-       ast_cli(fd, "Test #2 successful.\n");
-
 return_cleanup:
        if (h) {
                h = ast_heap_destroy(h);
@@ -172,51 +185,20 @@ return_cleanup:
        return res;
 }
 
-static char *handle_cli_heap_test(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
-{
-       int res;
-
-       switch (cmd) {
-       case CLI_INIT:
-               e->command = "heap test";
-               e->usage = ""
-                       "Usage: heap test\n"
-                       "";
-               return NULL;
-       case CLI_GENERATE:
-               return NULL;
-       }
-
-       if (a->argc != e->args) {
-               return CLI_SHOWUSAGE;
-       }
-
-       if ((res = test1(a->fd))) {
-               ast_cli(a->fd, "Test 1 failed! (%d)\n", res);
-               return CLI_FAILURE;
-       }
-
-       if ((res = test2(a->fd))) {
-               ast_cli(a->fd, "Test 2 failed! (%d)\n", res);
-               return CLI_FAILURE;
-       }
-
-       return CLI_SUCCESS;
-}
-
-static struct ast_cli_entry cli_heap[] = {
-       AST_CLI_DEFINE(handle_cli_heap_test, "Test the heap implementation"),
-};
-
 static int unload_module(void)
 {
-       ast_cli_unregister_multiple(cli_heap, ARRAY_LEN(cli_heap));
+       AST_TEST_UNREGISTER(heap_test_1);
+       AST_TEST_UNREGISTER(heap_test_2);
        return 0;
 }
 
 static int load_module(void)
 {
-       ast_cli_register_multiple(cli_heap, ARRAY_LEN(cli_heap));
+
+       AST_TEST_REGISTER(heap_test_1);
+
+       AST_TEST_REGISTER(heap_test_2);
+
        return AST_MODULE_LOAD_SUCCESS;
 }