1# This Source Code Form is subject to the terms of the Mozilla Public 2# License, v. 2.0. If a copy of the MPL was not distributed with this 3# file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 5from __future__ import absolute_import, print_function 6 7from argparse import ArgumentParser 8import json 9import os 10 11try: 12 import urlparse 13except ImportError: 14 import urllib.parse as urlparse 15 16from six import viewitems 17 18from mozpack.chrome.manifest import parse_manifest 19import mozpack.path as mozpath 20from .manifest_handler import ChromeManifestHandler 21 22 23class LcovRecord(object): 24 __slots__ = ("test_name", 25 "source_file", 26 "functions", 27 "function_exec_counts", 28 "function_count", 29 "covered_function_count", 30 "branches", 31 "branch_count", 32 "covered_branch_count", 33 "lines", 34 "line_count", 35 "covered_line_count") 36 37 def __init__(self): 38 self.functions = {} 39 self.function_exec_counts = {} 40 self.branches = {} 41 self.lines = {} 42 43 def __iadd__(self, other): 44 45 # These shouldn't differ. 46 self.source_file = other.source_file 47 if hasattr(other, 'test_name'): 48 self.test_name = other.test_name 49 self.functions.update(other.functions) 50 51 for name, count in viewitems(other.function_exec_counts): 52 self.function_exec_counts[name] = count + self.function_exec_counts.get(name, 0) 53 54 for key, taken in viewitems(other.branches): 55 self.branches[key] = taken + self.branches.get(key, 0) 56 57 for line, (exec_count, checksum) in viewitems(other.lines): 58 new_exec_count = exec_count 59 if line in self.lines: 60 old_exec_count, _ = self.lines[line] 61 new_exec_count += old_exec_count 62 self.lines[line] = new_exec_count, checksum 63 64 self.resummarize() 65 return self 66 67 def resummarize(self): 68 # Re-calculate summaries after generating or splitting a record. 69 self.function_count = len(self.functions.keys()) 70 # Function records may have moved between files, so filter here. 71 self.function_exec_counts = { 72 fn_name: count for fn_name, count in viewitems(self.function_exec_counts) 73 if fn_name in self.functions.values()} 74 self.covered_function_count = len([c for c in self.function_exec_counts.values() if c]) 75 self.line_count = len(self.lines) 76 self.covered_line_count = len([c for c, _ in self.lines.values() if c]) 77 self.branch_count = len(self.branches) 78 self.covered_branch_count = len([c for c in self.branches.values() if c]) 79 80 81class RecordRewriter(object): 82 # Helper class for rewriting/spliting individual lcov records according 83 # to what the preprocessor did. 84 def __init__(self): 85 self._ranges = None 86 87 def _get_range(self, line): 88 for start, end in self._ranges: 89 if line < start: 90 return None 91 if line < end: 92 return start, end 93 return None 94 95 def _get_mapped_line(self, line, r): 96 inc_source, inc_start = self._current_pp_info[r] 97 start, end = r 98 offs = line - start 99 return inc_start + offs 100 101 def _get_record(self, inc_source): 102 if inc_source in self._additions: 103 gen_rec = self._additions[inc_source] 104 else: 105 gen_rec = LcovRecord() 106 gen_rec.source_file = inc_source 107 self._additions[inc_source] = gen_rec 108 return gen_rec 109 110 def _rewrite_lines(self, record): 111 rewritten_lines = {} 112 for ln, line_info in viewitems(record.lines): 113 r = self._get_range(ln) 114 if r is None: 115 rewritten_lines[ln] = line_info 116 continue 117 new_ln = self._get_mapped_line(ln, r) 118 inc_source, _ = self._current_pp_info[r] 119 120 if inc_source != record.source_file: 121 gen_rec = self._get_record(inc_source) 122 gen_rec.lines[new_ln] = line_info 123 continue 124 125 # Move exec_count to the new lineno. 126 rewritten_lines[new_ln] = line_info 127 128 record.lines = rewritten_lines 129 130 def _rewrite_functions(self, record): 131 rewritten_fns = {} 132 133 # Sometimes we get multiple entries for a named function ("top-level", for 134 # instance). It's not clear the records that result are well-formed, but 135 # we act as though if a function has multiple FN's, the corresponding 136 # FNDA's are all the same. 137 for ln, fn_name in viewitems(record.functions): 138 r = self._get_range(ln) 139 if r is None: 140 rewritten_fns[ln] = fn_name 141 continue 142 new_ln = self._get_mapped_line(ln, r) 143 inc_source, _ = self._current_pp_info[r] 144 if inc_source != record.source_file: 145 gen_rec = self._get_record(inc_source) 146 gen_rec.functions[new_ln] = fn_name 147 if fn_name in record.function_exec_counts: 148 gen_rec.function_exec_counts[fn_name] = record.function_exec_counts[fn_name] 149 continue 150 rewritten_fns[new_ln] = fn_name 151 record.functions = rewritten_fns 152 153 def _rewrite_branches(self, record): 154 rewritten_branches = {} 155 for (ln, block_number, branch_number), taken in viewitems(record.branches): 156 r = self._get_range(ln) 157 if r is None: 158 rewritten_branches[ln, block_number, branch_number] = taken 159 continue 160 new_ln = self._get_mapped_line(ln, r) 161 inc_source, _ = self._current_pp_info[r] 162 if inc_source != record.source_file: 163 gen_rec = self._get_record(inc_source) 164 gen_rec.branches[(new_ln, block_number, branch_number)] = taken 165 continue 166 rewritten_branches[(new_ln, block_number, branch_number)] = taken 167 168 record.branches = rewritten_branches 169 170 def rewrite_record(self, record, pp_info): 171 # Rewrite the lines in the given record according to preprocessor info 172 # and split to additional records when pp_info has included file info. 173 self._current_pp_info = dict( 174 [(tuple([int(l) for l in k.split(',')]), v) for k, v in pp_info.items()]) 175 self._ranges = sorted(self._current_pp_info.keys()) 176 self._additions = {} 177 self._rewrite_lines(record) 178 self._rewrite_functions(record) 179 self._rewrite_branches(record) 180 181 record.resummarize() 182 183 generated_records = self._additions.values() 184 for r in generated_records: 185 r.resummarize() 186 return generated_records 187 188 189class LcovFile(object): 190 # Simple parser/pretty-printer for lcov format. 191 # lcov parsing based on http://ltp.sourceforge.net/coverage/lcov/geninfo.1.php 192 193 # TN:<test name> 194 # SF:<absolute path to the source file> 195 # FN:<line number of function start>,<function name> 196 # FNDA:<execution count>,<function name> 197 # FNF:<number of functions found> 198 # FNH:<number of function hit> 199 # BRDA:<line number>,<block number>,<branch number>,<taken> 200 # BRF:<number of branches found> 201 # BRH:<number of branches hit> 202 # DA:<line number>,<execution count>[,<checksum>] 203 # LF:<number of instrumented lines> 204 # LH:<number of lines with a non-zero execution count> 205 # end_of_record 206 PREFIX_TYPES = { 207 'TN': 0, 208 'SF': 0, 209 'FN': 1, 210 'FNDA': 1, 211 'FNF': 0, 212 'FNH': 0, 213 'BRDA': 3, 214 'BRF': 0, 215 'BRH': 0, 216 'DA': 2, 217 'LH': 0, 218 'LF': 0, 219 } 220 221 def __init__(self, lcov_paths): 222 self.lcov_paths = lcov_paths 223 224 def iterate_records(self, rewrite_source=None): 225 current_source_file = None 226 current_pp_info = None 227 current_lines = [] 228 for lcov_path in self.lcov_paths: 229 with open(lcov_path) as lcov_fh: 230 for line in lcov_fh: 231 line = line.rstrip() 232 if not line: 233 continue 234 235 if line == 'end_of_record': 236 # We skip records that we couldn't rewrite, that is records for which 237 # rewrite_url returns None. 238 if current_source_file is not None: 239 yield (current_source_file, current_pp_info, current_lines) 240 current_source_file = None 241 current_pp_info = None 242 current_lines = [] 243 continue 244 245 colon = line.find(':') 246 prefix = line[:colon] 247 248 if prefix == 'SF': 249 sf = line[(colon + 1):] 250 res = rewrite_source(sf) if rewrite_source is not None else (sf, None) 251 if res is None: 252 current_lines.append(line) 253 else: 254 current_source_file, current_pp_info = res 255 current_lines.append('SF:' + current_source_file) 256 else: 257 current_lines.append(line) 258 259 def parse_record(self, record_content): 260 self.current_record = LcovRecord() 261 262 for line in record_content: 263 colon = line.find(':') 264 265 prefix = line[:colon] 266 267 # We occasionally end up with multi-line scripts in data: 268 # uris that will trip up the parser, just skip them for now. 269 if colon < 0 or prefix not in self.PREFIX_TYPES: 270 continue 271 272 args = line[(colon + 1):].split(',', self.PREFIX_TYPES[prefix]) 273 274 def try_convert(a): 275 try: 276 return int(a) 277 except ValueError: 278 return a 279 args = [try_convert(a) for a in args] 280 281 try: 282 LcovFile.__dict__['parse_' + prefix](self, *args) 283 except ValueError: 284 print("Encountered an error in %s:\n%s" % 285 (self.lcov_fh.name, line)) 286 raise 287 except KeyError: 288 print("Invalid lcov line start in %s:\n%s" % 289 (self.lcov_fh.name, line)) 290 raise 291 except TypeError: 292 print("Invalid lcov line start in %s:\n%s" % 293 (self.lcov_fh.name, line)) 294 raise 295 296 ret = self.current_record 297 self.current_record = LcovRecord() 298 return ret 299 300 def print_file(self, fh, rewrite_source, rewrite_record): 301 for source_file, pp_info, record_content in self.iterate_records(rewrite_source): 302 if pp_info is not None: 303 record = self.parse_record(record_content) 304 for r in rewrite_record(record, pp_info): 305 fh.write(self.format_record(r)) 306 fh.write(self.format_record(record)) 307 else: 308 fh.write('\n'.join(record_content) + '\nend_of_record\n') 309 310 def format_record(self, record): 311 out_lines = [] 312 for name in LcovRecord.__slots__: 313 if hasattr(record, name): 314 out_lines.append(LcovFile.__dict__['format_' + name](self, record)) 315 return '\n'.join(out_lines) + '\nend_of_record\n' 316 317 def format_test_name(self, record): 318 return "TN:%s" % record.test_name 319 320 def format_source_file(self, record): 321 return "SF:%s" % record.source_file 322 323 def format_functions(self, record): 324 # Sorting results gives deterministic output (and is a lot faster than 325 # using OrderedDict). 326 fns = [] 327 for start_lineno, fn_name in sorted(viewitems(record.functions)): 328 fns.append('FN:%s,%s' % (start_lineno, fn_name)) 329 return '\n'.join(fns) 330 331 def format_function_exec_counts(self, record): 332 fndas = [] 333 for name, exec_count in sorted(viewitems(record.function_exec_counts)): 334 fndas.append('FNDA:%s,%s' % (exec_count, name)) 335 return '\n'.join(fndas) 336 337 def format_function_count(self, record): 338 return 'FNF:%s' % record.function_count 339 340 def format_covered_function_count(self, record): 341 return 'FNH:%s' % record.covered_function_count 342 343 def format_branches(self, record): 344 brdas = [] 345 for key in sorted(record.branches): 346 taken = record.branches[key] 347 taken = '-' if taken == 0 else taken 348 brdas.append('BRDA:%s' % 349 ','.join(map(str, list(key) + [taken]))) 350 return '\n'.join(brdas) 351 352 def format_branch_count(self, record): 353 return 'BRF:%s' % record.branch_count 354 355 def format_covered_branch_count(self, record): 356 return 'BRH:%s' % record.covered_branch_count 357 358 def format_lines(self, record): 359 das = [] 360 for line_no, (exec_count, checksum) in sorted(viewitems(record.lines)): 361 s = 'DA:%s,%s' % (line_no, exec_count) 362 if checksum: 363 s += ',%s' % checksum 364 das.append(s) 365 return '\n'.join(das) 366 367 def format_line_count(self, record): 368 return 'LF:%s' % record.line_count 369 370 def format_covered_line_count(self, record): 371 return 'LH:%s' % record.covered_line_count 372 373 def parse_TN(self, test_name): 374 self.current_record.test_name = test_name 375 376 def parse_SF(self, source_file): 377 self.current_record.source_file = source_file 378 379 def parse_FN(self, start_lineno, fn_name): 380 self.current_record.functions[start_lineno] = fn_name 381 382 def parse_FNDA(self, exec_count, fn_name): 383 self.current_record.function_exec_counts[fn_name] = exec_count 384 385 def parse_FNF(self, function_count): 386 self.current_record.function_count = function_count 387 388 def parse_FNH(self, covered_function_count): 389 self.current_record.covered_function_count = covered_function_count 390 391 def parse_BRDA(self, line_number, block_number, branch_number, taken): 392 taken = 0 if taken == '-' else taken 393 self.current_record.branches[(line_number, block_number, 394 branch_number)] = taken 395 396 def parse_BRF(self, branch_count): 397 self.current_record.branch_count = branch_count 398 399 def parse_BRH(self, covered_branch_count): 400 self.current_record.covered_branch_count = covered_branch_count 401 402 def parse_DA(self, line_number, execution_count, checksum=None): 403 self.current_record.lines[line_number] = (execution_count, checksum) 404 405 def parse_LH(self, covered_line_count): 406 self.current_record.covered_line_count = covered_line_count 407 408 def parse_LF(self, line_count): 409 self.current_record.line_count = line_count 410 411 412class UrlFinderError(Exception): 413 pass 414 415 416class UrlFinder(object): 417 # Given a "chrome://" or "resource://" url, uses data from the UrlMapBackend 418 # and install manifests to find a path to the source file and the corresponding 419 # (potentially pre-processed) file in the objdir. 420 def __init__(self, chrome_map_path, appdir, gredir, extra_chrome_manifests): 421 # Cached entries 422 self._final_mapping = {} 423 424 try: 425 with open(chrome_map_path) as fh: 426 url_prefixes, overrides, install_info, buildconfig = json.load(fh) 427 except IOError: 428 print("Error reading %s. Run |./mach build-backend -b ChromeMap| to " 429 "populate the ChromeMap backend." % chrome_map_path) 430 raise 431 432 self.topobjdir = buildconfig['topobjdir'] 433 self.MOZ_APP_NAME = buildconfig['MOZ_APP_NAME'] 434 self.OMNIJAR_NAME = buildconfig['OMNIJAR_NAME'] 435 436 # These are added dynamically in nsIResProtocolHandler, we might 437 # need to get them at run time. 438 if "resource:///" not in url_prefixes: 439 url_prefixes["resource:///"] = [appdir] 440 if "resource://gre/" not in url_prefixes: 441 url_prefixes["resource://gre/"] = [gredir] 442 443 self._url_prefixes = url_prefixes 444 self._url_overrides = overrides 445 446 self._respath = None 447 448 mac_bundle_name = buildconfig['MOZ_MACBUNDLE_NAME'] 449 if mac_bundle_name: 450 self._respath = mozpath.join('dist', 451 mac_bundle_name, 452 'Contents', 453 'Resources') 454 455 if not extra_chrome_manifests: 456 extra_path = os.path.join(self.topobjdir, '_tests', 'extra.manifest') 457 if os.path.isfile(extra_path): 458 extra_chrome_manifests = [extra_path] 459 460 if extra_chrome_manifests: 461 self._populate_chrome(extra_chrome_manifests) 462 463 self._install_mapping = install_info 464 465 def _populate_chrome(self, manifests): 466 handler = ChromeManifestHandler() 467 for m in manifests: 468 path = os.path.abspath(m) 469 for e in parse_manifest(None, path): 470 handler.handle_manifest_entry(e) 471 self._url_overrides.update(handler.overrides) 472 self._url_prefixes.update(handler.chrome_mapping) 473 474 def _find_install_prefix(self, objdir_path): 475 476 def _prefix(s): 477 for p in mozpath.split(s): 478 if '*' not in p: 479 yield p + '/' 480 481 offset = 0 482 for leaf in reversed(mozpath.split(objdir_path)): 483 offset += len(leaf) 484 if objdir_path[:-offset] in self._install_mapping: 485 pattern_prefix, is_pp = self._install_mapping[objdir_path[:-offset]] 486 full_leaf = objdir_path[len(objdir_path) - offset:] 487 src_prefix = ''.join(_prefix(pattern_prefix)) 488 self._install_mapping[objdir_path] = (mozpath.join(src_prefix, full_leaf), 489 is_pp) 490 break 491 offset += 1 492 493 def _install_info(self, objdir_path): 494 if objdir_path not in self._install_mapping: 495 # If our path is missing, some prefix of it may be in the install 496 # mapping mapped to a wildcard. 497 self._find_install_prefix(objdir_path) 498 if objdir_path not in self._install_mapping: 499 raise UrlFinderError("Couldn't find entry in manifest for %s" % 500 objdir_path) 501 return self._install_mapping[objdir_path] 502 503 def _abs_objdir_install_info(self, term): 504 obj_relpath = term[len(self.topobjdir) + 1:] 505 res = self._install_info(obj_relpath) 506 507 # Some urls on osx will refer to paths in the mac bundle, so we 508 # re-interpret them as being their original location in dist/bin. 509 if (not res and self._respath and 510 obj_relpath.startswith(self._respath)): 511 obj_relpath = obj_relpath.replace(self._respath, 'dist/bin') 512 res = self._install_info(obj_relpath) 513 514 if not res: 515 raise UrlFinderError("Couldn't find entry in manifest for %s" % 516 obj_relpath) 517 return res 518 519 def find_files(self, url): 520 # Returns a tuple of (source file, pp_info) 521 # for the given "resource:", "chrome:", or "file:" uri. 522 term = url 523 if term in self._url_overrides: 524 term = self._url_overrides[term] 525 526 if os.path.isabs(term) and term.startswith(self.topobjdir): 527 source_path, pp_info = self._abs_objdir_install_info(term) 528 return source_path, pp_info 529 530 for prefix, dests in viewitems(self._url_prefixes): 531 if term.startswith(prefix): 532 for dest in dests: 533 if not dest.endswith('/'): 534 dest += '/' 535 objdir_path = term.replace(prefix, dest) 536 537 while objdir_path.startswith('//'): 538 # The mochitest harness produces some wonky file:// uris 539 # that need to be fixed. 540 objdir_path = objdir_path[1:] 541 542 try: 543 if os.path.isabs(objdir_path) and objdir_path.startswith(self.topobjdir): 544 return self._abs_objdir_install_info(objdir_path) 545 else: 546 src_path, pp_info = self._install_info(objdir_path) 547 return mozpath.normpath(src_path), pp_info 548 except UrlFinderError: 549 pass 550 551 if (dest.startswith('resource://') or 552 dest.startswith('chrome://')): 553 result = self.find_files(term.replace(prefix, dest)) 554 if result: 555 return result 556 557 raise UrlFinderError("No objdir path for %s" % term) 558 559 def rewrite_url(self, url): 560 # This applies one-off rules and returns None for urls that we aren't 561 # going to be able to resolve to a source file ("about:" urls, for 562 # instance). 563 if url in self._final_mapping: 564 return self._final_mapping[url] 565 if url.endswith('> eval'): 566 return None 567 if url.endswith('> Function'): 568 return None 569 if ' -> ' in url: 570 url = url.split(' -> ')[1].rstrip() 571 if '?' in url: 572 url = url.split('?')[0] 573 574 url_obj = urlparse.urlparse(url) 575 if url_obj.scheme == 'jar': 576 app_name = self.MOZ_APP_NAME 577 omnijar_name = self.OMNIJAR_NAME 578 579 if app_name in url: 580 if omnijar_name in url: 581 # e.g. file:///home/worker/workspace/build/application/firefox/omni.ja!/components/MainProcessSingleton.js # noqa 582 parts = url_obj.path.split(omnijar_name + '!', 1) 583 elif '.xpi!' in url: 584 # e.g. file:///home/worker/workspace/build/application/firefox/browser/features/e10srollout@mozilla.org.xpi!/bootstrap.js # noqa 585 parts = url_obj.path.split('.xpi!', 1) 586 else: 587 # We don't know how to handle this jar: path, so return it to the 588 # caller to make it print a warning. 589 return url_obj.path, None 590 591 dir_parts = parts[0].rsplit(app_name + '/', 1) 592 url = mozpath.normpath( 593 mozpath.join(self.topobjdir, 'dist', 594 'bin', dir_parts[1].lstrip('/'), parts[1].lstrip('/')) 595 ) 596 elif '.xpi!' in url: 597 # This matching mechanism is quite brittle and based on examples seen in the wild. 598 # There's no rule to match the XPI name to the path in dist/xpi-stage. 599 parts = url_obj.path.split('.xpi!', 1) 600 addon_name = os.path.basename(parts[0]) 601 if '-test@mozilla.org' in addon_name: 602 addon_name = addon_name[:-len('-test@mozilla.org')] 603 elif addon_name.endswith('@mozilla.org'): 604 addon_name = addon_name[:-len('@mozilla.org')] 605 url = mozpath.normpath(mozpath.join(self.topobjdir, 'dist', 606 'xpi-stage', addon_name, parts[1].lstrip('/'))) 607 elif url_obj.scheme == 'file' and os.path.isabs(url_obj.path): 608 path = url_obj.path 609 if not os.path.isfile(path): 610 # This may have been in a profile directory that no 611 # longer exists. 612 return None 613 if not path.startswith(self.topobjdir): 614 return path, None 615 url = url_obj.path 616 elif url_obj.scheme in ('http', 'https', 'javascript', 'data', 'about'): 617 return None 618 619 result = self.find_files(url) 620 self._final_mapping[url] = result 621 return result 622 623 624class LcovFileRewriter(object): 625 # Class for partial parses of LCOV format and rewriting to resolve urls 626 # and preprocessed file lines. 627 def __init__(self, chrome_map_path, appdir='dist/bin/browser/', 628 gredir='dist/bin/', extra_chrome_manifests=[]): 629 self.url_finder = UrlFinder(chrome_map_path, appdir, gredir, extra_chrome_manifests) 630 self.pp_rewriter = RecordRewriter() 631 632 def rewrite_files(self, in_paths, output_file, output_suffix): 633 unknowns = set() 634 found_valid = [False] 635 636 def rewrite_source(url): 637 try: 638 res = self.url_finder.rewrite_url(url) 639 if res is None: 640 return None 641 except Exception as e: 642 if url not in unknowns: 643 print("Error: %s.\nCouldn't find source info for %s, removing record" % 644 (e, url)) 645 unknowns.add(url) 646 return None 647 648 source_file, pp_info = res 649 # We can't assert that the file exists here, because we don't have the source 650 # checkout available on test machines. We can bring back this assertion when 651 # bug 1432287 is fixed. 652 # assert os.path.isfile(source_file), "Couldn't find mapped source file %s at %s!" % ( 653 # url, source_file) 654 655 found_valid[0] = True 656 657 return res 658 659 in_paths = [os.path.abspath(in_path) for in_path in in_paths] 660 661 if output_file: 662 lcov_file = LcovFile(in_paths) 663 with open(output_file, 'w+') as out_fh: 664 lcov_file.print_file(out_fh, rewrite_source, self.pp_rewriter.rewrite_record) 665 else: 666 for in_path in in_paths: 667 lcov_file = LcovFile([in_path]) 668 with open(in_path + output_suffix, 'w+') as out_fh: 669 lcov_file.print_file(out_fh, rewrite_source, self.pp_rewriter.rewrite_record) 670 671 if not found_valid[0]: 672 print("WARNING: No valid records found in %s" % in_paths) 673 return 674 675 676def main(): 677 parser = ArgumentParser( 678 description="Given a set of gcov .info files produced " 679 "by spidermonkey's code coverage, re-maps file urls " 680 "back to source files and lines in preprocessed files " 681 "back to their original locations." 682 ) 683 parser.add_argument( 684 "--chrome-map-path", default="chrome-map.json", help="Path to the chrome-map.json file." 685 ) 686 parser.add_argument( 687 "--app-dir", 688 default="dist/bin/browser/", 689 help="Prefix of the appdir in use. This is used to map " 690 "urls starting with resource:///. It may differ by " 691 "app, but defaults to the valid value for firefox.", 692 ) 693 parser.add_argument( 694 "--gre-dir", 695 default="dist/bin/", 696 help="Prefix of the gre dir in use. This is used to map " 697 "urls starting with resource://gre. It may differ by " 698 "app, but defaults to the valid value for firefox.", 699 ) 700 parser.add_argument( 701 "--output-suffix", default=".out", help="The suffix to append to output files." 702 ) 703 parser.add_argument( 704 "--extra-chrome-manifests", 705 nargs='+', 706 help="Paths to files containing extra chrome registration.", 707 ) 708 parser.add_argument( 709 "--output-file", 710 default="", 711 help="The output file where the results are merged. Leave empty to make the rewriter not " 712 "merge files.", 713 ) 714 parser.add_argument("files", nargs='+', help="The set of files to process.") 715 716 args = parser.parse_args() 717 718 rewriter = LcovFileRewriter(args.chrome_map_path, args.app_dir, args.gre_dir, 719 args.extra_chrome_manifests) 720 721 files = [] 722 for f in args.files: 723 if os.path.isdir(f): 724 files += [os.path.join(f, e) for e in os.listdir(f)] 725 else: 726 files.append(f) 727 728 rewriter.rewrite_files(files, args.output_file, args.output_suffix) 729 730 731if __name__ == '__main__': 732 main() 733