Initial SQL-only 1C adapter baseline
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Check manual evidence files for a 1C extension validation plan."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
PENDING_MARKERS = ("status: pending", '"status": "pending"')
|
||||
PASSED_STATUSES = {"passed", "success", "ok"}
|
||||
STATUS_LINE_RE = re.compile(r"^\s*status\s*:\s*([A-Za-zА-Яа-я0-9_-]+)\s*$", re.IGNORECASE | re.MULTILINE)
|
||||
SECRET_TEXT_RE = re.compile(r"(password|passwd|pwd|secret|token|парол|секрет|usr|user)\s*[:=]", re.IGNORECASE)
|
||||
|
||||
|
||||
def load_json(path: Path) -> dict[str, Any]:
|
||||
return json.loads(path.read_text(encoding="utf-8-sig"))
|
||||
|
||||
|
||||
def write_json(path: Path, data: dict[str, Any]) -> None:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def issue(severity: str, code: str, message: str, *, path: Path | str | None = None, detail: dict[str, Any] | None = None) -> dict[str, Any]:
|
||||
result: dict[str, Any] = {"severity": severity, "code": code, "message": message}
|
||||
if path is not None:
|
||||
result["path"] = str(path)
|
||||
if detail:
|
||||
result["detail"] = detail
|
||||
return result
|
||||
|
||||
|
||||
def evidence_root(plan: dict[str, Any], override: Path | None = None) -> Path:
|
||||
if override:
|
||||
return override
|
||||
configured = ((plan.get("runner_config") or {}).get("evidence_root")) or ((plan.get("evidence") or {}).get("root"))
|
||||
if not configured:
|
||||
raise SystemExit("Validation plan has no evidence root.")
|
||||
return Path(str(configured))
|
||||
|
||||
|
||||
def text_status(text: str) -> str | None:
|
||||
match = STATUS_LINE_RE.search(text)
|
||||
return match.group(1).casefold() if match else None
|
||||
|
||||
|
||||
def check_json_evidence(path: Path, text: str, findings: list[dict[str, Any]]) -> tuple[bool, str | None]:
|
||||
try:
|
||||
payload = json.loads(text)
|
||||
except json.JSONDecodeError as exc:
|
||||
findings.append(issue("error", "invalid_json_evidence", f"Evidence JSON is invalid: {exc}", path=path))
|
||||
return False, None
|
||||
status = str(payload.get("status") or "").casefold()
|
||||
if status not in PASSED_STATUSES:
|
||||
findings.append(issue("error", "evidence_status_not_passed", "Evidence JSON status must be passed/success/ok.", path=path, detail={"status": status or None}))
|
||||
return False, status or None
|
||||
if path.name == "changed-objects-smoke.json":
|
||||
objects = payload.get("objects")
|
||||
if not isinstance(objects, list) or not objects:
|
||||
findings.append(issue("error", "missing_smoke_objects", "changed-objects-smoke.json must contain a non-empty objects list.", path=path))
|
||||
return False, status
|
||||
failed = [
|
||||
{"object_name": item.get("object_name"), "status": item.get("status")}
|
||||
for item in objects
|
||||
if not isinstance(item, dict) or str(item.get("status") or "").casefold() not in PASSED_STATUSES
|
||||
]
|
||||
if failed:
|
||||
findings.append(issue("error", "smoke_object_status_not_passed", "Every changed object smoke record must have status passed/success/ok.", path=path, detail={"failed": failed}))
|
||||
return False, status
|
||||
return True, status
|
||||
|
||||
|
||||
def check_text_evidence(path: Path, text: str, findings: list[dict[str, Any]]) -> tuple[bool, str | None]:
|
||||
status = text_status(text)
|
||||
if status not in PASSED_STATUSES:
|
||||
findings.append(issue("error", "evidence_status_not_passed", "Evidence text must contain a Status: passed/success/ok line.", path=path, detail={"status": status}))
|
||||
return False, status
|
||||
return True, status
|
||||
|
||||
|
||||
def check_evidence(plan_path: Path, output_root: Path | None = None) -> dict[str, Any]:
|
||||
findings: list[dict[str, Any]] = []
|
||||
file_checks: list[dict[str, Any]] = []
|
||||
plan = load_json(plan_path)
|
||||
if plan.get("schema") != "onec_extension_validation_plan.v1":
|
||||
findings.append(issue("error", "invalid_plan_schema", "Validation plan schema is not onec_extension_validation_plan.v1.", path=plan_path, detail={"schema": plan.get("schema")}))
|
||||
return build_result(plan_path, Path("."), findings, file_checks)
|
||||
|
||||
root = evidence_root(plan, output_root)
|
||||
if not root.exists() or not root.is_dir():
|
||||
findings.append(issue("error", "missing_evidence_root", "Evidence root is missing.", path=root))
|
||||
return build_result(plan_path, root, findings, file_checks)
|
||||
|
||||
for name in (plan.get("evidence") or {}).get("required_files") or []:
|
||||
relative = Path(str(name).replace("\\", "/"))
|
||||
path = root / relative
|
||||
check: dict[str, Any] = {
|
||||
"relative_path": str(relative).replace("\\", "/"),
|
||||
"path": str(path),
|
||||
"exists": path.exists(),
|
||||
"filled": False,
|
||||
}
|
||||
if not path.exists():
|
||||
findings.append(issue("error", "missing_evidence_file", "Required evidence file is missing.", path=path))
|
||||
else:
|
||||
text = path.read_text(encoding="utf-8-sig", errors="replace")
|
||||
stripped = text.strip()
|
||||
check["size"] = len(text.encode("utf-8"))
|
||||
if SECRET_TEXT_RE.search(text):
|
||||
findings.append(issue("error", "secret_like_text_in_evidence", "Evidence file contains secret-like key/value text.", path=path))
|
||||
pending = any(marker in text.casefold() for marker in PENDING_MARKERS)
|
||||
if pending:
|
||||
findings.append(issue("error", "pending_evidence_file", "Evidence file still contains a pending template.", path=path))
|
||||
if not stripped:
|
||||
findings.append(issue("error", "empty_evidence_file", "Evidence file is empty.", path=path))
|
||||
elif path.suffix.casefold() == ".json":
|
||||
passed, status = check_json_evidence(path, text, findings)
|
||||
check["status"] = status
|
||||
check["filled"] = passed and not pending
|
||||
else:
|
||||
passed, status = check_text_evidence(path, text, findings)
|
||||
check["status"] = status
|
||||
check["filled"] = passed and not pending
|
||||
file_checks.append(check)
|
||||
|
||||
manifest_path = root / "_codex_validation_evidence_manifest.json"
|
||||
if not manifest_path.exists():
|
||||
findings.append(issue("warning", "missing_evidence_manifest", "Evidence manifest is missing.", path=manifest_path))
|
||||
|
||||
return build_result(plan_path, root, findings, file_checks)
|
||||
|
||||
|
||||
def build_result(plan_path: Path, root: Path, findings: list[dict[str, Any]], file_checks: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
errors = [row for row in findings if row.get("severity") == "error"]
|
||||
warnings = [row for row in findings if row.get("severity") == "warning"]
|
||||
return {
|
||||
"schema": "onec_extension_validation_evidence_check.v1",
|
||||
"plan_path": str(plan_path),
|
||||
"evidence_root": str(root),
|
||||
"passed": not errors,
|
||||
"status": "validated" if not errors else "pending_or_blocked",
|
||||
"findings": findings,
|
||||
"file_checks": file_checks,
|
||||
"counts": {
|
||||
"files": len(file_checks),
|
||||
"filled": sum(1 for item in file_checks if item.get("filled")),
|
||||
"errors": len(errors),
|
||||
"warnings": len(warnings),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Check manual evidence files for a 1C extension validation plan.")
|
||||
parser.add_argument("--plan", type=Path, required=True)
|
||||
parser.add_argument("--evidence-root", type=Path)
|
||||
parser.add_argument("--output", type=Path)
|
||||
args = parser.parse_args()
|
||||
|
||||
result = check_evidence(args.plan, args.evidence_root)
|
||||
if args.output:
|
||||
write_json(args.output, result)
|
||||
print(json.dumps({"output": str(args.output) if args.output else None, "passed": result["passed"], "status": result["status"], "counts": result["counts"]}, ensure_ascii=False))
|
||||
return 0 if result["passed"] else 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user