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
4
"""Command-line support for coverage.py."""
13
from coverage import env
14
from coverage.collector import CTracer
15
from coverage.execfile import run_python_file, run_python_module
16
from coverage.misc import CoverageException, ExceptionDuringRun, NoSource
17
from coverage.debug import info_formatter, info_header
21
"""A namespace class for individual options we'll build parsers from."""
23
append = optparse.make_option(
24
'-a', '--append', action='store_true',
25
help="Append coverage data to .coverage, otherwise it starts clean each time.",
27
branch = optparse.make_option(
28
'', '--branch', action='store_true',
29
help="Measure branch coverage in addition to statement coverage.",
31
CONCURRENCY_CHOICES = [
32
"thread", "gevent", "greenlet", "eventlet", "multiprocessing",
34
concurrency = optparse.make_option(
35
'', '--concurrency', action='store', metavar="LIB",
36
choices=CONCURRENCY_CHOICES,
38
"Properly measure code using a concurrency library. "
39
"Valid values are: %s."
40
) % ", ".join(CONCURRENCY_CHOICES),
42
debug = optparse.make_option(
43
'', '--debug', action='store', metavar="OPTS",
44
help="Debug options, separated by commas",
46
directory = optparse.make_option(
47
'-d', '--directory', action='store', metavar="DIR",
48
help="Write the output files to DIR.",
50
fail_under = optparse.make_option(
51
'', '--fail-under', action='store', metavar="MIN", type="int",
52
help="Exit with a status of 2 if the total coverage is less than MIN.",
54
help = optparse.make_option(
55
'-h', '--help', action='store_true',
56
help="Get help on this command.",
58
ignore_errors = optparse.make_option(
59
'-i', '--ignore-errors', action='store_true',
60
help="Ignore errors while reading source files.",
62
include = optparse.make_option(
63
'', '--include', action='store',
64
metavar="PAT1,PAT2,...",
66
"Include only files whose paths match one of these patterns. "
67
"Accepts shell-style wildcards, which must be quoted."
70
pylib = optparse.make_option(
71
'-L', '--pylib', action='store_true',
73
"Measure coverage even inside the Python installed library, "
74
"which isn't done by default."
77
show_missing = optparse.make_option(
78
'-m', '--show-missing', action='store_true',
79
help="Show line numbers of statements in each module that weren't executed.",
81
skip_covered = optparse.make_option(
82
'--skip-covered', action='store_true',
83
help="Skip files with 100% coverage.",
85
omit = optparse.make_option(
86
'', '--omit', action='store',
87
metavar="PAT1,PAT2,...",
89
"Omit files whose paths match one of these patterns. "
90
"Accepts shell-style wildcards, which must be quoted."
93
output_xml = optparse.make_option(
94
'-o', '', action='store', dest="outfile",
96
help="Write the XML report to this file. Defaults to 'coverage.xml'",
98
parallel_mode = optparse.make_option(
99
'-p', '--parallel-mode', action='store_true',
101
"Append the machine name, process id and random number to the "
102
".coverage data file name to simplify collecting data from "
106
module = optparse.make_option(
107
'-m', '--module', action='store_true',
109
"<pyfile> is an importable Python module, not a script path, "
110
"to be run as 'python -m' would run it."
113
rcfile = optparse.make_option(
114
'', '--rcfile', action='store',
115
help="Specify configuration file. Defaults to '.coveragerc'",
117
source = optparse.make_option(
118
'', '--source', action='store', metavar="SRC1,SRC2,...",
119
help="A list of packages or directories of code to be measured.",
121
timid = optparse.make_option(
122
'', '--timid', action='store_true',
124
"Use a simpler but slower trace method. Try this if you get "
125
"seemingly impossible results!"
128
title = optparse.make_option(
129
'', '--title', action='store', metavar="TITLE",
130
help="A text string to use as the title on the HTML.",
132
version = optparse.make_option(
133
'', '--version', action='store_true',
134
help="Display version information and exit.",
138
class CoverageOptionParser(optparse.OptionParser, object):
139
"""Base OptionParser for coverage.py.
141
Problems don't exit the program.
142
Defaults are initialized for all options.
146
def __init__(self, *args, **kwargs):
147
super(CoverageOptionParser, self).__init__(
148
add_help_option=False, *args, **kwargs
174
self.disable_interspersed_args()
175
self.help_fn = self.help_noop
177
def help_noop(self, error=None, topic=None, parser=None):
178
"""No-op help function."""
181
class OptionParserError(Exception):
182
"""Used to stop the optparse error handler ending the process."""
185
def parse_args_ok(self, args=None, options=None):
186
"""Call optparse.parse_args, but return a triple:
193
super(CoverageOptionParser, self).parse_args(args, options)
194
except self.OptionParserError:
195
return False, None, None
196
return True, options, args
198
def error(self, msg):
199
"""Override optparse.error so sys.exit doesn't get called."""
201
raise self.OptionParserError
204
class GlobalOptionParser(CoverageOptionParser):
205
"""Command-line parser for coverage.py global option arguments."""
208
super(GlobalOptionParser, self).__init__()
216
class CmdOptionParser(CoverageOptionParser):
217
"""Parse one of the new-style commands for coverage.py."""
219
def __init__(self, action, options, defaults=None, usage=None, description=None):
220
"""Create an OptionParser for a coverage.py command.
222
`action` is the slug to put into `options.action`.
223
`options` is a list of Option's for the command.
224
`defaults` is a dict of default value for options.
225
`usage` is the usage string to display in help.
226
`description` is the description of the command, for the help text.
230
usage = "%prog " + usage
231
super(CmdOptionParser, self).__init__(
233
description=description,
235
self.set_defaults(action=action, **(defaults or {}))
236
self.add_options(options)
239
def __eq__(self, other):
240
# A convenience equality, so that I can put strings in unit test
241
# results, and they will compare equal to objects.
242
return (other == "<CmdOptionParser:%s>" % self.cmd)
244
def get_prog_name(self):
245
"""Override of an undocumented function in optparse.OptionParser."""
246
program_name = super(CmdOptionParser, self).get_prog_name()
248
# Include the sub-command for this parser as part of the command.
249
return "%(command)s %(subcommand)s" % {'command': program_name, 'subcommand': self.cmd}
259
'annotate': CmdOptionParser(
267
usage="[options] [modules]",
269
"Make annotated copies of the given files, marking statements that are executed "
270
"with > and statements that are missed with !."
274
'combine': CmdOptionParser(
279
usage="[options] <path1> <path2> ... <pathN>",
281
"Combine data from multiple coverage files collected "
282
"with 'run -p'. The combined results are written to a single "
283
"file representing the union of the data. The positional "
284
"arguments are data files or directories containing data files. "
285
"If no paths are provided, data files in the default data file's "
286
"directory are combined."
290
'debug': CmdOptionParser(
291
"debug", GLOBAL_ARGS,
294
"Display information on the internals of coverage.py, "
295
"for diagnosing problems. "
296
"Topics are 'data' to show a summary of the collected data, "
297
"or 'sys' to show installation information."
301
'erase': CmdOptionParser(
302
"erase", GLOBAL_ARGS,
303
description="Erase previously collected coverage data.",
306
'help': CmdOptionParser(
309
description="Describe how to use coverage.py",
312
'html': CmdOptionParser(
322
usage="[options] [modules]",
324
"Create an HTML report of the coverage of the files. "
325
"Each file gets its own page, with the source decorated to show "
326
"executed, excluded, and missed lines."
330
'report': CmdOptionParser(
340
usage="[options] [modules]",
341
description="Report coverage statistics on modules."
344
'run': CmdOptionParser(
358
usage="[options] <pyfile> [program options]",
359
description="Run a Python program, measuring code execution."
362
'xml': CmdOptionParser(
371
usage="[options] [modules]",
372
description="Generate an XML report of coverage results."
377
OK, ERR, FAIL_UNDER = 0, 1, 2
380
class CoverageScript(object):
381
"""The command-line interface to coverage.py."""
383
def __init__(self, _covpkg=None, _run_python_file=None,
384
_run_python_module=None, _help_fn=None, _path_exists=None):
385
# _covpkg is for dependency injection, so we can test this code.
387
self.covpkg = _covpkg
390
self.covpkg = coverage
392
# For dependency injection:
393
self.run_python_file = _run_python_file or run_python_file
394
self.run_python_module = _run_python_module or run_python_module
395
self.help_fn = _help_fn or self.help
396
self.path_exists = _path_exists or os.path.exists
397
self.global_option = False
401
self.program_name = os.path.basename(sys.argv[0])
402
if self.program_name == '__main__.py':
403
self.program_name = 'coverage'
405
# entry_points={'console_scripts':...} on Windows makes files
406
# called coverage.exe, coverage3.exe, and coverage-3.5.exe. These
407
# invoke coverage-script.py, coverage3-script.py, and
408
# coverage-3.5-script.py. argv[0] is the .py file, but we want to
409
# get back to the original form.
410
auto_suffix = "-script.py"
411
if self.program_name.endswith(auto_suffix):
412
self.program_name = self.program_name[:-len(auto_suffix)]
414
def command_line(self, argv):
415
"""The bulk of the command line interface to coverage.py.
417
`argv` is the argument list to process.
419
Returns 0 if all is well, 1 if something went wrong.
422
# Collect the command-line options.
424
self.help_fn(topic='minimum_help')
427
# The command syntax we parse depends on the first argument. Global
428
# switch syntax always starts with an option.
429
self.global_option = argv[0].startswith('-')
430
if self.global_option:
431
parser = GlobalOptionParser()
433
parser = CMDS.get(argv[0])
435
self.help_fn("Unknown command: '%s'" % argv[0])
439
parser.help_fn = self.help_fn
440
ok, options, args = parser.parse_args_ok(argv)
444
# Handle help and version.
445
if self.do_help(options, args, parser):
448
# We need to be able to import from the current directory, because
449
# plugins may try to, for example, to read Django settings.
452
# Listify the list options.
453
source = unshell_list(options.source)
454
omit = unshell_list(options.omit)
455
include = unshell_list(options.include)
456
debug = unshell_list(options.debug)
459
self.coverage = self.covpkg.coverage(
460
data_suffix=options.parallel_mode,
461
cover_pylib=options.pylib,
463
branch=options.branch,
464
config_file=options.rcfile,
469
concurrency=options.concurrency,
472
if options.action == "debug":
473
return self.do_debug(args)
475
elif options.action == "erase":
476
self.coverage.erase()
479
elif options.action == "run":
480
return self.do_run(options, args)
482
elif options.action == "combine":
485
data_dirs = args or None
486
self.coverage.combine(data_dirs)
490
# Remaining actions are reporting, with some common options.
492
morfs=unglob_args(args),
493
ignore_errors=options.ignore_errors,
501
if options.action == "report":
502
total = self.coverage.report(
503
show_missing=options.show_missing,
504
skip_covered=options.skip_covered, **report_args)
505
elif options.action == "annotate":
506
self.coverage.annotate(
507
directory=options.directory, **report_args)
508
elif options.action == "html":
509
total = self.coverage.html_report(
510
directory=options.directory, title=options.title,
512
elif options.action == "xml":
513
outfile = options.outfile
514
total = self.coverage.xml_report(outfile=outfile, **report_args)
516
if total is not None:
517
# Apply the command line fail-under options, and then use the config
518
# value, so we can get fail_under from the config file.
519
if options.fail_under is not None:
520
self.coverage.set_option("report:fail_under", options.fail_under)
522
if self.coverage.get_option("report:fail_under"):
523
# Total needs to be rounded, but don't want to report 100
524
# unless it is really 100.
530
if total >= self.coverage.get_option("report:fail_under"):
537
def help(self, error=None, topic=None, parser=None):
538
"""Display an error message, or the named topic."""
539
assert error or topic or parser
542
print("Use '%s help' for help." % (self.program_name,))
544
print(parser.format_help().strip())
546
help_params = dict(self.covpkg.__dict__)
547
help_params['program_name'] = self.program_name
548
if CTracer is not None:
549
help_params['extension_modifier'] = 'with C extension'
551
help_params['extension_modifier'] = 'without C extension'
552
help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip()
554
print(help_msg.format(**help_params))
556
print("Don't know topic %r" % topic)
558
def do_help(self, options, args, parser):
559
"""Deal with help requests.
561
Return True if it handled the request, False if not.
566
if self.global_option:
567
self.help_fn(topic='help')
569
self.help_fn(parser=parser)
572
if options.action == "help":
577
self.help_fn(parser=parser)
579
self.help_fn(topic=a)
581
self.help_fn(topic='help')
586
self.help_fn(topic='version')
591
def do_run(self, options, args):
592
"""Implementation of 'coverage run'."""
595
self.help_fn("Nothing to do.")
598
if options.append and self.coverage.get_option("run:parallel"):
599
self.help_fn("Can't append to data files in parallel mode.")
602
if options.concurrency == "multiprocessing":
603
# Can't set other run-affecting command line options with
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:
610
"Options affecting multiprocessing must be specified "
611
"in a configuration file."
615
if not self.coverage.get_option("run:parallel"):
616
if not options.append:
617
self.coverage.erase()
620
self.coverage.start()
624
self.run_python_module(args[0], args)
627
self.run_python_file(filename, args)
635
data_file = self.coverage.get_option("run:data_file")
636
if self.path_exists(data_file):
637
self.coverage.combine(data_paths=[data_file])
642
def do_debug(self, args):
643
"""Implementation of 'coverage debug'."""
646
self.help_fn("What information would you like: config, data, sys?")
651
sys_info = self.coverage.sys_info()
652
print(info_header("sys"))
653
for line in info_formatter(sys_info):
657
data = self.coverage.data
658
print(info_header("data"))
659
print("path: %s" % self.coverage.data_files.filename)
661
print("has_arcs: %r" % data.has_arcs())
662
summary = data.line_counts(fullpath=True)
663
filenames = sorted(summary.keys())
664
print("\n%d files:" % len(filenames))
666
line = "%s: %d lines" % (f, summary[f])
667
plugin = data.file_tracer(f)
669
line += " [%s]" % plugin
672
print("No data collected")
673
elif info == 'config':
674
print(info_header("config"))
675
config_info = self.coverage.config.__dict__.items()
676
for line in info_formatter(config_info):
679
self.help_fn("Don't know what you mean by %r" % info)
686
"""Turn a command-line argument into a list."""
690
# When running coverage.py as coverage.exe, some of the behavior
691
# of the shell is emulated: wildcards are expanded into a list of
692
# file names. So you have to single-quote patterns on the command
693
# line, but (not) helpfully, the single quotes are included in the
694
# argument, so we have to strip them off here.
699
def unglob_args(args):
700
"""Interpret shell wildcards for platforms that need it."""
704
if '?' in arg or '*' in arg:
705
globbed.extend(glob.glob(arg))
714
Coverage.py, version {__version__} {extension_modifier}
715
Measure, collect, and report on code coverage in Python programs.
717
usage: {program_name} <command> [options] [args]
720
annotate Annotate source files with execution information.
721
combine Combine a number of data files.
722
erase Erase previously collected coverage data.
723
help Get help on using coverage.py.
724
html Create an HTML report.
725
report Report coverage stats on modules.
726
run Run a Python program and measure code execution.
727
xml Create an XML report of coverage results.
729
Use "{program_name} help <command>" for detailed help on any command.
730
For full documentation, see {__url__}
734
Code coverage for Python. Use '{program_name} help' for help.
738
Coverage.py, version {__version__} {extension_modifier}
739
Documentation at {__url__}
745
"""The main entry point to coverage.py.
747
This is installed as the script entry point.
753
status = CoverageScript().command_line(argv)
754
except ExceptionDuringRun as err:
755
# An exception was caught while running the product code. The
756
# sys.exc_info() return tuple is packed into an ExceptionDuringRun
758
traceback.print_exception(*err.args)
760
except CoverageException as err:
761
# A controlled error inside coverage.py: print the message to the user.
764
except SystemExit as err:
765
# The user called `sys.exit()`. Exit with their argument, if any.