[asterisk-commits] mjordan: testsuite/asterisk/trunk r3282 - in /asterisk/trunk: ./ lib/python/a...
SVN commits to the Asterisk project
asterisk-commits at lists.digium.com
Mon Jun 25 14:47:27 CDT 2012
Author: mjordan
Date: Mon Jun 25 14:47:20 2012
New Revision: 3282
URL: http://svnview.digium.com/svn/testsuite?view=rev&rev=3282
Log:
Asterisk Test Suite Pluggable Framework (and Batch CDR test)
This patch adds a mechanism for the Asterisk Test Suite to execute tests
without a run-test script. Instead, the tests are driven completely by
their configuration, as defined in their test-config.yaml file. The test
configurations define pluggable components that are instantiated at run time
by a new module, TestRunner. This allows for more sharing of code between
tests, less code duplication, and potentially quicker test writing and
execution times.
As a proof of concept, the CDR tests were migrated over to use this new
framework. An existing test class, SimpleTestCase, was modified to act
as the primary test object, and a new class, CDRModule, was added to the
cdr module to support CDR verification.
Finally, a new test, batch_cdrs, was added to cover batch creation of
CDR entries.
Review: https://reviewboard.asterisk.org/r/1995/
Added:
asterisk/trunk/lib/python/asterisk/TestRunner.py (with props)
asterisk/trunk/tests/cdr/ForkCdrModule.py (with props)
asterisk/trunk/tests/cdr/batch_cdrs/
asterisk/trunk/tests/cdr/batch_cdrs/configs/
asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/
asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf (with props)
asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf (with props)
asterisk/trunk/tests/cdr/batch_cdrs/test-config.yaml (with props)
Removed:
asterisk/trunk/tests/cdr/cdr_accountcode/run-test
asterisk/trunk/tests/cdr/cdr_fork_end_time/run-test
asterisk/trunk/tests/cdr/cdr_originate_sip_congestion_log/run-test
asterisk/trunk/tests/cdr/cdr_unanswered_yes/run-test
asterisk/trunk/tests/cdr/cdr_userfield/run-test
asterisk/trunk/tests/cdr/console_dial_sip_answer/run-test
asterisk/trunk/tests/cdr/console_dial_sip_busy/run-test
asterisk/trunk/tests/cdr/console_dial_sip_congestion/run-test
asterisk/trunk/tests/cdr/console_dial_sip_transfer/run-test
asterisk/trunk/tests/cdr/console_fork_after_busy_forward/run-test
asterisk/trunk/tests/cdr/console_fork_before_dial/run-test
asterisk/trunk/tests/cdr/nocdr/run-test
Modified:
asterisk/trunk/lib/python/asterisk/SimpleTestCase.py
asterisk/trunk/lib/python/asterisk/TestCase.py
asterisk/trunk/lib/python/asterisk/cdr.py
asterisk/trunk/logger.conf
asterisk/trunk/runtests.py
asterisk/trunk/tests/cdr/cdr_accountcode/test-config.yaml
asterisk/trunk/tests/cdr/cdr_fork_end_time/test-config.yaml
asterisk/trunk/tests/cdr/cdr_originate_sip_congestion_log/test-config.yaml
asterisk/trunk/tests/cdr/cdr_unanswered_yes/test-config.yaml
asterisk/trunk/tests/cdr/cdr_userfield/test-config.yaml
asterisk/trunk/tests/cdr/console_dial_sip_answer/test-config.yaml
asterisk/trunk/tests/cdr/console_dial_sip_busy/test-config.yaml
asterisk/trunk/tests/cdr/console_dial_sip_congestion/test-config.yaml
asterisk/trunk/tests/cdr/console_dial_sip_transfer/test-config.yaml
asterisk/trunk/tests/cdr/console_fork_after_busy_forward/test-config.yaml
asterisk/trunk/tests/cdr/console_fork_before_dial/test-config.yaml
asterisk/trunk/tests/cdr/nocdr/test-config.yaml
asterisk/trunk/tests/cdr/tests.yaml
Modified: asterisk/trunk/lib/python/asterisk/SimpleTestCase.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/SimpleTestCase.py?view=diff&rev=3282&r1=3281&r2=3282
==============================================================================
--- asterisk/trunk/lib/python/asterisk/SimpleTestCase.py (original)
+++ asterisk/trunk/lib/python/asterisk/SimpleTestCase.py Mon Jun 25 14:47:20 2012
@@ -9,54 +9,186 @@
import sys
import logging
+import uuid
sys.path.append("lib/python")
from TestCase import TestCase
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
class SimpleTestCase(TestCase):
'''The base class for extremely simple tests requiring only a spawned call
into the dialplan where success can be reported via a user-defined AMI
event.'''
- event_count = 0
- expected_events = 1
- hangup_chan = None
- def __init__(self):
- TestCase.__init__(self)
+ default_expected_events = 1
+
+ default_channel = 'Local/100 at test'
+
+ default_application = 'Echo'
+
+ def __init__(self, test_path = '', test_config = None):
+ ''' Constructor
+
+ Parameters:
+ test_path Optional path to the location of the test directory
+ test_config Optional yaml loaded object containing config information
+ '''
+ TestCase.__init__(self, test_path)
self.create_asterisk()
+ self._test_runs = []
+ self._current_run = 0
+ self._event_count = 0
+ self.expected_events = SimpleTestCase.default_expected_events
+ self._tracking_channels = []
+ self._ignore_originate_failures = False
+ self._spawn_after_hangup = False
+
+ if test_config is None or 'test-iterations' not in test_config:
+ # No special test configuration defined, use defaults
+ self._test_runs.append({'channel': SimpleTestCase.default_channel,
+ 'application': SimpleTestCase.default_application,
+ 'variable': {'testuniqueid': '%s' % (str(uuid.uuid1()))},
+ })
+ else:
+ # Use the info in the test config to figure out what we want to run
+ for iteration in test_config['test-iterations']:
+ iteration['variable'] = {'testuniqueid': '%s' % (str(uuid.uuid1())),}
+ self._test_runs.append(iteration)
+ if 'expected_events' in test_config:
+ self.expected_events = test_config['expected_events']
+ if 'ignore-originate-failures' in test_config:
+ self._ignore_originate_failures = test_config['ignore-originate-failures']
+ if 'spawn-after-hangup' in test_config:
+ self._spawn_after_hangup = test_config['spawn-after-hangup']
+
+
def ami_connect(self, ami):
- logger.info("Initiating call to local/100 at test on Echo() for simple test")
+ ''' AMI connect handler '''
ami.registerEvent('UserEvent', self.__event_cb)
- ami.registerEvent('Newchannel', self.__channel_cb)
- ami.originate("local/100 at test", application="Echo").addErrback(self.handleOriginateFailure)
+ ami.registerEvent('Hangup', self.__hangup_cb)
+ ami.registerEvent('VarSet', self.__varset_cb)
- def __channel_cb(self, ami, event):
- if not self.hangup_chan:
- self.hangup_chan = event['channel']
+ # Kick off the test runs
+ self.__start_new_call(ami)
+
+
+ def __originate_call(self, ami, call_details):
+ ''' Actually originate a call
+
+ Parameters:
+ ami The AMI connection object
+ call_details A dictionary object containing the parameters to pass
+ to the originate
+ '''
+ def __swallow_originate_error(result):
+ return None
+
+ # Each originate call gets tagged with the channel variable
+ # 'testuniqueid', which contains a UUID as the value. When a VarSet
+ # event happens, it will contain the Asterisk channel name with the
+ # unique ID we've assigned, allowing us to associate the Asterisk
+ # channel name with the channel we originated
+ msg = "Originating call to %s" % call_details['channel']
+ if 'application' in call_details:
+ msg += " with application %s" % call_details['application']
+ df = ami.originate(channel = call_details['channel'],
+ application = call_details['application'],
+ variable = call_details['variable'])
+ else:
+ msg += " to %s@%s at %s" % (call_details['exten'],
+ call_details['context'],
+ call_details['priority'],)
+ df = ami.originate(channel = call_details['channel'],
+ context = call_details['context'],
+ exten = call_details['exten'],
+ priority = call_details['priority'],
+ variable = call_details['variable'])
+ if self._ignore_originate_failures:
+ df.addErrback(__swallow_originate_error)
+ else:
+ df.addErrback(self.handleOriginateFailure)
+ LOGGER.info(msg)
+
+
+ def __varset_cb(self, ami, event):
+ ''' VarSet event handler. This event helps us tie back the channel
+ name that Asterisk created with the call we just originated '''
+
+ if (event['variable'] == 'testuniqueid'):
+
+ if (len([chan for chan in self._tracking_channels if
+ chan['testuniqueid'] == event['value']])):
+ # Duplicate event, return
+ return
+
+ # There should only ever be one match, since we're
+ # selecting on a UUID
+ originating_channel = [chan for chan in self._test_runs
+ if (chan['variable']['testuniqueid']
+ == event['value'])][0]
+ self._tracking_channels.append({
+ 'channel': event['channel'],
+ 'testuniqueid': event['value'],
+ 'originating_channel': originating_channel})
+ LOGGER.debug("Tracking originated channel %s as %s (ID %s)" % (
+ originating_channel, event['channel'], event['value']))
+
+
+ def __hangup_cb(self, ami, event):
+ ''' Hangup Event handler. If configured to do so, this will spawn the
+ next new call '''
+
+ candidate_channel = [chan for chan in self._tracking_channels
+ if chan['channel'] == event['channel']]
+ if (len(candidate_channel)):
+ LOGGER.debug("Channel %s hung up; removing" % event['channel'])
+ self._tracking_channels.remove(candidate_channel[0])
+ if (self._spawn_after_hangup):
+ self._current_run += 1
+ self.__start_new_call(ami)
+
+
+ def __start_new_call(self, ami):
+ ''' Kick off the next new call, or, if we've run out of calls to make,
+ stop the test '''
+
+ if (self._current_run < len(self._test_runs)):
+ self.__originate_call(ami, self._test_runs[self._current_run])
+ else:
+ LOGGER.info("All calls executed, stopping")
+ self.stop_reactor()
+
def __event_cb(self, ami, event):
+ ''' UserEvent callback handler. This is the default way in which
+ new calls are kicked off. '''
+
if self.verify_event(event):
- self.event_count += 1
- if self.event_count == self.expected_events:
+ self._event_count += 1
+ if self._event_count == self.expected_events:
self.passed = True
- logger.info("Test ending, hanging up channel")
- self.ami[0].hangup(self.hangup_chan).addCallbacks(
- self.hangup)
+ LOGGER.info("Test ending, hanging up current channels")
+ for chan in self._tracking_channels:
+ self.ami[0].hangup(chan['channel']).addCallbacks(self.hangup)
+ else:
+ self._current_run += 1
+ self.__start_new_call(ami)
+
def hangup(self, result):
- '''Now that the channels are hung up, the test can be ended'''
- logger.info("Hangup complete, stopping reactor")
+ ''' Called when all channels are hung up'''
+
+ LOGGER.info("Hangup complete, stopping reactor")
self.stop_reactor()
+
def verify_event(self, event):
- """
- Hook method used to verify values in the event.
- """
+ ''' Virtual method used to verify values in the event. '''
return True
+
def run(self):
TestCase.run(self)
Modified: asterisk/trunk/lib/python/asterisk/TestCase.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/TestCase.py?view=diff&rev=3282&r1=3281&r2=3282
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestCase.py (original)
+++ asterisk/trunk/lib/python/asterisk/TestCase.py Mon Jun 25 14:47:20 2012
@@ -40,13 +40,20 @@
other utilities.
"""
- def __init__(self):
+ def __init__(self, test_path = ''):
"""
Create a new instance of a TestCase. Must be called by inheriting
classes.
- """
-
- self.test_name = os.path.dirname(sys.argv[0])
+
+ Parameters:
+ test_path Optional parameter that specifies the path where this test
+ resides
+ """
+
+ if not len(test_path):
+ self.test_name = os.path.dirname(sys.argv[0])
+ else:
+ self.test_name = test_path
self.base = self.test_name.replace("tests/", "", 1)
self.ast = []
self.ami = []
@@ -61,9 +68,10 @@
self.testStateController = None
self.pcap = None
self.pcapfilename = None
- self.__stopping = False
+ self._stopping = False
self.testlogdir = os.path.join(Asterisk.test_suite_root, self.base, str(os.getpid()))
self.ast_version = AsteriskVersion()
+ self._stop_callbacks = []
os.makedirs(self.testlogdir)
@@ -297,8 +305,8 @@
temp_defer = self.ast[index].stop()
stop_defers.append(temp_defer)
- d = defer.DeferredList(stop_defers, consumeErrors=True)
- d.addCallback(__check_success_failure)
+ defer.DeferredList(stop_defers, consumeErrors=True).addCallback(
+ __check_success_failure)
return result
self.__stop_deferred = defer.Deferred()
@@ -322,10 +330,12 @@
except twisted.internet.error.ReactorNotRunning:
# Something stopped it between our checks - at least we're stopped
pass
- if not self.__stopping:
+ if not self._stopping:
+ self._stopping = True
df = self.__stop_asterisk()
df.addCallback(__stop_reactor)
- self.__stopping = True
+ for callback in self._stop_callbacks:
+ df.addCallback(callback)
def __reactor_timeout(self):
"""
@@ -430,3 +440,19 @@
self.passed = False
else:
logger.info("Test Condition %s failed but expected failure was set; test status not modified" % test_condition.getName())
+
+ def evaluate_results(self):
+ """ Return whether or not the test has passed """
+ return self.passed
+
+ def register_stop_observer(self, callback):
+ ''' Register an observer that will be called when Asterisk is stopped
+
+ Parameters:
+ callback The deferred callback function to be called when Asterisk is stopped
+
+ Note:
+ This appends a callback to the deferred chain of callbacks executed when
+ all instances of Asterisk are stopped.
+ '''
+ self._stop_callbacks.append(callback)
Added: asterisk/trunk/lib/python/asterisk/TestRunner.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/TestRunner.py?view=auto&rev=3282
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestRunner.py (added)
+++ asterisk/trunk/lib/python/asterisk/TestRunner.py Mon Jun 25 14:47:20 2012
@@ -1,0 +1,278 @@
+#!/usr/bin/env python
+''' Test Runner
+
+This module provides an entry point, loading, and teardown of test
+runs for the Test Suite
+
+Copyright (C) 2012, Digium, Inc.
+Matt Jordan <mjordan at digium.com>
+
+This program is free software, distributed under the terms of
+the GNU General Public License Version 2.
+'''
+
+import sys
+import imp
+import logging
+import logging.config
+import os
+import yaml
+
+from twisted.internet import reactor
+
+LOGGER = logging.getLogger('TestRunner')
+
+sys.path.append('lib/python')
+
+class TestModuleFinder(object):
+ ''' Determines if a module is a test module that can be loaded '''
+
+ supported_paths = []
+
+ def __init__(self, path_entry):
+ if not path_entry in TestModuleFinder.supported_paths:
+ raise ImportError()
+ LOGGER.debug('TestModuleFinder supports path %s' % path_entry)
+ return
+
+ def find_module(self, fullname, suggested_path = None):
+ ''' Attempts to find the specified module
+
+ Parameters:
+ fullname The full name of the module to load
+ suggested_path Optional path to find the module at
+ '''
+ search_paths = TestModuleFinder.supported_paths
+ if suggested_path:
+ search_paths.append(suggested_path)
+ for path in search_paths:
+ if os.path.exists('%s/%s.py' % (path, fullname)):
+ return TestModuleLoader(path)
+ LOGGER.warn("Unable to find module '%s'" % fullname)
+ return None
+
+class TestModuleLoader(object):
+ ''' Loads modules defined in the tests '''
+
+ def __init__(self, path_entry):
+ ''' Constructor
+
+ Parameters:
+ path_entry The path the module is located at
+ '''
+ self._path_entry = path_entry
+
+ def _get_filename(self, fullname):
+ return '%s/%s.py' % (self._path_entry, fullname)
+
+ def load_module(self, fullname):
+ ''' Load the module into memory
+
+ Parameters:
+ fullname The full name of the module to load
+ '''
+ if fullname in sys.modules:
+ mod = sys.modules[fullname]
+ else:
+ mod = sys.modules.setdefault(fullname,
+ imp.load_source(fullname, self._get_filename(fullname)))
+
+ return mod
+
+sys.path_hooks.append(TestModuleFinder)
+
+def load_test_modules(test_config, test_object, test_path):
+ ''' Load optional modules for a test
+
+ Parameters:
+ test_config The test configuration object
+ test_object The test object that the modules will attach to
+ test_path The path to the test
+ '''
+
+ if not test_object:
+ return
+ if not 'test-modules' in test_config:
+ LOGGER.error("No test-modules block in configuration")
+ return
+ if 'modules' not in test_config['test-modules']:
+ # Not an error - just no optional modules specified
+ return
+
+ for module_spec in test_config['test-modules']['modules']:
+ # If there's a specific portion of the config for this module, use it
+ if ('config-section' in module_spec
+ and module_spec['config-section'] in test_config):
+ module_config = test_config[module_spec['config-section']]
+ else:
+ module_config = test_config
+
+ if ('load-from-test' in module_spec and module_spec['load-from-test']):
+ TestModuleFinder.supported_paths.append(test_path)
+ sys.path.append(test_path)
+
+ if ('load-from-path' in module_spec):
+ TestModuleFinder.supported_paths.append(
+ module_spec['load-from-path'])
+ sys.path.append(module_spec['load-from-path'])
+
+ module_type = load_and_parse_module(module_spec['typename'])
+ # Modules take in two parameters: the module configuration object,
+ # and the test object that they attach to
+ module_type(module_config, test_object)
+
+
+def load_and_parse_module(type_name):
+ ''' Take a qualified module/object name, load the module, and return
+ a typename specifying the object
+
+ Parameters:
+ type_name A fully qualified module/object to load into memory
+
+ Returns:
+ An object type that to be instantiated
+ None on error
+ '''
+
+ LOGGER.debug("Importing %s" % type_name)
+
+ # Split the object typename into its constituent parts - the module name
+ # and the actual type of the object in that module
+ parts = type_name.split('.')
+ module_name = ".".join(parts[:-1])
+
+ if not len(module_name):
+ LOGGER.error("No module specified: %s" % module_name)
+ return None
+
+ module = __import__(module_name)
+ for comp in parts[1:]:
+ module = getattr(module, comp)
+ return module
+
+def create_test_object(test_path, test_config):
+ ''' Create the specified test object from the test configuration
+
+ Parameters:
+ test_path The path to the test directory
+ test_config The test configuration object, read from the yaml file
+
+ Returns:
+ A test object that has at least the following:
+ - __init__(test_path) - constructor that takes in the location of the
+ test directory
+ - evaluate_results() - True if the test passed, False otherwise
+ Or None if the object couldn't be created.
+ '''
+ if not 'test-modules' in test_config:
+ LOGGER.error("No test-modules block in configuration")
+ return None
+ if not 'test-object' in test_config['test-modules']:
+ LOGGER.error("No test-object specified for this test")
+ return None
+
+ test_object_spec = test_config['test-modules']['test-object']
+
+ module_obj = load_and_parse_module(test_object_spec['typename'])
+ if module_obj is None:
+ return None
+
+ test_object_config = None
+ if ('config-section' in test_object_spec and
+ test_object_spec['config-section'] in test_config):
+ test_object_config = test_config[test_object_spec['config-section']]
+ else:
+ test_object_config = test_config
+
+ # The test object must support injection of its location as a parameter
+ # to the constructor, and its test-configuration object (or the full test
+ # config object, if none is specified)
+ test_obj = module_obj(test_path, test_object_config)
+ return test_obj
+
+
+def load_test_config(test_directory):
+ ''' Load and parse the yaml test config specified by the test_directory
+
+ Note: this will throw exceptions if an error occurs while parsing the yaml
+ file. This is expected: if you provide an invalid configuration, its far
+ easier to let this crash and fix the yaml then try and 'handle' a completely
+ invalid configuration gracefully.
+
+ Parameters:
+ test_directory The directory containing this test run's information
+
+ Returns:
+ An object containing the yaml configuration, or None on error
+ '''
+
+ test_config = None
+
+ # Load and parse the test configuration
+ test_config_path = ('%s/test-config.yaml' % test_directory)
+ if not os.path.exists(test_config_path):
+ LOGGER.error("No test-config.yaml file found in %s" % test_directory)
+ return test_config
+
+ file_stream = open(test_config_path)
+ test_config = yaml.load(file_stream, )
+ file_stream.close()
+
+ return test_config
+
+
+def main(argv = None):
+ ''' Main entry point for the test run
+
+ Returns:
+ 0 on successful test run
+ 1 on any error
+ '''
+
+ if argv is None:
+ args = sys.argv
+
+ # Set up logging - we're probably the first ones run!
+ logConfigFile = os.path.join(os.getcwd(), "%s" % 'LOGGER.conf')
+ if os.path.exists(logConfigFile):
+ try:
+ logging.config.fileConfig(logConfigFile, None, False)
+ except:
+ print "WARNING: failed to preserve existing loggers - some " \
+ "logging statements may be missing"
+ logging.config.fileConfig(logConfigFile)
+ else:
+ print "WARNING: no logging.conf file found; using default configuration"
+ logging.basicConfig()
+
+ if (len(args) < 2):
+ LOGGER.error("TestRunner requires the full path to the test directory" \
+ " to execute")
+ return 1
+ test_directory = args[1]
+
+ LOGGER.info("Starting test run for %s" % test_directory)
+ test_config = load_test_config(test_directory)
+ if test_config is None:
+ return 1
+
+ test_object = create_test_object(test_directory, test_config)
+ if test_object is None:
+ return 1
+
+ # Load other modules that may be specified
+ load_test_modules(test_config, test_object, test_directory)
+
+ # Kick off the twisted reactor
+ reactor.run()
+
+ LOGGER.info("Test run for %s completed with result %s" %
+ (test_directory, str(test_object.passed)))
+ if test_object.evaluate_results():
+ return 0
+
+ return 1
+
+
+if __name__ == '__main__':
+ sys.exit(main() or 0)
Propchange: asterisk/trunk/lib/python/asterisk/TestRunner.py
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: asterisk/trunk/lib/python/asterisk/TestRunner.py
------------------------------------------------------------------------------
svn:executable = *
Propchange: asterisk/trunk/lib/python/asterisk/TestRunner.py
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision
Propchange: asterisk/trunk/lib/python/asterisk/TestRunner.py
------------------------------------------------------------------------------
svn:mime-type = text/plain
Modified: asterisk/trunk/lib/python/asterisk/cdr.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/cdr.py?view=diff&rev=3282&r1=3281&r2=3282
==============================================================================
--- asterisk/trunk/lib/python/asterisk/cdr.py (original)
+++ asterisk/trunk/lib/python/asterisk/cdr.py Mon Jun 25 14:47:20 2012
@@ -18,7 +18,81 @@
import logging
import time
-logger = logging.getLogger(__name__)
+from collections import defaultdict
+
+LOGGER = logging.getLogger(__name__)
+
+class CDRModule(object):
+ ''' A module that checks a test for expected CDR results '''
+
+
+ def __init__(self, module_config, test_object):
+ ''' Constructor
+
+ Parameters:
+ module_config The yaml loaded configuration for the CDR Module
+ test_object A concrete implementation of TestClass
+ '''
+ self.test_object = test_object
+
+ # Build our expected CDR records
+ self.cdr_records = {}
+ for record in module_config:
+ file_name = record['file']
+ if file_name not in self.cdr_records:
+ self.cdr_records[file_name] = []
+ for csv_line in record['lines']:
+ # Set the record to the default fields, then update with what
+ # was passed in to us
+ dict_record = dict((k, None) for k in AsteriskCSVCDRLine.fields)
+ dict_record.update(csv_line)
+
+ self.cdr_records[file_name].append(AsteriskCSVCDRLine(
+ accountcode=dict_record['accountcode'], source=dict_record['source'],
+ destination=dict_record['destination'], dcontext=dict_record['dcontext'],
+ callerid=dict_record['callerid'], channel=dict_record['channel'],
+ dchannel=dict_record['dchannel'], lastapp=dict_record['lastapp'],
+ lastarg=dict_record['lastarg'], start=dict_record['start'],
+ answer=dict_record['answer'], end=dict_record['end'],
+ duration=dict_record['duration'], billsec=dict_record['billsec'],
+ disposition=dict_record['disposition'], amaflags=dict_record['amaflags'],
+ uniqueid=dict_record['uniqueid'], userfield=dict_record['userfield']))
+
+ # Hook ourselves onto the test object
+ test_object.register_stop_observer(self._check_cdr_records)
+
+ def _check_cdr_records(self, callback_param):
+ ''' A deferred callback method that is called by the TestCase
+ derived object when all Asterisk instances have stopped
+
+ Parameters:
+ callback_param
+ '''
+ LOGGER.debug("Checking CDR records...")
+ self.match_cdrs()
+ return callback_param
+
+
+ def match_cdrs(self):
+ ''' Called when all instances of Asterisk have exited. Derived
+ classes can override this to provide their own behavior for CDR
+ matching.
+ '''
+ expectations_met = True
+ for key in self.cdr_records:
+ cdr_expect = AsteriskCSVCDR(records=self.cdr_records[key])
+ cdr_file = AsteriskCSVCDR(fn="%s/%s/cdr-csv/%s.csv" %
+ (self.test_object.ast[0].base,
+ self.test_object.ast[0].directories['astlogdir'],
+ key))
+ if cdr_expect.match(cdr_file):
+ LOGGER.debug("%s.csv - CDR results met expectations" % key)
+ else:
+ LOGGER.error("%s.csv - CDR results did not meet expectations. Test Failed." % key)
+ expectations_met = False
+
+ self.test_object.passed = expectations_met
+
class AsteriskCSVCDRLine(astcsv.AsteriskCSVLine):
"A single Asterisk call detail record"
Modified: asterisk/trunk/logger.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/logger.conf?view=diff&rev=3282&r1=3281&r2=3282
==============================================================================
--- asterisk/trunk/logger.conf (original)
+++ asterisk/trunk/logger.conf Mon Jun 25 14:47:20 2012
@@ -35,6 +35,12 @@
handlers=stdout,normalFile,verboseFile
qualname=asterisk.TestCase
+[logger_TestRunner]
+level=NOTSET
+propagate=0
+handlers=stdout,normalFile,verboseFile
+qualname=TestRunner
+
[handler_stdout]
class=StreamHandler
level=WARN
Modified: asterisk/trunk/runtests.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/runtests.py?view=diff&rev=3282&r1=3281&r2=3282
==============================================================================
--- asterisk/trunk/runtests.py (original)
+++ asterisk/trunk/runtests.py Mon Jun 25 14:47:20 2012
@@ -50,6 +50,9 @@
"%s/run-test" % self.test_name,
]
+ if not os.path.exists(cmd[0]):
+ cmd = ["./lib/python/asterisk/TestRunner.py",
+ "%s" % self.test_name]
if os.path.exists(cmd[0]) and os.access(cmd[0], os.X_OK):
msg = "Running %s ..." % cmd
print msg
Added: asterisk/trunk/tests/cdr/ForkCdrModule.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/ForkCdrModule.py?view=auto&rev=3282
==============================================================================
--- asterisk/trunk/tests/cdr/ForkCdrModule.py (added)
+++ asterisk/trunk/tests/cdr/ForkCdrModule.py Mon Jun 25 14:47:20 2012
@@ -1,0 +1,103 @@
+#!/usr/bin/env python
+'''
+Copyright (C) 2012, Digium, Inc.
+Matt Jordan <mjordan at digium.com>
+
+This program is free software, distributed under the terms of
+the GNU General Public License Version 2.
+'''
+
+import sys
+import logging
+
+sys.path.append("lib/python")
+from cdr import CDRModule
+
+logger = logging.getLogger(__name__)
+
+class ForkCdrModuleBasic(CDRModule):
+ ''' A class that adds some additional CDR checking on top of CDRModule
+
+ In addition to checking the normal expectations, this class also checks
+ that the original CDRs duration is not shorter then the forked CDR duration.
+
+ Note that this class assumes the CDRs are in cdrtest_local.
+ '''
+
+ def __init__(self, module_config, test_object):
+ super(ForkCdrModuleBasic, self).__init__(module_config, test_object)
+
+ def match_cdrs(self):
+ super(ForkCdrModuleBasic, self).match_cdrs()
+
+ if (not self.test_object.passed):
+ return
+
+ cdr1 = AsteriskCSVCDR(fn="%s/%s/cdr-csv/%s.csv" %
+ (self.test_object.ast[0].base,
+ self.test_object.ast[0].directories['astlogdir'], "cdrtest_local"))
+
+ if int(cdr1[0].duration) < int(cdr1[1].duration):
+ logger.error("Fail: Original CDR duration shorter than forked")
+ self.test_object.passed = False
+ return
+
+
+class ForkCdrModuleEndTime(CDRModule):
+ ''' A class that adds some additional CDR checking of the end times on top
+ of CDRModule
+
+ In addition to checking the normal expectations, this class also checks
+ whether or not the end times of the CDRs are within some period of time
+ of each each other.
+
+ Note that this class assumes the CDRs are in cdrtest_local.
+ '''
+
+ def __init__(self, module_config, test_object):
+ super(ForkCdrModuleEndTime, self).__init__(module_config, test_object)
+
+ def match_cdrs(self):
+ super(ForkCdrModuleEndTime, self).match_cdrs()
+
+ if (not self.test_object.passed):
+ return
+
+ cdr1 = AsteriskCSVCDR(fn = "%s/%s/cdr-csv/%s.csv" %
+ (self.test_object.ast[0].base,
+ self.test_object.ast[0].directories['astlogdir'],
+ "cdrtest_local"))
+
+ #check for missing fields
+ for cdritem in cdr1:
+ if (cdritem.duration is None or
+ cdritem.start is None or
+ cdritem.end is None):
+ logger.Error("EPIC FAILURE: CDR record %s is missing one or " \
+ "more key fields. This should never be able to " \
+ "happen." % cdritem)
+ self.test_object.passed = False
+ return
+
+ # The dialplan is set up so that these two CDRs should each last at
+ # least 4 seconds. Giving it wiggle room, we'll just say we want it to
+ # be greater than 1 second.
+ if ((int(cdr1[0].duration) <= 1) or (int(cdr1[1].duration) <= 1)):
+ logger.error("FAILURE: One or both CDRs only lasted a second or " \
+ "less (expected more)")
+ self.test_object.passed = False
+ return
+
+ end = time.strptime(cdr1[0].end, "%Y-%m-%d %H:%M:%S")
+ beg = time.strptime(cdr1[1].start, "%Y-%m-%d %H:%M:%S")
+
+ #check that the end of the first CDR occured within a 1 second split of
+ # the beginning of the second CDR
+ if (abs(time.mktime(end) - time.mktime(beg)) > 1):
+ logger.error("Time discrepency between end1 and start2 must be " \
+ "one second or less.\n")
+ logger.error("Actual times: end cdr1 = %s begin cdr2 = %s" %
+ (cdr1[0].end, cdr1[1].start))
+ self.test_object.passed = False
+ return
+
Propchange: asterisk/trunk/tests/cdr/ForkCdrModule.py
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: asterisk/trunk/tests/cdr/ForkCdrModule.py
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision
Propchange: asterisk/trunk/tests/cdr/ForkCdrModule.py
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf?view=auto&rev=3282
==============================================================================
--- asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf (added)
+++ asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf Mon Jun 25 14:47:20 2012
@@ -1,0 +1,8 @@
+[general]
+
+batch = yes
+size = 10
+
+[csv]
+usegmtime = yes
+loguniqueid = yes
Propchange: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision
Propchange: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/cdr.conf
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf?view=auto&rev=3282
==============================================================================
--- asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf (added)
+++ asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf Mon Jun 25 14:47:20 2012
@@ -1,0 +1,20 @@
+[general]
+
+[globals]
+
+[default]
+
+exten => dial_busy,1,NoOp()
+ same => n,Set(CDR(accountcode)=cdrtest_local)
+ same => n,Dial(Local/busy at default)
+ same => n,Hangup()
+
+exten => busy,1,NoOp()
+ same => n,Busy()
+ same => n,Hangup()
+
+exten => dial_answer,1,NoOp()
+ same => n,Set(CDR(accountcode)=cdrtest_local)
+ same => n,Answer()
+ same => n,Wait(1)
+ same => n,Hangup()
Propchange: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf
------------------------------------------------------------------------------
svn:eol-style = native
Propchange: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf
------------------------------------------------------------------------------
svn:keywords = Author Date Id Revision
Propchange: asterisk/trunk/tests/cdr/batch_cdrs/configs/ast1/extensions.conf
------------------------------------------------------------------------------
svn:mime-type = text/plain
Added: asterisk/trunk/tests/cdr/batch_cdrs/test-config.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/batch_cdrs/test-config.yaml?view=auto&rev=3282
==============================================================================
--- asterisk/trunk/tests/cdr/batch_cdrs/test-config.yaml (added)
+++ asterisk/trunk/tests/cdr/batch_cdrs/test-config.yaml Mon Jun 25 14:47:20 2012
@@ -1,0 +1,155 @@
+testinfo:
+ summary: "Test batch creation of CDR entries"
+ description: |
+ "This test sets the CDR configuration to record CDR in a batch, triggered
+ either by 10 records being created or by the default timeout. 10 calls
+ are then made: 8 that result in a BUSY indication, and 2 that are answered
+ by a SIP endpoint. The test then verifies that all 10 CDR records were
+ created with the correct values, including duration and billsec."
+
+test-modules:
+ test-object:
+ config-section: test-object-config
+ typename: 'SimpleTestCase.SimpleTestCase'
+ modules:
+ -
+ config-section: 'cdr-config'
+ typename: 'cdr.CDRModule'
+
+test-object-config:
+ spawn-after-hangup: True
+ ignore-originate-failures: True
+ test-iterations:
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
+ channel: 'Local/dial_busy at default'
+ application: 'Echo'
+ -
[... 771 lines stripped ...]
More information about the asterisk-commits
mailing list