import asyncio
import collections
import logging
import struct
import threading
import unittest
from functools import partial
from itertools import count
from time import sleep
from time import time
from unittest import TestCase
from unittest import mock

from ZODB.utils import RLock
from ZODB.utils import maxtid
from zope.testing import setupstack

from ..Exceptions import ClientDisconnected
from ..Exceptions import ProtocolError
from .base import SizedMessageProtocol
from .base import ZEOBaseProtocol
from .client import ClientThread
from .client import Fallback
from .futures import AsyncTask
from .futures import ConcurrentFuture
from .futures import ConcurrentTask
from .futures import Future
from .marshal import decoder
from .marshal import encoder
from .server import best_protocol_version
from .server import new_connection
from .testing import FaithfulLoop
from .testing import Loop


logger = logging.getLogger(__name__)


class Base:
    enc = b'Z'
    seq_type = list

    def setUp(self):
        super().setUp()
        self.encode = encoder(self.enc)
        self.decode = decoder(self.enc)

    def unsized(self, data, unpickle=False):
        result = []
        while data:
            size, message = data[:2]
            data = data[2:]
            self.assertEqual(struct.unpack(">I", size)[0], len(message))
            if unpickle:
                message = self.decode(message)
            result.append(message)

        if len(result) == 1:
            result = result[0]
        return result

    def parse(self, data):
        return self.unsized(data, True)

    target = None

    def send(self, method, *args, **kw):
        target = kw.pop('target', self.target)
        called = kw.pop('called', True)
        no_output = kw.pop('no_output', True)
        self.assertFalse(kw)

        self.loop.protocol.data_received(
            sized(self.encode(0, True, method, args)))
        if target is not None:
            target = getattr(target, method)
            if called:
                target.assert_called_with(*args)
                target.reset_mock()
            else:
                self.assertFalse(target.called)
        if no_output:
            self.assertFalse(self.loop.transport.pop())

    def pop(self, count=None, parse=True):
        self.assertEqual(self.loop.exceptions, [])
        return self.unsized(self.loop.transport.pop(count), parse)


