1"""Command-line support for Coverage."""
2
3import optparse, re, sys, traceback
4
5from coverage.backward import sorted                # pylint: disable=W0622
6from coverage.execfile import run_python_file, run_python_module
7from coverage.misc import CoverageException, ExceptionDuringRun, NoSource
8
9
10class 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
98class 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
159class 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
196class 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
229GLOBAL_ARGS = [
230    Opts.rcfile,
231    Opts.help,
232    ]
233
234CMDS = {
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
330OK, ERR = 0, 1
331
332
333class 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
557def 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
571HELP_TOPICS = r"""
572
573== classic ====================================================================
574Coverage.py version %(__version__)s
575Measure, collect, and report on code coverage in Python programs.
576
577Usage:
578
579coverage -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
586coverage -e
587    Erase collected coverage data.
588
589coverage -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
593coverage -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
597coverage -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
602coverage -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
616Coverage data is saved in the file .coverage by default.  Set the
617COVERAGE_FILE environment variable to save it somewhere else.
618
619== help =======================================================================
620Coverage.py, version %(__version__)s
621Measure, collect, and report on code coverage in Python programs.
622
623usage: coverage <command> [options] [args]
624
625Commands:
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
635Use "coverage help <command>" for detailed help on any command.
636Use "coverage help classic" for help on older command syntax.
637For more information, see %(__url__)s
638
639== minimum_help ===============================================================
640Code coverage for Python.  Use 'coverage help' for help.
641
642== version ====================================================================
643Coverage.py, version %(__version__)s.  %(__url__)s
644
645"""
646
647
648def 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