cmdline.py 24.4 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740
"""Command-line support for Coverage."""

import optparse, os, sys, traceback

from coverage.backward import sorted                # pylint: disable=W0622
from coverage.execfile import run_python_file, run_python_module
from coverage.misc import CoverageException, ExceptionDuringRun, NoSource
from coverage.debug import info_formatter


class Opts(object):
    """A namespace class for individual options we'll build parsers from."""

    append = optparse.make_option(
        '-a', '--append', action='store_false', dest="erase_first",
        help="Append coverage data to .coverage, otherwise it is started "
                "clean with each run."
        )
    branch = optparse.make_option(
        '', '--branch', action='store_true',
        help="Measure branch coverage in addition to statement coverage."
        )
    debug = optparse.make_option(
        '', '--debug', action='store', metavar="OPTS",
        help="Debug options, separated by commas"
        )
    directory = optparse.make_option(
        '-d', '--directory', action='store', metavar="DIR",
        help="Write the output files to DIR."
        )
    fail_under = optparse.make_option(
        '', '--fail-under', action='store', metavar="MIN", type="int",
        help="Exit with a status of 2 if the total coverage is less than MIN."
        )
    help = optparse.make_option(
        '-h', '--help', action='store_true',
        help="Get help on this command."
        )
    ignore_errors = optparse.make_option(
        '-i', '--ignore-errors', action='store_true',
        help="Ignore errors while reading source files."
        )
    include = optparse.make_option(
        '', '--include', action='store',
        metavar="PAT1,PAT2,...",
        help="Include files only when their filename path matches one of "
                "these patterns.  Usually needs quoting on the command line."
        )
    pylib = optparse.make_option(
        '-L', '--pylib', action='store_true',
        help="Measure coverage even inside the Python installed library, "
                "which isn't done by default."
        )
    show_missing = optparse.make_option(
        '-m', '--show-missing', action='store_true',
        help="Show line numbers of statements in each module that weren't "
                "executed."
        )
    old_omit = optparse.make_option(
        '-o', '--omit', action='store',
        metavar="PAT1,PAT2,...",
        help="Omit files when their filename matches one of these patterns. "
                "Usually needs quoting on the command line."
        )
    omit = optparse.make_option(
        '', '--omit', action='store',
        metavar="PAT1,PAT2,...",
        help="Omit files when their filename matches one of these patterns. "
                "Usually needs quoting on the command line."
        )
    output_xml = optparse.make_option(
        '-o', '', action='store', dest="outfile",
        metavar="OUTFILE",
        help="Write the XML report to this file. Defaults to 'coverage.xml'"
        )
    parallel_mode = optparse.make_option(
        '-p', '--parallel-mode', action='store_true',
        help="Append the machine name, process id and random number to the "
                ".coverage data file name to simplify collecting data from "
                "many processes."
        )
    module = optparse.make_option(
        '-m', '--module', action='store_true',
        help="<pyfile> is an importable Python module, not a script path, "
                "to be run as 'python -m' would run it."
        )
    rcfile = optparse.make_option(
        '', '--rcfile', action='store',
        help="Specify configuration file.  Defaults to '.coveragerc'"
        )
    source = optparse.make_option(
        '', '--source', action='store', metavar="SRC1,SRC2,...",
        help="A list of packages or directories of code to be measured."
        )
    timid = optparse.make_option(
        '', '--timid', action='store_true',
        help="Use a simpler but slower trace method.  Try this if you get "
                "seemingly impossible results!"
        )
    title = optparse.make_option(
        '', '--title', action='store', metavar="TITLE",
        help="A text string to use as the title on the HTML."
        )
    version = optparse.make_option(
        '', '--version', action='store_true',
        help="Display version information and exit."
        )


class CoverageOptionParser(optparse.OptionParser, object):
    """Base OptionParser for coverage.

    Problems don't exit the program.
    Defaults are initialized for all options.

    """

    def __init__(self, *args, **kwargs):
        super(CoverageOptionParser, self).__init__(
            add_help_option=False, *args, **kwargs
            )
        self.set_defaults(
            actions=[],
            branch=None,
            debug=None,
            directory=None,
            fail_under=None,
            help=None,
            ignore_errors=None,
            include=None,
            omit=None,
            parallel_mode=None,
            module=None,
            pylib=None,
            rcfile=True,
            show_missing=None,
            source=None,
            timid=None,
            title=None,
            erase_first=None,
            version=None,
            )

        self.disable_interspersed_args()
        self.help_fn = self.help_noop

    def help_noop(self, error=None, topic=None, parser=None):
        """No-op help function."""
        pass

    class OptionParserError(Exception):
        """Used to stop the optparse error handler ending the process."""
        pass

    def parse_args(self, args=None, options=None):
        """Call optparse.parse_args, but return a triple:

        (ok, options, args)

        """
        try:
            options, args = \
                super(CoverageOptionParser, self).parse_args(args, options)
        except self.OptionParserError:
            return False, None, None
        return True, options, args

    def error(self, msg):
        """Override optparse.error so sys.exit doesn't get called."""
        self.help_fn(msg)
        raise self.OptionParserError


class ClassicOptionParser(CoverageOptionParser):
    """Command-line parser for coverage.py classic arguments."""

    def __init__(self):
        super(ClassicOptionParser, self).__init__()

        self.add_action('-a', '--annotate', 'annotate')
        self.add_action('-b', '--html', 'html')
        self.add_action('-c', '--combine', 'combine')
        self.add_action('-e', '--erase', 'erase')
        self.add_action('-r', '--report', 'report')
        self.add_action('-x', '--execute', 'execute')

        self.add_options([
            Opts.directory,
            Opts.help,
            Opts.ignore_errors,
            Opts.pylib,
            Opts.show_missing,
            Opts.old_omit,
            Opts.parallel_mode,
            Opts.timid,
            Opts.version,
        ])

    def add_action(self, dash, dashdash, action_code):
        """Add a specialized option that is the action to execute."""
        option = self.add_option(dash, dashdash, action='callback',
            callback=self._append_action
            )
        option.action_code = action_code

    def _append_action(self, option, opt_unused, value_unused, parser):
        """Callback for an option that adds to the `actions` list."""
        parser.values.actions.append(option.action_code)


class CmdOptionParser(CoverageOptionParser):
    """Parse one of the new-style commands for coverage.py."""

    def __init__(self, action, options=None, defaults=None, usage=None,
                cmd=None, description=None
                ):
        """Create an OptionParser for a coverage command.

        `action` is the slug to put into `options.actions`.
        `options` is a list of Option's for the command.
        `defaults` is a dict of default value for options.
        `usage` is the usage string to display in help.
        `cmd` is the command name, if different than `action`.
        `description` is the description of the command, for the help text.

        """
        if usage:
            usage = "%prog " + usage
        super(CmdOptionParser, self).__init__(
            prog="coverage %s" % (cmd or action),
            usage=usage,
            description=description,
        )
        self.set_defaults(actions=[action], **(defaults or {}))
        if options:
            self.add_options(options)
        self.cmd = cmd or action

    def __eq__(self, other):
        # A convenience equality, so that I can put strings in unit test
        # results, and they will compare equal to objects.
        return (other == "<CmdOptionParser:%s>" % self.cmd)

GLOBAL_ARGS = [
    Opts.rcfile,
    Opts.help,
    ]

