#
#  subunit: extensions to Python unittest to get test results from subprocesses.
#  Copyright (C) 2009  Robert Collins <robertc@robertcollins.net>
#
#  Licensed under either the Apache License, Version 2.0 or the BSD 3-clause
#  license at the users choice. A copy of both licenses are available in the
#  project source as Apache-2.0 and BSD. You may not use this file except in
#  compliance with one of these two licences.
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under these licenses is distributed on an "AS IS" BASIS, WITHOUT
#  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
#  license you chose for the specific language governing permissions and
#  limitations under that license.
#

import csv
import datetime
import sys
import unittest
from io import StringIO

import testtools
from testtools import TestCase
from testtools.content import TracebackContent, text_content
from testtools.testresult.doubles import ExtendedTestResult

import subunit
import iso8601
import subunit.test_results


class LoggingDecorator(subunit.test_results.HookedTestResultDecorator):
    def __init__(self, decorated):
        self._calls = 0
        super().__init__(decorated)

    def _before_event(self):
        self._calls += 1


class AssertBeforeTestResult(LoggingDecorator):
    """A TestResult for checking preconditions."""

    def __init__(self, decorated, test):
        self.test = test
        super().__init__(decorated)

    def _before_event(self):
        self.test.assertEqual(1, self.earlier._calls)
        super()._before_event()


class TimeCapturingResult(unittest.TestResult):
    def __init__(self):
        super().__init__()
        self._calls = []
        self.failfast = False

    def time(self, a_datetime):
        self._calls.append(a_datetime)

    def progress(self, offset, whence):
        pass

    def addDuration(self, test, duration):
        """Called to add a test duration.

        :param test: The test that completed.
        :param duration: The duration of the test as a float in seconds.
        """
        pass


class TestHookedTestResultDecorator(unittest.TestCase):
    def setUp(self):
        # An end to the chain
        terminal = unittest.TestResult()
        # Asserts that the call was made to self.result before asserter was
        # called.
        asserter = AssertBeforeTestResult(terminal, self)
        # The result object we call, which much increase its call count.
        self.result = LoggingDecorator(asserter)
        asserter.earlier = self.result
        self.decorated = asserter

    def tearDown(self):
        # The hook in self.result must have been called
        self.assertEqual(1, self.result._calls)
        # The hook in asserter must have been called too, otherwise the
        # assertion about ordering won't have completed.
        self.assertEqual(1, self.decorated._calls)

    def test_startTest(self):
        self.result.startTest(self)

    def test_startTestRun(self):
        self.result.startTestRun()

    def test_stopTest(self):
        self.result.stopTest(self)

    def test_stopTestRun(self):
        self.result.stopTestRun()

    def test_addError(self):
        self.result.addError(self, subunit.RemoteError())

    def test_addError_details(self):
        self.result.addError(self, details={})

    def test_addFailure(self):
        self.result.addFailure(self, subunit.RemoteError())

    def test_addFailure_details(self):
        self.result.addFailure(self, details={})

    def test_addSuccess(self):
        self.result.addSuccess(self)

    def test_addSuccess_details(self):
        self.result.addSuccess(self, details={})

    def test_addSkip(self):
        self.result.addSkip(self, "foo")

    def test_addSkip_details(self):
        self.result.addSkip(self, details={})

    def test_addExpectedFailure(self):
        self.result.addExpectedFailure(self, subunit.RemoteError())

    def test_addExpectedFailure_details(self):
        self.result.addExpectedFailure(self, details={})

    def test_addUnexpectedSuccess(self):
        self.result.addUnexpectedSuccess(self)

    def test_addUnexpectedSuccess_details(self):
        self.result.addUnexpectedSuccess(self, details={})

    def test_progress(self):
        self.result.progress(1, subunit.PROGRESS_SET)

    def test_wasSuccessful(self):
        self.result.wasSuccessful()

    def test_shouldStop(self):
        self.result.shouldStop

    def test_stop(self):
        self.result.stop()

    def test_time(self):
        self.result.time(None)

    def test_addDuration(self):
        self.result.addDuration(self, 1.5)


