@@ -0,0 +1,187 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""报警看板配置化验收脚本(issue #52,PRD 5.5 验收口径)。
|
||||
|
||||
验证四个能力点(覆盖 PRD 5.5「切换模板零改码」对告警面板的覆盖):
|
||||
1. **Ti 行业模板配置资产合法**:``templates/ti-cl4/dashboard/alarm_panel.ti.yaml``
|
||||
所描述的配置(等价 dict)通过 ``iAOP-cockpit-alarm-panel-v1`` 校验,
|
||||
并能渲染成 ``alarm_panel`` 组件 props(证明告警面板已配置化);
|
||||
2. **树脂模板同样可配置**:换一份配置资产(不同配色 / 规则源)即得到不同 props,
|
||||
前端代码零改动(PRD 5.5 验收口径「切换模板零改码」);
|
||||
3. **配置点真实生效**:``mute_lower`` 改变 → ``filter_alarms_by_mute`` 过滤结果
|
||||
改变;``require_ack`` 关闭 → props 里不再出现 ``ackTimeoutS``;
|
||||
4. **负例**:一份故意破坏的配置(缺 P0 配色 / 非法 severity / 缺 rulesSource)
|
||||
被正确拒绝并聚合多条错误(对齐 #50 校验器风格)。
|
||||
|
||||
用法(在 core/cockpit 目录下):
|
||||
python scripts/verify_alarm_config.py
|
||||
退出码:0 = 全部通过;1 = 存在未达标项。
|
||||
|
||||
说明:Ti 模板资产本身是 YAML;本脚本不依赖 PyYAML(避免引入运行时依赖),
|
||||
而是用与该 YAML 文件内容等价的 dict 进行校验——字段语义完全一致,
|
||||
若安装了 PyYAML,可直接解析原文件复现(见文末注释)。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
# 本脚本位于 core/cockpit/scripts/,需要把 core/ 加入 sys.path,
|
||||
# 才能 `from cockpit import ...`(cockpit 包位于 core/cockpit)
|
||||
sys.path.insert(
|
||||
0,
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))),
|
||||
)
|
||||
|
||||
from cockpit import ( # noqa: E402
|
||||
ALARM_CONFIG_SCHEMA_ID,
|
||||
filter_alarms_by_mute,
|
||||
load_alarm_config,
|
||||
render_alarm_panel_props,
|
||||
validate_alarm_config,
|
||||
)
|
||||
|
||||
|
||||
def _ti_config() -> dict:
|
||||
"""与 templates/ti-cl4/dashboard/alarm_panel.ti.yaml 等价的 dict。"""
|
||||
return {
|
||||
"$schema": ALARM_CONFIG_SCHEMA_ID,
|
||||
"severityColors": [
|
||||
{"severity": "P0", "fg": "#ff3b30", "bg": "rgba(255,59,48,0.12)",
|
||||
"icon": "alert-octagon", "border": "#ff3b30"},
|
||||
{"severity": "P1", "fg": "#f5a623", "bg": "rgba(245,166,35,0.12)",
|
||||
"icon": "alert-triangle", "border": "#f5a623"},
|
||||
{"severity": "P2", "fg": "#3aa0ff", "bg": "rgba(58,160,255,0.10)",
|
||||
"icon": "info"},
|
||||
],
|
||||
"rulesSource": {
|
||||
"kind": "asset",
|
||||
"ref": "ti-cl4/impurity-forecast/config/alert_rules.template.yaml",
|
||||
},
|
||||
"thresholdsSource": {
|
||||
"kind": "asset",
|
||||
"ref": "ti-cl4/impurity-forecast/config/alert_rules.template.yaml",
|
||||
},
|
||||
"showSop": True,
|
||||
"requireAck": True,
|
||||
"ackTimeoutS": 300,
|
||||
"muteLower": "P2",
|
||||
"groupBy": "severity",
|
||||
"sortBy": "severity",
|
||||
"maxItems": 50,
|
||||
}
|
||||
|
||||
|
||||
def _resin_config() -> dict:
|
||||
"""树脂行业模板的等价配置(不同配色 + 内联规则,证明换模板零改码)。"""
|
||||
return {
|
||||
"severityColors": [
|
||||
{"severity": "P0", "fg": "#e63329", "bg": "rgba(230,51,41,0.12)"},
|
||||
{"severity": "P1", "fg": "#ffb000", "bg": "rgba(255,176,0,0.12)"},
|
||||
{"severity": "P2", "fg": "#2bb673", "bg": "rgba(43,182,115,0.10)"},
|
||||
],
|
||||
"rulesSource": {
|
||||
"kind": "inline",
|
||||
"data": [
|
||||
{"id": "resin-temp-high", "severity": "P0"},
|
||||
{"id": "resin-crosslink-low", "severity": "P1"},
|
||||
],
|
||||
},
|
||||
"showSop": False,
|
||||
"requireAck": False,
|
||||
"muteLower": "P1", # 树脂模板只看 P0/P1,静默 P2 提示
|
||||
}
|
||||
|
||||
|
||||
def _check(name: str, fn) -> bool:
|
||||
try:
|
||||
ok = fn()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
print(f"[FAIL] {name}: 抛异常 {exc!r}")
|
||||
return False
|
||||
status = "PASS" if ok else "FAIL"
|
||||
print(f"[{status}] {name}")
|
||||
return bool(ok)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print("== 报警看板配置化验收(issue #52 / PRD 5.5)==")
|
||||
all_ok = True
|
||||
|
||||
# 1) Ti 模板资产合法 + 可渲染为 props
|
||||
def check_ti_valid() -> bool:
|
||||
res = validate_alarm_config(_ti_config())
|
||||
if not res.ok:
|
||||
for e in res.errors:
|
||||
print(f" - {e}")
|
||||
return False
|
||||
cfg = load_alarm_config(_ti_config())
|
||||
props = render_alarm_panel_props(cfg)
|
||||
# P0 红色告警配色进入 props;requireAck=True 携带 ackTimeoutS
|
||||
return (
|
||||
props["severityStyles"]["P0"]["fg"] == "#ff3b30"
|
||||
and props["requireAck"] is True
|
||||
and props.get("ackTimeoutS") == 300
|
||||
and props["rulesSource"]["ref"].endswith("alert_rules.template.yaml")
|
||||
)
|
||||
|
||||
all_ok &= _check("Ti 模板配置合法 + 渲染 props 正确", check_ti_valid)
|
||||
|
||||
# 2) 树脂模板换配置 → props 不同,前端代码零改动
|
||||
def check_resin_differs() -> bool:
|
||||
cfg = load_alarm_config(_resin_config())
|
||||
props = render_alarm_panel_props(cfg)
|
||||
# 树脂 P0 用不同红色;内联规则透传 data;requireAck 关闭不带 ackTimeoutS
|
||||
return (
|
||||
props["severityStyles"]["P0"]["fg"] == "#e63329"
|
||||
and props["rulesSource"]["kind"] == "inline"
|
||||
and len(props["rulesSource"]["data"]) == 2
|
||||
and "ackTimeoutS" not in props
|
||||
and props["showSop"] is False
|
||||
)
|
||||
|
||||
all_ok &= _check("树脂模板换配置 → props 随之变化(切换模板零改码)", check_resin_differs)
|
||||
|
||||
# 3) mute_lower 配置点真实驱动展示策略
|
||||
def check_mute_lower() -> bool:
|
||||
alarms = [
|
||||
{"severity": "P2", "id": "a"},
|
||||
{"severity": "P0", "id": "b"},
|
||||
{"severity": "P1", "id": "c"},
|
||||
]
|
||||
keep_all = filter_alarms_by_mute(alarms, "P2")
|
||||
only_p0_p1 = filter_alarms_by_mute(alarms, "P1")
|
||||
# mute=P2 全保留且按严重度降序;mute=P1 过滤掉 P2
|
||||
return (
|
||||
[a["id"] for a in keep_all] == ["b", "c", "a"]
|
||||
and [a["id"] for a in only_p0_p1] == ["b", "c"]
|
||||
)
|
||||
|
||||
all_ok &= _check("muteLower 配置点真实过滤 + 排序告警", check_mute_lower)
|
||||
|
||||
# 4) 负例:破坏的配置被拒绝并聚合多条错误
|
||||
def check_negative() -> bool:
|
||||
bad = {
|
||||
"severityColors": [
|
||||
{"severity": "P0", "fg": "#f00", "bg": "#000"},
|
||||
# 缺 P1 / P2
|
||||
{"severity": "P0", "fg": "#f00", "bg": "#000"}, # 重复 P0
|
||||
{"severity": "P9", "fg": "#f00", "bg": "#000"}, # 非法 severity
|
||||
],
|
||||
"rulesSource": {"kind": "asset"}, # 缺 ref
|
||||
"ackTimeoutS": 0, # 非正整数
|
||||
"sortBy": "color", # 非法排序键
|
||||
"maxItems": 0, # 越界
|
||||
}
|
||||
res = validate_alarm_config(bad)
|
||||
# 应收集到多条错误(聚合而非首条即返)
|
||||
return (not res.ok) and len(res.errors) >= 5
|
||||
|
||||
all_ok &= _check("负例:破坏的配置被拒绝并聚合多条错误", check_negative)
|
||||
|
||||
print("=" * 48)
|
||||
print("结果: " + ("全部通过 ✅" if all_ok else "存在未达标项 ❌"))
|
||||
return 0 if all_ok else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user