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

from twisted.python.failure import Failure
from twisted.trial import unittest
from twisted.internet.defer import Deferred, fail



class ResultOfCoroutineAssertionsTests(unittest.SynchronousTestCase):
    """
    Tests for L{SynchronousTestCase.successResultOf},
    L{SynchronousTestCase.failureResultOf}, and
    L{SynchronousTestCase.assertNoResult} when given a coroutine.
    """

    result = object()
    exception = Exception("Bad times")
    failure = Failure(exception)


    async def successResult(self):
        return self.result


    async def noCurrentResult(self):
        await Deferred()


    async def raisesException(self):
        raise self.exception


    def test_withoutResult(self):
        """
        L{SynchronousTestCase.successResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        with no current result.
        """
        self.assertRaises(
            self.failureException, self.successResultOf, self.noCurrentResult()
        )


    def test_successResultOfWithException(self):
        """
        L{SynchronousTestCase.successResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        that raises an exception.
        """
        self.assertRaises(
            self.failureException, self.successResultOf, self.raisesException()
        )


    def test_successResultOfWithFailureHasTraceback(self):
        """
        L{SynchronousTestCase.successResultOf} raises a
        L{SynchronousTestCase.failureException} that has the original failure
        traceback when called with a coroutine with a failure result.
        """
        try:
            self.successResultOf(self.raisesException())
        except self.failureException as e:
            self.assertIn(self.failure.getTraceback(), str(e))

    test_successResultOfWithFailureHasTraceback.todo = (
        "Tracebacks aren't preserved by exceptions later wrapped in Failures"
    )


    def test_failureResultOfWithoutResult(self):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        with no current result.
        """
        self.assertRaises(
            self.failureException, self.failureResultOf, self.noCurrentResult()
        )


    def test_failureResultOfWithSuccess(self):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        with a success result.
        """
        self.assertRaises(
            self.failureException, self.failureResultOf, self.successResult()
        )


    def test_failureResultOfWithWrongFailure(self):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        that raises an exception that was not expected.
        """
        self.assertRaises(
            self.failureException,
            self.failureResultOf, self.raisesException(), KeyError
        )


    def test_failureResultOfWithWrongExceptionOneExpectedException(self):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        that raises an exception with a failure type that was not expected, and
        the L{SynchronousTestCase.failureException} message contains the
        expected exception type.
        """
        try:
            self.failureResultOf(self.raisesException(), KeyError)
        except self.failureException as e:
            self.assertIn(
                "Failure of type ({0}.{1}) expected on".format(
                    KeyError.__module__, KeyError.__name__
                ),
                str(e)
            )


    def test_failureResultOfWithWrongExceptionOneExpectedExceptionHasTB(self):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        that raises an exception with a failure type that was not expected, and
        the L{SynchronousTestCase.failureException} message contains the
        original exception traceback.
        """
        try:
            self.failureResultOf(self.raisesException(), KeyError)
        except self.failureException as e:
            self.assertIn(self.failure.getTraceback(), str(e))

    test_failureResultOfWithWrongExceptionOneExpectedExceptionHasTB.todo = (
        "Tracebacks aren't preserved by exceptions later wrapped in Failures"
    )


    def test_failureResultOfWithWrongExceptionMultiExpectedExceptions(self):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        that raises an exception of a type that was not expected, and the
        L{SynchronousTestCase.failureException} message contains expected
        exception types in the error message.
        """
        try:
            self.failureResultOf(self.raisesException(), KeyError, IOError)
        except self.failureException as e:
            self.assertIn(
                "Failure of type ({0}.{1} or {2}.{3}) expected on".format(
                    KeyError.__module__, KeyError.__name__,
                    IOError.__module__, IOError.__name__,
                ),
                str(e)
            )


    def test_failureResultOfWithWrongExceptionMultiExpectedExceptionsHasTB(
        self
    ):
        """
        L{SynchronousTestCase.failureResultOf} raises
        L{SynchronousTestCase.failureException} when called with a coroutine
        that raises an exception of a type that was not expected, and the
        L{SynchronousTestCase.failureException} message contains the original
        exception traceback in the error message.
        """
        try:
            self.failureResultOf(self.raisesException(), KeyError, IOError)
        except self.failureException as e:
            self.assertIn(self.failure.getTraceback(), str(e))

    test_failureResultOfWithWrongExceptionMultiExpectedExceptionsHasTB.todo = (
        "Tracebacks aren't preserved by exceptions later wrapped in Failures"
    )


    def test_successResultOfWithSuccessResult(self):
        """
        When passed a coroutine which currently has a result (ie, if converted
        into a L{Deferred}, L{Deferred.addCallback} would cause the added
        callback to be called before C{addCallback} returns),
        L{SynchronousTestCase.successResultOf} returns that result.
        """
        self.assertIdentical(
            self.result, self.successResultOf(self.successResult())
        )


    def test_failureResultOfWithExpectedException(self):
        """
        When passed a coroutine which currently has an exception result (ie, if
        converted into a L{Deferred}, L{Deferred.addErrback} would cause the
        added errback to be called before C{addErrback} returns),
        L{SynchronousTestCase.failureResultOf} returns a L{Failure} containing
        that exception, if the exception type is expected.
        """
        self.assertEqual(
            self.failure.value,
            self.failureResultOf(
                self.raisesException(), self.failure.type, KeyError
            ).value
        )


    def test_failureResultOfWithException(self):
        """
        When passed a coroutine which currently has an exception result (ie, if
        converted into a L{Deferred}, L{Deferred.addErrback} would cause the
        added errback to be called before C{addErrback} returns),
        L{SynchronousTestCase.failureResultOf} returns returns a L{Failure}
        containing that exception.
        """
        self.assertEqual(
            self.failure.value,
            self.failureResultOf(self.raisesException()).value
        )


    def test_assertNoResultSuccess(self):
        """
        When passed a coroutine which currently has a success result (see
        L{test_withSuccessResult}), L{SynchronousTestCase.assertNoResult}
        raises L{SynchronousTestCase.failureException}.
        """
        self.assertRaises(
            self.failureException, self.assertNoResult, self.successResult()
        )


    def test_assertNoResultFailure(self):
        """
        When passed a coroutine which currently has an exception result (see
        L{test_withFailureResult}), L{SynchronousTestCase.assertNoResult}
        raises L{SynchronousTestCase.failureException}.
        """
        self.assertRaises(
            self.failureException, self.assertNoResult, self.raisesException()
        )


    def test_assertNoResult(self):
        """
        When passed a coroutine with no current result,
        L{SynchronousTestCase.assertNoResult} does not raise an exception.
        """
        self.assertNoResult(self.noCurrentResult())


    def test_assertNoResultPropagatesSuccess(self):
        """
        When passed a coroutine awaiting a L{Deferred} with no current result,
        which is then fired with a success result,
        L{SynchronousTestCase.assertNoResult} doesn't modify the result of the
        L{Deferred}.
        """
        d = Deferred()

        async def noCurrentResult():
            return await d

        c = noCurrentResult()
        self.assertNoResult(d)
        d.callback(self.result)
        self.assertEqual(self.result, self.successResultOf(c))


    def test_assertNoResultPropagatesLaterFailure(self):
        """
        When passed a coroutine awaiting a L{Deferred} with no current result,
        which is then fired with a L{Failure} result,
        L{SynchronousTestCase.assertNoResult} doesn't modify the result of the
        L{Deferred}.
        """
        f = Failure(self.exception)
        d = Deferred()

        async def noCurrentResult():
            return await d

        c = noCurrentResult()
        self.assertNoResult(d)
        d.errback(f)
        self.assertEqual(f.value, self.failureResultOf(c).value)


    def test_assertNoResultSwallowsImmediateFailure(self):
        """
        When passed a L{Deferred} which currently has a L{Failure} result,
        L{SynchronousTestCase.assertNoResult} changes the result of the
        L{Deferred} to a success.
        """
        d = fail(self.failure)

        async def raisesException():
            return await d

        c = raisesException()
        try:
            self.assertNoResult(d)
        except self.failureException:
            pass
        self.assertEqual(None, self.successResultOf(c))
