def _setup_conditions(self):
        """Register pre and post-test conditions.

        Note that we have to first register condition checks without related
        conditions, so that those that have dependencies can find them
        """
        self.condition_controller = TestConditionController(self.test_config,
                                                            self.ast,
                                                            self.stop_reactor)
        global_conditions = self.global_config.get_conditions()
        conditions = self.test_config.get_conditions()

        # Get those global conditions that are not in the self conditions
        for g_cond in global_conditions:
            disallowed = [i for i in conditions
                          if i[0].get_name() == g_cond[0].get_name()
                          and i[1] == g_cond[1]]
            if len(disallowed) == 0:
                conditions.append(g_cond)

        for cond in conditions:
            # cond is a 3-tuple of object, pre-post type, and related name
            obj, pre_post_type, related_name = cond
            if pre_post_type == "PRE":
                self.condition_controller.register_pre_test_condition(obj)
            elif pre_post_type == "POST":
                self.condition_controller.register_post_test_condition(obj, related_name)
            else:
                msg = "Unknown condition type [%s]" % pre_post_type
                LOGGER.warning(msg)
        self.condition_controller.register_observer(
            self.handle_condition_failure, 'Failed')
class TestCase(object):
    """The base class object for python tests. This class provides common
    functionality to all tests, including management of Asterisk instances, AMI,
    twisted reactor, and various other utilities.
    """

    def __init__(self, test_path='', test_config=None):
        """Create a new instance of a TestCase. Must be called by inheriting
        classes.

        Keyword Arguments:
        test_path Optional parameter that specifies the path where this test
                  resides
        test_config Loaded YAML test configuration
        """

        if not len(test_path):
            self.test_name = os.path.dirname(sys.argv[0])
        else:
            self.test_name = test_path

        # We're not using /tmp//full//test//name because it gets so long that
        # it doesn't fit in AF_UNIX paths (limited to around 108 chars) used
        # for the rasterisk CLI connection. As a quick fix, we hash the path
        # using md5, to make it unique enough.
        self.realbase = self.test_name.replace("tests/", "", 1)
        self.base = md5(self.realbase).hexdigest()
        # We provide a symlink to it from a named path.
        named_dir = os.path.join(Asterisk.test_suite_root, self.realbase)
        try:
            os.makedirs(os.path.dirname(named_dir))
        except OSError:
            pass
        try:
            join_path = os.path.relpath(
                os.path.join(Asterisk.test_suite_root, self.base),
                os.path.dirname(named_dir)
            )
            os.symlink(join_path, named_dir)
        except OSError:
            pass

        self.ast = []
        self.ami = []
        self.fastagi = []
        self.base_config_path = None
        self.reactor_timeout = 30
        self.passed = None
        self.fail_tokens = []
        self.timeout_id = None
        self.global_config = TestConfig(os.getcwd())
        self.test_config = TestConfig(self.test_name, self.global_config)
        self.condition_controller = None
        self.pcap = None
        self.pcapfilename = None
        self.create_pcap = False
        self._stopping = False
        self.testlogdir = self._set_test_log_directory()
        self.ast_version = AsteriskVersion()
        self._start_callbacks = []
        self._stop_callbacks = []
        self._ami_callbacks = []
        self._pcap_callbacks = []
        self._stop_deferred = None
        log_full = True
        log_messages = True

        if os.getenv("VALGRIND_ENABLE") == "true":
            self.reactor_timeout *= 20

        # Pull additional configuration from YAML config if possible
        if test_config:
            if 'config-path' in test_config:
                self.base_config_path = test_config['config-path']
            if 'reactor-timeout' in test_config:
                self.reactor_timeout = test_config['reactor-timeout']
            self.ast_conf_options = test_config.get('ast-config-options')
            log_full = test_config.get('log-full', True)
            log_messages = test_config.get('log-messages', True)
        else:
            self.ast_conf_options = None

        os.makedirs(self.testlogdir)

        # Set up logging
        setup_logging(self.testlogdir, log_full, log_messages)

        LOGGER.info("Executing " + self.test_name)

        if PCAP_AVAILABLE and self.create_pcap:
            self.pcapfilename = os.path.join(self.testlogdir, "dumpfile.pcap")
            self.pcap = self.create_pcap_listener(dumpfile=self.pcapfilename)

        self._setup_conditions()

        # Enable twisted logging
        observer = log.PythonLoggingObserver()
        observer.start()

        reactor.callWhenRunning(self._run)

    def _set_test_log_directory(self):
        """Determine which logging directory we should use for this test run

        Returns:
        The full path that should be used as the directory for all log data
        """
        i = 1
        base_path = os.path.join(Asterisk.test_suite_root, self.base)
        while os.path.isdir(os.path.join(base_path, "run_%d" % i)):
            i += 1
        full_path = os.path.join(base_path, "run_%d" % i)
        return full_path

    def _setup_conditions(self):
        """Register pre and post-test conditions.

        Note that we have to first register condition checks without related
        conditions, so that those that have dependencies can find them
        """
        self.condition_controller = TestConditionController(self.test_config,
                                                            self.ast,
                                                            self.stop_reactor)
        global_conditions = self.global_config.get_conditions()
        conditions = self.test_config.get_conditions()

        # Get those global conditions that are not in the self conditions
        for g_cond in global_conditions:
            disallowed = [i for i in conditions
                          if i[0].get_name() == g_cond[0].get_name()
                          and i[1] == g_cond[1]]
            if len(disallowed) == 0:
                conditions.append(g_cond)

        for cond in conditions:
            # cond is a 3-tuple of object, pre-post type, and related name
            obj, pre_post_type, related_name = cond
            if pre_post_type == "PRE":
                self.condition_controller.register_pre_test_condition(obj)
            elif pre_post_type == "POST":
                self.condition_controller.register_post_test_condition(obj, related_name)
            else:
                msg = "Unknown condition type [%s]" % pre_post_type
                LOGGER.warning(msg)
        self.condition_controller.register_observer(
            self.handle_condition_failure, 'Failed')

    def get_asterisk_hosts(self, count):
        """Return a list of host dictionaries for Asterisk instances

        Keyword Arguments:
        count  The number of Asterisk instances to create, if no remote
               Asterisk instances have been specified
        """
        if (self.global_config.config and
            'asterisk-instances' in self.global_config.config):
            asterisks = self.global_config.config.get('asterisk-instances')
        else:
            asterisks = [{'num': i + 1, 'host': '127.0.0.%d' % (i + 1)}
                         for i in range(count)]
        return asterisks

    def create_asterisk(self, count=1, base_configs_path=None):
        """Create n instances of Asterisk

        Note: if the instances of Asterisk being created are remote, the
        keyword arguments to this function are ignored.

        Keyword arguments:
        count             The number of Asterisk instances to create.  Each
                          Asterisk instance will be hosted on 127.0.0.x, where x
                          is the 1-based index of the instance created.
        base_configs_path Provides common configuration for Asterisk instances
                          to use. This is useful for certain test types that use
                          the same configuration all the time. This
                          configuration can be overwritten by individual tests,
                          however.
        """
        for i, ast_config in enumerate(self.get_asterisk_hosts(count)):
            local_num = ast_config.get('num')
            host = ast_config.get('host')

            if not host:
                msg = "Cannot manage Asterisk instance without 'host'"
                raise Exception(msg)

            if local_num:
                LOGGER.info("Creating Asterisk instance %d" % local_num)
                ast_instance = Asterisk(base=self.testlogdir, host=host,
                                        ast_conf_options=self.ast_conf_options)
            else:
                LOGGER.info("Managing Asterisk instance at %s" % host)
                ast_instance = Asterisk(base=self.testlogdir, host=host,
                                        remote_config=ast_config)
            self.ast.append(ast_instance)
            self.condition_controller.register_asterisk_instance(self.ast[i])

            if local_num:
                # If a base configuration for this Asterisk instance has been
                # provided, install it first
                if base_configs_path is None:
                    base_configs_path = self.base_config_path
                if base_configs_path:
                    ast_dir = "%s/ast%d" % (base_configs_path, local_num)
                    self.ast[i].install_configs(ast_dir,
                                                self.test_config.get_deps())
                # Copy test specific config files
                self.ast[i].install_configs("%s/configs/ast%d" %
                                            (self.test_name, local_num),
                                            self.test_config.get_deps())

    def create_ami_factory(self, count=1, username="******", secret="mysecret",
                           port=5038):
        """Create n instances of AMI.  Each AMI instance will attempt to connect
        to a previously created instance of Asterisk.  When a connection is
        complete, the ami_connect method will be called.

        Keyword arguments:
        count    The number of instances of AMI to create
        username The username to login with
        secret   The password to login with
        port     The port to connect over
        """

        def on_reconnect(login_deferred):
            """Called if the connection is lost and re-made"""
            login_deferred.addCallbacks(self._ami_connect, self.ami_login_error)

        for i, ast_config in enumerate(self.get_asterisk_hosts(count)):
            host = ast_config.get('host')
            ami_config = ast_config.get('ami', {})
            actual_user = ami_config.get('username', username)
            actual_secret = ami_config.get('secret', secret)
            actual_port = ami_config.get('port', port)

            self.ami.append(None)
            LOGGER.info("Creating AMIFactory %d to %s" % ((i + 1), host))
            try:
                ami_factory = manager.AMIFactory(actual_user, actual_secret, i,
                                             on_reconnect=on_reconnect)
            except:
                ami_factory = manager.AMIFactory(actual_user, actual_secret, i)
            deferred = ami_factory.login(ip=host, port=actual_port)
            deferred.addCallbacks(self._ami_connect, self.ami_login_error)

    def create_fastagi_factory(self, count=1):
        """Create n instances of AGI.  Each AGI instance will attempt to connect
        to a previously created instance of Asterisk.  When a connection is
        complete, the fastagi_connect method will be called.

        Keyword arguments:
        count The number of instances of AGI to create
        """

        for i, ast_config in enumerate(self.get_asterisk_hosts(count)):
            host = ast_config.get('host')

            self.fastagi.append(None)
            LOGGER.info("Creating FastAGI Factory %d" % (i + 1))
            fastagi_factory = fastagi.FastAGIFactory(self.fastagi_connect)
            reactor.listenTCP(4573, fastagi_factory,
                              self.reactor_timeout, host)

    def fastagi_connect(self, agi):
        """Callback called by starpy when FastAGI connects

        This method should be overridden by derived classes that use
        create_fastagi_factory

        Keyword arguments:
        agi The AGI manager
        """
        pass

    def create_pcap_listener(self, device=None, bpf_filter=None, dumpfile=None,
                             snaplen=None, buffer_size=None):
        """Create a single instance of a pcap listener.

        Keyword arguments:
        device      The interface to listen on. Defaults to the first interface
                    beginning with 'lo'.
        bpf_filter  BPF (filter) describing what packets to match, i.e.
                    "port 5060"
        dumpfile    The filename at which to save a pcap capture
        snaplen     Number of bytes to capture from each packet. Defaults to
                    65535.
        buffer_size The ring buffer size. Defaults to 0.

        """

        if not PCAP_AVAILABLE:
            msg = ("PCAP not available on this machine. "
                   "Test config is missing pcap dependency.")
            raise Exception(msg)

        # TestCase will create a listener for logging purposes, and individual
        # tests can create their own. Tests may only want to watch a specific
        # port, while a general logger will want to watch more general traffic
        # which can be filtered later.
        return PcapListener(device, bpf_filter, dumpfile, self._pcap_callback,
                            snaplen, buffer_size)

    def start_asterisk(self):
        """This method will be called when the reactor is running, but
        immediately before instances of Asterisk are launched. Derived classes
        can override this if needed.
        """
        pass

    def _start_asterisk(self):
        """Start the instances of Asterisk that were previously created. See
        create_asterisk. Note that this should be the first thing called
        when the reactor has started to run
        """
        def __check_success_failure(result):
            """Make sure the instances started properly"""
            for (success, value) in result:
                if not success:
                    LOGGER.error(value.getErrorMessage())
                    self.stop_reactor()
            return result

        def __perform_pre_checks(result):
            """Execute the pre-condition checks"""
            deferred = self.condition_controller.evaluate_pre_checks()
            if deferred is None:
                return result
            else:
                return deferred

        def __run_callback(result):
            """Notify the test that we are running"""
            for callback in self._start_callbacks:
                callback(self.ast)
            self.run()
            return result

        # Call the method that derived objects can override
        self.start_asterisk()

        # Gather up the deferred objects from each of the instances of Asterisk
        # and wait until all are finished before proceeding
        start_defers = []
        for index, ast in enumerate(self.ast):
            LOGGER.info("Starting Asterisk instance %d" % (index + 1))
            temp_defer = ast.start(self.test_config.get_deps())
            start_defers.append(temp_defer)

        deferred = defer.DeferredList(start_defers, consumeErrors=True)
        deferred.addCallback(__check_success_failure)
        deferred.addCallback(__perform_pre_checks)
        deferred.addCallback(__run_callback)

    def stop_asterisk(self):
        """This method is called when the reactor is running but immediately
        before instances of Asterisk are stopped. Derived classes can override
        this method if needed.
        """
        pass

    def _stop_asterisk(self):
        """Stops the instances of Asterisk.

        Returns:
        A deferred object that can be used to be notified when all instances
        of Asterisk have stopped.
         """
        def __check_success_failure(result):
            """Make sure the instances stopped properly"""
            for (success, value) in result:
                if not success:
                    LOGGER.warning(value.getErrorMessage())
                    # This should already be called when the reactor is being
                    # terminated. If we couldn't stop the instance of Asterisk,
                    # there isn't much else to do here other then complain
            self._stop_deferred.callback(self)
            return result

        def __stop_instances(result):
            """Stop the instances"""

            # Call the overridable method now
            self.stop_asterisk()
            # Gather up the stopped defers; check success failure of stopping
            # when all instances of Asterisk have stopped
            stop_defers = []
            for index, ast in enumerate(self.ast):
                LOGGER.info("Stopping Asterisk instance %d" % (index + 1))
                temp_defer = ast.stop()
                stop_defers.append(temp_defer)

            defer.DeferredList(stop_defers).addCallback(
                __check_success_failure)
            return result

        self._stop_deferred = defer.Deferred()
        deferred = self.condition_controller.evaluate_post_checks()
        if deferred:
            deferred.addCallback(__stop_instances)
        else:
            __stop_instances(None)
        return self._stop_deferred

    def stop_reactor(self):
        """Stop the reactor and cancel the test."""

        def __stop_reactor(result):
            """Called when the Asterisk instances are stopped"""
            LOGGER.info("Stopping Reactor")
            if reactor.running:
                try:
                    reactor.stop()
                except twisted_error.ReactorNotRunning:
                    # Something stopped it between our checks - at least we're
                    # stopped
                    pass
            return result
        if not self._stopping:
            self._stopping = True
            deferred = self._stop_asterisk()
            for callback in self._stop_callbacks:
                deferred.addCallback(callback)
            deferred.addCallback(__stop_reactor)

    def _reactor_timeout(self):
        """A wrapper function for stop_reactor(), so we know when a reactor
        timeout has occurred.
        """
        LOGGER.warning("Reactor timeout: '%s' seconds" % self.reactor_timeout)
        self.on_reactor_timeout()
        self.stop_reactor()

    def on_reactor_timeout(self):
        """Virtual method called when reactor times out"""
        pass

    def _run(self):
        """Private entry point called when the reactor first starts up. This
        needs to first ensure that Asterisk is fully up and running before
        moving on.
        """
        if self.ast:
            self._start_asterisk()
        else:
            # If no instances of Asterisk are needed, go ahead and just run
            self.run()

    def run(self):
        """Base implementation of the test execution method, run. Derived
        classes should override this and start their Asterisk dependent logic
        from this method.

        Derived classes must call this implementation, as this method provides a
        fail out mechanism in case the test hangs.
        """
        if self.reactor_timeout > 0:
            self.timeout_id = reactor.callLater(self.reactor_timeout,
                                                self._reactor_timeout)

    def ami_login_error(self, reason):
        """Handler for login errors into AMI. This will stop the test.

        Keyword arguments:
        ami The instance of AMI that raised the login error
        """
        LOGGER.error("Error logging into AMI: %s" % reason.getErrorMessage())
        LOGGER.error(reason.getTraceback())
        self.stop_reactor()
        return reason

    def ami_connect(self, ami):
        """Virtual method used after create_ami_factory() successfully logs into
        the Asterisk AMI.
        """
        pass

    def _ami_connect(self, ami):
        """Callback when AMI first connects"""
        LOGGER.info("AMI Connect instance %s" % (ami.id + 1))
        self.ami[ami.id] = ami
        try:
            for callback in self._ami_callbacks:
                callback(ami)
            self.ami_connect(ami)
        except:
            LOGGER.error("Exception raised in ami_connect:")
            LOGGER.error(traceback.format_exc())
            self.stop_reactor()
        return ami

    def pcap_callback(self, packet):
        """Virtual method used to receive captured packets."""
        pass

    def _pcap_callback(self, packet):
        """Packet capture callback"""
        self.pcap_callback(packet)
        for callback in self._pcap_callbacks:
            callback(packet)

    def handle_originate_failure(self, reason):
        """Fail the test on an Originate failure

        Convenience callback handler for twisted deferred errors for an AMI
        originate call. Derived classes can choose to add this handler to
        originate calls in order to handle them safely when they fail.
        This will stop the test if called.

        Keyword arguments:
        reason The reason the originate failed
        """
        LOGGER.error("Error sending originate: %s" % reason.getErrorMessage())
        LOGGER.error(reason.getTraceback())
        self.stop_reactor()
        return reason

    def reset_timeout(self):
        """Resets the reactor timeout"""
        if self.timeout_id is not None:
            original_time = datetime.fromtimestamp(self.timeout_id.getTime())
            self.timeout_id.reset(self.reactor_timeout)
            new_time = datetime.fromtimestamp(self.timeout_id.getTime())
            msg = ("Reactor timeout originally scheduled for %s, "
                   "rescheduled for %s" % (str(original_time), str(new_time)))
            LOGGER.info(msg)

    def handle_condition_failure(self, test_condition):
        """Callback handler for condition failures"""
        if test_condition.pass_expected:
            msg = ("Test Condition %s failed; setting passed status to False" %
                   test_condition.get_name())
            LOGGER.error(msg)
            self.passed = False
        else:
            msg = ("Test Condition %s failed but expected failure was set; "
                   "test status not modified" % test_condition.get_name())
            LOGGER.info(msg)

    def evaluate_results(self):
        """Return whether or not the test has passed"""

        while len(self.fail_tokens):
            fail_token = self.fail_tokens.pop(0)
            LOGGER.error("Fail token present: %s" % fail_token['message'])
            self.passed = False

        return self.passed

    def register_pcap_observer(self, callback):
        """Register an observer that will be called when a packet is received
        from a created pcap listener

        Keyword Arguments:
        callback The callback to receive the packet. The callback function
                 should take in a single parameter, which will be the packet
                 received
        """
        self._pcap_callbacks.append(callback)

    def register_start_observer(self, callback):
        """Register an observer that will be called when all Asterisk instances
        have started

        Keyword Arguments:
        callback The deferred callback function to be called when all instances
                 of Asterisk have started. The callback should take no
                 parameters.
        """
        self._start_callbacks.append(callback)

    def register_stop_observer(self, callback):
        """Register an observer that will be called when Asterisk is stopped

        Keyword Arguments:
        callback The deferred callback function to be called when Asterisk is
                 stopped

        Note:
        This appends a callback to the deferred chain of callbacks executed when
        all instances of Asterisk are stopped.
        """
        self._stop_callbacks.append(callback)

    def register_ami_observer(self, callback):
        """Register an observer that will be called when TestCase connects with
        Asterisk over the Manager interface

        Parameters:
        callback The deferred callback function to be called when AMI connects
        """
        self._ami_callbacks.append(callback)

    def create_fail_token(self, message):
        """Add a fail token to the test. If any fail tokens exist at the end of
        the test, the test will fail.

        Keyword Arguments:
        message A text message describing the failure

        Returns:
        A token that can be removed from the test at a later time, if the test
        should pass
        """
        fail_token = {'uuid': uuid.uuid4(), 'message': message}
        self.fail_tokens.append(fail_token)
        return fail_token

    def remove_fail_token(self, fail_token):
        """Remove a fail token from the test.

        Keyword Arguments:
        fail_token A previously created fail token to be removed from the test
        """
        if not fail_token in self.fail_tokens:
            LOGGER.warning('Attempted to remove an unknown fail token: %s',
                           fail_token['message'])
            self.passed = False
            return
        self.fail_tokens.remove(fail_token)

    def set_passed(self, value):
        """Accumulate pass/fail value.

        If a test module has already claimed that the test has failed, then this
        method will ignore any further attempts to change the pass/fail status.
        """
        if self.passed is False:
            return
        self.passed = value