diff --git a/changelog/13403.bugfix.rst b/changelog/13403.bugfix.rst new file mode 100644 index 00000000000..132cbfe0010 --- /dev/null +++ b/changelog/13403.bugfix.rst @@ -0,0 +1 @@ +Disable assertion rewriting of external modules diff --git a/src/_pytest/assertion/__init__.py b/src/_pytest/assertion/__init__.py index 532b96fe431..04d4c96b1e3 100644 --- a/src/_pytest/assertion/__init__.py +++ b/src/_pytest/assertion/__init__.py @@ -4,6 +4,7 @@ from __future__ import annotations from collections.abc import Generator +import os import sys from typing import Any from typing import TYPE_CHECKING @@ -106,8 +107,14 @@ class AssertionState: def __init__(self, config: Config, mode) -> None: self.mode = mode self.trace = config.trace.root.get("assertion") + self.config = config self.hook: rewrite.AssertionRewritingHook | None = None + @property + def rootpath(self): + """Get current root path (current working dir)""" + return str(self.config.invocation_params.dir) + def install_importhook(config: Config) -> rewrite.AssertionRewritingHook: """Try to install the rewrite hook, raise SystemError if it fails.""" diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index c4782c7c5a8..861ec77a70b 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -238,8 +238,9 @@ def _should_rewrite(self, name: str, fn: str, state: AssertionState) -> bool: # modules not passed explicitly on the command line are only # 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.rootpath): state.trace(f"matched test file {fn!r}") return True diff --git a/src/_pytest/pytester.py b/src/_pytest/pytester.py index 11127a88bb8..e1cea040e9f 100644 --- a/src/_pytest/pytester.py +++ b/src/_pytest/pytester.py @@ -749,6 +749,15 @@ def chdir(self) -> None: This is done automatically upon instantiation. """ self._monkeypatch.chdir(self.path) + self._monkeypatch.setattr( + self._request.config, + "invocation_params", + Config.InvocationParams( + args=self._request.config.invocation_params.args, + plugins=self._request.config.invocation_params.plugins, + dir=Path(self._path), + ), + ) def _makefile( self, diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e2e448fe5e6..260e836d554 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -12,6 +12,7 @@ import inspect import marshal import os +from os import mkdir from pathlib import Path import py_compile import re @@ -35,6 +36,7 @@ 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_rootpath_base(self, pytester: Pytester, monkeypatch: MonkeyPatch) -> None: + """ + Base cases for get rootpath from AssertionState + """ + from _pytest.assertion import AssertionState + + config = pytester.parseconfig() + state = AssertionState(config, "rewrite") + assert state.rootpath == str(config.invocation_params.dir) + new_rootpath = str(pytester.path / "test") + if not os.path.exists(new_rootpath): + os.mkdir(new_rootpath) + monkeypatch.setattr( + config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(new_rootpath), + ), + ) + state = AssertionState(config, "rewrite") + assert state.rootpath == 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 @@ -1971,6 +2004,99 @@ def test_simple_failure(): assert hook.find_spec("file") is not None assert self.find_spec_calls == ["file"] + def test_assert_rewrites_only_rootpath( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """ + If test files contained outside the rootpath, then skip them + """ + 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 + + rootpath = f"{os.getcwd()}/tests" + if not os.path.exists(rootpath): + mkdir(rootpath) + monkeypatch.setattr( + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(rootpath), + ), + ) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("file") is None + + def test_assert_rewrite_correct_for_conftfest( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """ + Conftest is always rewritten regardless of the root dir + """ + pytester.makeconftest( + """ + import pytest + @pytest.fixture + def fix(): return 1 + """ + ) + + rootpath = f"{os.getcwd()}/tests" + if not os.path.exists(rootpath): + mkdir(rootpath) + monkeypatch.setattr( + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(rootpath), + ), + ) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("conftest") is not None + + def test_assert_rewrite_correct_for_plugins( + self, pytester: Pytester, hook: AssertionRewritingHook, monkeypatch + ) -> None: + """ + Plugins has always been rewritten regardless of the root dir + """ + pkgdir = pytester.mkpydir("plugin") + pkgdir.joinpath("__init__.py").write_text( + "import pytest\n" + "@pytest.fixture\n" + "def special_asserter():\n" + " def special_assert(x, y):\n" + " assert x == y\n" + " return special_assert\n", + encoding="utf-8", + ) + hook.mark_rewrite("plugin") + rootpath = f"{os.getcwd()}/tests" + if not os.path.exists(rootpath): + mkdir(rootpath) + monkeypatch.setattr( + pytester._request.config, + "invocation_params", + Config.InvocationParams( + args=(), + plugins=(), + dir=Path(rootpath), + ), + ) + with mock.patch.object(hook, "fnpats", ["*.py"]): + assert hook.find_spec("plugin") is not None + @pytest.mark.skipif( sys.platform.startswith("win32"), reason="cannot remove cwd on Windows" )