Add lightweight unittest coverage for stability fixes
This commit is contained in:
20
tests/test_config_check.py
Normal file
20
tests/test_config_check.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
from services.config_check import validate_cfg
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigCheckTests(unittest.TestCase):
|
||||||
|
def test_admin_ids_without_admin_id_is_valid(self):
|
||||||
|
cfg = {
|
||||||
|
"telegram": {
|
||||||
|
"token": "x",
|
||||||
|
"admin_ids": [1, 2],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
errors, warnings = validate_cfg(cfg)
|
||||||
|
self.assertEqual(errors, [])
|
||||||
|
self.assertIsInstance(warnings, list)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
21
tests/test_disk_report.py
Normal file
21
tests/test_disk_report.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import unittest
|
||||||
|
import types
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Avoid runtime import of real app/aiogram in services.runner.
|
||||||
|
sys.modules.setdefault("app", types.SimpleNamespace(RESTIC_ENV={}))
|
||||||
|
|
||||||
|
from services.disk_report import _top_dirs_cmd
|
||||||
|
|
||||||
|
|
||||||
|
class DiskReportTests(unittest.TestCase):
|
||||||
|
def test_top_dirs_cmd_uses_exec_args_without_shell(self):
|
||||||
|
cmd = _top_dirs_cmd("/tmp/path with spaces", 5)
|
||||||
|
self.assertEqual(cmd[:4], ["du", "-x", "-h", "-d"])
|
||||||
|
self.assertNotIn("bash", cmd)
|
||||||
|
self.assertNotIn("-lc", cmd)
|
||||||
|
self.assertEqual(cmd[-1], "/tmp/path with spaces")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
59
tests/test_queue.py
Normal file
59
tests/test_queue.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import asyncio
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from services import runtime_state
|
||||||
|
from services import queue as queue_service
|
||||||
|
|
||||||
|
|
||||||
|
class QueueTests(unittest.IsolatedAsyncioTestCase):
|
||||||
|
async def asyncSetUp(self):
|
||||||
|
self.tmp = tempfile.TemporaryDirectory()
|
||||||
|
runtime_state.configure(f"{self.tmp.name}/runtime.json")
|
||||||
|
|
||||||
|
queue_service._pending.clear() # type: ignore[attr-defined]
|
||||||
|
queue_service._history.clear() # type: ignore[attr-defined]
|
||||||
|
queue_service._stats = { # type: ignore[attr-defined]
|
||||||
|
"processed": 0,
|
||||||
|
"avg_wait_sec": 0.0,
|
||||||
|
"avg_runtime_sec": 0.0,
|
||||||
|
"last_label": "",
|
||||||
|
"last_finished_at": 0.0,
|
||||||
|
}
|
||||||
|
queue_service._cfg = {"incidents": {"enabled": True}} # type: ignore[attr-defined]
|
||||||
|
|
||||||
|
async def asyncTearDown(self):
|
||||||
|
self.tmp.cleanup()
|
||||||
|
|
||||||
|
async def test_worker_logs_failed_job_to_incidents(self):
|
||||||
|
logged = []
|
||||||
|
|
||||||
|
def fake_log_incident(cfg, text, category=None):
|
||||||
|
logged.append((text, category))
|
||||||
|
|
||||||
|
orig = queue_service.log_incident
|
||||||
|
queue_service.log_incident = fake_log_incident
|
||||||
|
|
||||||
|
async def boom():
|
||||||
|
raise RuntimeError("boom")
|
||||||
|
|
||||||
|
worker_task = asyncio.create_task(queue_service.worker())
|
||||||
|
try:
|
||||||
|
await queue_service.enqueue("broken-job", boom)
|
||||||
|
await asyncio.wait_for(queue_service._queue.join(), timeout=2.0) # type: ignore[attr-defined]
|
||||||
|
finally:
|
||||||
|
worker_task.cancel()
|
||||||
|
with contextlib.suppress(asyncio.CancelledError):
|
||||||
|
await worker_task
|
||||||
|
queue_service.log_incident = orig
|
||||||
|
|
||||||
|
self.assertEqual(queue_service._stats.get("processed"), 1) # type: ignore[attr-defined]
|
||||||
|
self.assertTrue(any("queue_job_failed label=broken-job" in t for t, _c in logged))
|
||||||
|
self.assertTrue(any(c == "queue" for _t, c in logged))
|
||||||
|
|
||||||
|
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
28
tests/test_runtime_state.py
Normal file
28
tests/test_runtime_state.py
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from services import runtime_state
|
||||||
|
|
||||||
|
|
||||||
|
class RuntimeStateTests(unittest.TestCase):
|
||||||
|
def test_set_and_get_persist_between_loads(self):
|
||||||
|
with tempfile.TemporaryDirectory() as tmp:
|
||||||
|
path = Path(tmp) / "runtime.json"
|
||||||
|
runtime_state.configure(str(path))
|
||||||
|
|
||||||
|
runtime_state.set_state("foo", {"bar": 1})
|
||||||
|
self.assertEqual(runtime_state.get("foo"), {"bar": 1})
|
||||||
|
|
||||||
|
# Force a fresh in-memory state and load from disk again.
|
||||||
|
runtime_state._STATE = {} # type: ignore[attr-defined]
|
||||||
|
runtime_state._LOADED = False # type: ignore[attr-defined]
|
||||||
|
self.assertEqual(runtime_state.get("foo"), {"bar": 1})
|
||||||
|
|
||||||
|
raw = json.loads(path.read_text(encoding="utf-8"))
|
||||||
|
self.assertEqual(raw.get("foo"), {"bar": 1})
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user