# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt

"""Test LCOV-based summary reporting for coverage.py."""

from __future__ import annotations

import math
import textwrap

import coverage

from tests.coveragetest import CoverageTest


class LcovTest(CoverageTest):
    """Tests of the LCOV reports from coverage.py."""

    def create_initial_files(self) -> None:
        """
        Helper for tests that handles the common ceremony so the tests can
        show the consequences of changes in the setup.
        """
        self.make_file("main_file.py", """\
            def cuboid_volume(l):
                return (l*l*l)

            def IsItTrue():
                return True
            """)

        self.make_file("test_file.py", """\
            from main_file import cuboid_volume
            import unittest

            class TestCuboid(unittest.TestCase):
                def test_volume(self):
                    self.assertAlmostEqual(cuboid_volume(2),8)
                    self.assertAlmostEqual(cuboid_volume(1),1)
                    self.assertAlmostEqual(cuboid_volume(0),0)
                    self.assertAlmostEqual(cuboid_volume(5.5),166.375)
            """)

    def get_lcov_report_content(self, filename: str = "coverage.lcov") -> str:
        """Return the content of an LCOV report."""
        with open(filename) as file:
            return file.read()

    def test_lone_file(self) -> None:
        # For a single file with a couple of functions, the lcov should cover
        # the function definitions themselves, but not the returns.
        self.make_file("main_file.py", """\
            def cuboid_volume(l):
                return (l*l*l)

            def IsItTrue():
                return True
            """)
        expected_result = textwrap.dedent("""\
            SF:main_file.py
            DA:1,1
            DA:2,0
            DA:4,1
            DA:5,0
            LF:4
            LH:2
            FN:1,2,cuboid_volume
            FNDA:0,cuboid_volume
            FN:4,5,IsItTrue
            FNDA:0,IsItTrue
            FNF:2
            FNH:0
            end_of_record
            """)
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(source=["."])
        self.start_import_stop(cov, "main_file")
        pct = cov.lcov_report()
        assert pct == 50.0
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_line_checksums(self) -> None:
        self.make_file("main_file.py", """\
            def cuboid_volume(l):
                return (l*l*l)

            def IsItTrue():
                return True
            """)
        self.make_file(".coveragerc", "[lcov]\nline_checksums = true\n")
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(source=["."])
        self.start_import_stop(cov, "main_file")
        pct = cov.lcov_report()
        assert pct == 50.0
        expected_result = textwrap.dedent("""\
            SF:main_file.py
            DA:1,1,7URou3io0zReBkk69lEb/Q
            DA:2,0,Xqj6H1iz/nsARMCAbE90ng
            DA:4,1,ilhb4KUfytxtEuClijZPlQ
            DA:5,0,LWILTcvARcydjFFyo9qM0A
            LF:4
            LH:2
            FN:1,2,cuboid_volume
            FNDA:0,cuboid_volume
            FN:4,5,IsItTrue
            FNDA:0,IsItTrue
            FNF:2
            FNH:0
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_simple_line_coverage_two_files(self) -> None:
        # Test that line coverage is created when coverage is run,
        # and matches the output of the file below.
        self.create_initial_files()
        self.assert_doesnt_exist(".coverage")
        self.make_file(".coveragerc", "[lcov]\noutput = data.lcov\n")
        cov = coverage.Coverage(source=".")
        self.start_import_stop(cov, "test_file")
        pct = cov.lcov_report()
        assert pct == 50.0
        self.assert_exists("data.lcov")
        expected_result = textwrap.dedent("""\
            SF:main_file.py
            DA:1,1
            DA:2,0
            DA:4,1
            DA:5,0
            LF:4
            LH:2
            FN:1,2,cuboid_volume
            FNDA:0,cuboid_volume
            FN:4,5,IsItTrue
            FNDA:0,IsItTrue
            FNF:2
            FNH:0
            end_of_record
            SF:test_file.py
            DA:1,1
            DA:2,1
            DA:4,1
            DA:5,1
            DA:6,0
            DA:7,0
            DA:8,0
            DA:9,0
            LF:8
            LH:4
            FN:5,9,TestCuboid.test_volume
            FNDA:0,TestCuboid.test_volume
            FNF:1
            FNH:0
            end_of_record
            """)
        actual_result = self.get_lcov_report_content(filename="data.lcov")
        assert expected_result == actual_result

    def test_branch_coverage_one_file(self) -> None:
        # Test that the reporter produces valid branch coverage.
        self.make_file("main_file.py", """\
            def is_it_x(x):
                if x == 3:
                    return x
                else:
                    return False
            """)
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(branch=True, source=".")
        self.start_import_stop(cov, "main_file")
        pct = cov.lcov_report()
        assert math.isclose(pct, 16.666666666666668)
        self.assert_exists("coverage.lcov")
        expected_result = textwrap.dedent("""\
            SF:main_file.py
            DA:1,1
            DA:2,0
            DA:3,0
            DA:5,0
            LF:4
            LH:1
            FN:1,5,is_it_x
            FNDA:0,is_it_x
            FNF:1
            FNH:0
            BRDA:2,0,jump to line 3,-
            BRDA:2,0,jump to line 5,-
            BRF:2
            BRH:0
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_branch_coverage_two_files(self) -> None:
        # Test that valid branch coverage is generated
        # in the case of two files.
        self.make_file("main_file.py", """\
            def is_it_x(x):
                if x == 3:
                    return x
                else:
                    return False
            """)

        self.make_file("test_file.py", """\
            from main_file import *
            import unittest

            class TestIsItX(unittest.TestCase):
                def test_is_it_x(self):
                    self.assertEqual(is_it_x(3), 3)
                    self.assertEqual(is_it_x(4), False)
            """)
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(branch=True, source=".")
        self.start_import_stop(cov, "test_file")
        pct = cov.lcov_report()
        assert math.isclose(pct, 41.666666666666664)
        self.assert_exists("coverage.lcov")
        expected_result = textwrap.dedent("""\
            SF:main_file.py
            DA:1,1
            DA:2,0
            DA:3,0
            DA:5,0
            LF:4
            LH:1
            FN:1,5,is_it_x
            FNDA:0,is_it_x
            FNF:1
            FNH:0
            BRDA:2,0,jump to line 3,-
            BRDA:2,0,jump to line 5,-
            BRF:2
            BRH:0
            end_of_record
            SF:test_file.py
            DA:1,1
            DA:2,1
            DA:4,1
            DA:5,1
            DA:6,0
            DA:7,0
            LF:6
            LH:4
            FN:5,7,TestIsItX.test_is_it_x
            FNDA:0,TestIsItX.test_is_it_x
            FNF:1
            FNH:0
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_half_covered_branch(self) -> None:
        # Test that for a given branch that is only half covered,
        # the block numbers remain the same, and produces valid lcov.
        self.make_file("main_file.py", """\
            something = True

            if something:
                print("Yes, something")
            else:
                print("No, nothing")
            """)
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(branch=True, source=".")
        self.start_import_stop(cov, "main_file")
        pct = cov.lcov_report()
        assert math.isclose(pct, 66.66666666666667)
        self.assert_exists("coverage.lcov")
        expected_result = textwrap.dedent("""\
            SF:main_file.py
            DA:1,1
            DA:3,1
            DA:4,1
            DA:6,0
            LF:4
            LH:3
            BRDA:3,0,jump to line 4,1
            BRDA:3,0,jump to line 6,0
            BRF:2
            BRH:1
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_empty_init_files(self) -> None:
        # Test that an empty __init__.py still generates a (vacuous)
        # coverage record.
        self.make_file("__init__.py", "")
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(branch=True, source=".")
        self.start_import_stop(cov, "__init__")
        pct = cov.lcov_report()
        assert pct == 0.0
        self.assert_exists("coverage.lcov")
        expected_result = textwrap.dedent("""\
            SF:__init__.py
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_empty_init_file_skipped(self) -> None:
        # Test that the lcov reporter honors skip_empty.  Because skip_empty
        # keys off the overall number of lines of code, the result in this
        # case will be the same regardless of the age of the Python interpreter.
        self.make_file("__init__.py", "")
        self.make_file(".coveragerc", "[report]\nskip_empty = True\n")
        self.assert_doesnt_exist(".coverage")
        cov = coverage.Coverage(branch=True, source=".")
        self.start_import_stop(cov, "__init__")
        pct = cov.lcov_report()
        assert pct == 0.0
        self.assert_exists("coverage.lcov")
        expected_result = ""
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_excluded_lines(self) -> None:
        self.make_file(".coveragerc", """\
            [report]
            exclude_lines = foo
            """)
        self.make_file("runme.py", """\
            s = "Hello 1"
            t = "foo is ignored 2"
            if s.upper() == "BYE 3":
                i_am_missing_4()
                foo_is_missing_5()
            print("Done 6")
            # foo 7
            # line 8
            """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "runme")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:runme.py
            DA:1,1
            DA:3,1
            DA:4,0
            DA:6,1
            LF:4
            LH:3
            BRDA:3,0,jump to line 4,0
            BRDA:3,0,jump to line 6,1
            BRF:2
            BRH:1
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_exit_branches(self) -> None:
        self.make_file("runme.py", """\
            def foo(a):
                if a:
                    print(f"{a!r} is truthy")
            foo(True)
            foo(False)
            foo([])
            foo([0])
        """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "runme")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:runme.py
            DA:1,1
            DA:2,1
            DA:3,1
            DA:4,1
            DA:5,1
            DA:6,1
            DA:7,1
            LF:7
            LH:7
            FN:1,3,foo
            FNDA:1,foo
            FNF:1
            FNH:1
            BRDA:2,0,jump to line 3,1
            BRDA:2,0,return from function 'foo',1
            BRF:2
            BRH:2
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_genexpr_exit_arcs_pruned_full_coverage(self) -> None:
        self.make_file("runme.py", """\
            def foo(a):
                if any(x > 0 for x in a):
                    print(f"{a!r} has positives")
            foo([])
            foo([0])
            foo([0,1])
            foo([0,-1])
        """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "runme")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:runme.py
            DA:1,1
            DA:2,1
            DA:3,1
            DA:4,1
            DA:5,1
            DA:6,1
            DA:7,1
            LF:7
            LH:7
            FN:1,3,foo
            FNDA:1,foo
            FNF:1
            FNH:1
            BRDA:2,0,jump to line 3,1
            BRDA:2,0,return from function 'foo',1
            BRF:2
            BRH:2
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_genexpr_exit_arcs_pruned_never_true(self) -> None:
        self.make_file("runme.py", """\
            def foo(a):
                if any(x > 0 for x in a):
                    print(f"{a!r} has positives")
            foo([])
            foo([0])
        """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "runme")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:runme.py
            DA:1,1
            DA:2,1
            DA:3,0
            DA:4,1
            DA:5,1
            LF:5
            LH:4
            FN:1,3,foo
            FNDA:1,foo
            FNF:1
            FNH:1
            BRDA:2,0,jump to line 3,0
            BRDA:2,0,return from function 'foo',1
            BRF:2
            BRH:1
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_genexpr_exit_arcs_pruned_always_true(self) -> None:
        self.make_file("runme.py", """\
            def foo(a):
                if any(x > 0 for x in a):
                    print(f"{a!r} has positives")
            foo([1])
            foo([1,2])
        """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "runme")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:runme.py
            DA:1,1
            DA:2,1
            DA:3,1
            DA:4,1
            DA:5,1
            LF:5
            LH:5
            FN:1,3,foo
            FNDA:1,foo
            FNF:1
            FNH:1
            BRDA:2,0,jump to line 3,1
            BRDA:2,0,return from function 'foo',0
            BRF:2
            BRH:1
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_genexpr_exit_arcs_pruned_not_reached(self) -> None:
        self.make_file("runme.py", """\
            def foo(a):
                if any(x > 0 for x in a):
                    print(f"{a!r} has positives")
        """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "runme")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:runme.py
            DA:1,1
            DA:2,0
            DA:3,0
            LF:3
            LH:1
            FN:1,3,foo
            FNDA:0,foo
            FNF:1
            FNH:0
            BRDA:2,0,jump to line 3,-
            BRDA:2,0,return from function 'foo',-
            BRF:2
            BRH:0
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_always_raise(self) -> None:
        self.make_file("always_raise.py", """\
            try:
                if not_defined:
                    print("Yes")
                else:
                    print("No")
            except Exception:
                pass
        """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "always_raise")
        cov.lcov_report()
        expected_result = textwrap.dedent("""\
            SF:always_raise.py
            DA:1,1
            DA:2,1
            DA:3,0
            DA:5,0
            DA:6,1
            DA:7,1
            LF:6
            LH:4
            BRDA:2,0,jump to line 3,-
            BRDA:2,0,jump to line 5,-
            BRF:2
            BRH:0
            end_of_record
            """)
        actual_result = self.get_lcov_report_content()
        assert expected_result == actual_result

    def test_multiline_conditions(self) -> None:
        self.make_file("multi.py", """\
            def fun(x):
                if (
                    x
                ):
                    print("got here")
            """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "multi")
        cov.lcov_report()
        lcov = self.get_lcov_report_content()
        assert "BRDA:2,0,return from function 'fun',-" in lcov

    def test_module_exit(self) -> None:
        self.make_file("modexit.py", """\
            #! /usr/bin/env python
            def foo():
                return bar(
                )
            if "x" == "y":  # line 5
                foo()
            """)
        cov = coverage.Coverage(source=".", branch=True)
        self.start_import_stop(cov, "modexit")
        cov.lcov_report()
        lcov = self.get_lcov_report_content()
        print(lcov)
        assert "BRDA:5,0,exit the module,1" in lcov
