# Copyright (c) Twisted Matrix Laboratories.
# See LICENSE for details.

"""
Test the memcache client protocol.
"""


from twisted.internet.defer import Deferred, DeferredList, TimeoutError, gatherResults
from twisted.internet.error import ConnectionDone
from twisted.internet.task import Clock
from twisted.internet.testing import StringTransportWithDisconnection
from twisted.protocols.memcache import (
    ClientError,
    MemCacheProtocol,
    NoSuchCommand,
    ServerError,
)
from twisted.trial.unittest import TestCase


class CommandMixin:
    """
    Setup and tests for basic invocation of L{MemCacheProtocol} commands.
    """

    def _test(self, d, send, recv, result):
        """
        Helper test method to test the resulting C{Deferred} of a
        L{MemCacheProtocol} command.
        """
        raise NotImplementedError()

    def test_get(self):
        """
        L{MemCacheProtocol.get} returns a L{Deferred} which is called back with
        the value and the flag associated with the given key if the server
        returns a successful result.
        """
        return self._test(
            self.proto.get(b"foo"),
            b"get foo\r\n",
            b"VALUE foo 0 3\r\nbar\r\nEND\r\n",
            (0, b"bar"),
        )

    def test_emptyGet(self):
        """
        Test getting a non-available key: it succeeds but return L{None} as
        value and C{0} as flag.
        """
        return self._test(self.proto.get(b"foo"), b"get foo\r\n", b"END\r\n", (0, None))

    def test_getMultiple(self):
        """
        L{MemCacheProtocol.getMultiple} returns a L{Deferred} which is called
        back with a dictionary of flag, value for each given key.
        """
        return self._test(
            self.proto.getMultiple([b"foo", b"cow"]),
            b"get foo cow\r\n",
            b"VALUE foo 0 3\r\nbar\r\nVALUE cow 0 7\r\nchicken\r\nEND\r\n",
            {b"cow": (0, b"chicken"), b"foo": (0, b"bar")},
        )

    def test_getMultipleWithEmpty(self):
        """
        When L{MemCacheProtocol.getMultiple} is called with non-available keys,
        the corresponding tuples are (0, None).
        """
        return self._test(
            self.proto.getMultiple([b"foo", b"cow"]),
            b"get foo cow\r\n",
            b"VALUE cow 1 3\r\nbar\r\nEND\r\n",
            {b"cow": (1, b"bar"), b"foo": (0, None)},
        )

    def test_set(self):
        """
        L{MemCacheProtocol.set} returns a L{Deferred} which is called back with
        C{True} when the operation succeeds.
        """
        return self._test(
            self.proto.set(b"foo", b"bar"),
            b"set foo 0 0 3\r\nbar\r\n",
            b"STORED\r\n",
            True,
        )

    def test_add(self):
        """
        L{MemCacheProtocol.add} returns a L{Deferred} which is called back with
        C{True} when the operation succeeds.
        """
        return self._test(
            self.proto.add(b"foo", b"bar"),
            b"add foo 0 0 3\r\nbar\r\n",
            b"STORED\r\n",
            True,
        )

    def test_replace(self):
        """
        L{MemCacheProtocol.replace} returns a L{Deferred} which is called back
        with C{True} when the operation succeeds.
        """
        return self._test(
            self.proto.replace(b"foo", b"bar"),
            b"replace foo 0 0 3\r\nbar\r\n",
            b"STORED\r\n",
            True,
        )

    def test_errorAdd(self):
        """
        Test an erroneous add: if a L{MemCacheProtocol.add} is called but the
        key already exists on the server, it returns a B{NOT STORED} answer,
        which calls back the resulting L{Deferred} with C{False}.
        """
        return self._test(
            self.proto.add(b"foo", b"bar"),
            b"add foo 0 0 3\r\nbar\r\n",
            b"NOT STORED\r\n",
            False,
        )

    def test_errorReplace(self):
        """
        Test an erroneous replace: if a L{MemCacheProtocol.replace} is called
        but the key doesn't exist on the server, it returns a B{NOT STORED}
        answer, which calls back the resulting L{Deferred} with C{False}.
        """
        return self._test(
            self.proto.replace(b"foo", b"bar"),
            b"replace foo 0 0 3\r\nbar\r\n",
            b"NOT STORED\r\n",
            False,
        )

    def test_delete(self):
        """
        L{MemCacheProtocol.delete} returns a L{Deferred} which is called back
        with C{True} when the server notifies a success.
        """
        return self._test(
            self.proto.delete(b"bar"), b"delete bar\r\n", b"DELETED\r\n", True
        )

    def test_errorDelete(self):
        """
        Test an error during a delete: if key doesn't exist on the server, it
        returns a B{NOT FOUND} answer which calls back the resulting
        L{Deferred} with C{False}.
        """
        return self._test(
            self.proto.delete(b"bar"), b"delete bar\r\n", b"NOT FOUND\r\n", False
        )

    def test_increment(self):
        """
        Test incrementing a variable: L{MemCacheProtocol.increment} returns a
        L{Deferred} which is called back with the incremented value of the
        given key.
        """
        return self._test(self.proto.increment(b"foo"), b"incr foo 1\r\n", b"4\r\n", 4)

    def test_decrement(self):
        """
        Test decrementing a variable: L{MemCacheProtocol.decrement} returns a
        L{Deferred} which is called back with the decremented value of the
        given key.
        """
        return self._test(self.proto.decrement(b"foo"), b"decr foo 1\r\n", b"5\r\n", 5)

    def test_incrementVal(self):
        """
        L{MemCacheProtocol.increment} takes an optional argument C{value} which
        replaces the default value of 1 when specified.
        """
        return self._test(
            self.proto.increment(b"foo", 8), b"incr foo 8\r\n", b"4\r\n", 4
        )

    def test_decrementVal(self):
        """
        L{MemCacheProtocol.decrement} takes an optional argument C{value} which
        replaces the default value of 1 when specified.
        """
        return self._test(
            self.proto.decrement(b"foo", 3), b"decr foo 3\r\n", b"5\r\n", 5
        )

    def test_stats(self):
        """
        Test retrieving server statistics via the L{MemCacheProtocol.stats}
        command: it parses the data sent by the server and calls back the
        resulting L{Deferred} with a dictionary of the received statistics.
        """
        return self._test(
            self.proto.stats(),
            b"stats\r\n",
            b"STAT foo bar\r\nSTAT egg spam\r\nEND\r\n",
            {b"foo": b"bar", b"egg": b"spam"},
        )

    def test_statsWithArgument(self):
        """
        L{MemCacheProtocol.stats} takes an optional C{bytes} argument which,
        if specified, is sent along with the I{STAT} command.  The I{STAT}
        responses from the server are parsed as key/value pairs and returned
        as a C{dict} (as in the case where the argument is not specified).
        """
        return self._test(
            self.proto.stats(b"blah"),
            b"stats blah\r\n",
            b"STAT foo bar\r\nSTAT egg spam\r\nEND\r\n",
            {b"foo": b"bar", b"egg": b"spam"},
        )

    def test_version(self):
        """
        Test version retrieval via the L{MemCacheProtocol.version} command: it
        returns a L{Deferred} which is called back with the version sent by the
        server.
        """
        return self._test(
            self.proto.version(), b"version\r\n", b"VERSION 1.1\r\n", b"1.1"
        )

    def test_flushAll(self):
        """
        L{MemCacheProtocol.flushAll} returns a L{Deferred} which is called back
        with C{True} if the server acknowledges success.
        """
        return self._test(self.proto.flushAll(), b"flush_all\r\n", b"OK\r\n", True)


