Skip to content

Fix flaky editable install tests: stale .pyc due to integer-second mtime race - #5253

Merged
jaraco merged 4 commits into
mainfrom
copilot/fix-flaky-editable-tests
Jul 4, 2026
Merged

Fix flaky editable install tests: stale .pyc due to integer-second mtime race#5253
jaraco merged 4 commits into
mainfrom
copilot/fix-flaky-editable-tests

Conversation

Copilot AI commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Some editable-mode tests (test_editable_with_pyproject, TestOverallBehaviour.test_editable_install) were intermittently failing on fast machines (macOS ARM64, modern Ubuntu) with strict editable installs, producing stale values after source-file modifications.

Root cause: CPython stores .pyc source mtimes as 4-byte integers (seconds). When a test rewrites a source file within the same wall-clock second that its .pyc was compiled, the stored mtime matches the current mtime and the stale bytecode is reused silently.

Changes

  • _advance_mtime(path) — new test helper that bumps a file's mtime by 2 s via os.utime. Two seconds (vs. one) guards against rounding at second boundaries.
  • test_editable_with_pyproject — call _advance_mtime on mod.py after the in-test write; remove @pytest.mark.xfail(sys.platform == "darwin") workaround.
  • TestOverallBehaviour.test_editable_install — call _advance_mtime on mod1.py and mod2.py after their writes; remove the same xfail(darwin) marker.

No changes to production code — the link tree in strict mode is correct; the race was purely in test timing.

Pull Request Checklist

…le pyc

Python's .pyc validation uses integer-second mtimes. When tests write to
source files within the same wall-clock second that the .pyc was compiled,
the cached bytecode is reused silently. Add _advance_mtime() helper that
bumps mtime by 2 s via os.utime, and call it on .py files after each
in-test write. Remove the darwin-only xfail markers that were masking the
failures. Closes #4328.
Copilot AI changed the title [WIP] Fix unstable editable tests on macos-14-arm64 Fix flaky editable install tests: stale .pyc due to integer-second mtime race Jul 4, 2026
Copilot finished work on behalf of jaraco July 4, 2026 16:11
Copilot AI requested a review from jaraco July 4, 2026 16:11
Comment thread setuptools/tests/test_editable_install.py Outdated
Copilot finished work on behalf of jaraco July 4, 2026 16:21
Copilot AI requested a review from jaraco July 4, 2026 16:21
@jaraco

jaraco commented Jul 4, 2026

Copy link
Copy Markdown
Member

I was wondering about a different approach, but Claude convinced me the proposed approach is preferable.

Details

Looking at #5253, I'm wondering:

If we presume the analysis is correct and the cause is the stale but unchanged timestamp on the file causing the .pyc to be stale, would it not make more sense to purge the .pyc instead of mutating the mod time?

How would you weigh these two options?

I'll look at the issue to ground my answer in the actual context.

Let me look at the actual test code involved to give a grounded answer.

Good question — and having looked at the actual test, I think the two options are not as symmetric as they first appear. Here's how I'd weigh them.

What the test is actually doing

In test_editable_with_pyproject ([test_editable_install.py:151](setuptools/tests/test_editable_install.py:151)):

(project / "src/mypkg/mod.py").write_text("x = 42", encoding="utf-8")
assert venv.run(cmd).strip() == "3.14159.post0 foobar 42"

The first venv.run compiled a .pyc with the source's integer-second mtime. The rewrite lands in the same wall-clock second, so the stored mtime still matches and CPython reuses stale bytecode. Both options break that tie; the question is how.

The asymmetries that matter

1. Coverage — purging removes the thing the test exercises.
The mtime bump keeps the .pyc invalidation path live: the module still gets recompiled, just triggered correctly. If you rm the .pyc, the test passes even if invalidation were genuinely broken, because there's no cache to invalidate. That's masking rather than reflecting reality. For an editable-install test — where "does a source edit get picked up?" is close to the whole point — you don't want to quietly delete the mechanism under test.

2. Locatability — os.utime touches a file you already hold; purging means hunting __pycache__.
The mtime bump acts on mod.py, a path the test already constructed. Purging requires finding the right __pycache__/mod.*.pyc — and its location varies across the editable_opts matrix (strict link-tree vs. lenient, plus where the interpreter chose to write bytecode). You'd be encoding an assumption about CPython's cache layout into the test, and it may not even exist yet at write time. That's more fragile and more coupled.

3. Fidelity — the bump is the more realistic scenario, not the more artificial one.
It's tempting to see os.utime(+2s) as a hack, but a normal human edit lands more than a second after the last build and naturally advances mtime. The same-second collision is the artifact introduced by test speed. Bumping the mtime restores the ordinary case; it doesn't fabricate an unusual one.

Where purge would win

Purging is conceptually cleaner — it removes the confound entirely instead of adding a compensating nudge, and it can't be defeated by any future mtime-resolution weirdness. If this were a test whose subject had nothing to do with bytecode caching, "just delete the stale cache" would be the tidier fix. But that's not this test.

My recommendation

Keep the _advance_mtime approach (the PR's current direction). It preserves coverage of the invalidation path, is location-independent, and models the realistic edit. Two refinements worth making regardless:

  • Bump relative to the file's current mtime (os.utime to st_mtime + 2), not to a fixed wall-clock, so it's guaranteed monotonic even if the write already advanced mtime.
  • A one-line comment at the call site pointing at Some editable tests seem to be unstable (flaky) and platform sensitive #4328 / the integer-mtime rationale, since a bare +2s reads as a mystery constant to the next person.

The one case where I'd flip to purge: if the bump turns out flaky on some CI filesystem (coarse mtime granularity, or a mount that clamps mtime) — then removing the cache sidesteps the timestamp game entirely, at the cost of the coverage point above.

Copilot finished work on behalf of jaraco July 4, 2026 17:03
@jaraco
jaraco force-pushed the copilot/fix-flaky-editable-tests branch from 9b62841 to 45c53ba Compare July 4, 2026 17:21
@jaraco
jaraco marked this pull request as ready for review July 4, 2026 17:27
@jaraco jaraco closed this Jul 4, 2026
@jaraco jaraco reopened this Jul 4, 2026
@mergify

mergify Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Tick the box to add this pull request to the merge queue (same as @mergifyio queue).

  • Queue this pull request

@jaraco
jaraco merged commit 626d8c8 into main Jul 4, 2026
73 of 118 checks passed
@jaraco
jaraco deleted the copilot/fix-flaky-editable-tests branch July 4, 2026 19:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Some editable tests seem to be unstable (flaky) and platform sensitive

2 participants