diff options
Diffstat (limited to 'coverage')
-rw-r--r-- | coverage/cmdline.py | 3 | ||||
-rw-r--r-- | coverage/config.py | 8 | ||||
-rw-r--r-- | coverage/control.py | 8 | ||||
-rw-r--r-- | coverage/execfile.py | 18 | ||||
-rw-r--r-- | coverage/html.py | 14 | ||||
-rw-r--r-- | coverage/misc.py | 6 | ||||
-rw-r--r-- | coverage/parser.py | 121 | ||||
-rw-r--r-- | coverage/python.py | 2 | ||||
-rw-r--r-- | coverage/summary.py | 69 |
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: |