CMDS = {
    'annotate': CmdOptionParser("annotate",
        [
            Opts.directory,
            Opts.ignore_errors,
            Opts.omit,
            Opts.include,
            ] + GLOBAL_ARGS,
        usage = "[options] [modules]",
        description = "Make annotated copies of the given files, marking "
            "statements that are executed with > and statements that are "
            "missed with !."
        ),

    'combine': CmdOptionParser("combine", GLOBAL_ARGS,
        usage = " ",
        description = "Combine data from multiple coverage files collected "
            "with 'run -p'.  The combined results are written to a single "
            "file representing the union of the data."
        ),

    'debug': CmdOptionParser("debug", GLOBAL_ARGS,
        usage = "<topic>",
        description = "Display information on the internals of coverage.py, "
            "for diagnosing problems. "
            "Topics are 'data' to show a summary of the collected data, "
            "or 'sys' to show installation information."
        ),

    'erase': CmdOptionParser("erase", GLOBAL_ARGS,
        usage = " ",
        description = "Erase previously collected coverage data."
        ),

    'help': CmdOptionParser("help", GLOBAL_ARGS,
        usage = "[command]",
        description = "Describe how to use coverage.py"
        ),

    'html': CmdOptionParser("html",
        [
            Opts.directory,
            Opts.fail_under,
            Opts.ignore_errors,
            Opts.omit,
            Opts.include,
            Opts.title,
            ] + GLOBAL_ARGS,
        usage = "[options] [modules]",
        description = "Create an HTML report of the coverage of the files.  "
            "Each file gets its own page, with the source decorated to show "
            "executed, excluded, and missed lines."
        ),

    'report': CmdOptionParser("report",
        [
            Opts.fail_under,
            Opts.ignore_errors,
            Opts.omit,
            Opts.include,
            Opts.show_missing,
            ] + GLOBAL_ARGS,
        usage = "[options] [modules]",
        description = "Report coverage statistics on modules."
        ),

    'run': CmdOptionParser("execute",
        [
            Opts.append,
            Opts.branch,
            Opts.debug,
            Opts.pylib,
            Opts.parallel_mode,
            Opts.module,
            Opts.timid,
            Opts.source,
            Opts.omit,
            Opts.include,
            ] + GLOBAL_ARGS,
        defaults = {'erase_first': True},
        cmd = "run",
        usage = "[options] <pyfile> [program options]",
        description = "Run a Python program, measuring code execution."
        ),

    'xml': CmdOptionParser("xml",
        [
            Opts.fail_under,
            Opts.ignore_errors,
            Opts.omit,
            Opts.include,
            Opts.output_xml,
            ] + GLOBAL_ARGS,
        cmd = "xml",
        usage = "[options] [modules]",
        description = "Generate an XML report of coverage results."
        ),
    }


OK, ERR, FAIL_UNDER = 0, 1, 2


