diff --git a/build_tools/cflags-devmode.xml b/build_tools/cflags-devmode.xml
index 0b0f5e9078502b15d51f39666c38cce208d8c4ec..37f7a4d4f40be1cb352a6f8977793a7f6fd315d1 100644
--- a/build_tools/cflags-devmode.xml
+++ b/build_tools/cflags-devmode.xml
@@ -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>
diff --git a/include/asterisk/_private.h b/include/asterisk/_private.h
index b6b26597e9ce94a1b9561298802deed0207f8e26..cb1b61810890d230e7a8969a8fb852f40d6351f2 100644
--- a/include/asterisk/_private.h
+++ b/include/asterisk/_private.h
@@ -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
index 0000000000000000000000000000000000000000..bba40bdb812ea2f0d51a5fe1f64e4036d1725613
--- /dev/null
+++ b/include/asterisk/test.h
@@ -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 */
diff --git a/main/asterisk.c b/main/asterisk.c
index f8f3fa4680f1aa75377b6e326e8cbc5928942b40..5509788e25e02094ce71c47da363a5b99e10715a 100644
--- a/main/asterisk.c
+++ b/main/asterisk.c
@@ -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
index 0000000000000000000000000000000000000000..3e5f155a2869c68076d2269fd9c047a3d8fd3c15
--- /dev/null
+++ b/main/test.c
@@ -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;
+}
diff --git a/tests/test_heap.c b/tests/test_heap.c
index 54fd2b02741d8c1e01fc5a188d63b90cdafa1c6d..0757f29f77b812245dc15a53cb2a4b5be5f25411 100644
--- a/tests/test_heap.c
+++ b/tests/test_heap.c
@@ -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;
 }