diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 67393a8ca..71b32f2fa 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -16,7 +16,7 @@ How can we reproduce the problem? Please *be specific*. Don't link to a failing 1. What version of coverage.py shows the problem? The output of `coverage debug sys` is helpful. 1. What versions of what packages do you have installed? The output of `pip freeze` is helpful. 1. What code shows the problem? Give us a specific commit of a specific repo that we can check out. If you've already worked around the problem, please provide a commit before that fix. -1. What commands did you run? +1. What commands should we run to reproduce the problem? *Be specific*. Include everything, even `git clone`, `pip install`, and so on. Explain like we're five! **Expected behavior** A clear and concise description of what you expected to happen. diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 60e8d0a29..ab94a83e3 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -48,6 +48,7 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" - "pypy-3.7" - "pypy-3.8" - "pypy-3.9" @@ -77,6 +78,7 @@ jobs: uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" + allow-prereleases: true cache: pip cache-dependency-path: 'requirements/*.pip' @@ -178,7 +180,7 @@ jobs: echo "sha10=$SHA10" >> $GITHUB_ENV echo "slug=$SLUG" >> $GITHUB_ENV echo "report_dir=$REPORT_DIR" >> $GITHUB_ENV - echo "url=https://nedbat.github.io/coverage-reports/$REPORT_DIR" >> $GITHUB_ENV + echo "url=https://htmlpreview.github.io/?https://github.com/nedbat/coverage-reports/blob/main/reports/$SLUG/htmlcov/index.html" >> $GITHUB_ENV echo "branch=${REF#refs/heads/}" >> $GITHUB_ENV - name: "Summarize" @@ -217,6 +219,8 @@ jobs: # Make the commit message. echo "${{ env.total }}% - $COMMIT_MESSAGE" > commit.txt echo "" >> commit.txt + echo "[View the report](${{ env.url }})" >> commit.txt + echo "" >> commit.txt echo "${{ env.url }}" >> commit.txt echo "${{ env.sha10 }}: ${{ env.branch }}" >> commit.txt # Commit. diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 179f7a649..53e081455 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -77,9 +77,8 @@ jobs: # } # # PYVERSIONS. Available versions: # # https://github.com/actions/python-versions/blob/main/versions-manifest.json - # # Include prereleases if they are at rc stage. # # PyPy versions are handled further below in the "pypy" step. - # pys = ["cp37", "cp38", "cp39", "cp310", "cp311"] + # pys = ["cp37", "cp38", "cp39", "cp310", "cp311", "cp312"] # # # Some OS/arch combinations need overrides for the Python versions: # os_arch_pys = { @@ -104,16 +103,19 @@ jobs: - {"os": "ubuntu", "py": "cp39", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp310", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp311", "arch": "x86_64"} + - {"os": "ubuntu", "py": "cp312", "arch": "x86_64"} - {"os": "ubuntu", "py": "cp37", "arch": "i686"} - {"os": "ubuntu", "py": "cp38", "arch": "i686"} - {"os": "ubuntu", "py": "cp39", "arch": "i686"} - {"os": "ubuntu", "py": "cp310", "arch": "i686"} - {"os": "ubuntu", "py": "cp311", "arch": "i686"} + - {"os": "ubuntu", "py": "cp312", "arch": "i686"} - {"os": "ubuntu", "py": "cp37", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp38", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp39", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp310", "arch": "aarch64"} - {"os": "ubuntu", "py": "cp311", "arch": "aarch64"} + - {"os": "ubuntu", "py": "cp312", "arch": "aarch64"} - {"os": "macos", "py": "cp38", "arch": "arm64"} - {"os": "macos", "py": "cp39", "arch": "arm64"} - {"os": "macos", "py": "cp310", "arch": "arm64"} @@ -123,17 +125,20 @@ jobs: - {"os": "macos", "py": "cp39", "arch": "x86_64"} - {"os": "macos", "py": "cp310", "arch": "x86_64"} - {"os": "macos", "py": "cp311", "arch": "x86_64"} + - {"os": "macos", "py": "cp312", "arch": "x86_64"} - {"os": "windows", "py": "cp37", "arch": "x86"} - {"os": "windows", "py": "cp38", "arch": "x86"} - {"os": "windows", "py": "cp39", "arch": "x86"} - {"os": "windows", "py": "cp310", "arch": "x86"} - {"os": "windows", "py": "cp311", "arch": "x86"} + - {"os": "windows", "py": "cp312", "arch": "x86"} - {"os": "windows", "py": "cp37", "arch": "AMD64"} - {"os": "windows", "py": "cp38", "arch": "AMD64"} - {"os": "windows", "py": "cp39", "arch": "AMD64"} - {"os": "windows", "py": "cp310", "arch": "AMD64"} - {"os": "windows", "py": "cp311", "arch": "AMD64"} - # [[[end]]] (checksum: ded8a9f214bf59776562d91ae6828863) + - {"os": "windows", "py": "cp312", "arch": "AMD64"} + # [[[end]]] (checksum: 5e62f362263935c1e3a21299f8a1b649) fail-fast: false steps: @@ -149,6 +154,7 @@ jobs: - name: "Install Python 3.8" uses: actions/setup-python@v4 with: + # PYVERSIONS python-version: "3.8" cache: pip cache-dependency-path: 'requirements/*.pip' @@ -162,6 +168,7 @@ jobs: CIBW_BUILD: ${{ matrix.py }}-* CIBW_ARCHS: ${{ matrix.arch }} CIBW_ENVIRONMENT: PIP_DISABLE_PIP_VERSION_CHECK=1 + CIBW_PRERELEASE_PYTHONS: True CIBW_TEST_COMMAND: python -c "from coverage.tracer import CTracer; print('CTracer OK!')" run: | python -m cibuildwheel --output-dir wheelhouse @@ -175,6 +182,7 @@ jobs: with: name: dist path: wheelhouse/*.whl + retention-days: 7 sdist: name: "Source distribution" @@ -186,6 +194,7 @@ jobs: - name: "Install Python 3.8" uses: actions/setup-python@v4 with: + # PYVERSIONS python-version: "3.8" cache: pip cache-dependency-path: 'requirements/*.pip' @@ -207,6 +216,7 @@ jobs: with: name: dist path: dist/*.tar.gz + retention-days: 7 pypy: name: "PyPy wheel" @@ -241,3 +251,40 @@ jobs: with: name: dist path: dist/*.whl + retention-days: 7 + + sign: + # This signs our artifacts, but we don't use the signatures for anything + # yet. Someday maybe PyPI will have a way to upload and verify them. + name: "Sign artifacts" + needs: + - wheels + - sdist + - pypy + runs-on: ubuntu-latest + permissions: + id-token: write + steps: + - name: "Download artifacts" + uses: actions/download-artifact@v3 + with: + name: dist + + - name: "Sign artifacts" + uses: sigstore/gh-action-sigstore-python@v1.2.3 + with: + inputs: coverage-*.* + + - name: "List files" + run: | + ls -alR + + - name: "Upload signatures" + uses: actions/upload-artifact@v3 + with: + name: signatures + path: | + *.crt + *.sig + *.sigstore + retention-days: 7 diff --git a/.github/workflows/python-nightly.yml b/.github/workflows/python-nightly.yml index 94a30ecc2..319064c94 100644 --- a/.github/workflows/python-nightly.yml +++ b/.github/workflows/python-nightly.yml @@ -53,6 +53,7 @@ jobs: - "pypy-3.7-nightly" - "pypy-3.8-nightly" - "pypy-3.9-nightly" + - "pypy-3.10-nightly" fail-fast: false steps: diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index b0f0ee6ca..8ab3608bc 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -49,6 +49,7 @@ jobs: - "3.9" - "3.10" - "3.11" + - "3.12" - "pypy-3.7" - "pypy-3.9" exclude: @@ -65,6 +66,7 @@ jobs: uses: "actions/setup-python@v4" with: python-version: "${{ matrix.python-version }}" + allow-prereleases: true cache: pip cache-dependency-path: 'requirements/*.pip' diff --git a/.treerc b/.treerc index ddea2e92c..0916e24a9 100644 --- a/.treerc +++ b/.treerc @@ -14,5 +14,5 @@ ignore = *.gz *.zip _build _spell *.egg *.egg-info - .mypy_cache + .*_cache tmp diff --git a/CHANGES.rst b/CHANGES.rst index 937835ccc..4b567d6dc 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -19,6 +19,87 @@ development at the same time, such as 4.5.x and 5.0. .. scriv-start-here +.. _changes_7-2-7: + +Version 7.2.7 — 2023-05-29 +-------------------------- + +- Fix: reverted a `change from 6.4.3 <pull 1347_>`_ that helped Cython, but + also increased the size of data files when using dynamic contexts, as + described in the now-fixed `issue 1586`_. The problem is now avoided due to a + recent change (`issue 1538`_). Thanks to `Anders Kaseorg <pull 1629_>`_ + and David Szotten for persisting with problem reports and detailed diagnoses. + +- Wheels are now provided for CPython 3.12. + +.. _issue 1586: https://github.com/nedbat/coveragepy/issues/1586 +.. _pull 1629: https://github.com/nedbat/coveragepy/pull/1629 + + +.. _changes_7-2-6: + +Version 7.2.6 — 2023-05-23 +-------------------------- + +- Fix: the ``lcov`` command could raise an IndexError exception if a file is + translated to Python but then executed under its own name. Jinja2 does this + when rendering templates. Fixes `issue 1553`_. + +- Python 3.12 beta 1 now inlines comprehensions. Previously they were compiled + as invisible functions and coverage.py would warn you if they weren't + completely executed. This no longer happens under Python 3.12. + +- Fix: the ``coverage debug sys`` command includes some environment variables + in its output. This could have included sensitive data. Those values are + now hidden with asterisks, closing `issue 1628`_. + +.. _issue 1553: https://github.com/nedbat/coveragepy/issues/1553 +.. _issue 1628: https://github.com/nedbat/coveragepy/issues/1628 + + +.. _changes_7-2-5: + +Version 7.2.5 — 2023-04-30 +-------------------------- + +- Fix: ``html_report()`` could fail with an AttributeError on ``isatty`` if run + in an unusual environment where sys.stdout had been replaced. This is now + fixed. + + +.. _changes_7-2-4: + +Version 7.2.4 — 2023-04-28 +-------------------------- + +PyCon 2023 sprint fixes! + +- Fix: with ``relative_files = true``, specifying a specific file to include or + omit wouldn't work correctly (`issue 1604`_). This is now fixed, with + testing help by `Marc Gibbons <pull 1608_>`_. + +- Fix: the XML report would have an incorrect ``<source>`` element when using + relative files and the source option ended with a slash (`issue 1541`_). + This is now fixed, thanks to `Kevin Brown-Silva <pull 1608_>`_. + +- When the HTML report location is printed to the terminal, it's now a + terminal-compatible URL, so that you can click the location to open the HTML + file in your browser. Finishes `issue 1523`_ thanks to `Ricardo Newbery + <pull 1613_>`_. + +- Docs: a new :ref:`Migrating page <migrating>` with details about how to + migrate between major versions of coverage.py. It currently covers the + wildcard changes in 7.x. Thanks, `Brian Grohe <pull 1610_>`_. + +.. _issue 1523: https://github.com/nedbat/coveragepy/issues/1523 +.. _issue 1541: https://github.com/nedbat/coveragepy/issues/1541 +.. _issue 1604: https://github.com/nedbat/coveragepy/issues/1604 +.. _pull 1608: https://github.com/nedbat/coveragepy/pull/1608 +.. _pull 1609: https://github.com/nedbat/coveragepy/pull/1609 +.. _pull 1610: https://github.com/nedbat/coveragepy/pull/1610 +.. _pull 1613: https://github.com/nedbat/coveragepy/pull/1613 + + .. _changes_7-2-3: Version 7.2.3 — 2023-04-06 @@ -139,6 +220,7 @@ Version 7.1.0 — 2023-01-24 .. _issue 1319: https://github.com/nedbat/coveragepy/issues/1319 .. _issue 1538: https://github.com/nedbat/coveragepy/issues/1538 + .. _changes_7-0-5: Version 7.0.5 — 2023-01-10 @@ -352,7 +434,6 @@ update your settings. .. _pull 1479: https://github.com/nedbat/coveragepy/pull/1479 - .. _changes_6-6-0b1: Version 6.6.0b1 — 2022-10-31 @@ -1062,6 +1143,7 @@ Version 5.3.1 — 2020-12-19 .. _issue 1010: https://github.com/nedbat/coveragepy/issues/1010 .. _pull request 1066: https://github.com/nedbat/coveragepy/pull/1066 + .. _changes_53: Version 5.3 — 2020-09-13 diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 0ba35f628..c3dfef428 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -16,6 +16,7 @@ Alexander Todorov Alexander Walters Alpha Chen Ammar Askar +Anders Kaseorg Andrew Hoos Anthony Sottile Arcadiy Ivanov @@ -23,6 +24,7 @@ Aron Griffis Artem Dayneko Arthur Deygin Arthur Rio +Asher Foa Ben Carlsson Ben Finney Benjamin Parzella @@ -32,20 +34,26 @@ Bill Hart Bradley Burns Brandon Rhodes Brett Cannon +Brian Grohe +Bruno Oliveira Bruno P. Kinoshita Buck Evan +Buck Golemon Calen Pennington Carl Friedrich Bolz-Tereick Carl Gieringer Catherine Proulx +Charles Chan Chris Adams Chris Jerdonek Chris Rose Chris Warrick +Christian Clauss Christian Heimes Christine Lytwynec Christoph Blessing Christoph Zwerschke +Christopher Pickering Clément Pit-Claudel Conrad Ho Cosimo Lupo @@ -59,6 +67,7 @@ David Christian David MacIver David Stanek David Szotten +Dennis Sweeney Detlev Offenbach Devin Jeanpierre Dirk Thomas @@ -79,6 +88,7 @@ George-Cristian Bîrzan Greg Rogers Guido van Rossum Guillaume Chazarain +Holger Krekel Hugo van Kemenade Ian Moore Ilia Meerovich @@ -87,10 +97,13 @@ Ionel Cristian Mărieș Ivan Ciuvalschii J. M. F. Tsang JT Olds +Jakub Wilk +Janakarajan Natarajan Jerin Peter George Jessamyn Smith Joe Doherty Joe Jevnik +John Vandenberg Jon Chappell Jon Dufresne Joseph Tate @@ -99,21 +112,29 @@ Judson Neer Julian Berman Julien Voisin Justas Sadzevičius +Karthikeyan Singaravelan Kassandra Keeton +Kevin Brown-Silva Kjell Braden Krystian Kichewko Kyle Altendorf Lars Hupfeldt Nielsen +Latrice Wilgus Leonardo Pistone Lewis Gaul Lex Berezhny Loïc Dachary Lorenzo Micò +Louis Heredero +Luis Nell +Łukasz Stolcman Manuel Jacob Marc Abramowitz +Marc Gibbons Marc Legendre Marcelo Trylesinski Marcus Cobden +Mariatta Marius Gedminas Mark van der Wal Martin Fuzzey @@ -123,41 +144,53 @@ Matthew Boehm Matthew Desmarais Matus Valo Max Linke +Mayank Singhal Michael Krebs Michał Bultrowicz Michał Górny Mickie Betz Mike Fiedler +Min ho Kim Nathan Land +Naveen Srinivasan Naveen Yadav Neil Pilgrim +Nicholas Nadeau Nikita Bloshchanevich +Nikita Sobolev Nils Kattenbeck Noel O'Boyle +Oleg Höfling Oleh Krehel Olivier Grisel Ori Avtalion Pablo Carballo Pankaj Pandey Patrick Mezard +Pavel Tsialnou Peter Baughman Peter Ebden Peter Portante Phebe Polk Reya B +Ricardo Newbery Rodrigue Cloutier Roger Hu +Roland Illig Ross Lawley Roy Williams Russell Keith-Magee +S. Y. Lee Salvatore Zagaria Sandra Martocchia Scott Belden Sebastián Ramírez Sergey B Kirpichev +Shantanu Sigve Tjora Simon Willison Stan Hu +Stanisław Pitucha Stefan Behnel Stephan Deibel Stephan Richter @@ -167,15 +200,17 @@ Steve Leonard Steve Oswald Steve Peak Sviatoslav Sydorenko -S. Y. Lee Teake Nutma Ted Wexler Thijs Triemstra Thomas Grainger +Timo Furrer Titus Brown +Tom Gurion Valentin Lab Ville Skyttä Vince Salvino +Wonwin McBrootles Xie Yanbo Yilei "Dolee" Yang Yury Selivanov diff --git a/Makefile b/Makefile index f82f2ee27..b5276a944 100644 --- a/Makefile +++ b/Makefile @@ -20,6 +20,7 @@ clean_platform: clean: clean_platform ## Remove artifacts of test execution, installation, etc. @echo "Cleaning..." @-pip uninstall -yq coverage + @mkdir -p build # so the chmod won't fail if build doesn't exist @chmod -R 777 build @rm -rf build coverage.egg-info dist htmlcov @rm -f *.bak */*.bak */*/*.bak */*/*/*.bak */*/*/*/*.bak */*/*/*/*/*.bak @@ -31,7 +32,7 @@ clean: clean_platform ## Remove artifacts of test execution, installation, etc @rm -f tests/covmain.zip tests/zipmods.zip tests/zip1.zip @rm -rf doc/_build doc/_spell doc/sample_html_beta @rm -rf tmp - @rm -rf .cache .hypothesis .mypy_cache .pytest_cache + @rm -rf .cache .hypothesis .*_cache @rm -rf tests/actual @-make -C tests/gold/html clean diff --git a/README.rst b/README.rst index 897f8801d..5e4024d87 100644 --- a/README.rst +++ b/README.rst @@ -28,7 +28,7 @@ Coverage.py runs on these versions of Python: .. PYVERSIONS -* CPython 3.7 through 3.12.0a7 +* CPython 3.7 through 3.12.0b1 * PyPy3 7.3.11. Documentation is on `Read the Docs`_. Code repository and issue tracker are on @@ -39,6 +39,7 @@ Documentation is on `Read the Docs`_. Code repository and issue tracker are on **New in 7.x:** improved data combining; +``[run] exclude_also`` setting; ``report --format=``; type annotations. diff --git a/coverage/__init__.py b/coverage/__init__.py index 054e37dff..e3ed23223 100644 --- a/coverage/__init__.py +++ b/coverage/__init__.py @@ -14,8 +14,6 @@ # so disable its warning. # pylint: disable=useless-import-alias -import sys - from coverage.version import ( __version__ as __version__, version_info as version_info, diff --git a/coverage/annotate.py b/coverage/annotate.py index b4a02cb47..2ef89c967 100644 --- a/coverage/annotate.py +++ b/coverage/annotate.py @@ -13,7 +13,7 @@ from coverage.files import flat_rootname from coverage.misc import ensure_dir, isolate_module from coverage.plugin import FileReporter -from coverage.report import get_analysis_to_report +from coverage.report_core import get_analysis_to_report from coverage.results import Analysis from coverage.types import TMorf diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 4498eeec3..55f6c793e 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -952,13 +952,12 @@ def unglob_args(args: List[str]) -> List[str]: Use "{program_name} help <command>" for detailed help on any command. """, - "minimum_help": """\ - Code coverage for Python, version {__version__} {extension_modifier}. Use '{program_name} help' for help. - """, + "minimum_help": ( + "Code coverage for Python, version {__version__} {extension_modifier}. " + + "Use '{program_name} help' for help." + ), - "version": """\ - Coverage.py, version {__version__} {extension_modifier} - """, + "version": "Coverage.py, version {__version__} {extension_modifier}", } diff --git a/coverage/collector.py b/coverage/collector.py index 2f8c17520..ca7f5d94b 100644 --- a/coverage/collector.py +++ b/coverage/collector.py @@ -456,7 +456,7 @@ def mapped_file_dict(self, d: Mapping[str, T]) -> Dict[str, T]: assert isinstance(runtime_err, Exception) raise runtime_err - return {self.cached_mapped_file(k): v for k, v in items} + return {self.cached_mapped_file(k): v for k, v in items if v} def plugin_was_disabled(self, plugin: CoveragePlugin) -> None: """Record that `plugin` was disabled during the run.""" diff --git a/coverage/control.py b/coverage/control.py index e405a5bf4..723c4d876 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -29,7 +29,9 @@ from coverage.config import CoverageConfig, read_coverage_config from coverage.context import should_start_context_test_function, combine_context_switchers from coverage.data import CoverageData, combine_parallel_data -from coverage.debug import DebugControl, NoDebugging, short_stack, write_formatted_info +from coverage.debug import ( + DebugControl, NoDebugging, short_stack, write_formatted_info, relevant_environment_display +) from coverage.disposition import disposition_debug_msg from coverage.exceptions import ConfigError, CoverageException, CoverageWarning, PluginError from coverage.files import PathAliases, abs_file, relative_filename, set_relative_directory @@ -37,15 +39,15 @@ from coverage.inorout import InOrOut from coverage.jsonreport import JsonReporter from coverage.lcovreport import LcovReporter -from coverage.misc import bool_or_none, join_regex, human_sorted +from coverage.misc import bool_or_none, join_regex from coverage.misc import DefaultValue, ensure_dir_for_file, isolate_module from coverage.multiproc import patch_multiprocessing from coverage.plugin import FileReporter from coverage.plugin_support import Plugins from coverage.python import PythonFileReporter -from coverage.report import render_report +from coverage.report import SummaryReporter +from coverage.report_core import render_report from coverage.results import Analysis -from coverage.summary import SummaryReporter from coverage.types import ( FilePath, TConfigurable, TConfigSectionIn, TConfigValueIn, TConfigValueOut, TFileDisposition, TLineNo, TMorf, @@ -1298,14 +1300,7 @@ def plugin_info(plugins: List[Any]) -> List[str]: ("pid", os.getpid()), ("cwd", os.getcwd()), ("path", sys.path), - ("environment", human_sorted( - f"{k} = {v}" - for k, v in os.environ.items() - if ( - any(slug in k for slug in ("COV", "PY")) or - (k in ("HOME", "TEMP", "TMP")) - ) - )), + ("environment", [f"{k} = {v}" for k, v in relevant_environment_display(os.environ)]), ("command_line", " ".join(getattr(sys, "argv", ["-none-"]))), ] diff --git a/coverage/debug.py b/coverage/debug.py index 3ef6dae8a..3484792e2 100644 --- a/coverage/debug.py +++ b/coverage/debug.py @@ -12,16 +12,18 @@ import itertools import os import pprint +import re import reprlib import sys import types import _thread from typing import ( - Any, Callable, IO, Iterable, Iterator, Optional, List, Tuple, cast, + cast, + Any, Callable, IO, Iterable, Iterator, Mapping, Optional, List, Tuple, ) -from coverage.misc import isolate_module +from coverage.misc import human_sorted_items, isolate_module from coverage.types import TWritable os = isolate_module(os) @@ -489,3 +491,34 @@ def _clean_stack_line(s: str) -> str: # pragma: debugging s = s.replace(os.path.dirname(os.__file__) + "/", "") s = s.replace(sys.prefix + "/", "") return s + + +def relevant_environment_display(env: Mapping[str, str]) -> List[Tuple[str, str]]: + """Filter environment variables for a debug display. + + Select variables to display (with COV or PY in the name, or HOME, TEMP, or + TMP), and also cloak sensitive values with asterisks. + + Arguments: + env: a dict of environment variable names and values. + + Returns: + A list of pairs (name, value) to show. + + """ + slugs = {"COV", "PY"} + include = {"HOME", "TEMP", "TMP"} + cloak = {"API", "TOKEN", "KEY", "SECRET", "PASS", "SIGNATURE"} + + to_show = [] + for name, val in env.items(): + keep = False + if name in include: + keep = True + elif any(slug in name for slug in slugs): + keep = True + if keep: + if any(slug in name for slug in cloak): + val = re.sub(r"\w", "*", val) + to_show.append((name, val)) + return human_sorted_items(to_show) diff --git a/coverage/env.py b/coverage/env.py index bdc2c7854..3370970e3 100644 --- a/coverage/env.py +++ b/coverage/env.py @@ -40,13 +40,10 @@ class PYBEHAVIOR: # Does Python conform to PEP626, Precise line numbers for debugging and other tools. # https://www.python.org/dev/peps/pep-0626 - pep626 = CPYTHON and (PYVERSION > (3, 10, 0, "alpha", 4)) + pep626 = (PYVERSION > (3, 10, 0, "alpha", 4)) # Is "if __debug__" optimized away? - if PYPY: - optimize_if_debug = True - else: - optimize_if_debug = not pep626 + optimize_if_debug = not pep626 # Is "if not __debug__" optimized away? The exact details have changed # across versions. @@ -137,6 +134,10 @@ class PYBEHAVIOR: # only a 0-number line, which is ignored, giving a truly empty module. empty_is_empty = (PYVERSION >= (3, 11, 0, "beta", 4)) + # Are comprehensions inlined (new) or compiled as called functions (old)? + # Changed in https://github.com/python/cpython/pull/101441 + comprehensions_are_functions = (PYVERSION <= (3, 12, 0, "alpha", 7, 0)) + # Coverage.py specifics. # Are we using the C-implemented trace function? diff --git a/coverage/files.py b/coverage/files.py index 2a1177340..925d57723 100644 --- a/coverage/files.py +++ b/coverage/files.py @@ -209,9 +209,8 @@ def prep_patterns(patterns: Iterable[str]) -> List[str]: """ prepped = [] for p in patterns or []: - if p.startswith(("*", "?")): - prepped.append(p) - else: + prepped.append(p) + if not p.startswith(("*", "?")): prepped.append(abs_file(p)) return prepped diff --git a/coverage/html.py b/coverage/html.py index 570760604..532eb66c2 100644 --- a/coverage/html.py +++ b/coverage/html.py @@ -22,8 +22,8 @@ from coverage.exceptions import NoDataError from coverage.files import flat_rootname from coverage.misc import ensure_dir, file_be_gone, Hasher, isolate_module, format_local_datetime -from coverage.misc import human_sorted, plural -from coverage.report import get_analysis_to_report +from coverage.misc import human_sorted, plural, stdout_link +from coverage.report_core import get_analysis_to_report from coverage.results import Analysis, Numbers from coverage.templite import Templite from coverage.types import TLineNo, TMorf @@ -493,7 +493,9 @@ def index_file(self, first_html: str, final_html: str) -> None: index_file = os.path.join(self.directory, "index.html") write_html(index_file, html) - self.coverage._message(f"Wrote HTML report to {index_file}") + + print_href = stdout_link(index_file, f"file://{os.path.abspath(index_file)}") + self.coverage._message(f"Wrote HTML report to {print_href}") # Write the latest hashes for next time. self.incr.write() diff --git a/coverage/inorout.py b/coverage/inorout.py index ff46bac0d..d2dbdcdf7 100644 --- a/coverage/inorout.py +++ b/coverage/inorout.py @@ -528,7 +528,7 @@ def find_possibly_unexecuted_files(self) -> Iterable[Tuple[str, Optional[str]]]: Yields pairs: file path, and responsible plug-in name. """ for pkg in self.source_pkgs: - if (not pkg in sys.modules or + if (pkg not in sys.modules or not module_has_file(sys.modules[pkg])): continue pkg_file = source_for_file(cast(str, sys.modules[pkg].__file__)) diff --git a/coverage/jsonreport.py b/coverage/jsonreport.py index 24e33585c..9780e261a 100644 --- a/coverage/jsonreport.py +++ b/coverage/jsonreport.py @@ -12,7 +12,7 @@ from typing import Any, Dict, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING from coverage import __version__ -from coverage.report import get_analysis_to_report +from coverage.report_core import get_analysis_to_report from coverage.results import Analysis, Numbers from coverage.types import TMorf, TLineNo diff --git a/coverage/lcovreport.py b/coverage/lcovreport.py index 7d72e8135..3da164d5d 100644 --- a/coverage/lcovreport.py +++ b/coverage/lcovreport.py @@ -5,20 +5,25 @@ from __future__ import annotations -import sys import base64 -from hashlib import md5 +import hashlib +import sys from typing import IO, Iterable, Optional, TYPE_CHECKING from coverage.plugin import FileReporter -from coverage.report import get_analysis_to_report +from coverage.report_core import get_analysis_to_report from coverage.results import Analysis, Numbers from coverage.types import TMorf if TYPE_CHECKING: from coverage import Coverage - from coverage.data import CoverageData + + +def line_hash(line: str) -> str: + """Produce a hash of a source line for use in the LCOV file.""" + hashed = hashlib.md5(line.encode("utf-8")).digest() + return base64.b64encode(hashed).decode("ascii").rstrip("=") class LcovReporter: @@ -69,17 +74,17 @@ def get_lcov(self, fr: FileReporter, analysis: Analysis, outfile: IO[str]) -> No # characters of the encoding ("==") are removed from the hash to # allow genhtml to run on the resulting lcov file. if source_lines: - line = source_lines[covered-1].encode("utf-8") + if covered-1 >= len(source_lines): + break + line = source_lines[covered-1] else: - line = b"" - hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=") - outfile.write(f"DA:{covered},1,{hashed}\n") + line = "" + outfile.write(f"DA:{covered},1,{line_hash(line)}\n") for missed in sorted(analysis.missing): assert source_lines - line = source_lines[missed-1].encode("utf-8") - hashed = base64.b64encode(md5(line).digest()).decode().rstrip("=") - outfile.write(f"DA:{missed},0,{hashed}\n") + line = source_lines[missed-1] + outfile.write(f"DA:{missed},0,{line_hash(line)}\n") outfile.write(f"LF:{analysis.numbers.n_statements}\n") outfile.write(f"LH:{analysis.numbers.n_executed}\n") diff --git a/coverage/misc.py b/coverage/misc.py index 8cefa12e0..908b0dd24 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -386,3 +386,15 @@ def plural(n: int, thing: str = "", things: str = "") -> str: return thing else: return things or (thing + "s") + + +def stdout_link(text: str, url: str) -> str: + """Format text+url as a clickable link for stdout. + + If attached to a terminal, use escape sequences. Otherwise, just return + the text. + """ + if hasattr(sys.stdout, "isatty") and sys.stdout.isatty(): + return f"\033]8;;{url}\a{text}\033]8;;\a" + else: + return text diff --git a/coverage/parser.py b/coverage/parser.py index e653a9ccd..51a5a52da 100644 --- a/coverage/parser.py +++ b/coverage/parser.py @@ -251,7 +251,7 @@ def parse_source(self) -> None: """ try: self._raw_parse() - except (tokenize.TokenError, IndentationError) as err: + except (tokenize.TokenError, IndentationError, SyntaxError) as err: if hasattr(err, "lineno"): lineno = err.lineno # IndentationError else: @@ -1343,9 +1343,10 @@ def _code_object__ClassDef(self, node: ast.ClassDef) -> None: _code_object__Lambda = _make_expression_code_method("lambda") _code_object__GeneratorExp = _make_expression_code_method("generator expression") - _code_object__DictComp = _make_expression_code_method("dictionary comprehension") - _code_object__SetComp = _make_expression_code_method("set comprehension") - _code_object__ListComp = _make_expression_code_method("list comprehension") + if env.PYBEHAVIOR.comprehensions_are_functions: + _code_object__DictComp = _make_expression_code_method("dictionary comprehension") + _code_object__SetComp = _make_expression_code_method("set comprehension") + _code_object__ListComp = _make_expression_code_method("list comprehension") # Code only used when dumping the AST for debugging. diff --git a/coverage/report.py b/coverage/report.py index 09eed0a82..e1c7a071d 100644 --- a/coverage/report.py +++ b/coverage/report.py @@ -1,117 +1,281 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Reporter foundation for coverage.py.""" +"""Summary reporting""" from __future__ import annotations import sys -from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING +from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING -from coverage.exceptions import NoDataError, NotPython -from coverage.files import prep_patterns, GlobMatcher -from coverage.misc import ensure_dir_for_file, file_be_gone +from coverage.exceptions import ConfigError, NoDataError +from coverage.misc import human_sorted_items from coverage.plugin import FileReporter -from coverage.results import Analysis -from coverage.types import Protocol, TMorf +from coverage.report_core import get_analysis_to_report +from coverage.results import Analysis, Numbers +from coverage.types import TMorf if TYPE_CHECKING: from coverage import Coverage -class Reporter(Protocol): - """What we expect of reporters.""" - - report_type: str - - def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: - """Generate a report of `morfs`, written to `outfile`.""" - - -def render_report( - output_path: str, - reporter: Reporter, - morfs: Optional[Iterable[TMorf]], - msgfn: Callable[[str], None], -) -> float: - """Run a one-file report generator, managing the output file. - - This function ensures the output file is ready to be written to. Then writes - the report to it. Then closes the file and cleans up. - - """ - file_to_close = None - delete_file = False - - if output_path == "-": - outfile = sys.stdout - else: - # Ensure that the output directory is created; done here because this - # report pre-opens the output file. HtmlReporter does this on its own - # because its task is more complex, being multiple files. - ensure_dir_for_file(output_path) - outfile = open(output_path, "w", encoding="utf-8") - file_to_close = outfile - delete_file = True - - try: - ret = reporter.report(morfs, outfile=outfile) - if file_to_close is not None: - msgfn(f"Wrote {reporter.report_type} to {output_path}") - delete_file = False - return ret - finally: - if file_to_close is not None: - file_to_close.close() - if delete_file: - file_be_gone(output_path) # pragma: part covered (doesn't return) - - -def get_analysis_to_report( - coverage: Coverage, - morfs: Optional[Iterable[TMorf]], -) -> Iterator[Tuple[FileReporter, Analysis]]: - """Get the files to report on. - - For each morf in `morfs`, if it should be reported on (based on the omit - and include configuration options), yield a pair, the `FileReporter` and - `Analysis` for the morf. - - """ - file_reporters = coverage._get_file_reporters(morfs) - config = coverage.config - - if config.report_include: - matcher = GlobMatcher(prep_patterns(config.report_include), "report_include") - file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)] - - if config.report_omit: - matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit") - file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] - - if not file_reporters: - raise NoDataError("No data to report.") - - for fr in sorted(file_reporters): - try: - analysis = coverage._analyze(fr) - except NotPython: - # Only report errors for .py files, and only if we didn't - # explicitly suppress those errors. - # NotPython is only raised by PythonFileReporter, which has a - # should_be_python() method. - if fr.should_be_python(): # type: ignore[attr-defined] - if config.ignore_errors: - msg = f"Couldn't parse Python file '{fr.filename}'" - coverage._warn(msg, slug="couldnt-parse") - else: - raise - except Exception as exc: - if config.ignore_errors: - msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip() - coverage._warn(msg, slug="couldnt-parse") +class SummaryReporter: + """A reporter for writing the summary report.""" + + def __init__(self, coverage: Coverage) -> None: + self.coverage = coverage + self.config = self.coverage.config + self.branches = coverage.get_data().has_arcs() + self.outfile: Optional[IO[str]] = None + self.output_format = self.config.format or "text" + if self.output_format not in {"text", "markdown", "total"}: + raise ConfigError(f"Unknown report format choice: {self.output_format!r}") + self.fr_analysis: List[Tuple[FileReporter, Analysis]] = [] + self.skipped_count = 0 + self.empty_count = 0 + self.total = Numbers(precision=self.config.precision) + + def write(self, line: str) -> None: + """Write a line to the output, adding a newline.""" + assert self.outfile is not None + self.outfile.write(line.rstrip()) + self.outfile.write("\n") + + def write_items(self, items: Iterable[str]) -> None: + """Write a list of strings, joined together.""" + self.write("".join(items)) + + def _report_text( + self, + header: List[str], + lines_values: List[List[Any]], + total_line: List[Any], + end_lines: List[str], + ) -> None: + """Internal method that prints report data in text format. + + `header` is a list with captions. + `lines_values` is list of lists of sortable values. + `total_line` is a list with values of the total line. + `end_lines` is a list of ending lines with information about skipped files. + + """ + # Prepare the formatting strings, header, and column sorting. + max_name = max([len(line[0]) for line in lines_values] + [5]) + 1 + max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1 + max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values]) + formats = dict( + Name="{:{name_len}}", + Stmts="{:>7}", + Miss="{:>7}", + Branch="{:>7}", + BrPart="{:>7}", + Cover="{:>{n}}", + Missing="{:>10}", + ) + header_items = [ + formats[item].format(item, name_len=max_name, n=max_n) + for item in header + ] + header_str = "".join(header_items) + rule = "-" * len(header_str) + + # Write the header + self.write(header_str) + self.write(rule) + + formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}") + for values in lines_values: + # build string with line values + line_items = [ + formats[item].format(str(value), + name_len=max_name, n=max_n-1) for item, value in zip(header, values) + ] + self.write_items(line_items) + + # Write a TOTAL line + if lines_values: + self.write(rule) + + line_items = [ + formats[item].format(str(value), + name_len=max_name, n=max_n-1) for item, value in zip(header, total_line) + ] + self.write_items(line_items) + + for end_line in end_lines: + self.write(end_line) + + def _report_markdown( + self, + header: List[str], + lines_values: List[List[Any]], + total_line: List[Any], + end_lines: List[str], + ) -> None: + """Internal method that prints report data in markdown format. + + `header` is a list with captions. + `lines_values` is a sorted list of lists containing coverage information. + `total_line` is a list with values of the total line. + `end_lines` is a list of ending lines with information about skipped files. + + """ + # Prepare the formatting strings, header, and column sorting. + max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0) + max_name = max(max_name, len("**TOTAL**")) + 1 + formats = dict( + Name="| {:{name_len}}|", + Stmts="{:>9} |", + Miss="{:>9} |", + Branch="{:>9} |", + BrPart="{:>9} |", + Cover="{:>{n}} |", + Missing="{:>10} |", + ) + max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover ")) + header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header] + header_str = "".join(header_items) + rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] + + ["-: |".rjust(len(item)-1, "-") for item in header_items[1:]] + ) + + # Write the header + self.write(header_str) + self.write(rule_str) + + for values in lines_values: + # build string with line values + formats.update(dict(Cover="{:>{n}}% |")) + line_items = [ + formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1) + for item, value in zip(header, values) + ] + self.write_items(line_items) + + # Write the TOTAL line + formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |")) + total_line_items: List[str] = [] + for item, value in zip(header, total_line): + if value == "": + insert = value + elif item == "Cover": + insert = f" **{value}%**" else: - raise + insert = f" **{value}**" + total_line_items += formats[item].format(insert, name_len=max_name, n=max_n) + self.write_items(total_line_items) + for end_line in end_lines: + self.write(end_line) + + def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float: + """Writes a report summarizing coverage statistics per module. + + `outfile` is a text-mode file object to write the summary to. + + """ + self.outfile = outfile or sys.stdout + + self.coverage.get_data().set_query_contexts(self.config.report_contexts) + for fr, analysis in get_analysis_to_report(self.coverage, morfs): + self.report_one_file(fr, analysis) + + if not self.total.n_files and not self.skipped_count: + raise NoDataError("No data to report.") + + if self.output_format == "total": + self.write(self.total.pc_covered_str) + else: + self.tabular_report() + + return self.total.pc_covered + + def tabular_report(self) -> None: + """Writes tabular report formats.""" + # Prepare the header line and column sorting. + header = ["Name", "Stmts", "Miss"] + if self.branches: + header += ["Branch", "BrPart"] + header += ["Cover"] + if self.config.show_missing: + header += ["Missing"] + + column_order = dict(name=0, stmts=1, miss=2, cover=-1) + if self.branches: + column_order.update(dict(branch=3, brpart=4)) + + # `lines_values` is list of lists of sortable values. + lines_values = [] + + for (fr, analysis) in self.fr_analysis: + nums = analysis.numbers + + args = [fr.relative_filename(), nums.n_statements, nums.n_missing] + if self.branches: + args += [nums.n_branches, nums.n_partial_branches] + args += [nums.pc_covered_str] + if self.config.show_missing: + args += [analysis.missing_formatted(branches=True)] + args += [nums.pc_covered] + lines_values.append(args) + + # Line sorting. + sort_option = (self.config.sort or "name").lower() + reverse = False + if sort_option[0] == "-": + reverse = True + sort_option = sort_option[1:] + elif sort_option[0] == "+": + sort_option = sort_option[1:] + sort_idx = column_order.get(sort_option) + if sort_idx is None: + raise ConfigError(f"Invalid sorting option: {self.config.sort!r}") + if sort_option == "name": + lines_values = human_sorted_items(lines_values, reverse=reverse) + else: + lines_values.sort( + key=lambda line: (line[sort_idx], line[0]), # type: ignore[index] + reverse=reverse, + ) + + # Calculate total if we had at least one file. + total_line = ["TOTAL", self.total.n_statements, self.total.n_missing] + if self.branches: + total_line += [self.total.n_branches, self.total.n_partial_branches] + total_line += [self.total.pc_covered_str] + if self.config.show_missing: + total_line += [""] + + # Create other final lines. + end_lines = [] + if self.config.skip_covered and self.skipped_count: + file_suffix = "s" if self.skipped_count>1 else "" + end_lines.append( + f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage." + ) + if self.config.skip_empty and self.empty_count: + file_suffix = "s" if self.empty_count > 1 else "" + end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.") + + if self.output_format == "markdown": + formatter = self._report_markdown + else: + formatter = self._report_text + formatter(header, lines_values, total_line, end_lines) + + def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None: + """Report on just one file, the callback from report().""" + nums = analysis.numbers + self.total += nums + + no_missing_lines = (nums.n_missing == 0) + no_missing_branches = (nums.n_partial_branches == 0) + if self.config.skip_covered and no_missing_lines and no_missing_branches: + # Don't report on 100% files. + self.skipped_count += 1 + elif self.config.skip_empty and nums.n_statements == 0: + # Don't report on empty files. + self.empty_count += 1 else: - yield (fr, analysis) + self.fr_analysis.append((fr, analysis)) diff --git a/coverage/report_core.py b/coverage/report_core.py new file mode 100644 index 000000000..09eed0a82 --- /dev/null +++ b/coverage/report_core.py @@ -0,0 +1,117 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Reporter foundation for coverage.py.""" + +from __future__ import annotations + +import sys + +from typing import Callable, Iterable, Iterator, IO, Optional, Tuple, TYPE_CHECKING + +from coverage.exceptions import NoDataError, NotPython +from coverage.files import prep_patterns, GlobMatcher +from coverage.misc import ensure_dir_for_file, file_be_gone +from coverage.plugin import FileReporter +from coverage.results import Analysis +from coverage.types import Protocol, TMorf + +if TYPE_CHECKING: + from coverage import Coverage + + +class Reporter(Protocol): + """What we expect of reporters.""" + + report_type: str + + def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: + """Generate a report of `morfs`, written to `outfile`.""" + + +def render_report( + output_path: str, + reporter: Reporter, + morfs: Optional[Iterable[TMorf]], + msgfn: Callable[[str], None], +) -> float: + """Run a one-file report generator, managing the output file. + + This function ensures the output file is ready to be written to. Then writes + the report to it. Then closes the file and cleans up. + + """ + file_to_close = None + delete_file = False + + if output_path == "-": + outfile = sys.stdout + else: + # Ensure that the output directory is created; done here because this + # report pre-opens the output file. HtmlReporter does this on its own + # because its task is more complex, being multiple files. + ensure_dir_for_file(output_path) + outfile = open(output_path, "w", encoding="utf-8") + file_to_close = outfile + delete_file = True + + try: + ret = reporter.report(morfs, outfile=outfile) + if file_to_close is not None: + msgfn(f"Wrote {reporter.report_type} to {output_path}") + delete_file = False + return ret + finally: + if file_to_close is not None: + file_to_close.close() + if delete_file: + file_be_gone(output_path) # pragma: part covered (doesn't return) + + +def get_analysis_to_report( + coverage: Coverage, + morfs: Optional[Iterable[TMorf]], +) -> Iterator[Tuple[FileReporter, Analysis]]: + """Get the files to report on. + + For each morf in `morfs`, if it should be reported on (based on the omit + and include configuration options), yield a pair, the `FileReporter` and + `Analysis` for the morf. + + """ + file_reporters = coverage._get_file_reporters(morfs) + config = coverage.config + + if config.report_include: + matcher = GlobMatcher(prep_patterns(config.report_include), "report_include") + file_reporters = [fr for fr in file_reporters if matcher.match(fr.filename)] + + if config.report_omit: + matcher = GlobMatcher(prep_patterns(config.report_omit), "report_omit") + file_reporters = [fr for fr in file_reporters if not matcher.match(fr.filename)] + + if not file_reporters: + raise NoDataError("No data to report.") + + for fr in sorted(file_reporters): + try: + analysis = coverage._analyze(fr) + except NotPython: + # Only report errors for .py files, and only if we didn't + # explicitly suppress those errors. + # NotPython is only raised by PythonFileReporter, which has a + # should_be_python() method. + if fr.should_be_python(): # type: ignore[attr-defined] + if config.ignore_errors: + msg = f"Couldn't parse Python file '{fr.filename}'" + coverage._warn(msg, slug="couldnt-parse") + else: + raise + except Exception as exc: + if config.ignore_errors: + msg = f"Couldn't parse '{fr.filename}': {exc}".rstrip() + coverage._warn(msg, slug="couldnt-parse") + else: + raise + else: + yield (fr, analysis) diff --git a/coverage/summary.py b/coverage/summary.py deleted file mode 100644 index 5d373ec52..000000000 --- a/coverage/summary.py +++ /dev/null @@ -1,281 +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 - -"""Summary reporting""" - -from __future__ import annotations - -import sys - -from typing import Any, IO, Iterable, List, Optional, Tuple, TYPE_CHECKING - -from coverage.exceptions import ConfigError, NoDataError -from coverage.misc import human_sorted_items -from coverage.plugin import FileReporter -from coverage.report import get_analysis_to_report -from coverage.results import Analysis, Numbers -from coverage.types import TMorf - -if TYPE_CHECKING: - from coverage import Coverage - - -class SummaryReporter: - """A reporter for writing the summary report.""" - - def __init__(self, coverage: Coverage) -> None: - self.coverage = coverage - self.config = self.coverage.config - self.branches = coverage.get_data().has_arcs() - self.outfile: Optional[IO[str]] = None - self.output_format = self.config.format or "text" - if self.output_format not in {"text", "markdown", "total"}: - raise ConfigError(f"Unknown report format choice: {self.output_format!r}") - self.fr_analysis: List[Tuple[FileReporter, Analysis]] = [] - self.skipped_count = 0 - self.empty_count = 0 - self.total = Numbers(precision=self.config.precision) - - def write(self, line: str) -> None: - """Write a line to the output, adding a newline.""" - assert self.outfile is not None - self.outfile.write(line.rstrip()) - self.outfile.write("\n") - - def write_items(self, items: Iterable[str]) -> None: - """Write a list of strings, joined together.""" - self.write("".join(items)) - - def _report_text( - self, - header: List[str], - lines_values: List[List[Any]], - total_line: List[Any], - end_lines: List[str], - ) -> None: - """Internal method that prints report data in text format. - - `header` is a list with captions. - `lines_values` is list of lists of sortable values. - `total_line` is a list with values of the total line. - `end_lines` is a list of ending lines with information about skipped files. - - """ - # Prepare the formatting strings, header, and column sorting. - max_name = max([len(line[0]) for line in lines_values] + [5]) + 1 - max_n = max(len(total_line[header.index("Cover")]) + 2, len(" Cover")) + 1 - max_n = max([max_n] + [len(line[header.index("Cover")]) + 2 for line in lines_values]) - formats = dict( - Name="{:{name_len}}", - Stmts="{:>7}", - Miss="{:>7}", - Branch="{:>7}", - BrPart="{:>7}", - Cover="{:>{n}}", - Missing="{:>10}", - ) - header_items = [ - formats[item].format(item, name_len=max_name, n=max_n) - for item in header - ] - header_str = "".join(header_items) - rule = "-" * len(header_str) - - # Write the header - self.write(header_str) - self.write(rule) - - formats.update(dict(Cover="{:>{n}}%"), Missing=" {:9}") - for values in lines_values: - # build string with line values - line_items = [ - formats[item].format(str(value), - name_len=max_name, n=max_n-1) for item, value in zip(header, values) - ] - self.write_items(line_items) - - # Write a TOTAL line - if lines_values: - self.write(rule) - - line_items = [ - formats[item].format(str(value), - name_len=max_name, n=max_n-1) for item, value in zip(header, total_line) - ] - self.write_items(line_items) - - for end_line in end_lines: - self.write(end_line) - - def _report_markdown( - self, - header: List[str], - lines_values: List[List[Any]], - total_line: List[Any], - end_lines: List[str], - ) -> None: - """Internal method that prints report data in markdown format. - - `header` is a list with captions. - `lines_values` is a sorted list of lists containing coverage information. - `total_line` is a list with values of the total line. - `end_lines` is a list of ending lines with information about skipped files. - - """ - # Prepare the formatting strings, header, and column sorting. - max_name = max((len(line[0].replace("_", "\\_")) for line in lines_values), default=0) - max_name = max(max_name, len("**TOTAL**")) + 1 - formats = dict( - Name="| {:{name_len}}|", - Stmts="{:>9} |", - Miss="{:>9} |", - Branch="{:>9} |", - BrPart="{:>9} |", - Cover="{:>{n}} |", - Missing="{:>10} |", - ) - max_n = max(len(total_line[header.index("Cover")]) + 6, len(" Cover ")) - header_items = [formats[item].format(item, name_len=max_name, n=max_n) for item in header] - header_str = "".join(header_items) - rule_str = "|" + " ".join(["- |".rjust(len(header_items[0])-1, "-")] + - ["-: |".rjust(len(item)-1, "-") for item in header_items[1:]] - ) - - # Write the header - self.write(header_str) - self.write(rule_str) - - for values in lines_values: - # build string with line values - formats.update(dict(Cover="{:>{n}}% |")) - line_items = [ - formats[item].format(str(value).replace("_", "\\_"), name_len=max_name, n=max_n-1) - for item, value in zip(header, values) - ] - self.write_items(line_items) - - # Write the TOTAL line - formats.update(dict(Name="|{:>{name_len}} |", Cover="{:>{n}} |")) - total_line_items: List[str] = [] - for item, value in zip(header, total_line): - if value == "": - insert = value - elif item == "Cover": - insert = f" **{value}%**" - else: - insert = f" **{value}**" - total_line_items += formats[item].format(insert, name_len=max_name, n=max_n) - self.write_items(total_line_items) - for end_line in end_lines: - self.write(end_line) - - def report(self, morfs: Optional[Iterable[TMorf]], outfile: Optional[IO[str]] = None) -> float: - """Writes a report summarizing coverage statistics per module. - - `outfile` is a text-mode file object to write the summary to. - - """ - self.outfile = outfile or sys.stdout - - self.coverage.get_data().set_query_contexts(self.config.report_contexts) - for fr, analysis in get_analysis_to_report(self.coverage, morfs): - self.report_one_file(fr, analysis) - - if not self.total.n_files and not self.skipped_count: - raise NoDataError("No data to report.") - - if self.output_format == "total": - self.write(self.total.pc_covered_str) - else: - self.tabular_report() - - return self.total.pc_covered - - def tabular_report(self) -> None: - """Writes tabular report formats.""" - # Prepare the header line and column sorting. - header = ["Name", "Stmts", "Miss"] - if self.branches: - header += ["Branch", "BrPart"] - header += ["Cover"] - if self.config.show_missing: - header += ["Missing"] - - column_order = dict(name=0, stmts=1, miss=2, cover=-1) - if self.branches: - column_order.update(dict(branch=3, brpart=4)) - - # `lines_values` is list of lists of sortable values. - lines_values = [] - - for (fr, analysis) in self.fr_analysis: - nums = analysis.numbers - - args = [fr.relative_filename(), nums.n_statements, nums.n_missing] - if self.branches: - args += [nums.n_branches, nums.n_partial_branches] - args += [nums.pc_covered_str] - if self.config.show_missing: - args += [analysis.missing_formatted(branches=True)] - args += [nums.pc_covered] - lines_values.append(args) - - # Line sorting. - sort_option = (self.config.sort or "name").lower() - reverse = False - if sort_option[0] == "-": - reverse = True - sort_option = sort_option[1:] - elif sort_option[0] == "+": - sort_option = sort_option[1:] - sort_idx = column_order.get(sort_option) - if sort_idx is None: - raise ConfigError(f"Invalid sorting option: {self.config.sort!r}") - if sort_option == "name": - lines_values = human_sorted_items(lines_values, reverse=reverse) - else: - lines_values.sort( - key=lambda line: (line[sort_idx], line[0]), # type: ignore[index] - reverse=reverse, - ) - - # Calculate total if we had at least one file. - total_line = ["TOTAL", self.total.n_statements, self.total.n_missing] - if self.branches: - total_line += [self.total.n_branches, self.total.n_partial_branches] - total_line += [self.total.pc_covered_str] - if self.config.show_missing: - total_line += [""] - - # Create other final lines. - end_lines = [] - if self.config.skip_covered and self.skipped_count: - file_suffix = "s" if self.skipped_count>1 else "" - end_lines.append( - f"\n{self.skipped_count} file{file_suffix} skipped due to complete coverage." - ) - if self.config.skip_empty and self.empty_count: - file_suffix = "s" if self.empty_count > 1 else "" - end_lines.append(f"\n{self.empty_count} empty file{file_suffix} skipped.") - - if self.output_format == "markdown": - formatter = self._report_markdown - else: - formatter = self._report_text - formatter(header, lines_values, total_line, end_lines) - - def report_one_file(self, fr: FileReporter, analysis: Analysis) -> None: - """Report on just one file, the callback from report().""" - nums = analysis.numbers - self.total += nums - - no_missing_lines = (nums.n_missing == 0) - no_missing_branches = (nums.n_partial_branches == 0) - if self.config.skip_covered and no_missing_lines and no_missing_branches: - # Don't report on 100% files. - self.skipped_count += 1 - elif self.config.skip_empty and nums.n_statements == 0: - # Don't report on empty files. - self.empty_count += 1 - else: - self.fr_analysis.append((fr, analysis)) diff --git a/coverage/version.py b/coverage/version.py index 9cf7d9d19..c48974967 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -8,7 +8,7 @@ # version_info: same semantics as sys.version_info. # _dev: the .devN suffix if any. -version_info = (7, 2, 3, "final", 0) +version_info = (7, 2, 7, "final", 0) _dev = 0 diff --git a/coverage/xmlreport.py b/coverage/xmlreport.py index 2c8fd0cc1..819b4c6bc 100644 --- a/coverage/xmlreport.py +++ b/coverage/xmlreport.py @@ -12,12 +12,12 @@ import xml.dom.minidom from dataclasses import dataclass -from typing import Any, Dict, IO, Iterable, Optional, TYPE_CHECKING, cast +from typing import Any, Dict, IO, Iterable, Optional, TYPE_CHECKING from coverage import __version__, files from coverage.misc import isolate_module, human_sorted, human_sorted_items from coverage.plugin import FileReporter -from coverage.report import get_analysis_to_report +from coverage.report_core import get_analysis_to_report from coverage.results import Analysis from coverage.types import TMorf from coverage.version import __url__ @@ -67,7 +67,9 @@ def __init__(self, coverage: Coverage) -> None: if self.config.source: for src in self.config.source: if os.path.exists(src): - if not self.config.relative_files: + if self.config.relative_files: + src = src.rstrip(r"\/") + else: src = files.canonical_filename(src) self.source_paths.add(src) self.packages: Dict[str, PackageData] = {} @@ -255,4 +257,4 @@ def xml_file(self, fr: FileReporter, analysis: Analysis, has_arcs: bool) -> None def serialize_xml(dom: xml.dom.minidom.Document) -> str: """Serialize a minidom node to XML.""" - return cast(str, dom.toprettyxml()) + return dom.toprettyxml() diff --git a/doc/cmd.rst b/doc/cmd.rst index 0704e940a..7db6746a8 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -624,9 +624,10 @@ Here's a `sample report`__. __ https://nedbatchelder.com/files/sample_coverage_html/index.html -Lines are highlighted green for executed, red for missing, and gray for -excluded. The counts at the top of the file are buttons to turn on and off -the highlighting. +Lines are highlighted: green for executed, red for missing, and gray for +excluded. If you've used branch coverage, partial branches are yellow. The +colored counts at the top of the file are buttons to turn on and off the +highlighting. A number of keyboard shortcuts are available for navigating the report. Click the keyboard icon in the upper right to see the complete list. diff --git a/doc/conf.py b/doc/conf.py index f6310b577..bee8c14b2 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -66,11 +66,11 @@ # @@@ editable copyright = "2009–2023, Ned Batchelder" # pylint: disable=redefined-builtin # The short X.Y.Z version. -version = "7.2.3" +version = "7.2.7" # The full version, including alpha/beta/rc tags. -release = "7.2.3" +release = "7.2.7" # The date of release, in "monthname day, year" format. -release_date = "April 6, 2023" +release_date = "May 29, 2023" # @@@ end rst_epilog = """ diff --git a/doc/config.rst b/doc/config.rst index 152b3af48..0100d89e1 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -79,10 +79,7 @@ Here's a sample configuration file:: [report] # Regexes for lines to exclude from consideration - exclude_lines = - # Have to re-enable the standard pragma - pragma: no cover - + exclude_also = # Don't complain about missing debug-only code: def __repr__ if self\.debug @@ -375,6 +372,19 @@ See :ref:`cmd_combine_remapping` and :ref:`source_glob` for more information. Settings common to many kinds of reporting. +.. _config_report_exclude_also: + +[report] exclude_also +..................... + +(multi-string) A list of regular expressions. This setting is similar to +:ref:`config_report_exclude_lines`: it specifies patterns for lines to exclude +from reporting. This setting is preferred, because it will preserve the +default exclude patterns instead of overwriting them. + +.. versionadded:: 7.2.0 + + .. _config_report_exclude_lines: [report] exclude_lines @@ -384,7 +394,9 @@ Settings common to many kinds of reporting. containing a match for one of these regexes is excluded from being reported as missing. More details are in :ref:`excluding`. If you use this option, you are replacing all the exclude regexes, so you'll need to also supply the -"pragma: no cover" regex if you still want to use it. +"pragma: no cover" regex if you still want to use it. The +:ref:`config_report_exclude_also` setting can be used to specify patterns +without overwriting the default set. You can exclude lines introducing blocks, and the entire block is excluded. If you exclude a ``def`` line or decorator line, the entire function is excluded. @@ -395,19 +407,6 @@ you'll exclude any line with three or more of any character. If you write ``pass``, you'll also exclude the line ``my_pass="foo"``, and so on. -.. _config_report_exclude_also: - -[report] exclude_also -..................... - -(multi-string) A list of regular expressions. This setting is the same as -:ref:`config_report_exclude_lines`: it adds patterns for lines to exclude from -reporting. This setting will preserve the default exclude patterns instead of -overwriting them. - -.. versionadded:: 7.2.0 - - .. _config_report_fail_under: [report] fail_under diff --git a/doc/excluding.rst b/doc/excluding.rst index 4651e6bba..e9d28f156 100644 --- a/doc/excluding.rst +++ b/doc/excluding.rst @@ -80,14 +80,13 @@ debugging code, and are uninteresting to test themselves. You could exclude all of them by adding a regex to the exclusion list:: [report] - exclude_lines = + exclude_also = def __repr__ For example, here's a list of exclusions I've used:: [report] - exclude_lines = - pragma: no cover + exclude_also = def __repr__ if self.debug: if settings.DEBUG @@ -99,11 +98,10 @@ For example, here's a list of exclusions I've used:: class .*\bProtocol\): @(abc\.)?abstractmethod -Note that when using the ``exclude_lines`` option in a configuration file, you -are taking control of the entire list of regexes, so you need to re-specify the -default "pragma: no cover" match if you still want it to apply. The -``exclude_also`` option can be used instead to preserve the default -exclusions while adding new ones. +The :ref:`config_report_exclude_also` option adds regexes to the built-in +default list so that you can add your own exclusions. The older +:ref:`config_report_exclude_lines` option completely overwrites the list of +regexes. The regexes only have to match part of a line. Be careful not to over-match. A value of ``...`` will match any line with more than three characters in it. diff --git a/doc/faq.rst b/doc/faq.rst index b25dce0fd..d4f5a565e 100644 --- a/doc/faq.rst +++ b/doc/faq.rst @@ -121,7 +121,7 @@ Make sure you are using the C trace function. Coverage.py provides two implementations of the trace function. The C implementation runs much faster. To see what you are running, use ``coverage debug sys``. The output contains details of the environment, including a line that says either -``CTrace: available`` or ``CTracer: unavailable``. If it says unavailable, +``CTracer: available`` or ``CTracer: unavailable``. If it says unavailable, then you are using the slow Python implementation. Try re-installing coverage.py to see what happened and if you get the CTracer diff --git a/doc/index.rst b/doc/index.rst index b11dc90e9..2475eb402 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -18,7 +18,7 @@ supported on: .. PYVERSIONS -* Python versions 3.7 through 3.12.0a7. +* Python versions 3.7 through 3.12.0b1. * PyPy3 7.3.11. .. ifconfig:: prerelease @@ -234,4 +234,5 @@ More information trouble faq Change history <changes> + migrating sleepy diff --git a/doc/migrating.rst b/doc/migrating.rst new file mode 100644 index 000000000..443afac63 --- /dev/null +++ b/doc/migrating.rst @@ -0,0 +1,54 @@ +.. Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +.. For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +.. _migrating: + +========================== +Migrating between versions +========================== + +New versions of coverage.py or Python might require you to adjust your +settings, options, or other aspects how you use coverage.py. This page details +those changes. + +.. _migrating_cov7: + +Migrating to coverage.py 7.x +---------------------------- + +Consider these changes when migrating to coverage.py 7.x: + +- The way that wildcards when specifying file paths work in certain cases has + changed in 7.x: + + - Previously, ``*`` would incorrectly match directory separators, making + precise matching difficult. Patterns such as ``*tests/*`` + will need to be changed to ``*/tests/*``. + + - ``**`` now matches any number of nested directories. If you wish to retain + the behavior of ``**/tests/*`` in previous versions then ``*/**/tests/*`` + can be used instead. + +- When remapping file paths with ``[paths]``, a path will be remapped only if + the resulting path exists. Ensure that remapped ``[paths]`` exist when + upgrading as this is now being enforced. + +- The :ref:`config_report_exclude_also` setting is new in 7.2.0. It adds + exclusion regexes while keeping the default built-in set. It's better than + the older :ref:`config_report_exclude_lines` setting, which overwrote the + entire list. Newer versions of coverage.py will be adding to the default set + of exclusions. Using ``exclude_also`` will let you benefit from those + updates. + + +.. _migrating_py312: + +Migrating to Python 3.12 +------------------------ + +Keep these things in mind when running under Python 3.12: + +- Python 3.12 now inlines list, dict, and set comprehensions. Previously, they + were compiled as functions that were called internally. Coverage.py would + warn you if comprehensions weren't fully completed, but this no longer + happens with Python 3.12. diff --git a/doc/requirements.pip b/doc/requirements.pip index b13fedcd8..a1894b64f 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -6,11 +6,11 @@ # alabaster==0.7.13 # via sphinx -attrs==22.2.0 +attrs==23.1.0 # via scriv babel==2.12.1 # via sphinx -certifi==2022.12.7 +certifi==2023.5.7 # via requests charset-normalizer==3.1.0 # via requests @@ -32,8 +32,9 @@ idna==3.4 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via + # attrs # click # sphinx # sphinxcontrib-spelling @@ -45,21 +46,21 @@ livereload==2.6.3 # via sphinx-autobuild markupsafe==2.1.2 # via jinja2 -packaging==23.0 +packaging==23.1 # via sphinx pyenchant==3.2.2 # via # -r doc/requirements.in # sphinxcontrib-spelling -pygments==2.14.0 +pygments==2.15.1 # via sphinx pytz==2023.3 # via babel -requests==2.28.2 +requests==2.31.0 # via # scriv # sphinx -scriv==1.2.1 +scriv==1.3.1 # via -r doc/requirements.in six==1.16.0 # via livereload @@ -75,7 +76,7 @@ sphinx==5.3.0 # sphinxcontrib-spelling sphinx-autobuild==2021.3.14 # via -r doc/requirements.in -sphinx-rtd-theme==1.2.0 +sphinx-rtd-theme==1.2.1 # via -r doc/requirements.in sphinxcontrib-applehelp==1.0.2 # via sphinx @@ -97,9 +98,9 @@ sphinxcontrib-spelling==8.0.0 # via -r doc/requirements.in tornado==6.2 # via livereload -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via importlib-metadata -urllib3==1.26.15 +urllib3==2.0.2 # via requests zipp==3.15.0 # via importlib-metadata diff --git a/doc/sample_html/d_7b071bdc2a35fa80___init___py.html b/doc/sample_html/d_7b071bdc2a35fa80___init___py.html index a15b8decf..c5ac367ec 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80___init___py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80___init___py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80___main___py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -97,8 +97,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80___main___py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80___main___py.html b/doc/sample_html/d_7b071bdc2a35fa80___main___py.html index 7ea66a94b..c6883a4c7 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80___main___py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80___main___py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_cogapp_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -97,8 +97,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_cogapp_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80_cogapp_py.html b/doc/sample_html/d_7b071bdc2a35fa80_cogapp_py.html index 87112dbf9..19c55847b 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80_cogapp_py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80_cogapp_py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_makefiles_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -938,8 +938,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_makefiles_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80_makefiles_py.html b/doc/sample_html/d_7b071bdc2a35fa80_makefiles_py.html index 16ab2e910..75624055a 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80_makefiles_py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80_makefiles_py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_test_cogapp_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -126,8 +126,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_test_cogapp_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80_test_cogapp_py.html b/doc/sample_html/d_7b071bdc2a35fa80_test_cogapp_py.html index 3b6684674..057e91225 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80_test_cogapp_py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80_test_cogapp_py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_test_makefiles_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -2713,8 +2713,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_test_makefiles_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80_test_makefiles_py.html b/doc/sample_html/d_7b071bdc2a35fa80_test_makefiles_py.html index d0e5385ab..36392eff0 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80_test_makefiles_py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80_test_makefiles_py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_test_whiteutils_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -207,8 +207,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_test_whiteutils_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80_test_whiteutils_py.html b/doc/sample_html/d_7b071bdc2a35fa80_test_whiteutils_py.html index d99b6f77f..2ac062db2 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80_test_whiteutils_py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80_test_whiteutils_py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_whiteutils_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -187,8 +187,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="d_7b071bdc2a35fa80_whiteutils_py.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/d_7b071bdc2a35fa80_whiteutils_py.html b/doc/sample_html/d_7b071bdc2a35fa80_whiteutils_py.html index 5ca1352ac..db1286290 100644 --- a/doc/sample_html/d_7b071bdc2a35fa80_whiteutils_py.html +++ b/doc/sample_html/d_7b071bdc2a35fa80_whiteutils_py.html @@ -66,8 +66,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="index.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> <aside class="hidden"> <button type="button" class="button_next_chunk" data-shortcut="j"/> @@ -157,8 +157,8 @@ <h2> <a id="indexLink" class="nav" href="index.html">^ index</a> <a id="nextFileLink" class="nav" href="index.html">» next</a> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </footer> diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html index c12579608..a609bd62b 100644 --- a/doc/sample_html/index.html +++ b/doc/sample_html/index.html @@ -46,8 +46,8 @@ <h1>Cog coverage: <input id="filter" type="text" value="" placeholder="filter..." /> </form> <p class="text"> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> </header> @@ -157,8 +157,8 @@ <h1>Cog coverage: <footer> <div class="content"> <p> - <a class="nav" href="https://coverage.readthedocs.io/en/7.2.3">coverage.py v7.2.3</a>, - created at 2023-04-06 08:42 -0400 + <a class="nav" href="https://coverage.readthedocs.io/en/7.2.7">coverage.py v7.2.7</a>, + created at 2023-05-29 15:26 -0400 </p> </div> <aside class="hidden"> diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json index 17e633de6..a06dec4db 100644 --- a/doc/sample_html/status.json +++ b/doc/sample_html/status.json @@ -1 +1 @@ -{"format":2,"version":"7.2.3","globals":"2b43fc00a84f1e7415bbd5a0e2a010d6","files":{"d_7b071bdc2a35fa80___init___py":{"hash":"70ef41e14b11d599cdbcf53f562ebb16","index":{"nums":[2,1,1,0,0,0,0,0],"html_filename":"d_7b071bdc2a35fa80___init___py.html","relative_filename":"cogapp/__init__.py"}},"d_7b071bdc2a35fa80___main___py":{"hash":"6d9d0d551879aa3e73791f40c5739845","index":{"nums":[2,1,3,0,3,0,0,0],"html_filename":"d_7b071bdc2a35fa80___main___py.html","relative_filename":"cogapp/__main__.py"}},"d_7b071bdc2a35fa80_cogapp_py":{"hash":"7428c811d741c23b10655ff6c20fb85f","index":{"nums":[2,1,500,1,224,210,30,138],"html_filename":"d_7b071bdc2a35fa80_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"d_7b071bdc2a35fa80_makefiles_py":{"hash":"4b73eaf76fbb53af575b40165e831aac","index":{"nums":[2,1,22,0,18,14,0,14],"html_filename":"d_7b071bdc2a35fa80_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"d_7b071bdc2a35fa80_test_cogapp_py":{"hash":"34099de695d2cac204436597408d33d2","index":{"nums":[2,1,845,2,591,24,1,21],"html_filename":"d_7b071bdc2a35fa80_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"d_7b071bdc2a35fa80_test_makefiles_py":{"hash":"63fd1bdc011935abfd11301da94b383e","index":{"nums":[2,1,70,0,53,6,0,6],"html_filename":"d_7b071bdc2a35fa80_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"d_7b071bdc2a35fa80_test_whiteutils_py":{"hash":"ec69457cbd6dfbc85eefabdfc0931c99","index":{"nums":[2,1,68,0,50,0,0,0],"html_filename":"d_7b071bdc2a35fa80_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"d_7b071bdc2a35fa80_whiteutils_py":{"hash":"6dbf59193ab1bdcba86b017c86bb4724","index":{"nums":[2,1,43,0,5,34,4,4],"html_filename":"d_7b071bdc2a35fa80_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file +{"format":2,"version":"7.2.7","globals":"2b43fc00a84f1e7415bbd5a0e2a010d6","files":{"d_7b071bdc2a35fa80___init___py":{"hash":"70ef41e14b11d599cdbcf53f562ebb16","index":{"nums":[2,1,1,0,0,0,0,0],"html_filename":"d_7b071bdc2a35fa80___init___py.html","relative_filename":"cogapp/__init__.py"}},"d_7b071bdc2a35fa80___main___py":{"hash":"6d9d0d551879aa3e73791f40c5739845","index":{"nums":[2,1,3,0,3,0,0,0],"html_filename":"d_7b071bdc2a35fa80___main___py.html","relative_filename":"cogapp/__main__.py"}},"d_7b071bdc2a35fa80_cogapp_py":{"hash":"7428c811d741c23b10655ff6c20fb85f","index":{"nums":[2,1,500,1,224,210,30,138],"html_filename":"d_7b071bdc2a35fa80_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"d_7b071bdc2a35fa80_makefiles_py":{"hash":"4b73eaf76fbb53af575b40165e831aac","index":{"nums":[2,1,22,0,18,14,0,14],"html_filename":"d_7b071bdc2a35fa80_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"d_7b071bdc2a35fa80_test_cogapp_py":{"hash":"34099de695d2cac204436597408d33d2","index":{"nums":[2,1,845,2,591,24,1,21],"html_filename":"d_7b071bdc2a35fa80_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"d_7b071bdc2a35fa80_test_makefiles_py":{"hash":"63fd1bdc011935abfd11301da94b383e","index":{"nums":[2,1,70,0,53,6,0,6],"html_filename":"d_7b071bdc2a35fa80_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"d_7b071bdc2a35fa80_test_whiteutils_py":{"hash":"ec69457cbd6dfbc85eefabdfc0931c99","index":{"nums":[2,1,68,0,50,0,0,0],"html_filename":"d_7b071bdc2a35fa80_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"d_7b071bdc2a35fa80_whiteutils_py":{"hash":"6dbf59193ab1bdcba86b017c86bb4724","index":{"nums":[2,1,43,0,5,34,4,4],"html_filename":"d_7b071bdc2a35fa80_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file diff --git a/howto.txt b/howto.txt index 24f01ecb6..1d5f4f2e1 100644 --- a/howto.txt +++ b/howto.txt @@ -10,6 +10,7 @@ version_info = (4, 0, 2, "final", 0) - make sure: _dev = 0 - Edit supported Python version numbers. Search for "PYVERSIONS". + - Especially README.rst and doc/index.rst - Update source files with release facts: $ make edit_for_release - Get useful snippets for next steps, and beyond, in cheats.txt @@ -17,9 +18,7 @@ - Look over CHANGES.rst - Update README.rst - "New in x.y:" - - Python versions supported - Update docs - - Python versions in doc/index.rst - IF PRE-RELEASE: - Version of latest stable release in doc/index.rst - Make sure the docs are cogged: @@ -49,6 +48,7 @@ $ make publishbeta - ELSE: $ make publish + - commit and publish nedbatchelder.com - Kits: - Wait for kits to finish: - https://github.com/nedbat/coveragepy/actions/workflows/kit.yml @@ -83,9 +83,9 @@ - wait for the new tag build to finish successfully. - @ https://readthedocs.org/dashboard/coverage/advanced/ - change the default version to the new version +- Once CI passes, merge the bump-version branch to master and push it + - things to automate: - - url to link to latest changes in docs - - next version.py line - readthedocs api to do the readthedocs changes diff --git a/igor.py b/igor.py index ad0dbf8c5..f4c5d22f1 100644 --- a/igor.py +++ b/igor.py @@ -12,6 +12,7 @@ import datetime import glob import inspect +import itertools import os import platform import pprint @@ -77,10 +78,11 @@ def do_remove_extension(*args): "-c", "import coverage; print(coverage.__file__)" ], encoding="utf-8").strip()) + roots = [root] else: - root = "coverage" + roots = ["coverage", "build/*/coverage"] - for pattern in so_patterns: + for root, pattern in itertools.product(roots, so_patterns): pattern = os.path.join(root, pattern.strip()) if VERBOSITY: print(f"Searching for {pattern}") @@ -219,6 +221,10 @@ def do_combine_html(): cov.load() cov.combine() cov.save() + # A new Coverage to turn on messages. Better would be to have tighter + # control over message verbosity... + cov = coverage.Coverage(config_file="metacov.ini", messages=True) + cov.load() show_contexts = bool(os.environ.get('COVERAGE_DYNCTX') or os.environ.get('COVERAGE_CONTEXT')) cov.html_report(show_contexts=show_contexts) diff --git a/pyproject.toml b/pyproject.toml index 6b02c6a47..cf9c2c0e4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -54,6 +54,9 @@ balanced_clumps = [ "GetZipBytesTest", ] +[tool.ruff] +line-length = 100 + [tool.scriv] # Changelog management: https://pypi.org/project/scriv/ format = "rst" diff --git a/requirements/dev.pip b/requirements/dev.pip index 76304efc0..0cd90a3c4 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -4,19 +4,17 @@ # # make upgrade # -astroid==2.15.1 +astroid==2.15.5 # via pylint -attrs==22.2.0 - # via - # hypothesis - # pytest +attrs==23.1.0 + # via hypothesis bleach==6.0.0 # via readme-renderer build==0.10.0 # via check-manifest -cachetools==5.3.0 +cachetools==5.3.1 # via tox -certifi==2022.12.7 +certifi==2023.5.7 # via requests chardet==5.1.0 # via tox @@ -35,7 +33,7 @@ dill==0.3.6 # via pylint distlib==0.3.6 # via virtualenv -docutils==0.19 +docutils==0.20.1 # via readme-renderer exceptiongroup==1.1.1 # via @@ -43,7 +41,7 @@ exceptiongroup==1.1.1 # pytest execnet==1.9.0 # via pytest-xdist -filelock==3.10.7 +filelock==3.12.0 # via # tox # virtualenv @@ -51,12 +49,13 @@ flaky==3.7.0 # via -r requirements/pytest.in greenlet==2.0.2 # via -r requirements/dev.in -hypothesis==6.70.2 +hypothesis==6.75.6 # via -r requirements/pytest.in idna==3.4 # via requests -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via + # attrs # build # keyring # pluggy @@ -88,7 +87,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==9.1.0 # via jaraco-classes -packaging==23.0 +packaging==23.1 # via # build # pudb @@ -99,7 +98,7 @@ parso==0.8.3 # via jedi pkginfo==1.9.6 # via twine -platformdirs==3.2.0 +platformdirs==3.5.1 # via # pylint # tox @@ -110,37 +109,37 @@ pluggy==1.0.0 # tox pudb==2022.1.3 # via -r requirements/dev.in -pygments==2.14.0 +pygments==2.15.1 # via # pudb # readme-renderer # rich -pylint==2.17.1 +pylint==2.17.4 # via -r requirements/dev.in pyproject-api==1.5.1 # via tox pyproject-hooks==1.0.0 # via build -pytest==7.2.2 +pytest==7.3.1 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.2.1 +pytest-xdist==3.3.1 # via -r requirements/pytest.in readme-renderer==37.3 # via # -r requirements/dev.in # twine -requests==2.28.2 +requests==2.31.0 # via # -r requirements/dev.in # requests-toolbelt # twine -requests-toolbelt==0.10.1 +requests-toolbelt==1.0.0 # via twine rfc3986==2.0.0 # via twine -rich==13.3.3 +rich==13.3.5 # via twine six==1.16.0 # via bleach @@ -155,9 +154,9 @@ tomli==2.0.1 # pyproject-hooks # pytest # tox -tomlkit==0.11.7 +tomlkit==0.11.8 # via pylint -tox==4.4.8 +tox==4.5.2 # via # -r requirements/tox.in # tox-gh @@ -167,7 +166,7 @@ twine==4.0.2 # via -r requirements/dev.in typed-ast==1.5.4 # via astroid -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via # astroid # importlib-metadata @@ -176,7 +175,7 @@ typing-extensions==4.5.0 # pylint # rich # tox -urllib3==1.26.15 +urllib3==2.0.2 # via # requests # twine @@ -186,7 +185,7 @@ urwid==2.1.2 # urwid-readline urwid-readline==0.13 # via pudb -virtualenv==20.21.0 +virtualenv==20.23.0 # via # -r requirements/pip.in # tox @@ -200,10 +199,9 @@ zipp==3.15.0 # importlib-resources # The following packages are considered to be unsafe in a requirements file: -pip==23.0.1 +pip==23.1.2 # via -r requirements/pip.in -setuptools==65.7.0 +setuptools==67.8.0 # via - # -c requirements/pins.pip # -r requirements/pip.in # check-manifest diff --git a/requirements/kit.pip b/requirements/kit.pip index a126aa357..608c3f79b 100644 --- a/requirements/kit.pip +++ b/requirements/kit.pip @@ -4,7 +4,7 @@ # # make upgrade # -auditwheel==5.3.0 +auditwheel==5.4.0 # via -r requirements/kit.in bashlex==0.18 # via cibuildwheel @@ -12,23 +12,23 @@ bracex==2.3.post1 # via cibuildwheel build==0.10.0 # via -r requirements/kit.in -certifi==2022.12.7 +certifi==2023.5.7 # via cibuildwheel -cibuildwheel==2.12.1 +cibuildwheel==2.13.0 # via -r requirements/kit.in colorama==0.4.6 # via -r requirements/kit.in -filelock==3.10.7 +filelock==3.12.0 # via cibuildwheel -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via # auditwheel # build -packaging==23.0 +packaging==23.1 # via # build # cibuildwheel -platformdirs==3.2.0 +platformdirs==3.5.1 # via cibuildwheel pyelftools==0.29 # via auditwheel @@ -39,7 +39,7 @@ tomli==2.0.1 # build # cibuildwheel # pyproject-hooks -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via # cibuildwheel # importlib-metadata @@ -50,5 +50,5 @@ zipp==3.15.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -setuptools==65.7.0 +setuptools==67.8.0 # via -r requirements/kit.in diff --git a/requirements/light-threads.pip b/requirements/light-threads.pip index e53a4d13f..a633004df 100644 --- a/requirements/light-threads.pip +++ b/requirements/light-threads.pip @@ -27,9 +27,8 @@ zope-interface==6.0 # via gevent # The following packages are considered to be unsafe in a requirements file: -setuptools==65.7.0 +setuptools==67.8.0 # via - # -c requirements/pins.pip # gevent # zope-event # zope-interface diff --git a/requirements/lint.pip b/requirements/lint.pip index af91931e1..9ba71d60d 100644 --- a/requirements/lint.pip +++ b/requirements/lint.pip @@ -6,12 +6,11 @@ # alabaster==0.7.13 # via sphinx -astroid==2.15.1 +astroid==2.15.5 # via pylint -attrs==22.2.0 +attrs==23.1.0 # via # hypothesis - # pytest # scriv babel==2.12.1 # via sphinx @@ -19,9 +18,9 @@ bleach==6.0.0 # via readme-renderer build==0.10.0 # via check-manifest -cachetools==5.3.0 +cachetools==5.3.1 # via tox -certifi==2022.12.7 +certifi==2023.5.7 # via requests chardet==5.1.0 # via tox @@ -60,7 +59,7 @@ exceptiongroup==1.1.1 # pytest execnet==1.9.0 # via pytest-xdist -filelock==3.10.7 +filelock==3.12.0 # via # tox # virtualenv @@ -68,14 +67,15 @@ flaky==3.7.0 # via -r requirements/pytest.in greenlet==2.0.2 # via -r requirements/dev.in -hypothesis==6.70.2 +hypothesis==6.75.6 # via -r requirements/pytest.in idna==3.4 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via + # attrs # build # click # keyring @@ -118,7 +118,7 @@ mdurl==0.1.2 # via markdown-it-py more-itertools==9.1.0 # via jaraco-classes -packaging==23.0 +packaging==23.1 # via # build # pudb @@ -130,7 +130,7 @@ parso==0.8.3 # via jedi pkginfo==1.9.6 # via twine -platformdirs==3.2.0 +platformdirs==3.5.1 # via # pylint # tox @@ -145,23 +145,23 @@ pyenchant==3.2.2 # via # -r doc/requirements.in # sphinxcontrib-spelling -pygments==2.14.0 +pygments==2.15.1 # via # pudb # readme-renderer # rich # sphinx -pylint==2.17.1 +pylint==2.17.4 # via -r requirements/dev.in pyproject-api==1.5.1 # via tox pyproject-hooks==1.0.0 # via build -pytest==7.2.2 +pytest==7.3.1 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.2.1 +pytest-xdist==3.3.1 # via -r requirements/pytest.in pytz==2023.3 # via babel @@ -169,20 +169,20 @@ readme-renderer==37.3 # via # -r requirements/dev.in # twine -requests==2.28.2 +requests==2.31.0 # via # -r requirements/dev.in # requests-toolbelt # scriv # sphinx # twine -requests-toolbelt==0.10.1 +requests-toolbelt==1.0.0 # via twine rfc3986==2.0.0 # via twine -rich==13.3.3 +rich==13.3.5 # via twine -scriv==1.2.1 +scriv==1.3.1 # via -r doc/requirements.in six==1.16.0 # via @@ -202,7 +202,7 @@ sphinx==5.3.0 # sphinxcontrib-spelling sphinx-autobuild==2021.3.14 # via -r doc/requirements.in -sphinx-rtd-theme==1.2.0 +sphinx-rtd-theme==1.2.1 # via -r doc/requirements.in sphinxcontrib-applehelp==1.0.2 # via sphinx @@ -231,11 +231,11 @@ tomli==2.0.1 # pyproject-hooks # pytest # tox -tomlkit==0.11.7 +tomlkit==0.11.8 # via pylint tornado==6.2 # via livereload -tox==4.4.8 +tox==4.5.2 # via # -r requirements/tox.in # tox-gh @@ -245,7 +245,7 @@ twine==4.0.2 # via -r requirements/dev.in typed-ast==1.5.4 # via astroid -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via # astroid # importlib-metadata @@ -254,7 +254,7 @@ typing-extensions==4.5.0 # pylint # rich # tox -urllib3==1.26.15 +urllib3==2.0.2 # via # requests # twine @@ -264,7 +264,7 @@ urwid==2.1.2 # urwid-readline urwid-readline==0.13 # via pudb -virtualenv==20.21.0 +virtualenv==20.23.0 # via # -r requirements/pip.in # tox @@ -278,10 +278,9 @@ zipp==3.15.0 # importlib-resources # The following packages are considered to be unsafe in a requirements file: -pip==23.0.1 +pip==23.1.2 # via -r requirements/pip.in -setuptools==65.7.0 +setuptools==67.8.0 # via - # -c requirements/pins.pip # -r requirements/pip.in # check-manifest diff --git a/requirements/mypy.pip b/requirements/mypy.pip index ae1bbd97b..f3146a23e 100644 --- a/requirements/mypy.pip +++ b/requirements/mypy.pip @@ -4,10 +4,8 @@ # # make upgrade # -attrs==22.2.0 - # via - # hypothesis - # pytest +attrs==23.1.0 + # via hypothesis colorama==0.4.6 # via -r requirements/pytest.in exceptiongroup==1.1.1 @@ -18,27 +16,28 @@ execnet==1.9.0 # via pytest-xdist flaky==3.7.0 # via -r requirements/pytest.in -hypothesis==6.70.2 +hypothesis==6.75.6 # via -r requirements/pytest.in -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via + # attrs # pluggy # pytest iniconfig==2.0.0 # via pytest -mypy==1.1.1 +mypy==1.3.0 # via -r requirements/mypy.in mypy-extensions==1.0.0 # via mypy -packaging==23.0 +packaging==23.1 # via pytest pluggy==1.0.0 # via pytest -pytest==7.2.2 +pytest==7.3.1 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.2.1 +pytest-xdist==3.3.1 # via -r requirements/pytest.in sortedcontainers==2.4.0 # via hypothesis @@ -48,7 +47,7 @@ tomli==2.0.1 # pytest typed-ast==1.5.4 # via mypy -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via # importlib-metadata # mypy diff --git a/requirements/pins.pip b/requirements/pins.pip index b614c3119..3b38dcb3f 100644 --- a/requirements/pins.pip +++ b/requirements/pins.pip @@ -11,4 +11,4 @@ # checking the Python version like that, should it? # https://github.com/pypa/packaging/issues/678 # https://github.com/nedbat/coveragepy/issues/1556 -setuptools<66.0.0 +#setuptools<66.0.0 diff --git a/requirements/pip-tools.pip b/requirements/pip-tools.pip index 000f707a7..b720cb51f 100644 --- a/requirements/pip-tools.pip +++ b/requirements/pip-tools.pip @@ -8,13 +8,13 @@ build==0.10.0 # via pip-tools click==8.1.3 # via pip-tools -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via # build # click -packaging==23.0 +packaging==23.1 # via build -pip-tools==6.12.3 +pip-tools==6.13.0 # via -r requirements/pip-tools.in pyproject-hooks==1.0.0 # via build @@ -22,7 +22,7 @@ tomli==2.0.1 # via # build # pyproject-hooks -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via importlib-metadata wheel==0.40.0 # via pip-tools @@ -30,9 +30,7 @@ zipp==3.15.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -pip==23.0.1 +pip==23.1.2 + # via pip-tools +setuptools==67.8.0 # via pip-tools -setuptools==65.7.0 - # via - # -c requirements/pins.pip - # pip-tools diff --git a/requirements/pip.pip b/requirements/pip.pip index 927943a5e..0cc81df43 100644 --- a/requirements/pip.pip +++ b/requirements/pip.pip @@ -6,23 +6,23 @@ # distlib==0.3.6 # via virtualenv -filelock==3.10.7 +filelock==3.12.0 # via virtualenv -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via virtualenv -platformdirs==3.2.0 +platformdirs==3.5.1 # via virtualenv -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via # importlib-metadata # platformdirs -virtualenv==20.21.0 +virtualenv==20.23.0 # via -r requirements/pip.in zipp==3.15.0 # via importlib-metadata # The following packages are considered to be unsafe in a requirements file: -pip==23.0.1 +pip==23.1.2 # via -r requirements/pip.in -setuptools==65.7.0 +setuptools==67.8.0 # via -r requirements/pip.in diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 478860a97..d34f3d3d7 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -4,10 +4,8 @@ # # make upgrade # -attrs==22.2.0 - # via - # hypothesis - # pytest +attrs==23.1.0 + # via hypothesis colorama==0.4.6 # via -r requirements/pytest.in exceptiongroup==1.1.1 @@ -18,29 +16,30 @@ execnet==1.9.0 # via pytest-xdist flaky==3.7.0 # via -r requirements/pytest.in -hypothesis==6.70.2 +hypothesis==6.75.6 # via -r requirements/pytest.in -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via + # attrs # pluggy # pytest iniconfig==2.0.0 # via pytest -packaging==23.0 +packaging==23.1 # via pytest pluggy==1.0.0 # via pytest -pytest==7.2.2 +pytest==7.3.1 # via # -r requirements/pytest.in # pytest-xdist -pytest-xdist==3.2.1 +pytest-xdist==3.3.1 # via -r requirements/pytest.in sortedcontainers==2.4.0 # via hypothesis tomli==2.0.1 # via pytest -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via importlib-metadata zipp==3.15.0 # via importlib-metadata diff --git a/requirements/tox.pip b/requirements/tox.pip index c02835cbb..759f13cef 100644 --- a/requirements/tox.pip +++ b/requirements/tox.pip @@ -4,7 +4,7 @@ # # make upgrade # -cachetools==5.3.0 +cachetools==5.3.1 # via tox chardet==5.1.0 # via tox @@ -14,20 +14,20 @@ colorama==0.4.6 # tox distlib==0.3.6 # via virtualenv -filelock==3.10.7 +filelock==3.12.0 # via # tox # virtualenv -importlib-metadata==6.1.0 +importlib-metadata==6.6.0 # via # pluggy # tox # virtualenv -packaging==23.0 +packaging==23.1 # via # pyproject-api # tox -platformdirs==3.2.0 +platformdirs==3.5.1 # via # tox # virtualenv @@ -39,18 +39,18 @@ tomli==2.0.1 # via # pyproject-api # tox -tox==4.4.8 +tox==4.5.2 # via # -r requirements/tox.in # tox-gh tox-gh==1.0.0 # via -r requirements/tox.in -typing-extensions==4.5.0 +typing-extensions==4.6.2 # via # importlib-metadata # platformdirs # tox -virtualenv==20.21.0 +virtualenv==20.23.0 # via tox zipp==3.15.0 # via importlib-metadata diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index db3fdce8e..000000000 --- a/setup.cfg +++ /dev/null @@ -1,5 +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 - -[metadata] -license_files = LICENSE.txt diff --git a/setup.py b/setup.py index 2c375522d..90763f94c 100644 --- a/setup.py +++ b/setup.py @@ -118,6 +118,7 @@ long_description_content_type='text/x-rst', keywords='code coverage testing', license='Apache-2.0', + license_files=["LICENSE.txt"], classifiers=classifier_list, url="https://github.com/nedbat/coveragepy", project_urls={ diff --git a/tests/conftest.py b/tests/conftest.py index 41db85b49..51bab8d2b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -40,7 +40,7 @@ def set_warnings() -> None: warnings.simplefilter("once", DeprecationWarning) # Warnings to suppress: - # How come these warnings are successfully suppressed here, but not in setup.cfg?? + # How come these warnings are successfully suppressed here, but not in pyproject.toml?? warnings.filterwarnings( "ignore", diff --git a/tests/test_api.py b/tests/test_api.py index 596510ebc..5c903d65f 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -28,8 +28,7 @@ from coverage.types import FilePathClasses, FilePathType, Protocol, TCovKwargs from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin -from tests.goldtest import contains, doesnt_contain -from tests.helpers import arcz_to_arcs, assert_count_equal, assert_coverage_warnings +from tests.helpers import assert_count_equal, assert_coverage_warnings from tests.helpers import change_dir, nice_file, os_sep BAD_SQLITE_REGEX = r"file( is encrypted or)? is not a database" @@ -1489,146 +1488,3 @@ def test_combine_parallel_data_keep(self) -> None: # After combining, the .coverage file & the original combined file should still be there. self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 2) - - -class ReportMapsPathsTest(CoverageTest): - """Check that reporting implicitly maps paths.""" - - def make_files(self, data: str, settings: bool = False) -> None: - """Create the test files we need for line coverage.""" - src = """\ - if VER == 1: - print("line 2") - if VER == 2: - print("line 4") - if VER == 3: - print("line 6") - """ - self.make_file("src/program.py", src) - self.make_file("ver1/program.py", src) - self.make_file("ver2/program.py", src) - - if data == "line": - self.make_data_file( - lines={ - abs_file("ver1/program.py"): [1, 2, 3, 5], - abs_file("ver2/program.py"): [1, 3, 4, 5], - } - ) - else: - self.make_data_file( - arcs={ - abs_file("ver1/program.py"): arcz_to_arcs(".1 12 23 35 5."), - abs_file("ver2/program.py"): arcz_to_arcs(".1 13 34 45 5."), - } - ) - - if settings: - self.make_file(".coveragerc", """\ - [paths] - source = - src - ver1 - ver2 - """) - - def test_map_paths_during_line_report_without_setting(self) -> None: - self.make_files(data="line") - cov = coverage.Coverage() - cov.load() - cov.report(show_missing=True) - expected = textwrap.dedent(os_sep("""\ - Name Stmts Miss Cover Missing - ----------------------------------------------- - ver1/program.py 6 2 67% 4, 6 - ver2/program.py 6 2 67% 2, 6 - ----------------------------------------------- - TOTAL 12 4 67% - """)) - assert expected == self.stdout() - - def test_map_paths_during_line_report(self) -> None: - self.make_files(data="line", settings=True) - cov = coverage.Coverage() - cov.load() - cov.report(show_missing=True) - expected = textwrap.dedent(os_sep("""\ - Name Stmts Miss Cover Missing - ---------------------------------------------- - src/program.py 6 1 83% 6 - ---------------------------------------------- - TOTAL 6 1 83% - """)) - assert expected == self.stdout() - - def test_map_paths_during_branch_report_without_setting(self) -> None: - self.make_files(data="arcs") - cov = coverage.Coverage(branch=True) - cov.load() - cov.report(show_missing=True) - expected = textwrap.dedent(os_sep("""\ - Name Stmts Miss Branch BrPart Cover Missing - ------------------------------------------------------------- - ver1/program.py 6 2 6 3 58% 1->3, 4, 6 - ver2/program.py 6 2 6 3 58% 2, 3->5, 6 - ------------------------------------------------------------- - TOTAL 12 4 12 6 58% - """)) - assert expected == self.stdout() - - def test_map_paths_during_branch_report(self) -> None: - self.make_files(data="arcs", settings=True) - cov = coverage.Coverage(branch=True) - cov.load() - cov.report(show_missing=True) - expected = textwrap.dedent(os_sep("""\ - Name Stmts Miss Branch BrPart Cover Missing - ------------------------------------------------------------ - src/program.py 6 1 6 1 83% 6 - ------------------------------------------------------------ - TOTAL 6 1 6 1 83% - """)) - assert expected == self.stdout() - - def test_map_paths_during_annotate(self) -> None: - self.make_files(data="line", settings=True) - cov = coverage.Coverage() - cov.load() - cov.annotate() - self.assert_exists(os_sep("src/program.py,cover")) - self.assert_doesnt_exist(os_sep("ver1/program.py,cover")) - self.assert_doesnt_exist(os_sep("ver2/program.py,cover")) - - def test_map_paths_during_html_report(self) -> None: - self.make_files(data="line", settings=True) - cov = coverage.Coverage() - cov.load() - cov.html_report() - contains("htmlcov/index.html", os_sep("src/program.py")) - doesnt_contain("htmlcov/index.html", os_sep("ver1/program.py"), os_sep("ver2/program.py")) - - def test_map_paths_during_xml_report(self) -> None: - self.make_files(data="line", settings=True) - cov = coverage.Coverage() - cov.load() - cov.xml_report() - contains("coverage.xml", "src/program.py") - doesnt_contain("coverage.xml", "ver1/program.py", "ver2/program.py") - - def test_map_paths_during_json_report(self) -> None: - self.make_files(data="line", settings=True) - cov = coverage.Coverage() - cov.load() - cov.json_report() - def os_sepj(s: str) -> str: - return os_sep(s).replace("\\", r"\\") - contains("coverage.json", os_sepj("src/program.py")) - doesnt_contain("coverage.json", os_sepj("ver1/program.py"), os_sepj("ver2/program.py")) - - def test_map_paths_during_lcov_report(self) -> None: - self.make_files(data="line", settings=True) - cov = coverage.Coverage() - cov.load() - cov.lcov_report() - contains("coverage.lcov", os_sep("src/program.py")) - doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py")) diff --git a/tests/test_arcs.py b/tests/test_arcs.py index d80a46370..1f46064f6 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -24,7 +24,6 @@ reason="https://foss.heptapod.net/pypy/pypy/-/issues/3882", ) - class SimpleArcTest(CoverageTest): """Tests for coverage.py's arc measurement.""" @@ -559,6 +558,10 @@ def whileelse(seq): ) def test_confusing_for_loop_bug_175(self) -> None: + if env.PYBEHAVIOR.comprehensions_are_functions: + extra_arcz = " -22 2-2" + else: + extra_arcz = "" self.check_coverage("""\ o = [(1,2), (3,4)] o = [a for a in o] @@ -566,7 +569,7 @@ def test_confusing_for_loop_bug_175(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + arcz=".1 12 23 34 45 53 3." + extra_arcz, ) self.check_coverage("""\ o = [(1,2), (3,4)] @@ -574,7 +577,7 @@ def test_confusing_for_loop_bug_175(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 12 -22 2-2 23 34 42 2.", + arcz=".1 12 23 34 42 2." + extra_arcz, ) # https://bugs.python.org/issue44672 @@ -639,6 +642,10 @@ def test_generator_expression_another_way(self) -> None: ) def test_other_comprehensions(self) -> None: + if env.PYBEHAVIOR.comprehensions_are_functions: + extra_arcz = " -22 2-2" + else: + extra_arcz = "" # Set comprehension: self.check_coverage("""\ o = ((1,2), (3,4)) @@ -647,7 +654,7 @@ def test_other_comprehensions(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + arcz=".1 12 23 34 45 53 3." + extra_arcz, ) # Dict comprehension: self.check_coverage("""\ @@ -657,10 +664,14 @@ def test_other_comprehensions(self) -> None: x = tup[0] y = tup[1] """, - arcz=".1 -22 2-2 12 23 34 45 53 3.", + arcz=".1 12 23 34 45 53 3." + extra_arcz, ) def test_multiline_dict_comp(self) -> None: + if env.PYBEHAVIOR.comprehensions_are_functions: + extra_arcz = " 2-2" + else: + extra_arcz = "" # Multiline dict comp: self.check_coverage("""\ # comment @@ -675,7 +686,7 @@ def test_multiline_dict_comp(self) -> None: } x = 11 """, - arcz="-22 2B B-2 2-2" + arcz="-22 2B B-2" + extra_arcz, ) # Multi dict comp: self.check_coverage("""\ @@ -695,7 +706,7 @@ def test_multiline_dict_comp(self) -> None: } x = 15 """, - arcz="-22 2F F-2 2-2" + arcz="-22 2F F-2" + extra_arcz, ) diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index a9b64d158..07c76ce28 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -768,10 +768,11 @@ def test_sigterm_threading_saves_data(self) -> None: sigterm = true """) out = self.run_command("coverage run handler.py") - if env.LINUX: - assert out == "START\nSIGTERM\nTerminated\n" - else: - assert out == "START\nSIGTERM\n" + out_lines = out.splitlines() + assert len(out_lines) in [2, 3] + assert out_lines[:2] == ["START", "SIGTERM"] + if len(out_lines) == 3: + assert out_lines[2] == "Terminated" out = self.run_command("coverage report -m") expected = "handler.py 5 1 80% 6" assert self.squeezed_lines(out)[2] == expected diff --git a/tests/test_debug.py b/tests/test_debug.py index 60a7b10a4..e611134d0 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -19,7 +19,8 @@ from coverage import env from coverage.debug import ( DebugOutputFile, - clipped_repr, filter_text, info_formatter, info_header, short_id, short_stack, + clipped_repr, filter_text, info_formatter, info_header, relevant_environment_display, + short_id, short_stack, ) from tests.coveragetest import CoverageTest @@ -297,3 +298,22 @@ def test_short_stack_limit(self) -> None: def test_short_stack_skip(self) -> None: stack = f_one(skip=1).splitlines() assert "f_two" in stack[-1] + + +def test_relevant_environment_display() -> None: + env_vars = { + "HOME": "my home", + "HOME_DIR": "other place", + "XYZ_NEVER_MIND": "doesn't matter", + "SOME_PYOTHER": "xyz123", + "COVERAGE_THING": "abcd", + "MY_PYPI_TOKEN": "secret.something", + "TMP": "temporary", + } + assert relevant_environment_display(env_vars) == [ + ("COVERAGE_THING", "abcd"), + ("HOME", "my home"), + ("MY_PYPI_TOKEN", "******.*********"), + ("SOME_PYOTHER", "xyz123"), + ("TMP", "temporary"), + ] diff --git a/tests/test_html.py b/tests/test_html.py index 65f0cc763..476e75e80 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -24,7 +24,7 @@ from coverage.exceptions import NoDataError, NotPython, NoSource from coverage.files import abs_file, flat_rootname import coverage.html -from coverage.report import get_analysis_to_report +from coverage.report_core import get_analysis_to_report from coverage.types import TLineNo, TMorf from tests.coveragetest import CoverageTest, TESTS_DIR diff --git a/tests/test_lcov.py b/tests/test_lcov.py index 6d50b62b5..a3332081e 100644 --- a/tests/test_lcov.py +++ b/tests/test_lcov.py @@ -23,8 +23,6 @@ def create_initial_files(self) -> None: show the consequences of changes in the setup. """ self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def cuboid_volume(l): return (l*l*l) @@ -33,8 +31,6 @@ def IsItTrue(): """) self.make_file("test_file.py", """\ - #!/usr/bin/env python3 - from main_file import cuboid_volume import unittest @@ -52,11 +48,9 @@ def get_lcov_report_content(self, filename: str = "coverage.lcov") -> str: return file.read() def test_lone_file(self) -> None: - """For a single file with a couple of functions, the lcov should cover - the function definitions themselves, but not the returns.""" + # For a single file with a couple of functions, the lcov should cover + # the function definitions themselves, but not the returns. self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def cuboid_volume(l): return (l*l*l) @@ -66,10 +60,10 @@ def IsItTrue(): expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,7URou3io0zReBkk69lEb/Q - DA:6,1,ilhb4KUfytxtEuClijZPlQ - DA:4,0,Xqj6H1iz/nsARMCAbE90ng - DA:7,0,LWILTcvARcydjFFyo9qM0A + DA:1,1,7URou3io0zReBkk69lEb/Q + DA:4,1,ilhb4KUfytxtEuClijZPlQ + DA:2,0,Xqj6H1iz/nsARMCAbE90ng + DA:5,0,LWILTcvARcydjFFyo9qM0A LF:4 LH:2 end_of_record @@ -83,8 +77,8 @@ def IsItTrue(): assert expected_result == actual_result def test_simple_line_coverage_two_files(self) -> None: - """Test that line coverage is created when coverage is run, - and matches the output of the file below.""" + # Test that line coverage is created when coverage is run, + # and matches the output of the file below. self.create_initial_files() self.assert_doesnt_exist(".coverage") self.make_file(".coveragerc", "[lcov]\noutput = data.lcov\n") @@ -96,23 +90,23 @@ def test_simple_line_coverage_two_files(self) -> None: expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,7URou3io0zReBkk69lEb/Q - DA:6,1,ilhb4KUfytxtEuClijZPlQ - DA:4,0,Xqj6H1iz/nsARMCAbE90ng - DA:7,0,LWILTcvARcydjFFyo9qM0A + DA:1,1,7URou3io0zReBkk69lEb/Q + DA:4,1,ilhb4KUfytxtEuClijZPlQ + DA:2,0,Xqj6H1iz/nsARMCAbE90ng + DA:5,0,LWILTcvARcydjFFyo9qM0A LF:4 LH:2 end_of_record TN: SF:test_file.py - DA:3,1,R5Rb4IzmjKRgY/vFFc1TRg - DA:4,1,E/tvV9JPVDhEcTCkgrwOFw - DA:6,1,GP08LPBYJq8EzYveHJy2qA - DA:7,1,MV+jSLi6PFEl+WatEAptog - DA:8,0,qyqd1mF289dg6oQAQHA+gQ - DA:9,0,nmEYd5F1KrxemgC9iVjlqg - DA:10,0,jodMK26WYDizOO1C7ekBbg - DA:11,0,LtxfKehkX8o4KvC5GnN52g + DA:1,1,R5Rb4IzmjKRgY/vFFc1TRg + DA:2,1,E/tvV9JPVDhEcTCkgrwOFw + DA:4,1,GP08LPBYJq8EzYveHJy2qA + DA:5,1,MV+jSLi6PFEl+WatEAptog + DA:6,0,qyqd1mF289dg6oQAQHA+gQ + DA:7,0,nmEYd5F1KrxemgC9iVjlqg + DA:8,0,jodMK26WYDizOO1C7ekBbg + DA:9,0,LtxfKehkX8o4KvC5GnN52g LF:8 LH:4 end_of_record @@ -121,10 +115,8 @@ def test_simple_line_coverage_two_files(self) -> None: assert expected_result == actual_result def test_branch_coverage_one_file(self) -> None: - """Test that the reporter produces valid branch coverage.""" + # Test that the reporter produces valid branch coverage. self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def is_it_x(x): if x == 3: return x @@ -140,14 +132,14 @@ def is_it_x(x): expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,4MDXMbvwQ3L7va1tsphVzw - DA:4,0,MuERA6EYyZNpKPqoJfzwkA - DA:5,0,sAyiiE6iAuPMte9kyd0+3g - DA:7,0,W/g8GJDAYJkSSurt59Mzfw + DA:1,1,4MDXMbvwQ3L7va1tsphVzw + DA:2,0,MuERA6EYyZNpKPqoJfzwkA + DA:3,0,sAyiiE6iAuPMte9kyd0+3g + DA:5,0,W/g8GJDAYJkSSurt59Mzfw LF:4 LH:1 - BRDA:5,0,0,- - BRDA:7,0,1,- + BRDA:3,0,0,- + BRDA:5,0,1,- BRF:2 BRH:0 end_of_record @@ -156,11 +148,9 @@ def is_it_x(x): assert expected_result == actual_result def test_branch_coverage_two_files(self) -> None: - """Test that valid branch coverage is generated - in the case of two files.""" + # Test that valid branch coverage is generated + # in the case of two files. self.make_file("main_file.py", """\ - #!/usr/bin/env python3 - def is_it_x(x): if x == 3: return x @@ -169,8 +159,6 @@ def is_it_x(x): """) self.make_file("test_file.py", """\ - #!/usr/bin/env python3 - from main_file import * import unittest @@ -188,25 +176,25 @@ def test_is_it_x(self): expected_result = textwrap.dedent("""\ TN: SF:main_file.py - DA:3,1,4MDXMbvwQ3L7va1tsphVzw - DA:4,0,MuERA6EYyZNpKPqoJfzwkA - DA:5,0,sAyiiE6iAuPMte9kyd0+3g - DA:7,0,W/g8GJDAYJkSSurt59Mzfw + DA:1,1,4MDXMbvwQ3L7va1tsphVzw + DA:2,0,MuERA6EYyZNpKPqoJfzwkA + DA:3,0,sAyiiE6iAuPMte9kyd0+3g + DA:5,0,W/g8GJDAYJkSSurt59Mzfw LF:4 LH:1 - BRDA:5,0,0,- - BRDA:7,0,1,- + BRDA:3,0,0,- + BRDA:5,0,1,- BRF:2 BRH:0 end_of_record TN: SF:test_file.py - DA:3,1,9TxKIyoBtmhopmlbDNa8FQ - DA:4,1,E/tvV9JPVDhEcTCkgrwOFw - DA:6,1,C3s/c8C1Yd/zoNG1GnGexg - DA:7,1,9qPyWexYysgeKtB+YvuzAg - DA:8,0,LycuNcdqoUhPXeuXUTf5lA - DA:9,0,FPTWzd68bDx76HN7VHu1wA + DA:1,1,9TxKIyoBtmhopmlbDNa8FQ + DA:2,1,E/tvV9JPVDhEcTCkgrwOFw + DA:4,1,C3s/c8C1Yd/zoNG1GnGexg + DA:5,1,9qPyWexYysgeKtB+YvuzAg + DA:6,0,LycuNcdqoUhPXeuXUTf5lA + DA:7,0,FPTWzd68bDx76HN7VHu1wA LF:6 LH:4 BRF:0 @@ -217,9 +205,8 @@ def test_is_it_x(self): assert actual_result == expected_result def test_half_covered_branch(self) -> None: - """Test that for a given branch that is only half covered, - the block numbers remain the same, and produces valid lcov. - """ + # Test that for a given branch that is only half covered, + # the block numbers remain the same, and produces valid lcov. self.make_file("main_file.py", """\ something = True @@ -253,14 +240,13 @@ def test_half_covered_branch(self) -> None: assert actual_result == expected_result def test_empty_init_files(self) -> None: - """Test that in the case of an empty __init__.py file, the lcov - reporter will note that the file is there, and will note the empty - line. It will also note the lack of branches, and the checksum for - the line. - - Although there are no lines found, it will note one line as hit in - old Pythons, and no lines hit in newer Pythons. - """ + # Test that in the case of an empty __init__.py file, the lcov + # reporter will note that the file is there, and will note the empty + # line. It will also note the lack of branches, and the checksum for + # the line. + # + # Although there are no lines found, it will note one line as hit in + # old Pythons, and no lines hit in newer Pythons. self.make_file("__init__.py", "") self.assert_doesnt_exist(".coverage") diff --git a/tests/test_misc.py b/tests/test_misc.py index ba465cbd1..4b83f8aec 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -6,13 +6,14 @@ from __future__ import annotations import sys +from unittest import mock import pytest from coverage.exceptions import CoverageException from coverage.misc import file_be_gone from coverage.misc import Hasher, substitute_variables, import_third_party -from coverage.misc import human_sorted, human_sorted_items +from coverage.misc import human_sorted, human_sorted_items, stdout_link from tests.coveragetest import CoverageTest @@ -153,3 +154,21 @@ def test_human_sorted_items(words: str, ordered: str) -> None: oitems = [(k, v) for k in okeys for v in [1, 2]] assert human_sorted_items(items) == oitems assert human_sorted_items(items, reverse=True) == oitems[::-1] + + +def test_stdout_link_tty() -> None: + with mock.patch.object(sys.stdout, "isatty", lambda:True): + link = stdout_link("some text", "some url") + assert link == "\033]8;;some url\asome text\033]8;;\a" + + +def test_stdout_link_not_tty() -> None: + # Without mocking isatty, it reports False in a pytest suite. + assert stdout_link("some text", "some url") == "some text" + + +def test_stdout_link_with_fake_stdout() -> None: + # If stdout is another object, we should still be ok. + with mock.patch.object(sys, "stdout", object()): + link = stdout_link("some text", "some url") + assert link == "some text" diff --git a/tests/test_parser.py b/tests/test_parser.py index f74420b5d..8ff3226bc 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -128,7 +128,7 @@ def foo(): def test_indentation_error(self) -> None: msg = ( "Couldn't parse '<code>' as Python source: " + - "'unindent does not match any outer indentation level' at line 3" + "'unindent does not match any outer indentation level.*' at line 3" ) with pytest.raises(NotPython, match=msg): _ = self.parse_source("""\ @@ -138,11 +138,17 @@ def test_indentation_error(self) -> None: """) def test_token_error(self) -> None: - msg = "Couldn't parse '<code>' as Python source: 'EOF in multi-line string' at line 1" + submsgs = [ + r"EOF in multi-line string", # before 3.12.0b1 + r"unterminated triple-quoted string literal .detected at line 1.", # after 3.12.0b1 + ] + msg = ( + r"Couldn't parse '<code>' as Python source: '" + + r"(" + "|".join(submsgs) + ")" + + r"' at line 1" + ) with pytest.raises(NotPython, match=msg): - _ = self.parse_source("""\ - ''' - """) + _ = self.parse_source("'''") @xfail_pypy38 def test_decorator_pragmas(self) -> None: @@ -254,7 +260,10 @@ def bar(self): def test_fuzzed_double_parse(self) -> None: # https://bugs.chromium.org/p/oss-fuzz/issues/detail?id=50381 # The second parse used to raise `TypeError: 'NoneType' object is not iterable` - msg = "EOF in multi-line statement" + msg = ( + r"(EOF in multi-line statement)" # before 3.12.0b1 + + r"|(unmatched ']')" # after 3.12.0b1 + ) with pytest.raises(NotPython, match=msg): self.parse_source("]") with pytest.raises(NotPython, match=msg): @@ -322,10 +331,11 @@ def test_missing_arc_descriptions_for_small_callables(self) -> None: assert expected == parser.missing_arc_description(2, -2) expected = "line 3 didn't finish the generator expression on line 3" assert expected == parser.missing_arc_description(3, -3) - expected = "line 4 didn't finish the dictionary comprehension on line 4" - assert expected == parser.missing_arc_description(4, -4) - expected = "line 5 didn't finish the set comprehension on line 5" - assert expected == parser.missing_arc_description(5, -5) + if env.PYBEHAVIOR.comprehensions_are_functions: + expected = "line 4 didn't finish the dictionary comprehension on line 4" + assert expected == parser.missing_arc_description(4, -4) + expected = "line 5 didn't finish the set comprehension on line 5" + assert expected == parser.missing_arc_description(5, -5) def test_missing_arc_descriptions_for_exceptions(self) -> None: parser = self.parse_text("""\ diff --git a/tests/test_process.py b/tests/test_process.py index bdfa33164..e06b86f5d 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -8,6 +8,7 @@ import glob import os import os.path +import platform import re import stat import sys @@ -837,6 +838,12 @@ def test_coverage_custom_script(self) -> None: assert "hello-xyzzy" in out @pytest.mark.skipif(env.WINDOWS, reason="Windows can't make symlinks") + @pytest.mark.skipif( + platform.python_version().endswith("+"), + reason="setuptools barfs on dev versions: https://github.com/pypa/packaging/issues/678" + # https://github.com/nedbat/coveragepy/issues/1556 + # TODO: get rid of pkg_resources + ) def test_bug_862(self) -> None: # This simulates how pyenv and pyenv-virtualenv end up creating the # coverage executable. diff --git a/tests/test_report.py b/tests/test_report.py index c85c6b473..51a4fc683 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -1,68 +1,1081 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -"""Tests for helpers in report.py""" +"""Test text-based summary reporting for coverage.py""" from __future__ import annotations -from typing import IO, Iterable, List, Optional, Type +import glob +import io +import math +import os +import os.path +import py_compile +import re + +from typing import Tuple import pytest -from coverage.exceptions import CoverageException -from coverage.report import render_report -from coverage.types import TMorf +import coverage +from coverage import env +from coverage.control import Coverage +from coverage.data import CoverageData +from coverage.exceptions import ConfigError, NoDataError, NotPython +from coverage.files import abs_file +from coverage.report import SummaryReporter +from coverage.types import TConfigValueIn + +from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin +from tests.helpers import assert_coverage_warnings + + +class SummaryTest(UsingModulesMixin, CoverageTest): + """Tests of the text summary reporting for coverage.py.""" + + def make_mycode(self) -> None: + """Make the mycode.py file when needed.""" + self.make_file("mycode.py", """\ + import covmod1 + import covmodzip1 + a = 1 + print('done') + """) + + def test_report(self) -> None: + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + assert self.stdout() == 'done\n' + report = self.get_report(cov) + + # Name Stmts Miss Cover + # ------------------------------------------------------------------ + # c:/ned/coverage/tests/modules/covmod1.py 2 0 100% + # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100% + # mycode.py 4 0 100% + # ------------------------------------------------------------------ + # TOTAL 8 0 100% + + assert "/coverage/__init__/" not in report + assert "/tests/modules/covmod1.py " in report + assert "/tests/zipmods.zip/covmodzip1.py " in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 8 0 100%" + + def test_report_just_one(self) -> None: + # Try reporting just one module + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + report = self.get_report(cov, morfs=["mycode.py"]) + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_wildcard(self) -> None: + # Try reporting using wildcards to get the modules. + self.make_mycode() + # Wildcard is handled by shell or cmdline.py, so use real commands + self.run_command("coverage run mycode.py") + report = self.report_from_command("coverage report my*.py") + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_omitting(self) -> None: + # Try reporting while omitting some modules + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"]) + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_including(self) -> None: + # Try reporting while including some modules + self.make_mycode() + cov = coverage.Coverage() + self.start_import_stop(cov, "mycode") + report = self.get_report(cov, include=["mycode*"]) + + # Name Stmts Miss Cover + # ------------------------------- + # mycode.py 4 0 100% + # ------------------------------- + # TOTAL 4 0 100% + + assert self.line_count(report) == 5 + assert "/coverage/" not in report + assert "/tests/modules/covmod1.py " not in report + assert "/tests/zipmods.zip/covmodzip1.py " not in report + assert "mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" + + def test_report_include_relative_files_and_path(self) -> None: + """ + Test that when relative_files is True and a relative path to a module + is included, coverage is reported for the module. + + Ref: https://github.com/nedbat/coveragepy/issues/1604 + """ + self.make_mycode() + self.make_file(".coveragerc", """\ + [run] + relative_files = true + """) + self.make_file("submodule/mycode.py", "import mycode") + + cov = coverage.Coverage() + self.start_import_stop(cov, "submodule/mycode") + report = self.get_report(cov, include="submodule/mycode.py") + + # Name Stmts Miss Cover + # --------------------------------------- + # submodule/mycode.py 1 0 100% + # --------------------------------------- + # TOTAL 1 0 100% + + assert "submodule/mycode.py " in report + assert self.last_line_squeezed(report) == "TOTAL 1 0 100%" + + def test_report_include_relative_files_and_wildcard_path(self) -> None: + self.make_mycode() + self.make_file(".coveragerc", """\ + [run] + relative_files = true + """) + self.make_file("submodule/mycode.py", "import nested.submodule.mycode") + self.make_file("nested/submodule/mycode.py", "import mycode") + + cov = coverage.Coverage() + self.start_import_stop(cov, "submodule/mycode") + report = self.get_report(cov, include="*/submodule/mycode.py") + + # Name Stmts Miss Cover + # ------------------------------------------------- + # nested/submodule/mycode.py 1 0 100% + # submodule/mycode.py 1 0 100% + # ------------------------------------------------- + # TOTAL 2 0 100% + + reported_files = [line.split()[0] for line in report.splitlines()[2:4]] + assert reported_files == [ + "nested/submodule/mycode.py", + "submodule/mycode.py", + ] + + def test_omit_files_here(self) -> None: + # https://github.com/nedbat/coveragepy/issues/1407 + self.make_file("foo.py", "") + self.make_file("bar/bar.py", "") + self.make_file("tests/test_baz.py", """\ + def test_foo(): + assert True + test_foo() + """) + self.run_command("coverage run --source=. --omit='./*.py' -m tests.test_baz") + report = self.report_from_command("coverage report") + + # Name Stmts Miss Cover + # --------------------------------------- + # tests/test_baz.py 3 0 100% + # --------------------------------------- + # TOTAL 3 0 100% + + assert self.line_count(report) == 5 + assert "foo" not in report + assert "bar" not in report + assert "tests/test_baz.py" in report + assert self.last_line_squeezed(report) == "TOTAL 3 0 100%" + + def test_run_source_vs_report_include(self) -> None: + # https://github.com/nedbat/coveragepy/issues/621 + self.make_file(".coveragerc", """\ + [run] + source = . + + [report] + include = mod/*,tests/* + """) + # It should be OK to use that configuration. + cov = coverage.Coverage() + with self.assert_warnings(cov, []): + cov.start() + cov.stop() # pragma: nested -from tests.coveragetest import CoverageTest + def test_run_omit_vs_report_omit(self) -> None: + # https://github.com/nedbat/coveragepy/issues/622 + # report:omit shouldn't clobber run:omit. + self.make_mycode() + self.make_file(".coveragerc", """\ + [run] + omit = */covmodzip1.py + [report] + omit = */covmod1.py + """) + self.run_command("coverage run mycode.py") -class FakeReporter: - """A fake implementation of a one-file reporter.""" + # Read the data written, to see that the right files have been omitted from running. + covdata = CoverageData() + covdata.read() + files = [os.path.basename(p) for p in covdata.measured_files()] + assert "covmod1.py" in files + assert "covmodzip1.py" not in files - report_type = "fake report file" + def test_report_branches(self) -> None: + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + cov = coverage.Coverage(source=["."], branch=True) + self.start_import_stop(cov, "mybranch") + assert self.stdout() == 'x\n' + report = self.get_report(cov) - def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None: - self.output = output - self.error = error - self.morfs: Optional[Iterable[TMorf]] = None + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------------- + # mybranch.py 5 0 2 1 86% + # ----------------------------------------------- + # TOTAL 5 0 2 1 86% + assert self.line_count(report) == 5 + assert "mybranch.py " in report + assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%" - def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: - """Fake.""" - self.morfs = morfs - outfile.write(self.output) - if self.error: - raise self.error("You asked for it!") - return 17.25 + def test_report_show_missing(self) -> None: + self.make_file("mymissing.py", """\ + def missing(x, y): + if x: + print("x") + return x + if y: + print("y") + try: + print("z") + 1/0 + print("Never!") + except ZeroDivisionError: + pass + return x + missing(0, 1) + """) + cov = coverage.Coverage(source=["."]) + self.start_import_stop(cov, "mymissing") + assert self.stdout() == 'y\nz\n' + report = self.get_report(cov, show_missing=True) + # Name Stmts Miss Cover Missing + # -------------------------------------------- + # mymissing.py 14 3 79% 3-4, 10 + # -------------------------------------------- + # TOTAL 14 3 79% -class RenderReportTest(CoverageTest): - """Tests of render_report.""" + assert self.line_count(report) == 5 + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10" + assert squeezed[4] == "TOTAL 14 3 79%" - def test_stdout(self) -> None: - fake = FakeReporter(output="Hello!\n") - msgs: List[str] = [] - res = render_report("-", fake, [pytest, "coverage"], msgs.append) - assert res == 17.25 - assert fake.morfs == [pytest, "coverage"] - assert self.stdout() == "Hello!\n" - assert not msgs + def test_report_show_missing_branches(self) -> None: + self.make_file("mybranch.py", """\ + def branch(x, y): + if x: + print("x") + if y: + print("y") + branch(1, 1) + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "mybranch") + assert self.stdout() == 'x\ny\n' - def test_file(self) -> None: - fake = FakeReporter(output="Gréètings!\n") - msgs: List[str] = [] - res = render_report("output.txt", fake, [], msgs.append) - assert res == 17.25 + def test_report_show_missing_branches_and_lines(self) -> None: + self.make_file("main.py", """\ + import mybranch + """) + self.make_file("mybranch.py", """\ + def branch(x, y, z): + if x: + print("x") + if y: + print("y") + if z: + if x and y: + print("z") + return x + branch(1, 1, 0) + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == 'x\ny\n' + + def test_report_skip_covered_no_branches(self) -> None: + self.make_file("main.py", """ + import not_covered + + def normal(): + print("z") + normal() + """) + self.make_file("not_covered.py", """ + def not_covered(): + print("n") + """) + # --fail-under is handled by cmdline.py, use real commands. + out = self.run_command("coverage run main.py") + assert out == "z\n" + report = self.report_from_command("coverage report --skip-covered --fail-under=70") + + # Name Stmts Miss Cover + # ------------------------------------ + # not_covered.py 2 1 50% + # ------------------------------------ + # TOTAL 6 1 83% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "not_covered.py 2 1 50%" + assert squeezed[4] == "TOTAL 6 1 83%" + assert squeezed[6] == "1 file skipped due to complete coverage." + assert self.last_command_status == 0 + + def test_report_skip_covered_branches(self) -> None: + self.make_file("main.py", """ + import not_covered, covered + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("covered.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "n\nz\n" + report = self.get_report(cov, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # -------------------------------------------------- + # not_covered.py 4 0 2 1 83% + # -------------------------------------------------- + # TOTAL 13 0 4 1 94% + # + # 2 files skipped due to complete coverage. + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "not_covered.py 4 0 2 1 83%" + assert squeezed[4] == "TOTAL 13 0 4 1 94%" + assert squeezed[6] == "2 files skipped due to complete coverage." + + def test_report_skip_covered_branches_with_totals(self) -> None: + self.make_file("main.py", """ + import not_covered + import also_not_run + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("also_not_run.py", """ + def does_not_appear_in_this_film(ni): + print("Ni!") + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "n\nz\n" + report = self.get_report(cov, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # -------------------------------------------------- + # also_not_run.py 2 1 0 0 50% + # not_covered.py 4 0 2 1 83% + # -------------------------------------------------- + # TOTAL 13 1 4 1 88% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 8, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "also_not_run.py 2 1 0 0 50%" + assert squeezed[3] == "not_covered.py 4 0 2 1 83%" + assert squeezed[5] == "TOTAL 13 1 4 1 88%" + assert squeezed[7] == "1 file skipped due to complete coverage." + + def test_report_skip_covered_all_files_covered(self) -> None: + self.make_file("main.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(source=["."], branch=True) + self.start_import_stop(cov, "main") assert self.stdout() == "" - with open("output.txt", "rb") as f: - assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!" - assert msgs == ["Wrote fake report file to output.txt"] - - @pytest.mark.parametrize("error", [CoverageException, ZeroDivisionError]) - def test_exception(self, error: Type[Exception]) -> None: - fake = FakeReporter(error=error) - msgs: List[str] = [] - with pytest.raises(error, match="You asked for it!"): - render_report("output.txt", fake, [], msgs.append) + report = self.get_report(cov, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # TOTAL 3 0 0 0 100% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 5, report + squeezed = self.squeezed_lines(report) + assert squeezed[4] == "1 file skipped due to complete coverage." + + report = self.get_report(cov, squeeze=False, skip_covered=True, output_format="markdown") + + # | Name | Stmts | Miss | Branch | BrPart | Cover | + # |---------- | -------: | -------: | -------: | -------: | -------: | + # | **TOTAL** | **3** | **0** | **0** | **0** | **100%** | + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 5, report + assert report.split("\n")[0] == ( + '| Name | Stmts | Miss | Branch | BrPart | Cover |' + ) + assert report.split("\n")[1] == ( + '|---------- | -------: | -------: | -------: | -------: | -------: |' + ) + assert report.split("\n")[2] == ( + '| **TOTAL** | **3** | **0** | **0** | **0** | **100%** |' + ) + squeezed = self.squeezed_lines(report) + assert squeezed[4] == "1 file skipped due to complete coverage." + + total = self.get_report(cov, output_format="total", skip_covered=True) + assert total == "100\n" + + def test_report_skip_covered_longfilename(self) -> None: + self.make_file("long_______________filename.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(source=["."], branch=True) + self.start_import_stop(cov, "long_______________filename") + assert self.stdout() == "" + report = self.get_report(cov, squeeze=False, skip_covered=True) + + # Name Stmts Miss Branch BrPart Cover + # ----------------------------------------- + # TOTAL 3 0 0 0 100% + # + # 1 file skipped due to complete coverage. + + assert self.line_count(report) == 5, report + lines = self.report_lines(report) + assert lines[0] == "Name Stmts Miss Branch BrPart Cover" + squeezed = self.squeezed_lines(report) + assert squeezed[4] == "1 file skipped due to complete coverage." + + def test_report_skip_covered_no_data(self) -> None: + cov = coverage.Coverage() + cov.load() + with pytest.raises(NoDataError, match="No data to report."): + self.get_report(cov, skip_covered=True) + self.assert_doesnt_exist(".coverage") + + def test_report_skip_empty(self) -> None: + self.make_file("main.py", """ + import submodule + + def normal(): + print("z") + normal() + """) + self.make_file("submodule/__init__.py", "") + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + assert self.stdout() == "z\n" + report = self.get_report(cov, skip_empty=True) + + # Name Stmts Miss Cover + # ------------------------------------ + # main.py 4 0 100% + # ------------------------------------ + # TOTAL 4 0 100% + # + # 1 empty file skipped. + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "main.py 4 0 100%" + assert squeezed[4] == "TOTAL 4 0 100%" + assert squeezed[6] == "1 empty file skipped." + + def test_report_skip_empty_no_data(self) -> None: + self.make_file("__init__.py", "") + cov = coverage.Coverage() + self.start_import_stop(cov, "__init__") assert self.stdout() == "" - self.assert_doesnt_exist("output.txt") - assert not msgs + report = self.get_report(cov, skip_empty=True) + + # Name Stmts Miss Cover + # ------------------------------------ + # TOTAL 0 0 100% + # + # 1 empty file skipped. + + assert self.line_count(report) == 5, report + assert report.split("\n")[2] == "TOTAL 0 0 100%" + assert report.split("\n")[4] == "1 empty file skipped." + + def test_report_precision(self) -> None: + self.make_file(".coveragerc", """\ + [report] + precision = 3 + omit = */site-packages/* + """) + self.make_file("main.py", """ + import not_covered, covered + + def normal(z): + if z: + print("z") + normal(True) + normal(False) + """) + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + not_covered(True) + """) + self.make_file("covered.py", """ + def foo(): + pass + foo() + """) + cov = coverage.Coverage(branch=True) + self.start_import_stop(cov, "main") + assert self.stdout() == "n\nz\n" + report = self.get_report(cov, squeeze=False) + + # Name Stmts Miss Branch BrPart Cover + # ------------------------------------------------------ + # covered.py 3 0 0 0 100.000% + # main.py 6 0 2 0 100.000% + # not_covered.py 4 0 2 1 83.333% + # ------------------------------------------------------ + # TOTAL 13 0 4 1 94.118% + + assert self.line_count(report) == 7, report + squeezed = self.squeezed_lines(report) + assert squeezed[2] == "covered.py 3 0 0 0 100.000%" + assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%" + assert squeezed[6] == "TOTAL 13 0 4 1 94.118%" + + def test_report_precision_all_zero(self) -> None: + self.make_file("not_covered.py", """ + def not_covered(n): + if n: + print("n") + """) + self.make_file("empty.py", "") + cov = coverage.Coverage(source=["."]) + self.start_import_stop(cov, "empty") + report = self.get_report(cov, precision=6, squeeze=False) + + # Name Stmts Miss Cover + # ----------------------------------------- + # empty.py 0 0 100.000000% + # not_covered.py 3 3 0.000000% + # ----------------------------------------- + # TOTAL 3 3 0.000000% + + assert self.line_count(report) == 6, report + assert "empty.py 0 0 100.000000%" in report + assert "not_covered.py 3 3 0.000000%" in report + assert "TOTAL 3 3 0.000000%" in report + + def test_dotpy_not_python(self) -> None: + # We run a .py file, and when reporting, we can't parse it as Python. + # We should get an error message in the report. + + self.make_data_file(lines={"mycode.py": [1]}) + self.make_file("mycode.py", "This isn't python at all!") + cov = coverage.Coverage() + cov.load() + msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1" + with pytest.raises(NotPython, match=msg): + self.get_report(cov, morfs=["mycode.py"]) + + def test_accented_directory(self) -> None: + # Make a file with a non-ascii character in the directory name. + self.make_file("\xe2/accented.py", "print('accented')") + self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) + report_expected = ( + "Name Stmts Miss Cover\n" + + "-----------------------------------\n" + + "\xe2/accented.py 1 0 100%\n" + + "-----------------------------------\n" + + "TOTAL 1 0 100%\n" + ) + cov = coverage.Coverage() + cov.load() + output = self.get_report(cov, squeeze=False) + assert output == report_expected + + def test_accenteddotpy_not_python(self) -> None: + # We run a .py file with a non-ascii name, and when reporting, we can't + # parse it as Python. We should get an error message in the report. + + self.make_data_file(lines={"accented\xe2.py": [1]}) + self.make_file("accented\xe2.py", "This isn't python at all!") + cov = coverage.Coverage() + cov.load() + msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1" + with pytest.raises(NotPython, match=msg): + self.get_report(cov, morfs=["accented\xe2.py"]) + + def test_dotpy_not_python_ignored(self) -> None: + # We run a .py file, and when reporting, we can't parse it as Python, + # but we've said to ignore errors, so there's no error reported, + # though we still get a warning. + self.make_file("mycode.py", "This isn't python at all!") + self.make_data_file(lines={"mycode.py": [1]}) + cov = coverage.Coverage() + cov.load() + with pytest.raises(NoDataError, match="No data to report."): + with pytest.warns(Warning) as warns: + self.get_report(cov, morfs=["mycode.py"], ignore_errors=True) + assert_coverage_warnings( + warns, + re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"), + ) + + def test_dothtml_not_python(self) -> None: + # We run a .html file, and when reporting, we can't parse it as + # Python. Since it wasn't .py, no error is reported. + + # Pretend to run an html file. + self.make_file("mycode.html", "<h1>This isn't python at all!</h1>") + self.make_data_file(lines={"mycode.html": [1]}) + cov = coverage.Coverage() + cov.load() + with pytest.raises(NoDataError, match="No data to report."): + self.get_report(cov, morfs=["mycode.html"]) + + def test_report_no_extension(self) -> None: + self.make_file("xxx", """\ + # This is a python file though it doesn't look like it, like a main script. + a = b = c = d = 0 + a = 3 + b = 4 + if not b: + c = 6 + d = 7 + print(f"xxx: {a} {b} {c} {d}") + """) + self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]}) + cov = coverage.Coverage() + cov.load() + report = self.get_report(cov) + assert self.last_line_squeezed(report) == "TOTAL 7 1 86%" + + def test_report_with_chdir(self) -> None: + self.make_file("chdir.py", """\ + import os + print("Line One") + os.chdir("subdir") + print("Line Two") + print(open("something").read()) + """) + self.make_file("subdir/something", "hello") + out = self.run_command("coverage run --source=. chdir.py") + assert out == "Line One\nLine Two\nhello\n" + report = self.report_from_command("coverage report") + assert self.last_line_squeezed(report) == "TOTAL 5 0 100%" + report = self.report_from_command("coverage report --format=markdown") + assert self.last_line_squeezed(report) == "| **TOTAL** | **5** | **0** | **100%** |" + + def test_bug_156_file_not_run_should_be_zero(self) -> None: + # https://github.com/nedbat/coveragepy/issues/156 + self.make_file("mybranch.py", """\ + def branch(x): + if x: + print("x") + return x + branch(1) + """) + self.make_file("main.py", """\ + print("y") + """) + cov = coverage.Coverage(branch=True, source=["."]) + self.start_import_stop(cov, "main") + report = self.get_report(cov).splitlines() + assert "mybranch.py 5 5 2 0 0%" in report + + def run_TheCode_and_report_it(self) -> str: + """A helper for the next few tests.""" + cov = coverage.Coverage() + self.start_import_stop(cov, "TheCode") + return self.get_report(cov) + + def test_bug_203_mixed_case_listed_twice_with_rc(self) -> None: + self.make_file("TheCode.py", "a = 1\n") + self.make_file(".coveragerc", "[run]\nsource = .\n") + + report = self.run_TheCode_and_report_it() + assert "TheCode" in report + assert "thecode" not in report + + def test_bug_203_mixed_case_listed_twice(self) -> None: + self.make_file("TheCode.py", "a = 1\n") + + report = self.run_TheCode_and_report_it() + + assert "TheCode" in report + assert "thecode" not in report + + @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.") + def test_pyw_files(self) -> None: + # https://github.com/nedbat/coveragepy/issues/261 + self.make_file("start.pyw", """\ + import mod + print("In start.pyw") + """) + self.make_file("mod.pyw", """\ + print("In mod.pyw") + """) + cov = coverage.Coverage() + # start_import_stop can't import the .pyw file, so use the long form. + cov.start() + import start # pragma: nested # pylint: disable=import-error, unused-import + cov.stop() # pragma: nested + + report = self.get_report(cov) + assert "NoSource" not in report + report_lines = report.splitlines() + assert "start.pyw 2 0 100%" in report_lines + assert "mod.pyw 1 0 100%" in report_lines + + def test_tracing_pyc_file(self) -> None: + # Create two Python files. + self.make_file("mod.py", "a = 1\n") + self.make_file("main.py", "import mod\n") + + # Make one into a .pyc. + py_compile.compile("mod.py") + + # Run the program. + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + + report_lines = self.get_report(cov).splitlines() + assert "mod.py 1 0 100%" in report_lines + report = self.get_report(cov, squeeze=False, output_format="markdown") + assert report.split("\n")[3] == "| mod.py | 1 | 0 | 100% |" + assert report.split("\n")[4] == "| **TOTAL** | **2** | **0** | **100%** |" + + def test_missing_py_file_during_run(self) -> None: + # Create two Python files. + self.make_file("mod.py", "a = 1\n") + self.make_file("main.py", "import mod\n") + + # Make one into a .pyc, and remove the .py. + py_compile.compile("mod.py") + os.remove("mod.py") + + # Python 3 puts the .pyc files in a __pycache__ directory, and will + # not import from there without source. It will import a .pyc from + # the source location though. + pycs = glob.glob("__pycache__/mod.*.pyc") + assert len(pycs) == 1 + os.rename(pycs[0], "mod.pyc") + + # Run the program. + cov = coverage.Coverage() + self.start_import_stop(cov, "main") + + # Put back the missing Python file. + self.make_file("mod.py", "a = 1\n") + report = self.get_report(cov).splitlines() + assert "mod.py 1 0 100%" in report + + def test_empty_files(self) -> None: + # Shows that empty files like __init__.py are listed as having zero + # statements, not one statement. + cov = coverage.Coverage(branch=True) + cov.start() + import usepkgs # pragma: nested # pylint: disable=import-error, unused-import + cov.stop() # pragma: nested + report = self.get_report(cov) + assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report + assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report + report = self.get_report(cov, squeeze=False, output_format="markdown") + # get_report() escapes backslash so we expect forward slash escaped + # underscore + assert "tests/modules/pkg1//_/_init/_/_.py " in report + assert "| 1 | 0 | 0 | 0 | 100% |" in report + assert "tests/modules/pkg2//_/_init/_/_.py " in report + assert "| 0 | 0 | 0 | 0 | 100% |" in report + + def test_markdown_with_missing(self) -> None: + self.make_file("mymissing.py", """\ + def missing(x, y): + if x: + print("x") + return x + if y: + print("y") + try: + print("z") + 1/0 + print("Never!") + except ZeroDivisionError: + pass + return x + missing(0, 1) + """) + cov = coverage.Coverage(source=["."]) + self.start_import_stop(cov, "mymissing") + assert self.stdout() == 'y\nz\n' + report = self.get_report(cov, squeeze=False, output_format="markdown", show_missing=True) + + # | Name | Stmts | Miss | Cover | Missing | + # |------------- | -------: | -------: | ------: | --------: | + # | mymissing.py | 14 | 3 | 79% | 3-4, 10 | + # | **TOTAL** | **14** | **3** | **79%** | | + assert self.line_count(report) == 4 + report_lines = report.split("\n") + assert report_lines[2] == "| mymissing.py | 14 | 3 | 79% | 3-4, 10 |" + assert report_lines[3] == "| **TOTAL** | **14** | **3** | **79%** | |" + + assert self.get_report(cov, output_format="total") == "79\n" + assert self.get_report(cov, output_format="total", precision=2) == "78.57\n" + assert self.get_report(cov, output_format="total", precision=4) == "78.5714\n" + + def test_bug_1524(self) -> None: + self.make_file("bug1524.py", """\ + class Mine: + @property + def thing(self) -> int: + return 17 + + print(Mine().thing) + """) + cov = coverage.Coverage() + self.start_import_stop(cov, "bug1524") + assert self.stdout() == "17\n" + report = self.get_report(cov) + report_lines = report.splitlines() + assert report_lines[2] == "bug1524.py 5 0 100%" + + +class ReportingReturnValueTest(CoverageTest): + """Tests of reporting functions returning values.""" + + def run_coverage(self) -> Coverage: + """Run coverage on doit.py and return the coverage object.""" + self.make_file("doit.py", """\ + a = 1 + b = 2 + c = 3 + d = 4 + if a > 10: + f = 6 + g = 7 + """) + + cov = coverage.Coverage() + self.start_import_stop(cov, "doit") + return cov + + def test_report(self) -> None: + cov = self.run_coverage() + val = cov.report(include="*/doit.py") + assert math.isclose(val, 6 / 7 * 100) + + def test_html(self) -> None: + cov = self.run_coverage() + val = cov.html_report(include="*/doit.py") + assert math.isclose(val, 6 / 7 * 100) + + def test_xml(self) -> None: + cov = self.run_coverage() + val = cov.xml_report(include="*/doit.py") + assert math.isclose(val, 6 / 7 * 100) + + +class SummaryReporterConfigurationTest(CoverageTest): + """Tests of SummaryReporter.""" + + def make_rigged_file(self, filename: str, stmts: int, miss: int) -> None: + """Create a file that will have specific results. + + `stmts` and `miss` are ints, the number of statements, and + missed statements that should result. + """ + run = stmts - miss - 1 + dont_run = miss + source = "" + source += "a = 1\n" * run + source += "if a == 99:\n" + source += " a = 2\n" * dont_run + self.make_file(filename, source) + + def get_summary_text(self, *options: Tuple[str, TConfigValueIn]) -> str: + """Get text output from the SummaryReporter. + + The arguments are tuples: (name, value) for Coverage.set_option. + """ + self.make_rigged_file("file1.py", 339, 155) + self.make_rigged_file("file2.py", 13, 3) + self.make_rigged_file("file10.py", 234, 228) + self.make_file("doit.py", "import file1, file2, file10") + + cov = Coverage(source=["."], omit=["doit.py"]) + self.start_import_stop(cov, "doit") + for name, value in options: + cov.set_option(name, value) + printer = SummaryReporter(cov) + destination = io.StringIO() + printer.report([], destination) + return destination.getvalue() + + def test_test_data(self) -> None: + # We use our own test files as test data. Check that our assumptions + # about them are still valid. We want the three columns of numbers to + # sort in three different orders. + report = self.get_summary_text() + # Name Stmts Miss Cover + # ------------------------------ + # file1.py 339 155 54% + # file2.py 13 3 77% + # file10.py 234 228 3% + # ------------------------------ + # TOTAL 586 386 34% + lines = report.splitlines()[2:-2] + assert len(lines) == 3 + nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines] + # [ + # [339, 155, 54], + # [ 13, 3, 77], + # [234, 228, 3] + # ] + assert nums[1][0] < nums[2][0] < nums[0][0] + assert nums[1][1] < nums[0][1] < nums[2][1] + assert nums[2][2] < nums[0][2] < nums[1][2] + + def test_defaults(self) -> None: + """Run the report with no configuration options.""" + report = self.get_summary_text() + assert 'Missing' not in report + assert 'Branch' not in report + + def test_print_missing(self) -> None: + """Run the report printing the missing lines.""" + report = self.get_summary_text(('report:show_missing', True)) + assert 'Missing' in report + assert 'Branch' not in report + + def assert_ordering(self, text: str, *words: str) -> None: + """Assert that the `words` appear in order in `text`.""" + indexes = list(map(text.find, words)) + assert -1 not in indexes + msg = f"The words {words!r} don't appear in order in {text!r}" + assert indexes == sorted(indexes), msg + + def test_default_sort_report(self) -> None: + # Sort the text report by the default (Name) column. + report = self.get_summary_text() + self.assert_ordering(report, "file1.py", "file2.py", "file10.py") + + def test_sort_report_by_name(self) -> None: + # Sort the text report explicitly by the Name column. + report = self.get_summary_text(('report:sort', 'Name')) + self.assert_ordering(report, "file1.py", "file2.py", "file10.py") + + def test_sort_report_by_stmts(self) -> None: + # Sort the text report by the Stmts column. + report = self.get_summary_text(('report:sort', 'Stmts')) + self.assert_ordering(report, "file2.py", "file10.py", "file1.py") + + def test_sort_report_by_missing(self) -> None: + # Sort the text report by the Missing column. + report = self.get_summary_text(('report:sort', 'Miss')) + self.assert_ordering(report, "file2.py", "file1.py", "file10.py") + + def test_sort_report_by_cover(self) -> None: + # Sort the text report by the Cover column. + report = self.get_summary_text(('report:sort', 'Cover')) + self.assert_ordering(report, "file10.py", "file1.py", "file2.py") + + def test_sort_report_by_cover_plus(self) -> None: + # Sort the text report by the Cover column, including the explicit + sign. + report = self.get_summary_text(('report:sort', '+Cover')) + self.assert_ordering(report, "file10.py", "file1.py", "file2.py") + + def test_sort_report_by_cover_reversed(self) -> None: + # Sort the text report by the Cover column reversed. + report = self.get_summary_text(('report:sort', '-Cover')) + self.assert_ordering(report, "file2.py", "file1.py", "file10.py") + + def test_sort_report_by_invalid_option(self) -> None: + # Sort the text report by a nonsense column. + msg = "Invalid sorting option: 'Xyzzy'" + with pytest.raises(ConfigError, match=msg): + self.get_summary_text(('report:sort', 'Xyzzy')) + + def test_report_with_invalid_format(self) -> None: + # Ask for an invalid format. + msg = "Unknown report format choice: 'xyzzy'" + with pytest.raises(ConfigError, match=msg): + self.get_summary_text(('report:format', 'xyzzy')) diff --git a/tests/test_report_common.py b/tests/test_report_common.py new file mode 100644 index 000000000..685515172 --- /dev/null +++ b/tests/test_report_common.py @@ -0,0 +1,283 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Tests of behavior common to all reporting.""" + +from __future__ import annotations + +import textwrap + +import coverage +from coverage import env +from coverage.files import abs_file + +from tests.coveragetest import CoverageTest +from tests.goldtest import contains, doesnt_contain +from tests.helpers import arcz_to_arcs, os_sep + + +class ReportMapsPathsTest(CoverageTest): + """Check that reporting implicitly maps paths.""" + + def make_files(self, data: str, settings: bool = False) -> None: + """Create the test files we need for line coverage.""" + src = """\ + if VER == 1: + print("line 2") + if VER == 2: + print("line 4") + if VER == 3: + print("line 6") + """ + self.make_file("src/program.py", src) + self.make_file("ver1/program.py", src) + self.make_file("ver2/program.py", src) + + if data == "line": + self.make_data_file( + lines={ + abs_file("ver1/program.py"): [1, 2, 3, 5], + abs_file("ver2/program.py"): [1, 3, 4, 5], + } + ) + else: + self.make_data_file( + arcs={ + abs_file("ver1/program.py"): arcz_to_arcs(".1 12 23 35 5."), + abs_file("ver2/program.py"): arcz_to_arcs(".1 13 34 45 5."), + } + ) + + if settings: + self.make_file(".coveragerc", """\ + [paths] + source = + src + ver1 + ver2 + """) + + def test_map_paths_during_line_report_without_setting(self) -> None: + self.make_files(data="line") + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Cover Missing + ----------------------------------------------- + ver1/program.py 6 2 67% 4, 6 + ver2/program.py 6 2 67% 2, 6 + ----------------------------------------------- + TOTAL 12 4 67% + """)) + assert expected == self.stdout() + + def test_map_paths_during_line_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Cover Missing + ---------------------------------------------- + src/program.py 6 1 83% 6 + ---------------------------------------------- + TOTAL 6 1 83% + """)) + assert expected == self.stdout() + + def test_map_paths_during_branch_report_without_setting(self) -> None: + self.make_files(data="arcs") + cov = coverage.Coverage(branch=True) + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Branch BrPart Cover Missing + ------------------------------------------------------------- + ver1/program.py 6 2 6 3 58% 1->3, 4, 6 + ver2/program.py 6 2 6 3 58% 2, 3->5, 6 + ------------------------------------------------------------- + TOTAL 12 4 12 6 58% + """)) + assert expected == self.stdout() + + def test_map_paths_during_branch_report(self) -> None: + self.make_files(data="arcs", settings=True) + cov = coverage.Coverage(branch=True) + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent(os_sep("""\ + Name Stmts Miss Branch BrPart Cover Missing + ------------------------------------------------------------ + src/program.py 6 1 6 1 83% 6 + ------------------------------------------------------------ + TOTAL 6 1 6 1 83% + """)) + assert expected == self.stdout() + + def test_map_paths_during_annotate(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.annotate() + self.assert_exists(os_sep("src/program.py,cover")) + self.assert_doesnt_exist(os_sep("ver1/program.py,cover")) + self.assert_doesnt_exist(os_sep("ver2/program.py,cover")) + + def test_map_paths_during_html_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.html_report() + contains("htmlcov/index.html", os_sep("src/program.py")) + doesnt_contain("htmlcov/index.html", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + def test_map_paths_during_xml_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.xml_report() + contains("coverage.xml", "src/program.py") + doesnt_contain("coverage.xml", "ver1/program.py", "ver2/program.py") + + def test_map_paths_during_json_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.json_report() + def os_sepj(s: str) -> str: + return os_sep(s).replace("\\", r"\\") + contains("coverage.json", os_sepj("src/program.py")) + doesnt_contain("coverage.json", os_sepj("ver1/program.py"), os_sepj("ver2/program.py")) + + def test_map_paths_during_lcov_report(self) -> None: + self.make_files(data="line", settings=True) + cov = coverage.Coverage() + cov.load() + cov.lcov_report() + contains("coverage.lcov", os_sep("src/program.py")) + doesnt_contain("coverage.lcov", os_sep("ver1/program.py"), os_sep("ver2/program.py")) + + +class ReportWithJinjaTest(CoverageTest): + """Tests of Jinja-like behavior. + + Jinja2 compiles a template into Python code, and then runs the Python code + to render the template. But during rendering, it uses the template name + (for example, "template.j2") as the file name, not the Python code file + name. Then during reporting, we will try to parse template.j2 as Python + code. + + If the file can be parsed, it's included in the report (as a Python file!). + If it can't be parsed, then it's not included in the report. + + These tests confirm that code doesn't raise an exception (as reported in + #1553), and that the current (incorrect) behavior remains stable. Ideally, + good.j2 wouldn't be listed at all, since we can't report on it accurately. + + See https://github.com/nedbat/coveragepy/issues/1553 for more detail, and + https://github.com/nedbat/coveragepy/issues/1623 for an issue about this + behavior. + + """ + + def make_files(self) -> None: + """Create test files: two Jinja templates, and data from rendering them.""" + # A Jinja2 file that is syntactically acceptable Python (though it wont run). + self.make_file("good.j2", """\ + {{ data }} + line2 + line3 + """) + # A Jinja2 file that is a Python syntax error. + self.make_file("bad.j2", """\ + This is data: {{ data }}. + line 2 + line 3 + """) + self.make_data_file( + lines={ + abs_file("good.j2"): [1, 3, 5, 7, 9], + abs_file("bad.j2"): [1, 3, 5, 7, 9], + } + ) + + def test_report(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.report(show_missing=True) + expected = textwrap.dedent("""\ + Name Stmts Miss Cover Missing + --------------------------------------- + good.j2 3 1 67% 2 + --------------------------------------- + TOTAL 3 1 67% + """) + assert expected == self.stdout() + + def test_html(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.html_report() + contains("htmlcov/index.html", """\ + <tbody> + <tr class="file"> + <td class="name left"><a href="good_j2.html">good.j2</a></td> + <td>3</td> + <td>1</td> + <td>0</td> + <td class="right" data-ratio="2 3">67%</td> + </tr> + </tbody>""" + ) + doesnt_contain("htmlcov/index.html", "bad.j2") + + def test_xml(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.xml_report() + contains("coverage.xml", 'filename="good.j2"') + if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order. + contains("coverage.xml", + '<line number="1" hits="1"/>', + '<line number="2" hits="0"/>', + '<line number="3" hits="1"/>', + ) + doesnt_contain("coverage.xml", 'filename="bad.j2"') + if env.PYVERSION >= (3, 8): # Py3.7 puts attributes in the other order. + doesnt_contain("coverage.xml", '<line number="4"',) + + def test_json(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.json_report() + contains("coverage.json", + # Notice the .json report claims lines in good.j2 executed that + # don't even exist in good.j2... + '"files": {"good.j2": {"executed_lines": [1, 3, 5, 7, 9], ' + + '"summary": {"covered_lines": 2, "num_statements": 3', + ) + doesnt_contain("coverage.json", "bad.j2") + + def test_lcov(self) -> None: + self.make_files() + cov = coverage.Coverage() + cov.load() + cov.lcov_report() + with open("coverage.lcov") as lcov: + actual = lcov.read() + expected = textwrap.dedent("""\ + TN: + SF:good.j2 + DA:1,1,FHs1rDakj9p/NAzMCu3Kgw + DA:3,1,DGOyp8LEgI+3CcdFYw9uKQ + DA:2,0,5iUbzxp9w7peeTPjJbvmBQ + LF:3 + LH:2 + end_of_record + """) + assert expected == actual diff --git a/tests/test_report_core.py b/tests/test_report_core.py new file mode 100644 index 000000000..77e234b66 --- /dev/null +++ b/tests/test_report_core.py @@ -0,0 +1,68 @@ +# Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 +# For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt + +"""Tests for helpers in report.py""" + +from __future__ import annotations + +from typing import IO, Iterable, List, Optional, Type + +import pytest + +from coverage.exceptions import CoverageException +from coverage.report_core import render_report +from coverage.types import TMorf + +from tests.coveragetest import CoverageTest + + +class FakeReporter: + """A fake implementation of a one-file reporter.""" + + report_type = "fake report file" + + def __init__(self, output: str = "", error: Optional[Type[Exception]] = None) -> None: + self.output = output + self.error = error + self.morfs: Optional[Iterable[TMorf]] = None + + def report(self, morfs: Optional[Iterable[TMorf]], outfile: IO[str]) -> float: + """Fake.""" + self.morfs = morfs + outfile.write(self.output) + if self.error: + raise self.error("You asked for it!") + return 17.25 + + +class RenderReportTest(CoverageTest): + """Tests of render_report.""" + + def test_stdout(self) -> None: + fake = FakeReporter(output="Hello!\n") + msgs: List[str] = [] + res = render_report("-", fake, [pytest, "coverage"], msgs.append) + assert res == 17.25 + assert fake.morfs == [pytest, "coverage"] + assert self.stdout() == "Hello!\n" + assert not msgs + + def test_file(self) -> None: + fake = FakeReporter(output="Gréètings!\n") + msgs: List[str] = [] + res = render_report("output.txt", fake, [], msgs.append) + assert res == 17.25 + assert self.stdout() == "" + with open("output.txt", "rb") as f: + assert f.read().rstrip() == b"Gr\xc3\xa9\xc3\xa8tings!" + assert msgs == ["Wrote fake report file to output.txt"] + + @pytest.mark.parametrize("error", [CoverageException, ZeroDivisionError]) + def test_exception(self, error: Type[Exception]) -> None: + fake = FakeReporter(error=error) + msgs: List[str] = [] + with pytest.raises(error, match="You asked for it!"): + render_report("output.txt", fake, [], msgs.append) + assert self.stdout() == "" + self.assert_doesnt_exist("output.txt") + assert not msgs diff --git a/tests/test_summary.py b/tests/test_summary.py deleted file mode 100644 index f532a7b1f..000000000 --- a/tests/test_summary.py +++ /dev/null @@ -1,1028 +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 - -"""Test text-based summary reporting for coverage.py""" - -from __future__ import annotations - -import glob -import io -import math -import os -import os.path -import py_compile -import re - -from typing import Tuple - -import pytest - -import coverage -from coverage import env -from coverage.control import Coverage -from coverage.data import CoverageData -from coverage.exceptions import ConfigError, NoDataError, NotPython -from coverage.files import abs_file -from coverage.summary import SummaryReporter -from coverage.types import TConfigValueIn - -from tests.coveragetest import CoverageTest, TESTS_DIR, UsingModulesMixin -from tests.helpers import assert_coverage_warnings - - -class SummaryTest(UsingModulesMixin, CoverageTest): - """Tests of the text summary reporting for coverage.py.""" - - def make_mycode(self) -> None: - """Make the mycode.py file when needed.""" - self.make_file("mycode.py", """\ - import covmod1 - import covmodzip1 - a = 1 - print('done') - """) - - def test_report(self) -> None: - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - assert self.stdout() == 'done\n' - report = self.get_report(cov) - - # Name Stmts Miss Cover - # ------------------------------------------------------------------ - # c:/ned/coverage/tests/modules/covmod1.py 2 0 100% - # c:/ned/coverage/tests/zipmods.zip/covmodzip1.py 2 0 100% - # mycode.py 4 0 100% - # ------------------------------------------------------------------ - # TOTAL 8 0 100% - - assert "/coverage/__init__/" not in report - assert "/tests/modules/covmod1.py " in report - assert "/tests/zipmods.zip/covmodzip1.py " in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 8 0 100%" - - def test_report_just_one(self) -> None: - # Try reporting just one module - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - report = self.get_report(cov, morfs=["mycode.py"]) - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_report_wildcard(self) -> None: - # Try reporting using wildcards to get the modules. - self.make_mycode() - # Wildcard is handled by shell or cmdline.py, so use real commands - self.run_command("coverage run mycode.py") - report = self.report_from_command("coverage report my*.py") - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_report_omitting(self) -> None: - # Try reporting while omitting some modules - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - report = self.get_report(cov, omit=[f"{TESTS_DIR}/*", "*/site-packages/*"]) - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_report_including(self) -> None: - # Try reporting while including some modules - self.make_mycode() - cov = coverage.Coverage() - self.start_import_stop(cov, "mycode") - report = self.get_report(cov, include=["mycode*"]) - - # Name Stmts Miss Cover - # ------------------------------- - # mycode.py 4 0 100% - # ------------------------------- - # TOTAL 4 0 100% - - assert self.line_count(report) == 5 - assert "/coverage/" not in report - assert "/tests/modules/covmod1.py " not in report - assert "/tests/zipmods.zip/covmodzip1.py " not in report - assert "mycode.py " in report - assert self.last_line_squeezed(report) == "TOTAL 4 0 100%" - - def test_omit_files_here(self) -> None: - # https://github.com/nedbat/coveragepy/issues/1407 - self.make_file("foo.py", "") - self.make_file("bar/bar.py", "") - self.make_file("tests/test_baz.py", """\ - def test_foo(): - assert True - test_foo() - """) - self.run_command("coverage run --source=. --omit='./*.py' -m tests.test_baz") - report = self.report_from_command("coverage report") - - # Name Stmts Miss Cover - # --------------------------------------- - # tests/test_baz.py 3 0 100% - # --------------------------------------- - # TOTAL 3 0 100% - - assert self.line_count(report) == 5 - assert "foo" not in report - assert "bar" not in report - assert "tests/test_baz.py" in report - assert self.last_line_squeezed(report) == "TOTAL 3 0 100%" - - def test_run_source_vs_report_include(self) -> None: - # https://github.com/nedbat/coveragepy/issues/621 - self.make_file(".coveragerc", """\ - [run] - source = . - - [report] - include = mod/*,tests/* - """) - # It should be OK to use that configuration. - cov = coverage.Coverage() - with self.assert_warnings(cov, []): - cov.start() - cov.stop() # pragma: nested - - def test_run_omit_vs_report_omit(self) -> None: - # https://github.com/nedbat/coveragepy/issues/622 - # report:omit shouldn't clobber run:omit. - self.make_mycode() - self.make_file(".coveragerc", """\ - [run] - omit = */covmodzip1.py - - [report] - omit = */covmod1.py - """) - self.run_command("coverage run mycode.py") - - # Read the data written, to see that the right files have been omitted from running. - covdata = CoverageData() - covdata.read() - files = [os.path.basename(p) for p in covdata.measured_files()] - assert "covmod1.py" in files - assert "covmodzip1.py" not in files - - def test_report_branches(self) -> None: - self.make_file("mybranch.py", """\ - def branch(x): - if x: - print("x") - return x - branch(1) - """) - cov = coverage.Coverage(source=["."], branch=True) - self.start_import_stop(cov, "mybranch") - assert self.stdout() == 'x\n' - report = self.get_report(cov) - - # Name Stmts Miss Branch BrPart Cover - # ----------------------------------------------- - # mybranch.py 5 0 2 1 86% - # ----------------------------------------------- - # TOTAL 5 0 2 1 86% - assert self.line_count(report) == 5 - assert "mybranch.py " in report - assert self.last_line_squeezed(report) == "TOTAL 5 0 2 1 86%" - - def test_report_show_missing(self) -> None: - self.make_file("mymissing.py", """\ - def missing(x, y): - if x: - print("x") - return x - if y: - print("y") - try: - print("z") - 1/0 - print("Never!") - except ZeroDivisionError: - pass - return x - missing(0, 1) - """) - cov = coverage.Coverage(source=["."]) - self.start_import_stop(cov, "mymissing") - assert self.stdout() == 'y\nz\n' - report = self.get_report(cov, show_missing=True) - - # Name Stmts Miss Cover Missing - # -------------------------------------------- - # mymissing.py 14 3 79% 3-4, 10 - # -------------------------------------------- - # TOTAL 14 3 79% - - assert self.line_count(report) == 5 - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "mymissing.py 14 3 79% 3-4, 10" - assert squeezed[4] == "TOTAL 14 3 79%" - - def test_report_show_missing_branches(self) -> None: - self.make_file("mybranch.py", """\ - def branch(x, y): - if x: - print("x") - if y: - print("y") - branch(1, 1) - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "mybranch") - assert self.stdout() == 'x\ny\n' - - def test_report_show_missing_branches_and_lines(self) -> None: - self.make_file("main.py", """\ - import mybranch - """) - self.make_file("mybranch.py", """\ - def branch(x, y, z): - if x: - print("x") - if y: - print("y") - if z: - if x and y: - print("z") - return x - branch(1, 1, 0) - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == 'x\ny\n' - - def test_report_skip_covered_no_branches(self) -> None: - self.make_file("main.py", """ - import not_covered - - def normal(): - print("z") - normal() - """) - self.make_file("not_covered.py", """ - def not_covered(): - print("n") - """) - # --fail-under is handled by cmdline.py, use real commands. - out = self.run_command("coverage run main.py") - assert out == "z\n" - report = self.report_from_command("coverage report --skip-covered --fail-under=70") - - # Name Stmts Miss Cover - # ------------------------------------ - # not_covered.py 2 1 50% - # ------------------------------------ - # TOTAL 6 1 83% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "not_covered.py 2 1 50%" - assert squeezed[4] == "TOTAL 6 1 83%" - assert squeezed[6] == "1 file skipped due to complete coverage." - assert self.last_command_status == 0 - - def test_report_skip_covered_branches(self) -> None: - self.make_file("main.py", """ - import not_covered, covered - - def normal(z): - if z: - print("z") - normal(True) - normal(False) - """) - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - not_covered(True) - """) - self.make_file("covered.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "n\nz\n" - report = self.get_report(cov, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # -------------------------------------------------- - # not_covered.py 4 0 2 1 83% - # -------------------------------------------------- - # TOTAL 13 0 4 1 94% - # - # 2 files skipped due to complete coverage. - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "not_covered.py 4 0 2 1 83%" - assert squeezed[4] == "TOTAL 13 0 4 1 94%" - assert squeezed[6] == "2 files skipped due to complete coverage." - - def test_report_skip_covered_branches_with_totals(self) -> None: - self.make_file("main.py", """ - import not_covered - import also_not_run - - def normal(z): - if z: - print("z") - normal(True) - normal(False) - """) - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - not_covered(True) - """) - self.make_file("also_not_run.py", """ - def does_not_appear_in_this_film(ni): - print("Ni!") - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "n\nz\n" - report = self.get_report(cov, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # -------------------------------------------------- - # also_not_run.py 2 1 0 0 50% - # not_covered.py 4 0 2 1 83% - # -------------------------------------------------- - # TOTAL 13 1 4 1 88% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 8, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "also_not_run.py 2 1 0 0 50%" - assert squeezed[3] == "not_covered.py 4 0 2 1 83%" - assert squeezed[5] == "TOTAL 13 1 4 1 88%" - assert squeezed[7] == "1 file skipped due to complete coverage." - - def test_report_skip_covered_all_files_covered(self) -> None: - self.make_file("main.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(source=["."], branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "" - report = self.get_report(cov, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # ----------------------------------------- - # TOTAL 3 0 0 0 100% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 5, report - squeezed = self.squeezed_lines(report) - assert squeezed[4] == "1 file skipped due to complete coverage." - - report = self.get_report(cov, squeeze=False, skip_covered=True, output_format="markdown") - - # | Name | Stmts | Miss | Branch | BrPart | Cover | - # |---------- | -------: | -------: | -------: | -------: | -------: | - # | **TOTAL** | **3** | **0** | **0** | **0** | **100%** | - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 5, report - assert report.split("\n")[0] == ( - '| Name | Stmts | Miss | Branch | BrPart | Cover |' - ) - assert report.split("\n")[1] == ( - '|---------- | -------: | -------: | -------: | -------: | -------: |' - ) - assert report.split("\n")[2] == ( - '| **TOTAL** | **3** | **0** | **0** | **0** | **100%** |' - ) - squeezed = self.squeezed_lines(report) - assert squeezed[4] == "1 file skipped due to complete coverage." - - total = self.get_report(cov, output_format="total", skip_covered=True) - assert total == "100\n" - - def test_report_skip_covered_longfilename(self) -> None: - self.make_file("long_______________filename.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(source=["."], branch=True) - self.start_import_stop(cov, "long_______________filename") - assert self.stdout() == "" - report = self.get_report(cov, squeeze=False, skip_covered=True) - - # Name Stmts Miss Branch BrPart Cover - # ----------------------------------------- - # TOTAL 3 0 0 0 100% - # - # 1 file skipped due to complete coverage. - - assert self.line_count(report) == 5, report - lines = self.report_lines(report) - assert lines[0] == "Name Stmts Miss Branch BrPart Cover" - squeezed = self.squeezed_lines(report) - assert squeezed[4] == "1 file skipped due to complete coverage." - - def test_report_skip_covered_no_data(self) -> None: - cov = coverage.Coverage() - cov.load() - with pytest.raises(NoDataError, match="No data to report."): - self.get_report(cov, skip_covered=True) - self.assert_doesnt_exist(".coverage") - - def test_report_skip_empty(self) -> None: - self.make_file("main.py", """ - import submodule - - def normal(): - print("z") - normal() - """) - self.make_file("submodule/__init__.py", "") - cov = coverage.Coverage() - self.start_import_stop(cov, "main") - assert self.stdout() == "z\n" - report = self.get_report(cov, skip_empty=True) - - # Name Stmts Miss Cover - # ------------------------------------ - # main.py 4 0 100% - # ------------------------------------ - # TOTAL 4 0 100% - # - # 1 empty file skipped. - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "main.py 4 0 100%" - assert squeezed[4] == "TOTAL 4 0 100%" - assert squeezed[6] == "1 empty file skipped." - - def test_report_skip_empty_no_data(self) -> None: - self.make_file("__init__.py", "") - cov = coverage.Coverage() - self.start_import_stop(cov, "__init__") - assert self.stdout() == "" - report = self.get_report(cov, skip_empty=True) - - # Name Stmts Miss Cover - # ------------------------------------ - # TOTAL 0 0 100% - # - # 1 empty file skipped. - - assert self.line_count(report) == 5, report - assert report.split("\n")[2] == "TOTAL 0 0 100%" - assert report.split("\n")[4] == "1 empty file skipped." - - def test_report_precision(self) -> None: - self.make_file(".coveragerc", """\ - [report] - precision = 3 - omit = */site-packages/* - """) - self.make_file("main.py", """ - import not_covered, covered - - def normal(z): - if z: - print("z") - normal(True) - normal(False) - """) - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - not_covered(True) - """) - self.make_file("covered.py", """ - def foo(): - pass - foo() - """) - cov = coverage.Coverage(branch=True) - self.start_import_stop(cov, "main") - assert self.stdout() == "n\nz\n" - report = self.get_report(cov, squeeze=False) - - # Name Stmts Miss Branch BrPart Cover - # ------------------------------------------------------ - # covered.py 3 0 0 0 100.000% - # main.py 6 0 2 0 100.000% - # not_covered.py 4 0 2 1 83.333% - # ------------------------------------------------------ - # TOTAL 13 0 4 1 94.118% - - assert self.line_count(report) == 7, report - squeezed = self.squeezed_lines(report) - assert squeezed[2] == "covered.py 3 0 0 0 100.000%" - assert squeezed[4] == "not_covered.py 4 0 2 1 83.333%" - assert squeezed[6] == "TOTAL 13 0 4 1 94.118%" - - def test_report_precision_all_zero(self) -> None: - self.make_file("not_covered.py", """ - def not_covered(n): - if n: - print("n") - """) - self.make_file("empty.py", "") - cov = coverage.Coverage(source=["."]) - self.start_import_stop(cov, "empty") - report = self.get_report(cov, precision=6, squeeze=False) - - # Name Stmts Miss Cover - # ----------------------------------------- - # empty.py 0 0 100.000000% - # not_covered.py 3 3 0.000000% - # ----------------------------------------- - # TOTAL 3 3 0.000000% - - assert self.line_count(report) == 6, report - assert "empty.py 0 0 100.000000%" in report - assert "not_covered.py 3 3 0.000000%" in report - assert "TOTAL 3 3 0.000000%" in report - - def test_dotpy_not_python(self) -> None: - # We run a .py file, and when reporting, we can't parse it as Python. - # We should get an error message in the report. - - self.make_data_file(lines={"mycode.py": [1]}) - self.make_file("mycode.py", "This isn't python at all!") - cov = coverage.Coverage() - cov.load() - msg = r"Couldn't parse '.*[/\\]mycode.py' as Python source: '.*' at line 1" - with pytest.raises(NotPython, match=msg): - self.get_report(cov, morfs=["mycode.py"]) - - def test_accented_directory(self) -> None: - # Make a file with a non-ascii character in the directory name. - self.make_file("\xe2/accented.py", "print('accented')") - self.make_data_file(lines={abs_file("\xe2/accented.py"): [1]}) - report_expected = ( - "Name Stmts Miss Cover\n" + - "-----------------------------------\n" + - "\xe2/accented.py 1 0 100%\n" + - "-----------------------------------\n" + - "TOTAL 1 0 100%\n" - ) - cov = coverage.Coverage() - cov.load() - output = self.get_report(cov, squeeze=False) - assert output == report_expected - - def test_accenteddotpy_not_python(self) -> None: - # We run a .py file with a non-ascii name, and when reporting, we can't - # parse it as Python. We should get an error message in the report. - - self.make_data_file(lines={"accented\xe2.py": [1]}) - self.make_file("accented\xe2.py", "This isn't python at all!") - cov = coverage.Coverage() - cov.load() - msg = r"Couldn't parse '.*[/\\]accented\xe2.py' as Python source: '.*' at line 1" - with pytest.raises(NotPython, match=msg): - self.get_report(cov, morfs=["accented\xe2.py"]) - - def test_dotpy_not_python_ignored(self) -> None: - # We run a .py file, and when reporting, we can't parse it as Python, - # but we've said to ignore errors, so there's no error reported, - # though we still get a warning. - self.make_file("mycode.py", "This isn't python at all!") - self.make_data_file(lines={"mycode.py": [1]}) - cov = coverage.Coverage() - cov.load() - with pytest.raises(NoDataError, match="No data to report."): - with pytest.warns(Warning) as warns: - self.get_report(cov, morfs=["mycode.py"], ignore_errors=True) - assert_coverage_warnings( - warns, - re.compile(r"Couldn't parse Python file '.*[/\\]mycode.py' \(couldnt-parse\)"), - ) - - def test_dothtml_not_python(self) -> None: - # We run a .html file, and when reporting, we can't parse it as - # Python. Since it wasn't .py, no error is reported. - - # Pretend to run an html file. - self.make_file("mycode.html", "<h1>This isn't python at all!</h1>") - self.make_data_file(lines={"mycode.html": [1]}) - cov = coverage.Coverage() - cov.load() - with pytest.raises(NoDataError, match="No data to report."): - self.get_report(cov, morfs=["mycode.html"]) - - def test_report_no_extension(self) -> None: - self.make_file("xxx", """\ - # This is a python file though it doesn't look like it, like a main script. - a = b = c = d = 0 - a = 3 - b = 4 - if not b: - c = 6 - d = 7 - print(f"xxx: {a} {b} {c} {d}") - """) - self.make_data_file(lines={abs_file("xxx"): [2, 3, 4, 5, 7, 8]}) - cov = coverage.Coverage() - cov.load() - report = self.get_report(cov) - assert self.last_line_squeezed(report) == "TOTAL 7 1 86%" - - def test_report_with_chdir(self) -> None: - self.make_file("chdir.py", """\ - import os - print("Line One") - os.chdir("subdir") - print("Line Two") - print(open("something").read()) - """) - self.make_file("subdir/something", "hello") - out = self.run_command("coverage run --source=. chdir.py") - assert out == "Line One\nLine Two\nhello\n" - report = self.report_from_command("coverage report") - assert self.last_line_squeezed(report) == "TOTAL 5 0 100%" - report = self.report_from_command("coverage report --format=markdown") - assert self.last_line_squeezed(report) == "| **TOTAL** | **5** | **0** | **100%** |" - - def test_bug_156_file_not_run_should_be_zero(self) -> None: - # https://github.com/nedbat/coveragepy/issues/156 - self.make_file("mybranch.py", """\ - def branch(x): - if x: - print("x") - return x - branch(1) - """) - self.make_file("main.py", """\ - print("y") - """) - cov = coverage.Coverage(branch=True, source=["."]) - self.start_import_stop(cov, "main") - report = self.get_report(cov).splitlines() - assert "mybranch.py 5 5 2 0 0%" in report - - def run_TheCode_and_report_it(self) -> str: - """A helper for the next few tests.""" - cov = coverage.Coverage() - self.start_import_stop(cov, "TheCode") - return self.get_report(cov) - - def test_bug_203_mixed_case_listed_twice_with_rc(self) -> None: - self.make_file("TheCode.py", "a = 1\n") - self.make_file(".coveragerc", "[run]\nsource = .\n") - - report = self.run_TheCode_and_report_it() - assert "TheCode" in report - assert "thecode" not in report - - def test_bug_203_mixed_case_listed_twice(self) -> None: - self.make_file("TheCode.py", "a = 1\n") - - report = self.run_TheCode_and_report_it() - - assert "TheCode" in report - assert "thecode" not in report - - @pytest.mark.skipif(not env.WINDOWS, reason=".pyw files are only on Windows.") - def test_pyw_files(self) -> None: - # https://github.com/nedbat/coveragepy/issues/261 - self.make_file("start.pyw", """\ - import mod - print("In start.pyw") - """) - self.make_file("mod.pyw", """\ - print("In mod.pyw") - """) - cov = coverage.Coverage() - # start_import_stop can't import the .pyw file, so use the long form. - cov.start() - import start # pragma: nested # pylint: disable=import-error, unused-import - cov.stop() # pragma: nested - - report = self.get_report(cov) - assert "NoSource" not in report - report_lines = report.splitlines() - assert "start.pyw 2 0 100%" in report_lines - assert "mod.pyw 1 0 100%" in report_lines - - def test_tracing_pyc_file(self) -> None: - # Create two Python files. - self.make_file("mod.py", "a = 1\n") - self.make_file("main.py", "import mod\n") - - # Make one into a .pyc. - py_compile.compile("mod.py") - - # Run the program. - cov = coverage.Coverage() - self.start_import_stop(cov, "main") - - report_lines = self.get_report(cov).splitlines() - assert "mod.py 1 0 100%" in report_lines - report = self.get_report(cov, squeeze=False, output_format="markdown") - assert report.split("\n")[3] == "| mod.py | 1 | 0 | 100% |" - assert report.split("\n")[4] == "| **TOTAL** | **2** | **0** | **100%** |" - - def test_missing_py_file_during_run(self) -> None: - # Create two Python files. - self.make_file("mod.py", "a = 1\n") - self.make_file("main.py", "import mod\n") - - # Make one into a .pyc, and remove the .py. - py_compile.compile("mod.py") - os.remove("mod.py") - - # Python 3 puts the .pyc files in a __pycache__ directory, and will - # not import from there without source. It will import a .pyc from - # the source location though. - pycs = glob.glob("__pycache__/mod.*.pyc") - assert len(pycs) == 1 - os.rename(pycs[0], "mod.pyc") - - # Run the program. - cov = coverage.Coverage() - self.start_import_stop(cov, "main") - - # Put back the missing Python file. - self.make_file("mod.py", "a = 1\n") - report = self.get_report(cov).splitlines() - assert "mod.py 1 0 100%" in report - - def test_empty_files(self) -> None: - # Shows that empty files like __init__.py are listed as having zero - # statements, not one statement. - cov = coverage.Coverage(branch=True) - cov.start() - import usepkgs # pragma: nested # pylint: disable=import-error, unused-import - cov.stop() # pragma: nested - report = self.get_report(cov) - assert "tests/modules/pkg1/__init__.py 1 0 0 0 100%" in report - assert "tests/modules/pkg2/__init__.py 0 0 0 0 100%" in report - report = self.get_report(cov, squeeze=False, output_format="markdown") - # get_report() escapes backslash so we expect forward slash escaped - # underscore - assert "tests/modules/pkg1//_/_init/_/_.py " in report - assert "| 1 | 0 | 0 | 0 | 100% |" in report - assert "tests/modules/pkg2//_/_init/_/_.py " in report - assert "| 0 | 0 | 0 | 0 | 100% |" in report - - def test_markdown_with_missing(self) -> None: - self.make_file("mymissing.py", """\ - def missing(x, y): - if x: - print("x") - return x - if y: - print("y") - try: - print("z") - 1/0 - print("Never!") - except ZeroDivisionError: - pass - return x - missing(0, 1) - """) - cov = coverage.Coverage(source=["."]) - self.start_import_stop(cov, "mymissing") - assert self.stdout() == 'y\nz\n' - report = self.get_report(cov, squeeze=False, output_format="markdown", show_missing=True) - - # | Name | Stmts | Miss | Cover | Missing | - # |------------- | -------: | -------: | ------: | --------: | - # | mymissing.py | 14 | 3 | 79% | 3-4, 10 | - # | **TOTAL** | **14** | **3** | **79%** | | - assert self.line_count(report) == 4 - report_lines = report.split("\n") - assert report_lines[2] == "| mymissing.py | 14 | 3 | 79% | 3-4, 10 |" - assert report_lines[3] == "| **TOTAL** | **14** | **3** | **79%** | |" - - assert self.get_report(cov, output_format="total") == "79\n" - assert self.get_report(cov, output_format="total", precision=2) == "78.57\n" - assert self.get_report(cov, output_format="total", precision=4) == "78.5714\n" - - def test_bug_1524(self) -> None: - self.make_file("bug1524.py", """\ - class Mine: - @property - def thing(self) -> int: - return 17 - - print(Mine().thing) - """) - cov = coverage.Coverage() - self.start_import_stop(cov, "bug1524") - assert self.stdout() == "17\n" - report = self.get_report(cov) - report_lines = report.splitlines() - assert report_lines[2] == "bug1524.py 5 0 100%" - - -class ReportingReturnValueTest(CoverageTest): - """Tests of reporting functions returning values.""" - - def run_coverage(self) -> Coverage: - """Run coverage on doit.py and return the coverage object.""" - self.make_file("doit.py", """\ - a = 1 - b = 2 - c = 3 - d = 4 - if a > 10: - f = 6 - g = 7 - """) - - cov = coverage.Coverage() - self.start_import_stop(cov, "doit") - return cov - - def test_report(self) -> None: - cov = self.run_coverage() - val = cov.report(include="*/doit.py") - assert math.isclose(val, 6 / 7 * 100) - - def test_html(self) -> None: - cov = self.run_coverage() - val = cov.html_report(include="*/doit.py") - assert math.isclose(val, 6 / 7 * 100) - - def test_xml(self) -> None: - cov = self.run_coverage() - val = cov.xml_report(include="*/doit.py") - assert math.isclose(val, 6 / 7 * 100) - - -class SummaryReporterConfigurationTest(CoverageTest): - """Tests of SummaryReporter.""" - - def make_rigged_file(self, filename: str, stmts: int, miss: int) -> None: - """Create a file that will have specific results. - - `stmts` and `miss` are ints, the number of statements, and - missed statements that should result. - """ - run = stmts - miss - 1 - dont_run = miss - source = "" - source += "a = 1\n" * run - source += "if a == 99:\n" - source += " a = 2\n" * dont_run - self.make_file(filename, source) - - def get_summary_text(self, *options: Tuple[str, TConfigValueIn]) -> str: - """Get text output from the SummaryReporter. - - The arguments are tuples: (name, value) for Coverage.set_option. - """ - self.make_rigged_file("file1.py", 339, 155) - self.make_rigged_file("file2.py", 13, 3) - self.make_rigged_file("file10.py", 234, 228) - self.make_file("doit.py", "import file1, file2, file10") - - cov = Coverage(source=["."], omit=["doit.py"]) - self.start_import_stop(cov, "doit") - for name, value in options: - cov.set_option(name, value) - printer = SummaryReporter(cov) - destination = io.StringIO() - printer.report([], destination) - return destination.getvalue() - - def test_test_data(self) -> None: - # We use our own test files as test data. Check that our assumptions - # about them are still valid. We want the three columns of numbers to - # sort in three different orders. - report = self.get_summary_text() - # Name Stmts Miss Cover - # ------------------------------ - # file1.py 339 155 54% - # file2.py 13 3 77% - # file10.py 234 228 3% - # ------------------------------ - # TOTAL 586 386 34% - lines = report.splitlines()[2:-2] - assert len(lines) == 3 - nums = [list(map(int, l.replace('%', '').split()[1:])) for l in lines] - # [ - # [339, 155, 54], - # [ 13, 3, 77], - # [234, 228, 3] - # ] - assert nums[1][0] < nums[2][0] < nums[0][0] - assert nums[1][1] < nums[0][1] < nums[2][1] - assert nums[2][2] < nums[0][2] < nums[1][2] - - def test_defaults(self) -> None: - """Run the report with no configuration options.""" - report = self.get_summary_text() - assert 'Missing' not in report - assert 'Branch' not in report - - def test_print_missing(self) -> None: - """Run the report printing the missing lines.""" - report = self.get_summary_text(('report:show_missing', True)) - assert 'Missing' in report - assert 'Branch' not in report - - def assert_ordering(self, text: str, *words: str) -> None: - """Assert that the `words` appear in order in `text`.""" - indexes = list(map(text.find, words)) - assert -1 not in indexes - msg = f"The words {words!r} don't appear in order in {text!r}" - assert indexes == sorted(indexes), msg - - def test_default_sort_report(self) -> None: - # Sort the text report by the default (Name) column. - report = self.get_summary_text() - self.assert_ordering(report, "file1.py", "file2.py", "file10.py") - - def test_sort_report_by_name(self) -> None: - # Sort the text report explicitly by the Name column. - report = self.get_summary_text(('report:sort', 'Name')) - self.assert_ordering(report, "file1.py", "file2.py", "file10.py") - - def test_sort_report_by_stmts(self) -> None: - # Sort the text report by the Stmts column. - report = self.get_summary_text(('report:sort', 'Stmts')) - self.assert_ordering(report, "file2.py", "file10.py", "file1.py") - - def test_sort_report_by_missing(self) -> None: - # Sort the text report by the Missing column. - report = self.get_summary_text(('report:sort', 'Miss')) - self.assert_ordering(report, "file2.py", "file1.py", "file10.py") - - def test_sort_report_by_cover(self) -> None: - # Sort the text report by the Cover column. - report = self.get_summary_text(('report:sort', 'Cover')) - self.assert_ordering(report, "file10.py", "file1.py", "file2.py") - - def test_sort_report_by_cover_plus(self) -> None: - # Sort the text report by the Cover column, including the explicit + sign. - report = self.get_summary_text(('report:sort', '+Cover')) - self.assert_ordering(report, "file10.py", "file1.py", "file2.py") - - def test_sort_report_by_cover_reversed(self) -> None: - # Sort the text report by the Cover column reversed. - report = self.get_summary_text(('report:sort', '-Cover')) - self.assert_ordering(report, "file2.py", "file1.py", "file10.py") - - def test_sort_report_by_invalid_option(self) -> None: - # Sort the text report by a nonsense column. - msg = "Invalid sorting option: 'Xyzzy'" - with pytest.raises(ConfigError, match=msg): - self.get_summary_text(('report:sort', 'Xyzzy')) - - def test_report_with_invalid_format(self) -> None: - # Ask for an invalid format. - msg = "Unknown report format choice: 'xyzzy'" - with pytest.raises(ConfigError, match=msg): - self.get_summary_text(('report:format', 'xyzzy')) diff --git a/tests/test_venv.py b/tests/test_venv.py index ae5b303f7..a23561921 100644 --- a/tests/test_venv.py +++ b/tests/test_venv.py @@ -116,8 +116,12 @@ def sixth(x): __path__ = extend_path(__path__, __name__) """) make_file("bug888/app/testcov/main.py", """\ - import pkg_resources - for entry_point in pkg_resources.iter_entry_points('plugins'): + try: # pragma: no cover + entry_points = __import__("pkg_resources").iter_entry_points('plugins') + except ImportError: # pragma: no cover + import importlib.metadata + entry_points = importlib.metadata.entry_points(group="plugins") + for entry_point in entry_points: entry_point.load()() """) make_file("bug888/plugin/setup.py", """\ diff --git a/tests/test_xml.py b/tests/test_xml.py index 94b310e3e..731053207 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -15,7 +15,7 @@ import pytest import coverage -from coverage import Coverage +from coverage import Coverage, env from coverage.exceptions import NoDataError from coverage.files import abs_file from coverage.misc import import_local_file @@ -320,7 +320,7 @@ def test_accented_directory(self) -> None: def test_no_duplicate_packages(self) -> None: self.make_file( - "namespace/package/__init__.py", + "namespace/package/__init__.py", "from . import sample; from . import test; from .subpackage import test" ) self.make_file("namespace/package/sample.py", "print('package.sample')") @@ -476,15 +476,16 @@ def test_source_prefix(self) -> None: dom = ElementTree.parse("coverage.xml") self.assert_source(dom, "src") - def test_relative_source(self) -> None: + @pytest.mark.parametrize("trail", ["", "/", "\\"]) + def test_relative_source(self, trail: str) -> None: + if trail == "\\" and not env.WINDOWS: + pytest.skip("trailing backslash is only for Windows") self.make_file("src/mod.py", "print(17)") - cov = coverage.Coverage(source=["src"]) + cov = coverage.Coverage(source=[f"src{trail}"]) cov.set_option("run:relative_files", True) self.start_import_stop(cov, "mod", modfile="src/mod.py") cov.xml_report() - with open("coverage.xml") as x: - print(x.read()) dom = ElementTree.parse("coverage.xml") elts = dom.findall(".//sources/source") assert [elt.text for elt in elts] == ["src"] diff --git a/tox.ini b/tox.ini index 0a1fa6f60..80b31897e 100644 --- a/tox.ini +++ b/tox.ini @@ -10,6 +10,7 @@ toxworkdir = {env:TOXWORKDIR:.tox} [testenv] usedevelop = True +download = True extras = toml @@ -29,6 +30,7 @@ setenv = # For some tests, we need .pyc files written in the current directory, # so override any local setting. PYTHONPYCACHEPREFIX= + PYTHONWARNINGS=ignore:removed in Python 3.14; use ast.Constant:DeprecationWarning # $set_env.py: COVERAGE_PIP_ARGS - Extra arguments for `pip install` # `--no-build-isolation` will let tox work with no network. @@ -70,6 +72,8 @@ commands = - sphinx-build -b html -b linkcheck -aEnQW doc doc/_build/html [testenv:lint] +# Minimum of PYVERSIONS +basepython = python3.7 deps = -r requirements/lint.pip