1#! /usr/bin/env python3 2 3import argparse 4from collections import OrderedDict, namedtuple, defaultdict 5import glob 6import os 7import sys 8import demangle 9import read_gcov 10import merge_gcov 11 12def get_gcov_files(dir_name): 13 """ 14 Get list of *.gcov files in a directory. Returns list of bare filenames. 15 """ 16 fs = glob.glob(os.path.join(dir_name,'*.gcov')) 17 fs = [os.path.basename(f) for f in fs] 18 return fs 19 20 21def keep_file(gcov): 22 """ 23 Some files should not be considered in coverage. 24 Files to skip include system files (in /usr), the unit tests themselves 25 (in tests/ directories) and the unit test infrastructure (in external_codes/) 26 """ 27 source_file = gcov.tags['Source'] 28 29 path_elements = source_file.split('/') 30 31 # Only looking at specific depths of directories. This might need 32 # to be expanded, for example if unit test directories contain 33 # subdirectories. 34 try: 35 if path_elements[-2] == 'tests': 36 #print 'Unit test, skipping' 37 return False 38 if path_elements[-3] == 'external_codes': 39 #print 'External code, skipping' 40 return False 41 except IndexError: 42 pass 43 44 if source_file.startswith('/usr/'): 45 return False 46 47 return True 48 49def remove_unwanted_file(gcov, fname, dir_base): 50 if keep_file(gcov): 51 if get_total_covered_lines(gcov).covered > 0: 52 return True 53 54 55 if fname.endswith('.gcov'): 56 os.unlink(os.path.join(dir_base,fname)) 57 else: 58 print('Filter error, attempting to remove a non-gcov file: ',fname) 59 return False 60 61# There are two locations for the source filename associated with each gcov 62# file. First, the actual filename, without the '.gcov' suffix. If the -p 63# option to gcov is used, the full path is contained in the filename, with 64# the directory separator replaced with '#'. 65# The second location is the 'Source:' tag inside the gcov file. 66 67 68def read_and_filter_gcov_files(fnames, directory): 69 new_fnames = set() 70 gcov_map = dict() 71 for fname in fnames: 72 gcov = read_gcov.read_gcov(os.path.join(directory, fname)) 73 keep = remove_unwanted_file(gcov, fname, directory) 74 if keep: 75 source_file = gcov.tags['Source'] 76 new_fnames.add(fname) 77 gcov_map[fname] = gcov 78 79 return new_fnames, gcov_map 80 81def merge_gcov_files_in_dir(fnames, directory, output_dir=None, src_prefix=None): 82 to_merge = defaultdict(list) 83 for fname in fnames: 84 # Files from 'gcov -l' have '##' to separate the two parts of the path 85 if '##' in fname: 86 original_name, src_name = fname.split('##') 87 to_merge[src_name].append(fname) 88 else: 89 to_merge[fname].append(fname) 90 91 if output_dir is None: 92 output_dir = '' 93 94 for output_fname, input_fnames in to_merge.items(): 95 inputs = [os.path.join(directory, fname) for fname in input_fnames] 96 merge_gcov.merge_gcov_files(inputs, os.path.join(output_dir,output_fname), 97 src_prefix_to_add=src_prefix) 98 99 100def compare_gcov_dirs(dir_base, dir_unit): 101 base = set(get_gcov_files(dir_base)) 102 unit = set(get_gcov_files(dir_unit)) 103 104 base_names, base_gcov_map = read_and_filter_gcov_files(base, dir_base) 105 unit_names, unit_gcov_map = read_and_filter_gcov_files(unit, dir_unit) 106 107 both = unit.intersection(base_names) 108 only_base = base_names.difference(unit_names) 109 only_unit = unit_names.difference(base_names) 110 111 print('Files in both: ',len(both)) 112 print('Files only in base: ',len(only_base)) 113 print('Files only in unit: ',len(only_unit)) 114 115 return both, only_base, only_unit, base_gcov_map, unit_gcov_map 116 117 118def compare_gcov_files(both, only_base, only_unit, base_gcov_map, unit_gcov_map, dir_unit, dir_diff): 119 for fname in both: 120 gcov_base = base_gcov_map[fname] 121 gcov_unit = unit_gcov_map[fname] 122 gcov_diff = compute_gcov_diff(gcov_base, gcov_unit) 123 out_fname = os.path.join(dir_diff, fname) 124 if gcov_diff: 125 read_gcov.write_gcov(gcov_diff, open(out_fname, 'w')) 126 127 # completely uncovered in unit tests 128 # Assign all to unit tests 129 # Assign to diff only if some coverage in base 130 for fname in only_base: 131 gcov_base = base_gcov_map[fname] 132 handle_uncovered(gcov_base, fname, dir_diff) 133 handle_uncovered(gcov_base, fname, dir_unit, always_copy=True) 134 135def handle_uncovered(gcov_base, fname, dir_diff, always_copy=False): 136 # Need to see if there are any covered lines in the file 137 file_coverage = get_total_covered_lines(gcov_base) 138 if always_copy or file_coverage.covered > 0: 139 gcov_diff = mark_as_uncovered(gcov_base) 140 out_fname = os.path.join(dir_diff, fname) 141 if gcov_diff: 142 read_gcov.write_gcov(gcov_diff, open(out_fname, 'w')) 143 144def mark_as_uncovered(gcov_base): 145 diff_line_info = OrderedDict() 146 for line in gcov_base.line_info.keys(): 147 base_line = gcov_base.line_info[line] 148 Uncov_norm= '#####' 149 Nocode = '-' 150 151 diff_count = base_line.count 152 try: 153 base_count = int(base_line.count) 154 diff_count = Uncov_norm 155 except ValueError: 156 if base_line.count == Uncov_norm: 157 diff_count = Nocode 158 159 160 diff_line_info[line] = read_gcov.LineInfo(diff_count, line, base_line.src) 161 162 return read_gcov.GcovFile(gcov_base.fname, gcov_base.tags, diff_line_info, None, None, None, gcov_base.func_ranges) 163 164 165class FileCoverage: 166 def __init__(self, covered=0, uncovered=0, total=0): 167 self.covered = covered 168 self.uncovered = uncovered 169 self.total = total 170 171 def __add__(self, o): 172 return FileCoverage(self.covered + o.covered, self.uncovered + o.uncovered, self.total + o.total) 173 174 175class CompareCoverage: 176 def __init__(self, base=FileCoverage(), unit=FileCoverage(), rel=FileCoverage()): 177 self.base = base 178 self.unit = unit 179 self.rel = rel 180 181 def __add__(self, o): 182 return CompareCoverage(self.base + o.base, self.unit + o.unit, self.rel + o.rel) 183 184 185def get_total_covered_lines(gcov): 186 if not keep_file(gcov): 187 return FileCoverage(0, 0, 0) 188 189 total_covered_lines = 0 190 total_uncovered_lines = 0 191 total_lines = 0 192 193 Nocode = '-' 194 195 for line in gcov.line_info.keys(): 196 base_line = gcov.line_info[line] 197 198 199 base_count = 0 200 try: 201 base_count = int(base_line.count) 202 except ValueError: 203 pass 204 205 use_line = True 206 if gcov.func_ranges: 207 funcs = gcov.func_ranges.find_func(line) 208 for func_name in funcs: 209 if func_name: 210 if func_name.startswith("_GLOBAL__"): 211 use_line = False 212 base_count = 0 213 if 'static_initialization_and_destruction' in func_name: 214 use_line = False 215 base_count = 0 216 217 if use_line: 218 if base_line != Nocode: 219 total_lines += 1 220 221 if base_count == 0: 222 total_uncovered_lines += 1 223 else: 224 total_covered_lines += 1 225 226 return FileCoverage(total_covered_lines, total_uncovered_lines, total_lines) 227 228 229def compute_gcov_diff(gcov_base, gcov_unit, print_diff=False, print_diff_summary=False, coverage_stats=None): 230 diff_line_info = OrderedDict() 231 if print_diff or print_diff_summary: 232 print('file ',gcov_base.tags['Source']) 233 234 total_base_count = 0 235 236 base_totals_total = 0 237 base_totals_covered = 0 238 base_totals_uncovered = 0 239 240 unit_totals_total = 0 241 unit_totals_covered = 0 242 unit_totals_uncovered = 0 243 unit_totals_rel_covered = 0 244 unit_totals_rel_uncovered = 0 245 246 for line in gcov_base.line_info.keys(): 247 if line not in gcov_unit.line_info: 248 print('error, line not present: %d ,line=%s'%(line, gcov_base.line_info[line])) 249 base_line = gcov_base.line_info[line] 250 unit_line = gcov_unit.line_info[line] 251 252 Uncov_norm = '#####' 253 Uncov_exp = '=====' 254 Uncov = [Uncov_norm, Uncov_exp] 255 Nocode = '-' 256 diff_count = None 257 base_count = 0 258 try: 259 base_count = int(base_line.count) 260 except ValueError: 261 pass 262 263 unit_count = 0 264 try: 265 unit_count = int(unit_line.count) 266 except ValueError: 267 pass 268 269 # Skip some compiler-added functions 270 # Assuming base and unit are the same 271 if gcov_base.func_ranges: 272 funcs = gcov_base.func_ranges.find_func(line) 273 for func_name in funcs: 274 if func_name: 275 if func_name.startswith("_GLOBAL__"): 276 base_count = 0 277 unit_count = 0 278 if 'static_initialization_and_destruction' in func_name: 279 base_count = 0 280 unit_count = 0 281 282 total_base_count += base_count 283 284 285 if base_line.count != Nocode: 286 base_totals_total += 1 287 if base_line.count in Uncov: 288 base_totals_uncovered += 1 289 if base_count > 0: 290 base_totals_covered += 1 291 292 if unit_line.count != Nocode: 293 unit_totals_total += 1 294 if unit_line.count in Uncov: 295 unit_totals_uncovered += 1 296 if unit_count > 0: 297 unit_totals_covered += 1 298 299 line_has_diff = False 300 301 # base_line.count is the string value for the count 302 # base_count is the converted integer value 303 # will be 0 for No code or uncovered 304 305 if base_line.count == Nocode and unit_line.count == Nocode: 306 diff_count = Nocode 307 308 # doesn't work well if one side is nocode and the other has count 0 309 #elif base_line.count == Nocode and unit_line.count != Nocode: 310 # diff_count = Nocode 311 # line_has_diff = True 312 #elif base_line.count != Nocode and unit_line.count == Nocode: 313 # diff_count = Nocode 314 # line_has_diff = True 315 316 elif base_line.count == Nocode and unit_count == 0: 317 diff_count = Nocode 318 elif base_count == 0 and unit_line.count == Nocode: 319 diff_count = Nocode 320 321 elif base_line.count in Uncov and unit_line.count in Uncov: 322 #diff_count = 9999 # special count to indicate uncovered in base? 323 diff_count = Nocode # Not sure of the right solution 324 325 elif base_count != 0 and unit_count == 0: 326 diff_count = Uncov_norm 327 unit_totals_rel_uncovered += 1 328 line_has_diff = True 329 elif base_count == 0 and unit_count != 0: 330 diff_count = Nocode 331 line_has_diff = True 332 elif base_count != 0 and unit_count != 0: 333 diff_count = unit_count 334 unit_totals_rel_covered += 1 335 elif base_count == 0 and unit_count == 0: 336 diff_count = 0 337 338 if print_diff and line_has_diff: 339 if gcov_base.line_info[line].src.strip() != gcov_unit.line_info[line].src.strip(): 340 print('line diff, base: ',gcov_base.line_info[line]) 341 print(' unit: ',gcov_unit.line_info[line]) 342 print('%9s %9s %6d : %s'%(base_line.count, unit_line.count, line, gcov_unit.line_info[line].src)) 343 if diff_count is None: 344 print('Unhandled case: ',line,diff_count,base_line.count,unit_line.count) 345 346 diff_line_info[line] = read_gcov.LineInfo(diff_count, line, base_line.src) 347 348 if print_diff_summary: 349 print('Base Unit') 350 print('%4d/%-4d %4d/%-4d covered lines'%(base_totals_covered, base_totals_total, unit_totals_covered, unit_totals_total)) 351 print('%4d/%-4d %4d/%-4d uncovered lines'%(base_totals_uncovered, base_totals_total, unit_totals_uncovered, unit_totals_total)) 352 print(' %4d/%-4d uncovered lines relative to base'%(unit_totals_rel_uncovered, (unit_totals_rel_uncovered+unit_totals_rel_covered))) 353 354 355 if coverage_stats is not None: 356 base_coverage = FileCoverage(base_totals_covered, base_totals_uncovered, base_totals_total) 357 unit_coverage = FileCoverage(unit_totals_covered, unit_totals_uncovered, unit_totals_total) 358 rel_coverage = FileCoverage(unit_totals_rel_covered, unit_totals_rel_uncovered, unit_totals_rel_covered + unit_totals_rel_uncovered) 359 360 cc = CompareCoverage(base_coverage, unit_coverage, rel_coverage) 361 coverage_stats[gcov_base.tags['Source']] = cc 362 363 if total_base_count == 0: 364 return None 365 366 return read_gcov.GcovFile(gcov_base.fname, gcov_base.tags, diff_line_info, None, None, None, gcov_base.func_ranges) 367 368 369FunctionCoverageInfo = namedtuple('FunctionCoverageInfo',['uncovered','total','names']) 370 371def compute_gcov_func_diff(gcov_base, gcov_unit, print_diff=False, print_diff_summary=False, func_coverage_stats=None): 372 if print_diff or print_diff_summary: 373 print('file ',gcov_base.tags['Source']) 374 375 uncovered_funcs = [] 376 nfunc = 0 377 for func_line in gcov_base.function_info.keys(): 378 base_func = gcov_base.function_info[func_line] 379 unit_func = gcov_unit.function_info[func_line] 380 381 nfunc += len(base_func) 382 383 if len(unit_func) == 0 : 384 for idx in range(len(base_func)): 385 uncovered_funcs.append(base_func[idx].name) 386 # function summary not even printed. Seems to be an issue with templates 387 continue 388 for idx in range(min(len(base_func), len(unit_func))): 389 if base_func[idx].called_count > 0 and unit_func[idx].called_count == 0: 390 uncovered_funcs.append(gcov_base.function_info[func_line][idx].name) 391 392 nfunc_uncovered = len(uncovered_funcs) 393 if func_coverage_stats is not None: 394 func_coverage_stats[gcov_base.tags['Source']] = FunctionCoverageInfo(nfunc_uncovered,nfunc,uncovered_funcs) 395 396 397def print_function_coverage(func_coverage): 398 print('Function coverage') 399 for name,fc in func_coverage.items(): 400 print(name,fc.total,fc.uncovered) 401 for func in demangle.demangle(fc.names): 402 print(' ',func) 403 404def by_uncovered(x,y): 405 if x[1].uncovered == y[1].uncovered: 406 return 0; 407 if x[1].uncovered > y[1].uncovered: 408 return -1 409 return 1 410 411 412def print_coverage_summary(coverage_stats): 413 sorted_keys = sorted(coverage_stats.items(), cmp=by_uncovered) 414 415 for source,stats in sorted_keys: 416 percent = 0 417 418 if stats.total > 0: 419 percent = 100.0*stats.covered/stats.total 420 print(source,'%4d/%-4d'%(stats.covered,stats.total),stats.uncovered,'%.2f'%percent) 421 422def summarize_coverage_summary(coverage_stats): 423 by_dirs = defaultdict(FileCoverage) 424 for source, stats in coverage_stats.items(): 425 src = source 426 while True: 427 dirname = os.path.dirname(src) 428 if stats.total > 0: 429 #by_dirs[dirname] += stats 430 by_dirs[dirname] += stats 431 if dirname == '': 432 break 433 if os.path.basename(src) in ['src','/','build']: 434 break 435 if src == '/': 436 break 437 src = dirname 438 439 print('\n by Dir \n') 440 ordered = OrderedDict() 441 for k in sorted(by_dirs.keys()): 442 ordered[k] = by_dirs[k] 443 print_coverage_summary(ordered) 444 445def compare_gcov(fname, dir_base, dir_unit, dir_diff=None): 446 gcov_base = read_gcov.read_gcov(os.path.join(dir_base, fname)) 447 gcov_unit = read_gcov.read_gcov(os.path.join(dir_unit, fname)) 448 gcov_diff = compute_gcov_diff(gcov_base, gcov_unit) 449 if dir_diff and gcov_diff: 450 out_fname = os.path.join(dir_diff, fname) 451 read_gcov.write_gcov(gcov_diff, open(out_fname, 'w')) 452 453def print_gcov_diff(both, only_base, only_unit, base_gcov_map, unit_gcov_map): 454 coverage_summary = dict() 455 func_coverage_summary = dict() 456 for fname in both: 457 gcov_base = base_gcov_map[fname] 458 gcov_unit = unit_gcov_map[fname] 459 compute_gcov_diff(gcov_base, gcov_unit, print_diff_summary=False, print_diff=False, coverage_stats=coverage_summary) 460 461 #print 'Function Info' 462 compute_gcov_func_diff(gcov_base, gcov_unit, print_diff=False, func_coverage_stats=func_coverage_summary) 463 464 print('\nCompletely uncovered files (relative to base)\n') 465 for fname in only_base: 466 gcov_base = base_gcov_map[fname] 467 base_coverage = get_total_covered_lines(gcov_base) 468 if base_coverage.covered > 0: 469 unit_coverage = FileCoverage(0, 0, 0) 470 rel_coverage = FileCoverage(0, base_coverage.covered, base_coverage.covered) 471 src_name = gcov_base.tags['Source'] 472 if True: 473 print(src_name,'%4d/%-4d'%(base_coverage.covered, base_coverage.total)) 474 475 coverage_summary[src_name] = CompareCoverage(base_coverage, unit_coverage, rel_coverage) 476 477 rel_coverage_summary = {src_name:x.rel for src_name,x in coverage_summary.items()} 478 print_coverage_summary(rel_coverage_summary) 479 summarize_coverage_summary(rel_coverage_summary) 480 481 482 func_coverage_summary2 = {src_name:FileCoverage(x.total-x.uncovered, x.uncovered, x.total) for src_name,x in func_coverage_summary.items()} 483 print_function_coverage(func_coverage_summary) 484 summarize_coverage_summary(func_coverage_summary2) 485 486 487 488if __name__ == '__main__': 489 parser = argparse.ArgumentParser(description="Compare GCOV files") 490 491 # Compare gcov files in base and unit directories and write results to output directory 492 # This is used to generate the gcov files for the CDash report 493 compare_action = ['compare','c'] 494 495 # Just delete unwanted files from directory 496 process_action = ['process','p'] 497 498 # Compare gcov files in base and unit directories and print results 499 diff_action = ['diff','d'] 500 501 # Merge files with same source (from gcov -l) 502 merge_action = ['merge','m'] 503 504 actions = compare_action + process_action + diff_action + merge_action 505 506 parser.add_argument('-a','--action',default='compare',choices=actions) 507 parser.add_argument('--base-dir', 508 required=True, 509 help="Directory containing the input base gcov files") 510 parser.add_argument('--unit-dir', 511 help="Directory containing the input unit test (target) gcov files") 512 parser.add_argument('--output-dir', 513 help="Directory to write the difference gcov files") 514 parser.add_argument('-f','--file', 515 help="Limit analysis to single file") 516 parser.add_argument('-p','--prefix', 517 help="Source prefix to add when merging files") 518 args = parser.parse_args() 519 520 if args.action in compare_action: 521 if not args.unit_dir: 522 print('--unit-dir required for compare') 523 sys.exit(1) 524 525 if not args.output_dir: 526 print('--output-dir required for compare') 527 sys.exit(1) 528 529 both, only_base, only_unit, base_gcov_map, unit_gcov_map = compare_gcov_dirs(args.base_dir, args.unit_dir) 530 compare_gcov_files(both, only_base, only_unit, base_gcov_map, unit_gcov_map, args.unit_dir, args.output_dir) 531 532 if args.action in process_action: 533 base = set(get_gcov_files(args.base_dir)) 534 read_and_filter_gcov_files(base, args.base_dir) 535 536 if args.action in merge_action: 537 base = set(get_gcov_files(args.base_dir)) 538 merge_gcov_files_in_dir(base, args.base_dir, args.output_dir, args.prefix) 539 540 if args.action in diff_action: 541 if not args.unit_dir: 542 print('--unit-dir required for diff') 543 sys.exit(1) 544 545 if args.file: 546 compare_gcov(args.file, args.base_dir, args.unit_dir) 547 else: 548 both, only_base, only_unit, base_gcov_map, unit_gcov_map = compare_gcov_dirs(args.base_dir, args.unit_dir) 549 550 print_gcov_diff(both, only_base, only_unit, base_gcov_map, unit_gcov_map) 551