Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 51c08a8de7 | |||
| 43376abae6 | |||
| b2cca9551a |
@@ -102,10 +102,12 @@ def build_hits(
|
||||
unrecognized_keywords: tuple[str, ...],
|
||||
biz_exclude_keywords: tuple[str, ...],
|
||||
) -> list[AlertHit]:
|
||||
"""编排:cancelled 走 trace 优先(读到确认没卡则不报,读不到回退保底);failed 附卡点。
|
||||
"""编排:cancelled 先读 trace,判出原地卡点就带卡点报 T5,否则(可读没卡点/读不到)一律回退
|
||||
耗时/步数兜底——超长放弃照报(卡点列留空);failed 命中后附末段卡点。
|
||||
|
||||
trace 读取限量 max_trace_reads 次/轮;任何 trace 异常都在 trace_stuck 内部降级为
|
||||
「读不到」,cancelled 因而回退保底、failed 不附卡点,绝不影响报警发送。
|
||||
trace 读取限量 max_trace_reads 次/轮;任何 trace 异常都在 trace_stuck 内部降级为「读不到」,
|
||||
cancelled 因而走耗时兜底、failed 不附卡点,绝不影响报警发送。trace 只做「锦上添花」标注卡点,
|
||||
绝不因「可读但没判出卡点」把超长放弃吞掉(线上 trace 几乎总可读,否则 total_ms 阈值形同虚设)。
|
||||
"""
|
||||
base = Path(work_log_dir) if work_log_dir else None
|
||||
reads = 0
|
||||
@@ -120,18 +122,21 @@ def build_hits(
|
||||
td, threshold=stuck_threshold, max_tail=max_tail
|
||||
)
|
||||
reads += 1
|
||||
if res is not None and res.readable:
|
||||
if res.points:
|
||||
stuck = "、".join(_fmt_stuck(sp) for sp in res.points)
|
||||
hit = _dc_replace(make_hit(rec, "T5", "深度放弃"), stuck_point=stuck)
|
||||
else:
|
||||
hit = None # 读到且确认没卡 → 不报
|
||||
if res is not None and res.readable and res.points:
|
||||
stuck = "、".join(_fmt_stuck(sp) for sp in res.points)
|
||||
hit = _dc_replace(make_hit(rec, "T5", "深度放弃"), stuck_point=stuck)
|
||||
else:
|
||||
# trace 判出卡点 → 上面带卡点报。其余一律回退耗时/步数兜底:可读但没判出卡点、
|
||||
# 读不到、无 trace,都过 total_ms/step 阈值——超长放弃照报,不再因「trace 可读但不
|
||||
# 原地卡」把超长放弃整条吞掉(线上 trace 几乎总可读,否则耗时阈值形同虚设)。
|
||||
hit = classify_cancelled_fallback(
|
||||
rec,
|
||||
cancelled_ms_threshold=cancelled_ms_threshold,
|
||||
cancelled_step_threshold=cancelled_step_threshold,
|
||||
)
|
||||
# 没卡点但命中(超阈值放弃)→ 用末帧标「退出前在哪屏」(平台·环节·页面)
|
||||
if hit is not None and res is not None and res.last is not None:
|
||||
hit = _dc_replace(hit, stuck_point=res.last.label())
|
||||
if hit is not None:
|
||||
hits.append(hit)
|
||||
else:
|
||||
@@ -224,6 +229,8 @@ def _scan_and_alert() -> None:
|
||||
interval_min=interval_min,
|
||||
max_detail_per_type=settings.COMPARE_ALERT_MAX_DETAIL_PER_TYPE,
|
||||
max_total=settings.COMPARE_ALERT_MAX_TOTAL,
|
||||
cancelled_ms_threshold=settings.COMPARE_ALERT_CANCELLED_MS_THRESHOLD,
|
||||
cancelled_step_threshold=settings.COMPARE_ALERT_CANCELLED_STEP_THRESHOLD,
|
||||
)
|
||||
|
||||
try:
|
||||
|
||||
@@ -64,6 +64,31 @@ def classify_cancelled_fallback(
|
||||
return None
|
||||
|
||||
|
||||
def _target_technical_failure_reason(
|
||||
rec: Any, biz_exclude_keywords: tuple[str, ...]
|
||||
) -> str | None:
|
||||
"""某目标平台「真技术失败」(pricebot 原始 status=='failed' 且 reason 非业务话术)的原因;无 → None。
|
||||
|
||||
记录级 fail_reason 是「展示口径」——一条比价里若某平台是干净业务结局(京东未找到菜),它会被派生
|
||||
成 headline,盖住另一平台的真技术崩溃(淘宝'比价过程出错')。这里扫 raw_payload.platform_results
|
||||
补判:任一目标平台 status='failed' 且 reason 不含业务词(pricebot 偶把打烊/不配送漏标成 failed,
|
||||
用 biz_exclude 过滤掉这些业务误标)→ 真技术崩溃。缺 raw_payload/platform_results / 结构异常 → None。
|
||||
"""
|
||||
raw = getattr(rec, "raw_payload", None)
|
||||
pr = raw.get("platform_results") if isinstance(raw, dict) else None
|
||||
if not isinstance(pr, dict):
|
||||
return None
|
||||
for v in pr.values():
|
||||
if not isinstance(v, dict) or v.get("is_source"):
|
||||
continue
|
||||
if v.get("status") != "failed":
|
||||
continue
|
||||
reason = (v.get("reason") or "").strip()
|
||||
if not any(w in reason for w in biz_exclude_keywords):
|
||||
return reason or "比价过程出错"
|
||||
return None
|
||||
|
||||
|
||||
def classify_record(
|
||||
rec: Any,
|
||||
*,
|
||||
@@ -87,7 +112,11 @@ def classify_record(
|
||||
return make_hit(rec, "T6", f"识别失败·{fail_reason[:80]}")
|
||||
if any(w in fail_reason for w in timeout_keywords):
|
||||
return make_hit(rec, "T2", fail_reason[:80])
|
||||
return None
|
||||
# fail_reason 是干净业务 headline,但可能盖住某目标平台的真技术失败(比价过程出错)→ 补判 T1。
|
||||
tech = _target_technical_failure_reason(rec, biz_exclude_keywords)
|
||||
if tech:
|
||||
return make_hit(rec, "T1", f"技术失败·{tech[:80]}")
|
||||
return None # 纯业务失败,不报
|
||||
if status == "cancelled":
|
||||
return classify_cancelled_fallback(
|
||||
rec,
|
||||
|
||||
@@ -180,12 +180,26 @@ _TABLE_COLUMNS = [
|
||||
{"name": "phone", "display_name": "手机号", "data_type": "text"},
|
||||
{"name": "cost", "display_name": "用时", "data_type": "text"},
|
||||
{"name": "reason", "display_name": "失败原因", "data_type": "text"},
|
||||
{"name": "stuck", "display_name": "卡点", "data_type": "text"},
|
||||
{"name": "stuck", "display_name": "末帧", "data_type": "text"},
|
||||
{"name": "ver", "display_name": "版本", "data_type": "text"},
|
||||
{"name": "trace", "display_name": "trace", "data_type": "lark_md"},
|
||||
]
|
||||
|
||||
|
||||
def _type_criteria(t: str, cancelled_ms_threshold: int, cancelled_step_threshold: int) -> str:
|
||||
"""各触发类型的「判据」文案,展示在卡片摘要里让收报警的人一眼知道为什么报。
|
||||
T5 带当前配置阈值(耗时 ms→s、步数);T1 覆盖两条来源(整场系统错 + 混合单里任一平台 status=failed)。"""
|
||||
if t == "T5":
|
||||
return f"耗时>{round(cancelled_ms_threshold / 1000)}s 或 步数>{cancelled_step_threshold}"
|
||||
if t == "T1":
|
||||
return "无业务原因的系统错 或 任一平台 status=failed"
|
||||
if t == "T2":
|
||||
return "原因含 超时/启动/加载"
|
||||
if t == "T6":
|
||||
return "原因含 未识别"
|
||||
return ""
|
||||
|
||||
|
||||
def format_alert_card(
|
||||
hits: list[AlertHit],
|
||||
*,
|
||||
@@ -194,6 +208,8 @@ def format_alert_card(
|
||||
interval_min: int,
|
||||
max_detail_per_type: int,
|
||||
max_total: int,
|
||||
cancelled_ms_threshold: int = 90000,
|
||||
cancelled_step_threshold: int = 30,
|
||||
) -> dict:
|
||||
"""返回飞书 schema 2.0 卡片 dict(配合 send_feishu_card 发送)。
|
||||
|
||||
@@ -225,6 +241,15 @@ def format_alert_card(
|
||||
f"**合计 {total} 条**:" + " | ".join(count_parts)
|
||||
)
|
||||
|
||||
# ---------- 判据说明(本期出现的类型各给一行判据,T5 带当前配置阈值)----------
|
||||
criteria_parts = [
|
||||
f"{ALERT_TYPE_LABELS[t]}={_type_criteria(t, cancelled_ms_threshold, cancelled_step_threshold)}"
|
||||
for t in _TYPE_ORDER
|
||||
if grouped_count.get(t)
|
||||
]
|
||||
if criteria_parts:
|
||||
md_content += "\n判据:" + " | ".join(criteria_parts)
|
||||
|
||||
# ---------- 截断:超 max_total 只出摘要 ----------
|
||||
if total > max_total:
|
||||
md_content += f"\n超 {max_total} 条仅列计数,明细见分析库 comparison_record"
|
||||
|
||||
@@ -46,17 +46,22 @@ class StuckPoint:
|
||||
pipeline_step: str
|
||||
frames: int # 末段连续困住的帧数(上限 max_tail)
|
||||
stuck_ms: int | None = None # 末段连续卡住的时长(ms);无 timestamp 时 None
|
||||
detected_page: str | None = None # 末帧所在页面(pricebot detected_page 原值);无则 None
|
||||
|
||||
def label(self) -> str:
|
||||
p = PLATFORM_LABELS.get(self.platform, self.platform)
|
||||
s = PIPELINE_STEP_LABELS.get(self.pipeline_step, self.pipeline_step)
|
||||
return f"{p}·{s}"
|
||||
base = f"{p}·{s}"
|
||||
if self.detected_page:
|
||||
base += f"·{self.detected_page}" # 页面暂用 pricebot 原值(英文),无中文映射
|
||||
return base
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class StuckResult:
|
||||
readable: bool # trace 是否读到(区分「读不到」与「读到但没卡」)
|
||||
points: list[StuckPoint] # 卡死平台列表;readable=True 且空 = 确认没卡死
|
||||
last: StuckPoint | None = None # 末帧(帧数最多平台的末帧,带 detected_page);读不到 → None
|
||||
|
||||
|
||||
def dir_name_from_trace_url(trace_url: str | None) -> str | None:
|
||||
@@ -108,7 +113,7 @@ def _platform_stuck(
|
||||
stuck_ms = round((t_last - t_first).total_seconds() * 1000) if t_last and t_first else None
|
||||
if stuck_ms is not None and stuck_ms < 0:
|
||||
stuck_ms = None # 时钟不单调(帧 timestamp 回退)→ 降级为不显示时长
|
||||
return StuckPoint(platform, last_ps, count, stuck_ms)
|
||||
return StuckPoint(platform, last_ps, count, stuck_ms, detected_page=last_pg)
|
||||
|
||||
|
||||
def read_stuck_points(trace_dir: Path, *, threshold: int, max_tail: int) -> StuckResult:
|
||||
@@ -118,6 +123,8 @@ def read_stuck_points(trace_dir: Path, *, threshold: int, max_tail: int) -> Stuc
|
||||
return StuckResult(readable=False, points=[])
|
||||
points: list[StuckPoint] = []
|
||||
any_frames = False
|
||||
last: StuckPoint | None = None
|
||||
best_n = -1
|
||||
for pdir in sorted(trace_dir.iterdir()):
|
||||
if not pdir.is_dir():
|
||||
continue
|
||||
@@ -128,9 +135,15 @@ def read_stuck_points(trace_dir: Path, *, threshold: int, max_tail: int) -> Stuc
|
||||
sp = _platform_stuck(pdir.name, step_files, threshold, max_tail)
|
||||
if sp is not None:
|
||||
points.append(sp)
|
||||
# 末帧:取帧数最多平台的末帧(与 last_step 同口径),带 detected_page
|
||||
if len(step_files) > best_n:
|
||||
ps, pg, _ = _read_head(step_files[-1])
|
||||
if ps is not None:
|
||||
best_n = len(step_files)
|
||||
last = StuckPoint(pdir.name, ps, len(step_files), detected_page=pg)
|
||||
if not any_frames:
|
||||
return StuckResult(readable=False, points=[])
|
||||
return StuckResult(readable=True, points=points)
|
||||
return StuckResult(readable=True, points=points, last=last)
|
||||
except OSError:
|
||||
return StuckResult(readable=False, points=[])
|
||||
|
||||
@@ -151,9 +164,9 @@ def last_step(trace_dir: Path) -> StuckPoint | None:
|
||||
if best is None:
|
||||
return None
|
||||
_, platform, step_files = best
|
||||
ps, _pg, _ts = _read_head(step_files[-1])
|
||||
ps, pg, _ts = _read_head(step_files[-1])
|
||||
if ps is None:
|
||||
return None
|
||||
return StuckPoint(platform, ps, len(step_files)) # stuck_ms=None(failed 不算时长)
|
||||
return StuckPoint(platform, ps, len(step_files), detected_page=pg) # stuck_ms=None(failed 不算时长)
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
@@ -97,14 +97,19 @@ def last_step(trace_dir: Path) -> StuckPoint | None:
|
||||
```
|
||||
worker 对 cancelled 候选:
|
||||
res = read_stuck_points(dir)
|
||||
if not res.readable: # 读不到 trace(目录被清/生产一时读不到)→ 回退保底
|
||||
>90s或>30步 → T5「深度放弃·等待Xs/Y步」; 否则不报
|
||||
elif res.points: # 读到且有卡死平台 → 报卡死
|
||||
报 T5, reason = "卡在 " + "、".join(f"{平台}·{环节}" for res.points)
|
||||
else: # 读到且没卡死(末段在推进 = 正常深度使用后退出)→ 不报
|
||||
不报
|
||||
if res.readable and res.points: # 读到且有卡死平台 → 报卡死(带卡点环节)
|
||||
报 T5, reason="深度放弃", stuck_point = "、".join(f"{平台}·{环节}" for res.points)
|
||||
else: # 其余一律回退耗时/步数兜底(见下方 2026-08-06 修订)
|
||||
>90s或>30步 → T5「深度放弃」(卡点列留空); 否则不报
|
||||
```
|
||||
|
||||
> **2026-08-06 修订(compare-fail-alert 排查)**:原设计「读到且没卡死 → 不报」在线上是死路——
|
||||
> **线上 trace 几乎总可读**(WORK_LOG_DIR 已配、同机直读),于是耗时兜底那条分支基本永不触发,
|
||||
> `total_ms>90s` 阈值形同虚设,**超长放弃(实测 113s / 516s)一条都报不出来**。改为:只有「判出卡点」
|
||||
> 独占带卡点的 T5;**其余(可读没卡点 / 读不到 / 无 trace)一律回退耗时兜底**,超长照报(卡点留空)。
|
||||
> 权衡:这会重新引入第 1 节「误报」——用户正常浏览 90s+ 后退出也会报。若噪音大,调高
|
||||
> `COMPARE_ALERT_CANCELLED_MS_THRESHOLD`(如 180s/300s)收敛,不动代码。
|
||||
|
||||
### 6.2 failed(T1/T2/T6,判定不变 + 附卡点)
|
||||
|
||||
```
|
||||
|
||||
@@ -282,8 +282,8 @@ def test_card_has_table_seven_columns():
|
||||
cols = table["columns"]
|
||||
assert len(cols) == 7
|
||||
display_names = [c["display_name"] for c in cols]
|
||||
assert display_names == ["时间", "手机号", "用时", "失败原因", "卡点", "版本", "trace"]
|
||||
# 「卡点」列在「失败原因」后、「版本」前
|
||||
assert display_names == ["时间", "手机号", "用时", "失败原因", "末帧", "版本", "trace"]
|
||||
# 「末帧」列在「失败原因」后、「版本」前
|
||||
names = [c["name"] for c in cols]
|
||||
reason_idx = names.index("reason")
|
||||
stuck_idx = names.index("stuck")
|
||||
@@ -570,3 +570,29 @@ def test_card_stuck_point_shown_in_row():
|
||||
)
|
||||
row_without = card_without["body"]["elements"][1]["rows"][0]
|
||||
assert row_without["stuck"] == "-"
|
||||
|
||||
|
||||
def test_card_shows_criteria_legend_with_config_thresholds():
|
||||
"""卡片摘要含「判据」说明:深度放弃带当前配置阈值、技术失败说明任一平台 status=failed。"""
|
||||
hits = _card_hits(1, "T5", total_ms=95000) + _card_hits(1, "T1")
|
||||
card = format_alert_card(
|
||||
hits, window_label="w", phone_map={}, interval_min=15,
|
||||
max_detail_per_type=20, max_total=50,
|
||||
cancelled_ms_threshold=90000, cancelled_step_threshold=30,
|
||||
)
|
||||
md = card["body"]["elements"][0]["content"]
|
||||
assert "判据" in md
|
||||
assert "耗时>90s 或 步数>30" in md # 深度放弃判据带配置值
|
||||
assert "任一平台 status=failed" in md # 技术失败判据
|
||||
|
||||
|
||||
def test_card_criteria_reflects_custom_thresholds():
|
||||
"""判据里的阈值随配置变化(不是写死 90/30)。"""
|
||||
card = format_alert_card(
|
||||
_card_hits(1, "T5", total_ms=200000),
|
||||
window_label="w", phone_map={}, interval_min=15,
|
||||
max_detail_per_type=20, max_total=50,
|
||||
cancelled_ms_threshold=180000, cancelled_step_threshold=45,
|
||||
)
|
||||
md = card["body"]["elements"][0]["content"]
|
||||
assert "耗时>180s 或 步数>45" in md
|
||||
|
||||
@@ -59,3 +59,34 @@ def test_reason_texts():
|
||||
assert t1_empty.reason == "技术失败·比价过程出错"
|
||||
t5 = classify_record(_rec(status="cancelled", total_ms=98000, step_count=26), **KW)
|
||||
assert t5.reason == "深度放弃"
|
||||
|
||||
|
||||
# ---- failed 混合单:业务 headline 盖住某平台真技术失败(线上 trace 20260806_144544)----
|
||||
|
||||
def test_failed_business_headline_masks_target_technical_failure_reports_t1():
|
||||
# fail_reason 被派生成京东业务原因(未找到),却盖住淘宝 status=failed 的真技术崩溃 → 补判 T1。
|
||||
rec = _rec(
|
||||
status="failed", fail_reason="京东此店内未找到这些菜品",
|
||||
information="比价过程出错,请稍后重试",
|
||||
raw_payload={"platform_results": {
|
||||
"meituan": {"status": "source", "is_source": True},
|
||||
"jd_waimai": {"status": "items_not_found", "reason": "京东此店内未找到这些菜品", "is_source": False},
|
||||
"taobao_flash": {"status": "failed", "reason": "比价过程出错,请稍后重试", "is_source": False},
|
||||
}},
|
||||
)
|
||||
hit = classify_record(rec, **KW)
|
||||
assert hit is not None
|
||||
assert hit.alert_type == "T1"
|
||||
|
||||
|
||||
def test_failed_target_failed_but_business_reason_no_false_positive():
|
||||
# pricebot 把打烊漏标成 status=failed,但 reason 是业务话术 → 不算技术崩溃,不报(不误报)。
|
||||
rec = _rec(
|
||||
status="failed", fail_reason="京东此店内未找到这些菜品",
|
||||
information="比价过程出错,请稍后重试",
|
||||
raw_payload={"platform_results": {
|
||||
"jd_waimai": {"status": "items_not_found", "reason": "京东此店内未找到这些菜品", "is_source": False},
|
||||
"taobao_flash": {"status": "failed", "reason": "门店已打烊,无法比价", "is_source": False},
|
||||
}},
|
||||
)
|
||||
assert classify_record(rec, **KW) is None
|
||||
|
||||
@@ -46,10 +46,12 @@ def test_cancelled_stuck_reports_via_trace(tmp_path):
|
||||
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_no_report(tmp_path):
|
||||
# trace 确认没卡(在推进);即便 total_ms/step 超阈值也不报(信 trace,不回退保底)
|
||||
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):
|
||||
@@ -57,6 +59,21 @@ def test_cancelled_readable_not_stuck_no_report(tmp_path):
|
||||
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 == []
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ def test_stuck_when_tail_repeats_same_step(tmp_path):
|
||||
_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)]
|
||||
assert res.points == [StuckPoint("meituan", "add_one_dish", 20, detected_page="meal_detail_popup")]
|
||||
|
||||
|
||||
def test_not_stuck_when_progressing(tmp_path):
|
||||
@@ -81,7 +81,7 @@ def test_per_platform_one_stuck_one_normal(tmp_path):
|
||||
_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)]
|
||||
assert res.points == [StuckPoint("meituan", "add_one_dish", 18, detected_page="meal_detail_popup")]
|
||||
|
||||
|
||||
def test_last_step_returns_busiest_platform_last_env(tmp_path):
|
||||
@@ -92,7 +92,27 @@ def test_last_step_returns_busiest_platform_last_env(tmp_path):
|
||||
for i in range(3):
|
||||
_frame(e, i, "enter_store", "store")
|
||||
sp = last_step(tmp_path)
|
||||
assert sp == StuckPoint("meituan", "add_one_dish", 20)
|
||||
assert sp == StuckPoint("meituan", "add_one_dish", 20, detected_page="meal_detail_popup")
|
||||
|
||||
|
||||
def test_stuck_point_label_includes_page():
|
||||
# label 带页面(平台/环节中文 + 页面原始英文);无页面时只到环节
|
||||
assert StuckPoint("meituan", "add_one_dish", 20, detected_page="meal_detail_popup").label() \
|
||||
== "美团·加菜·meal_detail_popup"
|
||||
assert StuckPoint("meituan", "add_one_dish", 20).label() == "美团·加菜"
|
||||
|
||||
|
||||
def test_read_stuck_points_returns_last_frame(tmp_path):
|
||||
# res.last = 帧数最多平台的末帧(带 detected_page),供"退出前在哪屏"用
|
||||
m = tmp_path / "meituan"
|
||||
for i in range(6):
|
||||
_frame(m, i, "enter_store", "store")
|
||||
_frame(m, 6, "add_one_dish", "menu") # 末帧换到 menu
|
||||
e = tmp_path / "eleme"
|
||||
_frame(e, 0, "set_address", "home")
|
||||
res = read_stuck_points(tmp_path, threshold=15, max_tail=40)
|
||||
assert res.last == StuckPoint("meituan", "add_one_dish", 7, detected_page="menu")
|
||||
assert res.points == [] # 没卡死,但末帧照样有
|
||||
|
||||
|
||||
def test_dir_name_from_trace_url():
|
||||
|
||||
Reference in New Issue
Block a user