1 """Command-line support for Coverage.""" 2 3 import optparse, re, sys, traceback 4 5 from coverage.backward import sorted # pylint: disable=W0622 6 from coverage.execfile import run_python_file, run_python_module 7 from coverage.misc import CoverageException, ExceptionDuringRun, NoSource 8 9 10 class Opts(object): 11 """A namespace class for individual options we'll build parsers from.""" 12 13 append = optparse.make_option( 14 '-a', '--append', action='store_false', dest="erase_first", 15 help="Append coverage data to .coverage, otherwise it is started " 16 "clean with each run." 17 ) 18 branch = optparse.make_option( 19 '', '--branch', action='store_true', 20 help="Measure branch coverage in addition to statement coverage." 21 ) 22 directory = optparse.make_option( 23 '-d', '--directory', action='store', 24 metavar="DIR", 25 help="Write the output files to DIR." 26 ) 27 help = optparse.make_option( 28 '-h', '--help', action='store_true', 29 help="Get help on this command." 30 ) 31 ignore_errors = optparse.make_option( 32 '-i', '--ignore-errors', action='store_true', 33 help="Ignore errors while reading source files." 34 ) 35 include = optparse.make_option( 36 '', '--include', action='store', 37 metavar="PAT1,PAT2,...", 38 help="Include files only when their filename path matches one of " 39 "these patterns. Usually needs quoting on the command line." 40 ) 41 pylib = optparse.make_option( 42 '-L', '--pylib', action='store_true', 43 help="Measure coverage even inside the Python installed library, " 44 "which isn't done by default." 45 ) 46 show_missing = optparse.make_option( 47 '-m', '--show-missing', action='store_true', 48 help="Show line numbers of statements in each module that weren't " 49 "executed." 50 ) 51 old_omit = optparse.make_option( 52 '-o', '--omit', action='store', 53 metavar="PAT1,PAT2,...", 54 help="Omit files when their filename matches one of these patterns. " 55 "Usually needs quoting on the command line." 56 ) 57 omit = optparse.make_option( 58 '', '--omit', action='store', 59 metavar="PAT1,PAT2,...", 60 help="Omit files when their filename matches one of these patterns. " 61 "Usually needs quoting on the command line." 62 ) 63 output_xml = optparse.make_option( 64 '-o', '', action='store', dest="outfile", 65 metavar="OUTFILE", 66 help="Write the XML report to this file. Defaults to 'coverage.xml'" 67 ) 68 parallel_mode = optparse.make_option( 69 '-p', '--parallel-mode', action='store_true', 70 help="Append the machine name, process id and random number to the " 71 ".coverage data file name to simplify collecting data from " 72 "many processes." 73 ) 74 module = optparse.make_option( 75 '-m', '--module', action='store_true', 76 help="<pyfile> is an importable Python module, not a script path, " 77 "to be run as 'python -m' would run it." 78 ) 79 rcfile = optparse.make_option( 80 '', '--rcfile', action='store', 81 help="Specify configuration file. Defaults to '.coveragerc'" 82 ) 83 source = optparse.make_option( 84 '', '--source', action='store', metavar="SRC1,SRC2,...", 85 help="A list of packages or directories of code to be measured." 86 ) 87 timid = optparse.make_option( 88 '', '--timid', action='store_true', 89 help="Use a simpler but slower trace method. Try this if you get " 90 "seemingly impossible results!" 91 ) 92 version = optparse.make_option( 93 '', '--version', action='store_true', 94 help="Display version information and exit." 95 ) 96 97 98 class CoverageOptionParser(optparse.OptionParser, object): 99 """Base OptionParser for coverage. 100 101 Problems don't exit the program. 102 Defaults are initialized for all options. 103 104 """ 105 106 def __init__(self, *args, **kwargs): 107 super(CoverageOptionParser, self).__init__( 108 add_help_option=False, *args, **kwargs 109 ) 110 self.set_defaults( 111 actions=[], 112 branch=None, 113 directory=None, 114 help=None, 115 ignore_errors=None, 116 include=None, 117 omit=None, 118 parallel_mode=None, 119 module=None, 120 pylib=None, 121 rcfile=True, 122 show_missing=None, 123 source=None, 124 timid=None, 125 erase_first=None, 126 version=None, 127 ) 128 129 self.disable_interspersed_args() 130 self.help_fn = self.help_noop 131 132 def help_noop(self, error=None, topic=None, parser=None): 133 """No-op help function.""" 134 pass 135 136 class OptionParserError(Exception): 137 """Used to stop the optparse error handler ending the process.""" 138 pass 139 140 def parse_args(self, args=None, options=None): 141 """Call optparse.parse_args, but return a triple: 142 143 (ok, options, args) 144 145 """ 146 try: 147 options, args = \ 148 super(CoverageOptionParser, self).parse_args(args, options) 149 except self.OptionParserError: 150 return False, None, None 151 return True, options, args 152 153 def error(self, msg): 154 """Override optparse.error so sys.exit doesn't get called.""" 155 self.help_fn(msg) 156 raise self.OptionParserError 157 158 159 class ClassicOptionParser(CoverageOptionParser): 160 """Command-line parser for coverage.py classic arguments.""" 161 162 def __init__(self): 163 super(ClassicOptionParser, self).__init__() 164 165 self.add_action('-a', '--annotate', 'annotate') 166 self.add_action('-b', '--html', 'html') 167 self.add_action('-c', '--combine', 'combine') 168 self.add_action('-e', '--erase', 'erase') 169 self.add_action('-r', '--report', 'report') 170 self.add_action('-x', '--execute', 'execute') 171 172 self.add_options([ 173 Opts.directory, 174 Opts.help, 175 Opts.ignore_errors, 176 Opts.pylib, 177 Opts.show_missing, 178 Opts.old_omit, 179 Opts.parallel_mode, 180 Opts.timid, 181 Opts.version, 182 ]) 183 184 def add_action(self, dash, dashdash, action_code): 185 """Add a specialized option that is the action to execute.""" 186 option = self.add_option(dash, dashdash, action='callback', 187 callback=self._append_action 188 ) 189 option.action_code = action_code 190 191 def _append_action(self, option, opt_unused, value_unused, parser): 192 """Callback for an option that adds to the `actions` list.""" 193 parser.values.actions.append(option.action_code) 194 195 196 class CmdOptionParser(CoverageOptionParser): 197 """Parse one of the new-style commands for coverage.py.""" 198 199 def __init__(self, action, options=None, defaults=None, usage=None, 200 cmd=None, description=None 201 ): 202 """Create an OptionParser for a coverage command. 203 204 `action` is the slug to put into `options.actions`. 205 `options` is a list of Option's for the command. 206 `defaults` is a dict of default value for options. 207 `usage` is the usage string to display in help. 208 `cmd` is the command name, if different than `action`. 209 `description` is the description of the command, for the help text. 210 211 """ 212 if usage: 213 usage = "%prog " + usage 214 super(CmdOptionParser, self).__init__( 215 prog="coverage %s" % (cmd or action), 216 usage=usage, 217 description=description, 218 ) 219 self.set_defaults(actions=[action], **(defaults or {})) 220 if options: 221 self.add_options(options) 222 self.cmd = cmd or action 223 224 def __eq__(self, other): 225 # A convenience equality, so that I can put strings in unit test 226 # results, and they will compare equal to objects. 227 return (other == "<CmdOptionParser:%s>" % self.cmd) 228 229 GLOBAL_ARGS = [ 230 Opts.rcfile, 231 Opts.help, 232 ] 233 234 CMDS = { 235 'annotate': CmdOptionParser("annotate", 236 [ 237 Opts.directory, 238 Opts.ignore_errors, 239 Opts.omit, 240 Opts.include, 241 ] + GLOBAL_ARGS, 242 usage = "[options] [modules]", 243 description = "Make annotated copies of the given files, marking " 244 "statements that are executed with > and statements that are " 245 "missed with !." 246 ), 247 248 'combine': CmdOptionParser("combine", GLOBAL_ARGS, 249 usage = " ", 250 description = "Combine data from multiple coverage files collected " 251 "with 'run -p'. The combined results are written to a single " 252 "file representing the union of the data." 253 ), 254 255 'debug': CmdOptionParser("debug", GLOBAL_ARGS, 256 usage = "<topic>", 257 description = "Display information on the internals of coverage.py, " 258 "for diagnosing problems. " 259 "Topics are 'data' to show a summary of the collected data, " 260 "or 'sys' to show installation information." 261 ), 262 263 'erase': CmdOptionParser("erase", GLOBAL_ARGS, 264 usage = " ", 265 description = "Erase previously collected coverage data." 266 ), 267 268 'help': CmdOptionParser("help", GLOBAL_ARGS, 269 usage = "[command]", 270 description = "Describe how to use coverage.py" 271 ), 272 273 'html': CmdOptionParser("html", 274 [ 275 Opts.directory, 276 Opts.ignore_errors, 277 Opts.omit, 278 Opts.include, 279 ] + GLOBAL_ARGS, 280 usage = "[options] [modules]", 281 description = "Create an HTML report of the coverage of the files. " 282 "Each file gets its own page, with the source decorated to show " 283 "executed, excluded, and missed lines." 284 ), 285 286 'report': CmdOptionParser("report", 287 [ 288 Opts.ignore_errors, 289 Opts.omit, 290 Opts.include, 291 Opts.show_missing, 292 ] + GLOBAL_ARGS, 293 usage = "[options] [modules]", 294 description = "Report coverage statistics on modules." 295 ), 296 297 'run': CmdOptionParser("execute", 298 [ 299 Opts.append, 300 Opts.branch, 301 Opts.pylib, 302 Opts.parallel_mode, 303 Opts.module, 304 Opts.timid, 305 Opts.source, 306 Opts.omit, 307 Opts.include, 308 ] + GLOBAL_ARGS, 309 defaults = {'erase_first': True}, 310 cmd = "run", 311 usage = "[options] <pyfile> [program options]", 312 description = "Run a Python program, measuring code execution." 313 ), 314 315 'xml': CmdOptionParser("xml", 316 [ 317 Opts.ignore_errors, 318 Opts.omit, 319 Opts.include, 320 Opts.output_xml, 321 ] + GLOBAL_ARGS, 322 cmd = "xml", 323 defaults = {'outfile': 'coverage.xml'}, 324 usage = "[options] [modules]", 325 description = "Generate an XML report of coverage results." 326 ), 327 } 328 329 330 OK, ERR = 0, 1 331 332 333 class CoverageScript(object): 334 """The command-line interface to Coverage.""" 335 336 def __init__(self, _covpkg=None, _run_python_file=None, 337 _run_python_module=None, _help_fn=None): 338 # _covpkg is for dependency injection, so we can test this code. 339 if _covpkg: 340 self.covpkg = _covpkg 341 else: 342 import coverage 343 self.covpkg = coverage 344 345 # For dependency injection: 346 self.run_python_file = _run_python_file or run_python_file 347 self.run_python_module = _run_python_module or run_python_module 348 self.help_fn = _help_fn or self.help 349 350 self.coverage = None 351 352 def help(self, error=None, topic=None, parser=None): 353 """Display an error message, or the named topic.""" 354 assert error or topic or parser 355 if error: 356 print(error) 357 print("Use 'coverage help' for help.") 358 elif parser: 359 print(parser.format_help().strip()) 360 else: 361 # Parse out the topic we want from HELP_TOPICS 362 topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS) 363 topics = dict(zip(topic_list[1::2], topic_list[2::2])) 364 help_msg = topics.get(topic, '').strip() 365 if help_msg: 366 print(help_msg % self.covpkg.__dict__) 367 else: 368 print("Don't know topic %r" % topic) 369 370 def command_line(self, argv): 371 """The bulk of the command line interface to Coverage. 372 373 `argv` is the argument list to process. 374 375 Returns 0 if all is well, 1 if something went wrong. 376 377 """ 378 # Collect the command-line options. 379 380 if not argv: 381 self.help_fn(topic='minimum_help') 382 return OK 383 384 # The command syntax we parse depends on the first argument. Classic 385 # syntax always starts with an option. 386 classic = argv[0].startswith('-') 387 if classic: 388 parser = ClassicOptionParser() 389 else: 390 parser = CMDS.get(argv[0]) 391 if not parser: 392 self.help_fn("Unknown command: '%s'" % argv[0]) 393 return ERR 394 argv = argv[1:] 395 396 parser.help_fn = self.help_fn 397 ok, options, args = parser.parse_args(argv) 398 if not ok: 399 return ERR 400 401 # Handle help. 402 if options.help: 403 if classic: 404 self.help_fn(topic='help') 405 else: 406 self.help_fn(parser=parser) 407 return OK 408 409 if "help" in options.actions: 410 if args: 411 for a in args: 412 parser = CMDS.get(a) 413 if parser: 414 self.help_fn(parser=parser) 415 else: 416 self.help_fn(topic=a) 417 else: 418 self.help_fn(topic='help') 419 return OK 420 421 # Handle version. 422 if options.version: 423 self.help_fn(topic='version') 424 return OK 425 426 # Check for conflicts and problems in the options. 427 for i in ['erase', 'execute']: 428 for j in ['annotate', 'html', 'report', 'combine']: 429 if (i in options.actions) and (j in options.actions): 430 self.help_fn("You can't specify the '%s' and '%s' " 431 "options at the same time." % (i, j)) 432 return ERR 433 434 if not options.actions: 435 self.help_fn( 436 "You must specify at least one of -e, -x, -c, -r, -a, or -b." 437 ) 438 return ERR 439 args_allowed = ( 440 'execute' in options.actions or 441 'annotate' in options.actions or 442 'html' in options.actions or 443 'debug' in options.actions or 444 'report' in options.actions or 445 'xml' in options.actions 446 ) 447 if not args_allowed and args: 448 self.help_fn("Unexpected arguments: %s" % " ".join(args)) 449 return ERR 450 451 if 'execute' in options.actions and not args: 452 self.help_fn("Nothing to do.") 453 return ERR 454 455 # Listify the list options. 456 source = unshell_list(options.source) 457 omit = unshell_list(options.omit) 458 include = unshell_list(options.include) 459 460 # Do something. 461 self.coverage = self.covpkg.coverage( 462 data_suffix = options.parallel_mode, 463 cover_pylib = options.pylib, 464 timid = options.timid, 465 branch = options.branch, 466 config_file = options.rcfile, 467 source = source, 468 omit = omit, 469 include = include, 470 ) 471 472 if 'debug' in options.actions: 473 if not args: 474 self.help_fn("What information would you like: data, sys?") 475 return ERR 476 for info in args: 477 if info == 'sys': 478 print("-- sys ----------------------------------------") 479 for label, info in self.coverage.sysinfo(): 480 if info == []: 481 info = "-none-" 482 if isinstance(info, list): 483 print("%15s:" % label) 484 for e in info: 485 print("%15s %s" % ("", e)) 486 else: 487 print("%15s: %s" % (label, info)) 488 elif info == 'data': 489 print("-- data ---------------------------------------") 490 self.coverage.load() 491 print("path: %s" % self.coverage.data.filename) 492 print("has_arcs: %r" % self.coverage.data.has_arcs()) 493 summary = self.coverage.data.summary(fullpath=True) 494 if summary: 495 filenames = sorted(summary.keys()) 496 print("\n%d files:" % len(filenames)) 497 for f in filenames: 498 print("%s: %d lines" % (f, summary[f])) 499 else: 500 print("No data collected") 501 else: 502 self.help_fn("Don't know what you mean by %r" % info) 503 return ERR 504 return OK 505 506 if 'erase' in options.actions or options.erase_first: 507 self.coverage.erase() 508 else: 509 self.coverage.load() 510 511 if 'execute' in options.actions: 512 # Run the script. 513 self.coverage.start() 514 code_ran = True 515 try: 516 try: 517 if options.module: 518 self.run_python_module(args[0], args) 519 else: 520 self.run_python_file(args[0], args) 521 except NoSource: 522 code_ran = False 523 raise 524 finally: 525 if code_ran: 526 self.coverage.stop() 527 self.coverage.save() 528 529 if 'combine' in options.actions: 530 self.coverage.combine() 531 self.coverage.save() 532 533 # Remaining actions are reporting, with some common options. 534 report_args = dict( 535 morfs = args, 536 ignore_errors = options.ignore_errors, 537 omit = omit, 538 include = include, 539 ) 540 541 if 'report' in options.actions: 542 self.coverage.report( 543 show_missing=options.show_missing, **report_args) 544 if 'annotate' in options.actions: 545 self.coverage.annotate( 546 directory=options.directory, **report_args) 547 if 'html' in options.actions: 548 self.coverage.html_report( 549 directory=options.directory, **report_args) 550 if 'xml' in options.actions: 551 outfile = options.outfile 552 self.coverage.xml_report(outfile=outfile, **report_args) 553 554 return OK 555 556 557 def unshell_list(s): 558 """Turn a command-line argument into a list.""" 559 if not s: 560 return None 561 if sys.platform == 'win32': 562 # When running coverage as coverage.exe, some of the behavior 563 # of the shell is emulated: wildcards are expanded into a list of 564 # filenames. So you have to single-quote patterns on the command 565 # line, but (not) helpfully, the single quotes are included in the 566 # argument, so we have to strip them off here. 567 s = s.strip("'") 568 return s.split(',') 569 570 571 HELP_TOPICS = r""" 572 573 == classic ==================================================================== 574 Coverage.py version %(__version__)s 575 Measure, collect, and report on code coverage in Python programs. 576 577 Usage: 578 579 coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...] 580 Execute the module, passing the given command-line arguments, collecting 581 coverage data. With the -p option, include the machine name and process 582 id in the .coverage file name. With -L, measure coverage even inside the 583 Python installed library, which isn't done by default. With --timid, use a 584 simpler but slower trace method. 585 586 coverage -e 587 Erase collected coverage data. 588 589 coverage -c 590 Combine data from multiple coverage files (as created by -p option above) 591 and store it into a single file representing the union of the coverage. 592 593 coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...] 594 Report on the statement coverage for the given files. With the -m 595 option, show line numbers of the statements that weren't executed. 596 597 coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...] 598 Create an HTML report of the coverage of the given files. Each file gets 599 its own page, with the file listing decorated to show executed, excluded, 600 and missed lines. 601 602 coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...] 603 Make annotated copies of the given files, marking statements that 604 are executed with > and statements that are missed with !. 605 606 -d DIR 607 Write output files for -b or -a to this directory. 608 609 -i Ignore errors while reporting or annotating. 610 611 -o DIR,... 612 Omit reporting or annotating files when their filename path starts with 613 a directory listed in the omit list. 614 e.g. coverage -i -r -o c:\python25,lib\enthought\traits 615 616 Coverage data is saved in the file .coverage by default. Set the 617 COVERAGE_FILE environment variable to save it somewhere else. 618 619 == help ======================================================================= 620 Coverage.py, version %(__version__)s 621 Measure, collect, and report on code coverage in Python programs. 622 623 usage: coverage <command> [options] [args] 624 625 Commands: 626 annotate Annotate source files with execution information. 627 combine Combine a number of data files. 628 erase Erase previously collected coverage data. 629 help Get help on using coverage.py. 630 html Create an HTML report. 631 report Report coverage stats on modules. 632 run Run a Python program and measure code execution. 633 xml Create an XML report of coverage results. 634 635 Use "coverage help <command>" for detailed help on any command. 636 Use "coverage help classic" for help on older command syntax. 637 For more information, see %(__url__)s 638 639 == minimum_help =============================================================== 640 Code coverage for Python. Use 'coverage help' for help. 641 642 == version ==================================================================== 643 Coverage.py, version %(__version__)s. %(__url__)s 644 645 """ 646 647 648 def main(argv=None): 649 """The main entrypoint to Coverage. 650 651 This is installed as the script entrypoint. 652 653 """ 654 if argv is None: 655 argv = sys.argv[1:] 656 try: 657 status = CoverageScript().command_line(argv) 658 except ExceptionDuringRun: 659 # An exception was caught while running the product code. The 660 # sys.exc_info() return tuple is packed into an ExceptionDuringRun 661 # exception. 662 _, err, _ = sys.exc_info() 663 traceback.print_exception(*err.args) 664 status = ERR 665 except CoverageException: 666 # A controlled error inside coverage.py: print the message to the user. 667 _, err, _ = sys.exc_info() 668 print(err) 669 status = ERR 670 except SystemExit: 671 # The user called `sys.exit()`. Exit with their argument, if any. 672 _, err, _ = sys.exc_info() 673 if err.args: 674 status = err.args[0] 675 else: 676 status = None 677 return status 678