"""trace_stuck 单测:用 tmp 造 step_*.json(只含头部字段)验证卡死判据。""" import json from pathlib import Path from app.services.trace_stuck import ( StuckPoint, dir_name_from_trace_url, last_step, read_stuck_points, ) def _frame(pdir: Path, idx: int, step: str, page: str) -> None: """造一帧 step json:头部放 pipeline_step/detected_page,尾部塞大 windows 模拟真实。""" pdir.mkdir(parents=True, exist_ok=True) body = { "trace_id": "t", "step": idx, "platform": pdir.name, "pipeline_step": step, "detected_page": page, "windows": [{"nodes": ["x" * 200]}], } (pdir / f"step_{idx:03d}.json").write_text( json.dumps(body, ensure_ascii=False), encoding="utf-8" ) def test_stuck_when_tail_repeats_same_step(tmp_path): pdir = tmp_path / "meituan" for i in range(20): _frame(pdir, i, "add_one_dish", "meal_detail_popup") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert res.readable is True assert res.points == [StuckPoint("meituan", "add_one_dish", 20)] def test_not_stuck_when_progressing(tmp_path): pdir = tmp_path / "eleme" _frame(pdir, 0, "set_address", "home") for i in range(1, 8): _frame(pdir, i, "enter_store", "store") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert res.readable is True assert res.points == [] def test_adding_many_dishes_not_stuck_when_page_changes(tmp_path): # add_one_dish 重复但 detected_page 在跳(换菜)=推进,不判卡死 pdir = tmp_path / "meituan" for i in range(20): _frame(pdir, i, "add_one_dish", "menu" if i % 2 == 0 else "dish_popup") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert res.points == [] def test_below_threshold_not_stuck(tmp_path): pdir = tmp_path / "meituan" for i in range(10): # < 15 _frame(pdir, i, "add_one_dish", "meal_detail_popup") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert res.points == [] def test_missing_dir_not_readable(tmp_path): res = read_stuck_points(tmp_path / "nope", threshold=15, max_tail=40) assert res.readable is False assert res.points == [] def test_empty_dir_no_platform_frames_not_readable(tmp_path): (tmp_path / "emptysub").mkdir() res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert res.readable is False def test_per_platform_one_stuck_one_normal(tmp_path): m = tmp_path / "meituan" for i in range(18): _frame(m, i, "add_one_dish", "meal_detail_popup") e = tmp_path / "eleme" _frame(e, 0, "set_address", "home") for i in range(1, 6): _frame(e, i, "enter_store", "store") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert res.readable is True assert res.points == [StuckPoint("meituan", "add_one_dish", 18)] def test_last_step_returns_busiest_platform_last_env(tmp_path): m = tmp_path / "meituan" for i in range(20): _frame(m, i, "add_one_dish", "meal_detail_popup") e = tmp_path / "eleme" for i in range(3): _frame(e, i, "enter_store", "store") sp = last_step(tmp_path) assert sp == StuckPoint("meituan", "add_one_dish", 20) def test_dir_name_from_trace_url(): assert dir_name_from_trace_url("https://x/traces/20260804_1_abc/") == "20260804_1_abc" assert dir_name_from_trace_url("https://x/traces/20260804_1_abc") == "20260804_1_abc" assert dir_name_from_trace_url("") is None assert dir_name_from_trace_url(None) is None def test_corrupt_frame_does_not_crash(tmp_path): # 截断的 UTF-8(pricebot 被 SIGTERM 打断的末帧)不应抛 UnicodeDecodeError pdir = tmp_path / "meituan" pdir.mkdir() (pdir / "step_000.json").write_bytes(b'{"pipeline_step": "add\xff') res = read_stuck_points(tmp_path, threshold=1, max_tail=40) assert res.points == [] # 抠不出字段 → 不判卡死;关键是没崩溃 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" ) def test_stuck_ms_computed_from_timestamps(tmp_path): # 末段 16 帧都卡在 add_one_dish,timestamp 从 :00 到 :30(每帧+2s) → 卡住 30s pdir = tmp_path / "meituan" for i in range(16): _frame_ts(pdir, i, "add_one_dish", "meal_detail_popup", f"2026-08-04T12:00:{i * 2:02d}.000000") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert len(res.points) == 1 sp = res.points[0] assert sp.frames == 16 assert sp.stuck_ms == 30000 # (第15帧:30 - 第0帧:00) = 30s def test_stuck_ms_none_when_timestamp_missing(tmp_path): # 帧无 timestamp 字段 → stuck_ms 为 None(不报错) pdir = tmp_path / "meituan" for i in range(16): _frame(pdir, i, "add_one_dish", "meal_detail_popup") # 现有 _frame,无 timestamp res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert len(res.points) == 1 assert res.points[0].stuck_ms is None def test_stuck_ms_none_when_clock_goes_backwards(tmp_path): # 帧 timestamp 非单调(时钟回退):step_000=:30 ... step_015=:00 → 末帧早于段首 → 负时长 → 降级 None pdir = tmp_path / "meituan" for i in range(16): _frame_ts(pdir, i, "add_one_dish", "meal_detail_popup", f"2026-08-04T12:00:{(30 - i * 2):02d}.000000") res = read_stuck_points(tmp_path, threshold=15, max_tail=40) assert len(res.points) == 1 assert res.points[0].stuck_ms is None # 负时长降级为 None