diff --git a/CHANGES.rst b/CHANGES.rst index 600d21ee8..1fb83b5c6 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,6 +26,13 @@ Unreleased - Fix: nested matches of exclude patterns could exclude too much code, as reported in `issue 1779`_. This is now fixed. +- Changed: previously, coverage.py would consider a module docstring to be an + executable statement if it appeared after line 1 in the file, but not + executable if it was the first line. Now module docstrings are never counted + as executable statements. This can change coverage.py's count of the number + of statements in a file, which can slightly change the coverage percentage + reported. + - In the HTML report, the filter term and "hide covered" checkbox settings are remembered between viewings, thanks to `Daniel Diniz `_. diff --git a/tests/test_parser.py b/tests/test_parser.py index 0e208bbdb..bf3ccd28d 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -165,6 +165,20 @@ def bar(self): assert expected_arcs == parser.arcs() assert expected_exits == parser.exit_counts() + def test_module_docstrings(self) -> None: + parser = self.parse_text("""\ + '''The docstring on line 1''' + a = 2 + """) + assert {2} == parser.statements + + parser = self.parse_text("""\ + # Docstring is not line 1 + '''The docstring on line 2''' + a = 3 + """) + assert {3} == parser.statements + def test_fuzzed_double_parse(self) -> None: # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50381 # The second parse used to raise `TypeError: 'NoneType' object is not iterable` diff --git a/tests/test_report.py b/tests/test_report.py index 37850cb20..fca027f9b 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -668,6 +668,34 @@ def not_covered(n): assert "not_covered.py 3 3 0.000000%" in report assert "TOTAL 3 3 0.000000%" in report + def test_report_module_docstrings(self) -> None: + self.make_file("main.py", """\ + # Line 1 + '''Line 2 docstring.''' + import other + a = 4 + """) + self.make_file("other.py", """\ + '''Line 1''' + a = 2 + """) + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + report = self.get_report(cov) + + # Name Stmts Miss Cover + # ------------------------------ + # main.py 2 0 100% + # other.py 1 0 100% + # ------------------------------ + # TOTAL 3 0 100% + + assert self.line_count(report) == 6, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "main.py 2 0 100%" + assert squeezed[3] == "other.py 1 0 100%" + assert squeezed[5] == "TOTAL 3 0 100%" + def test_dotpy_not_python(self) -> None: # We run a .py file, and when reporting, we can't parse it as Python. # We should get an error message in the report.