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

+29
View File
@@ -84,6 +84,24 @@ fn should_request_capture(frames: u32, dispatched: bool, capture_mode: bool) ->
capture_mode && !dispatched && frames >= CAPTURE_AT_FRAME
}
/// 采样「派发了却回不来」的宽限帧数(M100 r2 p3-b)。
///
/// 为什么需要它:一次性闸门(上面那条)把「每帧重试」换成了「只试一次」,于是若
/// `window::screenshot` 的 Task **根本产不出 `Captured`**(窗口消失 / 后端报错),harness
/// 既不再采样也不 `exit()` ⇒ CI 上表现成**超时**,而不是红灯。超时会被当成基础设施抖动重跑
/// 过去,红才会被人看 —— 所以宁可在超过宽限后**具名红着退出**。
///
/// 30 帧在取证窗口里约 3~4 秒(实测 ~120ms/帧),远大于正常路径「第 3 帧派发 → 第 4~5 帧落盘」。
const CAPTURE_GIVE_UP_FRAMES: u32 = 30;
/// 采样是不是**卡住了**:过了宽限帧数,一次都没落盘。
///
/// 不区分「一直没派发」(窗口始终没就绪)与「派发了没回来」——两者对使用者的表现都是
/// 「没有图」,都该红着退出而不是挂死。
fn capture_stalled(frames: u32, writes: u32) -> bool {
writes == 0 && frames > CAPTURE_AT_FRAME + CAPTURE_GIVE_UP_FRAMES
}
struct Harness {
request: Request,
ui: Ui,
@@ -180,6 +198,17 @@ impl Harness {
self.first_frame = Some(self.started.elapsed());
}
self.frames += 1;
// 兜底(M100 r2 p3-b):采样派发了却回不来时,不许挂死也不许 exit 0。
// 这里用 `std::process::exit(1)` 而不是 `iced::exit()`:iced 0.14 没有「带退出码
// 结束」的入口,`exit()` 恒 0 —— 而这条的全部意义就是让 CI 看到**红**而不是超时。
if capture_stalled(self.frames, self.writes) {
eprintln!(
"[harness] 采样超时:已过 CAPTURE_AT_FRAME+{CAPTURE_GIVE_UP_FRAMES} 帧\
(当前 {} 帧)仍未收到 Captured,放弃并红着退出",
self.frames
);
std::process::exit(1);
}
// **一次一采样**(M100 r1 P2-1):以前这里是 `frames >= CAPTURE_AT_FRAME`,于是第 3 帧
// 之后**每一帧**都发一次 `window::screenshot`,而 `Captured` 每次都覆盖同一文件才
// `exit()`——已派发的采样不会因为 exit 被撤回 ⇒ **最后写入者说了算**。复审实测 12 次里
+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} 太小,正常路径会误触发");
}