[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