[asterisk-commits] mjordan: branch mjordan/cdrs-of-doom r384475 - in /team/mjordan/cdrs-of-doom:...

SVN commits to the Asterisk project asterisk-commits at lists.digium.com
Mon Apr 1 12:02:59 CDT 2013


Author: mjordan
Date: Mon Apr  1 12:02:56 2013
New Revision: 384475

URL: http://svnview.digium.com/svn/asterisk?view=rev&rev=384475
Log:
Update dial unit tests

Now with actual working parallel unanswered CDRs. And unit tests that
don't block waiting for a CDR that never comes. And that take into
account the backends weird way of dispatching CDRs that have a chain
but that still dispatch each CDR in that chain.
 A->B->C
 B->C
 C
 Ew.

Modified:
    team/mjordan/cdrs-of-doom/main/cdr.c
    team/mjordan/cdrs-of-doom/tests/test_cdr.c

Modified: team/mjordan/cdrs-of-doom/main/cdr.c
URL: http://svnview.digium.com/svn/asterisk/team/mjordan/cdrs-of-doom/main/cdr.c?view=diff&rev=384475&r1=384474&r2=384475
==============================================================================
--- team/mjordan/cdrs-of-doom/main/cdr.c (original)
+++ team/mjordan/cdrs-of-doom/main/cdr.c Mon Apr  1 12:02:56 2013
@@ -202,7 +202,7 @@
 
 /*! \brief The configuration settings for this module */
 struct module_config {
-	struct cdr_config *general;		/*< CDR global settings */
+	struct ast_cdr_config *general;		/*< CDR global settings */
 };
 
 /*! \brief The container for the module configuration */
@@ -247,7 +247,7 @@
 static void *module_config_alloc(void)
 {
 	struct module_config *mod_cfg;
-	struct cdr_config *cdr_config;
+	struct ast_cdr_config *cdr_config;
 
 	mod_cfg = ao2_alloc(sizeof(*mod_cfg), module_config_destructor);
 	if (!mod_cfg) {
@@ -313,13 +313,13 @@
 	void (* const init_function)(struct cdr_object *cdr);
 	void (* const process_channel_message)(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
 	void (* const process_channel_varset_message)(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot, const char *name, const char *value);
-	void (* const process_dial_message)(struct cdr_object *cdr, struct ast_channel_dial *dial_message);
+	int (* const process_dial_message)(struct cdr_object *cdr, struct ast_channel_dial *dial_message);
 };
 
 static void single_state_init_function(struct cdr_object *cdr);
 static void single_state_process_channel_message(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
 static void single_state_process_channel_varset_message(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot, const char *name, const char *value);
-static void single_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *dial_message);
+static int single_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *dial_message);
 
 struct cdr_object_fn_table single_state_fn_table = {
 	.name = "Single",
@@ -331,7 +331,7 @@
 
 static void dial_state_init_function(struct cdr_object *cdr);
 static void dial_state_process_channel_message(struct cdr_object *cdr, struct ast_channel_snapshot *snapshot);
-static void dial_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *dial_message);
+static int dial_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *dial_message);
 
 struct cdr_object_fn_table dial_state_fn_table = {
 	.name = "Dial",
@@ -393,7 +393,7 @@
 	return strcasecmp(left->name, match) ? 0 : (CMP_MATCH | CMP_STOP);
 }
 
-static void cdr_object_destructor(void *obj)
+static void cdr_object_dtor(void *obj)
 {
 	struct cdr_object *cdr = obj;
 	struct ast_var_t *it_var;
@@ -419,11 +419,12 @@
 
 static struct cdr_object *cdr_object_alloc(struct ast_channel_snapshot *chan)
 {
+	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	struct cdr_object *cdr;
 
 	ast_assert(chan != NULL);
 
-	cdr = ao2_alloc(sizeof(*cdr), cdr_object_destructor);
+	cdr = ao2_alloc(sizeof(*cdr), cdr_object_dtor);
 	if (!cdr) {
 		return NULL;
 	}
@@ -434,6 +435,8 @@
 	ast_string_field_set(cdr, linkedid, chan->linkedid);
 	ao2_ref(cdr->party_a, +1);
 
+	CDR_DEBUG(mod_cfg, "%p - created CDR for channel %s\n", cdr, chan->name);
+
 	transition_state(cdr, &single_state_fn_table);
 
 	return cdr;
@@ -449,6 +452,9 @@
 	}
 
 	/* TODO copy the variables over */
+	while (cdr->next) {
+		cdr = cdr->next;
+	}
 	cdr->next = new_cdr;
 	return new_cdr;
 }
@@ -538,7 +544,8 @@
 	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	struct ast_cdr *pub_cdr;
 
-	CDR_DEBUG(mod_cfg, "%p - Dispatching CDR for Party A %s\n", cdr, cdr->party_a->name);
+	CDR_DEBUG(mod_cfg, "%p - Dispatching CDR for Party A %s, Party B %s\n", cdr, cdr->party_a->name,
+			cdr->party_b ? cdr->party_b->name : "<none>");
 	pub_cdr = create_public_cdr_records(cdr);
 	ast_cdr_detach(pub_cdr);
 }
