-
-
Notifications
You must be signed in to change notification settings - Fork 440
/
Copy pathlcovreport.py
133 lines (105 loc) · 5.27 KB
/
lcovreport.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0
# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt
"""LCOV reporting for coverage.py."""
from __future__ import annotations
import base64
import hashlib
import sys
from typing import IO, Iterable, TYPE_CHECKING
from coverage.plugin import FileReporter
from coverage.report_core import get_analysis_to_report
from coverage.results import Analysis, Numbers
from coverage.types import TMorf
if TYPE_CHECKING:
from coverage import Coverage
def line_hash(line: str) -> str:
"""Produce a hash of a source line for use in the LCOV file."""
# The LCOV file format requires MD5 as a fingerprint of the file. This is
# not a security use. Some security scanners raise alarms about the use of
# MD5 here, but it is a false positive. This is not a security concern.
hashed = hashlib.md5(line.encode("utf-8")).digest()
return base64.b64encode(hashed).decode("ascii").rstrip("=")
class LcovReporter:
"""A reporter for writing LCOV coverage reports."""
report_type = "LCOV report"
def __init__(self, coverage: Coverage) -> None:
self.coverage = coverage
self.total = Numbers(self.coverage.config.precision)
def report(self, morfs: Iterable[TMorf] | None, outfile: IO[str]) -> float:
"""Renders the full lcov report.
`morfs` is a list of modules or filenames
outfile is the file object to write the file into.
"""
self.coverage.get_data()
outfile = outfile or sys.stdout
for fr, analysis in get_analysis_to_report(self.coverage, morfs):
self.get_lcov(fr, analysis, outfile)
return self.total.n_statements and self.total.pc_covered
def get_lcov(self, fr: FileReporter, analysis: Analysis, outfile: IO[str]) -> None:
"""Produces the lcov data for a single file.
This currently supports both line and branch coverage,
however function coverage is not supported.
"""
self.total += analysis.numbers
outfile.write("TN:\n")
outfile.write(f"SF:{fr.relative_filename()}\n")
source_lines = fr.source().splitlines()
for covered in sorted(analysis.executed):
if covered in analysis.excluded:
# Do not report excluded as executed
continue
# Note: Coverage.py currently only supports checking *if* a line
# has been executed, not how many times, so we set this to 1 for
# nice output even if it's technically incorrect.
# The lines below calculate a 64-bit encoded md5 hash of the line
# corresponding to the DA lines in the lcov file, for either case
# of the line being covered or missed in coverage.py. The final two
# 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 = ""
outfile.write(f"DA:{covered},1,{line_hash(line)}\n")
for missed in sorted(analysis.missing):
# We don't have to skip excluded lines here, because `missing`
# already doesn't have them.
assert source_lines
line = source_lines[missed-1]
outfile.write(f"DA:{missed},0,{line_hash(line)}\n")
outfile.write(f"LF:{analysis.numbers.n_statements}\n")
outfile.write(f"LH:{analysis.numbers.n_executed}\n")
# More information dense branch coverage data.
missing_arcs = analysis.missing_branch_arcs()
executed_arcs = analysis.executed_branch_arcs()
for block_number, block_line_number in enumerate(
sorted(analysis.branch_stats().keys()),
):
for branch_number, line_number in enumerate(
sorted(missing_arcs[block_line_number]),
):
# The exit branches have a negative line number,
# this will not produce valid lcov. Setting
# the line number of the exit branch to 0 will allow
# for valid lcov, while preserving the data.
line_number = max(line_number, 0)
outfile.write(f"BRDA:{line_number},{block_number},{branch_number},-\n")
# The start value below allows for the block number to be
# preserved between these two for loops (stopping the loop from
# resetting the value of the block number to 0).
for branch_number, line_number in enumerate(
sorted(executed_arcs[block_line_number]),
start=len(missing_arcs[block_line_number]),
):
line_number = max(line_number, 0)
outfile.write(f"BRDA:{line_number},{block_number},{branch_number},1\n")
# Summary of the branch coverage.
if analysis.has_arcs:
branch_stats = analysis.branch_stats()
brf = sum(t for t, k in branch_stats.values())
brh = brf - sum(t - k for t, k in branch_stats.values())
outfile.write(f"BRF:{brf}\n")
outfile.write(f"BRH:{brh}\n")
outfile.write("end_of_record\n")