Skip to content
Draft
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 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.
19 changes: 19 additions & 0 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions testing/test_assertrewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading