a7e8141497
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
112 lines
3.9 KiB
Python
112 lines
3.9 KiB
Python
"""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 == [] # 抠不出字段 → 不判卡死;关键是没崩溃
|