Run historical updater completion in a fresh interpreter so cached imports cannot revive retired dependency installers. Share Git and ZIP completion, carry receipt and recovery state, and preserve child exit status. Route plugin admission, binary acquisition, desktop launch and build paths through PM. Replace redundant helpers and tests with real worker, package, publication and launch checks. Keep the shipped compatibility surface fixed. Targeted Python and desktop checks pass. Native update journeys and fresh production image qualification remain pending. This is a checkpoint before those acceptance runs.
78 lines
2.6 KiB
Python
78 lines
2.6 KiB
Python
"""GC and downloads acquire the same partial owner before changing its files."""
|
|
from __future__ import annotations
|
|
|
|
from contextlib import nullcontext
|
|
import os
|
|
import time
|
|
|
|
import pytest
|
|
|
|
from pm.cli import _gc_store
|
|
from pm.download_state import partial_lock
|
|
from pm.lock import Facts
|
|
from pm.store import Store
|
|
|
|
|
|
@pytest.mark.parametrize("state", ["held", "recent-part", "recent-ranges", "unused"])
|
|
def test_gc_respects_partial_ownership_and_recent_resume(tmp_path, monkeypatch, state):
|
|
partials = tmp_path / "partials"
|
|
partials.mkdir()
|
|
part = partials / "example.part"
|
|
side = partials / "example.ranges"
|
|
part.write_bytes(b"partial")
|
|
side.write_text("[]", encoding="utf-8")
|
|
old = time.time() - 10 * 24 * 60 * 60
|
|
for path in (part, side):
|
|
os.utime(path, (old, old))
|
|
if state.startswith("recent-"):
|
|
(part if state == "recent-part" else side).touch()
|
|
monkeypatch.setattr("pm.paths.partials_root", lambda: partials)
|
|
store = Store(tmp_path / "store")
|
|
with partial_lock(partials, "example") if state == "held" else nullcontext():
|
|
_gc_store(store, Facts(store.root / "facts.json"))
|
|
assert part.exists() is (state != "unused")
|
|
assert side.exists() is (state != "unused")
|
|
if state == "held":
|
|
assert (partials / ".locks" / "example").is_file()
|
|
_gc_store(store, Facts(store.root / "facts.json"))
|
|
assert not part.exists() and not side.exists()
|
|
assert (partials / ".locks" / "example").is_file()
|
|
|
|
|
|
def test_pause_interrupts_an_owner_wait(tmp_path):
|
|
from threading import Event, Thread
|
|
from pm.downloader import Download, DownloadPaused, Source
|
|
|
|
destination = tmp_path / "already-present"
|
|
destination.write_bytes(b"complete")
|
|
download = Download([Source("https://example.invalid/model", destination)], partials_dir=tmp_path / "partials")
|
|
waiting = Event()
|
|
finished = Event()
|
|
errors = []
|
|
original_key = download._key
|
|
|
|
def key(url):
|
|
waiting.set()
|
|
return original_key(url)
|
|
|
|
download._key = key
|
|
|
|
def run():
|
|
try:
|
|
download.run()
|
|
except Exception as exc:
|
|
errors.append(exc)
|
|
finally:
|
|
finished.set()
|
|
|
|
with partial_lock(download.partials_dir, original_key(download.sources[0].url)):
|
|
worker = Thread(target=run)
|
|
worker.start()
|
|
assert waiting.wait(timeout=5)
|
|
download.pause()
|
|
stopped_while_locked = finished.wait(timeout=5)
|
|
worker.join(timeout=5)
|
|
assert stopped_while_locked
|
|
assert len(errors) == 1 and isinstance(errors[0], DownloadPaused)
|
|
assert destination.read_bytes() == b"complete"
|