diff --git a/.github/codecov.yml b/.github/codecov.yml index 167a6c11a..dc6cc4cbe 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -12,6 +12,4 @@ coverage: default: informational: true -comment: - layout: diff, files - behavior: new +comment: false diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index ad45b2eef..fbd3d8328 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -16,7 +16,10 @@ defaults: jobs: lint: name: Pylint etc - runs-on: ubuntu-latest + # Because pylint can report different things on different OS's (!) + # (https://github.com/PyCQA/pylint/issues/3489), run this on Mac where local + # pylint gets run. + runs-on: macos-latest steps: - name: "Check out the repo" diff --git a/CHANGES.rst b/CHANGES.rst index a4f13ff05..98f632842 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,41 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- +.. _changes_54: + +Version 5.4 --- 2021-01-24 +-------------------------- + +- The text report produced by ``coverage report`` now always outputs a TOTAL + line, even if only one Python file is reported. This makes regex parsing + of the output easier. Thanks, Judson Neer. This had been requested a number + of times (`issue 1086`_, `issue 922`_, `issue 732`_). + +- The ``skip_covered`` and ``skip_empty`` settings in the configuration file + can now be specified in the ``[html]`` section, so that text reports and HTML + reports can use separate settings. The HTML report will still use the + ``[report]`` settings if there isn't a value in the ``[html]`` section. + Closes `issue 1090`_. + +- Combining files on Windows across drives how works properly, fixing `issue + 577`_. Thanks, `Valentine Lab `_. + +- Fix an obscure warning from deep in the _decimal module, as reported in + `issue 1084`_. + +- Update to support Python 3.10 alphas in progress, including `PEP 626: Precise + line numbers for debugging and other tools `_. + +.. _issue 577: https://github.com/nedbat/coveragepy/issues/577 +.. _issue 732: https://github.com/nedbat/coveragepy/issues/732 +.. _issue 922: https://github.com/nedbat/coveragepy/issues/922 +.. _issue 1084: https://github.com/nedbat/coveragepy/issues/1084 +.. _issue 1086: https://github.com/nedbat/coveragepy/issues/1086 +.. _issue 1090: https://github.com/nedbat/coveragepy/issues/1090 +.. _pr1080: https://github.com/nedbat/coveragepy/pull/1080 +.. _pep626: https://www.python.org/dev/peps/pep-0626/ + + .. _changes_531: Version 5.3.1 --- 2020-12-19 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 3e52e45e9..455c40967 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -79,6 +79,7 @@ Jon Chappell Jon Dufresne Joseph Tate Josh Williams +Judson Neer Julian Berman Julien Voisin Justas Sadzevičius @@ -135,6 +136,7 @@ Ted Wexler Thijs Triemstra Thomas Grainger Titus Brown +Valentine Lab Vince Salvino Ville Skyttä Xie Yanbo diff --git a/MANIFEST.in b/MANIFEST.in index 60da201de..049ee1fd9 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,7 +22,6 @@ include metacov.ini include pylintrc include setup.py include tox.ini -include tox_wheels.ini include .editorconfig include .readthedocs.yml diff --git a/Makefile b/Makefile index ec1a5aa81..ff5d3c999 100644 --- a/Makefile +++ b/Makefile @@ -108,7 +108,7 @@ DOCBIN = .tox/doc/bin SPHINXOPTS = -aE SPHINXBUILD = $(DOCBIN)/sphinx-build $(SPHINXOPTS) SPHINXAUTOBUILD = $(DOCBIN)/sphinx-autobuild -p 9876 --ignore '.git/**' --open-browser -WEBHOME = ~/web/stellated/ +WEBHOME = ~/web/stellated WEBSAMPLE = $(WEBHOME)/files/sample_coverage_html WEBSAMPLEBETA = $(WEBHOME)/files/sample_coverage_html_beta diff --git a/NOTICE.txt b/NOTICE.txt index 2e7671024..37ded535b 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ Copyright 2001 Gareth Rees. All rights reserved. -Copyright 2004-2020 Ned Batchelder. All rights reserved. +Copyright 2004-2021 Ned Batchelder. All rights reserved. Except where noted otherwise, this software is licensed under the Apache License, Version 2.0 (the "License"); you may not use this work except in diff --git a/README.rst b/README.rst index 7708e9c6c..66cd938a8 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ Coverage.py runs on many versions of Python: * CPython 2.7. * CPython 3.5 through 3.10 alpha. -* PyPy2 7.3.1 and PyPy3 7.3.1. +* PyPy2 7.3.3 and PyPy3 7.3.3. Documentation is on `Read the Docs`_. Code repository and issue tracker are on `GitHub`_. diff --git a/coverage/backward.py b/coverage/backward.py index 9d1d78e5b..8af3452b2 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -245,15 +245,17 @@ def import_local_file(modname, modfile=None): """ try: - from importlib.machinery import SourceFileLoader + import importlib.util as importlib_util except ImportError: - SourceFileLoader = None + importlib_util = None if modfile is None: modfile = modname + '.py' - if SourceFileLoader: - # pylint: disable=no-value-for-parameter, deprecated-method - mod = SourceFileLoader(modname, modfile).load_module() + if importlib_util: + spec = importlib_util.spec_from_file_location(modname, modfile) + mod = importlib_util.module_from_spec(spec) + sys.modules[modname] = mod + spec.loader.exec_module(mod) else: for suff in imp.get_suffixes(): # pragma: part covered if suff[0] == '.py': diff --git a/coverage/collector.py b/coverage/collector.py index 9333d66a8..a4f1790dd 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -55,7 +55,7 @@ class Collector(object): _collectors = [] # The concurrency settings we support here. - SUPPORTED_CONCURRENCIES = set(["greenlet", "eventlet", "gevent", "thread"]) + SUPPORTED_CONCURRENCIES = {"greenlet", "eventlet", "gevent", "thread"} def __init__( self, should_trace, check_include, should_start_context, file_mapper, diff --git a/coverage/config.py b/coverage/config.py index 2af4a1cc8..803dcd5d7 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -217,6 +217,8 @@ def __init__(self): # Defaults for [html] self.extra_css = None self.html_dir = "htmlcov" + self.html_skip_covered = None + self.html_skip_empty = None self.html_title = "Coverage report" self.show_contexts = False @@ -384,6 +386,8 @@ def copy(self): # [html] ('extra_css', 'html:extra_css'), ('html_dir', 'html:directory'), + ('html_skip_covered', 'html:skip_covered', 'boolean'), + ('html_skip_empty', 'html:skip_empty', 'boolean'), ('html_title', 'html:title'), ('show_contexts', 'html:show_contexts', 'boolean'), diff --git a/coverage/control.py b/coverage/control.py index 086490730..8d129bcb5 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -955,8 +955,8 @@ def html_report( with override_config(self, ignore_errors=ignore_errors, report_omit=omit, report_include=include, html_dir=directory, extra_css=extra_css, html_title=title, - skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, - skip_empty=skip_empty, precision=precision, + html_skip_covered=skip_covered, show_contexts=show_contexts, report_contexts=contexts, + html_skip_empty=skip_empty, precision=precision, ): reporter = HtmlReporter(self) return reporter.report(morfs) diff --git a/coverage/ctracer/tracer.c b/coverage/ctracer/tracer.c index 045523524..00e4218d8 100644 --- a/coverage/ctracer/tracer.c +++ b/coverage/ctracer/tracer.c @@ -305,7 +305,7 @@ CTracer_check_missing_return(CTracer *self, PyFrameObject *frame) goto error; } } - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "missedreturn"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "missedreturn"); self->pdata_stack->depth--; self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } @@ -529,13 +529,13 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry->file_data = file_data; self->pcur_entry->file_tracer = file_tracer; - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "traced"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), filename, "traced"); } else { Py_XDECREF(self->pcur_entry->file_data); self->pcur_entry->file_data = NULL; self->pcur_entry->file_tracer = Py_None; - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, filename, "skipped"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), filename, "skipped"); } self->pcur_entry->disposition = disposition; @@ -552,7 +552,7 @@ CTracer_handle_call(CTracer *self, PyFrameObject *frame) self->pcur_entry->last_line = -frame->f_code->co_firstlineno; } else { - self->pcur_entry->last_line = frame->f_lineno; + self->pcur_entry->last_line = PyFrame_GetLineNumber(frame); } ok: @@ -633,7 +633,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) STATS( self->stats.lines++; ) if (self->pdata_stack->depth >= 0) { - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "line"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "line"); if (self->pcur_entry->file_data) { int lineno_from = -1; int lineno_to = -1; @@ -655,7 +655,7 @@ CTracer_handle_line(CTracer *self, PyFrameObject *frame) } } else { - lineno_from = lineno_to = frame->f_lineno; + lineno_from = lineno_to = PyFrame_GetLineNumber(frame); } if (lineno_from != -1) { @@ -744,7 +744,7 @@ CTracer_handle_return(CTracer *self, PyFrameObject *frame) } /* Pop the stack. */ - SHOWLOG(self->pdata_stack->depth, frame->f_lineno, frame->f_code->co_filename, "return"); + SHOWLOG(self->pdata_stack->depth, PyFrame_GetLineNumber(frame), frame->f_code->co_filename, "return"); self->pdata_stack->depth--; self->pcur_entry = &self->pdata_stack->stack[self->pdata_stack->depth]; } @@ -807,14 +807,14 @@ CTracer_trace(CTracer *self, PyFrameObject *frame, int what, PyObject *arg_unuse #if WHAT_LOG if (what <= (int)(sizeof(what_sym)/sizeof(const char *))) { ascii = MyText_AS_BYTES(frame->f_code->co_filename); - printf("trace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), frame->f_lineno); + printf("trace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); Py_DECREF(ascii); } #endif #if TRACE_LOG ascii = MyText_AS_BYTES(frame->f_code->co_filename); - if (strstr(MyBytes_AS_STRING(ascii), start_file) && frame->f_lineno == start_line) { + if (strstr(MyBytes_AS_STRING(ascii), start_file) && PyFrame_GetLineNumber(frame) == start_line) { logging = TRUE; } Py_DECREF(ascii); @@ -931,7 +931,7 @@ CTracer_call(CTracer *self, PyObject *args, PyObject *kwds) #if WHAT_LOG ascii = MyText_AS_BYTES(frame->f_code->co_filename); - printf("pytrace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), frame->f_lineno); + printf("pytrace: %s @ %s %d\n", what_sym[what], MyBytes_AS_STRING(ascii), PyFrame_GetLineNumber(frame)); Py_DECREF(ascii); #endif diff --git a/coverage/env.py b/coverage/env.py index 80153ecf1..ea78a5be8 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -11,35 +11,49 @@ WINDOWS = sys.platform == "win32" LINUX = sys.platform.startswith("linux") +# Python implementations. +CPYTHON = (platform.python_implementation() == "CPython") +PYPY = (platform.python_implementation() == "PyPy") +JYTHON = (platform.python_implementation() == "Jython") +IRONPYTHON = (platform.python_implementation() == "IronPython") + # Python versions. We amend version_info with one more value, a zero if an # official version, or 1 if built from source beyond an official version. PYVERSION = sys.version_info + (int(platform.python_version()[-1] == "+"),) PY2 = PYVERSION < (3, 0) PY3 = PYVERSION >= (3, 0) -# Python implementations. -PYPY = (platform.python_implementation() == 'PyPy') if PYPY: PYPYVERSION = sys.pypy_version_info PYPY2 = PYPY and PY2 PYPY3 = PYPY and PY3 -JYTHON = (platform.python_implementation() == 'Jython') -IRONPYTHON = (platform.python_implementation() == 'IronPython') - -# Python behavior +# Python behavior. class PYBEHAVIOR(object): """Flags indicating this Python's behavior.""" + pep626 = CPYTHON and (PYVERSION > (3, 10, 0, 'alpha', 4)) + # Is "if __debug__" optimized away? - optimize_if_debug = (not PYPY) + if PYPY3: + optimize_if_debug = True + elif PYPY2: + optimize_if_debug = False + else: + optimize_if_debug = not pep626 # Is "if not __debug__" optimized away? optimize_if_not_debug = (not PYPY) and (PYVERSION >= (3, 7, 0, 'alpha', 4)) + if pep626: + optimize_if_not_debug = False + if PYPY3: + optimize_if_not_debug = True # Is "if not __debug__" optimized away even better? optimize_if_not_debug2 = (not PYPY) and (PYVERSION >= (3, 8, 0, 'beta', 1)) + if pep626: + optimize_if_not_debug2 = False # Do we have yield-from? yield_from = (PYVERSION >= (3, 3)) @@ -66,13 +80,16 @@ class PYBEHAVIOR(object): # used to be an empty string (meaning the current directory). It changed # to be the actual path to the current directory, so that os.chdir wouldn't # affect the outcome. - actual_syspath0_dash_m = (not PYPY) and (PYVERSION >= (3, 7, 0, 'beta', 3)) + actual_syspath0_dash_m = CPYTHON and (PYVERSION >= (3, 7, 0, 'beta', 3)) + + # 3.7 changed how functions with only docstrings are numbered. + docstring_only_function = (not PYPY) and ((3, 7, 0, 'beta', 5) <= PYVERSION <= (3, 10)) # When a break/continue/return statement in a try block jumps to a finally # block, does the finally block do the break/continue/return (pre-3.8), or # does the finally jump back to the break/continue/return (3.8) to do the # work? - finally_jumps_back = (PYVERSION >= (3, 8)) + finally_jumps_back = ((3, 8) <= PYVERSION < (3, 10)) # When a function is decorated, does the trace function get called for the # @-line and also the def-line (new behavior in 3.8)? Or just the @-line @@ -85,6 +102,20 @@ class PYBEHAVIOR(object): # Python 3.9a1 made sys.argv[0] and other reported files absolute paths. report_absolute_files = (PYVERSION >= (3, 9)) + # Lines after break/continue/return/raise are no longer compiled into the + # bytecode. They used to be marked as missing, now they aren't executable. + omit_after_jump = pep626 + + # PyPy has always omitted statements after return. + omit_after_return = omit_after_jump or PYPY + + # Modules used to have firstlineno equal to the line number of the first + # real line of code. Now they always start at 1. + module_firstline_1 = pep626 + + # Are "if 0:" lines (and similar) kept in the compiled code? + keep_constant_test = pep626 + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/files.py b/coverage/files.py index 5c2ff1ace..59b2bd61d 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -359,17 +359,19 @@ def add(self, pattern, result): match an entire tree, and not just its root. """ + pattern_sep = sep(pattern) + if len(pattern) > 1: pattern = pattern.rstrip(r"\/") # The pattern can't end with a wildcard component. if pattern.endswith("*"): raise CoverageException("Pattern must not end with wildcards.") - pattern_sep = sep(pattern) # The pattern is meant to match a filepath. Let's make it absolute # unless it already is, or is meant to match any prefix. - if not pattern.startswith('*') and not isabs_anywhere(pattern): + if not pattern.startswith('*') and not isabs_anywhere(pattern + + pattern_sep): pattern = abs_file(pattern) if not pattern.endswith(pattern_sep): pattern += pattern_sep diff --git a/coverage/html.py b/coverage/html.py index 247d2ae19..0dfee7ca8 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -84,7 +84,7 @@ def __init__(self, cov): data = self.coverage.get_data() self.has_arcs = data.has_arcs() if self.config.show_contexts: - if data.measured_contexts() == set([""]): + if data.measured_contexts() == {""}: self.coverage._warn("No contexts were measured") data.set_query_contexts(self.config.report_contexts) @@ -173,6 +173,14 @@ def __init__(self, cov): self.coverage = cov self.config = self.coverage.config self.directory = self.config.html_dir + + self.skip_covered = self.config.html_skip_covered + if self.skip_covered is None: + self.skip_covered = self.config.skip_covered + self.skip_empty = self.config.html_skip_empty + if self.skip_empty is None: + self.skip_empty= self.config.skip_empty + title = self.config.html_title if env.PY2: title = title.decode("utf8") @@ -271,7 +279,7 @@ def html_file(self, fr, analysis): nums = analysis.numbers self.all_files_nums.append(nums) - if self.config.skip_covered: + if self.skip_covered: # Don't report on 100% files. no_missing_lines = (nums.n_missing == 0) no_missing_branches = (nums.n_partial_branches == 0) @@ -280,7 +288,7 @@ def html_file(self, fr, analysis): file_be_gone(html_path) return - if self.config.skip_empty: + if self.skip_empty: # Don't report on empty files. if nums.n_statements == 0: file_be_gone(html_path) diff --git a/coverage/misc.py b/coverage/misc.py index 5c4381ab6..96573f7a4 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -77,7 +77,7 @@ def new_contract(*args, **kwargs): def one_of(argnames): """Ensure that only one of the argnames is non-None.""" def _decorator(func): - argnameset = set(name.strip() for name in argnames.split(",")) + argnameset = {name.strip() for name in argnames.split(",")} def _wrapper(*args, **kwargs): vals = [kwargs.get(name) for name in argnameset] assert sum(val is not None for val in vals) == 1 diff --git a/coverage/optional.py b/coverage/optional.py deleted file mode 100644 index 507a1ada7..000000000 --- a/coverage/optional.py +++ /dev/null @@ -1,76 +0,0 @@ -# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 -# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt - -""" -Imports that we need at runtime, but might not be present. - -When importing one of these modules, always do it in the function where you -need the module. Some tests will need to remove the module. If you import -it at the top level of your module, then the test won't be able to simulate -the module being unimportable. - -The import will always succeed, but the value will be None if the module is -unavailable. - -Bad:: - - # MyModule.py - import unsure - - def use_unsure(): - unsure.something() - -Also bad:: - - # MyModule.py - from coverage.optional import unsure - - def use_unsure(): - unsure.something() - -Good:: - - # MyModule.py - - def use_unsure(): - from coverage.optional import unsure - if unsure is None: - raise Exception("Module unsure isn't available!") - - unsure.something() - -""" - -import contextlib - -# This file's purpose is to provide modules to be imported from here. -# pylint: disable=unused-import - -# TOML support is an install-time extra option. -try: - import toml -except ImportError: # pragma: not covered - toml = None - - -@contextlib.contextmanager -def without(modname): - """Hide a module for testing. - - Use this in a test function to make an optional module unavailable during - the test:: - - with coverage.optional.without('toml'): - use_toml_somehow() - - Arguments: - modname (str): the name of a module importable from - `coverage.optional`. - - """ - real_module = globals()[modname] - try: - globals()[modname] = None - yield - finally: - globals()[modname] = real_module diff --git a/coverage/parser.py b/coverage/parser.py index e3e431490..9c7a8d1e4 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -205,6 +205,12 @@ def _raw_parse(self): if not empty: self.raw_statements.update(self.byte_parser._find_statements()) + # The first line of modules can lie and say 1 always, even if the first + # line of code is later. If so, map 1 to the actual first line of the + # module. + if env.PYBEHAVIOR.module_firstline_1 and self._multiline: + self._multiline[1] = min(self.raw_statements) + def first_line(self, line): """Return the first line number of the statement including `line`.""" if line < 0: @@ -220,7 +226,7 @@ def first_lines(self, lines): Returns a set of the first lines. """ - return set(self.first_line(l) for l in lines) + return {self.first_line(l) for l in lines} def translate_lines(self, lines): """Implement `FileReporter.translate_lines`.""" @@ -332,9 +338,7 @@ def missing_arc_description(self, start, end, executed_arcs=None): fragment_pairs = self._missing_arc_fragments.get((start, end), [(None, None)]) msgs = [] - for fragment_pair in fragment_pairs: - smsg, emsg = fragment_pair - + for smsg, emsg in fragment_pairs: if emsg is None: if end < 0: # Hmm, maybe we have a one-line callable, let's check. @@ -389,34 +393,35 @@ def child_parsers(self): """ return (ByteParser(self.text, code=c) for c in code_objects(self.code)) - 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. Produces a sequence: (b0, l0), (b1, l1), ... - - Only byte offsets that correspond to line numbers are included in the - results. + def _line_numbers(self): + """Yield the line numbers possible in this code object. + Uses co_lnotab described in Python/compile.c to find the + line numbers. Produces a sequence: l0, l1, ... """ - # Adapted from dis.py in the standard library. - byte_increments = bytes_to_ints(self.code.co_lnotab[0::2]) - line_increments = bytes_to_ints(self.code.co_lnotab[1::2]) - - last_line_num = None - line_num = self.code.co_firstlineno - byte_num = 0 - for byte_incr, line_incr in zip(byte_increments, line_increments): - if byte_incr: - if line_num != last_line_num: - yield (byte_num, line_num) - last_line_num = line_num - byte_num += byte_incr - if env.PYBEHAVIOR.negative_lnotab and line_incr >= 0x80: - line_incr -= 0x100 - line_num += line_incr - if line_num != last_line_num: - yield (byte_num, line_num) + if hasattr(self.code, "co_lines"): + for _, _, line in self.code.co_lines(): + if line is not None: + yield line + else: + # Adapted from dis.py in the standard library. + byte_increments = bytes_to_ints(self.code.co_lnotab[0::2]) + line_increments = bytes_to_ints(self.code.co_lnotab[1::2]) + + last_line_num = None + line_num = self.code.co_firstlineno + byte_num = 0 + for byte_incr, line_incr in zip(byte_increments, line_increments): + if byte_incr: + if line_num != last_line_num: + yield line_num + last_line_num = line_num + byte_num += byte_incr + if env.PYBEHAVIOR.negative_lnotab and line_incr >= 0x80: + line_incr -= 0x100 + line_num += line_incr + if line_num != last_line_num: + yield line_num def _find_statements(self): """Find the statements in `self.code`. @@ -427,7 +432,7 @@ def _find_statements(self): """ for bp in self.child_parsers(): # Get all of the lineno information from this code. - for _, l in bp._bytes_lines(): + for l in bp._line_numbers(): yield l @@ -520,7 +525,7 @@ class AstArcAnalyzer(object): def __init__(self, text, statements, multiline): self.root_node = ast.parse(neuter_encoding_declaration(text)) # TODO: I think this is happening in too many places. - self.statements = set(multiline.get(l, l) for l in statements) + self.statements = {multiline.get(l, l) for l in statements} self.multiline = multiline if AST_DUMP: # pragma: debugging @@ -619,17 +624,19 @@ def _line__List(self, node): return node.lineno def _line__Module(self, node): - if node.body: + if env.PYBEHAVIOR.module_firstline_1: + return 1 + elif node.body: return self.line_for_node(node.body[0]) else: # Empty 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([ + OK_TO_DEFAULT = { "Assign", "Assert", "AugAssign", "Delete", "Exec", "Expr", "Global", "Import", "ImportFrom", "Nonlocal", "Pass", "Print", - ]) + } @contract(returns='ArcStarts') def add_arcs(self, node): @@ -661,7 +668,7 @@ def add_arcs(self, node): print("*** Unhandled: {}".format(node)) # Default for simple statements: one exit from this node. - return set([ArcStart(self.line_for_node(node))]) + return {ArcStart(self.line_for_node(node))} @one_of("from_start, prev_starts") @contract(returns='ArcStarts') @@ -677,7 +684,7 @@ def add_body_arcs(self, body, from_start=None, prev_starts=None): """ if prev_starts is None: - prev_starts = set([from_start]) + prev_starts = {from_start} for body_node in body: lineno = self.line_for_node(body_node) first_line = self.multiline.get(lineno, lineno) @@ -890,7 +897,7 @@ def _handle_decorated(self, node): self.add_arc(last, lineno) last = lineno # The body is handled in collect_arcs. - return set([ArcStart(last)]) + return {ArcStart(last)} _handle__ClassDef = _handle_decorated @@ -984,7 +991,7 @@ def _handle__Try(self, node): # If there are `except` clauses, then raises in the try body # will already jump to them. Start this set over for raises in # `except` and `else`. - try_block.raise_from = set([]) + try_block.raise_from = set() else: self.block_stack.pop() @@ -1079,7 +1086,7 @@ def _combine_finally_starts(self, starts, exits): if start.cause is not None: causes.append(start.cause.format(lineno=start.lineno)) cause = " or ".join(causes) - exits = set(ArcStart(xit.lineno, cause) for xit in exits) + exits = {ArcStart(xit.lineno, cause) for xit in exits} return exits @contract(returns='ArcStarts') @@ -1109,9 +1116,14 @@ def _handle__TryFinally(self, node): @contract(returns='ArcStarts') def _handle__While(self, node): - constant_test = self.is_constant_expr(node.test) start = to_top = self.line_for_node(node.test) + constant_test = self.is_constant_expr(node.test) + top_is_body0 = False if constant_test and (env.PY3 or constant_test == "Num"): + top_is_body0 = True + if env.PYBEHAVIOR.keep_constant_test: + top_is_body0 = False + if top_is_body0: to_top = self.line_for_node(node.body[0]) self.block_stack.append(LoopBlock(start=to_top)) from_start = ArcStart(start, cause="the condition on line {lineno} was never true") diff --git a/coverage/phystokens.py b/coverage/phystokens.py index b6866e7dd..54378b3bc 100644 --- a/coverage/phystokens.py +++ b/coverage/phystokens.py @@ -87,7 +87,7 @@ def source_token_lines(source): """ - ws_tokens = set([token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL]) + ws_tokens = {token.INDENT, token.DEDENT, token.NEWLINE, tokenize.NL} line = [] col = 0 diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 7d7a519b7..44bfc8d6a 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -107,7 +107,7 @@ def _trace(self, frame, event, arg_unused): if event == 'call': # Should we start a new context? if self.should_start_context and self.context is None: - context_maybe = self.should_start_context(frame) # pylint: disable=not-callable + context_maybe = self.should_start_context(frame) if context_maybe is not None: self.context = context_maybe self.started_context = True @@ -132,15 +132,15 @@ def _trace(self, frame, event, arg_unused): self.cur_file_name = filename disp = self.should_trace_cache.get(filename) if disp is None: - disp = self.should_trace(filename, frame) # pylint: disable=not-callable - self.should_trace_cache[filename] = disp # pylint: disable=unsupported-assignment-operation + disp = self.should_trace(filename, frame) + self.should_trace_cache[filename] = disp self.cur_file_dict = None if disp.trace: tracename = disp.source_filename - if tracename not in self.data: # pylint: disable=unsupported-membership-test - self.data[tracename] = {} # pylint: disable=unsupported-assignment-operation - self.cur_file_dict = self.data[tracename] # pylint: disable=unsubscriptable-object + if tracename not in self.data: + self.data[tracename] = {} + self.cur_file_dict = self.data[tracename] # The call event is really a "start frame" event, and happens for # function calls and re-entering generators. The f_lasti field is # -1 for calls, and a real offset for generators. Use <0 as the @@ -227,7 +227,7 @@ def stop(self): # has changed to None. dont_warn = (env.PYPY and env.PYPYVERSION >= (5, 4) and self.in_atexit and tf is None) if (not dont_warn) and tf != self._trace: # pylint: disable=comparison-with-callable - self.warn( # pylint: disable=not-callable + self.warn( "Trace function changed, measurement is likely wrong: %r" % (tf,), slug="trace-changed", ) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index 7a3b5c795..b28b83b4f 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -784,7 +784,7 @@ def measured_contexts(self): """ self._start_using() with self._connect() as con: - contexts = set(row[0] for row in con.execute("select distinct(context) from context")) + contexts = {row[0] for row in con.execute("select distinct(context) from context")} return contexts def file_tracer(self, filename): @@ -857,7 +857,7 @@ def lines(self, filename): arcs = self.arcs(filename) if arcs is not None: all_lines = itertools.chain.from_iterable(arcs) - return list(set(l for l in all_lines if l > 0)) + return list({l for l in all_lines if l > 0}) with self._connect() as con: file_id = self._file_id(filename) diff --git a/coverage/summary.py b/coverage/summary.py index 986cd2f2d..0c7fa5519 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -120,8 +120,8 @@ def report(self, morfs, outfile=None): for line in lines: self.writeout(line[0]) - # Write a TOTAl line if we had more than one file. - if self.total.n_files > 1: + # Write a TOTAL line if we had at least one file. + if self.total.n_files > 0: self.writeout(rule) args = ("TOTAL", self.total.n_statements, self.total.n_missing) if self.branches: diff --git a/coverage/tomlconfig.py b/coverage/tomlconfig.py index 25542f99e..3ad581571 100644 --- a/coverage/tomlconfig.py +++ b/coverage/tomlconfig.py @@ -11,6 +11,12 @@ from coverage.backward import configparser, path_types from coverage.misc import CoverageException, substitute_variables +# TOML support is an install-time extra option. +try: + import toml +except ImportError: # pragma: not covered + toml = None + class TomlDecodeError(Exception): """An exception class that exists even when toml isn't installed.""" @@ -29,8 +35,6 @@ def __init__(self, our_file): self.data = None def read(self, filenames): - from coverage.optional import toml - # RawConfigParser takes a filename or list of filenames, but we only # ever call this with a single filename. assert isinstance(filenames, path_types) diff --git a/coverage/version.py b/coverage/version.py index f10206db2..8cc58dfb1 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 3, 1, "final", 0) +version_info = (5, 4, 0, "final", 0) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/doc/conf.py b/doc/conf.py index 2fbf6c1e9..b76c0a235 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -59,18 +59,18 @@ # General information about the project. project = u'Coverage.py' -copyright = u'2009\N{EN DASH}2020, Ned Batchelder.' # CHANGEME # pylint: disable=redefined-builtin +copyright = u'2009\N{EN DASH}2021, Ned Batchelder.' # CHANGEME # pylint: disable=redefined-builtin # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = "5.3.1" # CHANGEME +version = "5.4" # CHANGEME # The full version, including alpha/beta/rc tags. -release = "5.3.1" # CHANGEME +release = "5.4" # CHANGEME # The date of release, in "monthname day, year" format. -release_date = "December 19, 2020" # CHANGEME +release_date = "January 24, 2021" # CHANGEME rst_epilog = """ .. |release_date| replace:: {release_date} diff --git a/doc/config.rst b/doc/config.rst index c6cb94dd5..3a8b0784d 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -315,16 +315,6 @@ setting also affects the interpretation of the ``fail_under`` setting. ``show_missing`` (boolean, default False): when running a summary report, show missing lines. See :ref:`cmd_report` for more information. -.. _config_report_skip_covered: - -``skip_covered`` (boolean, default False): Don't include files in the report -that are 100% covered files. See :ref:`cmd_report` for more information. - -.. _config_report_skip_empty: - -``skip_empty`` (boolean, default False): Don't include empty files (those that -have 0 statements) in the report. See :ref:`cmd_report` for more information. - .. _config_report_sort: ``sort`` (string, default "Name"): Sort the text report by the named column. @@ -344,18 +334,30 @@ also apply to HTML output, where appropriate. ``directory`` (string, default "htmlcov"): where to write the HTML report files. +.. _config_html_extra_css: + +``extra_css`` (string): the path to a file of CSS to apply to the HTML report. +The file will be copied into the HTML output directory. Don't name it +"style.css". This CSS is in addition to the CSS normally used, though you can +overwrite as many of the rules as you like. + .. _config_html_show_context: ``show_contexts`` (boolean): should the HTML report include an indication on each line of which contexts executed the line. See :ref:`dynamic_contexts` for details. -.. _config_html_extra_css: +.. _config_html_skip_covered: -``extra_css`` (string): the path to a file of CSS to apply to the HTML report. -The file will be copied into the HTML output directory. Don't name it -"style.css". This CSS is in addition to the CSS normally used, though you can -overwrite as many of the rules as you like. +``skip_covered`` (boolean, defaulted from ``[report] skip_covered``): Don't +include files in the report that are 100% covered files. See :ref:`cmd_report` +for more information. + +.. _config_html_skip_empty: + +``skip_empty`` (boolean, defaulted from ``[report] skip_empty``): Don't include +empty files (those that have 0 statements) in the report. See :ref:`cmd_report` +for more information. .. _config_html_title: diff --git a/doc/index.rst b/doc/index.rst index 0e7eb22ee..6f408b897 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,12 +18,12 @@ supported on: * Python versions 2.7, 3.5, 3.6, 3.7, 3.8, 3.9, and 3.10 alpha. -* PyPy2 7.3.1 and PyPy3 7.3.1. +* PyPy2 7.3.3 and PyPy3 7.3.3. .. ifconfig:: prerelease **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 5.3.1, `described here`_. + apply.** The latest stable version is coverage.py 5.4, `described here`_. .. _described here: http://coverage.readthedocs.io/ diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index a98954823..0bbd44d0a 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -2,13 +2,13 @@ python-coverage =============== -------------------------------------------------- -measure code coverage of Python program execution -------------------------------------------------- +---------------------------- +Measure Python code coverage +---------------------------- :Author: Ned Batchelder :Author: |author| -:Date: 2019-11-11 +:Date: 2021-01-24 :Copyright: Apache 2.0 license, attribution and disclaimer required. :Manual section: 1 :Manual group: Coverage.py diff --git a/doc/requirements.pip b/doc/requirements.pip index 26d03b8f5..eea4c8f99 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -3,11 +3,10 @@ # https://requires.io/github/nedbat/coveragepy/requirements/ doc8==0.8.1 -pyenchant==3.1.1 -sphinx==2.4.3 +pyenchant==3.2.0 +sphinx==3.3.1 sphinx-rst-builder==0.0.3 -# 5.x requires Sphinx 3 -sphinxcontrib-spelling==4.3.0 +sphinxcontrib-spelling==7.1.0 sphinx_rtd_theme==0.5.0 -sphinx-autobuild==0.7.1 -sphinx-tabs==1.2.0 +sphinx-autobuild==2020.9.1 +sphinx-tabs==1.3.0 diff --git a/doc/sample_html/cogapp___init___py.html b/doc/sample_html/cogapp___init___py.html index d4d31a79e..be126eb48 100644 --- a/doc/sample_html/cogapp___init___py.html +++ b/doc/sample_html/cogapp___init___py.html @@ -66,8 +66,8 @@

