Merge remote-tracking branch 'origin/master'
[idea/community.git] / python / helpers / coveragepy / coverage / cmdline.py
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
3
4 """Command-line support for coverage.py."""
5
6 import glob
7 import optparse
8 import os.path
9 import sys
10 import textwrap
11 import traceback
12
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
18
19
20 class Opts(object):
21     """A namespace class for individual options we'll build parsers from."""
22
23     append = optparse.make_option(
24         '-a', '--append', action='store_true',
25         help="Append coverage data to .coverage, otherwise it starts clean each time.",
26     )
27     branch = optparse.make_option(
28         '', '--branch', action='store_true',
29         help="Measure branch coverage in addition to statement coverage.",
30     )
31     CONCURRENCY_CHOICES = [
32         "thread", "gevent", "greenlet", "eventlet", "multiprocessing",
33     ]
34     concurrency = optparse.make_option(
35         '', '--concurrency', action='store', metavar="LIB",
36         choices=CONCURRENCY_CHOICES,
37         help=(
38             "Properly measure code using a concurrency library. "
39             "Valid values are: %s."
40         ) % ", ".join(CONCURRENCY_CHOICES),
41     )
42     debug = optparse.make_option(
43         '', '--debug', action='store', metavar="OPTS",
44         help="Debug options, separated by commas",
45     )
46     directory = optparse.make_option(
47         '-d', '--directory', action='store', metavar="DIR",
48         help="Write the output files to DIR.",
49     )
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.",
53     )
54     help = optparse.make_option(
55         '-h', '--help', action='store_true',
56         help="Get help on this command.",
57     )
58     ignore_errors = optparse.make_option(
59         '-i', '--ignore-errors', action='store_true',
60         help="Ignore errors while reading source files.",
61     )
62     include = optparse.make_option(
63         '', '--include', action='store',
64         metavar="PAT1,PAT2,...",
65         help=(
66             "Include only files whose paths match one of these patterns. "
67             "Accepts shell-style wildcards, which must be quoted."
68         ),
69     )
70     pylib = optparse.make_option(
71         '-L', '--pylib', action='store_true',
72         help=(
73             "Measure coverage even inside the Python installed library, "
74             "which isn't done by default."
75         ),
76     )
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.",
80     )
81     skip_covered = optparse.make_option(
82         '--skip-covered', action='store_true',
83         help="Skip files with 100% coverage.",
84     )
85     omit = optparse.make_option(
86         '', '--omit', action='store',
87         metavar="PAT1,PAT2,...",
88         help=(
89             "Omit files whose paths match one of these patterns. "
90             "Accepts shell-style wildcards, which must be quoted."
91         ),
92     )
93     output_xml = optparse.make_option(
94         '-o', '', action='store', dest="outfile",
95         metavar="OUTFILE",
96         help="Write the XML report to this file. Defaults to 'coverage.xml'",
97     )
98     parallel_mode = optparse.make_option(
99         '-p', '--parallel-mode', action='store_true',
100         help=(
101             "Append the machine name, process id and random number to the "
102             ".coverage data file name to simplify collecting data from "
103             "many processes."
104         ),
105     )
106     module = optparse.make_option(
107         '-m', '--module', action='store_true',
108         help=(
109             "<pyfile> is an importable Python module, not a script path, "
110             "to be run as 'python -m' would run it."
111         ),
112     )
113     rcfile = optparse.make_option(
114         '', '--rcfile', action='store',
115         help="Specify configuration file.  Defaults to '.coveragerc'",
116     )
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.",
120     )
121     timid = optparse.make_option(
122         '', '--timid', action='store_true',
123         help=(
124             "Use a simpler but slower trace method.  Try this if you get "
125             "seemingly impossible results!"
126         ),
127     )
128     title = optparse.make_option(
129         '', '--title', action='store', metavar="TITLE",
130         help="A text string to use as the title on the HTML.",
131     )
132     version = optparse.make_option(
133         '', '--version', action='store_true',
134         help="Display version information and exit.",
135     )
136
137
138 class CoverageOptionParser(optparse.OptionParser, object):
139     """Base OptionParser for coverage.py.
140
141     Problems don't exit the program.
142     Defaults are initialized for all options.
143
144     """
145
146     def __init__(self, *args, **kwargs):
147         super(CoverageOptionParser, self).__init__(
148             add_help_option=False, *args, **kwargs
149             )
150         self.set_defaults(
151             action=None,
152             append=None,
153             branch=None,
154             concurrency=None,
155             debug=None,
156             directory=None,
157             fail_under=None,
158             help=None,
159             ignore_errors=None,
160             include=None,
161             module=None,
162             omit=None,
163             parallel_mode=None,
164             pylib=None,
165             rcfile=True,
166             show_missing=None,
167             skip_covered=None,
168             source=None,
169             timid=None,
170             title=None,
171             version=None,
172             )
173
174         self.disable_interspersed_args()
175         self.help_fn = self.help_noop
176
177     def help_noop(self, error=None, topic=None, parser=None):
178         """No-op help function."""
179         pass
180
181     class OptionParserError(Exception):
182         """Used to stop the optparse error handler ending the process."""
183         pass
184
185     def parse_args_ok(self, args=None, options=None):
186         """Call optparse.parse_args, but return a triple:
187
188         (ok, options, args)
189
190         """
191         try:
192             options, args = \
193                 super(CoverageOptionParser, self).parse_args(args, options)
194         except self.OptionParserError:
195             return False, None, None
196         return True, options, args
197
198     def error(self, msg):
199         """Override optparse.error so sys.exit doesn't get called."""
200         self.help_fn(msg)
201         raise self.OptionParserError
202
203
204 class GlobalOptionParser(CoverageOptionParser):
205     """Command-line parser for coverage.py global option arguments."""
206
207     def __init__(self):
208         super(GlobalOptionParser, self).__init__()
209
210         self.add_options([
211             Opts.help,
212             Opts.version,
213         ])
214
215
216 class CmdOptionParser(CoverageOptionParser):
217     """Parse one of the new-style commands for coverage.py."""
218
219     def __init__(self, action, options, defaults=None, usage=None, description=None):
220         """Create an OptionParser for a coverage.py command.
221
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.
227
228         """
229         if usage:
230             usage = "%prog " + usage
231         super(CmdOptionParser, self).__init__(
232             usage=usage,
233             description=description,
234         )
235         self.set_defaults(action=action, **(defaults or {}))
236         self.add_options(options)
237         self.cmd = action
238
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)
243
244     def get_prog_name(self):
245         """Override of an undocumented function in optparse.OptionParser."""
246         program_name = super(CmdOptionParser, self).get_prog_name()
247
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}
250
251
252 GLOBAL_ARGS = [
253     Opts.debug,
254     Opts.help,
255     Opts.rcfile,
256     ]
257
258 CMDS = {
259     'annotate': CmdOptionParser(
260         "annotate",
261         [
262             Opts.directory,
263             Opts.ignore_errors,
264             Opts.include,
265             Opts.omit,
266             ] + GLOBAL_ARGS,
267         usage="[options] [modules]",
268         description=(
269             "Make annotated copies of the given files, marking statements that are executed "
270             "with > and statements that are missed with !."
271         ),
272     ),
273
274     'combine': CmdOptionParser(
275         "combine",
276         [
277             Opts.append,
278             ] + GLOBAL_ARGS,
279         usage="[options] <path1> <path2> ... <pathN>",
280         description=(
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."
287         ),
288     ),
289
290     'debug': CmdOptionParser(
291         "debug", GLOBAL_ARGS,
292         usage="<topic>",
293         description=(
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."
298         ),
299     ),
300
301     'erase': CmdOptionParser(
302         "erase", GLOBAL_ARGS,
303         description="Erase previously collected coverage data.",
304     ),
305
306     'help': CmdOptionParser(
307         "help", GLOBAL_ARGS,
308         usage="[command]",
309         description="Describe how to use coverage.py",
310     ),
311
312     'html': CmdOptionParser(
313         "html",
314         [
315             Opts.directory,
316             Opts.fail_under,
317             Opts.ignore_errors,
318             Opts.include,
319             Opts.omit,
320             Opts.title,
321             ] + GLOBAL_ARGS,
322         usage="[options] [modules]",
323         description=(
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."
327         ),
328     ),
329
330     'report': CmdOptionParser(
331         "report",
332         [
333             Opts.fail_under,
334             Opts.ignore_errors,
335             Opts.include,
336             Opts.omit,
337             Opts.show_missing,
338             Opts.skip_covered,
339             ] + GLOBAL_ARGS,
340         usage="[options] [modules]",
341         description="Report coverage statistics on modules."
342     ),
343
344     'run': CmdOptionParser(
345         "run",
346         [
347             Opts.append,
348             Opts.branch,
349             Opts.concurrency,
350             Opts.include,
351             Opts.module,
352             Opts.omit,
353             Opts.pylib,
354             Opts.parallel_mode,
355             Opts.source,
356             Opts.timid,
357             ] + GLOBAL_ARGS,
358         usage="[options] <pyfile> [program options]",
359         description="Run a Python program, measuring code execution."
360     ),
361
362     'xml': CmdOptionParser(
363         "xml",
364         [
365             Opts.fail_under,
366             Opts.ignore_errors,
367             Opts.include,
368             Opts.omit,
369             Opts.output_xml,
370             ] + GLOBAL_ARGS,
371         usage="[options] [modules]",
372         description="Generate an XML report of coverage results."
373     ),
374 }
375
376
377 OK, ERR, FAIL_UNDER = 0, 1, 2
378
379
380 class CoverageScript(object):
381     """The command-line interface to coverage.py."""
382
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.
386         if _covpkg:
387             self.covpkg = _covpkg
388         else:
389             import coverage
390             self.covpkg = coverage
391
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
398
399         self.coverage = None
400
401         self.program_name = os.path.basename(sys.argv[0])
402         if self.program_name == '__main__.py':
403             self.program_name = 'coverage'
404         if env.WINDOWS:
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)]
413
414     def command_line(self, argv):
415         """The bulk of the command line interface to coverage.py.
416
417         `argv` is the argument list to process.
418
419         Returns 0 if all is well, 1 if something went wrong.
420
421         """
422         # Collect the command-line options.
423         if not argv:
424             self.help_fn(topic='minimum_help')
425             return OK
426
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()
432         else:
433             parser = CMDS.get(argv[0])
434             if not parser:
435                 self.help_fn("Unknown command: '%s'" % argv[0])
436                 return ERR
437             argv = argv[1:]
438
439         parser.help_fn = self.help_fn
440         ok, options, args = parser.parse_args_ok(argv)
441         if not ok:
442             return ERR
443
444         # Handle help and version.
445         if self.do_help(options, args, parser):
446             return OK
447
448         # We need to be able to import from the current directory, because
449         # plugins may try to, for example, to read Django settings.
450         sys.path[0] = ''
451
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)
457
458         # Do something.
459         self.coverage = self.covpkg.coverage(
460             data_suffix=options.parallel_mode,
461             cover_pylib=options.pylib,
462             timid=options.timid,
463             branch=options.branch,
464             config_file=options.rcfile,
465             source=source,
466             omit=omit,
467             include=include,
468             debug=debug,
469             concurrency=options.concurrency,
470             )
471
472         if options.action == "debug":
473             return self.do_debug(args)
474
475         elif options.action == "erase":
476             self.coverage.erase()
477             return OK
478
479         elif options.action == "run":
480             return self.do_run(options, args)
481
482         elif options.action == "combine":
483             if options.append:
484                 self.coverage.load()
485             data_dirs = args or None
486             self.coverage.combine(data_dirs)
487             self.coverage.save()
488             return OK
489
490         # Remaining actions are reporting, with some common options.
491         report_args = dict(
492             morfs=unglob_args(args),
493             ignore_errors=options.ignore_errors,
494             omit=omit,
495             include=include,
496             )
497
498         self.coverage.load()
499
500         total = None
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,
511                 **report_args)
512         elif options.action == "xml":
513             outfile = options.outfile
514             total = self.coverage.xml_report(outfile=outfile, **report_args)
515
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)
521
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.
525                 if 99 < total < 100:
526                     total = 99
527                 else:
528                     total = round(total)
529
530                 if total >= self.coverage.get_option("report:fail_under"):
531                     return OK
532                 else:
533                     return FAIL_UNDER
534
535         return OK
536
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
540         if error:
541             print(error)
542             print("Use '%s help' for help." % (self.program_name,))
543         elif parser:
544             print(parser.format_help().strip())
545         else:
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'
550             else:
551                 help_params['extension_modifier'] = 'without C extension'
552             help_msg = textwrap.dedent(HELP_TOPICS.get(topic, '')).strip()
553             if help_msg:
554                 print(help_msg.format(**help_params))
555             else:
556                 print("Don't know topic %r" % topic)
557
558     def do_help(self, options, args, parser):
559         """Deal with help requests.
560
561         Return True if it handled the request, False if not.
562
563         """
564         # Handle help.
565         if options.help:
566             if self.global_option:
567                 self.help_fn(topic='help')
568             else:
569                 self.help_fn(parser=parser)
570             return True
571
572         if options.action == "help":
573             if args:
574                 for a in args:
575                     parser = CMDS.get(a)
576                     if parser:
577                         self.help_fn(parser=parser)
578                     else:
579                         self.help_fn(topic=a)
580             else:
581                 self.help_fn(topic='help')
582             return True
583
584         # Handle version.
585         if options.version:
586             self.help_fn(topic='version')
587             return True
588
589         return False
590
591     def do_run(self, options, args):
592         """Implementation of 'coverage run'."""
593
594         if not args:
595             self.help_fn("Nothing to do.")
596             return ERR
597
598         if options.append and self.coverage.get_option("run:parallel"):
599             self.help_fn("Can't append to data files in parallel mode.")
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
614
615         if not self.coverage.get_option("run:parallel"):
616             if not options.append:
617                 self.coverage.erase()
618
619         # Run the script.
620         self.coverage.start()
621         code_ran = True
622         try:
623             if options.module:
624                 self.run_python_module(args[0], args)
625             else:
626                 filename = args[0]
627                 self.run_python_file(filename, args)
628         except NoSource:
629             code_ran = False
630             raise
631         finally:
632             self.coverage.stop()
633             if code_ran:
634                 if options.append:
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])
638                 self.coverage.save()
639
640         return OK
641
642     def do_debug(self, args):
643         """Implementation of 'coverage debug'."""
644
645         if not args:
646             self.help_fn("What information would you like: config, data, sys?")
647             return ERR
648
649         for info in args:
650             if info == 'sys':
651                 sys_info = self.coverage.sys_info()
652                 print(info_header("sys"))
653                 for line in info_formatter(sys_info):
654                     print(" %s" % line)
655             elif info == 'data':
656                 self.coverage.load()
657                 data = self.coverage.data
658                 print(info_header("data"))
659                 print("path: %s" % self.coverage.data_files.filename)
660                 if data:
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))
665                     for f in filenames:
666                         line = "%s: %d lines" % (f, summary[f])
667                         plugin = data.file_tracer(f)
668                         if plugin:
669                             line += " [%s]" % plugin
670                         print(line)
671                 else:
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):
677                     print(" %s" % line)
678             else:
679                 self.help_fn("Don't know what you mean by %r" % info)
680                 return ERR
681
682         return OK
683
684
685 def unshell_list(s):
686     """Turn a command-line argument into a list."""
687     if not s:
688         return None
689     if env.WINDOWS:
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.
695         s = s.strip("'")
696     return s.split(',')
697
698
699 def unglob_args(args):
700     """Interpret shell wildcards for platforms that need it."""
701     if env.WINDOWS:
702         globbed = []
703         for arg in args:
704             if '?' in arg or '*' in arg:
705                 globbed.extend(glob.glob(arg))
706             else:
707                 globbed.append(arg)
708         args = globbed
709     return args
710
711
712 HELP_TOPICS = {
713     'help': """\
714         Coverage.py, version {__version__} {extension_modifier}
715         Measure, collect, and report on code coverage in Python programs.
716
717         usage: {program_name} <command> [options] [args]
718
719         Commands:
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.
728
729         Use "{program_name} help <command>" for detailed help on any command.
730         For full documentation, see {__url__}
731     """,
732
733     'minimum_help': """\
734         Code coverage for Python.  Use '{program_name} help' for help.
735     """,
736
737     'version': """\
738         Coverage.py, version {__version__} {extension_modifier}
739         Documentation at {__url__}
740     """,
741 }
742
743
744 def main(argv=None):
745     """The main entry point to coverage.py.
746
747     This is installed as the script entry point.
748
749     """
750     if argv is None:
751         argv = sys.argv[1:]
752     try:
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
757         # exception.
758         traceback.print_exception(*err.args)
759         status = ERR
760     except CoverageException as err:
761         # A controlled error inside coverage.py: print the message to the user.
762         print(err)
763         status = ERR
764     except SystemExit as err:
765         # The user called `sys.exit()`.  Exit with their argument, if any.
766         if err.args:
767             status = err.args[0]
768         else:
769             status = None
770     return status