1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
1 # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
2 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt |
3 |
3 |
4 """Command-line support for coverage.py.""" |
4 """Command-line support for coverage.py.""" |
|
5 |
|
6 from __future__ import print_function |
5 |
7 |
6 import glob |
8 import glob |
7 import optparse |
9 import optparse |
8 import os.path |
10 import os.path |
9 import sys |
11 import sys |
10 import textwrap |
12 import textwrap |
11 import traceback |
13 import traceback |
12 |
14 |
13 from coverage import env |
15 from coverage import env |
14 from coverage.collector import CTracer |
16 from coverage.collector import CTracer |
|
17 from coverage.debug import info_formatter, info_header |
15 from coverage.execfile import run_python_file, run_python_module |
18 from coverage.execfile import run_python_file, run_python_module |
16 from coverage.misc import CoverageException, ExceptionDuringRun, NoSource |
19 from coverage.misc import BaseCoverageException, ExceptionDuringRun, NoSource |
17 from coverage.debug import info_formatter, info_header |
20 from coverage.results import should_fail_under |
18 |
21 |
19 |
22 |
20 class Opts(object): |
23 class Opts(object): |
21 """A namespace class for individual options we'll build parsers from.""" |
24 """A namespace class for individual options we'll build parsers from.""" |
22 |
25 |
23 append = optparse.make_option( |
26 append = optparse.make_option( |
24 '-a', '--append', action='store_true', |
27 '-a', '--append', action='store_true', |
25 help="Append coverage data to .coverage, otherwise it is started clean with each run.", |
28 help="Append coverage data to .coverage, otherwise it starts clean each time.", |
26 ) |
29 ) |
27 branch = optparse.make_option( |
30 branch = optparse.make_option( |
28 '', '--branch', action='store_true', |
31 '', '--branch', action='store_true', |
29 help="Measure branch coverage in addition to statement coverage.", |
32 help="Measure branch coverage in addition to statement coverage.", |
30 ) |
33 ) |
46 directory = optparse.make_option( |
49 directory = optparse.make_option( |
47 '-d', '--directory', action='store', metavar="DIR", |
50 '-d', '--directory', action='store', metavar="DIR", |
48 help="Write the output files to DIR.", |
51 help="Write the output files to DIR.", |
49 ) |
52 ) |
50 fail_under = optparse.make_option( |
53 fail_under = optparse.make_option( |
51 '', '--fail-under', action='store', metavar="MIN", type="int", |
54 '', '--fail-under', action='store', metavar="MIN", type="float", |
52 help="Exit with a status of 2 if the total coverage is less than MIN.", |
55 help="Exit with a status of 2 if the total coverage is less than MIN.", |
53 ) |
56 ) |
54 help = optparse.make_option( |
57 help = optparse.make_option( |
55 '-h', '--help', action='store_true', |
58 '-h', '--help', action='store_true', |
56 help="Get help on this command.", |
59 help="Get help on this command.", |
214 |
217 |
215 |
218 |
216 class CmdOptionParser(CoverageOptionParser): |
219 class CmdOptionParser(CoverageOptionParser): |
217 """Parse one of the new-style commands for coverage.py.""" |
220 """Parse one of the new-style commands for coverage.py.""" |
218 |
221 |
219 def __init__(self, action, options=None, defaults=None, usage=None, description=None): |
222 def __init__(self, action, options, defaults=None, usage=None, description=None): |
220 """Create an OptionParser for a coverage.py command. |
223 """Create an OptionParser for a coverage.py command. |
221 |
224 |
222 `action` is the slug to put into `options.action`. |
225 `action` is the slug to put into `options.action`. |
223 `options` is a list of Option's for the command. |
226 `options` is a list of Option's for the command. |
224 `defaults` is a dict of default value for options. |
227 `defaults` is a dict of default value for options. |
231 super(CmdOptionParser, self).__init__( |
234 super(CmdOptionParser, self).__init__( |
232 usage=usage, |
235 usage=usage, |
233 description=description, |
236 description=description, |
234 ) |
237 ) |
235 self.set_defaults(action=action, **(defaults or {})) |
238 self.set_defaults(action=action, **(defaults or {})) |
236 if options: |
239 self.add_options(options) |
237 self.add_options(options) |
|
238 self.cmd = action |
240 self.cmd = action |
239 |
241 |
240 def __eq__(self, other): |
242 def __eq__(self, other): |
241 # A convenience equality, so that I can put strings in unit test |
243 # A convenience equality, so that I can put strings in unit test |
242 # results, and they will compare equal to objects. |
244 # results, and they will compare equal to objects. |
243 return (other == "<CmdOptionParser:%s>" % self.cmd) |
245 return (other == "<CmdOptionParser:%s>" % self.cmd) |
244 |
246 |
|
247 __hash__ = None # This object doesn't need to be hashed. |
|
248 |
245 def get_prog_name(self): |
249 def get_prog_name(self): |
246 """Override of an undocumented function in optparse.OptionParser.""" |
250 """Override of an undocumented function in optparse.OptionParser.""" |
247 program_name = super(CmdOptionParser, self).get_prog_name() |
251 program_name = super(CmdOptionParser, self).get_prog_name() |
248 |
252 |
249 # Include the sub-command for this parser as part of the command. |
253 # Include the sub-command for this parser as part of the command. |
250 return "%(command)s %(subcommand)s" % {'command': program_name, 'subcommand': self.cmd} |
254 return "{command} {subcommand}".format(command=program_name, subcommand=self.cmd) |
251 |
255 |
252 |
256 |
253 GLOBAL_ARGS = [ |
257 GLOBAL_ARGS = [ |
254 Opts.debug, |
258 Opts.debug, |
255 Opts.help, |
259 Opts.help, |
272 ), |
276 ), |
273 ), |
277 ), |
274 |
278 |
275 'combine': CmdOptionParser( |
279 'combine': CmdOptionParser( |
276 "combine", |
280 "combine", |
277 GLOBAL_ARGS, |
281 [ |
278 usage="<path1> <path2> ... <pathN>", |
282 Opts.append, |
|
283 ] + GLOBAL_ARGS, |
|
284 usage="[options] <path1> <path2> ... <pathN>", |
279 description=( |
285 description=( |
280 "Combine data from multiple coverage files collected " |
286 "Combine data from multiple coverage files collected " |
281 "with 'run -p'. The combined results are written to a single " |
287 "with 'run -p'. The combined results are written to a single " |
282 "file representing the union of the data. The positional " |
288 "file representing the union of the data. The positional " |
283 "arguments are data files or directories containing data files. " |
289 "arguments are data files or directories containing data files. " |
396 self.path_exists = _path_exists or os.path.exists |
402 self.path_exists = _path_exists or os.path.exists |
397 self.global_option = False |
403 self.global_option = False |
398 |
404 |
399 self.coverage = None |
405 self.coverage = None |
400 |
406 |
401 self.program_name = os.path.basename(sys.argv[0]) |
407 program_path = sys.argv[0] |
|
408 if program_path.endswith(os.path.sep + '__main__.py'): |
|
409 # The path is the main module of a package; get that path instead. |
|
410 program_path = os.path.dirname(program_path) |
|
411 self.program_name = os.path.basename(program_path) |
402 if env.WINDOWS: |
412 if env.WINDOWS: |
403 # entry_points={'console_scripts':...} on Windows makes files |
413 # entry_points={'console_scripts':...} on Windows makes files |
404 # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These |
414 # called coverage.exe, coverage3.exe, and coverage-3.5.exe. These |
405 # invoke coverage-script.py, coverage3-script.py, and |
415 # invoke coverage-script.py, coverage3-script.py, and |
406 # coverage-3.5-script.py. argv[0] is the .py file, but we want to |
416 # coverage-3.5-script.py. argv[0] is the .py file, but we want to |
441 |
451 |
442 # Handle help and version. |
452 # Handle help and version. |
443 if self.do_help(options, args, parser): |
453 if self.do_help(options, args, parser): |
444 return OK |
454 return OK |
445 |
455 |
446 # Check for conflicts and problems in the options. |
|
447 if not self.args_ok(options, args): |
|
448 return ERR |
|
449 |
|
450 # We need to be able to import from the current directory, because |
456 # We need to be able to import from the current directory, because |
451 # plugins may try to, for example, to read Django settings. |
457 # plugins may try to, for example, to read Django settings. |
452 sys.path[0] = '' |
458 sys.path[0] = '' |
453 |
459 |
454 # Listify the list options. |
460 # Listify the list options. |
456 omit = unshell_list(options.omit) |
462 omit = unshell_list(options.omit) |
457 include = unshell_list(options.include) |
463 include = unshell_list(options.include) |
458 debug = unshell_list(options.debug) |
464 debug = unshell_list(options.debug) |
459 |
465 |
460 # Do something. |
466 # Do something. |
461 self.coverage = self.covpkg.coverage( |
467 self.coverage = self.covpkg.Coverage( |
462 data_suffix=options.parallel_mode, |
468 data_suffix=options.parallel_mode, |
463 cover_pylib=options.pylib, |
469 cover_pylib=options.pylib, |
464 timid=options.timid, |
470 timid=options.timid, |
465 branch=options.branch, |
471 branch=options.branch, |
466 config_file=options.rcfile, |
472 config_file=options.rcfile, |
480 |
486 |
481 elif options.action == "run": |
487 elif options.action == "run": |
482 return self.do_run(options, args) |
488 return self.do_run(options, args) |
483 |
489 |
484 elif options.action == "combine": |
490 elif options.action == "combine": |
485 self.coverage.load() |
491 if options.append: |
|
492 self.coverage.load() |
486 data_dirs = args or None |
493 data_dirs = args or None |
487 self.coverage.combine(data_dirs) |
494 self.coverage.combine(data_dirs, strict=True) |
488 self.coverage.save() |
495 self.coverage.save() |
489 return OK |
496 return OK |
490 |
497 |
491 # Remaining actions are reporting, with some common options. |
498 # Remaining actions are reporting, with some common options. |
492 report_args = dict( |
499 report_args = dict( |
507 self.coverage.annotate( |
514 self.coverage.annotate( |
508 directory=options.directory, **report_args) |
515 directory=options.directory, **report_args) |
509 elif options.action == "html": |
516 elif options.action == "html": |
510 total = self.coverage.html_report( |
517 total = self.coverage.html_report( |
511 directory=options.directory, title=options.title, |
518 directory=options.directory, title=options.title, |
512 **report_args) |
519 skip_covered=options.skip_covered, **report_args) |
513 elif options.action == "xml": |
520 elif options.action == "xml": |
514 outfile = options.outfile |
521 outfile = options.outfile |
515 total = self.coverage.xml_report(outfile=outfile, **report_args) |
522 total = self.coverage.xml_report(outfile=outfile, **report_args) |
516 |
523 |
517 if total is not None: |
524 if total is not None: |
518 # Apply the command line fail-under options, and then use the config |
525 # Apply the command line fail-under options, and then use the config |
519 # value, so we can get fail_under from the config file. |
526 # value, so we can get fail_under from the config file. |
520 if options.fail_under is not None: |
527 if options.fail_under is not None: |
521 self.coverage.set_option("report:fail_under", options.fail_under) |
528 self.coverage.set_option("report:fail_under", options.fail_under) |
522 |
529 |
523 if self.coverage.get_option("report:fail_under"): |
530 fail_under = self.coverage.get_option("report:fail_under") |
524 |
531 precision = self.coverage.get_option("report:precision") |
525 # Total needs to be rounded, but be careful of 0 and 100. |
532 if should_fail_under(total, fail_under, precision): |
526 if 0 < total < 1: |
533 return FAIL_UNDER |
527 total = 1 |
|
528 elif 99 < total < 100: |
|
529 total = 99 |
|
530 else: |
|
531 total = round(total) |
|
532 |
|
533 if total >= self.coverage.get_option("report:fail_under"): |
|
534 return OK |
|
535 else: |
|
536 return FAIL_UNDER |
|
537 |
534 |
538 return OK |
535 return OK |
539 |
536 |
540 def help(self, error=None, topic=None, parser=None): |
537 def help(self, error=None, topic=None, parser=None): |
541 """Display an error message, or the named topic.""" |
538 """Display an error message, or the named topic.""" |
542 assert error or topic or parser |
539 assert error or topic or parser |
543 if error: |
540 if error: |
544 print(error) |
541 print(error, file=sys.stderr) |
545 print("Use '%s help' for help." % (self.program_name,)) |
542 print("Use '%s help' for help." % (self.program_name,), file=sys.stderr) |
546 elif parser: |
543 elif parser: |
547 print(parser.format_help().strip()) |
544 print(parser.format_help().strip()) |
548 else: |
545 else: |
549 help_params = dict(self.covpkg.__dict__) |
546 help_params = dict(self.covpkg.__dict__) |
550 help_params['program_name'] = self.program_name |
547 help_params['program_name'] = self.program_name |
589 self.help_fn(topic='version') |
586 self.help_fn(topic='version') |
590 return True |
587 return True |
591 |
588 |
592 return False |
589 return False |
593 |
590 |
594 def args_ok(self, options, args): |
|
595 """Check for conflicts and problems in the options. |
|
596 |
|
597 Returns True if everything is OK, or False if not. |
|
598 |
|
599 """ |
|
600 if options.action == "run" and not args: |
|
601 self.help_fn("Nothing to do.") |
|
602 return False |
|
603 |
|
604 return True |
|
605 |
|
606 def do_run(self, options, args): |
591 def do_run(self, options, args): |
607 """Implementation of 'coverage run'.""" |
592 """Implementation of 'coverage run'.""" |
|
593 |
|
594 if not args: |
|
595 self.help_fn("Nothing to do.") |
|
596 return ERR |
608 |
597 |
609 if options.append and self.coverage.get_option("run:parallel"): |
598 if options.append and self.coverage.get_option("run:parallel"): |
610 self.help_fn("Can't append to data files in parallel mode.") |
599 self.help_fn("Can't append to data files in parallel mode.") |
611 return ERR |
600 return ERR |
|
601 |
|
602 if options.concurrency == "multiprocessing": |
|
603 # Can't set other run-affecting command line options with |
|
604 # multiprocessing. |
|
605 for opt_name in ['branch', 'include', 'omit', 'pylib', 'source', 'timid']: |
|
606 # As it happens, all of these options have no default, meaning |
|
607 # they will be None if they have not been specified. |
|
608 if getattr(options, opt_name) is not None: |
|
609 self.help_fn( |
|
610 "Options affecting multiprocessing must be specified " |
|
611 "in a configuration file." |
|
612 ) |
|
613 return ERR |
612 |
614 |
613 if not self.coverage.get_option("run:parallel"): |
615 if not self.coverage.get_option("run:parallel"): |
614 if not options.append: |
616 if not options.append: |
615 self.coverage.erase() |
617 self.coverage.erase() |
616 |
618 |
746 status = CoverageScript().command_line(argv) |
753 status = CoverageScript().command_line(argv) |
747 except ExceptionDuringRun as err: |
754 except ExceptionDuringRun as err: |
748 # An exception was caught while running the product code. The |
755 # An exception was caught while running the product code. The |
749 # sys.exc_info() return tuple is packed into an ExceptionDuringRun |
756 # sys.exc_info() return tuple is packed into an ExceptionDuringRun |
750 # exception. |
757 # exception. |
751 traceback.print_exception(*err.args) |
758 traceback.print_exception(*err.args) # pylint: disable=no-value-for-parameter |
752 status = ERR |
759 status = ERR |
753 except CoverageException as err: |
760 except BaseCoverageException as err: |
754 # A controlled error inside coverage.py: print the message to the user. |
761 # A controlled error inside coverage.py: print the message to the user. |
755 print(err) |
762 print(err) |
756 status = ERR |
763 status = ERR |
757 except SystemExit as err: |
764 except SystemExit as err: |
758 # The user called `sys.exit()`. Exit with their argument, if any. |
765 # The user called `sys.exit()`. Exit with their argument, if any. |