@@ -547,7 +554,6 @@
 {
 	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 
-	/* If the end time is set, we've been finalized already */
 	if (ast_tvzero(cdr->timestamps[CDR_OBJECT_TIME_FINALIZED])) {
 		cdr->timestamps[CDR_OBJECT_TIME_FINALIZED] = ast_tvnow();
 	}
@@ -655,14 +661,14 @@
 	return;
 }
 
-static void single_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *message)
+static int single_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *message)
 {
 	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	struct ast_channel_snapshot *caller = message->caller;
 	struct ast_channel_snapshot *peer = message->peer;
 
+	/* We shouldn't get a Dial end without first transitioning to a dial state */
 	ast_assert(ast_strlen_zero(message->dialstatus));
-	ast_assert(!ast_strlen_zero(message->dialstring));
 
 	if (caller && !strcmp(cdr->party_a->name, caller->name)) {
 		cdr->party_a = swap_channel_snapshot(cdr->party_a, caller);
@@ -676,6 +682,7 @@
 	}
 
 	transition_state(cdr, &dial_state_fn_table);
+	return 0;
 }
 
 /* DIAL STATE */
@@ -694,7 +701,6 @@
 	} else if (cdr->party_b && !strcmp(cdr->party_b->name, snapshot->name)) {
 		cdr->party_b = swap_channel_snapshot(cdr->party_b, snapshot);
 	} else {
-		ast_log(AST_LOG_WARNING, "WTF\n");
 		ast_assert(0);
 	}
 
@@ -702,56 +708,50 @@
 	check_for_hangup(cdr);
 }
 
-static void dial_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *dial_message)
-{
+static int dial_state_process_dial_message(struct cdr_object *cdr, struct ast_channel_dial *dial_message)
+{
+	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	struct ast_channel_snapshot *caller = dial_message->caller;
 	struct ast_channel_snapshot *peer = dial_message->peer;
-	struct cdr_object *new_cdr;
-
-	ast_assert(!strcmp(cdr->party_a->name, caller->name));
-	cdr->party_a = swap_channel_snapshot(cdr->party_a, caller);
-
-	if (!ast_strlen_zero(dial_message->dialstatus)) {
+	struct ast_channel_snapshot *party_a;
+
+	if (caller) {
+		party_a = caller;
+	} else {
+		party_a = peer;
+	}
+	ast_assert(!strcmp(cdr->party_a->name, party_a->name));
+	cdr->party_a = swap_channel_snapshot(cdr->party_a, party_a);
+
+	if (ast_strlen_zero(dial_message->dialstatus)) {
+		/* If this isn't a dial end, don't process it here */
+		return 1;
+	}
+	if (cdr->party_b) {
 		if (strcmp(cdr->party_b->name, peer->name)) {
-			/* Not our status, defer to the next CDR in the chain */
-			ast_assert(cdr->next != NULL);
-			ast_assert(cdr->next->fn_table->process_dial_message != NULL);
-			cdr->next->fn_table->process_dial_message(cdr->next, dial_message);
-			return;
-		}
-
+			/* Not the status for this CDR */
+			return 1;
+		}
 		cdr->party_b = swap_channel_snapshot(cdr->party_b, peer);
-		/* Set the party A hangup cause based on the dial string. A subsequent hangup
-		 * message will update this if needed. This will at least let us set the CDR
-		 * disposition properly.
-		 */
-		if (!strcmp(dial_message->dialstatus, "BUSY")) {
-			cdr->party_a->hangupcause = AST_CAUSE_BUSY;
-			finalize_cdr_object(cdr);
-		} else if (!strcmp(dial_message->dialstatus, "CANCEL")) {
-			cdr->party_a->hangupcause = AST_CAUSE_NORMAL;
-			finalize_cdr_object(cdr);
-		} else if (!strcmp(dial_message->dialstatus, "CONGESTION")) {
-			cdr->party_a->hangupcause = AST_CAUSE_CONGESTION;
-			finalize_cdr_object(cdr);
-		} else if (!strcmp(dial_message->dialstatus, "FAILED")) {
-			cdr->party_a->hangupcause = AST_CAUSE_NO_ROUTE_DESTINATION;
-			finalize_cdr_object(cdr);
-		}
-		/* TODO: if ANSWER, do NOTHING? */
-	}
-	if (!ast_strlen_zero(dial_message->dialstring)) {
-		struct cdr_object *it_cdr = cdr;
-		/* If we're in a dial and we get a new dial, then ... yay! new CDR. */
-		/* No new dials if we aren't dialing someone ... */
-		ast_assert(peer != NULL);
-		while (it_cdr->next != NULL) {
-			it_cdr = it_cdr->next;
-		}
-		new_cdr = create_and_append_cdr(it_cdr);
-		new_cdr->party_b = swap_channel_snapshot(new_cdr->party_b, peer);
-		transition_state(new_cdr, &dial_state_fn_table);
-	}
+	}
+	/* Set the party A hangup cause based on the dial string. A
+	 * subsequent hangup message will update this if needed.
+	 */
+	if (!strcmp(dial_message->dialstatus, "BUSY")) {
+		cdr->party_a->hangupcause = AST_CAUSE_BUSY;
+		finalize_cdr_object(cdr);
+	} else if (!strcmp(dial_message->dialstatus, "CANCEL")) {
+		cdr->party_a->hangupcause = AST_CAUSE_NORMAL;
+		finalize_cdr_object(cdr);
+	} else if (!strcmp(dial_message->dialstatus, "CONGESTION")) {
+		cdr->party_a->hangupcause = AST_CAUSE_CONGESTION;
+		finalize_cdr_object(cdr);
+	} else if (!strcmp(dial_message->dialstatus, "FAILED")) {
+		cdr->party_a->hangupcause = AST_CAUSE_NO_ROUTE_DESTINATION;
+		finalize_cdr_object(cdr);
+	}
+	/* TODO: if ANSWER, do NOTHING? */
+	return 0;
 }
 
 