class CoverageScript(object):
    """The command-line interface to Coverage."""

    def __init__(self, _covpkg=None, _run_python_file=None,
                 _run_python_module=None, _help_fn=None):
        # _covpkg is for dependency injection, so we can test this code.
        if _covpkg:
            self.covpkg = _covpkg
        else:
            import coverage
            self.covpkg = coverage

        # For dependency injection:
        self.run_python_file = _run_python_file or run_python_file
        self.run_python_module = _run_python_module or run_python_module
        self.help_fn = _help_fn or self.help
        self.classic = False

        self.coverage = None

    def command_line(self, argv):
        """The bulk of the command line interface to Coverage.

        `argv` is the argument list to process.

        Returns 0 if all is well, 1 if something went wrong.

        """
        # Collect the command-line options.
        if not argv:
            self.help_fn(topic='minimum_help')
            return OK

        # The command syntax we parse depends on the first argument.  Classic
        # syntax always starts with an option.
        self.classic = argv[0].startswith('-')
        if self.classic:
            parser = ClassicOptionParser()
        else:
            parser = CMDS.get(argv[0])
            if not parser:
                self.help_fn("Unknown command: '%s'" % argv[0])
                return ERR
            argv = argv[1:]

        parser.help_fn = self.help_fn
        ok, options, args = parser.parse_args(argv)
        if not ok:
            return ERR

        # Handle help and version.
        if self.do_help(options, args, parser):
            return OK

        # Check for conflicts and problems in the options.
        if not self.args_ok(options, args):
            return ERR

        # Listify the list options.
        source = unshell_list(options.source)
        omit = unshell_list(options.omit)
        include = unshell_list(options.include)
        debug = unshell_list(options.debug)

        # Do something.
        self.coverage = self.covpkg.coverage(
            data_suffix = options.parallel_mode,
            cover_pylib = options.pylib,
            timid = options.timid,
            branch = options.branch,
            config_file = options.rcfile,
            source = source,
            omit = omit,
            include = include,
            debug = debug,
            )

        if 'debug' in options.actions:
            return self.do_debug(args)

        if 'erase' in options.actions or options.erase_first:
            self.coverage.erase()
        else:
            self.coverage.load()

        if 'execute' in options.actions:
            self.do_execute(options, args)

        if 'combine' in options.actions:
            self.coverage.combine()
            self.coverage.save()

        # Remaining actions are reporting, with some common options.
        report_args = dict(
            morfs = args,
            ignore_errors = options.ignore_errors,
            omit = omit,
            include = include,
            )

        if 'report' in options.actions:
            total = self.coverage.report(
                show_missing=options.show_missing, **report_args)
        if 'annotate' in options.actions:
            self.coverage.annotate(
                directory=options.directory, **report_args)
        if 'html' in options.actions:
            total = self.coverage.html_report(
                directory=options.directory, title=options.title,
                **report_args)
        if 'xml' in options.actions:
            outfile = options.outfile
            total = self.coverage.xml_report(outfile=outfile, **report_args)

        if options.fail_under is not None:
            if total >= options.fail_under:
                return OK
            else:
                return FAIL_UNDER
        else:
            return OK

    def help(self, error=None, topic=None, parser=None):
        """Display an error message, or the named topic."""
        assert error or topic or parser
        if error:
            print(error)
            print("Use 'coverage help' for help.")
        elif parser:
            print(parser.format_help().strip())
        else:
            help_msg = HELP_TOPICS.get(topic, '').strip()
            if help_msg:
                print(help_msg % self.covpkg.__dict__)
            else:
                print("Don't know topic %r" % topic)

    def do_help(self, options, args, parser):
        """Deal with help requests.

        Return True if it handled the request, False if not.

        """
        # Handle help.
        if options.help:
            if self.classic:
                self.help_fn(topic='help')
            else:
                self.help_fn(parser=parser)
            return True

        if "help" in options.actions:
            if args:
                for a in args:
                    parser = CMDS.get(a)
                    if parser:
                        self.help_fn(parser=parser)
                    else:
                        self.help_fn(topic=a)
            else:
                self.help_fn(topic='help')
            return True

        # Handle version.
        if options.version:
            self.help_fn(topic='version')
            return True

        return False

    def args_ok(self, options, args):
        """Check for conflicts and problems in the options.

        Returns True if everything is ok, or False if not.

        """
        for i in ['erase', 'execute']:
            for j in ['annotate', 'html', 'report', 'combine']:
                if (i in options.actions) and (j in options.actions):
                    self.help_fn("You can't specify the '%s' and '%s' "
                              "options at the same time." % (i, j))
                    return False

        if not options.actions:
            self.help_fn(
                "You must specify at least one of -e, -x, -c, -r, -a, or -b."
                )
            return False
        args_allowed = (
            'execute' in options.actions or
            'annotate' in options.actions or
            'html' in options.actions or
            'debug' in options.actions or
            'report' in options.actions or
            'xml' in options.actions
            )
        if not args_allowed and args:
            self.help_fn("Unexpected arguments: %s" % " ".join(args))
            return False

        if 'execute' in options.actions and not args:
            self.help_fn("Nothing to do.")
            return False

        return True

    def do_execute(self, options, args):
        """Implementation of 'coverage run'."""

        # Set the first path element properly.
        old_path0 = sys.path[0]

        # Run the script.
        self.coverage.start()
        code_ran = True
        try:
            try:
                if options.module:
                    sys.path[0] = ''
                    self.run_python_module(args[0], args)
                else:
                    filename = args[0]
                    sys.path[0] = os.path.abspath(os.path.dirname(filename))
                    self.run_python_file(filename, args)
            except NoSource:
                code_ran = False
                raise
        finally:
            self.coverage.stop()
            if code_ran:
                self.coverage.save()

            # Restore the old path
            sys.path[0] = old_path0

    def do_debug(self, args):
        """Implementation of 'coverage debug'."""

        if not args:
            self.help_fn("What information would you like: data, sys?")
            return ERR
        for info in args:
            if info == 'sys':
                print("-- sys ----------------------------------------")
                for line in info_formatter(self.coverage.sysinfo()):
                    print(" %s" % line)
            elif info == 'data':
                print("-- data ---------------------------------------")
                self.coverage.load()
                print("path: %s" % self.coverage.data.filename)
                print("has_arcs: %r" % self.coverage.data.has_arcs())
                summary = self.coverage.data.summary(fullpath=True)
                if summary:
                    filenames = sorted(summary.keys())
                    print("\n%d files:" % len(filenames))
                    for f in filenames:
                        print("%s: %d lines" % (f, summary[f]))
                else:
                    print("No data collected")
            else:
                self.help_fn("Don't know what you mean by %r" % info)
                return ERR
        return OK


