diff --git a/changelog/14552.bugfix.rst b/changelog/14552.bugfix.rst new file mode 100644 index 00000000000..aa83db3cc68 --- /dev/null +++ b/changelog/14552.bugfix.rst @@ -0,0 +1 @@ +Fixed assertion-rewrite cache behavior for moved test files: when a rewritten ``.pyc`` was reused after renaming or moving a test module/directory, nested code objects could keep a stale ``co_filename`` from the old path. This caused ``inspect.currentframe().f_code.co_filename`` (and related traceback/reporting paths) to point to the previous location instead of the current file path. diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 99815b70cf1..513ad69bd33 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -397,9 +397,28 @@ def _read_pyc( if not isinstance(co, types.CodeType): trace(f"_read_pyc({source}): not a code object") return None + # A cached pyc can be moved together with the source file (for example + # by renaming a package or test directory). In that case, the marshaled + # code object's ``co_filename`` still points to the old source path, + # which leaks stale filenames in tracebacks and ``inspect``. + source_str = str(source) + if co.co_filename != source_str: + co = _replace_code_filenames(co, source_str) return co +def _replace_code_filenames(co: types.CodeType, filename: str) -> types.CodeType: + return co.replace( + co_filename=filename, + co_consts=tuple( + _replace_code_filenames(const, filename) + if isinstance(const, types.CodeType) + else const + for const in co.co_consts + ), + ) + + def rewrite_asserts( mod: ast.Module, source: bytes, diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index e11863547ba..269f5b068fe 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1138,6 +1138,31 @@ def test_foo(): glob.glob("__pycache__/*.pyc") ) + def test_moved_test_file_updates_code_filename( + self, pytester: Pytester, monkeypatch: pytest.MonkeyPatch + ) -> None: + """Moving a test module must keep ``co_filename`` synchronized with ``__file__``.""" + monkeypatch.delenv("PYTHONPYCACHEPREFIX", raising=False) + + pytester.makepyfile( + **{ + "test1/test_a.py": """ + from inspect import currentframe + + def test_a(): + assert currentframe().f_code.co_filename == __file__ + """ + } + ) + + first = pytester.runpytest_subprocess("-s", "test1/test_a.py") + first.assert_outcomes(passed=1) + + pytester.path.joinpath("test1").rename(pytester.path.joinpath("test2")) + + second = pytester.runpytest_subprocess("-s", "test2/test_a.py") + second.assert_outcomes(passed=1) + @pytest.mark.skipif('"__pypy__" in sys.modules') def test_pyc_vs_pyo( self,