diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml index a0da0a1ea..97b4d88d3 100644 --- a/.github/workflows/cancel.yml +++ b/.github/workflows/cancel.yml @@ -4,7 +4,7 @@ # This action finds in-progress Action jobs for the same branch, and cancels # them. There's little point in continuing to run superceded jobs. -name: Cancel +name: "Cancel" on: push: @@ -13,7 +13,7 @@ jobs: cancel: runs-on: ubuntu-latest steps: - - name: Cancel Previous Runs + - name: "Cancel Previous Runs" uses: styfle/cancel-workflow-action@0.6.0 with: access_token: ${{ github.token }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index ad5a21cf8..ee798ada1 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -4,9 +4,11 @@ name: "Coverage" on: + # As currently structured, this adds too many jobs (checks?), so don't run it + # on pull requests yet. push: - branches: ["master"] - pull_request: + branches: + - master workflow_dispatch: defaults: @@ -15,32 +17,53 @@ defaults: jobs: coverage: - name: "Python ${{ matrix.python-version }}" - runs-on: ubuntu-latest + name: "Python ${{ matrix.python-version }} on ${{ matrix.os }}" + runs-on: "${{ matrix.os }}" strategy: matrix: + os: + - ubuntu-latest + - macos-latest + - windows-latest python-version: + # When changing this list, be sure to check the [gh-actions] list in + # tox.ini so that tox will run properly. - "2.7" - "3.5" - "3.9" + - "3.10.0-alpha.5" - "pypy3" - fail-fast: false + exclude: + # Windows PyPy doesn't seem to work? + - os: windows-latest + python-version: "pypy3" + # If one job fails, stop the whole thing. + fail-fast: true steps: - name: "Check out the repo" uses: "actions/checkout@v2" + with: + fetch-depth: "0" - name: "Set up Python" uses: "actions/setup-python@v2" with: python-version: "${{ matrix.python-version }}" + - name: "Install Visual C++ if needed" + if: runner.os == 'Windows' && matrix.python-version == '2.7' + run: | + choco install vcpython27 -f -y + - name: "Install dependencies" run: | set -xe python -VV python -m site + # Need to install setuptools first so that ci.pip will succeed. + python -m pip install -c requirements/pins.pip setuptools wheel python -m pip install -r requirements/ci.pip python -m pip install -c requirements/pins.pip tox-gh-actions @@ -50,14 +73,22 @@ jobs: run: | set -xe python -m tox - python -m igor combine_html - mv .metacov .coverage.${{ matrix.python-version }} + + - name: "Combine" + env: + COVERAGE_COVERAGE: "yes" + COVERAGE_RCFILE: "metacov.ini" + COVERAGE_METAFILE: ".metacov" + run: | + set -xe + COVERAGE_DEBUG=dataio python -m igor combine_html + mv .metacov .metacov.${{ matrix.python-version }}.${{ matrix.os }} - name: "Upload coverage data" uses: actions/upload-artifact@v2 with: name: metacov - path: .coverage.* + path: .metacov.* combine: name: "Combine coverage data" @@ -67,6 +98,8 @@ jobs: steps: - name: "Check out the repo" uses: "actions/checkout@v2" + with: + fetch-depth: "0" - name: "Set up Python" uses: "actions/setup-python@v2" @@ -78,7 +111,6 @@ jobs: set -xe python -VV python -m site - python -m pip install -r requirements/ci.pip python setup.py --quiet clean develop python igor.py zip_mods install_egg @@ -88,12 +120,77 @@ jobs: name: metacov - name: "Combine and report" + id: combine + env: + COVERAGE_RCFILE: "metacov.ini" + COVERAGE_METAFILE: ".metacov" run: | set -xe - coverage combine - coverage xml + python -m igor combine_html + python -m coverage json + echo "::set-output name=total::$(python -c "import json;print(format(json.load(open('coverage.json'))['totals']['percent_covered'],'.2f'))")" - name: "Upload to codecov" uses: codecov/codecov-action@v1 with: file: coverage.xml + + - name: "Upload HTML report" + uses: actions/upload-artifact@v2 + with: + name: html_report + path: htmlcov + + - name: "Upload JSON report" + uses: actions/upload-artifact@v2 + with: + name: json_report + path: coverage.json + + - name: "Create info for pushing to report repo" + id: info + run: | + export SHA10=$(echo ${{ github.sha }} | cut -c 1-10) + export SLUG=$(date +'%Y%m%d')_$SHA10 + export REF="${{ github.ref }}" + echo "::set-output name=sha10::$SHA10" + echo "::set-output name=slug::$SLUG" + echo "::set-output name=url::https://nedbat.github.io/coverage-reports/reports/$SLUG/htmlcov" + echo "::set-output name=branch::${REF#refs/heads/}" + + - name: "Push to report repository" + uses: sebastian-palma/github-action-push-to-another-repository@allow-creating-destination-directory + env: + API_TOKEN_GITHUB: ${{ secrets.COVERAGE_REPORTS_TOKEN }} + with: + source-directory: 'htmlcov' + destination-github-username: 'nedbat' + destination-repository-name: 'coverage-reports' + destination-repository-directory: 'reports/${{ steps.info.outputs.slug }}' + empty-repository: false + create-destination-directory: true + target-branch: main + commit-message: >- + ${{ steps.combine.outputs.total }}% - ${{ github.event.head_commit.message }} + + + ${{ steps.info.outputs.url }} + + ${{ steps.info.outputs.sha10 }}: ${{ steps.info.outputs.branch }} + user-email: ned@nedbatchelder.com + + - name: "Create redirection HTML file" + run: | + echo "" > coverage-report-redirect.html + echo "" >> coverage-report-redirect.html + echo "Coverage report redirect..." >> coverage-report-redirect.html + + - name: "Upload HTML redirect" + uses: actions/upload-artifact@v2 + with: + name: coverage-report-redirect.html + path: coverage-report-redirect.html + + - name: "Show link to report" + run: | + echo "Coverage report: ${{ steps.info.outputs.url }}" diff --git a/.github/workflows/kit.yml b/.github/workflows/kit.yml index 437e7d31b..854b4f299 100644 --- a/.github/workflows/kit.yml +++ b/.github/workflows/kit.yml @@ -4,7 +4,7 @@ # Based on: # https://github.com/joerick/cibuildwheel/blob/master/examples/github-deploy.yml -name: Build kits +name: "Kits" on: workflow_dispatch: @@ -15,88 +15,91 @@ defaults: jobs: build_wheels: - name: Build wheels on ${{ matrix.os }} + name: "Build wheels on ${{ matrix.os }}" runs-on: ${{ matrix.os }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] + os: + - ubuntu-latest + - windows-latest + - macos-latest fail-fast: false steps: - - name: Check out the repo + - name: "Check out the repo" uses: actions/checkout@v2 - - name: Install Python 3.7 + - name: "Install Python 3.7" uses: actions/setup-python@v2 with: python-version: "3.7" - - name: Install cibuildwheel + - name: "Install cibuildwheel" run: | python -m pip install -c requirements/pins.pip cibuildwheel - - name: Install Visual C++ for Python 2.7 + - name: "Install Visual C++ for Python 2.7" if: runner.os == 'Windows' run: | choco install vcpython27 -f -y - - name: Build wheels + - name: "Build wheels" env: # Don't build wheels for PyPy. CIBW_SKIP: pp* run: | python -m cibuildwheel --output-dir wheelhouse - - name: Upload wheels + - name: "Upload wheels" uses: actions/upload-artifact@v2 with: name: dist path: ./wheelhouse/*.whl build_sdist: - name: Build source distribution + name: "Build source distribution" runs-on: ubuntu-latest steps: - - name: Check out the repo + - name: "Check out the repo" uses: actions/checkout@v2 - - name: Install Python 3.7 + - name: "Install Python 3.7" uses: actions/setup-python@v2 with: python-version: "3.7" - - name: Build sdist + - name: "Build sdist" run: | python setup.py sdist - - name: Upload sdist + - name: "Upload sdist" uses: actions/upload-artifact@v2 with: name: dist path: dist/*.tar.gz build_pypy: - name: Build PyPy wheels + name: "Build PyPy wheels" runs-on: ubuntu-latest steps: - - name: Check out the repo + - name: "Check out the repo" uses: actions/checkout@v2 - - name: Install PyPy + - name: "Install PyPy" uses: actions/setup-python@v2 with: python-version: "pypy3" - - name: Install requirements + - name: "Install requirements" run: | pypy3 -m pip install -r requirements/wheel.pip - - name: Build wheels + - name: "Build wheels" run: | pypy3 setup.py bdist_wheel --python-tag pp36 pypy3 setup.py bdist_wheel --python-tag pp37 - - name: Upload wheels + - name: "Upload wheels" uses: actions/upload-artifact@v2 with: name: dist diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index fbd3d8328..1a1b7f03f 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -1,11 +1,12 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -name: "Quality checks" +name: "Quality" on: push: - branches: ["master"] + branches: + - master pull_request: workflow_dispatch: @@ -15,7 +16,7 @@ defaults: jobs: lint: - name: Pylint etc + name: "Pylint etc" # Because pylint can report different things on different OS's (!) # (https://github.com/PyCQA/pylint/issues/3489), run this on Mac where local # pylint gets run. @@ -42,7 +43,7 @@ jobs: python -m tox -e lint doc: - name: Build docs + name: "Build docs" runs-on: ubuntu-latest steps: diff --git a/.github/workflows/testsuite.yml b/.github/workflows/testsuite.yml index 59f5380b2..a88bfba4c 100644 --- a/.github/workflows/testsuite.yml +++ b/.github/workflows/testsuite.yml @@ -1,11 +1,12 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -name: "Test Suite" +name: "Tests" on: push: - branches: ["master"] + branches: + - master pull_request: workflow_dispatch: @@ -20,14 +21,20 @@ jobs: strategy: matrix: - os: [ubuntu-latest, macos-latest, windows-latest] + os: + - ubuntu-latest + - macos-latest + - windows-latest python-version: + # When changing this list, be sure to check the [gh-actions] list in + # tox.ini so that tox will run properly. - "2.7" - "3.5" - "3.6" - "3.7" - "3.8" - "3.9" + - "3.10.0-alpha.5" - "pypy3" exclude: # Windows PyPy doesn't seem to work? @@ -54,6 +61,8 @@ jobs: set -xe python -VV python -m site + # Need to install setuptools first so that ci.pip will succeed. + python -m pip install -c requirements/pins.pip setuptools wheel python -m pip install -r requirements/ci.pip python -m pip install -c requirements/pins.pip tox-gh-actions diff --git a/CHANGES.rst b/CHANGES.rst index 98f632842..afd5f16ae 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -21,6 +21,42 @@ want to know what's different in 5.0 since 4.5.x, see :ref:`whatsnew5x`. .. Version 9.8.1 --- 2027-07-27 .. ---------------------------- +.. _changes_55: + +Version 5.5 --- 2021-02-28 +-------------------------- + +- ``coverage combine`` has a new option, ``--keep`` to keep the original data + files after combining them. The default is still to delete the files after + they have been combined. This was requested in `issue 1108`_ and implemented + in `pull request 1110`_. Thanks, Éric Larivière. + +- When reporting missing branches in ``coverage report``, branches aren't + reported that jump to missing lines. This adds to the long-standing behavior + of not reporting branches from missing lines. Now branches are only reported + if both the source and destination lines are executed. Closes both `issue + 1065`_ and `issue 955`_. + +- Minor improvements to the HTML report: + + - The state of the line visibility selector buttons is saved in local storage + so you don't have to fiddle with them so often, fixing `issue 1123`_. + + - It has a little more room for line numbers so that 4-digit numbers work + well, fixing `issue 1124`_. + +- Improved the error message when combining line and branch data, so that users + will be more likely to understand what's happening, closing `issue 803`_. + +.. _issue 803: https://github.com/nedbat/coveragepy/issues/803 +.. _issue 955: https://github.com/nedbat/coveragepy/issues/955 +.. _issue 1065: https://github.com/nedbat/coveragepy/issues/1065 +.. _issue 1108: https://github.com/nedbat/coveragepy/issues/1108 +.. _pull request 1110: https://github.com/nedbat/coveragepy/pull/1110 +.. _issue 1123: https://github.com/nedbat/coveragepy/issues/1123 +.. _issue 1124: https://github.com/nedbat/coveragepy/issues/1124 + + .. _changes_54: Version 5.4 --- 2021-01-24 @@ -37,8 +73,8 @@ Version 5.4 --- 2021-01-24 ``[report]`` settings if there isn't a value in the ``[html]`` section. Closes `issue 1090`_. -- Combining files on Windows across drives how works properly, fixing `issue - 577`_. Thanks, `Valentine Lab `_. +- Combining files on Windows across drives now works properly, fixing `issue + 577`_. Thanks, `Valentin Lab `_. - Fix an obscure warning from deep in the _decimal module, as reported in `issue 1084`_. diff --git a/CONTRIBUTORS.txt b/CONTRIBUTORS.txt index 455c40967..76fbd4c31 100644 --- a/CONTRIBUTORS.txt +++ b/CONTRIBUTORS.txt @@ -19,6 +19,7 @@ Anthony Sottile Arcadiy Ivanov Aron Griffis Artem Dayneko +Arthur Deygin Ben Finney Bernát Gábor Bill Hart @@ -54,9 +55,10 @@ Dirk Thomas Dmitry Shishov Dmitry Trofimov Eduardo Schettino +Edward Loper Eli Skeggs Emil Madsen -Edward Loper +Éric Larivière Federico Bond Frazer McLean Geoff Bache @@ -136,7 +138,7 @@ Ted Wexler Thijs Triemstra Thomas Grainger Titus Brown -Valentine Lab +Valentin Lab Vince Salvino Ville Skyttä Xie Yanbo diff --git a/Makefile b/Makefile index ff5d3c999..d7bc15b7d 100644 --- a/Makefile +++ b/Makefile @@ -96,9 +96,12 @@ kit_local: # don't go crazy trying to figure out why our new code isn't installing. find ~/Library/Caches/pip/wheels -name 'coverage-*' -delete -download_kits: ## Download the built kits from GitHub +download_kits: ## Download the built kits from GitHub. python ci/download_gha_artifacts.py +check_kits: ## Check that dist/* are well-formed. + python -m twine check dist/* + build_ext: python setup.py build_ext @@ -118,6 +121,8 @@ $(DOCBIN): cmd_help: $(DOCBIN) @for cmd in annotate combine debug erase html json report run xml; do \ echo > doc/help/$$cmd.rst; \ + echo ".. This file is auto-generated by \"make dochtml\", don't edit it manually." >> doc/help/$$cmd.rst; \ + echo >> doc/help/$$cmd.rst; \ echo ".. code::" >> doc/help/$$cmd.rst; \ echo >> doc/help/$$cmd.rst; \ echo " $$ coverage $$cmd --help" >> doc/help/$$cmd.rst; \ diff --git a/README.rst b/README.rst index 66cd938a8..072f30ffe 100644 --- a/README.rst +++ b/README.rst @@ -8,7 +8,7 @@ Coverage.py Code coverage testing for Python. | |license| |versions| |status| -| |ci-status| |docs| |codecov| +| |test-status| |quality-status| |docs| |codecov| | |kit| |format| |repos| |downloads| | |stars| |forks| |contributors| | |tidelift| |twitter-coveragepy| |twitter-nedbat| @@ -95,9 +95,12 @@ Licensed under the `Apache 2.0 License`_. For details, see `NOTICE.txt`_. .. _NOTICE.txt: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt -.. |ci-status| image:: https://github.com/nedbat/coveragepy/workflows/Test%20Suite/badge.svg - :target: https://github.com/nedbat/coveragepy/actions?query=workflow%3A%22Test+Suite%22 - :alt: Build status +.. |test-status| image:: https://github.com/nedbat/coveragepy/actions/workflows/testsuite.yml/badge.svg?branch=master&event=push + :target: https://github.com/nedbat/coveragepy/actions/workflows/testsuite.yml + :alt: Test suite status +.. |quality-status| image:: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml/badge.svg?branch=master&event=push + :target: https://github.com/nedbat/coveragepy/actions/workflows/quality.yml + :alt: Quality check status .. |docs| image:: https://readthedocs.org/projects/coverage/badge/?version=latest&style=flat :target: https://coverage.readthedocs.io/ :alt: Documentation diff --git a/coverage/backunittest.py b/coverage/backunittest.py deleted file mode 100644 index 123bb2a13..000000000 --- a/coverage/backunittest.py +++ /dev/null @@ -1,33 +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 - -"""Implementations of unittest features from the future.""" - -import unittest - - -def unittest_has(method): - """Does `unittest.TestCase` have `method` defined?""" - return hasattr(unittest.TestCase, method) - - -class TestCase(unittest.TestCase): - """Just like unittest.TestCase, but with assert methods added. - - Designed to be compatible with 3.1 unittest. Methods are only defined if - `unittest` doesn't have them. - - """ - # pylint: disable=signature-differs, deprecated-method - - if not unittest_has('assertCountEqual'): - def assertCountEqual(self, *args, **kwargs): - return self.assertItemsEqual(*args, **kwargs) - - if not unittest_has('assertRaisesRegex'): - def assertRaisesRegex(self, *args, **kwargs): - return self.assertRaisesRegexp(*args, **kwargs) - - if not unittest_has('assertRegex'): - def assertRegex(self, *args, **kwargs): - return self.assertRegexpMatches(*args, **kwargs) diff --git a/coverage/backward.py b/coverage/backward.py index 8af3452b2..ac781ab96 100644 --- a/coverage/backward.py +++ b/coverage/backward.py @@ -78,7 +78,9 @@ try: import reprlib -except ImportError: +except ImportError: # pragma: not covered + # We need this on Python 2, but in testing environments, a backport is + # installed, so this import isn't used. import repr as reprlib # A function to iterate listlessly over a dict's items, and one to get the @@ -215,9 +217,6 @@ def __repr__(self): items = ("{}={!r}".format(k, self.__dict__[k]) for k in keys) return "{}({})".format(type(self).__name__, ", ".join(items)) - def __eq__(self, other): - return self.__dict__ == other.__dict__ - def format_local_datetime(dt): """Return a string with local timezone representing the date. diff --git a/coverage/cmdline.py b/coverage/cmdline.py index 9c9ae868a..0be0cca19 100644 --- a/coverage/cmdline.py +++ b/coverage/cmdline.py @@ -31,6 +31,10 @@ class Opts(object): '-a', '--append', action='store_true', help="Append coverage data to .coverage, otherwise it starts clean each time.", ) + keep = optparse.make_option( + '', '--keep', action='store_true', + help="Keep original coverage files, otherwise they are deleted.", + ) branch = optparse.make_option( '', '--branch', action='store_true', help="Measure branch coverage in addition to statement coverage.", @@ -215,6 +219,7 @@ def __init__(self, *args, **kwargs): help=None, ignore_errors=None, include=None, + keep=None, module=None, omit=None, contexts=None, @@ -333,6 +338,7 @@ def get_prog_name(self): "combine", [ Opts.append, + Opts.keep, ] + GLOBAL_ARGS, usage="[options] ... ", description=( @@ -585,7 +591,7 @@ def command_line(self, argv): if options.append: self.coverage.load() data_dirs = args or None - self.coverage.combine(data_dirs, strict=True) + self.coverage.combine(data_dirs, strict=True, keep=bool(options.keep)) self.coverage.save() return OK @@ -765,7 +771,7 @@ def do_debug(self, args): self.coverage.load() data = self.coverage.get_data() print(info_header("data")) - print("path: %s" % self.coverage.get_data().data_filename()) + print("path: %s" % data.data_filename()) if data: print("has_arcs: %r" % data.has_arcs()) summary = line_counts(data, fullpath=True) diff --git a/coverage/config.py b/coverage/config.py index 803dcd5d7..7ef7e7ae7 100644 --- a/coverage/config.py +++ b/coverage/config.py @@ -63,7 +63,7 @@ def options(self, section): real_section = section_prefix + section if configparser.RawConfigParser.has_section(self, real_section): return configparser.RawConfigParser.options(self, real_section) - raise configparser.NoSectionError + raise configparser.NoSectionError(section) def get_section(self, section): """Get the contents of a section, as a dictionary.""" @@ -87,7 +87,7 @@ def get(self, section, option, *args, **kwargs): if configparser.RawConfigParser.has_option(self, real_section, option): break else: - raise configparser.NoOptionError + raise configparser.NoOptionError(option, section) v = configparser.RawConfigParser.get(self, real_section, option, *args, **kwargs) v = substitute_variables(v, os.environ) @@ -477,6 +477,20 @@ def get_option(self, option_name): # If we get here, we didn't find the option. raise CoverageException("No such option: %r" % option_name) + def post_process_file(self, path): + """Make final adjustments to a file path to make it usable.""" + return os.path.expanduser(path) + + def post_process(self): + """Make final adjustments to settings to make them usable.""" + self.data_file = self.post_process_file(self.data_file) + self.html_dir = self.post_process_file(self.html_dir) + self.xml_output = self.post_process_file(self.xml_output) + self.paths = collections.OrderedDict( + (k, [self.post_process_file(f) for f in v]) + for k, v in self.paths.items() + ) + def config_files_to_try(config_file): """What config files should we try to read? @@ -551,12 +565,6 @@ def read_coverage_config(config_file, **kwargs): # Once all the config has been collected, there's a little post-processing # to do. - config.data_file = os.path.expanduser(config.data_file) - config.html_dir = os.path.expanduser(config.html_dir) - config.xml_output = os.path.expanduser(config.xml_output) - config.paths = collections.OrderedDict( - (k, [os.path.expanduser(f) for f in v]) - for k, v in config.paths.items() - ) + config.post_process() return config diff --git a/coverage/control.py b/coverage/control.py index 8d129bcb5..1623b0932 100644 --- a/coverage/control.py +++ b/coverage/control.py @@ -659,7 +659,7 @@ def save(self): data = self.get_data() data.write() - def combine(self, data_paths=None, strict=False): + def combine(self, data_paths=None, strict=False, keep=False): """Combine together a number of similarly-named coverage data files. All coverage data files whose name starts with `data_file` (from the @@ -674,12 +674,16 @@ def combine(self, data_paths=None, strict=False): If `strict` is true, then it is an error to attempt to combine when there are no data files to combine. + If `keep` is true, then original input data files won't be deleted. + .. versionadded:: 4.0 The `data_paths` parameter. .. versionadded:: 4.3 The `strict` parameter. + .. versionadded: 5.5 + The `keep` parameter. """ self._init() self._init_data(suffix=None) @@ -694,7 +698,13 @@ def combine(self, data_paths=None, strict=False): for pattern in paths[1:]: aliases.add(pattern, result) - combine_parallel_data(self._data, aliases=aliases, data_paths=data_paths, strict=strict) + combine_parallel_data( + self._data, + aliases=aliases, + data_paths=data_paths, + strict=strict, + keep=keep, + ) def get_data(self): """Get the collected data. diff --git a/coverage/data.py b/coverage/data.py index 82bf1d41c..5dd1dfe3f 100644 --- a/coverage/data.py +++ b/coverage/data.py @@ -52,7 +52,7 @@ def add_data_to_hash(data, filename, hasher): hasher.update(data.file_tracer(filename)) -def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): +def combine_parallel_data(data, aliases=None, data_paths=None, strict=False, keep=False): """Combine a number of data files together. Treat `data.filename` as a file prefix, and combine the data from all @@ -68,7 +68,7 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): If `data_paths` is not provided, then the directory portion of `data.filename` is used as the directory to search for data files. - Every data file found and combined is then deleted from disk. If a file + Unless `keep` is True every data file found and combined is then deleted from disk. If a file cannot be read, a warning will be issued, and the file will not be deleted. @@ -116,9 +116,10 @@ def combine_parallel_data(data, aliases=None, data_paths=None, strict=False): else: data.update(new_data, aliases=aliases) files_combined += 1 - if data._debug.should('dataio'): - data._debug.write("Deleting combined data file %r" % (f,)) - file_be_gone(f) + if not keep: + if data._debug.should('dataio'): + data._debug.write("Deleting combined data file %r" % (f,)) + file_be_gone(f) if strict and not files_combined: raise CoverageException("No usable data files") diff --git a/coverage/htmlfiles/coverage_html.js b/coverage/htmlfiles/coverage_html.js index 6bc9fdf59..27b49b36f 100644 --- a/coverage/htmlfiles/coverage_html.js +++ b/coverage/htmlfiles/coverage_html.js @@ -233,6 +233,8 @@ coverage.index_ready = function ($) { // -- pyfile stuff -- +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + coverage.pyfile_ready = function ($) { // If we're directed to a particular line number, highlight the line. var frag = location.hash; @@ -256,6 +258,22 @@ coverage.pyfile_ready = function ($) { $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); + } + coverage.assign_shortkeys(); coverage.wire_up_help_panel(); @@ -266,17 +284,26 @@ coverage.pyfile_ready = function ($) { }; coverage.toggle_lines = function (btn, cls) { - btn = $(btn); - var show = "show_"+cls; - if (btn.hasClass(show)) { - $("#source ." + cls).removeClass(show); - btn.removeClass(show); - } - else { + var onoff = !$(btn).hasClass("show_" + cls); + coverage.set_line_visibilty(cls, onoff); + coverage.build_scroll_markers(); + coverage.filters[cls] = onoff; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (cls, onoff) { + var show = "show_" + cls; + var btn = $(".button_toggle_" + cls); + if (onoff) { $("#source ." + cls).addClass(show); btn.addClass(show); } - coverage.build_scroll_markers(); + else { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); + } }; // Return the nth line div. diff --git a/coverage/htmlfiles/style.css b/coverage/htmlfiles/style.css index 3e7f9b66b..36ee2a6e6 100644 --- a/coverage/htmlfiles/style.css +++ b/coverage/htmlfiles/style.css @@ -34,7 +34,7 @@ a.nav:hover { text-decoration: underline; color: inherit; } @media (prefers-color-scheme: dark) { #header { border-color: #333; } } -.indexfile #footer { margin: 1rem 3rem; } +.indexfile #footer { margin: 1rem 3.5rem; } .pyfile #footer { margin: 1rem 1rem; } @@ -42,9 +42,9 @@ a.nav:hover { text-decoration: underline; color: inherit; } @media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } -#index { margin: 1rem 0 0 3rem; } +#index { margin: 1rem 0 0 3.5rem; } -#header .content { padding: 1rem 3rem; } +#header .content { padding: 1rem 3.5rem; } h1 { font-size: 1.25em; display: inline-block; } @@ -122,13 +122,13 @@ h2.stats { margin-top: .5em; font-size: 1em; } .keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } -#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } #source p { position: relative; white-space: pre; } #source p * { box-sizing: border-box; } -#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } @media (prefers-color-scheme: dark) { #source p .n { color: #777; } } diff --git a/coverage/htmlfiles/style.scss b/coverage/htmlfiles/style.scss index 8169269e3..158d1fb49 100644 --- a/coverage/htmlfiles/style.scss +++ b/coverage/htmlfiles/style.scss @@ -16,7 +16,7 @@ /* Don't edit this .css file. Edit the .scss file instead! */ // Dimensions -$left-gutter: 3rem; +$left-gutter: 3.5rem; // @@ -166,7 +166,7 @@ a.nav { } .indexfile #footer { - margin: 1rem 3rem; + margin: 1rem $left-gutter; } .pyfile #footer { @@ -181,7 +181,7 @@ a.nav { } #index { - margin: 1rem 0 0 3rem; + margin: 1rem 0 0 $left-gutter; } // Header styles diff --git a/coverage/misc.py b/coverage/misc.py index 96573f7a4..034e288eb 100644 --- a/coverage/misc.py +++ b/coverage/misc.py @@ -63,7 +63,7 @@ def _decorator(func): def new_contract(*args, **kwargs): """A proxy for contracts.new_contract that doesn't mind happening twice.""" try: - return raw_new_contract(*args, **kwargs) + raw_new_contract(*args, **kwargs) except ValueError: # During meta-coverage, this module is imported twice, and # PyContracts doesn't like redefining contracts. It's OK. diff --git a/coverage/multiproc.py b/coverage/multiproc.py index 0afcb0c9b..8b6651bc5 100644 --- a/coverage/multiproc.py +++ b/coverage/multiproc.py @@ -28,7 +28,7 @@ class ProcessWithCoverage(OriginalProcess): # pylint: disable=abstract-method """A replacement for multiprocess.Process that starts coverage.""" - def _bootstrap(self, *args, **kwargs): # pylint: disable=signature-differs + def _bootstrap(self, *args, **kwargs): """Wrapper around _bootstrap to start coverage.""" try: from coverage import Coverage # avoid circular import diff --git a/coverage/pytracer.py b/coverage/pytracer.py index 44bfc8d6a..7ab4d3ef9 100644 --- a/coverage/pytracer.py +++ b/coverage/pytracer.py @@ -14,6 +14,11 @@ if env.PY2: YIELD_VALUE = chr(YIELD_VALUE) +# When running meta-coverage, this file can try to trace itself, which confuses +# everything. Don't trace ourselves. + +THIS_FILE = __file__.rstrip("co") + class PyTracer(object): """Python implementation of the raw data tracer.""" @@ -72,25 +77,47 @@ def __repr__(self): def log(self, marker, *args): """For hard-core logging of what this tracer is doing.""" with open("/tmp/debug_trace.txt", "a") as f: - f.write("{} {:x}.{:x}[{}] {:x} {}\n".format( + f.write("{} {}[{}]".format( marker, id(self), - self.thread.ident, len(self.data_stack), - self.threading.currentThread().ident, - " ".join(map(str, args)) )) + if 0: + f.write(".{:x}.{:x}".format( + self.thread.ident, + self.threading.currentThread().ident, + )) + f.write(" {}".format(" ".join(map(str, args)))) + if 0: + f.write(" | ") + stack = " / ".join( + (fname or "???").rpartition("/")[-1] + for _, fname, _, _ in self.data_stack + ) + f.write(stack) + f.write("\n") def _trace(self, frame, event, arg_unused): """The trace function passed to sys.settrace.""" - #self.log(":", frame.f_code.co_filename, frame.f_lineno, event) + if THIS_FILE in frame.f_code.co_filename: + return None + + #self.log(":", frame.f_code.co_filename, frame.f_lineno, frame.f_code.co_name + "()", event) if (self.stopped and sys.gettrace() == self._trace): # pylint: disable=comparison-with-callable # The PyTrace.stop() method has been called, possibly by another # thread, let's deactivate ourselves now. - #self.log("X", frame.f_code.co_filename, frame.f_lineno) + if 0: + self.log("---\nX", frame.f_code.co_filename, frame.f_lineno) + f = frame + while f: + self.log(">", f.f_code.co_filename, f.f_lineno, f.f_code.co_name, f.f_trace) + f = f.f_back sys.settrace(None) + self.cur_file_dict, self.cur_file_name, self.last_line, self.started_context = ( + self.data_stack.pop() + ) return None if self.last_exc_back: @@ -104,6 +131,9 @@ def _trace(self, frame, event, arg_unused): ) self.last_exc_back = None + # if event != 'call' and frame.f_code.co_filename != self.cur_file_name: + # self.log("---\n*", frame.f_code.co_filename, self.cur_file_name, frame.f_lineno) + if event == 'call': # Should we start a new context? if self.should_start_context and self.context is None: @@ -153,8 +183,7 @@ def _trace(self, frame, event, arg_unused): # Record an executed line. if self.cur_file_dict is not None: lineno = frame.f_lineno - #if frame.f_code.co_filename != self.cur_file_name: - # self.log("*", frame.f_code.co_filename, self.cur_file_name, lineno) + if self.trace_arcs: self.cur_file_dict[(self.last_line, lineno)] = None else: diff --git a/coverage/results.py b/coverage/results.py index ae8366bf5..4916864df 100644 --- a/coverage/results.py +++ b/coverage/results.py @@ -146,10 +146,7 @@ def branch_stats(self): stats = {} for lnum in self._branch_lines(): exits = self.exit_counts[lnum] - try: - missing = len(missing_arcs[lnum]) - except KeyError: - missing = 0 + missing = len(missing_arcs[lnum]) stats[lnum] = (exits, exits - missing) return stats @@ -265,7 +262,7 @@ def __radd__(self, other): # Implementing 0+Numbers allows us to sum() a list of Numbers. if other == 0: return self - return NotImplemented + return NotImplemented # pragma: not covered (we never call it this way) def _line_ranges(statements, lines): @@ -315,7 +312,7 @@ def format_lines(statements, lines, arcs=None): line_exits = sorted(arcs) for line, exits in line_exits: for ex in sorted(exits): - if line not in lines: + if line not in lines and ex not in lines: dest = (ex if ex > 0 else "exit") line_items.append((line, "%d->%s" % (line, dest))) diff --git a/coverage/sqldata.py b/coverage/sqldata.py index b28b83b4f..a150fdfd0 100644 --- a/coverage/sqldata.py +++ b/coverage/sqldata.py @@ -486,9 +486,9 @@ def _choose_lines_or_arcs(self, lines=False, arcs=False): assert lines or arcs assert not (lines and arcs) if lines and self._has_arcs: - raise CoverageException("Can't add lines to existing arc data") + raise CoverageException("Can't add line measurements to existing branch data") if arcs and self._has_lines: - raise CoverageException("Can't add arcs to existing line data") + raise CoverageException("Can't add branch measurements to existing line data") if not self._has_arcs and not self._has_lines: self._has_lines = lines self._has_arcs = arcs diff --git a/coverage/summary.py b/coverage/summary.py index 0c7fa5519..65f804700 100644 --- a/coverage/summary.py +++ b/coverage/summary.py @@ -8,7 +8,7 @@ from coverage import env from coverage.report import get_analysis_to_report from coverage.results import Numbers -from coverage.misc import NotPython, CoverageException, output_encoding +from coverage.misc import CoverageException, output_encoding class SummaryReporter(object): @@ -78,29 +78,18 @@ def report(self, morfs, outfile=None): lines = [] for (fr, analysis) in self.fr_analysis: - try: - 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),) - text = fmt_coverage % args - # Add numeric percent coverage so that sorting makes sense. - args += (nums.pc_covered,) - lines.append((text, args)) - except Exception: - report_it = not self.config.ignore_errors - if report_it: - typ, msg = sys.exc_info()[:2] - # NotPython is only raised by PythonFileReporter, which has a - # should_be_python() method. - if typ is NotPython and not fr.should_be_python(): - report_it = False - if report_it: - self.writeout(self.fmt_err % (fr.relative_filename(), typ.__name__, msg)) + 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),) + text = fmt_coverage % args + # Add numeric percent coverage so that sorting makes sense. + args += (nums.pc_covered,) + lines.append((text, args)) # Sort the lines and write them out. if getattr(self.config, 'sort', None): diff --git a/coverage/version.py b/coverage/version.py index 8cc58dfb1..d141a11da 100644 --- a/coverage/version.py +++ b/coverage/version.py @@ -5,7 +5,7 @@ # This file is exec'ed in setup.py, don't import anything! # Same semantics as sys.version_info. -version_info = (5, 4, 0, "final", 0) +version_info = (5, 5, 0, "final", 0) def _make_version(major, minor, micro, releaselevel, serial): diff --git a/doc/cmd.rst b/doc/cmd.rst index f6087fecf..2b2086b16 100644 --- a/doc/cmd.rst +++ b/doc/cmd.rst @@ -287,6 +287,9 @@ setting to store relative file paths (see :ref:`relative_files If any of the data files can't be read, coverage.py will print a warning indicating the file and the problem. +The original input data files are deleted once they've been combined. If you +want to keep those files, use the ``--keep`` command-line option. + .. include:: help/combine.rst diff --git a/doc/conf.py b/doc/conf.py index b76c0a235..7ea5a8767 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,7 +39,8 @@ 'sphinx.ext.ifconfig', 'sphinxcontrib.spelling', 'sphinx.ext.intersphinx', - 'sphinx_rst_builder', + #'sphinx_rst_builder', + 'sphinxcontrib.restbuilder', 'sphinx.ext.extlinks', 'sphinx.ext.napoleon', 'sphinx_tabs.tabs', @@ -66,11 +67,11 @@ # built documents. # # The short X.Y version. -version = "5.4" # CHANGEME +version = "5.5" # CHANGEME # The full version, including alpha/beta/rc tags. -release = "5.4" # CHANGEME +release = "5.5" # CHANGEME # The date of release, in "monthname day, year" format. -release_date = "January 24, 2021" # CHANGEME +release_date = "February 28, 2021" # CHANGEME rst_epilog = """ .. |release_date| replace:: {release_date} diff --git a/doc/config.rst b/doc/config.rst index 3a8b0784d..34da8a066 100644 --- a/doc/config.rst +++ b/doc/config.rst @@ -319,6 +319,7 @@ missing lines. See :ref:`cmd_report` for more information. ``sort`` (string, default "Name"): Sort the text report by the named column. Allowed values are "Name", "Stmts", "Miss", "Branch", "BrPart", or "Cover". +Prefix with ``-`` for descending sort (for example, "-cover"). .. _config_html: @@ -353,12 +354,16 @@ details. include files in the report that are 100% covered files. See :ref:`cmd_report` for more information. +.. versionadded:: 5.4 + .. _config_html_skip_empty: ``skip_empty`` (boolean, defaulted from ``[report] skip_empty``): Don't include empty files (those that have 0 statements) in the report. See :ref:`cmd_report` for more information. +.. versionadded:: 5.4 + .. _config_html_title: ``title`` (string, default "Coverage report"): the title to use for the report. diff --git a/doc/help/annotate.rst b/doc/help/annotate.rst index 8f0883a04..c8d597193 100644 --- a/doc/help/annotate.rst +++ b/doc/help/annotate.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage annotate --help diff --git a/doc/help/combine.rst b/doc/help/combine.rst index 35180cdde..8a365958f 100644 --- a/doc/help/combine.rst +++ b/doc/help/combine.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage combine --help @@ -13,6 +15,7 @@ Options: -a, --append Append coverage data to .coverage, otherwise it starts clean each time. + --keep Keep original coverage files, otherwise they are deleted. --debug=OPTS Debug options, separated by commas. [env: COVERAGE_DEBUG] -h, --help Get help on this command. --rcfile=RCFILE Specify configuration file. By default '.coveragerc', diff --git a/doc/help/debug.rst b/doc/help/debug.rst index db1e64b26..b6361da56 100644 --- a/doc/help/debug.rst +++ b/doc/help/debug.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage debug --help diff --git a/doc/help/erase.rst b/doc/help/erase.rst index c8f45155a..372dd4fb6 100644 --- a/doc/help/erase.rst +++ b/doc/help/erase.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage erase --help diff --git a/doc/help/html.rst b/doc/help/html.rst index 8dfa285aa..7dbf91c84 100644 --- a/doc/help/html.rst +++ b/doc/help/html.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage html --help diff --git a/doc/help/json.rst b/doc/help/json.rst index cec488e55..a330167e2 100644 --- a/doc/help/json.rst +++ b/doc/help/json.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage json --help diff --git a/doc/help/report.rst b/doc/help/report.rst index 3408f2bb4..b8985e4bb 100644 --- a/doc/help/report.rst +++ b/doc/help/report.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage report --help diff --git a/doc/help/run.rst b/doc/help/run.rst index a336929a5..f71a09561 100644 --- a/doc/help/run.rst +++ b/doc/help/run.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage run --help diff --git a/doc/help/xml.rst b/doc/help/xml.rst index eb52750d4..2ad134c91 100644 --- a/doc/help/xml.rst +++ b/doc/help/xml.rst @@ -1,4 +1,6 @@ +.. This file is auto-generated by "make dochtml", don't edit it manually. + .. code:: $ coverage xml --help diff --git a/doc/index.rst b/doc/index.rst index 6f408b897..63ac1d9c3 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -23,7 +23,7 @@ supported on: .. ifconfig:: prerelease **This is a pre-release build. The usual warnings about possible bugs - apply.** The latest stable version is coverage.py 5.4, `described here`_. + apply.** The latest stable version is coverage.py 5.5, `described here`_. .. _described here: http://coverage.readthedocs.io/ diff --git a/doc/python-coverage.1.txt b/doc/python-coverage.1.txt index 0bbd44d0a..00c243de6 100644 --- a/doc/python-coverage.1.txt +++ b/doc/python-coverage.1.txt @@ -108,7 +108,8 @@ COMMAND REFERENCE Combine data from multiple coverage files collected with ``run -p``. The combined results are written to a single file representing the - union of the data. + union of the data. Unless --keep is provided the original input + coverage files are deleted. If `PATH` is specified, they are files or directories containing data to be combined. @@ -119,6 +120,9 @@ COMMAND REFERENCE Append coverage data to .coverage, otherwise it starts clean each time. + \--keep + Keep original coverage data files. + **debug** `TOPIC` ... Display information about the internals of coverage.py, for diagnosing diff --git a/doc/requirements.pip b/doc/requirements.pip index eea4c8f99..f1f01c66d 100644 --- a/doc/requirements.pip +++ b/doc/requirements.pip @@ -4,9 +4,9 @@ doc8==0.8.1 pyenchant==3.2.0 -sphinx==3.3.1 -sphinx-rst-builder==0.0.3 +sphinx==3.4.3 +sphinxcontrib-restbuilder==0.3 sphinxcontrib-spelling==7.1.0 -sphinx_rtd_theme==0.5.0 +sphinx_rtd_theme==0.5.1 sphinx-autobuild==2020.9.1 -sphinx-tabs==1.3.0 +sphinx-tabs==2.0.0 diff --git a/doc/sample_html/cogapp___init___py.html b/doc/sample_html/cogapp___init___py.html index be126eb48..7159ffdc7 100644 --- a/doc/sample_html/cogapp___init___py.html +++ b/doc/sample_html/cogapp___init___py.html @@ -66,8 +66,8 @@

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

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

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

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

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

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

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

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

diff --git a/doc/sample_html/coverage_html.js b/doc/sample_html/coverage_html.js index 6bc9fdf59..27b49b36f 100644 --- a/doc/sample_html/coverage_html.js +++ b/doc/sample_html/coverage_html.js @@ -233,6 +233,8 @@ coverage.index_ready = function ($) { // -- pyfile stuff -- +coverage.LINE_FILTERS_STORAGE = "COVERAGE_LINE_FILTERS"; + coverage.pyfile_ready = function ($) { // If we're directed to a particular line number, highlight the line. var frag = location.hash; @@ -256,6 +258,22 @@ coverage.pyfile_ready = function ($) { $(".button_toggle_mis").click(function (evt) {coverage.toggle_lines(evt.target, "mis");}); $(".button_toggle_par").click(function (evt) {coverage.toggle_lines(evt.target, "par");}); + coverage.filters = undefined; + try { + coverage.filters = localStorage.getItem(coverage.LINE_FILTERS_STORAGE); + } catch(err) {} + + if (coverage.filters) { + coverage.filters = JSON.parse(coverage.filters); + } + else { + coverage.filters = {run: false, exc: true, mis: true, par: true}; + } + + for (cls in coverage.filters) { + coverage.set_line_visibilty(cls, coverage.filters[cls]); + } + coverage.assign_shortkeys(); coverage.wire_up_help_panel(); @@ -266,17 +284,26 @@ coverage.pyfile_ready = function ($) { }; coverage.toggle_lines = function (btn, cls) { - btn = $(btn); - var show = "show_"+cls; - if (btn.hasClass(show)) { - $("#source ." + cls).removeClass(show); - btn.removeClass(show); - } - else { + var onoff = !$(btn).hasClass("show_" + cls); + coverage.set_line_visibilty(cls, onoff); + coverage.build_scroll_markers(); + coverage.filters[cls] = onoff; + try { + localStorage.setItem(coverage.LINE_FILTERS_STORAGE, JSON.stringify(coverage.filters)); + } catch(err) {} +}; + +coverage.set_line_visibilty = function (cls, onoff) { + var show = "show_" + cls; + var btn = $(".button_toggle_" + cls); + if (onoff) { $("#source ." + cls).addClass(show); btn.addClass(show); } - coverage.build_scroll_markers(); + else { + $("#source ." + cls).removeClass(show); + btn.removeClass(show); + } }; // Return the nth line div. diff --git a/doc/sample_html/index.html b/doc/sample_html/index.html index 6ce866387..ac2efcf0b 100644 --- a/doc/sample_html/index.html +++ b/doc/sample_html/index.html @@ -156,8 +156,8 @@

Coverage report: diff --git a/doc/sample_html/status.json b/doc/sample_html/status.json index 83336a370..daee1db4e 100644 --- a/doc/sample_html/status.json +++ b/doc/sample_html/status.json @@ -1 +1 @@ -{"format":2,"version":"5.4","globals":"a486ca194f909abc39de9a093fa5a484","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file +{"format":2,"version":"5.5","globals":"a486ca194f909abc39de9a093fa5a484","files":{"cogapp___init___py":{"hash":"6010eef3af87123028eb691d70094593","index":{"nums":[1,2,0,0,0,0,0],"html_filename":"cogapp___init___py.html","relative_filename":"cogapp/__init__.py"}},"cogapp___main___py":{"hash":"2cec3551dfd9a5818a6550318658ccd4","index":{"nums":[1,3,0,3,0,0,0],"html_filename":"cogapp___main___py.html","relative_filename":"cogapp/__main__.py"}},"cogapp_backward_py":{"hash":"f95e44a818c73b2187e6fadc6257f8ce","index":{"nums":[1,22,0,6,4,2,2],"html_filename":"cogapp_backward_py.html","relative_filename":"cogapp/backward.py"}},"cogapp_cogapp_py":{"hash":"f85acbdbacefaccb9c499ef6cbe2ffc4","index":{"nums":[1,485,1,215,200,28,132],"html_filename":"cogapp_cogapp_py.html","relative_filename":"cogapp/cogapp.py"}},"cogapp_makefiles_py":{"hash":"4fd2add44238312a5567022fe28737de","index":{"nums":[1,27,0,20,14,0,14],"html_filename":"cogapp_makefiles_py.html","relative_filename":"cogapp/makefiles.py"}},"cogapp_test_cogapp_py":{"hash":"ee9b3c832eaa47b9e3940133c58827af","index":{"nums":[1,790,6,549,20,0,18],"html_filename":"cogapp_test_cogapp_py.html","relative_filename":"cogapp/test_cogapp.py"}},"cogapp_test_makefiles_py":{"hash":"66093f767a400ce1720b94a7371de48b","index":{"nums":[1,71,0,53,6,0,6],"html_filename":"cogapp_test_makefiles_py.html","relative_filename":"cogapp/test_makefiles.py"}},"cogapp_test_whiteutils_py":{"hash":"068beefb2872fe6739fad2471c36a4f1","index":{"nums":[1,69,0,50,0,0,0],"html_filename":"cogapp_test_whiteutils_py.html","relative_filename":"cogapp/test_whiteutils.py"}},"cogapp_whiteutils_py":{"hash":"b16b0e7f940175106b11230fea9e8c8c","index":{"nums":[1,45,0,5,34,4,4],"html_filename":"cogapp_whiteutils_py.html","relative_filename":"cogapp/whiteutils.py"}}}} \ No newline at end of file diff --git a/doc/sample_html/style.css b/doc/sample_html/style.css index 3e7f9b66b..36ee2a6e6 100644 --- a/doc/sample_html/style.css +++ b/doc/sample_html/style.css @@ -34,7 +34,7 @@ a.nav:hover { text-decoration: underline; color: inherit; } @media (prefers-color-scheme: dark) { #header { border-color: #333; } } -.indexfile #footer { margin: 1rem 3rem; } +.indexfile #footer { margin: 1rem 3.5rem; } .pyfile #footer { margin: 1rem 1rem; } @@ -42,9 +42,9 @@ a.nav:hover { text-decoration: underline; color: inherit; } @media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } -#index { margin: 1rem 0 0 3rem; } +#index { margin: 1rem 0 0 3.5rem; } -#header .content { padding: 1rem 3rem; } +#header .content { padding: 1rem 3.5rem; } h1 { font-size: 1.25em; display: inline-block; } @@ -122,13 +122,13 @@ h2.stats { margin-top: .5em; font-size: 1em; } .keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } -#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } #source p { position: relative; white-space: pre; } #source p * { box-sizing: border-box; } -#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } @media (prefers-color-scheme: dark) { #source p .n { color: #777; } } diff --git a/howto.txt b/howto.txt index 8a912833d..aae6c47d1 100644 --- a/howto.txt +++ b/howto.txt @@ -45,12 +45,10 @@ $ make publish - Kits: - Manually trigger the kit GitHub Action - - https://github.com/nedbat/coveragepy/actions?query=workflow%3A%22Build+kits%22 - - Download built kits from GitHub Actions: - $ make clean download_kits + - https://github.com/nedbat/coveragepy/actions/workflows/kit.yml + - Download and check built kits from GitHub Actions: + $ make clean download_kits check_kits - examine the dist directory, and remove anything that looks malformed. - - check the dist directory: - $ python -m twine check dist/* - test the pypi upload: $ make test_upload - Update PyPI: diff --git a/igor.py b/igor.py index b2dc05cfe..3c6afa667 100644 --- a/igor.py +++ b/igor.py @@ -20,7 +20,11 @@ import warnings import zipfile -import pytest +try: + import pytest +except ImportError: + # We want to be able to run this for some tasks that don't need pytest. + pytest = None # Contants derived the same as in coverage/env.py. We can't import # that file here, it would be evaluated too early and not get the @@ -157,7 +161,7 @@ def run_tests_with_coverage(tracer, *runner_args): try: # Re-import coverage to get it coverage tested! I don't understand all # the mechanics here, but if I don't carry over the imported modules - # (in covmods), then things go haywire (os == None, eventually). + # (in covmods), then things go haywire (os is None, eventually). covmods = {} covdir = os.path.split(coverage.__file__)[0] # We have to make a list since we'll be deleting in the loop. @@ -383,7 +387,7 @@ def analyze_args(function): getargspec = inspect.getargspec with ignore_warnings(): # DeprecationWarning: Use inspect.signature() instead of inspect.getfullargspec() - argspec = getargspec(function) + argspec = getargspec(function) # pylint: disable=deprecated-method return bool(argspec[1]), len(argspec[0]) diff --git a/metacov.ini b/metacov.ini index daabbf82f..47ed31344 100644 --- a/metacov.ini +++ b/metacov.ini @@ -49,6 +49,8 @@ exclude_lines = # Lines that we can't run during metacov. pragma: no metacov + pytest.mark.skipif\(env.METACOV + if not env.METACOV: # These lines only happen if tests fail. raise AssertionError @@ -76,8 +78,7 @@ partial_branches = if .* env.JYTHON if .* env.IRONPYTHON -ignore_errors = true -precision = 1 +precision = 2 [paths] source = @@ -86,3 +87,7 @@ source = */coverage/trunk *\coveragepy /io + # GitHub Actions on Ubuntu uses /home/runner/work/coveragepy + # GitHub Actions on Mac uses /Users/runner/work/coveragepy + # GitHub Actions on Window uses D:\a\coveragepy\coveragepy + */coveragepy diff --git a/pylintrc b/pylintrc index d250e9b92..c55b89822 100644 --- a/pylintrc +++ b/pylintrc @@ -66,6 +66,7 @@ disable= global-statement, broad-except, no-else-return, + misplaced-comparison-constant, # Messages that may be silly: no-self-use, no-member, @@ -83,6 +84,8 @@ disable= bad-continuation, # Disable while we still support Python 2: useless-object-inheritance, + super-with-arguments, + raise-missing-from, # Messages that are noisy for now, eventually maybe we'll turn them on: invalid-name, protected-access, diff --git a/requirements/ci.pip b/requirements/ci.pip index 060d1de3f..72c6a7907 100644 --- a/requirements/ci.pip +++ b/requirements/ci.pip @@ -1,6 +1,8 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +-c pins.pip + # Things CI servers need for running tests. -r tox.pip -r pytest.pip diff --git a/requirements/dev.pip b/requirements/dev.pip index 2cd0fe0e3..791a2faed 100644 --- a/requirements/dev.pip +++ b/requirements/dev.pip @@ -14,11 +14,15 @@ pluggy==0.13.1 # for linting. greenlet==0.4.16 -pylint==2.5.3 -check-manifest==0.42 +astroid==2.5 +pylint==2.7.1 +check-manifest==0.46 readme_renderer==26.0 # for kitting. -requests==2.24.0 -twine==3.2.0 -libsass==0.20.0 +requests==2.25.1 +twine==3.3.0 +libsass==0.20.1 + +# Just so I have a debugger if I want it +pudb==2019.2 diff --git a/requirements/pins.pip b/requirements/pins.pip index 223e7cbdf..04721c8bb 100644 --- a/requirements/pins.pip +++ b/requirements/pins.pip @@ -5,3 +5,7 @@ cibuildwheel==1.7.0 tox-gh-actions==2.2.0 + +# setuptools 45.x is py3-only +setuptools==44.1.1 +wheel==0.35.1 diff --git a/requirements/pytest.pip b/requirements/pytest.pip index 43d4efe51..ecdf619c0 100644 --- a/requirements/pytest.pip +++ b/requirements/pytest.pip @@ -19,6 +19,3 @@ hypothesis==4.57.1 # Our testing mixins unittest-mixins==1.6 #-e/Users/ned/unittest_mixins - -# Just so I have a debugger if I want it -pudb==2019.2 diff --git a/requirements/wheel.pip b/requirements/wheel.pip index ae84163ab..f294ab3bf 100644 --- a/requirements/wheel.pip +++ b/requirements/wheel.pip @@ -1,8 +1,9 @@ # Licensed under the Apache License: http://www.apache.org/licenses/LICENSE-2.0 # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt +-c pins.pip + # Things needed to make wheels for coverage.py -# setuptools 45.x is py3-only -setuptools==44.1.1 -wheel==0.35.1 +setuptools +wheel diff --git a/tests/conftest.py b/tests/conftest.py index 10761cddf..81ec9f775 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,6 +16,11 @@ from coverage import env +# Pytest will rewrite assertions in test modules, but not elsewhere. +# This tells pytest to also rewrite assertions in coveragetest.py. +pytest.register_assert_rewrite("tests.coveragetest") +pytest.register_assert_rewrite("tests.helpers") + # Pytest can take additional options: # $set_env.py: PYTEST_ADDOPTS - Extra arguments to pytest. @@ -77,7 +82,7 @@ def fix_xdist_sys_path(): See: https://github.com/pytest-dev/pytest-xdist/issues/376 """ - if os.environ.get('PYTEST_XDIST_WORKER', ''): + if os.environ.get('PYTEST_XDIST_WORKER', ''): # pragma: part covered # We are running in an xdist worker. if sys.path[1] == '': # xdist has set sys.path[1] to ''. Clobber it. diff --git a/tests/coveragetest.py b/tests/coveragetest.py index dbadd226b..3363fa894 100644 --- a/tests/coveragetest.py +++ b/tests/coveragetest.py @@ -5,7 +5,7 @@ import contextlib import datetime -import functools +import difflib import glob import os import os.path @@ -13,23 +13,19 @@ import re import shlex import sys -import types +import unittest import pytest -from unittest_mixins import ( - EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, - DelayedAssertionMixin, -) +from unittest_mixins import EnvironmentAwareMixin, TempDirMixin import coverage from coverage import env -from coverage.backunittest import TestCase, unittest from coverage.backward import StringIO, import_local_file, string_class, shlex_quote from coverage.cmdline import CoverageScript -from coverage.misc import StopEverything -from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs +from tests.helpers import arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal from tests.helpers import run_command, SuperModuleCleaner +from tests.mixins import StdStreamCapturingMixin, StopEverythingMixin # Status returns for the command line. @@ -39,37 +35,12 @@ TESTS_DIR = os.path.dirname(__file__) -def convert_skip_exceptions(method): - """A decorator for test methods to convert StopEverything to SkipTest.""" - @functools.wraps(method) - def _wrapper(*args, **kwargs): - try: - result = method(*args, **kwargs) - except StopEverything: - raise unittest.SkipTest("StopEverything!") - return result - return _wrapper - - -class SkipConvertingMetaclass(type): - """Decorate all test methods to convert StopEverything to SkipTest.""" - def __new__(cls, name, bases, attrs): - for attr_name, attr_value in attrs.items(): - if attr_name.startswith('test_') and isinstance(attr_value, types.FunctionType): - attrs[attr_name] = convert_skip_exceptions(attr_value) - - return super(SkipConvertingMetaclass, cls).__new__(cls, name, bases, attrs) - - -CoverageTestMethodsMixin = SkipConvertingMetaclass('CoverageTestMethodsMixin', (), {}) - class CoverageTest( EnvironmentAwareMixin, StdStreamCapturingMixin, TempDirMixin, - DelayedAssertionMixin, - CoverageTestMethodsMixin, - TestCase, + StopEverythingMixin, + unittest.TestCase, ): """A base class for coverage.py test cases.""" @@ -84,7 +55,7 @@ class CoverageTest( # Temp dirs go to $TMPDIR/coverage_test/* temp_dir_prefix = "coverage_test/" - if os.getenv('COVERAGE_ENV_ID'): + if os.getenv('COVERAGE_ENV_ID'): # pragma: debugging temp_dir_prefix += "{}/".format(os.getenv('COVERAGE_ENV_ID')) # Keep the temp directories if the env says to. @@ -134,17 +105,27 @@ def get_module_name(self): self.last_module_name = 'coverage_test_' + str(random.random())[2:] return self.last_module_name - def assert_equal_arcs(self, a1, a2, msg=None): - """Assert that the arc lists `a1` and `a2` are equal.""" + def _check_arcs(self, a1, a2, arc_type): + """Check that the arc lists `a1` and `a2` are equal. + + If they are equal, return empty string. If they are unequal, return + a string explaining what is different. + """ # Make them into multi-line strings so we can see what's going wrong. s1 = arcs_to_arcz_repr(a1) s2 = arcs_to_arcz_repr(a2) - self.assertMultiLineEqual(s1, s2, msg) + if s1 != s2: + lines1 = s1.splitlines(True) + lines2 = s2.splitlines(True) + diff = "".join(difflib.ndiff(lines1, lines2)) + return "\n" + arc_type + " arcs differ: minus is expected, plus is actual\n" + diff + else: + return "" def check_coverage( self, text, lines=None, missing="", report="", excludes=None, partials="", - arcz=None, arcz_missing="", arcz_unpredicted="", + arcz=None, arcz_missing=None, arcz_unpredicted=None, arcs=None, arcs_missing=None, arcs_unpredicted=None, ): """Check the coverage measurement of `text`. @@ -173,9 +154,9 @@ def check_coverage( if arcs is None and arcz is not None: arcs = arcz_to_arcs(arcz) - if arcs_missing is None: + if arcs_missing is None and arcz_missing is not None: arcs_missing = arcz_to_arcs(arcz_missing) - if arcs_unpredicted is None: + if arcs_unpredicted is None and arcz_unpredicted is not None: arcs_unpredicted = arcz_to_arcs(arcz_unpredicted) # Start up coverage.py. @@ -198,7 +179,7 @@ def check_coverage( if isinstance(lines[0], int): # lines is just a list of numbers, it must match the statements # found in the code. - self.assertEqual(statements, lines) + assert statements == lines, "{!r} != {!r}".format(statements, lines) else: # lines is a list of possible line number lists, one of them # must match. @@ -210,7 +191,8 @@ def check_coverage( missing_formatted = analysis.missing_formatted() if isinstance(missing, string_class): - self.assertEqual(missing_formatted, missing) + msg = "{!r} != {!r}".format(missing_formatted, missing) + assert missing_formatted == missing, msg else: for missing_list in missing: if missing_formatted == missing_list: @@ -224,27 +206,20 @@ def check_coverage( # print(" actual:", analysis.arc_possibilities()) # print("Executed:") # print(" actual:", sorted(set(analysis.arcs_executed()))) - with self.delayed_assertions(): - self.assert_equal_arcs( - arcs, analysis.arc_possibilities(), - "Possible arcs differ: minus is expected, plus is actual" - ) - - self.assert_equal_arcs( - arcs_missing, analysis.arcs_missing(), - "Missing arcs differ: minus is expected, plus is actual" - ) - - self.assert_equal_arcs( - arcs_unpredicted, analysis.arcs_unpredicted(), - "Unpredicted arcs differ: minus is expected, plus is actual" - ) + # TODO: this would be nicer with pytest-check, once we can run that. + msg = ( + self._check_arcs(arcs, analysis.arc_possibilities(), "Possible") + + self._check_arcs(arcs_missing, analysis.arcs_missing(), "Missing") + + self._check_arcs(arcs_unpredicted, analysis.arcs_unpredicted(), "Unpredicted") + ) + if msg: + assert False, msg if report: frep = StringIO() cov.report(mod, file=frep, show_missing=True) rep = " ".join(frep.getvalue().split("\n")[2].split()[1:]) - self.assertEqual(report, rep) + assert report == rep, "{!r} != {!r}".format(report, rep) return cov @@ -308,24 +283,24 @@ def assert_same_files(self, flist1, flist2): """Assert that `flist1` and `flist2` are the same set of file names.""" flist1_nice = [self.nice_file(f) for f in flist1] flist2_nice = [self.nice_file(f) for f in flist2] - self.assertCountEqual(flist1_nice, flist2_nice) + assert_count_equal(flist1_nice, flist2_nice) def assert_exists(self, fname): """Assert that `fname` is a file that exists.""" msg = "File %r should exist" % fname - self.assertTrue(os.path.exists(fname), msg) + assert os.path.exists(fname), msg def assert_doesnt_exist(self, fname): """Assert that `fname` is a file that doesn't exist.""" msg = "File %r shouldn't exist" % fname - self.assertTrue(not os.path.exists(fname), msg) + assert not os.path.exists(fname), msg def assert_file_count(self, pattern, count): """Assert that there are `count` files matching `pattern`.""" files = sorted(glob.glob(pattern)) msg = "There should be {} files matching {!r}, but there are these: {}" msg = msg.format(count, pattern, files) - self.assertEqual(len(files), count, msg) + assert len(files) == count, msg def assert_starts_with(self, s, prefix, msg=None): """Assert that `s` starts with `prefix`.""" @@ -335,8 +310,8 @@ def assert_starts_with(self, s, prefix, msg=None): def assert_recent_datetime(self, dt, seconds=10, msg=None): """Assert that `dt` marks a time at most `seconds` seconds ago.""" age = datetime.datetime.now() - dt - self.assertGreaterEqual(age.total_seconds(), 0, msg) - self.assertLessEqual(age.total_seconds(), seconds, msg) + assert age.total_seconds() >= 0, msg + assert age.total_seconds() <= seconds, msg def command_line(self, args, ret=OK): """Run `args` through the command line. @@ -351,7 +326,7 @@ def command_line(self, args, ret=OK): """ ret_actual = command_line(args) - self.assertEqual(ret_actual, ret) + assert ret_actual == ret, "{!r} != {!r}".format(ret_actual, ret) # Some distros rename the coverage command, and need a way to indicate # their new command name to the tests. This is here for them to override, @@ -414,7 +389,7 @@ def run_command_status(self, cmd): if env.JYTHON: # pragma: only jython # Jython can't do reporting, so let's skip the test now. if command_args and command_args[0] in ('report', 'html', 'xml', 'annotate'): - self.skipTest("Can't run reporting commands in Jython") + pytest.skip("Can't run reporting commands in Jython") # Jython can't run "coverage" as a command because the shebang # refers to another shebang'd Python script. So run them as # modules. @@ -454,13 +429,13 @@ def working_root(self): def report_from_command(self, cmd): """Return the report from the `cmd`, with some convenience added.""" report = self.run_command(cmd).replace('\\', '/') - self.assertNotIn("error", report.lower()) + assert "error" not in report.lower() return report def report_lines(self, report): """Return the lines of the report, as a list.""" lines = report.split('\n') - self.assertEqual(lines[-1], "") + assert lines[-1] == "" return lines[:-1] def line_count(self, report): diff --git a/tests/gold/html/styled/style.css b/tests/gold/html/styled/style.css index 3e7f9b66b..36ee2a6e6 100644 --- a/tests/gold/html/styled/style.css +++ b/tests/gold/html/styled/style.css @@ -34,7 +34,7 @@ a.nav:hover { text-decoration: underline; color: inherit; } @media (prefers-color-scheme: dark) { #header { border-color: #333; } } -.indexfile #footer { margin: 1rem 3rem; } +.indexfile #footer { margin: 1rem 3.5rem; } .pyfile #footer { margin: 1rem 1rem; } @@ -42,9 +42,9 @@ a.nav:hover { text-decoration: underline; color: inherit; } @media (prefers-color-scheme: dark) { #footer .content { color: #aaa; } } -#index { margin: 1rem 0 0 3rem; } +#index { margin: 1rem 0 0 3.5rem; } -#header .content { padding: 1rem 3rem; } +#header .content { padding: 1rem 3.5rem; } h1 { font-size: 1.25em; display: inline-block; } @@ -122,13 +122,13 @@ h2.stats { margin-top: .5em; font-size: 1em; } .keyhelp .key { border: 1px solid black; border-color: #888 #333 #333 #888; padding: .1em .35em; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; font-weight: bold; background: #eee; } -#source { padding: 1em 0 1em 3rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } +#source { padding: 1em 0 1em 3.5rem; font-family: SFMono-Regular, Menlo, Monaco, Consolas, monospace; } #source p { position: relative; white-space: pre; } #source p * { box-sizing: border-box; } -#source p .n { float: left; text-align: right; width: 3rem; box-sizing: border-box; margin-left: -3rem; padding-right: 1em; color: #999; } +#source p .n { float: left; text-align: right; width: 3.5rem; box-sizing: border-box; margin-left: -3.5rem; padding-right: 1em; color: #999; } @media (prefers-color-scheme: dark) { #source p .n { color: #777; } } diff --git a/tests/helpers.py b/tests/helpers.py index 0621d7a94..a96b793ef 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -3,6 +3,7 @@ """Helpers for coverage.py tests.""" +import collections import glob import os import re @@ -197,7 +198,7 @@ def arcs_to_arcz_repr(arcs): """ repr_list = [] - for a, b in arcs: + for a, b in (arcs or ()): line = repr((a, b)) line += " # " line += _arcs_to_arcz_repr_one(a) @@ -222,3 +223,13 @@ def without_module(using_module, missing_module_name): """ return mock.patch.object(using_module, missing_module_name, None) + + +def assert_count_equal(a, b): + """ + A pytest-friendly implementation of assertCountEqual. + + Assert that `a` and `b` have the same elements, but maybe in different order. + This only works for hashable elements. + """ + assert collections.Counter(list(a)) == collections.Counter(list(b)) diff --git a/tests/mixins.py b/tests/mixins.py new file mode 100644 index 000000000..9d096d4d5 --- /dev/null +++ b/tests/mixins.py @@ -0,0 +1,70 @@ +# 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 class mixins + +Some of these are transitional while working toward pure-pytest style. +""" + +import functools +import types +import unittest + +import pytest + +from coverage.misc import StopEverything + + +def convert_skip_exceptions(method): + """A decorator for test methods to convert StopEverything to SkipTest.""" + @functools.wraps(method) + def _wrapper(*args, **kwargs): + try: + result = method(*args, **kwargs) + except StopEverything: + raise unittest.SkipTest("StopEverything!") + return result + return _wrapper + + +class SkipConvertingMetaclass(type): + """Decorate all test methods to convert StopEverything to SkipTest.""" + def __new__(cls, name, bases, attrs): + for attr_name, attr_value in attrs.items(): + if attr_name.startswith('test_') and isinstance(attr_value, types.FunctionType): + attrs[attr_name] = convert_skip_exceptions(attr_value) + + return super(SkipConvertingMetaclass, cls).__new__(cls, name, bases, attrs) + + +StopEverythingMixin = SkipConvertingMetaclass('StopEverythingMixin', (), {}) + + +class StdStreamCapturingMixin: + """ + Adapter from the pytest capsys fixture to more convenient methods. + + This doesn't also output to the real stdout, so we probably want to move + to "real" capsys when we can use fixtures in test methods. + + Once you've used one of these methods, the capturing is reset, so another + invocation will only return the delta. + + """ + @pytest.fixture(autouse=True) + def _capcapsys(self, capsys): + """Grab the fixture so our methods can use it.""" + self.capsys = capsys + + def stdouterr(self): + """Returns (out, err), two strings for stdout and stderr.""" + return self.capsys.readouterr() + + def stdout(self): + """Returns a string, the captured stdout.""" + return self.capsys.readouterr().out + + def stderr(self): + """Returns a string, the captured stderr.""" + return self.capsys.readouterr().err diff --git a/tests/test_api.py b/tests/test_api.py index f8b7b4b2c..391a52e0b 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -12,6 +12,7 @@ import sys import textwrap +import pytest from unittest_mixins import change_dir import coverage @@ -21,7 +22,8 @@ from coverage.files import abs_file, relative_filename from coverage.misc import CoverageException -from tests.coveragetest import CoverageTest, CoverageTestMethodsMixin, TESTS_DIR, UsingModulesMixin +from tests.coveragetest import CoverageTest, StopEverythingMixin, TESTS_DIR, UsingModulesMixin +from tests.helpers import assert_count_equal class ApiTest(CoverageTest): @@ -42,7 +44,7 @@ def assertFiles(self, files): """Assert that the files here are `files`, ignoring the usual junk.""" here = os.listdir(".") here = self.clean_files(here, ["*.pyc", "__pycache__", "*$py.class"]) - self.assertCountEqual(here, files) + assert_count_equal(here, files) def test_unexecuted_file(self): cov = coverage.Coverage() @@ -63,8 +65,8 @@ def test_unexecuted_file(self): self.start_import_stop(cov, "mycode") _, statements, missing, _ = cov.analysis("not_run.py") - self.assertEqual(statements, [1]) - self.assertEqual(missing, [1]) + assert statements == [1] + assert missing == [1] def test_filenames(self): @@ -82,14 +84,14 @@ def test_filenames(self): self.start_import_stop(cov, "mymain") filename, _, _, _ = cov.analysis("mymain.py") - self.assertEqual(os.path.basename(filename), "mymain.py") + assert os.path.basename(filename) == "mymain.py" filename, _, _, _ = cov.analysis("mymod.py") - self.assertEqual(os.path.basename(filename), "mymod.py") + assert os.path.basename(filename) == "mymod.py" filename, _, _, _ = cov.analysis(sys.modules["mymain"]) - self.assertEqual(os.path.basename(filename), "mymain.py") + assert os.path.basename(filename) == "mymain.py" filename, _, _, _ = cov.analysis(sys.modules["mymod"]) - self.assertEqual(os.path.basename(filename), "mymod.py") + assert os.path.basename(filename) == "mymod.py" # Import the Python file, executing it again, once it's been compiled # already. @@ -97,14 +99,14 @@ def test_filenames(self): self.start_import_stop(cov, "mymain") filename, _, _, _ = cov.analysis("mymain.py") - self.assertEqual(os.path.basename(filename), "mymain.py") + assert os.path.basename(filename) == "mymain.py" filename, _, _, _ = cov.analysis("mymod.py") - self.assertEqual(os.path.basename(filename), "mymod.py") + assert os.path.basename(filename) == "mymod.py" filename, _, _, _ = cov.analysis(sys.modules["mymain"]) - self.assertEqual(os.path.basename(filename), "mymain.py") + assert os.path.basename(filename) == "mymain.py" filename, _, _, _ = cov.analysis(sys.modules["mymod"]) - self.assertEqual(os.path.basename(filename), "mymod.py") + assert os.path.basename(filename) == "mymod.py" def test_ignore_stdlib(self): self.make_file("mymain.py", """\ @@ -115,15 +117,15 @@ def test_ignore_stdlib(self): # Measure without the stdlib. cov1 = coverage.Coverage() - self.assertEqual(cov1.config.cover_pylib, False) + assert cov1.config.cover_pylib is False self.start_import_stop(cov1, "mymain") # some statements were marked executed in mymain.py _, statements, missing, _ = cov1.analysis("mymain.py") - self.assertNotEqual(statements, missing) + assert statements != missing # but none were in colorsys.py _, statements, missing, _ = cov1.analysis("colorsys.py") - self.assertEqual(statements, missing) + assert statements == missing # Measure with the stdlib. cov2 = coverage.Coverage(cover_pylib=True) @@ -131,10 +133,10 @@ def test_ignore_stdlib(self): # some statements were marked executed in mymain.py _, statements, missing, _ = cov2.analysis("mymain.py") - self.assertNotEqual(statements, missing) + assert statements != missing # and some were marked executed in colorsys.py _, statements, missing, _ = cov2.analysis("colorsys.py") - self.assertNotEqual(statements, missing) + assert statements != missing def test_include_can_measure_stdlib(self): self.make_file("mymain.py", """\ @@ -150,57 +152,55 @@ def test_include_can_measure_stdlib(self): # some statements were marked executed in colorsys.py _, statements, missing, _ = cov1.analysis("colorsys.py") - self.assertNotEqual(statements, missing) + assert statements != missing # but none were in random.py _, statements, missing, _ = cov1.analysis("random.py") - self.assertEqual(statements, missing) + assert statements == missing def test_exclude_list(self): cov = coverage.Coverage() cov.clear_exclude() - self.assertEqual(cov.get_exclude_list(), []) + assert cov.get_exclude_list() == [] cov.exclude("foo") - self.assertEqual(cov.get_exclude_list(), ["foo"]) + assert cov.get_exclude_list() == ["foo"] cov.exclude("bar") - self.assertEqual(cov.get_exclude_list(), ["foo", "bar"]) - self.assertEqual(cov._exclude_regex('exclude'), "(?:foo)|(?:bar)") + assert cov.get_exclude_list() == ["foo", "bar"] + assert cov._exclude_regex('exclude') == "(?:foo)|(?:bar)" cov.clear_exclude() - self.assertEqual(cov.get_exclude_list(), []) + assert cov.get_exclude_list() == [] def test_exclude_partial_list(self): cov = coverage.Coverage() cov.clear_exclude(which='partial') - self.assertEqual(cov.get_exclude_list(which='partial'), []) + assert cov.get_exclude_list(which='partial') == [] cov.exclude("foo", which='partial') - self.assertEqual(cov.get_exclude_list(which='partial'), ["foo"]) + assert cov.get_exclude_list(which='partial') == ["foo"] cov.exclude("bar", which='partial') - self.assertEqual(cov.get_exclude_list(which='partial'), ["foo", "bar"]) - self.assertEqual( - cov._exclude_regex(which='partial'), "(?:foo)|(?:bar)" - ) + assert cov.get_exclude_list(which='partial') == ["foo", "bar"] + assert cov._exclude_regex(which='partial') == "(?:foo)|(?:bar)" cov.clear_exclude(which='partial') - self.assertEqual(cov.get_exclude_list(which='partial'), []) + assert cov.get_exclude_list(which='partial') == [] def test_exclude_and_partial_are_separate_lists(self): cov = coverage.Coverage() cov.clear_exclude(which='partial') cov.clear_exclude(which='exclude') cov.exclude("foo", which='partial') - self.assertEqual(cov.get_exclude_list(which='partial'), ['foo']) - self.assertEqual(cov.get_exclude_list(which='exclude'), []) + assert cov.get_exclude_list(which='partial') == ['foo'] + assert cov.get_exclude_list(which='exclude') == [] cov.exclude("bar", which='exclude') - self.assertEqual(cov.get_exclude_list(which='partial'), ['foo']) - self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar']) + assert cov.get_exclude_list(which='partial') == ['foo'] + assert cov.get_exclude_list(which='exclude') == ['bar'] cov.exclude("p2", which='partial') cov.exclude("e2", which='exclude') - self.assertEqual(cov.get_exclude_list(which='partial'), ['foo', 'p2']) - self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar', 'e2']) + assert cov.get_exclude_list(which='partial') == ['foo', 'p2'] + assert cov.get_exclude_list(which='exclude') == ['bar', 'e2'] cov.clear_exclude(which='partial') - self.assertEqual(cov.get_exclude_list(which='partial'), []) - self.assertEqual(cov.get_exclude_list(which='exclude'), ['bar', 'e2']) + assert cov.get_exclude_list(which='partial') == [] + assert cov.get_exclude_list(which='exclude') == ['bar', 'e2'] cov.clear_exclude(which='exclude') - self.assertEqual(cov.get_exclude_list(which='partial'), []) - self.assertEqual(cov.get_exclude_list(which='exclude'), []) + assert cov.get_exclude_list(which='partial') == [] + assert cov.get_exclude_list(which='exclude') == [] def test_datafile_default(self): # Default data file behavior: it's .coverage @@ -292,7 +292,7 @@ def test_empty_reporting(self): # empty summary reports raise exception, just like the xml report cov = coverage.Coverage() cov.erase() - with self.assertRaisesRegex(CoverageException, "No data to report."): + with pytest.raises(CoverageException, match="No data to report."): cov.report() def test_completely_zero_reporting(self): @@ -310,7 +310,7 @@ def test_completely_zero_reporting(self): # TOTAL 1 1 0% last = self.last_line_squeezed(self.stdout()) - self.assertEqual("TOTAL 1 1 0%", last) + assert "TOTAL 1 1 0%" == last def test_cov4_data_file(self): cov4_data = ( @@ -319,7 +319,7 @@ def test_cov4_data_file(self): ) self.make_file(".coverage", cov4_data) cov = coverage.Coverage() - with self.assertRaisesRegex(CoverageException, "Looks like a coverage 4.x data file"): + with pytest.raises(CoverageException, match="Looks like a coverage 4.x data file"): cov.load() cov.erase() @@ -336,11 +336,11 @@ def make_code1_code2(self): def check_code1_code2(self, cov): """Check the analysis is correct for code1.py and code2.py.""" _, statements, missing, _ = cov.analysis("code1.py") - self.assertEqual(statements, [1]) - self.assertEqual(missing, []) + assert statements == [1] + assert missing == [] _, statements, missing, _ = cov.analysis("code2.py") - self.assertEqual(statements, [1, 2]) - self.assertEqual(missing, []) + assert statements == [1, 2] + assert missing == [] def test_start_stop_start_stop(self): self.make_code1_code2() @@ -441,18 +441,18 @@ def test_combining_twice(self): self.assert_exists(".coverage") cov2 = coverage.Coverage() - with self.assertRaisesRegex(CoverageException, r"No data to combine"): - cov2.combine(strict=True) + with pytest.raises(CoverageException, match=r"No data to combine"): + cov2.combine(strict=True, keep=False) cov3 = coverage.Coverage() cov3.combine() # Now the data is empty! _, statements, missing, _ = cov3.analysis("code1.py") - self.assertEqual(statements, [1]) - self.assertEqual(missing, [1]) + assert statements == [1] + assert missing == [1] _, statements, missing, _ = cov3.analysis("code2.py") - self.assertEqual(statements, [1, 2]) - self.assertEqual(missing, [1, 2]) + assert statements == [1, 2] + assert missing == [1, 2] def test_combining_with_a_used_coverage(self): # Can you use a coverage object to run one shard of a parallel suite, @@ -521,16 +521,14 @@ def test_warnings(self): self.start_import_stop(cov, "hello") cov.get_data() - out = self.stdout() - self.assertIn("Hello\n", out) - - err = self.stderr() - self.assertIn(textwrap.dedent("""\ + out, err = self.stdouterr() + assert "Hello\n" in out + assert textwrap.dedent("""\ Coverage.py warning: Module sys has no Python source. (module-not-python) Coverage.py warning: Module xyzzy was never imported. (module-not-imported) Coverage.py warning: Module quux was never imported. (module-not-imported) Coverage.py warning: No data was collected. (no-data-collected) - """), err) + """) in err def test_warnings_suppressed(self): self.make_file("hello.py", """\ @@ -545,16 +543,11 @@ def test_warnings_suppressed(self): self.start_import_stop(cov, "hello") cov.get_data() - out = self.stdout() - self.assertIn("Hello\n", out) - - err = self.stderr() - self.assertIn( - "Coverage.py warning: Module sys has no Python source. (module-not-python)", - err - ) - self.assertNotIn("module-not-imported", err) - self.assertNotIn("no-data-collected", err) + out, err = self.stdouterr() + assert "Hello\n" in out + assert "Coverage.py warning: Module sys has no Python source. (module-not-python)" in err + assert "module-not-imported" not in err + assert "no-data-collected" not in err def test_warn_once(self): cov = coverage.Coverage() @@ -562,8 +555,8 @@ def test_warn_once(self): cov._warn("Warning, warning 1!", slug="bot", once=True) cov._warn("Warning, warning 2!", slug="bot", once=True) err = self.stderr() - self.assertIn("Warning, warning 1!", err) - self.assertNotIn("Warning, warning 2!", err) + assert "Warning, warning 1!" in err + assert "Warning, warning 2!" not in err def test_source_and_include_dont_conflict(self): # A bad fix made this case fail: https://github.com/nedbat/coveragepy/issues/541 @@ -592,7 +585,7 @@ def test_source_and_include_dont_conflict(self): --------------------------- TOTAL 1 0 100% """) - self.assertEqual(expected, self.stdout()) + assert expected == self.stdout() def make_test_files(self): """Create a simple file representing a method with two tests. @@ -637,18 +630,15 @@ def test_switch_context_testrunner(self): # Labeled data is collected data = cov.get_data() - self.assertEqual( - [u'', u'multiply_six', u'multiply_zero'], - sorted(data.measured_contexts()) - ) + assert [u'', u'multiply_six', u'multiply_zero'] == sorted(data.measured_contexts()) filenames = self.get_measured_filenames(data) suite_filename = filenames['testsuite.py'] data.set_query_context("multiply_six") - self.assertEqual([2, 8], sorted(data.lines(suite_filename))) + assert [2, 8] == sorted(data.lines(suite_filename)) data.set_query_context("multiply_zero") - self.assertEqual([2, 5], sorted(data.lines(suite_filename))) + assert [2, 5] == sorted(data.lines(suite_filename)) def test_switch_context_with_static(self): # This test simulates a coverage-aware test runner, @@ -678,18 +668,16 @@ def test_switch_context_with_static(self): # Labeled data is collected data = cov.get_data() - self.assertEqual( - [u'mysuite', u'mysuite|multiply_six', u'mysuite|multiply_zero'], - sorted(data.measured_contexts()), - ) + expected = [u'mysuite', u'mysuite|multiply_six', u'mysuite|multiply_zero'] + assert expected == sorted(data.measured_contexts()) filenames = self.get_measured_filenames(data) suite_filename = filenames['testsuite.py'] data.set_query_context("mysuite|multiply_six") - self.assertEqual([2, 8], sorted(data.lines(suite_filename))) + assert [2, 8] == sorted(data.lines(suite_filename)) data.set_query_context("mysuite|multiply_zero") - self.assertEqual([2, 5], sorted(data.lines(suite_filename))) + assert [2, 5] == sorted(data.lines(suite_filename)) def test_dynamic_context_conflict(self): cov = coverage.Coverage(source=["."]) @@ -698,24 +686,22 @@ def test_dynamic_context_conflict(self): # Switch twice, but only get one warning. cov.switch_context("test1") # pragma: nested cov.switch_context("test2") # pragma: nested - self.assertEqual( # pragma: nested - self.stderr(), - "Coverage.py warning: Conflicting dynamic contexts (dynamic-conflict)\n" - ) + expected = "Coverage.py warning: Conflicting dynamic contexts (dynamic-conflict)\n" + assert expected == self.stderr() cov.stop() # pragma: nested def test_switch_context_unstarted(self): # Coverage must be started to switch context msg = "Cannot switch context, coverage is not started" cov = coverage.Coverage() - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): cov.switch_context("test1") cov.start() cov.switch_context("test2") # pragma: nested cov.stop() # pragma: nested - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): cov.switch_context("test3") def test_config_crash(self): @@ -723,7 +709,7 @@ def test_config_crash(self): # exceptions from inside Coverage. cov = coverage.Coverage() cov.set_option("run:_crash", "test_config_crash") - with self.assertRaisesRegex(Exception, "Crashing because called by test_config_crash"): + with pytest.raises(Exception, match="Crashing because called by test_config_crash"): cov.start() def test_config_crash_no_crash(self): @@ -777,21 +763,19 @@ def test_current(self): assert cur0 is cur3 +@pytest.mark.skipif(not env.PYBEHAVIOR.namespaces_pep420, + reason="Python before 3.3 doesn't have namespace packages" +) class NamespaceModuleTest(UsingModulesMixin, CoverageTest): """Test PEP-420 namespace modules.""" - def setUp(self): - if not env.PYBEHAVIOR.namespaces_pep420: - self.skipTest("Python before 3.3 doesn't have namespace packages") - super(NamespaceModuleTest, self).setUp() - def test_explicit_namespace_module(self): self.make_file("main.py", "import namespace_420\n") cov = coverage.Coverage() self.start_import_stop(cov, "main") - with self.assertRaisesRegex(CoverageException, r"Module .* has no file"): + with pytest.raises(CoverageException, match=r"Module .* has no file"): cov.analysis(sys.modules['namespace_420']) def test_bug_572(self): @@ -805,7 +789,7 @@ def test_bug_572(self): cov.report() -class IncludeOmitTestsMixin(UsingModulesMixin, CoverageTestMethodsMixin): +class IncludeOmitTestsMixin(UsingModulesMixin, StopEverythingMixin): """Test methods for coverage methods taking include and omit.""" # We don't write any source files, but the data file will collide with @@ -815,12 +799,12 @@ class IncludeOmitTestsMixin(UsingModulesMixin, CoverageTestMethodsMixin): def filenames_in(self, summary, filenames): """Assert the `filenames` are in the keys of `summary`.""" for filename in filenames.split(): - self.assertIn(filename, summary) + assert filename in summary def filenames_not_in(self, summary, filenames): """Assert the `filenames` are not in the keys of `summary`.""" for filename in filenames.split(): - self.assertNotIn(filename, summary) + assert filename not in summary def test_nothing_specified(self): result = self.coverage_usepkgs() @@ -892,27 +876,27 @@ def test_source_include_exclusive(self): cov.stop() # pragma: nested def test_source_package_as_package(self): - self.assertFalse(os.path.isdir("pkg1")) + assert not os.path.isdir("pkg1") lines = self.coverage_usepkgs(source=["pkg1"]) self.filenames_in(lines, "p1a p1b") self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") # Because source= was specified, we do search for unexecuted files. - self.assertEqual(lines['p1c'], 0) + assert lines['p1c'] == 0 def test_source_package_as_dir(self): self.chdir(self.nice_file(TESTS_DIR, 'modules')) - self.assertTrue(os.path.isdir("pkg1")) + assert os.path.isdir("pkg1") lines = self.coverage_usepkgs(source=["pkg1"]) self.filenames_in(lines, "p1a p1b") self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") # Because source= was specified, we do search for unexecuted files. - self.assertEqual(lines['p1c'], 0) + assert lines['p1c'] == 0 def test_source_package_dotted_sub(self): lines = self.coverage_usepkgs(source=["pkg1.sub"]) self.filenames_not_in(lines, "p2a p2b othera otherb osa osb") # Because source= was specified, we do search for unexecuted files. - self.assertEqual(lines['runmod3'], 0) + assert lines['runmod3'] == 0 def test_source_package_dotted_p1b(self): lines = self.coverage_usepkgs(source=["pkg1.p1b"]) @@ -930,14 +914,14 @@ def test_source_package_part_omitted(self): lines = self.coverage_usepkgs(source=["pkg1"], omit=["pkg1/p1b.py"]) self.filenames_in(lines, "p1a") self.filenames_not_in(lines, "p1b") - self.assertEqual(lines['p1c'], 0) + assert lines['p1c'] == 0 def test_source_package_as_package_part_omitted(self): # https://github.com/nedbat/coveragepy/issues/638 lines = self.coverage_usepkgs(source=["pkg1"], omit=["*/p1b.py"]) self.filenames_in(lines, "p1a") self.filenames_not_in(lines, "p1b") - self.assertEqual(lines['p1c'], 0) + assert lines['p1c'] == 0 def test_ambiguous_source_package_as_dir(self): # pkg1 is a directory and a pkg, since we cd into tests/modules/ambiguous @@ -954,7 +938,7 @@ def test_ambiguous_source_package_as_package(self): self.filenames_in(lines, "p1a p1b") self.filenames_not_in(lines, "p2a p2b othera otherb osa osb ambiguous") # Because source= was specified, we do search for unexecuted files. - self.assertEqual(lines['p1c'], 0) + assert lines['p1c'] == 0 class ReportIncludeOmitTest(IncludeOmitTestsMixin, CoverageTest): @@ -1012,13 +996,13 @@ def fun2(x): self.start_import_stop(cov, "missing") nums = cov._analyze("missing.py").numbers - self.assertEqual(nums.n_files, 1) - self.assertEqual(nums.n_statements, 7) - self.assertEqual(nums.n_excluded, 1) - self.assertEqual(nums.n_missing, 3) - self.assertEqual(nums.n_branches, 2) - self.assertEqual(nums.n_partial_branches, 0) - self.assertEqual(nums.n_missing_branches, 2) + assert nums.n_files == 1 + assert nums.n_statements == 7 + assert nums.n_excluded == 1 + assert nums.n_missing == 3 + assert nums.n_branches == 2 + assert nums.n_partial_branches == 0 + assert nums.n_missing_branches == 2 class TestRunnerPluginTest(CoverageTest): @@ -1049,13 +1033,13 @@ def pretend_to_be_nose_with_cover(self, erase=False, cd=False): cov.combine() cov.save() cov.report(["no_biggie.py"], show_missing=True) - self.assertEqual(self.stdout(), textwrap.dedent("""\ + assert self.stdout() == textwrap.dedent("""\ Name Stmts Miss Cover Missing -------------------------------------------- no_biggie.py 4 1 75% 4 -------------------------------------------- TOTAL 4 1 75% - """)) + """) if cd: os.chdir("..") @@ -1094,13 +1078,13 @@ def pretend_to_be_pytestcov(self, append): report = StringIO() cov.report(show_missing=None, ignore_errors=True, file=report, skip_covered=None, skip_empty=None) - self.assertEqual(report.getvalue(), textwrap.dedent("""\ + assert report.getvalue() == textwrap.dedent("""\ Name Stmts Miss Cover ----------------------------- prog.py 4 1 75% ----------------------------- TOTAL 4 1 75% - """)) + """) self.assert_file_count(".coverage", 0) self.assert_file_count(".coverage.*", 1) @@ -1117,9 +1101,9 @@ def test_config_doesnt_change(self): self.make_file("simple.py", "a = 1") cov = coverage.Coverage() self.start_import_stop(cov, "simple") - self.assertEqual(cov.get_option("report:show_missing"), False) + assert cov.get_option("report:show_missing") is False cov.report(show_missing=True) - self.assertEqual(cov.get_option("report:show_missing"), False) + assert cov.get_option("report:show_missing") is False class RelativePathTest(CoverageTest): @@ -1140,7 +1124,7 @@ def test_moving_stuff(self): with change_dir("new"): cov = coverage.Coverage() cov.load() - with self.assertRaisesRegex(CoverageException, expected): + with pytest.raises(CoverageException, match=expected): cov.report() def test_moving_stuff_with_relative(self): diff --git a/tests/test_arcs.py b/tests/test_arcs.py index f3aa8ebb9..83e9e6b11 100644 --- a/tests/test_arcs.py +++ b/tests/test_arcs.py @@ -6,6 +6,7 @@ import pytest from tests.coveragetest import CoverageTest +from tests.helpers import assert_count_equal import coverage from coverage import env @@ -94,7 +95,7 @@ def fn(x): if x % 2: return True return False a = fn(1) - assert a == True + assert a is True """, arcz=".1 14 45 5. .2 2. 23 3.", arcz_missing="23 3.") @@ -270,13 +271,10 @@ def test_while_1(self): # With "while 1", the loop knows it's constant. if env.PYBEHAVIOR.keep_constant_test: arcz = ".1 12 23 34 45 36 62 57 7." - arcz_missing = "" elif env.PYBEHAVIOR.nix_while_true: arcz = ".1 13 34 45 36 63 57 7." - arcz_missing = "" else: arcz = ".1 12 23 34 45 36 63 57 7." - arcz_missing = "" self.check_coverage("""\ a, i = 1, 0 while 1: @@ -287,7 +285,6 @@ def test_while_1(self): assert a == 4 and i == 3 """, arcz=arcz, - arcz_missing=arcz_missing, ) def test_while_true(self): @@ -322,7 +319,7 @@ def method(self): return 1 """) out = self.run_command("coverage run --branch --source=. main.py") - self.assertEqual(out, 'done\n') + assert out == 'done\n' if env.PYBEHAVIOR.keep_constant_test: num_stmts = 3 elif env.PYBEHAVIOR.nix_while_true: @@ -332,7 +329,7 @@ def method(self): expected = "zero.py {n} {n} 0 0 0% 1-3".format(n=num_stmts) report = self.report_from_command("coverage report -m") squeezed = self.squeezed_lines(report) - self.assertIn(expected, squeezed[3]) + assert expected in squeezed[3] def test_bug_496_continue_in_constant_while(self): # https://github.com/nedbat/coveragepy/issues/496 @@ -1086,11 +1083,12 @@ def double_inputs(): ".2 23 34 45 52 2.", arcz_missing="2.", ) - self.assertEqual(self.stdout(), "20\n12\n") + assert self.stdout() == "20\n12\n" + @pytest.mark.skipif(not env.PYBEHAVIOR.yield_from, + reason="Python before 3.3 doesn't have 'yield from'" + ) def test_yield_from(self): - if not env.PYBEHAVIOR.yield_from: - self.skipTest("Python before 3.3 doesn't have 'yield from'") self.check_coverage("""\ def gen(inp): i = 2 @@ -1335,9 +1333,10 @@ def test_dict_literal(self): arcz=".1 19 9.", ) + @pytest.mark.skipif(not env.PYBEHAVIOR.unpackings_pep448, + reason="Don't have unpacked literals until 3.5" + ) def test_unpacked_literals(self): - if not env.PYBEHAVIOR.unpackings_pep448: - self.skipTest("Don't have unpacked literals until 3.5") self.check_coverage("""\ d = { 'a': 2, @@ -1374,7 +1373,8 @@ def test_pathologically_long_code_object(self): # opcodes. # Note that we no longer interpret bytecode at all, but it couldn't # hurt to keep the test... - for n in [10, 50, 100, 500, 1000, 2000]: + sizes = [10, 50, 100, 500, 1000, 2000] + for n in sizes: code = """\ data = [ """ + "".join("""\ @@ -1387,7 +1387,7 @@ def test_pathologically_long_code_object(self): print(len(data)) """ self.check_coverage(code, arcs=[(-1, 1), (1, 2*n+4), (2*n+4, -1)]) - self.assertEqual(self.stdout().split()[-1], str(n)) + assert self.stdout().split() == [str(n) for n in sizes] def test_partial_generators(self): # https://github.com/nedbat/coveragepy/issues/475 @@ -1410,14 +1410,10 @@ def f(a, b): filename = self.last_module_name + ".py" fr = cov._get_file_reporter(filename) arcs_executed = cov._analyze(filename).arcs_executed() - self.assertEqual( - fr.missing_arc_description(3, -3, arcs_executed), - "line 3 didn't finish the generator expression on line 3" - ) - self.assertEqual( - fr.missing_arc_description(4, -4, arcs_executed), - "line 4 didn't run the generator expression on line 4" - ) + expected = "line 3 didn't finish the generator expression on line 3" + assert expected == fr.missing_arc_description(3, -3, arcs_executed) + expected = "line 4 didn't run the generator expression on line 4" + assert expected == fr.missing_arc_description(4, -4, arcs_executed) class DecoratorArcTest(CoverageTest): @@ -1587,14 +1583,10 @@ def test_lambda_in_dict(self): ) +@pytest.mark.skipif(not env.PYBEHAVIOR.async_syntax, reason="Async features are new in Python 3.5") class AsyncTest(CoverageTest): """Tests of the new async and await keywords in Python 3.5""" - def setUp(self): - if not env.PYBEHAVIOR.async_syntax: - self.skipTest("Async features are new in Python 3.5") - super(AsyncTest, self).setUp() - def test_async(self): self.check_coverage("""\ import asyncio @@ -1620,7 +1612,7 @@ async def print_sum(x, y): # 8 "-89 9C C-8", arcz_unpredicted="5-3 9-8", ) - self.assertEqual(self.stdout(), "Compute 1 + 2 ...\n1 + 2 = 3\n") + assert self.stdout() == "Compute 1 + 2 ...\n1 + 2 = 3\n" def test_async_for(self): self.check_coverage("""\ @@ -1657,7 +1649,7 @@ async def doit(): # G "-AB BC C-A DE E-A ", # __anext__ arcz_unpredicted="CD", ) - self.assertEqual(self.stdout(), "a\nb\nc\n.\n") + assert self.stdout() == "a\nb\nc\n.\n" def test_async_with(self): self.check_coverage("""\ @@ -1739,4 +1731,4 @@ def fun1(x): data = cov.get_data() fun1_lines = data.lines(abs_file("fun1.py")) - self.assertCountEqual(fun1_lines, [1, 2, 5]) + assert_count_equal(fun1_lines, [1, 2, 5]) diff --git a/tests/test_backward.py b/tests/test_backward.py index 8acb8707c..d750022b3 100644 --- a/tests/test_backward.py +++ b/tests/test_backward.py @@ -3,20 +3,22 @@ """Tests that our version shims in backward.py are working.""" -from coverage.backunittest import TestCase +import unittest + from coverage.backward import iitems, binary_bytes, bytes_to_ints +from tests.helpers import assert_count_equal -class BackwardTest(TestCase): +class BackwardTest(unittest.TestCase): """Tests of things from backward.py.""" def test_iitems(self): d = {'a': 1, 'b': 2, 'c': 3} items = [('a', 1), ('b', 2), ('c', 3)] - self.assertCountEqual(list(iitems(d)), items) + assert_count_equal(list(iitems(d)), items) def test_binary_bytes(self): byte_values = [0, 255, 17, 23, 42, 57] bb = binary_bytes(byte_values) - self.assertEqual(len(bb), len(byte_values)) - self.assertEqual(byte_values, list(bytes_to_ints(bb))) + assert len(bb) == len(byte_values) + assert byte_values == list(bytes_to_ints(bb)) diff --git a/tests/test_cmdline.py b/tests/test_cmdline.py index 374adb0de..d51410280 100644 --- a/tests/test_cmdline.py +++ b/tests/test_cmdline.py @@ -113,7 +113,7 @@ def mock_command_line(self, args, options=None): def cmd_executes(self, args, code, ret=OK, options=None): """Assert that the `args` end up executing the sequence in `code`.""" called, status = self.mock_command_line(args, options=options) - self.assertEqual(status, ret, "Wrong status: got %r, wanted %r" % (status, ret)) + assert status == ret, "Wrong status: got %r, wanted %r" % (status, ret) # Remove all indentation, and execute with mock globals code = textwrap.dedent(code) @@ -136,7 +136,7 @@ def cmd_executes_same(self, args1, args2): """Assert that the `args1` executes the same as `args2`.""" m1, r1 = self.mock_command_line(args1) m2, r2 = self.mock_command_line(args2) - self.assertEqual(r1, r2) + assert r1 == r2 self.assert_same_mock_calls(m1, m2) def assert_same_mock_calls(self, m1, m2): @@ -147,7 +147,7 @@ def assert_same_mock_calls(self, m1, m2): if m1.mock_calls != m2.mock_calls: pp1 = pprint.pformat(m1.mock_calls) pp2 = pprint.pformat(m2.mock_calls) - self.assertMultiLineEqual(pp1+'\n', pp2+'\n') + assert pp1+'\n' == pp2+'\n' def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): """Run a command line, and check that it prints the right help. @@ -157,11 +157,11 @@ def cmd_help(self, args, help_msg=None, topic=None, ret=ERR): """ mk, status = self.mock_command_line(args) - self.assertEqual(status, ret, "Wrong status: got %s, wanted %s" % (status, ret)) + assert status == ret, "Wrong status: got %s, wanted %s" % (status, ret) if help_msg: - self.assertEqual(mk.mock_calls[-1], ('show_help', (help_msg,), {})) + assert mk.mock_calls[-1] == ('show_help', (help_msg,), {}) else: - self.assertEqual(mk.mock_calls[-1], ('show_help', (), {'topic': topic})) + assert mk.mock_calls[-1] == ('show_help', (), {'topic': topic}) class BaseCmdLineTestTest(BaseCmdLineTest): @@ -169,7 +169,7 @@ class BaseCmdLineTestTest(BaseCmdLineTest): def test_cmd_executes_same(self): # All the other tests here use self.cmd_executes_same in successful # ways, so here we just check that it fails. - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.cmd_executes_same("run", "debug") @@ -218,20 +218,20 @@ def test_combine(self): # coverage combine with args self.cmd_executes("combine datadir1", """\ cov = Coverage() - cov.combine(["datadir1"], strict=True) + cov.combine(["datadir1"], strict=True, keep=False) cov.save() """) # coverage combine, appending self.cmd_executes("combine --append datadir1", """\ cov = Coverage() cov.load() - cov.combine(["datadir1"], strict=True) + cov.combine(["datadir1"], strict=True, keep=False) cov.save() """) # coverage combine without args self.cmd_executes("combine", """\ cov = Coverage() - cov.combine(None, strict=True) + cov.combine(None, strict=True, keep=False) cov.save() """) @@ -239,12 +239,12 @@ def test_combine_doesnt_confuse_options_with_args(self): # https://github.com/nedbat/coveragepy/issues/385 self.cmd_executes("combine --rcfile cov.ini", """\ cov = Coverage(config_file='cov.ini') - cov.combine(None, strict=True) + cov.combine(None, strict=True, keep=False) cov.save() """) self.cmd_executes("combine --rcfile cov.ini data1 data2/more", """\ cov = Coverage(config_file='cov.ini') - cov.combine(["data1", "data2/more"], strict=True) + cov.combine(["data1", "data2/more"], strict=True, keep=False) cov.save() """) @@ -255,15 +255,15 @@ def test_debug(self): def test_debug_sys(self): self.command_line("debug sys") out = self.stdout() - self.assertIn("version:", out) - self.assertIn("data_file:", out) + assert "version:" in out + assert "data_file:" in out def test_debug_config(self): self.command_line("debug config") out = self.stdout() - self.assertIn("cover_pylib:", out) - self.assertIn("skip_covered:", out) - self.assertIn("skip_empty:", out) + assert "cover_pylib:" in out + assert "skip_covered:" in out + assert "skip_empty:" in out def test_erase(self): # coverage erase @@ -529,7 +529,7 @@ def test_run(self): def test_bad_concurrency(self): self.command_line("run --concurrency=nothing", ret=ERR) err = self.stderr() - self.assertIn("option --concurrency: invalid choice: 'nothing'", err) + assert "option --concurrency: invalid choice: 'nothing'" in err def test_no_multiple_concurrency(self): # You can't use multiple concurrency values on the command line. @@ -537,21 +537,17 @@ def test_no_multiple_concurrency(self): # values for this option, but optparse is not that flexible. self.command_line("run --concurrency=multiprocessing,gevent foo.py", ret=ERR) err = self.stderr() - self.assertIn("option --concurrency: invalid choice: 'multiprocessing,gevent'", err) + assert "option --concurrency: invalid choice: 'multiprocessing,gevent'" in err def test_multiprocessing_needs_config_file(self): # You can't use command-line args to add options to multiprocessing # runs, since they won't make it to the subprocesses. You need to use a # config file. self.command_line("run --concurrency=multiprocessing --branch foo.py", ret=ERR) - self.assertIn( - "Options affecting multiprocessing must only be specified in a configuration file.", - self.stderr() - ) - self.assertIn( - "Remove --branch from the command line.", - self.stderr() - ) + msg = "Options affecting multiprocessing must only be specified in a configuration file." + _, err = self.stdouterr() + assert msg in err + assert "Remove --branch from the command line." in err def test_run_debug(self): self.cmd_executes("run --debug=opt1 foo.py", """\ @@ -605,7 +601,7 @@ def test_run_module(self): def test_run_nothing(self): self.command_line("run", ret=ERR) - self.assertIn("Nothing to do", self.stderr()) + assert "Nothing to do" in self.stderr() def test_run_from_config(self): options = {"run:command_line": "myprog.py a 123 'a quoted thing' xyz"} @@ -660,7 +656,7 @@ def test_run_dashm_only(self): def test_cant_append_parallel(self): self.command_line("run --append --parallel-mode foo.py", ret=ERR) - self.assertIn("Can't append to data files in parallel mode.", self.stderr()) + assert "Can't append to data files in parallel mode." in self.stderr() def test_xml(self): # coverage xml [-i] [--omit DIR,...] [FILE1 FILE2 ...] @@ -781,7 +777,7 @@ def test_debug_data(self): data.write() self.command_line("debug data") - self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ + assert self.stdout() == textwrap.dedent("""\ -- data ------------------------------------------------------ path: FILENAME has_arcs: False @@ -789,16 +785,16 @@ def test_debug_data(self): 2 files: file1.py: 17 lines [a_plugin] file2.py: 23 lines - """).replace("FILENAME", data.data_filename())) + """).replace("FILENAME", data.data_filename()) def test_debug_data_with_no_data(self): data = CoverageData() self.command_line("debug data") - self.assertMultiLineEqual(self.stdout(), textwrap.dedent("""\ + assert self.stdout() == textwrap.dedent("""\ -- data ------------------------------------------------------ path: FILENAME No data collected - """).replace("FILENAME", data.data_filename())) + """).replace("FILENAME", data.data_filename()) class CmdLineStdoutTest(BaseCmdLineTest): @@ -807,31 +803,31 @@ class CmdLineStdoutTest(BaseCmdLineTest): def test_minimum_help(self): self.command_line("") out = self.stdout() - self.assertIn("Code coverage for Python", out) - self.assertLess(out.count("\n"), 4) + assert "Code coverage for Python" in out + assert out.count("\n") < 4 def test_version(self): self.command_line("--version") out = self.stdout() - self.assertIn("ersion ", out) + assert "ersion " in out if env.C_TRACER: - self.assertIn("with C extension", out) + assert "with C extension" in out else: - self.assertIn("without C extension", out) - self.assertLess(out.count("\n"), 4) + assert "without C extension" in out + assert out.count("\n") < 4 + @pytest.mark.skipif(env.JYTHON, reason="Jython gets mad if you patch sys.argv") def test_help_contains_command_name(self): # Command name should be present in help output. - if env.JYTHON: - self.skipTest("Jython gets mad if you patch sys.argv") fake_command_path = "lorem/ipsum/dolor".replace("/", os.sep) expected_command_name = "dolor" fake_argv = [fake_command_path, "sit", "amet"] with mock.patch.object(sys, 'argv', new=fake_argv): self.command_line("help") out = self.stdout() - self.assertIn(expected_command_name, out) + assert expected_command_name in out + @pytest.mark.skipif(env.JYTHON, reason="Jython gets mad if you patch sys.argv") def test_help_contains_command_name_from_package(self): # Command package name should be present in help output. # @@ -839,46 +835,44 @@ def test_help_contains_command_name_from_package(self): # has the `__main__.py` file's patch as the command name. Instead, the command name should # be derived from the package name. - if env.JYTHON: - self.skipTest("Jython gets mad if you patch sys.argv") fake_command_path = "lorem/ipsum/dolor/__main__.py".replace("/", os.sep) expected_command_name = "dolor" fake_argv = [fake_command_path, "sit", "amet"] with mock.patch.object(sys, 'argv', new=fake_argv): self.command_line("help") out = self.stdout() - self.assertIn(expected_command_name, out) + assert expected_command_name in out def test_help(self): self.command_line("help") lines = self.stdout().splitlines() - self.assertGreater(len(lines), 10) - self.assertEqual(lines[-1], "Full documentation is at {}".format(__url__)) + assert len(lines) > 10 + assert lines[-1] == "Full documentation is at {}".format(__url__) def test_cmd_help(self): self.command_line("help run") out = self.stdout() lines = out.splitlines() - self.assertIn("", lines[0]) - self.assertIn("--timid", out) - self.assertGreater(len(lines), 20) - self.assertEqual(lines[-1], "Full documentation is at {}".format(__url__)) + assert "" in lines[0] + assert "--timid" in out + assert len(lines) > 20 + assert lines[-1] == "Full documentation is at {}".format(__url__) def test_unknown_topic(self): # Should probably be an ERR return, but meh. self.command_line("help foobar") lines = self.stdout().splitlines() - self.assertEqual(lines[0], "Don't know topic 'foobar'") - self.assertEqual(lines[-1], "Full documentation is at {}".format(__url__)) + assert lines[0] == "Don't know topic 'foobar'" + assert lines[-1] == "Full documentation is at {}".format(__url__) def test_error(self): self.command_line("fooey kablooey", ret=ERR) err = self.stderr() - self.assertIn("fooey", err) - self.assertIn("help", err) + assert "fooey" in err + assert "help" in err def test_doc_url(self): - self.assertTrue(__url__.startswith("https://coverage.readthedocs.io")) + assert __url__.startswith("https://coverage.readthedocs.io") class CmdMainTest(CoverageTest): @@ -914,25 +908,26 @@ def setUp(self): def test_normal(self): ret = coverage.cmdline.main(['hello']) - self.assertEqual(ret, 0) - self.assertEqual(self.stdout(), "Hello, world!\n") + assert ret == 0 + assert self.stdout() == "Hello, world!\n" def test_raise(self): ret = coverage.cmdline.main(['raise']) - self.assertEqual(ret, 1) - self.assertEqual(self.stdout(), "") - err = self.stderr().split('\n') - self.assertEqual(err[0], 'Traceback (most recent call last):') - self.assertEqual(err[-3], ' raise Exception("oh noes!")') - self.assertEqual(err[-2], 'Exception: oh noes!') + assert ret == 1 + out, err = self.stdouterr() + assert out == "" + err = err.split('\n') + assert err[0] == 'Traceback (most recent call last):' + assert err[-3] == ' raise Exception("oh noes!")' + assert err[-2] == 'Exception: oh noes!' def test_internalraise(self): - with self.assertRaisesRegex(ValueError, "coverage is broken"): + with pytest.raises(ValueError, match="coverage is broken"): coverage.cmdline.main(['internalraise']) def test_exit(self): ret = coverage.cmdline.main(['exit']) - self.assertEqual(ret, 23) + assert ret == 23 class CoverageReportingFake(object): diff --git a/tests/test_collector.py b/tests/test_collector.py index f7e8a4c45..53d7f1752 100644 --- a/tests/test_collector.py +++ b/tests/test_collector.py @@ -46,5 +46,5 @@ def otherfunc(x): # Double-check that our files were checked. abs_files = {os.path.abspath(f) for f in should_trace_hook.filenames} - self.assertIn(os.path.abspath("f1.py"), abs_files) - self.assertIn(os.path.abspath("f2.py"), abs_files) + assert os.path.abspath("f1.py") in abs_files + assert os.path.abspath("f2.py") in abs_files diff --git a/tests/test_concurrency.py b/tests/test_concurrency.py index 2469e2968..86c69cf50 100644 --- a/tests/test_concurrency.py +++ b/tests/test_concurrency.py @@ -6,11 +6,13 @@ import glob import os import random +import re import sys import threading import time from flaky import flaky +import pytest import coverage from coverage import env @@ -91,7 +93,7 @@ def test_line_count(self): print("done") """ - self.assertEqual(line_count(CODE), 5) + assert line_count(CODE) == 5 # The code common to all the concurrency models. @@ -227,14 +229,14 @@ def try_some_code(self, code, concurrency, the_module, expected_out=None): expected_cant_trace = cant_trace_msg(concurrency, the_module) if expected_cant_trace is not None: - self.assertEqual(out, expected_cant_trace) + assert out == expected_cant_trace else: # We can fully measure the code if we are using the C tracer, which # can support all the concurrency, or if we are using threads. if expected_out is None: expected_out = "%d\n" % (sum(range(self.QLIMIT))) print(code) - self.assertEqual(out, expected_out) + assert out == expected_out # Read the coverage file and see that try_it.py has all its lines # executed. @@ -248,7 +250,7 @@ def try_some_code(self, code, concurrency, the_module, expected_out=None): print_simple_annotation(code, linenos) lines = line_count(code) - self.assertEqual(line_counts(data)['try_it.py'], lines) + assert line_counts(data)['try_it.py'] == lines def test_threads(self): code = (THREAD + SUM_RANGE_Q + PRINT_SUM_RANGE).format(QLIMIT=self.QLIMIT) @@ -362,15 +364,11 @@ def process_worker_main(args): """ +@pytest.mark.skipif(not multiprocessing, reason="No multiprocessing in this Python") @flaky(max_runs=30) # Sometimes a test fails due to inherent randomness. Try more times. class MultiprocessingTest(CoverageTest): """Test support of the multiprocessing module.""" - def setUp(self): - if not multiprocessing: - self.skipTest("No multiprocessing in this Python") # pragma: only jython - super(MultiprocessingTest, self).setUp() - def try_multiprocessing_code( self, code, expected_out, the_module, nprocs, concurrency="multiprocessing", args="" ): @@ -399,17 +397,17 @@ def try_multiprocessing_code( expected_cant_trace = cant_trace_msg(concurrency, the_module) if expected_cant_trace is not None: - self.assertEqual(out, expected_cant_trace) + assert out == expected_cant_trace else: - self.assertEqual(out.rstrip(), expected_out) - self.assertEqual(len(glob.glob(".coverage.*")), nprocs + 1) + assert out.rstrip() == expected_out + assert len(glob.glob(".coverage.*")) == nprocs + 1 out = self.run_command("coverage combine") - self.assertEqual(out, "") + assert out == "" out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] - self.assertRegex(last_line, r"TOTAL \d+ 0 100%") + assert re.search(r"TOTAL \d+ 0 100%", last_line) def test_multiprocessing_simple(self): nprocs = 3 @@ -459,14 +457,14 @@ def try_multiprocessing_code_with_branching(self, code, expected_out): continue out = self.run_command("coverage run --rcfile=multi.rc multi.py %s" % (start_method,)) - self.assertEqual(out.rstrip(), expected_out) + assert out.rstrip() == expected_out out = self.run_command("coverage combine") - self.assertEqual(out, "") + assert out == "" out = self.run_command("coverage report -m") last_line = self.squeezed_lines(out)[-1] - self.assertRegex(last_line, r"TOTAL \d+ 0 \d+ 0 100%") + assert re.search(r"TOTAL \d+ 0 \d+ 0 100%", last_line) def test_multiprocessing_with_branching(self): nprocs = 3 @@ -490,8 +488,8 @@ def test_multiprocessing_bootstrap_error_handling(self): _crash = _bootstrap """) out = self.run_command("coverage run multi.py") - self.assertIn("Exception during multiprocessing bootstrap init", out) - self.assertIn("Exception: Crashing because called by _bootstrap", out) + assert "Exception during multiprocessing bootstrap init" in out + assert "Exception: Crashing because called by _bootstrap" in out def test_bug890(self): # chdir in multiprocessing shouldn't keep us from finding the @@ -510,7 +508,7 @@ def test_bug890(self): concurrency = multiprocessing """) out = self.run_command("coverage run multi.py") - self.assertEqual(out.splitlines()[-1], "ok") + assert out.splitlines()[-1] == "ok" def test_coverage_stop_in_threads(): diff --git a/tests/test_config.py b/tests/test_config.py index 4225540c0..b1611c1b8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -7,8 +7,10 @@ from collections import OrderedDict import mock +import pytest import coverage +from coverage.config import HandyConfigParser from coverage.misc import CoverageException from tests.coveragetest import CoverageTest, UsingModulesMixin @@ -21,17 +23,17 @@ class ConfigTest(CoverageTest): def test_default_config(self): # Just constructing a coverage() object gets the right defaults. cov = coverage.Coverage() - self.assertFalse(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, ".coverage") + assert not cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == ".coverage" def test_arguments(self): # Arguments to the constructor are applied to the configuration. cov = coverage.Coverage(timid=True, data_file="fooey.dat", concurrency="multiprocessing") - self.assertTrue(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, "fooey.dat") - self.assertEqual(cov.config.concurrency, ["multiprocessing"]) + assert cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == "fooey.dat" + assert cov.config.concurrency == ["multiprocessing"] def test_config_file(self): # A .coveragerc file will be read into the configuration. @@ -42,9 +44,9 @@ def test_config_file(self): data_file = .hello_kitty.data """) cov = coverage.Coverage() - self.assertTrue(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, ".hello_kitty.data") + assert cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == ".hello_kitty.data" def test_named_config_file(self): # You can name the config file what you like. @@ -55,9 +57,9 @@ def test_named_config_file(self): data_file = delete.me """) cov = coverage.Coverage(config_file="my_cov.ini") - self.assertTrue(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, "delete.me") + assert cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == "delete.me" def test_toml_config_file(self): # A .coveragerc file will be read into the configuration. @@ -79,18 +81,15 @@ def test_toml_config_file(self): hello = "world" """) cov = coverage.Coverage(config_file="pyproject.toml") - self.assertTrue(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.concurrency, [u"a", u"b"]) - self.assertEqual(cov.config.data_file, u".hello_kitty.data") - self.assertEqual(cov.config.plugins, [u"plugins.a_plugin"]) - self.assertEqual(cov.config.precision, 3) - self.assertEqual(cov.config.html_title, u"tabblo & «ταБЬℓσ»") - self.assertAlmostEqual(cov.config.fail_under, 90.5) - self.assertEqual( - cov.config.get_plugin_options("plugins.a_plugin"), - {u"hello": u"world"} - ) + assert cov.config.timid + assert not cov.config.branch + assert cov.config.concurrency == [u"a", u"b"] + assert cov.config.data_file == u".hello_kitty.data" + assert cov.config.plugins == [u"plugins.a_plugin"] + assert cov.config.precision == 3 + assert cov.config.html_title == u"tabblo & «ταБЬℓσ»" + assert round(abs(cov.config.fail_under-90.5), 7) == 0 + assert cov.config.get_plugin_options("plugins.a_plugin") == {u"hello": u"world"} # Test that our class doesn't reject integers when loading floats self.make_file("pyproject.toml", """\ @@ -99,8 +98,8 @@ def test_toml_config_file(self): fail_under = 90 """) cov = coverage.Coverage(config_file="pyproject.toml") - self.assertAlmostEqual(cov.config.fail_under, 90) - self.assertIsInstance(cov.config.fail_under, float) + assert round(abs(cov.config.fail_under-90), 7) == 0 + assert isinstance(cov.config.fail_under, float) def test_ignored_config_file(self): # You can disable reading the .coveragerc file. @@ -110,9 +109,9 @@ def test_ignored_config_file(self): data_file = delete.me """) cov = coverage.Coverage(config_file=False) - self.assertFalse(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, ".coverage") + assert not cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == ".coverage" def test_config_file_then_args(self): # The arguments override the .coveragerc file. @@ -122,9 +121,9 @@ def test_config_file_then_args(self): data_file = weirdo.file """) cov = coverage.Coverage(timid=False, data_file=".mycov") - self.assertFalse(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, ".mycov") + assert not cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == ".mycov" def test_data_file_from_environment(self): # There's an environment variable for the data_file. @@ -135,10 +134,10 @@ def test_data_file_from_environment(self): """) self.set_environ("COVERAGE_FILE", "fromenv.dat") cov = coverage.Coverage() - self.assertEqual(cov.config.data_file, "fromenv.dat") + assert cov.config.data_file == "fromenv.dat" # But the constructor arguments override the environment variable. cov = coverage.Coverage(data_file="fromarg.dat") - self.assertEqual(cov.config.data_file, "fromarg.dat") + assert cov.config.data_file == "fromarg.dat" def test_debug_from_environment(self): self.make_file(".coveragerc", """\ @@ -147,7 +146,7 @@ def test_debug_from_environment(self): """) self.set_environ("COVERAGE_DEBUG", "callers, fooey") cov = coverage.Coverage() - self.assertEqual(cov.config.debug, ["dataio", "pids", "callers", "fooey"]) + assert cov.config.debug == ["dataio", "pids", "callers", "fooey"] def test_rcfile_from_environment(self): self.make_file("here.ini", """\ @@ -156,12 +155,12 @@ def test_rcfile_from_environment(self): """) self.set_environ("COVERAGE_RCFILE", "here.ini") cov = coverage.Coverage() - self.assertEqual(cov.config.data_file, "overthere.dat") + assert cov.config.data_file == "overthere.dat" def test_missing_rcfile_from_environment(self): self.set_environ("COVERAGE_RCFILE", "nowhere.ini") msg = "Couldn't read 'nowhere.ini' as a config file" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage() def test_parse_errors(self): @@ -185,7 +184,7 @@ def test_parse_errors(self): for bad_config, msg in bad_configs_and_msgs: print("Trying %r" % bad_config) self.make_file(".coveragerc", bad_config) - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage() def test_toml_parse_errors(self): @@ -211,7 +210,7 @@ def test_toml_parse_errors(self): for bad_config, msg in bad_configs_and_msgs: print("Trying %r" % bad_config) self.make_file("pyproject.toml", bad_config) - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage() def test_environment_vars_in_config(self): @@ -232,12 +231,9 @@ def test_environment_vars_in_config(self): self.set_environ("THING", "ZZZ") self.set_environ("OKAY", "yes") cov = coverage.Coverage() - self.assertEqual(cov.config.data_file, "hello-world.fooey") - self.assertEqual(cov.config.branch, True) - self.assertEqual( - cov.config.exclude_list, - ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] - ) + assert cov.config.data_file == "hello-world.fooey" + assert cov.config.branch is True + assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] def test_environment_vars_in_toml_config(self): # Config files can have $envvars in them. @@ -258,12 +254,9 @@ def test_environment_vars_in_toml_config(self): self.set_environ("DATA_FILE", "hello-world") self.set_environ("THING", "ZZZ") cov = coverage.Coverage() - self.assertEqual(cov.config.data_file, "hello-world.fooey") - self.assertEqual(cov.config.branch, True) - self.assertEqual( - cov.config.exclude_list, - ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] - ) + assert cov.config.data_file == "hello-world.fooey" + assert cov.config.branch is True + assert cov.config.exclude_list == ["the_$one", "anotherZZZ", "xZZZy", "xy", "huh${X}what"] def test_tilde_in_config(self): # Config entries that are file paths can be tilde-expanded. @@ -296,11 +289,11 @@ def expanduser(s): with mock.patch.object(coverage.config.os.path, 'expanduser', new=expanduser): cov = coverage.Coverage() - self.assertEqual(cov.config.data_file, "/Users/me/data.file") - self.assertEqual(cov.config.html_dir, "/Users/joe/html_dir") - self.assertEqual(cov.config.xml_output, "/Users/me/somewhere/xml.out") - self.assertEqual(cov.config.exclude_list, ["~/data.file", "~joe/html_dir"]) - self.assertEqual(cov.config.paths, {'mapping': ['/Users/me/src', '/Users/joe/source']}) + assert cov.config.data_file == "/Users/me/data.file" + assert cov.config.html_dir == "/Users/joe/html_dir" + assert cov.config.xml_output == "/Users/me/somewhere/xml.out" + assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"] + assert cov.config.paths == {'mapping': ['/Users/me/src', '/Users/joe/source']} def test_tilde_in_toml_config(self): # Config entries that are file paths can be tilde-expanded. @@ -329,23 +322,23 @@ def expanduser(s): with mock.patch.object(coverage.config.os.path, 'expanduser', new=expanduser): cov = coverage.Coverage() - self.assertEqual(cov.config.data_file, "/Users/me/data.file") - self.assertEqual(cov.config.html_dir, "/Users/joe/html_dir") - self.assertEqual(cov.config.xml_output, "/Users/me/somewhere/xml.out") - self.assertEqual(cov.config.exclude_list, ["~/data.file", "~joe/html_dir"]) + assert cov.config.data_file == "/Users/me/data.file" + assert cov.config.html_dir == "/Users/joe/html_dir" + assert cov.config.xml_output == "/Users/me/somewhere/xml.out" + assert cov.config.exclude_list == ["~/data.file", "~joe/html_dir"] def test_tweaks_after_constructor(self): # set_option can be used after construction to affect the config. cov = coverage.Coverage(timid=True, data_file="fooey.dat") cov.set_option("run:timid", False) - self.assertFalse(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, "fooey.dat") + assert not cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == "fooey.dat" - self.assertFalse(cov.get_option("run:timid")) - self.assertFalse(cov.get_option("run:branch")) - self.assertEqual(cov.get_option("run:data_file"), "fooey.dat") + assert not cov.get_option("run:timid") + assert not cov.get_option("run:branch") + assert cov.get_option("run:data_file") == "fooey.dat" def test_tweaks_paths_after_constructor(self): self.make_file(".coveragerc", """\ @@ -363,24 +356,24 @@ def test_tweaks_paths_after_constructor(self): old_paths["second"] = ["/second/a", "/second/b"] cov = coverage.Coverage() paths = cov.get_option("paths") - self.assertEqual(paths, old_paths) + assert paths == old_paths new_paths = OrderedDict() new_paths['magic'] = ['src', 'ok'] cov.set_option("paths", new_paths) - self.assertEqual(cov.get_option("paths"), new_paths) + assert cov.get_option("paths") == new_paths def test_tweak_error_checking(self): # Trying to set an unknown config value raises an error. cov = coverage.Coverage() - with self.assertRaisesRegex(CoverageException, "No such option: 'run:xyzzy'"): + with pytest.raises(CoverageException, match="No such option: 'run:xyzzy'"): cov.set_option("run:xyzzy", 12) - with self.assertRaisesRegex(CoverageException, "No such option: 'xyzzy:foo'"): + with pytest.raises(CoverageException, match="No such option: 'xyzzy:foo'"): cov.set_option("xyzzy:foo", 12) - with self.assertRaisesRegex(CoverageException, "No such option: 'run:xyzzy'"): + with pytest.raises(CoverageException, match="No such option: 'run:xyzzy'"): _ = cov.get_option("run:xyzzy") - with self.assertRaisesRegex(CoverageException, "No such option: 'xyzzy:foo'"): + with pytest.raises(CoverageException, match="No such option: 'xyzzy:foo'"): _ = cov.get_option("xyzzy:foo") def test_tweak_plugin_options(self): @@ -389,12 +382,12 @@ def test_tweak_plugin_options(self): cov.set_option("run:plugins", ["fooey.plugin", "xyzzy.coverage.plugin"]) cov.set_option("fooey.plugin:xyzzy", 17) cov.set_option("xyzzy.coverage.plugin:plugh", ["a", "b"]) - with self.assertRaisesRegex(CoverageException, "No such option: 'no_such.plugin:foo'"): + with pytest.raises(CoverageException, match="No such option: 'no_such.plugin:foo'"): cov.set_option("no_such.plugin:foo", 23) - self.assertEqual(cov.get_option("fooey.plugin:xyzzy"), 17) - self.assertEqual(cov.get_option("xyzzy.coverage.plugin:plugh"), ["a", "b"]) - with self.assertRaisesRegex(CoverageException, "No such option: 'no_such.plugin:foo'"): + assert cov.get_option("fooey.plugin:xyzzy") == 17 + assert cov.get_option("xyzzy.coverage.plugin:plugh") == ["a", "b"] + with pytest.raises(CoverageException, match="No such option: 'no_such.plugin:foo'"): _ = cov.get_option("no_such.plugin:foo") def test_unknown_option(self): @@ -403,7 +396,7 @@ def test_unknown_option(self): xyzzy = 17 """) msg = r"Unrecognized option '\[run\] xyzzy=' in config file .coveragerc" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): _ = coverage.Coverage() def test_unknown_option_toml(self): @@ -412,7 +405,7 @@ def test_unknown_option_toml(self): xyzzy = 17 """) msg = r"Unrecognized option '\[tool.coverage.run\] xyzzy=' in config file pyproject.toml" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): _ = coverage.Coverage() def test_misplaced_option(self): @@ -421,7 +414,7 @@ def test_misplaced_option(self): branch = True """) msg = r"Unrecognized option '\[report\] branch=' in config file .coveragerc" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): _ = coverage.Coverage() def test_unknown_option_in_other_ini_file(self): @@ -430,19 +423,19 @@ def test_unknown_option_in_other_ini_file(self): huh = what? """) msg = (r"Unrecognized option '\[coverage:run\] huh=' in config file setup.cfg") - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): _ = coverage.Coverage() - def test_note_is_obsolete(self): - self.make_file("main.py", "a = 1") - self.make_file(".coveragerc", """\ + def test_exceptions_from_missing_things(self): + self.make_file("config.ini", """\ [run] - note = I am here I am here I am here! + branch = True """) - cov = coverage.Coverage() - with self.assert_warnings(cov, [r"The '\[run] note' setting is no longer supported."]): - self.start_import_stop(cov, "main") - cov.report() + config = HandyConfigParser("config.ini") + with pytest.raises(Exception, match="No section: 'xyzzy'"): + config.options("xyzzy") + with pytest.raises(Exception, match="No option 'foo' in section: 'xyzzy'"): + config.get("xyzzy", "foo") class ConfigFileTest(UsingModulesMixin, CoverageTest): @@ -546,49 +539,49 @@ class ConfigFileTest(UsingModulesMixin, CoverageTest): def assert_config_settings_are_correct(self, cov): """Check that `cov` has all the settings from LOTSA_SETTINGS.""" - self.assertTrue(cov.config.timid) - self.assertEqual(cov.config.data_file, "something_or_other.dat") - self.assertTrue(cov.config.branch) - self.assertTrue(cov.config.cover_pylib) - self.assertEqual(cov.config.debug, ["callers", "pids", "dataio"]) - self.assertTrue(cov.config.parallel) - self.assertEqual(cov.config.concurrency, ["thread"]) - self.assertEqual(cov.config.source, ["myapp"]) - self.assertEqual(cov.config.source_pkgs, ["ned"]) - self.assertEqual(cov.config.disable_warnings, ["abcd", "efgh"]) - - self.assertEqual(cov.get_exclude_list(), ["if 0:", r"pragma:?\s+no cover", "another_tab"]) - self.assertTrue(cov.config.ignore_errors) - self.assertEqual(cov.config.run_omit, ["twenty"]) - self.assertEqual(cov.config.report_omit, ["one", "another", "some_more", "yet_more"]) - self.assertEqual(cov.config.report_include, ["thirty"]) - self.assertEqual(cov.config.precision, 3) - - self.assertEqual(cov.config.partial_list, [r"pragma:?\s+no branch"]) - self.assertEqual(cov.config.partial_always_list, ["if 0:", "while True:"]) - self.assertEqual(cov.config.plugins, ["plugins.a_plugin", "plugins.another"]) - self.assertTrue(cov.config.show_missing) - self.assertTrue(cov.config.skip_covered) - self.assertTrue(cov.config.skip_empty) - self.assertEqual(cov.config.html_dir, r"c:\tricky\dir.somewhere") - self.assertEqual(cov.config.extra_css, "something/extra.css") - self.assertEqual(cov.config.html_title, "Title & nums # nums!") - - self.assertEqual(cov.config.xml_output, "mycov.xml") - self.assertEqual(cov.config.xml_package_depth, 17) - - self.assertEqual(cov.config.paths, { + assert cov.config.timid + assert cov.config.data_file == "something_or_other.dat" + assert cov.config.branch + assert cov.config.cover_pylib + assert cov.config.debug == ["callers", "pids", "dataio"] + assert cov.config.parallel + assert cov.config.concurrency == ["thread"] + assert cov.config.source == ["myapp"] + assert cov.config.source_pkgs == ["ned"] + assert cov.config.disable_warnings == ["abcd", "efgh"] + + assert cov.get_exclude_list() == ["if 0:", r"pragma:?\s+no cover", "another_tab"] + assert cov.config.ignore_errors + assert cov.config.run_omit == ["twenty"] + assert cov.config.report_omit == ["one", "another", "some_more", "yet_more"] + assert cov.config.report_include == ["thirty"] + assert cov.config.precision == 3 + + assert cov.config.partial_list == [r"pragma:?\s+no branch"] + assert cov.config.partial_always_list == ["if 0:", "while True:"] + assert cov.config.plugins == ["plugins.a_plugin", "plugins.another"] + assert cov.config.show_missing + assert cov.config.skip_covered + assert cov.config.skip_empty + assert cov.config.html_dir == r"c:\tricky\dir.somewhere" + assert cov.config.extra_css == "something/extra.css" + assert cov.config.html_title == "Title & nums # nums!" + + assert cov.config.xml_output == "mycov.xml" + assert cov.config.xml_package_depth == 17 + + assert cov.config.paths == { 'source': ['.', '/home/ned/src/'], 'other': ['other', '/home/ned/other', 'c:\\Ned\\etc'] - }) + } - self.assertEqual(cov.config.get_plugin_options("plugins.a_plugin"), { + assert cov.config.get_plugin_options("plugins.a_plugin") == { 'hello': 'world', 'names': 'Jane/John/Jenny', - }) - self.assertEqual(cov.config.get_plugin_options("plugins.another"), {}) - self.assertEqual(cov.config.json_show_contexts, True) - self.assertEqual(cov.config.json_pretty_print, True) + } + assert cov.config.get_plugin_options("plugins.another") == {} + assert cov.config.json_show_contexts is True + assert cov.config.json_pretty_print is True def test_config_file_settings(self): self.make_file(".coveragerc", self.LOTSA_SETTINGS.format(section="")) @@ -633,9 +626,9 @@ def check_other_not_read_if_coveragerc(self, fname): branch = true """) cov = coverage.Coverage() - self.assertEqual(cov.config.run_include, ["foo"]) - self.assertEqual(cov.config.run_omit, None) - self.assertEqual(cov.config.branch, False) + assert cov.config.run_include == ["foo"] + assert cov.config.run_omit is None + assert cov.config.branch is False def test_setupcfg_only_if_not_coveragerc(self): self.check_other_not_read_if_coveragerc("setup.cfg") @@ -651,8 +644,8 @@ def check_other_config_need_prefixes(self, fname): branch = true """) cov = coverage.Coverage() - self.assertEqual(cov.config.run_omit, None) - self.assertEqual(cov.config.branch, False) + assert cov.config.run_omit is None + assert cov.config.branch is False def test_setupcfg_only_if_prefixed(self): self.check_other_config_need_prefixes("setup.cfg") @@ -689,8 +682,8 @@ def test_non_ascii(self): self.set_environ("TOX_ENVNAME", "weirdo") cov = coverage.Coverage() - self.assertEqual(cov.config.exclude_list, ["first", "✘weirdo", "third"]) - self.assertEqual(cov.config.html_title, "tabblo & «ταБЬℓσ» # numbers") + assert cov.config.exclude_list == ["first", "✘weirdo", "third"] + assert cov.config.html_title == "tabblo & «ταБЬℓσ» # numbers" def test_unreadable_config(self): # If a config file is explicitly specified, then it is an error for it @@ -701,20 +694,31 @@ def test_unreadable_config(self): ] for bad_file in bad_files: msg = "Couldn't read %r as a config file" % bad_file - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage(config_file=bad_file) def test_nocoveragerc_file_when_specified(self): cov = coverage.Coverage(config_file=".coveragerc") - self.assertFalse(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, ".coverage") + assert not cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == ".coverage" + + def test_note_is_obsolete(self): + self.make_file("main.py", "a = 1") + self.make_file(".coveragerc", """\ + [run] + note = I am here I am here I am here! + """) + cov = coverage.Coverage() + with self.assert_warnings(cov, [r"The '\[run] note' setting is no longer supported."]): + self.start_import_stop(cov, "main") + cov.report() def test_no_toml_installed_no_toml(self): # Can't read a toml file that doesn't exist. with without_module(coverage.tomlconfig, 'toml'): msg = "Couldn't read 'cov.toml' as a config file" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage(config_file="cov.toml") def test_no_toml_installed_explicit_toml(self): @@ -722,7 +726,7 @@ def test_no_toml_installed_explicit_toml(self): self.make_file("cov.toml", "# A toml file!") with without_module(coverage.tomlconfig, 'toml'): msg = "Can't read 'cov.toml' without TOML support" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage(config_file="cov.toml") def test_no_toml_installed_pyproject_toml(self): @@ -734,7 +738,7 @@ def test_no_toml_installed_pyproject_toml(self): """) with without_module(coverage.tomlconfig, 'toml'): msg = "Can't read 'pyproject.toml' without TOML support" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): coverage.Coverage() def test_no_toml_installed_pyproject_no_coverage(self): @@ -747,6 +751,6 @@ def test_no_toml_installed_pyproject_no_coverage(self): with without_module(coverage.tomlconfig, 'toml'): cov = coverage.Coverage() # We get default settings: - self.assertFalse(cov.config.timid) - self.assertFalse(cov.config.branch) - self.assertEqual(cov.config.data_file, ".coverage") + assert not cov.config.timid + assert not cov.config.branch + assert cov.config.data_file == ".coverage" diff --git a/tests/test_context.py b/tests/test_context.py index 137300a59..f51befae3 100644 --- a/tests/test_context.py +++ b/tests/test_context.py @@ -6,12 +6,15 @@ import inspect import os.path +import pytest + import coverage from coverage import env from coverage.context import qualname_from_frame from coverage.data import CoverageData from tests.coveragetest import CoverageTest +from tests.helpers import assert_count_equal class StaticContextTest(CoverageTest): @@ -22,14 +25,14 @@ def test_no_context(self): cov = coverage.Coverage() self.start_import_stop(cov, "main") data = cov.get_data() - self.assertCountEqual(data.measured_contexts(), [""]) + assert_count_equal(data.measured_contexts(), [""]) def test_static_context(self): self.make_file("main.py", "a = 1") cov = coverage.Coverage(context="gooey") self.start_import_stop(cov, "main") data = cov.get_data() - self.assertCountEqual(data.measured_contexts(), ["gooey"]) + assert_count_equal(data.measured_contexts(), ["gooey"]) SOURCE = """\ a = 1 @@ -64,10 +67,10 @@ def test_combining_line_contexts(self): for data in datas: combined.update(data) - self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) + assert combined.measured_contexts() == {'red', 'blue'} full_names = {os.path.basename(f): f for f in combined.measured_files()} - self.assertCountEqual(full_names, ['red.py', 'blue.py']) + assert_count_equal(full_names, ['red.py', 'blue.py']) fred = full_names['red.py'] fblue = full_names['blue.py'] @@ -75,7 +78,7 @@ def test_combining_line_contexts(self): def assert_combined_lines(filename, context, lines): # pylint: disable=cell-var-from-loop combined.set_query_context(context) - self.assertEqual(combined.lines(filename), lines) + assert combined.lines(filename) == lines assert_combined_lines(fred, 'red', self.LINES) assert_combined_lines(fred, 'blue', []) @@ -89,10 +92,10 @@ def test_combining_arc_contexts(self): for data in datas: combined.update(data) - self.assertEqual(combined.measured_contexts(), {'red', 'blue'}) + assert combined.measured_contexts() == {'red', 'blue'} full_names = {os.path.basename(f): f for f in combined.measured_files()} - self.assertCountEqual(full_names, ['red.py', 'blue.py']) + assert_count_equal(full_names, ['red.py', 'blue.py']) fred = full_names['red.py'] fblue = full_names['blue.py'] @@ -100,7 +103,7 @@ def test_combining_arc_contexts(self): def assert_combined_lines(filename, context, lines): # pylint: disable=cell-var-from-loop combined.set_query_context(context) - self.assertEqual(combined.lines(filename), lines) + assert combined.lines(filename) == lines assert_combined_lines(fred, 'red', self.LINES) assert_combined_lines(fred, 'blue', []) @@ -110,7 +113,7 @@ def assert_combined_lines(filename, context, lines): def assert_combined_arcs(filename, context, lines): # pylint: disable=cell-var-from-loop combined.set_query_context(context) - self.assertEqual(combined.arcs(filename), lines) + assert combined.arcs(filename) == lines assert_combined_arcs(fred, 'red', self.ARCS) assert_combined_arcs(fred, 'blue', []) @@ -157,13 +160,14 @@ def test_dynamic_alone(self): full_names = {os.path.basename(f): f for f in data.measured_files()} fname = full_names["two_tests.py"] - self.assertCountEqual( + assert_count_equal( data.measured_contexts(), - ["", "two_tests.test_one", "two_tests.test_two"]) + ["", "two_tests.test_one", "two_tests.test_two"] + ) def assert_context_lines(context, lines): data.set_query_context(context) - self.assertCountEqual(lines, data.lines(fname)) + assert_count_equal(lines, data.lines(fname)) assert_context_lines("", self.OUTER_LINES) assert_context_lines("two_tests.test_one", self.TEST_ONE_LINES) @@ -178,13 +182,14 @@ def test_static_and_dynamic(self): full_names = {os.path.basename(f): f for f in data.measured_files()} fname = full_names["two_tests.py"] - self.assertCountEqual( + assert_count_equal( data.measured_contexts(), - ["stat", "stat|two_tests.test_one", "stat|two_tests.test_two"]) + ["stat", "stat|two_tests.test_one", "stat|two_tests.test_two"] + ) def assert_context_lines(context, lines): data.set_query_context(context) - self.assertCountEqual(lines, data.lines(fname)) + assert_count_equal(lines, data.lines(fname)) assert_context_lines("stat", self.OUTER_LINES) assert_context_lines("stat|two_tests.test_one", self.TEST_ONE_LINES) @@ -251,40 +256,37 @@ class QualnameTest(CoverageTest): run_in_temp_dir = False def test_method(self): - self.assertEqual(Parent().meth(), "tests.test_context.Parent.meth") + assert Parent().meth() == "tests.test_context.Parent.meth" def test_inherited_method(self): - self.assertEqual(Child().meth(), "tests.test_context.Parent.meth") + assert Child().meth() == "tests.test_context.Parent.meth" def test_mi_inherited_method(self): - self.assertEqual(MultiChild().meth(), "tests.test_context.Parent.meth") + assert MultiChild().meth() == "tests.test_context.Parent.meth" def test_no_arguments(self): - self.assertEqual(no_arguments(), "tests.test_context.no_arguments") + assert no_arguments() == "tests.test_context.no_arguments" def test_plain_old_function(self): - self.assertEqual( - plain_old_function(0, 1), "tests.test_context.plain_old_function") + assert plain_old_function(0, 1) == "tests.test_context.plain_old_function" def test_fake_out(self): - self.assertEqual(fake_out(0), "tests.test_context.fake_out") + assert fake_out(0) == "tests.test_context.fake_out" def test_property(self): - self.assertEqual( - Parent().a_property, "tests.test_context.Parent.a_property") + assert Parent().a_property == "tests.test_context.Parent.a_property" def test_changeling(self): c = Child() c.meth = patch_meth - self.assertEqual(c.meth(c), "tests.test_context.patch_meth") + assert c.meth(c) == "tests.test_context.patch_meth" + @pytest.mark.skipif(not env.PY2, reason="Old-style classes are only in Python 2") def test_oldstyle(self): - if not env.PY2: - self.skipTest("Old-style classes are only in Python 2") - self.assertEqual(OldStyle().meth(), "tests.test_context.OldStyle.meth") - self.assertEqual(OldChild().meth(), "tests.test_context.OldStyle.meth") + assert OldStyle().meth() == "tests.test_context.OldStyle.meth" + assert OldChild().meth() == "tests.test_context.OldStyle.meth" def test_bug_829(self): # A class with a name like a function shouldn't confuse qualname_from_frame. class test_something(object): # pylint: disable=unused-variable - self.assertEqual(get_qualname(), None) + assert get_qualname() is None diff --git a/tests/test_coverage.py b/tests/test_coverage.py index 68eea1150..30a8edc5a 100644 --- a/tests/test_coverage.py +++ b/tests/test_coverage.py @@ -4,6 +4,8 @@ """Tests for coverage.py.""" +import pytest + import coverage from coverage import env from coverage.misc import CoverageException @@ -50,7 +52,7 @@ def test_successful_coverage(self): def test_failed_coverage(self): # If the lines are wrong, the message shows right and wrong. - with self.assertRaisesRegex(AssertionError, r"\[1, 2] != \[1]"): + with pytest.raises(AssertionError, match=r"\[1, 2] != \[1]"): self.check_coverage("""\ a = 1 b = 2 @@ -59,7 +61,7 @@ def test_failed_coverage(self): ) # If the list of lines possibilities is wrong, the msg shows right. msg = r"None of the lines choices matched \[1, 2]" - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.check_coverage("""\ a = 1 b = 2 @@ -67,7 +69,7 @@ def test_failed_coverage(self): ([1], [2]) ) # If the missing lines are wrong, the message shows right and wrong. - with self.assertRaisesRegex(AssertionError, r"'3' != '37'"): + with pytest.raises(AssertionError, match=r"'3' != '37'"): self.check_coverage("""\ a = 1 if a == 2: @@ -78,7 +80,7 @@ def test_failed_coverage(self): ) # If the missing lines possibilities are wrong, the msg shows right. msg = r"None of the missing choices matched '3'" - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.check_coverage("""\ a = 1 if a == 2: @@ -90,14 +92,14 @@ def test_failed_coverage(self): def test_exceptions_really_fail(self): # An assert in the checked code will really raise up to us. - with self.assertRaisesRegex(AssertionError, "This is bad"): + with pytest.raises(AssertionError, match="This is bad"): self.check_coverage("""\ a = 1 assert a == 99, "This is bad" """ ) # Other exceptions too. - with self.assertRaisesRegex(ZeroDivisionError, "division"): + with pytest.raises(ZeroDivisionError, match="division"): self.check_coverage("""\ a = 1 assert a == 1, "This is good" @@ -341,10 +343,8 @@ def test_del(self): """, [1,2,3,6,9], "") + @pytest.mark.skipif(env.PY3, reason="No more print statement in Python 3.") def test_print(self): - if env.PY3: # Print statement is gone in Py3k. - self.skipTest("No more print statement in Python 3.") - self.check_coverage("""\ print "hello, world!" print ("hey: %d" % @@ -484,12 +484,11 @@ def test_continue(self): """, lines=lines, missing=missing) + @pytest.mark.skipif(env.PY2, reason="Expected failure: peephole optimization of jumps to jumps") def test_strange_unexecuted_continue(self): # Peephole optimization of jumps to jumps can mean that some statements # never hit the line tracer. The behavior is different in different # versions of Python, so be careful when running this test. - if env.PY2: - self.skipTest("Expected failure: peephole optimization of jumps to jumps") self.check_coverage("""\ a = b = c = 0 for n in range(100): @@ -733,7 +732,7 @@ def test_elif(self): z = 7 assert x == 3 """, - [1,2,3,4,5,7,8], "4-7", report="7 3 4 1 45% 2->4, 4-7", + [1,2,3,4,5,7,8], "4-7", report="7 3 4 1 45% 4-7", ) self.check_coverage("""\ a = 1; b = 2; c = 3; @@ -745,7 +744,7 @@ def test_elif(self): z = 7 assert y == 5 """, - [1,2,3,4,5,7,8], "3, 7", report="7 2 4 2 64% 2->3, 3, 4->7, 7", + [1,2,3,4,5,7,8], "3, 7", report="7 2 4 2 64% 3, 7", ) self.check_coverage("""\ a = 1; b = 2; c = 3; @@ -757,7 +756,7 @@ def test_elif(self): z = 7 assert z == 7 """, - [1,2,3,4,5,7,8], "3, 5", report="7 2 4 2 64% 2->3, 3, 4->5, 5", + [1,2,3,4,5,7,8], "3, 5", report="7 2 4 2 64% 3, 5", ) def test_elif_no_else(self): @@ -769,7 +768,7 @@ def test_elif_no_else(self): y = 5 assert x == 3 """, - [1,2,3,4,5,6], "4-5", report="6 2 4 1 50% 2->4, 4-5", + [1,2,3,4,5,6], "4-5", report="6 2 4 1 50% 4-5", ) self.check_coverage("""\ a = 1; b = 2; c = 3; @@ -779,7 +778,7 @@ def test_elif_no_else(self): y = 5 assert y == 5 """, - [1,2,3,4,5,6], "3", report="6 1 4 2 70% 2->3, 3, 4->6", + [1,2,3,4,5,6], "3", report="6 1 4 2 70% 3, 4->6", ) def test_elif_bizarre(self): @@ -1844,7 +1843,7 @@ def test_not_singleton(self): coverage.Coverage() def test_old_name_and_new_name(self): - self.assertIs(coverage.coverage, coverage.Coverage) + assert coverage.coverage is coverage.Coverage class ReportingTest(CoverageTest): @@ -1857,19 +1856,19 @@ class ReportingTest(CoverageTest): def test_no_data_to_report_on_annotate(self): # Reporting with no data produces a nice message and no output # directory. - with self.assertRaisesRegex(CoverageException, "No data to report."): + with pytest.raises(CoverageException, match="No data to report."): self.command_line("annotate -d ann") self.assert_doesnt_exist("ann") def test_no_data_to_report_on_html(self): # Reporting with no data produces a nice message and no output # directory. - with self.assertRaisesRegex(CoverageException, "No data to report."): + with pytest.raises(CoverageException, match="No data to report."): self.command_line("html -d htmlcov") self.assert_doesnt_exist("htmlcov") def test_no_data_to_report_on_xml(self): # Reporting with no data produces a nice message. - with self.assertRaisesRegex(CoverageException, "No data to report."): + with pytest.raises(CoverageException, match="No data to report."): self.command_line("xml") self.assert_doesnt_exist("coverage.xml") diff --git a/tests/test_data.py b/tests/test_data.py index b3ac718e1..eac9c36fa 100644 --- a/tests/test_data.py +++ b/tests/test_data.py @@ -11,6 +11,7 @@ import threading import mock +import pytest from coverage.data import CoverageData, combine_parallel_data from coverage.data import add_data_to_hash, line_counts @@ -19,6 +20,7 @@ from coverage.misc import CoverageException from tests.coveragetest import CoverageTest +from tests.helpers import assert_count_equal LINES_1 = { @@ -77,28 +79,28 @@ class DataTestHelpers(CoverageTest): def assert_line_counts(self, covdata, counts, fullpath=False): """Check that the line_counts of `covdata` is `counts`.""" - self.assertEqual(line_counts(covdata, fullpath), counts) + assert line_counts(covdata, fullpath) == counts def assert_measured_files(self, covdata, measured): """Check that `covdata`'s measured files are `measured`.""" - self.assertCountEqual(covdata.measured_files(), measured) + assert_count_equal(covdata.measured_files(), measured) def assert_lines1_data(self, covdata): """Check that `covdata` has the data from LINES1.""" self.assert_line_counts(covdata, SUMMARY_1) self.assert_measured_files(covdata, MEASURED_FILES_1) - self.assertCountEqual(covdata.lines("a.py"), A_PY_LINES_1) - self.assertFalse(covdata.has_arcs()) + assert_count_equal(covdata.lines("a.py"), A_PY_LINES_1) + assert not covdata.has_arcs() def assert_arcs3_data(self, covdata): """Check that `covdata` has the data from ARCS3.""" self.assert_line_counts(covdata, SUMMARY_3) self.assert_measured_files(covdata, MEASURED_FILES_3) - self.assertCountEqual(covdata.lines("x.py"), X_PY_LINES_3) - self.assertCountEqual(covdata.arcs("x.py"), X_PY_ARCS_3) - self.assertCountEqual(covdata.lines("y.py"), Y_PY_LINES_3) - self.assertCountEqual(covdata.arcs("y.py"), Y_PY_ARCS_3) - self.assertTrue(covdata.has_arcs()) + assert_count_equal(covdata.lines("x.py"), X_PY_LINES_3) + assert_count_equal(covdata.arcs("x.py"), X_PY_ARCS_3) + assert_count_equal(covdata.lines("y.py"), Y_PY_LINES_3) + assert_count_equal(covdata.arcs("y.py"), Y_PY_ARCS_3) + assert covdata.has_arcs() class CoverageDataTest(DataTestHelpers, CoverageTest): @@ -108,27 +110,27 @@ class CoverageDataTest(DataTestHelpers, CoverageTest): def test_empty_data_is_false(self): covdata = CoverageData() - self.assertFalse(covdata) + assert not covdata def test_line_data_is_true(self): covdata = CoverageData() covdata.add_lines(LINES_1) - self.assertTrue(covdata) + assert covdata def test_arc_data_is_true(self): covdata = CoverageData() covdata.add_arcs(ARCS_3) - self.assertTrue(covdata) + assert covdata def test_empty_line_data_is_false(self): covdata = CoverageData() covdata.add_lines({}) - self.assertFalse(covdata) + assert not covdata def test_empty_arc_data_is_false(self): covdata = CoverageData() covdata.add_arcs({}) - self.assertFalse(covdata) + assert not covdata def test_adding_lines(self): covdata = CoverageData() @@ -157,13 +159,15 @@ def test_ok_to_add_arcs_twice(self): def test_cant_add_arcs_with_lines(self): covdata = CoverageData() covdata.add_lines(LINES_1) - with self.assertRaisesRegex(CoverageException, "Can't add arcs to existing line data"): + msg = "Can't add branch measurements to existing line data" + with pytest.raises(CoverageException, match=msg): covdata.add_arcs(ARCS_3) def test_cant_add_lines_with_arcs(self): covdata = CoverageData() covdata.add_arcs(ARCS_3) - with self.assertRaisesRegex(CoverageException, "Can't add lines to existing arc data"): + msg = "Can't add line measurements to existing branch data" + with pytest.raises(CoverageException, match=msg): covdata.add_lines(LINES_1) def test_touch_file_with_lines(self): @@ -183,34 +187,32 @@ def test_set_query_contexts(self): covdata.set_context('test_a') covdata.add_lines(LINES_1) covdata.set_query_contexts(['test_*']) - self.assertEqual(covdata.lines('a.py'), [1, 2]) + assert covdata.lines('a.py') == [1, 2] covdata.set_query_contexts(['other*']) - self.assertEqual(covdata.lines('a.py'), []) + assert covdata.lines('a.py') == [] def test_no_lines_vs_unmeasured_file(self): covdata = CoverageData() covdata.add_lines(LINES_1) covdata.touch_file('zzz.py') - self.assertEqual(covdata.lines('zzz.py'), []) - self.assertIsNone(covdata.lines('no_such_file.py')) + assert covdata.lines('zzz.py') == [] + assert covdata.lines('no_such_file.py') is None def test_lines_with_contexts(self): covdata = CoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) - self.assertEqual(covdata.lines('a.py'), [1, 2]) + assert covdata.lines('a.py') == [1, 2] covdata.set_query_contexts(['test*']) - self.assertEqual(covdata.lines('a.py'), [1, 2]) + assert covdata.lines('a.py') == [1, 2] covdata.set_query_contexts(['other*']) - self.assertEqual(covdata.lines('a.py'), []) + assert covdata.lines('a.py') == [] def test_contexts_by_lineno_with_lines(self): covdata = CoverageData() covdata.set_context('test_a') covdata.add_lines(LINES_1) - self.assertDictEqual( - covdata.contexts_by_lineno('a.py'), - {1: ['test_a'], 2: ['test_a']}) + assert covdata.contexts_by_lineno('a.py') == {1: ['test_a'], 2: ['test_a']} def test_no_duplicate_lines(self): covdata = CoverageData() @@ -218,7 +220,7 @@ def test_no_duplicate_lines(self): covdata.add_lines(LINES_1) covdata.set_context("context2") covdata.add_lines(LINES_1) - self.assertEqual(covdata.lines('a.py'), A_PY_LINES_1) + assert covdata.lines('a.py') == A_PY_LINES_1 def test_no_duplicate_arcs(self): covdata = CoverageData() @@ -226,39 +228,37 @@ def test_no_duplicate_arcs(self): covdata.add_arcs(ARCS_3) covdata.set_context("context2") covdata.add_arcs(ARCS_3) - self.assertEqual(covdata.arcs('x.py'), X_PY_ARCS_3) + assert covdata.arcs('x.py') == X_PY_ARCS_3 def test_no_arcs_vs_unmeasured_file(self): covdata = CoverageData() covdata.add_arcs(ARCS_3) covdata.touch_file('zzz.py') - self.assertEqual(covdata.lines('zzz.py'), []) - self.assertIsNone(covdata.lines('no_such_file.py')) - self.assertEqual(covdata.arcs('zzz.py'), []) - self.assertIsNone(covdata.arcs('no_such_file.py')) + assert covdata.lines('zzz.py') == [] + assert covdata.lines('no_such_file.py') is None + assert covdata.arcs('zzz.py') == [] + assert covdata.arcs('no_such_file.py') is None def test_arcs_with_contexts(self): covdata = CoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) - self.assertEqual(covdata.arcs('x.py'), [(-1, 1), (1, 2), (2, 3), (3, -1)]) + assert covdata.arcs('x.py') == [(-1, 1), (1, 2), (2, 3), (3, -1)] covdata.set_query_contexts(['test*']) - self.assertEqual(covdata.arcs('x.py'), [(-1, 1), (1, 2), (2, 3), (3, -1)]) + assert covdata.arcs('x.py') == [(-1, 1), (1, 2), (2, 3), (3, -1)] covdata.set_query_contexts(['other*']) - self.assertEqual(covdata.arcs('x.py'), []) + assert covdata.arcs('x.py') == [] def test_contexts_by_lineno_with_arcs(self): covdata = CoverageData() covdata.set_context('test_x') covdata.add_arcs(ARCS_3) - self.assertDictEqual( - covdata.contexts_by_lineno('x.py'), - {-1: ['test_x'], 1: ['test_x'], 2: ['test_x'], 3: ['test_x']}) + expected = {-1: ['test_x'], 1: ['test_x'], 2: ['test_x'], 3: ['test_x']} + assert expected == covdata.contexts_by_lineno('x.py') def test_contexts_by_lineno_with_unknown_file(self): covdata = CoverageData() - self.assertDictEqual( - covdata.contexts_by_lineno('xyz.py'), {}) + assert covdata.contexts_by_lineno('xyz.py') == {} def test_file_tracer_name(self): covdata = CoverageData() @@ -268,18 +268,18 @@ def test_file_tracer_name(self): "main.py": dict.fromkeys([20]), }) covdata.add_file_tracers({"p1.foo": "p1.plugin", "p2.html": "p2.plugin"}) - self.assertEqual(covdata.file_tracer("p1.foo"), "p1.plugin") - self.assertEqual(covdata.file_tracer("main.py"), "") - self.assertIsNone(covdata.file_tracer("p3.not_here")) + assert covdata.file_tracer("p1.foo") == "p1.plugin" + assert covdata.file_tracer("main.py") == "" + assert covdata.file_tracer("p3.not_here") is None def test_cant_file_tracer_unmeasured_files(self): covdata = CoverageData() msg = "Can't add file tracer data for unmeasured file 'p1.foo'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin"}) covdata.add_lines({"p2.html": dict.fromkeys([10, 11, 12])}) - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin"}) def test_cant_change_file_tracer_name(self): @@ -288,7 +288,7 @@ def test_cant_change_file_tracer_name(self): covdata.add_file_tracers({"p1.foo": "p1.plugin"}) msg = "Conflicting file tracer name for 'p1.foo': u?'p1.plugin' vs u?'p1.plugin.foo'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata.add_file_tracers({"p1.foo": "p1.plugin.foo"}) def test_update_lines(self): @@ -326,10 +326,10 @@ def test_update_cant_mix_lines_and_arcs(self): covdata2 = CoverageData(suffix='2') covdata2.add_arcs(ARCS_3) - with self.assertRaisesRegex(CoverageException, "Can't combine arc data with line data"): + with pytest.raises(CoverageException, match="Can't combine arc data with line data"): covdata1.update(covdata2) - with self.assertRaisesRegex(CoverageException, "Can't combine line data with arc data"): + with pytest.raises(CoverageException, match="Can't combine line data with arc data"): covdata2.update(covdata1) def test_update_file_tracers(self): @@ -360,10 +360,10 @@ def test_update_file_tracers(self): covdata3 = CoverageData(suffix='3') covdata3.update(covdata1) covdata3.update(covdata2) - self.assertEqual(covdata3.file_tracer("p1.html"), "html.plugin") - self.assertEqual(covdata3.file_tracer("p2.html"), "html.plugin2") - self.assertEqual(covdata3.file_tracer("p3.foo"), "foo_plugin") - self.assertEqual(covdata3.file_tracer("main.py"), "") + assert covdata3.file_tracer("p1.html") == "html.plugin" + assert covdata3.file_tracer("p2.html") == "html.plugin2" + assert covdata3.file_tracer("p3.foo") == "foo_plugin" + assert covdata3.file_tracer("main.py") == "" def test_update_conflicting_file_tracers(self): covdata1 = CoverageData(suffix='1') @@ -375,11 +375,11 @@ def test_update_conflicting_file_tracers(self): covdata2.add_file_tracers({"p1.html": "html.other_plugin"}) msg = "Conflicting file tracer name for 'p1.html': u?'html.plugin' vs u?'html.other_plugin'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata1.update(covdata2) msg = "Conflicting file tracer name for 'p1.html': u?'html.other_plugin' vs u?'html.plugin'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata2.update(covdata1) def test_update_file_tracer_vs_no_file_tracer(self): @@ -391,11 +391,11 @@ def test_update_file_tracer_vs_no_file_tracer(self): covdata2.add_lines({"p1.html": dict.fromkeys([1, 2, 3])}) msg = "Conflicting file tracer name for 'p1.html': u?'html.plugin' vs u?''" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata1.update(covdata2) msg = "Conflicting file tracer name for 'p1.html': u?'' vs u?'html.plugin'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata2.update(covdata1) def test_update_lines_empty(self): @@ -418,7 +418,7 @@ def test_asking_isnt_measuring(self): # Asking about an unmeasured file shouldn't make it seem measured. covdata = CoverageData() self.assert_measured_files(covdata, []) - self.assertEqual(covdata.arcs("missing.py"), None) + assert covdata.arcs("missing.py") is None self.assert_measured_files(covdata, []) def test_add_to_hash_with_lines(self): @@ -426,10 +426,10 @@ def test_add_to_hash_with_lines(self): covdata.add_lines(LINES_1) hasher = mock.Mock() add_data_to_hash(covdata, "a.py", hasher) - self.assertEqual(hasher.method_calls, [ + assert hasher.method_calls == [ mock.call.update([1, 2]), # lines mock.call.update(""), # file_tracer name - ]) + ] def test_add_to_hash_with_arcs(self): covdata = CoverageData() @@ -437,10 +437,10 @@ def test_add_to_hash_with_arcs(self): covdata.add_file_tracers({"y.py": "hologram_plugin"}) hasher = mock.Mock() add_data_to_hash(covdata, "y.py", hasher) - self.assertEqual(hasher.method_calls, [ + assert hasher.method_calls == [ mock.call.update([(-1, 17), (17, 23), (23, -1)]), # arcs mock.call.update("hologram_plugin"), # file_tracer name - ]) + ] def test_add_to_lines_hash_with_missing_file(self): # https://github.com/nedbat/coveragepy/issues/403 @@ -448,10 +448,10 @@ def test_add_to_lines_hash_with_missing_file(self): covdata.add_lines(LINES_1) hasher = mock.Mock() add_data_to_hash(covdata, "missing.py", hasher) - self.assertEqual(hasher.method_calls, [ + assert hasher.method_calls == [ mock.call.update([]), mock.call.update(None), - ]) + ] def test_add_to_arcs_hash_with_missing_file(self): # https://github.com/nedbat/coveragepy/issues/403 @@ -460,22 +460,22 @@ def test_add_to_arcs_hash_with_missing_file(self): covdata.add_file_tracers({"y.py": "hologram_plugin"}) hasher = mock.Mock() add_data_to_hash(covdata, "missing.py", hasher) - self.assertEqual(hasher.method_calls, [ + assert hasher.method_calls == [ mock.call.update([]), mock.call.update(None), - ]) + ] def test_empty_lines_are_still_lines(self): covdata = CoverageData() covdata.add_lines({}) covdata.touch_file("abc.py") - self.assertFalse(covdata.has_arcs()) + assert not covdata.has_arcs() def test_empty_arcs_are_still_arcs(self): covdata = CoverageData() covdata.add_arcs({}) covdata.touch_file("abc.py") - self.assertTrue(covdata.has_arcs()) + assert covdata.has_arcs() def test_read_and_write_are_opposites(self): covdata1 = CoverageData() @@ -527,34 +527,34 @@ def test_read_errors(self): msg = r"Couldn't .* '.*[/\\]{0}': \S+" self.make_file("xyzzy.dat", "xyzzy") - with self.assertRaisesRegex(CoverageException, msg.format("xyzzy.dat")): + with pytest.raises(CoverageException, match=msg.format("xyzzy.dat")): covdata = CoverageData("xyzzy.dat") covdata.read() - self.assertFalse(covdata) + assert not covdata self.make_file("empty.dat", "") - with self.assertRaisesRegex(CoverageException, msg.format("empty.dat")): + with pytest.raises(CoverageException, match=msg.format("empty.dat")): covdata = CoverageData("empty.dat") covdata.read() - self.assertFalse(covdata) + assert not covdata def test_read_sql_errors(self): with sqlite3.connect("wrong_schema.db") as con: con.execute("create table coverage_schema (version integer)") con.execute("insert into coverage_schema (version) values (99)") msg = r"Couldn't .* '.*[/\\]{}': wrong schema: 99 instead of \d+".format("wrong_schema.db") - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata = CoverageData("wrong_schema.db") covdata.read() - self.assertFalse(covdata) + assert not covdata with sqlite3.connect("no_schema.db") as con: con.execute("create table foobar (baz text)") msg = r"Couldn't .* '.*[/\\]{}': \S+".format("no_schema.db") - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata = CoverageData("no_schema.db") covdata.read() - self.assertFalse(covdata) + assert not covdata class CoverageDataFilesTest(DataTestHelpers, CoverageTest): @@ -589,11 +589,11 @@ def test_debug_output_with_debug_option(self): covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) - self.assertRegex( - debug.get_output(), + assert re.search( r"^Erasing data file '.*\.coverage'\n" r"Creating data file '.*\.coverage'\n" - r"Opening data file '.*\.coverage'\n$" + r"Opening data file '.*\.coverage'\n$", + debug.get_output() ) def test_debug_output_without_debug_option(self): @@ -608,7 +608,7 @@ def test_debug_output_without_debug_option(self): covdata2.read() self.assert_line_counts(covdata2, SUMMARY_1) - self.assertEqual(debug.get_output(), "") + assert debug.get_output() == "" def test_explicit_suffix(self): self.assert_doesnt_exist(".coverage.SUFFIX") @@ -627,7 +627,7 @@ def test_true_suffix(self): covdata1.write() self.assert_doesnt_exist(".coverage") data_files1 = glob.glob(".coverage.*") - self.assertEqual(len(data_files1), 1) + assert len(data_files1) == 1 # Another suffix=True will choose a different name. covdata2 = CoverageData(suffix=True) @@ -635,10 +635,10 @@ def test_true_suffix(self): covdata2.write() self.assert_doesnt_exist(".coverage") data_files2 = glob.glob(".coverage.*") - self.assertEqual(len(data_files2), 2) + assert len(data_files2) == 2 # In addition to being different, the suffixes have the pid in them. - self.assertTrue(all(str(os.getpid()) in fn for fn in data_files2)) + assert all(str(os.getpid()) in fn for fn in data_files2) def test_combining(self): self.assert_file_count(".coverage.*", 0) @@ -718,7 +718,7 @@ def test_combining_with_aliases(self): self.assert_line_counts(covdata3, {apy: 4, sub_bpy: 2, template_html: 1}, fullpath=True) self.assert_measured_files(covdata3, [apy, sub_bpy, template_html]) - self.assertEqual(covdata3.file_tracer(template_html), 'html.plugin') + assert covdata3.file_tracer(template_html) == 'html.plugin' def test_combining_from_different_directories(self): os.makedirs('cov1') @@ -778,7 +778,7 @@ def test_combining_from_files(self): def test_combining_from_nonexistent_directories(self): covdata = CoverageData() msg = "Couldn't combine from non-existent path 'xyzzy'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): combine_parallel_data(covdata, data_paths=['xyzzy']) def test_interleaved_erasing_bug716(self): @@ -817,5 +817,5 @@ def test_misfed_serialization(self): re.escape(repr(bad_data[:40])), len(bad_data), ) - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): covdata.loads(bad_data) diff --git a/tests/test_debug.py b/tests/test_debug.py index 228e33b0c..55001c96a 100644 --- a/tests/test_debug.py +++ b/tests/test_debug.py @@ -4,14 +4,15 @@ """Tests of coverage/debug.py""" import os +import re import pytest import coverage +from coverage import env from coverage.backward import StringIO from coverage.debug import filter_text, info_formatter, info_header, short_id, short_stack from coverage.debug import clipped_repr -from coverage.env import C_TRACER from tests.coveragetest import CoverageTest from tests.helpers import re_line, re_lines @@ -38,7 +39,7 @@ def test_info_formatter(self): ' jkl', ' nothing: -none-', ] - self.assertEqual(expected, lines) + assert expected == lines def test_info_formatter_with_generator(self): lines = list(info_formatter(('info%d' % i, i) for i in range(3))) @@ -47,10 +48,10 @@ def test_info_formatter_with_generator(self): ' info1: 1', ' info2: 2', ] - self.assertEqual(expected, lines) + assert expected == lines def test_too_long_label(self): - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): list(info_formatter([('this label is way too long and will not fit', 23)])) @@ -118,20 +119,20 @@ def test_debug_no_trace(self): out_lines = self.f1_debug_output([]) # We should have no output at all. - self.assertFalse(out_lines) + assert not out_lines def test_debug_trace(self): out_lines = self.f1_debug_output(["trace"]) # We should have a line like "Tracing 'f1.py'" - self.assertIn("Tracing 'f1.py'", out_lines) + assert "Tracing 'f1.py'" in out_lines # We should have lines like "Not tracing 'collector.py'..." coverage_lines = re_lines( out_lines, r"^Not tracing .*: is part of coverage.py$" ) - self.assertTrue(coverage_lines) + assert coverage_lines def test_debug_trace_pid(self): out_lines = self.f1_debug_output(["trace", "pid"]) @@ -139,11 +140,11 @@ def test_debug_trace_pid(self): # Now our lines are always prefixed with the process id. pid_prefix = r"^%5d\.[0-9a-f]{4}: " % os.getpid() pid_lines = re_lines(out_lines, pid_prefix) - self.assertEqual(pid_lines, out_lines) + assert pid_lines == out_lines # We still have some tracing, and some not tracing. - self.assertTrue(re_lines(out_lines, pid_prefix + "Tracing ")) - self.assertTrue(re_lines(out_lines, pid_prefix + "Not tracing ")) + assert re_lines(out_lines, pid_prefix + "Tracing ") + assert re_lines(out_lines, pid_prefix + "Not tracing ") def test_debug_callers(self): out_lines = self.f1_debug_output(["pid", "dataop", "dataio", "callers"]) @@ -153,15 +154,15 @@ def test_debug_callers(self): real_messages = re_lines(out_lines, r":\d+", match=False).splitlines() frame_pattern = r"\s+f1_debug_output : .*tests[/\\]test_debug.py:\d+$" frames = re_lines(out_lines, frame_pattern).splitlines() - self.assertEqual(len(real_messages), len(frames)) + assert len(real_messages) == len(frames) last_line = out_lines.splitlines()[-1] # The details of what to expect on the stack are empirical, and can change # as the code changes. This test is here to ensure that the debug code # continues working. It's ok to adjust these details over time. - self.assertRegex(real_messages[-1], r"^\s*\d+\.\w{4}: Adding file tracers: 0 files") - self.assertRegex(last_line, r"\s+add_file_tracers : .*coverage[/\\]sqldata.py:\d+$") + assert re.search(r"^\s*\d+\.\w{4}: Adding file tracers: 0 files", real_messages[-1]) + assert re.search(r"\s+add_file_tracers : .*coverage[/\\]sqldata.py:\d+$", last_line) def test_debug_config(self): out_lines = self.f1_debug_output(["config"]) @@ -175,11 +176,8 @@ def test_debug_config(self): """.split() for label in labels: label_pat = r"^\s*%s: " % label - self.assertEqual( - len(re_lines(out_lines, label_pat).splitlines()), - 1, - msg="Incorrect lines for %r" % label, - ) + msg = "Incorrect lines for %r" % label + assert 1 == len(re_lines(out_lines, label_pat).splitlines()), msg def test_debug_sys(self): out_lines = self.f1_debug_output(["sys"]) @@ -191,20 +189,17 @@ def test_debug_sys(self): """.split() for label in labels: label_pat = r"^\s*%s: " % label - self.assertEqual( - len(re_lines(out_lines, label_pat).splitlines()), - 1, - msg="Incorrect lines for %r" % label, - ) + msg = "Incorrect lines for %r" % label + assert 1 == len(re_lines(out_lines, label_pat).splitlines()), msg def test_debug_sys_ctracer(self): out_lines = self.f1_debug_output(["sys"]) tracer_line = re_line(out_lines, r"CTracer:").strip() - if C_TRACER: + if env.C_TRACER: expected = "CTracer: available" else: expected = "CTracer: unavailable" - self.assertEqual(expected, tracer_line) + assert expected == tracer_line def f_one(*args, **kwargs): @@ -227,15 +222,15 @@ class ShortStackTest(CoverageTest): def test_short_stack(self): stack = f_one().splitlines() - self.assertGreater(len(stack), 10) - self.assertIn("f_three", stack[-1]) - self.assertIn("f_two", stack[-2]) - self.assertIn("f_one", stack[-3]) + assert len(stack) > 10 + assert "f_three" in stack[-1] + assert "f_two" in stack[-2] + assert "f_one" in stack[-3] def test_short_stack_limit(self): stack = f_one(limit=5).splitlines() - self.assertEqual(len(stack), 5) + assert len(stack) == 5 def test_short_stack_skip(self): stack = f_one(skip=1).splitlines() - self.assertIn("f_two", stack[-1]) + assert "f_two" in stack[-1] diff --git a/tests/test_execfile.py b/tests/test_execfile.py index 5a718aaef..3cdd1ed9b 100644 --- a/tests/test_execfile.py +++ b/tests/test_execfile.py @@ -11,6 +11,8 @@ import re import sys +import pytest + from coverage import env from coverage.backward import binary_bytes from coverage.execfile import run_python_file, run_python_module @@ -30,27 +32,26 @@ def test_run_python_file(self): mod_globs = json.loads(self.stdout()) # The file should think it is __main__ - self.assertEqual(mod_globs['__name__'], "__main__") + assert mod_globs['__name__'] == "__main__" # It should seem to come from a file named try_execfile.py dunder_file = os.path.basename(mod_globs['__file__']) - self.assertEqual(dunder_file, "try_execfile.py") + assert dunder_file == "try_execfile.py" # It should have its correct module data. - self.assertEqual(mod_globs['__doc__'].splitlines()[0], - "Test file for run_python_file.") - self.assertEqual(mod_globs['DATA'], "xyzzy") - self.assertEqual(mod_globs['FN_VAL'], "my_fn('fooey')") + assert mod_globs['__doc__'].splitlines()[0] == "Test file for run_python_file." + assert mod_globs['DATA'] == "xyzzy" + assert mod_globs['FN_VAL'] == "my_fn('fooey')" # It must be self-importable as __main__. - self.assertEqual(mod_globs['__main__.DATA'], "xyzzy") + assert mod_globs['__main__.DATA'] == "xyzzy" # Argv should have the proper values. - self.assertEqual(mod_globs['argv0'], TRY_EXECFILE) - self.assertEqual(mod_globs['argv1-n'], ["arg1", "arg2"]) + assert mod_globs['argv0'] == TRY_EXECFILE + assert mod_globs['argv1-n'] == ["arg1", "arg2"] # __builtins__ should have the right values, like open(). - self.assertEqual(mod_globs['__builtins__.has_open'], True) + assert mod_globs['__builtins__.has_open'] is True def test_no_extra_file(self): # Make sure that running a file doesn't create an extra compiled file. @@ -58,9 +59,9 @@ def test_no_extra_file(self): desc = "a non-.py file!" """) - self.assertEqual(os.listdir("."), ["xxx"]) + assert os.listdir(".") == ["xxx"] run_python_file(["xxx"]) - self.assertEqual(os.listdir("."), ["xxx"]) + assert os.listdir(".") == ["xxx"] def test_universal_newlines(self): # Make sure we can read any sort of line ending. @@ -69,7 +70,7 @@ def test_universal_newlines(self): with open('nl.py', 'wb') as fpy: fpy.write(nl.join(pylines).encode('utf-8')) run_python_file(['nl.py']) - self.assertEqual(self.stdout(), "Hello, world!\n"*3) + assert self.stdout() == "Hello, world!\n"*3 def test_missing_final_newline(self): # Make sure we can deal with a Python file with no final newline. @@ -80,14 +81,14 @@ def test_missing_final_newline(self): #""") with open("abrupt.py") as f: abrupt = f.read() - self.assertEqual(abrupt[-1], '#') + assert abrupt[-1] == '#' run_python_file(["abrupt.py"]) - self.assertEqual(self.stdout(), "a is 1\n") + assert self.stdout() == "a is 1\n" def test_no_such_file(self): path = python_reported_file('xyzzy.py') msg = re.escape("No file to run: '{}'".format(path)) - with self.assertRaisesRegex(NoSource, msg): + with pytest.raises(NoSource, match=msg): run_python_file(["xyzzy.py"]) def test_directory_with_main(self): @@ -95,11 +96,11 @@ def test_directory_with_main(self): print("I am __main__") """) run_python_file(["with_main"]) - self.assertEqual(self.stdout(), "I am __main__\n") + assert self.stdout() == "I am __main__\n" def test_directory_without_main(self): self.make_file("without_main/__init__.py", "") - with self.assertRaisesRegex(NoSource, "Can't find '__main__' module in 'without_main'"): + with pytest.raises(NoSource, match="Can't find '__main__' module in 'without_main'"): run_python_file(["without_main"]) @@ -109,7 +110,7 @@ class RunPycFileTest(CoverageTest): def make_pyc(self): # pylint: disable=inconsistent-return-statements """Create a .pyc file, and return the path to it.""" if env.JYTHON: - self.skipTest("Can't make .pyc files on Jython") + pytest.skip("Can't make .pyc files on Jython") self.make_file("compiled.py", """\ def doit(): @@ -134,15 +135,15 @@ def doit(): def test_running_pyc(self): pycfile = self.make_pyc() run_python_file([pycfile]) - self.assertEqual(self.stdout(), "I am here!\n") + assert self.stdout() == "I am here!\n" def test_running_pyo(self): pycfile = self.make_pyc() pyofile = re.sub(r"[.]pyc$", ".pyo", pycfile) - self.assertNotEqual(pycfile, pyofile) + assert pycfile != pyofile os.rename(pycfile, pyofile) run_python_file([pyofile]) - self.assertEqual(self.stdout(), "I am here!\n") + assert self.stdout() == "I am here!\n" def test_running_pyc_from_wrong_python(self): pycfile = self.make_pyc() @@ -152,7 +153,7 @@ def test_running_pyc_from_wrong_python(self): fpyc.seek(0) fpyc.write(binary_bytes([0x2a, 0xeb, 0x0d, 0x0a])) - with self.assertRaisesRegex(NoCode, "Bad magic number in .pyc file"): + with pytest.raises(NoCode, match="Bad magic number in .pyc file"): run_python_file([pycfile]) # In some environments, the pycfile persists and pollutes another test. @@ -161,7 +162,7 @@ def test_running_pyc_from_wrong_python(self): def test_no_such_pyc_file(self): path = python_reported_file('xyzzy.pyc') msg = re.escape("No file to run: '{}'".format(path)) - with self.assertRaisesRegex(NoCode, msg): + with pytest.raises(NoCode, match=msg): run_python_file(["xyzzy.pyc"]) def test_running_py_from_binary(self): @@ -181,7 +182,7 @@ def test_running_py_from_binary(self): r"source code string cannot contain null bytes" # for py3 r")" ) - with self.assertRaisesRegex(Exception, msg): + with pytest.raises(Exception, match=msg): run_python_file([bf]) @@ -192,42 +193,48 @@ class RunModuleTest(UsingModulesMixin, CoverageTest): def test_runmod1(self): run_python_module(["runmod1", "hello"]) - self.assertEqual(self.stderr(), "") - self.assertEqual(self.stdout(), "runmod1: passed hello\n") + out, err = self.stdouterr() + assert out == "runmod1: passed hello\n" + assert err == "" def test_runmod2(self): run_python_module(["pkg1.runmod2", "hello"]) - self.assertEqual(self.stderr(), "") - self.assertEqual(self.stdout(), "pkg1.__init__: pkg1\nrunmod2: passed hello\n") + out, err = self.stdouterr() + assert out == "pkg1.__init__: pkg1\nrunmod2: passed hello\n" + assert err == "" def test_runmod3(self): run_python_module(["pkg1.sub.runmod3", "hello"]) - self.assertEqual(self.stderr(), "") - self.assertEqual(self.stdout(), "pkg1.__init__: pkg1\nrunmod3: passed hello\n") + out, err = self.stdouterr() + assert out == "pkg1.__init__: pkg1\nrunmod3: passed hello\n" + assert err == "" def test_pkg1_main(self): run_python_module(["pkg1", "hello"]) - self.assertEqual(self.stderr(), "") - self.assertEqual(self.stdout(), "pkg1.__init__: pkg1\npkg1.__main__: passed hello\n") + out, err = self.stdouterr() + assert out == "pkg1.__init__: pkg1\npkg1.__main__: passed hello\n" + assert err == "" def test_pkg1_sub_main(self): run_python_module(["pkg1.sub", "hello"]) - self.assertEqual(self.stderr(), "") - self.assertEqual(self.stdout(), "pkg1.__init__: pkg1\npkg1.sub.__main__: passed hello\n") + out, err = self.stdouterr() + assert out == "pkg1.__init__: pkg1\npkg1.sub.__main__: passed hello\n" + assert err == "" def test_pkg1_init(self): run_python_module(["pkg1.__init__", "wut?"]) - self.assertEqual(self.stderr(), "") - self.assertEqual(self.stdout(), "pkg1.__init__: pkg1\npkg1.__init__: __main__\n") + out, err = self.stdouterr() + assert out == "pkg1.__init__: pkg1\npkg1.__init__: __main__\n" + assert err == "" def test_no_such_module(self): - with self.assertRaisesRegex(NoSource, "No module named '?i_dont_exist'?"): + with pytest.raises(NoSource, match="No module named '?i_dont_exist'?"): run_python_module(["i_dont_exist"]) - with self.assertRaisesRegex(NoSource, "No module named '?i'?"): + with pytest.raises(NoSource, match="No module named '?i'?"): run_python_module(["i.dont_exist"]) - with self.assertRaisesRegex(NoSource, "No module named '?i'?"): + with pytest.raises(NoSource, match="No module named '?i'?"): run_python_module(["i.dont.exist"]) def test_no_main(self): - with self.assertRaises(NoSource): + with pytest.raises(NoSource): run_python_module(["pkg2", "hi"]) diff --git a/tests/test_filereporter.py b/tests/test_filereporter.py index eb1e91e88..d928eea40 100644 --- a/tests/test_filereporter.py +++ b/tests/test_filereporter.py @@ -28,23 +28,23 @@ def test_filenames(self): acu = PythonFileReporter("aa/afile.py") bcu = PythonFileReporter("aa/bb/bfile.py") ccu = PythonFileReporter("aa/bb/cc/cfile.py") - self.assertEqual(acu.relative_filename(), "aa/afile.py") - self.assertEqual(bcu.relative_filename(), "aa/bb/bfile.py") - self.assertEqual(ccu.relative_filename(), "aa/bb/cc/cfile.py") - self.assertEqual(acu.source(), "# afile.py\n") - self.assertEqual(bcu.source(), "# bfile.py\n") - self.assertEqual(ccu.source(), "# cfile.py\n") + assert acu.relative_filename() == "aa/afile.py" + assert bcu.relative_filename() == "aa/bb/bfile.py" + assert ccu.relative_filename() == "aa/bb/cc/cfile.py" + assert acu.source() == "# afile.py\n" + assert bcu.source() == "# bfile.py\n" + assert ccu.source() == "# cfile.py\n" def test_odd_filenames(self): acu = PythonFileReporter("aa/afile.odd.py") bcu = PythonFileReporter("aa/bb/bfile.odd.py") b2cu = PythonFileReporter("aa/bb.odd/bfile.py") - self.assertEqual(acu.relative_filename(), "aa/afile.odd.py") - self.assertEqual(bcu.relative_filename(), "aa/bb/bfile.odd.py") - self.assertEqual(b2cu.relative_filename(), "aa/bb.odd/bfile.py") - self.assertEqual(acu.source(), "# afile.odd.py\n") - self.assertEqual(bcu.source(), "# bfile.odd.py\n") - self.assertEqual(b2cu.source(), "# bfile.py\n") + assert acu.relative_filename() == "aa/afile.odd.py" + assert bcu.relative_filename() == "aa/bb/bfile.odd.py" + assert b2cu.relative_filename() == "aa/bb.odd/bfile.py" + assert acu.source() == "# afile.odd.py\n" + assert bcu.source() == "# bfile.odd.py\n" + assert b2cu.source() == "# bfile.py\n" def test_modules(self): import aa @@ -54,12 +54,12 @@ def test_modules(self): acu = PythonFileReporter(aa) bcu = PythonFileReporter(aa.bb) ccu = PythonFileReporter(aa.bb.cc) - self.assertEqual(acu.relative_filename(), native("aa/__init__.py")) - self.assertEqual(bcu.relative_filename(), native("aa/bb/__init__.py")) - self.assertEqual(ccu.relative_filename(), native("aa/bb/cc/__init__.py")) - self.assertEqual(acu.source(), "# aa\n") - self.assertEqual(bcu.source(), "# bb\n") - self.assertEqual(ccu.source(), "") # yes, empty + assert acu.relative_filename() == native("aa/__init__.py") + assert bcu.relative_filename() == native("aa/bb/__init__.py") + assert ccu.relative_filename() == native("aa/bb/cc/__init__.py") + assert acu.source() == "# aa\n" + assert bcu.source() == "# bb\n" + assert ccu.source() == "" # yes, empty def test_module_files(self): import aa.afile @@ -69,12 +69,12 @@ def test_module_files(self): acu = PythonFileReporter(aa.afile) bcu = PythonFileReporter(aa.bb.bfile) ccu = PythonFileReporter(aa.bb.cc.cfile) - self.assertEqual(acu.relative_filename(), native("aa/afile.py")) - self.assertEqual(bcu.relative_filename(), native("aa/bb/bfile.py")) - self.assertEqual(ccu.relative_filename(), native("aa/bb/cc/cfile.py")) - self.assertEqual(acu.source(), "# afile.py\n") - self.assertEqual(bcu.source(), "# bfile.py\n") - self.assertEqual(ccu.source(), "# cfile.py\n") + assert acu.relative_filename() == native("aa/afile.py") + assert bcu.relative_filename() == native("aa/bb/bfile.py") + assert ccu.relative_filename() == native("aa/bb/cc/cfile.py") + assert acu.source() == "# afile.py\n" + assert bcu.source() == "# bfile.py\n" + assert ccu.source() == "# cfile.py\n" def test_comparison(self): acu = FileReporter("aa/afile.py") @@ -100,5 +100,5 @@ def test_egg(self): ecu = PythonFileReporter(egg1) eecu = PythonFileReporter(egg1.egg1) - self.assertEqual(ecu.source(), u"") - self.assertIn(u"# My egg file!", eecu.source().splitlines()) + assert ecu.source() == u"" + assert u"# My egg file!" in eecu.source().splitlines() diff --git a/tests/test_files.py b/tests/test_files.py index 84e25f107..6040b8898 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -30,10 +30,10 @@ def abs_path(self, p): def test_simple(self): self.make_file("hello.py") files.set_relative_directory() - self.assertEqual(files.relative_filename(u"hello.py"), u"hello.py") + assert files.relative_filename(u"hello.py") == u"hello.py" a = self.abs_path("hello.py") - self.assertNotEqual(a, "hello.py") - self.assertEqual(files.relative_filename(a), "hello.py") + assert a != "hello.py" + assert files.relative_filename(a) == "hello.py" def test_peer_directories(self): self.make_file("sub/proj1/file1.py") @@ -43,8 +43,8 @@ def test_peer_directories(self): d = os.path.normpath("sub/proj1") self.chdir(d) files.set_relative_directory() - self.assertEqual(files.relative_filename(a1), "file1.py") - self.assertEqual(files.relative_filename(a2), a2) + assert files.relative_filename(a1) == "file1.py" + assert files.relative_filename(a2) == a2 def test_filepath_contains_absolute_prefix_twice(self): # https://github.com/nedbat/coveragepy/issues/194 @@ -55,7 +55,7 @@ def test_filepath_contains_absolute_prefix_twice(self): d = abs_file(os.curdir) trick = os.path.splitdrive(d)[1].lstrip(os.path.sep) rel = os.path.join('sub', trick, 'file1.py') - self.assertEqual(files.relative_filename(abs_file(rel)), rel) + assert files.relative_filename(abs_file(rel)) == rel def test_canonical_filename_ensure_cache_hit(self): self.make_file("sub/proj1/file1.py") @@ -63,12 +63,10 @@ def test_canonical_filename_ensure_cache_hit(self): self.chdir(d) files.set_relative_directory() canonical_path = files.canonical_filename('sub/proj1/file1.py') - self.assertEqual(canonical_path, self.abs_path('file1.py')) + assert canonical_path == self.abs_path('file1.py') # After the filename has been converted, it should be in the cache. - self.assertIn('sub/proj1/file1.py', files.CANONICAL_FILENAME_CACHE) - self.assertEqual( - files.canonical_filename('sub/proj1/file1.py'), - self.abs_path('file1.py')) + assert 'sub/proj1/file1.py' in files.CANONICAL_FILENAME_CACHE + assert files.canonical_filename('sub/proj1/file1.py') == self.abs_path('file1.py') @pytest.mark.parametrize("original, flat", [ @@ -149,10 +147,8 @@ def setUp(self): def assertMatches(self, matcher, filepath, matches): """The `matcher` should agree with `matches` about `filepath`.""" canonical = files.canonical_filename(filepath) - self.assertEqual( - matcher.match(canonical), matches, - "File %s should have matched as %s" % (filepath, matches) - ) + msg = "File %s should have matched as %s" % (filepath, matches) + assert matches == matcher.match(canonical), msg def test_tree_matcher(self): matches_to_try = [ @@ -167,7 +163,7 @@ def test_tree_matcher(self): files.canonical_filename("sub3/file4.py"), ] tm = TreeMatcher(trees) - self.assertEqual(tm.info(), trees) + assert tm.info() == trees for filepath, matches in matches_to_try: self.assertMatches(tm, filepath, matches) @@ -190,16 +186,9 @@ def test_module_matcher(self): ] modules = ['test', 'py.test', 'mymain'] mm = ModuleMatcher(modules) - self.assertEqual( - mm.info(), - modules - ) + assert mm.info() == modules for modulename, matches in matches_to_try: - self.assertEqual( - mm.match(modulename), - matches, - modulename, - ) + assert mm.match(modulename) == matches, modulename def test_fnmatch_matcher(self): matches_to_try = [ @@ -210,7 +199,7 @@ def test_fnmatch_matcher(self): (self.make_file("sub3/file5.c"), False), ] fnm = FnmatchMatcher(["*.py", "*/sub2/*"]) - self.assertEqual(fnm.info(), ["*.py", "*/sub2/*"]) + assert fnm.info() == ["*.py", "*/sub2/*"] for filepath, matches in matches_to_try: self.assertMatches(fnm, filepath, matches) @@ -244,11 +233,11 @@ def assert_mapped(self, aliases, inp, out): aliases.pprint() print(inp) print(out) - self.assertEqual(aliases.map(inp), files.canonical_filename(out)) + assert aliases.map(inp) == files.canonical_filename(out) def assert_unchanged(self, aliases, inp): """Assert that `inp` mapped through `aliases` is unchanged.""" - self.assertEqual(aliases.map(inp), inp) + assert aliases.map(inp) == inp def test_noop(self): aliases = PathAliases() @@ -283,11 +272,11 @@ def test_multiple_patterns(self): def test_cant_have_wildcard_at_end(self): aliases = PathAliases() msg = "Pattern must not end with wildcards." - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): aliases.add("/ned/home/*", "fooey") - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): aliases.add("/ned/home/*/", "fooey") - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): aliases.add("/ned/home/*/*/", "fooey") def test_no_accidental_munging(self): @@ -407,15 +396,11 @@ def test_find_python_files(self): ]) +@pytest.mark.skipif(not env.WINDOWS, reason="Only need to run Windows tests on Windows.") class WindowsFileTest(CoverageTest): """Windows-specific tests of file name handling.""" run_in_temp_dir = False - def setUp(self): - if not env.WINDOWS: - self.skipTest("Only need to run Windows tests on Windows.") - super(WindowsFileTest, self).setUp() - def test_actual_path(self): - self.assertEqual(actual_path(r'c:\Windows'), actual_path(r'C:\wINDOWS')) + assert actual_path(r'c:\Windows') == actual_path(r'C:\wINDOWS') diff --git a/tests/test_html.py b/tests/test_html.py index 825b0afbe..51e0b93cd 100644 --- a/tests/test_html.py +++ b/tests/test_html.py @@ -13,6 +13,7 @@ import sys import mock +import pytest from unittest_mixins import change_dir import coverage @@ -86,7 +87,7 @@ def assert_correct_timestamp(self, html): """Extract the timestamp from `html`, and assert it is recent.""" timestamp_pat = r"created at (\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})" m = re.search(timestamp_pat, html) - self.assertTrue(m, "Didn't find a timestamp!") + assert m, "Didn't find a timestamp!" timestamp = datetime.datetime(*map(int, m.groups())) # The timestamp only records the minute, so the delta could be from # 12:00 to 12:01:59, or two minutes. @@ -177,7 +178,7 @@ def func1(x): # A nice function # Because the source change was only a comment, the index is the same. index2 = self.get_html_index_content() - self.assertMultiLineEqual(index1, index2) + assert index1 == index2 def test_html_delta_from_coverage_change(self): # HTML generation can create only the files that have changed. @@ -220,7 +221,7 @@ def test_html_delta_from_settings_change(self): assert "htmlcov/main_file_py.html" in self.files_written index2 = self.get_html_index_content() - self.assertMultiLineEqual(index1, index2) + assert index1 == index2 def test_html_delta_from_coverage_version_change(self): # HTML generation can create only the files that have changed. @@ -244,7 +245,7 @@ def test_html_delta_from_coverage_version_change(self): index2 = self.get_html_index_content() fixed_index2 = index2.replace("XYZZY", self.real_coverage_version) - self.assertMultiLineEqual(index1, fixed_index2) + assert index1 == fixed_index2 def test_file_becomes_100(self): self.create_initial_files() @@ -270,7 +271,7 @@ def test_status_format_change(self): with open("htmlcov/status.json") as status_json: status_data = json.load(status_json) - self.assertEqual(status_data['format'], 2) + assert status_data['format'] == 2 status_data['format'] = 99 with open("htmlcov/status.json", "w") as status_json: json.dump(status_data, status_json) @@ -292,44 +293,36 @@ def test_default_title(self): self.create_initial_files() self.run_coverage() index = self.get_html_index_content() - self.assertIn("Coverage report", index) - self.assertIn("

Coverage report:", index) + assert "Coverage report" in index + assert "

Coverage report:" in index def test_title_set_in_config_file(self): self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = Metrics & stuff!\n") self.run_coverage() index = self.get_html_index_content() - self.assertIn("Metrics & stuff!", index) - self.assertIn("

Metrics & stuff!:", index) + assert "Metrics & stuff!" in index + assert "

Metrics & stuff!:" in index def test_non_ascii_title_set_in_config_file(self): self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = «ταБЬℓσ» numbers") self.run_coverage() index = self.get_html_index_content() - self.assertIn( - "«ταБЬℓσ»" - " numbers", index - ) - self.assertIn( - "<h1>«ταБЬℓσ»" - " numbers", index - ) + assert "<title>«ταБЬℓσ» numbers" in index + assert "<h1>«ταБЬℓσ» numbers" in index def test_title_set_in_args(self): self.create_initial_files() self.make_file(".coveragerc", "[html]\ntitle = Good title\n") self.run_coverage(htmlargs=dict(title="«ταБЬℓσ» & stüff!")) index = self.get_html_index_content() - self.assertIn( - "<title>«ταБЬℓσ»" - " & stüff!", index - ) - self.assertIn( - "

«ταБЬℓσ»" - " & stüff!:", index + expected = ( + "«ταБЬℓσ» " + + "& stüff!" ) + assert expected in index + assert "

«ταБЬℓσ» & stüff!:" in index class HtmlWithUnparsableFilesTest(HtmlTestHelpers, CoverageTest): @@ -342,7 +335,7 @@ def test_dotpy_not_python(self): self.start_import_stop(cov, "main") self.make_file("innocuous.py", "

This isn't python!

") msg = "Couldn't parse '.*innocuous.py' as Python source: .* at line 1" - with self.assertRaisesRegex(NotPython, msg): + with pytest.raises(NotPython, match=msg): cov.html_report() def test_dotpy_not_python_ignored(self): @@ -352,20 +345,11 @@ def test_dotpy_not_python_ignored(self): self.start_import_stop(cov, "main") self.make_file("innocuous.py", "

This isn't python!

") cov.html_report(ignore_errors=True) - self.assertEqual( - len(cov._warnings), - 1, - "Expected a warning to be thrown when an invalid python file is parsed") - self.assertIn( - "Couldn't parse Python file", - cov._warnings[0], - "Warning message should be in 'invalid file' warning" - ) - self.assertIn( - "innocuous.py", - cov._warnings[0], - "Filename should be in 'invalid file' warning" - ) + msg = "Expected a warning to be thrown when an invalid python file is parsed" + assert 1 == len(cov._warnings), msg + msg = "Warning message should be in 'invalid file' warning" + assert "Couldn't parse Python file" in cov._warnings[0], msg + assert "innocuous.py" in cov._warnings[0], "Filename should be in 'invalid file' warning" self.assert_exists("htmlcov/index.html") # This would be better as a glob, if the HTML layout changes: self.assert_doesnt_exist("htmlcov/innocuous.html") @@ -380,7 +364,7 @@ def test_dothtml_not_python(self): # Before reporting, change it to be an HTML file. self.make_file("innocuous.html", "

This isn't python at all!

") output = self.run_command("coverage html") - self.assertEqual(output.strip(), "No data to report.") + assert output.strip() == "No data to report." def test_execed_liar_ignored(self): # Jinja2 sets __file__ to be a non-Python file, and then execs code. @@ -428,13 +412,13 @@ def test_decode_error(self): with open("sub/not_ascii.py", "rb") as f: undecodable = f.read() - self.assertIn(b"?\xcb!", undecodable) + assert b"?\xcb!" in undecodable cov.html_report() html_report = self.get_html_report_content("sub/not_ascii.py") expected = "# Isn't this great?�!" - self.assertIn(expected, html_report) + assert expected in html_report def test_formfeeds(self): # https://github.com/nedbat/coveragepy/issues/360 @@ -444,7 +428,7 @@ def test_formfeeds(self): cov.html_report() formfeed_html = self.get_html_report_content("formfeed.py") - self.assertIn("line_two", formfeed_html) + assert "line_two" in formfeed_html class HtmlTest(HtmlTestHelpers, CoverageTest): @@ -462,7 +446,7 @@ def test_missing_source_file_incorrect_message(self): missing_file = os.path.join(self.temp_dir, "sub", "another.py") missing_file = os.path.realpath(missing_file) msg = "(?i)No source for code: '%s'" % re.escape(missing_file) - with self.assertRaisesRegex(NoSource, msg): + with pytest.raises(NoSource, match=msg): cov.html_report() def test_extensionless_file_collides_with_extension(self): @@ -538,7 +522,7 @@ def normal(): normal() """) res = self.run_coverage(covargs=dict(source="."), htmlargs=dict(skip_covered=True)) - self.assertEqual(res, 100.0) + assert res == 100.0 self.assert_doesnt_exist("htmlcov/main_file_py.html") def make_init_and_main(self): @@ -588,7 +572,7 @@ def test_copying_static_files_from_system(self): with open("htmlcov/jquery.min.js") as f: jquery = f.read() - self.assertEqual(jquery, "Not Really JQuery!") + assert jquery == "Not Really JQuery!" def test_copying_static_files_from_system_in_dir(self): # Make a new place for static files. @@ -611,7 +595,7 @@ def test_copying_static_files_from_system_in_dir(self): the_file = os.path.basename(fpath) with open(os.path.join("htmlcov", the_file)) as f: contents = f.read() - self.assertEqual(contents, "Not real.") + assert contents == "Not real." def test_cant_find_static_files(self): # Make the path point to useless places. @@ -621,7 +605,7 @@ def test_cant_find_static_files(self): cov = coverage.Coverage() self.start_import_stop(cov, "main") msg = "Couldn't find static file u?'.*'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): cov.html_report() def filepath_to_regex(path): @@ -1094,10 +1078,9 @@ def html_data_from_cov(self, cov, morf): """Get HTML report data from a `Coverage` object for a morf.""" with self.assert_warnings(cov, []): datagen = coverage.html.HtmlDataGeneration(cov) - for fr, analysis in get_analysis_to_report(cov, [morf]): - # This will only loop once, so it's fine to return inside the loop. - file_data = datagen.data_for_file(fr, analysis) - return file_data + fr, analysis = next(get_analysis_to_report(cov, [morf])) + file_data = datagen.data_for_file(fr, analysis) + return file_data SOURCE = """\ def helper(lineno): diff --git a/tests/test_misc.py b/tests/test_misc.py index 2f6fbe7c1..dad542acf 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -24,36 +24,36 @@ def test_string_hashing(self): h2.update("Goodbye!") h3 = Hasher() h3.update("Hello, world!") - self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) - self.assertEqual(h1.hexdigest(), h3.hexdigest()) + assert h1.hexdigest() != h2.hexdigest() + assert h1.hexdigest() == h3.hexdigest() def test_bytes_hashing(self): h1 = Hasher() h1.update(b"Hello, world!") h2 = Hasher() h2.update(b"Goodbye!") - self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + assert h1.hexdigest() != h2.hexdigest() def test_unicode_hashing(self): h1 = Hasher() h1.update(u"Hello, world! \N{SNOWMAN}") h2 = Hasher() h2.update(u"Goodbye!") - self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + assert h1.hexdigest() != h2.hexdigest() def test_dict_hashing(self): h1 = Hasher() h1.update({'a': 17, 'b': 23}) h2 = Hasher() h2.update({'b': 23, 'a': 17}) - self.assertEqual(h1.hexdigest(), h2.hexdigest()) + assert h1.hexdigest() == h2.hexdigest() def test_dict_collision(self): h1 = Hasher() h1.update({'a': 17, 'b': {'c': 1, 'd': 2}}) h2 = Hasher() h2.update({'a': 17, 'b': {'c': 1}, 'd': 2}) - self.assertNotEqual(h1.hexdigest(), h2.hexdigest()) + assert h1.hexdigest() != h2.hexdigest() class RemoveFileTest(CoverageTest): @@ -72,20 +72,16 @@ def test_remove_actual_file(self): def test_actual_errors(self): # Errors can still happen. # ". is a directory" on Unix, or "Access denied" on Windows - with self.assertRaises(OSError): + with pytest.raises(OSError): file_be_gone(".") +@pytest.mark.skipif(not USE_CONTRACTS, reason="Contracts are disabled, can't test them") class ContractTest(CoverageTest): """Tests of our contract decorators.""" run_in_temp_dir = False - def setUp(self): - super(ContractTest, self).setUp() - if not USE_CONTRACTS: - self.skipTest("Contracts are disabled") - def test_bytes(self): @contract(text='bytes|None') def need_bytes(text=None): diff --git a/tests/test_numbits.py b/tests/test_numbits.py index 232d48d3a..fc27a093e 100644 --- a/tests/test_numbits.py +++ b/tests/test_numbits.py @@ -47,7 +47,7 @@ def test_conversion(self, nums): numbits = nums_to_numbits(nums) good_numbits(numbits) nums2 = numbits_to_nums(numbits) - self.assertEqual(nums, set(nums2)) + assert nums == set(nums2) @given(line_number_sets, line_number_sets) @settings(default_settings) @@ -59,7 +59,7 @@ def test_union(self, nums1, nums2): nbu = numbits_union(nb1, nb2) good_numbits(nbu) union = numbits_to_nums(nbu) - self.assertEqual(nums1 | nums2, set(union)) + assert nums1 | nums2 == set(union) @given(line_number_sets, line_number_sets) @settings(default_settings) @@ -71,7 +71,7 @@ def test_intersection(self, nums1, nums2): nbi = numbits_intersection(nb1, nb2) good_numbits(nbi) intersection = numbits_to_nums(nbi) - self.assertEqual(nums1 & nums2, set(intersection)) + assert nums1 & nums2 == set(intersection) @given(line_number_sets, line_number_sets) @settings(default_settings) @@ -82,7 +82,7 @@ def test_any_intersection(self, nums1, nums2): good_numbits(nb2) inter = numbits_any_intersection(nb1, nb2) expect = bool(nums1 & nums2) - self.assertEqual(expect, bool(inter)) + assert expect == bool(inter) @given(line_numbers, line_number_sets) @settings(default_settings) @@ -91,7 +91,7 @@ def test_num_in_numbits(self, num, nums): numbits = nums_to_numbits(nums) good_numbits(numbits) is_in = num_in_numbits(num, numbits) - self.assertEqual(num in nums, is_in) + assert (num in nums) == is_in class NumbitsSqliteFunctionTest(CoverageTest): @@ -121,12 +121,12 @@ def test_numbits_union(self): "(select numbits from data where id = 9)" ")" ) + expected = [ + 7, 9, 14, 18, 21, 27, 28, 35, 36, 42, 45, 49, + 54, 56, 63, 70, 72, 77, 81, 84, 90, 91, 98, 99, + ] answer = numbits_to_nums(list(res)[0][0]) - self.assertEqual( - [7, 9, 14, 18, 21, 27, 28, 35, 36, 42, 45, 49, - 54, 56, 63, 70, 72, 77, 81, 84, 90, 91, 98, 99], - answer - ) + assert expected == answer def test_numbits_intersection(self): res = self.cursor.execute( @@ -136,7 +136,7 @@ def test_numbits_intersection(self): ")" ) answer = numbits_to_nums(list(res)[0][0]) - self.assertEqual([63], answer) + assert [63] == answer def test_numbits_any_intersection(self): res = self.cursor.execute( @@ -144,20 +144,20 @@ def test_numbits_any_intersection(self): (nums_to_numbits([1, 2, 3]), nums_to_numbits([3, 4, 5])) ) answer = [any_inter for (any_inter,) in res] - self.assertEqual([1], answer) + assert [1] == answer res = self.cursor.execute( "select numbits_any_intersection(?, ?)", (nums_to_numbits([1, 2, 3]), nums_to_numbits([7, 8, 9])) ) answer = [any_inter for (any_inter,) in res] - self.assertEqual([0], answer) + assert [0] == answer def test_num_in_numbits(self): res = self.cursor.execute("select id, num_in_numbits(12, numbits) from data order by id") answer = [is_in for (id, is_in) in res] - self.assertEqual([1, 1, 1, 1, 0, 1, 0, 0, 0, 0], answer) + assert [1, 1, 1, 1, 0, 1, 0, 0, 0, 0] == answer def test_numbits_to_nums(self): res = self.cursor.execute("select numbits_to_nums(?)", [nums_to_numbits([1, 2, 3])]) - self.assertEqual([1, 2, 3], json.loads(res.fetchone()[0])) + assert [1, 2, 3] == json.loads(res.fetchone()[0]) diff --git a/tests/test_oddball.py b/tests/test_oddball.py index 17f696477..b73078877 100644 --- a/tests/test_oddball.py +++ b/tests/test_oddball.py @@ -80,7 +80,7 @@ def recur(n): def test_long_recursion(self): # We can't finish a very deep recursion, but we don't crash. - with self.assertRaises(RuntimeError): + with pytest.raises(RuntimeError): self.check_coverage("""\ def recur(n): if n == 0: @@ -125,16 +125,14 @@ def recur(n): expected_missing += [9, 10, 11] _, statements, missing, _ = cov.analysis("recur.py") - self.assertEqual(statements, [1, 2, 3, 5, 7, 8, 9, 10, 11]) - self.assertEqual(expected_missing, missing) + assert statements == [1, 2, 3, 5, 7, 8, 9, 10, 11] + assert expected_missing == missing # Get a warning about the stackoverflow effect on the tracing function. if pytrace: # pragma: no metacov - self.assertEqual(cov._warnings, - ["Trace function changed, measurement is likely wrong: None"] - ) + assert cov._warnings == ["Trace function changed, measurement is likely wrong: None"] else: - self.assertEqual(cov._warnings, []) + assert cov._warnings == [] class MemoryLeakTest(CoverageTest): @@ -147,10 +145,8 @@ class MemoryLeakTest(CoverageTest): """ @flaky + @pytest.mark.skipif(env.JYTHON, reason="Don't bother on Jython") def test_for_leaks(self): - if env.JYTHON: - self.skipTest("Don't bother on Jython") - # Our original bad memory leak only happened on line numbers > 255, so # make a code object with more lines than that. Ugly string mumbo # jumbo to get 300 blank lines at the beginning.. @@ -196,11 +192,10 @@ def once(x): # line 301 class MemoryFumblingTest(CoverageTest): """Test that we properly manage the None refcount.""" + @pytest.mark.skipif(not env.C_TRACER, reason="Only the C tracer has refcounting issues") def test_dropping_none(self): # pragma: not covered - if not env.C_TRACER: - self.skipTest("Only the C tracer has refcounting issues") # TODO: Mark this so it will only be run sometimes. - self.skipTest("This is too expensive for now (30s)") + pytest.skip("This is too expensive for now (30s)") # Start and stop coverage thousands of times to flush out bad # reference counting, maybe. self.make_file("the_code.py", """\ @@ -224,18 +219,16 @@ def f(): print("Final None refcount: %d" % (sys.getrefcount(None))) """) status, out = self.run_command_status("python main.py") - self.assertEqual(status, 0) - self.assertIn("Final None refcount", out) - self.assertNotIn("Fatal", out) + assert status == 0 + assert "Final None refcount" in out + assert "Fatal" not in out +@pytest.mark.skipif(env.JYTHON, reason="Pyexpat isn't a problem on Jython") class PyexpatTest(CoverageTest): """Pyexpat screws up tracing. Make sure we've counter-defended properly.""" def test_pyexpat(self): - if env.JYTHON: - self.skipTest("Pyexpat isn't a problem on Jython") - # pyexpat calls the trace function explicitly (inexplicably), and does # it wrong for exceptions. Parsing a DOCTYPE for some reason throws # an exception internally, and triggers its wrong behavior. This test @@ -268,20 +261,18 @@ def foo(): self.start_import_stop(cov, "outer") _, statements, missing, _ = cov.analysis("trydom.py") - self.assertEqual(statements, [1, 3, 8, 9, 10, 11, 13]) - self.assertEqual(missing, []) + assert statements == [1, 3, 8, 9, 10, 11, 13] + assert missing == [] _, statements, missing, _ = cov.analysis("outer.py") - self.assertEqual(statements, [101, 102]) - self.assertEqual(missing, []) + assert statements == [101, 102] + assert missing == [] # Make sure pyexpat isn't recorded as a source file. # https://github.com/nedbat/coveragepy/issues/419 files = cov.get_data().measured_files() - self.assertFalse( - any(f.endswith("pyexpat.c") for f in files), - "Pyexpat.c is in the measured files!: %r:" % (files,) - ) + msg = "Pyexpat.c is in the measured files!: %r:" % (files,) + assert not any(f.endswith("pyexpat.c") for f in files), msg class ExceptionTest(CoverageTest): @@ -393,7 +384,7 @@ def doit(calls): for lines in lines_expected.values(): lines[:] = [l for l in lines if l not in invisible] - self.assertEqual(clean_lines, lines_expected) + assert clean_lines == lines_expected class DoctestTest(CoverageTest): @@ -451,11 +442,11 @@ def swap_it(): cov = coverage.Coverage(source=["sample"]) self.start_import_stop(cov, "main") - self.assertEqual(self.stdout(), "10\n7\n3\n5\n9\n12\n") + assert self.stdout() == "10\n7\n3\n5\n9\n12\n" _, statements, missing, _ = cov.analysis("sample.py") - self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]) - self.assertEqual(missing, []) + assert statements == [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12] + assert missing == [] def test_setting_new_trace_function(self): # https://github.com/nedbat/coveragepy/issues/436 @@ -488,23 +479,19 @@ def test_unsets_trace(): ) out = self.stdout().replace(self.last_module_name, "coverage_test") - - self.assertEqual( - out, - ( - "call: coverage_test.py @ 10\n" - "line: coverage_test.py @ 11\n" - "line: coverage_test.py @ 12\n" - "return: coverage_test.py @ 12\n" - ), + expected = ( + "call: coverage_test.py @ 10\n" + "line: coverage_test.py @ 11\n" + "line: coverage_test.py @ 12\n" + "return: coverage_test.py @ 12\n" ) + assert expected == out @pytest.mark.expensive - def test_atexit_gettrace(self): # pragma: no metacov + @pytest.mark.skipif(env.METACOV, reason="Can't set trace functions during meta-coverage") + def test_atexit_gettrace(self): # This is not a test of coverage at all, but of our understanding # of this edge-case behavior in various Pythons. - if env.METACOV: - self.skipTest("Can't set trace functions during meta-coverage") self.make_file("atexit_gettrace.py", """\ import atexit, sys @@ -523,13 +510,13 @@ def show_trace_function(): # This will show what the trace function is at the end of the program. """) status, out = self.run_command_status("python atexit_gettrace.py") - self.assertEqual(status, 0) + assert status == 0 if env.PYPY and env.PYPYVERSION >= (5, 4): # Newer PyPy clears the trace function before atexit runs. - self.assertEqual(out, "None\n") + assert out == "None\n" else: # Other Pythons leave the trace function in place. - self.assertEqual(out, "trace_function\n") + assert out == "trace_function\n" class ExecTest(CoverageTest): @@ -557,16 +544,15 @@ def test_correct_filename(self): self.start_import_stop(cov, "main") _, statements, missing, _ = cov.analysis("main.py") - self.assertEqual(statements, [1, 2, 3, 4, 35]) - self.assertEqual(missing, []) + assert statements == [1, 2, 3, 4, 35] + assert missing == [] _, statements, missing, _ = cov.analysis("to_exec.py") - self.assertEqual(statements, [31]) - self.assertEqual(missing, []) + assert statements == [31] + assert missing == [] + @pytest.mark.skipif(env.PY2, reason="Python 2 can't seem to compile the file.") def test_unencodable_filename(self): # https://github.com/nedbat/coveragepy/issues/891 - if env.PYVERSION < (3, 0): - self.skipTest("Python 2 can't seem to compile the file.") self.make_file("bug891.py", r"""exec(compile("pass", "\udcff.py", "exec"))""") cov = coverage.Coverage() self.start_import_stop(cov, "bug891") @@ -609,4 +595,4 @@ def test_path_exists(mock_exists): import py_compile py_compile.compile("bug416a.py") out = self.run_command("coverage run bug416.py") - self.assertEqual(out, "in test\nbug416a.py\n23\n17\n") + assert out == "in test\nbug416a.py\n23\n17\n" diff --git a/tests/test_parser.py b/tests/test_parser.py index 9d3f9f678..6edb6d1a0 100644 --- a/tests/test_parser.py +++ b/tests/test_parser.py @@ -5,6 +5,8 @@ import textwrap +import pytest + from coverage import env from coverage.misc import NotPython from coverage.parser import PythonParser @@ -40,9 +42,9 @@ def foo(self, a): class Bar: pass """) - self.assertEqual(parser.exit_counts(), { + assert parser.exit_counts() == { 2:1, 3:1, 4:2, 5:1, 7:1, 9:1, 10:1 - }) + } def test_generator_exit_counts(self): # https://github.com/nedbat/coveragepy/issues/324 @@ -53,12 +55,12 @@ def gen(input): list(gen([1,2,3])) """) - self.assertEqual(parser.exit_counts(), { + assert parser.exit_counts() == { 1:1, # def -> list 2:2, # for -> yield; for -> exit 3:2, # yield -> for; genexp exit 5:1, # list -> exit - }) + } def test_try_except(self): parser = self.parse_source("""\ @@ -72,9 +74,9 @@ def test_try_except(self): a = 8 b = 9 """) - self.assertEqual(parser.exit_counts(), { + assert parser.exit_counts() == { 1: 1, 2:1, 3:2, 4:1, 5:2, 6:1, 7:1, 8:1, 9:1 - }) + } def test_excluded_classes(self): parser = self.parse_source("""\ @@ -86,9 +88,9 @@ def __init__(self): class Bar: pass """) - self.assertEqual(parser.exit_counts(), { + assert parser.exit_counts() == { 1:0, 2:1, 3:1 - }) + } def test_missing_branch_to_excluded_code(self): parser = self.parse_source("""\ @@ -98,7 +100,7 @@ def test_missing_branch_to_excluded_code(self): a = 4 b = 5 """) - self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 5:1 }) + assert parser.exit_counts() == { 1:1, 2:1, 5:1 } parser = self.parse_source("""\ def foo(): if fooey: @@ -107,7 +109,7 @@ def foo(): a = 5 b = 6 """) - self.assertEqual(parser.exit_counts(), { 1:1, 2:2, 3:1, 5:1, 6:1 }) + assert parser.exit_counts() == { 1:1, 2:2, 3:1, 5:1, 6:1 } parser = self.parse_source("""\ def foo(): if fooey: @@ -116,14 +118,14 @@ def foo(): a = 5 b = 6 """) - self.assertEqual(parser.exit_counts(), { 1:1, 2:1, 3:1, 6:1 }) + assert parser.exit_counts() == { 1:1, 2:1, 3:1, 6:1 } def test_indentation_error(self): msg = ( "Couldn't parse '' as Python source: " "'unindent does not match any outer indentation level' at line 3" ) - with self.assertRaisesRegex(NotPython, msg): + with pytest.raises(NotPython, match=msg): _ = self.parse_source("""\ 0 spaces 2 @@ -132,7 +134,7 @@ def test_indentation_error(self): def test_token_error(self): msg = "Couldn't parse '' as Python source: 'EOF in multi-line string' at line 1" - with self.assertRaisesRegex(NotPython, msg): + with pytest.raises(NotPython, match=msg): _ = self.parse_source("""\ ''' """) @@ -174,8 +176,8 @@ def func(x=25): raw_statements = {3, 4, 5, 6, 8, 9, 10, 13, 15, 16, 17, 20, 22, 23, 25, 26} if env.PYBEHAVIOR.trace_decorated_def: raw_statements.update([11, 19]) - self.assertEqual(parser.raw_statements, raw_statements) - self.assertEqual(parser.statements, {8}) + assert parser.raw_statements == raw_statements + assert parser.statements == {8} def test_class_decorator_pragmas(self): parser = self.parse_source("""\ @@ -188,8 +190,8 @@ class Bar(object): def __init__(self): self.x = 8 """) - self.assertEqual(parser.raw_statements, {1, 2, 3, 5, 6, 7, 8}) - self.assertEqual(parser.statements, {1, 2, 3}) + assert parser.raw_statements == {1, 2, 3, 5, 6, 7, 8} + assert parser.statements == {1, 2, 3} def test_empty_decorated_function(self): parser = self.parse_source("""\ @@ -219,9 +221,9 @@ def bar(self): expected_arcs.update(set(arcz_to_arcs("-46 6-4"))) expected_exits.update({6: 1}) - self.assertEqual(expected_statements, parser.statements) - self.assertEqual(expected_arcs, parser.arcs()) - self.assertEqual(expected_exits, parser.exit_counts()) + assert expected_statements == parser.statements + assert expected_arcs == parser.arcs() + assert expected_exits == parser.exit_counts() class ParserMissingArcDescriptionTest(CoverageTest): @@ -252,31 +254,24 @@ def func10(): thing(12) more_stuff(13) """) - self.assertEqual( - parser.missing_arc_description(1, 2), - "line 1 didn't jump to line 2, because the condition on line 1 was never true" - ) - self.assertEqual( - parser.missing_arc_description(1, 3), - "line 1 didn't jump to line 3, because the condition on line 1 was never false" + expected = "line 1 didn't jump to line 2, because the condition on line 1 was never true" + assert expected == parser.missing_arc_description(1, 2) + expected = "line 1 didn't jump to line 3, because the condition on line 1 was never false" + assert expected == parser.missing_arc_description(1, 3) + expected = ( + "line 6 didn't return from function 'func5', " + + "because the loop on line 6 didn't complete" ) - self.assertEqual( - parser.missing_arc_description(6, -5), - "line 6 didn't return from function 'func5', " - "because the loop on line 6 didn't complete" - ) - self.assertEqual( - parser.missing_arc_description(6, 7), - "line 6 didn't jump to line 7, because the loop on line 6 never started" - ) - self.assertEqual( - parser.missing_arc_description(11, 12), - "line 11 didn't jump to line 12, because the condition on line 11 was never true" - ) - self.assertEqual( - parser.missing_arc_description(11, 13), - "line 11 didn't jump to line 13, because the condition on line 11 was never false" + assert expected == parser.missing_arc_description(6, -5) + expected = "line 6 didn't jump to line 7, because the loop on line 6 never started" + assert expected == parser.missing_arc_description(6, 7) + expected = "line 11 didn't jump to line 12, because the condition on line 11 was never true" + assert expected == parser.missing_arc_description(11, 12) + expected = ( + "line 11 didn't jump to line 13, " + + "because the condition on line 11 was never false" ) + assert expected == parser.missing_arc_description(11, 13) def test_missing_arc_descriptions_for_small_callables(self): parser = self.parse_text(u"""\ @@ -288,22 +283,14 @@ def test_missing_arc_descriptions_for_small_callables(self): ] x = 7 """) - self.assertEqual( - parser.missing_arc_description(2, -2), - "line 2 didn't finish the lambda on line 2" - ) - self.assertEqual( - parser.missing_arc_description(3, -3), - "line 3 didn't finish the generator expression on line 3" - ) - self.assertEqual( - parser.missing_arc_description(4, -4), - "line 4 didn't finish the dictionary comprehension on line 4" - ) - self.assertEqual( - parser.missing_arc_description(5, -5), - "line 5 didn't finish the set comprehension on line 5" - ) + expected = "line 2 didn't finish the lambda on line 2" + 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) def test_missing_arc_descriptions_for_exceptions(self): parser = self.parse_text(u"""\ @@ -314,14 +301,16 @@ def test_missing_arc_descriptions_for_exceptions(self): except ValueError: print("yikes") """) - self.assertEqual( - parser.missing_arc_description(3, 4), - "line 3 didn't jump to line 4, because the exception caught by line 3 didn't happen" + expected = ( + "line 3 didn't jump to line 4, " + + "because the exception caught by line 3 didn't happen" ) - self.assertEqual( - parser.missing_arc_description(5, 6), - "line 5 didn't jump to line 6, because the exception caught by line 5 didn't happen" + assert expected == parser.missing_arc_description(3, 4) + expected = ( + "line 5 didn't jump to line 6, " + + "because the exception caught by line 5 didn't happen" ) + assert expected == parser.missing_arc_description(5, 6) def test_missing_arc_descriptions_for_finally(self): parser = self.parse_text(u"""\ @@ -346,56 +335,56 @@ def function(): that_thing(19) """) if env.PYBEHAVIOR.finally_jumps_back: - self.assertEqual( - parser.missing_arc_description(18, 5), - "line 18 didn't jump to line 5, because the break on line 5 wasn't executed" - ) - self.assertEqual( - parser.missing_arc_description(5, 19), - "line 5 didn't jump to line 19, because the break on line 5 wasn't executed" + expected = "line 18 didn't jump to line 5, because the break on line 5 wasn't executed" + assert expected == parser.missing_arc_description(18, 5) + expected = "line 5 didn't jump to line 19, because the break on line 5 wasn't executed" + assert expected == parser.missing_arc_description(5, 19) + expected = ( + "line 18 didn't jump to line 10, " + + "because the continue on line 10 wasn't executed" ) - self.assertEqual( - parser.missing_arc_description(18, 10), - "line 18 didn't jump to line 10, because the continue on line 10 wasn't executed" + assert expected == parser.missing_arc_description(18, 10) + expected = ( + "line 10 didn't jump to line 2, " + + "because the continue on line 10 wasn't executed" ) - self.assertEqual( - parser.missing_arc_description(10, 2), - "line 10 didn't jump to line 2, because the continue on line 10 wasn't executed" + assert expected == parser.missing_arc_description(10, 2) + expected = ( + "line 18 didn't jump to line 14, " + + "because the return on line 14 wasn't executed" ) - self.assertEqual( - parser.missing_arc_description(18, 14), - "line 18 didn't jump to line 14, because the return on line 14 wasn't executed" + assert expected == parser.missing_arc_description(18, 14) + expected = ( + "line 14 didn't return from function 'function', " + + "because the return on line 14 wasn't executed" ) - self.assertEqual( - parser.missing_arc_description(14, -1), - "line 14 didn't return from function 'function', " - "because the return on line 14 wasn't executed" - ) - self.assertEqual( - parser.missing_arc_description(18, -1), - "line 18 didn't except from function 'function', " - "because the raise on line 16 wasn't executed" + assert expected == parser.missing_arc_description(14, -1) + expected = ( + "line 18 didn't except from function 'function', " + + "because the raise on line 16 wasn't executed" ) + assert expected == parser.missing_arc_description(18, -1) else: - self.assertEqual( - parser.missing_arc_description(18, 19), - "line 18 didn't jump to line 19, because the break on line 5 wasn't executed" + expected = ( + "line 18 didn't jump to line 19, " + + "because the break on line 5 wasn't executed" ) - self.assertEqual( - parser.missing_arc_description(18, 2), - "line 18 didn't jump to line 2, " - "because the continue on line 10 wasn't executed" - " or " + assert expected == parser.missing_arc_description(18, 19) + expected = ( + "line 18 didn't jump to line 2, " + + "because the continue on line 10 wasn't executed" + + " or " + "the continue on line 12 wasn't executed" ) - self.assertEqual( - parser.missing_arc_description(18, -1), - "line 18 didn't except from function 'function', " - "because the raise on line 16 wasn't executed" - " or " - "line 18 didn't return from function 'function', " + assert expected == parser.missing_arc_description(18, 2) + expected = ( + "line 18 didn't except from function 'function', " + + "because the raise on line 16 wasn't executed" + + " or " + + "line 18 didn't return from function 'function', " + "because the return on line 14 wasn't executed" ) + assert expected == parser.missing_arc_description(18, -1) def test_missing_arc_descriptions_bug460(self): parser = self.parse_text(u"""\ @@ -406,10 +395,7 @@ def test_missing_arc_descriptions_bug460(self): } x = 6 """) - self.assertEqual( - parser.missing_arc_description(2, -3), - "line 3 didn't finish the lambda on line 3", - ) + assert parser.missing_arc_description(2, -3) == "line 3 didn't finish the lambda on line 3" class ParserFileTest(CoverageTest): @@ -440,18 +426,14 @@ class Bar: fname = fname + ".py" self.make_file(fname, text, newline=newline) parser = self.parse_file(fname) - self.assertEqual( - parser.exit_counts(), - counts, - "Wrong for %r" % fname - ) + assert parser.exit_counts() == counts, "Wrong for %r" % fname def test_encoding(self): self.make_file("encoded.py", """\ coverage = "\xe7\xf6v\xear\xe3g\xe9" """) parser = self.parse_file("encoded.py") - self.assertEqual(parser.exit_counts(), {1: 1}) + assert parser.exit_counts() == {1: 1} def test_missing_line_ending(self): # Test that the set of statements is the same even if a final @@ -466,7 +448,7 @@ def test_missing_line_ending(self): """) parser = self.parse_file("normal.py") - self.assertEqual(parser.statements, {1}) + assert parser.statements == {1} self.make_file("abrupt.py", """\ out, err = subprocess.Popen( @@ -476,7 +458,7 @@ def test_missing_line_ending(self): # Double-check that some test helper wasn't being helpful. with open("abrupt.py") as f: - self.assertEqual(f.read()[-1], ")") + assert f.read()[-1] == ")" parser = self.parse_file("abrupt.py") - self.assertEqual(parser.statements, {1}) + assert parser.statements == {1} diff --git a/tests/test_phystokens.py b/tests/test_phystokens.py index 1256694a4..86b1fdbe4 100644 --- a/tests/test_phystokens.py +++ b/tests/test_phystokens.py @@ -7,6 +7,8 @@ import re import textwrap +import pytest + from coverage import env from coverage.phystokens import source_token_lines, source_encoding from coverage.phystokens import neuter_encoding_declaration, compile_unicode @@ -67,23 +69,23 @@ def check_tokenization(self, source): source = source.replace('\r\n', '\n') source = re.sub(r"(?m)[ \t]+$", "", source) tokenized = re.sub(r"(?m)[ \t]+$", "", tokenized) - self.assertMultiLineEqual(source, tokenized) + assert source == tokenized def check_file_tokenization(self, fname): """Use the contents of `fname` for `check_tokenization`.""" self.check_tokenization(get_python_source(fname)) def test_simple(self): - self.assertEqual(list(source_token_lines(SIMPLE)), SIMPLE_TOKENS) + assert list(source_token_lines(SIMPLE)) == SIMPLE_TOKENS self.check_tokenization(SIMPLE) def test_missing_final_newline(self): # We can tokenize source that is missing the final newline. - self.assertEqual(list(source_token_lines(SIMPLE.rstrip())), SIMPLE_TOKENS) + assert list(source_token_lines(SIMPLE.rstrip())) == SIMPLE_TOKENS def test_tab_indentation(self): # Mixed tabs and spaces... - self.assertEqual(list(source_token_lines(MIXED_WS)), MIXED_WS_TOKENS) + assert list(source_token_lines(MIXED_WS)) == MIXED_WS_TOKENS def test_bug_822(self): self.check_tokenization(BUG_822) @@ -128,48 +130,42 @@ class SourceEncodingTest(CoverageTest): def test_detect_source_encoding(self): for _, source, expected in ENCODING_DECLARATION_SOURCES: - self.assertEqual( - source_encoding(source), - expected, - "Wrong encoding in %r" % source - ) + assert source_encoding(source) == expected, "Wrong encoding in %r" % source + # PyPy3 gets this case wrong. Not sure what I can do about it, so skip the test. + @pytest.mark.skipif(env.PYPY3, reason="PyPy3 is wrong about non-comment encoding. Skip it.") def test_detect_source_encoding_not_in_comment(self): - if env.PYPY3: # pragma: no metacov - # PyPy3 gets this case wrong. Not sure what I can do about it, - # so skip the test. - self.skipTest("PyPy3 is wrong about non-comment encoding. Skip it.") # Should not detect anything here source = b'def parse(src, encoding=None):\n pass' - self.assertEqual(source_encoding(source), DEF_ENCODING) + assert source_encoding(source) == DEF_ENCODING def test_dont_detect_source_encoding_on_third_line(self): # A coding declaration doesn't count on the third line. source = b"\n\n# coding=cp850\n\n" - self.assertEqual(source_encoding(source), DEF_ENCODING) + assert source_encoding(source) == DEF_ENCODING def test_detect_source_encoding_of_empty_file(self): # An important edge case. - self.assertEqual(source_encoding(b""), DEF_ENCODING) + assert source_encoding(b"") == DEF_ENCODING def test_bom(self): # A BOM means utf-8. source = b"\xEF\xBB\xBFtext = 'hello'\n" - self.assertEqual(source_encoding(source), 'utf-8-sig') + assert source_encoding(source) == 'utf-8-sig' def test_bom_with_encoding(self): source = b"\xEF\xBB\xBF# coding: utf-8\ntext = 'hello'\n" - self.assertEqual(source_encoding(source), 'utf-8-sig') + assert source_encoding(source) == 'utf-8-sig' def test_bom_is_wrong(self): # A BOM with an explicit non-utf8 encoding is an error. source = b"\xEF\xBB\xBF# coding: cp850\n" - with self.assertRaisesRegex(SyntaxError, "encoding problem: utf-8"): + with pytest.raises(SyntaxError, match="encoding problem: utf-8"): source_encoding(source) def test_unknown_encoding(self): source = b"# coding: klingon\n" - with self.assertRaisesRegex(SyntaxError, "unknown encoding: klingon"): + with pytest.raises(SyntaxError, match="unknown encoding: klingon"): source_encoding(source) @@ -186,21 +182,17 @@ def test_neuter_encoding_declaration(self): # The neutered source should have the same number of lines. source_lines = source.splitlines() neutered_lines = neutered.splitlines() - self.assertEqual(len(source_lines), len(neutered_lines)) + assert len(source_lines) == len(neutered_lines) # Only one of the lines should be different. lines_different = sum( int(nline != sline) for nline, sline in zip(neutered_lines, source_lines) ) - self.assertEqual(lines_diff_expected, lines_different) + assert lines_diff_expected == lines_different # The neutered source will be detected as having no encoding # declaration. - self.assertEqual( - source_encoding(neutered), - DEF_ENCODING, - "Wrong encoding in %r" % neutered - ) + assert source_encoding(neutered) == DEF_ENCODING, "Wrong encoding in %r" % neutered def test_two_encoding_declarations(self): input_src = textwrap.dedent(u"""\ @@ -214,7 +206,7 @@ def test_two_encoding_declarations(self): # -*- coding: utf-16 -*- """) output_src = neuter_encoding_declaration(input_src) - self.assertEqual(expected_src, output_src) + assert expected_src == output_src def test_one_encoding_declaration(self): input_src = textwrap.dedent(u"""\ @@ -228,7 +220,7 @@ def test_one_encoding_declaration(self): # -*- coding: ascii -*- """) output_src = neuter_encoding_declaration(input_src) - self.assertEqual(expected_src, output_src) + assert expected_src == output_src class Bug529Test(CoverageTest): @@ -258,8 +250,8 @@ def test_two_strings_are_equal(self): unittest.main() ''') status, out = self.run_command_status("coverage run the_test.py") - self.assertEqual(status, 0) - self.assertIn("OK", out) + assert status == 0 + assert "OK" in out # If this test fails, the output will be super-confusing, because it # has a failing unit test contained within the failing unit test. @@ -276,7 +268,7 @@ def assert_compile_unicode(self, source): code = compile_unicode(source, "", "exec") globs = {} exec(code, globs) - self.assertEqual(globs['a'], 42) + assert globs['a'] == 42 def test_cp1252(self): uni = u"""# coding: cp1252\n# \u201C curly \u201D\n""" diff --git a/tests/test_plugins.py b/tests/test_plugins.py index 813d370e3..aeffdb808 100644 --- a/tests/test_plugins.py +++ b/tests/test_plugins.py @@ -7,6 +7,8 @@ import os.path from xml.etree import ElementTree +import pytest + import coverage from coverage import env from coverage.backward import StringIO, import_local_file @@ -53,10 +55,10 @@ def coverage_init(reg, options): config = FakeConfig("plugin1", {}) plugins = Plugins.load_plugins([], config) - self.assertFalse(plugins) + assert not plugins plugins = Plugins.load_plugins(["plugin1"], config) - self.assertTrue(plugins) + assert plugins def test_importing_and_configuring(self): self.make_file("plugin1.py", """\ @@ -74,10 +76,10 @@ def coverage_init(reg, options): config = FakeConfig("plugin1", {'a': 'hello'}) plugins = list(Plugins.load_plugins(["plugin1"], config)) - self.assertEqual(len(plugins), 1) - self.assertEqual(plugins[0].this_is, "me") - self.assertEqual(plugins[0].options, {'a': 'hello'}) - self.assertEqual(config.asked_for, ['plugin1']) + assert len(plugins) == 1 + assert plugins[0].this_is == "me" + assert plugins[0].options == {'a': 'hello'} + assert config.asked_for == ['plugin1'] def test_importing_and_configuring_more_than_one(self): self.make_file("plugin1.py", """\ @@ -105,23 +107,23 @@ def coverage_init(reg, options): config = FakeConfig("plugin1", {'a': 'hello'}) plugins = list(Plugins.load_plugins(["plugin1", "plugin2"], config)) - self.assertEqual(len(plugins), 2) - self.assertEqual(plugins[0].this_is, "me") - self.assertEqual(plugins[0].options, {'a': 'hello'}) - self.assertEqual(plugins[1].options, {}) - self.assertEqual(config.asked_for, ['plugin1', 'plugin2']) + assert len(plugins) == 2 + assert plugins[0].this_is == "me" + assert plugins[0].options == {'a': 'hello'} + assert plugins[1].options == {} + assert config.asked_for == ['plugin1', 'plugin2'] # The order matters... config = FakeConfig("plugin1", {'a': 'second'}) plugins = list(Plugins.load_plugins(["plugin2", "plugin1"], config)) - self.assertEqual(len(plugins), 2) - self.assertEqual(plugins[0].options, {}) - self.assertEqual(plugins[1].this_is, "me") - self.assertEqual(plugins[1].options, {'a': 'second'}) + assert len(plugins) == 2 + assert plugins[0].options == {} + assert plugins[1].this_is == "me" + assert plugins[1].options == {'a': 'second'} def test_cant_import(self): - with self.assertRaisesRegex(ImportError, "No module named '?plugin_not_there'?"): + with pytest.raises(ImportError, match="No module named '?plugin_not_there'?"): _ = Plugins.load_plugins(["plugin_not_there"], None) def test_plugin_must_define_coverage_init(self): @@ -130,7 +132,7 @@ def test_plugin_must_define_coverage_init(self): Nothing = 0 """) msg_pat = "Plugin module 'no_plugin' didn't define a coverage_init function" - with self.assertRaisesRegex(CoverageException, msg_pat): + with pytest.raises(CoverageException, match=msg_pat): list(Plugins.load_plugins(["no_plugin"], None)) @@ -156,11 +158,11 @@ def coverage_init(reg, options): cov.stop() # pragma: nested with open("evidence.out") as f: - self.assertEqual(f.read(), "we are here!") + assert f.read() == "we are here!" def test_missing_plugin_raises_import_error(self): # Prove that a missing plugin will raise an ImportError. - with self.assertRaisesRegex(ImportError, "No module named '?does_not_exist_woijwoicweo'?"): + with pytest.raises(ImportError, match="No module named '?does_not_exist_woijwoicweo'?"): cov = coverage.Coverage() cov.set_option("run:plugins", ["does_not_exist_woijwoicweo"]) cov.start() @@ -169,7 +171,7 @@ def test_missing_plugin_raises_import_error(self): def test_bad_plugin_isnt_hidden(self): # Prove that a plugin with an error in it will raise the error. self.make_file("plugin_over_zero.py", "1/0") - with self.assertRaises(ZeroDivisionError): + with pytest.raises(ZeroDivisionError): cov = coverage.Coverage() cov.set_option("run:plugins", ["plugin_over_zero"]) cov.start() @@ -195,16 +197,16 @@ def coverage_init(reg, options): out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] if env.C_TRACER: - self.assertIn('plugins.file_tracers: plugin_sys_info.Plugin', out_lines) + assert 'plugins.file_tracers: plugin_sys_info.Plugin' in out_lines else: - self.assertIn('plugins.file_tracers: plugin_sys_info.Plugin (disabled)', out_lines) - self.assertIn('plugins.configurers: -none-', out_lines) + assert 'plugins.file_tracers: plugin_sys_info.Plugin (disabled)' in out_lines + assert 'plugins.configurers: -none-' in out_lines expected_end = [ "-- sys: plugin_sys_info.Plugin -------------------------------", "hello: world", "-- end -------------------------------------------------------", ] - self.assertEqual(expected_end, out_lines[-len(expected_end):]) + assert expected_end == out_lines[-len(expected_end):] def test_plugin_with_no_sys_info(self): self.make_file("plugin_no_sys_info.py", """\ @@ -224,13 +226,13 @@ def coverage_init(reg, options): cov.stop() # pragma: nested out_lines = [line.strip() for line in debug_out.getvalue().splitlines()] - self.assertIn('plugins.file_tracers: -none-', out_lines) - self.assertIn('plugins.configurers: plugin_no_sys_info.Plugin', out_lines) + assert 'plugins.file_tracers: -none-' in out_lines + assert 'plugins.configurers: plugin_no_sys_info.Plugin' in out_lines expected_end = [ "-- sys: plugin_no_sys_info.Plugin ----------------------------", "-- end -------------------------------------------------------", ] - self.assertEqual(expected_end, out_lines[-len(expected_end):]) + assert expected_end == out_lines[-len(expected_end):] def test_local_files_are_importable(self): self.make_file("importing_plugin.py", """\ @@ -249,17 +251,15 @@ def coverage_init(reg, options): self.make_file("main_file.py", "print('MAIN')") out = self.run_command("coverage run main_file.py") - self.assertEqual(out, "MAIN\n") + assert out == "MAIN\n" out = self.run_command("coverage html") - self.assertEqual(out, "") + assert out == "" +@pytest.mark.skipif(env.C_TRACER, reason="This test is only about PyTracer.") class PluginWarningOnPyTracer(CoverageTest): """Test that we get a controlled exception with plugins on PyTracer.""" def test_exception_if_plugins_on_pytracer(self): - if env.C_TRACER: - self.skipTest("This test is only about PyTracer.") - self.make_file("simple.py", "a = 1") cov = coverage.Coverage() @@ -272,14 +272,10 @@ def test_exception_if_plugins_on_pytracer(self): self.start_import_stop(cov, "simple") +@pytest.mark.skipif(not env.C_TRACER, reason="Plugins are only supported with the C tracer.") class FileTracerTest(CoverageTest): """Tests of plugins that implement file_tracer.""" - def setUp(self): - if not env.C_TRACER: - self.skipTest("Plugins are only supported with the C tracer.") - super(FileTracerTest, self).setUp() - class GoodFileTracerTest(FileTracerTest): """Tests of file tracer plugin happy paths.""" @@ -304,11 +300,11 @@ def test_plugin1(self): self.start_import_stop(cov, "simple") _, statements, missing, _ = cov.analysis("simple.py") - self.assertEqual(statements, [1, 2, 3]) - self.assertEqual(missing, []) + assert statements == [1, 2, 3] + assert missing == [] zzfile = os.path.abspath(os.path.join("/src", "try_ABC.zz")) _, statements, _, _ = cov.analysis(zzfile) - self.assertEqual(statements, [105, 106, 107, 205, 206, 207]) + assert statements == [105, 106, 107, 205, 206, 207] def make_render_and_caller(self): """Make the render.py and caller.py files we need.""" @@ -370,21 +366,21 @@ def test_plugin2(self): # have 7 lines in it. If render() was called with line number 4, # then the plugin will claim that lines 4 and 5 were executed. _, statements, missing, _ = cov.analysis("foo_7.html") - self.assertEqual(statements, [1, 2, 3, 4, 5, 6, 7]) - self.assertEqual(missing, [1, 2, 3, 6, 7]) - self.assertIn("foo_7.html", line_counts(cov.get_data())) + assert statements == [1, 2, 3, 4, 5, 6, 7] + assert missing == [1, 2, 3, 6, 7] + assert "foo_7.html" in line_counts(cov.get_data()) _, statements, missing, _ = cov.analysis("bar_4.html") - self.assertEqual(statements, [1, 2, 3, 4]) - self.assertEqual(missing, [1, 4]) - self.assertIn("bar_4.html", line_counts(cov.get_data())) + assert statements == [1, 2, 3, 4] + assert missing == [1, 4] + assert "bar_4.html" in line_counts(cov.get_data()) - self.assertNotIn("quux_5.html", line_counts(cov.get_data())) + assert "quux_5.html" not in line_counts(cov.get_data()) _, statements, missing, _ = cov.analysis("uni_3.html") - self.assertEqual(statements, [1, 2, 3]) - self.assertEqual(missing, [1]) - self.assertIn("uni_3.html", line_counts(cov.get_data())) + assert statements == [1, 2, 3] + assert missing == [1] + assert "uni_3.html" in line_counts(cov.get_data()) def test_plugin2_with_branch(self): self.make_render_and_caller() @@ -400,12 +396,12 @@ def test_plugin2_with_branch(self): # have 7 lines in it. If render() was called with line number 4, # then the plugin will claim that lines 4 and 5 were executed. analysis = cov._analyze("foo_7.html") - self.assertEqual(analysis.statements, {1, 2, 3, 4, 5, 6, 7}) + assert analysis.statements == {1, 2, 3, 4, 5, 6, 7} # Plugins don't do branch coverage yet. - self.assertEqual(analysis.has_arcs(), True) - self.assertEqual(analysis.arc_possibilities(), []) + assert analysis.has_arcs() is True + assert analysis.arc_possibilities() == [] - self.assertEqual(analysis.missing, {1, 2, 3, 6, 7}) + assert analysis.missing == {1, 2, 3, 6, 7} def test_plugin2_with_text_report(self): self.make_render_and_caller() @@ -426,8 +422,8 @@ def test_plugin2_with_text_report(self): '--------------------------------------------------------', 'TOTAL 11 7 0 0 36%', ] - self.assertEqual(expected, report) - self.assertAlmostEqual(total, 36.36, places=2) + assert expected == report + assert round(abs(total-36.36), 2) == 0 def test_plugin2_with_html_report(self): self.make_render_and_caller() @@ -438,7 +434,7 @@ def test_plugin2_with_html_report(self): self.start_import_stop(cov, "caller") total = cov.html_report(include=["*.html"], omit=["uni*.html"]) - self.assertAlmostEqual(total, 36.36, places=2) + assert round(abs(total-36.36), 2) == 0 self.assert_exists("htmlcov/index.html") self.assert_exists("htmlcov/bar_4_html.html") @@ -453,7 +449,7 @@ def test_plugin2_with_xml_report(self): self.start_import_stop(cov, "caller") total = cov.xml_report(include=["*.html"], omit=["uni*.html"]) - self.assertAlmostEqual(total, 36.36, places=2) + assert round(abs(total-36.36), 2) == 0 dom = ElementTree.parse("coverage.xml") classes = {} @@ -525,8 +521,8 @@ def coverage_init(reg, options): '-----------------------------------------------', 'TOTAL 6 3 50%', ] - self.assertEqual(expected, report) - self.assertEqual(total, 50) + assert expected == report + assert total == 50 def test_find_unexecuted(self): self.make_file("unexecuted_plugin.py", """\ @@ -567,17 +563,17 @@ def coverage_init(reg, options): # The file we executed claims to have run line 999. _, statements, missing, _ = cov.analysis("foo.py") - self.assertEqual(statements, [99, 999, 9999]) - self.assertEqual(missing, [99, 9999]) + assert statements == [99, 999, 9999] + assert missing == [99, 9999] # The completely missing file is in the results. _, statements, missing, _ = cov.analysis("chimera.py") - self.assertEqual(statements, [99, 999, 9999]) - self.assertEqual(missing, [99, 999, 9999]) + assert statements == [99, 999, 9999] + assert missing == [99, 999, 9999] # But completely new filenames are not in the results. - self.assertEqual(len(cov.get_data().measured_files()), 3) - with self.assertRaises(CoverageException): + assert len(cov.get_data().measured_files()) == 3 + with pytest.raises(CoverageException): cov.analysis("fictional.py") @@ -641,7 +637,7 @@ def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, if our_error: errors = stderr.count("# Oh noes!") # The exception we're causing should only appear once. - self.assertEqual(errors, 1) + assert errors == 1 # There should be a warning explaining what's happening, but only one. # The message can be in two forms: @@ -650,12 +646,12 @@ def run_bad_plugin(self, module_name, plugin_name, our_error=True, excmsg=None, # Disabling plug-in '...' due to an exception: msg = "Disabling plug-in '%s.%s' due to " % (module_name, plugin_name) warnings = stderr.count(msg) - self.assertEqual(warnings, 1) + assert warnings == 1 if excmsg: - self.assertIn(excmsg, stderr) + assert excmsg in stderr if excmsgs: - self.assertTrue(any(em in stderr for em in excmsgs), "expected one of %r" % excmsgs) + assert any(em in stderr for em in excmsgs), "expected one of %r" % excmsgs def test_file_tracer_has_no_file_tracer_method(self): self.make_file("bad_plugin.py", """\ @@ -703,7 +699,7 @@ def coverage_init(reg, options): """) cov = self.run_plugin("bad_plugin") expected_msg = "Plugin 'bad_plugin.Plugin' needs to implement file_reporter()" - with self.assertRaisesRegex(NotImplementedError, expected_msg): + with pytest.raises(NotImplementedError, match=expected_msg): cov.report() def test_file_tracer_fails(self): @@ -942,8 +938,8 @@ def test_configurer_plugin(self): cov.start() cov.stop() # pragma: nested excluded = cov.get_option("report:exclude_lines") - self.assertIn("pragma: custom", excluded) - self.assertIn("pragma: or whatever", excluded) + assert "pragma: custom" in excluded + assert "pragma: or whatever" in excluded class DynamicContextPluginTest(CoverageTest): @@ -1048,16 +1044,14 @@ def test_plugin_standalone(self): # Labeled coverage is collected data = cov.get_data() filenames = self.get_measured_filenames(data) - self.assertEqual( - ['', 'doctest:HTML_TAG', 'test:HTML_TAG', 'test:RENDERERS'], - sorted(data.measured_contexts()), - ) + expected = ['', 'doctest:HTML_TAG', 'test:HTML_TAG', 'test:RENDERERS'] + assert expected == sorted(data.measured_contexts()) data.set_query_context("doctest:HTML_TAG") - self.assertEqual([2], data.lines(filenames['rendering.py'])) + assert [2] == data.lines(filenames['rendering.py']) data.set_query_context("test:HTML_TAG") - self.assertEqual([2], data.lines(filenames['rendering.py'])) + assert [2] == data.lines(filenames['rendering.py']) data.set_query_context("test:RENDERERS") - self.assertEqual([2, 5, 8, 11], sorted(data.lines(filenames['rendering.py']))) + assert [2, 5, 8, 11] == sorted(data.lines(filenames['rendering.py'])) def test_static_context(self): self.make_plugin_capitalized_testnames('plugin_tests.py') @@ -1078,7 +1072,7 @@ def test_static_context(self): 'mytests|test:HTML_TAG', 'mytests|test:RENDERERS', ] - self.assertEqual(expected, sorted(data.measured_contexts())) + assert expected == sorted(data.measured_contexts()) def test_plugin_with_test_function(self): self.make_plugin_capitalized_testnames('plugin_tests.py') @@ -1103,11 +1097,11 @@ def test_plugin_with_test_function(self): 'testsuite.test_html_tag', 'testsuite.test_renderers', ] - self.assertEqual(expected, sorted(data.measured_contexts())) + assert expected == sorted(data.measured_contexts()) def assert_context_lines(context, lines): data.set_query_context(context) - self.assertEqual(lines, sorted(data.lines(filenames['rendering.py']))) + assert lines == sorted(data.lines(filenames['rendering.py'])) assert_context_lines("doctest:HTML_TAG", [2]) assert_context_lines("testsuite.test_html_tag", [2]) @@ -1141,11 +1135,11 @@ def test_multiple_plugins(self): 'test:HTML_TAG', 'test:RENDERERS', ] - self.assertEqual(expected, sorted(data.measured_contexts())) + assert expected == sorted(data.measured_contexts()) def assert_context_lines(context, lines): data.set_query_context(context) - self.assertEqual(lines, sorted(data.lines(filenames['rendering.py']))) + assert lines == sorted(data.lines(filenames['rendering.py'])) assert_context_lines("test:HTML_TAG", [2]) assert_context_lines("test:RENDERERS", [2, 5, 8, 11]) diff --git a/tests/test_process.py b/tests/test_process.py index e48861568..9f07b9ccb 100644 --- a/tests/test_process.py +++ b/tests/test_process.py @@ -52,7 +52,7 @@ def test_environment(self): self.assert_doesnt_exist(".coverage") out = self.run_command("coverage run mycode.py") self.assert_exists(".coverage") - self.assertEqual(out, 'done\n') + assert out == 'done\n' def make_b_or_c_py(self): """Create b_or_c.py, used in a few of these tests.""" @@ -74,12 +74,12 @@ def make_b_or_c_py(self): def test_combine_parallel_data(self): self.make_b_or_c_py() out = self.run_command("coverage run -p b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") self.assert_file_count(".coverage.*", 1) out = self.run_command("coverage run -p b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") # After two -p runs, there should be two .coverage.machine.123 files. @@ -96,28 +96,28 @@ def test_combine_parallel_data(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 # Running combine again should fail, because there are no parallel data # files to combine. status, out = self.run_command_status("coverage combine") - self.assertEqual(status, 1) - self.assertEqual(out, "No data to combine\n") + assert status == 1 + assert out == "No data to combine\n" # And the originally combined data is still there. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 def test_combine_parallel_data_with_a_corrupt_file(self): self.make_b_or_c_py() out = self.run_command("coverage run -p b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") self.assert_file_count(".coverage.*", 1) out = self.run_command("coverage run -p b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") # After two -p runs, there should be two .coverage.machine.123 files. @@ -134,7 +134,7 @@ def test_combine_parallel_data_with_a_corrupt_file(self): r"Coverage.py warning: Couldn't use data file '.*\.coverage\.bad': " r"file (is encrypted or )?is not a database" ) - self.assertRegex(out, warning_regex) + assert re.search(warning_regex, out) # After combining, those two should be the only data files. self.assert_file_count(".coverage.*", 1) @@ -143,13 +143,13 @@ def test_combine_parallel_data_with_a_corrupt_file(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 def test_combine_no_usable_files(self): # https://github.com/nedbat/coveragepy/issues/629 self.make_b_or_c_py() out = self.run_command("coverage run b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 0) @@ -159,7 +159,7 @@ def test_combine_no_usable_files(self): # Combine the parallel coverage data files into .coverage, but nothing is readable. status, out = self.run_command_status("coverage combine") - self.assertEqual(status, 1) + assert status == 1 for n in "12": self.assert_exists(".coverage.bad{}".format(n)) @@ -168,8 +168,8 @@ def test_combine_no_usable_files(self): r"file (is encrypted or )?is not a database" .format(n) ) - self.assertRegex(out, warning_regex) - self.assertRegex(out, r"No usable data files") + assert re.search(warning_regex, out) + assert re.search(r"No usable data files", out) # After combining, we should have a main file and two parallel files. self.assert_exists(".coverage") @@ -179,13 +179,13 @@ def test_combine_no_usable_files(self): # executed (we only did b, not c). data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 6) + assert line_counts(data)['b_or_c.py'] == 6 def test_combine_parallel_data_in_two_steps(self): self.make_b_or_c_py() out = self.run_command("coverage run -p b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") self.assert_file_count(".coverage.*", 1) @@ -195,7 +195,7 @@ def test_combine_parallel_data_in_two_steps(self): self.assert_file_count(".coverage.*", 0) out = self.run_command("coverage run -p b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 1) @@ -210,13 +210,13 @@ def test_combine_parallel_data_in_two_steps(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 def test_combine_parallel_data_no_append(self): self.make_b_or_c_py() out = self.run_command("coverage run -p b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") self.assert_file_count(".coverage.*", 1) @@ -226,7 +226,7 @@ def test_combine_parallel_data_no_append(self): self.assert_file_count(".coverage.*", 0) out = self.run_command("coverage run -p b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 1) @@ -242,18 +242,40 @@ def test_combine_parallel_data_no_append(self): # because we didn't keep the data from running b. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 7) + assert line_counts(data)['b_or_c.py'] == 7 + + def test_combine_parallel_data_keep(self): + self.make_b_or_c_py() + out = self.run_command("coverage run -p b_or_c.py b") + assert out == 'done\n' + self.assert_doesnt_exist(".coverage") + self.assert_file_count(".coverage.*", 1) + + out = self.run_command("coverage run -p b_or_c.py c") + assert out == 'done\n' + self.assert_doesnt_exist(".coverage") + + # After two -p runs, there should be two .coverage.machine.123 files. + self.assert_file_count(".coverage.*", 2) + + # Combine the parallel coverage data files into .coverage with the keep flag. + self.run_command("coverage combine --keep") + + # After combining, the .coverage file & the original combined file should still be there. + self.assert_exists(".coverage") + self.assert_file_count(".coverage.*", 2) + def test_append_data(self): self.make_b_or_c_py() out = self.run_command("coverage run b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 0) out = self.run_command("coverage run --append b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 0) @@ -261,7 +283,7 @@ def test_append_data(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 def test_append_data_with_different_file(self): self.make_b_or_c_py() @@ -272,12 +294,12 @@ def test_append_data_with_different_file(self): """) out = self.run_command("coverage run b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") self.assert_exists(".mycovdata") out = self.run_command("coverage run --append b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") self.assert_exists(".mycovdata") @@ -285,13 +307,13 @@ def test_append_data_with_different_file(self): # executed. data = coverage.CoverageData(".mycovdata") data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 def test_append_can_create_a_data_file(self): self.make_b_or_c_py() out = self.run_command("coverage run --append b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_exists(".coverage") self.assert_file_count(".coverage.*", 0) @@ -299,7 +321,7 @@ def test_append_can_create_a_data_file(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 6) + assert line_counts(data)['b_or_c.py'] == 6 def test_combine_with_rc(self): self.make_b_or_c_py() @@ -311,11 +333,11 @@ def test_combine_with_rc(self): """) out = self.run_command("coverage run b_or_c.py b") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") out = self.run_command("coverage run b_or_c.py c") - self.assertEqual(out, 'done\n') + assert out == 'done\n' self.assert_doesnt_exist(".coverage") # After two runs, there should be two .coverage.machine.123 files. @@ -333,17 +355,17 @@ def test_combine_with_rc(self): # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['b_or_c.py'], 8) + assert line_counts(data)['b_or_c.py'] == 8 # Reporting should still work even with the .rc file out = self.run_command("coverage report") - self.assertMultiLineEqual(out, textwrap.dedent("""\ + assert out == textwrap.dedent("""\ Name Stmts Miss Cover ------------------------------- b_or_c.py 8 0 100% ------------------------------- TOTAL 8 0 100% - """)) + """) def test_combine_with_aliases(self): self.make_file("d1/x.py", """\ @@ -374,9 +396,9 @@ def test_combine_with_aliases(self): """) out = self.run_command("coverage run " + os.path.normpath("d1/x.py")) - self.assertEqual(out, '1 2\n') + assert out == '1 2\n' out = self.run_command("coverage run " + os.path.normpath("d2/x.py")) - self.assertEqual(out, '4 5\n') + assert out == '4 5\n' self.assert_file_count(".coverage.*", 2) @@ -391,11 +413,11 @@ def test_combine_with_aliases(self): data = coverage.CoverageData() data.read() summary = line_counts(data, fullpath=True) - self.assertEqual(len(summary), 1) + assert len(summary) == 1 actual = abs_file(list(summary.keys())[0]) expected = abs_file('src/x.py') - self.assertEqual(expected, actual) - self.assertEqual(list(summary.values())[0], 6) + assert expected == actual + assert list(summary.values())[0] == 6 def test_erase_parallel(self): self.make_file(".coveragerc", """\ @@ -423,8 +445,8 @@ def test_missing_source_file(self): self.run_command("coverage run fleeting.py") os.remove("fleeting.py") out = self.run_command("coverage html -d htmlcov") - self.assertRegex(out, "No source for code: '.*fleeting.py'") - self.assertNotIn("Traceback", out) + assert re.search("No source for code: '.*fleeting.py'", out) + assert "Traceback" not in out # It happens that the code paths are different for *.py and other # files, so try again with no extension. @@ -435,16 +457,16 @@ def test_missing_source_file(self): self.run_command("coverage run fleeting") os.remove("fleeting") status, out = self.run_command_status("coverage html -d htmlcov") - self.assertRegex(out, "No source for code: '.*fleeting'") - self.assertNotIn("Traceback", out) - self.assertEqual(status, 1) + assert re.search("No source for code: '.*fleeting'", out) + assert "Traceback" not in out + assert status == 1 def test_running_missing_file(self): status, out = self.run_command_status("coverage run xyzzy.py") - self.assertRegex(out, "No file to run: .*xyzzy.py") - self.assertNotIn("raceback", out) - self.assertNotIn("rror", out) - self.assertEqual(status, 1) + assert re.search("No file to run: .*xyzzy.py", out) + assert "raceback" not in out + assert "rror" not in out + assert status == 1 def test_code_throws(self): self.make_file("throw.py", """\ @@ -464,14 +486,14 @@ def f2(): if env.PYPY: # Pypy has an extra frame in the traceback for some reason out2 = re_lines(out2, "toplevel", match=False) - self.assertMultiLineEqual(out, out2) + assert out == out2 # But also make sure that the output is what we expect. path = python_reported_file('throw.py') msg = 'File "{}", line 5,? in f2'.format(re.escape(path)) - self.assertRegex(out, msg) - self.assertIn('raise Exception("hey!")', out) - self.assertEqual(status, 1) + assert re.search(msg, out) + assert 'raise Exception("hey!")' in out + assert status == 1 def test_code_exits(self): self.make_file("exit.py", """\ @@ -490,10 +512,10 @@ def f2(): # same output. No traceback. status, out = self.run_command_status("coverage run exit.py") status2, out2 = self.run_command_status("python exit.py") - self.assertMultiLineEqual(out, out2) - self.assertMultiLineEqual(out, "about to exit..\n") - self.assertEqual(status, status2) - self.assertEqual(status, 17) + assert out == out2 + assert out == "about to exit..\n" + assert status == status2 + assert status == 17 def test_code_exits_no_arg(self): self.make_file("exit_none.py", """\ @@ -506,15 +528,13 @@ def f1(): """) status, out = self.run_command_status("coverage run exit_none.py") status2, out2 = self.run_command_status("python exit_none.py") - self.assertMultiLineEqual(out, out2) - self.assertMultiLineEqual(out, "about to exit quietly..\n") - self.assertEqual(status, status2) - self.assertEqual(status, 0) + assert out == out2 + assert out == "about to exit quietly..\n" + assert status == status2 + assert status == 0 + @pytest.mark.skipif(not hasattr(os, "fork"), reason="Can't test os.fork, it doesn't exist.") def test_fork(self): - if not hasattr(os, 'fork'): - self.skipTest("Can't test os.fork since it doesn't exist.") - self.make_file("fork.py", """\ import os @@ -533,7 +553,7 @@ def main(): """) out = self.run_command("coverage run -p fork.py") - self.assertEqual(out, 'Child!\n') + assert out == 'Child!\n' self.assert_doesnt_exist(".coverage") # After running the forking program, there should be two @@ -544,7 +564,7 @@ def main(): # the file name. data_files = glob.glob(".coverage.*") nums = set(name.rpartition(".")[-1] for name in data_files) - self.assertEqual(len(nums), 2, "Same random: %s" % (data_files,)) + assert len(nums) == 2, "Same random: %s" % (data_files,) # Combine the parallel coverage data files into .coverage . self.run_command("coverage combine") @@ -555,7 +575,7 @@ def main(): data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['fork.py'], 9) + assert line_counts(data)['fork.py'] == 9 def test_warnings_during_reporting(self): # While fixing issue #224, the warnings were being printed far too @@ -576,7 +596,7 @@ def test_warnings_during_reporting(self): self.run_command("coverage run hello.py") out = self.run_command("coverage html") - self.assertEqual(out.count("Module xyzzy was never imported."), 0) + assert out.count("Module xyzzy was never imported.") == 0 def test_warns_if_never_run(self): # Note: the name of the function can't have "warning" in it, or the @@ -584,22 +604,21 @@ def test_warns_if_never_run(self): # will fail. out = self.run_command("coverage run i_dont_exist.py") path = python_reported_file('i_dont_exist.py') - self.assertIn("No file to run: '{}'".format(path), out) - self.assertNotIn("warning", out) - self.assertNotIn("Exception", out) + assert "No file to run: '{}'".format(path) in out + assert "warning" not in out + assert "Exception" not in out out = self.run_command("coverage run -m no_such_module") - self.assertTrue( + assert ( ("No module named no_such_module" in out) or ("No module named 'no_such_module'" in out) - ) - self.assertNotIn("warning", out) - self.assertNotIn("Exception", out) + ) + assert "warning" not in out + assert "Exception" not in out + @pytest.mark.skipif(env.METACOV, reason="Can't test tracers changing during metacoverage") def test_warnings_trace_function_changed_with_threads(self): # https://github.com/nedbat/coveragepy/issues/164 - if env.METACOV: - self.skipTest("Can't test tracers changing during metacoverage") self.make_file("bug164.py", """\ import threading @@ -615,8 +634,8 @@ def run(self): """) out = self.run_command("coverage run --timid bug164.py") - self.assertIn("Hello\n", out) - self.assertNotIn("warning", out) + assert "Hello\n" in out + assert "warning" not in out def test_warning_trace_function_changed(self): self.make_file("settrace.py", """\ @@ -626,11 +645,14 @@ def test_warning_trace_function_changed(self): print("Goodbye") """) out = self.run_command("coverage run --timid settrace.py") - self.assertIn("Hello\n", out) - self.assertIn("Goodbye\n", out) + assert "Hello\n" in out + assert "Goodbye\n" in out - self.assertIn("Trace function changed", out) + assert "Trace function changed" in out + # When meta-coverage testing, this test doesn't work, because it finds + # coverage.py's own trace function. + @pytest.mark.skipif(env.METACOV, reason="Can't test timid during coverage measurement.") def test_timid(self): # Test that the --timid command line argument properly swaps the tracer # function for a simpler one. @@ -641,11 +663,6 @@ def test_timid(self): # an environment variable set in igor.py to know whether to expect to see # the C trace function or not. - # When meta-coverage testing, this test doesn't work, because it finds - # coverage.py's own trace function. - if os.environ.get('COVERAGE_COVERAGE', ''): - self.skipTest("Can't test timid during coverage measurement.") - self.make_file("showtrace.py", """\ # Show the current frame's trace function, so that we can test what the # command-line options do to the trace function used. @@ -674,21 +691,21 @@ def test_timid(self): # When running without coverage, no trace function py_out = self.run_command("python showtrace.py") - self.assertEqual(py_out, "None\n") + assert py_out == "None\n" cov_out = self.run_command("coverage run showtrace.py") if os.environ.get('COVERAGE_TEST_TRACER', 'c') == 'c': # If the C trace function is being tested, then regular running should have # the C function, which registers itself as f_trace. - self.assertEqual(cov_out, "CTracer\n") + assert cov_out == "CTracer\n" else: # If the Python trace function is being tested, then regular running will # also show the Python function. - self.assertEqual(cov_out, "PyTracer\n") + assert cov_out == "PyTracer\n" # When running timidly, the trace function is always Python. timid_out = self.run_command("coverage run --timid showtrace.py") - self.assertEqual(timid_out, "PyTracer\n") + assert timid_out == "PyTracer\n" def test_warn_preimported(self): self.make_file("hello.py", """\ @@ -706,22 +723,19 @@ def f(): goodbye_path = os.path.abspath("goodbye.py") out = self.run_command("python hello.py") - self.assertIn("Goodbye!", out) + assert "Goodbye!" in out msg = ( "Coverage.py warning: " "Already imported a file that will be measured: {0} " "(already-imported)").format(goodbye_path) - self.assertIn(msg, out) + assert msg in out @pytest.mark.expensive - def test_fullcoverage(self): # pragma: no metacov - if env.PY2: # This doesn't work on Python 2. - self.skipTest("fullcoverage doesn't work on Python 2.") - # It only works with the C tracer, and if we aren't measuring ourselves. - if not env.C_TRACER or env.METACOV: - self.skipTest("fullcoverage only works with the C tracer.") - + @pytest.mark.skipif(env.METACOV, reason="Can't test fullcoverage when measuring ourselves") + @pytest.mark.skipif(env.PY2, reason="fullcoverage doesn't work on Python 2.") + @pytest.mark.skipif(not env.C_TRACER, reason="fullcoverage only works with the C tracer.") + def test_fullcoverage(self): # fullcoverage is a trick to get stdlib modules measured from # the very beginning of the process. Here we import os and # then check how many lines are measured. @@ -736,22 +750,21 @@ def test_fullcoverage(self): # pragma: no metacov self.set_environ("FOOEY", "BOO") self.set_environ("PYTHONPATH", fullcov) out = self.run_command("python -m coverage run -L getenv.py") - self.assertEqual(out, "FOOEY == BOO\n") + assert out == "FOOEY == BOO\n" data = coverage.CoverageData() data.read() # The actual number of executed lines in os.py when it's # imported is 120 or so. Just running os.getenv executes # about 5. - self.assertGreater(line_counts(data)['os.py'], 50) + assert line_counts(data)['os.py'] > 50 @xfail( env.PYPY3 and (env.PYPYVERSION >= (7, 1, 1)), "https://bitbucket.org/pypy/pypy/issues/3074" ) + # Jython as of 2.7.1rc3 won't compile a filename that isn't utf8. + @pytest.mark.skipif(env.JYTHON, reason="Jython can't handle this test") def test_lang_c(self): - if env.JYTHON: - # Jython as of 2.7.1rc3 won't compile a filename that isn't utf8. - self.skipTest("Jython can't handle this test") # LANG=C forces getfilesystemencoding on Linux to 'ascii', which causes # failures with non-ascii file names. We don't want to make a real file # with strange characters, though, because that gets the test runners @@ -766,7 +779,7 @@ def test_lang_c(self): """) self.set_environ("LANG", "C") out = self.run_command("coverage run weird_file.py") - self.assertEqual(out, "1\n2\n") + assert out == "1\n2\n" def test_deprecation_warnings(self): # Test that coverage doesn't trigger deprecation warnings. @@ -783,7 +796,7 @@ def test_deprecation_warnings(self): self.del_environ("COVERAGE_TESTING") out = self.run_command("python allok.py") - self.assertEqual(out, "No warnings!\n") + assert out == "No warnings!\n" def test_run_twice(self): # https://github.com/nedbat/coveragepy/issues/353 @@ -805,18 +818,18 @@ def foo(): inst.save() """) out = self.run_command("python run_twice.py") - self.assertEqual( - out, - "Run 1\n" - "Run 2\n" - "Coverage.py warning: Module foo was previously imported, but not measured " + expected = ( + "Run 1\n" + + "Run 2\n" + + "Coverage.py warning: Module foo was previously imported, but not measured " + "(module-not-measured)\n" ) + assert expected == out def test_module_name(self): # https://github.com/nedbat/coveragepy/issues/478 out = self.run_command("python -m coverage") - self.assertIn("Use 'coverage help' for help", out) + assert "Use 'coverage help' for help" in out TRY_EXECFILE = os.path.join(os.path.dirname(__file__), "modules/process_test/try_execfile.py") @@ -832,14 +845,14 @@ def assert_tryexecfile_output(self, expected, actual): """ # First, is this even credible try_execfile.py output? - self.assertIn('"DATA": "xyzzy"', actual) + assert '"DATA": "xyzzy"' in actual if env.JYTHON: # pragma: only jython # Argv0 is different for Jython, remove that from the comparison. expected = re_lines(expected, r'\s+"argv0":', match=False) actual = re_lines(actual, r'\s+"argv0":', match=False) - self.assertMultiLineEqual(expected, actual) + assert expected == actual def test_coverage_run_is_like_python(self): with open(TRY_EXECFILE) as f: @@ -861,9 +874,10 @@ def test_coverage_run_dashm_is_like_python_dashm(self): actual = self.run_command("coverage run -m process_test.try_execfile") self.assert_tryexecfile_output(expected, actual) + @pytest.mark.skipif(env.PYVERSION == (3, 5, 4, 'final', 0, 0), + reason="3.5.4 broke this: https://bugs.python.org/issue32551" + ) def test_coverage_run_dir_is_like_python_dir(self): - if env.PYVERSION == (3, 5, 4, 'final', 0, 0): # pragma: obscure - self.skipTest("3.5.4 broke this: https://bugs.python.org/issue32551") with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) @@ -891,9 +905,10 @@ def test_coverage_run_dashm_dir_no_init_is_like_python(self): else: self.assert_tryexecfile_output(expected, actual) + @pytest.mark.skipif(env.PY2, + reason="Python 2 runs __main__ twice, I can't be bothered to make it work." + ) def test_coverage_run_dashm_dir_with_init_is_like_python(self): - if env.PY2: - self.skipTest("Python 2 runs __main__ twice, I can't be bothered to make it work.") with open(TRY_EXECFILE) as f: self.make_file("with_main/__main__.py", f.read()) self.make_file("with_main/__init__.py", "") @@ -936,8 +951,8 @@ def test_coverage_run_dashm_superset_of_doubledashsource(self): self.assert_tryexecfile_output(expected, actual) st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual(self.line_count(out), 6, out) + assert st == 0 + assert self.line_count(out) == 6, out def test_coverage_run_script_imports_doubledashsource(self): # This file imports try_execfile, which compiles it to .pyc, so the @@ -955,8 +970,8 @@ def test_coverage_run_script_imports_doubledashsource(self): self.assert_tryexecfile_output(expected, actual) st, out = self.run_command_status("coverage report") - self.assertEqual(st, 0) - self.assertEqual(self.line_count(out), 6, out) + assert st == 0 + assert self.line_count(out) == 6, out def test_coverage_run_dashm_is_like_python_dashm_off_path(self): # https://github.com/nedbat/coveragepy/issues/242 @@ -974,7 +989,7 @@ def test_coverage_run_dashm_is_like_python_dashm_with__main__207(self): self.make_file("package/__main__.py", "print('main')") expected = self.run_command("python -m package") actual = self.run_command("coverage run -m package") - self.assertMultiLineEqual(expected, actual) + assert expected == actual def test_coverage_zip_is_like_python(self): # Test running coverage from a zip file itself. Some environments @@ -1015,14 +1030,13 @@ def test_coverage_custom_script(self): """) # If this test fails, it will be with "can't import thing". out = self.run_command("python run_coverage.py run how_is_it.py") - self.assertIn("hello-xyzzy", out) + assert "hello-xyzzy" in out out = self.run_command("python -m run_coverage run how_is_it.py") - self.assertIn("hello-xyzzy", out) + assert "hello-xyzzy" in out + @pytest.mark.skipif(env.WINDOWS, reason="Windows can't make symlinks") def test_bug_862(self): - if env.WINDOWS: - self.skipTest("Windows can't make symlinks") # This simulates how pyenv and pyenv-virtualenv end up creating the # coverage executable. self.make_file("elsewhere/bin/fake-coverage", """\ @@ -1035,7 +1049,7 @@ def test_bug_862(self): self.make_file("foo.py", "print('inside foo')") self.make_file("bar.py", "import foo") out = self.run_command("somewhere/bin/fake-coverage run bar.py") - self.assertEqual("inside foo\n", out) + assert "inside foo\n" == out def test_bug_909(self): # https://github.com/nedbat/coveragepy/issues/909 @@ -1086,21 +1100,22 @@ def excepthook(*args): cov_st, cov_out = self.run_command_status("coverage run excepthook.py") py_st, py_out = self.run_command_status("python excepthook.py") if not env.JYTHON: - self.assertEqual(cov_st, py_st) - self.assertEqual(cov_st, 1) + assert cov_st == py_st + assert cov_st == 1 - self.assertIn("in excepthook", py_out) - self.assertEqual(cov_out, py_out) + assert "in excepthook" in py_out + assert cov_out == py_out # Read the coverage file and see that excepthook.py has 7 lines # executed. data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['excepthook.py'], 7) + assert line_counts(data)['excepthook.py'] == 7 + @pytest.mark.skipif(not env.CPYTHON, + reason="non-CPython handles excepthook exits differently, punt for now." + ) def test_excepthook_exit(self): - if not env.CPYTHON: - self.skipTest("non-CPython handles excepthook exits differently, punt for now.") self.make_file("excepthook_exit.py", """\ import sys @@ -1114,15 +1129,14 @@ def excepthook(*args): """) cov_st, cov_out = self.run_command_status("coverage run excepthook_exit.py") py_st, py_out = self.run_command_status("python excepthook_exit.py") - self.assertEqual(cov_st, py_st) - self.assertEqual(cov_st, 0) + assert cov_st == py_st + assert cov_st == 0 - self.assertIn("in excepthook", py_out) - self.assertEqual(cov_out, py_out) + assert "in excepthook" in py_out + assert cov_out == py_out + @pytest.mark.skipif(env.PYPY, reason="PyPy handles excepthook throws differently.") def test_excepthook_throw(self): - if env.PYPY: - self.skipTest("PyPy handles excepthook throws differently, punt for now.") self.make_file("excepthook_throw.py", """\ import sys @@ -1140,41 +1154,37 @@ def excepthook(*args): cov_st, cov_out = self.run_command_status("coverage run excepthook_throw.py") py_st, py_out = self.run_command_status("python excepthook_throw.py") if not env.JYTHON: - self.assertEqual(cov_st, py_st) - self.assertEqual(cov_st, 1) + assert cov_st == py_st + assert cov_st == 1 - self.assertIn("in excepthook", py_out) - self.assertEqual(cov_out, py_out) + assert "in excepthook" in py_out + assert cov_out == py_out +@pytest.mark.skipif(env.JYTHON, reason="Coverage command names don't work on Jython") class AliasedCommandTest(CoverageTest): """Tests of the version-specific command aliases.""" run_in_temp_dir = False - def setUp(self): - if env.JYTHON: - self.skipTest("Coverage command names don't work on Jython") - super(AliasedCommandTest, self).setUp() - def test_major_version_works(self): # "coverage2" works on py2 cmd = "coverage%d" % sys.version_info[0] out = self.run_command(cmd) - self.assertIn("Code coverage for Python", out) + assert "Code coverage for Python" in out def test_wrong_alias_doesnt_work(self): # "coverage3" doesn't work on py2 assert sys.version_info[0] in [2, 3] # Let us know when Python 4 is out... badcmd = "coverage%d" % (5 - sys.version_info[0]) out = self.run_command(badcmd) - self.assertNotIn("Code coverage for Python", out) + assert "Code coverage for Python" not in out def test_specific_alias_works(self): # "coverage-2.7" works on py2.7 cmd = "coverage-%d.%d" % sys.version_info[:2] out = self.run_command(cmd) - self.assertIn("Code coverage for Python", out) + assert "Code coverage for Python" in out def test_aliases_used_in_messages(self): cmds = [ @@ -1184,8 +1194,8 @@ def test_aliases_used_in_messages(self): ] for cmd in cmds: out = self.run_command("%s foobar" % cmd) - self.assertIn("Unknown command: 'foobar'", out) - self.assertIn("Use '%s help' for help" % cmd, out) + assert "Unknown command: 'foobar'" in out + assert "Use '%s help' for help" % cmd in out class PydocTest(CoverageTest): @@ -1199,11 +1209,11 @@ def assert_pydoc_ok(self, name, thing): out = self.run_command("python -m pydoc " + name) # It should say "Help on..", and not have a traceback self.assert_starts_with(out, "Help on ") - self.assertNotIn("Traceback", out) + assert "Traceback" not in out # All of the lines in the docstring should be there somewhere. for line in thing.__doc__.splitlines(): - self.assertIn(line.strip(), out) + assert line.strip() in out def test_pydoc_coverage(self): self.assert_pydoc_ok("coverage", coverage) @@ -1228,29 +1238,25 @@ def setUp(self): e = 7 """) st, _ = self.run_command_status("coverage run --source=. forty_two_plus.py") - self.assertEqual(st, 0) + assert st == 0 def test_report_43_is_ok(self): st, out = self.run_command_status("coverage report --fail-under=43") - self.assertEqual(st, 0) - self.assertEqual(self.last_line_squeezed(out), "TOTAL 7 4 43%") + assert st == 0 + assert self.last_line_squeezed(out) == "TOTAL 7 4 43%" def test_report_43_is_not_ok(self): st, out = self.run_command_status("coverage report --fail-under=44") - self.assertEqual(st, 2) - self.assertEqual( - self.last_line_squeezed(out), - "Coverage failure: total of 43 is less than fail-under=44" - ) + assert st == 2 + expected = "Coverage failure: total of 43 is less than fail-under=44" + assert expected == self.last_line_squeezed(out) def test_report_42p86_is_not_ok(self): self.make_file(".coveragerc", "[report]\nprecision = 2") st, out = self.run_command_status("coverage report --fail-under=42.88") - self.assertEqual(st, 2) - self.assertEqual( - self.last_line_squeezed(out), - "Coverage failure: total of 42.86 is less than fail-under=42.88" - ) + assert st == 2 + expected = "Coverage failure: total of 42.86 is less than fail-under=42.88" + assert expected == self.last_line_squeezed(out) class FailUnderNoFilesTest(CoverageTest): @@ -1258,8 +1264,8 @@ class FailUnderNoFilesTest(CoverageTest): def test_report(self): self.make_file(".coveragerc", "[report]\nfail_under = 99\n") st, out = self.run_command_status("coverage report") - self.assertIn('No data to report.', out) - self.assertEqual(st, 1) + assert 'No data to report.' in out + assert st == 1 class FailUnderEmptyFilesTest(CoverageTest): @@ -1268,40 +1274,36 @@ def test_report(self): self.make_file(".coveragerc", "[report]\nfail_under = 99\n") self.make_file("empty.py", "") st, _ = self.run_command_status("coverage run empty.py") - self.assertEqual(st, 0) + assert st == 0 st, _ = self.run_command_status("coverage report") - self.assertEqual(st, 2) + assert st == 2 +@pytest.mark.skipif(env.JYTHON, reason="Jython doesn't like accented file names") class UnicodeFilePathsTest(CoverageTest): """Tests of using non-ascii characters in the names of files.""" - def setUp(self): - if env.JYTHON: - self.skipTest("Jython doesn't like accented file names") - super(UnicodeFilePathsTest, self).setUp() - def test_accented_dot_py(self): # Make a file with a non-ascii character in the filename. self.make_file(u"h\xe2t.py", "print('accented')") out = self.run_command(u"coverage run --source=. h\xe2t.py") - self.assertEqual(out, "accented\n") + assert out == "accented\n" # The HTML report uses ascii-encoded HTML entities. out = self.run_command("coverage html") - self.assertEqual(out, "") + assert out == "" self.assert_exists(u"htmlcov/h\xe2t_py.html") with open("htmlcov/index.html") as indexf: index = indexf.read() - self.assertIn('hât.py', index) + assert 'hât.py' in index # The XML report is always UTF8-encoded. out = self.run_command("coverage xml") - self.assertEqual(out, "") + assert out == "" with open("coverage.xml", "rb") as xmlf: xml = xmlf.read() - self.assertIn(u' filename="h\xe2t.py"'.encode('utf8'), xml) - self.assertIn(u' name="h\xe2t.py"'.encode('utf8'), xml) + assert u' filename="h\xe2t.py"'.encode('utf8') in xml + assert u' name="h\xe2t.py"'.encode('utf8') in xml report_expected = ( u"Name Stmts Miss Cover\n" @@ -1315,29 +1317,29 @@ def test_accented_dot_py(self): report_expected = report_expected.encode(output_encoding()) out = self.run_command("coverage report") - self.assertEqual(out, report_expected) + assert out == report_expected def test_accented_directory(self): # Make a file with a non-ascii character in the directory name. self.make_file(u"\xe2/accented.py", "print('accented')") out = self.run_command(u"coverage run --source=. \xe2/accented.py") - self.assertEqual(out, "accented\n") + assert out == "accented\n" # The HTML report uses ascii-encoded HTML entities. out = self.run_command("coverage html") - self.assertEqual(out, "") + assert out == "" self.assert_exists(u"htmlcov/\xe2_accented_py.html") with open("htmlcov/index.html") as indexf: index = indexf.read() - self.assertIn('â%saccented.py' % os.sep, index) + assert 'â%saccented.py' % os.sep in index # The XML report is always UTF8-encoded. out = self.run_command("coverage xml") - self.assertEqual(out, "") + assert out == "" with open("coverage.xml", "rb") as xmlf: xml = xmlf.read() - self.assertIn(b' filename="\xc3\xa2/accented.py"', xml) - self.assertIn(b' name="accented.py"', xml) + assert b' filename="\xc3\xa2/accented.py"' in xml + assert b' name="accented.py"' in xml dom = ElementTree.parse("coverage.xml") elts = dom.findall(u".//package[@name='â']") @@ -1361,17 +1363,13 @@ def test_accented_directory(self): report_expected = report_expected.encode(output_encoding()) out = self.run_command("coverage report") - self.assertEqual(out, report_expected) + assert out == report_expected +@pytest.mark.skipif(env.WINDOWS, reason="Windows can't delete the directory in use.") class YankedDirectoryTest(CoverageTest): """Tests of what happens when the current directory is deleted.""" - def setUp(self): - if env.WINDOWS: - self.skipTest("Windows can't delete the directory in use.") - super(YankedDirectoryTest, self).setUp() - BUG_806 = """\ import os import sys @@ -1386,18 +1384,18 @@ def setUp(self): def test_removing_directory(self): self.make_file("bug806.py", self.BUG_806) out = self.run_command("coverage run bug806.py noerror") - self.assertEqual(out, "noerror\n") + assert out == "noerror\n" def test_removing_directory_with_error(self): self.make_file("bug806.py", self.BUG_806) out = self.run_command("coverage run bug806.py") path = python_reported_file('bug806.py') - self.assertEqual(out, textwrap.dedent("""\ + assert out == textwrap.dedent("""\ Traceback (most recent call last): File "{}", line 8, in print(sys.argv[1]) IndexError: list index out of range - """.format(path))) + """.format(path)) def possible_pth_dirs(): @@ -1454,7 +1452,7 @@ def setUp(self): super(ProcessCoverageMixin, self).setUp() # Create the .pth file. - self.assertTrue(PTH_DIR) + assert PTH_DIR pth_contents = "import coverage; coverage.process_startup()\n" pth_path = os.path.join(PTH_DIR, "subcover_{}.pth".format(WORKER)) with open(pth_path, "w") as pth: @@ -1464,6 +1462,7 @@ def setUp(self): self.addCleanup(persistent_remove, self.pth_path) +@pytest.mark.skipif(env.METACOV, reason="Can't test sub-process pth file during metacoverage") class ProcessStartupTest(ProcessCoverageMixin, CoverageTest): """Test that we can measure coverage in sub-processes.""" @@ -1483,10 +1482,7 @@ def setUp(self): f.close() """) - def test_subprocess_with_pth_files(self): # pragma: no metacov - if env.METACOV: - self.skipTest("Can't test sub-process pth file suppport during metacoverage") - + def test_subprocess_with_pth_files(self): # An existing data file should not be read when a subprocess gets # measured automatically. Create the data file here with bogus data in # it. @@ -1499,22 +1495,19 @@ def test_subprocess_with_pth_files(self): # pragma: no metacov data_file = .mycovdata """) self.set_environ("COVERAGE_PROCESS_START", "coverage.ini") - import main # pylint: disable=unused-import + import main # pylint: disable=unused-import, import-error with open("out.txt") as f: - self.assertEqual(f.read(), "Hello, world!\n") + assert f.read() == "Hello, world!\n" # Read the data from .coverage self.assert_exists(".mycovdata") data = coverage.CoverageData(".mycovdata") data.read() - self.assertEqual(line_counts(data)['sub.py'], 3) + assert line_counts(data)['sub.py'] == 3 - def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov + def test_subprocess_with_pth_files_and_parallel(self): # https://github.com/nedbat/coveragepy/issues/492 - if env.METACOV: - self.skipTest("Can't test sub-process pth file suppport during metacoverage") - self.make_file("coverage.ini", """\ [run] parallel = true @@ -1524,7 +1517,7 @@ def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov self.run_command("coverage run main.py") with open("out.txt") as f: - self.assertEqual(f.read(), "Hello, world!\n") + assert f.read() == "Hello, world!\n" self.run_command("coverage combine") @@ -1532,13 +1525,15 @@ def test_subprocess_with_pth_files_and_parallel(self): # pragma: no metacov self.assert_exists(".coverage") data = coverage.CoverageData() data.read() - self.assertEqual(line_counts(data)['sub.py'], 3) + assert line_counts(data)['sub.py'] == 3 # assert that there are *no* extra data files left over after a combine data_files = glob.glob(os.getcwd() + '/.coverage*') - self.assertEqual(len(data_files), 1, - "Expected only .coverage after combine, looks like there are " - "extra data files that were not cleaned up: %r" % data_files) + msg = ( + "Expected only .coverage after combine, looks like there are " + + "extra data files that were not cleaned up: %r" % data_files + ) + assert len(data_files) == 1, msg class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): @@ -1557,7 +1552,7 @@ class ProcessStartupWithSourceTest(ProcessCoverageMixin, CoverageTest): def assert_pth_and_source_work_together( self, dashm, package, source - ): # pragma: no metacov + ): """Run the test for a particular combination of factors. The arguments are all strings: @@ -1572,9 +1567,6 @@ def assert_pth_and_source_work_together( ``--source`` argument. """ - if env.METACOV: - self.skipTest("Can't test sub-process pth file support during metacoverage") - def fullname(modname): """What is the full module name for `modname` for this test?""" if package and dashm: @@ -1616,7 +1608,7 @@ def path(basename): self.run_command(cmd) with open("out.txt") as f: - self.assertEqual(f.read(), "Hello, world!") + assert f.read() == "Hello, world!" # Read the data from .coverage self.assert_exists(".coverage") @@ -1624,8 +1616,8 @@ def path(basename): data.read() summary = line_counts(data) print(summary) - self.assertEqual(summary[source + '.py'], 3) - self.assertEqual(len(summary), 1) + assert summary[source + '.py'] == 3 + assert len(summary) == 1 def test_dashm_main(self): self.assert_pth_and_source_work_together('-m', '', 'main') diff --git a/tests/test_python.py b/tests/test_python.py index 441ef499a..0175f5afd 100644 --- a/tests/test_python.py +++ b/tests/test_python.py @@ -28,7 +28,7 @@ def test_get_encoded_zip_files(self): filename = filename.replace("/", os.sep) zip_data = get_zip_bytes(filename) zip_text = zip_data.decode(encoding) - self.assertIn('All OK', zip_text) + assert 'All OK' in zip_text # Run the code to see that we really got it encoded properly. __import__("encoded_"+encoding) diff --git a/tests/test_results.py b/tests/test_results.py index 377c150bd..0453424b4 100644 --- a/tests/test_results.py +++ b/tests/test_results.py @@ -18,30 +18,30 @@ class NumbersTest(CoverageTest): def test_basic(self): n1 = Numbers(n_files=1, n_statements=200, n_missing=20) - self.assertEqual(n1.n_statements, 200) - self.assertEqual(n1.n_executed, 180) - self.assertEqual(n1.n_missing, 20) - self.assertEqual(n1.pc_covered, 90) + assert n1.n_statements == 200 + assert n1.n_executed == 180 + assert n1.n_missing == 20 + assert n1.pc_covered == 90 def test_addition(self): n1 = Numbers(n_files=1, n_statements=200, n_missing=20) n2 = Numbers(n_files=1, n_statements=10, n_missing=8) n3 = n1 + n2 - self.assertEqual(n3.n_files, 2) - self.assertEqual(n3.n_statements, 210) - self.assertEqual(n3.n_executed, 182) - self.assertEqual(n3.n_missing, 28) - self.assertAlmostEqual(n3.pc_covered, 86.666666666) + assert n3.n_files == 2 + assert n3.n_statements == 210 + assert n3.n_executed == 182 + assert n3.n_missing == 28 + assert round(abs(n3.pc_covered-86.666666666), 7) == 0 def test_sum(self): n1 = Numbers(n_files=1, n_statements=200, n_missing=20) n2 = Numbers(n_files=1, n_statements=10, n_missing=8) n3 = sum([n1, n2]) - self.assertEqual(n3.n_files, 2) - self.assertEqual(n3.n_statements, 210) - self.assertEqual(n3.n_executed, 182) - self.assertEqual(n3.n_missing, 28) - self.assertAlmostEqual(n3.pc_covered, 86.666666666) + assert n3.n_files == 2 + assert n3.n_statements == 210 + assert n3.n_executed == 182 + assert n3.n_missing == 28 + assert round(abs(n3.pc_covered-86.666666666), 7) == 0 def test_pc_covered_str(self): # Numbers._precision is a global, which is bad. @@ -50,10 +50,10 @@ def test_pc_covered_str(self): n1 = Numbers(n_files=1, n_statements=1000, n_missing=1) n999 = Numbers(n_files=1, n_statements=1000, n_missing=999) n1000 = Numbers(n_files=1, n_statements=1000, n_missing=1000) - self.assertEqual(n0.pc_covered_str, "100") - self.assertEqual(n1.pc_covered_str, "99") - self.assertEqual(n999.pc_covered_str, "1") - self.assertEqual(n1000.pc_covered_str, "0") + assert n0.pc_covered_str == "100" + assert n1.pc_covered_str == "99" + assert n999.pc_covered_str == "1" + assert n1000.pc_covered_str == "0" def test_pc_covered_str_precision(self): # Numbers._precision is a global, which is bad. @@ -62,21 +62,21 @@ def test_pc_covered_str_precision(self): n1 = Numbers(n_files=1, n_statements=10000, n_missing=1) n9999 = Numbers(n_files=1, n_statements=10000, n_missing=9999) n10000 = Numbers(n_files=1, n_statements=10000, n_missing=10000) - self.assertEqual(n0.pc_covered_str, "100.0") - self.assertEqual(n1.pc_covered_str, "99.9") - self.assertEqual(n9999.pc_covered_str, "0.1") - self.assertEqual(n10000.pc_covered_str, "0.0") + assert n0.pc_covered_str == "100.0" + assert n1.pc_covered_str == "99.9" + assert n9999.pc_covered_str == "0.1" + assert n10000.pc_covered_str == "0.0" Numbers.set_precision(0) def test_covered_ratio(self): n = Numbers(n_files=1, n_statements=200, n_missing=47) - self.assertEqual(n.ratio_covered, (153, 200)) + assert n.ratio_covered == (153, 200) n = Numbers( n_files=1, n_statements=200, n_missing=47, n_branches=10, n_missing_branches=3, n_partial_branches=1000, ) - self.assertEqual(n.ratio_covered, (160, 210)) + assert n.ratio_covered == (160, 210) @pytest.mark.parametrize("total, fail_under, precision, result", [ diff --git a/tests/test_setup.py b/tests/test_setup.py index 9ab103914..febc383ea 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -24,12 +24,12 @@ def test_metadata(self): status, output = self.run_command_status( "python setup.py --description --version --url --author" ) - self.assertEqual(status, 0) + assert status == 0 out = output.splitlines() - self.assertIn("measurement", out[0]) - self.assertEqual(coverage.__version__, out[1]) - self.assertIn("github.com/nedbat/coveragepy", out[2]) - self.assertIn("Ned Batchelder", out[3]) + assert "measurement" in out[0] + assert coverage.__version__ == out[1] + assert "github.com/nedbat/coveragepy" in out[2] + assert "Ned Batchelder" in out[3] def test_more_metadata(self): # Let's be sure we pick up our own setup.py @@ -38,12 +38,12 @@ def test_more_metadata(self): from setup import setup_args classifiers = setup_args['classifiers'] - self.assertGreater(len(classifiers), 7) + assert len(classifiers) > 7 self.assert_starts_with(classifiers[-1], "Development Status ::") - self.assertIn("Programming Language :: Python :: %d" % sys.version_info[:1], classifiers) - self.assertIn("Programming Language :: Python :: %d.%d" % sys.version_info[:2], classifiers) + assert "Programming Language :: Python :: %d" % sys.version_info[:1] in classifiers + assert "Programming Language :: Python :: %d.%d" % sys.version_info[:2] in classifiers long_description = setup_args['long_description'].splitlines() - self.assertGreater(len(long_description), 7) - self.assertNotEqual(long_description[0].strip(), "") - self.assertNotEqual(long_description[-1].strip(), "") + assert len(long_description) > 7 + assert long_description[0].strip() != "" + assert long_description[-1].strip() != "" diff --git a/tests/test_summary.py b/tests/test_summary.py index feaa0fe0b..8596c45c4 100644 --- a/tests/test_summary.py +++ b/tests/test_summary.py @@ -10,6 +10,8 @@ import py_compile import re +import pytest + import coverage from coverage import env from coverage.backward import StringIO @@ -44,7 +46,7 @@ def omit_site_packages(self): def test_report(self): self.make_mycode() out = self.run_command("coverage run mycode.py") - self.assertEqual(out, 'done\n') + assert out == 'done\n' report = self.report_from_command("coverage report") # Name Stmts Miss Cover @@ -55,11 +57,11 @@ def test_report(self): # ------------------------------------------------------------------ # TOTAL 8 0 100% - self.assertNotIn("/coverage/__init__/", report) - self.assertIn("/tests/modules/covmod1.py ", report) - self.assertIn("/tests/zipmods.zip/covmodzip1.py ", report) - self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "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): # Try reporting just one module @@ -73,12 +75,12 @@ def test_report_just_one(self): # ------------------------------- # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 5) - self.assertNotIn("/coverage/", report) - self.assertNotIn("/tests/modules/covmod1.py ", report) - self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) - self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "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): # Try reporting using wildcards to get the modules. @@ -92,12 +94,12 @@ def test_report_wildcard(self): # ------------------------------- # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 5) - self.assertNotIn("/coverage/", report) - self.assertNotIn("/tests/modules/covmod1.py ", report) - self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) - self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "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): # Try reporting while omitting some modules @@ -112,12 +114,12 @@ def test_report_omitting(self): # ------------------------------- # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 5) - self.assertNotIn("/coverage/", report) - self.assertNotIn("/tests/modules/covmod1.py ", report) - self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) - self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "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): # Try reporting while including some modules @@ -131,12 +133,12 @@ def test_report_including(self): # ------------------------------- # TOTAL 4 0 100% - self.assertEqual(self.line_count(report), 5) - self.assertNotIn("/coverage/", report) - self.assertNotIn("/tests/modules/covmod1.py ", report) - self.assertNotIn("/tests/zipmods.zip/covmodzip1.py ", report) - self.assertIn("mycode.py ", report) - self.assertEqual(self.last_line_squeezed(report), "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_run_source_vs_report_include(self): # https://github.com/nedbat/coveragepy/issues/621 @@ -170,8 +172,8 @@ def test_run_omit_vs_report_omit(self): covdata = CoverageData() covdata.read() files = [os.path.basename(p) for p in covdata.measured_files()] - self.assertIn("covmod1.py", files) - self.assertNotIn("covmodzip1.py", files) + assert "covmod1.py" in files + assert "covmodzip1.py" not in files def test_report_branches(self): self.make_file("mybranch.py", """\ @@ -182,7 +184,7 @@ def branch(x): branch(1) """) out = self.run_command("coverage run --source=. --branch mybranch.py") - self.assertEqual(out, 'x\n') + assert out == 'x\n' report = self.report_from_command("coverage report") # Name Stmts Miss Branch BrPart Cover @@ -191,9 +193,9 @@ def branch(x): # ----------------------------------------------- # TOTAL 5 0 2 1 86% - self.assertEqual(self.line_count(report), 5) - self.assertIn("mybranch.py ", report) - self.assertEqual(self.last_line_squeezed(report), "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): self.make_file("mymissing.py", """\ @@ -213,7 +215,7 @@ def missing(x, y): missing(0, 1) """) out = self.run_command("coverage run --source=. mymissing.py") - self.assertEqual(out, 'y\nz\n') + assert out == 'y\nz\n' report = self.report_from_command("coverage report --show-missing") # Name Stmts Miss Cover Missing @@ -222,10 +224,10 @@ def missing(x, y): # -------------------------------------------- # TOTAL 14 3 79% 3-4, 10 - self.assertEqual(self.line_count(report), 5) + assert self.line_count(report) == 5 squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "mymissing.py 14 3 79% 3-4, 10") - self.assertEqual(squeezed[4], "TOTAL 14 3 79%") + 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): self.make_file("mybranch.py", """\ @@ -238,7 +240,7 @@ def branch(x, y): """) self.omit_site_packages() out = self.run_command("coverage run --branch mybranch.py") - self.assertEqual(out, 'x\ny\n') + assert out == 'x\ny\n' report = self.report_from_command("coverage report --show-missing") # Name Stmts Miss Branch BrPart Cover Missing @@ -247,10 +249,10 @@ def branch(x, y): # ---------------------------------------------------------- # TOTAL 6 0 4 2 80% - self.assertEqual(self.line_count(report), 5) + assert self.line_count(report) == 5 squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "mybranch.py 6 0 4 2 80% 2->4, 4->exit") - self.assertEqual(squeezed[4], "TOTAL 6 0 4 2 80%") + assert squeezed[2] == "mybranch.py 6 0 4 2 80% 2->4, 4->exit" + assert squeezed[4] == "TOTAL 6 0 4 2 80%" def test_report_show_missing_branches_and_lines(self): self.make_file("main.py", """\ @@ -270,7 +272,7 @@ def branch(x, y, z): """) self.omit_site_packages() out = self.run_command("coverage run --branch main.py") - self.assertEqual(out, 'x\ny\n') + assert out == 'x\ny\n' report = self.report_from_command("coverage report --show-missing") report_lines = report.splitlines() @@ -278,11 +280,11 @@ def branch(x, y, z): 'Name Stmts Miss Branch BrPart Cover Missing', '---------------------------------------------------------', 'main.py 1 0 0 0 100%', - 'mybranch.py 10 2 8 3 61% 2->4, 4->6, 6->7, 7-8', + 'mybranch.py 10 2 8 3 61% 2->4, 4->6, 7-8', '---------------------------------------------------------', 'TOTAL 11 2 8 3 63%', ] - self.assertEqual(expected, report_lines) + assert expected == report_lines def test_report_skip_covered_no_branches(self): self.make_file("main.py", """ @@ -298,7 +300,7 @@ def not_covered(): """) self.omit_site_packages() out = self.run_command("coverage run main.py") - self.assertEqual(out, "z\n") + assert out == "z\n" report = self.report_from_command("coverage report --skip-covered --fail-under=70") # Name Stmts Miss Cover @@ -309,12 +311,12 @@ def not_covered(): # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 7, report) + assert self.line_count(report) == 7, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "not_covered.py 2 1 50%") - self.assertEqual(squeezed[4], "TOTAL 6 1 83%") - self.assertEqual(squeezed[6], "1 file skipped due to complete coverage.") - self.assertEqual(self.last_command_status, 0) + 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): self.make_file("main.py", """ @@ -339,7 +341,7 @@ def foo(): """) self.omit_site_packages() out = self.run_command("coverage run --branch main.py") - self.assertEqual(out, "n\nz\n") + assert out == "n\nz\n" report = self.report_from_command("coverage report --skip-covered") # Name Stmts Miss Branch BrPart Cover @@ -350,11 +352,11 @@ def foo(): # # 2 files skipped due to complete coverage. - self.assertEqual(self.line_count(report), 7, report) + assert self.line_count(report) == 7, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "not_covered.py 4 0 2 1 83%") - self.assertEqual(squeezed[4], "TOTAL 13 0 4 1 94%") - self.assertEqual(squeezed[6], "2 files skipped due to complete coverage.") + 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): self.make_file("main.py", """ @@ -379,7 +381,7 @@ def does_not_appear_in_this_film(ni): """) self.omit_site_packages() out = self.run_command("coverage run --branch main.py") - self.assertEqual(out, "n\nz\n") + assert out == "n\nz\n" report = self.report_from_command("coverage report --skip-covered") # Name Stmts Miss Branch BrPart Cover @@ -391,12 +393,12 @@ def does_not_appear_in_this_film(ni): # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 8, report) + assert self.line_count(report) == 8, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "also_not_run.py 2 1 0 0 50%") - self.assertEqual(squeezed[3], "not_covered.py 4 0 2 1 83%") - self.assertEqual(squeezed[5], "TOTAL 13 1 4 1 88%") - self.assertEqual(squeezed[7], "1 file skipped due to complete coverage.") + 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): self.make_file("main.py", """ @@ -405,7 +407,7 @@ def foo(): foo() """) out = self.run_command("coverage run --source=. --branch main.py") - self.assertEqual(out, "") + assert out == "" report = self.report_from_command("coverage report --skip-covered") # Name Stmts Miss Branch BrPart Cover @@ -415,9 +417,9 @@ def foo(): # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 6, report) + assert self.line_count(report) == 6, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[5], "1 file skipped due to complete coverage.") + assert squeezed[5] == "1 file skipped due to complete coverage." def test_report_skip_covered_longfilename(self): self.make_file("long_______________filename.py", """ @@ -426,7 +428,7 @@ def foo(): foo() """) out = self.run_command("coverage run --source=. --branch long_______________filename.py") - self.assertEqual(out, "") + assert out == "" report = self.report_from_command("coverage report --skip-covered") # Name Stmts Miss Branch BrPart Cover @@ -436,20 +438,20 @@ def foo(): # # 1 file skipped due to complete coverage. - self.assertEqual(self.line_count(report), 6, report) + assert self.line_count(report) == 6, report lines = self.report_lines(report) - self.assertEqual(lines[0], "Name Stmts Miss Branch BrPart Cover") + assert lines[0] == "Name Stmts Miss Branch BrPart Cover" squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[5], "1 file skipped due to complete coverage.") + assert squeezed[5] == "1 file skipped due to complete coverage." def test_report_skip_covered_no_data(self): report = self.report_from_command("coverage report --skip-covered") # No data to report. - self.assertEqual(self.line_count(report), 1, report) + assert self.line_count(report) == 1, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[0], "No data to report.") + assert squeezed[0] == "No data to report." def test_report_skip_empty(self): self.make_file("main.py", """ @@ -462,7 +464,7 @@ def normal(): self.make_file("submodule/__init__.py", "") self.omit_site_packages() out = self.run_command("coverage run main.py") - self.assertEqual(out, "z\n") + assert out == "z\n" report = self.report_from_command("coverage report --skip-empty") # Name Stmts Miss Cover @@ -473,18 +475,18 @@ def normal(): # # 1 empty file skipped. - self.assertEqual(self.line_count(report), 7, report) + assert self.line_count(report) == 7, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "main.py 4 0 100%") - self.assertEqual(squeezed[4], "TOTAL 4 0 100%") - self.assertEqual(squeezed[6], "1 empty file skipped.") - self.assertEqual(self.last_command_status, 0) + assert squeezed[2] == "main.py 4 0 100%" + assert squeezed[4] == "TOTAL 4 0 100%" + assert squeezed[6] == "1 empty file skipped." + assert self.last_command_status == 0 def test_report_skip_empty_no_data(self): self.make_file("__init__.py", "") self.omit_site_packages() out = self.run_command("coverage run __init__.py") - self.assertEqual(out, "") + assert out == "" report = self.report_from_command("coverage report --skip-empty") # Name Stmts Miss Cover @@ -492,10 +494,10 @@ def test_report_skip_empty_no_data(self): # # 1 empty file skipped. - self.assertEqual(self.line_count(report), 6, report) + assert self.line_count(report) == 6, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[3], "TOTAL 0 0 100%") - self.assertEqual(squeezed[5], "1 empty file skipped.") + assert squeezed[3] == "TOTAL 0 0 100%" + assert squeezed[5] == "1 empty file skipped." def test_report_precision(self): self.make_file(".coveragerc", """\ @@ -524,7 +526,7 @@ def foo(): foo() """) out = self.run_command("coverage run --branch main.py") - self.assertEqual(out, "n\nz\n") + assert out == "n\nz\n" report = self.report_from_command("coverage report") # Name Stmts Miss Branch BrPart Cover @@ -535,11 +537,11 @@ def foo(): # ------------------------------------------------------ # TOTAL 13 0 4 1 94.118% - self.assertEqual(self.line_count(report), 7, report) + assert self.line_count(report) == 7, report squeezed = self.squeezed_lines(report) - self.assertEqual(squeezed[2], "covered.py 3 0 0 0 100.000%") - self.assertEqual(squeezed[4], "not_covered.py 4 0 2 1 83.333%") - self.assertEqual(squeezed[6], "TOTAL 13 0 4 1 94.118%") + 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_dotpy_not_python(self): # We run a .py file, and when reporting, we can't parse it as Python. @@ -560,15 +562,10 @@ def test_dotpy_not_python(self): errmsg = re.sub(r"parse '.*mycode.py", "parse 'mycode.py", errmsg) # The actual error message varies version to version errmsg = re.sub(r": '.*' at", ": 'error' at", errmsg) - self.assertEqual( - "Couldn't parse 'mycode.py' as Python source: 'error' at line 1", - errmsg, - ) + assert errmsg == "Couldn't parse 'mycode.py' as Python source: 'error' at line 1" + @pytest.mark.skipif(env.JYTHON, reason="Jython doesn't like accented file names") def test_accenteddotpy_not_python(self): - if env.JYTHON: - self.skipTest("Jython doesn't like accented file names") - # 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. @@ -590,7 +587,7 @@ def test_accenteddotpy_not_python(self): expected = u"Couldn't parse 'accented\xe2.py' as Python source: 'error' at line 1" if env.PY2: expected = expected.encode(output_encoding()) - self.assertEqual(expected, errmsg) + assert expected == errmsg def test_dotpy_not_python_ignored(self): # We run a .py file, and when reporting, we can't parse it as Python, @@ -606,9 +603,9 @@ def test_dotpy_not_python_ignored(self): # ---------------------------- # No data to report. - self.assertEqual(self.line_count(report), 4) - self.assertIn('No data to report.', report) - self.assertIn('(couldnt-parse)', report) + assert self.line_count(report) == 4 + assert 'No data to report.' in report + assert '(couldnt-parse)' in report def test_dothtml_not_python(self): # We run a .html file, and when reporting, we can't parse it as @@ -625,8 +622,8 @@ def test_dothtml_not_python(self): # ---------------------------- # No data to report. - self.assertEqual(self.line_count(report), 3) - self.assertIn('No data to report.', report) + assert self.line_count(report) == 3 + assert 'No data to report.' in report def test_report_no_extension(self): self.make_file("xxx", """\ @@ -640,9 +637,9 @@ def test_report_no_extension(self): print("xxx: %r %r %r %r" % (a, b, c, d)) """) out = self.run_command("coverage run --source=. xxx") - self.assertEqual(out, "xxx: 3 4 0 7\n") + assert out == "xxx: 3 4 0 7\n" report = self.report_from_command("coverage report") - self.assertEqual(self.last_line_squeezed(report), "TOTAL 7 1 86%") + assert self.last_line_squeezed(report) == "TOTAL 7 1 86%" def test_report_with_chdir(self): self.make_file("chdir.py", """\ @@ -654,9 +651,9 @@ def test_report_with_chdir(self): """) self.make_file("subdir/something", "hello") out = self.run_command("coverage run --source=. chdir.py") - self.assertEqual(out, "Line One\nLine Two\nhello\n") + assert out == "Line One\nLine Two\nhello\n" report = self.report_from_command("coverage report") - self.assertEqual(self.last_line_squeezed(report), "TOTAL 5 0 100%") + assert self.last_line_squeezed(report) == "TOTAL 5 0 100%" def get_report(self, cov): """Get the report from `cov`, and canonicalize it.""" @@ -680,10 +677,10 @@ def branch(x): """) cov = coverage.Coverage(branch=True, source=["."]) cov.start() - import main # pragma: nested # pylint: disable=unused-import + import main # pragma: nested # pylint: disable=unused-import, import-error cov.stop() # pragma: nested report = self.get_report(cov).splitlines() - self.assertIn("mybranch.py 5 5 2 0 0%", report) + assert "mybranch.py 5 5 2 0 0%" in report def run_TheCode_and_report_it(self): """A helper for the next few tests.""" @@ -699,21 +696,19 @@ def test_bug_203_mixed_case_listed_twice_with_rc(self): report = self.run_TheCode_and_report_it() - self.assertIn("TheCode", report) - self.assertNotIn("thecode", report) + assert "TheCode" in report + assert "thecode" not in report def test_bug_203_mixed_case_listed_twice(self): self.make_file("TheCode.py", "a = 1\n") report = self.run_TheCode_and_report_it() - self.assertIn("TheCode", report) - self.assertNotIn("thecode", report) + 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): - if not env.WINDOWS: - self.skipTest(".pyw files are only on Windows.") - # https://github.com/nedbat/coveragepy/issues/261 self.make_file("start.pyw", """\ import mod @@ -728,10 +723,10 @@ def test_pyw_files(self): cov.stop() # pragma: nested report = self.get_report(cov) - self.assertNotIn("NoSource", report) + assert "NoSource" not in report report = report.splitlines() - self.assertIn("start.pyw 2 0 100%", report) - self.assertIn("mod.pyw 1 0 100%", report) + assert "start.pyw 2 0 100%" in report + assert "mod.pyw 1 0 100%" in report def test_tracing_pyc_file(self): # Create two Python files. @@ -744,17 +739,14 @@ def test_tracing_pyc_file(self): # Run the program. cov = coverage.Coverage() cov.start() - import main # pragma: nested # pylint: disable=unused-import + import main # pragma: nested # pylint: disable=unused-import, import-error cov.stop() # pragma: nested report = self.get_report(cov).splitlines() - self.assertIn("mod.py 1 0 100%", report) + assert "mod.py 1 0 100%" in report + @pytest.mark.skipif(env.PYPY2, reason="PyPy2 doesn't run bare .pyc files") def test_missing_py_file_during_run(self): - # PyPy2 doesn't run bare .pyc files. - if env.PYPY2: - self.skipTest("PyPy2 doesn't run bare .pyc files") - # Create two Python files. self.make_file("mod.py", "a = 1\n") self.make_file("main.py", "import mod\n") @@ -768,19 +760,19 @@ def test_missing_py_file_during_run(self): # the source location though. if env.PY3 and not env.JYTHON: pycs = glob.glob("__pycache__/mod.*.pyc") - self.assertEqual(len(pycs), 1) + assert len(pycs) == 1 os.rename(pycs[0], "mod.pyc") # Run the program. cov = coverage.Coverage() cov.start() - import main # pragma: nested # pylint: disable=unused-import + import main # pragma: nested # pylint: disable=unused-import, import-error cov.stop() # pragma: nested # Put back the missing Python file. self.make_file("mod.py", "a = 1\n") report = self.get_report(cov).splitlines() - self.assertIn("mod.py 1 0 100%", report) + assert "mod.py 1 0 100%" in report class SummaryTest2(UsingModulesMixin, CoverageTest): @@ -804,8 +796,8 @@ def test_empty_files(self): report = repout.getvalue().replace('\\', '/') report = re.sub(r"\s+", " ", report) - self.assertIn("tests/modules/pkg1/__init__.py 1 0 0 0 100%", report) - self.assertIn("tests/modules/pkg2/__init__.py 0 0 0 0 100%", report) + 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 class ReportingReturnValueTest(CoverageTest): @@ -830,17 +822,17 @@ def run_coverage(self): def test_report(self): cov = self.run_coverage() val = cov.report(include="*/doit.py") - self.assertAlmostEqual(val, 85.7, 1) + assert round(abs(val-85.7), 1) == 0 def test_html(self): cov = self.run_coverage() val = cov.html_report(include="*/doit.py") - self.assertAlmostEqual(val, 85.7, 1) + assert round(abs(val-85.7), 1) == 0 def test_xml(self): cov = self.run_coverage() val = cov.xml_report(include="*/doit.py") - self.assertAlmostEqual(val, 85.7, 1) + assert round(abs(val-85.7), 1) == 0 class TestSummaryReporterConfiguration(CoverageTest): @@ -896,37 +888,35 @@ def test_test_data(self): # TOTAL 586 386 34% lines = report.splitlines()[2:-2] - self.assertEqual(len(lines), 3) + 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] # ] - self.assertTrue(nums[1][0] < nums[2][0] < nums[0][0]) - self.assertTrue(nums[1][1] < nums[0][1] < nums[2][1]) - self.assertTrue(nums[2][2] < nums[0][2] < nums[1][2]) + 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): """Run the report with no configuration options.""" report = self.get_summary_text() - self.assertNotIn('Missing', report) - self.assertNotIn('Branch', report) + assert 'Missing' not in report + assert 'Branch' not in report def test_print_missing(self): """Run the report printing the missing lines.""" report = self.get_summary_text(('report:show_missing', True)) - self.assertIn('Missing', report) - self.assertNotIn('Branch', report) + assert 'Missing' in report + assert 'Branch' not in report def assert_ordering(self, text, *words): """Assert that the `words` appear in order in `text`.""" indexes = list(map(text.find, words)) assert -1 not in indexes - self.assertEqual( - indexes, sorted(indexes), - "The words %r don't appear in order in %r" % (words, text) - ) + msg = "The words %r don't appear in order in %r" % (words, text) + assert indexes == sorted(indexes), msg def test_sort_report_by_stmts(self): # Sort the text report by the Stmts column. @@ -943,8 +933,18 @@ def test_sort_report_by_cover(self): report = self.get_summary_text(('report:sort', 'Cover')) self.assert_ordering(report, "file3.py", "file1.py", "file2.py") + def test_sort_report_by_cover_plus(self): + # Sort the text report by the Cover column, including the explicit + sign. + report = self.get_summary_text(('report:sort', '+Cover')) + self.assert_ordering(report, "file3.py", "file1.py", "file2.py") + + def test_sort_report_by_cover_reversed(self): + # 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", "file3.py") + def test_sort_report_by_invalid_option(self): # Sort the text report by a nonsense column. msg = "Invalid sorting option: 'Xyzzy'" - with self.assertRaisesRegex(CoverageException, msg): + with pytest.raises(CoverageException, match=msg): self.get_summary_text(('report:sort', 'Xyzzy')) diff --git a/tests/test_templite.py b/tests/test_templite.py index 321db8307..770e97f97 100644 --- a/tests/test_templite.py +++ b/tests/test_templite.py @@ -6,6 +6,8 @@ import re +import pytest + from coverage.templite import Templite, TempliteSyntaxError, TempliteValueError from tests.coveragetest import CoverageTest @@ -39,7 +41,7 @@ def try_render(self, text, ctx=None, result=None): # If result is None, then an exception should have prevented us getting # to here. assert result is not None - self.assertEqual(actual, result) + assert actual == result def assertSynErr(self, msg): """Assert that a `TempliteSyntaxError` will happen. @@ -48,15 +50,12 @@ def assertSynErr(self, msg): """ pat = "^" + re.escape(msg) + "$" - return self.assertRaisesRegex(TempliteSyntaxError, pat) + return pytest.raises(TempliteSyntaxError, match=pat) def test_passthrough(self): # Strings without variables are passed through unchanged. - self.assertEqual(Templite("Hello").render(), "Hello") - self.assertEqual( - Templite("Hello, 20% fun time!").render(), - "Hello, 20% fun time!" - ) + assert Templite("Hello").render() == "Hello" + assert Templite("Hello, 20% fun time!").render() == "Hello, 20% fun time!" def test_variables(self): # Variables use {{var}} syntax. @@ -64,7 +63,7 @@ def test_variables(self): def test_undefined_variables(self): # Using undefined names is an error. - with self.assertRaisesRegex(Exception, "'name'"): + with pytest.raises(Exception, match="'name'"): self.try_render("Hi, {{name}}!") def test_pipes(self): @@ -87,8 +86,8 @@ def test_reusability(self): } template = Templite("This is {{name|upper}}{{punct}}", globs) - self.assertEqual(template.render({'name':'Ned'}), "This is NED!") - self.assertEqual(template.render({'name':'Ben'}), "This is BEN!") + assert template.render({'name':'Ned'}) == "This is NED!" + assert template.render({'name':'Ben'}) == "This is BEN!" def test_attribute(self): # Variables' attributes can be accessed with dots. @@ -298,7 +297,7 @@ def test_non_ascii(self): def test_exception_during_evaluation(self): # TypeError: Couldn't evaluate {{ foo.bar.baz }}: regex = "^Couldn't evaluate None.bar$" - with self.assertRaisesRegex(TempliteValueError, regex): + with pytest.raises(TempliteValueError, match=regex): self.try_render( "Hey {{foo.bar.baz}} there", {'foo': None}, "Hey ??? there" ) diff --git a/tests/test_testing.py b/tests/test_testing.py index 34ea32635..f5d9f9421 100644 --- a/tests/test_testing.py +++ b/tests/test_testing.py @@ -8,20 +8,21 @@ import os import re import sys +import unittest import pytest import coverage from coverage import tomlconfig -from coverage.backunittest import TestCase, unittest from coverage.files import actual_path from coverage.misc import StopEverything -from tests.coveragetest import CoverageTest, convert_skip_exceptions +from tests.coveragetest import CoverageTest from tests.helpers import ( - arcs_to_arcz_repr, arcz_to_arcs, + arcs_to_arcz_repr, arcz_to_arcs, assert_count_equal, CheckUniqueFilenames, re_lines, re_line, without_module, ) +from tests.mixins import convert_skip_exceptions def test_xdist_sys_path_nuttiness_is_fixed(): @@ -30,16 +31,13 @@ def test_xdist_sys_path_nuttiness_is_fixed(): assert os.environ.get('PYTHONPATH') is None -class TestingTest(TestCase): - """Tests of helper methods on `backunittest.TestCase`.""" - - def test_assert_count_equal(self): - self.assertCountEqual(set(), set()) - self.assertCountEqual({1,2,3}, {3,1,2}) - with self.assertRaises(AssertionError): - self.assertCountEqual({1,2,3}, set()) - with self.assertRaises(AssertionError): - self.assertCountEqual({1,2,3}, {4,5,6}) +def test_assert_count_equal(): + assert_count_equal(set(), set()) + assert_count_equal({"a": 1, "b": 2}, ["b", "a"]) + with pytest.raises(AssertionError): + assert_count_equal({1,2,3}, set()) + with pytest.raises(AssertionError): + assert_count_equal({1,2,3}, {4,5,6}) class CoverageTestTest(CoverageTest): @@ -49,11 +47,11 @@ def test_file_exists(self): self.make_file("whoville.txt", "We are here!") self.assert_exists("whoville.txt") self.assert_doesnt_exist("shadow.txt") - msg = "False is not true : File 'whoville.txt' shouldn't exist" - with self.assertRaisesRegex(AssertionError, msg): + msg = "File 'whoville.txt' shouldn't exist" + with pytest.raises(AssertionError, match=msg): self.assert_doesnt_exist("whoville.txt") - msg = "False is not true : File 'shadow.txt' should exist" - with self.assertRaisesRegex(AssertionError, msg): + msg = "File 'shadow.txt' should exist" + with pytest.raises(AssertionError, match=msg): self.assert_exists("shadow.txt") def test_file_count(self): @@ -65,27 +63,27 @@ def test_file_count(self): self.assert_file_count("afile.*", 1) self.assert_file_count("*.q", 0) msg = re.escape( - "3 != 13 : There should be 13 files matching 'a*.txt', but there are these: " + "There should be 13 files matching 'a*.txt', but there are these: " "['abcde.txt', 'afile.txt', 'axczz.txt']" ) - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.assert_file_count("a*.txt", 13) msg = re.escape( - "2 != 12 : There should be 12 files matching '*c*.txt', but there are these: " + "There should be 12 files matching '*c*.txt', but there are these: " "['abcde.txt', 'axczz.txt']" ) - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.assert_file_count("*c*.txt", 12) msg = re.escape( - "1 != 11 : There should be 11 files matching 'afile.*', but there are these: " + "There should be 11 files matching 'afile.*', but there are these: " "['afile.txt']" ) - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.assert_file_count("afile.*", 11) msg = re.escape( - "0 != 10 : There should be 10 files matching '*.q', but there are these: []" + "There should be 10 files matching '*.q', but there are these: []" ) - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.assert_file_count("*.q", 10) def test_assert_startwith(self): @@ -93,10 +91,10 @@ def test_assert_startwith(self): self.assert_starts_with("xyz\nabc", "xy") self.assert_starts_with("xyzzy", ("x", "z")) msg = re.escape("'xyz' doesn't start with 'a'") - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.assert_starts_with("xyz", "a") msg = re.escape("'xyz\\nabc' doesn't start with 'a'") - with self.assertRaisesRegex(AssertionError, msg): + with pytest.raises(AssertionError, match=msg): self.assert_starts_with("xyz\nabc", "a") def test_assert_recent_datetime(self): @@ -107,17 +105,17 @@ def now_delta(seconds): # Default delta is 10 seconds. self.assert_recent_datetime(now_delta(0)) self.assert_recent_datetime(now_delta(-9)) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_recent_datetime(now_delta(-11)) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_recent_datetime(now_delta(1)) # Delta is settable. self.assert_recent_datetime(now_delta(0), seconds=120) self.assert_recent_datetime(now_delta(-100), seconds=120) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_recent_datetime(now_delta(-1000), seconds=120) - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_recent_datetime(now_delta(1), seconds=120) def test_assert_warnings(self): @@ -143,13 +141,13 @@ def test_assert_warnings(self): # But if there are a bunch of expected warnings, they have to all happen. warn_regex = r"Didn't find warning 'You' in \['Hello there!'\]" - with self.assertRaisesRegex(AssertionError, warn_regex): + with pytest.raises(AssertionError, match=warn_regex): with self.assert_warnings(cov, ["Hello.*!", "You"]): cov._warn("Hello there!") # Make a different warning than expected, it should raise an assertion. warn_regex = r"Didn't find warning 'Not me' in \['Hello there!'\]" - with self.assertRaisesRegex(AssertionError, warn_regex): + with pytest.raises(AssertionError, match=warn_regex): with self.assert_warnings(cov, ["Not me"]): cov._warn("Hello there!") @@ -159,13 +157,13 @@ def test_assert_warnings(self): # But it should fail if the unexpected warning does appear. warn_regex = r"Found warning 'Bye' in \['Hi', 'Bye'\]" - with self.assertRaisesRegex(AssertionError, warn_regex): + with pytest.raises(AssertionError, match=warn_regex): with self.assert_warnings(cov, ["Hi"], not_warnings=["Bye"]): cov._warn("Hi") cov._warn("Bye") # assert_warnings shouldn't hide a real exception. - with self.assertRaisesRegex(ZeroDivisionError, "oops"): + with pytest.raises(ZeroDivisionError, match="oops"): with self.assert_warnings(cov, ["Hello there!"]): raise ZeroDivisionError("oops") @@ -178,7 +176,7 @@ def test_assert_no_warnings(self): # If you said there would be no warnings, and there were, fail! warn_regex = r"Unexpected warnings: \['Watch out!'\]" - with self.assertRaisesRegex(AssertionError, warn_regex): + with pytest.raises(AssertionError, match=warn_regex): with self.assert_warnings(cov, []): cov._warn("Watch out!") @@ -192,21 +190,21 @@ def test_sub_python_is_this_python(self): print(os.environ['COV_FOOBAR']) """) out = self.run_command("python showme.py").splitlines() - self.assertEqual(actual_path(out[0]), actual_path(sys.executable)) - self.assertEqual(out[1], os.__file__) - self.assertEqual(out[2], 'XYZZY') + assert actual_path(out[0]) == actual_path(sys.executable) + assert out[1] == os.__file__ + assert out[2] == 'XYZZY' # Try it with a "coverage debug sys" command. out = self.run_command("coverage debug sys") executable = re_line(out, "executable:") executable = executable.split(":", 1)[1].strip() - self.assertTrue(_same_python_executable(executable, sys.executable)) + assert _same_python_executable(executable, sys.executable) # "environment: COV_FOOBAR = XYZZY" or "COV_FOOBAR = XYZZY" environ = re_line(out, "COV_FOOBAR") _, _, environ = environ.rpartition(":") - self.assertEqual(environ.strip(), "COV_FOOBAR = XYZZY") + assert environ.strip() == "COV_FOOBAR = XYZZY" def test_run_command_stdout_stderr(self): # run_command should give us both stdout and stderr. @@ -216,8 +214,8 @@ def test_run_command_stdout_stderr(self): print("StdOut") """) out = self.run_command("python outputs.py") - self.assertIn("StdOut\n", out) - self.assertIn("StdErr\n", out) + assert "StdOut\n" in out + assert "StdErr\n" in out class CheckUniqueFilenamesTest(CoverageTest): @@ -243,10 +241,61 @@ def test_detect_duplicate(self): assert stub.method("file2", 1723, b="what") == (23, "file2", 1723, "what") # A duplicate file name trips an assertion. - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): stub.method("file1") +class CheckCoverageTest(CoverageTest): + """Tests of the failure assertions in check_coverage.""" + + CODE = """\ + a, b = 1, 1 + def oops(x): + if x % 2: + raise Exception("odd") + try: + a = 6 + oops(1) + a = 8 + except: + b = 10 + assert a == 6 and b == 10 + """ + ARCZ = ".1 12 -23 34 3-2 4-2 25 56 67 78 8B 9A AB B." + ARCZ_MISSING = "3-2 78 8B" + ARCZ_UNPREDICTED = "79" + + def test_check_coverage_possible(self): + msg = r"(?s)Possible arcs differ: .*- \(6, 3\).*\+ \(6, 7\)" + with pytest.raises(AssertionError, match=msg): + self.check_coverage( + self.CODE, + arcz=self.ARCZ.replace("7", "3"), + arcz_missing=self.ARCZ_MISSING, + arcz_unpredicted=self.ARCZ_UNPREDICTED, + ) + + def test_check_coverage_missing(self): + msg = r"(?s)Missing arcs differ: .*- \(3, 8\).*\+ \(7, 8\)" + with pytest.raises(AssertionError, match=msg): + self.check_coverage( + self.CODE, + arcz=self.ARCZ, + arcz_missing=self.ARCZ_MISSING.replace("7", "3"), + arcz_unpredicted=self.ARCZ_UNPREDICTED, + ) + + def test_check_coverage_unpredicted(self): + msg = r"(?s)Unpredicted arcs differ: .*- \(3, 9\).*\+ \(7, 9\)" + with pytest.raises(AssertionError, match=msg): + self.check_coverage( + self.CODE, + arcz=self.ARCZ, + arcz_missing=self.ARCZ_MISSING, + arcz_unpredicted=self.ARCZ_UNPREDICTED.replace("7", "3") + ) + + @pytest.mark.parametrize("text, pat, result", [ ("line1\nline2\nline3\n", "line", "line1\nline2\nline3\n"), ("line1\nline2\nline3\n", "[13]", "line1\nline3\n"), diff --git a/tests/test_version.py b/tests/test_version.py index 11b180d52..00d65624f 100644 --- a/tests/test_version.py +++ b/tests/test_version.py @@ -16,24 +16,19 @@ class VersionTest(CoverageTest): def test_version_info(self): # Make sure we didn't screw up the version_info tuple. - self.assertIsInstance(coverage.version_info, tuple) - self.assertEqual([type(d) for d in coverage.version_info], [int, int, int, str, int]) - self.assertIn(coverage.version_info[3], ['alpha', 'beta', 'candidate', 'final']) + assert isinstance(coverage.version_info, tuple) + assert [type(d) for d in coverage.version_info] == [int, int, int, str, int] + assert coverage.version_info[3] in ['alpha', 'beta', 'candidate', 'final'] def test_make_version(self): - self.assertEqual(_make_version(4, 0, 0, 'alpha', 0), "4.0a0") - self.assertEqual(_make_version(4, 0, 0, 'alpha', 1), "4.0a1") - self.assertEqual(_make_version(4, 0, 0, 'final', 0), "4.0") - self.assertEqual(_make_version(4, 1, 2, 'beta', 3), "4.1.2b3") - self.assertEqual(_make_version(4, 1, 2, 'final', 0), "4.1.2") - self.assertEqual(_make_version(5, 10, 2, 'candidate', 7), "5.10.2rc7") + assert _make_version(4, 0, 0, 'alpha', 0) == "4.0a0" + assert _make_version(4, 0, 0, 'alpha', 1) == "4.0a1" + assert _make_version(4, 0, 0, 'final', 0) == "4.0" + assert _make_version(4, 1, 2, 'beta', 3) == "4.1.2b3" + assert _make_version(4, 1, 2, 'final', 0) == "4.1.2" + assert _make_version(5, 10, 2, 'candidate', 7) == "5.10.2rc7" def test_make_url(self): - self.assertEqual( - _make_url(4, 0, 0, 'final', 0), - "https://coverage.readthedocs.io" - ) - self.assertEqual( - _make_url(4, 1, 2, 'beta', 3), - "https://coverage.readthedocs.io/en/coverage-4.1.2b3" - ) + assert _make_url(4, 0, 0, 'final', 0) == "https://coverage.readthedocs.io" + expected = "https://coverage.readthedocs.io/en/coverage-4.1.2b3" + assert _make_url(4, 1, 2, 'beta', 3) == expected diff --git a/tests/test_xml.py b/tests/test_xml.py index e3be7a543..13e015d6b 100644 --- a/tests/test_xml.py +++ b/tests/test_xml.py @@ -9,6 +9,7 @@ import re from xml.etree import ElementTree +import pytest from unittest_mixins import change_dir import coverage @@ -87,11 +88,11 @@ def test_assert_source(self): self.assert_source(dom, "something") self.assert_source(dom, "another") - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_source(dom, "hello") - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_source(dom, "foo") - with self.assertRaises(AssertionError): + with pytest.raises(AssertionError): self.assert_source(dom, "thing") @@ -274,10 +275,7 @@ def package_and_class_tags(self, cov): def assert_package_and_class_tags(self, cov, result): """Check the XML package and class tags from `cov` match `result`.""" - self.assertEqual( - unbackslash(list(self.package_and_class_tags(cov))), - unbackslash(result), - ) + assert unbackslash(list(self.package_and_class_tags(cov))) == unbackslash(result) def test_package_names(self): self.make_tree(width=1, depth=3) diff --git a/tox.ini b/tox.ini index 317dea42f..6eeee5bc0 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ # For details: https://github.com/nedbat/coveragepy/blob/master/NOTICE.txt [tox] +# When changing this list, be sure to check the [gh-actions] list below. envlist = py{27,35,36,37,38,39,310}, pypy{2,3}, doc, lint skip_missing_interpreters = {env:COVERAGE_SKIP_MISSING_INTERPRETERS:True} toxworkdir = {env:TOXWORKDIR:.tox} @@ -93,5 +94,6 @@ python = 3.7: py37 3.8: py38 3.9: py39 + 3.10: py310 pypy: pypy pypy3: pypy3