@@ -815,6 +815,8 @@
 	struct ast_channel_dial *payload = stasis_message_data(message);
 	struct ast_channel_snapshot *party_a;
 	struct ast_channel_snapshot *party_b;
+	struct cdr_object *it_cdr;
+	int res = 1;
 
 	ast_assert(payload != NULL);
 
@@ -832,13 +834,33 @@
 
 	cdr = find_or_create_cdr_by_channel(party_a);
 	if (!cdr) {
-		ast_assert(0);
+		ast_debug(1, "No CDR for %s; passing on dial message\n", party_a->name);
 		return;
 	}
+
+	if (ast_test_flag(&party_a->flags, AST_FLAG_OUTGOING)
+		&& !ast_test_flag(&party_a->flags, AST_FLAG_ORIGINATED)) {
+		CDR_DEBUG(mod_cfg, "%p - removing CDR for outbound channel %s\n", cdr, party_a->name);
+		ao2_unlink(active_cdrs_by_channel, cdr);
+		return;
+	}
+
 	CDR_DEBUG(mod_cfg, "%p - processing dial message for channel %s, peer %s\n",
 			cdr, party_a->name, party_b ? party_b->name : "(none)");
-	if (cdr->fn_table->process_dial_message) {
-		cdr->fn_table->process_dial_message(cdr, payload);
+	for (it_cdr = cdr; res && it_cdr; it_cdr = it_cdr->next) {
+		if (it_cdr->fn_table->process_dial_message) {
+			res = it_cdr->fn_table->process_dial_message(it_cdr, payload);
+		}
+	}
+	/* If no CDR handled the dial message, make a new one */
+	if (res) {
+		struct cdr_object *new_cdr;
+
+		new_cdr = create_and_append_cdr(cdr);
+		if (!new_cdr) {
+			return;
+		}
+		new_cdr->fn_table->process_dial_message(new_cdr, payload);
 	}
 }
 
@@ -847,6 +869,7 @@
 	RAII_VAR(struct cdr_object *, cdr, NULL, ao2_cleanup);
 	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	struct stasis_cache_update *update = stasis_message_data(message);
