diff --git a/crates/app-iced/src/pages/harness.rs b/crates/app-iced/src/pages/harness.rs index 55873f6..b97a376 100644 --- a/crates/app-iced/src/pages/harness.rs +++ b/crates/app-iced/src/pages/harness.rs @@ -97,15 +97,18 @@ fn should_request_capture(frames: u32, dispatched: bool, capture_mode: bool) -> /// ⇒ 这里的判据根本求值不到**。那一路由 `run()` 里的**终态不变量**兜住(跑完没图 ⇒ 红着退出); /// 而后端报错是另一回事:`run()` 直接返回 `Err`,本来就是非 0。 /// -/// 30 帧在取证窗口里约 3~4 秒(实测 ~120ms/帧),远大于正常路径「第 3 帧派发 → 第 4~5 帧落盘」。 +/// 30 帧**不换算成秒**(M100 r4 P3-1):帧间隔在不同机器/负载下量级差异极大——复审实测 2~3ms/帧, +/// 而本机 `[perf]` 日志里是 `frames=4 elapsed=1133ms` / `frames=5 elapsed=1605ms`(≈200~300ms/帧)。 +/// 所以这里只承诺「若干帧的宽限」,不写时间预算;要调就按「正常路径是第 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 +fn capture_stalled(frames: u32, written: u32) -> bool { + written == 0 && frames > CAPTURE_AT_FRAME + CAPTURE_GIVE_UP_FRAMES } struct Harness { @@ -246,8 +249,8 @@ impl Harness { let Some(path) = self.request.screenshot.clone() else { return Task::none(); }; - // 只在真的写成功时计数(U1):`writes` 若在 save 之前自增,一次落盘失败也会被 - // 记成「有图了」,而 `run()` 的终态不变量正是拿它判「有没有证据」。 + // 只在真的写成功时计数(U1):这个计数器(旧字段名叫 `writes`)若在 save **之前** + // 自增,一次落盘失败也会被记成「有图了」,而 `run()` 的终态不变量正是拿它判「有没有证据」。 match shot::save(&screenshot, &path) { Ok(summary) => { eprintln!("{}", summary.describe()); diff --git a/crates/app-iced/src/pages/harness/app.rs b/crates/app-iced/src/pages/harness/app.rs index 0076efb..2e2dea1 100644 --- a/crates/app-iced/src/pages/harness/app.rs +++ b/crates/app-iced/src/pages/harness/app.rs @@ -22,13 +22,26 @@ use crate::theme::Ui; use super::{demo_model, Harness, Message, Request, CAPTURE_POSITION}; -/// **终态判据**:取证跑完了却一张图都没落盘 ⇒ 失败。 +/// **终态判据**:取证运行跑完了却一张图都没落盘 ⇒ 失败。 /// /// 单独拎成纯函数是为了让它能被断言(`run()` 要真窗口才能跑)。它守的是 M100 r3 P2-1 那个 /// 真实缺陷:窗口在第 3 帧消失时,`window::frames()` 不再产消息 ⇒ 帧超时兜底**求值不到**, /// 而 iced_winit 对「最后一个窗口 Destroyed」直接退出 ⇒ 曾经是 **exit 0 + 没有 PNG**。 -pub(super) fn no_evidence(written: u32) -> bool { - written == 0 +/// +/// ## 为什么必须有 `capture_mode` 前提(M100 r4 P2-1) +/// +/// 不给 `--screenshot` 是文档明确支持的**交互预览模式**(「没给 `--screenshot` 就只是开窗看」): +/// 该模式 `subscription()` 直接 `Subscription::none()`,**永远不会有 `Captured`** ⇒ `written` 恒 0。 +/// 没有这个前提,用户**正常关掉预览窗**会拿到 exit 1 和一句「取证结束但没有任何图落盘」的 +/// 假消息——那是把"从未要求出图"判成"取证失败",通道的退出码就此失去区分力 +/// (真崩溃 / 真取证失败 / 正常关个预览窗全是 1)。 +/// +/// ## 为什么用计数器而不是 `path.exists()` +/// +/// `written` 是**本进程**成功落盘的次数,不会被执行前就存在的旧 PNG 骗过;换成「目标文件存在」 +/// 会在脏路径下给出「exit 0 + 上一次的旧图」。这一点写在这里,免得后来人「顺手改成 exists」。 +pub(super) fn no_evidence(capture_mode: bool, written: u32) -> bool { + capture_mode && written == 0 } /// 启动(阻塞到出图或关窗)。 @@ -209,7 +222,13 @@ pub fn run(request: Request) -> Result<(), String> { // // 所以判据从「超时」升级成「终态」:**跑完了没有图就是失败**。它比超时强,因为覆盖 // 「帧不再产生」这一类;帧超时那条保留,管「有帧但采样回不来」。 - if no_evidence(written.load(Ordering::Relaxed)) { + // + // **前提是这次真的是取证运行**(M100 r4 P2-1):不给 `--screenshot` 的交互预览模式本来 + // 就不出图,判成失败会打出假消息、并让退出码失去区分力。前提写在判据里,不写在这里。 + if no_evidence( + request.screenshot.is_some(), + written.load(Ordering::Relaxed), + ) { eprintln!("[harness] 取证结束但没有任何图落盘(窗口提前消失 / 落盘失败),按失败退出"); std::process::exit(1); } diff --git a/crates/app-iced/src/pages/harness/tests.rs b/crates/app-iced/src/pages/harness/tests.rs index fc1c0c5..730952c 100644 --- a/crates/app-iced/src/pages/harness/tests.rs +++ b/crates/app-iced/src/pages/harness/tests.rs @@ -222,23 +222,26 @@ fn the_stall_guard_fires_only_after_the_grace_window() { assert!(grace >= 10, "宽限帧数 {grace} 太小,正常路径会误触发"); } -/// **终态不变量:跑完了没有图就是失败**(M100 r3 P2-1)。 +/// **终态不变量:跑完了没有图就是失败 —— 但只在取证运行里这么判**(r3 P2-1 → r4 P2-1)。 /// -/// 抓得住:谁把 `run()` 里那条后置检查删掉/改弱(删掉之后,窗口在第 3 帧消失会让进程 -/// **exit 0 且没有 PNG** —— 复审变异实测就是这个形状,只看退出码的脚本会读到「成功 + 零证据」)。 -/// 抓不住:这条检查**有没有被走到**(窗口消失的时序要真窗口,见报告里的变异复现); -/// 也抓不住「图写到了错误的路径」(那要校验目标文件,属另一件事)。 +/// 抓得住:① 判据被改成恒 false(终态检查成摆设);② **判据丢掉 `capture_mode` 前提** +/// (r4 实测到的真缺陷:不给 `--screenshot` 的交互预览模式本来就不出图,却会被判失败 ⇒ +/// exit 1 + 一句「取证结束但没有任何图落盘」的假消息,退出码因此失去区分力)。 +/// 抓不住:`run()` 里那个**调用点**被删掉(判据还在、本测试照绿)——那一层只能靠变异实测 +/// (M3:第 3 帧改关窗 ⇒ 期望 exit 1),测试替不了。 #[test] -fn finishing_without_a_single_write_is_a_failure() { +fn finishing_without_a_single_write_is_a_failure_only_while_capturing() { use super::app::no_evidence; + // 红线(r4 P2-1):交互预览模式关窗**不是**失败 assert!( - no_evidence(0), - "一张图都没落盘时,取证结束必须判失败(不许 exit 0)" + !no_evidence(false, 0), + "交互预览模式(没给 --screenshot)从未要求出图,关窗必须 exit 0" ); - assert!(!no_evidence(1), "落过盘就算有证据"); assert!( - !no_evidence(2), - "重复落盘也算有证据(虽然不该发生,见一次性闸门)" + no_evidence(true, 0), + "取证运行一张图都没落盘 ⇒ 必须判失败(不许 exit 0)" ); + assert!(!no_evidence(true, 1), "落过盘就算有证据"); + assert!(!no_evidence(false, 1), "预览模式更不该报错"); }