mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-08 19:56:53 +00:00
A gate run died two seconds in with PermissionError on compose/package-cache/pip.conf. The worktree is bind-mounted into the container, so a container that rendered these files owns them afterwards while the directory stays the host's. Overwriting needs the file's permission and unlinking the directory's, which is why the host could no longer write a file in a folder it owns. _write now catches the refusal and replaces the inode instead. Only after a refusal, never by default: compose/swarm/cache.override.yml bind-mounts an apt list into a container by inode, and swapping one that was writable anyway would leave that mount reading a file nobody updates any more. The new test pins that half as well, so the guard cannot be simplified away. Shown failing first: the previous _write raises PermissionError on the same input the test feeds the new one. Shown working in place: lint-ansible came back in 3m40s through a run during which make dotenv-force re-rendered these very files, where the same sequence had aborted the target after two seconds before. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
68 lines
2.1 KiB
Python
68 lines
2.1 KiB
Python
"""Unit tests for writing the generated package-cache files.
|
|
|
|
The shape measured in a gate run: a container had rendered
|
|
compose/package-cache/pip.conf under its own uid, and the next host-side
|
|
render died on it with PermissionError while the directory stayed writable.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import stat
|
|
import tempfile
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
from utils.cache.render import _write
|
|
|
|
|
|
class TestWrite(unittest.TestCase):
|
|
"""A file the renderer cannot overwrite is replaced, not given up on."""
|
|
|
|
def test_an_unwritable_file_is_replaced(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
target = Path(directory, "pip.conf")
|
|
target.write_text("stale", encoding="utf-8")
|
|
target.chmod(stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH)
|
|
|
|
_write(target, "fresh")
|
|
|
|
self.assertEqual(
|
|
target.read_text(
|
|
encoding="utf-8"
|
|
), # nocheck: cache-read tempdir fixture rewritten in this test
|
|
"fresh",
|
|
)
|
|
|
|
def test_a_writable_file_keeps_its_inode(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
target = Path(directory, "arch.list")
|
|
target.write_text("stale", encoding="utf-8")
|
|
before = target.stat().st_ino
|
|
|
|
_write(target, "fresh")
|
|
|
|
self.assertEqual(
|
|
target.read_text(
|
|
encoding="utf-8"
|
|
), # nocheck: cache-read tempdir fixture rewritten in this test
|
|
"fresh",
|
|
)
|
|
self.assertEqual(target.stat().st_ino, before)
|
|
|
|
def test_a_missing_parent_is_created(self) -> None:
|
|
with tempfile.TemporaryDirectory() as directory:
|
|
target = Path(directory, "apt", "debian.list")
|
|
|
|
_write(target, "fresh")
|
|
|
|
self.assertEqual(
|
|
target.read_text(
|
|
encoding="utf-8"
|
|
), # nocheck: cache-read tempdir fixture rewritten in this test
|
|
"fresh",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|