Files
pulse/scripts/tests/test_docs_mirror.py
rcourtman d227cfcb10 Stop background git from racing test_docs_mirror temp cleanup
test_staged_root_sourced_doc_with_stale_mirror_fails errored in the
"Script smoke tests & backend build" job on PR #1857 (run 33609703555)
with OSError [Errno 39] Directory not empty: '.git' raised from
TemporaryDirectory cleanup. The test body passed; a background git
process spawned by init/add/commit (auto-gc, fsmonitor, or maintenance)
was still writing under .git when shutil.rmtree ran. The suite passes on
main most of the time and locally, so this is a race, not a logic bug.

Disable gc.auto, core.fsmonitor and maintenance.auto for the throwaway
repos, and point GIT_CONFIG_GLOBAL/GIT_CONFIG_SYSTEM at os.devnull so a
runner's host config cannot re-enable them. Construct both temp
directories with ignore_cleanup_errors=True as a belt-and-braces fallback
so a straggler can never fail a test whose assertions already passed.
2026-09-02 09:55:47 +01:00

183 lines
6.5 KiB
Python

#!/usr/bin/env python3
"""Guard the shipped-docs mirror check that backs the pre-commit hook."""
from __future__ import annotations
import importlib.util
import os
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
ROOT = Path(__file__).resolve().parents[2]
SCRIPT = ROOT / "scripts" / "check_docs_mirror.py"
SPEC = importlib.util.spec_from_file_location("check_docs_mirror", SCRIPT)
assert SPEC is not None and SPEC.loader is not None
docs_mirror = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = docs_mirror
SPEC.loader.exec_module(docs_mirror)
# Background git processes (auto-gc, fsmonitor, maintenance) can still be
# writing under .git when TemporaryDirectory cleanup runs, which makes rmtree
# fail with "Directory not empty". Disable them for the throwaway repos and
# ignore host config so it cannot re-enable them.
GIT_ENV = {
**os.environ,
"GIT_CONFIG_GLOBAL": os.devnull,
"GIT_CONFIG_SYSTEM": os.devnull,
}
def run_git(root: Path, *args: str) -> None:
subprocess.run(
[
"git",
"-C",
str(root),
"-c",
"user.email=test@example.invalid",
"-c",
"user.name=test",
"-c",
"gc.auto=0",
"-c",
"core.fsmonitor=false",
"-c",
"maintenance.auto=false",
*args,
],
check=True,
capture_output=True,
env=GIT_ENV,
)
def write(root: Path, relative: str, content: str) -> None:
path = root / relative
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(content, encoding="utf-8")
class DocsMirrorMappingTest(unittest.TestCase):
def test_docs_sourced_mapping(self) -> None:
self.assertEqual(
docs_mirror.source_for("frontend-modern/public/docs/i18n/de/README.md"),
"docs/i18n/de/README.md",
)
def test_root_sourced_mapping(self) -> None:
self.assertEqual(
docs_mirror.source_for("frontend-modern/public/docs/SECURITY.md"),
"SECURITY.md",
)
class DocsMirrorStagedTest(unittest.TestCase):
def setUp(self) -> None:
self._temporary = tempfile.TemporaryDirectory(ignore_cleanup_errors=True)
self.addCleanup(self._temporary.cleanup)
self.root = Path(self._temporary.name)
run_git(self.root, "init", "-q")
def test_synced_staged_pair_passes(self) -> None:
write(self.root, "docs/GUIDE.md", "# Guide\n")
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide\n")
run_git(self.root, "add", "docs/GUIDE.md", "frontend-modern/public/docs/GUIDE.md")
errors, warnings = docs_mirror.check_staged(self.root)
self.assertEqual(errors, [])
self.assertEqual(warnings, [])
def test_staged_source_with_stale_mirror_fails(self) -> None:
write(self.root, "docs/GUIDE.md", "# Guide v1\n")
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
run_git(self.root, "add", "-A")
run_git(self.root, "commit", "-q", "-m", "seed")
write(self.root, "docs/GUIDE.md", "# Guide v2\n")
run_git(self.root, "add", "docs/GUIDE.md")
errors, warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md", errors[0])
self.assertIn("git show :docs/GUIDE.md", errors[0])
self.assertEqual(warnings, [])
def test_staged_root_sourced_doc_with_stale_mirror_fails(self) -> None:
write(self.root, "SECURITY.md", "# Security v1\n")
write(self.root, "frontend-modern/public/docs/SECURITY.md", "# Security v1\n")
run_git(self.root, "add", "-A")
run_git(self.root, "commit", "-q", "-m", "seed")
write(self.root, "SECURITY.md", "# Security v2\n")
run_git(self.root, "add", "SECURITY.md")
errors, _warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("SECURITY.md and frontend-modern/public/docs/SECURITY.md", errors[0])
def test_staged_orphan_mirror_fails(self) -> None:
write(self.root, "frontend-modern/public/docs/NEW.md", "# New\n")
run_git(self.root, "add", "frontend-modern/public/docs/NEW.md")
errors, _warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("no repo source docs/NEW.md", errors[0])
def test_preexisting_drift_only_warns_on_unrelated_commit(self) -> None:
write(self.root, "docs/GUIDE.md", "# Guide v2\n")
write(self.root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
run_git(self.root, "add", "-A")
run_git(self.root, "commit", "-q", "-m", "seed drift")
write(self.root, "unrelated.txt", "x\n")
run_git(self.root, "add", "unrelated.txt")
errors, warnings = docs_mirror.check_staged(self.root)
self.assertEqual(errors, [])
self.assertEqual(len(warnings), 1)
self.assertIn("docs/GUIDE.md and frontend-modern/public/docs/GUIDE.md", warnings[0])
def test_staged_pair_with_github_tree_link_fails(self) -> None:
content = "See https://github.com/rcourtman/Pulse/blob/main/docs/OTHER.md\n"
write(self.root, "docs/GUIDE.md", content)
write(self.root, "frontend-modern/public/docs/GUIDE.md", content)
run_git(self.root, "add", "-A")
errors, _warnings = docs_mirror.check_staged(self.root)
self.assertEqual(len(errors), 1)
self.assertIn("must not link to", errors[0])
class DocsMirrorWorktreeTest(unittest.TestCase):
def test_worktree_drift_and_sync(self) -> None:
with tempfile.TemporaryDirectory(ignore_cleanup_errors=True) as temporary:
root = Path(temporary)
write(root, "docs/GUIDE.md", "# Guide v2\n")
write(root, "frontend-modern/public/docs/GUIDE.md", "# Guide v1\n")
write(root, "SECURITY.md", "# Security\n")
write(root, "frontend-modern/public/docs/SECURITY.md", "# Security\n")
errors, checked = docs_mirror.check_worktree(root)
self.assertEqual(checked, 2)
self.assertEqual(len(errors), 1)
self.assertIn("cp docs/GUIDE.md frontend-modern/public/docs/GUIDE.md", errors[0])
def test_repo_shipped_docs_are_synced(self) -> None:
errors, checked = docs_mirror.check_worktree(ROOT)
self.assertEqual(errors, [])
self.assertGreater(checked, 0)
if __name__ == "__main__":
unittest.main()