summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNed Batchelder <ned@nedbatchelder.com>2023-05-14 14:58:06 -0400
committerNed Batchelder <ned@nedbatchelder.com>2023-05-14 16:08:49 -0400
commit610a56fb7ef43614be8752c4561c6c0f118db021 (patch)
tree8a7ca4aaad8d41dd63c86a664e9841635035ed9e
parentaefde5326dde3a6a232d888a1b1b4e1202ff2b3b (diff)
downloadpython-coveragepy-git-610a56fb7ef43614be8752c4561c6c0f118db021.tar.gz
fix: lcov report indexeerror for some Jinja2 files. #1553
-rw-r--r--CHANGES.rst6
-rw-r--r--coverage/lcovreport.py2
-rw-r--r--tests/test_report_common.py125
3 files changed, 133 insertions, 0 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 4a1570a7..5a37e43d 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -20,10 +20,16 @@ development at the same time, such as 4.5.x and 5.0.
Unreleased
----------
+- Fix: the ``lcov`` command could raise an IndexError exception if a file is
+ translated to Python but then executed under its own name. Jinja2 does this
+ when rendering templates. Fixes `issue 1553`_.
+
- Python 3.12 beta 1 now inlines comprehensions. Previously they were compiled
as invisible functions and coverage.py would warn you if they weren't
completely executed. This no longer happens under Python 3.12.
+.. _issue 1553: https://github.com/nedbat/coveragepy/issues/1553
+
.. scriv-start-here
diff --git a/coverage/lcovreport.py b/coverage/lcovreport.py
index 8e3a5768..3da164d5 100644
--- a/coverage/lcovreport.py
+++ b/coverage/lcovreport.py
@@ -74,6 +74,8 @@ class LcovReporter:
# characters of the encoding ("==") are removed from the hash to
# allow genhtml to run on the resulting lcov file.
if source_lines:
+ if covered-1 >= len(source_lines):
+ break
line = source_lines[covered-1]
else:
line = ""
diff --git a/tests/test_report_common.py b/tests/test_report_common.py
index 2b20ed92..68551517 100644
--- a/tests/test_report_common.py
+++ b/tests/test_report_common.py
@@ -8,6 +8,7 @@ from __future__ import annotations
import textwrap
import coverage
+from coverage import env
from coverage.files import abs_file
from tests.coveragetest import CoverageTest
@@ -156,3 +157,127 @@ class ReportMapsPathsTest(CoverageTest):
cov.lcov_report()
contains("coverage.lcov", os_sep("src/program.py"))
doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py"))
+
+
+class ReportWithJinjaTest(CoverageTest):
+ """Tests of Jinja-like behavior.
+
+ Jinja2 compiles a template into Python code, and then runs the Python code
+ to render the template. But during rendering, it uses the template name
+ (for example, "template.j2") as the file name, not the Python code file
+ name. Then during reporting, we will try to parse template.j2 as Python
+ code.
+
+ If the file can be parsed, it's included in the report (as a Python file!).
+ If it can't be parsed, then it's not included in the report.
+
+ These tests confirm that code doesn't raise an exception (as reported in
+ #1553), and that the current (incorrect) behavior remains stable. Ideally,
+ good.j2 wouldn't be listed at all, since we can't report on it accurately.
+
+ See https://github.com/nedbat/coveragepy/issues/1553 for more detail, and
+ https://github.com/nedbat/coveragepy/issues/1623 for an issue about this
+ behavior.
+
+ """
+
+ def make_files(self) -> None:
+ """Create test files: two Jinja templates, and data from rendering them."""
+ # A Jinja2 file that is syntactically acceptable Python (though it wont run).
+ self.make_file("good.j2", """\
+ {{ data }}
+ line2
+ line3
+ """)
+ # A Jinja2 file that is a Python syntax error.
+ self.make_file("bad.j2", """\
+ This is data: {{ data }}.
+ line 2
+ line 3
+ """)
+ self.make_data_file(
+ lines={
+ abs_file("good.j2"): [1, 3, 5, 7, 9],
+ abs_file("bad.j2"): [1, 3, 5, 7, 9],
+ }
+ )
+
+ def test_report(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.report(show_missing=True)
+ expected = textwrap.dedent("""\
+ Name Stmts Miss Cover Missing
+ ---------------------------------------
+ good.j2 3 1 67% 2
+ ---------------------------------------
+ TOTAL 3 1 67%
+ """)
+ assert expected == self.stdout()
+
+ def test_html(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.html_report()
+ contains("htmlcov/index.html", """\
+ <tbody>
+ <tr class="file">
+ <td class="name left"><a href="good_j2.html">good.j2</a></td>
+ <td>3</td>
+ <td>1</td>
+ <td>0</td>
+ <td class="right" data-ratio="2 3">67%</td>
+ </tr>
+ </tbody>"""
+ )
+ doesnt_contain("htmlcov/index.html", "bad.j2")
+
+ def test_xml(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.xml_report()
+ contains("coverage.xml", 'filename="good.j2"')
+ if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order.
+ contains("coverage.xml",
+ '<line number="1" hits="1"/>',
+ '<line number="2" hits="0"/>',
+ '<line number="3" hits="1"/>',
+ )
+ doesnt_contain("coverage.xml", 'filename="bad.j2"')
+ if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order.
+ doesnt_contain("coverage.xml", '<line number="4"',)
+
+ def test_json(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.json_report()
+ contains("coverage.json",
+ # Notice the .json report claims lines in good.j2 executed that
+ # don't even exist in good.j2...
+ '"files": {"good.j2": {"executed_lines": [1, 3, 5, 7, 9], ' +
+ '"summary": {"covered_lines": 2, "num_statements": 3',
+ )
+ doesnt_contain("coverage.json", "bad.j2")
+
+ def test_lcov(self) -> None:
+ self.make_files()
+ cov = coverage.Coverage()
+ cov.load()
+ cov.lcov_report()
+ with open("coverage.lcov") as lcov:
+ actual = lcov.read()
+ expected = textwrap.dedent("""\
+ TN:
+ SF:good.j2
+ DA:1,1,FHs1rDakj9p/NAzMCu3Kgw
+ DA:3,1,DGOyp8LEgI+3CcdFYw9uKQ
+ DA:2,0,5iUbzxp9w7peeTPjJbvmBQ
+ LF:3
+ LH:2
+ end_of_record
+ """)
+ assert expected == actual