"""Rehearse an isolated v1 upgrade and rollback with the original alpha executable.""" import argparse import gzip import hashlib import json import os from pathlib import Path import subprocess import sys import tarfile import tempfile import zipfile from service_fixture import cli, serving ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "website")) from release_assets import SHA256, ORIGINALS FIXTURE = ROOT / "tests/fixtures/alpha-d5b5e56.ratdb.gz" LEGACY = {"id": "same", "n": 17, "flag": True, "values": [None, "café"]} NAMED = {"id": "same", "n": -2.5, "nested": {"ok": False}} def archived_binary(directory): windows = os.name == "nt" archive = next(p for p in ORIGINALS.iterdir() if p.name.endswith("windows-x86_64.zip" if windows else "linux-x86_64.tar.gz")) assert hashlib.sha256(archive.read_bytes()).hexdigest() == SHA256[archive.name] binary = directory / ("ratdb.exe" if windows else "ratdb") if windows: with zipfile.ZipFile(archive) as bundle: binary.write_bytes(bundle.read("ratdb.exe")) else: with tarfile.open(archive) as bundle: binary.write_bytes(bundle.extractfile("ratdb").read()) binary.chmod(0o700) return binary def generate(old, source): cli(old, "init", source) cli(old, "put", source, "records", "same", json.dumps(LEGACY, ensure_ascii=True)) with serving(old, source) as connect, connect() as db: db.select_warren("named") db._request("create", kind="den", name="empty") db.select_den("records") db.put("same", NAMED) FIXTURE.parent.mkdir(parents=True, exist_ok=True) FIXTURE.write_bytes(gzip.compress(source.read_bytes(), mtime=0)) def check(exe, source, changed): cli(exe, "check", source) with serving(exe, source) as connect, connect() as db: assert set(db._request("warrens")["names"]) == {"default", "named"} db.select_warren("named") assert set(db._request("dens")["names"]) == {"empty", "records"} db.select_den("records") assert db.get("same") == NAMED db.select_warren("default") db.select_den("records") assert db.get("same") == LEGACY assert db.get("candidate") == ({"id": "candidate", "test": True} if changed else None) return db._request("stats")["generation"] def stage(current, original, backup, candidate): cli(current, "backup", original, backup) print("backup", flush=True) input() cli(current, "restore", backup, candidate) print("restore", flush=True) input() cli(current, "put", candidate, "records", "candidate", '{"test":true}') print("write", flush=True) input() def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("binary", type=Path) parser.add_argument("--generate-fixture", action="store_true") args = parser.parse_args() current = args.binary.resolve() with tempfile.TemporaryDirectory() as tmp: directory = Path(tmp) old = archived_binary(directory) if args.generate_fixture: generate(old, directory / "alpha.ratdb") return payload = gzip.decompress(FIXTURE.read_bytes()) assert hashlib.sha256(payload).hexdigest() == "d6fc2263fa2f34f8355ee893369ce6d1499c0c1b533934aafc9d4452fff16b57" original = directory / "live.ratdb" original.write_bytes(payload) generation = check(old, original, False) for boundary in ("backup", "restore", "write"): backup = directory / (boundary + ".backup") candidate = directory / (boundary + ".candidate") worker = subprocess.Popen([sys.executable, "-B", __file__, "--stage", current, original, backup, candidate], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True) try: for expected in ("backup", "restore", "write"): assert worker.stdout.readline().strip() == expected if expected == boundary: break worker.stdin.write("\n") worker.stdin.flush() finally: worker.kill() worker.communicate(timeout=20) assert original.read_bytes() == payload == backup.read_bytes() if boundary == "backup": assert not candidate.exists() else: assert check(current, candidate, boundary == "write") == generation + (boundary == "write") rollback = directory / (boundary + ".rollback") cli(old, "restore", backup, rollback) assert check(old, rollback, False) == generation print(f"Alpha v1 generation {generation}: 3 interrupted upgrade boundaries and rollbacks passed") if __name__ == "__main__": if sys.argv[1:2] == ["--stage"]: stage(*sys.argv[2:]) else: main()