summaryrefslogtreecommitdiff
path: root/coverage
diff options
context:
space:
mode:
Diffstat (limited to 'coverage')
-rw-r--r--coverage/cmdline.py3
-rw-r--r--coverage/config.py8
-rw-r--r--coverage/control.py8
-rw-r--r--coverage/execfile.py18
-rw-r--r--coverage/html.py14
-rw-r--r--coverage/misc.py6
-rw-r--r--coverage/parser.py121
-rw-r--r--coverage/python.py2
-rw-r--r--coverage/summary.py69
9 files changed, 170 insertions, 79 deletions
diff --git a/coverage/cmdline.py b/coverage/cmdline.py
index 89420241..0b121779 100644
--- a/coverage/cmdline.py
+++ b/coverage/cmdline.py
@@ -320,6 +320,7 @@ CMDS = {
Opts.include,
Opts.omit,
Opts.title,
+ Opts.skip_covered,
] + GLOBAL_ARGS,
usage="[options] [modules]",
description=(
@@ -510,7 +511,7 @@ class CoverageScript(object):
elif options.action == "html":
total = self.coverage.html_report(
directory=options.directory, title=options.title,
- **report_args)
+ skip_covered=options.skip_covered, **report_args)
elif options.action == "xml":
outfile = options.outfile
total = self.coverage.xml_report(outfile=outfile, **report_args)
diff --git a/coverage/config.py b/coverage/config.py
index ad3efa91..287844b8 100644
--- a/coverage/config.py
+++ b/coverage/config.py
@@ -368,7 +368,6 @@ class CoverageConfig(object):
Returns the value of the option.
"""
-
# Check all the hard-coded options.
for option_spec in self.CONFIG_FILE_OPTIONS:
attr, where = option_spec[:2]
@@ -383,6 +382,11 @@ class CoverageConfig(object):
# If we get here, we didn't find the option.
raise CoverageException("No such option: %r" % option_name)
+ def sanity_check(self):
+ """Check interactions among settings, and raise if there's a problem."""
+ if (self.source is not None) and (self.include is not None):
+ raise CoverageException("--include and --source are mutually exclusive")
+
def read_coverage_config(config_file, **kwargs):
"""Read the coverage.py configuration.
@@ -439,4 +443,6 @@ def read_coverage_config(config_file, **kwargs):
# 4) from constructor arguments:
config.from_args(**kwargs)
+ config.sanity_check()
+
return config_file, config
diff --git a/coverage/control.py b/coverage/control.py
index 982b1070..992ca585 100644
--- a/coverage/control.py
+++ b/coverage/control.py
@@ -219,7 +219,7 @@ class Coverage(object):
self.source = []
self.source_pkgs = []
for src in self.config.source or []:
- if os.path.exists(src):
+ if os.path.isdir(src):
self.source.append(files.canonical_filename(src))
else:
self.source_pkgs.append(src)
@@ -961,6 +961,8 @@ class Coverage(object):
included in the report. Files matching `omit` will not be included in
the report.
+ If `skip_covered` is True, don't report on files with 100% coverage.
+
Returns a float, the total percentage covered.
"""
@@ -994,7 +996,8 @@ class Coverage(object):
reporter.report(morfs, directory=directory)
def html_report(self, morfs=None, directory=None, ignore_errors=None,
- omit=None, include=None, extra_css=None, title=None):
+ omit=None, include=None, extra_css=None, title=None,
+ skip_covered=None):
"""Generate an HTML report.
The HTML is written to `directory`. The file "index.html" is the
@@ -1016,6 +1019,7 @@ class Coverage(object):
self.config.from_args(
ignore_errors=ignore_errors, omit=omit, include=include,
html_dir=directory, extra_css=extra_css, html_title=title,
+ skip_covered=skip_covered,
)
reporter = HtmlReporter(self, self.config)
return reporter.report(morfs)
diff --git a/coverage/execfile.py b/coverage/execfile.py
index 5633c59e..f598c60b 100644
--- a/coverage/execfile.py
+++ b/coverage/execfile.py
@@ -10,7 +10,7 @@ import types
from coverage.backward import BUILTINS
from coverage.backward import PYC_MAGIC_NUMBER, imp, importlib_util_find_spec
-from coverage.misc import ExceptionDuringRun, NoCode, NoSource, isolate_module
+from coverage.misc import CoverageException, ExceptionDuringRun, NoCode, NoSource, isolate_module
from coverage.phystokens import compile_unicode
from coverage.python import get_python_source
@@ -166,11 +166,17 @@ def run_python_file(filename, args, package=None, modulename=None, path0=None):
sys.path[0] = path0 if path0 is not None else my_path0
try:
- # Make a code object somehow.
- if filename.endswith((".pyc", ".pyo")):
- code = make_code_from_pyc(filename)
- else:
- code = make_code_from_py(filename)
+ try:
+ # Make a code object somehow.
+ if filename.endswith((".pyc", ".pyo")):
+ code = make_code_from_pyc(filename)
+ else:
+ code = make_code_from_py(filename)
+ except CoverageException:
+ raise
+ except Exception as exc:
+ msg = "Couldn't run {filename!r} as Python code: {exc.__class__.__name__}: {exc}"
+ raise CoverageException(msg.format(filename=filename, exc=exc))
# Execute the code object.
try:
diff --git a/coverage/html.py b/coverage/html.py
index f04339de..22783ef7 100644
--- a/coverage/html.py
+++ b/coverage/html.py
@@ -171,6 +171,15 @@ class HtmlReporter(Reporter):
def html_file(self, fr, analysis):
"""Generate an HTML file for one source file."""
+ # Get the numbers for this file.
+ nums = analysis.numbers
+ if self.config.skip_covered:
+ # Don't report on 100% files.
+ no_missing_lines = (nums.n_missing == 0)
+ no_missing_branches = (nums.n_partial_branches == 0)
+ if no_missing_lines and no_missing_branches:
+ return
+
source = fr.source()
# Find out if the file on disk is already correct.
@@ -184,9 +193,6 @@ class HtmlReporter(Reporter):
self.status.set_file_hash(rootname, this_hash)
- # Get the numbers for this file.
- nums = analysis.numbers
-
if self.has_arcs:
missing_branch_arcs = analysis.missing_branch_arcs()
arcs_executed = analysis.arcs_executed()
@@ -384,7 +390,7 @@ class HtmlStatus(object):
'files': files,
}
with open(status_file, "w") as fout:
- json.dump(status, fout)
+ json.dump(status, fout, separators=(',', ':'))
# Older versions of ShiningPanda look for the old name, status.dat.
# Accommodate them if we are running under Jenkins.
diff --git a/coverage/misc.py b/coverage/misc.py
index 9b1894f3..5d330c6d 100644
--- a/coverage/misc.py
+++ b/coverage/misc.py
@@ -12,7 +12,7 @@ import sys
import types
from coverage import env
-from coverage.backward import string_class, to_bytes, unicode_class
+from coverage.backward import to_bytes, unicode_class
ISOLATED_MODULES = {}
@@ -179,8 +179,8 @@ class Hasher(object):
def update(self, v):
"""Add `v` to the hash, recursively if needed."""
self.md5.update(to_bytes(str(type(v))))
- if isinstance(v, string_class):
- self.md5.update(to_bytes(v))
+ if isinstance(v, unicode_class):
+ self.md5.update(v.encode('utf8'))
elif isinstance(v, bytes):
self.md5.update(v)
elif v is None:
diff --git a/coverage/parser.py b/coverage/parser.py
index 71334b63..540ad098 100644
--- a/coverage/parser.py
+++ b/coverage/parser.py
@@ -433,23 +433,35 @@ class ByteParser(object):
class LoopBlock(object):
"""A block on the block stack representing a `for` or `while` loop."""
+ @contract(start=int)
def __init__(self, start):
+ # The line number where the loop starts.
self.start = start
+ # A set of ArcStarts, the arcs from break statements exiting this loop.
self.break_exits = set()
class FunctionBlock(object):
"""A block on the block stack representing a function definition."""
+ @contract(start=int, name=str)
def __init__(self, start, name):
+ # The line number where the function starts.
self.start = start
+ # The name of the function.
self.name = name
class TryBlock(object):
"""A block on the block stack representing a `try` block."""
- def __init__(self, handler_start=None, final_start=None):
+ @contract(handler_start='int|None', final_start='int|None')
+ def __init__(self, handler_start, final_start):
+ # The line number of the first "except" handler, if any.
self.handler_start = handler_start
+ # The line number of the "finally:" clause, if any.
self.final_start = final_start
+
+ # The ArcStarts for breaks/continues/returns/raises inside the "try:"
+ # that need to route through the "finally:" clause.
self.break_from = set()
self.continue_from = set()
self.return_from = set()
@@ -459,8 +471,13 @@ class TryBlock(object):
class ArcStart(collections.namedtuple("Arc", "lineno, cause")):
"""The information needed to start an arc.
- `lineno` is the line number the arc starts from. `cause` is a fragment
- used as the startmsg for AstArcAnalyzer.missing_arc_fragments.
+ `lineno` is the line number the arc starts from.
+
+ `cause` is an English text fragment used as the `startmsg` for
+ AstArcAnalyzer.missing_arc_fragments. It will be used to describe why an
+ arc wasn't executed, so should fit well into a sentence of the form,
+ "Line 17 didn't run because {cause}." The fragment can include "{lineno}"
+ to have `lineno` interpolated into it.
"""
def __new__(cls, lineno, cause=None):
@@ -493,7 +510,9 @@ class AstArcAnalyzer(object):
self.arcs = set()
- # A map from arc pairs to a pair of sentence fragments: (startmsg, endmsg).
+ # A map from arc pairs to a list of pairs of sentence fragments:
+ # { (start, end): [(startmsg, endmsg), ...], }
+ #
# For an arc from line 17, they should be usable like:
# "Line 17 {endmsg}, because {startmsg}"
self.missing_arc_fragments = collections.defaultdict(list)
@@ -570,6 +589,7 @@ class AstArcAnalyzer(object):
# Modules have no line number, they always start at 1.
return 1
+ # The node types that just flow to the next node with no complications.
OK_TO_DEFAULT = set([
"Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global",
"Import", "ImportFrom", "Nonlocal", "Pass", "Print",
@@ -586,12 +606,15 @@ class AstArcAnalyzer(object):
handler = getattr(self, "_handle__" + node_name, None)
if handler is not None:
return handler(node)
-
- if 0:
- node_name = node.__class__.__name__
- if node_name not in self.OK_TO_DEFAULT:
+ else:
+ # No handler: either it's something that's ok to default (a simple
+ # statement), or it's something we overlooked. Change this 0 to 1
+ # to see if it's overlooked.
+ if 0 and node_name not in self.OK_TO_DEFAULT:
print("*** Unhandled: {0}".format(node))
- return set([ArcStart(self.line_for_node(node), cause=None)])
+
+ # Default for simple statements: one exit from this node.
+ return set([ArcStart(self.line_for_node(node))])
@contract(returns='ArcStarts')
def add_body_arcs(self, body, from_start=None, prev_starts=None):
@@ -621,11 +644,11 @@ class AstArcAnalyzer(object):
"""Is this a compile-time constant?"""
node_name = node.__class__.__name__
if node_name in ["NameConstant", "Num"]:
- return True
+ return "Num"
elif node_name == "Name":
- if env.PY3 and node.id in ["True", "False", "None"]:
- return True
- return False
+ if node.id in ["True", "False", "None"]:
+ return "Name"
+ return None
# In the fullness of time, these might be good tests to write:
# while EXPR:
@@ -634,6 +657,15 @@ class AstArcAnalyzer(object):
# listcomps hidden in lists: x = [[i for i in range(10)]]
# nested function definitions
+
+ # Exit processing: process_*_exits
+ #
+ # These functions process the four kinds of jump exits: break, continue,
+ # raise, and return. To figure out where an exit goes, we have to look at
+ # the block stack context. For example, a break will jump to the nearest
+ # enclosing loop block, or the nearest enclosing finally block, whichever
+ # is nearer.
+
@contract(exits='ArcStarts')
def process_break_exits(self, exits):
"""Add arcs due to jumps from `exits` being breaks."""
@@ -692,7 +724,12 @@ class AstArcAnalyzer(object):
)
break
- ## Handlers
+
+ # Handlers: _handle__*
+ #
+ # Each handler deals with a specific AST node type, dispatched from
+ # add_arcs. These functions mirror the Python semantics of each syntactic
+ # construct.
@contract(returns='ArcStarts')
def _handle__Break(self, node):
@@ -722,7 +759,7 @@ class AstArcAnalyzer(object):
self.add_arc(last, lineno)
last = lineno
# The body is handled in collect_arcs.
- return set([ArcStart(last, cause=None)])
+ return set([ArcStart(last)])
_handle__ClassDef = _handle_decorated
@@ -749,7 +786,7 @@ class AstArcAnalyzer(object):
else_exits = self.add_body_arcs(node.orelse, from_start=from_start)
exits |= else_exits
else:
- # no else clause: exit from the for line.
+ # No else clause: exit from the for line.
exits.add(from_start)
return exits
@@ -795,11 +832,11 @@ class AstArcAnalyzer(object):
else:
final_start = None
- try_block = TryBlock(handler_start=handler_start, final_start=final_start)
+ try_block = TryBlock(handler_start, final_start)
self.block_stack.append(try_block)
start = self.line_for_node(node)
- exits = self.add_body_arcs(node.body, from_start=ArcStart(start, cause=None))
+ exits = self.add_body_arcs(node.body, from_start=ArcStart(start))
# We're done with the `try` body, so this block no longer handles
# exceptions. We keep the block so the `finally` clause can pick up
@@ -842,30 +879,46 @@ class AstArcAnalyzer(object):
try_block.return_from # or a `return`.
)
- exits = self.add_body_arcs(node.finalbody, prev_starts=final_from)
+ final_exits = self.add_body_arcs(node.finalbody, prev_starts=final_from)
+
if try_block.break_from:
- break_exits = self._combine_finally_starts(try_block.break_from, exits)
- self.process_break_exits(break_exits)
+ self.process_break_exits(
+ self._combine_finally_starts(try_block.break_from, final_exits)
+ )
if try_block.continue_from:
- continue_exits = self._combine_finally_starts(try_block.continue_from, exits)
- self.process_continue_exits(continue_exits)
+ self.process_continue_exits(
+ self._combine_finally_starts(try_block.continue_from, final_exits)
+ )
if try_block.raise_from:
- raise_exits = self._combine_finally_starts(try_block.raise_from, exits)
- self.process_raise_exits(raise_exits)
+ self.process_raise_exits(
+ self._combine_finally_starts(try_block.raise_from, final_exits)
+ )
if try_block.return_from:
- return_exits = self._combine_finally_starts(try_block.return_from, exits)
- self.process_return_exits(return_exits)
+ self.process_return_exits(
+ self._combine_finally_starts(try_block.return_from, final_exits)
+ )
+
+ if exits:
+ # The finally clause's exits are only exits for the try block
+ # as a whole if the try block had some exits to begin with.
+ exits = final_exits
return exits
+ @contract(starts='ArcStarts', exits='ArcStarts', returns='ArcStarts')
def _combine_finally_starts(self, starts, exits):
- """Helper for building the cause of `finally` branches."""
+ """Helper for building the cause of `finally` branches.
+
+ "finally" clauses might not execute their exits, and the causes could
+ be due to a failure to execute any of the exits in the try block. So
+ we use the causes from `starts` as the causes for `exits`.
+ """
causes = []
- for lineno, cause in sorted(starts):
- if cause is not None:
- causes.append(cause.format(lineno=lineno))
+ for start in sorted(starts):
+ if start.cause is not None:
+ causes.append(start.cause.format(lineno=start.lineno))
cause = " or ".join(causes)
- exits = set(ArcStart(ex.lineno, cause) for ex in exits)
+ exits = set(ArcStart(xit.lineno, cause) for xit in exits)
return exits
@contract(returns='ArcStarts')
@@ -897,9 +950,9 @@ class AstArcAnalyzer(object):
def _handle__While(self, node):
constant_test = self.is_constant_expr(node.test)
start = to_top = self.line_for_node(node.test)
- if constant_test:
+ if constant_test and (env.PY3 or constant_test == "Num"):
to_top = self.line_for_node(node.body[0])
- self.block_stack.append(LoopBlock(start=start))
+ self.block_stack.append(LoopBlock(start=to_top))
from_start = ArcStart(start, cause="the condition on line {lineno} was never true")
exits = self.add_body_arcs(node.body, from_start=from_start)
for xit in exits:
diff --git a/coverage/python.py b/coverage/python.py
index 601318c5..c3ca0e1e 100644
--- a/coverage/python.py
+++ b/coverage/python.py
@@ -75,7 +75,7 @@ def get_zip_bytes(filename):
an empty string if the file is empty.
"""
- markers = ['.zip'+os.sep, '.egg'+os.sep]
+ markers = ['.zip'+os.sep, '.egg'+os.sep, '.pex'+os.sep]
for marker in markers:
if marker in filename:
parts = filename.split(marker)
diff --git a/coverage/summary.py b/coverage/summary.py
index b0fa71a0..d94ce8b2 100644
--- a/coverage/summary.py
+++ b/coverage/summary.py
@@ -25,12 +25,50 @@ class SummaryReporter(Reporter):
for native strings (bytes on Python 2, Unicode on Python 3).
"""
- file_reporters = self.find_file_reporters(morfs)
+ if outfile is None:
+ outfile = sys.stdout
+
+ def writeout(line):
+ """Write a line to the output, adding a newline."""
+ if env.PY2:
+ line = line.encode(output_encoding())
+ outfile.write(line.rstrip())
+ outfile.write("\n")
+
+ fr_analysis = []
+ skipped_count = 0
+ total = Numbers()
+
+ fmt_err = u"%s %s: %s"
+
+ for fr in self.find_file_reporters(morfs):
+ try:
+ analysis = self.coverage._analyze(fr)
+ nums = analysis.numbers
+ total += nums
+
+ if self.config.skip_covered:
+ # Don't report on 100% files.
+ no_missing_lines = (nums.n_missing == 0)
+ no_missing_branches = (nums.n_partial_branches == 0)
+ if no_missing_lines and no_missing_branches:
+ skipped_count += 1
+ continue
+ fr_analysis.append((fr, analysis))
+ except Exception:
+ report_it = not self.config.ignore_errors
+ if report_it:
+ typ, msg = sys.exc_info()[:2]
+ # NotPython is only raised by PythonFileReporter, which has a
+ # should_be_python() method.
+ if typ is NotPython and not fr.should_be_python():
+ report_it = False
+ if report_it:
+ writeout(fmt_err % (fr.relative_filename(), typ.__name__, msg))
# Prepare the formatting strings, header, and column sorting.
- max_name = max([len(fr.relative_filename()) for fr in file_reporters] + [5])
+ max_name = max([len(fr.relative_filename()) for (fr, analysis) in fr_analysis] + [5])
fmt_name = u"%%- %ds " % max_name
- fmt_err = u"%s %s: %s"
fmt_skip_covered = u"\n%s file%s skipped due to complete coverage."
header = (fmt_name % "Name") + u" Stmts Miss"
@@ -50,16 +88,6 @@ class SummaryReporter(Reporter):
if self.branches:
column_order.update(dict(branch=3, brpart=4))
- if outfile is None:
- outfile = sys.stdout
-
- def writeout(line):
- """Write a line to the output, adding a newline."""
- if env.PY2:
- line = line.encode(output_encoding())
- outfile.write(line.rstrip())
- outfile.write("\n")
-
# Write the header
writeout(header)
writeout(rule)
@@ -69,22 +97,9 @@ class SummaryReporter(Reporter):
# sortable values.
lines = []
- total = Numbers()
- skipped_count = 0
-
- for fr in file_reporters:
+ for (fr, analysis) in fr_analysis:
try:
- analysis = self.coverage._analyze(fr)
nums = analysis.numbers
- total += nums
-
- if self.config.skip_covered:
- # Don't report on 100% files.
- no_missing_lines = (nums.n_missing == 0)
- no_missing_branches = (nums.n_partial_branches == 0)
- if no_missing_lines and no_missing_branches:
- skipped_count += 1
- continue
args = (fr.relative_filename(), nums.n_statements, nums.n_missing)
if self.branches: