diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2a7da8136..e9a2f525d 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -54,6 +54,7 @@ jobs: python: "3.5" os: windows-latest tox_env: "py35-xdist" + use_coverage: true - name: "windows-py36" python: "3.6" os: windows-latest @@ -70,6 +71,7 @@ jobs: python: "3.8" os: windows-latest tox_env: "py38" + use_coverage: true - name: "ubuntu-py35" python: "3.5" @@ -83,6 +85,7 @@ jobs: python: "3.7" os: ubuntu-latest tox_env: "py37-lsof-numpy-oldattrs-pexpect-twisted" + use_coverage: true - name: "ubuntu-py37-pluggy" python: "3.7" os: ubuntu-latest @@ -91,8 +94,6 @@ jobs: python: "3.7" os: ubuntu-latest tox_env: "py37-freeze" - # coverage does not apply for freeze test, skip it - skip_coverage: true - name: "ubuntu-py38" python: "3.8" os: ubuntu-latest @@ -101,8 +102,6 @@ jobs: python: "pypy3" os: ubuntu-latest tox_env: "pypy3-xdist" - # coverage too slow with pypy3, skip it - skip_coverage: true - name: "macos-py37" python: "3.7" @@ -112,21 +111,21 @@ jobs: python: "3.8" os: macos-latest tox_env: "py38-xdist" + use_coverage: true - name: "linting" python: "3.7" os: ubuntu-latest tox_env: "linting" - skip_coverage: true - name: "docs" python: "3.7" os: ubuntu-latest tox_env: "docs" - skip_coverage: true - name: "doctesting" python: "3.7" os: ubuntu-latest tox_env: "doctesting" + use_coverage: true steps: - uses: actions/checkout@v1 @@ -140,11 +139,11 @@ jobs: pip install tox coverage - name: Test without coverage - if: "matrix.skip_coverage" + if: "! matrix.use_coverage" run: "tox -e ${{ matrix.tox_env }}" - name: Test with coverage - if: "! matrix.skip_coverage" + if: "matrix.use_coverage" env: _PYTEST_TOX_COVERAGE_RUN: "coverage run -m" COVERAGE_PROCESS_START: ".coveragerc" @@ -152,12 +151,12 @@ jobs: run: "tox -e ${{ matrix.tox_env }}" - name: Prepare coverage token - if: (!matrix.skip_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' )) + if: (matrix.use_coverage && ( github.repository == 'pytest-dev/pytest' || github.event_name == 'pull_request' )) run: | python scripts/append_codecov_token.py - name: Report coverage - if: (!matrix.skip_coverage) + if: (matrix.use_coverage) env: CODECOV_NAME: ${{ matrix.name }} run: bash scripts/report-coverage.sh -F GHA,${{ runner.os }} diff --git a/AUTHORS b/AUTHORS index 5eea42ea2..bd4c1fe5b 100644 --- a/AUTHORS +++ b/AUTHORS @@ -114,6 +114,7 @@ Guido Wesdorp Guoqiang Zhang Harald Armin Massa Henk-Jaap Wagenaar +Holger Kohr Hugo van Kemenade Hui Wang (coldnight) Ian Bicking diff --git a/changelog/6497.bugfix.rst b/changelog/6497.bugfix.rst new file mode 100644 index 000000000..66d436abd --- /dev/null +++ b/changelog/6497.bugfix.rst @@ -0,0 +1,4 @@ +Fix bug in the comparison of request key with cached key in fixture. + +A construct ``if key == cached_key:`` can fail either because ``==`` is explicitly disallowed, or for, e.g., NumPy arrays, where the result of ``a == b`` cannot generally be converted to `bool`. +The implemented fix replaces `==` with ``is``. diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index 4cb373cc4..09245d84d 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -77,12 +77,11 @@ class Code: # maybe don't try this checking if not p.check(): raise OSError("py.path check failed.") + return p except OSError: # XXX maybe try harder like the weird logic # in the standard lib [linecache.updatecache] does? - p = self.raw.co_filename - - return p + return self.raw.co_filename @property def fullsource(self) -> Optional["Source"]: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 9e60d56ce..08486fbff 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -548,8 +548,9 @@ class FixtureRequest: frameinfo = inspect.getframeinfo(frame[0]) source_path = py.path.local(frameinfo.filename) source_lineno = frameinfo.lineno - if source_path.relto(funcitem.config.rootdir): - source_path_str = source_path.relto(funcitem.config.rootdir) + rel_source_path = source_path.relto(funcitem.config.rootdir) + if rel_source_path: + source_path_str = rel_source_path else: source_path_str = str(source_path) msg = ( @@ -896,7 +897,9 @@ class FixtureDef: cached_result = getattr(self, "cached_result", None) if cached_result is not None: result, cache_key, err = cached_result - if my_cache_key == cache_key: + # note: comparison with `==` can fail (or be expensive) for e.g. + # numpy arrays (#6497) + if my_cache_key is cache_key: if err is not None: _, val, tb = err raise val.with_traceback(tb) diff --git a/src/_pytest/monkeypatch.py b/src/_pytest/monkeypatch.py index 090bf61d6..8aec7b818 100644 --- a/src/_pytest/monkeypatch.py +++ b/src/_pytest/monkeypatch.py @@ -4,6 +4,7 @@ import re import sys import warnings from contextlib import contextmanager +from typing import Generator import pytest from _pytest.fixtures import fixture @@ -108,7 +109,7 @@ class MonkeyPatch: self._savesyspath = None @contextmanager - def context(self): + def context(self) -> Generator["MonkeyPatch", None, None]: """ Context manager that returns a new :class:`MonkeyPatch` object which undoes any patching done inside the ``with`` block upon exit: diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 5e5eddc5b..24145016c 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -685,7 +685,7 @@ def raises( # noqa: F811 """ __tracebackhide__ = True for exc in filterfalse( - inspect.isclass, always_iterable(expected_exception, BASE_TYPE) + inspect.isclass, always_iterable(expected_exception, BASE_TYPE) # type: ignore[arg-type] # noqa: F821 ): msg = "exceptions must be derived from BaseException, not %s" raise TypeError(msg % type(exc)) diff --git a/testing/python/fixtures.py b/testing/python/fixtures.py index 8cfaae50d..bfbe35951 100644 --- a/testing/python/fixtures.py +++ b/testing/python/fixtures.py @@ -1102,6 +1102,38 @@ class TestFixtureUsages: "*Fixture 'badscope' from test_invalid_scope.py got an unexpected scope value 'functions'" ) + @pytest.mark.parametrize("scope", ["function", "session"]) + def test_parameters_without_eq_semantics(self, scope, testdir): + testdir.makepyfile( + """ + class NoEq1: # fails on `a == b` statement + def __eq__(self, _): + raise RuntimeError + + class NoEq2: # fails on `if a == b:` statement + def __eq__(self, _): + class NoBool: + def __bool__(self): + raise RuntimeError + return NoBool() + + import pytest + @pytest.fixture(params=[NoEq1(), NoEq2()], scope={scope!r}) + def no_eq(request): + return request.param + + def test1(no_eq): + pass + + def test2(no_eq): + pass + """.format( + scope=scope + ) + ) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*4 passed*"]) + def test_funcarg_parametrized_and_used_twice(self, testdir): testdir.makepyfile( """ @@ -3662,13 +3694,30 @@ class TestParameterizedSubRequest: " test_foos.py::test_foo", "", "Requested fixture 'fix_with_param' defined in:", - "*fix.py:4", + "{}:4".format(fixfile), "Requested here:", "test_foos.py:4", "*1 failed*", ] ) + # With non-overlapping rootdir, passing tests_dir. + rootdir = testdir.mkdir("rootdir") + rootdir.chdir() + result = testdir.runpytest("--rootdir", rootdir, tests_dir) + result.stdout.fnmatch_lines( + [ + "The requested fixture has no parameter defined for test:", + " test_foos.py::test_foo", + "", + "Requested fixture 'fix_with_param' defined in:", + "{}:4".format(fixfile), + "Requested here:", + "{}:4".format(testfile), + "*1 failed*", + ] + ) + def test_pytest_fixture_setup_and_post_finalizer_hook(testdir): testdir.makeconftest( diff --git a/testing/test_pytester.py b/testing/test_pytester.py index ad951fdf5..d9230f90b 100644 --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -704,3 +704,13 @@ def test_testdir_outcomes_with_multiple_errors(testdir): result.assert_outcomes(error=2) assert result.parseoutcomes() == {"error": 2} + + +def test_makefile_joins_absolute_path(testdir: Testdir) -> None: + absfile = testdir.tmpdir / "absfile" + if sys.platform == "win32": + with pytest.raises(OSError): + testdir.makepyfile(**{str(absfile): ""}) + else: + p1 = testdir.makepyfile(**{str(absfile): ""}) + assert str(p1) == (testdir.tmpdir / absfile) + ".py"