113 lines
5.2 KiB
Python
113 lines
5.2 KiB
Python
#!/usr/bin/env python3
|
|
"""Controlled public smoke for extension ConfigCASSave preparation.
|
|
|
|
The smoke uses no storage coordinates. It creates one extension saved-state
|
|
copy, verifies readback, rolls it back by opaque receipt, then proves that the
|
|
same public selector is immediately ready for another prepare.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
from urllib.error import HTTPError, URLError
|
|
from urllib.request import Request, urlopen
|
|
|
|
|
|
def rpc(base_url: str, method: str, payload: dict, timeout: float) -> dict:
|
|
request = Request(
|
|
base_url.rstrip("/") + "/rpc",
|
|
data=json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8"),
|
|
method="POST",
|
|
headers={"Content-Type": "application/json; charset=utf-8"},
|
|
)
|
|
try:
|
|
with urlopen(request, timeout=timeout) as response:
|
|
result = json.loads(response.read().decode("utf-8"))
|
|
except (HTTPError, URLError) as exc:
|
|
raise AssertionError(f"{method} transport failure: {exc}") from exc
|
|
if not isinstance(result, dict):
|
|
raise AssertionError(f"{method} returned a non-object response")
|
|
return result
|
|
|
|
|
|
def require(condition: bool, message: str) -> None:
|
|
if not condition:
|
|
raise AssertionError(message)
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Public extension saved-state prepare/rollback smoke.")
|
|
parser.add_argument("--base-url", default="http://docker.cin.su:8011")
|
|
parser.add_argument("--base-id", default="upo_test")
|
|
parser.add_argument("--extension", default="фс_ДоработкиОбщее")
|
|
parser.add_argument("--ref", default="Catalog.Номенклатура")
|
|
parser.add_argument("--timeout", type=float, default=120.0)
|
|
parser.add_argument("--report", type=Path)
|
|
parser.add_argument("--apply", action="store_true", help="Perform the controlled SQL prepare and rollback.")
|
|
args = parser.parse_args()
|
|
report: dict = {
|
|
"schema": "onec_extension_saved_state_prepare_smoke.v1",
|
|
"base_url": args.base_url,
|
|
"base_id": args.base_id,
|
|
"extension": args.extension,
|
|
"ref": args.ref,
|
|
"status": "pending",
|
|
"passed": False,
|
|
}
|
|
target = {
|
|
"base_id": args.base_id,
|
|
"extension": args.extension,
|
|
"ref": args.ref,
|
|
"layer": "extension_saved_state",
|
|
}
|
|
try:
|
|
initial = rpc(args.base_url, "metadata.saved_state.prepare", target | {"mode": "plan"}, args.timeout)
|
|
report["initial_plan"] = {"status": initial.get("status"), "counts": initial.get("counts")}
|
|
require(initial.get("status") == "plan_ready", f"initial plan must be plan_ready, got {initial.get('status')}")
|
|
if not args.apply:
|
|
report["status"] = "plan_ready"
|
|
report["passed"] = True
|
|
else:
|
|
applied = rpc(
|
|
args.base_url,
|
|
"metadata.saved_state.prepare",
|
|
target | {"mode": "apply_and_verify", "allow_sql_saved_state_prepare": True},
|
|
args.timeout,
|
|
)
|
|
report["prepare"] = {"status": applied.get("status"), "counts": applied.get("counts"), "verification": applied.get("verification")}
|
|
require(applied.get("status") == "verified" and applied.get("applied") is True, "prepare must be verified")
|
|
receipt_id = str(applied.get("prepare_receipt_id") or "")
|
|
require(receipt_id, "prepare response must include an opaque receipt")
|
|
rolled_back = rpc(
|
|
args.base_url,
|
|
"metadata.saved_state.ensure.rollback",
|
|
{"base_id": args.base_id, "prepare_receipt_id": receipt_id, "allow_sql_saved_state_rollback": True},
|
|
args.timeout,
|
|
)
|
|
report["rollback"] = {"status": rolled_back.get("status"), "counts": rolled_back.get("counts")}
|
|
require(rolled_back.get("status") == "rolled_back" and rolled_back.get("applied") is True, "prepare rollback must succeed")
|
|
started = time.monotonic()
|
|
final_plan = rpc(args.base_url, "metadata.saved_state.prepare", target | {"mode": "plan"}, args.timeout)
|
|
elapsed_ms = round((time.monotonic() - started) * 1000, 1)
|
|
report["post_rollback_plan"] = {"status": final_plan.get("status"), "counts": final_plan.get("counts"), "elapsed_ms": elapsed_ms}
|
|
require(final_plan.get("status") == "plan_ready", f"post-rollback plan must be plan_ready, got {final_plan.get('status')}")
|
|
require(int((final_plan.get("counts") or {}).get("existing_saved_records") or 0) == 0, "rollback must leave no saved records")
|
|
report["status"] = "verified_and_rolled_back"
|
|
report["passed"] = True
|
|
except Exception as exc:
|
|
report["status"] = "failed"
|
|
report["error"] = str(exc)
|
|
if args.report:
|
|
args.report.parent.mkdir(parents=True, exist_ok=True)
|
|
args.report.write_text(json.dumps(report, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
|
print(json.dumps(report, ensure_ascii=False, indent=2))
|
|
return 0 if report["passed"] else 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|