Skip to content

Commit 610a56f

Browse files
committed
fix: lcov report indexeerror for some Jinja2 files. #1553
1 parent aefde53 commit 610a56f

File tree

3 files changed

+133
-0
lines changed

3 files changed

+133
-0
lines changed

CHANGES.rst

+6
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ development at the same time, such as 4.5.x and 5.0.
2020
Unreleased
2121
----------
2222

23+
- Fix: the ``lcov`` command could raise an IndexError exception if a file is
24+
translated to Python but then executed under its own name. Jinja2 does this
25+
when rendering templates. Fixes `issue 1553`_.
26+
2327
- Python 3.12 beta 1 now inlines comprehensions. Previously they were compiled
2428
as invisible functions and coverage.py would warn you if they weren't
2529
completely executed. This no longer happens under Python 3.12.
2630

31+
.. _issue 1553: https://github.com/nedbat/coveragepy/issues/1553
32+
2733

2834
.. scriv-start-here
2935

coverage/lcovreport.py

+2
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ def get_lcov(self, fr: FileReporter, analysis: Analysis, outfile: IO[str]) -> No
7474
# characters of the encoding ("==") are removed from the hash to
7575
# allow genhtml to run on the resulting lcov file.
7676
if source_lines:
77+
if covered-1 >= len(source_lines):
78+
break
7779
line = source_lines[covered-1]
7880
else:
7981
line = ""

tests/test_report_common.py

+125
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import textwrap
99

1010
import coverage
11+
from coverage import env
1112
from coverage.files import abs_file
1213

1314
from tests.coveragetest import CoverageTest
@@ -156,3 +157,127 @@ def test_map_paths_during_lcov_report(self) -> None:
156157
cov.lcov_report()
157158
contains("coverage.lcov", os_sep("src/program.py"))
158159
doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py"))
160+
161+
162+
class ReportWithJinjaTest(CoverageTest):
163+
"""Tests of Jinja-like behavior.
164+
165+
Jinja2 compiles a template into Python code, and then runs the Python code
166+
to render the template. But during rendering, it uses the template name
167+
(for example, "template.j2") as the file name, not the Python code file
168+
name. Then during reporting, we will try to parse template.j2 as Python
169+
code.
170+
171+
If the file can be parsed, it's included in the report (as a Python file!).
172+
If it can't be parsed, then it's not included in the report.
173+
174+
These tests confirm that code doesn't raise an exception (as reported in
175+
#1553), and that the current (incorrect) behavior remains stable. Ideally,
176+
good.j2 wouldn't be listed at all, since we can't report on it accurately.
177+
178+
See https://github.com/nedbat/coveragepy/issues/1553 for more detail, and
179+
https://github.com/nedbat/coveragepy/issues/1623 for an issue about this
180+
behavior.
181+
182+
"""
183+
184+
def make_files(self) -> None:
185+
"""Create test files: two Jinja templates, and data from rendering them."""
186+
# A Jinja2 file that is syntactically acceptable Python (though it wont run).
187+
self.make_file("good.j2", """\
188+
{{ data }}
189+
line2
190+
line3
191+
""")
192+
# A Jinja2 file that is a Python syntax error.
193+
self.make_file("bad.j2", """\
194+
This is data: {{ data }}.
195+
line 2
196+
line 3
197+
""")
198+
self.make_data_file(
199+
lines={
200+
abs_file("good.j2"): [1, 3, 5, 7, 9],
201+
abs_file("bad.j2"): [1, 3, 5, 7, 9],
202+
}
203+
)
204+
205+
def test_report(self) -> None:
206+
self.make_files()
207+
cov = coverage.Coverage()
208+
cov.load()
209+
cov.report(show_missing=True)
210+
expected = textwrap.dedent("""\
211+
Name Stmts Miss Cover Missing
212+
---------------------------------------
213+
good.j2 3 1 67% 2
214+
---------------------------------------
215+
TOTAL 3 1 67%
216+
""")
217+
assert expected == self.stdout()
218+
219+
def test_html(self) -> None:
220+
self.make_files()
221+
cov = coverage.Coverage()
222+
cov.load()
223+
cov.html_report()
224+
contains("htmlcov/index.html", """\
225+
<tbody>
226+
<tr class="file">
227+
<td class="name left"><a href="good_j2.html">good.j2</a></td>
228+
<td>3</td>
229+
<td>1</td>
230+
<td>0</td>
231+
<td class="right" data-ratio="2 3">67%</td>
232+
</tr>
233+
</tbody>"""
234+
)
235+
doesnt_contain("htmlcov/index.html", "bad.j2")
236+
237+
def test_xml(self) -> None:
238+
self.make_files()
239+
cov = coverage.Coverage()
240+
cov.load()
241+
cov.xml_report()
242+
contains("coverage.xml", 'filename="good.j2"')
243+
if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order.
244+
contains("coverage.xml",
245+
'<line number="1" hits="1"/>',
246+
'<line number="2" hits="0"/>',
247+
'<line number="3" hits="1"/>',
248+
)
249+
doesnt_contain("coverage.xml", 'filename="bad.j2"')
250+
if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order.
251+
doesnt_contain("coverage.xml", '<line number="4"',)
252+
253+
def test_json(self) -> None:
254+
self.make_files()
255+
cov = coverage.Coverage()
256+
cov.load()
257+
cov.json_report()
258+
contains("coverage.json",
259+
# Notice the .json report claims lines in good.j2 executed that
260+
# don't even exist in good.j2...
261+
'"files": {"good.j2": {"executed_lines": [1, 3, 5, 7, 9], ' +
262+
'"summary": {"covered_lines": 2, "num_statements": 3',
263+
)
264+
doesnt_contain("coverage.json", "bad.j2")
265+
266+
def test_lcov(self) -> None:
267+
self.make_files()
268+
cov = coverage.Coverage()
269+
cov.load()
270+
cov.lcov_report()
271+
with open("coverage.lcov") as lcov:
272+
actual = lcov.read()
273+
expected = textwrap.dedent("""\
274+
TN:
275+
SF:good.j2
276+
DA:1,1,FHs1rDakj9p/NAzMCu3Kgw
277+
DA:3,1,DGOyp8LEgI+3CcdFYw9uKQ
278+
DA:2,0,5iUbzxp9w7peeTPjJbvmBQ
279+
LF:3
280+
LH:2
281+
end_of_record
282+
""")
283+
assert expected == actual

0 commit comments

Comments
 (0)