class TestAutoTimingTestResultDecorator(unittest.TestCase):
    def setUp(self):
        # And end to the chain which captures time events.
        terminal = TimeCapturingResult()
        # The result object under test.
        self.result = subunit.test_results.AutoTimingTestResultDecorator(terminal)
        self.decorated = terminal

    def test_without_time_calls_time_is_called_and_not_None(self):
        self.result.startTest(self)
        self.assertEqual(1, len(self.decorated._calls))
        self.assertNotEqual(None, self.decorated._calls[0])

    def test_no_time_from_progress(self):
        self.result.progress(1, subunit.PROGRESS_CUR)
        self.assertEqual(0, len(self.decorated._calls))

    def test_no_time_from_shouldStop(self):
        self.decorated.stop()
        self.result.shouldStop
        self.assertEqual(0, len(self.decorated._calls))

    def test_calling_time_inhibits_automatic_time(self):
        # Calling time() outputs a time signal immediately and prevents
        # automatically adding one when other methods are called.
        time = datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC)
        self.result.time(time)
        self.result.startTest(self)
        self.result.stopTest(self)
        self.assertEqual(1, len(self.decorated._calls))
        self.assertEqual(time, self.decorated._calls[0])

    def test_calling_time_None_enables_automatic_time(self):
        time = datetime.datetime(2009, 10, 11, 12, 13, 14, 15, iso8601.UTC)
        self.result.time(time)
        self.assertEqual(1, len(self.decorated._calls))
        self.assertEqual(time, self.decorated._calls[0])
        # Calling None passes the None through, in case other results care.
        self.result.time(None)
        self.assertEqual(2, len(self.decorated._calls))
        self.assertEqual(None, self.decorated._calls[1])
        # Calling other methods doesn't generate an automatic time event.
        self.result.startTest(self)
        self.assertEqual(3, len(self.decorated._calls))
        self.assertNotEqual(None, self.decorated._calls[2])

    def test_set_failfast_True(self):
        self.assertFalse(self.decorated.failfast)
        self.result.failfast = True
        self.assertTrue(self.decorated.failfast)


class TestTagCollapsingDecorator(TestCase):
    def test_tags_collapsed_outside_of_tests(self):
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
        tag_collapser.tags({"a"}, set())
        tag_collapser.tags({"b"}, set())
        tag_collapser.startTest(self)
        self.assertEqual(
            [
                ("tags", {"a", "b"}, set()),
                ("startTest", self),
            ],
            result._events,
        )

    def test_tags_collapsed_outside_of_tests_are_flushed(self):
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
        tag_collapser.startTestRun()
        tag_collapser.tags({"a"}, set())
        tag_collapser.tags({"b"}, set())
        tag_collapser.startTest(self)
        tag_collapser.addSuccess(self)
        tag_collapser.stopTest(self)
        tag_collapser.stopTestRun()
        self.assertEqual(
            [
                ("startTestRun",),
                ("tags", {"a", "b"}, set()),
                ("startTest", self),
                ("addSuccess", self),
                ("stopTest", self),
                ("stopTestRun",),
            ],
            result._events,
        )

    def test_tags_forwarded_after_tests(self):
        test = subunit.RemotedTestCase("foo")
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
        tag_collapser.startTestRun()
        tag_collapser.startTest(test)
        tag_collapser.addSuccess(test)
        tag_collapser.stopTest(test)
        tag_collapser.tags({"a"}, {"b"})
        tag_collapser.stopTestRun()
        self.assertEqual(
            [
                ("startTestRun",),
                ("startTest", test),
                ("addSuccess", test),
                ("stopTest", test),
                ("tags", {"a"}, {"b"}),
                ("stopTestRun",),
            ],
            result._events,
        )

    def test_tags_collapsed_inside_of_tests(self):
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
        test = subunit.RemotedTestCase("foo")
        tag_collapser.startTest(test)
        tag_collapser.tags({"a"}, set())
        tag_collapser.tags({"b"}, {"a"})
        tag_collapser.tags({"c"}, set())
        tag_collapser.stopTest(test)
        self.assertEqual([("startTest", test), ("tags", {"b", "c"}, {"a"}), ("stopTest", test)], result._events)

    def test_tags_collapsed_inside_of_tests_different_ordering(self):
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
        test = subunit.RemotedTestCase("foo")
        tag_collapser.startTest(test)
        tag_collapser.tags(set(), {"a"})
        tag_collapser.tags({"a", "b"}, set())
        tag_collapser.tags({"c"}, set())
        tag_collapser.stopTest(test)
        self.assertEqual([("startTest", test), ("tags", {"a", "b", "c"}, set()), ("stopTest", test)], result._events)

    def test_tags_sent_before_result(self):
        # Because addSuccess and friends tend to send subunit output
        # immediately, and because 'tags:' before a result line means
        # something different to 'tags:' after a result line, we need to be
        # sure that tags are emitted before 'addSuccess' (or whatever).
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TagCollapsingDecorator(result)
        test = subunit.RemotedTestCase("foo")
        tag_collapser.startTest(test)
        tag_collapser.tags({"a"}, set())
        tag_collapser.addSuccess(test)
        tag_collapser.stopTest(test)
        self.assertEqual(
            [("startTest", test), ("tags", {"a"}, set()), ("addSuccess", test), ("stopTest", test)], result._events
        )


