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