[svn-commits] mmichelson: testsuite/asterisk/trunk r3324 - in /asterisk/trunk: lib/python/a...

SVN commits to the Digium repositories svn-commits at lists.digium.com
Mon Jul 16 11:25:31 CDT 2012


Author: mmichelson
Date: Mon Jul 16 11:25:24 2012
New Revision: 3324

URL: http://svnview.digium.com/svn/testsuite?view=rev&rev=3324
Log:
Merge AMI module changes into mainline testsuite trunk.

This adds a new pluggable test module intended to be able
to handle general cases where AMI event reception is required.

There are two modes of operation. 'headermatch' instances operate
by doing simple regex matches on AMI headers under specific conditions
and will fail if expected matches are not present. 'callback' instances
call a callback in order to determine if the test is proceeding as
desired.

Review: https://reviewboard.asterisk.org/r/2013


Added:
    asterisk/trunk/sample-yaml/
      - copied from r3311, asterisk/team/mmichelson/bridge-tests/sample-yaml/
    asterisk/trunk/sample-yaml/ami-config.yaml.sample
      - copied unchanged from r3311, asterisk/team/mmichelson/bridge-tests/sample-yaml/ami-config.yaml.sample
Modified:
    asterisk/trunk/lib/python/asterisk/TestCase.py
    asterisk/trunk/lib/python/asterisk/TestRunner.py
    asterisk/trunk/lib/python/asterisk/ami.py
    asterisk/trunk/lib/python/asterisk/cdr.py
    asterisk/trunk/tests/cdr/ForkCdrModule.py
    asterisk/trunk/tests/cdr/cdr_fork_end_time/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

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=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestCase.py (original)
+++ asterisk/trunk/lib/python/asterisk/TestCase.py Mon Jul 16 11:25:24 2012
@@ -59,7 +59,7 @@
         self.ami = []
         self.fastagi = []
         self.reactor_timeout = 30
-        self.passed = False
+        self.passed = None
         self.defaultLogLevel = "WARN"
         self.defaultLogFileName = "logger.conf"
         self.timeoutId = None
@@ -72,6 +72,7 @@
         self.testlogdir = os.path.join(Asterisk.test_suite_root, self.base, str(os.getpid()))
         self.ast_version = AsteriskVersion()
         self._stop_callbacks = []
+        self._ami_callbacks = []
 
         os.makedirs(self.testlogdir)
 
@@ -392,6 +393,8 @@
         self.ami[ami.id] = ami
         try:
             self.ami_connect(ami)
+            for callback in self._ami_callbacks:
+                callback(ami)
         except:
             logger.error("Exception raised in ami_connect:")
             logger.error(traceback.format_exc())
@@ -456,3 +459,20 @@
         all instances of Asterisk are stopped.
         '''
         self._stop_callbacks.append(callback)
+
+    def register_ami_observer(self, callback):
+        ''' Register an observer that will be called when TestCase connects with
+        Asterisk over the Manager interface
+
+        Parameters:
+        callback The deferred callback function to be called when AMI connects
+        '''
+        self._ami_callbacks.append(callback)
+
+    def set_passed(self, value):
+        '''Accumulate pass/fail value. If a test module has already
+        claimed that the test has failed, then this method will ignore
+        any further attempts to change the pass/fail status.'''
+        if self.passed == False:
+            return
+        self.passed = value

Modified: asterisk/trunk/lib/python/asterisk/TestRunner.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/TestRunner.py?view=diff&rev=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/lib/python/asterisk/TestRunner.py (original)
+++ asterisk/trunk/lib/python/asterisk/TestRunner.py Mon Jul 16 11:25:24 2012
@@ -107,15 +107,6 @@
         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
@@ -220,6 +211,28 @@
 
     return test_config
 
+def read_module_paths(test_config, test_path):
+    '''
+    Read additional paths required for loading modules for the test
+
+    Parameters:
+    test_config The test configuration object
+    '''
+
+    if not 'test-modules' in test_config:
+        # Don't log anything. The test will complain later when
+        # attempting to load modules
+        return
+
+    if ('add-test-to-search-path' in test_config['test-modules'] and
+            test_config['test-modules']['add-test-to-search-path']):
+        TestModuleFinder.supported_paths.append(test_path)
+        sys.path.append(test_path)
+
+    if 'add-to-search-path' in test_config['test-modules']:
+        for path in test_config['test-modules']['add-to-search-path']:
+            TestModuleFinder.supported_paths.append(path)
+            sys.path.append(path)
 
 def main(argv = None):
     ''' Main entry point for the test run
