<p>Jenkins2 <strong>merged</strong> this change.</p><p><a href="https://gerrit.asterisk.org/9048">View Change</a></p><div style="white-space:pre-wrap">Approvals:
  Joshua Colp: Looks good to me, but someone else must approve
  George Joseph: Looks good to me, approved
  Jenkins2: Approved for Submit

</div><pre style="font-family: monospace,monospace; white-space: pre-wrap;">pjsip/statsd/contacts: Sporadically failing due to unexpected messages<br><br>After the pjsip qualify rewrite the contacts statsd test failed fairly<br>regularly because it was now receiving two messages that it did not receive<br>before the rewrite. Namely, the messages that occur on shutdown. However,<br>due to some current shutdown handling in Asterisk these final messages still<br>may or may not be received by the test.<br><br>Unfortunately, the way the statsd tests were written did not allow for optional<br>messages. Really it only allowed for a strict result set that was not too<br>configurable.<br><br>This patch alleviates the brittleness, and fixes the test, by creating<br>suitable condition based message handling and matching routines that the test<br>can now use for optional messages received.<br><br>An attempt was made to generically write the message matching routines in hopes<br>that other future event listeners and handlers could take advantage of it.<br><br>Change-Id: Iaae769c7a4fe2dcac4865eb7dc4e5b6a1b25900b<br>---<br>A lib/python/asterisk/matcher.py<br>A lib/python/asterisk/matcher_listener.py<br>M lib/python/asterisk/pluggable_modules.py<br>A lib/python/asterisk/self_test/test2_matcher.py<br>D lib/python/mockd.py<br>M self_test<br>M tests/channels/pjsip/statsd/contacts/test-config.yaml<br>M tests/channels/pjsip/statsd/registrations/test-config.yaml<br>8 files changed, 775 insertions(+), 188 deletions(-)<br><br></pre><pre style="font-family: monospace,monospace; white-space: pre-wrap;">diff --git a/lib/python/asterisk/matcher.py b/lib/python/asterisk/matcher.py<br>new file mode 100644<br>index 0000000..87c62be<br>--- /dev/null<br>+++ b/lib/python/asterisk/matcher.py<br>@@ -0,0 +1,327 @@<br>+"""Module for handling pattern and conditional message matching and<br>+aggregation.<br>+<br>+Copyright (C) 2018, Digium, Inc.<br>+Kevin Harwell <kharwell@digium.com><br>+<br>+This program is free software, distributed under the terms of<br>+the GNU General Public License Version 2.<br>+"""<br>+<br>+import logging<br>+<br>+from test_suite_utils import all_match<br>+from pluggable_registry import PLUGGABLE_EVENT_REGISTRY,\<br>+    PLUGGABLE_ACTION_REGISTRY<br>+<br>+<br>+LOGGER = logging.getLogger(__name__)<br>+<br>+<br>+class ConditionError(Exception):<br>+    """Error raised a condition(s) fail"""<br>+<br>+    def __init__(self, failures):<br>+        """Create a condition(s) error exception.<br>+<br>+        Keyword Arguments:<br>+        failures - A list of failed condition objects<br>+        """<br>+<br>+        if not isinstance(failures, list):<br>+            failures = [failures]<br>+<br>+        msg = ''<br>+        for f in failures:<br>+            msg += f.error() + '\n'<br>+<br>+        super(ConditionError, self).__init__(msg)<br>+<br>+<br>+class Condition(object):<br>+<br>+    @staticmethod<br>+    def create(config, match_fun=None):<br>+        """Create a condition object from the given configuration.<br>+<br>+        Keyword Arguments:<br>+        config - Data in a dictionary format used in object creation<br>+        match_fun - The function used to match the pattern and value<br>+<br>+        Configuration options:<br>+        match - A regex string, dictionary, or list.<br>+        count - Expected number of matches. Can be a single value (ex: 2),<br>+            range (ex: 2-5), or a lower (ex: <4) or upper (ex: >2) limit.<br>+        optional - alias for 'match' and expects 0 or 1 matches.<br>+        """<br>+<br>+        if isinstance(config, str) or isinstance(config, unicode):<br>+            config = {'match': config}<br>+<br>+        if 'optional' in config:<br>+            config['match'] = config['optional']<br>+            minimum = '<2'<br>+        else:<br>+            minimum, _, maximum = str(config.get('count', '1')).partition('-')<br>+<br>+        if minimum.startswith('>'):<br>+            minimum = int(minimum[1:]) + 1<br>+            maximum = float('inf')<br>+        elif minimum.startswith('<'):<br>+            maximum = int(minimum[1:]) - 1<br>+            minimum = 0<br>+        else:<br>+            minimum = int(minimum)<br>+            maximum = int(maximum) if maximum else minimum<br>+<br>+        if minimum > maximum:<br>+            raise SyntaxError("Invalid count: minimum '{0}' can't be greater "<br>+                              "than maximum '{1}'".format(minimum, maximum))<br>+<br>+        return Condition(config.get('match'), minimum, maximum)<br>+<br>+    def __init__(self, pattern=None, minimum=1, maximum=1, match_fun=None):<br>+        """Constructor<br>+<br>+        Keyword Arguments:<br>+        pattern - The pattern that will be checked against<br>+        minimum - The expected minimum number of matches<br>+        maximum - The expected maximum number of matches<br>+        match_fun - The function used to match the pattern and value<br>+        """<br>+<br>+        self.pattern = pattern<br>+        self.minimum = minimum<br>+        self.maximum = maximum<br>+        self.match_fun = match_fun or all_match<br>+<br>+        self.count = 0<br>+<br>+    def check_match(self, value):<br>+        """Check if the given value matches the expected pattern.<br>+<br>+        Keyword Arguments:<br>+        value - The value to check against the expected pattern<br>+        """<br>+<br>+        if self.match_fun(self.pattern, value):<br>+            LOGGER.debug("Matched condition: {0}".format(self.pattern))<br>+            self.count += 1<br>+            return True<br>+<br>+        return False<br>+<br>+    def check_max(self):<br>+        """Check if the current match count is less than or equal to the<br>+        configured maximum.<br>+        """<br>+<br>+        return self.count <= self.maximum<br>+<br>+    def check_min(self):<br>+        """Check if the current match count is greater than or equal to the<br>+        configured minimum.<br>+        """<br>+<br>+        return self.count >= self.minimum<br>+<br>+    def error(self):<br>+        """Error out the conditional."""<br>+<br>+        return ("\nCondition: '{0}'\nExpected >= {1} and <= {2} but "<br>+                "received {3}".format(self.pattern, self.minimum,<br>+                                      self.maximum, self.count))<br>+<br>+<br>+class Conditions(object):<br>+<br>+    @staticmethod<br>+    def create(config, on_match=None, match_fun=None):<br>+        """Create a conditions object from the given configuration.<br>+<br>+        Keyword Arguments:<br>+        config - Data in a dictionary format used in object creation<br>+        on_match - Optional callback to raise on a match. Handler Must be<br>+            proto-typed as 'handler(matched, value)'<br>+        match_fun - Optional function used to match the pattern and value<br>+<br>+        Configuration options:<br>+        conditions - A list of condition configuration data<br>+        trigger-on-any - Raise the 'on_match' event if any condition has been<br>+            fully met (meaning at least one match with all its minimum met).<br>+            Defaults to False<br>+        trigger-on-all - Raise the 'on_match' event if all conditions have been<br>+            fully met (meaning all have matched and met their minimum).<br>+            Defaults to True<br>+<br>+        Note:<br>+        If both trigger-on-any and trigger-on-all are False then the 'on_match'<br>+        event is raised upon the first basic match (meaning a minimum may or<br>+        may not have been met yet)<br>+        """<br>+<br>+        conditions = []<br>+        for c in config['conditions']:<br>+            conditions.append(Condition.create(c, match_fun))<br>+<br>+        # Any is checked prior to all, so okay for all to also be True<br>+        return Conditions(conditions, config.get('trigger-on-any', False),<br>+                          config.get('trigger-on-all', True), on_match)<br>+<br>+    def __init__(self, conditions, trigger_on_any=False, trigger_on_all=True,<br>+                 on_match=None):<br>+        """Constructor<br>+<br>+        Keyword Arguments:<br>+        conditions - A list of condition objects<br>+        trigger_on_any - check returns true if any condition is met<br>+        trigger_on_all - check returns true if all conditions are met<br>+        on_match - Optional callback to raise on a match. Handler Must be<br>+            proto-typed as 'handler(matched, value)'<br>+        """<br>+<br>+        self.conditions = conditions or []<br>+        self.trigger_on_any = trigger_on_any<br>+        self.trigger_on_all = trigger_on_all<br>+        self.on_match = on_match or (lambda y, z: None)<br>+<br>+    def check(self, value):<br>+        """Check if the given value matches a stored pattern, and if so then<br>+        also make sure that any other relevant conditional criteria have been<br>+        met.<br>+<br>+        Keyword Arguments:<br>+        value - The value to check against the patterns<br>+<br>+        Return:<br>+        True given the following, false otherwise:<br>+            trigger_on_any was set and at least one required conditional<br>+            was met.<br>+<br>+            trigger_on_all was set and all required conditional were met.<br>+<br>+            An item matched, and neither of the above parameters were set.<br>+        """<br>+<br>+        matched = []<br>+        for c in self.conditions:<br>+            if not c.check_match(value):<br>+                continue<br>+<br>+            if not c.check_max():<br>+                raise ConditionError(c)<br>+<br>+            matched.append(c)<br>+<br>+<br>+        if not matched:<br>+            return False<br>+<br>+        if self.trigger_on_any:<br>+            if not any(c.check_min() for c in matched):<br>+                return False<br>+        elif self.trigger_on_all:<br>+            if not all(c.check_min() for c in self.conditions):<br>+                return False<br>+<br>+        LOGGER.debug("Conditions triggered: {0}".format([c.pattern for c in matched]))<br>+        self.on_match(matched, value)<br>+        return True<br>+<br>+    def check_final(self):<br>+        """Check final conditionals and fail on those not met."""<br>+<br>+        failures = [c for c in self.conditions if not c.check_min()]<br>+        if failures:<br>+            raise ConditionError(failures)<br>+        return True<br>+<br>+<br>+class PluggableConditions(object):<br>+<br>+    def __init__(self, config, test_object, on_match=None):<br>+        """Constructor<br>+<br>+        Keyword Arguments:<br>+        config - Configuration for this module<br>+        test_object - The test case driver<br>+        on_match - Optional callback to raise on a match. Handler Must be<br>+            proto-typed as 'handler(matched, value)'<br>+        """<br>+<br>+        self.config = config<br>+        self.test_object = test_object<br>+        self.test_object.register_stop_observer(self.__handle_stop)<br>+<br>+        self.conditions = Conditions.create(self.config, on_match)<br>+<br>+    def fail_and_stop(self, error_msg):<br>+        """Fail the test and stop the reactor.<br>+<br>+        Keyword Arguments:<br>+        error_msg - The error message to log<br>+        """<br>+<br>+        LOGGER.error(error_msg)<br>+<br>+        self.test_object.set_passed(False)<br>+        self.test_object.stop_reactor()<br>+<br>+    def check(self, value):<br>+        try:<br>+            return self.conditions.check(value)<br>+        except ConditionError as e:<br>+            self.fail_and_stop(e)<br>+<br>+    def check_final(self):<br>+        if self.test_object.passed:<br>+            try:<br>+                self.conditions.check_final()<br>+            except ConditionError as e:<br>+                self.fail_and_stop(e)<br>+        return self.test_object.passed<br>+<br>+    def __handle_stop(self, *args):<br>+        """Check any final conditions prior to test end.<br>+<br>+        Keyword Arguments:<br>+        args: Unused<br>+        """<br>+<br>+        self.check_final()<br>+<br>+<br>+class PluggableConditionsEventModule(object):<br>+    """Registry wrapper for pluggable conditional event checks."""<br>+<br>+    def __init__(self, test_object, triggered_callback, config):<br>+        """Constructor<br>+<br>+        Keyword Arguments:<br>+        test_object - The TestCase driver<br>+        triggered_callback - Conditionally called when matched<br>+        config - Configuration for this module<br>+<br>+        Configuration options:<br>+        type - The <module.class> of the object type to create that is<br>+            listens for events and passes event data to the conditional<br>+            matcher.<br>+        """<br>+<br>+        self.triggered_callback = triggered_callback<br>+<br>+        module_name, _, obj_type = config['type'].partition('.')<br>+<br>+        module = __import__(module_name, fromlist=[obj_type])<br>+        if not module:<br>+            raise Exception("Unable to import module '{0}'.".format(module_name))<br>+<br>+        obj = getattr(module, obj_type)<br>+<br>+        self.conditions = obj(config, test_object, self.__handle_match)<br>+<br>+    def __handle_match(self, matched, event):<br>+        self.triggered_callback(self, matched, event)<br>+<br>+<br>+PLUGGABLE_EVENT_REGISTRY.register('event', PluggableConditionsEventModule)<br>diff --git a/lib/python/asterisk/matcher_listener.py b/lib/python/asterisk/matcher_listener.py<br>new file mode 100644<br>index 0000000..d6b621d<br>--- /dev/null<br>+++ b/lib/python/asterisk/matcher_listener.py<br>@@ -0,0 +1,76 @@<br>+"""Module that match messages and events for a defined listener.<br>+<br>+Copyright (C) 2018, Digium, Inc.<br>+Kevin Harwell <kharwell@digium.com><br>+<br>+This program is free software, distributed under the terms of<br>+the GNU General Public License Version 2.<br>+"""<br>+<br>+import logging<br>+import re<br>+<br>+from twisted.internet.protocol import DatagramProtocol<br>+from twisted.internet import reactor<br>+<br>+from matcher import PluggableConditions<br>+<br>+LOGGER = logging.getLogger(__name__)<br>+<br>+<br>+class UdpProtocol(DatagramProtocol):<br>+    """Protocol to use for receiving messages."""<br>+<br>+    def __init__(self, server):<br>+        """Constructor.<br>+<br>+        Keyword Arguments:<br>+        server - The udp server object<br>+        config - Configuration used by the protocol<br>+        """<br>+<br>+        self.server = server<br>+<br>+    def datagramReceived(self, datagram, address):<br>+        """Receive incoming data.<br>+<br>+        Keyword Arguments:<br>+        datagram - The data that was received<br>+        address - The address that the data came from<br>+        """<br>+<br>+        LOGGER.debug('Received %s from %s', datagram, address)<br>+        self.server.handle_message(datagram)<br>+<br>+<br>+class Udp(PluggableConditions):<br>+    """Pluggable module that that checks messages received over UDP"""<br>+<br>+    def __init__(self, config, test_object, on_match=None):<br>+        """Constructor<br>+<br>+        Keyword Arguments:<br>+        config - configuration for this module<br>+        test_object - the TestCase driver<br>+        on_match - Optional callback called upon a conditional match<br>+        """<br>+<br>+        super(Udp, self).__init__(config, test_object, on_match)<br>+<br>+        self.filter_msgs = config.get('filter', ['stasis.message', 'channels.'])<br>+<br>+        if not isinstance(self.filter_msgs, list):<br>+            self.filter_msgs = [self.filter_msgs]<br>+<br>+        reactor.listenUDP(config.get('port', 8125), UdpProtocol(self))<br>+<br>+    def handle_message(self, msg):<br>+        """Handle messages received over udp and check the message against the<br>+        configured conditions.<br>+<br>+        Keyword Arguments:<br>+        msg -- The message received via the udp<br>+        """<br>+<br>+        if not any(f for f in self.filter_msgs if re.match(f, msg)):<br>+            self.check(msg)<br>diff --git a/lib/python/asterisk/pluggable_modules.py b/lib/python/asterisk/pluggable_modules.py<br>index 256d222..344f908 100755<br>--- a/lib/python/asterisk/pluggable_modules.py<br>+++ b/lib/python/asterisk/pluggable_modules.py<br>@@ -22,6 +22,8 @@<br>     PLUGGABLE_EVENT_REGISTRY,\<br>     PluggableRegistry<br> <br>+import matcher<br>+<br> LOGGER = logging.getLogger(__name__)<br> <br> <br>diff --git a/lib/python/asterisk/self_test/test2_matcher.py b/lib/python/asterisk/self_test/test2_matcher.py<br>new file mode 100755<br>index 0000000..0357ce9<br>--- /dev/null<br>+++ b/lib/python/asterisk/self_test/test2_matcher.py<br>@@ -0,0 +1,299 @@<br>+#!/usr/bin/env python<br>+"""Module for testing the message_match module.<br>+<br>+Copyright (C) 2018, Digium, Inc.<br>+Kevin Harwell <kharwell@digium.com><br>+<br>+This program is free software, distributed under the terms of<br>+the GNU General Public License Version 2.<br>+"""<br>+<br>+import logging<br>+import sys<br>+import unittest<br>+<br>+sys.path.append('lib/python')  # noqa<br>+from asterisk.matcher import PluggableConditions<br>+<br>+<br>+LOGGER = logging.getLogger(__name__)<br>+<br>+<br>+class TestObject(object):<br>+    """Simple test object that implements methods used by message<br>+    match conditions.<br>+    """<br>+<br>+    def __init__(self):<br>+        """Constructor"""<br>+<br>+        self.passed = True<br>+<br>+    def set_passed(self, passed):<br>+        """Set the passed value"""<br>+<br>+        self.passed = passed<br>+<br>+    def stop_reactor(self):<br>+        """Noop"""<br>+<br>+        pass<br>+<br>+    def register_stop_observer(self, callback):<br>+        """Noop"""<br>+<br>+        pass<br>+<br>+<br>+class PluggableConditionsTests(unittest.TestCase):<br>+    """Unit tests for message match conditions."""<br>+<br>+    def setUp(self):<br>+        """Setup test object"""<br>+<br>+        self.test_object = TestObject()<br>+<br>+    def test_001_defaults(self):<br>+        """Test condition defaults"""<br>+<br>+        config = {'conditions': ['hello']}<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+    def test_002_match(self):<br>+        """Test basic matching"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'match': 'hello'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+    def test_003_optional(self):<br>+        """Test optional matching"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'optional': 'hello'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        # Check with no message handled<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Now check once message handled<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+    def test_004_count(self):<br>+        """Test count condition"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '2'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        # Check with no message handled<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        # Check with one message handled<br>+        self.test_object.set_passed(True)<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        # Check with two messages handled<br>+        self.test_object.set_passed(True)<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Check with three messages handled<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+    def test_005_count(self):<br>+        """Test range count condition"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '2-4'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        # Check with no message handled<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        # Check with one message handled<br>+        self.test_object.set_passed(True)<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        # Check with two messages handled<br>+        self.test_object.set_passed(True)<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Check with four messages handled<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Check with five messages handled<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+    def test_006_min(self):<br>+        """Test minimum condition"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '>1'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        # Check with no message handled<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        # Check with one message handled<br>+        self.test_object.set_passed(True)<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        # Check with two messages handled<br>+        self.test_object.set_passed(True)<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+    def test_007_max(self):<br>+        """Test maximum condition"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '<2'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        # Check with no message handled<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Check with one message handled<br>+        self.assertTrue(conditions.check('hello'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Check with two messages handled<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+    def test_008_max(self):<br>+        """Test no match condition"""<br>+<br>+        config = {<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '0'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        # Check with no message handled<br>+        self.assertTrue(conditions.check_final())<br>+<br>+        # Check with one message handled<br>+        self.assertFalse(conditions.check('hello'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+    def test_009_trigger_on_any(self):<br>+        """Test trigger on any option"""<br>+<br>+        config = {<br>+            'trigger-on-any': True,<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '1'},<br>+                {'match': 'world', 'count': '2'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        self.assertFalse(conditions.check('world'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        self.test_object.set_passed(True)<br>+        self.assertTrue(conditions.check('hello'))  # 'hello' is met<br>+        self.assertFalse(conditions.check_final())  # 'world' not me<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        self.assertFalse(conditions.check('world'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        self.test_object.set_passed(True)<br>+        self.assertTrue(conditions.check('world'))  # 'world' is met<br>+        self.assertFalse(conditions.check_final())  # 'hello' not met<br>+<br>+    def test_010_trigger_on_all(self):<br>+        """Test trigger on all option"""<br>+<br>+        config = {<br>+            'trigger-on-all': True,<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '1'},<br>+                {'match': 'world', 'count': '2'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        self.assertFalse(conditions.check('world'))<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        self.test_object.set_passed(True)<br>+        self.assertFalse(conditions.check('hello'))  # 'world' not met<br>+        self.assertFalse(conditions.check_final())<br>+<br>+        self.test_object.set_passed(True)<br>+        self.assertTrue(conditions.check('world'))<br>+        self.assertTrue(conditions.check_final())<br>+<br>+    def test_010_trigger_on_first(self):<br>+        """Test trigger on first match (other conditions don't apply)"""<br>+<br>+        config = {<br>+            'trigger-on-any': False,<br>+            'trigger-on-all': False,<br>+            'conditions': [<br>+                {'match': 'hello', 'count': '1'},<br>+                {'match': 'world', 'count': '2'}<br>+            ]<br>+        }<br>+<br>+        conditions = PluggableConditions(config, self.test_object)<br>+<br>+        self.assertTrue(conditions.check('world'))<br>+        self.assertFalse(conditions.check_final())  # 'hello' & 'world' not met<br>+<br>+<br>+if __name__ == "__main__":<br>+    """Run the unit tests"""<br>+<br>+    logging.basicConfig(stream=sys.stdout, level=logging.DEBUG,<br>+                        format="%(module)s:%(lineno)d - %(message)s")<br>+    unittest.main()<br>diff --git a/lib/python/mockd.py b/lib/python/mockd.py<br>deleted file mode 100644<br>index 3d4299b..0000000<br>--- a/lib/python/mockd.py<br>+++ /dev/null<br>@@ -1,114 +0,0 @@<br>-'''<br>-Copyright (C) 2015, Digium, Inc.<br>-Tyler Cambron <tcambron@digium.com><br>-<br>-This program is free software, distributed under the terms of<br>-the GNU General Public License Version 2.<br>-'''<br>-<br>-import sys<br>-import logging<br>-<br>-from twisted.internet.protocol import DatagramProtocol<br>-from twisted.internet import reactor<br>-from test_suite_utils import all_match<br>-<br>-LOGGER = logging.getLogger(__name__)<br>-<br>-<br>-class MockDProtocol(DatagramProtocol):<br>-    '''Protocol for the Mock Server to use for receiving messages.'''<br>-<br>-    def __init__(self, mockd_server):<br>-        '''Constructor.<br>-<br>-        Keyword Arguments:<br>-        mockd_server -- An instance of the mock StatsD server<br>-        '''<br>-        self.mockd_server = mockd_server<br>-<br>-    def datagramReceived(self, datagram, address):<br>-        '''An override function to handle incoming datagrams.<br>-<br>-        Keyword Arguments:<br>-        datagram -- The datagram that was received by the server<br>-        address -- The address that the datagram came from<br>-<br>-        Accept the datagram and send it to be checked against the config<br>-        '''<br>-        skip = ['stasis.message', 'channels.']<br>-        LOGGER.debug('Server received %s from %s', datagram, address)<br>-<br>-        if not (skip[0] in datagram or skip[1] in datagram):<br>-            self.mockd_server.message_handler(datagram)<br>-<br>-<br>-class MockDServer(object):<br>-    '''Pluggable Module that acts as a mock StatsD server'''<br>-<br>-    def __init__(self, config, test_object):<br>-        '''Constructor<br>-<br>-        Keyword Arguments:<br>-        config -- This object's YAML derived configuration<br>-        test_object -- The test object it plugs onto<br>-        '''<br>-        self.config = config<br>-        self.test_object = test_object<br>-        self.packets = []<br>-        self.prefix = self.config.get('prefix')<br>-<br>-        self.test_object.register_stop_observer(self._stop_handler)<br>-<br>-        reactor.listenUDP(8125, MockDProtocol(self))<br>-<br>-    def message_handler(self, message):<br>-        '''Datagram message handler<br>-<br>-        Keyword Arguments:<br>-        message -- The datagram that was received by the server<br>-<br>-        Check the message against the config and pass the test if they match<br>-        '''<br>-        if self.prefix and not message.startswith(self.prefix):<br>-            return<br>-        self.packets.append(message)<br>-<br>-    def _stop_handler(self, result):<br>-        '''A deferred callback called as a result of the test stopping<br>-<br>-        Keyword Arguments:<br>-        result -- The deferred parameter passed from callback to callback<br>-        '''<br>-        LOGGER.info('Checking packets received')<br>-<br>-        packets = self.config.get('packets')<br>-<br>-        if (packets[0] == 'ReceiveNothing') and (len(self.packets) == 0):<br>-            LOGGER.info('Server correctly received nothing')<br>-            self.test_object.set_passed(True)<br>-            return result<br>-<br>-        if len(self.packets) != len(packets):<br>-            LOGGER.error('Number of received packets {0} is not equal to '<br>-                'the number of configured packets '<br>-                '{1}'.format(len(self.packets), len(packets)))<br>-            self.test_object.set_passed(False)<br>-            return result<br>-<br>-        if self.config.get('regex', False):<br>-            cmp_fn = all_match<br>-        else:<br>-            cmp_fn = lambda expected, actual: expected == actual<br>-        failed_matches = [(actual, expected) for actual, expected in<br>-            zip(self.packets, packets) if not cmp_fn(expected, actual)]<br>-<br>-        if len(failed_matches) != 0:<br>-            LOGGER.error('The following packets failed to match: {0}'<br>-                .format(failed_matches))<br>-            self.test_object.set_passed(False)<br>-            return result<br>-<br>-        self.test_object.set_passed(True)<br>-        LOGGER.info('All packets matched')<br>-        return result<br>diff --git a/self_test b/self_test<br>index 02d8e21..1936014 100755<br>--- a/self_test<br>+++ b/self_test<br>@@ -22,11 +22,11 @@<br>       fi<br> }<br> <br>-ALL_TESTS=$(find lib/python/asterisk/self_test -name 'test_*.py' -exec basename '{}' .py \;)<br>+ALL_TESTS=$(find lib/python/asterisk/self_test -name 'test*.py' -exec basename '{}' .py \;)<br> for i in $ALL_TESTS; do<br>      run_test $i python $PYTHON<br>    run_test $i python2 $PYTHON2<br>- run_test $i python3 $PYTHON3<br>+ [ "${i#test2}" = "${i}" ] && run_test $i python3 $PYTHON3<br> done<br> <br> # Temporary code for running unit tests that are not compatible with python3<br>diff --git a/tests/channels/pjsip/statsd/contacts/test-config.yaml b/tests/channels/pjsip/statsd/contacts/test-config.yaml<br>index 5d4c66e..d4141a0 100644<br>--- a/tests/channels/pjsip/statsd/contacts/test-config.yaml<br>+++ b/tests/channels/pjsip/statsd/contacts/test-config.yaml<br>@@ -12,48 +12,49 @@<br>         typename: 'sipp.SIPpTestCase'<br>     modules:<br>         -<br>-            typename: 'mockd.MockDServer'<br>-            config-section: 'statsd-config'<br>+            config-section: event-action-config<br>+            typename: 'pluggable_modules.EventActionModule'<br> <br> test-object-config:<br>-    fail-on-any: False<br>     reactor-timeout: 10<br>     test-iterations:<br>         -<br>             scenarios:<br>                 - { 'key-args': {'scenario': 'options.xml', '-i': '127.0.0.1', '-p': '5061'} }<br> <br>-statsd-config:<br>-    regex: True<br>-    prefix: 'PJSIP.contacts'<br>-    packets:<br>-        -<br>-            'PJSIP\.contacts\.states\.Unreachable:0\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.Reachable:0\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.Unknown:0\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.(Created|NonQualified):0\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.Removed:0\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.(Created|NonQualified):\+1\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.(Created|NonQualified):\-1\|g'<br>-        -<br>-            'PJSIP\.contacts\.states\.Reachable:\+1\|g'<br>-        -<br>-            'PJSIP\.contacts\.sipp@@d0c8ec670653c9643ca96622ef658bbb\.rtt:.*\|ms'<br>+event-action-config:<br>+    event:<br>+        type: 'matcher_listener.Udp'<br>+        conditions:<br>+            -<br>+                'PJSIP\.contacts\.states\.Unreachable:0\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.Reachable:0\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.Unknown:0\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.(Created|NonQualified):0\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.Removed:0\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.(Created|NonQualified):\+1\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.(Created|NonQualified):\-1\|g'<br>+            -<br>+                'PJSIP\.contacts\.states\.Reachable:\+1\|g'<br>+            -<br>+                'PJSIP\.contacts\.sipp@@d0c8ec670653c9643ca96622ef658bbb\.rtt:.*\|ms'<br>+            -<br>+                optional: 'PJSIP\.contacts\.states\.Reachable:\-1\|g'<br>+            -<br>+                optional: 'PJSIP\.contacts\.states\.Removed:\+1\|g'<br> <br> properties:<br>     dependencies:<br>-        - python: 'autobahn.websocket'<br>-        - python: 'starpy'<br>         - python: 'twisted'<br>-        - asterisk: 'res_pjsip'<br>         - asterisk: 'res_pjsip_outbound_registration'<br>         - asterisk: 'res_statsd'<br>+        - asterisk: 'res_pjsip'<br>     tags:<br>         - statsd<br>-        - apps<br>+        - pjsip<br>diff --git a/tests/channels/pjsip/statsd/registrations/test-config.yaml b/tests/channels/pjsip/statsd/registrations/test-config.yaml<br>index 4e69e6c..b27e092 100644<br>--- a/tests/channels/pjsip/statsd/registrations/test-config.yaml<br>+++ b/tests/channels/pjsip/statsd/registrations/test-config.yaml<br>@@ -4,61 +4,57 @@<br>         'This test performs an outbound registration, and verifies that<br>         the expected StatsD statistics are generated as a result.'<br> <br>-properties:<br>-    dependencies:<br>-        - python: 'twisted'<br>-        - python: 'starpy'<br>-        - asterisk: 'res_pjsip'<br>-        - asterisk: 'res_pjsip_outbound_registration'<br>-        - asterisk: 'res_statsd'<br>-        - sipp:<br>-            version: 'v3.0'<br>-    tags:<br>-        - pjsip<br>-<br> test-modules:<br>-    add-test-to-search-path: 'True'<br>     test-object:<br>         config-section: test-object-config<br>         typename: 'sipp.SIPpTestCase'<br>     modules:<br>         -<br>-            typename: 'mockd.MockDServer'<br>-            config-section: 'statsd-config'<br>+            config-section: event-action-config<br>+            typename: 'pluggable_modules.EventActionModule'<br> <br>-statsd-config:<br>-    prefix: 'PJSIP.registrations'<br>-    packets:<br>-        -<br>-            'PJSIP.registrations.count:0|g'<br>-        -<br>-            'PJSIP.registrations.state.Registered:0|g'<br>-        -<br>-            'PJSIP.registrations.state.Unregistered:0|g'<br>-        -<br>-            'PJSIP.registrations.state.Rejected:0|g'<br>-        -<br>-            'PJSIP.registrations.count:+1|g'<br>-        -<br>-            'PJSIP.registrations.state.Unregistered:+1|g'<br>-        -<br>-            'PJSIP.registrations.state.Unregistered:-1|g'<br>-        -<br>-            'PJSIP.registrations.state.Registered:+1|g'<br>-        -<br>-            'PJSIP.registrations.state.Registered:-1|g'<br>-        -<br>-            'PJSIP.registrations.state.Unregistered:+1|g'<br>-        -<br>-            'PJSIP.registrations.count:-1|g'<br>-        -<br>-            'PJSIP.registrations.state.Unregistered:-1|g'<br> <br> test-object-config:<br>-    fail-on-any: False<br>     test-iterations:<br>         -<br>             scenarios:<br>                 - { 'key-args': {'scenario': 'register.xml', '-i': '127.0.0.1', '-p': '5061'} }<br> <br>+event-action-config:<br>+    event:<br>+        type: 'matcher_listener.Udp'<br>+        conditions:<br>+            -<br>+                'PJSIP\.registrations\.count:0\|g'<br>+            -<br>+                'PJSIP\.registrations\.state\.Registered:0\|g'<br>+            -<br>+                'PJSIP\.registrations\.state\.Unregistered:0\|g'<br>+            -<br>+                'PJSIP\.registrations\.state\.Rejected:0\|g'<br>+            -<br>+                'PJSIP\.registrations\.count:\+1\|g'<br>+            -<br>+                'PJSIP\.registrations\.state\.Registered:\+1\|g'<br>+            -<br>+                'PJSIP\.registrations\.state\.Registered:\-1\|g'<br>+            -<br>+                match: 'PJSIP\.registrations\.state\.Unregistered:\+1\|g'<br>+                count: 2<br>+            -<br>+                'PJSIP\.registrations\.count:\-1\|g'<br>+            -<br>+                match: 'PJSIP\.registrations\.state\.Unregistered:\-1\|g'<br>+                count: 2<br> <br>+properties:<br>+    dependencies:<br>+        - python: 'twisted'<br>+        - asterisk: 'res_statsd'<br>+        - asterisk: 'res_pjsip'<br>+        - asterisk: 'res_pjsip_outbound_registration'<br>+        - sipp:<br>+            version: 'v3.0'<br>+    tags:<br>+        - statsd<br>+        - pjsip<br></pre><p>To view, visit <a href="https://gerrit.asterisk.org/9048">change 9048</a>. To unsubscribe, visit <a href="https://gerrit.asterisk.org/settings">settings</a>.</p><div itemscope itemtype="http://schema.org/EmailMessage"><div itemscope itemprop="action" itemtype="http://schema.org/ViewAction"><link itemprop="url" href="https://gerrit.asterisk.org/9048"/><meta itemprop="name" content="View Change"/></div></div>

<div style="display:none"> Gerrit-Project: testsuite </div>
<div style="display:none"> Gerrit-Branch: 13 </div>
<div style="display:none"> Gerrit-MessageType: merged </div>
<div style="display:none"> Gerrit-Change-Id: Iaae769c7a4fe2dcac4865eb7dc4e5b6a1b25900b </div>
<div style="display:none"> Gerrit-Change-Number: 9048 </div>
<div style="display:none"> Gerrit-PatchSet: 4 </div>
<div style="display:none"> Gerrit-Owner: Kevin Harwell <kharwell@digium.com> </div>
<div style="display:none"> Gerrit-Reviewer: Corey Farrell <git@cfware.com> </div>
<div style="display:none"> Gerrit-Reviewer: George Joseph <gjoseph@digium.com> </div>
<div style="display:none"> Gerrit-Reviewer: Jenkins2 </div>
<div style="display:none"> Gerrit-Reviewer: Joshua Colp <jcolp@digium.com> </div>