blob: 20eb0f4a00e55df2b3038f5d08d736ae250c49f2 [file] [log] [blame]
Paul Sokolovskya31a18c2023-02-10 12:54:10 +07001#!/usr/bin/env python3
Basil Eljuse4b14afb2020-09-30 13:07:23 +01002##############################################################################
3# Copyright (c) 2020, ARM Limited and Contributors. All rights reserved.
4#
5# SPDX-License-Identifier: BSD-3-Clause
6##############################################################################
7
8import os
9import sys
10import json
11import re
12import argparse
13
14
15def function_coverage(function_tuples, info_file):
16 """
17 Parses and get information from intermediate json file to info
18 file for function coverage
19
20 :param function_tuples: List of tuples with function name
21 and its data as pairs.
22 :param info_file: Handler to for file writing coverage
23 """
24 total_func = 0
25 covered_func = 0
26 function_names = []
27 function_cov = []
28 for func_name, func_data in function_tuples:
29 function_names.append(
30 'FN:{},{}\n'.format(
31 func_data["line_number"],
32 func_name))
33 total_func += 1
34 if func_data["covered"]:
35 covered_func += 1
36 function_cov.append('FNDA:1,{}\n'.format(func_name))
37 else:
38 function_cov.append('FNDA:0,{}\n'.format(func_name))
39 info_file.write("\n".join(function_names))
40 info_file.write("\n".join(function_cov))
41 info_file.write('FNF:{}\n'.format(total_func))
42 info_file.write('FNH:{}\n'.format(covered_func))
43
44
45def line_coverage(lines_dict, info_file):
46 """
47 Parses and get information from intermediate json file to info
48 file for line coverage
49
50 :param lines_dict: Dictionary of lines with line number as key
51 and its data as value
52 :param info_file: Handler to for file writing coverage
53 """
54 total_lines = 0
55 covered_lines = 0
56 for line in lines_dict:
57 total_lines += 1
58 if lines_dict[line]['covered']:
59 covered_lines += 1
60 info_file.write('DA:' + line + ',1\n')
61 else:
62 info_file.write('DA:' + line + ',0\n')
63 info_file.write('LF:' + str(total_lines) + '\n')
64 info_file.write('LH:' + str(covered_lines) + '\n')
65
66
67def sanity_check(branch_line, lines_dict, abs_path_file):
68 """
69 Check if the 'branch_line' line of the C source corresponds to actual
70 branching instructions in the assembly code. Also, check if that
71 line is covered. If it's not covered, this branching statement can
72 be omitted from the report.
73 Returns False and prints an error message if check is not successful,
74 True otherwise
75
76 :param branch_line: Source code line with the branch instruction
77 :param lines_dict: Dictionary of lines with line number as key
78 and its data as value
79 :param abs_path_file: File name of the source file
80 """
81 if str(branch_line) not in lines_dict:
82 return False
83 found_branching = False
84 for i in lines_dict[str(branch_line)]['elf_index']:
85 for j in lines_dict[str(branch_line)]['elf_index'][i]:
86 string = lines_dict[str(branch_line)]['elf_index'][i][j][0]
87 # these cover all the possible branching instructions
88 if ('\tb' in string or
89 '\tcbnz' in string or
90 '\tcbz' in string or
91 '\ttbnz' in string or
92 '\ttbz' in string):
93 # '\tbl' in string or # already covered by '\tb'
94 # '\tblr' in string or # already covered by '\tb'
95 # '\tbr' in string or # already covered by '\tb'
96 found_branching = True
97 if not found_branching:
98 error_log.write(
99 '\nSomething possibly wrong:\n\tFile ' +
100 abs_path_file +
101 ', line ' +
102 str(branch_line) +
103 '\n\tshould be a branching statement but couldn\'t ' +
104 'find correspondence in assembly code')
105 return True
106
107
108def manage_if_branching(branch_line, lines_dict, info_file, abs_path_file):
109 """
110 Takes care of branch coverage, branch_line is the source code
111 line in which the 'if' statement is located the function produces
112 branch coverage info based on C source code and json file content
113
114 :param branch_line: Source code line with the 'if' instruction
115 :param lines_dict: Dictionary of lines with line number as key
116 and its data as value
117 :param info_file: Handler to for file writing coverage
118 :param abs_path_file: File name of the source file
119 """
120 total_branch_local = 0
121 covered_branch_local = 0
122
123 if not sanity_check(branch_line, lines_dict, abs_path_file):
124 return(total_branch_local, covered_branch_local)
125 total_branch_local += 2
126 current_line = branch_line # used to read lines one by one
127 # check for multiline if-condition and update current_line accordingly
128 parenthesis_count = 0
129 while True:
130 end_of_condition = False
131 for char in lines[current_line]:
132 if char == ')':
133 parenthesis_count -= 1
134 if parenthesis_count == 0:
135 end_of_condition = True
136 elif char == '(':
137 parenthesis_count += 1
138 if end_of_condition:
139 break
140 current_line += 1
141 # first branch
142 # simple case: 'if' statements with no braces
143 if '{' not in lines[current_line] and '{' not in lines[current_line + 1]:
144
145 if (str(current_line + 1) in lines_dict and
146 lines_dict[str(current_line + 1)]['covered']):
147 info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '1\n')
148 covered_branch_local += 1
149 else:
150 info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '0\n')
151 current_line += 1
152
153 # more complex case: '{' after the 'if' statement
154 else:
155 if '{' in lines[current_line]:
156 current_line += 1
157 else:
158 current_line += 2
159
160 # we need to check whether at least one line in the block is covered
161 found_covered_line = False
162
163 # this is a simpler version of a stack used to check when a code block
164 # ends at the moment, it just checks for '{' and '}', doesn't take into
165 # account the presence of commented braces
166 brace_counter = 1
167 while True:
168 end_of_block = False
169 for char in lines[current_line]:
170 if char == '}':
171 brace_counter -= 1
172 if brace_counter == 0:
173 end_of_block = True
174 elif char == '{':
175 brace_counter += 1
176 if end_of_block:
177 break
178 if (str(current_line) in lines_dict and
179 lines_dict[str(current_line)]['covered']):
180 found_covered_line = True
181
182 current_line += 1
183
184 if found_covered_line:
185 info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '1\n')
186 covered_branch_local += 1
187 else:
188 info_file.write('BRDA:' + str(branch_line) + ',0,' + '0,' + '0\n')
189
190 # second branch (if present). If not present, second branch is covered by
191 # default
192 current_line -= 1
193 candidate_else_line = current_line
194 while 'else' not in lines[current_line] and candidate_else_line + \
195 2 >= current_line:
196 current_line += 1
197 if current_line == len(lines):
198 break
199
200 # no 'else': branch covered by default
201 if current_line == candidate_else_line + 3:
202 info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '1\n')
203 covered_branch_local += 1
204 return(total_branch_local, covered_branch_local)
205
206 # 'else' found: check if opening braces are present
207 if '{' not in lines[current_line - 1] and '{' not in lines[current_line]:
208 if str(current_line + 1) in lines_dict:
209 if lines_dict[str(current_line + 1)]['covered']:
210 info_file.write(
211 'BRDA:' +
212 str(branch_line) +
213 ',0,' +
214 '1,' +
215 '1\n')
216 covered_branch_local += 1
217 else:
218 info_file.write(
219 'BRDA:' +
220 str(branch_line) +
221 ',0,' +
222 '1,' +
223 '0\n')
224 else:
225 info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '0\n')
226
227 else:
228 if '{' in lines[current_line]:
229 current_line += 1
230 else:
231 current_line += 2
232 found_covered_line = False
233 while '}' not in lines[current_line]:
234 if (str(current_line) in lines_dict and
235 lines_dict[str(current_line)]['covered']):
236 found_covered_line = True
237 break
238 current_line += 1
239 if found_covered_line:
240 info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '1\n')
241 covered_branch_local += 1
242 else:
243 info_file.write('BRDA:' + str(branch_line) + ',0,' + '1,' + '0\n')
244
245 return(total_branch_local, covered_branch_local)
246
247
248def manage_switch_branching(switch_line, lines_dict, info_file, abs_path_file):
249 """
250 Takes care of branch coverage, branch_line is the source code
251 line in which the 'switch' statement is located the function produces
252 branch coverage info based on C source code and json file content
253
254 :param switch_line: Source code line with the 'switch' instruction
255 :param lines_dict: Dictionary of lines with line number as key
256 and its data as value
257 :param info_file: Handler to for file writing coverage
258 :param abs_path_file: File name of the source file
259 """
260
261 total_branch_local = 0
262 covered_branch_local = 0
263
264 if not sanity_check(switch_line, lines_dict, abs_path_file):
265 return(total_branch_local, covered_branch_local)
266
267 current_line = switch_line # used to read lines one by one
268 branch_counter = 0 # used to count the number of switch branches
269 brace_counter = 0
270
271 # parse the switch-case line by line, checking if every 'case' is covered
272 # the switch-case ends with a '}'
273 while True:
274 if '{' in lines[current_line]:
275 brace_counter += 1
276 if '}' in lines[current_line]:
277 brace_counter -= 1
278 if brace_counter == 0:
279 return(total_branch_local, covered_branch_local)
280 if 'case' in lines[current_line] or 'default' in lines[current_line]:
281 covered = False
282 total_branch_local += 1
283 inner_brace = 0
284 current_line += 1
285 while (('case' not in lines[current_line]
286 and 'default' not in lines[current_line]) or
287 inner_brace > 0):
288 if (str(current_line) in lines_dict and
289 lines_dict[str(current_line)]['covered']):
290 covered = True
291 if '{' in lines[current_line]:
292 inner_brace += 1
293 brace_counter += 1
294 if '}' in lines[current_line]:
295 inner_brace -= 1
296 brace_counter -= 1
297 if brace_counter == 0:
298 break
299 current_line += 1
300 if covered:
301 info_file.write(
302 'BRDA:' +
303 str(switch_line) +
304 ',0,' +
305 str(branch_counter) +
306 ',1\n')
307 covered_branch_local += 1
308 else:
309 info_file.write(
310 'BRDA:' +
311 str(switch_line) +
312 ',0,' +
313 str(branch_counter) +
314 ',0\n')
315 if brace_counter == 0:
316 return(total_branch_local, covered_branch_local)
317 branch_counter += 1
318 else:
319 current_line += 1
320
321 return(total_branch_local, covered_branch_local)
322
323
324def branch_coverage(abs_path_file, info_file, lines_dict):
325 """
326 Produces branch coverage information, using the functions
327 'manage_if_branching' and 'manage_switch_branching'
328
329 :param abs_path_file: File name of the source file
330 :param info_file: Handler to for file writing coverage
331 :param lines_dict: Dictionary of lines with line number as key
332 and its data as value
333 """
334 total_branch = 0
335 covered_branch = 0
336
337 # branch coverage: if statements
338 branching_lines = []
339
340 # regex: find all the lines starting with 'if' or 'else if'
341 # (possibly preceded by whitespaces/tabs)
342 pattern = re.compile(r"^\s+if|^\s+} else if|^\s+else if")
343 for i, line in enumerate(open(abs_path_file)):
344 for match in re.finditer(pattern, line):
345 branching_lines.append(i + 1)
346 while branching_lines:
347 t = manage_if_branching(branching_lines.pop(0), lines_dict,
348 info_file, abs_path_file)
349 total_branch += t[0]
350 covered_branch += t[1]
351
352 # branch coverage: switch statements
353 switch_lines = []
354
355 # regex: find all the lines starting with 'switch'
356 # (possibly preceded by whitespaces/tabs)
357 pattern = re.compile(r"^\s+switch")
358 for i, line in enumerate(open(abs_path_file)):
359 for match in re.finditer(pattern, line):
360 switch_lines.append(i + 1)
361 while switch_lines:
362 t = manage_switch_branching(switch_lines.pop(0), lines_dict,
363 info_file, abs_path_file)
364 total_branch += t[0]
365 covered_branch += t[1]
366
367 info_file.write('BRF:' + str(total_branch) + '\n')
368 info_file.write('BRH:' + str(covered_branch) + '\n')
369
370
371parser = argparse.ArgumentParser(
372 description="Script to convert intermediate json file to LCOV info file")
373parser.add_argument('--workspace', metavar='PATH',
374 help='Folder with source files structure',
375 required=True)
376parser.add_argument('--json', metavar='PATH',
377 help='Intermediate json file name',
378 required=True)
379parser.add_argument('--info', metavar='PATH',
380 help='Output info file name',
381 default="coverage.info")
382args = parser.parse_args()
383with open(args.json) as json_file:
384 json_data = json.load(json_file)
385info_file = open(args.info, "w+")
386error_log = open("error_log.txt", "w+")
387file_list = json_data['source_files'].keys()
388
389for relative_path in file_list:
390 abs_path_file = os.path.join(args.workspace, relative_path)
391 if not os.path.exists(abs_path_file):
Paul Sokolovskydde61a82023-02-10 13:01:17 +0700392 print("warning: cannot find '%s'" % abs_path_file)
Basil Eljuse4b14afb2020-09-30 13:07:23 +0100393 continue
394 source = open(abs_path_file)
395 lines = source.readlines()
396 info_file.write('TN:\n')
397 info_file.write('SF:' + os.path.abspath(abs_path_file) + '\n')
398 lines = [-1] + lines # shifting the lines indexes to the right
399 function_coverage(
400 json_data['source_files'][relative_path]['functions'].items(),
401 info_file)
402 branch_coverage(abs_path_file, info_file,
403 json_data['source_files'][relative_path]['lines'])
404 line_coverage(json_data['source_files'][relative_path]['lines'],
405 info_file)
406 info_file.write('end_of_record\n\n')
407 source.close()
408
409json_file.close()
410info_file.close()
411error_log.close()