@@ -257,6 +270,8 @@
     if test_config is None:
         return 1
 
+    read_module_paths(test_config, test_directory)
+
     test_object = create_test_object(test_directory, test_config)
     if test_object is None:
         return 1
@@ -276,4 +291,4 @@
 
 
 if __name__ == '__main__':
-    sys.exit(main() or 0)
+    sys.exit(main() or 0)

Modified: asterisk/trunk/lib/python/asterisk/ami.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/lib/python/asterisk/ami.py?view=diff&rev=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/lib/python/asterisk/ami.py (original)
+++ asterisk/trunk/lib/python/asterisk/ami.py Mon Jul 16 11:25:24 2012
@@ -3,8 +3,197 @@
 import datetime
 import sys
 import logging
+import re
 
 logger = logging.getLogger(__name__)
+
+class AMIEventInstance(object):
+    ''' 
+    Base class for specific instances of AMI event observers
+
+    This handles common elements for both headermatch and callback
+    types of AMI event observers, allowing the individual types
+    to focus on their specific duties.
+    '''
+    def __init__(self, instance_config, test_object):
+        self.test_object = test_object
+        self.match_conditions = instance_config['conditions']['match']
+        self.nonmatch_conditions = instance_config['conditions'].get('nomatch', {})
+        self.ids = instance_config['id'].split(',') if 'id' in instance_config else ['0']
+        self.passed = True
+
+        if 'count' in instance_config:
+            count = instance_config['count']
+            if count[0] == '<':
+                # Need at most this many events
+                self.count_min = 0
+                self.count_max = int(count[1:])
+            elif count[0] == '>':
+                # Need at least this many events
+                self.count_min = int(count[1:])
+                self.count_max = float("inf")
+            else:
+                # Need exactly this many events
+                self.count_min = int(count)
+                self.count_max = int(count)
+        else:
+            self.count_min = 0
+            self.count_max = float("inf")
+
+        self.event_count = 0
+
+        if 'Event' not in self.match_conditions:
+            logger.error("No event specified to match on. Aborting test")
+            raise Exception
+
+        test_object.register_ami_observer(self.ami_connect)
+        test_object.register_stop_observer(self.__check_result)
+
+    def ami_connect(self, ami):
+        if str(ami.id) in self.ids:
+            logger.debug("Registering event %s" % self.match_conditions['Event'])
+            ami.registerEvent(self.match_conditions['Event'], self.__event_callback)
+
+    def event_callback(self, ami, event):
+        '''
+        Virtual method overridden by specific AMI Event
+        instance types
+        '''
+        pass
+
+    def __event_callback(self, ami, event):
+        '''
+        Check event conditions to see if subclasses should
+        be called into
+        '''
+
+        for k,v in self.match_conditions.items():
+            if not re.match(v, event.get(k.lower())):
+                logger.debug("Condition %s: %s does not match %s: %s in event" %
+                        (k, v, k, event.get(k.lower())))
+                return
+            else:
+                logger.debug("Condition %s: %s matches %s: %s in event" %
+                        (k, v, k, event.get(k.lower())))
+
+        for k,v in self.nonmatch_conditions.items():
+            if re.match(v, event.get(k.lower())):
+                logger.debug("Condition %s: %s matches %s: %s in event" %
+                        (k, v, k, event.get(k.lower())))
+                return
+            else:
+                logger.debug("Condition %s: %s does not match %s: %s in event" %
+                        (k, v, k, event.get(k.lower())))
+
+        self.event_count += 1
+
+        #Conditions have matched up as expected
+        #so leave it to the individual types to determine
+        #how to proceed
+
+        return self.event_callback(ami, event)
+
+    def check_result(self, callback_param):
+        '''Virtual method to be overridden by subclasses'''
+        pass
+
+    def __check_result(self, callback_param):
+        '''
+        This will check against event counts and the like and then
+        call into overridden veresions
+        '''
+        if (self.event_count > self.count_max
+                or self.event_count < self.count_min):
+            logger.warning("Event occurred %d times, which is out of the"
+                    " allowable range" % self.event_count)
+            self.test_object.set_passed(False)
+            return callback_param
+        return self.check_result(callback_param)
+
+class AMIHeaderMatchInstance(AMIEventInstance):
+    '''
+    A subclass of AMIEventInstance that operates by matching headers of
+    AMI events to expected values. If a header does not match its expected
+    value, then the test will fail
+    '''
+    def __init__(self, instance_config, test_object):
+        super(AMIHeaderMatchInstance, self).__init__(instance_config, test_object)
+        logger.debug("Initializing an AMIHeaderMatchInstance")
+        self.match_requirements = (
+                instance_config['requirements'].get('match', {}))
+        self.nonmatch_requirements = (
+                instance_config['requirements'].get('nomatch', {}))
+
+    def event_callback(self, ami, event):
+        for k,v in self.match_requirements.items():
+            if not re.match(v, event.get(k.lower())):
+                logger.warning("Requirement %s: %s does not match %s: %s in event" %
+                        (k, v, k, event.get(k.lower())))
+                self.passed = False
+            else:
+                logger.debug("Requirement %s: %s matches %s: %s in event" %
+                        (k, v, k, event.get(k.lower())))
+
+        for k,v in self.nonmatch_requirements.items():
+            if re.match(v, event.get(k.lower(), '')):
+                logger.warning("Requirement %s: %s matches %s: %s in event" %
+                        (k, v, k, event.get(k.lower(), '')))
+                self.passed = False
+            else:
+                logger.debug("Requirement %s: %s does not match %s: %s in event" %
+                        (k, v, k, event.get(k.lower(), '')))
+
+        return (ami, event)
+
+    def check_result(self, callback_param):
+        self.test_object.set_passed(self.passed)
+        return callback_param
+
+class AMICallbackInstance(AMIEventInstance):
+    '''
+    Subclass of AMIEventInstance that operates by calling a user-defined
+    callback function. The callback function returns the current disposition
+    of the test (i.e. whether the test is currently passing or failing).
+    '''
+    def __init__(self, instance_config, test_object):
+        super(AMICallbackInstance, self).__init__(instance_config, test_object)
+        self.callback_module = instance_config['callback_module']
+        self.callback_method = instance_config['callback_method']
+        if 'start' in instance_config:
+            self.passed = True if instance_config['start'] == 'pass' else False
+
+    def event_callback(self, ami, event):
+        callback_module = __import__(self.callback_module)
+        method = getattr(callback_module, self.callback_method)
+        self.passed = method(ami, event)
+
+    def check_result(self, callback_param):
+        self.test_object.set_passed(self.passed)
+        return callback_param
+
+class AMIEventInstanceFactory:
+    @staticmethod
+    def create_instance(instance_config, test_object):
+        instance_type = instance_config['type']
+        if instance_type == "headermatch":
+            logger.debug("instance type is 'headermatch'")
+            return AMIHeaderMatchInstance(instance_config, test_object)
+        elif instance_type == "callback":
+            logger.debug("instance type is 'callback'")
+            return AMICallbackInstance(instance_config, test_object)
+        else:
+            logger.error("Invalid type %s specified for AMI event instance" %
+                    instance_type)
+            raise Exception
+
+class AMIEventModule(object):
+    def __init__(self, module_config, test_object):
+        logger.debug("Initializing AMIEvent module")
+        self.test_object = test_object
+        self.ami_instances = []
+        for instance in module_config:
+            self.ami_instances.append(AMIEventInstanceFactory.create_instance(instance,
+                test_object))
 
 class AMI:
     def __init__(self, on_login, on_error, timeout=60, user="mark", secret="mysecret", host="127.0.0.1", port=5038):

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=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/lib/python/asterisk/cdr.py (original)
+++ asterisk/trunk/lib/python/asterisk/cdr.py Mon Jul 16 11:25:24 2012
@@ -91,7 +91,7 @@
                 LOGGER.error("%s.csv - CDR results did not meet expectations.  Test Failed." % key)
                 expectations_met = False
 
