# -*- coding: utf-8 -*-
# Moovida - Home multimedia server
# Copyright (C) 2006-2009 Fluendo Embedded S.L. (www.fluendo.com).
# All rights reserved.
#
# This file is available under one of two license agreements.
#
# This file is licensed under the GPL version 3.
# See "LICENSE.GPL" in the root of this distribution including a special
# exception to use Moovida with Fluendo's plugins.
#
# The GPL part of Moovida is also available under a commercial licensing
# agreement from Fluendo.
# See "LICENSE.Moovida" in the root directory of this distribution package
# for details on that license.


from twisted.trial import unittest
from twisted.internet import defer, task, reactor
from twisted.python import failure

from elisa.core.utils.cancellable_defer import CancellableDeferred, \
        CancelledError, cancellable_deferred_iterator, cancellable_coiterate

class FooError(Exception):
    pass
 
class DeferredCancellerTest(unittest.TestCase):
    def setUp(self):
        self.callback_results = None
        self.errback_results = None
        self.callback2_results = None
        self.cancellerCalled = False
        
    def _callback(self, data):
        self.callback_results = data
        return args[0]

    def _callback2(self, data):
        self.callback2_results = data

    def _errback(self, data):
        self.errback_results = data

    
    def testNoCanceller(self):
        """
        CancellableDeferred without a canceller errbacks
        """
        d=CancellableDeferred()
        d.addCallbacks(self._callback, self._errback)
        d.cancel()
        self.assertEquals(self.errback_results.type, CancelledError)

        # Test that further callbacks *are* swallowed
        d.callback(None)

        # But that a second is not
        self.assertRaises(defer.AlreadyCalledError, d.callback, None)
        
    def testCanceller(self):
        """
        test that the canceller gets called
        """
        def cancel(d):
            self.cancellerCalled=True
            
        d=CancellableDeferred(canceller=cancel)
        d.addCallbacks(self._callback, self._errback)
        d.cancel()
        self.assertEquals(self.cancellerCalled, True)
        self.assertEquals(self.errback_results.type, CancelledError)

        # Test that further callbacks are *not* swallowed
        self.assertRaises(defer.AlreadyCalledError, d.callback, None)
        
    def testCancellerWithCallback(self):
        """
        If we explicitly callback from the canceller, don't callback CancelledError
        """
        def cancel(d):
            self.cancellerCalled=True
            d.errback(FooError())
        d=CancellableDeferred(canceller=cancel)
        d.addCallbacks(self._callback, self._errback)
        d.cancel()
        self.assertEquals(self.cancellerCalled, True)
        self.assertEquals(self.errback_results.type, FooError)

    def testCancelAlreadyCalled(self):
        """
        test what happens when calling cancel on a deferred that already fired
        """
        def cancel(d):
            self.cancellerCalled=True
        d=CancellableDeferred(canceller=cancel)
        d.callback(None)
        self.assertRaises(defer.AlreadyCalledError, d.cancel)
        self.assertEquals(self.cancellerCalled, False)
    
    def testCancelNestedCancellableDeferred(self):
        """
        test whether the cancel get propagated to the nested deferred
        """
        def innerCancel(d):
            self.assertIdentical(d, innerCancellableDeferred)
            self.cancellerCalled=True
        def cancel(d):
            self.assert_(False)
            
        innerCancellableDeferred=CancellableDeferred(canceller=innerCancel)
        d=CancellableDeferred(canceller=cancel)
        d.callback(None)
        d.addCallback(lambda data: innerCancellableDeferred)
        d.cancel()
        d.addCallbacks(self._callback, self._errback)
        self.assertEquals(self.cancellerCalled, True)
        self.assertEquals(self.errback_results.type, CancelledError)