class ClientTests(Base, setupstack.TestCase, ClientThread):
    """Test ``ClientIO``.

    The tests emulate a server and its responses to verify
    that ``ClientIO`` and ``client.Protocol`` instances behave
    as they should:
    messages sent by those instances can be accessed via ``pop``;
    emulated server actions are generated by calls to ``respond``
    or ``server_async_call``.

    The tests always use an ``asyncio`` loop (even ``uvloop`` tests).
    """

    maxDiff = None

    def tearDown(self):
        self.exit_future_mode()
        self.close()
        super().tearDown()
        loop = self.loop
        if loop is not None:
            self.assertEqual(loop.exceptions, [])
        # ``mock`` creates cyclic structures; break the cycles
        _break_mock_cycles(self.target)

    # For normal operation all (server) interface calls are synchronous:
    # they wait for the result.
    # However, we want to perform our tests without a real server;
    # instead we emulate server responses (via ``respond``)
    # and asynchronous server calls to us via ``send``.
    # For this to work, interface calls must not wait for a result
    # (which we ourselves often must produce).
    # We achieve this by switching to "future mode":
    # in this mode, the interface calls return futures representing
    # the final result. We can call their ``result`` method to
    # wait for their result
    future_mode = None

    def enter_future_mode(self):
        if self.future_mode is None:
            self.future_mode = self.io_call
            # switch for most interface calls to future mode
            # exceptions are those that explicitly use ``wait=True`.
            self.io_call = partial(self.io_call, wait=False)

    def exit_future_mode(self):
        if self.future_mode is not None:
            self.io_call = self.future_mode  # back to synchronous mode
            del self.future_mode

    def start(self,
              addrs=(('127.0.0.1', 8200), ), loop_addrs=None,
              read_only=False,
              finish_start=False,
              future_mode=True,
              ):
        # To create a client, we need to specify an address, a client
        # object and a cache.

        wrapper = mock.Mock(__name__="__name__")
        self.target = wrapper
        cache = MemoryCache()
        # We can also provide an event loop.  We'll use a testing loop
        # so we don't have to actually make any network connection.
        self.loop = loop = FaithfulLoop(
            addrs if loop_addrs is None else loop_addrs)
        ClientThread.__init__(self,
                              addrs, wrapper, cache, 'TEST',
                              read_only, timeout=1)
        loop.run_until_inactive()
        self.assertFalse(wrapper.notify_disconnected.called)
        protocol = loop.protocol
        transport = loop.transport

        if finish_start:
            protocol.data_received(sized(self.enc + b'5'))
            self.assertEqual(self.pop(2, False), self.enc + b'5')
            self.respond(1, None)
            self.respond(2, 'a'*8)
            self.pop(4)
            self.assertEqual(self.pop(), (3, False, 'get_info', ()))
            self.respond(3, dict(length=42))

        if future_mode:
            self.enter_future_mode()
        return (wrapper, cache, self.loop, self.client, protocol, transport)

    def pop(self, *args, **kw):
        self.loop.run_until_inactive()
        return super().pop(*args, **kw)

    def respond(self, message_id, result, async_=False,
                check_exc=True, return_msg=False, protocol=None):
        """emulate a server response."""
        msg = sized(self.encode(message_id, async_, '.reply', result))
        if return_msg:
            return msg
        if protocol is None:
            protocol = self.loop.protocol
        protocol.data_received(msg)
        if check_exc:
            self.assertEqual(self.loop.exceptions, [])

    def server_async_call(self, method, *args,
                          check_exc=True, return_msg=False):
        msg = sized(self.encode(0, True, method, args))
        if return_msg:
            return msg
        self.loop.protocol.data_received(msg)
        if check_exc:
            self.assertEqual(self.loop.exceptions, [])

    def sync_call(self, meth, *args):
        """call future returning *meth* with *args* and wait for result."""
        return meth(*args).result(2)

    def observe(self, f, *args):
        self.observed = f(*args)

    def testClientBasics(self):

        # Here, we'll go through the basic usage of the asyncio ZEO
        # network client.  The client is responsible for the core
        # functionality of a ZEO client storage.  The client storage
        # is largely just a wrapper around the asyncio client.

        wrapper, cache, loop, client, protocol, transport = self.start()
        self.assertFalse(wrapper.notify_disconnected.called)

        # The client isn't connected until the server sends it some data.
        self.assertFalse(client.connected.done() or transport.data)

        # The server sends the client it's protocol. In this case,
        # it's a very high one.  The client will send it's highest that
        # it can use.
        protocol.data_received(sized(self.enc + b'99999'))

        # The client sends back a handshake, and registers the
        # storage, and requests the last transaction.
        self.assertEqual(self.pop(2, False), self.enc + b'5')
        self.assertEqual(loop.exceptions, [])
        self.assertEqual(self.pop(), (1, False, 'register', ('TEST', False)))

        # The client isn't connected until it initializes it's cache:
        self.assertFalse(client.connected.done() or transport.data)

        # If we try to make calls while the client is *initially*
        # connecting, we get an error. This is because some dufus
        # decided to create a client storage without waiting for it to
        # connect.
        self.assertRaises(ClientDisconnected, self.sync_call,
                          self.call, 'foo', 1, 2)

        # When the client is reconnecting, it's ready flag is set to False and
        # it queues calls:
        client.ready = False
        f1 = self.call('foo', 1, 2)
        self.assertFalse(f1.done())

        # If we try to make an async call, we get an immediate error:
        self.assertRaises(ClientDisconnected, self.sync_call,
                          self.async_, 'bar', 3, 4)

        # The wrapper object (ClientStorage) hasn't been notified:
        self.assertFalse(wrapper.notify_connected.called)

        # Let's respond to the register call:
        self.respond(1, None)

        # The client requests the last transaction:
        self.assertEqual(self.pop(), (2, False, 'lastTransaction', ()))

        # We respond
        self.respond(2, b'a'*8)

        # After verification, the client requests info:
        self.assertEqual(self.pop(), (3, False, 'get_info', ()))
        self.respond(3, dict(length=42))

        # Now we're connected, the cache was initialized, and the
        # queued message has been sent:
        self.assertTrue(client.connected.done())
        self.assertEqual(cache.getLastTid(), b'a'*8)
        self.assertEqual(self.pop(), (4, False, 'foo', (1, 2)))

        # The wrapper object (ClientStorage) has been notified:
        wrapper.notify_connected.assert_called_with(client, {'length': 42})

        self.respond(4, 42)
        self.assertEqual(f1.result(), 42)

        # Now we can make async calls:
        self.async_('bar', 3, 4)
        self.assertEqual(self.pop(), (0, True, 'bar', (3, 4)))

        # Loading objects gets special handling to leverage the cache.
        loaded = self.load_before(b'1'*8, maxtid)

        # The data wasn't in the cache, so we made a server call:
        self.assertEqual(self.pop(),
                         ((b'1'*8, maxtid),
                          False,
                          'loadBefore',
                          (b'1'*8, maxtid)))
        # Note load_before uses ``(oid, tid)`` as message id.
        self.respond((b'1'*8, maxtid), (b'data', b'a'*8, None))
        self.assertEqual(loaded.result(), (b'data', b'a'*8, None))

        # If we make another request, it will be satisfied from the cache:
        loaded = self.load_before(b'1'*8, maxtid)
        self.assertEqual(loaded.result(), (b'data', b'a'*8, None))
        self.assertFalse(transport.data)

        # Let's send an invalidation:
        self.send('invalidateTransaction', b'b'*8, self.seq_type([b'1'*8]))

        # Now, if we try to load current again, we'll make a server request.
        loaded = self.load_before(b'1'*8, maxtid)

        # Note that if we make another request for the same object,
        # the requests will be collapsed:
        loaded2 = self.load_before(b'1'*8, maxtid)

        self.assertEqual(self.pop(),
                         ((b'1'*8, maxtid),
                          False,
                          'loadBefore',
                          (b'1'*8, maxtid)))
        self.respond((b'1'*8, maxtid), (b'data2', b'b'*8, None))
        self.assertEqual(loaded.result(), (b'data2', b'b'*8, None))
        self.assertEqual(loaded2.result(), (b'data2', b'b'*8, None))

        # Loading non-current data may also be satisfied from cache
        loaded = self.load_before(b'1'*8, b'b'*8)
        self.assertEqual(loaded.result(), (b'data', b'a'*8, b'b'*8))
        self.assertFalse(transport.data)
        loaded = self.load_before(b'1'*8, b'c'*8)
        self.assertEqual(loaded.result(), (b'data2', b'b'*8, None))
        self.assertFalse(transport.data)
        loaded = self.load_before(b'1'*8, b'_'*8)

        self.assertEqual(self.pop(),
                         ((b'1'*8, b'_'*8),
                          False,
                          'loadBefore',
                          (b'1'*8, b'_'*8)))
        self.respond((b'1'*8, b'_'*8), (b'data0', b'^'*8, b'_'*8))
        self.assertEqual(loaded.result(), (b'data0', b'^'*8, b'_'*8))

        # When committing transactions, we need to update the cache
        # with committed data.  To do this, we pass a (oid, data, resolved)
        # iteratable to tpc_finish_co.

        tids = []

        def finished_cb(tid):
            tids.append(tid)

        committed = self.tpc_finish(
            b'd'*8,
            [(b'2'*8, 'committed 2', False),
             (b'1'*8, 'committed 3', True),
             (b'4'*8, 'committed 4', False),
             ],
            finished_cb)
        self.assertFalse(committed.done() or
                         cache.load(b'2'*8) or
                         cache.load(b'4'*8))
        self.assertEqual(cache.load(b'1'*8), (b'data2', b'b'*8))
        self.assertEqual(self.pop(),
                         (5, False, 'tpc_finish', (b'd'*8,)))
        self.respond(5, b'e'*8)
        self.assertEqual(committed.result(), b'e'*8)
        self.assertEqual(cache.load(b'1'*8), None)
        self.assertEqual(cache.load(b'2'*8), ('committed 2', b'e'*8))
        self.assertEqual(cache.load(b'4'*8), ('committed 4', b'e'*8))
        self.assertEqual(tids.pop(), b'e'*8)

        # If the protocol is disconnected, it will reconnect and will
        # resolve outstanding requests with exceptions:
        loaded = self.load_before(b'1'*8, maxtid)
        f1 = self.call('foo', 1, 2)
        self.assertFalse(loaded.done() or f1.done())
        self.assertEqual(
            self.pop(),
            [((b'11111111', b'\x7f\xff\xff\xff\xff\xff\xff\xff'),
              False, 'loadBefore', (b'1'*8, maxtid)),
             (6, False, 'foo', (1, 2))],
            )
        exc = TypeError(43)

        self.assertFalse(wrapper.notify_disconnected.called)
        wrapper.notify_connected.reset_mock()
        protocol.connection_lost(exc)
        wrapper.notify_disconnected.assert_called_with()

        self.assertIsInstance(loaded.exception(), ClientDisconnected)
        self.assertEqual(loaded.exception().args, (exc,))
        self.assertIsInstance(f1.exception(), ClientDisconnected)
        self.assertEqual(f1.exception().args, (exc,))

        # Because we reconnected, a new protocol and transport were created:
        self.assertIsNot(protocol, loop.protocol)
        self.assertTrue(protocol.closed)
        self.assertIsNot(transport, loop.transport)
        self.assertTrue(transport.closed)
        protocol = loop.protocol
        transport = loop.transport

        # and we have a new incomplete connect future:
        self.assertFalse(client.connected.done() or transport.data)

        # This time we'll send a lower protocol version.  The client
        # will send it back, because it's lower than the client's
        # protocol:
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.assertEqual(self.pop(), (1, False, 'register', ('TEST', False)))
        self.assertFalse(wrapper.notify_connected.called)

        # If the register response is a tid, then the client won't
        # request lastTransaction
        self.respond(1, b'e'*8)
        self.assertEqual(self.pop(), (2, False, 'get_info', ()))
        self.respond(2, dict(length=42))

        # Because the server tid matches the cache tid, we're done connecting
        wrapper.notify_connected.assert_called_with(client, {'length': 42})
        self.assertTrue(client.connected.done() and not transport.data)
        self.assertEqual(cache.getLastTid(), b'e'*8)

        # Because we were able to update the cache, we didn't have to
        # invalidate the database cache:
        self.assertFalse(wrapper.invalidateTransaction.called)

        # The close method closes the connection and cache:
        loop.call_soon_threadsafe(client.close)
        loop.run_until_inactive()
        self.assertTrue(transport.closed and cache.closed and protocol.closed)

        # The client doesn't reconnect
        self.assertEqual(loop.protocol, protocol)
        self.assertEqual(loop.transport, transport)

    def test_cache_behind(self):
        wrapper, cache, loop, client, protocol, transport = self.start()

        cache.setLastTid(b'a'*8)
        cache.store(b'4'*8, b'a'*8, None, '4 data')
        cache.store(b'2'*8, b'a'*8, None, '2 data')

        self.assertFalse(client.connected.done() or transport.data)
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.respond(1, None)
        self.respond(2, b'e'*8)
        self.pop(4)

        # We have to verify the cache, so we're not done connecting:
        self.assertFalse(client.connected.done())
        self.assertEqual(self.pop(),
                         (3, False, 'getInvalidations', (b'a'*8, )))
        self.respond(3, (b'e'*8, [b'4'*8]))

        self.assertEqual(self.pop(), (4, False, 'get_info', ()))
        self.respond(4, dict(length=42))

        # Now that verification is done, we're done connecting
        self.assertTrue(client.connected.done() and not transport.data)
        self.assertEqual(cache.getLastTid(), b'e'*8)

        # And the cache has been updated:
        self.assertEqual(cache.load(b'2'*8),
                         ('2 data', b'a'*8))  # unchanged
        self.assertEqual(cache.load(b'4'*8), None)

        # Because we were able to update the cache, we didn't have to
        # invalidate the database cache:
        self.assertFalse(wrapper.invalidateCache.called)

    def test_cache_way_behind(self):
        wrapper, cache, loop, client, protocol, transport = self.start()

        cache.setLastTid(b'a'*8)
        cache.store(b'4'*8, b'a'*8, None, '4 data')
        self.assertTrue(cache)

        self.assertFalse(client.connected.done() or transport.data)
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.respond(1, None)
        self.respond(2, b'e'*8)
        self.pop(4)

        # We have to verify the cache, so we're not done connecting:
        self.assertFalse(client.connected.done())
        self.assertEqual(self.pop(),
                         (3, False, 'getInvalidations', (b'a'*8, )))

        # We respond None, indicating that we're too far out of date:
        self.respond(3, None)

        self.assertEqual(self.pop(), (4, False, 'get_info', ()))
        self.respond(4, dict(length=42))

        # Now that verification is done, we're done connecting
        self.assertTrue(client.connected.done() and not transport.data)
        self.assertEqual(cache.getLastTid(), b'e'*8)

        # But the cache is now empty and we invalidated the database cache
        self.assertFalse(cache)
        wrapper.invalidateCache.assert_called_with()

    def test_multiple_addresses(self):
        # We can pass multiple addresses to client constructor
        addrs = [('1.2.3.4', 8200), ('2.2.3.4', 8200)]
        wrapper, cache, loop, client, protocol, transport = self.start(
            addrs, ())

        # We haven't connected yet
        self.assertIsNone(protocol)
        self.assertIsNone(transport)

        # There are 2 connection attempts outstanding:
        self.loop.run_until_inactive()
        self.assertEqual(sorted(loop.connecting), addrs)

        # We cause the first one to fail:
        loop.call_soon_threadsafe(loop.fail_connecting, addrs[0])
        self.loop.run_until_inactive()
        self.assertEqual(sorted(loop.connecting), addrs[1:])

        # The failed connection is attempted in the future:
        delay, func, args, handle = loop.later.pop(0)
        self.assertTrue(1 <= delay <= 2)
        handle.cancel()
        loop.call_soon_threadsafe(func, *args)
        self.loop.run_until_inactive()
        self.assertEqual(sorted(loop.connecting), addrs)

        # Let's connect the second address
        loop.call_soon_threadsafe(loop.connect_connecting, addrs[1])
        self.loop.run_until_inactive()
        self.assertEqual(sorted(loop.connecting), addrs[:1])
        protocol = loop.protocol
        transport = loop.transport
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        # cancel the heartbeat to make debugging easier
        protocol.heartbeat_handle.cancel()
        self.respond(1, None)

        # Now, when the first connection fails, it won't be retried,
        # because we're already connected.
        # (first in later is heartbeat)
        self.assertEqual(sorted(loop.later[1:]), [])
        loop.call_soon_threadsafe(loop.fail_connecting, addrs[0])
        self.loop.run_until_inactive()
        self.assertEqual(sorted(loop.connecting), [])
        self.assertEqual(sorted(loop.later[1:]), [])

    def test_bad_server_tid(self):
        # If in verification we get a server_tid behind the cache's, make sure
        # we retry the connection later.
        wrapper, cache, loop, client, protocol, transport = self.start()
        cache.store(b'4'*8, b'a'*8, None, '4 data')
        cache.setLastTid('b'*8)
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.respond(1, None)
        self.respond(2, 'a'*8)
        self.pop()
        self.assertFalse(client.connected.done() or transport.data)
        delay, func, args, _ = loop.later.pop(1)  # first in later is heartbeat
        self.assertTrue(8 < delay < 10)
        self.assertEqual(len(loop.later), 1)  # first in later is heartbeat
        self.loop.call_soon_threadsafe(func, *args)  # connect again
        self.loop.run_until_inactive()
        self.assertIsNot(protocol, loop.protocol)
        self.assertIsNot(transport, loop.transport)
        protocol = loop.protocol
        transport = loop.transport
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.respond(1, None)
        self.respond(2, 'b'*8)
        self.pop(4)
        self.assertEqual(self.pop(), (3, False, 'get_info', ()))
        self.respond(3, dict(length=42))
        self.assertTrue(client.connected.done() and not transport.data)
        self.assertTrue(client.ready)

    def test_readonly_fallback(self):
        addrs = [('1.2.3.4', 8200), ('2.2.3.4', 8200)]
        wrapper, cache, loop, client, protocol, transport = self.start(
            addrs, (), read_only=Fallback)

        self.assertTrue(self.is_read_only())

        # We'll treat the first address as read-only and we'll let it connect:
        loop.call_soon_threadsafe(loop.connect_connecting, addrs[0])
        self.loop.run_until_inactive()
        protocol, transport = loop.protocol, loop.transport
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        # We see that the client tried a writable connection:
        self.assertEqual(self.pop(),
                         (1, False, 'register', ('TEST', False)))
        # We respond with a read-only exception:
        self.respond(1, ('ZODB.POSException.ReadOnlyError', ()), True)
        self.assertTrue(self.is_read_only())

        # The client tries for a read-only connection:
        self.assertEqual(self.pop(), (2, False, 'register', ('TEST', True)))
        # We respond with successfully:
        self.respond(2, None)
        self.pop(2)
        self.respond(3, 'b'*8)
        self.assertTrue(self.is_read_only())

        # At this point, the client is ready and using the protocol,
        # and the protocol is read-only:
        self.assertTrue(client.ready)
        self.assertEqual(client.protocol, protocol._protocol)
        self.assertEqual(protocol.read_only, True)
        connected = client.connected

        # The client asks for info, and we respond:
        self.assertEqual(self.pop(), (4, False, 'get_info', ()))
        self.respond(4, dict(length=42))

        self.assertTrue(connected.done())

        # We connect the second address:
        loop.call_soon_threadsafe(loop.connect_connecting, addrs[1])
        self.loop.run_until_inactive()
        loop.protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(loop.transport.pop(2)),
                         self.enc + b'5')
        self.assertEqual(self.parse(loop.transport.pop()),
                         (1, False, 'register', ('TEST', False)))
        self.assertTrue(self.is_read_only())

        # We respond and the writable connection succeeds:
        self.respond(1, None)

        # at this point, a lastTransaction request is emitted:

        self.assertEqual(self.parse(loop.transport.pop()),
                         (2, False, 'lastTransaction', ()))
        self.assertFalse(self.is_read_only())

        # Now, the original protocol is closed, and the client is
        # no-longer ready:
        self.assertFalse(client.ready)
        self.assertIsNot(client.protocol, protocol._protocol)
        self.assertEqual(client.protocol, loop.protocol._protocol)
        self.assertEqual(protocol.closed, True)
        self.assertIsNot(client.connected, connected)
        self.assertFalse(client.connected.done())
        protocol, transport = loop.protocol, loop.transport
        self.assertEqual(protocol.read_only, False)

        # Now, we finish verification
        self.respond(2, 'b'*8)
        self.respond(3, dict(length=42))
        self.assertTrue(client.ready)
        self.assertTrue(client.connected.done())

    def test_invalidations_while_verifying(self):
        # While we're verifying, invalidations are ignored
        wrapper, cache, loop, client, protocol, transport = self.start()
        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.respond(1, None)
        self.pop(4)
        self.send('invalidateTransaction', b'b'*8, [b'1'*8], called=False)
        self.respond(2, b'a'*8)
        self.send('invalidateTransaction', b'c'*8, self.seq_type([b'1'*8]),
                  no_output=False)
        self.assertEqual(self.pop(), (3, False, 'get_info', ()))

        # We'll disconnect:
        protocol.connection_lost(Exception("lost"))
        self.assertIsNot(protocol, loop.protocol)
        self.assertIsNot(transport, loop.transport)
        protocol = loop.protocol
        transport = loop.transport

        # Similarly, invalidations aren't processed while reconnecting:

        protocol.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport.pop(2)), self.enc + b'5')
        self.respond(1, None)
        self.pop(4)
        self.send('invalidateTransaction', b'd'*8, [b'1'*8], called=False)
        self.respond(2, b'c'*8)
        self.send('invalidateTransaction', b'e'*8, self.seq_type([b'1'*8]),
                  no_output=False)
        self.assertEqual(self.pop(), (3, False, 'get_info', ()))

    def test_flow_control(self):
        # When sending a lot of data (blobs), we don't want to fill up
        # memory behind a slow socket. Asycio's flow control helper
        # seems a bit complicated. We'd rather pass an iterator that's
        # consumed as we can.

        wrapper, cache, loop, client, protocol, transport = self.start(
            finish_start=True)

        # Give the transport a small capacity:
        transport.capacity = 2
        self.async_('foo')
        self.async_('bar')
        self.async_('baz')
        self.async_('splat')

        # The first 2 were sent, but the remaining were queued.
        self.assertEqual(self.pop(),
                         [(0, True, 'foo', ()), (0, True, 'bar', ())])

        # But popping them allowed sending to resume:
        self.assertEqual(self.pop(),
                         [(0, True, 'baz', ()), (0, True, 'splat', ())])

        # This is especially handy with iterators:
        self.async_iter((name, ()) for name in 'abcde')
        self.async_('f')
        self.assertEqual(self.pop(), [(0, True, 'a', ()), (0, True, 'b', ())])
        self.assertEqual(self.pop(), [(0, True, 'c', ()), (0, True, 'd', ())])
        self.assertEqual(self.pop(), [(0, True, 'e', ()), (0, True, 'f', ())])
        self.assertEqual(self.pop(), [])

    def test_bad_protocol(self):
        wrapper, cache, loop, client, protocol, transport = self.start()
        with mock.patch("ZEO.asyncio.client.logger.error") as error:
            self.assertFalse(error.called)
            protocol.data_received(sized(self.enc + b'200'))
            self.assertIsInstance(error.call_args[0][1], ProtocolError)

    def test_get_peername(self):
        wrapper, cache, loop, client, protocol, transport = self.start(
            finish_start=True)
        self.assertEqual(client.get_peername(), '1.2.3.4')

    def test_call_async(self):
        # There are a few (1?) cases where we call into client storage
        # where it needs to call back asyncronously. Because we're
        # calling from the same thread, we don't need to use a futurte.
        wrapper, cache, loop, client, protocol, transport = self.start(
            finish_start=True)

        client.call_async('foo', (1, ))
        self.assertEqual(self.pop(), (0, True, 'foo', (1, )))

    def test_ClientDisconnected_on_call_timeout(self):
        (wrapper, cache, loop, client, protocol,
         transport) = self.start(future_mode=False)
        self.assertRaises(ClientDisconnected, self.call, 'foo')
        client.ready = False
        self.assertRaises(ClientDisconnected, self.call, 'foo')

    def test_errors_in_data_received(self):
        # There was a bug in ZEO.async.client.Protocol.data_recieved
        # that caused it to fail badly if errors were raised while
        # handling data.

        wrapper, cache, loop, client, protocol, transport = self.start(
            finish_start=True)

        wrapper.receiveBlobStart.side_effect = ValueError('test')

        chunk = 'x' * 99999
        try:
            loop.protocol.data_received(
                sized(
                    self.encode(0, True, 'receiveBlobStart', ('oid', 'serial'))
                    ) +
                sized(
                    self.encode(
                        0, True, 'receiveBlobChunk', ('oid', 'serial', chunk))
                    )
                )
        except ValueError:
            pass
        loop.protocol.data_received(sized(
            self.encode(0, True, 'receiveBlobStop', ('oid', 'serial'))
            ))
        wrapper.receiveBlobChunk.assert_called_with('oid', 'serial', chunk)
        wrapper.receiveBlobStop.assert_called_with('oid', 'serial')

    def test_heartbeat(self):
        # Protocols run heartbeats on a configurable (sort of)
        # heartbeat interval, which defaults to every 60 seconds.
        wrapper, cache, loop, client, protocol, transport = self.start(
            finish_start=True)

        delay, func, args, handle = loop.later.pop()
        self.assertEqual(
            (delay, func, args, handle),
            (60, protocol.heartbeat, (), protocol.heartbeat_handle),
            )
        self.assertFalse(loop.later or handle._cancelled)

        # The heartbeat function sends heartbeat data and reschedules itself.
        self.loop.call_soon_threadsafe(func)
        self.loop.run_until_inactive()
        self.assertEqual(self.pop(), (-1, 0, '.reply', None))
        self.assertTrue(protocol.heartbeat_handle != handle)

        delay, func, args, handle = loop.later.pop()
        self.assertEqual(
            (delay, func, args, handle),
            (60, protocol.heartbeat, (), protocol.heartbeat_handle),
            )
        self.assertFalse(loop.later or handle._cancelled)

        # The heartbeat is cancelled when the protocol connection is lost:
        protocol.connection_lost(None)
        self.assertTrue(handle._cancelled)

    def test_asyncall_doesnt_overtake_reply(self):
        """verify that an asynchronous call after a reply does not
        become effective before the reply."""
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=True)
        # our syncchronous call
        f = self.tpc_finish(None, (), lambda *args: None, wait=False)
        self.assertEqual(self.pop(), (4, False, "tpc_finish", (None,)))
        # emulate the reply, followed by an asynchronous call
        # of ``invalidateTransaction``.
        # It is important that the two messages are delivered together
        msg = (self.respond(4, "b"*8, return_msg=True) +
               self.server_async_call("invalidateTransaction", "c"*8, (),
                                      return_msg=True))
        protocol.data_received(msg)
        self.assertEqual(loop.exceptions, [])
        f.result()  # no exception
        self.assertEqual(cache.getLastTid(), "c"*8)

    def test_reply_doesnt_overtake_asyncall(self):
        """verify that a reply does not overtake an asynchronous call."""
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=True)
        # our synchronous call
        f = self.call("sync", wait=False)
        self.assertEqual(self.pop(), (4, False, "sync", ()))
        # emulate an asynchronous call followed by the reply
        self.observed = None

        def observer():
            """used as the storage's `receiveBlobStop`.
            It records the state of ``f`` and therefore allows
            to check what is processed first.
            """
            self.observed = bool(f.done())

        io.receiveBlobStop = observer
        # Emulate an async call to the client followed by the reply.
        # It is important that the two messages are delivered together.
        msg = (self.server_async_call("receiveBlobStop", return_msg=True) +
               self.respond(4, None, return_msg=True))
        protocol.data_received(msg)
        self.assertEqual(loop.exceptions, [])
        self.assertIs(self.observed, False)

    def test_serialized_registration(self):
        addrs = [('1.2.3.4', 8200), ('2.2.3.4', 8200)]
        wrapper, cache, loop, io, protocol, transport = self.start(
            addrs, ())
        # connect to the first address
        loop.call_soon_threadsafe(loop.connect_connecting, addrs[0])
        self.loop.run_until_inactive()
        protocol_1 = loop.protocol
        transport_1 = loop.transport
        # connect to the second address
        loop.call_soon_threadsafe(loop.connect_connecting, addrs[1])
        self.loop.run_until_inactive()
        protocol_2 = loop.protocol
        transport_2 = loop.transport
        # verify not locked
        self.assertFalse(io.register_lock.locked())
        # start the first protocol
        protocol_1.data_received(sized(self.enc + b'5'))
        self.assertTrue(io.register_lock.locked())
        self.assertEqual(self.unsized(transport_1.pop(2)), self.enc + b'5')
        self.assertEqual(len(transport_1.data), 2)  # register call
        # start the second protocol
        protocol_2.data_received(sized(self.enc + b'5'))
        self.assertEqual(self.unsized(transport_2.pop(2)), self.enc + b'5')
        self.assertEqual(len(transport_2.data), 0)  # waits for lock
        self.assertFalse(io.connected.done())
        # finish first protocol connection
        self.respond(1, None, protocol=protocol_1)  # register
        self.respond(2, b"a"*8, protocol=protocol_1)  # lastTransaction
        self.assertTrue(io.ready)
        self.assertFalse(io.connected.done())
        self.respond(3, {}, protocol=protocol_1)  # get_info
        # the second protocol has got the lock but released it immediately
        # because it was closed due to the accepted protocol 1
        self.assertFalse(io.register_lock.locked())
        self.assertTrue(protocol_2._protocol.closed)
        self.assertTrue(io.ready)
        self.assertTrue(io.connected.done())

    def test_close_with_running_loop(self):
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=True, future_mode=False)
        self.close()
        self.assertFalse(io.ready)
        self.assertTrue(self.is_closed())
        self.assertTrue(loop.is_closed())
        self.close()  # subsequent calls do not have an effect

    def test_close_with_stopped_loop(self):
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=True, future_mode=False)
        loop.call_soon_threadsafe(loop.stop)  # schedule loop stop
        # risks race condition as the loop is going to be closed during
        # stop processing
        # loop.run_until_inactive()
        sleep(0.1)  # give the loop time to stop
        self.close()
        self.assertFalse(io.ready)
        self.assertTrue(self.is_closed())
        self.assertTrue(loop.is_closed())
        self.close()  # subsequent calls do not have an effect

    def test_io_close(self):
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=True)
        tf = self.call("test")
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())
        self.assertTrue(protocol.closed)
        self.assertTrue(tf.done())
        self.assertIsInstance(tf.exception(), ClientDisconnected)

    def test_io_close_after_connection_loss(self):
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=True)
        tf1 = self.call("test")
        loop.run_until_inactive()
        # With the following 2 lines, ``connection_lost`` will
        # effectively overtake the call: i.e. it appears as
        # if the connection were lost before the call.
        # This means that the call will start waiting for a reconnection.
        # Further down, we verify that ``close`` cleans up the state;
        # this is necessary as the closing prevents a reconnection.
        tf2 = self.call("test")
        protocol.connection_lost(None)
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())
        self.assertTrue(protocol.closed)
        self.assertTrue(tf1.done())
        self.assertIsInstance(tf1.exception(), ClientDisconnected)
        self.assertTrue(tf2.done())
        self.assertIsInstance(tf2.exception(), ClientDisconnected)

    def test_io_close_before_connection(self):
        storage_mock, cache, loop, io, protocol, transport = self.start(
            loop_addrs=(),  # prevent auto connection
            finish_start=False)
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())

    def test_io_close_after_register_exception(self):
        storage_mock, cache, loop, io, protocol, transport = self.start(
            finish_start=False)
        protocol.data_received(sized(self.enc + b'5'))
        # let the register call fail
        self.respond(1, ("Exception", "register_failed"), async_=True)
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())
        self.assertTrue(protocol.closed)

    def test_io_close_after_register_exception_before_reconnection(self):
        addr = ('127.0.0.1', 8200)
        storage_mock, cache, loop, io, protocol, transport = self.start(
            addrs=(addr,),
            loop_addrs=(),  # prevent auto connection
            finish_start=False)
        loop.call_soon_threadsafe(loop.connect_connecting, addr)  # connect
        loop.run_until_inactive()
        loop.protocol.data_received(sized(self.enc + b'5'))
        # let the register call fail
        self.respond(1, ("Exception", "register_failed"), async_=True)
        # check "reconnection called for"
        try_reconnecting = loop.later[1]  # entry 0 is "heartbeat"
        connect = try_reconnecting[1]
        self.assertEqual(connect.__func__.__name__,
                         "try_connecting")
        loop.call_soon_threadsafe(connect)
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())
        self.assertTrue(loop.protocol.closed)

    def test_io_close_after_register_exception_after_reconnection(self):
        addr = ('127.0.0.1', 8200)
        storage_mock, cache, loop, io, protocol, transport = self.start(
            addrs=(addr,),
            loop_addrs=(),  # prevent auto connection
            finish_start=False)
        loop.call_soon_threadsafe(loop.connect_connecting, addr)  # connect
        loop.run_until_inactive()
        loop.protocol.data_received(sized(self.enc + b'5'))
        # let the register call fail
        self.respond(1, ("Exception", "register_failed"), async_=True)
        # check "reconnection called for"
        try_reconnecting = loop.later[1]  # entry 0 is "heartbeat"
        connect = try_reconnecting[1]
        self.assertEqual(connect.__func__.__name__,
                         "try_connecting")
        loop.call_soon_threadsafe(connect)
        loop.call_soon_threadsafe(loop.connect_connecting, addr)  # connect
        loop.run_until_inactive()
        loop.protocol.data_received(sized(self.enc + b'5'))
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())
        self.assertTrue(loop.protocol.closed)

    def test_io_close_after_register_exception_after_connection_lost(self):
        addr = ('127.0.0.1', 8200)
        storage_mock, cache, loop, io, protocol, transport = self.start(
            addrs=(addr,),
            loop_addrs=(),  # prevent auto connection
            finish_start=False)
        loop.call_soon_threadsafe(loop.connect_connecting, addr)  # connect
        loop.run_until_inactive()
        loop.protocol.data_received(sized(self.enc + b'5'))
        # let the register call fail
        self.respond(1, ("Exception", "register_failed"), async_=True)
        loop.protocol.connection_lost("disconnected")
        loop.call_soon_threadsafe(self.observe, io.close)
        loop.run_until_inactive()
        self.assertTrue(self.observed.done())
        self.assertTrue(loop.protocol.closed)


