[Asterisk-code-review] app_queue: check for leaking Stasis subscriptions after Redirect() (testsuite[17])

Friendly Automation asteriskteam at digium.com
Fri May 8 08:47:04 CDT 2020


Friendly Automation has submitted this change. ( https://gerrit.asterisk.org/c/testsuite/+/14385 )

Change subject: app_queue: check for leaking Stasis subscriptions after Redirect()
......................................................................

app_queue: check for leaking Stasis subscriptions after Redirect()

ASTERISK-28829

Change-Id: I90451cca574e2fa7d825b4cdc0bab03e331c0e9b
---
A lib/python/asterisk/taskprocessor_test_condition.py
M test-config.yaml
A tests/apps/queues/redirect/configs/ast1/extensions.conf
A tests/apps/queues/redirect/configs/ast1/pjsip.conf
A tests/apps/queues/redirect/configs/ast1/queues.conf
A tests/apps/queues/redirect/test-config.yaml
M tests/apps/queues/tests.yaml
7 files changed, 371 insertions(+), 0 deletions(-)

Approvals:
  Joshua Colp: Looks good to me, but someone else must approve
  George Joseph: Looks good to me, approved
  Friendly Automation: Approved for Submit



diff --git a/lib/python/asterisk/taskprocessor_test_condition.py b/lib/python/asterisk/taskprocessor_test_condition.py
new file mode 100644
index 0000000..889b69b
--- /dev/null
+++ b/lib/python/asterisk/taskprocessor_test_condition.py
@@ -0,0 +1,151 @@
+#!/usr/bin/env python
+
+import logging
+
+from twisted.internet import defer
+from .test_conditions import TestCondition
+
+LOGGER = logging.getLogger(__name__)
+
+# a list of task processors that should be ignored by this test
+IGNORED_TASKPROCESSORS = [
+    # the outsess traskprocessor will exist until 30 seconds after the call has ended
+    'pjsip/outsess/'
+]
+
+class Taskprocessor(object):
+    """A small object that tracks a task processor.
+
+    The object parses a line from the Asterisk CLI command core show taskprocessors to
+    populate its values
+    """
+    processor = ""
+
+    def __init__(self, line):
+        """Constructor
+
+        Keyword Arguments:
+        line A raw line received from Asterisk that describes a task processor
+        """
+        line = line.strip()
+        tokens = line.split()
+
+        if len(tokens) == 6:
+            self.processor = tokens[0].strip()
+            self.processed = int(tokens[1])
+            self.in_queue = int(tokens[2])
+            self.max_depth = int(tokens[3])
+            self.low_water = int(tokens[4])
+            self.high_water = int(tokens[5])
+
+
+class TaskprocessorTestCondition(TestCondition):
+    """Base class for the task processor pre- and post-test
+    checks. This class provides a common mechanism to get the task
+    processors from Asterisk and populate them in a dictionary for
+    tracking
+    """
+
+    def __init__(self, test_config):
+        """Constructor
+
+        Keyword Arguments:
+        test_config The test config object
+        """
+        super(TaskprocessorTestCondition, self).__init__(test_config)
+        self.task_processors = {}
+
+    def is_taskprocessor_ignored(self, name):
+        for ignored_taskprocessor in IGNORED_TASKPROCESSORS:
+            if name.startswith(ignored_taskprocessor):
+                return True
+
+        return False
+
+    def get_task_processors(self, ast):
+        """Get the task processors from some instance of Asterisk"""
+
+        def __show_taskprocessors_callback(result):
+            """Callback when CLI command has finished"""
+
+            lines = result.output
+            if 'No such command' in lines:
+                return result
+            if 'Unable to connect to remote asterisk' in lines:
+                return result
+
+            line_tokens = lines.split('\n')
+            task_processors = []
+
+            for line in line_tokens:
+                task_processor = Taskprocessor(line)
+                if task_processor.processor != '' and not self.is_taskprocessor_ignored(task_processor.processor):
+                    LOGGER.debug("Tracking %s", task_processor.processor)
+                    task_processors.append(task_processor)
+
+            self.task_processors[result.host] = task_processors
+
+        return ast.cli_exec("core show taskprocessors").addCallback(__show_taskprocessors_callback)
+
+
+class TaskprocessorPreTestCondition(TaskprocessorTestCondition):
+    """The Task Processor Pre-TestCondition object"""
+
+    def evaluate(self, related_test_condition=None):
+        """Evaluate the test condition"""
+
+        def __raise_finished(result, finish_deferred):
+            """Called when all CLI commands have finished"""
+            finished_deferred.callback(self)
+            return result
+
+        # Automatically pass the pre-test condition - whatever task processors
+        # are currently open are needed by Asterisk and merely expected to exist
+        # when the test is finished
+        super(TaskprocessorPreTestCondition, self).pass_check()
+
+        finished_deferred = defer.Deferred()
+        exec_list = [super(TaskprocessorPreTestCondition, self).get_task_processors(ast)
+                     for ast in self.ast]
+        defer.DeferredList(exec_list).addCallback(__raise_finished, finished_deferred)
+
+        return finished_deferred
+
+
+class TaskprocessorPostTestCondition(TaskprocessorTestCondition):
+    """The Task Processor Post-TestCondition object"""
+
+    def evaluate(self, related_test_condition=None):
+        """Evaluate the test condition"""
+
+        def __task_processors_obtained(result, finished_deferred):
+            """Callback when all CLI commands have finished"""
+            for ast_host in related_test_condition.task_processors.keys():
+                if not ast_host in self.task_processors:
+                    super(TaskprocessorPostTestCondition, self).fail_check(
+                        "Asterisk host in pre-test check [%s]"
+                        " not found in post-test check" % ast_host)
+                else:
+                    # Find all task processors in post-check not in pre-check
+                    for task_processor in self.task_processors[ast_host]:
+                        if (len([
+                                t for t
+                                in related_test_condition.task_processors[ast_host]
+                                if task_processor.processor == t.processor]) == 0):
+                            super(TaskprocessorPostTestCondition, self).fail_check(
+                                "Failed to find task processor %s in "
+                                "pre-test check" % (task_processor.processor))
+            super(TaskprocessorPostTestCondition, self).pass_check()
+            finished_deferred.callback(self)
+            return result
+
+        if related_test_condition is None:
+            msg = "No pre-test condition object provided"
+            super(TaskprocessorPostTestCondition, self).fail_check(msg)
+            return
+
+        finished_deferred = defer.Deferred()
+        exec_list = [super(TaskprocessorPostTestCondition, self).get_task_processors(ast)
+                     for ast in self.ast]
+        defer.DeferredList(exec_list).addCallback(__task_processors_obtained, finished_deferred)
+        return finished_deferred
diff --git a/test-config.yaml b/test-config.yaml
index 6ea1339..dd00437 100644
--- a/test-config.yaml
+++ b/test-config.yaml
@@ -64,6 +64,13 @@
                 post:
                     typename: 'memory_test_condition.MemoryPostTestCondition'
                     related-type: 'memory_test_condition.MemoryPreTestCondition'
+            -
+                name: 'taskprocessors'
+                pre:
+                    typename: 'taskprocessor_test_condition.TaskprocessorPreTestCondition'
+                post:
+                    typename: 'taskprocessor_test_condition.TaskprocessorPostTestCondition'
+                    related-type: 'taskprocessor_test_condition.TaskprocessorPreTestCondition'
 
 # Exclude all long-running tests (greater than one minute)
 config-fast:
diff --git a/tests/apps/queues/redirect/configs/ast1/extensions.conf b/tests/apps/queues/redirect/configs/ast1/extensions.conf
new file mode 100644
index 0000000..ae535bf
--- /dev/null
+++ b/tests/apps/queues/redirect/configs/ast1/extensions.conf
@@ -0,0 +1,13 @@
+[default]
+
+exten => 101,1,Queue(queue,Rtc)
+exten => 102,1,Wait(5)
+
+[park]
+
+exten => s,1,Wait(5)
+
+[dialbob]
+
+exten => s,1,Dial(PJSIP/bob)
+    same => Hangup()
diff --git a/tests/apps/queues/redirect/configs/ast1/pjsip.conf b/tests/apps/queues/redirect/configs/ast1/pjsip.conf
new file mode 100644
index 0000000..c4add2f
--- /dev/null
+++ b/tests/apps/queues/redirect/configs/ast1/pjsip.conf
@@ -0,0 +1,33 @@
+[global]
+type=global
+debug=yes
+
+[local]
+type=transport
+protocol=udp
+bind=127.0.0.1:5060
+
+[endpoint_t](!)
+type=endpoint
+context=default
+direct_media=no
+disallow=all
+allow=ulaw
+
+[aor_t](!)
+type=aor
+max_contacts=1
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+[alice](aor_t)
+[alice](endpoint_t)
+aors=alice
+
+[bob](aor_t)
+[bob](endpoint_t)
+aors=bob
+
+[carol](aor_t)
+[carol](endpoint_t)
+aors=carol
diff --git a/tests/apps/queues/redirect/configs/ast1/queues.conf b/tests/apps/queues/redirect/configs/ast1/queues.conf
new file mode 100644
index 0000000..cac3974
--- /dev/null
+++ b/tests/apps/queues/redirect/configs/ast1/queues.conf
@@ -0,0 +1,4 @@
+[general]
+
+[queue]
+member => Local/s at dialbob/n
diff --git a/tests/apps/queues/redirect/test-config.yaml b/tests/apps/queues/redirect/test-config.yaml
new file mode 100644
index 0000000..510dcc2
--- /dev/null
+++ b/tests/apps/queues/redirect/test-config.yaml
@@ -0,0 +1,162 @@
+testinfo:
+    summary: "Redirected queue call"
+    description: |
+        "We have Alice call the queue, which has Local/dialbob as member. Bob will
+        be called through this local channel. Upon answering, we'll have Carol call
+        the dialplan and Wait() there for further instructions.
+
+        After Carol's call is setup, we Redirect() Alice's call to the dialplan,
+        which will break her call/bridge with Bob. We then use Bridge() to bridge
+        Alice's call with Carol's call that was still waiting in the dialplan.
+
+        After this scenario succeeded, there should be no leaked Stasis subscriptions,
+        which we check by comparing the list of taskprocessors at the start of the test
+        with the list at the end of the test. ASTERISK-28829 will cause two subscriptions/
+        task processors to leak: stasis/p:bridge:all and stasis/p:channel:all"
+
+test-modules:
+    test-object:
+        config-section: test-object-config
+        typename: 'test_case.TestCaseModule'
+    modules:
+        -
+            config-section: pjsua-config
+            typename: 'phones.PjsuaPhoneController'
+        -
+            config-section: pluggable-config
+            typename: 'pluggable_modules.EventActionModule'
+
+test-object-config:
+    connect-ami: True
+
+pjsua-config:
+    transports:
+        -
+            name: 'local-ipv4-1'
+            bind: '127.0.0.1'
+            bindport: '5061'
+        -
+            name: 'local-ipv4-2'
+            bind: '127.0.0.1'
+            bindport: '5062'
+        -
+            name: 'local-ipv4-3'
+            bind: '127.0.0.1'
+            bindport: '5063'
+
+    accounts:
+        -
+            name: 'alice'
+            username: 'alice'
+            domain: '127.0.0.1'
+            transport: 'local-ipv4-1'
+        -
+            name: 'bob'
+            username: 'bob'
+            domain: '127.0.0.1'
+            transport: 'local-ipv4-2'
+        -
+            name: 'carol'
+            username: 'carol'
+            domain: '127.0.0.1'
+            transport: 'local-ipv4-3'
+
+pluggable-config:
+    -
+        ami-events:
+            conditions:
+                match:
+                    Event: 'UserEvent'
+                    UserEvent: 'PJsuaPhonesReady'
+            count: 1
+        # alice dials the queue
+        pjsua_phone:
+            action: 'call'
+            pjsua_account: 'alice'
+            call_uri: 'sip:101 at 127.0.0.1'
+    -
+        # queue rings bob, bob answers, alice is bridged with bob
+        ami-events:
+            conditions:
+                match:
+                    Event: 'BridgeEnter'
+                    Channel: 'PJSIP/alice-00000000'
+                    Context: 'default'
+            count: 1
+        # then carol starts a call that ends in the dialplan Wait()
+        pjsua_phone:
+            action: 'call'
+            pjsua_account: 'carol'
+            call_uri: 'sip:102 at 127.0.0.1'
+    -
+        # when we detect carol's call..
+        ami-events:
+            conditions:
+                match:
+                    Event: 'Newexten'
+                    Channel: 'PJSIP/carol-00000002'
+                    Exten: '102'
+            count: 1
+        # .. we redirect alice to a park extension (this will halt her call with bob)
+        ami-actions:
+            -
+                action:
+                    Action: 'Redirect'
+                    Channel: 'PJSIP/alice-00000000'
+                    Context: 'park'
+                    Exten: 's'
+                    Priority: '1'
+    -
+        # wait for alice to be parked..
+        ami-events:
+            conditions:
+                match:
+                    Event: 'Newexten'
+                    Channel: 'PJSIP/alice-00000000'
+                    Context: 'park'
+            count: 1
+        # .. then bridge alice with carol
+        ami-actions:
+            action:
+                Action: 'Bridge'
+                Channel1: 'PJSIP/alice-00000000'
+                Channel2: 'PJSIP/carol-00000002'
+    # once alice has entered the bridge with carol, we're done
+    -
+        ami-events:
+            conditions:
+                match:
+                    Event: 'BridgeEnter'
+                    Channel: 'PJSIP/carol-00000002'
+                    Context: 'default'
+            count: 1
+        ami-actions:
+            action:
+                Action: 'Hangup'
+                Channel: 'PJSIP/alice-00000000'
+    -
+        ami-events:
+            conditions:
+                match:
+                    Event: 'Hangup'
+                    Channel: 'PJSIP/carol-00000002'
+                    Context: 'default'
+            count: 1
+        stop_test:
+
+properties:
+    minversion: '13.6.0'
+    dependencies:
+        - python : twisted
+        - python : starpy
+        - python : pjsua
+        - asterisk : res_pjsip
+        - asterisk : app_queue
+    tags:
+        - pjsip
+        - queues
+        - chan_local
+    testconditions:
+        -
+            # this will check for leaking stasis subscriptions/taskprocessors at the end of the testrun
+            name: 'taskprocessors'
diff --git a/tests/apps/queues/tests.yaml b/tests/apps/queues/tests.yaml
index be78da0..e1c0d0c 100644
--- a/tests/apps/queues/tests.yaml
+++ b/tests/apps/queues/tests.yaml
@@ -16,3 +16,4 @@
     - test: 'wrapup_time_per_member'
     - test: 'reason_pause_ami'
     - test: 'queue_member_forward'
+    - test: 'redirect'

-- 
To view, visit https://gerrit.asterisk.org/c/testsuite/+/14385
To unsubscribe, or for help writing mail filters, visit https://gerrit.asterisk.org/settings

Gerrit-Project: testsuite
Gerrit-Branch: 17
Gerrit-Change-Id: I90451cca574e2fa7d825b4cdc0bab03e331c0e9b
Gerrit-Change-Number: 14385
Gerrit-PatchSet: 1
Gerrit-Owner: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Friendly Automation
Gerrit-Reviewer: George Joseph <gjoseph at digium.com>
Gerrit-Reviewer: Joshua Colp <jcolp at sangoma.com>
Gerrit-Reviewer: lvl <digium at lvlconsultancy.nl>
Gerrit-MessageType: merged
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.digium.com/pipermail/asterisk-code-review/attachments/20200508/c654efc0/attachment-0001.html>


More information about the asterisk-code-review mailing list