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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test doesn't validate the invatiant

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand what is missing here for you.

  • The tests fails without the fix.
  • It reproduces the incorrect behaviour by moving the source and cached bytecode.

Which invariant do you mean?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test would pass without changes when one disables bytecode writing for example

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ill show a more detailed example once I get back to the computer


@pytest.mark.skipif('"__pypy__" in sys.modules')
def test_pyc_vs_pyo(
self,
Expand Down