mirror of
https://github.com/kevinveenbirkenbach/computer-playbook.git
synced 2026-11-06 10:59:59 +00:00
fix(sys-ctl-cln-bkps): prevent infinite cleanup loop and add regression tests
Stop cleanup script from busy-looping when disk usage is above threshold but no backup versions exist by breaking early when average_version_directories == 0. Add unit tests to verify: - script exits immediately when disk usage is below threshold - script breaks safely when disk usage is high but no backups are present This prevents CI hangs and excessive CPU usage caused by infinite iterations. https://chatgpt.com/share/6962de45-3750-800f-bc8d-5ba205beeaaf
This commit is contained in:
4 files changed
+121
No files matched your search
@@ -204,6 +204,11 @@ while isSmallerThenMaximumBackupSize(maximum_backup_size_percent, backup_dir):
|
||||
average_version_directories = average_version_directories_per_application(
|
||||
backup_dir
|
||||
)
|
||||
if average_version_directories <= 0:
|
||||
print(
|
||||
"No backup versions found to delete (average_version_directories=0). Exiting."
|
||||
)
|
||||
break
|
||||
print(
|
||||
f"Average version directories per application directory: {average_version_directories}"
|
||||
)
|
||||
|
||||
Whitespace-only changes.
Whitespace-only changes.
@@ -0,0 +1,116 @@
|
||||
# tests/unit/roles/sys-ctl-cln-bkps/files/test_script.py
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import runpy
|
||||
import sys
|
||||
import unittest
|
||||
from contextlib import redirect_stdout
|
||||
from pathlib import Path
|
||||
from types import ModuleType, SimpleNamespace
|
||||
from unittest.mock import patch
|
||||
|
||||
# CI checkout path (as shown in your logs)
|
||||
SCRIPT_PATH = Path("/opt/src/infinito/roles/sys-ctl-cln-bkps/files/script.py")
|
||||
|
||||
|
||||
def _fake_psutil(percent: int) -> ModuleType:
|
||||
"""Create a fake psutil module with disk_usage()."""
|
||||
fake = ModuleType("psutil")
|
||||
fake.disk_usage = lambda _: SimpleNamespace(percent=percent)
|
||||
return fake
|
||||
|
||||
|
||||
class TestSysCtlClnBkpsScript(unittest.TestCase):
|
||||
def _run_script(
|
||||
self,
|
||||
argv: list[str],
|
||||
disk_percent: int,
|
||||
listdir_map: dict[str, list[str]],
|
||||
) -> tuple[str, object]:
|
||||
"""
|
||||
Execute the script as __main__ with provided argv, returning stdout and rmtree mock.
|
||||
Fully mocks psutil and os.listdir so the test does not depend on system packages or FS.
|
||||
"""
|
||||
if not SCRIPT_PATH.is_file():
|
||||
raise FileNotFoundError(
|
||||
f"script.py not found at expected path: {SCRIPT_PATH}"
|
||||
)
|
||||
|
||||
fake_psutil = _fake_psutil(disk_percent)
|
||||
|
||||
def listdir_side_effect(path: str) -> list[str]:
|
||||
return listdir_map.get(path, [])
|
||||
|
||||
buf = io.StringIO()
|
||||
with (
|
||||
patch.dict(sys.modules, {"psutil": fake_psutil}),
|
||||
patch.object(sys, "argv", argv),
|
||||
patch("os.listdir", side_effect=listdir_side_effect),
|
||||
patch("shutil.rmtree") as rmtree_mock,
|
||||
patch("time.time", return_value=0),
|
||||
redirect_stdout(buf),
|
||||
):
|
||||
runpy.run_path(str(SCRIPT_PATH), run_name="__main__")
|
||||
|
||||
return buf.getvalue(), rmtree_mock
|
||||
|
||||
def test_exits_immediately_when_disk_usage_below_threshold(self) -> None:
|
||||
"""
|
||||
If disk usage is already <= threshold, the while-loop must not run.
|
||||
Script should print final usage and finish message.
|
||||
"""
|
||||
backup_dir = "/var/lib/infinito/backup"
|
||||
argv = [
|
||||
"script.py",
|
||||
"--maximum-backup-size-percent",
|
||||
"75",
|
||||
"--backups-folder-path",
|
||||
backup_dir,
|
||||
]
|
||||
|
||||
out, rmtree_mock = self._run_script(
|
||||
argv=argv,
|
||||
disk_percent=10, # below threshold
|
||||
listdir_map={backup_dir: []},
|
||||
)
|
||||
|
||||
self.assertIn("Cleaning up finished.", out)
|
||||
self.assertIn(f"% of disk {backup_dir} are used", out)
|
||||
rmtree_mock.assert_not_called()
|
||||
|
||||
def test_breaks_when_no_versions_found_even_if_disk_usage_high(self) -> None:
|
||||
"""
|
||||
Regression test for the busy-loop:
|
||||
If disk usage is above threshold but no backup structure exists,
|
||||
average_version_directories_per_application() returns 0 and the script
|
||||
must break out.
|
||||
"""
|
||||
backup_dir = "/var/lib/infinito/backup"
|
||||
argv = [
|
||||
"script.py",
|
||||
"--maximum-backup-size-percent",
|
||||
"75",
|
||||
"--backups-folder-path",
|
||||
backup_dir,
|
||||
]
|
||||
|
||||
out, rmtree_mock = self._run_script(
|
||||
argv=argv,
|
||||
disk_percent=90, # above threshold
|
||||
listdir_map={
|
||||
backup_dir: [], # no host dirs -> total_app_directories == 0 -> average == 0
|
||||
},
|
||||
)
|
||||
|
||||
self.assertIn("Delete Iteration: 1", out)
|
||||
self.assertIn(
|
||||
"No backup versions found to delete (average_version_directories=0). Exiting.",
|
||||
out,
|
||||
)
|
||||
self.assertIn("Cleaning up finished.", out)
|
||||
rmtree_mock.assert_not_called()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user