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