2e86d2ce27
- read_stuck_points 最忙平台末帧损坏 → last fallthrough 到干净平台 (重构最险分支的回归测试,原只在一次性验证脚本里) - _fmt_last round 边界:500→<1s、999→停留1s - continue 注释点明与旧版 _platform_stuck→None 等价 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
214 lines
9.4 KiB
Python
214 lines
9.4 KiB
Python
"""build_hits 集成测:cancelled trace 优先/保底切换、failed 附卡点、限量。"""
|
|
import json
|
|
from pathlib import Path
|
|
|
|
from app.core.compare_alert_worker import _fmt_last, _fmt_stuck, build_hits
|
|
from app.services.trace_stuck import StuckPoint
|
|
|
|
|
|
class _Rec:
|
|
def __init__(self, **kw):
|
|
self.trace_id = kw.get("trace_id", "t")
|
|
self.status = kw.get("status", "cancelled")
|
|
self.total_ms = kw.get("total_ms")
|
|
self.step_count = kw.get("step_count")
|
|
self.fail_reason = kw.get("fail_reason")
|
|
self.information = kw.get("information")
|
|
self.app_version = kw.get("app_version")
|
|
self.created_at = kw.get("created_at")
|
|
self.trace_url = kw.get("trace_url")
|
|
self.user_id = kw.get("user_id")
|
|
|
|
|
|
def _frame(pdir: Path, idx: int, step: str, page: str) -> None:
|
|
pdir.mkdir(parents=True, exist_ok=True)
|
|
body = {"pipeline_step": step, "detected_page": page, "windows": [{"n": ["x" * 200]}]}
|
|
(pdir / f"step_{idx:03d}.json").write_text(
|
|
json.dumps(body, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
|
|
|
|
def _frame_ts(pdir: Path, idx: int, step: str, page: str, ts: str) -> None:
|
|
pdir.mkdir(parents=True, exist_ok=True)
|
|
body = {"pipeline_step": step, "detected_page": page, "timestamp": ts,
|
|
"windows": [{"n": ["x" * 200]}]}
|
|
(pdir / f"step_{idx:03d}.json").write_text(
|
|
json.dumps(body, ensure_ascii=False), encoding="utf-8"
|
|
)
|
|
|
|
|
|
_KW = dict(
|
|
stuck_threshold=15, max_tail=40, max_trace_reads=30,
|
|
cancelled_ms_threshold=90000, cancelled_step_threshold=30,
|
|
timeout_keywords=("超时",), unrecognized_keywords=("未识别",), biz_exclude_keywords=(),
|
|
)
|
|
|
|
|
|
def test_cancelled_stuck_reports_via_trace(tmp_path):
|
|
for i in range(18):
|
|
_frame(tmp_path / "20260804_x" / "meituan", i, "add_one_dish", "meal_detail_popup")
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/20260804_x/",
|
|
total_ms=5000, step_count=3) # 保底不会中,靠 trace 判卡死
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert len(hits) == 1
|
|
assert hits[0].alert_type == "T5"
|
|
assert hits[0].reason == "深度放弃"
|
|
assert "美团·加菜" in hits[0].stuck_point
|
|
assert "帧" in hits[0].stuck_point
|
|
assert "meal_detail_popup" in hits[0].stuck_point # 卡点带末帧页面
|
|
|
|
|
|
def test_cancelled_readable_not_stuck_long_duration_reports(tmp_path):
|
|
# trace 可读但没判出原地卡点:仍过耗时/步数阈值兜底,超长(>90s)照报 T5;没卡点也用末帧标"退出前在哪屏"。
|
|
# (线上 trace 几乎总可读,若不回退则 total_ms 阈值形同虚设、超长放弃永不报——见 compare-fail-alert 排查。)
|
|
p = tmp_path / "20260804_y" / "eleme"
|
|
_frame(p, 0, "set_address", "home")
|
|
for i in range(1, 6):
|
|
_frame(p, i, "enter_store", "store")
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/20260804_y/",
|
|
total_ms=95000, step_count=40)
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert len(hits) == 1
|
|
assert hits[0].alert_type == "T5"
|
|
assert hits[0].reason == "深度放弃"
|
|
assert hits[0].stuck_point == "饿了么·进店·store" # 无卡点 → 用末帧(平台·环节·页面)标退出前在哪屏
|
|
|
|
|
|
def test_cancelled_readable_not_stuck_short_no_report(tmp_path):
|
|
# trace 可读没卡点、且耗时/步数都没超阈值 → 正常早退,不报(兜底阈值把住,不误报)。
|
|
p = tmp_path / "20260804_ys" / "eleme"
|
|
_frame(p, 0, "set_address", "home")
|
|
for i in range(1, 6):
|
|
_frame(p, i, "enter_store", "store")
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/20260804_ys/",
|
|
total_ms=5000, step_count=3)
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert hits == []
|
|
|
|
|
|
def test_cancelled_unreadable_falls_back(tmp_path):
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/nope/",
|
|
total_ms=95000, step_count=3)
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert len(hits) == 1
|
|
assert hits[0].alert_type == "T5" and "深度放弃" in hits[0].reason
|
|
|
|
|
|
def test_no_work_log_dir_uses_fallback(tmp_path):
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/y/",
|
|
total_ms=95000, step_count=3)
|
|
hits = build_hits([rec], work_log_dir="", **_KW)
|
|
assert len(hits) == 1 and "深度放弃" in hits[0].reason
|
|
|
|
|
|
def test_failed_gets_stuck_point_appended(tmp_path):
|
|
for i in range(20):
|
|
_frame(tmp_path / "20260804_f" / "meituan", i, "add_one_dish", "meal_detail_popup")
|
|
rec = _Rec(status="failed", fail_reason="启动超时",
|
|
trace_url="https://x/traces/20260804_f/")
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert len(hits) == 1
|
|
assert hits[0].alert_type == "T2"
|
|
assert "美团·加菜" in hits[0].stuck_point
|
|
assert "卡在" not in hits[0].reason
|
|
|
|
|
|
def test_max_trace_reads_zero_skips_trace(tmp_path):
|
|
for i in range(18):
|
|
_frame(tmp_path / "20260804_z" / "meituan", i, "add_one_dish", "meal_detail_popup")
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/20260804_z/",
|
|
total_ms=95000, step_count=3)
|
|
kw = {**_KW, "max_trace_reads": 0}
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **kw)
|
|
# 没读 trace → 回退保底 → deep(95s) → 深度放弃
|
|
assert len(hits) == 1 and "深度放弃" in hits[0].reason
|
|
|
|
|
|
def test_shared_reads_budget_across_branches(tmp_path):
|
|
# cancelled 和 failed 共用 max_trace_reads 预算;预算=1 时 cancelled 先消耗,failed 拿不到卡点
|
|
for i in range(18):
|
|
_frame(tmp_path / "20260804_c" / "meituan", i, "add_one_dish", "meal_detail_popup")
|
|
for i in range(20):
|
|
_frame(tmp_path / "20260804_d" / "meituan", i, "add_one_dish", "meal_detail_popup")
|
|
cancelled = _Rec(status="cancelled", trace_url="https://x/traces/20260804_c/",
|
|
total_ms=5000, step_count=3)
|
|
failed = _Rec(status="failed", fail_reason="启动超时",
|
|
trace_url="https://x/traces/20260804_d/")
|
|
kw = {**_KW, "max_trace_reads": 1}
|
|
hits = build_hits([cancelled, failed], work_log_dir=str(tmp_path), **kw)
|
|
assert len(hits) == 2
|
|
# cancelled 消耗了唯一预算 → 报卡死,卡点在 stuck_point
|
|
assert hits[0].alert_type == "T5" and hits[0].stuck_point is not None
|
|
# failed 超预算 → 仍报 T2,但 stuck_point 为 None
|
|
assert hits[1].alert_type == "T2" and hits[1].stuck_point is None
|
|
|
|
|
|
# ---- _fmt_stuck 单测 ----
|
|
|
|
def test_fmt_stuck_with_ms():
|
|
sp = StuckPoint(platform="meituan", pipeline_step="add_one_dish", frames=110, stuck_ms=32000)
|
|
assert _fmt_stuck(sp) == "美团·加菜 110帧/32s"
|
|
|
|
|
|
def test_fmt_stuck_without_ms():
|
|
sp = StuckPoint(platform="meituan", pipeline_step="add_one_dish", frames=110, stuck_ms=None)
|
|
assert _fmt_stuck(sp) == "美团·加菜 110帧"
|
|
|
|
|
|
# ---- _fmt_last 单测(末帧路径:环节·页面 + 停留,不显总帧数)----
|
|
|
|
def test_fmt_last_with_dwell():
|
|
sp = StuckPoint("meituan", "checkout", frames=480,
|
|
detected_page="checkout_page", dwell_ms=8000)
|
|
assert _fmt_last(sp) == "美团·结算·checkout_page 停留8s"
|
|
|
|
|
|
def test_fmt_last_sub_second():
|
|
sp = StuckPoint("meituan", "checkout", frames=480,
|
|
detected_page="checkout_page", dwell_ms=300)
|
|
assert _fmt_last(sp) == "美团·结算·checkout_page 停留<1s"
|
|
# <1s 阈值边界:round(500/1000)=0 → <1s;round(999/1000)=1 → 停留1s
|
|
sp500 = StuckPoint("meituan", "checkout", frames=480,
|
|
detected_page="checkout_page", dwell_ms=500)
|
|
assert _fmt_last(sp500) == "美团·结算·checkout_page 停留<1s"
|
|
sp999 = StuckPoint("meituan", "checkout", frames=480,
|
|
detected_page="checkout_page", dwell_ms=999)
|
|
assert _fmt_last(sp999) == "美团·结算·checkout_page 停留1s"
|
|
|
|
|
|
def test_fmt_last_without_dwell():
|
|
sp = StuckPoint("meituan", "checkout", frames=480,
|
|
detected_page="checkout_page", dwell_ms=None)
|
|
assert _fmt_last(sp) == "美团·结算·checkout_page"
|
|
|
|
|
|
# ---- 末帧路径带 dwell 集成 ----
|
|
|
|
def test_failed_stuck_point_has_dwell(tmp_path):
|
|
# failed 末帧 5 帧都在 checkout,ts :00→:08 → stuck_point 附「停留8s」
|
|
p = tmp_path / "20260807_f" / "meituan"
|
|
for i in range(5):
|
|
_frame_ts(p, i, "checkout", "checkout_page",
|
|
f"2026-08-07T12:00:{i * 2:02d}.000000")
|
|
rec = _Rec(status="failed", fail_reason="启动超时",
|
|
trace_url="https://x/traces/20260807_f/")
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert len(hits) == 1
|
|
assert hits[0].alert_type == "T2"
|
|
assert hits[0].stuck_point == "美团·结算·checkout_page 停留8s"
|
|
|
|
|
|
def test_cancelled_fallback_stuck_point_has_dwell(tmp_path):
|
|
# cancelled 超阈值(95s)但末段 5<15 不卡死 → 兜底,末帧带 ts → 附「停留8s」
|
|
p = tmp_path / "20260807_c" / "eleme"
|
|
_frame_ts(p, 0, "set_address", "home", "2026-08-07T12:00:00.000000")
|
|
for i in range(1, 6): # enter_store 5 帧 :02→:10
|
|
_frame_ts(p, i, "enter_store", "store",
|
|
f"2026-08-07T12:00:{i * 2:02d}.000000")
|
|
rec = _Rec(status="cancelled", trace_url="https://x/traces/20260807_c/",
|
|
total_ms=95000, step_count=40)
|
|
hits = build_hits([rec], work_log_dir=str(tmp_path), **_KW)
|
|
assert len(hits) == 1
|
|
assert hits[0].alert_type == "T5"
|
|
assert hits[0].stuck_point == "饿了么·进店·store 停留8s"
|