class MsgpackClientTests(ClientTests):
    enc = b'M'
    seq_type = tuple


class MemoryCache:

    def __init__(self):
        # { oid -> [(start, end, data)] }
        self.data = collections.defaultdict(list)
        self.last_tid = None
        self._lock = RLock()

    clear = __init__

    closed = False

    def close(self):
        self.closed = True

    def __len__(self):
        return len(self.data)

    def load(self, oid):
        revisions = self.data[oid]
        if revisions:
            start, end, data = revisions[-1]
            if not end:
                return data, start
        return None

    def store(self, oid, start_tid, end_tid, data):
        assert start_tid is not None
        revisions = self.data[oid]
        data = (start_tid, end_tid, data)
        if not revisions or data != revisions[-1]:
            revisions.append(data)
        revisions.sort()

    def loadBefore(self, oid, tid):
        for start, end, data in self.data[oid]:
            if start < tid and (end is None or end >= tid):
                return data, start, end

    def invalidate(self, oid, tid):
        revisions = self.data[oid]
        if revisions:
            if tid is None:
                del revisions[:]
            else:
                start, end, data = revisions[-1]
                if end is None:
                    revisions[-1] = start, tid, data

    def getLastTid(self):
        return self.last_tid

    def setLastTid(self, tid):
        if self.last_tid is not None and tid < self.last_tid:
            raise ValueError("tids must increase")
        self.last_tid = tid


