246 lines
11 KiB
Python
246 lines
11 KiB
Python
#!/usr/bin/env python3
|
||
from __future__ import annotations
|
||
|
||
import argparse
|
||
import json
|
||
import re
|
||
import urllib.request
|
||
from pathlib import Path
|
||
from typing import Any
|
||
|
||
|
||
DEFAULT_BASE_URL = "http://docker.cin.su:8011"
|
||
|
||
|
||
def rpc(base_url: str, method: str, payload: dict[str, Any]) -> dict[str, Any]:
|
||
body = json.dumps({"method": method, "payload": payload}, ensure_ascii=False).encode("utf-8")
|
||
request = urllib.request.Request(
|
||
base_url.rstrip("/") + "/rpc",
|
||
data=body,
|
||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||
method="POST",
|
||
)
|
||
with urllib.request.urlopen(request, timeout=int(payload.get("timeout_seconds") or 60)) as response:
|
||
return json.loads(response.read().decode("utf-8"))
|
||
|
||
|
||
def should_auto_select(file_name: str) -> bool:
|
||
value = str(file_name or "").strip().lower()
|
||
return not value or value.startswith("placeholder")
|
||
|
||
|
||
def discover_module_target(base_url: str, base_id: str, table: str, timeout_seconds: int) -> dict[str, Any]:
|
||
saved_state = rpc(
|
||
base_url,
|
||
"metadata.saved_state.modules.search",
|
||
{
|
||
"base_id": base_id,
|
||
"tables": [table],
|
||
"limit": 20,
|
||
"scan_limit": 100,
|
||
"include_storage": True,
|
||
"timeout_seconds": timeout_seconds,
|
||
},
|
||
)
|
||
if saved_state.get("status") != "ok":
|
||
raise AssertionError(f"saved-state modules search failed: {saved_state.get('status')}")
|
||
for module in saved_state.get("modules") or []:
|
||
source = module.get("source") if isinstance(module.get("source"), dict) else {}
|
||
file_name = str(source.get("file_name") or module.get("file_name") or "").strip()
|
||
if not file_name:
|
||
continue
|
||
for stream in module.get("streams") or []:
|
||
if not isinstance(stream, dict):
|
||
continue
|
||
preview = str(stream.get("preview") or "")
|
||
if not stream.get("has_bsl_marker") and not any(
|
||
marker in preview for marker in ("Процедура ", "Функция ", "&НаКлиенте", "&НаСервере")
|
||
):
|
||
continue
|
||
declaration = re.search(r"(?im)^\s*(?:Процедура|Функция)\s+[A-Za-zА-Яа-яЁё_][\wА-Яа-яЁё]*\s*\([^)]*\)", preview)
|
||
old = declaration.group(0).strip() if declaration else "#Если " if "#Если " in preview else ""
|
||
if not old:
|
||
continue
|
||
write_plan_target = stream.get("write_plan_target") if isinstance(stream.get("write_plan_target"), dict) else {}
|
||
stream_index = int(stream.get("stream_index") or 0)
|
||
return {
|
||
"file_name": file_name,
|
||
"stream_index": stream_index,
|
||
"module_ref": stream.get("module_ref") or f"{table}:{file_name}#stream:{stream_index}",
|
||
"old": old,
|
||
"new": old + " ",
|
||
"expected_contains": old.strip(),
|
||
"expected_sha1": write_plan_target.get("expected_sha1") or "",
|
||
"discovery": {"status": saved_state.get("status"), "counts": saved_state.get("counts")},
|
||
}
|
||
raise AssertionError("no saved-state BSL module stream candidates found")
|
||
|
||
|
||
def main() -> int:
|
||
|
||
parser = argparse.ArgumentParser(description="Smoke saved-state BSL module stream write with apply_and_rollback.")
|
||
parser.add_argument("--base-url", default=DEFAULT_BASE_URL)
|
||
parser.add_argument("--base-id", default="upo_test")
|
||
parser.add_argument("--table", default="ConfigCASSave")
|
||
parser.add_argument("--file-name", default="", help="Saved-state module file name. Empty or placeholder* auto-selects a BSL stream.")
|
||
parser.add_argument(
|
||
"--module-ref",
|
||
default="",
|
||
help="Exact module_ref returned by discovery. Useful for embedded form modules without a #stream suffix.",
|
||
)
|
||
parser.add_argument("--stream-index", type=int, default=4)
|
||
parser.add_argument("--old", default="Перем Параметры; ")
|
||
parser.add_argument("--new", default="Перем Параметры; ")
|
||
parser.add_argument("--expected-contains", default="Перем Параметры;")
|
||
parser.add_argument("--expected-sha1", default="")
|
||
parser.add_argument("--report", type=Path, required=True)
|
||
parser.add_argument("--timeout-seconds", type=int, default=30)
|
||
parser.add_argument("--allow-empty-saved-state", action="store_true", help="Exit successfully with skipped status when the saved-state table has no modules.")
|
||
args = parser.parse_args()
|
||
|
||
auto_target: dict[str, Any] = {}
|
||
if should_auto_select(args.file_name):
|
||
try:
|
||
auto_target = discover_module_target(args.base_url, args.base_id, args.table, args.timeout_seconds)
|
||
except AssertionError as exc:
|
||
if not args.allow_empty_saved_state:
|
||
raise
|
||
result = {
|
||
"schema": "onec_module_stream_write_smoke.v1",
|
||
"status": "skipped_no_saved_state_candidate",
|
||
"skipped": True,
|
||
"base_id": args.base_id,
|
||
"table": args.table,
|
||
"saved_state_table": args.table,
|
||
"diagnostics": {"message": str(exc)},
|
||
}
|
||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
|
||
return 0
|
||
args.file_name = auto_target["file_name"]
|
||
args.stream_index = int(auto_target["stream_index"])
|
||
args.old = auto_target["old"]
|
||
args.new = auto_target["new"]
|
||
args.expected_contains = auto_target["expected_contains"]
|
||
if auto_target.get("expected_sha1") and not args.expected_sha1:
|
||
args.expected_sha1 = str(auto_target["expected_sha1"])
|
||
|
||
module_ref = str(
|
||
args.module_ref
|
||
or auto_target.get("module_ref")
|
||
or f"{args.table}:{args.file_name}#stream:{args.stream_index}"
|
||
)
|
||
saved_state = rpc(
|
||
args.base_url,
|
||
"metadata.saved_state.modules.search",
|
||
{
|
||
"base_id": args.base_id,
|
||
"tables": [args.table],
|
||
"file_name": args.file_name,
|
||
"limit": 1,
|
||
"scan_limit": 10,
|
||
"include_storage": True,
|
||
"timeout_seconds": args.timeout_seconds,
|
||
},
|
||
)
|
||
saved_state_preflight = {
|
||
"status": saved_state.get("status"),
|
||
"counts": saved_state.get("counts") or {},
|
||
}
|
||
if args.allow_empty_saved_state:
|
||
if saved_state.get("status") == "ok" and ((saved_state.get("counts") or {}).get("modules") or 0) == 0:
|
||
result = {
|
||
"schema": "onec_module_stream_write_smoke.v1",
|
||
"status": "skipped_no_saved_state",
|
||
"skipped": True,
|
||
"base_id": args.base_id,
|
||
"table": args.table,
|
||
"module_ref": module_ref,
|
||
"saved_state_preflight": {"status": saved_state.get("status"), "counts": saved_state.get("counts")},
|
||
}
|
||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
|
||
return 0
|
||
|
||
write_payload: dict[str, Any] = {
|
||
"base_id": args.base_id,
|
||
"target": {"kind": "module", "module_ref": module_ref},
|
||
"mode": "apply_and_rollback",
|
||
"allow_sql_saved_state_apply": True,
|
||
"allow_sql_saved_state_rollback": True,
|
||
"old": args.old,
|
||
"new": args.new,
|
||
"expected_contains": args.expected_contains,
|
||
"timeout_seconds": args.timeout_seconds,
|
||
}
|
||
if args.expected_sha1:
|
||
write_payload["expected_sha1"] = args.expected_sha1
|
||
written = rpc(args.base_url, "metadata.write", write_payload)
|
||
write_result = written.get("result") if isinstance(written.get("result"), dict) else {}
|
||
write_plan = write_result.get("write_plan") if isinstance(write_result.get("write_plan"), dict) else {}
|
||
if written.get("status") != "verified_and_rolled_back" or write_plan.get("allowed") is not True:
|
||
result = {
|
||
"schema": "onec_module_stream_write_smoke.v1",
|
||
"status": "write_failed",
|
||
"base_id": args.base_id,
|
||
"table": args.table,
|
||
"module_ref": module_ref,
|
||
"saved_state_preflight": saved_state_preflight,
|
||
"metadata_write": written,
|
||
"write_plan": write_plan,
|
||
}
|
||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
print(json.dumps({"status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
|
||
return 1
|
||
|
||
final = rpc(
|
||
args.base_url,
|
||
"query.run",
|
||
{
|
||
"base_id": args.base_id,
|
||
"diagnostic": True,
|
||
"query": (
|
||
"SELECT FileName, DataSize, DATALENGTH(BinaryData) AS BinaryBytes, "
|
||
"CONVERT(varchar(40), HASHBYTES('SHA1', BinaryData), 2) AS BinarySHA1 "
|
||
f"FROM {args.table} WHERE FileName = '{args.file_name}'"
|
||
),
|
||
"timeout_seconds": args.timeout_seconds,
|
||
},
|
||
)
|
||
result = {
|
||
"schema": "onec_module_stream_write_smoke.v1",
|
||
"status": "verified_and_rolled_back",
|
||
"base_id": args.base_id,
|
||
"table": args.table,
|
||
"module_ref": module_ref,
|
||
"saved_state_preflight": saved_state_preflight,
|
||
"write_plan": {
|
||
"status": write_plan.get("status"),
|
||
"allowed": write_plan.get("allowed"),
|
||
"apply_method": (write_plan.get("route") or {}).get("apply_method"),
|
||
"target_kind": (write_plan.get("target") or {}).get("target_kind"),
|
||
},
|
||
"metadata_write": {
|
||
"status": written.get("status"),
|
||
"routed_method": written.get("routed_method"),
|
||
"result_status": write_result.get("status"),
|
||
"applied": write_result.get("applied"),
|
||
"rolled_back": write_result.get("rolled_back"),
|
||
},
|
||
"proposal": {key: (write_result.get("proposal") or {}).get(key) for key in ("status", "original", "encoded", "edits", "validation", "counts")},
|
||
"apply_result": write_result.get("apply_result"),
|
||
"rollback_result": write_result.get("rollback_result"),
|
||
"verify_after_rollback": final,
|
||
}
|
||
args.report.parent.mkdir(parents=True, exist_ok=True)
|
||
args.report.write_text(json.dumps(result, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||
print(json.dumps({"schema": result["schema"], "status": result["status"], "path": str(args.report)}, ensure_ascii=False, indent=2))
|
||
return 0
|
||
|
||
|
||
if __name__ == "__main__":
|
||
raise SystemExit(main())
|