class MemCacheTests(CommandMixin, TestCase):
    """
    Test client protocol class L{MemCacheProtocol}.
    """

    def setUp(self):
        """
        Create a memcache client, connect it to a string protocol, and make it
        use a deterministic clock.
        """
        self.proto = MemCacheProtocol()
        self.clock = Clock()
        self.proto.callLater = self.clock.callLater
        self.transport = StringTransportWithDisconnection()
        self.transport.protocol = self.proto
        self.proto.makeConnection(self.transport)

    def _test(self, d, send, recv, result):
        """
        Implementation of C{_test} which checks that the command sends C{send}
        data, and that upon reception of C{recv} the result is C{result}.

        @param d: the resulting deferred from the memcache command.
        @type d: C{Deferred}

        @param send: the expected data to be sent.
        @type send: C{bytes}

        @param recv: the data to simulate as reception.
        @type recv: C{bytes}

        @param result: the expected result.
        @type result: C{any}
        """

        def cb(res):
            self.assertEqual(res, result)

        self.assertEqual(self.transport.value(), send)
        d.addCallback(cb)
        self.proto.dataReceived(recv)
        return d

    def test_invalidGetResponse(self):
        """
        If the value returned doesn't match the expected key of the current
        C{get} command, an error is raised in L{MemCacheProtocol.dataReceived}.
        """
        self.proto.get(b"foo")
        self.assertRaises(
            RuntimeError,
            self.proto.dataReceived,
            b"VALUE bar 0 7\r\nspamegg\r\nEND\r\n",
        )

    def test_invalidMultipleGetResponse(self):
        """
        If the value returned doesn't match one the expected keys of the
        current multiple C{get} command, an error is raised error in
        L{MemCacheProtocol.dataReceived}.
        """
        self.proto.getMultiple([b"foo", b"bar"])
        self.assertRaises(
            RuntimeError,
            self.proto.dataReceived,
            b"VALUE egg 0 7\r\nspamegg\r\nEND\r\n",
        )

    def test_invalidEndResponse(self):
        """
        If an END is received in response to an operation that isn't C{get},
        C{gets}, or C{stats}, an error is raised in
        L{MemCacheProtocol.dataReceived}.
        """
        self.proto.set(b"key", b"value")
        self.assertRaises(RuntimeError, self.proto.dataReceived, b"END\r\n")

    def test_timeOut(self):
        """
        Test the timeout on outgoing requests: when timeout is detected, all
        current commands fail with a L{TimeoutError}, and the connection is
        closed.
        """
        d1 = self.proto.get(b"foo")
        d2 = self.proto.get(b"bar")
        d3 = Deferred()
        self.proto.connectionLost = d3.callback

        self.clock.advance(self.proto.persistentTimeOut)
        self.assertFailure(d1, TimeoutError)
        self.assertFailure(d2, TimeoutError)

        def checkMessage(error):
            self.assertEqual(str(error), "Connection timeout")

        d1.addCallback(checkMessage)
        self.assertFailure(d3, ConnectionDone)
        return gatherResults([d1, d2, d3])

    def test_timeoutRemoved(self):
        """
        When a request gets a response, no pending timeout call remains around.
        """
        d = self.proto.get(b"foo")

        self.clock.advance(self.proto.persistentTimeOut - 1)
        self.proto.dataReceived(b"VALUE foo 0 3\r\nbar\r\nEND\r\n")

        def check(result):
            self.assertEqual(result, (0, b"bar"))
            self.assertEqual(len(self.clock.calls), 0)

        d.addCallback(check)
        return d

    def test_timeOutRaw(self):
        """
        Test the timeout when raw mode was started: the timeout is not reset
        until all the data has been received, so we can have a L{TimeoutError}
        when waiting for raw data.
        """
        d1 = self.proto.get(b"foo")
        d2 = Deferred()
        self.proto.connectionLost = d2.callback

        self.proto.dataReceived(b"VALUE foo 0 10\r\n12345")
        self.clock.advance(self.proto.persistentTimeOut)
        self.assertFailure(d1, TimeoutError)
        self.assertFailure(d2, ConnectionDone)
        return gatherResults([d1, d2])

    def test_timeOutStat(self):
        """
        Test the timeout when stat command has started: the timeout is not
        reset until the final B{END} is received.
        """
        d1 = self.proto.stats()
        d2 = Deferred()
        self.proto.connectionLost = d2.callback

        self.proto.dataReceived(b"STAT foo bar\r\n")
        self.clock.advance(self.proto.persistentTimeOut)
        self.assertFailure(d1, TimeoutError)
        self.assertFailure(d2, ConnectionDone)
        return gatherResults([d1, d2])

    def test_timeoutPipelining(self):
        """
        When two requests are sent, a timeout call remains around for the
        second request, and its timeout time is correct.
        """
        d1 = self.proto.get(b"foo")
        d2 = self.proto.get(b"bar")
        d3 = Deferred()
        self.proto.connectionLost = d3.callback

        self.clock.advance(self.proto.persistentTimeOut - 1)
        self.proto.dataReceived(b"VALUE foo 0 3\r\nbar\r\nEND\r\n")

        def check(result):
            self.assertEqual(result, (0, b"bar"))
            self.assertEqual(len(self.clock.calls), 1)
            for i in range(self.proto.persistentTimeOut):
                self.clock.advance(1)
            return self.assertFailure(d2, TimeoutError).addCallback(checkTime)

        def checkTime(ignored):
            # Check that the timeout happened C{self.proto.persistentTimeOut}
            # after the last response
            self.assertEqual(self.clock.seconds(), 2 * self.proto.persistentTimeOut - 1)

        d1.addCallback(check)
        self.assertFailure(d3, ConnectionDone)
        return d1

    def test_timeoutNotReset(self):
        """
        Check that timeout is not resetted for every command, but keep the
        timeout from the first command without response.
        """
        d1 = self.proto.get(b"foo")
        d3 = Deferred()
        self.proto.connectionLost = d3.callback

        self.clock.advance(self.proto.persistentTimeOut - 1)
        d2 = self.proto.get(b"bar")
        self.clock.advance(1)
        self.assertFailure(d1, TimeoutError)
        self.assertFailure(d2, TimeoutError)
        self.assertFailure(d3, ConnectionDone)
        return gatherResults([d1, d2, d3])

    def test_timeoutCleanDeferreds(self):
        """
        C{timeoutConnection} cleans the list of commands that it fires with
        C{TimeoutError}: C{connectionLost} doesn't try to fire them again, but
        sets the disconnected state so that future commands fail with a
        C{RuntimeError}.
        """
        d1 = self.proto.get(b"foo")
        self.clock.advance(self.proto.persistentTimeOut)
        self.assertFailure(d1, TimeoutError)
        d2 = self.proto.get(b"bar")
        self.assertFailure(d2, RuntimeError)
        return gatherResults([d1, d2])

    def test_connectionLost(self):
        """
        When disconnection occurs while commands are still outstanding, the
        commands fail.
        """
        d1 = self.proto.get(b"foo")
        d2 = self.proto.get(b"bar")
        self.transport.loseConnection()
        done = DeferredList([d1, d2], consumeErrors=True)

        def checkFailures(results):
            for success, result in results:
                self.assertFalse(success)
                result.trap(ConnectionDone)

        return done.addCallback(checkFailures)

    def test_tooLongKey(self):
        """
        An error is raised when trying to use a too long key: the called
        command returns a L{Deferred} which fails with a L{ClientError}.
        """
        d1 = self.assertFailure(self.proto.set(b"a" * 500, b"bar"), ClientError)
        d2 = self.assertFailure(self.proto.increment(b"a" * 500), ClientError)
        d3 = self.assertFailure(self.proto.get(b"a" * 500), ClientError)
        d4 = self.assertFailure(self.proto.append(b"a" * 500, b"bar"), ClientError)
        d5 = self.assertFailure(self.proto.prepend(b"a" * 500, b"bar"), ClientError)
        d6 = self.assertFailure(
            self.proto.getMultiple([b"foo", b"a" * 500]), ClientError
        )
        return gatherResults([d1, d2, d3, d4, d5, d6])

    def test_invalidCommand(self):
        """
        When an unknown command is sent directly (not through public API), the
        server answers with an B{ERROR} token, and the command fails with
        L{NoSuchCommand}.
        """
        d = self.proto._set(b"egg", b"foo", b"bar", 0, 0, b"")
        self.assertEqual(self.transport.value(), b"egg foo 0 0 3\r\nbar\r\n")
        self.assertFailure(d, NoSuchCommand)
        self.proto.dataReceived(b"ERROR\r\n")
        return d

    def test_clientError(self):
        """
        Test the L{ClientError} error: when the server sends a B{CLIENT_ERROR}
        token, the originating command fails with L{ClientError}, and the error
        contains the text sent by the server.
        """
        a = b"eggspamm"
        d = self.proto.set(b"foo", a)
        self.assertEqual(self.transport.value(), b"set foo 0 0 8\r\neggspamm\r\n")
        self.assertFailure(d, ClientError)

        def check(err):
            self.assertEqual(str(err), repr(b"We don't like egg and spam"))

        d.addCallback(check)
        self.proto.dataReceived(b"CLIENT_ERROR We don't like egg and spam\r\n")
        return d

    def test_serverError(self):
        """
        Test the L{ServerError} error: when the server sends a B{SERVER_ERROR}
        token, the originating command fails with L{ServerError}, and the error
        contains the text sent by the server.
        """
        a = b"eggspamm"
        d = self.proto.set(b"foo", a)
        self.assertEqual(self.transport.value(), b"set foo 0 0 8\r\neggspamm\r\n")
        self.assertFailure(d, ServerError)

        def check(err):
            self.assertEqual(str(err), repr(b"zomg"))

        d.addCallback(check)
        self.proto.dataReceived(b"SERVER_ERROR zomg\r\n")
        return d

    def test_unicodeKey(self):
        """
        Using a non-string key as argument to commands raises an error.
        """
        d1 = self.assertFailure(self.proto.set("foo", b"bar"), ClientError)
        d2 = self.assertFailure(self.proto.increment("egg"), ClientError)
        d3 = self.assertFailure(self.proto.get(1), ClientError)
        d4 = self.assertFailure(self.proto.delete("bar"), ClientError)
        d5 = self.assertFailure(self.proto.append("foo", b"bar"), ClientError)
        d6 = self.assertFailure(self.proto.prepend("foo", b"bar"), ClientError)
        d7 = self.assertFailure(self.proto.getMultiple([b"egg", 1]), ClientError)
        return gatherResults([d1, d2, d3, d4, d5, d6, d7])

    def test_unicodeValue(self):
        """
        Using a non-string value raises an error.
        """
        return self.assertFailure(self.proto.set(b"foo", "bar"), ClientError)

    def test_pipelining(self):
        """
        Multiple requests can be sent subsequently to the server, and the
        protocol orders the responses correctly and dispatch to the
        corresponding client command.
        """
        d1 = self.proto.get(b"foo")
        d1.addCallback(self.assertEqual, (0, b"bar"))
        d2 = self.proto.set(b"bar", b"spamspamspam")
        d2.addCallback(self.assertEqual, True)
        d3 = self.proto.get(b"egg")
        d3.addCallback(self.assertEqual, (0, b"spam"))
        self.assertEqual(
            self.transport.value(),
            b"get foo\r\nset bar 0 0 12\r\nspamspamspam\r\nget egg\r\n",
        )
        self.proto.dataReceived(
            b"VALUE foo 0 3\r\nbar\r\nEND\r\n"
            b"STORED\r\n"
            b"VALUE egg 0 4\r\nspam\r\nEND\r\n"
        )
        return gatherResults([d1, d2, d3])

    def test_getInChunks(self):
        """
        If the value retrieved by a C{get} arrive in chunks, the protocol
        is able to reconstruct it and to produce the good value.
        """
        d = self.proto.get(b"foo")
        d.addCallback(self.assertEqual, (0, b"0123456789"))
        self.assertEqual(self.transport.value(), b"get foo\r\n")
        self.proto.dataReceived(b"VALUE foo 0 10\r\n0123456")
        self.proto.dataReceived(b"789")
        self.proto.dataReceived(b"\r\nEND")
        self.proto.dataReceived(b"\r\n")
        return d

    def test_append(self):
        """
        L{MemCacheProtocol.append} behaves like a L{MemCacheProtocol.set}
        method: it returns a L{Deferred} which is called back with C{True} when
        the operation succeeds.
        """
        return self._test(
            self.proto.append(b"foo", b"bar"),
            b"append foo 0 0 3\r\nbar\r\n",
            b"STORED\r\n",
            True,
        )

    def test_prepend(self):
        """
        L{MemCacheProtocol.prepend} behaves like a L{MemCacheProtocol.set}
        method: it returns a L{Deferred} which is called back with C{True} when
        the operation succeeds.
        """
        return self._test(
            self.proto.prepend(b"foo", b"bar"),
            b"prepend foo 0 0 3\r\nbar\r\n",
            b"STORED\r\n",
            True,
        )

    def test_gets(self):
        """
        L{MemCacheProtocol.get} handles an additional cas result when
        C{withIdentifier} is C{True} and forward it in the resulting
        L{Deferred}.
        """
        return self._test(
            self.proto.get(b"foo", True),
            b"gets foo\r\n",
            b"VALUE foo 0 3 1234\r\nbar\r\nEND\r\n",
            (0, b"1234", b"bar"),
        )

    def test_emptyGets(self):
        """
        Test getting a non-available key with gets: it succeeds but return
        L{None} as value, C{0} as flag and an empty cas value.
        """
        return self._test(
            self.proto.get(b"foo", True), b"gets foo\r\n", b"END\r\n", (0, b"", None)
        )

    def test_getsMultiple(self):
        """
        L{MemCacheProtocol.getMultiple} handles an additional cas field in the
        returned tuples if C{withIdentifier} is C{True}.
        """
        return self._test(
            self.proto.getMultiple([b"foo", b"bar"], True),
            b"gets foo bar\r\n",
            b"VALUE foo 0 3 1234\r\negg\r\n" b"VALUE bar 0 4 2345\r\nspam\r\nEND\r\n",
            {b"bar": (0, b"2345", b"spam"), b"foo": (0, b"1234", b"egg")},
        )

    def test_getsMultipleIterableKeys(self):
        """
        L{MemCacheProtocol.getMultiple} accepts any iterable of keys.
        """
        return self._test(
            self.proto.getMultiple(iter([b"foo", b"bar"]), True),
            b"gets foo bar\r\n",
            b"VALUE foo 0 3 1234\r\negg\r\n" b"VALUE bar 0 4 2345\r\nspam\r\nEND\r\n",
            {b"bar": (0, b"2345", b"spam"), b"foo": (0, b"1234", b"egg")},
        )

    def test_getsMultipleWithEmpty(self):
        """
        When getting a non-available key with L{MemCacheProtocol.getMultiple}
        when C{withIdentifier} is C{True}, the other keys are retrieved
        correctly, and the non-available key gets a tuple of C{0} as flag,
        L{None} as value, and an empty cas value.
        """
        return self._test(
            self.proto.getMultiple([b"foo", b"bar"], True),
            b"gets foo bar\r\n",
            b"VALUE foo 0 3 1234\r\negg\r\nEND\r\n",
            {b"bar": (0, b"", None), b"foo": (0, b"1234", b"egg")},
        )

    def test_checkAndSet(self):
        """
        L{MemCacheProtocol.checkAndSet} passes an additional cas identifier
        that the server handles to check if the data has to be updated.
        """
        return self._test(
            self.proto.checkAndSet(b"foo", b"bar", cas=b"1234"),
            b"cas foo 0 0 3 1234\r\nbar\r\n",
            b"STORED\r\n",
            True,
        )

    def test_casUnknowKey(self):
        """
        When L{MemCacheProtocol.checkAndSet} response is C{EXISTS}, the
        resulting L{Deferred} fires with C{False}.
        """
        return self._test(
            self.proto.checkAndSet(b"foo", b"bar", cas=b"1234"),
            b"cas foo 0 0 3 1234\r\nbar\r\n",
            b"EXISTS\r\n",
            False,
        )


class CommandFailureTests(CommandMixin, TestCase):
    """
    Tests for correct failure of commands on a disconnected
    L{MemCacheProtocol}.
    """

    def setUp(self):
        """
        Create a disconnected memcache client, using a deterministic clock.
        """
        self.proto = MemCacheProtocol()
        self.clock = Clock()
        self.proto.callLater = self.clock.callLater
        self.transport = StringTransportWithDisconnection()
        self.transport.protocol = self.proto
        self.proto.makeConnection(self.transport)
        self.transport.loseConnection()

    def _test(self, d, send, recv, result):
        """
        Implementation of C{_test} which checks that the command fails with
        C{RuntimeError} because the transport is disconnected. All the
        parameters except C{d} are ignored.
        """
        return self.assertFailure(d, RuntimeError)