class ServerTests(Base, setupstack.TestCase):

    # The server side of things is pretty simple compared to the
    # client, because it's the clien't job to make and keep
    # connections. Servers are pretty passive.

    def connect(self, finish=False):
        protocol = server_protocol(self.enc == b'M')
        self.loop = protocol.loop
        self.target = protocol.zeo_storage
        if finish:
            self.assertEqual(self.pop(parse=False),
                             self.enc + best_protocol_version)
            protocol.data_received(sized(self.enc + b'5'))
        return protocol

    message_id = 0
    target = None

    def call(self, meth, *args, **kw):
        if kw:
            expect = kw.pop('expect', self)
            target = kw.pop('target', self.target)
            self.assertFalse(kw)

            if target is not None:
                target = getattr(target, meth)
                if expect is not self:
                    target.return_value = expect

        self.message_id += 1
        self.loop.protocol.data_received(
            sized(self.encode(self.message_id, False, meth, args)))

        if target is not None:
            target.assert_called_once_with(*args)
            target.reset_mock()

        if expect is not self:
            self.assertEqual(self.pop(),
                             (self.message_id, False, '.reply', expect))

    def testServerBasics(self):
        # A simple listening thread accepts connections.  It creats
        # asyncio connections by calling ZEO.asyncio.new_connection:
        protocol = self.connect()
        self.assertFalse(protocol.zeo_storage.notify_connected.called)

        # The server sends it's protocol.
        self.assertEqual(self.pop(parse=False),
                         self.enc + best_protocol_version)

        # The client sends it's protocol:
        protocol.data_received(sized(self.enc + b'5'))

        self.assertEqual(protocol.protocol_version, self.enc + b'5')

        protocol.zeo_storage.notify_connected.assert_called_once_with(protocol)

        # The client registers:
        self.call('register', False, expect=None)

        # It does other things, like, send hearbeats:
        protocol.data_received(sized(self.encode(-1, 0, '.reply', None)))

        # The client can make async calls:
        self.send('register')

        # Let's close the connection
        self.assertFalse(protocol.zeo_storage.notify_disconnected.called)
        protocol.connection_lost(None)
        protocol.zeo_storage.notify_disconnected.assert_called_once_with()

    def test_invalid_methods(self):
        protocol = self.connect(True)
        protocol.zeo_storage.notify_connected.assert_called_once_with(protocol)

        # If we try to call a method that isn't in the protocol's
        # white list, it will disconnect:
        self.assertFalse(protocol.loop.transport.closed)
        self.call('foo', target=None)
        self.assertTrue(protocol.loop.transport.closed)


