The selection record (the install's facts.json, a side environment's active.json) is already written last, atomically and fsynced, so it is the generation's completion marker. But nothing flushed the tree it vouches for first: a power loss mid-publish could persist the record while the venv's files were still in the page cache, and the next boot would select a half-written environment whose pyvenv.cfg happened to exist. Both generation publishers (Venv via sync_venv, and _ensure_generation for side environments) now call pm.environments.flush_before_selecting() after the tree is complete and before the record is written: one os.sync() rather than an fsync per file, which would cost minutes for a venv. Windows has no os.sync; there the atomic record is the only guarantee, documented in place. Reusing the existing record instead of adding a new marker file means no migration: generations published by earlier builds stay selectable, and no reader needs a legacy rule. Every reader of the selection (selected_venv, environment_python, and their callers) already requires the record.
75 lines
3.0 KiB
Python
75 lines
3.0 KiB
Python
"""A generation's selection record is its completion marker, so it may only be
|
|
written after the finished tree has been flushed to disk. Otherwise a power loss
|
|
mid-publish can persist the (fsynced) record while the venv's files are still in
|
|
the page cache, and the next boot selects a half-written environment.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
|
|
|
|
def _flush_probe(monkeypatch, observe):
|
|
"""Replace the filesystem-wide flush with one that records what was on disk
|
|
when it ran; ``raising=False`` so the contract is exercised on Windows too."""
|
|
seen: list = []
|
|
monkeypatch.setattr(os, "sync", lambda: seen.append(observe()), raising=False)
|
|
return seen
|
|
|
|
|
|
def test_install_selection_is_committed_only_after_its_tree_is_flushed(tmp_path, monkeypatch):
|
|
import pm.install as install
|
|
from pm import paths
|
|
from pm.environments import install_state_dir, runtime_facts_path, selected_venv
|
|
|
|
store = tmp_path / "tools"
|
|
store.mkdir()
|
|
monkeypatch.setattr("pm.paths.store_root", lambda: store)
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
monkeypatch.setattr(paths, "repo_root", lambda: repo)
|
|
venv = install.get_package("venv")
|
|
monkeypatch.setattr(venv, "expected_stamp", lambda extras, **kwargs: "stamp")
|
|
candidate = install_state_dir(repo) / "environments" / "fresh" / "venv"
|
|
|
|
def apply(*args, **kwargs):
|
|
candidate.mkdir(parents=True)
|
|
(candidate / "pyvenv.cfg").write_text("home = fixture\n", encoding="utf-8")
|
|
return {"environment": candidate}
|
|
|
|
monkeypatch.setattr(venv, "apply", apply)
|
|
|
|
def committed() -> bool:
|
|
return runtime_facts_path(repo).is_file() and selected_venv(repo) == candidate.resolve()
|
|
|
|
flushes = _flush_probe(monkeypatch, lambda: (candidate / "pyvenv.cfg").is_file() and not committed())
|
|
install.sync_venv([], explicit=True)
|
|
|
|
assert flushes == [True], "the tree must be flushed after it is complete and before it is selected"
|
|
assert selected_venv(repo) == candidate.resolve()
|
|
|
|
|
|
def test_side_environment_selection_is_committed_only_after_its_tree_is_flushed(tmp_path, monkeypatch):
|
|
from pm import _uv, operations
|
|
|
|
base = tmp_path / "base" / "python"
|
|
base.parent.mkdir()
|
|
base.touch()
|
|
monkeypatch.setattr(_uv, "_toolchain", lambda **kwargs: (None, base))
|
|
root = tmp_path / "side"
|
|
built: list = []
|
|
|
|
def build(generation, base_python):
|
|
environment = generation / "venv"
|
|
python = operations._python(environment)
|
|
python.parent.mkdir(parents=True)
|
|
python.touch()
|
|
(environment / "pyvenv.cfg").write_text("home = fixture\n", encoding="utf-8")
|
|
built.append(environment)
|
|
return python
|
|
|
|
flushes = _flush_probe(monkeypatch, lambda: bool(built) and not (root / "active.json").exists())
|
|
python = operations._ensure_generation("proof", root, {}, build, record={}, explicit=True)
|
|
|
|
assert flushes == [True], "the tree must be flushed after it is complete and before it is selected"
|
|
assert operations.environment_python("proof", root=root) == python
|