96 lines
3.6 KiB
Python
96 lines
3.6 KiB
Python
from __future__ import annotations
|
|
|
|
import sys
|
|
import unittest
|
|
from contextlib import contextmanager
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
from unittest.mock import patch
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
|
|
import core.toolfail as tf # noqa: E402
|
|
|
|
NOW = datetime.now(timezone.utc)
|
|
|
|
|
|
class _FakeSession:
|
|
"""按 SQL 关键字分流两段查询:messages 段与 usage_events(tool_malformed)段。"""
|
|
|
|
def __init__(self, msg_rows, malformed_rows):
|
|
self._msg_rows = msg_rows
|
|
self._malformed_rows = malformed_rows
|
|
|
|
def execute(self, clause, params=None):
|
|
sql = str(clause)
|
|
rows = self._malformed_rows if "tool_malformed" in sql else self._msg_rows
|
|
return SimpleNamespace(fetchall=lambda: rows)
|
|
|
|
|
|
def _scan(msg_rows, malformed_rows, **kw):
|
|
@contextmanager
|
|
def fake_scope():
|
|
yield _FakeSession(msg_rows, malformed_rows)
|
|
|
|
with patch.object(tf, "session_scope", fake_scope):
|
|
return tf.scan_tool_failures(**kw)
|
|
|
|
|
|
class TestToolfailMalformed(unittest.TestCase):
|
|
def test_malformed_rows_cluster(self):
|
|
"""usage_events 的 tool_malformed 行聚成 kind=malformed 的 cluster。"""
|
|
err = "Expecting value: line 1 column 1 (char 0)"
|
|
rows = [
|
|
("t1", "u1", NOW - timedelta(hours=1), "write", err, "].cells[1]", '"}'),
|
|
("t2", "u1", NOW - timedelta(hours=2), "write", err, "].merge(", '"}'),
|
|
]
|
|
out = _scan([], rows, days=7, min_count=2, min_tasks=2)
|
|
self.assertEqual(len(out), 1)
|
|
c = out[0]
|
|
self.assertEqual(c["tool"], "write")
|
|
self.assertEqual(c["kind"], "malformed")
|
|
self.assertEqual(c["count"], 2)
|
|
self.assertEqual(c["count_24h"], 2)
|
|
self.assertEqual(c["task_count"], 2)
|
|
self.assertIn("…", c["sample"]) # sample = head … tail
|
|
|
|
def test_err_signature_normalized(self):
|
|
"""报错里的行号/列号数字被归一,不同位置的同类报错聚成一条。"""
|
|
rows = [
|
|
("t1", "u1", NOW, "write", "Expecting ',' delimiter: line 1 column 57 (char 56)", "h", "t"),
|
|
("t2", "u2", NOW, "write", "Expecting ',' delimiter: line 3 column 9 (char 88)", "h", "t"),
|
|
]
|
|
out = _scan([], rows, days=7, min_count=2, min_tasks=2)
|
|
self.assertEqual(len(out), 1)
|
|
|
|
def test_merges_with_message_clusters(self):
|
|
"""两段数据源并存互不干扰:messages 报错与 malformed 各出各的 cluster。"""
|
|
msg_rows = [
|
|
("t1", "u1", NOW, "glob", "[Error] base path not found"),
|
|
("t2", "u2", NOW, "glob", "[Error] base path not found"),
|
|
]
|
|
mal_rows = [
|
|
("t3", "u1", NOW, "write", "Expecting value", "h", "t"),
|
|
("t4", "u2", NOW, "write", "Expecting value", "h", "t"),
|
|
]
|
|
out = _scan(msg_rows, mal_rows, days=7, min_count=2, min_tasks=2)
|
|
kinds = sorted(c["kind"] for c in out)
|
|
self.assertEqual(kinds, ["error", "malformed"])
|
|
|
|
def test_quiet_cluster_daily_bucket(self):
|
|
"""3 天前的畸形记录:count_24h=0(已安静),落在对应历史桶。"""
|
|
rows = [
|
|
("t1", "u1", NOW - timedelta(days=3, hours=1), "write", "Expecting value", "h", "t"),
|
|
("t2", "u2", NOW - timedelta(days=3, hours=2), "write", "Expecting value", "h", "t"),
|
|
]
|
|
out = _scan([], rows, days=7, min_count=2, min_tasks=2)
|
|
c = out[0]
|
|
self.assertEqual(c["count_24h"], 0)
|
|
self.assertEqual(sum(c["daily"]), 2)
|
|
self.assertEqual(c["daily"][-4], 2) # 尾桶=近24h,往前退 3 桶
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|