class TestTimeCollapsingDecorator(TestCase):
    def make_time(self):
        # Heh heh.
        return datetime.datetime(2000, 1, self.getUniqueInteger(), tzinfo=iso8601.UTC)

    def test_initial_time_forwarded(self):
        # We always forward the first time event we see.
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
        a_time = self.make_time()
        tag_collapser.time(a_time)
        self.assertEqual([("time", a_time)], result._events)

    def test_time_collapsed_to_first_and_last(self):
        # If there are many consecutive time events, only the first and last
        # are sent through.
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
        times = [self.make_time() for i in range(5)]
        for a_time in times:
            tag_collapser.time(a_time)
        tag_collapser.startTest(subunit.RemotedTestCase("foo"))
        self.assertEqual([("time", times[0]), ("time", times[-1])], result._events[:-1])

    def test_only_one_time_sent(self):
        # If we receive a single time event followed by a non-time event, we
        # send exactly one time event.
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
        a_time = self.make_time()
        tag_collapser.time(a_time)
        tag_collapser.startTest(subunit.RemotedTestCase("foo"))
        self.assertEqual([("time", a_time)], result._events[:-1])

    def test_duplicate_times_not_sent(self):
        # Many time events with the exact same time are collapsed into one
        # time event.
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
        a_time = self.make_time()
        for i in range(5):
            tag_collapser.time(a_time)
        tag_collapser.startTest(subunit.RemotedTestCase("foo"))
        self.assertEqual([("time", a_time)], result._events[:-1])

    def test_no_times_inserted(self):
        result = ExtendedTestResult()
        tag_collapser = subunit.test_results.TimeCollapsingDecorator(result)
        a_time = self.make_time()
        tag_collapser.time(a_time)
        foo = subunit.RemotedTestCase("foo")
        tag_collapser.startTest(foo)
        tag_collapser.addSuccess(foo)
        tag_collapser.stopTest(foo)
        self.assertEqual([("time", a_time), ("startTest", foo), ("addSuccess", foo), ("stopTest", foo)], result._events)


