Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/14552.bugfix.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Fixed assertion-rewrite cache invalidation for moved test files: a rewritten ``.pyc`` whose ``co_filename`` no longer matches the current source path is now treated as stale and rewritten. Previously, renaming or moving a test module/directory could leak the old path into ``inspect.currentframe().f_code.co_filename`` and related traceback/reporting paths.
7 changes: 7 additions & 0 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,13 @@ 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.
# Treat that as stale so the caller rewrites and recreates the cache.
if co.co_filename != str(source):
trace(f"_read_pyc({source}): stale filename {co.co_filename!r}")
return None
return co


Expand Down
25 changes: 25 additions & 0 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,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,
Expand Down