-
Notifications
You must be signed in to change notification settings - Fork 1
/
fix_coverage.py
240 lines (184 loc) · 8.14 KB
/
fix_coverage.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
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
#!/usr/bin/env python
# Author: Emily Dolson
# Copyright: 2018
# License: MIT
"""
fix_coverage.py is a script for modifying C++ coverage reports to
mark uninstantiated templates as executable regions missing coverage.
Usage: ./fix_coverage.py [coverage_report_file]
Assumes that coverage file has the format:
[source_line_number] | [execution_count]| [code]
./force_coverage must have been run on the source code before it was
compiled in order for this to work.
"""
import sys
import re
def de_comment_line(line):
"""
Returns a version of the string @line with all
C++ comments removed
"""
line = re.sub(r"/\*.*\*/", "", line) # Remove /* comments
line = re.sub(r"//.*", "", line) # Remove // comments
return line
def validate_line(line, second_bar):
"""
Returns true if this line contains code that should be executre.
Arguments: line - a string containing the full text of this line
second_bar - an int indicating the index in the string
at which the second "|" character occurs,
denoting the start of the code portion.
"""
if second_bar == -1: # This isn't a valid code coverage line
return False
line = line[second_bar+1:].strip() # Get the code part of the line
line = de_comment_line(line) # Remove all comments
if line == "": # Don't report empty lines as uncovered
return False
if line == "{": # A single brace can't be executed
return False
if line == "}":
return False
# Have another rule for skipping lines? Add it here!
return True
def is_first_line(line):
"""
Returns true if line is the first line of a new file
"""
return line.split("|")[0].strip() == "1"
def is_functionlike_macro_definition(line):
"""
Returns true if this line contains a function-like macro definition
(e.g. #define TEST(a) a)
"""
if re.findall(r".*#define\s\S*\(.*\)\s", line):
# Function-like macros can't have spaces between name and
# parentheses. This should be the only way this line
# could be one
return True
return False
def is_macro_continuation(line):
"""
Takes a line and returns a boolean indicating whether or not
a macro defined on this line would be continued on the next line
(i.e. is the last non-comment character a "\" ?)
"""
line = de_comment_line(line)
if line.endswith("\\"):
return True
return False
def opens_multiline_comment(line):
"""
Return true if the string @param line contains a
/* without a following */, meaning it opens a multiline
comment but does not close it.
"""
line = de_comment_line(line)
return len(re.findall(r"/\*", line)) > 0
def closes_multiline_comment(line):
"""
Return true if the string @param line contains a
*/ without a preceeding /*, meaning it closes an
already-open multiline comment.
"""
line = de_comment_line(line)
return len(re.findall(r"\*/", line)) > 0
def cover_line(line):
"""
This function takes a string containing a line that should
potentially have an execution count and returns a version
of that line that does have an execution count if deemed
appropriate by the rules in validate_line().
Basically, if there is currently no number where there should
be an execution count (indicating this line did not make
it into the compiled binary), a zero is placed there to
indicate that this line was executed 0 times. Test coverage
viewers will interpret this to mean that the line could
potentially have been executed.
"""
first_bar = line.find("|")
second_bar = line.find("|", first_bar+1)
if validate_line(line, second_bar) and \
line[second_bar-1].strip() == "":
# If this line could have been executed but wasn't (no
# number between first and second bars), put a zero
# before the second bar, indicating that it was
# executed zero times. Test coverage viewers will interpret
# this as meaning the line should have been covered
# but wasn't.
return "".join([line[:second_bar-1],
"0", line[second_bar:]])
# There's already an execution count - this
# template must have been instantiated
return line
def main():
# TODO: At some point we'll probably want options, at which point we should
# use an actual argument parser
if len(sys.argv) < 2:
print("Usage: python fix_coverage.py [name_of_coverage_report_file]")
exit(1)
lines = [] # Accumulate list of lines to glue back together at the end
with open(sys.argv[1]) as infile:
force_cover_active = 0 # Counter of open nested templates
macro_active = False # Are we in a function-like macros definition?
multi_line_comment_active = False # Are we in a multi-line comment?
for line in infile:
# ~~~~~~~~~~~~~~~~~~ Book-keeping section ~~~~~~~~~~~~~~~~~~~~~~
# (adjusts force_cover_active count)
line = line.rstrip() # Remove trailing whitespace
if is_first_line(line):
# Zero out regions at beginning of new file just
# in case stuff got screwed up
force_cover_active = 0
# We don't know how many template function definitions start on
# this line, but we know each will be preceeded by this string
force_cover_active += line.count("_FORCE_COVER_START_")
# Special case where region of forced coverage starts at
# the end of this line (so we don't count the part before
# the body as executable)
if line.endswith("/*_FORCE_COVER_START_*/") and \
force_cover_active == line.count("_FORCE_COVER_START_"):
lines.append(line + "\n")
continue
# ~~~~~~~~~~~~~ Multi-line comment section ~~~~~~~~~~~~~~~~~~~~~~
# If we're in a multi-line comment nothing else matters
if opens_multiline_comment(line):
multi_line_comment_active = True
if closes_multiline_comment(line):
multi_line_comment_active = False
if multi_line_comment_active:
lines.append(line + "\n")
continue
# ~~~~~~~~~~~~ Function-like macro section ~~~~~~~~~~~~~~~~~~~~~~
# Function-like macros defined in the code can be tested just
# like any other code, but they don't show up in the AST the
# way other stuff does (because the pre-processor handles them).
# So, we're going to manually check for them here so we can
# report when they haven't been covered by tests.
# TODO: Make this optional
if is_functionlike_macro_definition(line):
macro_active = True
if macro_active: # Add coverage data if we're in a macro
line = cover_line(line)
if not is_macro_continuation(line):
# If this macro doesn't have a continuation character,
# then this is the last line of it
macro_active = False
# ~~~~~~~~~~~~~~~ Template coverage section ~~~~~~~~~~~~~~~~~~~~~~
# (where we do the actual covering of potentially
# uninstantiated templates)
if not force_cover_active: # Don't need to change line because
lines.append(line + "\n") # we aren't in a template definition
continue
# In template. Might need to do stuff. cover_line() will figure
# it out
lines.append(cover_line(line) + "\n")
# ~~~~~~~~~~~~~~~~ Closing book-keeping section ~~~~~~~~~~~~~~~~~
# (adjusts force_cover_active count)
# Closing force_cover comments happens at the end of the line
force_cover_active -= line.count("_FORCE_COVER_END_")
# Rewrite file with modified execution counts
with open(sys.argv[1], "w") as infile:
infile.writelines(lines)
if __name__ == "__main__":
main()