def unshell_list(s):
    """Turn a command-line argument into a list."""
    if not s:
        return None
    if sys.platform == 'win32':
        # When running coverage as coverage.exe, some of the behavior
        # of the shell is emulated: wildcards are expanded into a list of
        # filenames.  So you have to single-quote patterns on the command
        # line, but (not) helpfully, the single quotes are included in the
        # argument, so we have to strip them off here.
        s = s.strip("'")
    return s.split(',')


HELP_TOPICS = {
# -------------------------
'classic':
r"""Coverage.py version %(__version__)s
Measure, collect, and report on code coverage in Python programs.

Usage:

coverage -x [-p] [-L] [--timid] MODULE.py [ARG1 ARG2 ...]
    Execute the module, passing the given command-line arguments, collecting
    coverage data.  With the -p option, include the machine name and process
    id in the .coverage file name.  With -L, measure coverage even inside the
    Python installed library, which isn't done by default.  With --timid, use a
    simpler but slower trace method.

coverage -e
    Erase collected coverage data.

coverage -c
    Combine data from multiple coverage files (as created by -p option above)
    and store it into a single file representing the union of the coverage.

coverage -r [-m] [-i] [-o DIR,...] [FILE1 FILE2 ...]
    Report on the statement coverage for the given files.  With the -m
    option, show line numbers of the statements that weren't executed.

coverage -b -d DIR [-i] [-o DIR,...] [FILE1 FILE2 ...]
    Create an HTML report of the coverage of the given files.  Each file gets
    its own page, with the file listing decorated to show executed, excluded,
    and missed lines.

coverage -a [-d DIR] [-i] [-o DIR,...] [FILE1 FILE2 ...]
    Make annotated copies of the given files, marking statements that
    are executed with > and statements that are missed with !.

-d DIR
    Write output files for -b or -a to this directory.

-i  Ignore errors while reporting or annotating.

-o DIR,...
    Omit reporting or annotating files when their filename path starts with
    a directory listed in the omit list.
    e.g. coverage -i -r -o c:\python25,lib\enthought\traits

Coverage data is saved in the file .coverage by default.  Set the
COVERAGE_FILE environment variable to save it somewhere else.
""",
# -------------------------
'help': """\
Coverage.py, version %(__version__)s
Measure, collect, and report on code coverage in Python programs.

usage: coverage <command> [options] [args]

Commands:
    annotate    Annotate source files with execution information.
    combine     Combine a number of data files.
    erase       Erase previously collected coverage data.
    help        Get help on using coverage.py.
    html        Create an HTML report.
    report      Report coverage stats on modules.
    run         Run a Python program and measure code execution.
    xml         Create an XML report of coverage results.

Use "coverage help <command>" for detailed help on any command.
Use "coverage help classic" for help on older command syntax.
For more information, see %(__url__)s
""",
# -------------------------
'minimum_help': """\
Code coverage for Python.  Use 'coverage help' for help.
""",
# -------------------------
'version': """\
Coverage.py, version %(__version__)s.  %(__url__)s
""",
}


def main(argv=None):
    """The main entry point to Coverage.

    This is installed as the script entry point.

    """
    if argv is None:
        argv = sys.argv[1:]
    try:
        status = CoverageScript().command_line(argv)
    except ExceptionDuringRun:
        # An exception was caught while running the product code.  The
        # sys.exc_info() return tuple is packed into an ExceptionDuringRun
        # exception.
        _, err, _ = sys.exc_info()
        traceback.print_exception(*err.args)
        status = ERR
    except CoverageException:
        # A controlled error inside coverage.py: print the message to the user.
        _, err, _ = sys.exc_info()
        print(err)
        status = ERR
    except SystemExit:
        # The user called `sys.exit()`.  Exit with their argument, if any.
        _, err, _ = sys.exc_info()
        if err.args:
            status = err.args[0]
        else:
            status = None
    return status