Move retention, orphan pruning, status and clear operations into a topical sibling and route callers and tests directly to their defining module. Keep the shared store paths and git execution in checkpoint_manager; shorten the local child-env WHAT docstring.
75 lines
2.5 KiB
Python
75 lines
2.5 KiB
Python
"""``utils.rmtree_readonly`` removes trees that ``shutil.rmtree`` refuses.
|
|
|
|
Git marks loose object files read-only on Windows (``WinError 5``), and package
|
|
installs arrive as read-only trees on POSIX, so every cleanup path that deletes a
|
|
clone needs the retry. Regression coverage for #117170, #117176, #117179 and #117184.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import stat
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
import utils
|
|
from utils import rmtree_readonly
|
|
|
|
|
|
def _read_only_object_dir(root: Path) -> Path:
|
|
"""A clone-shaped tree whose loose object and its directory are read-only."""
|
|
obj_dir = root / ".git" / "objects" / "4b"
|
|
obj_dir.mkdir(parents=True)
|
|
obj = obj_dir / "825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
obj.write_text("blob", encoding="utf-8")
|
|
obj.chmod(0o444)
|
|
obj_dir.chmod(0o555) # POSIX unlink needs a writable parent
|
|
return obj_dir
|
|
|
|
|
|
def test_checkpoint_clear_all_removes_tree_with_read_only_object(tmp_path):
|
|
"""Driven through a production call site (#117170): ``checkpoint_maintenance.clear_all`` must
|
|
reach ``rmtree_readonly`` — a bare ``shutil.rmtree`` there reports ``deleted=False``."""
|
|
from tools.checkpoint_maintenance import clear_all
|
|
|
|
root = tmp_path / "checkpoints"
|
|
obj_dir = _read_only_object_dir(root)
|
|
assert not (obj_dir / "825dc642cb6eb9a060e54bf8d69288fbee4904").stat().st_mode & stat.S_IWUSR
|
|
|
|
out = clear_all(root)
|
|
|
|
assert out["deleted"] is True
|
|
assert not root.exists()
|
|
|
|
|
|
@pytest.mark.platforms("windows")
|
|
def test_removes_read_only_file_in_writable_directory(tmp_path):
|
|
"""The Git-for-Windows shape: the file is read-only, its directory is writable."""
|
|
root = tmp_path / "plugins" / "demo"
|
|
obj_dir = root / ".git" / "objects" / "4b"
|
|
obj_dir.mkdir(parents=True)
|
|
(obj_dir / "825dc642cb6eb9a060e54bf8d69288fbee4904").write_text("blob", encoding="utf-8")
|
|
(obj_dir / "825dc642cb6eb9a060e54bf8d69288fbee4904").chmod(stat.S_IREAD)
|
|
|
|
rmtree_readonly(root)
|
|
|
|
assert not root.exists()
|
|
|
|
|
|
def test_non_permission_failures_propagate(tmp_path, monkeypatch):
|
|
"""Only ``PermissionError`` is retried — everything else keeps rmtree semantics."""
|
|
attempts: list = []
|
|
|
|
def _fake(path, **kwargs):
|
|
attempts.append(path)
|
|
raise OSError(39, "Directory not empty", str(path))
|
|
|
|
monkeypatch.setattr(utils.shutil, "rmtree", _fake)
|
|
|
|
with pytest.raises(OSError) as excinfo:
|
|
rmtree_readonly(tmp_path)
|
|
|
|
assert excinfo.value.errno == 39
|
|
assert len(attempts) == 1 # no second attempt for a non-permission failure
|