2 * Asterisk -- An open source telephony toolkit.
4 * Copyright (C) 2009, Digium, Inc.
6 * David Vossel <dvossel@digium.com>
8 * See http://www.asterisk.org for more information about
9 * the Asterisk project. Please do not directly contact
10 * any of the maintainers of this project for assistance;
11 * the project provides a web site, mailing lists and IRC
12 * channels for your use.
14 * This program is free software, distributed under the terms of
15 * the GNU General Public License Version 2. See the LICENSE file
16 * at the top of the source tree.
21 * \brief Unit Test Framework
23 * \author David Vossel <dvossel@digium.com>
28 ASTERISK_FILE_VERSION(__FILE__, "$Revision$");
30 #include "asterisk/_private.h"
33 #include "asterisk/test.h"
34 #include "asterisk/logger.h"
35 #include "asterisk/linkedlists.h"
36 #include "asterisk/utils.h"
37 #include "asterisk/cli.h"
38 #include "asterisk/term.h"
39 #include "asterisk/version.h"
40 #include "asterisk/paths.h"
41 #include "asterisk/time.h"
43 /*! This array corresponds to the values defined in the ast_test_state enum */
44 static const char * const test_result2str[] = {
45 [AST_TEST_NOT_RUN] = "NOT RUN",
46 [AST_TEST_PASS] = "PASS",
47 [AST_TEST_FAIL] = "FAIL",
50 /*! holds all the information pertaining to a single defined test */
52 struct ast_test_info info; /*!< holds test callback information */
53 struct ast_test_args args; /*!< function callback arguments */
54 enum ast_test_result_state state; /*!< current test state */
55 unsigned int time; /*!< time in ms test took */
56 ast_test_cb_t *cb; /*!< test callback function */
57 AST_LIST_ENTRY(ast_test) entry;
60 /*! global structure containing both total and last test execution results */
61 static struct ast_test_execute_results {
62 unsigned int total_tests; /*!< total number of tests, regardless if they have been executed or not */
63 unsigned int total_passed; /*!< total number of executed tests passed */
64 unsigned int total_failed; /*!< total number of executed tests failed */
65 unsigned int total_time; /*!< total time of all executed tests */
66 unsigned int last_passed; /*!< number of passed tests during last execution */
67 unsigned int last_failed; /*!< number of failed tests during last execution */
68 unsigned int last_time; /*!< total time of the last test execution */
74 TEST_NAME_CATEGORY = 2,
77 /*! List of registered test definitions */
78 static AST_LIST_HEAD_STATIC(tests, ast_test);
80 /*! static function prototypes */
81 static struct ast_test *test_alloc(ast_test_cb_t *cb);
82 static struct ast_test *test_free(struct ast_test *test);
83 static int test_insert(struct ast_test *test);
84 static struct ast_test *test_remove(ast_test_cb_t *cb);
85 static int test_cat_cmp(const char *cat1, const char *cat2);
87 int ast_test_status_update(struct ast_test_status_args *args, const char *fmt, ...)
89 struct ast_str *buf = NULL;
92 /* it is not an error if no cli args exist. */
97 if (!(buf = ast_str_create(128))) {
102 ast_str_set_va(&buf, 0, fmt, ap);
105 ast_cli(args->cli->fd, "%s", ast_str_buffer(buf));
111 int ast_test_register(ast_test_cb_t *cb)
113 struct ast_test *test;
117 ast_log(LOG_WARNING, "Attempted to register test without all required information\n");
121 /* create test object */
122 if (!(test = test_alloc(cb))) {
126 /* insert into list */
127 if (test_insert(test)) {
135 int ast_test_unregister(ast_test_cb_t *cb)
137 struct ast_test *test;
139 /* find test and remove */
140 if (!(test = test_remove(cb))) {
141 return -1; /* not found */
144 /* free test object */
152 * \brief executes a single test, storing the results in the test->result structure.
154 * \note The last_results structure which contains global statistics about test execution
155 * must be updated when using this function. See use in test_execute_multiple().
157 static void test_execute(struct ast_test *test)
159 struct timeval begin;
161 /* clear any previous error results before starting */
162 ast_str_reset(test->args.ast_test_error_str);
165 /* the callback gets the pointer to the pointer of the error buf */
166 test->state = test->cb(&test->info, TEST_EXECUTE, &test->args);
167 /* record the total time the test took */
168 test->time = ast_tvdiff_ms(ast_tvnow(), begin);
169 /* clear any status update args that may have been set */
170 memset(&test->args.status_update, 0, sizeof(struct ast_test_status_args));
173 static void test_xml_entry(struct ast_test *test, FILE *f)
175 if (!f || !test || test->state == AST_TEST_NOT_RUN) {
179 fprintf(f, "\t<testcase time=\"%d.%d\" name=\"%s%s\"%s>\n",
180 test->time / 1000, test->time % 1000,
181 test->info.category, test->info.name,
182 test->state == AST_TEST_PASS ? "/" : "");
184 if (test->state == AST_TEST_FAIL) {
185 fprintf(f, "\t\t<failure>%s</failure>\n",
186 S_OR(ast_str_buffer(test->args.ast_test_error_str), "NA"));
187 fprintf(f, "\t</testcase>\n");
192 static void test_txt_entry(struct ast_test *test, FILE *f)
198 fprintf(f, "\nName: %s\n", test->info.name);
199 fprintf(f, "Category: %s\n", test->info.category);
200 fprintf(f, "Summary: %s\n", test->info.summary);
201 fprintf(f, "Description: %s\n", test->info.description);
202 fprintf(f, "Result: %s\n", test_result2str[test->state]);
203 if (test->state == AST_TEST_FAIL) {
204 fprintf(f, "Error Description: %s\n", S_OR(ast_str_buffer(test->args.ast_test_error_str), "NA"));
206 if (test->state != AST_TEST_NOT_RUN) {
207 fprintf(f, "Time: %d\n", test->time);
213 * \brief Executes registered unit tests
215 * \param name of test to run (optional)
216 * \param test category to run (optional)
217 * \param cli args for cli test updates (optional)
219 * \return number of tests executed.
221 * \note This function has three modes of operation
222 * -# When given a name and category, a matching individual test will execute if found.
223 * -# When given only a category all matching tests within that category will execute.
224 * -# If given no name or category all registered tests will execute.
226 static int test_execute_multiple(const char *name, const char *category, struct ast_cli_args *cli)
228 char result_buf[32] = { 0 };
229 struct ast_test *test = NULL;
230 enum test_mode mode = TEST_ALL; /* 3 modes, 0 = run all, 1 = only by category, 2 = only by name and category */
234 if (!ast_strlen_zero(category)) {
235 if (!ast_strlen_zero(name)) {
236 mode = TEST_NAME_CATEGORY;
238 mode = TEST_CATEGORY;
242 AST_LIST_LOCK(&tests);
243 /* clear previous execution results */
244 memset(&last_results, 0, sizeof(last_results));
245 AST_LIST_TRAVERSE(&tests, test, entry) {
250 if (!test_cat_cmp(test->info.category, category)) {
254 case TEST_NAME_CATEGORY:
255 if (!(strcmp(test->info.category, category)) && !(strcmp(test->info.name, name))) {
265 ast_cli(cli->fd, "START %s - %s \n", test->info.category, test->info.name);
268 /* set the test status update argument. it is ok if cli is NULL */
269 test->args.status_update.cli = cli;
271 /* execute the test and save results */
274 /* update execution specific counts here */
275 last_results.last_time += test->time;
276 if (test->state == AST_TEST_PASS) {
277 last_results.last_passed++;
279 last_results.last_failed++;
283 term_color(result_buf,
284 test_result2str[test->state],
285 (test->state == AST_TEST_FAIL) ? COLOR_RED : COLOR_GREEN,
288 ast_cli(cli->fd, "END %s - %s Time: %dms Result: %s %s\n",
293 ast_str_buffer(test->args.ast_test_error_str));
297 /* update total counts as well during this iteration
298 * even if the current test did not execute this time */
299 last_results.total_time += test->time;
300 last_results.total_tests++;
301 if (test->state != AST_TEST_NOT_RUN) {
302 if (test->state == AST_TEST_PASS) {
303 last_results.total_passed++;
305 last_results.total_failed++;
309 res = last_results.last_passed + last_results.last_failed;
310 AST_LIST_UNLOCK(&tests);
317 * \brief Generate test results.
319 * \param name of test result to generate (optional)
320 * \param test category to generate (optional)
321 * \param path to xml file to generate. (optional)
322 * \param path to txt file to generate, (optional)
327 * \note This function has three modes of operation.
328 * -# When given both a name and category, results will be generated for that single test.
329 * -# When given only a category, results for every test within the category will be generated.
330 * -# When given no name or category, results for every registered test will be generated.
332 * In order for the results to be generated, an xml and or txt file path must be provided.
334 static int test_generate_results(const char *name, const char *category, const char *xml_path, const char *txt_path)
336 enum test_mode mode = TEST_ALL; /* 0 generate all, 1 generate by category only, 2 generate by name and category */
337 FILE *f_xml = NULL, *f_txt = NULL;
339 struct ast_test *test = NULL;
341 /* verify at least one output file was given */
342 if (ast_strlen_zero(xml_path) && ast_strlen_zero(txt_path)) {
346 /* define what mode is to be used */
347 if (!ast_strlen_zero(category)) {
348 if (!ast_strlen_zero(name)) {
349 mode = TEST_NAME_CATEGORY;
351 mode = TEST_CATEGORY;
354 /* open files for writing */
355 if (!ast_strlen_zero(xml_path)) {
356 if (!(f_xml = fopen(xml_path, "w"))) {
357 ast_log(LOG_WARNING, "Could not open file %s for xml test results\n", xml_path);
362 if (!ast_strlen_zero(txt_path)) {
363 if (!(f_txt = fopen(txt_path, "w"))) {
364 ast_log(LOG_WARNING, "Could not open file %s for text output of test results\n", txt_path);
370 AST_LIST_LOCK(&tests);
371 /* xml header information */
374 * http://confluence.atlassian.com/display/BAMBOO/JUnit+parsing+in+Bamboo
376 fprintf(f_xml, "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
377 fprintf(f_xml, "<testsuite errors=\"0\" time=\"%d.%d\" tests=\"%d\" "
378 "name=\"AsteriskUnitTests\">\n",
379 last_results.total_time / 1000, last_results.total_time % 1000,
380 last_results.total_tests);
381 fprintf(f_xml, "\t<properties>\n");
382 fprintf(f_xml, "\t\t<property name=\"version\" value=\"%s\"/>\n", ASTERISK_VERSION);
383 fprintf(f_xml, "\t</properties>\n");
386 /* txt header information */
388 fprintf(f_txt, "Asterisk Version: %s\n", ASTERISK_VERSION);
389 fprintf(f_txt, "Asterisk Version Number: %d\n", ASTERISK_VERSION_NUM);
390 fprintf(f_txt, "Number of Tests: %d\n", last_results.total_tests);
391 fprintf(f_txt, "Number of Tests Executed: %d\n", (last_results.total_passed + last_results.total_failed));
392 fprintf(f_txt, "Passed Tests: %d\n", last_results.total_passed);
393 fprintf(f_txt, "Failed Tests: %d\n", last_results.total_failed);
394 fprintf(f_txt, "Total Execution Time: %d\n", last_results.total_time);
397 /* export each individual test */
398 AST_LIST_TRAVERSE(&tests, test, entry) {
401 if (!test_cat_cmp(test->info.category, category)) {
402 test_xml_entry(test, f_xml);
403 test_txt_entry(test, f_txt);
406 case TEST_NAME_CATEGORY:
407 if (!(strcmp(test->info.category, category)) && !(strcmp(test->info.name, name))) {
408 test_xml_entry(test, f_xml);
409 test_txt_entry(test, f_txt);
413 test_xml_entry(test, f_xml);
414 test_txt_entry(test, f_txt);
417 AST_LIST_UNLOCK(&tests);
421 fprintf(f_xml, "</testsuite>\n");
433 * \brief adds test to container sorted first by category then by name
435 * \return 0 on success, -1 on failure
437 static int test_insert(struct ast_test *test)
439 struct ast_test *cur = NULL;
444 /* This is a slow operation that may need to be optimized in the future
445 * as the test framework expands. At the moment we are doing string
446 * comparisons on every item within the list to insert in sorted order. */
447 AST_LIST_LOCK(&tests);
448 AST_LIST_TRAVERSE_SAFE_BEGIN(&tests, cur, entry) {
449 if ((i = strcmp(test->info.category, cur->info.category)) < 0) {
450 AST_LIST_INSERT_BEFORE_CURRENT(test, entry);
453 } else if (!i) { /* same category, now insert by name within that category*/
454 if ((i = strcmp(test->info.name, cur->info.name)) < 0) {
455 AST_LIST_INSERT_BEFORE_CURRENT(test, entry);
459 /* Error, duplicate found */
465 AST_LIST_TRAVERSE_SAFE_END;
467 if (!inserted && !res) {
468 AST_LIST_INSERT_TAIL(&tests, test, entry);
472 AST_LIST_UNLOCK(&tests);
479 * \brief removes test from container
481 * \return ast_test removed from list on success, or NULL on failure
483 static struct ast_test *test_remove(ast_test_cb_t *cb)
485 struct ast_test *cur = NULL;
487 AST_LIST_LOCK(&tests);
488 AST_LIST_TRAVERSE_SAFE_BEGIN(&tests, cur, entry) {
490 AST_LIST_REMOVE_CURRENT(entry);
494 AST_LIST_TRAVERSE_SAFE_END;
495 AST_LIST_UNLOCK(&tests);
501 * \brief compares two test categories to determine if cat1 resides in cat2
507 static int test_cat_cmp(const char *cat1, const char *cat2)
512 if (!cat1 || !cat2) {
523 return strncmp(cat1, cat2, len2) ? 1 : 0;
527 * \brief frees a ast_test object and all it's data members
530 static struct ast_test *test_free(struct ast_test *test)
536 ast_free(test->args.ast_test_error_str);
544 * \brief allocates an ast_test object.
546 static struct ast_test *test_alloc(ast_test_cb_t *cb)
548 struct ast_test *test;
550 if (!cb || !(test = ast_calloc(1, sizeof(*test)))) {
556 test->cb(&test->info, TEST_INIT, &test->args);
558 if (ast_strlen_zero(test->info.name) ||
559 ast_strlen_zero(test->info.category) ||
560 ast_strlen_zero(test->info.summary) ||
561 ast_strlen_zero(test->info.description) ||
562 !(test->args.ast_test_error_str = ast_str_create(128))) {
564 return test_free(test);
571 static char *test_cli_show_registered(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
573 #define FORMAT "%-30.30s %-25.25s %-40.40s %-13.13s\n"
574 static const char * const option1[] = { "all", "category", NULL };
575 static const char * const option2[] = { "name", NULL };
576 struct ast_test *test = NULL;
580 e->command = "test show registered";
583 "Usage: 'test show registered' can be used in three ways.\n"
584 " 1. 'test show registered all' shows all registered tests\n"
585 " 2. 'test show registered category [test category]' shows all tests in the given\n"
587 " 3. 'test show registered category [test category] name [test name]' shows all\n"
588 " tests in a given category matching a given name\n";
592 return ast_cli_complete(a->word, option1, a->n);
595 return ast_cli_complete(a->word, option2, a->n);
599 if ((a->argc < 4) || (a->argc == 6) || (a->argc > 7) ||
600 ((a->argc == 4) && strcmp(a->argv[3], "all")) ||
601 ((a->argc == 7) && strcmp(a->argv[5], "name"))) {
602 return CLI_SHOWUSAGE;
604 ast_cli(a->fd, FORMAT, "Name", "Category", "Summary", "Test Result");
605 AST_LIST_LOCK(&tests);
606 AST_LIST_TRAVERSE(&tests, test, entry) {
607 if ((a->argc == 4) ||
608 ((a->argc == 5) && !test_cat_cmp(test->info.category, a->argv[4])) ||
609 ((a->argc == 7) && !strcmp(test->info.category, a->argv[4]) && !strcmp(test->info.name, a->argv[6]))) {
611 ast_cli(a->fd, FORMAT, test->info.name, test->info.category, test->info.summary, test_result2str[test->state]);
615 AST_LIST_UNLOCK(&tests);
616 ast_cli(a->fd, "%d Registered Tests Matched\n", count);
624 static char *test_cli_execute_registered(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
626 static const char * const option1[] = { "all", "category", NULL };
627 static const char * const option2[] = { "name", NULL };
630 e->command = "test execute";
632 "Usage: test execute can be used in three ways.\n"
633 " 1. 'test execute all' runs all registered tests\n"
634 " 2. 'test execute category [test category]' runs all tests in the given\n"
636 " 3. 'test execute category [test category] name [test name]' runs all\n"
637 " tests in a given category matching a given name\n";
641 return ast_cli_complete(a->word, option1, a->n);
644 return ast_cli_complete(a->word, option2, a->n);
649 if (a->argc < 3|| a->argc > 6) {
650 return CLI_SHOWUSAGE;
653 if ((a->argc == 3) && !strcmp(a->argv[2], "all")) { /* run all registered tests */
654 ast_cli(a->fd, "Running all available tests...\n\n");
655 test_execute_multiple(NULL, NULL, a);
656 } else if (a->argc == 4) { /* run only tests within a category */
657 ast_cli(a->fd, "Running all available tests matching category %s\n\n", a->argv[3]);
658 test_execute_multiple(NULL, a->argv[3], a);
659 } else if (a->argc == 6) { /* run only a single test matching the category and name */
660 ast_cli(a->fd, "Running all available tests matching category %s and name %s\n\n", a->argv[5], a->argv[3]);
661 test_execute_multiple(a->argv[5], a->argv[3], a);
663 return CLI_SHOWUSAGE;
666 AST_LIST_LOCK(&tests);
667 if (!(last_results.last_passed + last_results.last_failed)) {
668 ast_cli(a->fd, "--- No Tests Found! ---\n");
670 ast_cli(a->fd, "\n%d Test(s) Executed %d Passed %d Failed\n",
671 (last_results.last_passed + last_results.last_failed),
672 last_results.last_passed,
673 last_results.last_failed);
674 AST_LIST_UNLOCK(&tests);
682 static char *test_cli_show_results(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
684 #define FORMAT_RES_ALL "%s%s %-30.30s %-25.25s %-40.40s\n"
685 static const char * const option1[] = { "all", "failed", "passed", NULL };
686 char result_buf[32] = { 0 };
687 struct ast_test *test = NULL;
690 int mode; /* 0 for show all, 1 for show fail, 2 for show passed */
694 e->command = "test show results";
696 "Usage: test show results can be used in three ways\n"
697 " 1. 'test show results all' Displays results for all executed tests.\n"
698 " 2. 'test show results passed' Displays results for all passed tests.\n"
699 " 3. 'test show results failed' Displays results for all failed tests.\n";
703 return ast_cli_complete(a->word, option1, a->n);
710 return CLI_SHOWUSAGE;
711 } else if (!strcmp(a->argv[3], "passed")) {
713 } else if (!strcmp(a->argv[3], "failed")) {
715 } else if (!strcmp(a->argv[3], "all")) {
718 return CLI_SHOWUSAGE;
721 ast_cli(a->fd, FORMAT_RES_ALL, "Result", "", "Name", "Category", "Error Description");
722 AST_LIST_LOCK(&tests);
723 AST_LIST_TRAVERSE(&tests, test, entry) {
724 if (test->state == AST_TEST_NOT_RUN) {
727 test->state == AST_TEST_FAIL ? failed++ : passed++;
728 if (!mode || ((mode == 1) && (test->state == AST_TEST_FAIL)) || ((mode == 2) && (test->state == AST_TEST_PASS))) {
729 /* give our results pretty colors */
730 term_color(result_buf, test_result2str[test->state],
731 (test->state == AST_TEST_FAIL) ? COLOR_RED : COLOR_GREEN,
732 0, sizeof(result_buf));
734 ast_cli(a->fd, FORMAT_RES_ALL,
739 (test->state == AST_TEST_FAIL) ? S_OR(ast_str_buffer(test->args.ast_test_error_str), "Not Avaliable") : "");
742 AST_LIST_UNLOCK(&tests);
744 ast_cli(a->fd, "%d Test(s) Executed %d Passed %d Failed\n", (failed + passed), passed, failed);
751 static char *test_cli_generate_results(struct ast_cli_entry *e, int cmd, struct ast_cli_args *a)
753 static const char * const option[] = { "xml", "txt", NULL };
754 const char *file = NULL;
755 const char *type = "";
758 struct ast_str *buf = NULL;
759 struct timeval time = ast_tvnow();
763 e->command = "test generate results";
765 "Usage: 'test generate results'\n"
766 " Generates test results in either xml or txt format. An optional \n"
767 " file path may be provided to specify the location of the xml or\n"
769 " \nExample usage:\n"
770 " 'test generate results xml' this writes to a default file\n"
771 " 'test generate results xml /path/to/file.xml' writes to specified file\n";
775 return ast_cli_complete(a->word, option, a->n);
781 if (a->argc < 4 || a->argc > 5) {
782 return CLI_SHOWUSAGE;
783 } else if (!strcmp(a->argv[3], "xml")) {
786 } else if (!strcmp(a->argv[3], "txt")) {
789 return CLI_SHOWUSAGE;
795 if (!(buf = ast_str_create(256))) {
798 ast_str_set(&buf, 0, "%s/asterisk_test_results-%ld.%s", ast_config_AST_LOG_DIR, time.tv_sec, type);
800 file = ast_str_buffer(buf);
804 res = test_generate_results(NULL, NULL, file, NULL);
806 res = test_generate_results(NULL, NULL, NULL, file);
810 ast_cli(a->fd, "Results Generated Successfully: %s\n", S_OR(file, ""));
812 ast_cli(a->fd, "Results Could Not Be Generated: %s\n", S_OR(file, ""));
823 static struct ast_cli_entry test_cli[] = {
824 AST_CLI_DEFINE(test_cli_show_registered, "show registered tests"),
825 AST_CLI_DEFINE(test_cli_execute_registered, "execute registered tests"),
826 AST_CLI_DEFINE(test_cli_show_results, "show last test results"),
827 AST_CLI_DEFINE(test_cli_generate_results, "generate test results to file"),
829 #endif /* TEST_FRAMEWORK */
833 #ifdef TEST_FRAMEWORK
834 /* Register cli commands */
835 ast_cli_register_multiple(test_cli, ARRAY_LEN(test_cli));