This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""配置预览渲染引擎测试(issue #65)。
|
||||
|
||||
覆盖:
|
||||
1. 布局预览(widget 卡片 + 网格占用率 + 越界/重叠检测);
|
||||
2. 告警预览(规则渲染 + 模拟触发评估);
|
||||
3. NL 查询预览(模板 + 示例问句生成);
|
||||
4. 从 ConfigStore 一键预览(含空配置的宽容处理);
|
||||
5. PreviewResult 的 ok/to_dict。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import _bootstrap # noqa: F401
|
||||
|
||||
from template_console.config_store import ConfigKind, ConfigStore # noqa: E402
|
||||
from template_console.preview import ( # noqa: E402
|
||||
AlarmRule,
|
||||
GRID_COLUMNS,
|
||||
NLQueryTemplate,
|
||||
PreviewKind,
|
||||
PreviewResult,
|
||||
preview_from_store,
|
||||
render_alarm_preview,
|
||||
render_layout_preview,
|
||||
render_nl_query_preview,
|
||||
)
|
||||
|
||||
|
||||
class LayoutPreviewTest(unittest.TestCase):
|
||||
"""布局预览。"""
|
||||
|
||||
def test_basic_render(self):
|
||||
widgets = [
|
||||
{"type": "process_view", "src": "x.svg", "x": 0, "y": 0, "w": 12, "h": 4,
|
||||
"description": "工艺流程"},
|
||||
{"type": "trend", "bind": "R-801.TEMP", "x": 0, "y": 4, "w": 6, "h": 2},
|
||||
]
|
||||
r = render_layout_preview(widgets)
|
||||
self.assertEqual(len(r.items), 2)
|
||||
self.assertEqual(r.items[0]["type"], "process_view")
|
||||
self.assertEqual(r.items[1]["bind"], "R-801.TEMP")
|
||||
self.assertTrue(r.ok) # 无越界/重叠
|
||||
self.assertTrue(any("网格占用率" in n for n in r.notes))
|
||||
|
||||
def test_overflow_warning(self):
|
||||
# x+w 超过 12 列
|
||||
widgets = [{"type": "trend", "bind": "p", "x": 8, "y": 0, "w": 6, "h": 2}]
|
||||
r = render_layout_preview(widgets)
|
||||
self.assertFalse(r.ok)
|
||||
self.assertTrue(any("越界" in w for w in r.warnings))
|
||||
|
||||
def test_overlap_warning(self):
|
||||
widgets = [
|
||||
{"type": "trend", "bind": "a", "x": 0, "y": 0, "w": 6, "h": 2},
|
||||
{"type": "kpi_card", "metric": "m", "label": "L", "x": 3, "y": 0, "w": 6, "h": 2},
|
||||
]
|
||||
r = render_layout_preview(widgets)
|
||||
self.assertFalse(r.ok)
|
||||
self.assertTrue(any("重叠" in w for w in r.warnings))
|
||||
|
||||
def test_bad_widget_type(self):
|
||||
widgets = [{"type": "unknown", "x": 0, "y": 0, "w": 1, "h": 1}]
|
||||
r = render_layout_preview(widgets)
|
||||
self.assertFalse(r.ok)
|
||||
self.assertTrue(any("非法类型" in w for w in r.warnings))
|
||||
|
||||
def test_kpi_card_carries_metric_and_label(self):
|
||||
widgets = [{"type": "kpi_card", "metric": "yield", "label": "产率",
|
||||
"x": 0, "y": 0, "w": 3, "h": 2}]
|
||||
r = render_layout_preview(widgets)
|
||||
self.assertEqual(r.items[0]["metric"], "yield")
|
||||
self.assertEqual(r.items[0]["label"], "产率")
|
||||
|
||||
def test_empty_widgets(self):
|
||||
r = render_layout_preview([])
|
||||
self.assertEqual(r.items, [])
|
||||
self.assertTrue(r.ok)
|
||||
|
||||
|
||||
class AlarmPreviewTest(unittest.TestCase):
|
||||
"""告警预览。"""
|
||||
|
||||
def test_render_rules(self):
|
||||
rules = [AlarmRule("R-801.TEMP", "反应釜温度", ">", 120.0, "critical")]
|
||||
r = render_alarm_preview(rules)
|
||||
self.assertEqual(len(r.items), 1)
|
||||
self.assertIn("critical", r.items[0]["text"])
|
||||
self.assertTrue(r.ok)
|
||||
|
||||
def test_evaluate_triggered(self):
|
||||
rules = [AlarmRule("R-801.TEMP", "温度", ">", 120.0, "critical")]
|
||||
r = render_alarm_preview(rules, current_values={"R-801.TEMP": 130.0})
|
||||
self.assertTrue(r.items[0]["triggered"])
|
||||
self.assertEqual(r.items[0]["state"], "触发")
|
||||
self.assertTrue(any("1/1" in n for n in r.notes))
|
||||
|
||||
def test_evaluate_not_triggered(self):
|
||||
rules = [AlarmRule("P1", "温度", ">", 120.0, "warn")]
|
||||
r = render_alarm_preview(rules, current_values={"P1": 100.0})
|
||||
self.assertFalse(r.items[0]["triggered"])
|
||||
self.assertEqual(r.items[0]["state"], "正常")
|
||||
|
||||
def test_operators(self):
|
||||
for op, val, thr in [(">=", 120, 120), ("<", 50, 100), ("<=", 100, 100), ("==", 5, 5)]:
|
||||
rule = AlarmRule("P", "m", op, thr, "warn")
|
||||
self.assertTrue(rule.evaluate(val), f"{op} {val} {thr} 应触发")
|
||||
|
||||
def test_bad_level_and_operator(self):
|
||||
rules = [AlarmRule("P", "m", "~", 1.0, level="boom")]
|
||||
r = render_alarm_preview(rules)
|
||||
self.assertFalse(r.ok)
|
||||
self.assertTrue(any("非法级别" in w for w in r.warnings))
|
||||
self.assertTrue(any("非法运算符" in w for w in r.warnings))
|
||||
|
||||
|
||||
class NLQueryPreviewTest(unittest.TestCase):
|
||||
"""NL 查询预览。"""
|
||||
|
||||
def test_render_with_examples(self):
|
||||
t = NLQueryTemplate(
|
||||
name="batch_query",
|
||||
question_template="最近一批的{metric}是多少?",
|
||||
slots={"metric": ["产率", "能耗"]},
|
||||
data_source="tdengine",
|
||||
answer_hint="返回当批聚合值",
|
||||
)
|
||||
r = render_nl_query_preview([t])
|
||||
self.assertEqual(len(r.items), 1)
|
||||
self.assertEqual(len(r.items[0]["examples"]), 2)
|
||||
self.assertIn("产率", r.items[0]["examples"][0])
|
||||
self.assertEqual(r.items[0]["data_source"], "tdengine")
|
||||
|
||||
def test_empty_question_warns(self):
|
||||
t = NLQueryTemplate(name="x", question_template="", slots={})
|
||||
r = render_nl_query_preview([t])
|
||||
self.assertFalse(r.ok)
|
||||
|
||||
def test_no_slots_returns_template(self):
|
||||
t = NLQueryTemplate(name="x", question_template="整体能耗?", slots={})
|
||||
self.assertEqual(t.render_examples(), ["整体能耗?"])
|
||||
|
||||
|
||||
class StorePreviewTest(unittest.TestCase):
|
||||
"""从 ConfigStore 一键预览。"""
|
||||
|
||||
def setUp(self):
|
||||
self._tmp = tempfile.mkdtemp()
|
||||
self.store = ConfigStore(self._tmp)
|
||||
|
||||
def tearDown(self):
|
||||
import shutil
|
||||
shutil.rmtree(self._tmp, ignore_errors=True)
|
||||
|
||||
def test_layout_preview_from_store(self):
|
||||
self.store.upsert(ConfigKind.LAYOUT, "dashboard",
|
||||
[{"type": "trend", "bind": "p", "x": 0, "y": 0, "w": 6, "h": 2}])
|
||||
r = preview_from_store(self.store, PreviewKind.LAYOUT)
|
||||
self.assertEqual(len(r.items), 1)
|
||||
self.assertEqual(r.items[0]["bind"], "p")
|
||||
|
||||
def test_empty_layout_is_graceful(self):
|
||||
r = preview_from_store(self.store, PreviewKind.LAYOUT)
|
||||
self.assertEqual(r.items, [])
|
||||
self.assertTrue(any("未配置" in n for n in r.notes))
|
||||
|
||||
def test_alarm_preview_from_store(self):
|
||||
self.store.upsert(ConfigKind.MODEL_PARAM, "alarm_temp",
|
||||
{"point_id": "R-801.TEMP", "metric": "温度",
|
||||
"operator": ">", "threshold": 120, "level": "critical"})
|
||||
r = preview_from_store(self.store, PreviewKind.ALARM,
|
||||
current_values={"R-801.TEMP": 130})
|
||||
self.assertTrue(r.items[0]["triggered"])
|
||||
|
||||
def test_nl_query_preview_from_store(self):
|
||||
self.store.upsert(ConfigKind.RAG_CONFIG, "nl_batch",
|
||||
{"name": "批次查询", "question_template": "{m}多少?",
|
||||
"slots": {"m": ["产率"]}, "data_source": "tdengine"})
|
||||
r = preview_from_store(self.store, PreviewKind.NL_QUERY)
|
||||
self.assertEqual(len(r.items), 1)
|
||||
self.assertEqual(r.items[0]["name"], "批次查询")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user