8Validate the OI-424 formatter backup archive rotation in
9``oct.formatter.oct_format``.
13- Confirm ``_archive_file`` trims old backups to the configured cap.
14- Confirm pruning is scoped to the source file (other files untouched).
15- Confirm ``max_backups == 0`` disables pruning.
16- Confirm a per-file unlink failure is swallowed.
17- Confirm ``FormatterContext.max_backups_per_file`` is threaded through
18 to the archiver in the write path.
22Domain: TESTS.FORMATTER
24L3: fixture creation, assertion details
25L4: deep tracing of archive operations
29Inputs: tmp_path pytest fixture
30Outputs: pass/fail assertions
35from pathlib
import Path
40 _DEFAULT_MAX_ARCHIVE_BYTES,
41 _DEFAULT_MAX_BACKUPS_PER_FILE,
44 _prune_archive_by_size,
49 archive_dir: Path, source_name: str, count: int, start_age: float = 100.0,
51 """Create ``count`` fake backups with staggered mtimes (oldest first).
53 Returns the list of created paths in the order they were created.
55 archive_dir.mkdir(parents=
True, exist_ok=
True)
56 created: list[Path] = []
58 for i
in range(count):
59 stamp = 20260101_000000 + i
60 path = archive_dir / f
"{source_name}.{stamp}.bak"
61 path.write_text(f
"backup {i}\n", encoding=
"utf-8")
63 mtime = now - (start_age - i)
64 os.utime(path, (mtime, mtime))
70 """Archiving into a dir with 15 backups prunes to ``max_backups``."""
71 source = tmp_path /
"foo.py"
72 source.write_text(
"print('x')\n", encoding=
"utf-8")
73 archive_dir = tmp_path /
".formatter_archive"
76 _archive_file(source, max_backups=10)
78 remaining = sorted(archive_dir.glob(
"foo.py.*.bak"))
80 assert len(remaining) == 10
82 for stale
in seeded[:6]:
83 assert not stale.exists()
85 for fresh
in seeded[6:]:
90 """A fresh archive dir retains exactly one backup after archival."""
91 source = tmp_path /
"bar.py"
92 source.write_text(
"print('y')\n", encoding=
"utf-8")
94 _archive_file(source, max_backups=10)
96 archive_dir = tmp_path /
".formatter_archive"
97 assert len(list(archive_dir.glob(
"bar.py.*.bak"))) == 1
101 """Pruning ``foo.py`` must not delete ``bar.py`` backups."""
102 archive_dir = tmp_path /
".formatter_archive"
106 removed = _prune_archive(archive_dir,
"foo.py", max_backups=2)
110 for bar
in bar_seeds:
113 assert len(list(archive_dir.glob(
"foo.py.*.bak"))) == 2
117 """``max_backups=0`` preserves every existing backup."""
118 archive_dir = tmp_path /
".formatter_archive"
121 removed = _prune_archive(archive_dir,
"foo.py", max_backups=0)
124 assert len(list(archive_dir.glob(
"foo.py.*.bak"))) == 20
128 tmp_path: Path, monkeypatch,
130 """A single ``OSError`` during unlink must not stop pruning the rest."""
131 archive_dir = tmp_path /
".formatter_archive"
135 real_unlink = Path.unlink
136 fail_name = sorted(archive_dir.glob(
"foo.py.*.bak"))[0].name
138 def flaky_unlink(self, *args, **kwargs):
139 if self.name == fail_name:
140 raise OSError(
"simulated")
141 return real_unlink(self, *args, **kwargs)
143 monkeypatch.setattr(Path,
"unlink", flaky_unlink)
145 removed = _prune_archive(archive_dir,
"foo.py", max_backups=2)
152 """Sanity check on the documented default."""
153 assert _DEFAULT_MAX_BACKUPS_PER_FILE == 10
162 archive_dir: Path, name: str, size: int, mtime_offset: float,
164 """Create a ``.bak`` of given size with an offset mtime (seconds)."""
165 archive_dir.mkdir(parents=
True, exist_ok=
True)
166 path = archive_dir / name
167 path.write_bytes(b
"x" * size)
169 os.utime(path, (now + mtime_offset, now + mtime_offset))
174 """OI-516: aggregate size cap evicts oldest-first."""
175 archive = tmp_path /
".formatter_archive"
180 removed = _prune_archive_by_size(archive, max_total_bytes=500)
183 remaining = sorted(p.name
for p
in archive.glob(
"*.bak"))
184 assert remaining == [
"a.py.002.bak",
"a.py.003.bak"]
188 """OI-516: already under cap → nothing removed."""
189 archive = tmp_path /
".formatter_archive"
191 assert _prune_archive_by_size(archive, max_total_bytes=10_000) == 0
192 assert len(list(archive.glob(
"*.bak"))) == 1
196 """OI-516: ``max_total_bytes <= 0`` disables size pruning entirely."""
197 archive = tmp_path /
".formatter_archive"
199 assert _prune_archive_by_size(archive, 0) == 0
200 assert _prune_archive_by_size(archive, -1) == 0
201 assert len(list(archive.glob(
"*.bak"))) == 1
205 """OI-516: ``_archive_file`` triggers size pruning after writing the backup."""
206 source = tmp_path /
"big.py"
207 source.write_bytes(b
"y" * 400)
209 archive = tmp_path /
".formatter_archive"
216 _archive_file(source, max_backups=10, max_archive_bytes=800)
218 total = sum(p.stat().st_size
for p
in archive.glob(
"*.bak"))
223 """OI-516: default aggregate cap is 5 MiB."""
224 assert _DEFAULT_MAX_ARCHIVE_BYTES == 5 * 1024 * 1024