feat(gc): add pkgmgr gc --report, a read-only disk inventory

pkgmgr gc --report lists each linked worktree and orphaned .claude/worktrees/agent-* dir with its size and last activity, the changes a cleanup would keep, the biggest children of every --root, ELF core dumps, docker system df with --docker, and free versus attributed space per filesystem; --json prints the same as JSON. It deletes and writes nothing and refuses --apply. git run() gains input_text for cat-file --batch-check.

🎯 Fix: 90 %
💥 Break: 2 %
💸 Cost: new
⏱️ Time: new
🛡️ Security: 100 %
🏆 Score: 88
🚦 Gate: cost new, time new: cleared by the operator's commit word for pkgmgr gc --report (2026-11-07)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kevin Veen-BirkenbachandClaude Opus 5.5 committed 2026-11-07 11:42:26 +00:00
1 parent 1b9b93a542
commit c78e0a34a3
26 files changed
+1699 -2

No files matched your search

+215
View File
@@ -0,0 +1,215 @@
from __future__ import annotations
import io
import json
import os
import subprocess
import tempfile
import unittest
from contextlib import redirect_stdout
from unittest.mock import patch
from pkgmgr.actions.gc.report import disk_report, render_text
from pkgmgr.actions.gc.report import docker_usage as docker_module
from pkgmgr.actions.gc.report import walk as walk_module
from pkgmgr.cli.commands.worktree import handle_gc
from pkgmgr.cli.parser import create_parser
GIT_ENV = {
**os.environ,
"GIT_CONFIG_GLOBAL": os.devnull,
"GIT_CONFIG_NOSYSTEM": "1",
"GIT_AUTHOR_NAME": "t",
"GIT_AUTHOR_EMAIL": "t@example.org",
"GIT_COMMITTER_NAME": "t",
"GIT_COMMITTER_EMAIL": "t@example.org",
}
THRESHOLD = 32 * 1024
ELF_CORE = b"\x7fELF\x02\x01\x01" + bytes(9) + (4).to_bytes(2, "little")
ELF_EXEC = b"\x7fELF\x02\x01\x01" + bytes(9) + (2).to_bytes(2, "little")
DOCKER_DF = (
'{"Active":"1","Reclaimable":"0B","Size":"1GB","TotalCount":"3","Type":"Images"}\n'
)
def git(cwd: str, *args: str) -> str:
return subprocess.run(
["git", *args], cwd=cwd, env=GIT_ENV, check=True, capture_output=True, text=True
).stdout
def write(path: str, data: bytes | str, size: int | None = None) -> str:
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fh:
fh.write(data.encode() if isinstance(data, str) else data)
if size is not None:
os.truncate(path, size)
return path
class TestDiskReport(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.tmp = os.path.realpath(self._tmp.name)
self.repo = os.path.join(self.tmp, "repo")
git(self.tmp, "init", "-q", "-b", "main", self.repo)
write(os.path.join(self.repo, "a.txt"), "base")
git(self.repo, "add", "a.txt")
git(self.repo, "commit", "-q", "-m", "base")
git(self.repo, "checkout", "-q", "-b", "other")
write(os.path.join(self.repo, "a.txt"), "kept")
write(os.path.join(self.repo, "b.txt"), "kept-b")
git(self.repo, "add", "a.txt", "b.txt")
git(self.repo, "commit", "-q", "-m", "other")
git(self.repo, "checkout", "-q", "main")
self.clean = self._worktree("wt-clean", "-b", "clean")
self.kept = self._worktree("wt-kept", "-b", "kept")
write(os.path.join(self.kept, "a.txt"), "kept")
git(self.kept, "add", "a.txt")
write(os.path.join(self.kept, "b.txt"), "kept-b")
self.lost = self._worktree("wt-lost", "-b", "lost")
write(os.path.join(self.lost, "new.txt"), "only here")
self.detached = self._worktree("wt-detached", "--detach")
write(os.path.join(self.detached, "c.txt"), "c")
git(self.detached, "add", "c.txt")
git(self.detached, "commit", "-q", "-m", "unreachable")
write(os.path.join(self.detached, "a.txt"), "kept")
agents = os.path.join(self.repo, ".claude", "worktrees")
self.agent = self._worktree(os.path.join(agents, "agent-y"), "-b", "agent-y")
self.orphan = os.path.join(agents, "agent-x")
write(os.path.join(self.orphan, "notes.txt"), "left behind")
self.root = os.path.join(self.tmp, "root")
self.dump = write(
os.path.join(self.root, "core"), ELF_CORE + b"c" * 64 * 1024, 1 << 30
)
self.binary = write(
os.path.join(self.root, "core.12"), ELF_EXEC + b"e" * 64 * 1024
)
write(os.path.join(self.root, "bigdir", "blob"), b"b" * 128 * 1024)
write(os.path.join(self.root, "small"), "s")
def tearDown(self) -> None:
self._tmp.cleanup()
def _worktree(self, name: str, *args: str) -> str:
path = os.path.join(self.tmp, name)
git(self.repo, "worktree", "add", "-q", *args, path, "main")
return path
def _report(self) -> dict:
with patch.object(docker_module, "_docker", return_value=DOCKER_DF):
return disk_report([self.root], [self.repo], THRESHOLD, docker=True)
def _cli(self, *argv: str) -> tuple[int, str]:
args = create_parser("test").parse_args(["gc", *argv])
out = io.StringIO()
with (
patch("pkgmgr.cli.commands.branch.main_repo_root", return_value=self.repo),
patch(
"pkgmgr.cli.commands.branch.cleanup",
side_effect=AssertionError("the report must not run a cleanup"),
),
patch.object(docker_module, "_docker", return_value=DOCKER_DF),
redirect_stdout(out),
self.assertRaises(SystemExit) as stop,
):
handle_gc(args, None)
return stop.exception.code, out.getvalue()
def test_cli_report_prints_the_json_inventory(self) -> None:
code, out = self._cli("--report", "--json", "--root", self.root, "--docker")
report = json.loads(out)
self.assertEqual(code, 0)
self.assertIn(self.dump, [dump["path"] for dump in report["core_dumps"]])
self.assertIn(self.lost, [tree["path"] for tree in report["worktrees"]])
self.assertIsNotNone(report["docker"])
def test_cli_report_refuses_apply(self) -> None:
self.assertEqual(self._cli("--report", "--apply"), (2, ""))
def test_worktrees_and_orphaned_agent_dir(self) -> None:
trees = {t["path"]: t for t in self._report()["worktrees"]}
expected = {
self.clean,
self.kept,
self.lost,
self.detached,
self.agent,
self.orphan,
}
self.assertEqual(set(trees), expected)
self.assertTrue(trees[self.orphan]["orphaned"])
self.assertFalse(trees[self.agent]["orphaned"])
self.assertEqual(trees[self.kept]["branch"], "kept")
self.assertIsNone(trees[self.detached]["branch"])
for tree in trees.values():
self.assertGreater(tree["size_bytes"], 0)
self.assertIsNotNone(tree["last_activity"])
self.assertEqual(tree["foreign_entries"], 0)
def test_foreign_entries_through_patched_owner_check(self) -> None:
with patch.object(
walk_module,
"is_foreign",
side_effect=lambda path, st: os.path.basename(path) == "new.txt",
):
trees = {t["path"]: t for t in self._report()["worktrees"]}
self.assertEqual(trees[self.lost]["foreign_entries"], 1)
self.assertEqual(trees[self.clean]["foreign_entries"], 0)
def test_preserved_verdicts_and_nothing_written(self) -> None:
before = git(self.kept, "status", "--porcelain=v2", "--untracked-files=all")
verdicts = {p["path"]: p for p in self._report()["preserved"]}
self.assertEqual(set(verdicts), {self.kept, self.lost, self.detached})
self.assertTrue(verdicts[self.kept]["preserved"])
self.assertEqual(verdicts[self.kept]["paths"], 2)
self.assertFalse(verdicts[self.lost]["preserved"])
self.assertEqual(verdicts[self.lost]["reasons"], ["new.txt: content in no ref"])
self.assertFalse(verdicts[self.detached]["preserved"])
self.assertEqual(
verdicts[self.detached]["reasons"], ["detached HEAD reachable from no ref"]
)
after = git(self.kept, "status", "--porcelain=v2", "--untracked-files=all")
self.assertEqual(before, after)
def test_roots_core_dumps_docker_and_balance(self) -> None:
report = self._report()
children = report["roots"][0]["children"]
self.assertEqual(
[(os.path.basename(c["path"]), c["kind"]) for c in children],
[("bigdir", "dir"), ("core", "core-dump"), ("core.12", "file")],
)
self.assertEqual([d["path"] for d in report["core_dumps"]], [self.dump])
dump = report["core_dumps"][0]
self.assertEqual(dump["apparent_bytes"], 1 << 30)
self.assertLess(dump["size_bytes"], dump["apparent_bytes"])
self.assertEqual(report["docker"][0]["total"], 3)
self.assertEqual(len(report["balance"]), 1)
listed = sum(c["size_bytes"] for c in children)
listed += sum(t["size_bytes"] for t in report["worktrees"])
self.assertEqual(report["balance"][0]["attributed_bytes"], listed)
self.assertEqual(report["warnings"], [])
json.dumps(report)
text = render_text(report)
self.assertIn("orphaned", text)
self.assertIn("core-dump", text)
def test_unknown_repo_and_root_are_warnings(self) -> None:
missing = os.path.join(self.tmp, "absent")
report = disk_report([missing], [self.root], THRESHOLD, docker=False)
self.assertEqual(report["worktrees"], [])
self.assertIsNone(report["docker"])
self.assertEqual(len(report["warnings"]), 2)
if __name__ == "__main__":
unittest.main()
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,47 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pkgmgr.actions.gc.report.balance import balance
class TestBalance(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = os.path.realpath(self._tmp.name)
for rel in ("a/b", "c", "a-x"):
os.makedirs(os.path.join(self.root, rel))
def tearDown(self) -> None:
self._tmp.cleanup()
def _path(self, rel: str) -> str:
return os.path.join(self.root, rel)
def test_nested_and_repeated_items_count_once(self) -> None:
items = [
(self._path("a/b"), 50),
(self._path("a"), 100),
(self._path("a-x"), 7),
(self._path("c"), 10),
(self._path("c"), 10),
(self._path("gone"), 1000),
]
rows = balance([self.root, self.root], items)
self.assertEqual(len(rows), 1)
row = rows[0]
self.assertEqual(row["attributed_bytes"], 117)
self.assertTrue(os.path.ismount(row["filesystem"]))
self.assertGreater(row["total_bytes"], 0)
self.assertLessEqual(row["free_bytes"], row["total_bytes"])
def test_unreadable_paths_get_no_row(self) -> None:
self.assertEqual(balance([self._path("gone")], []), [])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,79 @@
from __future__ import annotations
import os
import tempfile
import unittest
from unittest.mock import ANY, patch
from pkgmgr.actions.gc.report.core_dumps import (
ELF_HEADER,
ET_CORE,
core_dumps,
is_core_dump,
)
ET_EXEC = 2
def elf(e_type: int, order: str = "little") -> bytes:
data = 1 if order == "little" else 2
return b"\x7fELF" + bytes([2, data, 1]) + bytes(9) + e_type.to_bytes(2, order)
class TestCoreDumps(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = os.path.realpath(self._tmp.name)
def tearDown(self) -> None:
self._tmp.cleanup()
def _file(self, name: str, data: bytes, size: int | None = None) -> str:
path = os.path.join(self.root, name)
with open(path, "wb") as fh:
fh.write(data + bytes(46))
if size is not None:
os.truncate(path, size)
return path
def test_et_core_is_detected_in_both_byte_orders(self) -> None:
self.assertTrue(is_core_dump(self._file("core", elf(ET_CORE))))
self.assertTrue(is_core_dump(self._file("core.1", elf(ET_CORE, "big"))))
def test_other_elf_types_and_non_elf_are_ignored(self) -> None:
self.assertFalse(is_core_dump(self._file("core", elf(ET_EXEC))))
self.assertFalse(is_core_dump(self._file("core.2", b"#!/bin/sh\n")))
short = os.path.join(self.root, "core.3")
with open(short, "wb") as fh:
fh.write(b"\x7fELF")
self.assertFalse(is_core_dump(short))
def test_symlink_is_not_followed(self) -> None:
target = self._file("real", elf(ET_CORE))
link = os.path.join(self.root, "core")
os.symlink(target, link)
self.assertFalse(is_core_dump(link))
def test_reads_no_more_than_the_elf_header(self) -> None:
path = self._file("core", elf(ET_CORE), size=1 << 20)
with patch("os.read", wraps=os.read) as read:
is_core_dump(path)
read.assert_called_once_with(ANY, ELF_HEADER)
def test_sparse_dump_is_sized_by_blocks_and_others_dropped(self) -> None:
dump = self._file("core", elf(ET_CORE), size=1 << 30)
binary = self._file("core.7", elf(ET_EXEC))
dumps = core_dumps([dump, binary, dump])
self.assertEqual([d["path"] for d in dumps], [dump])
self.assertEqual(dumps[0]["apparent_bytes"], 1 << 30)
self.assertLess(dumps[0]["size_bytes"], dumps[0]["apparent_bytes"])
self.assertEqual(dumps[0]["mtime"], os.lstat(dump).st_mtime)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,62 @@
from __future__ import annotations
import subprocess
import unittest
from unittest.mock import patch
from pkgmgr.actions.gc.report import docker_usage as module
DF_OUTPUT = (
'{"Active":"2","Reclaimable":"1.2GB (50%)","Size":"2.4GB",'
'"TotalCount":"10","Type":"Images"}\n'
'{"Active":"0","Reclaimable":"0B","Size":"0B","TotalCount":"0",'
'"Type":"Build Cache"}\n'
)
class TestDockerUsage(unittest.TestCase):
def test_rows_are_parsed_from_json_lines(self) -> None:
with patch.object(module, "_docker", return_value=DF_OUTPUT) as docker:
rows, warning = module.docker_usage()
docker.assert_called_once_with(["system", "df", "--format", "{{json .}}"])
self.assertIsNone(warning)
self.assertEqual(
rows[0],
{
"type": "Images",
"total": 10,
"active": 2,
"size": "2.4GB",
"reclaimable": "1.2GB (50%)",
},
)
self.assertEqual(rows[1]["type"], "Build Cache")
def test_missing_docker_is_a_warning(self) -> None:
with patch.object(module, "_docker", side_effect=FileNotFoundError):
rows, warning = module.docker_usage()
self.assertIsNone(rows)
self.assertIn("not installed", warning)
def test_failing_docker_is_a_warning(self) -> None:
error = subprocess.CalledProcessError(
1, ["docker", "system", "df"], stderr="daemon down"
)
with patch.object(module, "_docker", side_effect=error):
rows, warning = module.docker_usage()
self.assertIsNone(rows)
self.assertIn("daemon down", warning)
def test_unparsable_output_is_a_warning(self) -> None:
with patch.object(module, "_docker", return_value="TYPE TOTAL\n[1]\n"):
rows, warning = module.docker_usage()
self.assertIsNone(rows)
self.assertIn("not JSON lines", warning)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,70 @@
from __future__ import annotations
import os
import tempfile
import unittest
from unittest.mock import patch
from pkgmgr.actions.gc.report import preserved as module
HEAD_BLOB = "1" * 40
INDEX_BLOB = "2" * 40
STATUS = "\0".join(
[
f"1 .M N... 100644 100644 100644 {HEAD_BLOB} {INDEX_BLOB} dir/a file.txt",
" ".join(
["u UU N...", *["100644"] * 4, HEAD_BLOB, INDEX_BLOB, HEAD_BLOB, "c.txt"]
),
"? new file",
"",
]
)
class TestPreservedParsing(unittest.TestCase):
def test_status_records_keep_paths_with_spaces(self) -> None:
with patch.object(module, "_git", return_value=STATUS):
entries = module._status_entries("/wt")
self.assertEqual(
entries,
[
("dir/a file.txt", INDEX_BLOB),
("c.txt", module.UNMERGED),
("new file", None),
],
)
def test_blob_must_sit_at_the_same_path_of_some_tip(self) -> None:
wanted = [("a", {HEAD_BLOB}), ("b", {HEAD_BLOB, INDEX_BLOB})]
answer = f"{HEAD_BLOB}\nt2:a missing\n{HEAD_BLOB}\nt2:b missing"
with patch.object(module, "run", return_value=answer) as run:
absent = module._absent("/repo", ["t1", "t2"], wanted)
self.assertEqual(absent, ["b"])
self.assertEqual(run.call_args.kwargs["input_text"], "t1:a\nt2:a\nt1:b\nt2:b\n")
def test_special_files_are_never_opened(self) -> None:
with tempfile.TemporaryDirectory() as tree:
os.mkfifo(os.path.join(tree, "fifo"))
os.mkdir(os.path.join(tree, "sub"))
with patch.object(module, "run") as run:
hashes = module._hashes(tree, ["fifo", "gone", "sub"])
run.assert_not_called()
self.assertEqual(
hashes,
{"fifo": module.SPECIAL, "gone": module.DELETED, "sub": module.DIRECTORY},
)
def test_no_tips_means_nothing_is_preserved(self) -> None:
with patch.object(module, "run") as run:
self.assertEqual(module._absent("/repo", [], [("a", {HEAD_BLOB})]), ["a"])
run.assert_not_called()
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,123 @@
from __future__ import annotations
import unittest
from pkgmgr.actions.gc.report.render import render_text
NOW = 1_000_000.0
def report(**overrides: object) -> dict:
base = {
"generated_at": NOW,
"threshold_bytes": 1 << 20,
"worktrees": [
{
"path": "/r/.worktrees/x",
"branch": None,
"orphaned": False,
"missing": False,
"size_bytes": 3 << 30,
"last_activity": NOW - 2 * 86400,
"foreign_entries": 4,
},
{
"path": "/r/.claude/worktrees/agent-1",
"branch": None,
"orphaned": True,
"missing": False,
"size_bytes": 5 << 20,
"last_activity": NOW - 90,
"foreign_entries": 0,
},
],
"preserved": [
{
"path": "/r/.worktrees/x",
"branch": None,
"paths": 2,
"preserved": False,
"reasons": [
"detached HEAD reachable from no ref",
"a: content in no ref",
],
}
],
"roots": [
{
"root": "/tmp",
"children": [
{
"path": "/tmp/core",
"kind": "core-dump",
"size_bytes": 2 << 20,
"newest_mtime": NOW - 3 * 3600,
}
],
}
],
"core_dumps": [
{
"path": "/tmp/core",
"size_bytes": 2 << 20,
"apparent_bytes": 1 << 30,
"mtime": NOW - 3 * 3600,
}
],
"docker": None,
"balance": [
{
"filesystem": "/",
"total_bytes": 100 << 30,
"free_bytes": 10 << 30,
"attributed_bytes": 3 << 30,
}
],
"warnings": ["docker unavailable: not installed"],
}
return {**base, **overrides}
class TestRenderText(unittest.TestCase):
def test_sections_and_rows(self) -> None:
lines = render_text(report()).splitlines()
self.assertIn("== worktrees", lines)
worktree = next(line for line in lines if line.endswith("/r/.worktrees/x"))
self.assertEqual(
worktree.split(),
["3.0G", "2.0d", "4", "linked", "(detached)", "/r/.worktrees/x"],
)
orphan = next(line for line in lines if "agent-1" in line)
self.assertEqual(orphan.split()[1:5], ["1.5m", "0", "orphaned", "-"])
verdict = next(line for line in lines if line.startswith("no "))
self.assertIn(
"detached HEAD reachable from no ref; a: content in no ref", verdict
)
self.assertIn("== root /tmp (over 1.0M)", lines)
rows = [line.split() for line in lines]
self.assertIn(["2.0M", "1.0G", "3.0h", "/tmp/core"], rows)
self.assertIn(["/", "100.0G", "10.0G", "3.0G"], rows)
self.assertEqual(lines[-1], "WARN docker unavailable: not installed")
self.assertNotIn("== docker", lines)
def test_docker_rows_and_empty_sections(self) -> None:
docker = [
{
"type": "Images",
"total": 3,
"active": 1,
"size": "1GB",
"reclaimable": "0B",
}
]
text = render_text(report(docker=docker, core_dumps=[], warnings=[]))
self.assertIn("== core dumps\n(none)", text)
self.assertIn("== docker", text)
self.assertIn("Images 3 1 1GB 0B", text)
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,79 @@
from __future__ import annotations
import os
import tempfile
import unittest
from pkgmgr.actions.gc.report.roots import scan_root
THRESHOLD = 16 * 1024
ET_CORE_HEADER = b"\x7fELF\x02\x01\x01" + bytes(9) + (4).to_bytes(2, "little")
class TestScanRoot(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = os.path.realpath(self._tmp.name)
def tearDown(self) -> None:
self._tmp.cleanup()
def _write(self, rel: str, size: int = 0, head: bytes = b"") -> str:
path = os.path.join(self.root, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fh:
fh.write(head + b"d" * size)
return path
def _big(self, rel: str, size: int = 64 * 1024) -> str:
return self._write(rel, size)
def test_children_over_threshold_get_kinds_biggest_first(self) -> None:
self._big("node_modules/pkg/index.js", 200 * 1024)
self._big("wt/data", 180 * 1024)
self._write("wt/.git", head=b"gitdir: /elsewhere\n")
self._big("clone/.git/objects/pack", 160 * 1024)
self._big("bare.git/objects/blob", 140 * 1024)
self._write("bare.git/HEAD", head=b"ref: refs/heads/main\n")
self._big("venv/lib/site.py", 120 * 1024)
self._write("venv/pyvenv.cfg", head=b"home = /usr\n")
self._big(".cache/blob", 100 * 1024)
self._big("tagged/blob", 90 * 1024)
self._write("tagged/CACHEDIR.TAG", head=b"Signature: x\n")
self._big("plain/blob", 80 * 1024)
self._write("core", 70 * 1024, head=ET_CORE_HEADER)
self._big("loose.bin", 60 * 1024)
self._write("small/file", 10)
cores: list[str] = []
scanned = scan_root(self.root, THRESHOLD, cores)
kinds = [(os.path.basename(c["path"]), c["kind"]) for c in scanned["children"]]
self.assertEqual(
kinds,
[
("node_modules", "node_modules"),
("wt", "worktree"),
("clone", "git-repo"),
("bare.git", "git-repo"),
("venv", "venv"),
(".cache", "cache"),
("tagged", "cache"),
("plain", "dir"),
("core", "core-dump"),
("loose.bin", "file"),
],
)
self.assertEqual(scanned["root"], self.root)
self.assertEqual(cores, [os.path.join(self.root, "core")])
sizes = [c["size_bytes"] for c in scanned["children"]]
self.assertEqual(sizes, sorted(sizes, reverse=True))
self.assertTrue(all(c["newest_mtime"] > 0 for c in scanned["children"]))
def test_missing_root_raises(self) -> None:
with self.assertRaises(OSError):
scan_root(os.path.join(self.root, "absent"), THRESHOLD, [])
if __name__ == "__main__":
unittest.main()
@@ -0,0 +1,113 @@
from __future__ import annotations
import os
import tempfile
import unittest
from unittest.mock import patch
from pkgmgr.actions.gc.report import walk as walk_module
from pkgmgr.actions.gc.report.walk import BLOCK, walk
def allocated(*paths: str) -> int:
return sum(os.lstat(path).st_blocks * BLOCK for path in paths)
class TestWalk(unittest.TestCase):
def setUp(self) -> None:
self._tmp = tempfile.TemporaryDirectory()
self.root = os.path.realpath(self._tmp.name)
def tearDown(self) -> None:
self._tmp.cleanup()
def _write(self, rel: str, data: bytes = b"x") -> str:
path = os.path.join(self.root, rel)
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as fh:
fh.write(data)
return path
def test_sparse_file_counts_allocated_blocks_only(self) -> None:
path = self._write("sparse")
os.truncate(path, 1 << 30)
stats = walk(self.root)
self.assertEqual(stats.size_bytes, allocated(self.root, path))
self.assertLess(stats.size_bytes, 1 << 30)
def test_symlink_target_is_not_followed(self) -> None:
with tempfile.TemporaryDirectory() as outside:
target = os.path.join(outside, "big")
with open(target, "wb") as fh:
fh.write(b"y" * (1 << 20))
link = os.path.join(self.root, "link")
os.symlink(target, link)
stats = walk(self.root)
self.assertEqual(stats.size_bytes, allocated(self.root, link))
self.assertLess(stats.size_bytes, 1 << 20)
def test_hard_link_is_counted_once(self) -> None:
first = self._write("a", b"z" * 65536)
os.link(first, os.path.join(self.root, "b"))
self.assertEqual(walk(self.root).size_bytes, allocated(self.root, first))
def test_foreign_entries_are_pruned_below_the_first_foreign_one(self) -> None:
self._write("foreign_dir/one")
self._write("foreign_dir/sub/two")
self._write("foreign_file")
self._write("mine")
plain = walk(self.root)
with patch.object(
walk_module,
"is_foreign",
side_effect=lambda path, st: os.path.relpath(path, self.root).startswith(
"foreign"
),
):
stats = walk(self.root)
self.assertEqual(plain.foreign_entries, 0)
self.assertEqual(stats.foreign_entries, 2)
self.assertEqual(stats.size_bytes, plain.size_bytes)
def test_files_named_like_core_dumps_are_candidates(self) -> None:
expected = {self._write("core"), self._write("deep/core.123")}
self._write("core.abc")
self._write("not-core")
os.makedirs(os.path.join(self.root, "dir", "core"))
self.assertEqual(set(walk(self.root).cores), expected)
@unittest.skipIf(os.getuid() == 0, "root lists every directory")
def test_unlistable_directory_is_counted(self) -> None:
locked = os.path.join(self.root, "locked")
os.mkdir(locked)
os.chmod(locked, 0)
try:
stats = walk(self.root)
finally:
os.chmod(locked, 0o700)
self.assertEqual(stats.unreadable, 1)
def test_missing_path_reports_unreadable(self) -> None:
stats = walk(os.path.join(self.root, "absent"))
self.assertEqual((stats.size_bytes, stats.unreadable), (0, 1))
def test_other_filesystem_is_skipped(self) -> None:
self._write("file")
stats = walk(self.root, dev=os.lstat(self.root).st_dev + 1)
self.assertEqual(stats.size_bytes, 0)
if __name__ == "__main__":
unittest.main()
+15
View File
@@ -40,6 +40,21 @@ class TestGitRun(unittest.TestCase):
self.assertTrue(kwargs["text"])
self.assertTrue(kwargs["capture_output"])
def test_scan_input_text_replaces_closed_stdin(self) -> None:
completed = MagicMock()
completed.stdout = "abc\n"
with patch(
"pkgmgr.core.git.run.subprocess.run", return_value=completed
) as mock_run:
out = run(["cat-file", "--batch-check"], scan=True, input_text="x\n")
self.assertEqual(out, "abc")
kwargs = mock_run.call_args.kwargs
self.assertEqual(kwargs["input"], "x\n")
self.assertNotIn("stdin", kwargs)
self.assertIn("timeout", kwargs)
def test_failure_raises_giterror_with_details(self) -> None:
# Build a CalledProcessError with stdout/stderr populated
import subprocess as sp