+	struct cdr_object *it_cdr;
 
 	ast_assert(update != NULL);
 	if (ast_channel_snapshot() == update->type) {
@@ -856,26 +879,37 @@
 			stasis_message_data(update->new_snapshot);
 		const char *name = new_snapshot ? new_snapshot->name : old_snapshot->name;
 
+		if (new_snapshot && ast_test_flag(&new_snapshot->flags, AST_FLAG_OUTGOING)
+			&& !ast_test_flag(&new_snapshot->flags, AST_FLAG_ORIGINATED)) {
+			cdr = ao2_find(active_cdrs_by_channel, name, OBJ_KEY);
+			if (cdr) {
+				CDR_DEBUG(mod_cfg, "%p - removing CDR for outbound channel %s\n", cdr, name);
+				ao2_unlink(active_cdrs_by_channel, cdr);
+			}
+			return;
+		}
+
 		if (new_snapshot && !old_snapshot) {
 			cdr = cdr_object_alloc(new_snapshot);
 			if (!cdr) {
 				return;
 			}
-			CDR_DEBUG(mod_cfg, "%p - created CDR for channel %s\n", cdr, name);
 			ao2_link(active_cdrs_by_channel, cdr);
 		}
 
 		if (!cdr) {
 			cdr = ao2_find(active_cdrs_by_channel, name, OBJ_KEY);
 			if (!cdr) {
-				ast_log(AST_LOG_WARNING, "Unable to find CDR for channel %s\n", name);
+				ast_debug(1, "No CDR for channel %s\n", name);
 				return;
 			}
 		}
 
-		if (cdr->fn_table->process_channel_message && new_snapshot) {
-			CDR_DEBUG(mod_cfg, "%p - processing new channel snapshot %s\n", cdr, new_snapshot->name);
-			cdr->fn_table->process_channel_message(cdr, new_snapshot);
+		for (it_cdr = cdr; it_cdr; it_cdr = it_cdr->next) {
+			if (it_cdr->fn_table->process_channel_message && new_snapshot) {
+				CDR_DEBUG(mod_cfg, "%p - processing new channel snapshot %s\n", it_cdr, new_snapshot->name);
+				it_cdr->fn_table->process_channel_message(it_cdr, new_snapshot);
+			}
 		}
 
 		if (!new_snapshot) {
@@ -895,23 +929,19 @@
 
 
 
-struct cdr_config *ast_cdr_get_config(void)
+struct ast_cdr_config *ast_cdr_get_config(void)
 {
 	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	ao2_ref(mod_cfg->general, +1);
 	return mod_cfg->general;
 }
 
-void ast_cdr_set_config(struct cdr_config *config)
-{
-	RAII_VAR(struct module_config *, mod_cfg, module_config_alloc(), ao2_cleanup);
-
-	if (!mod_cfg) {
-		return;
-	}
+void ast_cdr_set_config(struct ast_cdr_config *config)
+{
+	RAII_VAR(struct module_config *, mod_cfg, ao2_global_obj_ref(module_configs), ao2_cleanup);
 	ao2_cleanup(mod_cfg->general);
 	mod_cfg->general = config;
-	ao2_global_obj_replace_unref(module_configs, mod_cfg);
+	ao2_ref(mod_cfg->general, +1);
 }
 
 int check_cdr_enabled(void)
@@ -2165,16 +2195,16 @@
 			return 1;
 		}
 
-		aco_option_register(&cfg_info, "enable", ACO_EXACT, general_options, DEFAULT_ENABLED, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, settings), CDR_ENABLED);
-		aco_option_register(&cfg_info, "unanswered", ACO_EXACT, general_options, DEFAULT_UNANSWERED, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, settings), CDR_UNANSWERED);
-		aco_option_register(&cfg_info, "congestion", ACO_EXACT, general_options, DEFAULT_CONGESTION, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, settings), CDR_CONGESTION);
-		aco_option_register(&cfg_info, "batch", ACO_EXACT, general_options, DEFAULT_BATCHMODE, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, settings), CDR_BATCHMODE);
-		aco_option_register(&cfg_info, "endbeforehexten", ACO_EXACT, general_options, DEFAULT_END_BEFORE_H_EXTEN, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, settings), CDR_END_BEFORE_H_EXTEN);
-		aco_option_register(&cfg_info, "initiatedseconds", ACO_EXACT, general_options, DEFAULT_INITIATED_SECONDS, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, settings), CDR_INITIATED_SECONDS);
-		aco_option_register(&cfg_info, "scheduleronly", ACO_EXACT, general_options, DEFAULT_BATCH_SCHEDULER_ONLY, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, batch_settings.settings), BATCH_MODE_SCHEDULER_ONLY);
-		aco_option_register(&cfg_info, "safeshutdown", ACO_EXACT, general_options, DEFAULT_BATCH_SAFE_SHUTDOWN, OPT_BOOLFLAG_T, 1, FLDSET(struct cdr_config, batch_settings.settings), BATCH_MODE_SAFE_SHUTDOWN);
-		aco_option_register(&cfg_info, "size", ACO_EXACT, general_options, DEFAULT_BATCH_SIZE, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct cdr_config, batch_settings.size), 0, MAX_BATCH_SIZE);
-		aco_option_register(&cfg_info, "time", ACO_EXACT, general_options, DEFAULT_BATCH_TIME, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct cdr_config, batch_settings.time), 0, MAX_BATCH_TIME);
+		aco_option_register(&cfg_info, "enable", ACO_EXACT, general_options, DEFAULT_ENABLED, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_ENABLED);
+		aco_option_register(&cfg_info, "unanswered", ACO_EXACT, general_options, DEFAULT_UNANSWERED, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_UNANSWERED);
+		aco_option_register(&cfg_info, "congestion", ACO_EXACT, general_options, DEFAULT_CONGESTION, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_CONGESTION);
+		aco_option_register(&cfg_info, "batch", ACO_EXACT, general_options, DEFAULT_BATCHMODE, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_BATCHMODE);
+		aco_option_register(&cfg_info, "endbeforehexten", ACO_EXACT, general_options, DEFAULT_END_BEFORE_H_EXTEN, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_END_BEFORE_H_EXTEN);
+		aco_option_register(&cfg_info, "initiatedseconds", ACO_EXACT, general_options, DEFAULT_INITIATED_SECONDS, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, settings), CDR_INITIATED_SECONDS);
+		aco_option_register(&cfg_info, "scheduleronly", ACO_EXACT, general_options, DEFAULT_BATCH_SCHEDULER_ONLY, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, batch_settings.settings), BATCH_MODE_SCHEDULER_ONLY);
+		aco_option_register(&cfg_info, "safeshutdown", ACO_EXACT, general_options, DEFAULT_BATCH_SAFE_SHUTDOWN, OPT_BOOLFLAG_T, 1, FLDSET(struct ast_cdr_config, batch_settings.settings), BATCH_MODE_SAFE_SHUTDOWN);
+		aco_option_register(&cfg_info, "size", ACO_EXACT, general_options, DEFAULT_BATCH_SIZE, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_cdr_config, batch_settings.size), 0, MAX_BATCH_SIZE);
+		aco_option_register(&cfg_info, "time", ACO_EXACT, general_options, DEFAULT_BATCH_TIME, OPT_UINT_T, PARSE_IN_RANGE, FLDSET(struct ast_cdr_config, batch_settings.time), 0, MAX_BATCH_TIME);
 	}
 
 	if (aco_process_config(&cfg_info, reload)) {
@@ -2212,7 +2242,7 @@
 	ao2_ref(active_cdrs_by_channel, -1);
 }
 
