1#!/usr/bin/env python3 2# 3# Script to find coverage info after running tests. 4# 5# Example: 6# ./scripts/cov.py \ 7# lfs.t.a.gcda lfs_util.t.a.gcda \ 8# -Flfs.c -Flfs_util.c -slines 9# 10# Copyright (c) 2022, The littlefs authors. 11# Copyright (c) 2020, Arm Limited. All rights reserved. 12# SPDX-License-Identifier: BSD-3-Clause 13# 14 15import collections as co 16import csv 17import itertools as it 18import json 19import math as m 20import os 21import re 22import shlex 23import subprocess as sp 24 25# TODO use explode_asserts to avoid counting assert branches? 26# TODO use dwarf=info to find functions for inline functions? 27 28GCOV_PATH = ['gcov'] 29 30 31# integer fields 32class Int(co.namedtuple('Int', 'x')): 33 __slots__ = () 34 def __new__(cls, x=0): 35 if isinstance(x, Int): 36 return x 37 if isinstance(x, str): 38 try: 39 x = int(x, 0) 40 except ValueError: 41 # also accept +-∞ and +-inf 42 if re.match('^\s*\+?\s*(?:∞|inf)\s*$', x): 43 x = m.inf 44 elif re.match('^\s*-\s*(?:∞|inf)\s*$', x): 45 x = -m.inf 46 else: 47 raise 48 assert isinstance(x, int) or m.isinf(x), x 49 return super().__new__(cls, x) 50 51 def __str__(self): 52 if self.x == m.inf: 53 return '∞' 54 elif self.x == -m.inf: 55 return '-∞' 56 else: 57 return str(self.x) 58 59 def __int__(self): 60 assert not m.isinf(self.x) 61 return self.x 62 63 def __float__(self): 64 return float(self.x) 65 66 none = '%7s' % '-' 67 def table(self): 68 return '%7s' % (self,) 69 70 diff_none = '%7s' % '-' 71 diff_table = table 72 73 def diff_diff(self, other): 74 new = self.x if self else 0 75 old = other.x if other else 0 76 diff = new - old 77 if diff == +m.inf: 78 return '%7s' % '+∞' 79 elif diff == -m.inf: 80 return '%7s' % '-∞' 81 else: 82 return '%+7d' % diff 83 84 def ratio(self, other): 85 new = self.x if self else 0 86 old = other.x if other else 0 87 if m.isinf(new) and m.isinf(old): 88 return 0.0 89 elif m.isinf(new): 90 return +m.inf 91 elif m.isinf(old): 92 return -m.inf 93 elif not old and not new: 94 return 0.0 95 elif not old: 96 return 1.0 97 else: 98 return (new-old) / old 99 100 def __add__(self, other): 101 return self.__class__(self.x + other.x) 102 103 def __sub__(self, other): 104 return self.__class__(self.x - other.x) 105 106 def __mul__(self, other): 107 return self.__class__(self.x * other.x) 108 109# fractional fields, a/b 110class Frac(co.namedtuple('Frac', 'a,b')): 111 __slots__ = () 112 def __new__(cls, a=0, b=None): 113 if isinstance(a, Frac) and b is None: 114 return a 115 if isinstance(a, str) and b is None: 116 a, b = a.split('/', 1) 117 if b is None: 118 b = a 119 return super().__new__(cls, Int(a), Int(b)) 120 121 def __str__(self): 122 return '%s/%s' % (self.a, self.b) 123 124 def __float__(self): 125 return float(self.a) 126 127 none = '%11s %7s' % ('-', '-') 128 def table(self): 129 t = self.a.x/self.b.x if self.b.x else 1.0 130 return '%11s %7s' % ( 131 self, 132 '∞%' if t == +m.inf 133 else '-∞%' if t == -m.inf 134 else '%.1f%%' % (100*t)) 135 136 diff_none = '%11s' % '-' 137 def diff_table(self): 138 return '%11s' % (self,) 139 140 def diff_diff(self, other): 141 new_a, new_b = self if self else (Int(0), Int(0)) 142 old_a, old_b = other if other else (Int(0), Int(0)) 143 return '%11s' % ('%s/%s' % ( 144 new_a.diff_diff(old_a).strip(), 145 new_b.diff_diff(old_b).strip())) 146 147 def ratio(self, other): 148 new_a, new_b = self if self else (Int(0), Int(0)) 149 old_a, old_b = other if other else (Int(0), Int(0)) 150 new = new_a.x/new_b.x if new_b.x else 1.0 151 old = old_a.x/old_b.x if old_b.x else 1.0 152 return new - old 153 154 def __add__(self, other): 155 return self.__class__(self.a + other.a, self.b + other.b) 156 157 def __sub__(self, other): 158 return self.__class__(self.a - other.a, self.b - other.b) 159 160 def __mul__(self, other): 161 return self.__class__(self.a * other.a, self.b + other.b) 162 163 def __lt__(self, other): 164 self_t = self.a.x/self.b.x if self.b.x else 1.0 165 other_t = other.a.x/other.b.x if other.b.x else 1.0 166 return (self_t, self.a.x) < (other_t, other.a.x) 167 168 def __gt__(self, other): 169 return self.__class__.__lt__(other, self) 170 171 def __le__(self, other): 172 return not self.__gt__(other) 173 174 def __ge__(self, other): 175 return not self.__lt__(other) 176 177# coverage results 178class CovResult(co.namedtuple('CovResult', [ 179 'file', 'function', 'line', 180 'calls', 'hits', 'funcs', 'lines', 'branches'])): 181 _by = ['file', 'function', 'line'] 182 _fields = ['calls', 'hits', 'funcs', 'lines', 'branches'] 183 _sort = ['funcs', 'lines', 'branches', 'hits', 'calls'] 184 _types = { 185 'calls': Int, 'hits': Int, 186 'funcs': Frac, 'lines': Frac, 'branches': Frac} 187 188 __slots__ = () 189 def __new__(cls, file='', function='', line=0, 190 calls=0, hits=0, funcs=0, lines=0, branches=0): 191 return super().__new__(cls, file, function, int(Int(line)), 192 Int(calls), Int(hits), Frac(funcs), Frac(lines), Frac(branches)) 193 194 def __add__(self, other): 195 return CovResult(self.file, self.function, self.line, 196 max(self.calls, other.calls), 197 max(self.hits, other.hits), 198 self.funcs + other.funcs, 199 self.lines + other.lines, 200 self.branches + other.branches) 201 202 203def openio(path, mode='r', buffering=-1): 204 # allow '-' for stdin/stdout 205 if path == '-': 206 if mode == 'r': 207 return os.fdopen(os.dup(sys.stdin.fileno()), mode, buffering) 208 else: 209 return os.fdopen(os.dup(sys.stdout.fileno()), mode, buffering) 210 else: 211 return open(path, mode, buffering) 212 213def collect(gcda_paths, *, 214 gcov_path=GCOV_PATH, 215 sources=None, 216 everything=False, 217 **args): 218 results = [] 219 for path in gcda_paths: 220 # get coverage info through gcov's json output 221 # note, gcov-path may contain extra args 222 cmd = GCOV_PATH + ['-b', '-t', '--json-format', path] 223 if args.get('verbose'): 224 print(' '.join(shlex.quote(c) for c in cmd)) 225 proc = sp.Popen(cmd, 226 stdout=sp.PIPE, 227 stderr=sp.PIPE if not args.get('verbose') else None, 228 universal_newlines=True, 229 errors='replace', 230 close_fds=False) 231 data = json.load(proc.stdout) 232 proc.wait() 233 if proc.returncode != 0: 234 if not args.get('verbose'): 235 for line in proc.stderr: 236 sys.stdout.write(line) 237 sys.exit(-1) 238 239 # collect line/branch coverage 240 for file in data['files']: 241 # ignore filtered sources 242 if sources is not None: 243 if not any( 244 os.path.abspath(file['file']) == os.path.abspath(s) 245 for s in sources): 246 continue 247 else: 248 # default to only cwd 249 if not everything and not os.path.commonpath([ 250 os.getcwd(), 251 os.path.abspath(file['file'])]) == os.getcwd(): 252 continue 253 254 # simplify path 255 if os.path.commonpath([ 256 os.getcwd(), 257 os.path.abspath(file['file'])]) == os.getcwd(): 258 file_name = os.path.relpath(file['file']) 259 else: 260 file_name = os.path.abspath(file['file']) 261 262 for func in file['functions']: 263 func_name = func.get('name', '(inlined)') 264 # discard internal functions (this includes injected test cases) 265 if not everything: 266 if func_name.startswith('__'): 267 continue 268 269 # go ahead and add functions, later folding will merge this if 270 # there are other hits on this line 271 results.append(CovResult( 272 file_name, func_name, func['start_line'], 273 func['execution_count'], 0, 274 Frac(1 if func['execution_count'] > 0 else 0, 1), 275 0, 276 0)) 277 278 for line in file['lines']: 279 func_name = line.get('function_name', '(inlined)') 280 # discard internal function (this includes injected test cases) 281 if not everything: 282 if func_name.startswith('__'): 283 continue 284 285 # go ahead and add lines, later folding will merge this if 286 # there are other hits on this line 287 results.append(CovResult( 288 file_name, func_name, line['line_number'], 289 0, line['count'], 290 0, 291 Frac(1 if line['count'] > 0 else 0, 1), 292 Frac( 293 sum(1 if branch['count'] > 0 else 0 294 for branch in line['branches']), 295 len(line['branches'])))) 296 297 return results 298 299 300def fold(Result, results, *, 301 by=None, 302 defines=None, 303 **_): 304 if by is None: 305 by = Result._by 306 307 for k in it.chain(by or [], (k for k, _ in defines or [])): 308 if k not in Result._by and k not in Result._fields: 309 print("error: could not find field %r?" % k) 310 sys.exit(-1) 311 312 # filter by matching defines 313 if defines is not None: 314 results_ = [] 315 for r in results: 316 if all(getattr(r, k) in vs for k, vs in defines): 317 results_.append(r) 318 results = results_ 319 320 # organize results into conflicts 321 folding = co.OrderedDict() 322 for r in results: 323 name = tuple(getattr(r, k) for k in by) 324 if name not in folding: 325 folding[name] = [] 326 folding[name].append(r) 327 328 # merge conflicts 329 folded = [] 330 for name, rs in folding.items(): 331 folded.append(sum(rs[1:], start=rs[0])) 332 333 return folded 334 335def table(Result, results, diff_results=None, *, 336 by=None, 337 fields=None, 338 sort=None, 339 summary=False, 340 all=False, 341 percent=False, 342 **_): 343 all_, all = all, __builtins__.all 344 345 if by is None: 346 by = Result._by 347 if fields is None: 348 fields = Result._fields 349 types = Result._types 350 351 # fold again 352 results = fold(Result, results, by=by) 353 if diff_results is not None: 354 diff_results = fold(Result, diff_results, by=by) 355 356 # organize by name 357 table = { 358 ','.join(str(getattr(r, k) or '') for k in by): r 359 for r in results} 360 diff_table = { 361 ','.join(str(getattr(r, k) or '') for k in by): r 362 for r in diff_results or []} 363 names = list(table.keys() | diff_table.keys()) 364 365 # sort again, now with diff info, note that python's sort is stable 366 names.sort() 367 if diff_results is not None: 368 names.sort(key=lambda n: tuple( 369 types[k].ratio( 370 getattr(table.get(n), k, None), 371 getattr(diff_table.get(n), k, None)) 372 for k in fields), 373 reverse=True) 374 if sort: 375 for k, reverse in reversed(sort): 376 names.sort( 377 key=lambda n: tuple( 378 (getattr(table[n], k),) 379 if getattr(table.get(n), k, None) is not None else () 380 for k in ([k] if k else [ 381 k for k in Result._sort if k in fields])), 382 reverse=reverse ^ (not k or k in Result._fields)) 383 384 385 # build up our lines 386 lines = [] 387 388 # header 389 header = [] 390 header.append('%s%s' % ( 391 ','.join(by), 392 ' (%d added, %d removed)' % ( 393 sum(1 for n in table if n not in diff_table), 394 sum(1 for n in diff_table if n not in table)) 395 if diff_results is not None and not percent else '') 396 if not summary else '') 397 if diff_results is None: 398 for k in fields: 399 header.append(k) 400 elif percent: 401 for k in fields: 402 header.append(k) 403 else: 404 for k in fields: 405 header.append('o'+k) 406 for k in fields: 407 header.append('n'+k) 408 for k in fields: 409 header.append('d'+k) 410 header.append('') 411 lines.append(header) 412 413 def table_entry(name, r, diff_r=None, ratios=[]): 414 entry = [] 415 entry.append(name) 416 if diff_results is None: 417 for k in fields: 418 entry.append(getattr(r, k).table() 419 if getattr(r, k, None) is not None 420 else types[k].none) 421 elif percent: 422 for k in fields: 423 entry.append(getattr(r, k).diff_table() 424 if getattr(r, k, None) is not None 425 else types[k].diff_none) 426 else: 427 for k in fields: 428 entry.append(getattr(diff_r, k).diff_table() 429 if getattr(diff_r, k, None) is not None 430 else types[k].diff_none) 431 for k in fields: 432 entry.append(getattr(r, k).diff_table() 433 if getattr(r, k, None) is not None 434 else types[k].diff_none) 435 for k in fields: 436 entry.append(types[k].diff_diff( 437 getattr(r, k, None), 438 getattr(diff_r, k, None))) 439 if diff_results is None: 440 entry.append('') 441 elif percent: 442 entry.append(' (%s)' % ', '.join( 443 '+∞%' if t == +m.inf 444 else '-∞%' if t == -m.inf 445 else '%+.1f%%' % (100*t) 446 for t in ratios)) 447 else: 448 entry.append(' (%s)' % ', '.join( 449 '+∞%' if t == +m.inf 450 else '-∞%' if t == -m.inf 451 else '%+.1f%%' % (100*t) 452 for t in ratios 453 if t) 454 if any(ratios) else '') 455 return entry 456 457 # entries 458 if not summary: 459 for name in names: 460 r = table.get(name) 461 if diff_results is None: 462 diff_r = None 463 ratios = None 464 else: 465 diff_r = diff_table.get(name) 466 ratios = [ 467 types[k].ratio( 468 getattr(r, k, None), 469 getattr(diff_r, k, None)) 470 for k in fields] 471 if not all_ and not any(ratios): 472 continue 473 lines.append(table_entry(name, r, diff_r, ratios)) 474 475 # total 476 r = next(iter(fold(Result, results, by=[])), None) 477 if diff_results is None: 478 diff_r = None 479 ratios = None 480 else: 481 diff_r = next(iter(fold(Result, diff_results, by=[])), None) 482 ratios = [ 483 types[k].ratio( 484 getattr(r, k, None), 485 getattr(diff_r, k, None)) 486 for k in fields] 487 lines.append(table_entry('TOTAL', r, diff_r, ratios)) 488 489 # find the best widths, note that column 0 contains the names and column -1 490 # the ratios, so those are handled a bit differently 491 widths = [ 492 ((max(it.chain([w], (len(l[i]) for l in lines)))+1+4-1)//4)*4-1 493 for w, i in zip( 494 it.chain([23], it.repeat(7)), 495 range(len(lines[0])-1))] 496 497 # print our table 498 for line in lines: 499 print('%-*s %s%s' % ( 500 widths[0], line[0], 501 ' '.join('%*s' % (w, x) 502 for w, x in zip(widths[1:], line[1:-1])), 503 line[-1])) 504 505 506def annotate(Result, results, *, 507 annotate=False, 508 lines=False, 509 branches=False, 510 **args): 511 # if neither branches/lines specified, color both 512 if annotate and not lines and not branches: 513 lines, branches = True, True 514 515 for path in co.OrderedDict.fromkeys(r.file for r in results).keys(): 516 # flatten to line info 517 results = fold(Result, results, by=['file', 'line']) 518 table = {r.line: r for r in results if r.file == path} 519 520 # calculate spans to show 521 if not annotate: 522 spans = [] 523 last = None 524 func = None 525 for line, r in sorted(table.items()): 526 if ((lines and int(r.hits) == 0) 527 or (branches and r.branches.a < r.branches.b)): 528 if last is not None and line - last.stop <= args['context']: 529 last = range( 530 last.start, 531 line+1+args['context']) 532 else: 533 if last is not None: 534 spans.append((last, func)) 535 last = range( 536 line-args['context'], 537 line+1+args['context']) 538 func = r.function 539 if last is not None: 540 spans.append((last, func)) 541 542 with open(path) as f: 543 skipped = False 544 for i, line in enumerate(f): 545 # skip lines not in spans? 546 if not annotate and not any(i+1 in s for s, _ in spans): 547 skipped = True 548 continue 549 550 if skipped: 551 skipped = False 552 print('%s@@ %s:%d: %s @@%s' % ( 553 '\x1b[36m' if args['color'] else '', 554 path, 555 i+1, 556 next(iter(f for _, f in spans)), 557 '\x1b[m' if args['color'] else '')) 558 559 # build line 560 if line.endswith('\n'): 561 line = line[:-1] 562 563 if i+1 in table: 564 r = table[i+1] 565 line = '%-*s // %s hits%s' % ( 566 args['width'], 567 line, 568 r.hits, 569 ', %s branches' % (r.branches,) 570 if int(r.branches.b) else '') 571 572 if args['color']: 573 if lines and int(r.hits) == 0: 574 line = '\x1b[1;31m%s\x1b[m' % line 575 elif branches and r.branches.a < r.branches.b: 576 line = '\x1b[35m%s\x1b[m' % line 577 578 print(line) 579 580 581def main(gcda_paths, *, 582 by=None, 583 fields=None, 584 defines=None, 585 sort=None, 586 hits=False, 587 **args): 588 # figure out what color should be 589 if args.get('color') == 'auto': 590 args['color'] = sys.stdout.isatty() 591 elif args.get('color') == 'always': 592 args['color'] = True 593 else: 594 args['color'] = False 595 596 # find sizes 597 if not args.get('use', None): 598 results = collect(gcda_paths, **args) 599 else: 600 results = [] 601 with openio(args['use']) as f: 602 reader = csv.DictReader(f, restval='') 603 for r in reader: 604 if not any('cov_'+k in r and r['cov_'+k].strip() 605 for k in CovResult._fields): 606 continue 607 try: 608 results.append(CovResult( 609 **{k: r[k] for k in CovResult._by 610 if k in r and r[k].strip()}, 611 **{k: r['cov_'+k] 612 for k in CovResult._fields 613 if 'cov_'+k in r 614 and r['cov_'+k].strip()})) 615 except TypeError: 616 pass 617 618 # fold 619 results = fold(CovResult, results, by=by, defines=defines) 620 621 # sort, note that python's sort is stable 622 results.sort() 623 if sort: 624 for k, reverse in reversed(sort): 625 results.sort( 626 key=lambda r: tuple( 627 (getattr(r, k),) if getattr(r, k) is not None else () 628 for k in ([k] if k else CovResult._sort)), 629 reverse=reverse ^ (not k or k in CovResult._fields)) 630 631 # write results to CSV 632 if args.get('output'): 633 with openio(args['output'], 'w') as f: 634 writer = csv.DictWriter(f, 635 (by if by is not None else CovResult._by) 636 + ['cov_'+k for k in ( 637 fields if fields is not None else CovResult._fields)]) 638 writer.writeheader() 639 for r in results: 640 writer.writerow( 641 {k: getattr(r, k) for k in ( 642 by if by is not None else CovResult._by)} 643 | {'cov_'+k: getattr(r, k) for k in ( 644 fields if fields is not None else CovResult._fields)}) 645 646 # find previous results? 647 if args.get('diff'): 648 diff_results = [] 649 try: 650 with openio(args['diff']) as f: 651 reader = csv.DictReader(f, restval='') 652 for r in reader: 653 if not any('cov_'+k in r and r['cov_'+k].strip() 654 for k in CovResult._fields): 655 continue 656 try: 657 diff_results.append(CovResult( 658 **{k: r[k] for k in CovResult._by 659 if k in r and r[k].strip()}, 660 **{k: r['cov_'+k] 661 for k in CovResult._fields 662 if 'cov_'+k in r 663 and r['cov_'+k].strip()})) 664 except TypeError: 665 pass 666 except FileNotFoundError: 667 pass 668 669 # fold 670 diff_results = fold(CovResult, diff_results, 671 by=by, defines=defines) 672 673 # print table 674 if not args.get('quiet'): 675 if (args.get('annotate') 676 or args.get('lines') 677 or args.get('branches')): 678 # annotate sources 679 annotate(CovResult, results, **args) 680 else: 681 # print table 682 table(CovResult, results, 683 diff_results if args.get('diff') else None, 684 by=by if by is not None else ['function'], 685 fields=fields if fields is not None 686 else ['lines', 'branches'] if not hits 687 else ['calls', 'hits'], 688 sort=sort, 689 **args) 690 691 # catch lack of coverage 692 if args.get('error_on_lines') and any( 693 r.lines.a < r.lines.b for r in results): 694 sys.exit(2) 695 elif args.get('error_on_branches') and any( 696 r.branches.a < r.branches.b for r in results): 697 sys.exit(3) 698 699 700if __name__ == "__main__": 701 import argparse 702 import sys 703 parser = argparse.ArgumentParser( 704 description="Find coverage info after running tests.", 705 allow_abbrev=False) 706 parser.add_argument( 707 'gcda_paths', 708 nargs='*', 709 help="Input *.gcda files.") 710 parser.add_argument( 711 '-v', '--verbose', 712 action='store_true', 713 help="Output commands that run behind the scenes.") 714 parser.add_argument( 715 '-q', '--quiet', 716 action='store_true', 717 help="Don't show anything, useful with -o.") 718 parser.add_argument( 719 '-o', '--output', 720 help="Specify CSV file to store results.") 721 parser.add_argument( 722 '-u', '--use', 723 help="Don't parse anything, use this CSV file.") 724 parser.add_argument( 725 '-d', '--diff', 726 help="Specify CSV file to diff against.") 727 parser.add_argument( 728 '-a', '--all', 729 action='store_true', 730 help="Show all, not just the ones that changed.") 731 parser.add_argument( 732 '-p', '--percent', 733 action='store_true', 734 help="Only show percentage change, not a full diff.") 735 parser.add_argument( 736 '-b', '--by', 737 action='append', 738 choices=CovResult._by, 739 help="Group by this field.") 740 parser.add_argument( 741 '-f', '--field', 742 dest='fields', 743 action='append', 744 choices=CovResult._fields, 745 help="Show this field.") 746 parser.add_argument( 747 '-D', '--define', 748 dest='defines', 749 action='append', 750 type=lambda x: (lambda k,v: (k, set(v.split(','))))(*x.split('=', 1)), 751 help="Only include results where this field is this value.") 752 class AppendSort(argparse.Action): 753 def __call__(self, parser, namespace, value, option): 754 if namespace.sort is None: 755 namespace.sort = [] 756 namespace.sort.append((value, True if option == '-S' else False)) 757 parser.add_argument( 758 '-s', '--sort', 759 nargs='?', 760 action=AppendSort, 761 help="Sort by this field.") 762 parser.add_argument( 763 '-S', '--reverse-sort', 764 nargs='?', 765 action=AppendSort, 766 help="Sort by this field, but backwards.") 767 parser.add_argument( 768 '-Y', '--summary', 769 action='store_true', 770 help="Only show the total.") 771 parser.add_argument( 772 '-F', '--source', 773 dest='sources', 774 action='append', 775 help="Only consider definitions in this file. Defaults to anything " 776 "in the current directory.") 777 parser.add_argument( 778 '--everything', 779 action='store_true', 780 help="Include builtin and libc specific symbols.") 781 parser.add_argument( 782 '--hits', 783 action='store_true', 784 help="Show total hits instead of coverage.") 785 parser.add_argument( 786 '-A', '--annotate', 787 action='store_true', 788 help="Show source files annotated with coverage info.") 789 parser.add_argument( 790 '-L', '--lines', 791 action='store_true', 792 help="Show uncovered lines.") 793 parser.add_argument( 794 '-B', '--branches', 795 action='store_true', 796 help="Show uncovered branches.") 797 parser.add_argument( 798 '-c', '--context', 799 type=lambda x: int(x, 0), 800 default=3, 801 help="Show n additional lines of context. Defaults to 3.") 802 parser.add_argument( 803 '-W', '--width', 804 type=lambda x: int(x, 0), 805 default=80, 806 help="Assume source is styled with this many columns. Defaults to 80.") 807 parser.add_argument( 808 '--color', 809 choices=['never', 'always', 'auto'], 810 default='auto', 811 help="When to use terminal colors. Defaults to 'auto'.") 812 parser.add_argument( 813 '-e', '--error-on-lines', 814 action='store_true', 815 help="Error if any lines are not covered.") 816 parser.add_argument( 817 '-E', '--error-on-branches', 818 action='store_true', 819 help="Error if any branches are not covered.") 820 parser.add_argument( 821 '--gcov-path', 822 default=GCOV_PATH, 823 type=lambda x: x.split(), 824 help="Path to the gcov executable, may include paths. " 825 "Defaults to %r." % GCOV_PATH) 826 sys.exit(main(**{k: v 827 for k, v in vars(parser.parse_intermixed_args()).items() 828 if v is not None})) 829