152 lines
6.0 KiB
Python
152 lines
6.0 KiB
Python
# -*- 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)。
|
|||
|
|
# ---------------------------------------------------------------------------
|