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

"""
Tests for Trial's interaction with the Python warning system.
"""

import sys, warnings
from StringIO import StringIO

from twisted.python.filepath import FilePath
from twisted.trial.unittest import TestCase, _collectWarnings
from twisted.trial.reporter import TestResult

class Mask(object):
    """
    Hide a L{TestCase} definition from trial's automatic discovery mechanism.
    """
    class MockTests(TestCase):
        """
        A test case which is used by L{FlushWarningsTests} to verify behavior
        which cannot be verified by code inside a single test method.
        """
        message = "some warning text"
        category = UserWarning

        def test_unflushed(self):
            """
            Generate a warning and don't flush it.
            """
            warnings.warn(self.message, self.category)


        def test_flushed(self):
            """
            Generate a warning and flush it.
            """
            warnings.warn(self.message, self.category)
            self.assertEqual(len(self.flushWarnings()), 1)



class FlushWarningsTests(TestCase):
    """
    Tests for L{TestCase.flushWarnings}, an API for examining the warnings
    emitted so far in a test.
    """

    def assertDictSubset(self, set, subset):
        """
        Assert that all the keys present in C{subset} are also present in
        C{set} and that the corresponding values are equal.
        """
        for k, v in subset.iteritems():
            self.assertEqual(set[k], v)


    def assertDictSubsets(self, sets, subsets):
        """
        For each pair of corresponding elements in C{sets} and C{subsets},
        assert that the element from C{subsets} is a subset of the element from
        C{sets}.
        """
        self.assertEqual(len(sets), len(subsets))
        for a, b in zip(sets, subsets):
            self.assertDictSubset(a, b)


    def test_none(self):
        """
        If no warnings are emitted by a test, L{TestCase.flushWarnings} returns
        an empty list.
        """
        self.assertEqual(self.flushWarnings(), [])


    def test_several(self):
        """
        If several warnings are emitted by a test, L{TestCase.flushWarnings}
        returns a list containing all of them.
        """
        firstMessage = "first warning message"
        firstCategory = UserWarning
        warnings.warn(message=firstMessage, category=firstCategory)

        secondMessage = "second warning message"
        secondCategory = RuntimeWarning
        warnings.warn(message=secondMessage, category=secondCategory)

        self.assertDictSubsets(
            self.flushWarnings(),
            [{'category': firstCategory, 'message': firstMessage},
             {'category': secondCategory, 'message': secondMessage}])


    def test_repeated(self):
        """
        The same warning triggered twice from the same place is included twice
        in the list returned by L{TestCase.flushWarnings}.
        """
        message = "the message"
        category = RuntimeWarning
        for i in range(2):
            warnings.warn(message=message, category=category)

        self.assertDictSubsets(
            self.flushWarnings(),
            [{'category': category, 'message': message}] * 2)


    def test_cleared(self):
        """
        After a particular warning event has been returned by
        L{TestCase.flushWarnings}, it is not returned by subsequent calls.
        """
        message = "the message"
        category = RuntimeWarning
        warnings.warn(message=message, category=category)
        self.assertDictSubsets(
            self.flushWarnings(),
            [{'category': category, 'message': message}])
        self.assertEqual(self.flushWarnings(), [])


    def test_unflushed(self):
        """
        Any warnings emitted by a test which are not flushed are emitted to the
        Python warning system.
        """
        result = TestResult()
        case = Mask.MockTests('test_unflushed')
        case.run(result)
        warningsShown = self.flushWarnings([Mask.MockTests.test_unflushed])
        self.assertEqual(warningsShown[0]['message'], 'some warning text')
        self.assertIdentical(warningsShown[0]['category'], UserWarning)

        where = case.test_unflushed.im_func.func_code
        filename = where.co_filename
        # If someone edits MockTests.test_unflushed, the value added to
        # firstlineno might need to change.
        lineno = where.co_firstlineno + 4

        self.assertEqual(warningsShown[0]['filename'], filename)
        self.assertEqual(warningsShown[0]['lineno'], lineno)

        self.assertEqual(len(warningsShown), 1)


    def test_flushed(self):
        """
        Any warnings emitted by a test which are flushed are not emitted to the
        Python warning system.
        """
        result = TestResult()
        case = Mask.MockTests('test_flushed')
        output = StringIO()
        monkey = self.patch(sys, 'stdout', output)
        case.run(result)
        monkey.restore()
        self.assertEqual(output.getvalue(), "")


    def test_warningsConfiguredAsErrors(self):
        """
        If a warnings filter has been installed which turns warnings into
        exceptions, tests have an error added to the reporter for them for each
        unflushed warning.
        """
        class CustomWarning(Warning):
            pass

        result = TestResult()
        case = Mask.MockTests('test_unflushed')
        case.category = CustomWarning

        originalWarnings = warnings.filters[:]
        try:
            warnings.simplefilter('error')
            case.run(result)
            self.assertEqual(len(result.errors), 1)
            self.assertIdentical(result.errors[0][0], case)
            result.errors[0][1].trap(CustomWarning)
        finally:
            warnings.filters[:] = originalWarnings


    def test_flushedWarningsConfiguredAsErrors(self):
        """
        If a warnings filter has been installed which turns warnings into
        exceptions, tests which emit those warnings but flush them do not have
        an error added to the reporter.
        """
        class CustomWarning(Warning):
            pass

        result = TestResult()
        case = Mask.MockTests('test_flushed')
        case.category = CustomWarning

        originalWarnings = warnings.filters[:]
        try:
            warnings.simplefilter('error')
            case.run(result)
            self.assertEqual(result.errors, [])
        finally:
            warnings.filters[:] = originalWarnings


    def test_multipleFlushes(self):
        """
        Any warnings emitted after a call to L{TestCase.flushWarnings} can be
        flushed by another call to L{TestCase.flushWarnings}.
        """
        warnings.warn("first message")
        self.assertEqual(len(self.flushWarnings()), 1)
        warnings.warn("second message")
        self.assertEqual(len(self.flushWarnings()), 1)


    def test_filterOnOffendingFunction(self):
        """
        The list returned by L{TestCase.flushWarnings} includes only those
        warnings which refer to the source of the function passed as the value
        for C{offendingFunction}, if a value is passed for that parameter.
        """
        firstMessage = "first warning text"
        firstCategory = UserWarning
        def one():
            warnings.warn(firstMessage, firstCategory, stacklevel=1)

        secondMessage = "some text"
        secondCategory = RuntimeWarning
        def two():
            warnings.warn(secondMessage, secondCategory, stacklevel=1)

        one()
        two()

        self.assertDictSubsets(
            self.flushWarnings(offendingFunctions=[one]),
            [{'category': firstCategory, 'message': firstMessage}])
        self.assertDictSubsets(
            self.flushWarnings(offendingFunctions=[two]),
            [{'category': secondCategory, 'message': secondMessage}])


    def test_invalidFilter(self):
        """
        If an object which is neither a function nor a method is included in
        the C{offendingFunctions} list, L{TestCase.flushWarnings} raises
        L{ValueError}.  Such a call flushes no warnings.
        """
        warnings.warn("oh no")
        self.assertRaises(ValueError, self.flushWarnings, [None])
        self.assertEqual(len(self.flushWarnings()), 1)


    def test_missingSource(self):
        """
        If a function the source of which is not available is including in the
        C{offendingFunctions} list, L{TestCase.flushWarnings} raises
        L{IOError}.  Such a call flushes no warnings.
        """
        package = FilePath(self.mktemp()).child('twisted_private_helper')
        package.makedirs()
        package.child('__init__.py').setContent('')
        package.child('missingsourcefile.py').setContent('''
import warnings
def foo():
    warnings.warn("oh no")
''')
        sys.path.insert(0, package.parent().path)
        self.addCleanup(sys.path.remove, package.parent().path)
        from twisted_private_helper import missingsourcefile
        self.addCleanup(sys.modules.pop, 'twisted_private_helper')
        self.addCleanup(sys.modules.pop, missingsourcefile.__name__)
        package.child('missingsourcefile.py').remove()

        missingsourcefile.foo()
        self.assertRaises(
            IOError, self.flushWarnings, [missingsourcefile.foo])
        self.assertEqual(len(self.flushWarnings()), 1)