diff --git a/doc/sample_html/cogapp___main___py.html b/doc/sample_html/cogapp___main___py.html index 97140bde7..52a0236e1 100644 --- a/doc/sample_html/cogapp___main___py.html +++ b/doc/sample_html/cogapp___main___py.html @@ -62,8 +62,8 @@

diff --git a/doc/sample_html/cogapp_backward_py.html b/doc/sample_html/cogapp_backward_py.html index 17149a30a..b79f30a03 100644 --- a/doc/sample_html/cogapp_backward_py.html +++ b/doc/sample_html/cogapp_backward_py.html @@ -99,8 +99,8 @@

diff --git a/doc/sample_html/cogapp_cogapp_py.html b/doc/sample_html/cogapp_cogapp_py.html index 3f0e5a567..6fa600fa2 100644 --- a/doc/sample_html/cogapp_cogapp_py.html +++ b/doc/sample_html/cogapp_cogapp_py.html @@ -865,8 +865,8 @@

diff --git a/doc/sample_html/cogapp_makefiles_py.html b/doc/sample_html/cogapp_makefiles_py.html index df5fd59e5..699bf9879 100644 --- a/doc/sample_html/cogapp_makefiles_py.html +++ b/doc/sample_html/cogapp_makefiles_py.html @@ -103,8 +103,8 @@