-        self.test_object.passed = expectations_met
+        self.test_object.set_passed(expectations_met)
 
 
 class AsteriskCSVCDRLine(astcsv.AsteriskCSVLine):

Modified: asterisk/trunk/tests/cdr/ForkCdrModule.py
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/ForkCdrModule.py?view=diff&rev=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/tests/cdr/ForkCdrModule.py (original)
+++ asterisk/trunk/tests/cdr/ForkCdrModule.py Mon Jul 16 11:25:24 2012
@@ -39,7 +39,7 @@
 
         if int(cdr1[0].duration) < int(cdr1[1].duration):
             logger.error("Fail: Original CDR duration shorter than forked")
-            self.test_object.passed = False
+            self.test_object.set_passed(False)
         return
 
 
@@ -76,7 +76,7 @@
                 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
+                self.test_object.set_passed(False)
                 return
 
         # The dialplan is set up so that these two CDRs should each last at
@@ -85,7 +85,7 @@
         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
+            self.test_object.set_passed(False)
             return
 
         end = time.strptime(cdr1[0].end, "%Y-%m-%d %H:%M:%S")
@@ -98,6 +98,6 @@
                          "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
+            self.test_object.set_passed(False)
             return
 

Modified: asterisk/trunk/tests/cdr/cdr_fork_end_time/test-config.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/cdr_fork_end_time/test-config.yaml?view=diff&rev=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/tests/cdr/cdr_fork_end_time/test-config.yaml (original)
+++ asterisk/trunk/tests/cdr/cdr_fork_end_time/test-config.yaml Mon Jul 16 11:25:24 2012
@@ -10,12 +10,14 @@
          time of the second (<=1 second to accomodate potential timing differences).'
 
 test-modules:
