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