99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
"""The backup-check job: the newest dump and the markers under BACKUP_DIR."""
|
|||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import time
|
||
|
|
from datetime import UTC, datetime
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from pshelpers import make_settings, needs_pg
|
||
|
|
|
||
|
|
from packetstation.settings import Settings
|
||
|
|
from packetstation.worker import jobs, runs
|
||
|
|
from packetstation.worker.jobs import backup_check
|
||
|
|
from packetstation.worker.scheduler import Scheduler
|
||
|
|
|
||
|
|
HOUR = 3600
|
||
|
|
|
||
|
|
|
||
|
|
def put(folder: Path, name: str, data: bytes = b"PGDMP dump", hours_ago: float = 1.0) -> Path:
|
||
|
|
p = folder / name
|
||
|
|
p.write_bytes(data)
|
||
|
|
t = time.time() - hours_ago * HOUR
|
||
|
|
os.utime(p, (t, t))
|
||
|
|
return p
|
||
|
|
|
||
|
|
|
||
|
|
def test_fresh_dump_is_fine(tmp_path: Path) -> None:
|
||
|
|
put(tmp_path, "packetstation-test-20261020.dump", hours_ago=30)
|
||
|
|
put(tmp_path, "packetstation-test-20261021.dump", data=b"x" * 2048, hours_ago=2)
|
||
|
|
put(tmp_path, "last-success", b"", hours_ago=2)
|
||
|
|
put(tmp_path, "last-failure", b"", hours_ago=50)
|
||
|
|
put(tmp_path, "notes.txt", hours_ago=0)
|
||
|
|
counts, problems = backup_check.inspect(tmp_path)
|
||
|
|
assert problems == []
|
||
|
|
assert counts["dumps"] == 2 and counts["newest"] == "packetstation-test-20261021.dump"
|
||
|
|
assert counts["size"] == 2048 and 1.9 <= counts["age_hours"] <= 2.1
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.parametrize(
|
||
|
|
"setup,expect",
|
||
|
|
[
|
||
|
|
(lambda d: None, "No backup dump"),
|
||
|
|
(lambda d: put(d, "a.dump", hours_ago=27), "27 hours old"),
|
||
|
|
(lambda d: put(d, "a.dump", b"", hours_ago=1), "is empty"),
|
||
|
|
(
|
||
|
|
lambda d: (put(d, "a.dump"), put(d, "last-success", b"", 5), put(d, "last-failure", b"", 0.5)),
|
||
|
|
"last backup run failed",
|
||
|
|
),
|
||
|
|
(lambda d: (put(d, "a.dump"), put(d, "last-failure", b"", 0.5)), "last backup run failed"),
|
||
|
|
],
|
||
|
|
)
|
||
|
|
def test_problems(tmp_path: Path, setup: Any, expect: str) -> None:
|
||
|
|
setup(tmp_path)
|
||
|
|
_, problems = backup_check.inspect(tmp_path)
|
||
|
|
assert any(expect in p for p in problems), problems
|
||
|
|
|
||
|
|
|
||
|
|
def test_missing_folder(tmp_path: Path) -> None:
|
||
|
|
counts, problems = backup_check.inspect(tmp_path / "nope")
|
||
|
|
assert counts["dumps"] == 0 and problems and "not there" in problems[0]
|
||
|
|
|
||
|
|
|
||
|
|
def test_age_uses_the_given_clock(tmp_path: Path) -> None:
|
||
|
|
p = put(tmp_path, "a.dump", hours_ago=0)
|
||
|
|
at = datetime.fromtimestamp(p.stat().st_mtime, UTC)
|
||
|
|
_, problems = backup_check.inspect(tmp_path, now=at.replace(year=at.year + 1))
|
||
|
|
assert problems and "hours old" in problems[0]
|
||
|
|
|
||
|
|
|
||
|
|
@needs_pg
|
||
|
|
def test_job_raises_then_resolves(pg_settings: Settings, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
||
|
|
calls: list[tuple[str, str, dict[str, Any]]] = []
|
||
|
|
monkeypatch.setattr(jobs, "raise_event", lambda conn, t, **kw: calls.append(("raise", t, kw)))
|
||
|
|
monkeypatch.setattr(jobs, "resolve_event", lambda conn, t, **kw: calls.append(("resolve", t, kw)))
|
||
|
|
folder = tmp_path / "backups"
|
||
|
|
folder.mkdir()
|
||
|
|
s = pg_settings.model_copy(update={"backup_dir": folder})
|
||
|
|
sch = Scheduler(s, build="pytest", jobs={"backup-check": backup_check.SPEC})
|
||
|
|
|
||
|
|
row = runs.run_job(sch, "backup-check", "schedule")
|
||
|
|
assert row["status"] == "ok" and row["counts"]["dumps"] == 0 and row["counts"]["problems"]
|
||
|
|
((kind, etype, kw),) = calls
|
||
|
|
assert (kind, etype, kw["subject"], kw["condition"]) == ("raise", "backup_stale", "ci", True)
|
||
|
|
assert kw["title"] == "Backup failed or stale (ci)" and "No backup dump" in kw["body"]
|
||
|
|
|
||
|
|
calls.clear()
|
||
|
|
put(folder, "ps.dump")
|
||
|
|
put(folder, "last-success", b"")
|
||
|
|
row = runs.run_job(sch, "backup-check", "schedule")
|
||
|
|
assert row["counts"]["problems"] == [] and row["counts"]["newest"] == "ps.dump"
|
||
|
|
assert calls == [("resolve", "backup_stale", {"subject": "ci", "title": "Backup failed or stale (ci)"})]
|
||
|
|
|
||
|
|
|
||
|
|
def test_settings_backup_dir_default(tmp_path: Path) -> None:
|
||
|
|
assert make_settings(tmp_path).backup_dir == tmp_path / "no-backups"
|