tests/scripts/test_backup_databases.py
Rama main · réplica de lectura del repositorio original.
"""Tests for scripts/backup_databases.py."""
from __future__ import annotations
import argparse
import sqlite3
from pathlib import Path
from scripts import backup_databases
def _make_real_sqlite_db(path: Path, marker_value: str) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
conn = sqlite3.connect(str(path))
try:
conn.execute("CREATE TABLE marker (value TEXT)")
conn.execute("INSERT INTO marker VALUES (?)", (marker_value,))
conn.commit()
finally:
conn.close()
def test_backup_one_writes_into_a_stamp_named_folder(tmp_path: Path):
src = tmp_path / "test.sqlite"
_make_real_sqlite_db(src, "hello-backup")
dest_dir = tmp_path / "backups"
dest_dir.mkdir()
result = backup_databases.backup_one(src, "test", dest_dir, "20260504-120000")
assert result is not None and result.exists()
assert result == dest_dir / "20260504-120000" / "test.sqlite"
conn = sqlite3.connect(str(result))
try:
assert conn.execute("SELECT value FROM marker").fetchone()[0] == "hello-backup"
finally:
conn.close()
def test_discover_sources_includes_auth_and_per_user_dbs(tmp_path: Path):
_make_real_sqlite_db(tmp_path / "lectio_auth.sqlite", "auth")
for fn in ("lectio_reader.sqlite", "lectio_meta.sqlite3", "lectio_starred_archive.sqlite"):
_make_real_sqlite_db(tmp_path / "users" / "u_a" / fn, "a")
_make_real_sqlite_db(tmp_path / "users" / "u_b" / "lectio_meta.sqlite3", "b")
stems = sorted(stem for _p, stem in backup_databases.discover_sources(tmp_path))
assert "lectio_auth" in stems
assert "users-u_a-lectio_reader" in stems
assert "users-u_a-lectio_meta" in stems
assert "users-u_a-lectio_starred_archive" in stems
assert "users-u_b-lectio_meta" in stems
def test_prune_old_keeps_n_most_recent(tmp_path: Path):
dest_dir = tmp_path / "backups"
dest_dir.mkdir()
for ts in ["20260101-000000", "20260102-000000", "20260103-000000", "20260104-000000", "20260105-000000"]:
gen_dir = dest_dir / ts
gen_dir.mkdir()
(gen_dir / "lectio_meta.sqlite3").write_bytes(b"\x00" * 16)
backup_databases.prune_old(dest_dir, keep=2)
assert sorted(p.name for p in dest_dir.iterdir()) == ["20260104-000000", "20260105-000000"]
# ── size-aware retention ──────────────────────────────────────────────────────
# A count alone is unsafe here: one generation of the live data is ~9GB, so the
# old `--keep 7` meant ~59GB of backups on a 72GB disk, and pruning ran *after*
# writing so the peak was eight generations. Two backups in one session took the
# disk to 98%.
import pytest # noqa: E402
@pytest.mark.parametrize(
"text,expected",
[
("1024", 1024),
("1K", 1024),
("1KB", 1024),
("2M", 2 * 1024**2),
("3G", 3 * 1024**3),
("1T", 1024**4),
("1.5G", int(1.5 * 1024**3)),
(" 20g ", 20 * 1024**3),
("0", 0),
],
)
def test_parse_size(text, expected):
assert backup_databases.parse_size(text) == expected
@pytest.mark.parametrize("bad", ["", "abc", "-1G", "G"])
def test_parse_size_rejects_garbage(bad):
with pytest.raises(argparse.ArgumentTypeError):
backup_databases.parse_size(bad)
def _gen(dest_dir: Path, stamp: str, stems: list[str], size: int) -> None:
gen_dir = dest_dir / stamp
gen_dir.mkdir(exist_ok=True)
for stem in stems:
(gen_dir / f"{stem}.sqlite").write_bytes(b"\x00" * size)
def test_generations_group_by_directory(tmp_path: Path):
"""A generation is only useful whole — a reader DB without its meta DB from
the same instant is not a restore point."""
d = tmp_path / "b"
d.mkdir()
stems = ["users-u_a-lectio_reader", "users-u_a-lectio_meta"]
_gen(d, "20260101-000000", stems, 8)
_gen(d, "20260102-000000", stems, 8)
gens = backup_databases.generations(d)
assert [stamp for stamp, _ in gens] == ["20260102-000000", "20260101-000000"]
assert all(len(paths) == 2 for _s, paths in gens)
def test_prune_by_budget_drops_oldest_generations(tmp_path: Path):
d = tmp_path / "b"
d.mkdir()
stems = ["lectio_meta"]
for stamp in ("20260101-000000", "20260102-000000", "20260103-000000"):
_gen(d, stamp, stems, 100)
# Budget fits two generations of 100 bytes.
backup_databases.prune_old(d, keep=0, max_bytes=250)
assert sorted(p.name for p in d.iterdir()) == ["20260102-000000", "20260103-000000"]
def test_budget_never_deletes_the_newest_generation(tmp_path: Path):
"""A backup directory that prunes its way to empty is worse than one that is
over budget."""
d = tmp_path / "b"
d.mkdir()
stems = ["lectio_meta"]
_gen(d, "20260101-000000", stems, 500)
_gen(d, "20260102-000000", stems, 500)
backup_databases.prune_old(d, keep=0, max_bytes=1)
assert [p.name for p in d.iterdir()] == ["20260102-000000"]
def test_count_and_budget_compose(tmp_path: Path):
d = tmp_path / "b"
d.mkdir()
stems = ["lectio_meta"]
for stamp in ("20260101-000000", "20260102-000000", "20260103-000000", "20260104-000000"):
_gen(d, stamp, stems, 100)
# keep=3 drops the oldest; the 250-byte budget then drops one more.
backup_databases.prune_old(d, keep=3, max_bytes=250)
assert sorted(p.name for p in d.iterdir()) == ["20260103-000000", "20260104-000000"]
def test_prune_is_a_noop_when_within_both_limits(tmp_path: Path):
d = tmp_path / "b"
d.mkdir()
stems = ["lectio_meta"]
_gen(d, "20260101-000000", stems, 10)
backup_databases.prune_old(d, keep=3, max_bytes=1024)
assert len(list(d.iterdir())) == 1