class MsgpackServerTests(ServerTests):
    enc = b'M'
    seq_type = tuple


def server_protocol(msgpack,
                    zeo_storage=None,
                    protocol_version=None,
                    addr=('1.2.3.4', '42'),
                    ):
    if zeo_storage is None:
        zeo_storage = mock.Mock()
    loop = Loop()
    sock = ()  # anything not None
    new_connection(loop, addr, sock, zeo_storage, msgpack)
    if protocol_version:
        loop.protocol.data_received(sized(protocol_version))
    return loop.protocol


def sized(message):
    return struct.pack(">I", len(message)) + message


class Logging:

    def __init__(self, level=logging.ERROR):
        self.level = level

    def __enter__(self):
        self.handler = logging.StreamHandler()
        logging.getLogger().addHandler(self.handler)
        logging.getLogger().setLevel(self.level)

    def __exit__(self, *args):
        logging.getLogger().removeHandler(self.handler)
        logging.getLogger().setLevel(logging.NOTSET)


class ZEOBaseProtocolTests(setupstack.TestCase):

    def setUp(self):
        self.loop = loop = Loop()
        loop.create_connection(lambda: ZEOBaseProtocol(loop, "proto"),
                               sock=True)

    def tearDown(self):
        self.loop.protocol.close()
        self.loop.close()

    def test_write_message_iter(self):
        """test https://github.com/zopefoundation/ZEO/issues/150."""
        loop = self.loop
        protocol, transport = loop.protocol, loop.transport
        transport.capacity = 1  # single message

        def it(tag):
            yield tag
            yield tag

        protocol.write_message_iter(it(b"0"))
        protocol.write_message_iter(it(b"1"))

        for b in b"0011":
            l, t = transport.pop(2)
            self.assertEqual(l, b"\x00\x00\x00\x01")
            self.assertEqual(t, to_byte(b))


    def test_repr(self):
        repr(self.loop.protocol)  # satisfied if no exception


