feat: 完成 issue #28 ② Kafka topic 命名/分区模板化(按模板+点位维度)
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Kafka topic 命名/分区模板化测试(issue #28)。
|
||||
|
||||
覆盖:
|
||||
1. 模板配置资产加载(config/kafka.template.yaml,含点位维度覆盖规则);
|
||||
2. topic 推导:`{topic_prefix}.{device_id}.points`(与 edge-gateway 上行一致);
|
||||
3. 分区:device_id 一致性哈希(确定性、单设备分区内有序);
|
||||
4. 点位维度覆盖:LAB-* 前缀 → 质检独立 topic,其余走设备 topic;
|
||||
5. 生产路由:按 (topic, partition) 批量分组。
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import _bootstrap # noqa: F401
|
||||
|
||||
from data_bus.kafka_naming import ( # noqa: E402
|
||||
DEFAULT_CONFIG_PATH,
|
||||
KafkaTopicNaming,
|
||||
)
|
||||
|
||||
CONFIG = os.path.join(
|
||||
os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
|
||||
"config", "kafka.template.yaml",
|
||||
)
|
||||
|
||||
|
||||
def sample(device_id="CLF-01", point_id="CLF-01.TEMP", value=850.5, ts=1.0):
|
||||
return {"device_id": device_id, "point_id": point_id,
|
||||
"value": value, "ts": ts}
|
||||
|
||||
|
||||
class TestConfigLoad(unittest.TestCase):
|
||||
"""模板配置资产加载与摘要。"""
|
||||
|
||||
def test_from_template_config(self):
|
||||
naming = KafkaTopicNaming.from_template_config(CONFIG)
|
||||
self.assertEqual(naming._tpl.template, "ti-cl4")
|
||||
self.assertEqual(naming.num_partitions, 12)
|
||||
self.assertEqual(naming.acks, "all")
|
||||
self.assertEqual(naming.retention_hours, 168)
|
||||
brief = naming.brief()
|
||||
self.assertIn("LAB-", brief["per_point_rules"])
|
||||
self.assertIn("ALM-", brief["per_point_rules"])
|
||||
|
||||
def test_default_config_path_exists(self):
|
||||
self.assertTrue(os.path.isfile(DEFAULT_CONFIG_PATH))
|
||||
|
||||
|
||||
class TestTopicNaming(unittest.TestCase):
|
||||
"""topic 推导(与 edge-gateway 上行格式一致)。"""
|
||||
|
||||
def setUp(self):
|
||||
self.naming = KafkaTopicNaming.from_template_config(CONFIG)
|
||||
|
||||
def test_device_topic_format(self):
|
||||
# 与 upstream/kafka_sink.py 的 {prefix}.{device}.points 一致
|
||||
self.assertEqual(self.naming.topic("CLF-01"),
|
||||
"iaop.ti-cl4.CLF-01.points")
|
||||
self.assertEqual(self.naming.topic("S7-01"),
|
||||
"iaop.ti-cl4.S7-01.points")
|
||||
|
||||
def test_point_level_topic_override(self):
|
||||
# LAB-* 前缀命中质量标签独立通道;普通点走设备 topic
|
||||
self.assertEqual(
|
||||
self.naming.topic("CLF-01", point_id="LAB-01.TI_PURITY"),
|
||||
"iaop.ti-cl4.quality.points")
|
||||
self.assertEqual(
|
||||
self.naming.topic("CLF-01", point_id="ALM-01.TEMP_HI"),
|
||||
"iaop.ti-cl4.alarm.points")
|
||||
self.assertEqual(
|
||||
self.naming.topic("CLF-01", point_id="CLF-01.TEMP"),
|
||||
"iaop.ti-cl4.CLF-01.points")
|
||||
|
||||
def test_sanitize_device_id(self):
|
||||
# device_id 原样保留(与 edge-gateway 上行一致,点位字典保证合法)
|
||||
self.assertEqual(self.naming.topic("CLF-01"),
|
||||
"iaop.ti-cl4.CLF-01.points")
|
||||
|
||||
|
||||
class TestPartition(unittest.TestCase):
|
||||
"""分区一致性哈希:确定性 + 单设备有序。"""
|
||||
|
||||
def setUp(self):
|
||||
self.naming = KafkaTopicNaming.from_template_config(CONFIG)
|
||||
|
||||
def test_deterministic(self):
|
||||
self.assertEqual(self.naming.partition("CLF-01"),
|
||||
self.naming.partition("CLF-01"))
|
||||
self.assertIn(self.naming.partition("CLF-01"), range(12))
|
||||
|
||||
def test_same_device_same_partition(self):
|
||||
# 单设备所有点位落到同一分区 → 分区内有序
|
||||
p1 = self.naming.partition("CLF-01")
|
||||
for pid in ("CLF-01.TEMP", "CLF-01.PRES", "CLF-01.FEED"):
|
||||
self.assertEqual(self.naming.partition("CLF-01"), p1)
|
||||
|
||||
def test_partitions_preview(self):
|
||||
mapping = self.naming.partitions(["CLF-01", "S7-01", "E-01"])
|
||||
self.assertEqual(set(mapping), {"CLF-01", "S7-01", "E-01"})
|
||||
self.assertTrue(all(0 <= v < 12 for v in mapping.values()))
|
||||
|
||||
|
||||
class TestRouting(unittest.TestCase):
|
||||
"""按 (topic, partition) 批量分组(生产路由)。"""
|
||||
|
||||
def setUp(self):
|
||||
self.naming = KafkaTopicNaming.from_template_config(CONFIG)
|
||||
|
||||
def test_group_by_topic_and_partition(self):
|
||||
rows = [
|
||||
sample("CLF-01", "CLF-01.TEMP"),
|
||||
sample("CLF-01", "CLF-01.PRES"),
|
||||
sample("CLF-01", "LAB-01.TI_PURITY"), # 质量标签 → 独立 topic
|
||||
sample("S7-01", "S7-01.PUMP_A"),
|
||||
]
|
||||
groups = self.naming.routing(rows)
|
||||
# 3 个不同 (topic, partition) 组:CLF 默认 / LAB 质量 / S7
|
||||
self.assertEqual(len(groups), 3)
|
||||
topics = {t for t, _ in groups}
|
||||
self.assertIn("iaop.ti-cl4.quality.points", topics)
|
||||
# CLF 默认 topic 内 2 条同一分区
|
||||
clf_key = ("iaop.ti-cl4.CLF-01.points",
|
||||
self.naming.partition("CLF-01"))
|
||||
self.assertEqual(len(groups[clf_key]), 2)
|
||||
|
||||
def test_rows_without_point_id(self):
|
||||
rows = [{"device_id": "CLF-01", "value": 1.0, "ts": 2.0}]
|
||||
groups = self.naming.routing(rows)
|
||||
self.assertEqual(len(groups), 1)
|
||||
key = next(iter(groups))
|
||||
self.assertEqual(key[0], "iaop.ti-cl4.CLF-01.points")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user