-static void cdr_enable_batch_mode(struct cdr_config *config)
+static void cdr_enable_batch_mode(struct ast_cdr_config *config)
 {
 	SCOPED_LOCK(batch, &cdr_batch_lock, ast_mutex_lock, ast_mutex_unlock);
 

Modified: team/mjordan/cdrs-of-doom/tests/test_cdr.c
URL: http://svnview.digium.com/svn/asterisk/team/mjordan/cdrs-of-doom/tests/test_cdr.c?view=diff&rev=384475&r1=384474&r2=384475
==============================================================================
--- team/mjordan/cdrs-of-doom/tests/test_cdr.c (original)
+++ team/mjordan/cdrs-of-doom/tests/test_cdr.c Mon Apr  1 12:02:56 2013
@@ -23,9 +23,24 @@
 #include "asterisk/causes.h"
 #include "asterisk/time.h"
 
+#define TEST_CATEGORY "/main/cdr/"
+
 #define MOCK_CDR_BACKEND "mock_cdr_backend"
 
 #define CHANNEL_TECH_NAME "CDRTestChannel"
+
+/*! \brief A placeholder for Asterisk's 'real' CDR configuration */
+static struct ast_cdr_config *saved_config;
+
+/*! \brief A configuration suitable for CDRs with unanswered records */
+static struct ast_cdr_config unanswered_cdr_config = {
+	.settings.flags = CDR_ENABLED | CDR_UNANSWERED | CDR_DEBUG,
+};
+
+#define SWAP_CONFIG(ao2_config, template) do { \
+	*(ao2_config) = (template); \
+	ast_cdr_set_config((ao2_config)); \
+	} while (0)
 
 static AST_LIST_HEAD(, test_cdr_entry) actual_cdr_entries = AST_LIST_HEAD_INIT_VALUE;
 
@@ -85,7 +100,6 @@
 	cdr_wrapper->cdr = mock_cdr;
 
 	AST_LIST_LOCK(&actual_cdr_entries);
-	ast_log(LOG_NOTICE, "Received CDR for mock backend: %d\n", mock_cdr_count + 1);
 	AST_LIST_INSERT_TAIL(&actual_cdr_entries, cdr_wrapper, list);
 	mock_cdr_count++;
 	ast_cond_signal(&mock_cdr_cond);