class SizedMessageProtocolTests(setupstack.TestCase):
    def setUp(self):
        self.loop = loop = Loop()
        self.received = received = []
        loop.create_connection(
            lambda: SizedMessageProtocol(received.append), sock=True)
        self.transport = loop.transport
        self.protocol = loop.protocol

    def tearDown(self):
        self.protocol.close()
        self.loop.close()

    def test_sm_write_message(self):
        protocol, transport = self.protocol, self.transport
        for i in range(2):
            protocol.write_message(to_byte(i))
        for i in range(2):
            l, b = transport.pop(2)
            self.assertEqual(l, b"\x00\x00\x00\x01")
            self.assertEqual(b, to_byte(i))
        protocol.write_message(b"12")
        l, m = transport.pop(2)
        self.assertEqual(l, b"\x00\x00\x00\x02")
        self.assertEqual(m, b"12")

    def test_sm_write_iter(self):
        protocol, transport = self.protocol, self.transport
        protocol.write_message_iter(to_byte(b) for b in b"0123")
        for b in b"0123":
            l, t = transport.pop(2)
            self.assertEqual(l, b"\x00\x00\x00\x01")
            self.assertEqual(t, to_byte(b))

    def test_sm_receive(self):
        protocol = self.protocol
        msgs = b"0 11 222".split()
        for msg in msgs:
            protocol.data_received(sized(msg))
        self.assertEqual(self.received, msgs)

    def test_sm_data_received(self):
        msg = b"a test message"
        data = sized(msg) * 2
        receive = self.protocol.data_received
        expected = [msg] * 2

        def check(*split_size):
            idx = 0
            for size in split_size:
                nidx = idx + size
                receive(data[idx:nidx])
                idx = nidx
            receive(data[idx:])
            self.assertEqual(self.received, expected)
            del self.received[:]

        # single chunk; i.e. split chunk
        check()
        # individual messages
        check(len(data) // 2)
        # combine from 2 chunks
        check(2, 2, 2)
        # combine from 3 chunks
        check(5, 2)
        # optimal
        check(4, len(msg), 4, len(msg))

        # check message processing closed the protocol
        # once closed, no further data is processed
        proto = self.protocol
        old_receive = proto.receive

        def close(msg):
            proto.close()
            proto.set_receive(old_receive)

        proto.set_receive(close)
        receive(data)
        self.assertEqual(self.received, [])


def to_byte(i):
    return bytes([i])


def _break_mock_cycles(m):
    """break (``mock`` introduced) cycles in mock *m*.

    Do not break cycles due to mock calling.
    """
    m._mock_parent = m._mock_new_parent = None
    for c in m._mock_children.values():
        _break_mock_cycles(c)
    if isinstance(m._mock_return_value, mock.Mock):
        _break_mock_cycles(m._mock_return_value)


class OptimizeTestsBase:
    def setUp(self):
        self.loop = FaithfulLoop()
        asyncio.set_event_loop(self.loop)

    def tearDown(self):
        self.loop.close()
        asyncio.set_event_loop(None)
        self.assertEqual(self.loop.exceptions, [])


class FutureTestsBase(OptimizeTestsBase):
    def setUp(self):
        super().setUp()
        self.fut = fut = self.make_future(self.loop)
        self.callback = cb = mock.Mock()
        fut.add_done_callback(cb)

    def tearDown(self):
        fut = self.fut
        fut.cancel()
        self.assertEqual(self.loop.exceptions, [])
        _break_mock_cycles(self.callback)
        super().tearDown()

    def test_set_result(self):
        self.fut.set_result(1)
        self.assertEqual(self.fut.result(), 1)
        self.check_called_not_scheduled()

    def test_set_exception(self):
        exc = Exception()
        self.fut.set_exception(exc)
        self.assertIs(self.fut.exception(), exc)
        self.check_called_not_scheduled()

    def test_remove_add_callback(self):
        fut = self.fut
        cb = self.callback
        self.assertEqual(fut.remove_done_callback(cb), 1)
        fut.set_result(1)
        fut.add_done_callback(cb)
        self.check_called_not_scheduled()

    def test_callback_can_add_callback(self):
        fut = self.fut
        cbno = count()
        li = [1, 0, 0, 1]

        def mk_callback():
            """make numbered callback and register it."""
            i = next(cbno)

            @fut.add_done_callback
            def callback(unused):
                li.append(i)
                if not li[i]:
                    # add new callback
                    mk_callback()  # noqa: F821

        mk_callback()
        mk_callback()
        mk_callback()
        fut.set_result(None)
        self.assertEqual(li[4:], [0, 1, 2, 3, 4, 5])
        mk_callback = None  # break reference cycle

    def test_exception_in_callback(self):
        fut = self.fut
        l = []

        def f(_): l.append('f')
        def g(_): l.append('g'); raise RuntimeError('g')
        def h(_): l.append('h')
        def i(_): l.append('i'); raise RuntimeError('i')
        def j(_): l.append('j')

        for x in f,g,h,i,j:
            fut.add_done_callback(x)

        self.assertEqual(self.loop.exceptions, [])
        fut.set_result(None)
        self.assertTrue(fut.done())
        self.assertEqual(l, ['f','g','h','i','j'])

        self.assertEqual(len(self.loop.exceptions), 2)
        e0 = self.loop.exceptions[0]
        e1 = self.loop.exceptions[1]
        self.assertIs(type(e0), dict)
        self.assertIs(type(e1), dict)
        self.assertEqual(set(e0.keys()), {'message', 'exception'})
        self.assertEqual(set(e1.keys()), {'message', 'exception'})
        self.assertRegex(e0['message'], r'Exception in callback .*\bg\b')
        self.assertRegex(e1['message'], r'Exception in callback .*\bi\b')
        # RuntimeError('x') != RuntimeError('x')  -> compare by hand
        x0 = e0['exception']
        x1 = e1['exception']
        self.assertIs(type(x0), RuntimeError)
        self.assertIs(type(x1), RuntimeError)
        self.assertEqual(x0.args, ('g',))
        self.assertEqual(x1.args, ('i',))
        self.loop.exceptions = []

    def test_cancel(self):
        _ = self.fut.cancel('zzz')
        self.assertIs(_, True)
        self.assertTrue(self.fut.cancelled())
        with self.assertRaises(asyncio.CancelledError) as e:
            self.fut.result()
        self.assertEqual(e.exception.args, ('zzz',))
        self.check_called_not_scheduled()

    def check_called_not_scheduled(self):
        fut = self.fut
        self.assertTrue(fut.done())
        self.callback.assert_called_once()
        self.assertEqual(len(self.loop._ready), 0)


class FutureTests(FutureTestsBase, TestCase):
    def make_future(self, loop):
        return Future(loop=self.loop)

class ConcurrentFutureTests(FutureTestsBase, TestCase):
    def make_future(self, loop):
        return ConcurrentFuture(loop=loop)

    def test_result_timeout(self):
        self.assertFalse(self.fut.done())
        with self.assertRaises(asyncio.TimeoutError):
            self.fut.result(0.1)  # should timeout - noone set result on .fut
        self.assertFalse(self.fut.done())


class CoroutineExecutorTestsBase(OptimizeTestsBase):
    def run_loop(self):
        loop = self.loop
        loop.call_soon(loop.stop)
        loop.run_forever()

    def test_noop(self):

        async def noop():
            pass

        t = self.make_task(noop)
        self.assertTrue(t.done())
        self.assertIsNone(t.result())

    def test_async_future(self):
        self.check_future(self.loop.create_future(), True)

    def test_repr(self):

        async def noop():
            pass

        t = self.make_task(noop)
        repr(t)  # satisfied if no exception

    def test_optimized_future(self):
        self.check_future(Future(loop=self.loop))

    def check_future(self, fut, run_loop=False):

        async def wait():
            return await fut

        t = self.make_task(wait)
        self.assertFalse(t.done())
        fut.set_result(1)
        if run_loop:
            self.run_loop()
        self.assertTrue(t.done())
        self.assertEqual(t.result(), 1)

    def test_exception(self):
        fut = Future(loop=self.loop)

        async def exc():
            await fut

        t = self.make_task(exc)
        exc = Exception()
        fut.set_exception(exc)
        self.assertTrue(t.done())
        self.assertIs(t.exception(), exc)
        exc.__traceback__ = None  # break reference cycle

    def test_handled_exception(self):

        fut = Future(loop=self.loop)

        async def exc():
            try:
                await fut
            except Exception:
                return 1

        t = self.make_task(exc)
        exc = Exception()
        fut.set_exception(exc)
        self.assertTrue(t.done())
        self.assertEqual(t.result(), 1)
        exc.__traceback__ = None  # break reference cycle

    def test_cancel_future(self):
        fut = Future(loop=self.loop)

        async def exc():
            await fut

        t = self.make_task(exc)
        _ = fut.cancel('zzz')
        self.assertIs(_, True)
        self.assertTrue(t.done())
        self.assertTrue(t.cancelled())
        with self.assertRaises(asyncio.CancelledError) as e:
            t.result()
        self.assertEqual(e.exception.args, ('zzz',))

    def test_cancel_task_while_blocked_on_optimized_future(self):
        self._test_cancel_task_while_blocked(Future(loop=self.loop))

    def test_cancel_task_while_blocked_on_async_future(self):
        self._test_cancel_task_while_blocked(asyncio.Future(loop=self.loop))

    def _test_cancel_task_while_blocked(self, blocked_on):
        l = []
        go = Future(loop=self.loop)
        waitready = Future(loop=self.loop)
        waiting = blocked_on

        async def f():
            await go  # wait for loop to start
            l.append(1)
            l.append(2)
            waitready.set_result(None)
            await waiting
            l.append(3)

        t = self.make_task(f)
        self.assertFalse(t.done())

        @waitready.add_done_callback
        def _(_):
            _ = t.cancel('zzz')
            self.assertIs(_, True)

        self.loop.call_soon(lambda: go.set_result(None))
        with self.assertRaises(asyncio.CancelledError):
            self.loop.run_until_complete(t)
        self.assertTrue(t.done())
        self.assertTrue(t.cancelled())
        with self.assertRaises(asyncio.CancelledError) as e:
            t.result()
        self.assertEqual(e.exception.args, ('zzz',))
        self.assertTrue(waiting.done())
        self.assertTrue(waiting.cancelled())
        self.assertEqual(l, [1,2])
        _ = t.cancel()
        self.assertIs(_, False)


    def test_cancel_task_while_running(self):
        l = []
        t = None
        go = Future(loop=self.loop)
        waiting = Future(loop=self.loop)

        async def f():
            await go
            l.append(1)
            l.append(2)
            _ = t.cancel('zzz')
            self.assertIs(_, True)
            l.append(3)
            await waiting
            l.append(4)

        t = self.make_task(f)
        self.assertFalse(t.done())
        go.set_result(None)
        self.assertTrue(t.done())
        self.assertTrue(t.cancelled())
        with self.assertRaises(asyncio.CancelledError) as e:
            t.result()
        self.assertEqual(e.exception.args, ('zzz',))
        self.assertTrue(waiting.done())
        self.assertTrue(waiting.cancelled())
        self.assertEqual(l, [1,2,3])


    def test_nested_coro(self):
        l = []

        async def f():
            await g('a')
            await g('b')

        async def g(s):
            await h(s, 1)
            await h(s, 2)
            await h(s, 3)

        async def h(s, n):
            for i in range(1,n+1):
                l.append(s*i)

        t = self.make_task(f)
        self.assertTrue(t.done())
        self.assertEqual(l, ['a', 'a','aa', 'a','aa','aaa', 'b', 'b','bb', 'b','bb','bbb'])

    def test_nest_to_async_coro(self):
        go = Future(loop=self.loop)

        async def f():
            await go  # wait for loop to start before running into asyncio.sleep
            _ = await asyncio.sleep(1, 'zzz')
            return _

        tstart = time()
        t = self.make_task(f)
        self.assertFalse(t.done())
        self.loop.call_soon(lambda: go.set_result(None))
        self.loop.run_until_complete(t)
        tend = time()
        self.assertTrue(t.done())
        self.assertEqual(t.result(), 'zzz')
        self.assertGreaterEqual(tend - tstart, 1)

    def test_return(self):

        # plain return
        async def f():
            return 123

        t = self.make_task(f)
        self.assertTrue(t.done())
        self.assertEqual(t.result(), 123)

        # nested returns
        async def g():
            x = await f()
            return 1000 + x

        t = self.make_task(g)
        self.assertTrue(t.done())
        self.assertEqual(t.result(), 1123)

        # verify that return can go through `except Exception`
        # (but not through `except BaseException`)
        async def h():
            try:
                return 456
                raise Exception('aaa')
            except Exception:
                raise

        t = self.make_task(h)
        self.assertTrue(t.done())
        self.assertEqual(t.result(), 456)


class AsyncTaskTests(CoroutineExecutorTestsBase, TestCase):
    def make_task(self, coro):
        return AsyncTask(coro(), self.loop)


class ConcurrentTaskTests(CoroutineExecutorTestsBase, TestCase):
    def make_task(self, coro):
        loop = self.loop
        t = ConcurrentTask(coro(), loop)
        self.run_loop()
        return t
