Files
massive-cloud/alg/00_算法交付与输出规范/validate_result.py

137 lines
5.8 KiB
Python
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""算法测量结果契约 1.0 的零第三方依赖校验器。"""
from __future__ import annotations
import json
import math
import re
import sys
from pathlib import Path
from typing import Any
METRIC_CODE_RE = re.compile(r"^[a-z0-9]+(?:[._-][a-z0-9]+)*$")
TASK_NO_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]*$")
TASK_NAME_PREFIX_RE = re.compile(r"^任务\s*\d+")
FINAL_STATUSES = {"pass", "warning", "fail"}
def is_number(value: Any) -> bool:
return isinstance(value, (int, float)) and not isinstance(value, bool) and math.isfinite(value)
def validate(payload: Any) -> list[str]:
errors: list[str] = []
if not isinstance(payload, dict):
return ["根节点必须是 JSON 对象"]
if not isinstance(payload.get("schemaVersion"), int) or payload["schemaVersion"] < 1:
errors.append("schemaVersion 必须是大于等于 1 的整数")
if payload.get("measurementContractVersion") != "1.0":
errors.append('measurementContractVersion 必须为 "1.0"')
if payload.get("status") not in {"completed", "error"}:
errors.append("status 必须为 completed 或 error")
measurements = payload.get("measurements")
if payload.get("status") == "completed" and not isinstance(measurements, list):
errors.append("completed 结果必须包含 measurements 数组")
measurements = []
elif measurements is None:
measurements = []
elif not isinstance(measurements, list):
errors.append("measurements 必须是数组")
measurements = []
unique_keys: set[tuple[str, str, str]] = set()
for index, item in enumerate(measurements):
path = f"measurements[{index}]"
if not isinstance(item, dict):
errors.append(f"{path} 必须是对象")
continue
task_no = item.get("taskNo")
metric_code = item.get("metricCode")
name = item.get("name")
position = item.get("position", "")
value = item.get("value", ...)
status = item.get("status")
if not isinstance(task_no, str) or not TASK_NO_RE.fullmatch(task_no):
errors.append(f"{path}.taskNo 必须是非空规范字符串,数字任务号也要加引号")
if not isinstance(metric_code, str) or not METRIC_CODE_RE.fullmatch(metric_code):
errors.append(f"{path}.metricCode 格式无效")
if not isinstance(name, str) or not name.strip():
errors.append(f"{path}.name 必须是非空字符串")
elif TASK_NAME_PREFIX_RE.search(name):
errors.append(f"{path}.name 不得包含任务号前缀,请使用 taskNo 字段")
if position and not isinstance(position, str):
errors.append(f"{path}.position 必须是字符串")
if value is ... or (value is not None and not is_number(value)):
errors.append(f"{path}.value 必须是有限数值或 null")
if status not in FINAL_STATUSES:
errors.append(f"{path}.status 必须为 pass、warning 或 fail,正式结果不得使用 info")
if value is None and status == "pass":
errors.append(f"{path} 的 value 为 null 时不能判定为 pass")
if "baseline" in item and (not is_number(item["baseline"]) or item["baseline"] <= 0):
errors.append(f"{path}.baseline 必须是正数")
if "tolerance" in item and (not is_number(item["tolerance"]) or item["tolerance"] < 0):
errors.append(f"{path}.tolerance 必须是非负有限数值")
if isinstance(task_no, str) and isinstance(metric_code, str) and isinstance(position, str):
key = (task_no, metric_code, position)
if key in unique_keys:
errors.append(f"{path} 与其他正式测量项重复:taskNo + metricCode + position 必须唯一")
unique_keys.add(key)
extra = payload.get("extra", {})
if extra is not None and not isinstance(extra, dict):
errors.append("extra 必须是对象")
extra = {}
intermediate = extra.get("intermediateMetrics", []) if isinstance(extra, dict) else []
if not isinstance(intermediate, list):
errors.append("extra.intermediateMetrics 必须是数组")
intermediate = []
for index, item in enumerate(intermediate):
path = f"extra.intermediateMetrics[{index}]"
if not isinstance(item, dict):
errors.append(f"{path} 必须是对象")
continue
if any(field in item for field in ("taskNo", "status", "tolerance")):
errors.append(f"{path} 不得包含 taskNo、status 或 tolerance")
code = item.get("metricCode")
if not isinstance(code, str) or not METRIC_CODE_RE.fullmatch(code):
errors.append(f"{path}.metricCode 格式无效")
if not isinstance(item.get("name"), str) or not item["name"].strip():
errors.append(f"{path}.name 必须是非空字符串")
value = item.get("value", ...)
if value is ... or (value is not None and not is_number(value)):
errors.append(f"{path}.value 必须是有限数值或 null")
return errors
def main() -> int:
if len(sys.argv) != 2:
print(f"用法: {Path(sys.argv[0]).name} /path/to/result.json", file=sys.stderr)
return 2
path = Path(sys.argv[1])
try:
payload = json.loads(path.read_text(encoding="utf-8"), parse_constant=lambda value: (_ for _ in ()).throw(ValueError(f"非法数值 {value}")))
except (OSError, json.JSONDecodeError, ValueError) as exc:
print(f"校验失败: 无法读取标准 JSON:{exc}", file=sys.stderr)
return 1
errors = validate(payload)
if errors:
print(f"校验失败,共 {len(errors)} 个问题:", file=sys.stderr)
for error in errors:
print(f"- {error}", file=sys.stderr)
return 1
print(f"校验通过:{path}")
return 0
if __name__ == "__main__":
raise SystemExit(main())