1818"""
1919
2020import 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
2334def 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+
64150def 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