# -*- 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`` 还原, 且字段结构对齐前端 ```` 约定。 用法(在 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 # 字段结构对齐前端 约定 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)。 # ---------------------------------------------------------------------------