summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/__init__.py10
-rw-r--r--coverage/annotate.py32
-rw-r--r--coverage/backward.py31
-rw-r--r--coverage/bytecode.py24
-rw-r--r--coverage/cmdline.py224
-rw-r--r--coverage/codeunit.py58
-rw-r--r--coverage/collector.py71
-rw-r--r--coverage/config.py112
-rw-r--r--coverage/control.py361
-rw-r--r--coverage/data.py86
-rw-r--r--coverage/execfile.py34
-rw-r--r--coverage/files.py23
-rw-r--r--coverage/html.py90
-rw-r--r--coverage/htmlfiles/coverage_html.js87
-rw-r--r--coverage/htmlfiles/index.html170
-rw-r--r--coverage/htmlfiles/pyfile.html119
-rw-r--r--coverage/htmlfiles/style.css40
-rw-r--r--coverage/misc.py29
-rw-r--r--coverage/parser.py254
-rw-r--r--coverage/phystokens.py23
-rw-r--r--coverage/report.py37
-rw-r--r--coverage/results.py24
-rw-r--r--coverage/summary.py31
-rw-r--r--coverage/templite.py58
-rw-r--r--coverage/tracer.c185
-rw-r--r--coverage/xmlreport.py74
26 files changed, 1528 insertions, 759 deletions
diff --git a/coverage/__init__.py b/coverage/__init__.py
index 7deeff50..9fd4a8d2 100644
--- a/coverage/__init__.py
+++ b/coverage/__init__.py
@@ -5,11 +5,11 @@ http://nedbatchelder.com/code/coverage
"""
-__version__ = "3.2b3" # see detailed history in CHANGES.txt
+__version__ = "3.3.2a1" # see detailed history in CHANGES.txt
__url__ = "http://nedbatchelder.com/code/coverage"
-from coverage.control import coverage
+from coverage.control import coverage, process_startup
from coverage.data import CoverageData
from coverage.cmdline import main, CoverageScript
from coverage.misc import CoverageException
@@ -26,10 +26,10 @@ _the_coverage = None
def _singleton_method(name):
"""Return a function to the `name` method on a singleton `coverage` object.
-
+
The singleton object is created the first time one of these functions is
called.
-
+
"""
def wrapper(*args, **kwargs):
"""Singleton wrapper around a coverage method."""
@@ -55,7 +55,7 @@ annotate = _singleton_method('annotate')
# COPYRIGHT AND LICENSE
#
# Copyright 2001 Gareth Rees. All rights reserved.
-# Copyright 2004-2009 Ned Batchelder. All rights reserved.
+# Copyright 2004-2010 Ned Batchelder. All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
diff --git a/coverage/annotate.py b/coverage/annotate.py
index 2fa9d5cf..5cbdd6a0 100644
--- a/coverage/annotate.py
+++ b/coverage/annotate.py
@@ -6,11 +6,11 @@ from coverage.report import Reporter
class AnnotateReporter(Reporter):
"""Generate annotated source files showing line coverage.
-
+
This reporter creates annotated copies of the measured source files. Each
.py file is copied as a .py,cover file, with a left-hand margin annotating
each line::
-
+
> def h(x):
- if 0: #pragma: no cover
- pass
@@ -18,30 +18,38 @@ class AnnotateReporter(Reporter):
! a = 1
> else:
> a = 2
-
+
> h(2)
Executed lines use '>', lines not executed use '!', lines excluded from
consideration use '-'.
-
+
"""
def __init__(self, coverage, ignore_errors=False):
super(AnnotateReporter, self).__init__(coverage, ignore_errors)
self.directory = None
-
+
blank_re = re.compile(r"\s*(#|$)")
else_re = re.compile(r"\s*else\s*:\s*(#|$)")
- def report(self, morfs, directory=None, omit_prefixes=None):
- """Run the report."""
- self.report_files(self.annotate_file, morfs, directory, omit_prefixes)
-
+ def report(self, morfs, directory=None, omit_prefixes=None,
+ include_prefixes=None):
+ """Run the report.
+
+ See `coverage.report()` for arguments.
+
+ """
+ self.report_files(
+ self.annotate_file, morfs, directory, omit_prefixes,
+ include_prefixes
+ )
+
def annotate_file(self, cu, analysis):
"""Annotate a single file.
-
+
`cu` is the CodeUnit for the file to annotate.
-
+
"""
if not cu.relative:
return
@@ -77,7 +85,7 @@ class AnnotateReporter(Reporter):
if self.blank_re.match(line):
dest.write(' ')
elif self.else_re.match(line):
- # Special logic for lines containing only 'else:'.
+ # Special logic for lines containing only 'else:'.
if i >= len(statements) and j >= len(missing):
dest.write('! ')
elif i >= len(statements) or j >= len(missing):
diff --git a/coverage/backward.py b/coverage/backward.py
index 66cfbb96..425bcc6e 100644
--- a/coverage/backward.py
+++ b/coverage/backward.py
@@ -14,7 +14,6 @@ try:
except NameError:
from sets import Set as set
-
# Python 2.3 doesn't have `sorted`.
try:
sorted = sorted
@@ -26,7 +25,6 @@ except NameError:
return lst
# Pythons 2 and 3 differ on where to get StringIO
-
try:
from cStringIO import StringIO
BytesIO = StringIO
@@ -34,39 +32,42 @@ except ImportError:
from io import StringIO, BytesIO
# What's a string called?
-
try:
string_class = basestring
except NameError:
string_class = str
# Where do pickles come from?
-
try:
import cPickle as pickle
except ImportError:
import pickle
# range or xrange?
-
try:
range = xrange
except NameError:
range = range
# Exec is a statement in Py2, a function in Py3
-
-if sys.hexversion > 0x03000000:
- def exec_function(source, filename, global_map):
+if sys.version_info >= (3, 0):
+ def exec_code_object(code, global_map):
"""A wrapper around exec()."""
- exec(compile(source, filename, "exec"), global_map)
+ exec(code, global_map)
else:
# OK, this is pretty gross. In Py2, exec was a statement, but that will
# be a syntax error if we try to put it in a Py3 file, even if it is never
# executed. So hide it inside an evaluated string literal instead.
- eval(compile("""\
-def exec_function(source, filename, global_map):
- exec compile(source, filename, "exec") in global_map
-""",
- "<exec_function>", "exec"
- ))
+ eval(
+ compile(
+ "def exec_code_object(code, global_map):\n"
+ " exec code in global_map\n",
+ "<exec_function>", "exec"
+ )
+ )
+
+# ConfigParser was renamed to the more-standard configparser
+try:
+ import configparser
+except ImportError:
+ import ConfigParser as configparser
diff --git a/coverage/bytecode.py b/coverage/bytecode.py
index 62a19bae..ab522d6c 100644
--- a/coverage/bytecode.py
+++ b/coverage/bytecode.py
@@ -14,15 +14,15 @@ class ByteCode(object):
class ByteCodes(object):
"""Iterator over byte codes in `code`.
-
+
Returns `ByteCode` objects.
-
+
"""
def __init__(self, code):
self.code = code
self.offset = 0
-
- if sys.hexversion > 0x03000000:
+
+ if sys.version_info >= (3, 0):
def __getitem__(self, i):
return self.code[i]
else:
@@ -31,30 +31,30 @@ class ByteCodes(object):
def __iter__(self):
return self
-
+
def __next__(self):
if self.offset >= len(self.code):
raise StopIteration
-
+
bc = ByteCode()
bc.op = self[self.offset]
bc.offset = self.offset
-
+
next_offset = self.offset+1
if bc.op >= opcode.HAVE_ARGUMENT:
bc.arg = self[self.offset+1] + 256*self[self.offset+2]
next_offset += 2
-
+
label = -1
if bc.op in opcode.hasjrel:
label = next_offset + bc.arg
elif bc.op in opcode.hasjabs:
label = bc.arg
bc.jump_to = label
-
+
bc.next_offset = self.offset = next_offset
return bc
-
+
next = __next__ # Py2k uses an old-style non-dunder name.
@@ -62,10 +62,10 @@ class CodeObjects(object):
"""Iterate over all the code objects in `code`."""
def __init__(self, code):
self.stack = [code]
-
+
def __iter__(self):
return self
-
+
def __next__(self):
if self.stack:
# We're going to return the code object on the stack, but first
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 1029ad63..cb47690c 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -1,17 +1,23 @@
"""Command-line support for Coverage."""
-import optparse, sys
+import optparse, re, sys, traceback
+from coverage.backward import sorted # pylint: disable-msg=W0622
from coverage.execfile import run_python_file
-from coverage.misc import CoverageException
+from coverage.misc import CoverageException, ExceptionDuringRun
class Opts(object):
"""A namespace class for individual options we'll build parsers from."""
-
+
+ append = optparse.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.Option(
'', '--branch', action='store_true',
- help="Measure branch execution. HIGHLY EXPERIMENTAL!"
+ help="Measure branch coverage in addition to statement coverage."
)
directory = optparse.Option(
'-d', '--directory', action='store',
@@ -26,6 +32,12 @@ class Opts(object):
'-i', '--ignore-errors', action='store_true',
help="Ignore errors while reading source files."
)
+ include = optparse.Option(
+ '', '--include', action='store',
+ metavar="PRE1,PRE2,...",
+ help="Include files only when their filename path starts with one of "
+ "these prefixes."
+ )
pylib = optparse.Option(
'-L', '--pylib', action='store_true',
help="Measure coverage even inside the Python installed library, "
@@ -55,26 +67,31 @@ class Opts(object):
)
parallel_mode = optparse.Option(
'-p', '--parallel-mode', action='store_true',
- help="Include the machine name and process id in the .coverage "
- "data file name."
+ help="Append the machine name, process id and random number to the "
+ ".coverage data file name to simplify collecting data from "
+ "many processes."
+ )
+ rcfile = optparse.Option(
+ '', '--rcfile', action='store',
+ help="Specify configuration file. Defaults to '.coveragerc'"
)
timid = optparse.Option(
'', '--timid', action='store_true',
help="Use a simpler but slower trace method. Try this if you get "
"seemingly impossible results!"
)
- append = optparse.Option(
- '-a', '--append', action='store_false', dest="erase_first",
- help="Append coverage data to .coverage, otherwise it is started "
- "clean with each run."
+ version = optparse.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):
@@ -87,26 +104,33 @@ class CoverageOptionParser(optparse.OptionParser, object):
directory=None,
help=None,
ignore_errors=None,
+ include=None,
omit=None,
parallel_mode=None,
pylib=None,
+ rcfile=True,
show_missing=None,
timid=None,
erase_first=None,
+ version=None,
)
self.disable_interspersed_args()
- self.help_fn = lambda: None
+ 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 = \
@@ -114,7 +138,7 @@ class CoverageOptionParser(optparse.OptionParser, object):
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)
@@ -126,7 +150,7 @@ class ClassicOptionParser(CoverageOptionParser):
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')
@@ -143,6 +167,7 @@ class ClassicOptionParser(CoverageOptionParser):
Opts.old_omit,
Opts.parallel_mode,
Opts.timid,
+ Opts.version,
])
def add_action(self, dash, dashdash, action_code):
@@ -151,7 +176,7 @@ class ClassicOptionParser(CoverageOptionParser):
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)
@@ -159,19 +184,19 @@ class ClassicOptionParser(CoverageOptionParser):
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
@@ -190,6 +215,10 @@ class CmdOptionParser(CoverageOptionParser):
# results, and they will compare equal to objects.
return (other == "<CmdOptionParser:%s>" % self.cmd)
+GLOBAL_ARGS = [
+ Opts.rcfile,
+ Opts.help,
+ ]
CMDS = {
'annotate': CmdOptionParser("annotate",
@@ -197,15 +226,35 @@ CMDS = {
Opts.directory,
Opts.ignore_errors,
Opts.omit,
- Opts.help,
- ],
+ 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 !."
),
- 'help': CmdOptionParser("help", [Opts.help],
+ '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"
),
@@ -215,43 +264,23 @@ CMDS = {
Opts.directory,
Opts.ignore_errors,
Opts.omit,
- Opts.help,
- ],
+ Opts.include,
+ ] + 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."
),
-
- 'combine': CmdOptionParser("combine", [Opts.help],
- usage = " ",
- description = "Combine data from multiple coverage files collected "
- "with 'run -p'. The combined results are stored into a single "
- "file representing the union of the coverage."
- ),
-
- 'debug': CmdOptionParser("debug", [Opts.help],
- usage = "<topic>",
- description = "Display information the internals of coverage.py, "
- "for diagnosing problems. "
- "Topics are 'data' to show a summary of the data collected in "
- ".coverage, or 'sys' to show installation information."
- ),
-
- 'erase': CmdOptionParser("erase", [Opts.help],
- usage = " ",
- description = "Erase previously collected coverage data."
- ),
'report': CmdOptionParser("report",
[
Opts.ignore_errors,
Opts.omit,
+ Opts.include,
Opts.show_missing,
- Opts.help,
- ],
+ ] + GLOBAL_ARGS,
usage = "[options] [modules]",
- description = "Report coverage stats on modules."
+ description = "Report coverage statistics on modules."
),
'run': CmdOptionParser("execute",
@@ -261,21 +290,22 @@ CMDS = {
Opts.pylib,
Opts.parallel_mode,
Opts.timid,
- Opts.help,
- ],
- defaults = {'erase_first':True},
+ 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."
+ description = "Run a Python program, measuring code execution."
),
-
+
'xml': CmdOptionParser("xml",
[
Opts.ignore_errors,
Opts.omit,
+ Opts.include,
Opts.output_xml,
- Opts.help,
- ],
+ ] + GLOBAL_ARGS,
cmd = "xml",
defaults = {'outfile': 'coverage.xml'},
usage = "[options] [modules]",
@@ -286,9 +316,10 @@ CMDS = {
OK, ERR = 0, 1
+
class CoverageScript(object):
"""The command-line interface to Coverage."""
-
+
def __init__(self, _covpkg=None, _run_python_file=None, _help_fn=None):
# _covpkg is for dependency injection, so we can test this code.
if _covpkg:
@@ -296,13 +327,13 @@ class CoverageScript(object):
else:
import coverage
self.covpkg = coverage
-
+
# _run_python_file is for dependency injection also.
self.run_python_file = _run_python_file or run_python_file
-
+
# _help_fn is for dependency injection.
self.help_fn = _help_fn or self.help
-
+
self.coverage = None
def help(self, error=None, topic=None, parser=None):
@@ -315,7 +346,6 @@ class CoverageScript(object):
print(parser.format_help().strip())
else:
# Parse out the topic we want from HELP_TOPICS
- import re
topic_list = re.split("(?m)^=+ (\w+) =+$", HELP_TOPICS)
topics = dict(zip(topic_list[1::2], topic_list[2::2]))
help_msg = topics.get(topic, '').strip()
@@ -326,14 +356,14 @@ class CoverageScript(object):
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
@@ -375,6 +405,11 @@ class CoverageScript(object):
self.help_fn(topic='help')
return OK
+ # Handle version.
+ if options.version:
+ self.help_fn(topic='version')
+ return OK
+
# Check for conflicts and problems in the options.
for i in ['erase', 'execute']:
for j in ['annotate', 'html', 'report', 'combine']:
@@ -399,17 +434,28 @@ class CoverageScript(object):
if not args_allowed and args:
self.help_fn("Unexpected arguments: %s" % " ".join(args))
return ERR
-
+
if 'execute' in options.actions and not args:
self.help_fn("Nothing to do.")
return ERR
-
+
+ # Listify the list options.
+ omit = None
+ if options.omit:
+ omit = options.omit.split(',')
+ include = None
+ if options.include:
+ include = options.include.split(',')
+
# Do something.
self.coverage = self.covpkg.coverage(
- data_suffix = bool(options.parallel_mode),
+ data_suffix = options.parallel_mode,
cover_pylib = options.pylib,
timid = options.timid,
branch = options.branch,
+ config_file = options.rcfile,
+ omit_prefixes = omit,
+ include_prefixes = include,
)
if 'debug' in options.actions:
@@ -429,9 +475,12 @@ class CoverageScript(object):
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:
@@ -465,11 +514,9 @@ class CoverageScript(object):
'ignore_errors': options.ignore_errors,
}
- omit = None
- if options.omit:
- omit = options.omit.split(',')
report_args['omit_prefixes'] = omit
-
+ report_args['include_prefixes'] = include
+
if 'report' in options.actions:
self.coverage.report(
show_missing=options.show_missing, **report_args)
@@ -481,8 +528,6 @@ class CoverageScript(object):
directory=options.directory, **report_args)
if 'xml' in options.actions:
outfile = options.outfile
- if outfile == '-':
- outfile = None
self.coverage.xml_report(outfile=outfile, **report_args)
return OK
@@ -491,7 +536,7 @@ class CoverageScript(object):
HELP_TOPICS = r"""
== classic ====================================================================
-Coverage version %(__version__)s
+Coverage.py version %(__version__)s
Measure, collect, and report on code coverage in Python programs.
Usage:
@@ -537,14 +582,14 @@ Coverage data is saved in the file .coverage by default. Set the
COVERAGE_FILE environment variable to save it somewhere else.
== help =======================================================================
-Coverage version %(__version__)s
+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.
+ 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.
@@ -559,19 +604,36 @@ 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():
+def main(argv=None):
"""The main entrypoint to Coverage.
-
+
This is installed as the script entrypoint.
-
+
"""
+ if argv is None:
+ argv = sys.argv[1:]
try:
- status = CoverageScript().command_line(sys.argv[1:])
+ 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 status code.
+ _, err, _ = sys.exc_info()
+ status = err.args[0]
return status
diff --git a/coverage/codeunit.py b/coverage/codeunit.py
index 28fa0551..01708957 100644
--- a/coverage/codeunit.py
+++ b/coverage/codeunit.py
@@ -6,22 +6,27 @@ from coverage.backward import string_class, StringIO
from coverage.misc import CoverageException
-def code_unit_factory(morfs, file_locator, omit_prefixes=None):
+def code_unit_factory(
+ morfs, file_locator, omit_prefixes=None, include_prefixes=None
+ ):
"""Construct a list of CodeUnits from polymorphic inputs.
-
+
`morfs` is a module or a filename, or a list of same.
+
`file_locator` is a FileLocator that can help resolve filenames.
- `omit_prefixes` is a list of prefixes. CodeUnits that match those prefixes
- will be omitted from the list.
-
+
+ `include_prefixes` is a list of prefixes. Only CodeUnits that match those
+ prefixes will be included in the list. `omit_prefixes` is a list of
+ prefixes to omit from the list.
+
Returns a list of CodeUnit objects.
-
+
"""
# Be sure we have a list.
if not isinstance(morfs, (list, tuple)):
morfs = [morfs]
-
+
# On Windows, the shell doesn't expand wildcards. Do it here.
globbed = []
for morf in morfs:
@@ -32,7 +37,18 @@ def code_unit_factory(morfs, file_locator, omit_prefixes=None):
morfs = globbed
code_units = [CodeUnit(morf, file_locator) for morf in morfs]
-
+
+ if include_prefixes:
+ assert not isinstance(include_prefixes, string_class) # common mistake
+ prefixes = [file_locator.abs_file(p) for p in include_prefixes]
+ filtered = []
+ for cu in code_units:
+ for prefix in prefixes:
+ if cu.filename.startswith(prefix):
+ filtered.append(cu)
+ break
+ code_units = filtered
+
if omit_prefixes:
code_units = omit_filter(omit_prefixes, code_units)
@@ -57,13 +73,13 @@ def omit_filter(omit_prefixes, code_units):
class CodeUnit(object):
"""Code unit: a filename or module.
-
+
Instance attributes:
-
+
`name` is a human-readable name for this code unit.
`filename` is the os path from which we can read the source.
`relative` is a boolean.
-
+
"""
def __init__(self, morf, file_locator):
@@ -98,40 +114,40 @@ class CodeUnit(object):
# Annoying comparison operators. Py3k wants __lt__ etc, and Py2k needs all
# of them defined.
-
+
def __lt__(self, other):
return self.name < other.name
-
+
def __le__(self, other):
return self.name <= other.name
def __eq__(self, other):
return self.name == other.name
-
+
def __ne__(self, other):
return self.name != other.name
def __gt__(self, other):
return self.name > other.name
-
+
def __ge__(self, other):
return self.name >= other.name
def flat_rootname(self):
"""A base for a flat filename to correspond to this code unit.
-
+
Useful for writing files about the code where you want all the files in
the same directory, but need to differentiate same-named files from
different directories.
-
+
For example, the file a/b/c.py might return 'a_b_c'
-
+
"""
if self.modname:
return self.modname.replace('.', '_')
else:
- root = os.path.splitdrive(os.path.splitext(self.name)[0])[1]
- return root.replace('\\', '_').replace('/', '_')
+ root = os.path.splitdrive(self.name)[1]
+ return root.replace('\\', '_').replace('/', '_').replace('.', '_')
def source_file(self):
"""Return an open file for reading the source of the code unit."""
@@ -143,7 +159,7 @@ class CodeUnit(object):
source = self.file_locator.get_zip_data(self.filename)
if source is not None:
return StringIO(source)
-
+
# Couldn't find source.
raise CoverageException(
"No source for code %r." % self.filename
diff --git a/coverage/collector.py b/coverage/collector.py
index 1a831c19..06ccda7e 100644
--- a/coverage/collector.py
+++ b/coverage/collector.py
@@ -12,7 +12,7 @@ except ImportError:
class PyTracer(object):
"""Python implementation of the raw data tracer."""
-
+
# Because of poor implementations of trace-function-manipulating tools,
# the Python trace function must be kept very simple. In particular, there
# must be only one function ever set as the trace function, both through
@@ -37,22 +37,24 @@ class PyTracer(object):
self.last_line = 0
self.data_stack = []
self.last_exc_back = None
+ self.last_exc_firstlineno = 0
self.arcs = False
def _trace(self, frame, event, arg_unused):
"""The trace function passed to sys.settrace."""
-
+
#print "trace event: %s %r @%d" % (
# event, frame.f_code.co_filename, frame.f_lineno)
-
+
if self.last_exc_back:
if frame == self.last_exc_back:
# Someone forgot a return event.
if self.arcs and self.cur_file_data:
- self.cur_file_data[(self.last_line, -1)] = None
+ pair = (self.last_line, -self.last_exc_firstlineno)
+ self.cur_file_data[pair] = None
self.cur_file_data, self.last_line = self.data_stack.pop()
self.last_exc_back = None
-
+
if event == 'call':
# Entering a new function context. Decide if we should trace
# in this file.
@@ -65,6 +67,8 @@ class PyTracer(object):
self.cur_file_data = self.data[tracename]
else:
self.cur_file_data = None
+ # Set the last_line to -1 because the next arc will be entering a
+ # code block, indicated by (-1, n).
self.last_line = -1
elif event == 'line':
# Record an executed line.
@@ -78,14 +82,16 @@ class PyTracer(object):
self.last_line = frame.f_lineno
elif event == 'return':
if self.arcs and self.cur_file_data:
- self.cur_file_data[(self.last_line, -1)] = None
+ first = frame.f_code.co_firstlineno
+ self.cur_file_data[(self.last_line, -first)] = None
# Leaving this function, pop the filename stack.
self.cur_file_data, self.last_line = self.data_stack.pop()
elif event == 'exception':
#print "exc", self.last_line, frame.f_lineno
self.last_exc_back = frame.f_back
+ self.last_exc_firstlineno = frame.f_code.co_firstlineno
return self._trace
-
+
def start(self):
"""Start this Tracer."""
sys.settrace(self._trace)
@@ -94,22 +100,27 @@ class PyTracer(object):
"""Stop this Tracer."""
sys.settrace(None)
+ def get_stats(self):
+ """Return a dictionary of statistics, or None."""
+ return None
+
class Collector(object):
"""Collects trace data.
- Creates a Tracer object for each thread, since they track stack information.
- Each Tracer points to the same shared data, contributing traced data points.
-
+ Creates a Tracer object for each thread, since they track stack
+ information. Each Tracer points to the same shared data, contributing
+ traced data points.
+
When the Collector is started, it creates a Tracer for the current thread,
and installs a function to create Tracers for each new thread started.
When the Collector is stopped, all active Tracers are stopped.
-
+
Threads started while the Collector is stopped will never have Tracers
associated with them.
-
+
"""
-
+
# The stack of active Collectors. Collectors are added here when started,
# and popped when stopped. Collectors on the stack are paused when not
# the top, and resumed when they become the top again.
@@ -117,20 +128,20 @@ class Collector(object):
def __init__(self, should_trace, timid, branch):
"""Create a collector.
-
+
`should_trace` is a function, taking a filename, and returning a
canonicalized filename, or False depending on whether the file should
be traced or not.
-
+
If `timid` is true, then a slower simpler trace function will be
used. This is important for some environments where manipulation of
tracing functions make the faster more sophisticated trace function not
operate properly.
-
+
If `branch` is true, then branches will be measured. This involves
collecting data on which statements followed each other (arcs). Use
`get_arc_data` to get the arc data.
-
+
"""
self.should_trace = should_trace
self.branch = branch
@@ -144,6 +155,9 @@ class Collector(object):
# trace function.
self._trace_class = Tracer or PyTracer
+ def __repr__(self):
+ return "<Collector at 0x%x>" % id(self)
+
def tracer_name(self):
"""Return the class name of the tracer we're using."""
return self._trace_class.__name__
@@ -153,7 +167,7 @@ class Collector(object):
# A dictionary mapping filenames to dicts with linenumber keys,
# or mapping filenames to dicts with linenumber pairs as keys.
self.data = {}
-
+
# A cache of the results from should_trace, the decision about whether
# to trace execution in a file. A dict of filename to (filename or
# False).
@@ -192,6 +206,7 @@ class Collector(object):
if self._collectors:
self._collectors[-1].pause()
self._collectors.append(self)
+ #print >>sys.stderr, "Started: %r" % self._collectors
# Install the tracer on this thread.
self._start_tracer()
# Install our installation tracer in threading, to jump start other
@@ -200,12 +215,13 @@ class Collector(object):
def stop(self):
"""Stop collecting trace information."""
+ #print >>sys.stderr, "Stopping: %r" % self._collectors
assert self._collectors
assert self._collectors[-1] is self
- self.pause()
+ self.pause()
self.tracers = []
-
+
# Remove this Collector from the stack, and resume the one underneath
# (if any).
self._collectors.pop()
@@ -216,8 +232,13 @@ class Collector(object):
"""Pause tracing, but be prepared to `resume`."""
for tracer in self.tracers:
tracer.stop()
+ stats = tracer.get_stats()
+ if stats:
+ print("\nCoverage.py tracer stats:")
+ for k in sorted(stats.keys()):
+ print("%16s: %s" % (k, stats[k]))
threading.settrace(None)
-
+
def resume(self):
"""Resume tracing after a `pause`."""
for tracer in self.tracers:
@@ -226,9 +247,9 @@ class Collector(object):
def get_line_data(self):
"""Return the line data collected.
-
+
Data is { filename: { lineno: None, ...}, ...}
-
+
"""
if self.branch:
# If we were measuring branches, then we have to re-build the dict
@@ -236,7 +257,7 @@ class Collector(object):
line_data = {}
for f, arcs in self.data.items():
line_data[f] = ldf = {}
- for l1, _ in arcs:
+ for l1, _ in list(arcs.keys()):
if l1:
ldf[l1] = None
return line_data
@@ -245,7 +266,7 @@ class Collector(object):
def get_arc_data(self):
"""Return the arc data collected.
-
+
Data is { filename: { (l1, l2): None, ...}, ...}
Note that no data is collected or returned if the Collector wasn't
diff --git a/coverage/config.py b/coverage/config.py
new file mode 100644
index 00000000..133444d8
--- /dev/null
+++ b/coverage/config.py
@@ -0,0 +1,112 @@
+"""Config file for coverage.py"""
+
+import os
+from coverage.backward import configparser # pylint: disable-msg=W0622
+
+
+class CoverageConfig(object):
+ """Coverage.py configuration.
+
+ The attributes of this class are the various settings that control the
+ operation of coverage.py.
+
+ """
+
+ def __init__(self):
+ """Initialize the configuration attributes to their defaults."""
+ # Defaults for [run]
+ self.branch = False
+ self.cover_pylib = False
+ self.data_file = ".coverage"
+ self.parallel = False
+ self.timid = False
+
+ # Defaults for [report]
+ self.exclude_list = ['(?i)# *pragma[: ]*no *cover']
+ self.ignore_errors = False
+ self.omit_prefixes = None
+ self.include_prefixes = None
+
+ # Defaults for [html]
+ self.html_dir = "htmlcov"
+
+ # Defaults for [xml]
+ self.xml_output = "coverage.xml"
+
+ def from_environment(self, env_var):
+ """Read configuration from the `env_var` environment variable."""
+ # Timidity: for nose users, read an environment variable. This is a
+ # cheap hack, since the rest of the command line arguments aren't
+ # recognized, but it solves some users' problems.
+ env = os.environ.get(env_var, '')
+ if env:
+ self.timid = ('--timid' in env)
+
+ def from_args(self, **kwargs):
+ """Read config values from `kwargs`."""
+ for k, v in kwargs.items():
+ if v is not None:
+ setattr(self, k, v)
+
+ def from_file(self, *files):
+ """Read configuration from .rc files.
+
+ Each argument in `files` is a file name to read.
+
+ """
+ cp = configparser.RawConfigParser()
+ cp.read(files)
+
+ # [run]
+ if cp.has_option('run', 'branch'):
+ self.branch = cp.getboolean('run', 'branch')
+ if cp.has_option('run', 'cover_pylib'):
+ self.cover_pylib = cp.getboolean('run', 'cover_pylib')
+ if cp.has_option('run', 'data_file'):
+ self.data_file = cp.get('run', 'data_file')
+ if cp.has_option('run', 'parallel'):
+ self.parallel = cp.getboolean('run', 'parallel')
+ if cp.has_option('run', 'timid'):
+ self.timid = cp.getboolean('run', 'timid')
+ if cp.has_option('run', 'omit'):
+ self.omit_prefixes = self.get_list(cp, 'run', 'omit')
+ if cp.has_option('run', 'include'):
+ self.include_prefixes = self.get_list(cp, 'run', 'include')
+
+ # [report]
+ if cp.has_option('report', 'exclude_lines'):
+ # exclude_lines is a list of lines, leave out the blank ones.
+ exclude_list = cp.get('report', 'exclude_lines')
+ self.exclude_list = list(filter(None, exclude_list.split('\n')))
+ if cp.has_option('report', 'ignore_errors'):
+ self.ignore_errors = cp.getboolean('report', 'ignore_errors')
+ if cp.has_option('report', 'omit'):
+ self.omit_prefixes = self.get_list(cp, 'report', 'omit')
+ if cp.has_option('report', 'include'):
+ self.include_prefixes = self.get_list(cp, 'report', 'include')
+
+ # [html]
+ if cp.has_option('html', 'directory'):
+ self.html_dir = cp.get('html', 'directory')
+
+ # [xml]
+ if cp.has_option('xml', 'output'):
+ self.xml_output = cp.get('xml', 'output')
+
+ def get_list(self, cp, section, option):
+ """Read a list of strings from the ConfigParser `cp`.
+
+ The value of `section` and `option` is treated as a comma- and newline-
+ separated list of strings. Each value is stripped of whitespace.
+
+ Returns the list of strings.
+
+ """
+ value_list = cp.get(section, option)
+ values = []
+ for value_line in value_list.split('\n'):
+ for value in value_line.split(','):
+ value = value.strip()
+ if value:
+ values.append(value)
+ return values
diff --git a/coverage/control.py b/coverage/control.py
index 23740ca4..d07abaf3 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -1,14 +1,16 @@
"""Core control stuff for Coverage."""
-import os, socket
+import atexit, os, random, socket, sys
from coverage.annotate import AnnotateReporter
-from coverage.backward import string_class # pylint: disable-msg=W0622
+from coverage.backward import string_class
from coverage.codeunit import code_unit_factory, CodeUnit
from coverage.collector import Collector
+from coverage.config import CoverageConfig
from coverage.data import CoverageData
from coverage.files import FileLocator
from coverage.html import HtmlReporter
+from coverage.misc import bool_or_none
from coverage.results import Analysis
from coverage.summary import SummaryReporter
from coverage.xmlreport import XmlReporter
@@ -17,9 +19,9 @@ class coverage(object):
"""Programmatic access to Coverage.
To use::
-
+
from coverage import coverage
-
+
cov = coverage()
cov.start()
#.. blah blah (run your code) blah blah ..
@@ -28,82 +30,130 @@ class coverage(object):
"""
- def __init__(self, data_file=None, data_suffix=False, cover_pylib=False,
- auto_data=False, timid=False, branch=False):
- """Create a new coverage measurement context.
-
+ def __init__(self, data_file=None, data_suffix=None, cover_pylib=None,
+ auto_data=False, timid=None, branch=None, config_file=True,
+ omit_prefixes=None, include_prefixes=None):
+ """
`data_file` is the base name of the data file to use, defaulting to
- ".coverage". `data_suffix` is appended to `data_file` to create the
- final file name. If `data_suffix` is simply True, then a suffix is
- created with the machine and process identity included.
-
+ ".coverage". `data_suffix` is appended (with a dot) to `data_file` to
+ create the final file name. If `data_suffix` is simply True, then a
+ suffix is created with the machine and process identity included.
+
`cover_pylib` is a boolean determining whether Python code installed
with the Python interpreter is measured. This includes the Python
standard library and any packages installed with the interpreter.
-
+
If `auto_data` is true, then any existing data file will be read when
coverage measurement starts, and data will be saved automatically when
measurement stops.
-
+
If `timid` is true, then a slower and simpler trace function will be
used. This is important for some environments where manipulation of
tracing functions breaks the faster trace function.
-
- If `branch` is true, then measure branch execution.
+
+ If `branch` is true, then branch coverage will be measured in addition
+ to the usual statement coverage.
+
+ `config_file` determines what config file to read. If it is a string,
+ it is the name of the config file to read. If it is True, then a
+ standard file is read (".coveragerc"). If it is False, then no file is
+ read.
+
+ `omit_prefixes` and `include_prefixes` are lists of filename prefixes.
+ Files that match `include_prefixes` will be measured, files that match
+ `omit_prefixes` will not.
"""
from coverage import __version__
-
- self.cover_pylib = cover_pylib
+
+ # Build our configuration from a number of sources:
+ # 1: defaults:
+ self.config = CoverageConfig()
+
+ # 2: from the coveragerc file:
+ if config_file:
+ if config_file is True:
+ config_file = ".coveragerc"
+ self.config.from_file(config_file)
+
+ # 3: from environment variables:
+ self.config.from_environment('COVERAGE_OPTIONS')
+ env_data_file = os.environ.get('COVERAGE_FILE')
+ if env_data_file:
+ self.config.data_file = env_data_file
+
+ # 4: from constructor arguments:
+ self.config.from_args(
+ data_file=data_file, cover_pylib=cover_pylib, timid=timid,
+ branch=branch, parallel=bool_or_none(data_suffix),
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
+
self.auto_data = auto_data
-
+ self.atexit_registered = False
+
self.exclude_re = ""
- self.exclude_list = []
-
+ self._compile_exclude()
+
self.file_locator = FileLocator()
-
- # Timidity: for nose users, read an environment variable. This is a
- # cheap hack, since the rest of the command line arguments aren't
- # recognized, but it solves some users' problems.
- timid = timid or ('--timid' in os.environ.get('COVERAGE_OPTIONS', ''))
+
+ self.omit_prefixes = self._abs_files(self.config.omit_prefixes)
+ self.include_prefixes = self._abs_files(self.config.include_prefixes)
+
self.collector = Collector(
- self._should_trace, timid=timid, branch=branch
+ self._should_trace, timid=self.config.timid,
+ branch=self.config.branch
)
- # Create the data file.
- if data_suffix:
+ # Suffixes are a bit tricky. We want to use the data suffix only when
+ # collecting data, not when combining data. So we save it as
+ # `self.run_suffix` now, and promote it to `self.data_suffix` if we
+ # find that we are collecting data later.
+ if data_suffix or self.config.parallel:
if not isinstance(data_suffix, string_class):
- # if data_suffix=True, use .machinename.pid
- data_suffix = ".%s.%s" % (socket.gethostname(), os.getpid())
+ # if data_suffix=True, use .machinename.pid.random
+ data_suffix = True
else:
data_suffix = None
+ self.data_suffix = None
+ self.run_suffix = data_suffix
+ # Create the data file. We do this at construction time so that the
+ # data file will be written into the directory where the process
+ # started rather than wherever the process eventually chdir'd to.
self.data = CoverageData(
- basename=data_file, suffix=data_suffix,
+ basename=self.config.data_file,
collector="coverage v%s" % __version__
)
- # The default exclude pattern.
- self.exclude('# *pragma[: ]*[nN][oO] *[cC][oO][vV][eE][rR]')
-
# The prefix for files considered "installed with the interpreter".
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
+ # Look at where the "os" module is located. That's the indication
+ # for "installed with the interpreter".
os_file = self.file_locator.canonical_filename(os.__file__)
self.pylib_prefix = os.path.split(os_file)[0]
+ # To avoid tracing the coverage code itself, we skip anything located
+ # where we are.
here = self.file_locator.canonical_filename(__file__)
self.cover_prefix = os.path.split(here)[0]
def _should_trace(self, filename, frame):
"""Decide whether to trace execution in `filename`
-
+
+ This function is called from the trace function. As each new file name
+ is encountered, this function determines whether it is traced or not.
+
Returns a canonicalized filename if it should be traced, False if it
should not.
-
+
"""
- if filename == '<string>':
- # There's no point in ever tracing string executions, we can't do
- # anything with the data later anyway.
+ if filename[0] == '<':
+ # Lots of non-file execution is represented with artificial
+ # filenames like "<string>", "<doctest readme.txt[0]>", or
+ # "<exec_function>". Don't ever trace these executions, since we
+ # can't do anything with the data later anyway.
return False
# Compiled Python files have two filenames: frame.f_code.co_filename is
@@ -123,7 +173,7 @@ class coverage(object):
# If we aren't supposed to trace installed code, then check if this is
# near the Python standard library and skip it if so.
- if not self.cover_pylib:
+ if not self.config.cover_pylib:
if canonical.startswith(self.pylib_prefix):
return False
@@ -132,6 +182,17 @@ class coverage(object):
if canonical.startswith(self.cover_prefix):
return False
+ # Check the file against the include and omit prefixes.
+ if self.include_prefixes:
+ for prefix in self.include_prefixes:
+ if canonical.startswith(prefix):
+ break
+ else:
+ return False
+ for prefix in self.omit_prefixes:
+ if canonical.startswith(prefix):
+ return False
+
return canonical
# To log what should_trace returns, change this to "if 1:"
@@ -143,11 +204,16 @@ class coverage(object):
print("should_trace: %r -> %r" % (filename, ret))
return ret
+ def _abs_files(self, files):
+ """Return a list of absolute file names for the names in `files`."""
+ files = files or []
+ return [self.file_locator.abs_file(f) for f in files]
+
def use_cache(self, usecache):
"""Control the use of a data file (incorrectly called a cache).
-
+
`usecache` is true or false, whether to read and write data on disk.
-
+
"""
self.data.usefile(usecache)
@@ -155,16 +221,21 @@ class coverage(object):
"""Load previously-collected coverage data from the data file."""
self.collector.reset()
self.data.read()
-
+
def start(self):
"""Start measuring code coverage."""
+ if self.run_suffix:
+ # Calling start() means we're running code, so use the run_suffix
+ # as the data_suffix when we eventually save the data.
+ self.data_suffix = self.run_suffix
if self.auto_data:
self.load()
# Save coverage data when Python exits.
- import atexit
- atexit.register(self.save)
+ if not self.atexit_registered:
+ atexit.register(self.save)
+ self.atexit_registered = True
self.collector.start()
-
+
def stop(self):
"""Stop measuring code coverage."""
self.collector.stop()
@@ -172,47 +243,61 @@ class coverage(object):
def erase(self):
"""Erase previously-collected coverage data.
-
+
This removes the in-memory data collected in this session as well as
discarding the data file.
-
+
"""
self.collector.reset()
self.data.erase()
def clear_exclude(self):
"""Clear the exclude list."""
- self.exclude_list = []
+ self.config.exclude_list = []
self.exclude_re = ""
def exclude(self, regex):
"""Exclude source lines from execution consideration.
-
+
`regex` is a regular expression. Lines matching this expression are
not considered executable when reporting code coverage. A list of
regexes is maintained; this function adds a new regex to the list.
Matching any of the regexes excludes a source line.
-
+
"""
- self.exclude_list.append(regex)
- self.exclude_re = "(" + ")|(".join(self.exclude_list) + ")"
+ self.config.exclude_list.append(regex)
+ self._compile_exclude()
+
+ def _compile_exclude(self):
+ """Build the internal usable form of the exclude list."""
+ self.exclude_re = "(" + ")|(".join(self.config.exclude_list) + ")"
def get_exclude_list(self):
"""Return the list of excluded regex patterns."""
- return self.exclude_list
+ return self.config.exclude_list
def save(self):
"""Save the collected coverage data to the data file."""
+ data_suffix = self.data_suffix
+ if data_suffix and not isinstance(data_suffix, string_class):
+ # If data_suffix was a simple true value, then make a suffix with
+ # plenty of distinguishing information. We do this here in
+ # `save()` at the last minute so that the pid will be correct even
+ # if the process forks.
+ data_suffix = "%s.%s.%06d" % (
+ socket.gethostname(), os.getpid(), random.randint(0, 99999)
+ )
+
self._harvest_data()
- self.data.write()
+ self.data.write(suffix=data_suffix)
def combine(self):
"""Combine together a number of similarly-named coverage data files.
-
+
All coverage data files whose name starts with `data_file` (from the
coverage() constructor) will be read, and combined together into the
current measurements.
-
+
"""
self.data.combine_parallel_data()
@@ -230,14 +315,15 @@ class coverage(object):
def analysis2(self, morf):
"""Analyze a module.
-
+
`morf` is a module or a filename. It will be analyzed to determine
its coverage statistics. The return value is a 5-tuple:
-
+
* The filename for the module.
* A list of line numbers of executable statements.
* A list of line numbers of excluded statements.
- * A list of line numbers of statements not run (missing from execution).
+ * A list of line numbers of statements not run (missing from
+ execution).
* A readable formatted string of the missing line numbers.
The analysis uses the source file itself and the current measured
@@ -252,66 +338,126 @@ class coverage(object):
def _analyze(self, it):
"""Analyze a single morf or code unit.
-
+
Returns an `Analysis` object.
"""
if not isinstance(it, CodeUnit):
it = code_unit_factory(it, self.file_locator)[0]
-
+
return Analysis(self, it)
- def report(self, morfs=None, show_missing=True, ignore_errors=False,
- file=None, omit_prefixes=None): # pylint: disable-msg=W0622
+ def report(self, morfs=None, show_missing=True, ignore_errors=None,
+ file=None, # pylint: disable-msg=W0622
+ omit_prefixes=None, include_prefixes=None
+ ):
"""Write a summary report to `file`.
-
+
Each module in `morfs` is listed, with counts of statements, executed
statements, missing statements, and a list of lines missed.
-
+
+ `include_prefixes` is a list of filename prefixes. Modules that match
+ those prefixes will be included in the report. Modules that match
+ `omit_prefixes` will not be included in the report.
+
"""
- reporter = SummaryReporter(self, show_missing, ignore_errors)
- reporter.report(morfs, outfile=file, omit_prefixes=omit_prefixes)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
+ reporter = SummaryReporter(
+ self, show_missing, self.config.ignore_errors
+ )
+ reporter.report(
+ morfs, outfile=file, omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes
+ )
- def annotate(self, morfs=None, directory=None, ignore_errors=False,
- omit_prefixes=None):
+ def annotate(self, morfs=None, directory=None, ignore_errors=None,
+ omit_prefixes=None, include_prefixes=None):
"""Annotate a list of modules.
-
+
Each module in `morfs` is annotated. The source is written to a new
file, named with a ",cover" suffix, with each line prefixed with a
marker to indicate the coverage of the line. Covered lines have ">",
excluded lines have "-", and missing lines have "!".
-
+
+ See `coverage.report()` for other arguments.
+
"""
- reporter = AnnotateReporter(self, ignore_errors)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
+ reporter = AnnotateReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, directory=directory, omit_prefixes=omit_prefixes)
+ morfs, directory=directory,
+ omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes
+ )
- def html_report(self, morfs=None, directory=None, ignore_errors=False,
- omit_prefixes=None):
+ def html_report(self, morfs=None, directory=None, ignore_errors=None,
+ omit_prefixes=None, include_prefixes=None):
"""Generate an HTML report.
-
+
+ See `coverage.report()` for other arguments.
+
"""
- reporter = HtmlReporter(self, ignore_errors)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes,
+ html_dir=directory,
+ )
+ reporter = HtmlReporter(self, self.config.ignore_errors)
reporter.report(
- morfs, directory=directory, omit_prefixes=omit_prefixes)
+ morfs, directory=self.config.html_dir,
+ omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes
+ )
- def xml_report(self, morfs=None, outfile=None, ignore_errors=False,
- omit_prefixes=None):
+ def xml_report(self, morfs=None, outfile=None, ignore_errors=None,
+ omit_prefixes=None, include_prefixes=None):
"""Generate an XML report of coverage results.
-
+
The report is compatible with Cobertura reports.
-
+
+ Each module in `morfs` is included in the report. `outfile` is the
+ path to write the file to, "-" will write to stdout.
+
+ See `coverage.report()` for other arguments.
+
"""
- if outfile:
- outfile = open(outfile, "w")
- reporter = XmlReporter(self, ignore_errors)
- reporter.report(morfs, omit_prefixes=omit_prefixes, outfile=outfile)
+ self.config.from_args(
+ ignore_errors=ignore_errors,
+ omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes,
+ xml_output=outfile,
+ )
+ file_to_close = None
+ if self.config.xml_output:
+ if self.config.xml_output == '-':
+ outfile = sys.stdout
+ else:
+ outfile = open(self.config.xml_output, "w")
+ file_to_close = outfile
+ try:
+ reporter = XmlReporter(self, self.config.ignore_errors)
+ reporter.report(
+ morfs, omit_prefixes=self.config.omit_prefixes,
+ include_prefixes=self.config.include_prefixes, outfile=outfile
+ )
+ finally:
+ if file_to_close:
+ file_to_close.close()
def sysinfo(self):
- """Return a list of key,value pairs showing internal information."""
-
+ """Return a list of (key, value) pairs showing internal information."""
+
import coverage as covmod
- import platform, re, sys
+ import platform, re
info = [
('version', covmod.__version__),
@@ -319,7 +465,7 @@ class coverage(object):
('cover_prefix', self.cover_prefix),
('pylib_prefix', self.pylib_prefix),
('tracer', self.collector.tracer_name()),
- ('data_file', self.data.filename),
+ ('data_path', self.data.filename),
('python', sys.version.replace('\n', '')),
('platform', platform.platform()),
('cwd', os.getcwd()),
@@ -330,3 +476,32 @@ class coverage(object):
]),
]
return info
+
+
+def process_startup():
+ """Call this at Python startup to perhaps measure coverage.
+
+ If the environment variable COVERAGE_PROCESS_START is defined, coverage
+ measurement is started. The value of the variable is the config file
+ to use.
+
+ There are two ways to configure your Python installation to invoke this
+ function when Python starts:
+
+ #. Create or append to sitecustomize.py to add these lines::
+
+ import coverage
+ coverage.process_startup()
+
+ #. Create a .pth file in your Python installation containing::
+
+ import coverage; coverage.process_startup()
+
+ """
+ cps = os.environ.get("COVERAGE_PROCESS_START")
+ if cps:
+ cov = coverage(config_file=cps, auto_data=True)
+ if os.environ.get("COVERAGE_COVERAGE"):
+ # Measuring coverage within coverage.py takes yet more trickery.
+ cov.cover_prefix = "Please measure coverage.py!"
+ cov.start()
diff --git a/coverage/data.py b/coverage/data.py
index 452ce73d..1b883750 100644
--- a/coverage/data.py
+++ b/coverage/data.py
@@ -2,53 +2,40 @@
import os
-from coverage.backward import pickle, sorted # pylint: disable-msg=W0622
+from coverage.backward import pickle, sorted # pylint: disable-msg=W0622
class CoverageData(object):
"""Manages collected coverage data, including file storage.
-
+
The data file format is a pickled dict, with these keys:
-
+
* collector: a string identifying the collecting software
* lines: a dict mapping filenames to sorted lists of line numbers
executed:
{ 'file1': [17,23,45], 'file2': [1,2,3], ... }
-
+
* arcs: a dict mapping filenames to sorted lists of line number pairs:
{ 'file1': [(17,23), (17,25), (25,26)], ... }
"""
-
- # Name of the data file (unless environment variable is set).
- filename_default = ".coverage"
-
- # Environment variable naming the data file.
- filename_env = "COVERAGE_FILE"
- def __init__(self, basename=None, suffix=None, collector=None):
+ def __init__(self, basename=None, collector=None):
"""Create a CoverageData.
-
+
`basename` is the name of the file to use for storing data.
-
- `suffix` is a suffix to append to the base file name. This can be used
- for multiple or parallel execution, so that many coverage data files
- can exist simultaneously.
`collector` is a string describing the coverage measurement software.
"""
- self.collector = collector
-
+ self.collector = collector or 'unknown'
+
self.use_file = True
# Construct the filename that will be used for data file storage, if we
# ever do any file storage.
- self.filename = (basename or
- os.environ.get(self.filename_env, self.filename_default))
- if suffix:
- self.filename += suffix
+ self.filename = basename or ".coverage"
self.filename = os.path.abspath(self.filename)
# A map from canonical Python source file name to a dictionary in
@@ -61,14 +48,14 @@ class CoverageData(object):
# }
#
self.lines = {}
-
+
# A map from canonical Python source file name to a dictionary with an
# entry for each pair of line numbers forming an arc:
#
# { filename: { (l1,l2): None, ... }, ...}
#
self.arcs = {}
-
+
def usefile(self, use_file=True):
"""Set whether or not to use a disk file for data."""
self.use_file = use_file
@@ -80,10 +67,20 @@ class CoverageData(object):
else:
self.lines, self.arcs = {}, {}
- def write(self):
- """Write the collected coverage data to a file."""
+ def write(self, suffix=None):
+ """Write the collected coverage data to a file.
+
+ `suffix` is a suffix to append to the base file name. This can be used
+ for multiple or parallel execution, so that many coverage data files
+ can exist simultaneously. A dot will be used to join the base name and
+ the suffix.
+
+ """
if self.use_file:
- self.write_file(self.filename)
+ filename = self.filename
+ if suffix:
+ filename += "." + suffix
+ self.write_file(filename)
def erase(self):
"""Erase the data, both in this object, and from its file storage."""
@@ -92,7 +89,7 @@ class CoverageData(object):
os.remove(self.filename)
self.lines = {}
self.arcs = {}
-
+
def line_data(self):
"""Return the map from filenames to lists of line numbers executed."""
return dict(
@@ -104,11 +101,11 @@ class CoverageData(object):
return dict(
[(f, sorted(amap.keys())) for f, amap in self.arcs.items()]
)
-
+
def write_file(self, filename):
"""Write the coverage data to `filename`."""
- # Create the file data.
+ # Create the file data.
data = {}
data['lines'] = self.line_data()
@@ -141,10 +138,10 @@ class CoverageData(object):
def _read_file(self, filename):
"""Return the stored coverage data from the given file.
-
+
Returns two values, suitable for assigning to `self.lines` and
`self.arcs`.
-
+
"""
lines = {}
arcs = {}
@@ -167,35 +164,38 @@ class CoverageData(object):
def combine_parallel_data(self):
"""Combine a number of data files together.
-
+
Treat `self.filename` as a file prefix, and combine the data from all
- of the data files starting with that prefix.
-
+ of the data files starting with that prefix plus a dot.
+
"""
data_dir, local = os.path.split(self.filename)
+ localdot = local + '.'
for f in os.listdir(data_dir or '.'):
- if f.startswith(local):
+ if f.startswith(localdot):
full_path = os.path.join(data_dir, f)
new_lines, new_arcs = self._read_file(full_path)
for filename, file_data in new_lines.items():
self.lines.setdefault(filename, {}).update(file_data)
for filename, file_data in new_arcs.items():
self.arcs.setdefault(filename, {}).update(file_data)
+ if f != local:
+ os.remove(full_path)
def add_line_data(self, line_data):
"""Add executed line data.
-
+
`line_data` is { filename: { lineno: None, ... }, ...}
-
+
"""
for filename, linenos in line_data.items():
self.lines.setdefault(filename, {}).update(linenos)
def add_arc_data(self, arc_data):
"""Add measured arc data.
-
+
`arc_data` is { filename: { (l1,l2): None, ... }, ...}
-
+
"""
for filename, arcs in arc_data.items():
self.arcs.setdefault(filename, {}).update(arcs)
@@ -206,7 +206,7 @@ class CoverageData(object):
def executed_lines(self, filename):
"""A map containing all the line numbers executed in `filename`.
-
+
If `filename` hasn't been collected at all (because it wasn't executed)
then return an empty map.
@@ -219,11 +219,11 @@ class CoverageData(object):
def summary(self, fullpath=False):
"""Return a dict summarizing the coverage data.
-
+
Keys are based on the filenames, and values are the number of executed
lines. If `fullpath` is true, then the keys are the full pathnames of
the files, otherwise they are the basenames of the files.
-
+
"""
summ = {}
if fullpath:
diff --git a/coverage/execfile.py b/coverage/execfile.py
index ddcfa149..333163f8 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -2,8 +2,8 @@
import imp, os, sys
-from coverage.backward import exec_function
-from coverage.misc import NoSource
+from coverage.backward import exec_code_object
+from coverage.misc import NoSource, ExceptionDuringRun
try:
@@ -16,11 +16,11 @@ except KeyError:
def run_python_file(filename, args):
"""Run a python file as if it were the main program on the command line.
-
+
`filename` is the path to the file to execute, it need not be a .py file.
`args` is the argument array to present as sys.argv, including the first
element representing the file being executed.
-
+
"""
# Create a module to serve as __main__
old_main_mod = sys.modules['__main__']
@@ -36,15 +36,37 @@ def run_python_file(filename, args):
sys.path[0] = os.path.dirname(filename)
try:
+ # Open the source file.
try:
source = open(filename, 'rU').read()
except IOError:
raise NoSource("No file to run: %r" % filename)
- exec_function(source, filename, main_mod.__dict__)
+
+ # We have the source. `compile` still needs the last line to be clean,
+ # so make sure it is, then compile a code object from it.
+ if source[-1] != '\n':
+ source += '\n'
+ code = compile(source, filename, "exec")
+
+ # Execute the source file.
+ try:
+ exec_code_object(code, main_mod.__dict__)
+ except SystemExit:
+ # The user called sys.exit(). Just pass it along to the upper
+ # layers, where it will be handled.
+ raise
+ except:
+ # Something went wrong while executing the user code.
+ # Get the exc_info, and pack them into an exception that we can
+ # throw up to the outer loop. We peel two layers off the traceback
+ # so that the coverage.py code doesn't appear in the final printed
+ # traceback.
+ typ, err, tb = sys.exc_info()
+ raise ExceptionDuringRun(typ, err, tb.tb_next.tb_next)
finally:
# Restore the old __main__
sys.modules['__main__'] = old_main_mod
-
+
# Restore the old argv and path
sys.argv = old_argv
sys.path[0] = old_path0
diff --git a/coverage/files.py b/coverage/files.py
index 400646ca..5690679f 100644
--- a/coverage/files.py
+++ b/coverage/files.py
@@ -6,6 +6,7 @@ class FileLocator(object):
"""Understand how filenames work."""
def __init__(self):
+ # The absolute path to our current directory.
self.relative_dir = self.abs_file(os.curdir) + os.sep
# Cache of results of calling the canonical_filename() method, to
@@ -18,18 +19,20 @@ class FileLocator(object):
def relative_filename(self, filename):
"""Return the relative form of `filename`.
-
+
The filename will be relative to the current directory when the
- FileLocator was constructed.
-
+ `FileLocator` was constructed.
+
"""
- return filename.replace(self.relative_dir, "")
+ if filename.startswith(self.relative_dir):
+ filename = filename.replace(self.relative_dir, "")
+ return filename
def canonical_filename(self, filename):
"""Return a canonical filename for `filename`.
-
+
An absolute path with no redundant components and normalized case.
-
+
"""
if filename not in self.canonical_filename_cache:
f = filename
@@ -38,6 +41,8 @@ class FileLocator(object):
f = os.path.basename(f)
if not os.path.isabs(f):
for path in [os.curdir] + sys.path:
+ if path is None:
+ continue
g = os.path.join(path, f)
if os.path.exists(g):
f = g
@@ -48,11 +53,11 @@ class FileLocator(object):
def get_zip_data(self, filename):
"""Get data from `filename` if it is a zip file path.
-
+
Returns the string data read from the zip file, or None if no zip file
could be found or `filename` isn't in it. The data returned will be
an empty string if the file is empty.
-
+
"""
import zipimport
markers = ['.zip'+os.sep, '.egg'+os.sep]
@@ -67,7 +72,7 @@ class FileLocator(object):
data = zi.get_data(parts[1])
except IOError:
continue
- if sys.hexversion > 0x03000000:
+ if sys.version_info >= (3, 0):
data = data.decode('utf8') # TODO: How to do this properly?
return data
return None
diff --git a/coverage/html.py b/coverage/html.py
index 3877c834..94ba0dea 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -18,38 +18,44 @@ def data_filename(fname):
def data(fname):
"""Return the contents of a data file of ours."""
return open(data_filename(fname)).read()
-
+
class HtmlReporter(Reporter):
"""HTML reporting."""
-
+
def __init__(self, coverage, ignore_errors=False):
super(HtmlReporter, self).__init__(coverage, ignore_errors)
self.directory = None
self.source_tmpl = Templite(data("htmlfiles/pyfile.html"), globals())
-
+
self.files = []
self.arcs = coverage.data.has_arcs()
- def report(self, morfs, directory, omit_prefixes=None):
+ def report(self, morfs, directory, omit_prefixes=None,
+ include_prefixes=None
+ ):
"""Generate an HTML report for `morfs`.
-
+
`morfs` is a list of modules or filenames. `directory` is where to put
- the HTML files. `omit_prefixes` is a list of strings, prefixes of
- modules to omit from the report.
-
+ the HTML files.
+
+ See `coverage.report()` for other arguments.
+
"""
assert directory, "must provide a directory for html reporting"
-
+
# Process all the files.
- self.report_files(self.html_file, morfs, directory, omit_prefixes)
+ self.report_files(
+ self.html_file, morfs, directory, omit_prefixes, include_prefixes
+ )
# Write the index file.
self.index_file()
# Create the once-per-directory files.
for static in [
- "style.css", "jquery-1.3.2.min.js", "jquery.tablesorter.min.js"
+ "style.css", "coverage_html.js",
+ "jquery-1.3.2.min.js", "jquery.tablesorter.min.js"
]:
shutil.copyfile(
data_filename("htmlfiles/" + static),
@@ -58,55 +64,69 @@ class HtmlReporter(Reporter):
def html_file(self, cu, analysis):
"""Generate an HTML file for one source file."""
-
+
source = cu.source_file().read()
- nums = analysis.numbers
+ nums = analysis.numbers
missing_branch_arcs = analysis.missing_branch_arcs()
n_par = 0 # accumulated below.
arcs = self.arcs
# These classes determine which lines are highlighted by default.
- c_run = " run hide"
- c_exc = " exc"
- c_mis = " mis"
- c_par = " par"
+ c_run = "run hide_run"
+ c_exc = "exc"
+ c_mis = "mis"
+ c_par = "par " + c_run
lines = []
-
+
for lineno, line in enumerate(source_token_lines(source)):
lineno += 1 # 1-based line numbers.
# Figure out how to mark this line.
- line_class = ""
- annotate = ""
+ line_class = []
+ annotate_html = ""
+ annotate_title = ""
if lineno in analysis.statements:
- line_class += " stm"
+ line_class.append("stm")
if lineno in analysis.excluded:
- line_class += c_exc
+ line_class.append(c_exc)
elif lineno in analysis.missing:
- line_class += c_mis
+ line_class.append(c_mis)
elif self.arcs and lineno in missing_branch_arcs:
- line_class += c_par
+ line_class.append(c_par)
n_par += 1
- annotate = " ".join(map(str, missing_branch_arcs[lineno]))
+ annlines = []
+ for b in missing_branch_arcs[lineno]:
+ if b < 0:
+ annlines.append("exit")
+ else:
+ annlines.append(str(b))
+ annotate_html = "&nbsp;&nbsp; ".join(annlines)
+ if len(annlines) > 1:
+ annotate_title = "no jumps to these line numbers"
+ elif len(annlines) == 1:
+ annotate_title = "no jump to this line number"
elif lineno in analysis.statements:
- line_class += c_run
-
+ line_class.append(c_run)
+
# Build the HTML for the line
- html = ""
+ html = []
for tok_type, tok_text in line:
if tok_type == "ws":
- html += escape(tok_text)
+ html.append(escape(tok_text))
else:
tok_html = escape(tok_text) or '&nbsp;'
- html += "<span class='%s'>%s</span>" % (tok_type, tok_html)
+ html.append(
+ "<span class='%s'>%s</span>" % (tok_type, tok_html)
+ )
lines.append({
- 'html': html,
+ 'html': ''.join(html),
'number': lineno,
- 'class': line_class.strip() or "pln",
- 'annotate': annotate,
+ 'class': ' '.join(line_class) or "pln",
+ 'annotate': annotate_html,
+ 'annotate_title': annotate_title,
})
# Write the HTML page for this file.
@@ -160,10 +180,10 @@ def format_pct(p):
def spaceless(html):
"""Squeeze out some annoying extra space from an HTML string.
-
+
Nicely-formatted templates mean lots of extra space in the result. Get
rid of some.
-
+
"""
html = re.sub(">\s+<p ", ">\n<p ", html)
return html
diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js
new file mode 100644
index 00000000..5219e36b
--- /dev/null
+++ b/coverage/htmlfiles/coverage_html.js
@@ -0,0 +1,87 @@
+// Coverage.py HTML report browser code.
+
+// Loaded on index.html
+function index_ready($) {
+ // Look for a cookie containing previous sort settings:
+ sort_list = [];
+ cookie_name = "COVERAGE_INDEX_SORT";
+
+ // This almost makes it worth installing the jQuery cookie plugin:
+ if (document.cookie.indexOf(cookie_name) > -1) {
+ cookies = document.cookie.split(";");
+ for (var i=0; i < cookies.length; i++) {
+ parts = cookies[i].split("=")
+
+ if ($.trim(parts[0]) == cookie_name && parts[1]) {
+ sort_list = eval("[[" + parts[1] + "]]");
+ break;
+ }
+ }
+ }
+
+ // Create a new widget which exists only to save and restore
+ // the sort order:
+ $.tablesorter.addWidget({
+ id: "persistentSort",
+
+ // Format is called by the widget before displaying:
+ format: function(table) {
+ if (table.config.sortList.length == 0 && sort_list.length > 0) {
+ // This table hasn't been sorted before - we'll use
+ // our stored settings:
+ $(table).trigger('sorton', [sort_list]);
+ }
+ else {
+ // This is not the first load - something has
+ // already defined sorting so we'll just update
+ // our stored value to match:
+ sort_list = table.config.sortList;
+ }
+ }
+ });
+
+ // Configure our tablesorter to handle the variable number of
+ // columns produced depending on report options:
+ var headers = {};
+ var col_count = $("table.index > thead > tr > th").length;
+
+ headers[0] = { sorter: 'text' };
+ for (var i = 1; i < col_count-1; i++) {
+ headers[i] = { sorter: 'digit' };
+ }
+ headers[col_count-1] = { sorter: 'percent' };
+
+ // Enable the table sorter:
+ $("table.index").tablesorter({
+ widgets: ['persistentSort'],
+ headers: headers
+ });
+
+ // Watch for page unload events so we can save the final sort settings:
+ $(window).unload(function() {
+ document.cookie = cookie_name + "=" + sort_list.toString() + "; path=/"
+ });
+}
+
+// -- pyfile stuff --
+
+function pyfile_ready($) {
+ // If we're directed to a particular line number, highlight the line.
+ var frag = location.hash;
+ if (frag.length > 2 && frag[1] == 'n') {
+ $(frag).addClass('highlight');
+ }
+}
+
+function toggle_lines(btn, cls) {
+ btn = $(btn);
+ var hide = "hide_"+cls;
+ if (btn.hasClass(hide)) {
+ $("#source ."+cls).removeClass(hide);
+ btn.removeClass(hide);
+ }
+ else {
+ $("#source ."+cls).addClass(hide);
+ btn.addClass(hide);
+ }
+}
diff --git a/coverage/htmlfiles/index.html b/coverage/htmlfiles/index.html
index 5c562de2..c1ef8ad7 100644
--- a/coverage/htmlfiles/index.html
+++ b/coverage/htmlfiles/index.html
@@ -1,89 +1,81 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
- <head>
- <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
- <title>Coverage report</title>
- <link rel='stylesheet' href='style.css' type='text/css'>
- <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
- <script type='text/javascript' src='jquery.tablesorter.min.js'></script>
- </head>
- <body>
-
- <div id='header'>
- <div class='content'>
- <h1>Coverage report:
- <span class='pc_cov'>{{totals.pc_covered|format_pct}}%</span>
- </h1>
- </div>
- </div>
-
- <div id='index'>
- <table class='index'>
- <thead>
- {# The title='' attr doesn't work in Safari. #}
- <tr class='tablehead' title='Click to sort'>
- <th class='name left'>Module</th>
- <th>statements</th>
- <th>run</th>
- <th>excluded</th>
- {% if arcs %}
- <th>branches</th>
- <th>br exec</th>
- {% endif %}
- <th class='right'>coverage</th>
- </tr>
- </thead>
- <tbody>
- {% for file in files %}
- <tr class='file'>
- <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td>
- <td>{{file.nums.n_statements}}</td>
- <td>{{file.nums.n_executed}}</td>
- <td>{{file.nums.n_excluded}}</td>
- {% if arcs %}
- <td>{{file.nums.n_branches}}</td>
- <td>{{file.nums.n_executed_branches}}</td>
- {% endif %}
- <td class='right'>{{file.nums.pc_covered|format_pct}}%</td>
- </tr>
- {% endfor %}
- </tbody>
- <tfoot>
- <tr class='total'>
- <td class='name left'>Total</td>
- <td>{{totals.n_statements}}</td>
- <td>{{totals.n_executed}}</td>
- <td>{{totals.n_excluded}}</td>
- {% if arcs %}
- <td>{{totals.n_branches}}</td>
- <td>{{totals.n_executed_branches}}</td>
- {% endif %}
- <td class='right'>{{totals.pc_covered|format_pct}}%</td>
- </tr>
- </tfoot>
- </table>
- </div>
-
- <div id='footer'>
- <div class='content'>
- <p>
- <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a>
- </p>
- </div>
- </div>
-
- <script type="text/javascript" charset="utf-8">
- jQuery(function() {
- jQuery("table.index").tablesorter({
- headers: {
- 0: { sorter:'text' },
- 1: { sorter:'digit' },
- 2: { sorter:'digit' },
- 3: { sorter:'digit' },
- 4: { sorter:'percent' },
- }
- });
- });
- </script>
- </body>
-</html>
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
+ <title>Coverage report</title>
+ <link rel='stylesheet' href='style.css' type='text/css'>
+ <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
+ <script type='text/javascript' src='jquery.tablesorter.min.js'></script>
+ <script type='text/javascript' src='coverage_html.js'></script>
+ <script type='text/javascript' charset='utf-8'>
+ jQuery(document).ready(index_ready);
+ </script>
+</head>
+<body id='indexfile'>
+
+<div id='header'>
+ <div class='content'>
+ <h1>Coverage report:
+ <span class='pc_cov'>{{totals.pc_covered|format_pct}}%</span>
+ </h1>
+ </div>
+</div>
+
+<div id='index'>
+ <table class='index'>
+ <thead>
+ {# The title='' attr doesn't work in Safari. #}
+ <tr class='tablehead' title='Click to sort'>
+ <th class='name left headerSortDown'>Module</th>
+ <th>statements</th>
+ <th>missing</th>
+ <th>excluded</th>
+ {% if arcs %}
+ <th>branches</th>
+ <th>partial</th>
+ {% endif %}
+ <th class='right'>coverage</th>
+ </tr>
+ </thead>
+ {# HTML syntax requires thead, tfoot, tbody #}
+ <tfoot>
+ <tr class='total'>
+ <td class='name left'>Total</td>
+ <td>{{totals.n_statements}}</td>
+ <td>{{totals.n_missing}}</td>
+ <td>{{totals.n_excluded}}</td>
+ {% if arcs %}
+ <td>{{totals.n_branches}}</td>
+ <td>{{totals.n_missing_branches}}</td>
+ {% endif %}
+ <td class='right'>{{totals.pc_covered|format_pct}}%</td>
+ </tr>
+ </tfoot>
+ <tbody>
+ {% for file in files %}
+ <tr class='file'>
+ <td class='name left'><a href='{{file.html_filename}}'>{{file.cu.name}}</a></td>
+ <td>{{file.nums.n_statements}}</td>
+ <td>{{file.nums.n_missing}}</td>
+ <td>{{file.nums.n_excluded}}</td>
+ {% if arcs %}
+ <td>{{file.nums.n_branches}}</td>
+ <td>{{file.nums.n_missing_branches}}</td>
+ {% endif %}
+ <td class='right'>{{file.nums.pc_covered|format_pct}}%</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+</div>
+
+<div id='footer'>
+ <div class='content'>
+ <p>
+ <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a>
+ </p>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/coverage/htmlfiles/pyfile.html b/coverage/htmlfiles/pyfile.html
index ca65152d..035691c8 100644
--- a/coverage/htmlfiles/pyfile.html
+++ b/coverage/htmlfiles/pyfile.html
@@ -1,58 +1,61 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
-<html>
-<head>
-<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
-<title>Coverage for {{cu.name|escape}}</title>
-<link rel='stylesheet' href='style.css' type='text/css'>
-<script type='text/javascript' src='jquery-1.3.2.min.js'></script>
-<script type='text/javascript'>
-function toggle_lines(btn, cls) {
- var btn = $(btn);
- if (btn.hasClass("hide")) {
- $("#source ."+cls).removeClass("hide");
- btn.removeClass("hide");
- }
- else {
- $("#source ."+cls).addClass("hide");
- btn.addClass("hide");
- }
-}
-</script>
-</head>
-<body>
-<div id='header'>
- <div class='content'>
- <h1>Coverage for <b>{{cu.name|escape}}</b> :
- <span class='pc_cov'>{{nums.pc_covered|format_pct}}%</span>
- </h1>
- <h2 class='stats'>
- {{nums.n_statements}} statements
- <span class='{{c_run.strip}}' onclick='toggle_lines(this, "run")'>{{nums.n_executed}} run</span>
- <span class='{{c_exc.strip}}' onclick='toggle_lines(this, "exc")'>{{nums.n_excluded}} excluded</span>
- <span class='{{c_mis.strip}}' onclick='toggle_lines(this, "mis")'>{{nums.n_missing}} missing</span>
- {% if arcs %}
- <span class='{{c_par.strip}}' onclick='toggle_lines(this, "par")'>{{n_par}} partial</span>
- {% endif %}
- </h2>
- </div>
-</div>
-
-<div id='source'>
-<table cellspacing='0' cellpadding='0'>
-<tr>
-<td class='linenos' valign='top'>
- {% for line in lines %}
- <p class='{{line.class}}'>{{line.number}}</p>
- {% endfor %}
-</td>
-<td class='text' valign='top'>
- {% for line in lines %}
- <p class='{{line.class}}'>{% if line.annotate %}<span class='annotate'>{{line.annotate}}</span>{% endif %}{{line.html}}<span class='strut'>&nbsp;</span></p>
- {% endfor %}
-</td>
-</tr>
-</table>
-</div>
-
-</body>
-</html>
+<!doctype html PUBLIC "-//W3C//DTD html 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
+<html>
+<head>
+ <meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
+ {# IE8 rounds line-height incorrectly, and adding this emulateIE7 line makes it right! #}
+ {# http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/7684445e-f080-4d8f-8529-132763348e21 #}
+ <meta http-equiv='X-UA-Compatible' content='IE=emulateIE7' />
+ <title>Coverage for {{cu.name|escape}}: {{nums.pc_covered|format_pct}}%</title>
+ <link rel='stylesheet' href='style.css' type='text/css'>
+ <script type='text/javascript' src='jquery-1.3.2.min.js'></script>
+ <script type='text/javascript' src='coverage_html.js'></script>
+ <script type='text/javascript' charset='utf-8'>
+ jQuery(document).ready(pyfile_ready);
+ </script>
+</head>
+<body id='pyfile'>
+
+<div id='header'>
+ <div class='content'>
+ <h1>Coverage for <b>{{cu.name|escape}}</b> :
+ <span class='pc_cov'>{{nums.pc_covered|format_pct}}%</span>
+ </h1>
+ <h2 class='stats'>
+ {{nums.n_statements}} statements
+ <span class='{{c_run}}' onclick='toggle_lines(this, "run")'>{{nums.n_executed}} run</span>
+ <span class='{{c_mis}}' onclick='toggle_lines(this, "mis")'>{{nums.n_missing}} missing</span>
+ <span class='{{c_exc}}' onclick='toggle_lines(this, "exc")'>{{nums.n_excluded}} excluded</span>
+ {% if arcs %}
+ <span class='{{c_par}}' onclick='toggle_lines(this, "par")'>{{n_par}} partial</span>
+ {% endif %}
+ </h2>
+ </div>
+</div>
+
+<div id='source'>
+ <table cellspacing='0' cellpadding='0'>
+ <tr>
+ <td class='linenos' valign='top'>
+ {% for line in lines %}
+ <p id='n{{line.number}}' class='{{line.class}}'><a href='#n{{line.number}}'>{{line.number}}</a></p>
+ {% endfor %}
+ </td>
+ <td class='text' valign='top'>
+ {% for line in lines %}
+ <p id='t{{line.number}}' class='{{line.class}}'>{% if line.annotate %}<span class='annotate' title='{{line.annotate_title}}'>{{line.annotate}}</span>{% endif %}{{line.html}}<span class='strut'>&nbsp;</span></p>
+ {% endfor %}
+ </td>
+ </tr>
+ </table>
+</div>
+
+<div id='footer'>
+ <div class='content'>
+ <p>
+ <a class='nav' href='index.html'>&#xab; index</a> &nbsp; &nbsp; <a class='nav' href='{{__url__}}'>coverage.py v{{__version__}}</a>
+ </p>
+ </div>
+</div>
+
+</body>
+</html>
diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css
index dd6c991a..9a06a2b4 100644
--- a/coverage/htmlfiles/style.css
+++ b/coverage/htmlfiles/style.css
@@ -20,7 +20,7 @@ body {
html>body {
font-size: 16px;
- }
+ }
/* Set base font size to 12/16 */
p {
@@ -53,10 +53,14 @@ a.nav:hover {
font-family: "courier new", monospace;
}
-#footer {
+#indexfile #footer {
margin: 1em 3em;
}
+#pyfile #footer {
+ margin: 1em 1em;
+ }
+
#footer .content {
padding: 0;
font-size: 85%;
@@ -89,9 +93,14 @@ h2.stats {
cursor: pointer;
border-color: #999 #ccc #ccc #999;
}
-.stats span.hide {
+.stats span.hide_run, .stats span.hide_exc,
+.stats span.hide_mis, .stats span.hide_par,
+.stats span.par.hide_run.hide_par {
border-color: #ccc #999 #999 #ccc;
}
+.stats span.par.hide_run {
+ border-color: #999 #ccc #ccc #999;
+}
/* Source file styles */
.linenos p {
@@ -100,9 +109,21 @@ h2.stats {
padding: 0 .5em;
color: #999999;
font-family: verdana, sans-serif;
- font-size: .625em; /* 10/16 */
+ font-size: .625em; /* 10/16 */
line-height: 1.6em; /* 16/10 */
}
+.linenos p.highlight {
+ background: #ffdd00;
+ }
+.linenos p a {
+ text-decoration: none;
+ color: #999999;
+ }
+.linenos p a:hover {
+ text-decoration: underline;
+ color: #999999;
+ }
+
td.text {
width: 100%;
}
@@ -110,14 +131,14 @@ td.text {
margin: 0;
padding: 0 0 0 .5em;
border-left: 2px solid #ffffff;
- white-space: nowrap;
+ white-space: nowrap;
}
.text p.mis {
background: #ffdddd;
border-left: 2px solid #ff0000;
}
-.text p.run {
+.text p.run, .text p.run.hide_par {
background: #ddffdd;
border-left: 2px solid #00ff00;
}
@@ -125,11 +146,12 @@ td.text {
background: #eeeeee;
border-left: 2px solid #808080;
}
-.text p.par {
+.text p.par, .text p.par.hide_run {
background: #ffffaa;
border-left: 2px solid #eeee99;
}
-.text p.hide {
+.text p.hide_run, .text p.hide_exc, .text p.hide_mis, .text p.hide_par,
+.text p.hide_run.hide_par {
background: inherit;
}
@@ -140,7 +162,7 @@ td.text {
float: right;
padding-right: .5em;
}
-.text p.hide span.annotate {
+.text p.hide_par span.annotate {
display: none;
}
diff --git a/coverage/misc.py b/coverage/misc.py
index 329a8417..4218536d 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -2,10 +2,10 @@
def nice_pair(pair):
"""Make a nice string representation of a pair of numbers.
-
+
If the numbers are equal, just return the number, otherwise return the pair
with a dash between them, indicating the range.
-
+
"""
start, end = pair
if start == end:
@@ -20,10 +20,10 @@ def format_lines(statements, lines):
Format a list of line numbers for printing by coalescing groups of lines as
long as the lines represent consecutive statements. This will coalesce
even if there are gaps between statements.
-
+
For example, if `statements` is [1,2,3,4,5,10,11,12,13,14] and
`lines` is [1,2,5,10,11,13,14] then the result will be "1-2, 5-11, 13-14".
-
+
"""
pairs = []
i = 0
@@ -47,18 +47,27 @@ def format_lines(statements, lines):
def expensive(fn):
"""A decorator to cache the result of an expensive operation.
-
+
Only applies to methods with no arguments.
-
+
"""
attr = "_cache_" + fn.__name__
def _wrapped(self):
+ """Inner fn that checks the cache."""
if not hasattr(self, attr):
setattr(self, attr, fn(self))
return getattr(self, attr)
return _wrapped
+def bool_or_none(b):
+ """Return bool(b), but preserve None."""
+ if b is None:
+ return None
+ else:
+ return bool(b)
+
+
class CoverageException(Exception):
"""An exception specific to Coverage."""
pass
@@ -66,3 +75,11 @@ class CoverageException(Exception):
class NoSource(CoverageException):
"""Used to indicate we couldn't find the source for a module."""
pass
+
+class ExceptionDuringRun(CoverageException):
+ """An exception happened while running customer code.
+
+ Construct it with three arguments, the values from `sys.exc_info`.
+
+ """
+ pass
diff --git a/coverage/parser.py b/coverage/parser.py
index 01b38af3..b090f02d 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -9,13 +9,13 @@ from coverage.misc import nice_pair, CoverageException, NoSource, expensive
class CodeParser(object):
"""Parse code to find executable lines, excluded lines, etc."""
-
+
def __init__(self, text=None, filename=None, exclude=None):
"""
Source can be provided as `text`, the text itself, or `filename`, from
- which text will be read. Excluded lines are those that match `exclude`,
- a regex.
-
+ which text will be read. Excluded lines are those that match
+ `exclude`, a regex.
+
"""
assert text or filename, "CodeParser needs either text or filename"
self.filename = filename or "<code>"
@@ -33,7 +33,7 @@ class CodeParser(object):
self.text = self.text.replace('\r\n', '\n')
self.exclude = exclude
-
+
self.show_tokens = False
# The text lines of the parsed code.
@@ -41,22 +41,22 @@ class CodeParser(object):
# The line numbers of excluded lines of code.
self.excluded = set()
-
+
# The line numbers of docstring lines.
self.docstrings = set()
-
+
# The line numbers of class definitions.
self.classdefs = set()
# A dict mapping line numbers to (lo,hi) for multi-line statements.
self.multiline = {}
-
+
# The line numbers that start statements.
self.statement_starts = set()
# Lazily-created ByteParser
self._byte_parser = None
-
+
def _get_byte_parser(self):
"""Create a ByteParser on demand."""
if not self._byte_parser:
@@ -67,9 +67,9 @@ class CodeParser(object):
def _raw_parse(self):
"""Parse the source to find the interesting facts about its lines.
-
+
A handful of member fields are updated.
-
+
"""
# Find lines which match an exclusion pattern.
if self.exclude:
@@ -77,7 +77,7 @@ class CodeParser(object):
for i, ltext in enumerate(self.lines):
if re_exclude.search(ltext):
self.excluded.add(i+1)
-
+
# Tokenize, to find excluded suites, to find docstrings, and to find
# multi-line statements.
indent = 0
@@ -88,7 +88,7 @@ class CodeParser(object):
tokgen = tokenize.generate_tokens(StringIO(self.text).readline)
for toktype, ttext, (slineno, _), (elineno, _), ltext in tokgen:
- if self.show_tokens:
+ if self.show_tokens: # pragma: no cover
print("%10s %5s %-20r %r" % (
tokenize.tok_name.get(toktype, toktype),
nice_pair((slineno, elineno)), ttext, ltext
@@ -111,7 +111,9 @@ class CodeParser(object):
excluding = True
elif toktype == token.STRING and prev_toktype == token.INDENT:
# Strings that are first on an indented line are docstrings.
- # (a trick from trace.py in the stdlib.)
+ # (a trick from trace.py in the stdlib.) This works for
+ # 99.9999% of cases. For the rest (!) see:
+ # http://stackoverflow.com/questions/1769332/x/1769794#1769794
for i in range(slineno, elineno+1):
self.docstrings.add(i)
elif toktype == token.NEWLINE:
@@ -123,7 +125,7 @@ class CodeParser(object):
for l in range(first_line, elineno+1):
self.multiline[l] = rng
first_line = None
-
+
if ttext.strip() and toktype != tokenize.COMMENT:
# A non-whitespace token.
if first_line is None:
@@ -135,7 +137,7 @@ class CodeParser(object):
excluding = False
if excluding:
self.excluded.add(elineno)
-
+
prev_toktype = toktype
# Find the starts of the executable statements.
@@ -153,11 +155,11 @@ class CodeParser(object):
def first_lines(self, lines, ignore=None):
"""Map the line numbers in `lines` to the correct first line of the
statement.
-
+
Skip any line mentioned in `ignore`.
-
+
Returns a sorted list of the first lines.
-
+
"""
ignore = ignore or []
lset = set()
@@ -168,31 +170,31 @@ class CodeParser(object):
if new_l not in ignore:
lset.add(new_l)
return sorted(lset)
-
+
def parse_source(self):
"""Parse source text to find executable lines, excluded lines, etc.
Return values are 1) a sorted list of executable line numbers, and
2) a sorted list of excluded line numbers.
-
+
Reported line numbers are normalized to the first line of multi-line
statements.
-
+
"""
self._raw_parse()
-
+
excluded_lines = self.first_lines(self.excluded)
ignore = excluded_lines + list(self.docstrings)
lines = self.first_lines(self.statement_starts, ignore)
-
+
return lines, excluded_lines
def arcs(self):
"""Get information about the arcs available in the code.
-
+
Returns a sorted list of line number pairs. Line numbers have been
normalized to the first line of multiline statements.
-
+
"""
all_arcs = []
for l1, l2 in self.byte_parser._all_arcs():
@@ -205,27 +207,32 @@ class CodeParser(object):
def exit_counts(self):
"""Get a mapping from line numbers to count of exits from that line.
-
+
Excluded lines are excluded.
-
+
"""
excluded_lines = self.first_lines(self.excluded)
exit_counts = {}
for l1, l2 in self.arcs():
- if l1 == -1:
+ if l1 < 0:
+ # Don't ever report -1 as a line number
continue
if l1 in excluded_lines:
+ # Don't report excluded lines as line numbers.
+ continue
+ if l2 in excluded_lines:
+ # Arcs to excluded lines shouldn't count.
continue
if l1 not in exit_counts:
exit_counts[l1] = 0
exit_counts[l1] += 1
-
+
# Class definitions have one extra exit, so remove one for each:
for l in self.classdefs:
- # Ensure key is there - #pragma: no cover will mean its not
+ # Ensure key is there: classdefs can include excluded lines.
if l in exit_counts:
exit_counts[l] -= 1
-
+
return exit_counts
exit_counts = expensive(exit_counts)
@@ -249,6 +256,11 @@ OPS_CHUNK_END = _opcode_set(
'BREAK_LOOP', 'CONTINUE_LOOP',
)
+# Opcodes that unconditionally begin a new code chunk. By starting new chunks
+# with unconditional jump instructions, we neatly deal with jumps to jumps
+# properly.
+OPS_CHUNK_BEGIN = _opcode_set('JUMP_ABSOLUTE', 'JUMP_FORWARD')
+
# Opcodes that push a block on the block stack.
OPS_PUSH_BLOCK = _opcode_set('SETUP_LOOP', 'SETUP_EXCEPT', 'SETUP_FINALLY')
@@ -266,6 +278,8 @@ OP_BREAK_LOOP = _opcode('BREAK_LOOP')
OP_END_FINALLY = _opcode('END_FINALLY')
OP_COMPARE_OP = _opcode('COMPARE_OP')
COMPARE_EXCEPTION = 10 # just have to get this const from the code.
+OP_LOAD_CONST = _opcode('LOAD_CONST')
+OP_RETURN_VALUE = _opcode('RETURN_VALUE')
class ByteParser(object):
@@ -294,14 +308,14 @@ class ByteParser(object):
def child_parsers(self):
"""Iterate over all the code objects nested within this one.
-
+
The iteration includes `self` as its first value.
-
+
"""
return map(lambda c: ByteParser(code=c), CodeObjects(self.code))
- # Getting numbers from the lnotab value changed in Py3.0.
- if sys.hexversion >= 0x03000000:
+ # Getting numbers from the lnotab value changed in Py3.0.
+ if sys.version_info >= (3, 0):
def _lnotab_increments(self, lnotab):
"""Return a list of ints from the lnotab bytes in 3.x"""
return list(lnotab)
@@ -312,15 +326,15 @@ class ByteParser(object):
def _bytes_lines(self):
"""Map byte offsets to line numbers in `code`.
-
+
Uses co_lnotab described in Python/compile.c to map byte offsets to
line numbers. Returns a list: [(b0, l0), (b1, l1), ...]
-
+
"""
# Adapted from dis.py in the standard library.
byte_increments = self._lnotab_increments(self.code.co_lnotab[0::2])
line_increments = self._lnotab_increments(self.code.co_lnotab[1::2])
-
+
bytes_lines = []
last_line_num = None
line_num = self.code.co_firstlineno
@@ -335,13 +349,13 @@ class ByteParser(object):
if line_num != last_line_num:
bytes_lines.append((byte_num, line_num))
return bytes_lines
-
+
def _find_statements(self):
"""Find the statements in `self.code`.
-
+
Return a set of line numbers that start statements. Recurses into all
code objects reachable from `self.code`.
-
+
"""
stmts = set()
for bp in self.child_parsers():
@@ -349,12 +363,12 @@ class ByteParser(object):
for _, l in bp._bytes_lines():
stmts.add(l)
return stmts
-
- def _disassemble(self):
+
+ def _disassemble(self): # pragma: no cover
"""Disassemble code, for ad-hoc experimenting."""
-
+
import dis
-
+
for bp in self.child_parsers():
print("\n%s: " % bp.code)
dis.dis(bp.code)
@@ -364,41 +378,52 @@ class ByteParser(object):
def _split_into_chunks(self):
"""Split the code object into a list of `Chunk` objects.
-
+
Each chunk is only entered at its first instruction, though there can
be many exits from a chunk.
-
+
Returns a list of `Chunk` objects.
-
+
"""
# The list of chunks so far, and the one we're working on.
chunks = []
chunk = None
bytes_lines_map = dict(self._bytes_lines())
-
+
# The block stack: loops and try blocks get pushed here for the
# implicit jumps that can occur.
# Each entry is a tuple: (block type, destination)
block_stack = []
-
+
# Some op codes are followed by branches that should be ignored. This
# is a count of how many ignores are left.
ignore_branch = 0
+ # We have to handle the last two bytecodes specially.
+ ult = penult = None
+
for bc in ByteCodes(self.code.co_code):
- # Maybe have to start a new block
+ # Maybe have to start a new chunk
if bc.offset in bytes_lines_map:
+ # Start a new chunk for each source line number.
if chunk:
chunk.exits.add(bc.offset)
chunk = Chunk(bc.offset, bytes_lines_map[bc.offset])
chunks.append(chunk)
-
+ elif bc.op in OPS_CHUNK_BEGIN:
+ # Jumps deserve their own unnumbered chunk. This fixes
+ # problems with jumps to jumps getting confused.
+ if chunk:
+ chunk.exits.add(bc.offset)
+ chunk = Chunk(bc.offset)
+ chunks.append(chunk)
+
if not chunk:
chunk = Chunk(bc.offset)
chunks.append(chunk)
- # Look at the opcode
+ # Look at the opcode
if bc.jump_to >= 0 and bc.op not in OPS_NO_JUMP:
if ignore_branch:
# Someone earlier wanted us to ignore this branch.
@@ -406,10 +431,10 @@ class ByteParser(object):
else:
# The opcode has a jump, it's an exit for this chunk.
chunk.exits.add(bc.jump_to)
-
+
if bc.op in OPS_CODE_END:
# The opcode can exit the code object.
- chunk.exits.add(-1)
+ chunk.exits.add(-self.code.co_firstlineno)
if bc.op in OPS_PUSH_BLOCK:
# The opcode adds a block to the block_stack.
block_stack.append((bc.op, bc.jump_to))
@@ -438,8 +463,32 @@ class ByteParser(object):
# This is an except clause. We want to overlook the next
# branch, so that except's don't count as branches.
ignore_branch += 1
-
+
+ penult = ult
+ ult = bc
+
+
if chunks:
+ # The last two bytecodes could be a dummy "return None" that
+ # shouldn't be counted as real code. Every Python code object seems
+ # to end with a return, and a "return None" is inserted if there
+ # isn't an explicit return in the source.
+ if ult and penult:
+ if penult.op == OP_LOAD_CONST and ult.op == OP_RETURN_VALUE:
+ if self.code.co_consts[penult.arg] is None:
+ # This is "return None", but is it dummy? A real line
+ # would be a last chunk all by itself.
+ if chunks[-1].byte != penult.offset:
+ exit = -self.code.co_firstlineno
+ # Split the last chunk
+ last_chunk = chunks[-1]
+ last_chunk.exits.remove(exit)
+ last_chunk.exits.add(penult.offset)
+ chunk = Chunk(penult.offset)
+ chunk.exits.add(exit)
+ chunks.append(chunk)
+
+ # Give all the chunks a length.
chunks[-1].length = bc.next_offset - chunks[-1].byte
for i in range(len(chunks)-1):
chunks[i].length = chunks[i+1].byte - chunks[i].byte
@@ -448,35 +497,35 @@ class ByteParser(object):
def _arcs(self):
"""Find the executable arcs in the code.
-
+
Returns a set of pairs, (from,to). From and to are integer line
- numbers. If from is -1, then the arc is an entrance into the code
- object. If to is -1, the arc is an exit from the code object.
-
+ numbers. If from is < 0, then the arc is an entrance into the code
+ object. If to is < 0, the arc is an exit from the code object.
+
"""
chunks = self._split_into_chunks()
-
+
# A map from byte offsets to chunks jumped into.
byte_chunks = dict([(c.byte, c) for c in chunks])
# Build a map from byte offsets to actual lines reached.
- byte_lines = {-1:[-1]}
+ byte_lines = {}
bytes_to_add = set([c.byte for c in chunks])
-
+
while bytes_to_add:
byte_to_add = bytes_to_add.pop()
- if byte_to_add in byte_lines or byte_to_add == -1:
+ if byte_to_add in byte_lines or byte_to_add < 0:
continue
-
+
# Which lines does this chunk lead to?
bytes_considered = set()
bytes_to_consider = [byte_to_add]
lines = set()
-
+
while bytes_to_consider:
byte = bytes_to_consider.pop()
bytes_considered.add(byte)
-
+
# Find chunk for byte
try:
ch = byte_chunks[byte]
@@ -488,89 +537,94 @@ class ByteParser(object):
# No chunk for this byte!
raise Exception("Couldn't find chunk @ %d" % byte)
byte_chunks[byte] = ch
-
+
if ch.line:
lines.add(ch.line)
else:
for ex in ch.exits:
- if ex == -1:
- lines.add(-1)
+ if ex < 0:
+ lines.add(ex)
elif ex not in bytes_considered:
bytes_to_consider.append(ex)
bytes_to_add.update(ch.exits)
byte_lines[byte_to_add] = lines
-
+
# Figure out for each chunk where the exits go.
arcs = set()
for chunk in chunks:
if chunk.line:
for ex in chunk.exits:
- for exit_line in byte_lines[ex]:
+ if ex < 0:
+ exit_lines = [ex]
+ else:
+ exit_lines = byte_lines[ex]
+ for exit_line in exit_lines:
if chunk.line != exit_line:
arcs.add((chunk.line, exit_line))
for line in byte_lines[0]:
arcs.add((-1, line))
-
+
return arcs
-
+
def _all_chunks(self):
"""Returns a list of `Chunk` objects for this code and its children.
-
+
See `_split_into_chunks` for details.
-
+
"""
chunks = []
for bp in self.child_parsers():
chunks.extend(bp._split_into_chunks())
-
+
return chunks
def _all_arcs(self):
"""Get the set of all arcs in this code object and its children.
-
+
See `_arcs` for details.
-
+
"""
arcs = set()
for bp in self.child_parsers():
arcs.update(bp._arcs())
-
+
return arcs
class Chunk(object):
"""A sequence of bytecodes with a single entrance.
-
+
To analyze byte code, we have to divide it into chunks, sequences of byte
codes such that each basic block has only one entrance, the first
- instruction in the block.
-
+ instruction in the block.
+
This is almost the CS concept of `basic block`_, except that we're willing
to have many exits from a chunk, and "basic block" is a more cumbersome
term.
-
+
.. _basic block: http://en.wikipedia.org/wiki/Basic_block
-
- An exit of -1 means the chunk can leave the code (return).
-
+
+ An exit < 0 means the chunk can leave the code (return). The exit is
+ the negative of the starting line number of the code block.
+
"""
def __init__(self, byte, line=0):
self.byte = byte
self.line = line
self.length = 0
self.exits = set()
-
+
def __repr__(self):
return "<%d+%d @%d %r>" % (
self.byte, self.length, self.line, list(self.exits)
)
-class AdHocMain(object):
+class AdHocMain(object): # pragma: no cover
"""An ad-hoc main for code parsing experiments."""
-
+
def main(self, args):
"""A main function for trying the code from the command line."""
@@ -597,7 +651,7 @@ class AdHocMain(object):
"-t", action="store_true", dest="tokens",
help="Show tokens"
)
-
+
options, args = parser.parse_args()
if options.recursive:
if args:
@@ -612,12 +666,12 @@ class AdHocMain(object):
def adhoc_one_file(self, options, filename):
"""Process just one file."""
-
+
if options.dis or options.chunks:
try:
bp = ByteParser(filename=filename)
except CoverageException:
- _, err, _ = sys.exc_info()
+ _, err, _ = sys.exc_info()
print("%s" % (err,))
return
@@ -644,7 +698,7 @@ class AdHocMain(object):
arc_width, arc_chars = self.arc_ascii_art(arcs)
else:
arc_width, arc_chars = 0, {}
-
+
exit_counts = cp.exit_counts()
for i, ltext in enumerate(cp.lines):
@@ -668,19 +722,19 @@ class AdHocMain(object):
def arc_ascii_art(self, arcs):
"""Draw arcs as ascii art.
-
+
Returns a width of characters needed to draw all the arcs, and a
dictionary mapping line numbers to ascii strings to draw for that line.
-
+
"""
arc_chars = {}
for lfrom, lto in sorted(arcs):
- if lfrom == -1:
+ if lfrom < 0:
arc_chars[lto] = arc_chars.get(lto, '') + 'v'
- elif lto == -1:
+ elif lto < 0:
arc_chars[lfrom] = arc_chars.get(lfrom, '') + '^'
else:
- if lfrom == lto-1:
+ if lfrom == lto - 1:
# Don't show obvious arcs.
continue
if lfrom < lto:
diff --git a/coverage/phystokens.py b/coverage/phystokens.py
index 2862490f..60b87932 100644
--- a/coverage/phystokens.py
+++ b/coverage/phystokens.py
@@ -5,16 +5,17 @@ from coverage.backward import StringIO # pylint: disable-msg=W0622
def phys_tokens(toks):
"""Return all physical tokens, even line continuations.
-
+
tokenize.generate_tokens() doesn't return a token for the backslash that
continues lines. This wrapper provides those tokens so that we can
re-create a faithful representation of the original source.
-
+
Returns the same values as generate_tokens()
-
+
"""
last_line = None
last_lineno = -1
+ last_ttype = None
for ttype, ttext, (slineno, scol), (elineno, ecol), ltext in toks:
if last_lineno != elineno:
if last_line and last_line[-2:] == "\\\n":
@@ -34,7 +35,11 @@ def phys_tokens(toks):
# so we need to figure out if the backslash is already in the
# string token or not.
inject_backslash = True
- if ttype == token.STRING:
+ if last_ttype == tokenize.COMMENT:
+ # Comments like this \
+ # should never result in a new token.
+ inject_backslash = False
+ elif ttype == token.STRING:
if "\n" in ttext and ttext.split('\n', 1)[0][-1] == '\\':
# It's a multiline string and the first line ends with
# a backslash, so we don't need to inject another.
@@ -49,19 +54,20 @@ def phys_tokens(toks):
last_line
)
last_line = ltext
+ last_ttype = ttype
yield ttype, ttext, (slineno, scol), (elineno, ecol), ltext
last_lineno = elineno
def source_token_lines(source):
"""Generate a series of lines, one for each line in `source`.
-
+
Each line is a list of pairs, each pair is a token::
-
+
[('key', 'def'), ('ws', ' '), ('nam', 'hello'), ('op', '('), ... ]
Each pair has a token class, and the token text.
-
+
If you concatenate all the token texts, and then join them with newlines,
you should have your original `source` back, with two differences:
trailing whitespace is not preserved, and a final line with no newline
@@ -71,7 +77,8 @@ def source_token_lines(source):
ws_tokens = [token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL]
line = []
col = 0
- tokgen = tokenize.generate_tokens(StringIO(source.expandtabs(8)).readline)
+ source = source.expandtabs(8).replace('\r\n', '\n')
+ tokgen = tokenize.generate_tokens(StringIO(source).readline)
for ttype, ttext, (_, scol), (_, ecol), _ in phys_tokens(tokgen):
mark_start = True
for part in re.split('(\n)', ttext):
diff --git a/coverage/report.py b/coverage/report.py
index 7f3e3e02..a676e186 100644
--- a/coverage/report.py
+++ b/coverage/report.py
@@ -2,48 +2,59 @@
import os
from coverage.codeunit import code_unit_factory
-from coverage.misc import NoSource
+from coverage.misc import CoverageException, NoSource
class Reporter(object):
"""A base class for all reporters."""
-
+
def __init__(self, coverage, ignore_errors=False):
"""Create a reporter.
-
+
`coverage` is the coverage instance. `ignore_errors` controls how
skittish the reporter will be during file processing.
"""
self.coverage = coverage
self.ignore_errors = ignore_errors
-
+
# The code units to report on. Set by find_code_units.
self.code_units = []
-
+
# The directory into which to place the report, used by some derived
# classes.
self.directory = None
- def find_code_units(self, morfs, omit_prefixes):
+ def find_code_units(self, morfs, omit_prefixes, include_prefixes):
"""Find the code units we'll report on.
-
+
`morfs` is a list of modules or filenames. `omit_prefixes` is a list
of prefixes to leave out of the list.
-
+
+ See `coverage.report()` for other arguments.
+
"""
morfs = morfs or self.coverage.data.executed_files()
self.code_units = code_unit_factory(
- morfs, self.coverage.file_locator, omit_prefixes)
+ morfs, self.coverage.file_locator, omit_prefixes,
+ include_prefixes
+ )
self.code_units.sort()
def report_files(self, report_fn, morfs, directory=None,
- omit_prefixes=None):
+ omit_prefixes=None, include_prefixes=None):
"""Run a reporting function on a number of morfs.
-
+
`report_fn` is called for each relative morf in `morfs`.
-
+
+ `include_prefixes` is a list of filename prefixes. CodeUnits that match
+ those prefixes will be included in the list. CodeUnits that match
+ `omit_prefixes` will be omitted from the list.
+
"""
- self.find_code_units(morfs, omit_prefixes)
+ self.find_code_units(morfs, omit_prefixes, include_prefixes)
+
+ if not self.code_units:
+ raise CoverageException("No data to report.")
self.directory = directory
if self.directory and not os.path.exists(self.directory):
diff --git a/coverage/results.py b/coverage/results.py
index 77c461ad..e80ec0a4 100644
--- a/coverage/results.py
+++ b/coverage/results.py
@@ -9,11 +9,11 @@ from coverage.parser import CodeParser
class Analysis(object):
"""The results of analyzing a code unit."""
-
+
def __init__(self, cov, code_unit):
self.coverage = cov
self.code_unit = code_unit
-
+
self.filename = self.code_unit.filename
ext = os.path.splitext(self.filename)[1]
source = None
@@ -40,10 +40,10 @@ class Analysis(object):
n_missing_branches = sum([len(v) for v in mba.values()])
else:
n_branches = n_missing_branches = 0
-
+
self.numbers = Numbers(
n_files=1,
- n_statements=len(self.statements),
+ n_statements=len(self.statements),
n_excluded=len(self.excluded),
n_missing=len(self.missing),
n_branches=n_branches,
@@ -52,9 +52,9 @@ class Analysis(object):
def missing_formatted(self):
"""The missing line numbers, formatted nicely.
-
+
Returns a string like "1-2, 5-11, 13-14".
-
+
"""
return format_lines(self.statements, self.missing)
@@ -102,12 +102,12 @@ class Analysis(object):
"""How many total branches are there?"""
exit_counts = self.parser.exit_counts()
return sum([count for count in exit_counts.values() if count > 1])
-
+
def missing_branch_arcs(self):
"""Return arcs that weren't executed from branch lines.
-
+
Returns {l1:[l2a,l2b,...], ...}
-
+
"""
missing = self.arcs_missing()
branch_lines = set(self.branch_lines())
@@ -122,7 +122,7 @@ class Analysis(object):
class Numbers(object):
"""The numerical results of measuring coverage.
-
+
This holds the basic statistics from `Analysis`, and is used to roll
up statistics across files.
@@ -141,12 +141,12 @@ class Numbers(object):
"""Returns the number of executed statements."""
return self.n_statements - self.n_missing
n_executed = property(_get_n_executed)
-
+
def _get_n_executed_branches(self):
"""Returns the number of executed branches."""
return self.n_branches - self.n_missing_branches
n_executed_branches = property(_get_n_executed_branches)
-
+
def _get_pc_covered(self):
"""Returns a single percentage value for coverage."""
if self.n_statements > 0:
diff --git a/coverage/summary.py b/coverage/summary.py
index e0e9eba7..89b31020 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -8,25 +8,30 @@ from coverage.results import Numbers
class SummaryReporter(Reporter):
"""A reporter for writing the summary report."""
-
+
def __init__(self, coverage, show_missing=True, ignore_errors=False):
super(SummaryReporter, self).__init__(coverage, ignore_errors)
self.show_missing = show_missing
self.branches = coverage.data.has_arcs()
- def report(self, morfs, omit_prefixes=None, outfile=None):
- """Writes a report summarizing coverage statistics per module."""
-
- self.find_code_units(morfs, omit_prefixes)
+ def report(self, morfs, omit_prefixes=None, outfile=None,
+ include_prefixes=None
+ ):
+ """Writes a report summarizing coverage statistics per module.
+
+ See `coverage.report()` for other arguments.
+
+ """
+ self.find_code_units(morfs, omit_prefixes, include_prefixes)
# Prepare the formatting strings
max_name = max([len(cu.name) for cu in self.code_units] + [5])
fmt_name = "%%- %ds " % max_name
fmt_err = "%s %s: %s\n"
- header = (fmt_name % "Name") + " Stmts Exec"
+ header = (fmt_name % "Name") + " Stmts Miss"
fmt_coverage = fmt_name + "%6d %6d"
if self.branches:
- header += " Branch BrExec"
+ header += " Branch BrPart"
fmt_coverage += " %6d %6d"
header += " Cover"
fmt_coverage += " %5d%%"
@@ -45,20 +50,20 @@ class SummaryReporter(Reporter):
outfile.write(rule)
total = Numbers()
-
+
for cu in self.code_units:
try:
analysis = self.coverage._analyze(cu)
nums = analysis.numbers
- args = (cu.name, nums.n_statements, nums.n_executed)
+ args = (cu.name, nums.n_statements, nums.n_missing)
if self.branches:
- args += (nums.n_branches, nums.n_executed_branches)
+ args += (nums.n_branches, nums.n_missing_branches)
args += (nums.pc_covered,)
if self.show_missing:
args += (analysis.missing_formatted(),)
outfile.write(fmt_coverage % args)
total += nums
- except KeyboardInterrupt: #pragma: no cover
+ except KeyboardInterrupt: # pragma: no cover
raise
except:
if not self.ignore_errors:
@@ -67,9 +72,9 @@ class SummaryReporter(Reporter):
if total.n_files > 1:
outfile.write(rule)
- args = ("TOTAL", total.n_statements, total.n_executed)
+ args = ("TOTAL", total.n_statements, total.n_missing)
if self.branches:
- args += (total.n_branches, total.n_executed_branches)
+ args += (total.n_branches, total.n_missing_branches)
args += (total.pc_covered,)
if self.show_missing:
args += ("",)
diff --git a/coverage/templite.py b/coverage/templite.py
index f61fbdca..c39e061e 100644
--- a/coverage/templite.py
+++ b/coverage/templite.py
@@ -2,46 +2,46 @@
# Coincidentally named the same as http://code.activestate.com/recipes/496702/
-import re
+import re, sys
class Templite(object):
"""A simple template renderer, for a nano-subset of Django syntax.
Supported constructs are extended variable access::
-
+
{{var.modifer.modifier|filter|filter}}
-
+
loops::
-
+
{% for var in list %}...{% endfor %}
-
+
and ifs::
-
+
{% if var %}...{% endif %}
Comments are within curly-hash markers::
-
+
{# This will be ignored #}
Construct a Templite with the template text, then use `render` against a
dictionary context to create a finished string.
-
+
"""
def __init__(self, text, *contexts):
"""Construct a Templite with the given `text`.
-
+
`contexts` are dictionaries of values to use for future renderings.
These are good for filters and global values.
-
+
"""
self.text = text
self.context = {}
for context in contexts:
self.context.update(context)
-
+
# Split the text to form a list of tokens.
toks = re.split(r"(?s)({{.*?}}|{%.*?%}|{#.*?#})", text)
-
+
# Parse the tokens into a nested list of operations. Each item in the
# list is a tuple with an opcode, and arguments. They'll be
# interpreted by TempliteEngine.
@@ -80,47 +80,53 @@ class Templite(object):
ops = ops_stack.pop()
assert ops[-1][0] == words[0][3:]
else:
- raise Exception("Don't understand tag %r" % words)
+ raise SyntaxError("Don't understand tag %r" % words)
else:
ops.append(('lit', tok))
-
+
assert not ops_stack, "Unmatched action tag: %r" % ops_stack[-1][0]
self.ops = ops
def render(self, context=None):
"""Render this template by applying it to `context`.
-
+
`context` is a dictionary of values to use in this rendering.
-
+
"""
# Make the complete context we'll use.
ctx = dict(self.context)
if context:
ctx.update(context)
-
+
# Run it through an engine, and return the result.
engine = _TempliteEngine(ctx)
engine.execute(self.ops)
- return engine.result
+ return "".join(engine.result)
class _TempliteEngine(object):
"""Executes Templite objects to produce strings."""
def __init__(self, context):
self.context = context
- self.result = ""
+ self.result = []
def execute(self, ops):
"""Execute `ops` in the engine.
-
+
Called recursively for the bodies of if's and loops.
-
+
"""
for op, args in ops:
if op == 'lit':
- self.result += args
+ self.result.append(args)
elif op == 'exp':
- self.result += str(self.evaluate(args))
+ try:
+ self.result.append(str(self.evaluate(args)))
+ except:
+ exc_class, exc, _ = sys.exc_info()
+ new_exc = exc_class("Couldn't evaluate {{ %s }}: %s"
+ % (args, exc))
+ raise new_exc
elif op == 'if':
expr, body = args
if self.evaluate(expr):
@@ -132,13 +138,13 @@ class _TempliteEngine(object):
self.context[var] = val
self.execute(body)
else:
- raise Exception("TempliteEngine doesn't grok op %r" % op)
+ raise AssertionError("TempliteEngine doesn't grok op %r" % op)
def evaluate(self, expr):
"""Evaluate an expression.
-
+
`expr` can have pipes and dots to indicate data access and filtering.
-
+
"""
if "|" in expr:
pipes = expr.split("|")
diff --git a/coverage/tracer.c b/coverage/tracer.c
index f05988d3..1d227295 100644
--- a/coverage/tracer.c
+++ b/coverage/tracer.c
@@ -6,8 +6,16 @@
#include "structmember.h"
#include "frameobject.h"
-#undef WHAT_LOG /* Define to log the WHAT params in the trace function. */
-#undef TRACE_LOG /* Define to log our bookkeeping. */
+/* Compile-time debugging helpers */
+#undef WHAT_LOG /* Define to log the WHAT params in the trace function. */
+#undef TRACE_LOG /* Define to log our bookkeeping. */
+#undef COLLECT_STATS /* Collect counters: stats are printed when tracer is stopped. */
+
+#if COLLECT_STATS
+#define STATS(x) x
+#else
+#define STATS(x)
+#endif
/* Py 2.x and 3.x compatibility */
@@ -33,6 +41,10 @@
#endif /* Py3k */
+/* The values returned to indicate ok or error. */
+#define RET_OK 0
+#define RET_ERROR -1
+
/* An entry on the data stack. For each call frame, we need to record the
dictionary to capture data, and the last line number executed in that
frame.
@@ -52,7 +64,7 @@ typedef struct {
PyObject * data;
PyObject * should_trace_cache;
PyObject * arcs;
-
+
/* Has the tracer been started? */
int started;
/* Are we tracing arcs, or just lines? */
@@ -63,7 +75,7 @@ typedef struct {
data for a single source file. The data stack parallels the call stack:
each call pushes the new frame's file data onto the data stack, and each
return pops file data off.
-
+
The file data is a dictionary whose form depends on the tracing options.
If tracing arcs, the keys are line number pairs. If not tracing arcs,
the keys are line numbers. In both cases, the value is irrelevant
@@ -85,7 +97,21 @@ typedef struct {
/* The parent frame for the last exception event, to fix missing returns. */
PyFrameObject * last_exc_back;
-
+ int last_exc_firstlineno;
+
+#if COLLECT_STATS
+ struct {
+ unsigned int calls;
+ unsigned int lines;
+ unsigned int returns;
+ unsigned int exceptions;
+ unsigned int others;
+ unsigned int new_files;
+ unsigned int missed_returns;
+ unsigned int stack_reallocs;
+ unsigned int errors;
+ } stats;
+#endif /* COLLECT_STATS */
} Tracer;
#define STACK_DELTA 100
@@ -93,27 +119,41 @@ typedef struct {
static int
Tracer_init(Tracer *self, PyObject *args, PyObject *kwds)
{
+#if COLLECT_STATS
+ self->stats.calls = 0;
+ self->stats.lines = 0;
+ self->stats.returns = 0;
+ self->stats.exceptions = 0;
+ self->stats.others = 0;
+ self->stats.new_files = 0;
+ self->stats.missed_returns = 0;
+ self->stats.stack_reallocs = 0;
+ self->stats.errors = 0;
+#endif /* COLLECT_STATS */
+
self->should_trace = NULL;
self->data = NULL;
self->should_trace_cache = NULL;
self->arcs = NULL;
-
+
self->started = 0;
self->tracing_arcs = 0;
self->depth = -1;
self->data_stack = PyMem_Malloc(STACK_DELTA*sizeof(DataStackEntry));
if (self->data_stack == NULL) {
- return -1;
+ STATS( self->stats.errors++; )
+ PyErr_NoMemory();
+ return RET_ERROR;
}
self->data_stack_alloc = STACK_DELTA;
- self->cur_file_data = NULL;
+ self->cur_file_data = NULL;
self->last_line = -1;
self->last_exc_back = NULL;
- return 0;
+ return RET_OK;
}
static void
@@ -136,7 +176,7 @@ Tracer_dealloc(Tracer *self)
static const char *
indent(int n)
{
- static const char * spaces =
+ static const char * spaces =
" "
" "
" "
@@ -184,19 +224,21 @@ static const char * what_sym[] = {"CALL", "EXC ", "LINE", "RET "};
static int
Tracer_record_pair(Tracer *self, int l1, int l2)
{
- int ret = 0;
-
+ int ret = RET_OK;
+
PyObject * t = PyTuple_New(2);
if (t != NULL) {
PyTuple_SET_ITEM(t, 0, MyInt_FromLong(l1));
PyTuple_SET_ITEM(t, 1, MyInt_FromLong(l2));
if (PyDict_SetItem(self->cur_file_data, t, Py_None) < 0) {
- ret = -1;
+ STATS( self->stats.errors++; )
+ ret = RET_ERROR;
}
Py_DECREF(t);
}
else {
- ret = -1;
+ STATS( self->stats.errors++; )
+ ret = RET_ERROR;
}
return ret;
}
@@ -207,14 +249,15 @@ Tracer_record_pair(Tracer *self, int l1, int l2)
static int
Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
{
+ int ret = RET_OK;
PyObject * filename = NULL;
PyObject * tracename = NULL;
- #if WHAT_LOG
+ #if WHAT_LOG
if (what <= sizeof(what_sym)/sizeof(const char *)) {
printf("trace: %s @ %s %d\n", what_sym[what], MyText_AS_STRING(frame->f_code->co_filename), frame->f_lineno);
}
- #endif
+ #endif
#if TRACE_LOG
if (strstr(MyText_AS_STRING(frame->f_code->co_filename), start_file) && frame->f_lineno == start_line) {
@@ -231,14 +274,15 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
that frame is gone. Our handling for RETURN doesn't need the
actual frame, but we do log it, so that will look a little off if
you're looking at the detailed log.
-
+
If someday we need to examine the frame when doing RETURN, then
we'll need to keep more of the missed frame's state.
*/
+ STATS( self->stats.missed_returns++; )
if (self->depth >= 0) {
if (self->tracing_arcs && self->cur_file_data) {
- if (Tracer_record_pair(self, self->last_line, -1) < 0) {
- return -1;
+ if (Tracer_record_pair(self, self->last_line, -self->last_exc_firstlineno) < 0) {
+ return RET_ERROR;
}
}
SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn");
@@ -249,19 +293,23 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
}
self->last_exc_back = NULL;
}
-
+
switch (what) {
case PyTrace_CALL: /* 0 */
+ STATS( self->stats.calls++; )
/* Grow the stack. */
self->depth++;
if (self->depth >= self->data_stack_alloc) {
+ STATS( self->stats.stack_reallocs++; )
/* We've outgrown our data_stack array: make it bigger. */
int bigger = self->data_stack_alloc + STACK_DELTA;
DataStackEntry * bigger_data_stack = PyMem_Realloc(self->data_stack, bigger * sizeof(DataStackEntry));
if (bigger_data_stack == NULL) {
+ STATS( self->stats.errors++; )
+ PyErr_NoMemory();
self->depth--;
- return -1;
+ return RET_ERROR;
}
self->data_stack = bigger_data_stack;
self->data_stack_alloc = bigger;
@@ -275,6 +323,7 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
filename = frame->f_code->co_filename;
tracename = PyDict_GetItem(self->should_trace_cache, filename);
if (tracename == NULL) {
+ STATS( self->stats.new_files++; )
/* We've never considered this file before. */
/* Ask should_trace about it. */
PyObject * args = Py_BuildValue("(OO)", filename, frame);
@@ -282,9 +331,13 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
Py_DECREF(args);
if (tracename == NULL) {
/* An error occurred inside should_trace. */
- return -1;
+ STATS( self->stats.errors++; )
+ return RET_ERROR;
+ }
+ if (PyDict_SetItem(self->should_trace_cache, filename, tracename) < 0) {
+ STATS( self->stats.errors++; )
+ return RET_ERROR;
}
- PyDict_SetItem(self->should_trace_cache, filename, tracename);
}
else {
Py_INCREF(tracename);
@@ -295,8 +348,16 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
PyObject * file_data = PyDict_GetItem(self->data, tracename);
if (file_data == NULL) {
file_data = PyDict_New();
- PyDict_SetItem(self->data, tracename, file_data);
+ if (file_data == NULL) {
+ STATS( self->stats.errors++; )
+ return RET_ERROR;
+ }
+ ret = PyDict_SetItem(self->data, tracename, file_data);
Py_DECREF(file_data);
+ if (ret < 0) {
+ STATS( self->stats.errors++; )
+ return RET_ERROR;
+ }
}
self->cur_file_data = file_data;
SHOWLOG(self->depth, frame->f_lineno, filename, "traced");
@@ -305,18 +366,20 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
self->cur_file_data = NULL;
SHOWLOG(self->depth, frame->f_lineno, filename, "skipped");
}
-
+
Py_DECREF(tracename);
self->last_line = -1;
break;
-
+
case PyTrace_RETURN: /* 3 */
+ STATS( self->stats.returns++; )
/* A near-copy of this code is above in the missing-return handler. */
if (self->depth >= 0) {
if (self->tracing_arcs && self->cur_file_data) {
- if (Tracer_record_pair(self, self->last_line, -1) < 0) {
- return -1;
+ int first = frame->f_code->co_firstlineno;
+ if (Tracer_record_pair(self, self->last_line, -first) < 0) {
+ return RET_ERROR;
}
}
@@ -326,8 +389,9 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
self->depth--;
}
break;
-
+
case PyTrace_LINE: /* 2 */
+ STATS( self->stats.lines++; )
if (self->depth >= 0) {
SHOWLOG(self->depth, frame->f_lineno, frame->f_code->co_filename, "line");
if (self->cur_file_data) {
@@ -335,38 +399,54 @@ Tracer_trace(Tracer *self, PyFrameObject *frame, int what, PyObject *arg)
if (self->tracing_arcs) {
/* Tracing arcs: key is (last_line,this_line). */
if (Tracer_record_pair(self, self->last_line, frame->f_lineno) < 0) {
- return -1;
+ return RET_ERROR;
}
}
else {
/* Tracing lines: key is simply this_line. */
- PyDict_SetItem(self->cur_file_data, MyInt_FromLong(frame->f_lineno), Py_None);
+ PyObject * this_line = MyInt_FromLong(frame->f_lineno);
+ if (this_line == NULL) {
+ STATS( self->stats.errors++; )
+ return RET_ERROR;
+ }
+ ret = PyDict_SetItem(self->cur_file_data, this_line, Py_None);
+ Py_DECREF(this_line);
+ if (ret < 0) {
+ STATS( self->stats.errors++; )
+ return RET_ERROR;
+ }
}
}
self->last_line = frame->f_lineno;
}
break;
-
+
case PyTrace_EXCEPTION:
/* Some code (Python 2.3, and pyexpat anywhere) fires an exception event
without a return event. To detect that, we'll keep a copy of the
parent frame for an exception event. If the next event is in that
frame, then we must have returned without a return event. We can
synthesize the missing event then.
-
+
Python itself fixed this problem in 2.4. Pyexpat still has the bug.
I've reported the problem with pyexpat as http://bugs.python.org/issue6359 .
If it gets fixed, this code should still work properly. Maybe some day
the bug will be fixed everywhere coverage.py is supported, and we can
remove this missing-return detection.
-
+
More about this fix: http://nedbatchelder.com/blog/200907/a_nasty_little_bug.html
*/
+ STATS( self->stats.exceptions++; )
self->last_exc_back = frame->f_back;
+ self->last_exc_firstlineno = frame->f_code->co_firstlineno;
+ break;
+
+ default:
+ STATS( self->stats.others++; )
break;
}
- return 0;
+ return RET_OK;
}
static PyObject *
@@ -391,6 +471,28 @@ Tracer_stop(Tracer *self, PyObject *args)
return Py_BuildValue("");
}
+static PyObject *
+Tracer_get_stats(Tracer *self)
+{
+#if COLLECT_STATS
+ return Py_BuildValue(
+ "{sI,sI,sI,sI,sI,sI,sI,sI,si,sI}",
+ "calls", self->stats.calls,
+ "lines", self->stats.lines,
+ "returns", self->stats.returns,
+ "exceptions", self->stats.exceptions,
+ "others", self->stats.others,
+ "new_files", self->stats.new_files,
+ "missed_returns", self->stats.missed_returns,
+ "stack_reallocs", self->stats.stack_reallocs,
+ "stack_alloc", self->data_stack_alloc,
+ "errors", self->stats.errors
+ );
+#else
+ return Py_BuildValue("");
+#endif /* COLLECT_STATS */
+}
+
static PyMemberDef
Tracer_members[] = {
{ "should_trace", T_OBJECT, offsetof(Tracer, should_trace), 0,
@@ -410,12 +512,15 @@ Tracer_members[] = {
static PyMethodDef
Tracer_methods[] = {
- { "start", (PyCFunction) Tracer_start, METH_VARARGS,
+ { "start", (PyCFunction) Tracer_start, METH_VARARGS,
PyDoc_STR("Start the tracer") },
- { "stop", (PyCFunction) Tracer_stop, METH_VARARGS,
+ { "stop", (PyCFunction) Tracer_stop, METH_VARARGS,
PyDoc_STR("Stop the tracer") },
+ { "get_stats", (PyCFunction) Tracer_get_stats, METH_VARARGS,
+ PyDoc_STR("Get statistics about the tracing") },
+
{ NULL }
};
@@ -488,7 +593,7 @@ PyInit_tracer(void)
if (mod == NULL) {
return NULL;
}
-
+
TracerType.tp_new = PyType_GenericNew;
if (PyType_Ready(&TracerType) < 0) {
Py_DECREF(mod);
@@ -497,8 +602,8 @@ PyInit_tracer(void)
Py_INCREF(&TracerType);
PyModule_AddObject(mod, "Tracer", (PyObject *)&TracerType);
-
- return mod;
+
+ return mod;
}
#else
diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py
index 0fe43712..17d9fd5f 100644
--- a/coverage/xmlreport.py
+++ b/coverage/xmlreport.py
@@ -14,23 +14,26 @@ def rate(hit, num):
class XmlReporter(Reporter):
"""A reporter for writing Cobertura-style XML coverage results."""
-
+
def __init__(self, coverage, ignore_errors=False):
super(XmlReporter, self).__init__(coverage, ignore_errors)
-
+
self.packages = None
self.xml_out = None
+ self.arcs = coverage.data.has_arcs()
- def report(self, morfs, omit_prefixes=None, outfile=None):
+ def report(self, morfs, omit_prefixes=None, include_prefixes=None,
+ outfile=None
+ ):
"""Generate a Cobertura-compatible XML report for `morfs`.
-
- `morfs` is a list of modules or filenames. `omit_prefixes` is a list
- of strings, prefixes of modules to omit from the report.
-
+
+ `morfs` is a list of modules or filenames.
+
+ See `coverage.report()` for other arguments.
+
"""
# Initial setup.
outfile = outfile or sys.stdout
- self.find_code_units(morfs, omit_prefixes)
# Create the DOM that will store the data.
impl = xml.dom.minidom.getDOMImplementation()
@@ -39,8 +42,9 @@ class XmlReporter(Reporter):
"http://cobertura.sourceforge.net/xml/coverage-03.dtd"
)
self.xml_out = impl.createDocument(None, "coverage", docType)
- xcoverage = self.xml_out.documentElement
+ # Write header stuff.
+ xcoverage = self.xml_out.documentElement
xcoverage.setAttribute("version", __version__)
xcoverage.setAttribute("timestamp", str(int(time.time()*1000)))
xcoverage.appendChild(self.xml_out.createComment(
@@ -48,13 +52,17 @@ class XmlReporter(Reporter):
))
xpackages = self.xml_out.createElement("packages")
xcoverage.appendChild(xpackages)
- self.packages = {}
- self.report_files(self.xml_file, morfs, omit_prefixes=omit_prefixes)
+ # Call xml_file for each file in the data.
+ self.packages = {}
+ self.report_files(
+ self.xml_file, morfs, omit_prefixes=omit_prefixes,
+ include_prefixes=include_prefixes
+ )
lnum_tot, lhits_tot = 0, 0
bnum_tot, bhits_tot = 0, 0
-
+
# Populate the XML DOM with the package info.
for pkg_name, pkg_data in self.packages.items():
class_elts, lhits, lnum, bhits, bnum = pkg_data
@@ -73,16 +81,16 @@ class XmlReporter(Reporter):
lhits_tot += lhits
bnum_tot += bnum
bhits_tot += bhits
-
+
xcoverage.setAttribute("line-rate", str(rate(lhits_tot, lnum_tot)))
xcoverage.setAttribute("branch-rate", str(rate(bhits_tot, bnum_tot)))
-
+
# Use the DOM to write the output file.
outfile.write(self.xml_out.toprettyxml())
def xml_file(self, cu, analysis):
"""Add to the XML report for a single file."""
-
+
# Create the 'lines' and 'package' XML elements, which
# are populated later. Note that a package == a directory.
dirname, fname = os.path.split(cu.name)
@@ -101,26 +109,36 @@ class XmlReporter(Reporter):
xclass.setAttribute("filename", cu.name + ext)
xclass.setAttribute("complexity", "0.0")
+ branch_lines = analysis.branch_lines()
+
# For each statement, create an XML 'line' element.
for line in analysis.statements:
- l = self.xml_out.createElement("line")
- l.setAttribute("number", str(line))
+ xline = self.xml_out.createElement("line")
+ xline.setAttribute("number", str(line))
- # Q: can we get info about the number of times
- # a statement is executed? If so, that should be
- # recorded here.
- l.setAttribute("hits", str(int(not line in analysis.missing)))
+ # Q: can we get info about the number of times a statement is
+ # executed? If so, that should be recorded here.
+ xline.setAttribute("hits", str(int(not line in analysis.missing)))
- # Q: can we get info about whether this statement
- # is a branch? If so, that data should be
- # used here.
- #l.setAttribute("branch", "false")
- xlines.appendChild(l)
+ if self.arcs:
+ if line in branch_lines:
+ xline.setAttribute("branch", "true")
+ xlines.appendChild(xline)
class_lines = 1.0 * len(analysis.statements)
class_hits = class_lines - len(analysis.missing)
- class_branches = 0.0
- class_branch_hits = 0.0
+
+ if self.arcs:
+ # We assume here that every branch line has 2 exits, which is
+ # usually true. In theory, though, we could have a branch line
+ # with more exits..
+ class_branches = 2.0 * len(branch_lines)
+ missed_branch_targets = analysis.missing_branch_arcs().values()
+ missing_branches = sum([len(b) for b in missed_branch_targets])
+ class_branch_hits = class_branches - missing_branches
+ else:
+ class_branches = 0.0
+ class_branch_hits = 0.0
# Finalize the statistics that are collected in the XML DOM.
line_rate = rate(class_hits, class_lines)