125 lines
4.2 KiB
Python
125 lines
4.2 KiB
Python
"""根因分析(RootCauseAnalyzer)单元测试。"""
|
||
import _bootstrap # noqa: F401
|
||
import unittest
|
||
from datetime import datetime, timedelta, timezone
|
||
|
||
from fault_log_analyzer.models import Event, FaultLog
|
||
from fault_log_analyzer.root_cause import RootCauseAnalyzer, _as_aware
|
||
|
||
BASE = datetime(2025, 1, 1, 10, 0, 0, tzinfo=timezone.utc)
|
||
NAIVE_BASE = datetime(2025, 1, 1, 10, 0, 0)
|
||
|
||
|
||
def _log(message, occurred_at=BASE, host_id="h-1", fault_log_id="fl-1"):
|
||
return FaultLog(
|
||
fault_log_id=fault_log_id,
|
||
host_id=host_id,
|
||
message=message,
|
||
level="ERROR",
|
||
occurred_at=occurred_at,
|
||
)
|
||
|
||
|
||
class TestAsAware(unittest.TestCase):
|
||
def test_aware_kept(self):
|
||
self.assertEqual(_as_aware(BASE), BASE)
|
||
|
||
def test_naive_becomes_utc(self):
|
||
out = _as_aware(NAIVE_BASE)
|
||
self.assertEqual(out.tzinfo, timezone.utc)
|
||
self.assertEqual(out, BASE)
|
||
|
||
def test_none(self):
|
||
self.assertIsNone(_as_aware(None))
|
||
|
||
|
||
class TestRootCauseAnalyzer(unittest.TestCase):
|
||
def setUp(self):
|
||
self.analyzer = RootCauseAnalyzer(window_minutes=5)
|
||
|
||
def test_log_hint_only(self):
|
||
rc = self.analyzer.analyze(_log("No space left on device /var"), [])
|
||
self.assertEqual(rc.cause_type, "disk_full")
|
||
self.assertEqual(rc.confidence, 0.6)
|
||
self.assertEqual(rc.fault_log_id, "fl-1")
|
||
|
||
def test_unknown_without_evidence(self):
|
||
rc = self.analyzer.analyze(_log("some unrelated message"), [])
|
||
self.assertEqual(rc.cause_type, "unknown")
|
||
self.assertEqual(rc.confidence, 0.0)
|
||
|
||
def test_matching_event_corroborates(self):
|
||
event = Event(
|
||
event_id="e-1",
|
||
host_id="h-1",
|
||
metric="disk_used_percent",
|
||
value=97.2,
|
||
threshold=90.0,
|
||
fired_at=BASE,
|
||
)
|
||
rc = self.analyzer.analyze(_log("No space left on device"), [event])
|
||
self.assertEqual(rc.cause_type, "disk_full")
|
||
# 0.6(日志) + 0.25(指标) + 0.15(互相印证) = 1.0
|
||
self.assertEqual(rc.confidence, 1.0)
|
||
self.assertTrue(any(e["type"] == "event" and e["event_id"] == "e-1" for e in rc.evidence))
|
||
|
||
def test_naive_datetime_aware_event(self):
|
||
"""naive 日志时间 + aware 事件时间混用不应抛异常(鲁棒性)。"""
|
||
event = Event(
|
||
event_id="e-1",
|
||
host_id="h-1",
|
||
metric="mem_used_percent",
|
||
value=95.0,
|
||
threshold=90.0,
|
||
fired_at=BASE,
|
||
)
|
||
rc = self.analyzer.analyze(_log("out of memory", occurred_at=NAIVE_BASE), [event])
|
||
self.assertEqual(rc.cause_type, "memory_exhausted")
|
||
self.assertEqual(rc.confidence, 1.0)
|
||
|
||
def test_naive_event_outside_window(self):
|
||
"""窗口外的 naive 事件不应被关联,也不应抛异常(鲁棒性)。"""
|
||
outside = Event(
|
||
event_id="e-1",
|
||
host_id="h-1",
|
||
metric="disk_used_percent",
|
||
value=97.2,
|
||
threshold=90.0,
|
||
fired_at=NAIVE_BASE + timedelta(hours=1),
|
||
)
|
||
rc = self.analyzer.analyze(_log("No space left on device"), [outside])
|
||
self.assertEqual(rc.cause_type, "disk_full")
|
||
self.assertEqual(rc.confidence, 0.6)
|
||
self.assertEqual(len(rc.evidence), 1) # 仅日志证据,无事件证据
|
||
|
||
def test_host_mismatch_excluded(self):
|
||
event = Event(
|
||
event_id="e-1",
|
||
host_id="h-other",
|
||
metric="disk_used_percent",
|
||
value=97.2,
|
||
threshold=90.0,
|
||
fired_at=BASE,
|
||
)
|
||
rc = self.analyzer.analyze(_log("No space left on device"), [event])
|
||
self.assertEqual(rc.cause_type, "disk_full")
|
||
self.assertEqual(rc.confidence, 0.6)
|
||
|
||
def test_event_only_cause(self):
|
||
"""日志无关键字但事件命中规则库时,以事件规则为准。"""
|
||
event = Event(
|
||
event_id="e-1",
|
||
host_id="h-1",
|
||
metric="cpu_usage",
|
||
value=99.0,
|
||
threshold=95.0,
|
||
fired_at=BASE,
|
||
)
|
||
rc = self.analyzer.analyze(_log("service degraded"), [event])
|
||
self.assertEqual(rc.cause_type, "cpu_saturation")
|
||
self.assertEqual(rc.confidence, 0.25)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
unittest.main()
|