Files
shaguabijia-app-server/tests/test_trace_stuck.py
T
2026-08-05 16:33:30 +08:00

155 lines
5.7 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 == [] # 抠不出字段 → 不判卡死;关键是没崩溃
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