class TestByTestResultTests(testtools.TestCase):
    def setUp(self):
        super().setUp()
        self.log = []
        self.result = subunit.test_results.TestByTestResult(self.on_test)
        self.result._now = iter(range(5)).__next__

    def assertCalled(self, **kwargs):
        defaults = {
            "test": self,
            "tags": set(),
            "details": None,
            "start_time": 0,
            "stop_time": 1,
        }
        defaults.update(kwargs)
        self.assertEqual([defaults], self.log)

    def on_test(self, **kwargs):
        self.log.append(kwargs)

    def test_no_tests_nothing_reported(self):
        self.result.startTestRun()
        self.result.stopTestRun()
        self.assertEqual([], self.log)

    def test_add_success(self):
        self.result.startTest(self)
        self.result.addSuccess(self)
        self.result.stopTest(self)
        self.assertCalled(status="success")

    def test_add_success_details(self):
        self.result.startTest(self)
        details = {"foo": "bar"}
        self.result.addSuccess(self, details=details)
        self.result.stopTest(self)
        self.assertCalled(status="success", details=details)

    def test_tags(self):
        if not getattr(self.result, "tags", None):
            self.skipTest("No tags in testtools")
        self.result.tags(["foo"], [])
        self.result.startTest(self)
        self.result.addSuccess(self)
        self.result.stopTest(self)
        self.assertCalled(status="success", tags={"foo"})

    def test_add_error(self):
        self.result.startTest(self)
        try:
            1 / 0
        except ZeroDivisionError:
            error = sys.exc_info()
        self.result.addError(self, error)
        self.result.stopTest(self)
        self.assertCalled(status="error", details={"traceback": TracebackContent(error, self)})

    def test_add_error_details(self):
        self.result.startTest(self)
        details = {"foo": text_content("bar")}
        self.result.addError(self, details=details)
        self.result.stopTest(self)
        self.assertCalled(status="error", details=details)

    def test_add_failure(self):
        self.result.startTest(self)
        try:
            self.fail("intentional failure")
        except self.failureException:
            failure = sys.exc_info()
        self.result.addFailure(self, failure)
        self.result.stopTest(self)
        self.assertCalled(status="failure", details={"traceback": TracebackContent(failure, self)})

    def test_add_failure_details(self):
        self.result.startTest(self)
        details = {"foo": text_content("bar")}
        self.result.addFailure(self, details=details)
        self.result.stopTest(self)
        self.assertCalled(status="failure", details=details)

    def test_add_xfail(self):
        self.result.startTest(self)
        try:
            1 / 0
        except ZeroDivisionError:
            error = sys.exc_info()
        self.result.addExpectedFailure(self, error)
        self.result.stopTest(self)
        self.assertCalled(status="xfail", details={"traceback": TracebackContent(error, self)})

    def test_add_xfail_details(self):
        self.result.startTest(self)
        details = {"foo": text_content("bar")}
        self.result.addExpectedFailure(self, details=details)
        self.result.stopTest(self)
        self.assertCalled(status="xfail", details=details)

    def test_add_unexpected_success(self):
        self.result.startTest(self)
        details = {"foo": "bar"}
        self.result.addUnexpectedSuccess(self, details=details)
        self.result.stopTest(self)
        self.assertCalled(status="success", details=details)

    def test_add_skip_reason(self):
        self.result.startTest(self)
        reason = self.getUniqueString()
        self.result.addSkip(self, reason)
        self.result.stopTest(self)
        self.assertCalled(status="skip", details={"reason": text_content(reason)})

    def test_add_skip_details(self):
        self.result.startTest(self)
        details = {"foo": "bar"}
        self.result.addSkip(self, details=details)
        self.result.stopTest(self)
        self.assertCalled(status="skip", details=details)

    def test_twice(self):
        self.result.startTest(self)
        self.result.addSuccess(self, details={"foo": "bar"})
        self.result.stopTest(self)
        self.result.startTest(self)
        self.result.addSuccess(self)
        self.result.stopTest(self)
        self.assertEqual(
            [
                {
                    "test": self,
                    "status": "success",
                    "start_time": 0,
                    "stop_time": 1,
                    "tags": set(),
                    "details": {"foo": "bar"},
                },
                {"test": self, "status": "success", "start_time": 2, "stop_time": 3, "tags": set(), "details": None},
            ],
            self.log,
        )


class TestCsvResult(testtools.TestCase):
    def parse_stream(self, stream):
        stream.seek(0)
        reader = csv.reader(stream)
        return list(reader)

    def test_csv_output(self):
        stream = StringIO()
        result = subunit.test_results.CsvResult(stream)
        result._now = iter(range(5)).__next__
        result.startTestRun()
        result.startTest(self)
        result.addSuccess(self)
        result.stopTest(self)
        result.stopTestRun()
        self.assertEqual(
            [
                ["test", "status", "start_time", "stop_time"],
                [self.id(), "success", "0", "1"],
            ],
            self.parse_stream(stream),
        )

    def test_just_header_when_no_tests(self):
        stream = StringIO()
        result = subunit.test_results.CsvResult(stream)
        result.startTestRun()
        result.stopTestRun()
        self.assertEqual([["test", "status", "start_time", "stop_time"]], self.parse_stream(stream))

    def test_no_output_before_events(self):
        stream = StringIO()
        subunit.test_results.CsvResult(stream)
        self.assertEqual([], self.parse_stream(stream))


class TestTestIdPrintingResult(testtools.TestCase):
    def setUp(self):
        super().setUp()
        self.stream = StringIO()
        self.result = subunit.test_results.TestIdPrintingResult(self.stream)

    def test_addDuration(self):
        # addDuration should not raise an exception
        test = subunit.RemotedTestCase("foo")
        self.result.addDuration(test, 2.5)
        # TestIdPrintingResult doesn't output anything for addDuration
        self.assertEqual("", self.stream.getvalue())
