185 lines
7.6 KiB
Python
185 lines
7.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Validate a machine-readable BSP GPIO implementation plan."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
DIRECTIONS = {"input", "output", "peripheral", "none"}
|
|
ACTIVE_LEVELS = {"high", "low", "paired", "unknown", None}
|
|
DECISIONS = {"confirmed", "inherited", "no-gpio", "provisional", "blocked"}
|
|
IMPLEMENTABLE = {"confirmed", "inherited", "no-gpio"}
|
|
REQUIRED_SIGNAL_FIELDS = {
|
|
"function",
|
|
"pad",
|
|
"mux",
|
|
"gpio",
|
|
"direction",
|
|
"active_level",
|
|
"boot_value",
|
|
"runtime_owner",
|
|
"dts_consumers",
|
|
"source_evidence",
|
|
"decision",
|
|
"note",
|
|
}
|
|
|
|
|
|
def validate_plan(payload: object, allow_unconfirmed: bool = False) -> dict[str, object]:
|
|
errors: list[str] = []
|
|
warnings: list[str] = []
|
|
if not isinstance(payload, dict):
|
|
return {"passed": False, "errors": ["顶层必须是 JSON 对象"], "warnings": []}
|
|
|
|
if payload.get("schema_version") != 1:
|
|
errors.append("schema_version 必须为 1")
|
|
if not isinstance(payload.get("project"), str) or not payload.get("project", "").strip():
|
|
errors.append("project 必须是非空字符串")
|
|
if payload.get("validation_mode") not in {"emxres-config", "full-dts-audit"}:
|
|
errors.append("validation_mode 必须为 emxres-config 或 full-dts-audit")
|
|
sources = payload.get("sources")
|
|
if not isinstance(sources, list) or not sources or not all(
|
|
isinstance(item, str) and item.strip() for item in sources
|
|
):
|
|
errors.append("sources 必须是非空字符串数组")
|
|
|
|
confirmation = payload.get("confirmation")
|
|
confirmed = isinstance(confirmation, dict) and confirmation.get("confirmed") is True
|
|
if not confirmed and not allow_unconfirmed:
|
|
errors.append("confirmation.confirmed 必须为 true")
|
|
if confirmed:
|
|
if not isinstance(confirmation.get("confirmed_at"), str) or not confirmation.get(
|
|
"confirmed_at", ""
|
|
).strip():
|
|
errors.append("确认后必须填写 confirmation.confirmed_at")
|
|
if not isinstance(confirmation.get("summary"), str) or not confirmation.get(
|
|
"summary", ""
|
|
).strip():
|
|
errors.append("确认后必须填写 confirmation.summary")
|
|
|
|
signals = payload.get("signals")
|
|
if not isinstance(signals, list) or not signals:
|
|
errors.append("signals 必须是非空数组")
|
|
signals = []
|
|
|
|
functions: set[str] = set()
|
|
pads: set[str] = set()
|
|
gpios: set[str] = set()
|
|
for index, signal in enumerate(signals):
|
|
prefix = f"signals[{index}]"
|
|
if not isinstance(signal, dict):
|
|
errors.append(f"{prefix} 必须是对象")
|
|
continue
|
|
missing = sorted(REQUIRED_SIGNAL_FIELDS - set(signal))
|
|
if missing:
|
|
errors.append(f"{prefix} 缺少字段: {', '.join(missing)}")
|
|
continue
|
|
|
|
function = signal.get("function")
|
|
if not isinstance(function, str) or not function.strip():
|
|
errors.append(f"{prefix}.function 必须是非空字符串")
|
|
elif function in functions:
|
|
errors.append(f"重复功能名: {function}")
|
|
else:
|
|
functions.add(function)
|
|
|
|
direction = signal.get("direction")
|
|
decision = signal.get("decision")
|
|
active_level = signal.get("active_level")
|
|
boot_value = signal.get("boot_value")
|
|
pad = signal.get("pad")
|
|
gpio = signal.get("gpio")
|
|
|
|
if direction not in DIRECTIONS:
|
|
errors.append(f"{prefix}.direction 非法: {direction!r}")
|
|
if active_level not in ACTIVE_LEVELS:
|
|
errors.append(f"{prefix}.active_level 非法: {active_level!r}")
|
|
if decision not in DECISIONS:
|
|
errors.append(f"{prefix}.decision 非法: {decision!r}")
|
|
elif decision not in IMPLEMENTABLE and not allow_unconfirmed:
|
|
errors.append(f"{prefix} 仍为 {decision},不能进入实现")
|
|
|
|
if not isinstance(signal.get("runtime_owner"), str):
|
|
errors.append(f"{prefix}.runtime_owner 必须是字符串")
|
|
if not isinstance(signal.get("note"), str):
|
|
errors.append(f"{prefix}.note 必须是字符串")
|
|
if not isinstance(signal.get("dts_consumers"), list) or not all(
|
|
isinstance(item, str) for item in signal.get("dts_consumers", [])
|
|
):
|
|
errors.append(f"{prefix}.dts_consumers 必须是字符串数组")
|
|
evidence = signal.get("source_evidence")
|
|
if not isinstance(evidence, list) or not evidence or not all(
|
|
isinstance(item, str) and item.strip() for item in evidence
|
|
):
|
|
errors.append(f"{prefix}.source_evidence 必须是非空字符串数组")
|
|
|
|
if decision == "no-gpio":
|
|
if direction != "none" or any(
|
|
value is not None
|
|
for value in (pad, signal.get("mux"), gpio, active_level, boot_value)
|
|
):
|
|
errors.append(
|
|
f"{prefix} 的 no-gpio 行必须使用 direction=none 且 pad/mux/gpio/active_level/boot_value=null"
|
|
)
|
|
elif direction in {"input", "output"}:
|
|
if not isinstance(pad, str) or not pad.strip():
|
|
errors.append(f"{prefix} 的 GPIO 行必须填写 pad")
|
|
if not isinstance(gpio, str) or not gpio.strip():
|
|
errors.append(f"{prefix} 的 GPIO 行必须填写 gpio")
|
|
if active_level is None:
|
|
errors.append(f"{prefix} 的 GPIO 行不能使用 active_level=null")
|
|
if active_level == "unknown" and not allow_unconfirmed:
|
|
errors.append(f"{prefix} 的 active_level 仍为 unknown")
|
|
if direction == "output" and boot_value not in {0, 1}:
|
|
errors.append(f"{prefix} 的输出必须填写 boot_value=0 或 1")
|
|
if direction == "input" and boot_value is not None:
|
|
errors.append(f"{prefix} 的输入必须使用 boot_value=null")
|
|
elif direction == "peripheral":
|
|
if not isinstance(pad, str) or not pad.strip():
|
|
errors.append(f"{prefix} 的外设复用必须填写 pad")
|
|
if gpio is not None or active_level is not None or boot_value is not None:
|
|
errors.append(f"{prefix} 的外设复用必须使用 gpio/active_level/boot_value=null")
|
|
elif direction == "none" and decision != "no-gpio":
|
|
errors.append(f"{prefix} 只有 no-gpio 决策可以使用 direction=none")
|
|
|
|
if isinstance(pad, str) and pad:
|
|
if pad in pads:
|
|
errors.append(f"重复 Pad: {pad}")
|
|
pads.add(pad)
|
|
if isinstance(gpio, str) and gpio:
|
|
if gpio in gpios:
|
|
errors.append(f"重复 GPIO: {gpio}")
|
|
gpios.add(gpio)
|
|
|
|
if allow_unconfirmed and not confirmed:
|
|
warnings.append("计划尚未获得用户确认,仅通过候选结构检查")
|
|
return {
|
|
"passed": not errors,
|
|
"signal_count": len(signals),
|
|
"errors": errors,
|
|
"warnings": warnings,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
parser.add_argument("plan", type=Path)
|
|
parser.add_argument("--allow-unconfirmed", action="store_true")
|
|
args = parser.parse_args()
|
|
try:
|
|
payload = json.loads(args.plan.read_text(encoding="utf-8"))
|
|
result = validate_plan(payload, allow_unconfirmed=args.allow_unconfirmed)
|
|
except (OSError, json.JSONDecodeError) as exc:
|
|
print(json.dumps({"passed": False, "errors": [str(exc)]}, ensure_ascii=False, indent=2))
|
|
return 2
|
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
|
return 0 if result["passed"] else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|