Option C Tools
Loading...
Searching...
No Matches
test_doxygen_cleanup.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_docs/test_doxygen_cleanup.py
4
5"""
6Purpose
7-------
8Validate the OI-423 stale Doxyfile sweeper in ``oct.docs.oct_docs``.
9
10Responsibilities
11----------------
12- Confirm old ``oct-doxyfile-*.Doxyfile`` temp files are removed.
13- Confirm recent ``oct-doxyfile-*.Doxyfile`` temp files are preserved.
14- Confirm unrelated temp files are never touched.
15- Confirm the sweeper is resilient to per-file OS errors.
16- Confirm the sweeper returns the count of removed files.
17
18Diagnostics
19-----------
20Domain: DOCS-TESTS
21L2: test lifecycle
22L3: assertion details
23L4: deep tracing
24
25Contracts
26---------
27Inputs: tmp_path, monkeypatch pytest fixtures
28Outputs: pass/fail assertions
29"""
30
31import os
32import tempfile
33import time
34from pathlib import Path
35
36import pytest
37
38from oct.docs import oct_docs
39from oct.docs.oct_docs import (
40 _sweep_stale_doxyfiles,
41 _STALE_DOXYFILE_MAX_AGE_SECONDS,
42)
43
44
45@pytest.fixture
46def isolated_tmp(tmp_path: Path, monkeypatch) -> Path:
47 """Redirect ``tempfile.gettempdir()`` to an isolated directory.
48
49 Prevents tests from touching (or being polluted by) the real system
50 temp directory.
51 """
52 monkeypatch.setattr(tempfile, "gettempdir", lambda: str(tmp_path))
53 # oct_docs imported tempfile at module level, so we also need to
54 # patch the module-level reference.
55 monkeypatch.setattr(oct_docs.tempfile, "gettempdir", lambda: str(tmp_path))
56 return tmp_path
57
58
59def _make_stale(path: Path, age_seconds: float) -> None:
60 path.write_text("# fake Doxyfile\n", encoding="utf-8")
61 past = time.time() - age_seconds
62 os.utime(path, (past, past))
63
64
65def test_sweep_removes_old_stale_doxyfile(isolated_tmp: Path) -> None:
66 """A 48h-old oct-doxyfile-*.Doxyfile is removed."""
67 stale = isolated_tmp / "oct-doxyfile-abc123.Doxyfile"
68 _make_stale(stale, 48 * 3600)
69
70 removed = _sweep_stale_doxyfiles()
71
72 assert removed == 1
73 assert not stale.exists()
74
75
76def test_sweep_preserves_recent_doxyfile(isolated_tmp: Path) -> None:
77 """A freshly-created oct-doxyfile-*.Doxyfile is preserved."""
78 recent = isolated_tmp / "oct-doxyfile-xyz.Doxyfile"
79 recent.write_text("# live Doxyfile\n", encoding="utf-8")
80
81 removed = _sweep_stale_doxyfiles()
82
83 assert removed == 0
84 assert recent.exists()
85
86
87def test_sweep_ignores_unrelated_temp_files(isolated_tmp: Path) -> None:
88 """Files without the oct-doxyfile- prefix are never touched."""
89 foreign = isolated_tmp / "foo.Doxyfile"
90 _make_stale(foreign, 48 * 3600)
91
92 removed = _sweep_stale_doxyfiles()
93
94 assert removed == 0
95 assert foreign.exists()
96
97
98def test_sweep_returns_count(isolated_tmp: Path) -> None:
99 """The return value equals the number of files actually removed."""
100 for i in range(3):
102 isolated_tmp / f"oct-doxyfile-{i}.Doxyfile", 48 * 3600,
103 )
104 # Two recent, one unrelated: should not be counted.
105 (isolated_tmp / "oct-doxyfile-live.Doxyfile").write_text("x", encoding="utf-8")
106 _make_stale(isolated_tmp / "unrelated.Doxyfile", 48 * 3600)
107
108 removed = _sweep_stale_doxyfiles()
109
110 assert removed == 3
111
112
114 isolated_tmp: Path, monkeypatch,
115) -> None:
116 """A per-file OSError during unlink must not propagate."""
117 stale = isolated_tmp / "oct-doxyfile-locked.Doxyfile"
118 _make_stale(stale, 48 * 3600)
119
120 real_unlink = Path.unlink
121
122 def fake_unlink(self, *args, **kwargs):
123 if self.name == "oct-doxyfile-locked.Doxyfile":
124 raise OSError("simulated permission error")
125 return real_unlink(self, *args, **kwargs)
126
127 monkeypatch.setattr(Path, "unlink", fake_unlink)
128
129 # Must not raise.
130 removed = _sweep_stale_doxyfiles()
131 assert removed == 0
132
133
134def test_sweep_respects_custom_max_age(isolated_tmp: Path) -> None:
135 """An explicit max_age_seconds overrides the 24h default."""
136 # 1 hour old — under the default 24h, but over a custom 30min cap.
137 path = isolated_tmp / "oct-doxyfile-borderline.Doxyfile"
138 _make_stale(path, 3600)
139
140 assert _sweep_stale_doxyfiles() == 0
141 assert path.exists()
142 assert _sweep_stale_doxyfiles(max_age_seconds=1800) == 1
143 assert not path.exists()
144
145
147 """Sanity: the module-level constant matches the documented value."""
148 assert _STALE_DOXYFILE_MAX_AGE_SECONDS == 24 * 60 * 60
None test_sweep_respects_custom_max_age(Path isolated_tmp)
None test_sweep_handles_unlink_failure_gracefully(Path isolated_tmp, monkeypatch)
Path isolated_tmp(Path tmp_path, monkeypatch)
None test_sweep_ignores_unrelated_temp_files(Path isolated_tmp)
None _make_stale(Path path, float age_seconds)
None test_sweep_returns_count(Path isolated_tmp)
None test_sweep_preserves_recent_doxyfile(Path isolated_tmp)
None test_sweep_removes_old_stale_doxyfile(Path isolated_tmp)