diff --git a/changelog/13403.bugfix.rst b/changelog/13403.bugfix.rst new file mode 100644 index 00000000000..8b3cb7d92b4 --- /dev/null +++ b/changelog/13403.bugfix.rst @@ -0,0 +1 @@ +Disable assertion for modules outside current working dir(cwd) diff --git a/changelog/13492.doc.rst b/changelog/13492.doc.rst new file mode 100644 index 00000000000..6e3f72a860e --- /dev/null +++ b/changelog/13492.doc.rst @@ -0,0 +1 @@ +Fixed outdated warning about ``faulthandler`` not working on Windows. diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 22f3ca8e258..a205dbaf032 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -110,6 +110,7 @@ class AssertionState: def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") + self.invocation_path = str(config.invocation_params.dir) self.hook: rewrite.AssertionRewritingHook | None = None diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 566549d66f2..df6d73f536e 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -250,7 +250,9 @@ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool: # rewritten if they match the naming convention for test files fn_path = PurePath(fn) for pat in self.fnpats: - if fnmatch_ex(pat, fn_path): + if fnmatch_ex(pat, fn_path) and fn_path.is_relative_to( + state.invocation_path + ): state.trace(f"matched test file {fn!r}") return True diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index d5b67abdaee..ec4d69dc9b5 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -40,6 +40,7 @@ from _pytest import timing from _pytest._code import Source +from _pytest.assertion.rewrite import assertstate_key from _pytest.capture import _get_multicapture from _pytest.compat import NOTSET from _pytest.compat import NotSetType @@ -751,6 +752,9 @@ def chdir(self) -> None: This is done automatically upon instantiation. """ self._monkeypatch.chdir(self.path) + self._monkeypatch.setattr( + self._request.config.stash[assertstate_key], "invocation_path", self.path + ) def _makefile( self, diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index b9384008483..d5e1b94aae2 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -249,6 +249,17 @@ def test_issue88_initial_file_multinodes(self, pytester: Pytester) -> None: result = pytester.runpytest(p, "--collect-only") result.stdout.fnmatch_lines(["*MyFile*test_issue88*", "*Module*test_issue88*"]) + def test_rewrite(self, pytester: Pytester) -> None: + pytester.copy_example("rewrite") + result = pytester.runpytest("tests/test_main.py::test_func", "-v") + result.stdout.fnmatch_lines(["*Full diff*"]) + + result = pytester.runpytest("tests/test_main.py::test_plugin", "-v") + result.stdout.fnmatch_lines(["*Full diff*"]) + + result = pytester.runpytest("tests/test_main.py::test_lib", "-v") + result.stdout.no_fnmatch_line("*Full diff*") + def test_issue93_initialnode_importing_capturing(self, pytester: Pytester) -> None: pytester.makeconftest( """ diff --git a/testing/example_scripts/rewrite/pytest.ini b/testing/example_scripts/rewrite/pytest.ini new file mode 100644 index 00000000000..7c479554025 --- /dev/null +++ b/testing/example_scripts/rewrite/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +python_files = *.py diff --git a/testing/example_scripts/rewrite/some_plugin/__init__.py b/testing/example_scripts/rewrite/some_plugin/__init__.py new file mode 100644 index 00000000000..89eed48202d --- /dev/null +++ b/testing/example_scripts/rewrite/some_plugin/__init__.py @@ -0,0 +1,13 @@ +from __future__ import annotations + +from collections.abc import Callable + +import pytest + + +@pytest.fixture +def special_asserter() -> Callable[[int, int], None]: + def special_assert(a: int, b: int) -> None: + assert {"plugin_a": a} == {"plugin_b": b} + + return special_assert diff --git a/testing/example_scripts/rewrite/src/__init__.py b/testing/example_scripts/rewrite/src/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/testing/example_scripts/rewrite/src/main.py b/testing/example_scripts/rewrite/src/main.py new file mode 100644 index 00000000000..b76c92bc424 --- /dev/null +++ b/testing/example_scripts/rewrite/src/main.py @@ -0,0 +1,9 @@ +"""Stub file for testing""" + +from __future__ import annotations + + +def func(x: int, y: int) -> int: + """Stub function""" + assert (x) > 0 + return 0 if x == y else 1 if x > y else -1 diff --git a/testing/example_scripts/rewrite/tests/__init__.py b/testing/example_scripts/rewrite/tests/__init__.py new file mode 100644 index 00000000000..d16fe849d88 --- /dev/null +++ b/testing/example_scripts/rewrite/tests/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + + +__package__ = "" diff --git a/testing/example_scripts/rewrite/tests/conftest.py b/testing/example_scripts/rewrite/tests/conftest.py new file mode 100644 index 00000000000..4c0ef378079 --- /dev/null +++ b/testing/example_scripts/rewrite/tests/conftest.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +from _pytest.fixtures import fixture + + +pytest_plugins = ["pytester", "some_plugin"] + + +@fixture +def b() -> int: + return 1 + + +@fixture +def a() -> int: + return 2 diff --git a/testing/example_scripts/rewrite/tests/test_main.py b/testing/example_scripts/rewrite/tests/test_main.py new file mode 100644 index 00000000000..b3136b5bb32 --- /dev/null +++ b/testing/example_scripts/rewrite/tests/test_main.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +from pathlib import Path +import sys + + +sys.path.append(str(Path(__file__).parent.parent)) + +from collections.abc import Callable +from venv.lib64.python3.site_packages.external_lib import external_lib + +from src.main import func + + +def test_plugin(a: int, b: int, special_asserter: Callable[[int, int], bool]) -> None: + special_asserter(a, b) + + +def test_func(a: int, b: int) -> None: + assert {"func_a": func(a, b)} == {"func_a": 0} + + +def test_lib(a: int) -> None: + external_lib.some_check(a) diff --git a/testing/example_scripts/rewrite/venv/__init__.py b/testing/example_scripts/rewrite/venv/__init__.py new file mode 100644 index 00000000000..d16fe849d88 --- /dev/null +++ b/testing/example_scripts/rewrite/venv/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + + +__package__ = "" diff --git a/testing/example_scripts/rewrite/venv/lib64/__init__.py b/testing/example_scripts/rewrite/venv/lib64/__init__.py new file mode 100644 index 00000000000..d16fe849d88 --- /dev/null +++ b/testing/example_scripts/rewrite/venv/lib64/__init__.py @@ -0,0 +1,4 @@ +from __future__ import annotations + + +__package__ = "" diff --git a/testing/example_scripts/rewrite/venv/lib64/python3/site_packages/external_lib/__init__.py b/testing/example_scripts/rewrite/venv/lib64/python3/site_packages/external_lib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/testing/example_scripts/rewrite/venv/lib64/python3/site_packages/external_lib/external_lib.py b/testing/example_scripts/rewrite/venv/lib64/python3/site_packages/external_lib/external_lib.py new file mode 100644 index 00000000000..a99581bbc19 --- /dev/null +++ b/testing/example_scripts/rewrite/venv/lib64/python3/site_packages/external_lib/external_lib.py @@ -0,0 +1,2 @@ +def some_check(lib_a: int) -> None: + assert {'lib_a': lib_a} != {'lib_a': lib_a} diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 92664354470..720bba32a6d 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -29,12 +29,14 @@ from _pytest.assertion.rewrite import _get_maxsize_for_saferepr from _pytest.assertion.rewrite import _saferepr from _pytest.assertion.rewrite import AssertionRewritingHook +from _pytest.assertion.rewrite import assertstate_key from _pytest.assertion.rewrite import get_cache_dir from _pytest.assertion.rewrite import PYC_TAIL from _pytest.assertion.rewrite import PYTEST_TAG from _pytest.assertion.rewrite import rewrite_asserts from _pytest.config import Config from _pytest.config import ExitCode +from _pytest.monkeypatch import MonkeyPatch from _pytest.pathlib import make_numbered_dir from _pytest.pytester import Pytester import pytest @@ -370,6 +372,7 @@ def test_rewrites_plugin_as_a_package(self, pytester: Pytester) -> None: pytester.makeconftest('pytest_plugins = ["plugin"]') pytester.makepyfile("def test(special_asserter): special_asserter(1, 2)\n") result = pytester.runpytest() + result.stdout.fnmatch_lines(["*assert 1 == 2*"]) def test_honors_pep_235(self, pytester: Pytester, monkeypatch) -> None: @@ -1294,6 +1297,36 @@ def test_meta_path(): ) assert pytester.runpytest().ret == 0 + def test_invocation_dir(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + """Test get invocation param from AssertionState""" + from _pytest.assertion import AssertionState + + config = pytester.parseconfig() + state = AssertionState(config, "rewrite") + + assert state.invocation_path == str(config.invocation_params.dir) + + new_rootpath = pytester.path / "test" + if not os.path.exists(new_rootpath): + os.mkdir(new_rootpath) + monkeypatch.setattr( + config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=new_rootpath, + ), + ) + state = AssertionState(config, "rewrite") + assert state.invocation_path == str(new_rootpath) + + @pytest.mark.skipif( + sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" + ) + @pytest.mark.skipif( + sys.platform.startswith("sunos5"), reason="cannot remove cwd on Solaris" + ) def test_write_pyc(self, pytester: Pytester, tmp_path) -> None: from _pytest.assertion import AssertionState from _pytest.assertion.rewrite import _write_pyc @@ -1989,6 +2022,32 @@ def test_simple_failure(): assert hook.find_spec("file") is not None assert self.find_spec_calls == ["file"] + def test_assert_rewrites_only_invocation_path( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """Do not rewrite assertions in tests outside `AssertState.rootpath` (#13403).""" + pytester.makepyfile( + **{ + "file.py": """\ + def test_simple_failure(): + assert 1 + 1 == 3 + """ + } + ) + + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("file") is not None + + invocation_path = f"{os.getcwd()}/tests" + monkeypatch.setattr( + pytester._request.config.stash[assertstate_key], + "invocation_path", + invocation_path, + ) + + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("file") is None + @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" )