@@ -139,13 +153,11 @@
 	struct timespec wait_time = { .tv_sec = wait_now.tv_sec + 5, .tv_nsec = wait_now.tv_usec * 1000 };
 	enum ast_test_result_state res = AST_TEST_PASS;
 	while (count < record) {
-		if (mock_cdr_count == count) {
-			AST_LIST_LOCK(&actual_cdr_entries);
+		AST_LIST_LOCK(&actual_cdr_entries);
+		if (mock_cdr_count < record) {
 			ast_cond_timedwait(&mock_cdr_cond, &actual_cdr_entries.lock, &wait_time);
-			AST_LIST_UNLOCK(&actual_cdr_entries);
 		}
-		AST_LIST_LOCK(&actual_cdr_entries);
-		cdr_wrapper = AST_LIST_FIRST(&actual_cdr_entries);
+		cdr_wrapper = AST_LIST_REMOVE_HEAD(&actual_cdr_entries, list);
 		AST_LIST_UNLOCK(&actual_cdr_entries);
 
 		if (!cdr_wrapper) {
@@ -154,40 +166,32 @@
 		}
 		actual = cdr_wrapper->cdr;
 
-		for (; actual && expected; actual = actual->next, expected = expected->next) {
-			VERIFY_STRING_FIELD(accountcode, actual, expected);
-			VERIFY_NUMERIC_FIELD(amaflags, actual, expected);
-			VERIFY_STRING_FIELD(channel, actual, expected);
-			VERIFY_STRING_FIELD(clid, actual, expected);
-			VERIFY_STRING_FIELD(dcontext, actual, expected);
-			VERIFY_NUMERIC_FIELD(disposition, actual, expected);
-			VERIFY_STRING_FIELD(dst, actual, expected);
-			VERIFY_STRING_FIELD(dstchannel, actual, expected);
-			/*VERIFY_NUMERIC_FIELD(flags, actual, expected);*/
-			VERIFY_STRING_FIELD(lastapp, actual, expected);
-			VERIFY_STRING_FIELD(lastdata, actual, expected);
-			VERIFY_STRING_FIELD(linkedid, actual, expected);
-			VERIFY_STRING_FIELD(peeraccount, actual, expected);
-			VERIFY_STRING_FIELD(src, actual, expected);
-			VERIFY_STRING_FIELD(uniqueid, actual, expected);
-			VERIFY_STRING_FIELD(userfield, actual, expected);
-			VERIFY_TIME_VALUE(start, actual);
-			VERIFY_TIME_VALUE(end, actual);
-			/* Note: there's no way we can really calculate a duration or
-			 * billsec - the unit tests are too short. However, if billsec is
-			 * non-zero in the expected, then make sure we have an answer time
-			 */
-			if (expected->billsec) {
-				VERIFY_TIME_VALUE(answer, actual);
-			}
+		VERIFY_STRING_FIELD(accountcode, actual, expected);
+		VERIFY_NUMERIC_FIELD(amaflags, actual, expected);
+		VERIFY_STRING_FIELD(channel, actual, expected);
+		VERIFY_STRING_FIELD(clid, actual, expected);
+		VERIFY_STRING_FIELD(dcontext, actual, expected);
+		VERIFY_NUMERIC_FIELD(disposition, actual, expected);
+		VERIFY_STRING_FIELD(dst, actual, expected);
+		VERIFY_STRING_FIELD(dstchannel, actual, expected);
+		VERIFY_STRING_FIELD(lastapp, actual, expected);
+		VERIFY_STRING_FIELD(lastdata, actual, expected);
+		VERIFY_STRING_FIELD(linkedid, actual, expected);
+		VERIFY_STRING_FIELD(peeraccount, actual, expected);
+		VERIFY_STRING_FIELD(src, actual, expected);
+		VERIFY_STRING_FIELD(uniqueid, actual, expected);
+		VERIFY_STRING_FIELD(userfield, actual, expected);
+		VERIFY_TIME_VALUE(start, actual);
+		VERIFY_TIME_VALUE(end, actual);
+		/* Note: there's no way we can really calculate a duration or
+		 * billsec - the unit tests are too short. However, if billsec is
+		 * non-zero in the expected, then make sure we have an answer time
+		 */
+		if (expected->billsec) {
+			VERIFY_TIME_VALUE(answer, actual);
 		}
 
-		if (actual || expected) {
-			ast_test_status_update(test, "Unbalanced CDR records - %s had more than %s\n",
-				actual ? "actual" : "expected",
-				actual ? "actual" : "expected");
-			res = AST_TEST_FAIL;
-		}
+		expected = expected->next;
 		++count;
 	}
 
@@ -227,6 +231,8 @@
 AST_TEST_DEFINE(test_cdr_channel_creation)
 {
 	RAII_VAR(struct ast_channel *, chan, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_party_caller caller = ALICE_CALLERID;
 	struct ast_cdr expected = {
@@ -247,8 +253,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_channel_creation";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test that a CDR is created when a channel is created";
 		info->description =
 			"Test that a CDR is created when a channel is created";
@@ -257,6 +263,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_channel_set_caller(chan, &caller, NULL);
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan), sizeof(expected.uniqueid));
@@ -269,14 +277,14 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
 AST_TEST_DEFINE(test_cdr_unanswered_inbound_call)
 {
 	RAII_VAR(struct ast_channel *, chan, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_party_caller caller = ALICE_CALLERID;
 	struct ast_cdr expected = {
@@ -297,8 +305,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_unanswered_inbound_call";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test inbound unanswered calls";
 		info->description =
 			"Test the properties of a CDR for a call that is\n"
@@ -309,6 +317,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_channel_set_caller(chan, &caller, NULL);
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan), sizeof(expected.uniqueid));
@@ -327,14 +337,14 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
 AST_TEST_DEFINE(test_cdr_unanswered_outbound_call)
 {
 	RAII_VAR(struct ast_channel *, chan, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr expected = {
 		.clid = "\"\" <>",
@@ -354,8 +364,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_unanswered_outbound_call";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test outbound unanswered calls";
 		info->description =
 			"Test the properties of a CDR for a call that is\n"
@@ -364,6 +374,8 @@
 	case TEST_EXECUTE:
 		break;
 	}
+
+	SWAP_CONFIG(config, unanswered_cdr_config);
 
 	chan = ast_channel_alloc(0, AST_STATE_DOWN, NULL, NULL, "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan), sizeof(expected.uniqueid));
@@ -380,8 +392,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -409,8 +419,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_single_party";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test cdrs for a single party";
 		info->description =
 			"Test the properties of a CDR for a call that is\n"
@@ -448,8 +458,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -457,6 +465,8 @@
 {
 	RAII_VAR(struct ast_channel *, chan_caller, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_callee, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr expected = {
 		.clid = "\"Alice\" <100>",
@@ -477,8 +487,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_dial_unanswered";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test CDRs for a dial that isn't answered";
 		info->description =
 			"Test the properties of a CDR for a channel that\n"
@@ -488,6 +498,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan_caller = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(expected.uniqueid));
 	ast_copy_string(expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(expected.linkedid));
@@ -525,8 +537,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -535,6 +545,8 @@
 {
 	RAII_VAR(struct ast_channel *, chan_caller, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_callee, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr expected = {
 		.clid = "\"Alice\" <100>",
@@ -555,8 +567,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_dial_busy";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test CDRs for a dial that results in a busy";
 		info->description =
 			"Test the properties of a CDR for a channel that\n"
@@ -566,6 +578,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan_caller = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(expected.uniqueid));
 	ast_copy_string(expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(expected.linkedid));
@@ -603,8 +617,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -612,6 +624,8 @@
 {
 	RAII_VAR(struct ast_channel *, chan_caller, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_callee, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr expected = {
 		.clid = "\"Alice\" <100>",
@@ -632,8 +646,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_dial_congestion";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test CDRs for a dial that results in congestion";
 		info->description =
 			"Test the properties of a CDR for a channel that\n"
@@ -643,6 +657,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan_caller = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(expected.uniqueid));
 	ast_copy_string(expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(expected.linkedid));
@@ -680,8 +696,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -689,6 +703,8 @@
 {
 	RAII_VAR(struct ast_channel *, chan_caller, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_callee, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr expected = {
 		.clid = "\"Alice\" <100>",
@@ -709,8 +725,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_dial_unavailable";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test CDRs for a dial that results in unavailable";
 		info->description =
 			"Test the properties of a CDR for a channel that\n"
@@ -720,6 +736,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan_caller = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(expected.uniqueid));
 	ast_copy_string(expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(expected.linkedid));
@@ -757,8 +775,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -766,6 +782,8 @@
 {
 	RAII_VAR(struct ast_channel *, chan_caller, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_callee, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr expected = {
 		.clid = "\"Alice\" <100>",
@@ -786,8 +804,8 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_dial_caller_cancel";
-		info->category = "/main/cdr/";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
 		info->summary = "Test CDRs for a dial where the caller cancels";
 		info->description =
 			"Test the properties of a CDR for a channel that\n"
@@ -798,6 +816,8 @@
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan_caller = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
 	ast_copy_string(expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(expected.uniqueid));
 	ast_copy_string(expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(expected.linkedid));
@@ -837,8 +857,6 @@
 
 	result = verify_mock_cdr_record(test, &expected, 1);
 
-	clear_mock_cdr_backend();
-
 	return result;
 }
 
@@ -848,6 +866,8 @@
 	RAII_VAR(struct ast_channel *, chan_bob, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_charlie, NULL, safe_channel_release);
 	RAII_VAR(struct ast_channel *, chan_david, NULL, safe_channel_release);
+	RAII_VAR(struct ast_cdr_config *, config, ao2_alloc(sizeof(*config), NULL),
+			ao2_cleanup);
 
 	struct ast_cdr bob_expected = {
 		.clid = "\"Alice\" <100>",
@@ -903,19 +923,27 @@
 
 	switch (cmd) {
 	case TEST_INIT:
-		info->name = "cdr_dial_parallel_failed";
-		info->category = "/main/cdr/";
-		info->summary = "blah";
+		info->name = __func__;
+		info->category = TEST_CATEGORY;
+		info->summary = "Test a parallel dial where all channels fail to answer";
 		info->description =
-			"blah\n";
+			"This tests dialing three parties: Bob, Charlie, David. Charlie\n"
+			"returns BUSY; David returns CONGESTION; Bob fails to answer and\n"
+			"Alice hangs up. Three records are created for Alice as a result.\n";
 		return AST_TEST_NOT_RUN;
 	case TEST_EXECUTE:
 		break;
 	}
 
+	SWAP_CONFIG(config, unanswered_cdr_config);
+
 	chan_caller = ast_channel_alloc(0, AST_STATE_DOWN, "100", "Alice", "100", "100", "default", NULL, 0, CHANNEL_TECH_NAME "/Alice");
-	ast_copy_string(expected->uniqueid, ast_channel_uniqueid(chan_caller), sizeof(expected->uniqueid));
-	ast_copy_string(expected->linkedid, ast_channel_linkedid(chan_caller), sizeof(expected->linkedid));
+	ast_copy_string(bob_expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(bob_expected.uniqueid));
+	ast_copy_string(bob_expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(bob_expected.linkedid));
+	ast_copy_string(charlie_expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(charlie_expected.uniqueid));
+	ast_copy_string(charlie_expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(charlie_expected.linkedid));
+	ast_copy_string(david_expected.uniqueid, ast_channel_uniqueid(chan_caller), sizeof(david_expected.uniqueid));
+	ast_copy_string(david_expected.linkedid, ast_channel_linkedid(chan_caller), sizeof(david_expected.linkedid));
 
 	/* Channel enters Dial app */
 	ast_channel_appl_set(chan_caller, "Dial");
@@ -928,16 +956,19 @@
 	ast_set_flag(ast_channel_flags(chan_bob), AST_FLAG_OUTGOING);
 	ast_channel_appl_set(chan_bob, "AppDial");
 	ast_channel_data_set(chan_bob, "(Outgoing Line)");
+	publish_channel_snapshot(chan_bob);
 
 	chan_charlie = ast_channel_alloc(0, AST_STATE_DOWN, NULL, NULL, "300", NULL, NULL, ast_channel_linkedid(chan_caller), 0, CHANNEL_TECH_NAME "/Charlie");
 	ast_set_flag(ast_channel_flags(chan_charlie), AST_FLAG_OUTGOING);
 	ast_channel_appl_set(chan_charlie, "AppDial");
 	ast_channel_data_set(chan_charlie, "(Outgoing Line)");
+	publish_channel_snapshot(chan_charlie);
 
 	chan_david = ast_channel_alloc(0, AST_STATE_DOWN, NULL, NULL, "400", NULL, NULL, ast_channel_linkedid(chan_caller), 0, CHANNEL_TECH_NAME "/David");
 	ast_set_flag(ast_channel_flags(chan_david), AST_FLAG_OUTGOING);
 	ast_channel_appl_set(chan_david, "AppDial");
 	ast_channel_data_set(chan_david, "(Outgoing Line)");
+	publish_channel_snapshot(chan_david);
 
 	/* Dial starts */
 	ast_channel_publish_dial(chan_caller, chan_bob, "Bob", NULL);
@@ -955,10 +986,17 @@
 	}
 
 	/* David is congested */
-	ast_channel_publish_dial(chan_caller, chan_david, NULL, "CONGESTED");
+	ast_channel_publish_dial(chan_caller, chan_david, NULL, "CONGESTION");
 	ast_channel_hangupcause_set(chan_david, AST_CAUSE_CONGESTION);
 	if (!ast_hangup(chan_david)) {
 		chan_david = NULL;
+	}
+
+	/* Bob is canceled */
+	ast_channel_publish_dial(chan_caller, chan_bob, NULL, "CANCEL");
+	ast_channel_hangupcause_set(chan_bob, AST_CAUSE_NORMAL);
+	if (!ast_hangup(chan_bob)) {
+		chan_bob = NULL;
 	}
 
 	/* Alice hangs up */
@@ -967,18 +1005,34 @@
 		chan_caller = NULL;
 	}
 
-	/* Bob is cancelled */
-	ast_channel_publish_dial(chan_caller, chan_bob, NULL, "CANCEL");
-	ast_channel_hangupcause_set(chan_bob, AST_CAUSE_NORMAL);
-	if (!ast_hangup(chan_bob)) {
-		chan_bob = NULL;
-	}
-
-	result = verify_mock_cdr_record(test, expected, 1);
-
+	result = verify_mock_cdr_record(test, expected, 3);
+
+	return result;
+}
+
+/*!
+ * \internal \brief Callback function called before each test executes
+ */
+static int test_cdr_init_cb(struct ast_test_info *info, struct ast_test *test)
+{
+	/* Back up the real config */
+	saved_config = ast_cdr_get_config();
 	clear_mock_cdr_backend();
-
-	return result;
+	return 0;
+}
+
+/*!
+ * \internal \brief Callback function called after each test executes
+ */
+static int test_cdr_cleanup_cb(struct ast_test_info *info, struct ast_test *test)
+{
+	/* Restore the real config */
+	ast_cdr_set_config(saved_config);
+	ao2_cleanup(saved_config);
+	saved_config = NULL;
+	clear_mock_cdr_backend();
+
+	return 0;
 }
 
 
@@ -1017,8 +1071,10 @@
 	AST_TEST_REGISTER(test_cdr_dial_caller_cancel);
 	AST_TEST_REGISTER(test_cdr_dial_parallel_failed);
 
+	ast_test_register_init(TEST_CATEGORY, test_cdr_init_cb);
+	ast_test_register_cleanup(TEST_CATEGORY, test_cdr_cleanup_cb);
+
 	ast_channel_register(&test_cdr_chan_tech);
-
 	ast_cdr_register(MOCK_CDR_BACKEND, "Mock CDR backend", mock_cdr_backend);
 
 	return AST_MODULE_LOAD_SUCCESS;




More information about the asterisk-commits mailing list