+    add-to-search-path:
+        -
+            'tests/cdr'
     test-object:
         config-section: test-object-config
         typename: 'SimpleTestCase.SimpleTestCase'
     modules:
         -
-            load-from-path: 'tests/cdr'
             config-section: 'cdr-config'
             typename: 'ForkCdrModule.ForkCdrModuleEndTime'
 

Modified: asterisk/trunk/tests/cdr/console_fork_after_busy_forward/test-config.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/console_fork_after_busy_forward/test-config.yaml?view=diff&rev=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/tests/cdr/console_fork_after_busy_forward/test-config.yaml (original)
+++ asterisk/trunk/tests/cdr/console_fork_after_busy_forward/test-config.yaml Mon Jul 16 11:25:24 2012
@@ -7,12 +7,14 @@
         expected behavior.'
 
 test-modules:
+    add-to-search-path:
+        -
+            'tests/cdr'
     test-object:
         config-section: test-object-config
         typename: 'SimpleTestCase.SimpleTestCase'
     modules:
         -
-            load-from-path: 'tests/cdr'
             config-section: 'cdr-config'
             typename: 'ForkCdrModule.ForkCdrModuleBasic'
 
@@ -57,4 +59,4 @@
         - asterisk : 'cdr_csv'
     tags:
         - CDR
-        - chan_local
+        - chan_local

Modified: asterisk/trunk/tests/cdr/console_fork_before_dial/test-config.yaml
URL: http://svnview.digium.com/svn/testsuite/asterisk/trunk/tests/cdr/console_fork_before_dial/test-config.yaml?view=diff&rev=3324&r1=3323&r2=3324
==============================================================================
--- asterisk/trunk/tests/cdr/console_fork_before_dial/test-config.yaml (original)
+++ asterisk/trunk/tests/cdr/console_fork_before_dial/test-config.yaml Mon Jul 16 11:25:24 2012
@@ -6,12 +6,14 @@
         answers and then immediately hangs up. CDRs are tested for expected behavior.'
 
 test-modules:
+    add-to-search-path:
+        -
+            'tests/cdr'
     test-object:
         config-section: test-object-config
         typename: 'SimpleTestCase.SimpleTestCase'
     modules:
         -
-            load-from-path: 'tests/cdr'
             config-section: 'cdr-config'
             typename: 'ForkCdrModule.ForkCdrModuleBasic'
 
@@ -60,4 +62,4 @@
     tags:
         - dial
         - CDR
-        - chan_local
+        - chan_local




More information about the svn-commits mailing list