#!/usr/bin/env python
'''
Copyright (C) 2014, Digium, Inc.
Walter Doekes <walter+asterisk@wjd.nu>

This program is free software, distributed under the terms of
the GNU General Public License Version 2.


Bug ASTERISK-20784:

    A INVITE> asterisk INVITE> B
    A <200 OK asterisk <200 OK B
    A -ACK--> asterisk -ACK--> B
              asterisk <INVITE B
              asterisk 200 OK> B
                      (ack lost)
    A -BYE--> asterisk
    A <200 OK asterisk

At this point the B leg retransmit the 200 to the INVITE and finally
leaks a dialog object.  Or at least, it did until this bug was fixed.

We cannot just use the yaml-based sipp.SIPpTestCase config, because
we need to extract a bit of extra info from Asterisk after the
scenario's complete.
'''

import logging
import os
import sys
import time

sys.path.append("lib/python")
sys.path.append("utils")

from twisted.internet import reactor
from asterisk.sipp import SIPpTest

WORKING_DIR = os.path.abspath(os.path.dirname(__file__))
TEST_DIR = os.path.dirname(os.path.realpath(__file__))

LOGGER = logging.getLogger(__name__)


class MySIPpTest(SIPpTest):
    def __init__(self):
        # A list of jobs to complete before stopping completely. This
        # list is extended during the stoppage.
        self.stop_jobs = [self.get_sleep,
                          self.get_channels,
                          self.get_sip_objects]

        SIPP_SCENARIOS = [
            {'scenario': 'bob.xml',
             '-p': '5063',
             '-default_behaviors': '-bye'},
            {'scenario': 'alice.xml',
             '-p': '5062',
             '-s': 'bob',
             '-default_behaviors': '-bye'},
        ]

        super(MySIPpTest, self).__init__(WORKING_DIR, TEST_DIR, SIPP_SCENARIOS)

    def clean_cli_output(self, output):
        if output.endswith('Asterisk ending (0).\n'):
            output = output[0:-21]
        return output.strip()

    def should_be_done_by_now(self):
        # We use a timert1 of 50 which should stop retransmissions
        # after 3.2 seconds.
        # t0 = call is initiated
        # t1 = call is complete (except for Bob-leg)
        # t5 = asterisk has timed out sending 200s, start sending BYE
        # t9 = asterisk has timed out sending BYE, destroy object
        # (in practice, it turns out it takes about 13+ seconds)
        # t20 = give everyone some extra time
        return (time.time() - self.t0 > 20)

    def get_channels(self):
        def ret(obj):
            output = self.clean_cli_output(obj.output)
            if output.startswith('0 active channels'):
                # No need to schedule ourselves anymore.
                pass
            elif self.should_be_done_by_now():
                LOGGER.warn('Expected 0 active calls: %r' % (output,))
                self.passed = False
            else:
                # Try again in a while.
                self.stop_jobs.extend([self.get_sleep, self.get_channels])
            self.next_job()
        self.ast[0].cli_exec('core show channels count').addCallback(ret)

    def get_sip_objects(self):
        def ret(obj):
            output = self.clean_cli_output(obj.output)
            dialog_objs = output.split('-= Dialog objects:', 1)[-1].strip()
            if not dialog_objs:
                # No need to schedule ourselves anymore.
                pass
            elif self.should_be_done_by_now():
                LOGGER.warn('Expected 0 sip dialog objects:\n  %s' %
                            (dialog_objs.replace('\n', '\n  '),))
                self.passed = False
            else:
                # Try again in a while.
                self.stop_jobs.extend([self.get_sleep, self.get_sip_objects])
            self.next_job()
        self.ast[0].cli_exec('sip show objects').addCallback(ret)

    def get_sleep(self):
        reactor.callLater(1, self.next_job)

    def next_job(self):
        if not self.stop_jobs:
            # Really done.
            super(MySIPpTest, self).stop_reactor()
        else:
            # Schedule the next job.
            first_job = self.stop_jobs.pop(0)
            first_job()

    def run(self):
        """
        This is called by the TestCase when we start.  Start counting
        the clock.
        """
        super(MySIPpTest, self).run()
        self.t0 = time.time()

    def stop_reactor(self):
        """
        This is called by the TestCase when we're done.  Defer that
        until after we've pulled some info out of asterisk.
        """
        self.next_job()


def main():
    test = MySIPpTest()
    reactor.run()

    # If it failed, bail.
    if not test.passed:
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())

# vim:sw=4:ts=4:expandtab:textwidth=79
