Files
iAOP/core/cockpit/scripts/verify_renderer.py
T

152 lines
6.0 KiB
Python
Raw Normal View History

# -*- coding: utf-8 -*-
"""图表组件渲染引擎验证脚本(issue #51,PRD 5.5 渲染层适配器验收)。
验证四个能力点:
1. **PRD 5.5 原始示例**(氯化车间)经布局校验后可渲染为合法 ``RenderPlan``,
且每类 widget 映射到正确前端组件 + props;
2. **现有树脂模板兼容**:与 ``templates/resin/dashboard/cockpit.resin.yaml``
等价的布局可渲染,且两个 trend 组件 id 唯一(虚拟滚动 key);
3. **主题 token 注入**:dark / light 两套 CSS 变量按主题切换;
4. **渲染计划可序列化**:``plan_to_json`` 输出可被 ``json.loads`` 还原,
且字段结构对齐前端 ``<WidgetHost>`` 约定。
用法(在 core/cockpit 目录下):
python scripts/verify_renderer.py
退出码:0 = 全部通过;1 = 存在未达标项。
说明:本脚本不依赖 PyYAML(与 #50 verify_layout_schema.py 一致,避免引入
运行时依赖),使用等价 dict 复现树脂驾驶舱布局;若装了 PyYAML 可直接解析
原 YAML 文件复现(见文末注释)。
"""
from __future__ import annotations
import os
import sys
# 本脚本位于 core/cockpit/scripts/,需要把 core/ 加入 sys.path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
from cockpit import ( # noqa: E402
LAYOUT_SCHEMA_ID,
THEME_TOKENS,
WIDGET_COMPONENT,
plan_to_json,
render_layout,
validate_layout,
)
from cockpit.layout import load_layout # noqa: E402
def _ti_layout() -> dict:
"""PRD 5.5 氯化车间示例。"""
return {
"$schema": "iAOP-cockpit-layout-v1",
"title": "氯化车间驾驶舱",
"theme": "dark",
"widgets": [
{"type": "process_view", "src": "ti_four_state.svg", "x": 0, "y": 0, "w": 6, "h": 4},
{"type": "trend", "bind": "CLF-01.TEMP", "x": 6, "y": 0, "w": 6, "h": 2},
{"type": "kpi_card", "metric": "Ti_purity", "label": "Ti 纯度", "x": 6, "y": 2, "w": 3, "h": 2},
{"type": "alarm_panel", "x": 0, "y": 4, "w": 12, "h": 3},
{"type": "nl_query", "x": 9, "y": 2, "w": 3, "h": 2},
],
}
def _resin_like_layout() -> dict:
"""等价于 templates/resin/dashboard/cockpit.resin.yaml 的布局。"""
return {
"$schema": "iAOP-cockpit-layout-v1",
"title": "吸附树脂车间驾驶舱",
"theme": "dark",
"widgets": [
{"type": "process_view", "src": "resin_four_state.svg", "x": 0, "y": 0, "w": 12, "h": 4},
{"type": "trend", "bind": "R-801.TEMP", "x": 0, "y": 4, "w": 6, "h": 2},
{"type": "trend", "bind": "R-801.AGIT", "x": 6, "y": 4, "w": 6, "h": 2},
{"type": "kpi_card", "metric": "resin_exchange_capacity", "label": "交换容量",
"x": 0, "y": 6, "w": 3, "h": 2},
{"type": "alarm_panel", "x": 0, "y": 8, "w": 9, "h": 3},
{"type": "nl_query", "x": 9, "y": 8, "w": 3, "h": 3},
],
}
def _check_render(name: str, data: dict) -> bool:
"""校验 + 渲染一份布局,打印结果,返回是否通过。"""
res = validate_layout(data)
if not res.ok:
print(f"[FAIL] {name}: 布局校验未通过")
for e in res.errors:
print(f" - {e}")
return False
plan = render_layout(load_layout(data))
print(f"[PASS] {name}: 渲染 {plan.widget_count} 个组件,主题={plan.theme}")
for spec in plan.specs:
print(f" - {spec.id} → {spec.component} "
f"({spec.grid.width_pct}%, {spec.props})")
return True
def _check_themes() -> bool:
"""主题 token:dark / light 两套 CSS 变量都存在且不同。"""
ok = True
for theme in ("dark", "light"):
data = _ti_layout()
data["theme"] = theme
plan = render_layout(load_layout(data))
tokens = plan.theme_tokens
match = tokens == THEME_TOKENS[theme]
print(f"[{'PASS' if match else 'FAIL'}] 主题 token({theme}): {len(tokens)} 个 CSS 变量")
ok = ok and match
if THEME_TOKENS["dark"]["--cockpit-bg"] == THEME_TOKENS["light"]["--cockpit-bg"]:
print("[FAIL] dark/light 底色不应相同")
ok = False
else:
print("[PASS] dark/light 底色不同(确为两套配色)")
return ok
def _check_serialization() -> bool:
"""渲染计划可 JSON 序列化往返。"""
import json
plan = render_layout(load_layout(_resin_like_layout()))
text = plan_to_json(plan)
parsed = json.loads(text)
ok = parsed["title"] == "吸附树脂车间驾驶舱" and parsed["widgetCount"] == 6
# 字段结构对齐前端 <WidgetHost> 约定
fields_ok = all({"component", "grid", "props", "perf"} <= set(w) for w in parsed["widgets"])
print(f"[{'PASS' if ok and fields_ok else 'FAIL'}] 序列化往返: JSON 可解析、字段齐全")
return ok and fields_ok
def main() -> int:
print("=" * 70)
print("图表组件渲染引擎验证(issue #51 / PRD 5.5 渲染层适配器)")
print("=" * 70)
print(f"已注册前端组件映射: {WIDGET_COMPONENT}")
print(f"布局版本: {LAYOUT_SCHEMA_ID}")
print("-" * 70)
all_ok = True
all_ok &= _check_render("PRD 5.5 氯化车间示例", _ti_layout())
all_ok &= _check_render("树脂模板兼容", _resin_like_layout())
all_ok &= _check_themes()
all_ok &= _check_serialization()
print("-" * 70)
print(f"结果: {'全部通过 ✅' if all_ok else '存在未达标项 ❌'}")
return 0 if all_ok else 1
if __name__ == "__main__":
raise SystemExit(main())
# ---------------------------------------------------------------------------
# 附:若安装了 PyYAML,可直接解析树脂模板原文件复现渲染:
# import yaml
# with open("../../templates/resin/dashboard/cockpit.resin.yaml", encoding="utf-8") as f:
# data = yaml.safe_load(f)
# render_layout(load_layout(data))
# 字段语义与本脚本的 _resin_like_layout() 完全一致(iAOP-cockpit-layout-v1)。
# ---------------------------------------------------------------------------