From f67b24cc7b7df104a934302453da04019b0c03cf Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 3 Nov 2026 19:20:50 +0800 Subject: [PATCH] =?UTF-8?q?fix(app-iced):=20M100=20r4=20P2-1=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E7=BB=88=E6=80=81=E5=88=A4=E6=8D=AE=E5=8A=A0?= =?UTF-8?q?=E3=80=8C=E5=8F=96=E8=AF=81=E6=A8=A1=E5=BC=8F=E3=80=8D=E5=89=8D?= =?UTF-8?q?=E6=8F=90=EF=BC=8C=E4=BA=A4=E4=BA=92=E9=A2=84=E8=A7=88=E5=85=B3?= =?UTF-8?q?=E7=AA=97=E4=B8=8D=E5=86=8D=20exit=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复审在 `d298691` 的导出上实测:交互预览模式(不给 `--screenshot`)关窗会被新判据判成失败, exit 1 + 一句「取证结束但没有任何图落盘(窗口提前消失 / 落盘失败)」的**假消息**。 那是把「从未要求出图」判成「取证失败」,退出码因此失去区分力 (真崩溃 / 真取证失败 / 正常关个预览窗全是 1)。 - `no_evidence(capture_mode, written) = capture_mode && written == 0`;调用点传 `request.screenshot.is_some()`。判据里写明:交互预览模式下 `subscription` 是 `None`、 永远不会有 `Captured`,所以「没写盘」在那里不是失败。 - 注释补一条「为什么用计数器而不是 `path.exists()`」:旧 PNG 会让 exists 骗过判据。 - 断言改名 `finishing_without_a_single_write_is_a_failure_only_while_capturing`, 三条:`(false,0)=false`(这次的红线)/ `(true,0)=true` / `(true,1)=false`。 顺手 p3: - p3-1 `CAPTURE_GIVE_UP_FRAMES` 注释删掉「约 3~4 秒(~120ms/帧)」这个没测准的数字, 改成写明帧间隔量级差异(复审实测 2~3ms/帧 vs 本机 `[perf]` 的 ≈200~300ms/帧), 「要调按第 3 帧派发→第 4~5 帧落盘的结构关系调,别按秒」。 - p3-2 旧字段名 `writes` 的注释改成「这个计数器(旧字段名叫 `writes`)」;顺带把 `capture_stalled` 的形参 `writes` 改名 `written`,与字段对齐。 - p3-3 `examples/pages_demo.rs` 的 usage 漏 `--appearance`/`--passthrough`:该文件**不在本 mission scope**(scope 只有 `pages/harness*`),不扩 scope,欠账仍记在已开的 finding 里。 实测: - 交互预览(WM_CLOSE 关窗,取真实退出码):最终二进制 5/5 有效跑 exit=0、`没有任何图落盘` 行 0; 另 2 次 exit=0xC000013A(STATUS_CONTROL_C_EXIT,控制台 Ctrl 事件打断,日志停在半途)不计。 修前同场景 4/4 exit=1。 - M3 变异(第 3 帧 `window::screenshot` → `window::close`)3/3 exit=1 + 具名行(没被误伤)。 - M4 变异(`CAPTURE_GIVE_UP_FRAMES` 30→0)2/2 exit=1 走「采样超时」(没被误伤)。 - 正常取证 `--appearance=real` ×6:exit=0 / png=yes / `落盘次数=1` / `passthrough_ext=on` / 错误行 0 / sha256 `414a71fcb77e9b38…`(与 r1~r4 基线逐字节相同,像素没动)。 --- crates/app-iced/src/pages/harness.rs | 13 +++++++---- crates/app-iced/src/pages/harness/app.rs | 27 ++++++++++++++++++---- crates/app-iced/src/pages/harness/tests.rs | 25 +++++++++++--------- 3 files changed, 45 insertions(+), 20 deletions(-) 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), "预览模式更不该报错"); }