class FakeWarning(Warning):
    pass



class CollectWarningsTests(TestCase):
    """
    Tests for L{_collectWarnings}.
    """
    def test_callsObserver(self):
        """
        L{_collectWarnings} calls the observer with each emitted warning.
        """
        firstMessage = "dummy calls observer warning"
        secondMessage = firstMessage[::-1]
        events = []
        def f():
            events.append('call')
            warnings.warn(firstMessage)
            warnings.warn(secondMessage)
            events.append('returning')

        _collectWarnings(events.append, f)

        self.assertEqual(events[0], 'call')
        self.assertEqual(events[1].message, firstMessage)
        self.assertEqual(events[2].message, secondMessage)
        self.assertEqual(events[3], 'returning')
        self.assertEqual(len(events), 4)


    def test_suppresses(self):
        """
        Any warnings emitted by a call to a function passed to
        L{_collectWarnings} are not actually emitted to the warning system.
        """
        output = StringIO()
        self.patch(sys, 'stdout', output)
        _collectWarnings(lambda x: None, warnings.warn, "text")
        self.assertEqual(output.getvalue(), "")


    def test_callsFunction(self):
        """
        L{_collectWarnings} returns the result of calling the callable passed to
        it with the parameters given.
        """
        arguments = []
        value = object()

        def f(*args, **kwargs):
            arguments.append((args, kwargs))
            return value

        result = _collectWarnings(lambda x: None, f, 1, 'a', b=2, c='d')
        self.assertEqual(arguments, [((1, 'a'), {'b': 2, 'c': 'd'})])
        self.assertIdentical(result, value)


    def test_duplicateWarningCollected(self):
        """
        Subsequent emissions of a warning from a particular source site can be
        collected by L{_collectWarnings}.  In particular, the per-module
        emitted-warning cache should be bypassed (I{__warningregistry__}).
        """
        # Make sure the worst case is tested: if __warningregistry__ isn't in a
        # module's globals, then the warning system will add it and start using
        # it to avoid emitting duplicate warnings.  Delete __warningregistry__
        # to ensure that even modules which are first imported as a test is
        # running still interact properly with the warning system.
        global __warningregistry__
        del __warningregistry__

        def f():
            warnings.warn("foo")
        warnings.simplefilter('default')
        f()
        events = []
        _collectWarnings(events.append, f)
        self.assertEqual(len(events), 1)
        self.assertEqual(events[0].message, "foo")
        self.assertEqual(len(self.flushWarnings()), 1)


    def test_immutableObject(self):
        """
        L{_collectWarnings}'s behavior is not altered by the presence of an
        object which cannot have attributes set on it as a value in
        C{sys.modules}.
        """
        key = object()
        sys.modules[key] = key
        self.addCleanup(sys.modules.pop, key)
        self.test_duplicateWarningCollected()
