<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>