trainlog/tests/test_generation_archival.py

124 lines
5.3 KiB
Python

#!/usr/bin/env python3
"""Failure-boundary tests for the production generation archive ledger."""
import sqlite3
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT / "tools"))
import sync_generation_exchange as generation # noqa: E402
FIXTURE = ROOT / "build" / "tui" / "sync-generation-fixture"
PEER = "peer_11111111-1111-4111-8111-111111111111"
class GenerationArchiveTest(unittest.TestCase):
def setUp(self):
self.temporary = tempfile.TemporaryDirectory(prefix="trainlog-archive-test-")
self.root = Path(self.temporary.name)
self.database = self.root / "desktop.sqlite"
subprocess.run([str(FIXTURE), str(self.database)], check=True)
self.owned = self.root / "owned"
self.objects = self.root / "objects"
def tearDown(self):
self.temporary.cleanup()
def acknowledge(self, generation_id: str, manifest: dict, digest: str) -> Path:
generation.publish(self.database, generation_id, self.objects)
with sqlite3.connect(self.database) as database:
producer = database.execute(
"SELECT producer_peer_id FROM sync_generations WHERE generation_id=?",
(generation_id,),
).fetchone()[0]
ack = generation.ack_document(
manifest["run_id"], generation_id, producer, PEER, digest,
"consumed", generation.now(), "",
)
path = self.root / f"{generation_id}.ack.json"
path.write_bytes(generation.canonical(ack))
self.assertEqual("acknowledged", generation.accept_ack(self.database, path))
return path
def create_acknowledged(self):
values = []
for _ in range(3):
stage, manifest, digest = generation.capture_desktop(
self.database, self.owned, PEER
)
del stage
ack = self.acknowledge(manifest["generation_id"], manifest, digest)
values.append((manifest, ack))
return values
def test_interrupted_archive_resumes_and_replay_stays_idempotent(self):
values = self.create_acknowledged()
oldest = values[0][0]["generation_id"]
temporary = self.owned / "archives" / "generations" / f".{oldest}.tmp"
temporary.mkdir(parents=True)
(temporary / "interrupted").write_text("partial")
self.assertEqual(1, generation.archive_acknowledged(self.database, self.owned, PEER))
self.assertFalse(temporary.exists())
self.assertEqual("unchanged", generation.accept_ack(self.database, values[0][1]))
with sqlite3.connect(self.database) as database:
self.assertEqual(
1,
database.execute(
"SELECT COUNT(*) FROM sync_generation_archives WHERE generation_id=?",
(oldest,),
).fetchone()[0],
)
def test_invalid_archive_and_no_space_never_commit_ledger(self):
values = self.create_acknowledged()
oldest = values[0][0]["generation_id"]
destination = self.owned / "archives" / "generations" / oldest
destination.mkdir(parents=True)
(destination / "manifest.json").write_text("{}")
with self.assertRaises(generation.GenerationError):
generation.archive_acknowledged(self.database, self.owned, PEER)
with sqlite3.connect(self.database) as database:
self.assertEqual(0, database.execute("SELECT COUNT(*) FROM sync_generation_archives").fetchone()[0])
destination.unlink(missing_ok=True) if destination.is_file() else None
for child in destination.iterdir():
child.unlink()
destination.rmdir()
with mock.patch.object(generation.shutil, "copytree", side_effect=OSError(28, "no space")):
with self.assertRaises(OSError):
generation.archive_acknowledged(self.database, self.owned, PEER)
with sqlite3.connect(self.database) as database:
self.assertEqual(0, database.execute("SELECT COUNT(*) FROM sync_generation_archives").fetchone()[0])
def test_missing_then_late_ack_is_protected_until_correlated(self):
values = self.create_acknowledged()
stage, manifest, digest = generation.capture_desktop(self.database, self.owned, PEER)
del stage
generation.publish(self.database, manifest["generation_id"], self.objects)
generation.archive_acknowledged(self.database, self.owned, PEER)
with sqlite3.connect(self.database) as database:
self.assertEqual(
"waiting_acknowledgement",
database.execute(
"SELECT status FROM sync_generations WHERE generation_id=?",
(manifest["generation_id"],),
).fetchone()[0],
)
self.assertEqual(
0,
database.execute(
"SELECT COUNT(*) FROM sync_generation_archives WHERE generation_id=?",
(manifest["generation_id"],),
).fetchone()[0],
)
self.acknowledge(manifest["generation_id"], manifest, digest)
self.assertGreaterEqual(generation.archive_acknowledged(self.database, self.owned, PEER), 0)
if __name__ == "__main__":
unittest.main()