diff options
author | russell <russell@f38db490-d61c-443f-a65b-d21fe96a405b> | 2010-06-28 18:34:18 +0000 |
---|---|---|
committer | russell <russell@f38db490-d61c-443f-a65b-d21fe96a405b> | 2010-06-28 18:34:18 +0000 |
commit | 4b7b8dd790147d834373bef43b57034fcec3d1a0 (patch) | |
tree | 5f64d6331a0260404d5b6d96513d0c28bb76b770 | |
parent | 3894ede3a1a894660486867ee5af813d25d9aa6b (diff) |
Backport unit test API to 1.4.
Review: https://reviewboard.asterisk.org/r/750/
git-svn-id: http://svn.digium.com/svn/asterisk/branches/1.4@272878 f38db490-d61c-443f-a65b-d21fe96a405b
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | build_tools/cflags-devmode.xml | 2 | ||||
-rw-r--r-- | include/asterisk.h | 1 | ||||
-rw-r--r-- | include/asterisk/linkedlists.h | 30 | ||||
-rw-r--r-- | include/asterisk/test.h | 215 | ||||
-rw-r--r-- | main/Makefile | 2 | ||||
-rw-r--r-- | main/asterisk.c | 7 | ||||
-rw-r--r-- | main/test.c | 899 | ||||
-rw-r--r-- | tests/Makefile | 32 | ||||
-rw-r--r-- | tests/test_skel.c | 82 |
10 files changed, 1270 insertions, 2 deletions
@@ -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) diff --git a/build_tools/cflags-devmode.xml b/build_tools/cflags-devmode.xml index 9ce9a68d5..6e7e645c9 100644 --- a/build_tools/cflags-devmode.xml +++ b/build_tools/cflags-devmode.xml @@ -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> diff --git a/include/asterisk.h b/include/asterisk.h index 1f6c2c723..e54b9b6a8 100644 --- a/include/asterisk.h +++ b/include/asterisk.h @@ -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; diff --git a/include/asterisk/linkedlists.h b/include/asterisk/linkedlists.h index a41ba857b..6810deea8 100644 --- a/include/asterisk/linkedlists.h +++ b/include/asterisk/linkedlists.h @@ -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) @@ -681,6 +682,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 \param list This is a pointer to the list to be appended. diff --git a/include/asterisk/test.h b/include/asterisk/test.h new file mode 100644 index 000000000..f97df80d7 --- /dev/null +++ b/include/asterisk/test.h @@ -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 */ diff --git a/main/Makefile b/main/Makefile index f72ac0780..e55731f17 100644 --- a/main/Makefile +++ b/main/Makefile @@ -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 diff --git a/main/asterisk.c b/main/asterisk.c index db8271735..1a8ddebd0 100644 --- a/main/asterisk.c +++ b/main/asterisk.c @@ -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 index 000000000..1fada7fa8 --- /dev/null +++ b/main/test.c @@ -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 index 000000000..712b4401b --- /dev/null +++ b/tests/Makefile @@ -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 index 000000000..c1ce5e844 --- /dev/null +++ b/tests/test_skel.c @@ -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"); |