diff --git a/doc/sample_html/cogapp_test_cogapp_py.html b/doc/sample_html/cogapp_test_cogapp_py.html index 4894b141d..b7f9c9fdb 100644 --- a/doc/sample_html/cogapp_test_cogapp_py.html +++ b/doc/sample_html/cogapp_test_cogapp_py.html @@ -2535,8 +2535,8 @@

diff --git a/doc/sample_html/cogapp_test_makefiles_py.html b/doc/sample_html/cogapp_test_makefiles_py.html index 26028b2f8..b928d6005 100644 --- a/doc/sample_html/cogapp_test_makefiles_py.html +++ b/doc/sample_html/cogapp_test_makefiles_py.html @@ -179,8 +179,8 @@

diff --git a/doc/sample_html/cogapp_test_whiteutils_py.html b/doc/sample_html/cogapp_test_whiteutils_py.html index 88c631b2e..54c01da27 100644 --- a/doc/sample_html/cogapp_test_whiteutils_py.html +++ b/doc/sample_html/cogapp_test_whiteutils_py.html @@ -158,8 +158,8 @@

diff --git a/doc/sample_html/cogapp_whiteutils_py.html b/doc/sample_html/cogapp_whiteutils_py.html index b7f2d7d50..388fa0afb 100644 --- a/doc/sample_html/cogapp_whiteutils_py.html +++ b/doc/sample_html/cogapp_whiteutils_py.html @@ -130,8 +130,8 @@

diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html index be4b02c74..6ce866387 100644 --- a/doc/sample_html/index.html +++ b/doc/sample_html/index.html @@ -156,8 +156,8 @@

