|
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
|
2 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt |
|
3 |
|
4 """LCOV reporting for coverage.py.""" |
|
5 |
|
6 import sys |
|
7 import base64 |
|
8 from hashlib import md5 |
|
9 |
|
10 from coverage.report import get_analysis_to_report |
|
11 |
|
12 |
|
13 class LcovReporter: |
|
14 """A reporter for writing LCOV coverage reports.""" |
|
15 |
|
16 report_type = "LCOV report" |
|
17 |
|
18 def __init__(self, coverage): |
|
19 self.coverage = coverage |
|
20 self.config = self.coverage.config |
|
21 |
|
22 def report(self, morfs, outfile=None): |
|
23 """Renders the full lcov report. |
|
24 |
|
25 'morfs' is a list of modules or filenames |
|
26 |
|
27 outfile is the file object to write the file into. |
|
28 """ |
|
29 |
|
30 self.coverage.get_data() |
|
31 outfile = outfile or sys.stdout |
|
32 |
|
33 for fr, analysis in get_analysis_to_report(self.coverage, morfs): |
|
34 self.get_lcov(fr, analysis, outfile) |
|
35 |
|
36 def get_lcov(self, fr, analysis, outfile=None): |
|
37 """Produces the lcov data for a single file. |
|
38 |
|
39 This currently supports both line and branch coverage, |
|
40 however function coverage is not supported. |
|
41 """ |
|
42 outfile.write("TN:\n") |
|
43 outfile.write(f"SF:{fr.relative_filename()}\n") |
|
44 source_lines = fr.source().splitlines() |
|
45 |
|
46 for covered in sorted(analysis.executed): |
|
47 # Note: Coverage.py currently only supports checking *if* a line |
|
48 # has been executed, not how many times, so we set this to 1 for |
|
49 # nice output even if it's technically incorrect. |
|
50 |
|
51 # The lines below calculate a 64-bit encoded md5 hash of the line |
|
52 # corresponding to the DA lines in the lcov file, for either case |
|
53 # of the line being covered or missed in coverage.py. The final two |
|
54 # characters of the encoding ("==") are removed from the hash to |
|
55 # allow genhtml to run on the resulting lcov file. |
|
56 if source_lines: |
|
57 line = source_lines[covered-1].encode("utf-8") |
|
58 else: |
|
59 line = b"" |
|
60 hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=") |
|
61 outfile.write(f"DA:{covered},1,{hashed}\n") |
|
62 |
|
63 for missed in sorted(analysis.missing): |
|
64 assert source_lines |
|
65 line = source_lines[missed-1].encode("utf-8") |
|
66 hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=") |
|
67 outfile.write(f"DA:{missed},0,{hashed}\n") |
|
68 |
|
69 outfile.write(f"LF:{len(analysis.statements)}\n") |
|
70 outfile.write(f"LH:{len(analysis.executed)}\n") |
|
71 |
|
72 # More information dense branch coverage data. |
|
73 missing_arcs = analysis.missing_branch_arcs() |
|
74 executed_arcs = analysis.executed_branch_arcs() |
|
75 for block_number, block_line_number in enumerate( |
|
76 sorted(analysis.branch_stats().keys()) |
|
77 ): |
|
78 for branch_number, line_number in enumerate( |
|
79 sorted(missing_arcs[block_line_number]) |
|
80 ): |
|
81 # The exit branches have a negative line number, |
|
82 # this will not produce valid lcov. Setting |
|
83 # the line number of the exit branch to 0 will allow |
|
84 # for valid lcov, while preserving the data. |
|
85 line_number = max(line_number, 0) |
|
86 outfile.write(f"BRDA:{line_number},{block_number},{branch_number},-\n") |
|
87 |
|
88 # The start value below allows for the block number to be |
|
89 # preserved between these two for loops (stopping the loop from |
|
90 # resetting the value of the block number to 0). |
|
91 for branch_number, line_number in enumerate( |
|
92 sorted(executed_arcs[block_line_number]), |
|
93 start=len(missing_arcs[block_line_number]), |
|
94 ): |
|
95 line_number = max(line_number, 0) |
|
96 outfile.write(f"BRDA:{line_number},{block_number},{branch_number},1\n") |
|
97 |
|
98 # Summary of the branch coverage. |
|
99 if analysis.has_arcs(): |
|
100 branch_stats = analysis.branch_stats() |
|
101 brf = sum(t for t, k in branch_stats.values()) |
|
102 brh = brf - sum(t - k for t, k in branch_stats.values()) |
|
103 outfile.write(f"BRF:{brf}\n") |
|
104 outfile.write(f"BRH:{brh}\n") |
|
105 |
|
106 outfile.write("end_of_record\n") |