160 lines
6.9 KiB
Python
160 lines
6.9 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""t-261007082307-2e2294: репо-гигиена — fetch origin в дневном тике (hermetic).
|
||
|
||
Запуск: ./venv/bin/python tests/test_repo_sync.py
|
||
|
||
Проверяет: _repo_sync_check пишет RepoSynced с behind/ahead (fetch в git-
|
||
песочнице), один замер на день (повтор не плодит записи), при behind>0
|
||
первый день эскалации НЕТ, второй день подряд — EscalationRaised (счётчик
|
||
дней из истории стора, рестарт демона его не сбрасывает), ошибка fetch
|
||
не валит шаг (behind=unknown честно фиксируется), env-порог
|
||
SIMBIOT_REPO_SYNC_MAX_AGE_DAYS управляет чувствительностью.
|
||
"""
|
||
import json
|
||
import os
|
||
import shutil
|
||
import subprocess
|
||
import sys
|
||
import tempfile
|
||
from datetime import timedelta
|
||
|
||
# ── изоляция: своя БД в /tmp, git-песочница вместо реального репо ──
|
||
_DST = "/tmp/simbiot-test-reposync.db"
|
||
for p in (_DST, _DST + "-wal", _DST + "-shm"):
|
||
if os.path.exists(p):
|
||
os.unlink(p)
|
||
os.environ["SIMBIOT_DB"] = _DST
|
||
os.environ["SIMBIOT_REPO_SYNC_MAX_AGE_DAYS"] = "2"
|
||
sys.path.insert(0, "/root/simbiot-harness")
|
||
|
||
from harness import es, paths # noqa: E402
|
||
from harness.levels import LEVELS # noqa: E402
|
||
from harness import scheduler # noqa: E402
|
||
|
||
fails = []
|
||
|
||
|
||
def check(name, cond, extra=""):
|
||
print(("PASS" if cond else "FAIL"), name, ("| " + extra) if not cond else "")
|
||
if not cond:
|
||
fails.append(name)
|
||
|
||
|
||
def _git(cwd, *args):
|
||
r = subprocess.run(["git"] + list(args), cwd=cwd,
|
||
capture_output=True, timeout=30, check=True)
|
||
return r.stdout.decode().strip()
|
||
|
||
|
||
def synced_events(limit=200):
|
||
out = []
|
||
for e in es.get_events(limit=limit):
|
||
if e["event_type"] == "RepoSynced":
|
||
e["data"] = json.loads(e["data"])
|
||
out.append(e)
|
||
return out
|
||
|
||
|
||
def escalations(limit=500):
|
||
return [e for e in es.get_events(limit=limit)
|
||
if e["event_type"] == "EscalationRaised"]
|
||
|
||
|
||
def make_sandbox():
|
||
"""bare origin + клон: init запушен, локально ещё 1 незапушенный коммит."""
|
||
base = tempfile.mkdtemp(prefix="simbiot-reposync-")
|
||
origin = os.path.join(base, "origin.git")
|
||
_git(base, "init", "--bare", "-b", "master", origin)
|
||
clone = os.path.join(base, "clone")
|
||
_git(base, "clone", origin, clone)
|
||
for d in (clone,):
|
||
_git(d, "config", "user.email", "test@simbiot.local")
|
||
_git(d, "config", "user.name", "simbiot-test")
|
||
_git(clone, "commit", "--allow-empty", "-m", "init")
|
||
_git(clone, "push", "origin", "master")
|
||
_git(clone, "commit", "--allow-empty", "-m", "local-only") # ahead=1
|
||
return clone, origin, base
|
||
|
||
|
||
es.init_db()
|
||
clone, origin, base = make_sandbox()
|
||
_real_root = paths.REPO_ROOT
|
||
paths.REPO_ROOT = clone # песочница вместо реального репо
|
||
try:
|
||
# ── 1. happy path: fetch ok, behind=0/ahead=1, RepoSynced записан ──
|
||
res = scheduler._repo_sync_check(LEVELS["day"])
|
||
check("fetch ok", res.get("fetch_ok") is True)
|
||
check("behind=0", res.get("behind") == 0)
|
||
check("ahead=1 (незапушенный коммит виден)", res.get("ahead") == 1)
|
||
evs = synced_events(50)
|
||
check("RepoSynced записан", len(evs) == 1, f"{len(evs)}")
|
||
today = evs[0]["aggregate_id"].split(":", 1)[1]
|
||
check("aggregate_id = repo-sync:<сегодня>",
|
||
evs[0]["aggregate_id"] == "repo-sync:" +
|
||
scheduler._now().strftime("%Y-%m-%d"))
|
||
|
||
# ── 2. повтор в тот же день — замер тот же, записей не больше ──
|
||
again = scheduler._repo_sync_check(LEVELS["day"])
|
||
check("повтор: те же числа", again.get("behind") == 0 and again.get("ahead") == 1)
|
||
check("повтор в тот же день не плодит записи", len(synced_events(50)) == 1)
|
||
|
||
# ── 3. upstream ушёл вперёд: behind=1, эскалации ещё НЕТ (1-й день) ──
|
||
work = os.path.join(base, "worktree")
|
||
_git(base, "clone", origin, work)
|
||
_git(work, "config", "user.email", "test@simbiot.local")
|
||
_git(work, "config", "user.name", "simbiot-test")
|
||
_git(work, "commit", "--allow-empty", "-m", "upstream change")
|
||
_git(work, "push", "origin", "master")
|
||
res3 = scheduler._repo_sync_check(LEVELS["day"])
|
||
check("behind=1 увиден", res3.get("behind") == 1)
|
||
check("эскалации НЕТ в первый день", len(escalations()) == 0)
|
||
|
||
# ── 4. вчера тоже был behind>0 → сегодня 2-й подряд → эскалация ──
|
||
yday = (scheduler._now() - timedelta(days=1)).strftime("%Y-%m-%d")
|
||
eid, _v = es.append_event("harness", f"repo-sync:{yday}", "RepoSynced", {
|
||
"ts": yday + "T06:00:00+00:00", "fetch_ok": True,
|
||
"behind": 1, "ahead": 1})
|
||
db = es._conn()
|
||
db.execute("UPDATE events SET timestamp=? WHERE id=?",
|
||
(yday + "T06:00:00.000000+00:00", eid))
|
||
db.commit()
|
||
db.close()
|
||
res4 = scheduler._repo_sync_check(LEVELS["day"])
|
||
check("behind=1 стабилен", res4.get("behind") == 1)
|
||
escs = escalations()
|
||
check("эскалация ПОДНЯТА на 2-й день подряд", len(escs) == 1, f"{len(escs)}")
|
||
if escs:
|
||
reason = json.loads(escs[-1]["data"]).get("reason", "")
|
||
check("причина упоминает behind и 2 дня",
|
||
"behind=1" in reason and "2 дн" in reason, reason[:120])
|
||
finally:
|
||
paths.REPO_ROOT = _real_root
|
||
shutil.rmtree(base, ignore_errors=True)
|
||
|
||
# ── 5. ошибка fetch не валит шаг (свежая БД: дедуп дня не мешает) ──
|
||
_DST2 = "/tmp/simbiot-test-reposync2.db"
|
||
for p in (_DST2, _DST2 + "-wal", _DST2 + "-shm"):
|
||
if os.path.exists(p):
|
||
os.unlink(p)
|
||
es.DB_PATH = _DST2
|
||
es._signature_schema_ready = False # латч на процесс: новая БД требует миграции
|
||
es.init_db()
|
||
paths.REPO_ROOT = "/nonexistent-repo-dir"
|
||
try:
|
||
res5 = scheduler._repo_sync_check(LEVELS["day"])
|
||
check("fetch-fail: fetch_ok=False, не упал", res5.get("fetch_ok") is False)
|
||
evs5 = synced_events(50)
|
||
check("fetch-fail зафиксирован событием", len(evs5) == 1)
|
||
if evs5:
|
||
check("behind=unknown (None), не выдуманный 0",
|
||
evs5[0]["data"].get("behind") is None)
|
||
check("ошибка сохранена", bool(evs5[0]["data"].get("error")))
|
||
finally:
|
||
paths.REPO_ROOT = _real_root
|
||
|
||
# ── 6. env-порог читается при импорте ──
|
||
check("порог = 2 из env", scheduler._REPO_SYNC_MAX_AGE_DAYS == 2)
|
||
|
||
print()
|
||
print("ИТОГ:", "ВСЕ ОК" if not fails else f"ПРОВАЛЫ: {fails}")
|
||
sys.exit(1 if fails else 0)
|