Coverage report: diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json index fe49a4d49..83336a370 100644 --- a/doc/sample_html/status.json +++ b/doc/sample_html/status.json @@ -1 +1 @@ -{"format":2,"version":"5.3.1","globals":"28441ac12ca4ad5182670460eb380fe5","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file +{"format":2,"version":"5.4","globals":"a486ca194f909abc39de9a093fa5a484","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file diff --git a/igor.py b/igor.py index 31d4bacc2..b2dc05cfe 100644 --- a/igor.py +++ b/igor.py @@ -22,6 +22,12 @@ import pytest +# Contants derived the same as in coverage/env.py. We can't import +# that file here, it would be evaluated too early and not get the +# settings we make in this file. + +CPYTHON = (platform.python_implementation() == "CPython") +PYPY = (platform.python_implementation() == "PyPy") @contextlib.contextmanager def ignore_warnings(): @@ -73,7 +79,18 @@ def label_for_tracer(tracer): def should_skip(tracer): """Is there a reason to skip these tests?""" - if tracer == "py": + skipper = "" + + # $set_env.py: COVERAGE_ONE_TRACER - Only run tests for one tracer. + only_one = os.environ.get("COVERAGE_ONE_TRACER") + if only_one: + if CPYTHON: + if tracer == "py": + skipper = "Only one tracer: no Python tracer for CPython" + else: + if tracer == "c": + skipper = "No C tracer for {}".format(platform.python_implementation()) + elif tracer == "py": # $set_env.py: COVERAGE_NO_PYTRACER - Don't run the tests under the Python tracer. skipper = os.environ.get("COVERAGE_NO_PYTRACER") else: @@ -94,7 +111,7 @@ def make_env_id(tracer): """An environment id that will keep all the test runs distinct.""" impl = platform.python_implementation().lower() version = "%s%s" % sys.version_info[:2] - if '__pypy__' in sys.builtin_module_names: + if PYPY: version += "_%s%s" % sys.pypy_version_info[:2] env_id = "%s%s_%s" % (impl, version, tracer) return env_id @@ -108,6 +125,7 @@ def run_tests(tracer, *runner_args): if 'COVERAGE_ENV_ID' in os.environ: os.environ['COVERAGE_ENV_ID'] = make_env_id(tracer) print_banner(label_for_tracer(tracer)) + return pytest.main(list(runner_args)) @@ -325,9 +343,13 @@ def print_banner(label): version = platform.python_version() - if '__pypy__' in sys.builtin_module_names: + if PYPY: version += " (pypy %s)" % ".".join(str(v) for v in sys.pypy_version_info) + rev = platform.python_revision() + if rev: + version += " (rev {})".format(rev) + try: which_python = os.path.relpath(sys.executable) except ValueError: diff --git a/lab/branch_trace.py b/lab/branch_trace.py new file mode 100644 index 000000000..7e8e88f9a --- /dev/null +++ b/lab/branch_trace.py @@ -0,0 +1,17 @@ +import sys + +pairs = set() +last = -1 + +def trace(frame, event, arg): + global last + if event == "line": + this = frame.f_lineno + pairs.add((last, this)) + last = this + return trace + +code = open(sys.argv[1]).read() +sys.settrace(trace) +exec(code) +print(sorted(pairs)) diff --git a/lab/show_pyc.py b/lab/show_pyc.py index 7573c1c31..2e21eb643 100644 --- a/lab/show_pyc.py +++ b/lab/show_pyc.py @@ -1,6 +1,14 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +""" +Dump the contents of a .pyc file. + +The output will only be correct if run with the same version of Python that +produced the .pyc. + +""" + import binascii import dis import marshal @@ -55,21 +63,37 @@ def show_py_text(text, fname=""): ('CO_ITERABLE_COROUTINE', 0x00100), ('CO_ASYNC_GENERATOR', 0x00200), ('CO_GENERATOR_ALLOWED', 0x01000), - ('CO_FUTURE_DIVISION', 0x02000), - ('CO_FUTURE_ABSOLUTE_IMPORT', 0x04000), - ('CO_FUTURE_WITH_STATEMENT', 0x08000), - ('CO_FUTURE_PRINT_FUNCTION', 0x10000), - ('CO_FUTURE_UNICODE_LITERALS', 0x20000), - ('CO_FUTURE_BARRY_AS_BDFL', 0x40000), - ('CO_FUTURE_GENERATOR_STOP', 0x80000), ] +if sys.version_info < (3, 9): + CO_FLAGS += [ + ('CO_FUTURE_DIVISION', 0x02000), + ('CO_FUTURE_ABSOLUTE_IMPORT', 0x04000), + ('CO_FUTURE_WITH_STATEMENT', 0x08000), + ('CO_FUTURE_PRINT_FUNCTION', 0x10000), + ('CO_FUTURE_UNICODE_LITERALS', 0x20000), + ('CO_FUTURE_BARRY_AS_BDFL', 0x40000), + ('CO_FUTURE_GENERATOR_STOP', 0x80000), + ] +else: + CO_FLAGS += [ + ('CO_FUTURE_DIVISION', 0x0020000), + ('CO_FUTURE_ABSOLUTE_IMPORT', 0x0040000), + ('CO_FUTURE_WITH_STATEMENT', 0x0080000), + ('CO_FUTURE_PRINT_FUNCTION', 0x0100000), + ('CO_FUTURE_UNICODE_LITERALS', 0x0200000), + ('CO_FUTURE_BARRY_AS_BDFL', 0x0400000), + ('CO_FUTURE_GENERATOR_STOP', 0x0800000), + ('CO_FUTURE_ANNOTATIONS', 0x1000000), + ] + + def show_code(code, indent='', number=None): label = "" if number is not None: label = "%d: " % number print("%s%scode" % (indent, label)) - indent += ' ' + indent += " " print("%sname %r" % (indent, code.co_name)) print("%sargcount %d" % (indent, code.co_argcount)) print("%snlocals %d" % (indent, code.co_nlocals)) @@ -80,9 +104,9 @@ def show_code(code, indent='', number=None): print("%sconsts" % indent) for i, const in enumerate(code.co_consts): if type(const) == types.CodeType: - show_code(const, indent+' ', number=i) + show_code(const, indent+" ", number=i) else: - print(" %s%d: %r" % (indent, i, const)) + print(" %s%d: %r" % (indent, i, const)) print("%snames %r" % (indent, code.co_names)) print("%svarnames %r" % (indent, code.co_varnames)) print("%sfreevars %r" % (indent, code.co_freevars)) @@ -90,6 +114,14 @@ def show_code(code, indent='', number=None): print("%sfilename %r" % (indent, code.co_filename)) print("%sfirstlineno %d" % (indent, code.co_firstlineno)) show_hex("lnotab", code.co_lnotab, indent=indent) + print(" %s%s" % (indent, ", ".join("%r:%r" % (line, byte) for byte, line in lnotab_interpreted(code)))) + if hasattr(code, "co_linetable"): + show_hex("linetable", code.co_linetable, indent=indent) + if hasattr(code, "co_lines"): + print(" %sco_lines %s" % ( + indent, + ", ".join("%r:%r-%r" % (line, start, end) for start, end, line in code.co_lines()) + )) def show_hex(label, h, indent): h = binascii.hexlify(h) @@ -100,6 +132,34 @@ def show_hex(label, h, indent): for i in range(0, len(h), 60): print("%s %s" % (indent, h[i:i+60].decode('ascii'))) +if sys.version_info >= (3,): + def bytes_to_ints(bytes_value): + return bytes_value +else: + def bytes_to_ints(bytes_value): + for byte in bytes_value: + yield ord(byte) + +def lnotab_interpreted(code): + # Adapted from dis.py in the standard library. + byte_increments = bytes_to_ints(code.co_lnotab[0::2]) + line_increments = bytes_to_ints(code.co_lnotab[1::2]) + + last_line_num = None + line_num = code.co_firstlineno + byte_num = 0 + for byte_incr, line_incr in zip(byte_increments, line_increments): + if byte_incr: + if line_num != last_line_num: + yield (byte_num, line_num) + last_line_num = line_num + byte_num += byte_incr + if sys.version_info >= (3, 6) and line_incr >= 0x80: + line_incr -= 0x100 + line_num += line_incr + if line_num != last_line_num: + yield (byte_num, line_num) + def flag_words(flags, flag_defs): words = [] for word, flag in flag_defs: diff --git a/setup.cfg b/setup.cfg index 16e2bc6cc..7ba8525a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,11 +2,15 @@ addopts = -q -n3 --strict --force-flaky --no-flaky-report -rfe --failed-first markers = expensive: too slow to run during "make smoke" -# How come this warning is suppressed successfully here, but not in conftest.py?? + +# How come these warnings are suppressed successfully here, but not in conftest.py?? filterwarnings = ignore:dns.hash module will be removed:DeprecationWarning ignore:Using or importing the ABCs:DeprecationWarning +# xfail tests that pass should fail the test suite +xfail_strict=true + [pep8] # E265 block comment should start with '# ' # E266 too many leading '#' for block comment diff --git a/setup.py b/setup.py index 86a054ab2..d1bfe6608 100644 --- a/setup.py +++ b/setup.py @@ -14,7 +14,20 @@ from distutils.core import Extension # pylint: disable=wrong-import-order from distutils.command.build_ext import build_ext # pylint: disable=wrong-import-order from distutils import errors # pylint: disable=wrong-import-order - +import distutils.log # pylint: disable=wrong-import-order + +# $set_env.py: COVERAGE_QUIETER - Set to remove some noise from test output. +if bool(int(os.getenv("COVERAGE_QUIETER", "0"))): + # Distutils has its own mini-logging code, and it sets the level too high. + # When I ask for --quiet when running tessts, I don't want to see warnings. + old_set_verbosity = distutils.log.set_verbosity + def better_set_verbosity(v): + """--quiet means no warnings!""" + if v <= 0: + distutils.log.set_threshold(distutils.log.ERROR) + else: + old_set_verbosity(v) + distutils.log.set_verbosity = better_set_verbosity # Get or massage our metadata. We exec coverage/version.py so we can avoid # importing the product code into setup.py. diff --git a/tests/conftest.py b/tests/conftest.py index 82a6b0f2f..10761cddf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -25,21 +25,32 @@ def set_warnings(): warnings.simplefilter("default") warnings.simplefilter("once", DeprecationWarning) - # A warning to suppress: + # Warnings to suppress: + # How come these warnings are successfully suppressed here, but not in setup.cfg?? + # setuptools/py33compat.py:54: DeprecationWarning: The value of convert_charrefs will become # True in 3.5. You are encouraged to set the value explicitly. # unescape = getattr(html, 'unescape', html_parser.HTMLParser().unescape) - # How come this warning is successfully suppressed here, but not in setup.cfg?? warnings.filterwarnings( "ignore", category=DeprecationWarning, - message="The value of convert_charrefs will become True in 3.5.", + message=r"The value of convert_charrefs will become True in 3.5.", ) + warnings.filterwarnings( "ignore", category=DeprecationWarning, - message=".* instead of inspect.getfullargspec", + message=r".* instead of inspect.getfullargspec", ) + + # :681: + # ImportWarning: VendorImporter.exec_module() not found; falling back to load_module() + warnings.filterwarnings( + "ignore", + category=ImportWarning, + message=r".*exec_module\(\) not found; falling back to load_module\(\)", + ) + if env.PYPY3: # pypy3 warns about unclosed files a lot. warnings.filterwarnings("ignore", r".*unclosed file", category=ResourceWarning) diff --git a/tests/coveragetest.py b/tests/coveragetest.py index f4961ed9b..dbadd226b 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -219,6 +219,11 @@ def check_coverage( self.fail("None of the missing choices matched %r" % missing_formatted) if arcs is not None: + # print("Possible arcs:") + # print(" expected:", arcs) + # print(" actual:", analysis.arc_possibilities()) + # print("Executed:") + # print(" actual:", sorted(set(analysis.arcs_executed()))) with self.delayed_assertions(): self.assert_equal_arcs( arcs, analysis.arc_possibilities(), @@ -504,5 +509,5 @@ def command_line(args): def xfail(condition, reason): - """A decorator to mark as test as expected to fail.""" + """A decorator to mark a test as expected to fail.""" return pytest.mark.xfail(condition, reason=reason, strict=True) diff --git a/tests/gold/html/partial_626/index.html b/tests/gold/html/partial_626/index.html new file mode 100644 index 000000000..f1b1465ed --- /dev/null +++ b/tests/gold/html/partial_626/index.html @@ -0,0 +1,93 @@ + + + + + Coverage report + + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ n + s + m + x + b + p + c   change column sorting +

+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Modulestatementsmissingexcludedbranchespartialcoverage
Total9016287%
partial.py9016287%
+

+ No items found using the specified filter. +

+
+ + + diff --git a/tests/gold/html/partial_626/partial_py.html b/tests/gold/html/partial_626/partial_py.html new file mode 100644 index 000000000..adb0aaf0f --- /dev/null +++ b/tests/gold/html/partial_626/partial_py.html @@ -0,0 +1,83 @@ + + + + + + Coverage for partial.py: 87% + + + + + + + + + + +
+ Hide keyboard shortcuts +

Hot-keys on this page

+
+

+ r + m + x + p   toggle line displays +

+

+ j + k   next/prev highlighted chunk +

+

+ 0   (zero) top of page +

+

+ 1   (one) first highlighted chunk +

+
+
+
+

1# partial branches and excluded lines 

+

2a = 2 

+

3 

+

4while "no peephole".upper(): # t4 4 ↛ 7line 4 didn't jump to line 7, because the condition on line 4 was never false

+

5 break 

+

6 

+

7while a: # pragma: no branch 

+

8 break 

+

9 

+

10if 0: 

+

11 never_happen() 

+

12 

+

13if 13: 13 ↛ 16line 13 didn't jump to line 16, because the condition on line 13 was never false

+

14 a = 14 

+

15 

+

16if a == 16: 

+

17 raise ZeroDivisionError("17") 

+
+ + + diff --git a/tests/gold/html/support/coverage_html.js b/tests/gold/html/support/coverage_html.js index 22152333e..6bc9fdf59 100644 --- a/tests/gold/html/support/coverage_html.js +++ b/tests/gold/html/support/coverage_html.js @@ -172,7 +172,10 @@ coverage.index_ready = function ($) { // Look for a localStorage item containing previous sort settings: var sort_list = []; var storage_name = "COVERAGE_INDEX_SORT"; - var stored_list = localStorage.getItem(storage_name); + var stored_list = undefined; + try { + stored_list = localStorage.getItem(storage_name); + } catch(err) {} if (stored_list) { sort_list = JSON.parse('[[' + stored_list + ']]'); @@ -221,8 +224,10 @@ coverage.index_ready = function ($) { coverage.wire_up_filter(); // Watch for page unload events so we can save the final sort settings: - $(window).unload(function () { - localStorage.setItem(storage_name, sort_list.toString()) + $(window).on("unload", function () { + try { + localStorage.setItem(storage_name, sort_list.toString()) + } catch(err) {} }); }; diff --git a/tests/gold/html/support/style.css b/tests/gold/html/support/style.css index e8ff57657..3e7f9b66b 100644 --- a/tests/gold/html/support/style.css +++ b/tests/gold/html/support/style.css @@ -4,11 +4,17 @@ /* Don't edit this .css file. Edit the .scss file instead! */ html, body, h1, h2, h3, p, table, td, th { margin: 0; padding: 0; border: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; } -body { font-family: georgia, serif; font-size: 1em; } +body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; font-size: 1em; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { body { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { body { color: #eee; } } html > body { font-size: 16px; } -p { font-size: .75em; line-height: 1.33333333em; } +a:active, a:focus { outline: 2px dashed #007acc; } + +p { font-size: .875em; line-height: 1.4em; } table { border-collapse: collapse; } @@ -19,106 +25,267 @@ table tr.hidden { display: none !important; } p#no_rows { display: none; font-size: 1.2em; } a.nav { text-decoration: none; color: inherit; } + a.nav:hover { text-decoration: underline; color: inherit; } #header { background: #f8f8f8; width: 100%; border-bottom: 1px solid #eee; } -.indexfile #footer { margin: 1em 3em; } +@media (prefers-color-scheme: dark) { #header { background: black; } } -.pyfile #footer { margin: 1em 1em; } +@media (prefers-color-scheme: dark) { #header { border-color: #333; } } -#footer .content { padding: 0; font-size: 85%; font-family: verdana, sans-serif; color: #666666; font-style: italic; } +.indexfile #footer { margin: 1rem 3rem; } -#index { margin: 1em 0 0 3em; } +.pyfile #footer { margin: 1rem 1rem; } -#header .content { padding: 1em 3rem; } +#footer .content { padding: 0; color: #666; font-style: italic; } + +@media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } + +#index { margin: 1rem 0 0 3rem; } + +#header .content { padding: 1rem 3rem; } h1 { font-size: 1.25em; display: inline-block; } -#filter_container { display: inline-block; float: right; margin: 0 2em 0 0; } -#filter_container input { width: 10em; } +#filter_container { float: right; margin: 0 2em 0 0; } + +#filter_container input { width: 10em; padding: 0.2em 0.5em; border: 2px solid #ccc; background: #fff; color: #000; } + +@media (prefers-color-scheme: dark) { #filter_container input { border-color: #444; } } + +@media (prefers-color-scheme: dark) { #filter_container input { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #filter_container input { color: #eee; } } + +#filter_container input:focus { border-color: #007acc; } h2.stats { margin-top: .5em; font-size: 1em; } -.stats span { border: 1px solid; border-radius: .1em; padding: .1em .5em; margin: 0 .1em; cursor: pointer; border-color: #ccc #999 #999 #ccc; } -.stats span.run { background: #eeffee; } -.stats span.run.show_run { border-color: #999 #ccc #ccc #999; background: #ddffdd; } -.stats span.mis { background: #ffeeee; } -.stats span.mis.show_mis { border-color: #999 #ccc #ccc #999; background: #ffdddd; } -.stats span.exc { background: #f7f7f7; } -.stats span.exc.show_exc { border-color: #999 #ccc #ccc #999; background: #eeeeee; } -.stats span.par { background: #ffffd5; } -.stats span.par.show_par { border-color: #999 #ccc #ccc #999; background: #ffffaa; } +.stats button { font-family: inherit; font-size: inherit; border: 1px solid; border-radius: .2em; color: inherit; padding: .1em .5em; margin: 1px calc(.1em + 1px); cursor: pointer; border-color: #ccc; } + +@media (prefers-color-scheme: dark) { .stats button { border-color: #444; } } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button:active, .stats button:focus { outline: 2px dashed #007acc; } + +.stats button.run { background: #eeffee; } + +@media (prefers-color-scheme: dark) { .stats button.run { background: #373d29; } } + +.stats button.run.show_run { background: #dfd; border: 2px solid #00dd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.run.show_run { background: #373d29; } } + +.stats button.mis { background: #ffeeee; } + +@media (prefers-color-scheme: dark) { .stats button.mis { background: #4b1818; } } + +.stats button.mis.show_mis { background: #fdd; border: 2px solid #ff0000; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.mis.show_mis { background: #4b1818; } } + +.stats button.exc { background: #f7f7f7; } + +@media (prefers-color-scheme: dark) { .stats button.exc { background: #333; } } + +.stats button.exc.show_exc { background: #eee; border: 2px solid #808080; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.exc.show_exc { background: #333; } } + +.stats button.par { background: #ffffd5; } + +@media (prefers-color-scheme: dark) { .stats button.par { background: #650; } } -#source p .annotate.long, .help_panel { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; box-shadow: #cccccc .2em .2em .2em; color: #333; padding: .25em .5em; } +.stats button.par.show_par { background: #ffa; border: 2px solid #dddd00; margin: 0 .1em; } + +@media (prefers-color-scheme: dark) { .stats button.par.show_par { background: #650; } } + +.help_panel, #source p .annotate.long { display: none; position: absolute; z-index: 999; background: #ffffcc; border: 1px solid #888; border-radius: .2em; color: #333; padding: .25em .5em; } #source p .annotate.long { white-space: normal; float: right; top: 1.75em; right: 1em; height: auto; } #keyboard_icon { float: right; margin: 5px; cursor: pointer; } .help_panel { padding: .5em; border: 1px solid #883; } + .help_panel .legend { font-style: italic; margin-bottom: 1em; } -.indexfile .help_panel { width: 20em; height: 4em; } -.pyfile .help_panel { width: 16em; height: 8em; } + +.indexfile .help_panel { width: 20em; min-height: 4em; } + +.pyfile .help_panel { width: 16em; min-height: 8em; } #panel_icon { float: right; cursor: pointer; } .keyhelp { margin: .75em; } -.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: monospace; font-weight: bold; background: #eee; } -#source { padding: 1em 0 1em 3rem; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; } +.keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } + +#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } + #source p { position: relative; white-space: pre; } + #source p * { box-sizing: border-box; } -#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999999; font-family: verdana, sans-serif; } -#source p .n a { text-decoration: none; color: #999999; font-size: .8333em; line-height: 1em; } -#source p .n a:hover { text-decoration: underline; color: #999999; } + +#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n { color: #777; } } + +#source p .n a { text-decoration: none; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a { color: #777; } } + +#source p .n a:hover { text-decoration: underline; color: #999; } + +@media (prefers-color-scheme: dark) { #source p .n a:hover { color: #777; } } + #source p.highlight .n { background: #ffdd00; } -#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid white; } + +#source p .t { display: inline-block; width: 100%; box-sizing: border-box; margin-left: -.5em; padding-left: 0.3em; border-left: 0.2em solid #fff; } + +@media (prefers-color-scheme: dark) { #source p .t { border-color: #1e1e1e; } } + #source p .t:hover { background: #f2f2f2; } + +@media (prefers-color-scheme: dark) { #source p .t:hover { background: #282828; } } + #source p .t:hover ~ .r .annotate.long { display: block; } -#source p .t .com { color: green; font-style: italic; line-height: 1px; } + +#source p .t .com { color: #008000; font-style: italic; line-height: 1px; } + +@media (prefers-color-scheme: dark) { #source p .t .com { color: #6A9955; } } + #source p .t .key { font-weight: bold; line-height: 1px; } -#source p .t .str { color: #000080; } + +#source p .t .str { color: #0451A5; } + +@media (prefers-color-scheme: dark) { #source p .t .str { color: #9CDCFE; } } + #source p.mis .t { border-left: 0.2em solid #ff0000; } -#source p.mis.show_mis .t { background: #ffdddd; } + +#source p.mis.show_mis .t { background: #fdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t { background: #4b1818; } } + #source p.mis.show_mis .t:hover { background: #f2d2d2; } -#source p.run .t { border-left: 0.2em solid #00ff00; } -#source p.run.show_run .t { background: #ddffdd; } + +@media (prefers-color-scheme: dark) { #source p.mis.show_mis .t:hover { background: #532323; } } + +#source p.run .t { border-left: 0.2em solid #00dd00; } + +#source p.run.show_run .t { background: #dfd; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t { background: #373d29; } } + #source p.run.show_run .t:hover { background: #d2f2d2; } + +@media (prefers-color-scheme: dark) { #source p.run.show_run .t:hover { background: #404633; } } + #source p.exc .t { border-left: 0.2em solid #808080; } -#source p.exc.show_exc .t { background: #eeeeee; } + +#source p.exc.show_exc .t { background: #eee; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t { background: #333; } } + #source p.exc.show_exc .t:hover { background: #e2e2e2; } -#source p.par .t { border-left: 0.2em solid #eeee99; } -#source p.par.show_par .t { background: #ffffaa; } + +@media (prefers-color-scheme: dark) { #source p.exc.show_exc .t:hover { background: #3c3c3c; } } + +#source p.par .t { border-left: 0.2em solid #dddd00; } + +#source p.par.show_par .t { background: #ffa; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t { background: #650; } } + #source p.par.show_par .t:hover { background: #f2f2a2; } -#source p .r { position: absolute; top: 0; right: 2.5em; font-family: verdana, sans-serif; } -#source p .annotate { font-family: georgia; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p.par.show_par .t:hover { background: #6d5d0c; } } + +#source p .r { position: absolute; top: 0; right: 2.5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; } + +#source p .annotate { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; color: #666; padding-right: .5em; } + +@media (prefers-color-scheme: dark) { #source p .annotate { color: #ddd; } } + #source p .annotate.short:hover ~ .long { display: block; } + #source p .annotate.long { width: 30em; right: 2.5em; } + #source p input { display: none; } + #source p input ~ .r label.ctx { cursor: pointer; border-radius: .25em; } + #source p input ~ .r label.ctx::before { content: "▶ "; } + #source p input ~ .r label.ctx:hover { background: #d5f7ff; color: #666; } -#source p input:checked ~ .r label.ctx { background: #aaeeff; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { background: #0f3a42; } } + +@media (prefers-color-scheme: dark) { #source p input ~ .r label.ctx:hover { color: #aaa; } } + +#source p input:checked ~ .r label.ctx { background: #aef; color: #666; border-radius: .75em .75em 0 0; padding: 0 .5em; margin: -.25em 0; } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { background: #056; } } + +@media (prefers-color-scheme: dark) { #source p input:checked ~ .r label.ctx { color: #aaa; } } + #source p input:checked ~ .r label.ctx::before { content: "▼ "; } + #source p input:checked ~ .ctxs { padding: .25em .5em; overflow-y: scroll; max-height: 10.5em; } + #source p label.ctx { color: #999; display: inline-block; padding: 0 .5em; font-size: .8333em; } -#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: verdana, sans-serif; white-space: nowrap; background: #aaeeff; border-radius: .25em; margin-right: 1.75em; } + +@media (prefers-color-scheme: dark) { #source p label.ctx { color: #777; } } + +#source p .ctxs { display: block; max-height: 0; overflow-y: hidden; transition: all .2s; padding: 0 .5em; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; white-space: nowrap; background: #aef; border-radius: .25em; margin-right: 1.75em; } + +@media (prefers-color-scheme: dark) { #source p .ctxs { background: #056; } } + #source p .ctxs span { display: block; text-align: right; } +#index { font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-size: 0.875em; } + +#index table.index { margin-left: -.5em; } + #index td, #index th { text-align: right; width: 5em; padding: .25em .5em; border-bottom: 1px solid #eee; } -#index td.left, #index th.left { padding-left: 0; } -#index td.right, #index th.right { padding-right: 0; } + +@media (prefers-color-scheme: dark) { #index td, #index th { border-color: #333; } } + #index td.name, #index th.name { text-align: left; width: auto; } -#index th { font-style: italic; color: #333; border-bottom: 1px solid #ccc; cursor: pointer; } -#index th:hover { background: #eee; border-bottom: 1px solid #999; } -#index th.headerSortDown, #index th.headerSortUp { border-bottom: 1px solid #000; white-space: nowrap; background: #eee; } -#index th.headerSortDown:after { content: " ↓"; } -#index th.headerSortUp:after { content: " ↑"; } -#index td.name a { text-decoration: none; color: #000; } + +#index th { font-style: italic; color: #333; cursor: pointer; } + +@media (prefers-color-scheme: dark) { #index th { color: #ddd; } } + +#index th:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index th:hover { background: #333; } } + +#index th.headerSortDown, #index th.headerSortUp { white-space: nowrap; background: #eee; } + +@media (prefers-color-scheme: dark) { #index th.headerSortDown, #index th.headerSortUp { background: #333; } } + +#index th.headerSortDown:after { content: " ↑"; } + +#index th.headerSortUp:after { content: " ↓"; } + +#index td.name a { text-decoration: none; color: inherit; } + #index tr.total td, #index tr.total_dynamic td { font-weight: bold; border-top: 1px solid #ccc; border-bottom: none; } -#index tr.file:hover { background: #eeeeee; } -#index tr.file:hover td.name { text-decoration: underline; color: #000; } -#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: white; border-left: 1px solid #eee; will-change: transform; } -#scroll_marker .marker { background: #ddd; position: absolute; min-height: 3px; width: 100%; } +#index tr.file:hover { background: #eee; } + +@media (prefers-color-scheme: dark) { #index tr.file:hover { background: #333; } } + +#index tr.file:hover td.name { text-decoration: underline; color: inherit; } + +#scroll_marker { position: fixed; right: 0; top: 0; width: 16px; height: 100%; background: #fff; border-left: 1px solid #eee; will-change: transform; } + +@media (prefers-color-scheme: dark) { #scroll_marker { background: #1e1e1e; } } + +@media (prefers-color-scheme: dark) { #scroll_marker { border-color: #333; } } + +#scroll_marker .marker { background: #ccc; position: absolute; min-height: 3px; width: 100%; } + +@media (prefers-color-scheme: dark) { #scroll_marker .marker { background: #444; } } diff --git a/tests/helpers.py b/tests/helpers.py index 9c6a0ad8e..0621d7a94 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -10,6 +10,7 @@ import subprocess import sys +import mock from unittest_mixins import ModuleCleaner from coverage import env @@ -203,3 +204,21 @@ def arcs_to_arcz_repr(arcs): line += _arcs_to_arcz_repr_one(b) repr_list.append(line) return "\n".join(repr_list) + "\n" + + +def without_module(using_module, missing_module_name): + """ + Hide a module for testing. + + Use this in a test function to make an optional module unavailable during + the test:: + + with without_module(product.something, 'toml'): + use_toml_somehow() + + Arguments: + using_module: a module in which to hide `missing_module_name`. + missing_module_name (str): the name of the module to hide. + + """ + return mock.patch.object(using_module, missing_module_name, None) diff --git a/tests/plugin1.py b/tests/plugin1.py index a070af367..3283fbdae 100644 --- a/tests/plugin1.py +++ b/tests/plugin1.py @@ -44,7 +44,7 @@ def line_number_range(self, frame): class FileReporter(coverage.FileReporter): """Dead-simple FileReporter.""" def lines(self): - return set([105, 106, 107, 205, 206, 207]) + return {105, 106, 107, 205, 206, 207} def coverage_init(reg, options): # pylint: disable=unused-argument diff --git a/tests/test_api.py b/tests/test_api.py index 3552f8f48..f8b7b4b2c 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -306,9 +306,11 @@ def test_completely_zero_reporting(self): # Name Stmts Miss Cover # -------------------------------- # foo/bar.py 1 1 0% + # -------------------------------- + # TOTAL 1 1 0% - last = self.last_line_squeezed(self.stdout()).replace("\\", "/") - self.assertEqual("foo/bar.py 1 1 0%", last) + last = self.last_line_squeezed(self.stdout()) + self.assertEqual("TOTAL 1 1 0%", last) def test_cov4_data_file(self): cov4_data = ( @@ -587,6 +589,8 @@ def test_source_and_include_dont_conflict(self): Name Stmts Miss Cover --------------------------- b.py 1 0 100% + --------------------------- + TOTAL 1 0 100% """) self.assertEqual(expected, self.stdout()) @@ -1049,6 +1053,8 @@ def pretend_to_be_nose_with_cover(self, erase=False, cd=False): Name Stmts Miss Cover Missing -------------------------------------------- no_biggie.py 4 1 75% 4 + -------------------------------------------- + TOTAL 4 1 75% """)) if cd: os.chdir("..") @@ -1092,6 +1098,8 @@ def pretend_to_be_pytestcov(self, append): Name Stmts Miss Cover ----------------------------- prog.py 4 1 75% + ----------------------------- + TOTAL 4 1 75% """)) self.assert_file_count(".coverage", 0) self.assert_file_count(".coverage.*", 1) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index c16f3fa7e..f3aa8ebb9 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -3,6 +3,8 @@ """Tests for coverage.py's arc measurement.""" +import pytest + from tests.coveragetest import CoverageTest import coverage @@ -25,6 +27,7 @@ def test_simple_sequence(self): b = 3 """, arcz=".1 13 3.") + line1 = 1 if env.PYBEHAVIOR.module_firstline_1 else 2 self.check_coverage("""\ a = 2 @@ -32,7 +35,8 @@ def test_simple_sequence(self): c = 5 """, - arcz="-22 23 35 5-2") + arcz="-{0}2 23 35 5-{0}".format(line1) + ) def test_function_def(self): self.check_coverage("""\ @@ -215,6 +219,13 @@ def test_nested_loop(self): ) def test_break(self): + if env.PYBEHAVIOR.omit_after_jump: + arcz = ".1 12 23 35 15 5." + arcz_missing = "15" + else: + arcz = ".1 12 23 35 15 41 5." + arcz_missing = "15 41" + self.check_coverage("""\ for i in range(10): a = i @@ -222,9 +233,17 @@ def test_break(self): a = 99 assert a == 0 # 5 """, - arcz=".1 12 23 35 15 41 5.", arcz_missing="15 41") + arcz=arcz, arcz_missing=arcz_missing + ) def test_continue(self): + if env.PYBEHAVIOR.omit_after_jump: + arcz = ".1 12 23 31 15 5." + arcz_missing = "" + else: + arcz = ".1 12 23 31 15 41 5." + arcz_missing = "41" + self.check_coverage("""\ for i in range(10): a = i @@ -232,7 +251,8 @@ def test_continue(self): a = 99 assert a == 9 # 5 """, - arcz=".1 12 23 31 15 41 5.", arcz_missing="41") + arcz=arcz, arcz_missing=arcz_missing + ) def test_nested_breaks(self): self.check_coverage("""\ @@ -246,12 +266,17 @@ def test_nested_breaks(self): """, arcz=".1 12 23 34 45 25 56 51 67 17 7.", arcz_missing="17 25") - def test_while_true(self): + def test_while_1(self): # With "while 1", the loop knows it's constant. - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + arcz = ".1 12 23 34 45 36 62 57 7." + arcz_missing = "" + elif env.PYBEHAVIOR.nix_while_true: arcz = ".1 13 34 45 36 63 57 7." + arcz_missing = "" else: arcz = ".1 12 23 34 45 36 63 57 7." + arcz_missing = "" self.check_coverage("""\ a, i = 1, 0 while 1: @@ -262,10 +287,15 @@ def test_while_true(self): assert a == 4 and i == 3 """, arcz=arcz, + arcz_missing=arcz_missing, ) + + def test_while_true(self): # With "while True", 2.x thinks it's computation, # 3.x thinks it's constant. - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + arcz = ".1 12 23 34 45 36 62 57 7." + elif env.PYBEHAVIOR.nix_while_true: arcz = ".1 13 34 45 36 63 57 7." elif env.PY3: arcz = ".1 12 23 34 45 36 63 57 7." @@ -293,7 +323,9 @@ def method(self): """) out = self.run_command("coverage run --branch --source=. main.py") self.assertEqual(out, 'done\n') - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + num_stmts = 3 + elif env.PYBEHAVIOR.nix_while_true: num_stmts = 2 else: num_stmts = 3 @@ -305,7 +337,9 @@ def method(self): def test_bug_496_continue_in_constant_while(self): # https://github.com/nedbat/coveragepy/issues/496 # A continue in a while-true needs to jump to the right place. - if env.PYBEHAVIOR.nix_while_true: + if env.PYBEHAVIOR.keep_constant_test: + arcz = ".1 12 23 34 45 52 46 67 7." + elif env.PYBEHAVIOR.nix_while_true: arcz = ".1 13 34 45 53 46 67 7." elif env.PY3: arcz = ".1 12 23 34 45 53 46 67 7." @@ -495,6 +529,14 @@ def test_try_except(self): assert a == 3 and b == 1 """, arcz=".1 12 23 36 45 56 6.", arcz_missing="45 56") + + def test_raise_followed_by_statement(self): + if env.PYBEHAVIOR.omit_after_jump: + arcz = ".1 12 23 34 46 67 78 8." + arcz_missing = "" + else: + arcz = ".1 12 23 34 46 58 67 78 8." + arcz_missing = "58" self.check_coverage("""\ a, b = 1, 1 try: @@ -505,8 +547,7 @@ def test_try_except(self): b = 7 assert a == 3 and b == 7 """, - arcz=".1 12 23 34 46 58 67 78 8.", - arcz_missing="58", + arcz=arcz, arcz_missing=arcz_missing, ) def test_hidden_raise(self): @@ -579,15 +620,15 @@ def test_try_finally(self): try: a = 4 raise Exception("Yikes!") - a = 6 + # line 6 finally: c = 8 except: d = 10 # A assert a == 4 and c == 8 and d == 10 # B """, - arcz=".1 12 23 34 45 58 68 89 8B 9A AB B.", - arcz_missing="68 8B", + arcz=".1 12 23 34 45 58 89 9A AB B.", + arcz_missing="", ) def test_finally_in_loop(self): @@ -1086,38 +1127,75 @@ class OptimizedIfTest(CoverageTest): """Tests of if statements being optimized away.""" def test_optimized_away_if_0(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3, 4, 8, 9] + arcz = ".1 12 23 24 34 48 49 89 9." + arcz_missing = "24" + # 49 isn't missing because line 4 is matched by the default partial + # exclusion regex, and no branches are considered missing if they + # start from an excluded line. + else: + lines = [1, 2, 3, 8, 9] + arcz = ".1 12 23 28 38 89 9." + arcz_missing = "28" + self.check_coverage("""\ a = 1 if len([2]): c = 3 - if 0: # this line isn't in the compiled code. + if 0: if len([5]): d = 6 else: e = 8 f = 9 """, - lines=[1, 2, 3, 8, 9], - arcz=".1 12 23 28 38 89 9.", - arcz_missing="28", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) def test_optimized_away_if_1(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3, 4, 5, 6, 9] + arcz = ".1 12 23 24 34 45 49 56 69 59 9." + arcz_missing = "24 59" + # 49 isn't missing because line 4 is matched by the default partial + # exclusion regex, and no branches are considered missing if they + # start from an excluded line. + else: + lines = [1, 2, 3, 5, 6, 9] + arcz = ".1 12 23 25 35 56 69 59 9." + arcz_missing = "25 59" + self.check_coverage("""\ a = 1 if len([2]): c = 3 - if 1: # this line isn't in the compiled code, - if len([5]): # but these are. + if 1: + if len([5]): d = 6 else: e = 8 f = 9 """, - lines=[1, 2, 3, 5, 6, 9], - arcz=".1 12 23 25 35 56 69 59 9.", - arcz_missing="25 59", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) + + def test_optimized_away_if_1_no_else(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3, 4, 5] + arcz = ".1 12 23 25 34 45 5." + arcz_missing = "" + # 25 isn't missing because line 2 is matched by the default partial + # exclusion regex, and no branches are considered missing if they + # start from an excluded line. + else: + lines = [1, 3, 4, 5] + arcz = ".1 13 34 45 5." + arcz_missing = "" self.check_coverage("""\ a = 1 if 1: @@ -1125,11 +1203,24 @@ def test_optimized_away_if_1(self): c = 4 d = 5 """, - lines=[1, 3, 4, 5], - arcz=".1 13 34 45 5.", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, ) - def test_optimized_nested(self): + def test_optimized_if_nested(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 8, 11, 12, 13, 14, 15] + arcz = ".1 12 28 2F 8B 8F BC CD DE EF F." + arcz_missing = "" + # 2F and 8F aren't missing because they're matched by the default + # partial exclusion regex, and no branches are considered missing + # if they start from an excluded line. + else: + lines = [1, 12, 14, 15] + arcz = ".1 1C CE EF F." + arcz_missing = "" + self.check_coverage("""\ a = 1 if 0: @@ -1147,14 +1238,34 @@ def test_optimized_nested(self): h = 14 i = 15 """, - lines=[1, 12, 14, 15], - arcz=".1 1C CE EF F.", + lines=lines, + arcz=arcz, + arcz_missing=arcz_missing, + ) + + def test_dunder_debug(self): + # Since some of our tests use __debug__, let's make sure it is true as + # we expect + assert __debug__ + # Check that executed code has __debug__ + self.check_coverage("""\ + assert __debug__, "assert __debug__" + """ ) + # Check that if it didn't have debug, it would let us know. + with pytest.raises(AssertionError): + self.check_coverage("""\ + assert not __debug__, "assert not __debug__" + """ + ) def test_if_debug(self): - if not env.PYBEHAVIOR.optimize_if_debug: - self.skipTest("PyPy doesn't optimize away 'if __debug__:'") - # CPython optimizes away "if __debug__:" + if env.PYBEHAVIOR.optimize_if_debug: + arcz = ".1 12 24 41 26 61 1." + arcz_missing = "" + else: + arcz = ".1 12 23 31 34 41 26 61 1." + arcz_missing = "31" self.check_coverage("""\ for value in [True, False]: if value: @@ -1163,29 +1274,30 @@ def test_if_debug(self): else: x = 6 """, - arcz=".1 12 24 41 26 61 1.", + arcz=arcz, + arcz_missing=arcz_missing, ) def test_if_not_debug(self): - # Before 3.7, no Python optimized away "if not __debug__:" - if not env.PYBEHAVIOR.optimize_if_debug: - self.skipTest("PyPy doesn't optimize away 'if __debug__:'") + arcz_missing = "" + if env.PYBEHAVIOR.pep626: + arcz = ".1 12 23 34 42 37 72 28 8." elif env.PYBEHAVIOR.optimize_if_not_debug2: - arcz = ".1 12 24 41 26 61 1." - arcz_missing = "" + arcz = ".1 12 23 35 52 37 72 28 8." elif env.PYBEHAVIOR.optimize_if_not_debug: - arcz = ".1 12 23 31 26 61 1." - arcz_missing = "" + arcz = ".1 12 23 34 42 37 72 28 8." else: - arcz = ".1 12 23 31 34 41 26 61 1." - arcz_missing = "34 41" + arcz = ".1 12 23 34 45 42 52 37 72 28 8." + arcz_missing = "45 52" self.check_coverage("""\ + lines = set() for value in [True, False]: if value: if not __debug__: - x = 4 + lines.add(5) else: - x = 6 + lines.add(7) + assert lines == set([7]) """, arcz=arcz, arcz_missing=arcz_missing, diff --git a/tests/test_collector.py b/tests/test_collector.py index 9989b2292..f7e8a4c45 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -45,6 +45,6 @@ def otherfunc(x): self.start_import_stop(cov, "f2") # Double-check that our files were checked. - abs_files = set(os.path.abspath(f) for f in should_trace_hook.filenames) + abs_files = {os.path.abspath(f) for f in should_trace_hook.filenames} self.assertIn(os.path.abspath("f1.py"), abs_files) self.assertIn(os.path.abspath("f2.py"), abs_files) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 7109f1707..2469e2968 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -409,7 +409,7 @@ def try_multiprocessing_code( out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] - self.assertRegex(last_line, r"multi.py \d+ 0 100%") + self.assertRegex(last_line, r"TOTAL \d+ 0 100%") def test_multiprocessing_simple(self): nprocs = 3 @@ -466,7 +466,7 @@ def try_multiprocessing_code_with_branching(self, code, expected_out): out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] - self.assertRegex(last_line, r"multi.py \d+ 0 \d+ 0 100%") + self.assertRegex(last_line, r"TOTAL \d+ 0 \d+ 0 100%") def test_multiprocessing_with_branching(self): nprocs = 3 diff --git a/tests/test_config.py b/tests/test_config.py index dd86303f2..4225540c0 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,9 +10,9 @@ import coverage from coverage.misc import CoverageException -import coverage.optional from tests.coveragetest import CoverageTest, UsingModulesMixin +from tests.helpers import without_module class ConfigTest(CoverageTest): @@ -712,7 +712,7 @@ def test_nocoveragerc_file_when_specified(self): def test_no_toml_installed_no_toml(self): # Can't read a toml file that doesn't exist. - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): msg = "Couldn't read 'cov.toml' as a config file" with self.assertRaisesRegex(CoverageException, msg): coverage.Coverage(config_file="cov.toml") @@ -720,7 +720,7 @@ def test_no_toml_installed_no_toml(self): def test_no_toml_installed_explicit_toml(self): # Can't specify a toml config file if toml isn't installed. self.make_file("cov.toml", "# A toml file!") - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): msg = "Can't read 'cov.toml' without TOML support" with self.assertRaisesRegex(CoverageException, msg): coverage.Coverage(config_file="cov.toml") @@ -732,7 +732,7 @@ def test_no_toml_installed_pyproject_toml(self): [tool.coverage.run] xyzzy = 17 """) - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): msg = "Can't read 'pyproject.toml' without TOML support" with self.assertRaisesRegex(CoverageException, msg): coverage.Coverage() @@ -744,7 +744,7 @@ def test_no_toml_installed_pyproject_no_coverage(self): [tool.something] xyzzy = 17 """) - with coverage.optional.without('toml'): + with without_module(coverage.tomlconfig, 'toml'): cov = coverage.Coverage() # We get default settings: self.assertFalse(cov.config.timid) diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 02b577e52..68eea1150 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -368,6 +368,22 @@ def test_raise(self): """, [1,2,5,6], "") + def test_raise_followed_by_statement(self): + if env.PYBEHAVIOR.omit_after_jump: + lines = [1,2,4,5] + missing = "" + else: + lines = [1,2,3,4,5] + missing = "3" + self.check_coverage("""\ + try: + raise Exception("hello") + a = 3 + except: + pass + """, + lines=lines, missing=missing) + def test_return(self): self.check_coverage("""\ def fn(): @@ -401,6 +417,25 @@ def fn(): """, [1,2,3,7,8], "") + def test_return_followed_by_statement(self): + if env.PYBEHAVIOR.omit_after_return: + lines = [1,2,3,6,7] + missing = "" + else: + lines = [1,2,3,4,6,7] + missing = "4" + self.check_coverage("""\ + def fn(): + a = 2 + return a + a = 4 + + x = fn() + assert(x == 2) + """, + lines=lines, missing=missing, + ) + def test_yield(self): self.check_coverage("""\ def gen(): @@ -416,6 +451,13 @@ def gen(): [1,2,3,6,8,9], "") def test_break(self): + if env.PYBEHAVIOR.omit_after_jump: + lines = [1,2,3,5] + missing = "" + else: + lines = [1,2,3,4,5] + missing = "4" + self.check_coverage("""\ for x in range(10): a = 2 + x @@ -423,9 +465,16 @@ def test_break(self): a = 4 assert a == 2 """, - [1,2,3,4,5], "4") + lines=lines, missing=missing) def test_continue(self): + if env.PYBEHAVIOR.omit_after_jump: + lines = [1,2,3,5] + missing = "" + else: + lines = [1,2,3,4,5] + missing = "4" + self.check_coverage("""\ for x in range(10): a = 2 + x @@ -433,13 +482,14 @@ def test_continue(self): a = 4 assert a == 11 """, - [1,2,3,4,5], "4") + lines=lines, missing=missing) - def test_strange_unexecuted_continue(self): # pragma: not covered + def test_strange_unexecuted_continue(self): # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different - # versions of Python, so don't run this test: - self.skipTest("Expected failure: peephole optimization of jumps to jumps") + # versions of Python, so be careful when running this test. + if env.PY2: + self.skipTest("Expected failure: peephole optimization of jumps to jumps") self.check_coverage("""\ a = b = c = 0 for n in range(100): @@ -463,7 +513,9 @@ def test_strange_unexecuted_continue(self): # pragma: not covered c += 1 assert a == 33 and b == 50 and c == 50 """, - [1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], "") + lines=[1,2,3,4,5,6,8,9,10, 12,13,14,15,16,17,19,20,21], + missing=["", "6"], + ) def test_import(self): self.check_coverage("""\ @@ -570,7 +622,9 @@ def test_extra_doc_string(self): b = 3 assert (a,b) == (1,3) """, - [1,3,4], "") + ([1,3,4], [1,2,3,4]), + "", + ) self.check_coverage("""\ a = 1 "An extra docstring, should be a comment." @@ -580,7 +634,9 @@ def test_extra_doc_string(self): c = 6 assert (a,b,c) == (1,3,6) """, - ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7]), "") + ([1,3,6,7], [1,3,5,6,7], [1,3,4,5,6,7], [1,2,3,4,5,6,7]), + "", + ) def test_nonascii(self): self.check_coverage("""\ @@ -874,12 +930,18 @@ def test_absurd_split_if(self): [1,2,4,5,7,9,10], "4, 7") def test_constant_if(self): + if env.PYBEHAVIOR.keep_constant_test: + lines = [1, 2, 3] + else: + lines = [2, 3] self.check_coverage("""\ if 1: a = 2 assert a == 2 """, - [2,3], "") + lines, + "", + ) def test_while(self): self.check_coverage("""\ @@ -895,10 +957,9 @@ def test_while(self): while a: b += 1 break - b = 99 assert a == 3 and b == 1 """, - [1,2,3,4,5,6], "5") + [1,2,3,4,5], "") def test_while_else(self): # Take the else branch. @@ -919,12 +980,11 @@ def test_while_else(self): b += 1 a -= 1 break - b = 123 else: b = 99 assert a == 2 and b == 1 """, - [1,2,3,4,5,6,8,9], "6-8") + [1,2,3,4,5,7,8], "7") def test_split_while(self): self.check_coverage("""\ @@ -969,10 +1029,9 @@ def test_for(self): for i in [1,2,3,4,5]: a += i break - a = 99 assert a == 1 """, - [1,2,3,4,5,6], "5") + [1,2,3,4,5], "") def test_for_else(self): self.check_coverage("""\ @@ -989,12 +1048,11 @@ def test_for_else(self): for i in range(5): a += i+1 break - a = 99 else: a = 123 assert a == 1 """, - [1,2,3,4,5,7,8], "5-7") + [1,2,3,4,6,7], "6") def test_split_for(self): self.check_coverage("""\ @@ -1076,6 +1134,19 @@ def test_try_except(self): arcz=".1 12 23 45 58 37 78 8.", arcz_missing="45 58", ) + + def test_try_except_stranded_else(self): + if env.PYBEHAVIOR.omit_after_jump: + # The else can't be reached because the try ends with a raise. + lines = [1,2,3,4,5,6,9] + missing = "" + arcz = ".1 12 23 34 45 56 69 9." + arcz_missing = "" + else: + lines = [1,2,3,4,5,6,8,9] + missing = "8" + arcz = ".1 12 23 34 45 56 69 89 9." + arcz_missing = "89" self.check_coverage("""\ a = 0 try: @@ -1087,9 +1158,10 @@ def test_try_except(self): a = 123 assert a == 99 """, - [1,2,3,4,5,6,8,9], "8", - arcz=".1 12 23 34 45 56 69 89 9.", - arcz_missing="89", + lines=lines, + missing=missing, + arcz=arcz, + arcz_missing=arcz_missing, ) def test_try_finally(self): @@ -1355,12 +1427,11 @@ def test_excluding_for_else(self): for i in range(5): a += i+1 break - a = 99 else: #pragma: NO COVER a = 123 assert a == 1 """, - [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) + [1,2,3,4,7], "", excludes=['#pragma: NO COVER']) def test_excluding_while(self): self.check_coverage("""\ @@ -1368,10 +1439,9 @@ def test_excluding_while(self): while a*b: #pragma: NO COVER b += 1 break - b = 99 assert a == 3 and b == 0 """, - [1,6], "", excludes=['#pragma: NO COVER']) + [1,5], "", excludes=['#pragma: NO COVER']) self.check_coverage("""\ a = 3; b = 0 while ( @@ -1379,10 +1449,9 @@ def test_excluding_while(self): ): #pragma: NO COVER b += 1 break - b = 99 assert a == 3 and b == 0 """, - [1,8], "", excludes=['#pragma: NO COVER']) + [1,7], "", excludes=['#pragma: NO COVER']) def test_excluding_while_else(self): self.check_coverage("""\ @@ -1390,12 +1459,11 @@ def test_excluding_while_else(self): while a: b += 1 break - b = 99 else: #pragma: NO COVER b = 123 assert a == 3 and b == 1 """, - [1,2,3,4,5,8], "5", excludes=['#pragma: NO COVER']) + [1,2,3,4,7], "", excludes=['#pragma: NO COVER']) def test_excluding_try_except(self): self.check_coverage("""\ @@ -1443,58 +1511,15 @@ def test_excluding_try_except(self): arcz=".1 12 23 37 45 58 78 8.", arcz_missing="45 58", ) - self.check_coverage("""\ - a = 0 - try: - a = 1 - raise Exception("foo") - except: - a = 99 - else: #pragma: NO COVER - a = 123 - assert a == 99 - """, - [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 34 45 56 69 89 9.", - arcz_missing="89", - ) - def test_excluding_try_except_pass(self): - self.check_coverage("""\ - a = 0 - try: - a = 1 - except: #pragma: NO COVER - x = 2 - assert a == 1 - """, - [1,2,3,6], "", excludes=['#pragma: NO COVER']) - self.check_coverage("""\ - a = 0 - try: - a = 1 - raise Exception("foo") - except ImportError: #pragma: NO COVER - x = 2 - except: - a = 123 - assert a == 123 - """, - [1,2,3,4,7,8,9], "", excludes=['#pragma: NO COVER']) - self.check_coverage("""\ - a = 0 - try: - a = 1 - except: #pragma: NO COVER - x = 2 - else: - a = 123 - assert a == 123 - """, - [1,2,3,7,8], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 37 45 58 78 8.", - arcz_missing="45 58", - ) + def test_excluding_try_except_stranded_else(self): + if env.PYBEHAVIOR.omit_after_jump: + # The else can't be reached because the try ends with a raise. + arcz = ".1 12 23 34 45 56 69 9." + arcz_missing = "" + else: + arcz = ".1 12 23 34 45 56 69 89 9." + arcz_missing = "89" self.check_coverage("""\ a = 0 try: @@ -1507,8 +1532,8 @@ def test_excluding_try_except_pass(self): assert a == 99 """, [1,2,3,4,5,6,9], "", excludes=['#pragma: NO COVER'], - arcz=".1 12 23 34 45 56 69 89 9.", - arcz_missing="89", + arcz=arcz, + arcz_missing=arcz_missing, ) def test_excluding_if_pass(self): @@ -1775,6 +1800,19 @@ def test_try_except_finally(self): arcz=".1 12 23 37 45 59 79 9A A.", arcz_missing="45 59", ) + + def test_try_except_finally_stranded_else(self): + if env.PYBEHAVIOR.omit_after_jump: + # The else can't be reached because the try ends with a raise. + lines = [1,2,3,4,5,6,10,11] + missing = "" + arcz = ".1 12 23 34 45 56 6A AB B." + arcz_missing = "" + else: + lines = [1,2,3,4,5,6,8,10,11] + missing = "8" + arcz = ".1 12 23 34 45 56 6A 8A AB B." + arcz_missing = "8A" self.check_coverage("""\ a = 0; b = 0 try: @@ -1788,9 +1826,10 @@ def test_try_except_finally(self): b = 2 assert a == 99 and b == 2 """, - [1,2,3,4,5,6,8,10,11], "8", - arcz=".1 12 23 34 45 56 6A 8A AB B.", - arcz_missing="8A", + lines=lines, + missing=missing, + arcz=arcz, + arcz_missing=arcz_missing, ) diff --git a/tests/test_files.py b/tests/test_files.py index 9df4e5d0d..84e25f107 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -349,6 +349,20 @@ def test_multiple_wildcard(self): './django/foo/bar.py' ) + def test_windows_root_paths(self): + aliases = PathAliases() + aliases.add('X:\\', '/tmp/src') + self.assert_mapped( + aliases, + "X:\\a\\file.py", + "/tmp/src/a/file.py" + ) + self.assert_mapped( + aliases, + "X:\\file.py", + "/tmp/src/file.py" + ) + def test_leading_wildcard(self): aliases = PathAliases() aliases.add('*/d1', './mysrc1') diff --git a/tests/test_html.py b/tests/test_html.py index 85f082040..825b0afbe 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -495,7 +495,8 @@ def test_reporting_on_unmeasured_file(self): self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/other_py.html") - def test_report_skip_covered_no_branches(self): + def make_main_and_not_covered(self): + """Helper to create files for skip_covered scenarios.""" self.make_file("main_file.py", """ import not_covered @@ -507,39 +508,41 @@ def normal(): def not_covered(): print("n") """) + + def test_report_skip_covered(self): + self.make_main_and_not_covered() 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_100(self): - self.make_file("main_file.py", """ - def normal(): - print("z") - normal() - """) - res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) - self.assertEqual(res, 100.0) + def test_html_skip_covered(self): + self.make_main_and_not_covered() + self.make_file(".coveragerc", "[html]\nskip_covered = True") + self.run_coverage() + 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 + self.make_main_and_not_covered() + 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") + def test_report_skip_covered_100(self): + self.make_file("main_file.py", """ 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") + res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) + self.assertEqual(res, 100.0) self.assert_doesnt_exist("htmlcov/main_file_py.html") - self.assert_exists("htmlcov/not_covered_py.html") - def test_report_skip_empty_files(self): + def make_init_and_main(self): + """Helper to create files for skip_empty scenarios.""" self.make_file("submodule/__init__.py", "") self.make_file("main_file.py", """ import submodule @@ -548,11 +551,22 @@ def normal(): print("z") normal() """) + + def test_report_skip_empty(self): + self.make_init_and_main() self.run_coverage(htmlargs=dict(skip_empty=True)) self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/main_file_py.html") self.assert_doesnt_exist("htmlcov/submodule___init___py.html") + def test_html_skip_empty(self): + self.make_init_and_main() + self.make_file(".coveragerc", "[html]\nskip_empty = True") + self.run_coverage() + self.assert_exists("htmlcov/index.html") + self.assert_exists("htmlcov/main_file_py.html") + self.assert_doesnt_exist("htmlcov/submodule___init___py.html") + class HtmlStaticFileTest(CoverageTest): """Tests of the static file copying for the HTML report.""" @@ -938,23 +952,41 @@ def test_partial(self): cov = coverage.Coverage(config_file="partial.ini") partial = self.start_import_stop(cov, "partial") - cov.html_report(partial, directory="out/partial") - compare_html(gold_path("html/partial"), "out/partial") - contains( - "out/partial/partial_py.html", - '

', - '

', - # The "if 0" and "if 1" statements are optimized away. - '

', - # The "raise ZeroDivisionError" is excluded by regex in the .ini. - '

', - ) - contains( - "out/partial/index.html", - 'partial.py', - '91%' - ) + if env.PYBEHAVIOR.pep626: + cov.html_report(partial, directory="out/partial_626") + compare_html(gold_path("html/partial_626"), "out/partial_626") + contains( + "out/partial_626/partial_py.html", + '

', + '

', + # The "if 0" and "if 1" statements are marked as run. + '

', + # The "raise ZeroDivisionError" is excluded by regex in the .ini. + '

', + ) + contains( + "out/partial_626/index.html", + 'partial.py', + '87%' + ) + else: + cov.html_report(partial, directory="out/partial") + compare_html(gold_path("html/partial"), "out/partial") + contains( + "out/partial/partial_py.html", + '

', + '

', + # The "if 0" and "if 1" statements are optimized away. + '

', + # The "raise ZeroDivisionError" is excluded by regex in the .ini. + '

', + ) + contains( + "out/partial/index.html", + 'partial.py', + '91%' + ) def test_styled(self): self.make_file("a.py", """\ diff --git a/tests/test_parser.py b/tests/test_parser.py index 0e6a0859c..9d3f9f678 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -171,11 +171,11 @@ def meth(self): def func(x=25): return 26 """) - raw_statements = set([3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26]) + raw_statements = {3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26} if env.PYBEHAVIOR.trace_decorated_def: raw_statements.update([11, 19]) self.assertEqual(parser.raw_statements, raw_statements) - self.assertEqual(parser.statements, set([8])) + self.assertEqual(parser.statements, {8}) def test_class_decorator_pragmas(self): parser = self.parse_source("""\ @@ -188,8 +188,8 @@ class Bar(object): def __init__(self): self.x = 8 """) - self.assertEqual(parser.raw_statements, set([1, 2, 3, 5, 6, 7, 8])) - self.assertEqual(parser.statements, set([1, 2, 3])) + self.assertEqual(parser.raw_statements, {1, 2, 3, 5, 6, 7, 8}) + self.assertEqual(parser.statements, {1, 2, 3}) def test_empty_decorated_function(self): parser = self.parse_source("""\ @@ -214,7 +214,7 @@ def bar(self): expected_arcs = set(arcz_to_arcs(".1 14 48 8. .2 2. -8A A-8")) expected_exits = {1: 1, 2: 1, 4: 1, 8: 1, 10: 1} - if (not env.PYPY) and (env.PYVERSION >= (3, 7, 0, 'beta', 5)): + if env.PYBEHAVIOR.docstring_only_function: # 3.7 changed how functions with only docstrings are numbered. expected_arcs.update(set(arcz_to_arcs("-46 6-4"))) expected_exits.update({6: 1}) @@ -330,70 +330,73 @@ def function(): try: if something(4): break + elif something(6): + x = 7 else: - if something(7): + if something(9): continue else: continue - if also_this(11): - return 12 + if also_this(13): + return 14 else: - raise Exception(14) + raise Exception(16) finally: - this_thing(16) - that_thing(17) + this_thing(18) + that_thing(19) """) if env.PYBEHAVIOR.finally_jumps_back: self.assertEqual( - parser.missing_arc_description(16, 5), - "line 16 didn't jump to line 5, because the break on line 5 wasn't executed" + parser.missing_arc_description(18, 5), + "line 18 didn't jump to line 5, because the break on line 5 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(5, 17), - "line 5 didn't jump to line 17, because the break on line 5 wasn't executed" + parser.missing_arc_description(5, 19), + "line 5 didn't jump to line 19, because the break on line 5 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(16, 8), - "line 16 didn't jump to line 8, because the continue on line 8 wasn't executed" + parser.missing_arc_description(18, 10), + "line 18 didn't jump to line 10, because the continue on line 10 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(8, 2), - "line 8 didn't jump to line 2, because the continue on line 8 wasn't executed" + parser.missing_arc_description(10, 2), + "line 10 didn't jump to line 2, because the continue on line 10 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(16, 12), - "line 16 didn't jump to line 12, because the return on line 12 wasn't executed" + parser.missing_arc_description(18, 14), + "line 18 didn't jump to line 14, because the return on line 14 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(12, -1), - "line 12 didn't return from function 'function', " - "because the return on line 12 wasn't executed" + parser.missing_arc_description(14, -1), + "line 14 didn't return from function 'function', " + "because the return on line 14 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(16, -1), - "line 16 didn't except from function 'function', " - "because the raise on line 14 wasn't executed" + parser.missing_arc_description(18, -1), + "line 18 didn't except from function 'function', " + "because the raise on line 16 wasn't executed" ) else: self.assertEqual( - parser.missing_arc_description(16, 17), - "line 16 didn't jump to line 17, because the break on line 5 wasn't executed" + parser.missing_arc_description(18, 19), + "line 18 didn't jump to line 19, because the break on line 5 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(16, 2), - "line 16 didn't jump to line 2, " - "because the continue on line 8 wasn't executed" + parser.missing_arc_description(18, 2), + "line 18 didn't jump to line 2, " + "because the continue on line 10 wasn't executed" " or " - "the continue on line 10 wasn't executed" + "the continue on line 12 wasn't executed" ) self.assertEqual( - parser.missing_arc_description(16, -1), - "line 16 didn't except from function 'function', " - "because the raise on line 14 wasn't executed" + parser.missing_arc_description(18, -1), + "line 18 didn't except from function 'function', " + "because the raise on line 16 wasn't executed" " or " - "line 16 didn't return from function 'function', " - "because the return on line 12 wasn't executed" + "line 18 didn't return from function 'function', " + "because the return on line 14 wasn't executed" ) + def test_missing_arc_descriptions_bug460(self): parser = self.parse_text(u"""\ x = 1 @@ -463,7 +466,7 @@ def test_missing_line_ending(self): """) parser = self.parse_file("normal.py") - self.assertEqual(parser.statements, set([1])) + self.assertEqual(parser.statements, {1}) self.make_file("abrupt.py", """\ out, err = subprocess.Popen( @@ -476,4 +479,4 @@ def test_missing_line_ending(self): self.assertEqual(f.read()[-1], ")") parser = self.parse_file("abrupt.py") - self.assertEqual(parser.statements, set([1])) + self.assertEqual(parser.statements, {1}) diff --git a/tests/test_plugins.py b/tests/test_plugins.py index d14f5c472..813d370e3 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -400,12 +400,12 @@ def test_plugin2_with_branch(self): # have 7 lines in it. If render() was called with line number 4, # then the plugin will claim that lines 4 and 5 were executed. analysis = cov._analyze("foo_7.html") - self.assertEqual(analysis.statements, set([1, 2, 3, 4, 5, 6, 7])) + self.assertEqual(analysis.statements, {1, 2, 3, 4, 5, 6, 7}) # Plugins don't do branch coverage yet. self.assertEqual(analysis.has_arcs(), True) self.assertEqual(analysis.arc_possibilities(), []) - self.assertEqual(analysis.missing, set([1, 2, 3, 6, 7])) + self.assertEqual(analysis.missing, {1, 2, 3, 6, 7}) def test_plugin2_with_text_report(self): self.make_render_and_caller() @@ -522,6 +522,8 @@ def coverage_init(reg, options): 'Name Stmts Miss Cover Missing', '-----------------------------------------------', 'unsuspecting.py 6 3 50% 2, 4, 6', + '-----------------------------------------------', + 'TOTAL 6 3 50%', ] self.assertEqual(expected, report) self.assertEqual(total, 50) @@ -553,7 +555,7 @@ def line_number_range(self, frame): class MyReporter(coverage.FileReporter): def lines(self): - return set([99, 999, 9999]) + return {99, 999, 9999} def coverage_init(reg, options): reg.add_file_tracer(Plugin()) diff --git a/tests/test_process.py b/tests/test_process.py index 249beb001..e48861568 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -341,6 +341,8 @@ def test_combine_with_rc(self): Name Stmts Miss Cover ------------------------------- b_or_c.py 8 0 100% + ------------------------------- + TOTAL 8 0 100% """)) def test_combine_with_aliases(self): @@ -743,7 +745,7 @@ def test_fullcoverage(self): # pragma: no metacov self.assertGreater(line_counts(data)['os.py'], 50) @xfail( - env.PYPY3 and (env.PYPYVERSION >= (7, 1, 1)) and env.LINUX, + env.PYPY3 and (env.PYPYVERSION >= (7, 1, 1)), "https://bitbucket.org/pypy/pypy/issues/3074" ) def test_lang_c(self): @@ -1097,7 +1099,7 @@ def excepthook(*args): self.assertEqual(line_counts(data)['excepthook.py'], 7) def test_excepthook_exit(self): - if env.PYPY or env.JYTHON: + if not env.CPYTHON: self.skipTest("non-CPython handles excepthook exits differently, punt for now.") self.make_file("excepthook_exit.py", """\ import sys @@ -1231,7 +1233,7 @@ def setUp(self): def test_report_43_is_ok(self): st, out = self.run_command_status("coverage report --fail-under=43") self.assertEqual(st, 0) - self.assertEqual(self.last_line_squeezed(out), "forty_two_plus.py 7 4 43%") + self.assertEqual(self.last_line_squeezed(out), "TOTAL 7 4 43%") def test_report_43_is_not_ok(self): st, out = self.run_command_status("coverage report --fail-under=44") @@ -1305,6 +1307,8 @@ def test_accented_dot_py(self): u"Name Stmts Miss Cover\n" u"----------------------------\n" u"h\xe2t.py 1 0 100%\n" + u"----------------------------\n" + u"TOTAL 1 0 100%\n" ) if env.PY2: @@ -1348,8 +1352,10 @@ def test_accented_directory(self): report_expected = ( u"Name Stmts Miss Cover\n" u"-----------------------------------\n" - u"\xe2%saccented.py 1 0 100%%\n" % os.sep - ) + u"\xe2%saccented.py 1 0 100%%\n" + u"-----------------------------------\n" + u"TOTAL 1 0 100%%\n" + ) % os.sep if env.PY2: report_expected = report_expected.encode(output_encoding()) diff --git a/tests/test_results.py b/tests/test_results.py index 86806cfd9..377c150bd 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -114,7 +114,7 @@ def test_should_fail_under_invalid_value(): @pytest.mark.parametrize("statements, lines, result", [ - (set([1,2,3,4,5,10,11,12,13,14]), set([1,2,5,10,11,13,14]), "1-2, 5-11, 13-14"), + ({1,2,3,4,5,10,11,12,13,14}, {1,2,5,10,11,13,14}, "1-2, 5-11, 13-14"), ([1,2,3,4,5,10,11,12,13,14,98,99], [1,2,5,10,11,13,14,99], "1-2, 5-11, 13-14, 99"), ([1,2,3,4,98,99,100,101,102,103,104], [1,2,99,102,103,104], "1-2, 99, 102-104"), ([17], [17], "17"), @@ -128,8 +128,8 @@ def test_format_lines(statements, lines, result): @pytest.mark.parametrize("statements, lines, arcs, result", [ ( - set([1,2,3,4,5,10,11,12,13,14]), - set([1,2,5,10,11,13,14]), + {1,2,3,4,5,10,11,12,13,14}, + {1,2,5,10,11,13,14}, (), "1-2, 5-11, 13-14" ), diff --git a/tests/test_summary.py b/tests/test_summary.py index eb25a4d80..feaa0fe0b 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -70,13 +70,15 @@ def test_report_just_one(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_report_wildcard(self): # Try reporting using wildcards to get the modules. @@ -87,13 +89,15 @@ def test_report_wildcard(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_report_omitting(self): # Try reporting while omitting some modules @@ -105,13 +109,15 @@ def test_report_omitting(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_report_including(self): # Try reporting while including some modules @@ -122,13 +128,15 @@ def test_report_including(self): # Name Stmts Miss Cover # ------------------------------- # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertNotIn("/coverage/", report) self.assertNotIn("/tests/modules/covmod1.py ", report) self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mycode.py 4 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 4 0 100%") def test_run_source_vs_report_include(self): # https://github.com/nedbat/coveragepy/issues/621 @@ -179,11 +187,13 @@ def branch(x): # Name Stmts Miss Branch BrPart Cover # ----------------------------------------------- - # mybranch.py 5 0 2 1 85% + # mybranch.py 5 0 2 1 86% + # ----------------------------------------------- + # TOTAL 5 0 2 1 86% - self.assertEqual(self.line_count(report), 3) + self.assertEqual(self.line_count(report), 5) self.assertIn("mybranch.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mybranch.py 5 0 2 1 86%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 5 0 2 1 86%") def test_report_show_missing(self): self.make_file("mymissing.py", """\ @@ -209,10 +219,13 @@ def missing(x, y): # Name Stmts Miss Cover Missing # -------------------------------------------- # mymissing.py 14 3 79% 3-4, 10 + # -------------------------------------------- + # TOTAL 14 3 79% 3-4, 10 - self.assertEqual(self.line_count(report), 3) - self.assertIn("mymissing.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mymissing.py 14 3 79% 3-4, 10") + self.assertEqual(self.line_count(report), 5) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "mymissing.py 14 3 79% 3-4, 10") + self.assertEqual(squeezed[4], "TOTAL 14 3 79%") def test_report_show_missing_branches(self): self.make_file("mybranch.py", """\ @@ -231,10 +244,13 @@ def branch(x, y): # Name Stmts Miss Branch BrPart Cover Missing # ---------------------------------------------------------- # mybranch.py 6 0 4 2 80% 2->4, 4->exit + # ---------------------------------------------------------- + # TOTAL 6 0 4 2 80% - self.assertEqual(self.line_count(report), 3) - self.assertIn("mybranch.py ", report) - self.assertEqual(self.last_line_squeezed(report), "mybranch.py 6 0 4 2 80% 2->4, 4->exit") + self.assertEqual(self.line_count(report), 5) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[2], "mybranch.py 6 0 4 2 80% 2->4, 4->exit") + self.assertEqual(squeezed[4], "TOTAL 6 0 4 2 80%") def test_report_show_missing_branches_and_lines(self): self.make_file("main.py", """\ @@ -394,12 +410,14 @@ def foo(): # Name Stmts Miss Branch BrPart Cover # ------------------------------------------- + # ----------------------------------------- + # TOTAL 3 0 0 0 100% # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 4, report) + self.assertEqual(self.line_count(report), 6, report) squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[3], "1 file skipped due to complete coverage.") + self.assertEqual(squeezed[5], "1 file skipped due to complete coverage.") def test_report_skip_covered_longfilename(self): self.make_file("long_______________filename.py", """ @@ -413,14 +431,16 @@ def foo(): # Name Stmts Miss Branch BrPart Cover # ----------------------------------------- + # ----------------------------------------- + # TOTAL 3 0 0 0 100% # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 4, report) + self.assertEqual(self.line_count(report), 6, 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.") + self.assertEqual(squeezed[5], "1 file skipped due to complete coverage.") def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") @@ -472,9 +492,10 @@ def test_report_skip_empty_no_data(self): # # 1 empty file skipped. - self.assertEqual(self.line_count(report), 4, report) - lines = self.report_lines(report) - self.assertEqual(lines[3], "1 empty file skipped.") + self.assertEqual(self.line_count(report), 6, report) + squeezed = self.squeezed_lines(report) + self.assertEqual(squeezed[3], "TOTAL 0 0 100%") + self.assertEqual(squeezed[5], "1 empty file skipped.") def test_report_precision(self): self.make_file(".coveragerc", """\ @@ -621,7 +642,7 @@ def test_report_no_extension(self): out = self.run_command("coverage run --source=. xxx") self.assertEqual(out, "xxx: 3 4 0 7\n") report = self.report_from_command("coverage report") - self.assertEqual(self.last_line_squeezed(report), "xxx 7 1 86%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 7 1 86%") def test_report_with_chdir(self): self.make_file("chdir.py", """\ @@ -635,7 +656,7 @@ def test_report_with_chdir(self): out = self.run_command("coverage run --source=. chdir.py") self.assertEqual(out, "Line One\nLine Two\nhello\n") report = self.report_from_command("coverage report") - self.assertEqual(self.last_line_squeezed(report), "chdir.py 5 0 100%") + self.assertEqual(self.last_line_squeezed(report), "TOTAL 5 0 100%") def get_report(self, cov): """Get the report from `cov`, and canonicalize it.""" diff --git a/tests/test_testing.py b/tests/test_testing.py index 2fda956bf..34ea32635 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -12,14 +12,16 @@ import pytest import coverage +from coverage import tomlconfig from coverage.backunittest import TestCase, unittest from coverage.files import actual_path from coverage.misc import StopEverything -import coverage.optional from tests.coveragetest import CoverageTest, convert_skip_exceptions -from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs -from tests.helpers import CheckUniqueFilenames, re_lines, re_line +from tests.helpers import ( + arcs_to_arcz_repr, arcz_to_arcs, + CheckUniqueFilenames, re_lines, re_line, without_module, +) def test_xdist_sys_path_nuttiness_is_fixed(): @@ -33,11 +35,11 @@ class TestingTest(TestCase): def test_assert_count_equal(self): self.assertCountEqual(set(), set()) - self.assertCountEqual(set([1,2,3]), set([3,1,2])) + self.assertCountEqual({1,2,3}, {3,1,2}) with self.assertRaises(AssertionError): - self.assertCountEqual(set([1,2,3]), set()) + self.assertCountEqual({1,2,3}, set()) with self.assertRaises(AssertionError): - self.assertCountEqual(set([1,2,3]), set([4,5,6])) + self.assertCountEqual({1,2,3}, {4,5,6}) class CoverageTestTest(CoverageTest): @@ -323,12 +325,11 @@ def _same_python_executable(e1, e2): return False # pragma: only failure -def test_optional_without(): - # pylint: disable=reimported - from coverage.optional import toml as toml1 - with coverage.optional.without('toml'): - from coverage.optional import toml as toml2 - from coverage.optional import toml as toml3 +def test_without_module(): + toml1 = tomlconfig.toml + with without_module(tomlconfig, 'toml'): + toml2 = tomlconfig.toml + toml3 = tomlconfig.toml assert toml1 is toml3 is not None assert toml2 is None diff --git a/tox.ini b/tox.ini index 077d4c1fd..317dea42f 100644 --- a/tox.ini +++ b/tox.ini @@ -48,6 +48,7 @@ commands = python igor.py test_with_tracer c {posargs} [testenv:anypy] +# $set_env.py: COVERAGE_PYTHON - The custom Python for "tox -e anypy" # For running against my own builds of CPython, or any other specific Python. basepython = {env:COVERAGE_PYTHON}