Skip to content

Commit 7bd76e8

Browse files
committed
Regexes make all of this better.
1 parent 5a1282b commit 7bd76e8

File tree

1 file changed

+146
-28
lines changed

1 file changed

+146
-28
lines changed

fix_coverage.py

Lines changed: 146 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,17 @@
1818
"""
1919

2020
import sys
21+
import re
22+
23+
24+
def de_comment_line(line):
25+
"""
26+
Returns a version of the string @line with all
27+
C++ comments removed
28+
"""
29+
line = re.sub(r"/\*.*\*/", "", line) # Remove /* comments
30+
line = re.sub(r"//.*", "", line) # Remove // comments
31+
return line
2132

2233

2334
def validate_line(line, second_bar):
@@ -33,17 +44,10 @@ def validate_line(line, second_bar):
3344
return False
3445

3546
line = line[second_bar+1:].strip() # Get the code part of the line
36-
line = line.replace("/*_FORCE_COVER_START_*/", "") # Don't muck up line
37-
line = line.replace("/*_FORCE_COVER_END_*/", "") # with our comments
47+
line = de_comment_line(line) # Remove all comments
3848

3949
if line == "": # Don't report empty lines as uncovered
4050
return False
41-
if line.startswith("//"): # Don't report commented lines as uncovered
42-
return False
43-
if line.startswith("/*"): # Don't report commented lines as uncovered
44-
return False # TODO: There could still be code on this line
45-
if line.startswith("*"): # * often indicats line is part of a comment
46-
return False
4751
if line == "{": # A single brace can't be executed
4852
return False
4953
if line == "}":
@@ -61,53 +65,167 @@ def is_first_line(line):
6165
return line.split("|")[0].strip() == "1"
6266

6367

68+
def is_functionlike_macro_definition(line):
69+
"""
70+
Returns true if this line contains a function-like macro definition
71+
(e.g. #define TEST(a) a)
72+
"""
73+
74+
if re.findall(r".*#define\s\S*\(.*\)\s", line):
75+
# Function-like macros can't have spaces between name and
76+
# parentheses. This should be the only way this line
77+
# could be one
78+
return True
79+
80+
return False
81+
82+
83+
def is_macro_continuation(line):
84+
"""
85+
Takes a line and returns a boolean indicating whether or not
86+
a macro defined on this line would be continued on the next line
87+
(i.e. is the last non-comment character a "\" ?)
88+
"""
89+
line = de_comment_line(line)
90+
if line.endswith("\\"):
91+
return True
92+
return False
93+
94+
95+
def opens_multiline_comment(line):
96+
"""
97+
Return true if the string @param line contains a
98+
/* without a following */, meaning it opens a multiline
99+
comment but does not close it.
100+
"""
101+
102+
line = de_comment_line(line)
103+
return len(re.findall(r"/\*", line)) > 0
104+
105+
106+
def closes_multiline_comment(line):
107+
"""
108+
Return true if the string @param line contains a
109+
*/ without a preceeding /*, meaning it closes an
110+
already-open multiline comment.
111+
"""
112+
113+
line = de_comment_line(line)
114+
return len(re.findall(r"\*/", line)) > 0
115+
116+
117+
def cover_line(line):
118+
"""
119+
This function takes a string containing a line that should
120+
potentially have an execution count and returns a version
121+
of that line that does have an execution count if deemed
122+
appropriate by the rules in validate_line().
123+
124+
Basically, if there is currently no number where there should
125+
be an execution count (indicating this line did not make
126+
it into the compiled binary), a zero is placed there to
127+
indicate that this line was executed 0 times. Test coverage
128+
viewers will interpret this to mean that the line could
129+
potentially have been executed.
130+
"""
131+
first_bar = line.find("|")
132+
second_bar = line.find("|", first_bar+1)
133+
134+
if validate_line(line, second_bar) and \
135+
line[second_bar-1].strip() == "":
136+
# If this line could have been executed but wasn't (no
137+
# number between first and second bars), put a zero
138+
# before the second bar, indicating that it was
139+
# executed zero times. Test coverage viewers will interpret
140+
# this as meaning the line should have been covered
141+
# but wasn't.
142+
return "".join([line[:second_bar-1],
143+
"0", line[second_bar:]])
144+
145+
# There's already an execution count - this
146+
# template must have been instantiated
147+
return line
148+
149+
64150
def main():
65151

152+
# TODO: At some point we'll probably want options, at which point we should
153+
# use an actual argument parser
66154
if len(sys.argv) < 2:
67155
print("Usage: python fix_coverage.py [name_of_coverage_report_file]")
68156
exit(1)
69157

70158
lines = [] # Accumulate list of lines to glue back together at the end
71159
with open(sys.argv[1]) as infile:
72160
force_cover_active = 0 # Counter of open nested templates
161+
macro_active = False # Are we in a function-like macros definition?
162+
multi_line_comment_active = False # Are we in a multi-line comment?
73163

74164
for line in infile:
165+
166+
# ~~~~~~~~~~~~~~~~~~ Book-keeping section ~~~~~~~~~~~~~~~~~~~~~~
167+
# (adjusts force_cover_active count)
168+
75169
if is_first_line(line):
76170
# Zero out regions at beginning of new file just
77171
# in case stuff got screwed up
78172
force_cover_active = 0
79173

174+
# We don't know how many template function definitions start on
175+
# this line, but we know each will be preceeded by this string
80176
force_cover_active += line.count("_FORCE_COVER_START_")
81177

82-
# Special case where region of forced coverage starts at
83-
# the end of this line
178+
# Special case where region of forced coverage starts at
179+
# the end of this line (so we don't count the part before
180+
# the body as executable)
84181
if line.endswith("_FORCE_COVER_START_") and \
85182
force_cover_active == line.count("_FORCE_COVER_START_"):
86183
lines.append(line)
87184
continue
88185

186+
# ~~~~~~~~~~~~~ Multi-line comment section ~~~~~~~~~~~~~~~~~~~~~~
187+
# If we're in a multi-line comment nothing else matters
188+
189+
if opens_multiline_comment(line):
190+
multi_line_comment_active = True
191+
if closes_multiline_comment(line):
192+
multi_line_comment_active = False
193+
if multi_line_comment_active:
194+
lines.append(line)
195+
continue
196+
197+
# ~~~~~~~~~~~~ Function-like macro section ~~~~~~~~~~~~~~~~~~~~~~
198+
199+
# Function-like macros defined in the code can be tested just
200+
# like any other code, but they don't show up in the AST the
201+
# way other stuff does (because the pre-processor handles them).
202+
# So, we're going to manually check for them here so we can
203+
# report when they haven't been covered by tests.
204+
# TODO: Make this optional
205+
if is_functionlike_macro_definition(line):
206+
macro_active = True
207+
208+
if macro_active: # Add coverage data if we're in a macro
209+
line = cover_line(line)
210+
if not is_macro_continuation(line):
211+
# If this macro doesn't have a continuation character,
212+
# then this is the last line of it
213+
macro_active = False
214+
215+
# ~~~~~~~~~~~~~~~ Template coverage section ~~~~~~~~~~~~~~~~~~~~~~
216+
# (where we do the actual covering of potentially
217+
# uninstantiated templates)
218+
89219
if not force_cover_active: # Don't need to change line because
90220
lines.append(line) # we aren't in a template definition
91221
continue
92222

93-
if force_cover_active: # In template. Might need to do stuff.
94-
first_bar = line.find("|")
95-
second_bar = line.find("|", first_bar+1)
96-
97-
if validate_line(line, second_bar) and \
98-
line[second_bar-1].strip() == "":
99-
# If this line could have been executed but wasn't (no
100-
# number between first and second bars), put a zero
101-
# before the second bar, indicating that it was
102-
# executed zero times. Test coverage viewers will interpret
103-
# this as meaning the line should have been covered
104-
# but wasn't.
105-
lines.append("".join([line[:second_bar-1],
106-
"0", line[second_bar:]]))
107-
else:
108-
# There's already an execution count - this
109-
# template must have been instantiated
110-
lines.append(line)
223+
# In template. Might need to do stuff. cover_line() will figure
224+
# it out
225+
lines.append(cover_line(line))
226+
227+
# ~~~~~~~~~~~~~~~~ Closing book-keeping section ~~~~~~~~~~~~~~~~~
228+
# (adjusts force_cover_active count)
111229

112230
# Closing force_cover comments happens at the end of the line
113231
force_cover_active -= line.count("_FORCE_COVER_END_")

0 commit comments

Comments
 (0)