diff options
37 files changed, 501 insertions, 164 deletions
diff --git a/.travis.yml b/.travis.yml index 1b86b343..4f9f0187 100644 --- a/.travis.yml +++ b/.travis.yml @@ -8,11 +8,20 @@ sudo: false python: - 2.7 +addons: + apt: + sources: + - deadsnakes + packages: + - python3.5 + - python3.5-dev + env: - TOXENV=py26 - TOXENV=py27 - TOXENV=py33 - TOXENV=py34 + - TOXENV=py35 - TOXENV=pypy - TOXENV=py27 COVERAGE_COVERAGE=yes diff --git a/CHANGES.rst b/CHANGES.rst index 91218bba..28e8b24c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -18,11 +18,34 @@ Unreleased data, leaving you with an empty .coverage data file. Fixes issues `issue 525`_, `issue 412`_, `issue 516`_, and probably `issue 511`_. +- Branch coverage fixes: + + - Branch coverage could misunderstand a finally clause on a try block that + never continued on to the following statement, as described in `issue + 493`_. This is now fixed. Thanks to Joe Doherty for the report and Loïc + Dachary for the fix. + + - A while loop with a constant condition (while True) and a continue + statement would be mis-analyzed, as described in `issue 496`_. This is now + fixed, thanks to a bug report by Eli Skeggs and a fix by Loïc Dachary. + + - While loops with constant conditions that were never executed could result + in a non-zero coverage report. Artem Dayneko reported this in `issue + 502`_, and Loïc Dachary provided the fix. + +- The HTML report now supports a ``--skip-covered`` option like the other + reporting commands. Thanks, Loïc Dachary for the implementation, closing + `issue 433`_. + - Options can now be read from a tox.ini file, if any. Like setup.cfg, sections are prefixed with "coverage:", so ``[run]`` options will be read from the ``[coverage:run]`` section of tox.ini. Implements part of `issue 519`_. Thanks, Stephen Finucane. +- Specifying both ``--source`` and ``--include`` no longer silently ignores the + include setting, instead it fails with a message. Thanks, Nathan Land and + Loïc Dachary. Closes `issue 265`_. + - The ``Coverage.combine`` method has a new parameter, ``strict=False``, to support failing if there are no data files to combine. @@ -31,6 +54,12 @@ Unreleased file names had the process id also, making collisions (nearly) impossible. But it was disconcerting. This is now fixed. +- The text report now properly sizes headers when skipping some files, fixing + `issue 524`_. Thanks, Anthony Sottile and Loïc Dachary. + +- Coverage.py can now search .pex files for source, just as it can .zip and + .egg. Thanks, Peter Ebden. + - Data files are now about 15% smaller. - Improvements in the ``[run] debug`` setting: @@ -48,11 +77,20 @@ Unreleased - Fixed an unusual bug involving multiple coding declarations affecting code containing code in multi-line strings: `issue 529`_. +- Coverage.py will no longer be misled into thinking that a plain file is a + package when interpreting ``--source`` options. Thanks, Cosimo Lupo. + +- If you try to run a non-Python file with coverage.py, you will now get a more + useful error message. `Issue 514`_. + - The default pragma regex changed slightly, but this will only matter to you if you are deranged and use mixed-case pragmas. - Deal properly with non-ASCII file names in an ASCII-only world, `issue 533`_. +- Programs that set Unicode configuration values could cause UnicodeErrors when + generating HTML reports. Pytest-cov is one example. This is now fixed. + - Prevented deprecation warnings from configparser that happened in some circumstances, closing `issue 530`_. @@ -63,12 +101,23 @@ Unreleased - Switched to pytest from nose for running the coverage.py tests. +- Renamed AUTHORS.txt to CONTRIBUTORS.txt, since there are other ways to + contribute than by writing code. Also put the count of contributors into the + author string in setup.py, though this might be too cute. + +.. _issue 265: https://bitbucket.org/ned/coveragepy/issues/265/when-using-source-include-is-silently .. _issue 412: https://bitbucket.org/ned/coveragepy/issues/412/coverage-combine-should-error-if-no +.. _issue 433: https://bitbucket.org/ned/coveragepy/issues/433/coverage-html-does-not-suport-skip-covered +.. _issue 493: https://bitbucket.org/ned/coveragepy/issues/493/confusing-branching-failure +.. _issue 496: https://bitbucket.org/ned/coveragepy/issues/496/incorrect-coverage-with-branching-and +.. _issue 502: https://bitbucket.org/ned/coveragepy/issues/502/incorrect-coverage-report-with-cover .. _issue 505: https://bitbucket.org/ned/coveragepy/issues/505/use-canonical-filename-for-debounce +.. _issue 514: https://bitbucket.org/ned/coveragepy/issues/514/path-to-problem-file-not-reported-when .. _issue 510: https://bitbucket.org/ned/coveragepy/issues/510/erase-still-needed-in-42 .. _issue 511: https://bitbucket.org/ned/coveragepy/issues/511/version-42-coverage-combine-empties .. _issue 516: https://bitbucket.org/ned/coveragepy/issues/516/running-coverage-combine-twice-deletes-all .. _issue 519: https://bitbucket.org/ned/coveragepy/issues/519/coverage-run-sections-in-toxini-or-as +.. _issue 524: https://bitbucket.org/ned/coveragepy/issues/524/coverage-report-with-skip-covered-column .. _issue 525: https://bitbucket.org/ned/coveragepy/issues/525/coverage-combine-when-not-in-parallel-mode .. _issue 529: https://bitbucket.org/ned/coveragepy/issues/529/encoding-marker-may-only-appear-on-the .. _issue 530: https://bitbucket.org/ned/coveragepy/issues/530/deprecationwarning-you-passed-a-bytestring diff --git a/AUTHORS.txt b/CONTRIBUTORS.txt index 75a57f79..bb1f7bc0 100644 --- a/AUTHORS.txt +++ b/CONTRIBUTORS.txt @@ -1,13 +1,15 @@ Coverage.py was originally written by Gareth Rees, and since 2004 has been extended and maintained by Ned Batchelder. -Other contributions have been made by: +Other contributions, including writing code, updating docs, and submitting +useful bug reports, have been made by: Adi Roiban Alex Gaynor Alexander Todorov Anthony Sottile Arcadiy Ivanov +Artem Dayneko Ben Finney Bill Hart Brandon Rhodes @@ -17,11 +19,13 @@ Calen Pennington Carl Gieringer Catherine Proulx Chris Adams +Chris Jerdonek Chris Rose Christian Heimes Christine Lytwynec Christoph Zwerschke Conrad Ho +Cosimo Lupo Dan Riti Dan Wandschneider Danek Duvall @@ -44,6 +48,7 @@ Imri Goldberg Ionel Cristian Mărieș JT Olds Jessamyn Smith +Joe Doherty Jon Chappell Joseph Tate Josh Williams @@ -51,6 +56,7 @@ Julian Berman Krystian Kichewko Leonardo Pistone Lex Berezhny +Loïc Dachary Marc Abramowitz Marcus Cobden Mark van der Wal @@ -63,6 +69,7 @@ Nathan Land Noel O'Boyle Pablo Carballo Patrick Mezard +Peter Ebden Peter Portante Rodrigue Cloutier Roger Hu diff --git a/MANIFEST.in b/MANIFEST.in index 31e2230c..462f24ff 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,7 +3,7 @@ # MANIFEST.in file for coverage.py -include AUTHORS.txt +include CONTRIBUTORS.txt include CHANGES.rst include LICENSE.txt include MANIFEST.in @@ -44,6 +44,9 @@ pep8: test: tox -e py27,py35 $(ARGS) +smoke: + COVERAGE_NO_PYTRACER=1 tox -e py27,py35 -- -n 6 -m "not expensive" $(ARGS) + metacov: COVERAGE_COVERAGE=yes tox $(ARGS) @@ -98,7 +101,7 @@ docreqs: pip install -r doc/requirements.pip dochtml: - $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html + PYTHONPATH=$(CURDIR) $(SPHINXBUILD) -b html $(SPHINXOPTS) doc/_build/html @echo @echo "Build finished. The HTML pages are in doc/_build/html." diff --git a/appveyor.yml b/appveyor.yml index 914e2974..7697cba7 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,6 +9,10 @@ environment: CMD_IN_ENV: "cmd /E:ON /V:ON /C .\\ci\\run_with_env.cmd" + # Parallel pytest gets tangled up with tests that try to create and destroy + # .pth files in the shared virtualenv. Disable parallel tests. + PYTEST_ADDOPTS: "-n 0" + matrix: - JOB: "2.6 32-bit" TOXENV: "py26" 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: diff --git a/doc/changes.rst b/doc/changes.rst index c75b2ba8..45f8627c 100644 --- a/doc/changes.rst +++ b/doc/changes.rst @@ -322,7 +322,6 @@ Version 4.0.1 --- 2015-10-13 Version 4.0 --- 2015-09-20 -------------------------- - Backward incompatibilities: - Python versions supported are now: diff --git a/doc/cmd.rst b/doc/cmd.rst index bbeff182..8b97ce37 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -366,6 +366,8 @@ is a data file that is used to speed up reporting the next time. If you generate a new report into the same directory, coverage.py will skip generating unchanged pages, making the process faster. +The ``--skip-covered`` switch will leave out any file with 100% coverage, +letting you focus on the files that still need attention. .. _cmd_annotation: diff --git a/doc/faq.rst b/doc/faq.rst index 6609ab32..c0c6759a 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -123,4 +123,4 @@ Since 2004, `Ned Batchelder`_ has extended and maintained it with the help of .. _Gareth Rees: http://garethrees.org/ .. _Ned Batchelder: http://nedbatchelder.com -.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/AUTHORS.txt +.. _many others: http://bitbucket.org/ned/coveragepy/src/tip/CONTRIBUTORS.txt diff --git a/doc/install.rst b/doc/install.rst index bcea93f1..5774d1b1 100644 --- a/doc/install.rst +++ b/doc/install.rst @@ -24,17 +24,16 @@ Installation .. :history: 20131005T210600, updated for 3.7. .. :history: 20131212T213500, updated for 3.7.1. .. :history: 20140927T102700, updated for 4.0a1. +.. :history: 20161218T173000, remove alternate instructions w/ Distribute .. highlight:: console .. _coverage_pypi: http://pypi.python.org/pypi/coverage .. _setuptools: http://pypi.python.org/pypi/setuptools -.. _Distribute: http://packages.python.org/distribute/ -Installing coverage.py is done in the usual ways. The simplest way is with -pip:: +You can install coverage.py in the usual ways. The simplest way is with pip:: $ pip install coverage @@ -45,18 +44,6 @@ pip:: $ pip install --pre coverage -The alternate old-school technique is: - -#. Install (or already have installed) `setuptools`_ or `Distribute`_. - -#. Download the appropriate kit from the - `coverage.py page on the Python Package Index`__. - -#. Run ``python setup.py install``. - -.. __: coverage_pypi_ - - .. _install_extension: C Extension diff --git a/doc/requirements.pip b/doc/requirements.pip index b1f108dc..e6482bff 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -3,8 +3,8 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ pyenchant==1.6.8 -sphinx==1.4.9 -sphinxcontrib-spelling==2.2.0 +sphinx==1.5.1 +sphinxcontrib-spelling==2.3.0 sphinx_rtd_theme==0.1.9 # A version of doc8 with a -q flag. diff --git a/doc/source.rst b/doc/source.rst index 8f5b31bc..8d831c44 100644 --- a/doc/source.rst +++ b/doc/source.rst @@ -35,9 +35,10 @@ source inside these directories or packages will be measured. Specifying the source option also enables coverage.py to report on unexecuted files, since it can search the source tree for files that haven't been measured at all. Only importable files (ones at the root of the tree, or in directories with a -``__init__.py`` file) will be considered, and files with unusual punctuation in +``__init__.py`` file) will be considered. Files with unusual punctuation in their names will be skipped (they are assumed to be scratch files written by -text editors). +text editors). Files that do not end with ``.py`` or ``.pyo`` or ``.pyc`` +will also be skipped. You can further fine-tune coverage.py's attention with the ``--include`` and ``--omit`` switches (or ``[run] include`` and ``[run] omit`` configuration @@ -28,6 +28,12 @@ warnings.simplefilter("default") # Silence specific warnings that are not our fault. warnings.filterwarnings("ignore", module="xdist", message="type argument to addoption") +warnings.filterwarnings( + # https://github.com/pytest-dev/pytest/issues/2118 + "ignore", + module="_pytest", + message="This usage is deprecated, please use pytest.* instead" +) @contextlib.contextmanager @@ -156,7 +162,7 @@ def run_tests_with_coverage(tracer, *runner_args): sys.modules.update(covmods) # Run tests, with the arguments from our command line. - run_tests(tracer, *runner_args) + status = run_tests(tracer, *runner_args) finally: cov.stop() @@ -165,6 +171,8 @@ def run_tests_with_coverage(tracer, *runner_args): cov.combine() cov.save() + return status + def do_combine_html(): """Combine data from a meta-coverage run, and make the HTML and XML reports.""" diff --git a/requirements/dev.pip b/requirements/dev.pip index b8e0c71a..f127e293 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -8,14 +8,14 @@ -r tox.pip # for linting. -greenlet==0.4.10 +greenlet==0.4.11 mock==2.0.0 -PyContracts==1.7.12 +PyContracts==1.7.15 pyenchant==1.6.8 pylint==1.6.4 pytest unittest-mixins==1.1.1 # for kitting. -requests==2.12.1 +requests==2.12.4 twine==1.8.1 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index 5af33d35..d4ca1e41 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,3 +1,3 @@ # Things needed to make wheels for coverage.py -setuptools==29.0.0 +setuptools==32.3.0 wheel==0.29.0 @@ -46,6 +46,11 @@ with open(cov_ver_py) as version_file: with open("README.rst") as readme: long_description = readme.read().replace("http://coverage.readthedocs.io", __url__) +with open("CONTRIBUTORS.txt") as contributors: + paras = contributors.read().split("\n\n") + num_others = len(paras[-1].splitlines()) + num_others += 1 # Count Gareth Rees, who is mentioned in the top paragraph. + classifier_list = classifiers.splitlines() if version_info[3] == 'alpha': @@ -86,7 +91,7 @@ setup_args = dict( # We need to get HTML assets from our htmlfiles directory. zip_safe=False, - author='Ned Batchelder and others', + author='Ned Batchelder and {0} others'.format(num_others), author_email='ned@nedbatchelder.com', description=doc, long_description=long_description, diff --git a/tests/coveragetest.py b/tests/coveragetest.py index dacb9b63..539dd594 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -240,17 +240,17 @@ class CoverageTest( with self.delayed_assertions(): self.assert_equal_args( analysis.arc_possibilities(), arcs, - "Possible arcs differ", + "Possible arcs differ: minus is actual, plus is expected" ) self.assert_equal_args( analysis.arcs_missing(), arcs_missing, - "Missing arcs differ" + "Missing arcs differ: minus is actual, plus is expected" ) self.assert_equal_args( analysis.arcs_unpredicted(), arcs_unpredicted, - "Unpredicted arcs differ" + "Unpredicted arcs differ: minus is actual, plus is expected" ) if report: @@ -397,9 +397,8 @@ class CoverageTest( # Add our test modules directory to PYTHONPATH. I'm sure there's too # much path munging here, but... - here = os.path.dirname(self.nice_file(coverage.__file__, "..")) - testmods = self.nice_file(here, 'tests/modules') - zipfile = self.nice_file(here, 'tests/zipmods.zip') + testmods = self.nice_file(self.working_root(), 'tests/modules') + zipfile = self.nice_file(self.working_root(), 'tests/zipmods.zip') pypath = os.getenv('PYTHONPATH', '') if pypath: pypath += os.pathsep @@ -410,6 +409,10 @@ class CoverageTest( print(self.last_command_output) return self.last_command_status, self.last_command_output + def working_root(self): + """Where is the root of the coverage.py working tree?""" + return os.path.dirname(self.nice_file(coverage.__file__, "..")) + def report_from_command(self, cmd): """Return the report from the `cmd`, with some convenience added.""" report = self.run_command(cmd).replace('\\', '/') diff --git a/tests/farm/html/src/partial.ini b/tests/farm/html/src/partial.ini new file mode 100644 index 00000000..cdb241b5 --- /dev/null +++ b/tests/farm/html/src/partial.ini @@ -0,0 +1,9 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt + +[run] +branch = True + +[report] +exclude_lines = + raise AssertionError diff --git a/tests/farm/html/src/partial.py b/tests/farm/html/src/partial.py index 66dddacd..0f8fbe3c 100644 --- a/tests/farm/html/src/partial.py +++ b/tests/farm/html/src/partial.py @@ -1,9 +1,9 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://bitbucket.org/ned/coveragepy/src/default/NOTICE.txt -# partial branches +# partial branches and excluded lines -a = 3 +a = 6 while True: break @@ -18,4 +18,7 @@ if 0: never_happen() if 1: - a = 13 + a = 21 + +if a == 23: + raise AssertionError("Can't") diff --git a/tests/test_arcs.py b/tests/test_arcs.py index b03ac533..d9b77dce 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -252,12 +252,12 @@ class LoopArcTest(CoverageTest): """, arcz=".1 12 23 34 45 36 63 57 7.", ) - # With "while True", 2.x thinks it's computation, 3.x thinks it's - # constant. + # With "while True", 2.x thinks it's computation, + # 3.x thinks it's constant. if env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." else: - arcz = ".1 12 23 27 34 45 36 62 57 7." + arcz = ".1 12 23 34 45 36 62 57 7." self.check_coverage("""\ a, i = 1, 0 while True: @@ -270,6 +270,37 @@ class LoopArcTest(CoverageTest): arcz=arcz, ) + def test_zero_coverage_while_loop(self): + # https://bitbucket.org/ned/coveragepy/issue/502 + self.make_file("main.py", "print('done')") + self.make_file("zero.py", """\ + def method(self): + while True: + return 1 + """) + out = self.run_command("coverage run --branch --source=. main.py") + self.assertEqual(out, 'done\n') + report = self.report_from_command("coverage report -m") + squeezed = self.squeezed_lines(report) + self.assertIn("zero.py 3 3 0 0 0% 1-3", squeezed[3]) + + def test_bug_496_continue_in_constant_while(self): + # https://bitbucket.org/ned/coveragepy/issue/496 + if env.PY3: + arcz = ".1 12 23 34 45 53 46 6." + else: + arcz = ".1 12 23 34 45 52 46 6." + self.check_coverage("""\ + up = iter('ta') + while True: + char = next(up) + if char == 't': + continue + break + """, + arcz=arcz + ) + def test_for_if_else_for(self): self.check_coverage("""\ def branches_2(l): @@ -762,16 +793,19 @@ class ExceptionArcTest(CoverageTest): def test_return_finally(self): self.check_coverage("""\ a = [1] - def func(): - try: - return 10 - finally: - a.append(6) - - assert func() == 10 - assert a == [1, 6] - """, - arcz=".1 12 28 89 9. -23 34 46 6-2", + def check_token(data): + if data: + try: + return 5 + finally: + a.append(7) + return 8 + assert check_token(False) == 8 + assert a == [1] + assert check_token(True) == 5 + assert a == [1, 7] + """, + arcz=".1 12 29 9A AB BC C-1 -23 34 45 57 7-2 38 8-2", ) def test_except_jump_finally(self): diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 3b982ebe..45898f1f 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -16,7 +16,7 @@ import coverage.cmdline from coverage import env from coverage.config import CoverageConfig from coverage.data import CoverageData, CoverageDataFiles -from coverage.misc import ExceptionDuringRun +from coverage.misc import CoverageException, ExceptionDuringRun from tests.coveragetest import CoverageTest, OK, ERR @@ -39,7 +39,7 @@ class BaseCmdLineTest(CoverageTest): ) defaults.html_report( directory=None, ignore_errors=None, include=None, omit=None, morfs=[], - title=None, + skip_covered=None, title=None ) defaults.report( ignore_errors=None, include=None, omit=None, morfs=[], @@ -459,6 +459,10 @@ class CmdLineTest(BaseCmdLineTest): .save() """) + def test_bad_run_args_with_both_source_and_include(self): + with self.assertRaisesRegex(CoverageException, 'mutually exclusive'): + self.command_line("run --include=pre1,pre2 --source=lol,wut foo.py", ret=ERR) + def test_bad_concurrency(self): self.command_line("run --concurrency=nothing", ret=ERR) out = self.stdout() diff --git a/tests/test_config.py b/tests/test_config.py index 6cb5e468..2aa592b9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -239,7 +239,6 @@ class ConfigFileTest(CoverageTest): branch = 1 cover_pylib = TRUE parallel = on - include = a/ , b/ concurrency = thread source = myapp plugins = @@ -329,7 +328,6 @@ class ConfigFileTest(CoverageTest): self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"]) self.assertTrue(cov.config.ignore_errors) - self.assertEqual(cov.config.include, ["a/", "b/"]) self.assertEqual(cov.config.omit, ["one", "another", "some_more", "yet_more"]) self.assertEqual(cov.config.precision, 3) diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 889d6cfd..8585b16d 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -145,6 +145,25 @@ class RunPycFileTest(CoverageTest): with self.assertRaisesRegex(NoCode, "No file to run: 'xyzzy.pyc'"): run_python_file("xyzzy.pyc", []) + def test_running_py_from_binary(self): + # Use make_file to get the bookkeeping. Ideally, it would + # be able to write binary files. + bf = self.make_file("binary") + with open(bf, "wb") as f: + f.write(b'\x7fELF\x02\x01\x01\x00\x00\x00') + + msg = ( + r"Couldn't run 'binary' as Python code: " + r"(TypeError|ValueError): " + r"(" + r"compile\(\) expected string without null bytes" # for py2 + r"|" + r"source code string cannot contain null bytes" # for py3 + r")" + ) + with self.assertRaisesRegex(Exception, msg): + run_python_file(bf, [bf]) + class RunModuleTest(CoverageTest): """Test run_python_module.""" diff --git a/tests/test_html.py b/tests/test_html.py index 1df602f2..1c9fa434 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -6,6 +6,7 @@ import datetime import glob +import json import os import os.path import re @@ -208,6 +209,27 @@ class HtmlDeltaTest(HtmlTestHelpers, CoverageTest): fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) self.assertMultiLineEqual(index1, fixed_index2) + def test_status_format_change(self): + self.create_initial_files() + self.run_coverage() + self.remove_html_files() + + with open("htmlcov/status.json") as status_json: + status_data = json.load(status_json) + + self.assertEqual(status_data['format'], 1) + status_data['format'] = 2 + with open("htmlcov/status.json", "w") as status_json: + json.dump(status_data, status_json) + + self.run_coverage() + + # All the files have been reported again. + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/helper1_py.html") + self.assert_exists("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/helper2_py.html") + class HtmlTitleTest(HtmlTestHelpers, CoverageTest): """Tests of the HTML title support.""" @@ -425,6 +447,40 @@ class HtmlTest(HtmlTestHelpers, CoverageTest): self.run_coverage() self.assert_exists("htmlcov/status.dat") + def test_report_skip_covered_no_branches(self): + self.make_file("main_file.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + self.run_coverage(htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + + def test_report_skip_covered_branches(self): + self.make_file("main_file.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + self.run_coverage(covargs=dict(branch=True), htmlargs=dict(skip_covered=True)) + self.assert_exists("htmlcov/index.html") + self.assert_doesnt_exist("htmlcov/main_file_py.html") + self.assert_exists("htmlcov/not_covered_py.html") + class HtmlStaticFileTest(CoverageTest): """Tests of the static file copying for the HTML report.""" @@ -686,7 +742,7 @@ class HtmlGoldTests(CoverageGoldTest): with change_dir("src"): # pylint: disable=import-error - cov = coverage.Coverage(branch=True) + cov = coverage.Coverage(config_file="partial.ini") cov.start() import partial # pragma: nested cov.stop() # pragma: nested @@ -700,6 +756,8 @@ class HtmlGoldTests(CoverageGoldTest): '<p id="t14" class="stm run hide_run">', # The "if 0" and "if 1" statements are optimized away. '<p id="t17" class="pln">', + # The "raise AssertionError" is excluded by regex in the .ini. + '<p id="t24" class="exc">', ) contains( "out/partial/index.html", diff --git a/tests/test_misc.py b/tests/test_misc.py index 38be3456..96b5100b 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -34,6 +34,13 @@ class HasherTest(CoverageTest): h2.update(b"Goodbye!") self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + def test_unicode_hashing(self): + h1 = Hasher() + h1.update(u"Hello, world! \N{SNOWMAN}") + h2 = Hasher() + h2.update(u"Goodbye!") + self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + def test_dict_hashing(self): h1 = Hasher() h1.update({'a': 17, 'b': 23}) diff --git a/tests/test_process.py b/tests/test_process.py index dda43ba8..2e94c19a 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -11,6 +11,8 @@ import re import sys import textwrap +import pytest + import coverage from coverage import env, CoverageData from coverage.misc import output_encoding @@ -692,6 +694,7 @@ class ProcessTest(CoverageTest): self.assertEqual(len(infos), 1) self.assertEqual(infos[0]['note'], u"These are musical notes: ♫𝅗𝅥♩") + @pytest.mark.expensive def test_fullcoverage(self): # pragma: not covered if env.PY2: # This doesn't work on Python 2. self.skipTest("fullcoverage doesn't work on Python 2.") @@ -1139,25 +1142,38 @@ def possible_pth_dirs(): yield distutils.sysconfig.get_python_lib() +def find_writable_pth_directory(): + """Find a place to write a .pth file.""" + for pth_dir in possible_pth_dirs(): # pragma: part covered + try_it = os.path.join(pth_dir, "touch_{0}.it".format(WORKER)) + with open(try_it, "w") as f: + try: + f.write("foo") + except (IOError, OSError): # pragma: not covered + continue + + os.remove(try_it) + return pth_dir + + return None + +WORKER = os.environ.get('PYTEST_XDIST_WORKER', '') +PTH_DIR = find_writable_pth_directory() + + class ProcessCoverageMixin(object): """Set up a .pth file to coverage-measure all sub-processes.""" def setUp(self): super(ProcessCoverageMixin, self).setUp() - # Find a place to put a .pth file. + + # Create the .pth file. + self.assert_(PTH_DIR) pth_contents = "import coverage; coverage.process_startup()\n" - for pth_dir in possible_pth_dirs(): # pragma: part covered - worker = os.environ.get('PYTEST_XDIST_WORKER', '') - pth_path = os.path.join(pth_dir, "subcover_{0}.pth".format(worker)) - with open(pth_path, "w") as pth: - try: - pth.write(pth_contents) - self.pth_path = pth_path - break - except (IOError, OSError): # pragma: not covered - pass - else: # pragma: not covered - raise Exception("Couldn't find a place for the .pth file") + pth_path = os.path.join(PTH_DIR, "subcover_{0}.pth".format(WORKER)) + with open(pth_path, "w") as pth: + pth.write(pth_contents) + self.pth_path = pth_path self.addCleanup(os.remove, self.pth_path) diff --git a/tests/test_summary.py b/tests/test_summary.py index bda65681..5ba00389 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -361,6 +361,27 @@ class SummaryTest(CoverageTest): squeezed = self.squeezed_lines(report) self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + def test_report_skip_covered_longfilename(self): + self.make_file("long_______________filename.py", """ + def foo(): + pass + foo() + """) + out = self.run_command("coverage run --branch long_______________filename.py") + self.assertEqual(out, "") + report = self.report_from_command("coverage report --skip-covered") + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # + # 1 file skipped due to complete coverage. + + self.assertEqual(self.line_count(report), 4, report) + lines = self.report_lines(report) + self.assertEqual(lines[0], "Name Stmts Miss Branch BrPart Cover") + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") @@ -381,18 +402,18 @@ class SummaryTest(CoverageTest): self.make_file("mycode.py", "This isn't python at all!") report = self.report_from_command("coverage report mycode.py") + # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- - # mycode NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # No data to report. - last = self.squeezed_lines(report)[-2] + errmsg = self.squeezed_lines(report)[0] # The actual file name varies run to run. - last = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", last) + errmsg = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", errmsg) # The actual error message varies version to version - last = re.sub(r": '.*' at", ": 'error' at", last) + errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) self.assertEqual( - last, + errmsg, "mycode.py NotPython: Couldn't parse 'mycode.py' as Python source: 'error' at line 1" ) @@ -405,16 +426,16 @@ class SummaryTest(CoverageTest): self.make_file(u"accented\xe2.py", "This isn't python at all!") report = self.report_from_command(u"coverage report accented\xe2.py") + # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # Name Stmts Miss Cover # ---------------------------- - # xxxx NotPython: Couldn't parse '...' as Python source: 'invalid syntax' at line 1 # No data to report. - last = self.squeezed_lines(report)[-2] + errmsg = self.squeezed_lines(report)[0] # The actual file name varies run to run. - last = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", last) + errmsg = re.sub(r"parse '.*(accented.*?\.py)", r"parse '\1", errmsg) # The actual error message varies version to version - last = re.sub(r": '.*' at", ": 'error' at", last) + errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) expected = ( u"accented\xe2.py NotPython: " u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1" @@ -422,7 +443,7 @@ class SummaryTest(CoverageTest): if env.PY2: # pylint: disable=redefined-variable-type expected = expected.encode(output_encoding()) - self.assertEqual(last, expected) + self.assertEqual(errmsg, expected) def test_dotpy_not_python_ignored(self): # We run a .py file, and when reporting, we can't parse it as Python, @@ -10,18 +10,18 @@ usedevelop = True deps = # https://requires.io/github/nedbat/coveragepy/requirements/ - git+https://github.com/nedbat/pytest.git@bug2038#egg=pytest==0.0 + pytest>=3.0.5 pytest-xdist==1.15.0 pytest-warnings==0.2.0 pip==9.0.1 mock==2.0.0 - PyContracts==1.7.12 + PyContracts==1.7.15 unittest-mixins==1.1.1 #-egit+/Users/ned/unittest_mixins#egg=unittest-mixins==0.0 py26: unittest2==1.1.0 py{27,33,34,35,36}: gevent==1.1.2 - py{26,27,33,34,35,36}: eventlet==0.19.0 - py{26,27,33,34,35,36}: greenlet==0.4.10 + py{26,27,33,34,35,36}: eventlet==0.20.0 + py{26,27,33,34,35,36}: greenlet==0.4.11 # Windows can't update the pip version with pip running, so use Python # to install things. @@ -66,7 +66,7 @@ basepython = pypy3 # return. deps = -rdoc/requirements.pip commands = - doc8 -q doc CHANGES.rst README.rst + doc8 -q --ignore-path doc/_build doc CHANGES.rst README.rst sphinx-build -b html -b linkcheck -aEnq doc doc/_build/html sphinx-build -b html -b linkcheck -aEnQW doc doc/_build/html rst2html.py --strict CHANGES.rst doc/_build/trash |