# Copyright 2013 Red Hat, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

import threading
from unittest import mock
import warnings


import eventlet
import fixtures
from oslo_config import cfg
from oslo_utils import eventletutils
import testscenarios

import oslo_messaging
from oslo_messaging import rpc
from oslo_messaging.rpc import dispatcher
from oslo_messaging.rpc import server as rpc_server_module
from oslo_messaging import server as server_module
from oslo_messaging.tests import utils as test_utils

load_tests = testscenarios.load_tests_apply_scenarios


class ServerSetupMixin:

    class Server:
        def __init__(self, transport, topic, server, endpoint, serializer,
                     exchange):
            self.controller = ServerSetupMixin.ServerController()
            target = oslo_messaging.Target(topic=topic, server=server,
                                           exchange=exchange)
            self.server = oslo_messaging.get_rpc_server(transport,
                                                        target,
                                                        [endpoint,
                                                         self.controller],
                                                        serializer=serializer)

        def wait(self):
            # Wait for the executor to process the stop message, indicating all
            # test messages have been processed
            self.controller.stopped.wait()

            # Check start() does nothing with a running server
            self.server.start()
            self.server.stop()
            self.server.wait()

        def start(self):
            self.server.start()

    class ServerController:
        def __init__(self):
            self.stopped = eventletutils.Event()

        def stop(self, ctxt):
            self.stopped.set()

    class TestSerializer:

        def serialize_entity(self, ctxt, entity):
            return ('s' + entity) if entity else entity

        def deserialize_entity(self, ctxt, entity):
            return ('d' + entity) if entity else entity

        def serialize_context(self, ctxt):
            return {k: 's' + v for k, v in ctxt.items()}

        def deserialize_context(self, ctxt):
            return {k: 'd' + v for k, v in ctxt.items()}

    def __init__(self):
        self.serializer = self.TestSerializer()

    def _setup_server(self, transport, endpoint, topic=None, server=None,
                      exchange=None):
        server = self.Server(transport,
                             topic=topic or 'testtopic',
                             server=server or 'testserver',
                             endpoint=endpoint,
                             serializer=self.serializer,
                             exchange=exchange)

        server.start()
        return server

    def _stop_server(self, client, server, topic=None, exchange=None):
        client.cast({}, 'stop')
        server.wait()

    def _setup_client(self, transport, topic='testtopic', exchange=None):
        target = oslo_messaging.Target(topic=topic, exchange=exchange)
        return oslo_messaging.get_rpc_client(transport, target=target,
                                             serializer=self.serializer)