class TestCancellableDeferredIterator(unittest.TestCase):
    """
    Test whether the cancellable deferred iterator works as expected
    """

    def test_cancel(self):
        """
        test whether the cancel call on the iterator deferred get propagated
        to the current deferred and that the correct failure gets fired
        """
        res = []

        def callback(result, i):
            res.append(result)

            return result

        def run_callback(dfr, i):
            if i == 3:
                self.failUnless(dfr.called)
                return

            dfr.callback('success')

        @cancellable_deferred_iterator
        def iterator():
            for i in xrange(10):
                dfr = CancellableDeferred(canceller=lambda dfr: None)
                dfr.addCallback(callback, i)

                if i == 3:
                    reactor.callLater(0, self.cancellable_iterator.cancel)
                
                reactor.callLater(0, run_callback, dfr, i)

                yield dfr

        def iterator_callback(result):
            # check that the iterator didn't go on
            self.failUnlessEqual(len(res), 3)

            self.failUnless(isinstance(result, failure.Failure))
            # check that the failure propagated
            result.trap(CancelledError)

        self.cancellable_iterator = iterator()
        dfr = task.coiterate(iter(self.cancellable_iterator))
        dfr.addBoth(iterator_callback)

        return dfr

    def test_exception(self):
        """
        test whether the raised exceptions gets propagated and the iteration
        stopped
        """
        class MyException(Exception):
            pass

        res = []

        def called(result, i):
            res.append(result)

            return result

        def run_callback(dfr, i):
            if i == 3:
                self.failUnless(dfr.called)
                return

            dfr.callback('success')

        @cancellable_deferred_iterator
        def iterator():
            for i in xrange(10):
                dfr = CancellableDeferred(canceller=lambda dfr: None)
                dfr.addBoth(called, i)

                if i == 3:
                    raise MyException('die')
                
                reactor.callLater(0, run_callback, dfr, i)

                yield dfr

        def iterator_callback(result):
            # check that the iterator didn't go on
            self.failUnlessEqual(len(res), 3)
            result.trap(MyException)

        self.cancellable_iterator = iterator()
        dfr = task.coiterate(iter(self.cancellable_iterator))
        dfr.addBoth(iterator_callback)

        return dfr


    def test_iteration(self):
        """
        test whether the iterator works under normal conditions
        """

        res = []

        def called(result, i):
            res.append(result)
            return result

        def run_callback(dfr, i):
            dfr.callback('success')

        @cancellable_deferred_iterator
        def iterator():
            for i in xrange(10):
                dfr = CancellableDeferred(canceller=lambda dfr: None)
                dfr.addBoth(called, i)

                reactor.callLater(0, run_callback, dfr, i)

                yield dfr

        def iterator_callback(result):
            # check that the iterator did go on
            self.failUnlessEqual(len(res), 10)

        self.cancellable_iterator = iterator()
        dfr = task.coiterate(iter(self.cancellable_iterator))
        dfr.addBoth(iterator_callback)

        return dfr

class TestCancellableCoiterator(unittest.TestCase):
    """
    these tests are similar to the L{TestCancellabelDeferredIterator} but with
    the difference that they check the cancellable_coiterator.
    """

    def test_cancel(self):
        """
        test whether the cancel call on the iterator deferred get propagated
        to the current deferred and that the correct failure gets fired
        """
        res = []

        def callback(result, i):
            res.append(result)

            return result

        def run_callback(dfr, i):
            if i == 3:
                self.failUnless(dfr.called)
                return

            dfr.callback('success')

        def iterator():
            for i in xrange(10):
                dfr = CancellableDeferred(canceller=lambda dfr: None)
                dfr.addCallback(callback, i)

                if i == 3:
                    reactor.callLater(0, self.dfr.cancel)
                
                reactor.callLater(0, run_callback, dfr, i)

                yield dfr

        def iterator_callback(result):
            # check that the iterator didn't go on
            self.failUnlessEqual(len(res), 3)

            self.failUnless(isinstance(result, failure.Failure))
            # check that the failure propagated
            result.trap(CancelledError)

        dfr = self.dfr = cancellable_coiterate(iterator)

        self.failUnless(isinstance(dfr, CancellableDeferred))

        dfr.addBoth(iterator_callback)
        return dfr

    def test_exception(self):
        """
        test whether the raised exceptions gets propagated and the iteration
        stopped
        """
        class MyException(Exception):
            pass

        res = []

        def called(result, i):
            res.append(result)

            return result

        def run_callback(dfr, i):
            if i == 3:
                self.failUnless(dfr.called)
                return

            dfr.callback('success')

        def iterator():
            for i in xrange(10):
                dfr = CancellableDeferred(canceller=lambda dfr: None)
                dfr.addBoth(called, i)

                if i == 3:
                    raise MyException('die')
                
                reactor.callLater(0, run_callback, dfr, i)

                yield dfr

        def iterator_callback(result):
            # check that the iterator didn't go on
            self.failUnlessEqual(len(res), 3)
            result.trap(MyException)

        
        dfr = cancellable_coiterate(iterator)
        self.failUnless(isinstance(dfr,CancellableDeferred))
        dfr.addBoth(iterator_callback)

        return dfr


    def test_iteration(self):
        """
        test whether the iterator works under normal conditions
        """

        res = []

        def called(result, i):
            res.append(result)
            return result

        def run_callback(dfr, i):
            dfr.callback('success')

        def iterator():
            for i in xrange(10):
                dfr = CancellableDeferred(canceller=lambda dfr: None)
                dfr.addBoth(called, i)

                reactor.callLater(0, run_callback, dfr, i)

                yield dfr

        def iterator_callback(result):
            # check that the iterator did go on
            self.failUnlessEqual(len(res), 10)

        dfr = cancellable_coiterate(iterator)
        self.failUnless(isinstance(dfr, CancellableDeferred))
        dfr.addBoth(iterator_callback)

        return dfr


