Basil Eljuse | 4b14afb | 2020-09-30 13:07:23 +0100 | [diff] [blame] | 1 | # !/usr/bin/env python |
| 2 | ############################################################################## |
| 3 | # Copyright (c) 2020, ARM Limited and Contributors. All rights reserved. |
| 4 | # |
| 5 | # SPDX-License-Identifier: BSD-3-Clause |
| 6 | ############################################################################## |
| 7 | |
| 8 | import os |
| 9 | import sys |
| 10 | import json |
| 11 | import re |
| 12 | import argparse |
| 13 | |
| 14 | |
| 15 | def 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 | |
| 45 | def 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 | |
| 67 | def 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 | |
| 108 | def 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 | |
| 248 | def 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 | |
| 324 | def 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") |
Saul Romero | 884d214 | 2023-01-16 10:31:22 +0000 | [diff] [blame^] | 343 | for i, line in enumerate(open(abs_path_file, encoding='utf-8')): |
Basil Eljuse | 4b14afb | 2020-09-30 13:07:23 +0100 | [diff] [blame] | 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") |
Saul Romero | 884d214 | 2023-01-16 10:31:22 +0000 | [diff] [blame^] | 358 | for i, line in enumerate(open(abs_path_file, encoding='utf-8')): |
Basil Eljuse | 4b14afb | 2020-09-30 13:07:23 +0100 | [diff] [blame] | 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 | |
| 371 | parser = argparse.ArgumentParser( |
| 372 | description="Script to convert intermediate json file to LCOV info file") |
| 373 | parser.add_argument('--workspace', metavar='PATH', |
| 374 | help='Folder with source files structure', |
| 375 | required=True) |
| 376 | parser.add_argument('--json', metavar='PATH', |
| 377 | help='Intermediate json file name', |
| 378 | required=True) |
| 379 | parser.add_argument('--info', metavar='PATH', |
| 380 | help='Output info file name', |
| 381 | default="coverage.info") |
| 382 | args = parser.parse_args() |
Saul Romero | 884d214 | 2023-01-16 10:31:22 +0000 | [diff] [blame^] | 383 | with open(args.json, encoding='utf-8') as json_file: |
Basil Eljuse | 4b14afb | 2020-09-30 13:07:23 +0100 | [diff] [blame] | 384 | json_data = json.load(json_file) |
| 385 | info_file = open(args.info, "w+") |
| 386 | error_log = open("error_log.txt", "w+") |
| 387 | file_list = json_data['source_files'].keys() |
| 388 | |
| 389 | for 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): |
| 392 | continue |
Saul Romero | 884d214 | 2023-01-16 10:31:22 +0000 | [diff] [blame^] | 393 | source = open(abs_path_file, encoding='utf-8') |
Basil Eljuse | 4b14afb | 2020-09-30 13:07:23 +0100 | [diff] [blame] | 394 | lines = source.readlines() |
| 395 | info_file.write('TN:\n') |
| 396 | info_file.write('SF:' + os.path.abspath(abs_path_file) + '\n') |
| 397 | lines = [-1] + lines # shifting the lines indexes to the right |
| 398 | function_coverage( |
| 399 | json_data['source_files'][relative_path]['functions'].items(), |
| 400 | info_file) |
| 401 | branch_coverage(abs_path_file, info_file, |
| 402 | json_data['source_files'][relative_path]['lines']) |
| 403 | line_coverage(json_data['source_files'][relative_path]['lines'], |
| 404 | info_file) |
| 405 | info_file.write('end_of_record\n\n') |
| 406 | source.close() |
| 407 | |
| 408 | json_file.close() |
| 409 | info_file.close() |
| 410 | error_log.close() |