fix(app-iced): M100 r2 p3-b —— 采样回不来时红着退出,不再挂死

r1 的一次性闸门把「每帧重试」换成了「只试一次」,于是若 `window::screenshot` 的 Task 根本
产不出 `Captured`(窗口消失 / 后端报错),harness 既不再采样也不 `exit()` ⇒ CI 上表现成
**超时**而不是红灯。超时会被当成基础设施抖动重跑过去,红才会被人看。

- 新增 `CAPTURE_GIVE_UP_FRAMES = 30`(实测 ~120ms/帧 ⇒ 约 3~4 秒,远大于正常路径
  「第 3 帧派发 → 第 4~5 帧落盘」)。
- 新增纯判据 `capture_stalled(frames, writes) = writes == 0 && frames > CAPTURE_AT_FRAME + N`:
  不区分「一直没派发」与「派发了没回来」——两者对使用者的表现都是「没有图」,都该红着退出。
- 触发时打一行具名错误并用 `std::process::exit(1)` 退出:iced 0.14 没有「带退出码结束」的入口
  (`iced::exit()` 恒 0),而这条的全部意义就是让 CI 看到红。

实测:
- 正常路径 ×5:exit 0、`落盘次数=1`、`passthrough_ext=on`、sha `0d7b4f5b`,`采样超时` 行 0 次。
- 变异(N 改 0):两条新单测红(399 passed / 2 failed),真二进制跑出
  `[harness] 采样超时:已过 CAPTURE_AT_FRAME+0 帧(当前 4 帧)仍未收到 Captured,放弃并红着退出`
  且 **exit=1**(×3)。改回 30 后单测 401 passed、二进制 exit 0。
This commit is contained in:
AskaEth committed 2026-11-03 16:23:13 +08:00
1 parent 839a142db0
commit 95530a5311
2 files changed
+80 -1

No files matched your search

+51 -1
View File
@@ -5,7 +5,7 @@
//! 来自 `GWL_EXSTYLE` 的 `WS_EX_TRANSPARENT`);`指针事件=N` 只是辅助指标(它只证明消息到没到)。
use super::request::Request;
use super::should_request_capture;
use super::{capture_stalled, should_request_capture, CAPTURE_AT_FRAME, CAPTURE_GIVE_UP_FRAMES};
use crate::cli::Backend;
use crate::pages::Screen;
@@ -171,3 +171,53 @@ fn the_passthrough_capability_is_three_states() {
"句柄 0 = 还没拿到窗口"
);
}
/// **兜底不许在正常路径误触发**(M100 r2 p3-b)。
///
/// 抓得住:宽限帧数被改小到「采样还在飞行中就判死」(例如把它写成 0)——那时第 4 帧
/// (第 3 帧派发、第 5 帧才落盘)就会误触发,CI 上表现为「正常出图却红」。
/// 抓不住:真实 `window::screenshot` 卡死的时序(那要注入一个不回的采样,测试替不了)。
#[test]
fn the_stall_guard_never_fires_on_the_normal_path() {
// 实测时序:第 3 帧派发,第 4~5 帧才收到 Captured(采样在飞行中 writes 还是 0)
let mut first_bad: Option<(u32, bool, u32)> = None;
for frames in 1..=200u32 {
let dispatched = frames >= CAPTURE_AT_FRAME;
let writes = u32::from(dispatched && frames >= CAPTURE_AT_FRAME + 2);
if capture_stalled(frames, writes) {
first_bad.get_or_insert((frames, dispatched, writes));
}
}
assert!(
first_bad.is_none(),
"正常路径误触发兜底(帧, dispatched, writes)={first_bad:?}"
);
}
/// **兜底只在「有帧但没采样」时触发**,且必须过了宽限窗口才触发。
///
/// 抓得住:① 判据写成 `frames >= CAPTURE_AT_FRAME`(宽限为 0)——正常路径会立刻误触发;
/// ② 判据漏掉 `writes == 0`(已经落过盘还报超时);
/// ③ 宽限窗口被写成 `>=` 边界错位(差一帧)。
/// 抓不住:`exit(1)` 到底有没有被操作系统收到(那要跑真二进制,见报告里的变异实测)。
#[test]
fn the_stall_guard_fires_only_after_the_grace_window() {
let threshold = CAPTURE_AT_FRAME + CAPTURE_GIVE_UP_FRAMES;
assert!(
!capture_stalled(threshold, 0),
"刚好到阈值还不算超时(要给最后一帧留余量)"
);
assert!(
capture_stalled(threshold + 1, 0),
"越过阈值且一次都没落盘 ⇒ 必须放弃(红着退出,不许挂死)"
);
assert!(
!capture_stalled(u32::MAX, 1),
"已经落过盘就不算卡住(正常路径由 exit() 结束)"
);
assert!(!capture_stalled(1, 0), "前几帧本来就没有图,不算卡住");
// 宽限必须明显大于「派发→落盘」的实测间隔(约 2 帧),否则正常路径会误触发。
// 用局部绑定而不是直接比较常量:`assert!(CONST >= 10)` 会被 clippy 判成常量断言。
let grace = CAPTURE_GIVE_UP_FRAMES;
assert!(grace >= 10, "宽限帧数 {grace} 太小,正常路径会误触发");
}