Files
cc-ci/tests/unit/test_warmsnap.py
autonomic-bot 32f00717ac fix(2w): W0.9 WC1.1 hardening (proven live: healthy upgrade + marquee rollback)
Bugs found by the live proof, fixed:
- warmsnap: snapshot now swaps a <recipe>/snapshot/ SUBDIR, not the whole
  <recipe>/ dir — so the reconciler's sibling last_good file survives a
  snapshot swap (was being clobbered).
- warm_reconcile: deploy_version captures abra's stdout (it writes FATA to
  stdout) in the error; add wait_undeployed() after every undeploy so
  snapshot/restore/redeploy don't race a half-removed swarm stack; the upgrade
  deploy is wrapped so a deploy FAILURE (not just unhealthy) also triggers
  rollback. (57 unit pass.)

LIVE PROOF on warm keycloak (annotated fake tags via CCCI_SKIP_FETCH):
(a) healthy upgrade 10.7.1->10.7.9: snapshot+deploy+health-pass, last_good
    committed=10.7.9, marker realm preserved.
(b) MARQUEE rollback: broken latest 10.7.10 (lint-fail) -> rollback to 10.7.9,
    HEALTHY, marker realm INTACT (data preserved through broken-upgrade+restore),
    last_good NOT advanced, rollback alert written (attempted=10.7.10,
    last_good=10.7.9, recovered=True). keycloak recovered to canonical
    10.7.1+26.6.2 healthy.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-29 01:21:05 +01:00

71 lines
3.0 KiB
Python

"""Unit tests for the WC3 snapshot helper's pure path/meta logic (runner/harness/warmsnap.py).
The docker/tar snapshot+restore round-trip is integration (proven live on cc-ci against a real
keycloak volume, W0.5). Here we cover the layout, meta read, and the has_snapshot completeness check.
"""
from __future__ import annotations
import json
import os
import sys
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "runner"))
from harness import warmsnap # noqa: E402
def test_warm_root_env_override(monkeypatch):
monkeypatch.setenv("CCCI_WARM_ROOT", "/tmp/ci-warm-test")
assert warmsnap.warm_root() == "/tmp/ci-warm-test"
assert warmsnap.app_dir("keycloak") == "/tmp/ci-warm-test/keycloak"
# snapshot lives in a subdir so sibling state (last_good) survives the atomic swap.
assert warmsnap.snap_dir("keycloak") == "/tmp/ci-warm-test/keycloak/snapshot"
assert warmsnap.meta_path("keycloak") == "/tmp/ci-warm-test/keycloak/snapshot/meta.json"
assert warmsnap.volumes_dir("keycloak") == "/tmp/ci-warm-test/keycloak/snapshot/volumes"
def test_warm_root_default(monkeypatch):
monkeypatch.delenv("CCCI_WARM_ROOT", raising=False)
assert warmsnap.warm_root() == "/var/lib/ci-warm"
def test_read_meta_absent(monkeypatch, tmp_path):
monkeypatch.setenv("CCCI_WARM_ROOT", str(tmp_path))
assert warmsnap.read_meta("keycloak") is None
assert warmsnap.has_snapshot("keycloak") is False
def _write_snapshot(tmp_path, recipe, volumes):
snapdir = tmp_path / recipe / "snapshot"
(snapdir / "volumes").mkdir(parents=True)
(snapdir / "meta.json").write_text(json.dumps({"recipe": recipe, "volumes": volumes}))
for v in volumes:
(snapdir / "volumes" / f"{v}.tar").write_bytes(b"fake")
def test_has_snapshot_complete(monkeypatch, tmp_path):
monkeypatch.setenv("CCCI_WARM_ROOT", str(tmp_path))
_write_snapshot(tmp_path, "keycloak", ["a_mariadb", "a_providers"])
assert warmsnap.has_snapshot("keycloak") is True
meta = warmsnap.read_meta("keycloak")
assert meta["volumes"] == ["a_mariadb", "a_providers"]
def test_last_good_survives_sibling_of_snapshot(monkeypatch, tmp_path):
# The reconciler's last_good lives in <recipe>/ (sibling of snapshot/) — a test that the layout
# keeps them separate so a snapshot swap can't clobber last_good.
monkeypatch.setenv("CCCI_WARM_ROOT", str(tmp_path))
appdir = tmp_path / "keycloak"
assert warmsnap.snap_dir("keycloak").startswith(str(appdir))
assert os.path.dirname(warmsnap.snap_dir("keycloak")) == str(appdir)
def test_has_snapshot_incomplete_missing_tar(monkeypatch, tmp_path):
monkeypatch.setenv("CCCI_WARM_ROOT", str(tmp_path))
# meta lists two volumes but only one tar present -> incomplete -> not usable.
snapdir = tmp_path / "keycloak" / "snapshot"
(snapdir / "volumes").mkdir(parents=True)
(snapdir / "meta.json").write_text(json.dumps({"recipe": "keycloak", "volumes": ["a", "b"]}))
(snapdir / "volumes" / "a.tar").write_bytes(b"fake")
assert warmsnap.has_snapshot("keycloak") is False