class TestRPCServer(test_utils.BaseTestCase, ServerSetupMixin):

    def __init__(self, *args):
        super().__init__(*args)
        ServerSetupMixin.__init__(self)

    def setUp(self):
        super().setUp(conf=cfg.ConfigOpts())
        # FakeExchangeManager uses a class-level exchanges mapping; "reset" it
        # before tests assert amount of items stored
        self.useFixture(fixtures.MonkeyPatch(
            'oslo_messaging._drivers.impl_fake.FakeExchangeManager._exchanges',
            new_value={}))

    def test_constructor(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')
        target = oslo_messaging.Target(topic='foo', server='bar')
        endpoints = [object()]
        serializer = object()
        access_policy = dispatcher.DefaultRPCAccessPolicy

        server = oslo_messaging.get_rpc_server(transport,
                                               target,
                                               endpoints,
                                               serializer=serializer,
                                               access_policy=access_policy,
                                               executor='threading')
        self.assertIs(server.conf, self.conf)
        self.assertIs(server.transport, transport)
        self.assertIsInstance(server.dispatcher, oslo_messaging.RPCDispatcher)
        self.assertIs(server.dispatcher.endpoints, endpoints)
        self.assertIs(server.dispatcher.serializer, serializer)
        self.assertEqual('threading', server.executor_type)

    @mock.patch('warnings.warn')
    def test_constructor_with_eventlet_executor(self, warn):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')
        target = oslo_messaging.Target(topic='foo', server='bar')
        endpoints = [object()]
        serializer = object()
        access_policy = dispatcher.DefaultRPCAccessPolicy

        warnings.simplefilter("always", DeprecationWarning)
        server = oslo_messaging.get_rpc_server(transport,
                                               target,
                                               endpoints,
                                               serializer=serializer,
                                               access_policy=access_policy,
                                               executor='eventlet')
        self.assertIs(server.conf, self.conf)
        self.assertIs(server.transport, transport)
        self.assertIsInstance(server.dispatcher, oslo_messaging.RPCDispatcher)
        self.assertIs(server.dispatcher.endpoints, endpoints)
        self.assertIs(server.dispatcher.serializer, serializer)
        self.assertEqual('eventlet', server.executor_type)
        self.assertEqual([
            mock.call(
                "Using the 'executor' argument is deprecated: "
                "the eventlet executor is now deprecated. "
                "Threading will be the only execution model available.",
                category=DeprecationWarning, stacklevel=3),
            mock.call(
                "Eventlet usages are deprecated and the removal "
                "of Eventlet from OpenStack is planned, for this "
                "reason the Eventlet executor is deprecated. "
                "Start migrating your stack to the threading executor. "
                "Please also start considering removing your internal "
                "Eventlet usages. in version '2025.1' and will be "
                "removed in version '2026.1'",
                category=DeprecationWarning, stacklevel=3)
        ], warn.mock_calls)

    def test_constructor_with_unrecognized_executor(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')
        target = oslo_messaging.Target(topic='foo', server='bar')
        endpoints = [object()]
        serializer = object()
        access_policy = dispatcher.DefaultRPCAccessPolicy

        self.assertRaises(
            server_module.ExecutorLoadFailure,
            oslo_messaging.get_rpc_server,
            transport=transport,
            target=target,
            endpoints=endpoints,
            serializer=serializer,
            access_policy=access_policy,
            executor='boom')

    def test_server_wait_method(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')
        target = oslo_messaging.Target(topic='foo', server='bar')
        endpoints = [object()]
        serializer = object()

        class MagicMockIgnoreArgs(mock.MagicMock):
            """MagicMock ignores arguments.

            A MagicMock which can never misinterpret the arguments passed to
            it during construction.
            """

            def __init__(self, *args, **kwargs):
                super().__init__()

        server = oslo_messaging.get_rpc_server(transport, target, endpoints,
                                               serializer=serializer)
        # Mocking executor
        server._executor_cls = MagicMockIgnoreArgs
        server._create_listener = MagicMockIgnoreArgs()
        server.dispatcher = MagicMockIgnoreArgs()
        # Here assigning executor's listener object to listener variable
        # before calling wait method, because in wait method we are
        # setting executor to None.
        server.start()
        listener = server.listener
        server.stop()
        # call server wait method
        server.wait()
        self.assertEqual(1, listener.cleanup.call_count)

    def test_no_target_server(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        server = oslo_messaging.get_rpc_server(
            transport,
            oslo_messaging.Target(topic='testtopic'),
            [])
        try:
            server.start()
        except Exception as ex:
            self.assertIsInstance(ex, oslo_messaging.InvalidTarget, ex)
            self.assertEqual('testtopic', ex.target.topic)
        else:
            self.assertTrue(False)

    def test_no_server_topic(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')
        target = oslo_messaging.Target(server='testserver')
        server = oslo_messaging.get_rpc_server(transport, target, [])
        try:
            server.start()
        except Exception as ex:
            self.assertIsInstance(ex, oslo_messaging.InvalidTarget, ex)
            self.assertEqual('testserver', ex.target.server)
        else:
            self.assertTrue(False)

    def _test_no_client_topic(self, call=True):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        client = self._setup_client(transport, topic=None)

        method = client.call if call else client.cast

        try:
            method({}, 'ping', arg='foo')
        except Exception as ex:
            self.assertIsInstance(ex, oslo_messaging.InvalidTarget, ex)
            self.assertIsNotNone(ex.target)
        else:
            self.assertTrue(False)

    def test_no_client_topic_call(self):
        self._test_no_client_topic(call=True)

    def test_no_client_topic_cast(self):
        self._test_no_client_topic(call=False)

    def test_client_call_timeout(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        finished = False
        wait = threading.Condition()

        class TestEndpoint:
            def ping(self, ctxt, arg):
                with wait:
                    if not finished:
                        wait.wait()

        server_thread = self._setup_server(transport, TestEndpoint())
        client = self._setup_client(transport)

        try:
            client.prepare(timeout=0).call({}, 'ping', arg='foo')
        except Exception as ex:
            self.assertIsInstance(ex, oslo_messaging.MessagingTimeout, ex)
        else:
            self.assertTrue(False)

        with wait:
            finished = True
            wait.notify()

        self._stop_server(client, server_thread)

    def test_unknown_executor(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        try:
            oslo_messaging.get_rpc_server(transport, None, [], executor='foo')
        except Exception as ex:
            self.assertIsInstance(ex, oslo_messaging.ExecutorLoadFailure)
            self.assertEqual('foo', ex.executor)
        else:
            self.assertTrue(False)

    def test_cast(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        class TestEndpoint:
            def __init__(self):
                self.pings = []

            def ping(self, ctxt, arg):
                self.pings.append(arg)

        endpoint = TestEndpoint()
        server_thread = self._setup_server(transport, endpoint)
        client = self._setup_client(transport)

        client.cast({}, 'ping', arg='foo')
        client.cast({}, 'ping', arg='bar')

        self._stop_server(client, server_thread)

        self.assertEqual(['dsfoo', 'dsbar'], endpoint.pings)

    def test_call(self):
        # NOTE(milan): using a separate transport instance for each the client
        # and the server to be able to check independent transport instances
        # can communicate over same exchange&topic
        transport_srv = oslo_messaging.get_rpc_transport(self.conf,
                                                         url='fake:')
        transport_cli = oslo_messaging.get_rpc_transport(self.conf,
                                                         url='fake:')

        class TestEndpoint:
            def ping(self, ctxt, arg):
                return arg

        server_thread = self._setup_server(transport_srv, TestEndpoint())
        client = self._setup_client(transport_cli)

        self.assertIsNone(client.call({}, 'ping', arg=None))
        self.assertEqual(0, client.call({}, 'ping', arg=0))
        self.assertFalse(client.call({}, 'ping', arg=False))
        self.assertEqual([], client.call({}, 'ping', arg=[]))
        self.assertEqual({}, client.call({}, 'ping', arg={}))
        self.assertEqual('dsdsfoo', client.call({}, 'ping', arg='foo'))

        self._stop_server(client, server_thread)

    def test_direct_call(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        class TestEndpoint:
            def ping(self, ctxt, arg):
                return arg

        server_thread = self._setup_server(transport, TestEndpoint())
        client = self._setup_client(transport)

        direct = client.prepare(server='testserver')
        self.assertIsNone(direct.call({}, 'ping', arg=None))
        self.assertEqual(0, client.call({}, 'ping', arg=0))
        self.assertFalse(client.call({}, 'ping', arg=False))
        self.assertEqual([], client.call({}, 'ping', arg=[]))
        self.assertEqual({}, client.call({}, 'ping', arg={}))
        self.assertEqual('dsdsfoo', direct.call({}, 'ping', arg='foo'))

        self._stop_server(client, server_thread)

    def test_context(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        class TestEndpoint:
            def ctxt_check(self, ctxt, key):
                return ctxt[key]

        server_thread = self._setup_server(transport, TestEndpoint())
        client = self._setup_client(transport)

        self.assertEqual('dsdsb',
                         client.call({'dsa': 'b'},
                                     'ctxt_check',
                                     key='a'))

        self._stop_server(client, server_thread)

    def test_failure(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        class TestEndpoint:
            def ping(self, ctxt, arg):
                raise ValueError(arg)

        debugs = []
        errors = []

        def stub_debug(msg, *a, **kw):
            if (a and len(a) == 1 and isinstance(a[0], dict) and a[0]):
                a = a[0]
            debugs.append(str(msg) % a)

        def stub_error(msg, *a, **kw):
            if (a and len(a) == 1 and isinstance(a[0], dict) and a[0]):
                a = a[0]
            errors.append(str(msg) % a)

        self.useFixture(fixtures.MockPatchObject(
            rpc_server_module.LOG, 'debug', stub_debug))
        self.useFixture(fixtures.MockPatchObject(
            rpc_server_module.LOG, 'error', stub_error))

        server_thread = self._setup_server(transport, TestEndpoint())
        client = self._setup_client(transport)

        try:
            client.call({}, 'ping', arg='foo')
        except Exception as ex:
            self.assertIsInstance(ex, ValueError)
            self.assertEqual('dsfoo', str(ex))
            self.assertTrue(len(debugs) == 2)
            self.assertGreater(len(errors), 0)
        else:
            self.assertTrue(False)

        self._stop_server(client, server_thread)

    def test_expected_failure(self):
        transport = oslo_messaging.get_rpc_transport(self.conf, url='fake:')

        debugs = []
        errors = []

        def stub_debug(msg, *a, **kw):
            if (a and len(a) == 1 and isinstance(a[0], dict) and a[0]):
                a = a[0]
            debugs.append(str(msg) % a)

        def stub_error(msg, *a, **kw):
            if (a and len(a) == 1 and isinstance(a[0], dict) and a[0]):
                a = a[0]
            errors.append(str(msg) % a)

        self.useFixture(fixtures.MockPatchObject(
            rpc_server_module.LOG, 'debug', stub_debug))
        self.useFixture(fixtures.MockPatchObject(
            rpc_server_module.LOG, 'error', stub_error))

        class TestEndpoint:
            @oslo_messaging.expected_exceptions(ValueError)
            def ping(self, ctxt, arg):
                raise ValueError(arg)

        server_thread = self._setup_server(transport, TestEndpoint())
        client = self._setup_client(transport)

        try:
            client.call({}, 'ping', arg='foo')
        except Exception as ex:
            self.assertIsInstance(ex, ValueError)
            self.assertEqual('dsfoo', str(ex))
            self.assertGreater(len(debugs), 0)
            self.assertTrue(len(errors) == 0)
        else:
            self.assertTrue(False)

        self._stop_server(client, server_thread)

    @mock.patch('oslo_messaging.rpc.server.LOG')
    def test_warning_when_notifier_transport(self, log):
        transport = oslo_messaging.get_notification_transport(self.conf)
        target = oslo_messaging.Target(topic='foo', server='bar')
        endpoints = [object()]
        serializer = object()

        oslo_messaging.get_rpc_server(transport, target,
                                      endpoints, serializer=serializer)
        log.warning.assert_called_once_with(
            "Using notification transport for RPC. Please use "
            "get_rpc_transport to obtain an RPC transport "
            "instance.")


class TestMultipleServers(test_utils.BaseTestCase, ServerSetupMixin):

    _exchanges = [
        ('same_exchange', dict(exchange1=None, exchange2=None)),
        ('diff_exchange', dict(exchange1='x1', exchange2='x2')),
    ]

    _topics = [
        ('same_topic', dict(topic1='t', topic2='t')),
        ('diff_topic', dict(topic1='t1', topic2='t2')),
    ]

    _server = [
        ('same_server', dict(server1=None, server2=None)),
        ('diff_server', dict(server1='s1', server2='s2')),
    ]

    _fanout = [
        ('not_fanout', dict(fanout1=None, fanout2=None)),
        ('fanout', dict(fanout1=True, fanout2=True)),
    ]

    _method = [
        ('call', dict(call1=True, call2=True)),
        ('cast', dict(call1=False, call2=False)),
    ]

    _endpoints = [
        ('one_endpoint',
         dict(multi_endpoints=False,
              expect1=['ds1', 'ds2'],
              expect2=['ds1', 'ds2'])),
        ('two_endpoints',
         dict(multi_endpoints=True,
              expect1=['ds1'],
              expect2=['ds2'])),
    ]

    @classmethod
    def generate_scenarios(cls):
        cls.scenarios = testscenarios.multiply_scenarios(cls._exchanges,
                                                         cls._topics,
                                                         cls._server,
                                                         cls._fanout,
                                                         cls._method,
                                                         cls._endpoints)

        # fanout call not supported
        def filter_fanout_call(scenario):
            params = scenario[1]
            fanout = params['fanout1'] or params['fanout2']
            call = params['call1'] or params['call2']
            return not (call and fanout)

        # listening multiple times on same topic/server pair not supported
        def filter_same_topic_and_server(scenario):
            params = scenario[1]
            single_topic = params['topic1'] == params['topic2']
            single_server = params['server1'] == params['server2']
            return not (single_topic and single_server)

        # fanout to multiple servers on same topic and exchange each endpoint
        # will receive both messages
        def fanout_to_servers(scenario):
            params = scenario[1]
            fanout = params['fanout1'] or params['fanout2']
            single_exchange = params['exchange1'] == params['exchange2']
            single_topic = params['topic1'] == params['topic2']
            multi_servers = params['server1'] != params['server2']
            if fanout and single_exchange and single_topic and multi_servers:
                params['expect1'] = params['expect1'][:] + params['expect1']
                params['expect2'] = params['expect2'][:] + params['expect2']
            return scenario

        # multiple endpoints on same topic and exchange
        # either endpoint can get either message
        def single_topic_multi_endpoints(scenario):
            params = scenario[1]
            single_exchange = params['exchange1'] == params['exchange2']
            single_topic = params['topic1'] == params['topic2']
            if single_topic and single_exchange and params['multi_endpoints']:
                params['expect_either'] = (params['expect1'] +
                                           params['expect2'])
                params['expect1'] = params['expect2'] = []
            else:
                params['expect_either'] = []
            return scenario

        for f in [filter_fanout_call, filter_same_topic_and_server]:
            cls.scenarios = [i for i in cls.scenarios if f(i)]
        for m in [fanout_to_servers, single_topic_multi_endpoints]:
            cls.scenarios = [m(i) for i in cls.scenarios]

    def __init__(self, *args):
        super().__init__(*args)
        ServerSetupMixin.__init__(self)

    def setUp(self):
        super().setUp(conf=cfg.ConfigOpts())
        self.useFixture(fixtures.MonkeyPatch(
            'oslo_messaging._drivers.impl_fake.FakeExchangeManager._exchanges',
            new_value={}))

    def test_multiple_servers(self):
        transport1 = oslo_messaging.get_rpc_transport(self.conf,
                                                      url='fake:')
        if self.exchange1 != self.exchange2:
            transport2 = oslo_messaging.get_rpc_transport(self.conf,
                                                          url='fake:')
        else:
            transport2 = transport1

        class TestEndpoint:
            def __init__(self):
                self.pings = []

            def ping(self, ctxt, arg):
                self.pings.append(arg)

            def alive(self, ctxt):
                return 'alive'

        if self.multi_endpoints:
            endpoint1, endpoint2 = TestEndpoint(), TestEndpoint()
        else:
            endpoint1 = endpoint2 = TestEndpoint()

        server1 = self._setup_server(transport1, endpoint1,
                                     topic=self.topic1,
                                     exchange=self.exchange1,
                                     server=self.server1)
        server2 = self._setup_server(transport2, endpoint2,
                                     topic=self.topic2,
                                     exchange=self.exchange2,
                                     server=self.server2)

        client1 = self._setup_client(transport1, topic=self.topic1,
                                     exchange=self.exchange1)
        client2 = self._setup_client(transport2, topic=self.topic2,
                                     exchange=self.exchange2)

        client1 = client1.prepare(server=self.server1)
        client2 = client2.prepare(server=self.server2)

        if self.fanout1:
            client1.call({}, 'alive')
            client1 = client1.prepare(fanout=True)
        if self.fanout2:
            client2.call({}, 'alive')
            client2 = client2.prepare(fanout=True)

        (client1.call if self.call1 else client1.cast)({}, 'ping', arg='1')
        (client2.call if self.call2 else client2.cast)({}, 'ping', arg='2')

        self._stop_server(client1.prepare(fanout=None),
                          server1, topic=self.topic1, exchange=self.exchange1)
        self._stop_server(client2.prepare(fanout=None),
                          server2, topic=self.topic2, exchange=self.exchange2)

        def check(pings, expect):
            self.assertEqual(len(expect), len(pings))
            for a in expect:
                self.assertIn(a, pings)

        if self.expect_either:
            check(endpoint1.pings + endpoint2.pings, self.expect_either)
        else:
            check(endpoint1.pings, self.expect1)
            check(endpoint2.pings, self.expect2)


TestMultipleServers.generate_scenarios()


class TestServerLocking(test_utils.BaseTestCase):
    def setUp(self):
        super().setUp(conf=cfg.ConfigOpts())

        def _logmethod(name):
            def method(self, *args, **kwargs):
                with self._lock:
                    self._calls.append(name)
            return method

        executors = []

        class FakeExecutor:
            def __init__(self, *args, **kwargs):
                self._lock = threading.Lock()
                self._calls = []
                executors.append(self)

            submit = _logmethod('submit')
            shutdown = _logmethod('shutdown')

        self.executors = executors

        class MessageHandlingServerImpl(oslo_messaging.MessageHandlingServer):
            def _create_listener(self):
                return mock.Mock()

            def _process_incoming(self, incoming):
                pass

        self.server = MessageHandlingServerImpl(mock.Mock(), mock.Mock())
        self.server._executor_cls = FakeExecutor

    def test_start_stop_wait(self):
        # Test a simple execution of start, stop, wait in order

        eventlet.spawn(self.server.start)
        self.server.stop()
        self.server.wait()

        self.assertEqual(1, len(self.executors))
        self.assertEqual(['shutdown'], self.executors[0]._calls)
        self.assertTrue(self.server.listener.cleanup.called)

    def test_reversed_order(self):
        # Test that if we call wait, stop, start, these will be correctly
        # reordered

        eventlet.spawn(self.server.wait)
        # This is non-deterministic, but there's not a great deal we can do
        # about that
        eventlet.sleep(0)

        eventlet.spawn(self.server.stop)
        eventlet.sleep(0)

        eventlet.spawn(self.server.start)

        self.server.wait()

        self.assertEqual(1, len(self.executors))
        self.assertEqual(['shutdown'], self.executors[0]._calls)

    def test_wait_for_running_task(self):
        # Test that if 2 threads call a method simultaneously, both will wait,
        # but only 1 will call the underlying executor method.

        start_event = eventletutils.Event()
        finish_event = eventletutils.Event()

        running_event = eventletutils.Event()
        done_event = eventletutils.Event()

        _runner = [None]

        class SteppingFakeExecutor(self.server._executor_cls):
            def __init__(self, *args, **kwargs):
                # Tell the test which thread won the race
                _runner[0] = eventlet.getcurrent()
                running_event.set()

                start_event.wait()
                super().__init__(*args, **kwargs)
                done_event.set()

                finish_event.wait()

        self.server._executor_cls = SteppingFakeExecutor

        start1 = eventlet.spawn(self.server.start)
        start2 = eventlet.spawn(self.server.start)

        # Wait until one of the threads starts running
        running_event.wait()
        runner = _runner[0]
        waiter = start2 if runner == start1 else start2

        waiter_finished = eventletutils.Event()
        waiter.link(lambda _: waiter_finished.set())

        # At this point, runner is running start(), and waiter() is waiting for
        # it to complete. runner has not yet logged anything.
        self.assertEqual(0, len(self.executors))
        self.assertFalse(waiter_finished.is_set())

        # Let the runner log the call
        start_event.set()
        done_event.wait()

        # We haven't signalled completion yet, so submit shouldn't have run
        self.assertEqual(1, len(self.executors))
        self.assertEqual([], self.executors[0]._calls)
        self.assertFalse(waiter_finished.is_set())

        # Let the runner complete
        finish_event.set()
        waiter.wait()
        runner.wait()

        # Check that both threads have finished, start was only called once,
        # and execute ran
        self.assertTrue(waiter_finished.is_set())
        self.assertEqual(1, len(self.executors))
        self.assertEqual([], self.executors[0]._calls)

    def test_start_stop_wait_stop_wait(self):
        # Test that we behave correctly when calling stop/wait more than once.
        # Subsequent calls should be noops.

        self.server.start()
        self.server.stop()
        self.server.wait()
        self.server.stop()
        self.server.wait()

        self.assertEqual(len(self.executors), 1)
        self.assertEqual(['shutdown'], self.executors[0]._calls)
        self.assertTrue(self.server.listener.cleanup.called)

    def test_state_wrapping(self):
        # Test that we behave correctly if a thread waits, and the server state
        # has wrapped when it it next scheduled

        # Ensure that if 2 threads wait for the completion of 'start', the
        # first will wait until complete_event is signalled, but the second
        # will continue
        complete_event = eventletutils.Event()
        complete_waiting_callback = eventletutils.Event()

        start_state = self.server._states['start']
        old_wait_for_completion = start_state.wait_for_completion
        waited = [False]

        def new_wait_for_completion(*args, **kwargs):
            if not waited[0]:
                waited[0] = True
                complete_waiting_callback.set()
                complete_event.wait()
            old_wait_for_completion(*args, **kwargs)

        start_state.wait_for_completion = new_wait_for_completion

        # thread1 will wait for start to complete until we signal it
        thread1 = eventlet.spawn(self.server.stop)
        thread1_finished = eventletutils.Event()
        thread1.link(lambda _: thread1_finished.set())

        self.server.start()
        complete_waiting_callback.wait()

        # The server should have started, but stop should not have been called
        self.assertEqual(1, len(self.executors))
        self.assertEqual([], self.executors[0]._calls)
        self.assertFalse(thread1_finished.is_set())

        self.server.stop()
        self.server.wait()

        # We should have gone through all the states, and thread1 should still
        # be waiting
        self.assertEqual(1, len(self.executors))
        self.assertEqual(['shutdown'], self.executors[0]._calls)
        self.assertFalse(thread1_finished.is_set())

        # Start again
        self.server.start()

        # We should now record 4 executors (2 for each server)
        self.assertEqual(2, len(self.executors))
        self.assertEqual(['shutdown'], self.executors[0]._calls)
        self.assertEqual([], self.executors[1]._calls)
        self.assertFalse(thread1_finished.is_set())

        # Allow thread1 to complete
        complete_event.set()
        thread1_finished.wait()

        # thread1 should now have finished, and stop should not have been
        # called again on either the first or second executor
        self.assertEqual(2, len(self.executors))
        self.assertEqual(['shutdown'], self.executors[0]._calls)
        self.assertEqual([], self.executors[1]._calls)
        self.assertTrue(thread1_finished.is_set())

    @mock.patch.object(server_module, 'DEFAULT_LOG_AFTER', 1)
    @mock.patch.object(server_module, 'LOG')
    def test_logging(self, mock_log):
        # Test that we generate a log message if we wait longer than
        # DEFAULT_LOG_AFTER

        log_event = eventletutils.Event()
        mock_log.warning.side_effect = lambda _, __: log_event.set()

        # Call stop without calling start. We should log a wait after 1 second
        thread = eventlet.spawn(self.server.stop)
        log_event.wait()

        # Redundant given that we already waited, but it's nice to assert
        self.assertTrue(mock_log.warning.called)
        thread.kill()

    @mock.patch.object(server_module, 'LOG')
    def test_logging_explicit_wait(self, mock_log):
        # Test that we generate a log message if we wait longer than
        # the number of seconds passed to log_after

        log_event = eventletutils.Event()
        mock_log.warning.side_effect = lambda _, __: log_event.set()

        # Call stop without calling start. We should log a wait after 1 second
        thread = eventlet.spawn(self.server.stop, log_after=1)
        log_event.wait()

        # Redundant given that we already waited, but it's nice to assert
        self.assertTrue(mock_log.warning.called)
        thread.kill()

    @mock.patch.object(server_module, 'LOG')
    def test_logging_with_timeout(self, mock_log):
        # Test that we log a message after log_after seconds if we've also
        # specified an absolute timeout

        log_event = eventletutils.Event()
        mock_log.warning.side_effect = lambda _, __: log_event.set()

        # Call stop without calling start. We should log a wait after 1 second
        thread = eventlet.spawn(self.server.stop, log_after=1, timeout=2)
        log_event.wait()

        # Redundant given that we already waited, but it's nice to assert
        self.assertTrue(mock_log.warning.called)
        thread.kill()

    def test_timeout_wait(self):
        # Test that we will eventually timeout when passing the timeout option
        # if a preceding condition is not satisfied.

        self.assertRaises(server_module.TaskTimeout,
                          self.server.stop, timeout=1)

    def test_timeout_running(self):
        # Test that we will eventually timeout if we're waiting for another
        # thread to complete this task

        # Start the server, which will also instantiate an executor
        self.server.start()
        self.server.stop()
        shutdown_called = eventletutils.Event()

        # Patch the executor's stop method to be very slow
        def slow_shutdown(wait):
            shutdown_called.set()
            eventlet.sleep(10)
        self.executors[0].shutdown = slow_shutdown

        # Call wait in a new thread
        thread = eventlet.spawn(self.server.wait)

        # Wait until the thread is in the slow stop method
        shutdown_called.wait()

        # Call wait again in the main thread with a timeout
        self.assertRaises(server_module.TaskTimeout,
                          self.server.wait, timeout=1)
        thread.kill()

    @mock.patch.object(server_module, 'LOG')
    def test_log_after_zero(self, mock_log):
        # Test that we do not log a message after DEFAULT_LOG_AFTER if the
        # caller gave log_after=1

        # Call stop without calling start.
        self.assertRaises(server_module.TaskTimeout,
                          self.server.stop, log_after=0, timeout=2)

        # We timed out. Ensure we didn't log anything.
        self.assertFalse(mock_log.warning.called)


class TestRPCExposeDecorator(test_utils.BaseTestCase):

    def foo(self):
        pass

    @rpc.expose
    def bar(self):
        """bar docstring"""
        pass

    def test_undecorated(self):
        self.assertRaises(AttributeError, lambda: self.foo.exposed)

    def test_decorated(self):
        self.assertEqual(True, self.bar.exposed)
        self.assertEqual("""bar docstring""", self.bar.__doc__)
        self.assertEqual('bar', self.bar.__name__)
