From c166944c8e519d86ac3d8e7291c2b48e2d19ed41 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Tue, 3 Nov 2026 22:48:05 +0800 Subject: [PATCH] =?UTF-8?q?docs(app-iced):=20M101=20r3=20=E2=80=94?= =?UTF-8?q?=E2=80=94=20=E8=A1=A5=E3=80=8C=E9=A1=B5=E5=A4=96=E5=80=BC=20/?= =?UTF-8?q?=20=E9=A6=96=E5=B8=A7=E5=8F=96=E6=95=B0=E3=80=8D=E5=85=B1?= =?UTF-8?q?=E4=BA=AB=E7=82=B9=EF=BC=8C=E5=AE=88=E9=97=A8=E6=94=B6=E7=AA=84?= =?UTF-8?q?=EF=BC=8C=E6=96=87=E6=A1=A3=E5=8F=A3=E5=BE=84=E7=BB=9F=E4=B8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit r2 复审 P1:报告与代码文档的「仍然共享」清单只写了 3 项,漏了「读页外真值 / 首帧取数」这半边。 本轮按复审给的第一条路(文档/契约层面诚实交代,不动结构): - `settings/mod.rs` 的「仍然共享」扩到 6 条,新增: 4. 读页外真值 —— `States` 只有 appearance + groups,General 要显示 `Model.lang` 就得改 `settings/state.rs` + `pages/mod.rs`(2 个共享文件)或在 `app/state.rs` 用真值初始化 `Groups.language_appearance`(1 个);给出建议形状:一次性给 `States` 加 `model: &'a Model`。 5. 首帧取数 / 异步发起 —— `Body` 返回不了 `Task`,组自己发不了首帧探测;两条路(`Boot` 分支派发 1 个共享文件;或把 `Body` 返回类型加一条 `Task` 旁路,1 个文件一次性)与「不改的代价」都写明。 6. 隐性代价:`Groups: Sync + Default`(`demo_groups` 的 `static LazyLock` 撑 harness), 非 `Sync` 字段会让编译红在共享的 `settings/state.rs`;给了改法。 - `general.rs` 文档改成正确指引(消息加在本线的 `language_appearance.rs`,落点 `app/update/settings/language_appearance.rs`,**不要**改 `pages/mod.rs` / `pages/message.rs` / `app/update/page_messages.rs`),并指向上面第 4、5 条。 - `tests/mod.rs` 的「接真控件改 3 个文件」订正为 2 个,并删掉对**已被删除**的 `only_the_appearance_group_carries_a_real_control_body` 的点名;`appearance_section.rs` 的 模块文档与断言消息同步订正(`body = Some(appearance::body)`)。 - 守门 `the_group_dispatch_has_no_catch_all_arm` 判据**结构化收窄**:用大括号深度计数只看 `match message {` 本层(深度 1)的臂,嵌套 match 整段跳过、`|` 折行 or-pattern 续行放行; 只有「本层里含 `=>` 且不以该组变体路径开头」才算兜底。新增两条守门自测: `the_guard_ignores_nested_matches_and_folded_or_patterns`(复审 P2-3 的假红探针)与 `the_guard_still_catches_both_catch_all_shapes`(`_ =>` 与 `other =>` 仍要红); 误伤类别如实写进测试 doc 的「抓不住什么」。 - P3:`java::Message::Ping` 的文档改成指向真实存在的绊线(`tests/java_section.rs` 的 `the_java_group_is_still_demo_rows`);`demo_groups()` 文档写明 `Sync + Default` 的隐性要求。 --- .../app-iced/src/app/update/settings/tests.rs | 204 ++++++++++++++---- crates/app-iced/src/pages/settings/general.rs | 11 +- crates/app-iced/src/pages/settings/java.rs | 3 +- crates/app-iced/src/pages/settings/mod.rs | 23 +- crates/app-iced/src/pages/settings/state.rs | 5 + .../settings/tests/appearance_section.rs | 6 +- .../app-iced/src/pages/settings/tests/mod.rs | 9 +- 7 files changed, 206 insertions(+), 55 deletions(-) diff --git a/crates/app-iced/src/app/update/settings/tests.rs b/crates/app-iced/src/app/update/settings/tests.rs index 3c8c14e..e457a52 100644 --- a/crates/app-iced/src/app/update/settings/tests.rs +++ b/crates/app-iced/src/app/update/settings/tests.rs @@ -4,10 +4,39 @@ //! 兜底会把「加了变体忘了接线」从**编译错误**降级成「点了没反应」。两级 dispatch 各守一条: //! 那一层守 `pages::Message`,本文件守 `GroupMessage` 与四个 handler。 //! -//! r1 复审的 P3-2 指出上一版有洞:只拦 `_ =>` 的话,`other => { … }` 这种 **binding 兜底** -//! 照样取消穷尽性、照样静默。本版判据反过来写——**除已知臂外,任何含 `=>` 的行都算兜底**: -//! `settings.rs` 里每条臂都必须以 `pages::settings::GroupMessage::` 开头且恰好 4 条;handler 里 -//! 每条臂都必须以该组自己的变体路径开头;三个还是空枚举的 handler 一行 `=>` 都不许有。 +//! ## 判据(r2 复审 P2-3 之后收窄过一次) +//! +//! 上一版的判据是「除已知前缀外,任何含 `=>` 的行都算兜底」——**过宽**:往 handler 里加一个 +//! **没有兜底臂**的嵌套 match(`match true { true => …, false => … }`),或者 rustfmt 把长 +//! or-pattern 折行成 `| X =>`,都会让它假红。四条填页线一写真实逻辑就撞,守门反而推高成本。 +//! +//! 现在的判据是**结构化的**:只找 `match message {` 这一层(用大括号深度计数把嵌套 match 排除 +//! 在外),**只看深度 1 的臂**—— +//! +//! - `app/update/settings.rs` 的 `on_settings`:恰好 **4 条**臂,每条以 `pages::settings::GroupMessage::` +//! (或短写 `GroupMessage::`)开头; +//! - 四个 handler:每条臂以该组自己的 `pages::settings::<线>::Message::`(或短写 `Message::`)开头, +//! 还没有臂的(空枚举 `match message {}`)一条臂都没有; +//! - 以 `|` 开头的折行 or-pattern 续行放行(它不可能是兜底)。 +//! +//! ## 抓得住什么 / 抓不住什么 +//! +//! 抓得住: +//! 1. `on_settings` 里加 `_ =>` 或 binding 臂 `other =>`(`GroupMessage` 不再穷尽); +//! 2. handler 把 `match message {}` 改成 `_ => Task::none()` / `other => { … }`; +//! 3. `on_settings` 的臂数不是 4(删了一条、或把两条合并); +//! 4. handler 的臂写成别的组的变体路径(前缀不对)。 +//! +//! 抓不住(老实列出来): +//! 1. **嵌套 match 里的兜底**——深度 ≥2 的臂不看。这是刻意的:嵌套 match 的穷尽性由它自己的类型 +//! 保证,跟 `GroupMessage` 漏接线无关;代价是没人守「嵌套 match 自己写了个 `_ =>`」; +//! 2. **跨行 pattern**(除 `|` 续行)——把 struct 字段拆成多行、`=>` 落在第 3 行那种写法会被 +//! 当成「前缀不对」而误判兜底。当前代码风格不出现;真出现时放宽规则,别把守门删掉; +//! 3. **字符串/字符字面量里的 `{` `}`** 会扰乱深度计数(本文件与四个 handler 都没有这种写法); +//! 4. handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样 +//! ——类型对得上就编得过,只有人读能看出来; +//! 5. `match message` 被改名或换变量名时找不到匹配,臂数为 0 —— 这时 +//! `the_group_dispatch_has_exactly_four_arms` 会红(不是静默通过)。 //! //! 站位:本文件是 `app::update::settings::tests`,`include_str!` 的路径相对本文件所在目录。 @@ -21,6 +50,58 @@ use crate::cli::Options; use crate::pages; use crate::theme::Families; +/// `on_settings` 的臂允许的前缀。 +const GROUP_ARMS: &[&str] = &["pages::settings::GroupMessage::", "GroupMessage::"]; + +/// 一行的净大括号增量(`{` 加、`}` 减)。 +fn brace_delta(line: &str) -> i32 { + let open = line.matches('{').count() as i32; + let close = line.matches('}').count() as i32; + open - close +} + +/// 扫一条 `match message { … }`,只看**本层**(深度 1)的臂。 +/// +/// 返回 `(本层的合法臂数, 违规行)`:违规 = 本层里含 `=>`、又不是「已知前缀开头」的行 +/// (`|` 开头的折行 or-pattern 续行放行)。嵌套 match 的臂在深度 ≥2,整段跳过。 +fn dispatch_arms(path: &str, source: &str, allowed: &[&str]) -> (usize, Vec) { + let mut arms = 0usize; + let mut offending = Vec::new(); + let mut depth = 0i32; + let mut inside = false; + + for (index, line) in source.lines().enumerate() { + let trimmed = line.trim(); + if trimmed.starts_with("//") { + continue; + } + + if !inside { + if trimmed.contains("match message {") { + inside = true; + depth = 1; + } + continue; + } + + if depth == 1 && trimmed.contains("=>") { + let pattern = trimmed.trim_start_matches('|').trim_start(); + if allowed.iter().any(|prefix| pattern.starts_with(prefix)) { + arms += 1; + } else if !trimmed.starts_with('|') { + offending.push(format!("{path}:{}: {trimmed}", index + 1)); + } + } + + depth += brace_delta(trimmed); + if depth <= 0 { + break; + } + } + + (arms, offending) +} + /// 不碰磁盘的 App(mock 后端 + 内存设置桥),与 `app/tests.rs` 的 `headless` 同一副骨架。 fn headless() -> App { const FAMILIES: Families = Families { @@ -38,69 +119,51 @@ fn headless() -> App { ) } -/// 收集「不该存在的臂」:非注释行里出现 `=>`,但不以该文件允许的前缀开头。 -fn offending_arms(path: &str, source: &str, allowed: Option<&str>) -> Vec { - source - .lines() - .enumerate() - .filter_map(|(index, line)| { - let trimmed = line.trim(); - if trimmed.starts_with("//") || !trimmed.contains("=>") { - return None; - } - match allowed { - Some(prefix) if trimmed.starts_with(prefix) => None, - _ => Some(format!("{path}:{}: {trimmed}", index + 1)), - } - }) - .collect() -} +// --------------------------------------------------------------- 守门本体 -/// `on_settings` 与四个 handler 里都**不许**出现任何形式的兜底臂(`_ =>` 或 `other =>`)。 -/// -/// 抓得住:① 给 `on_settings` 加 `_ =>` 或 `other =>`(`GroupMessage` 不再穷尽); -/// ② 四个 handler 把 `match message {}` 改成 `_ => Task::none()` / `other => { … }`(同样取消穷尽性); -/// ③ 给 settings.rs 多接一条不认识的臂(前缀不对)。 -/// 抓不住:handler 忽略消息(`let _ = message; Task::none()`)、两个 handler 身体写反或写成一样 -/// —— 类型对得上就编得过;也抓不住「新变体接了个空壳」。那两条只有人读能看得出来。 +/// `on_settings` 与四个 handler 的**本层**臂里不许出现任何形式的兜底(`_ =>` 或 `other =>`)。 #[test] fn the_group_dispatch_has_no_catch_all_arm() { - let sources: [(&str, &str, Option<&str>); 5] = [ + let sources: [(&str, &str, &[&str]); 5] = [ ( "app/update/settings.rs", include_str!("../settings.rs"), - Some("pages::settings::GroupMessage::"), + GROUP_ARMS, ), ( "app/update/settings/java.rs", include_str!("java.rs"), - Some("pages::settings::java::Message::"), + &["pages::settings::java::Message::", "Message::"], ), ( "app/update/settings/download.rs", include_str!("download.rs"), - None, + &["pages::settings::download::Message::", "Message::"], ), ( "app/update/settings/language_appearance.rs", include_str!("language_appearance.rs"), - None, + &[ + "pages::settings::language_appearance::Message::", + "Message::", + ], ), ( "app/update/settings/ai_about.rs", include_str!("ai_about.rs"), - None, + &["pages::settings::ai_about::Message::", "Message::"], ), ]; let mut offending = Vec::new(); for (path, source, allowed) in sources { - offending.extend(offending_arms(path, source, allowed)); + let (_, bad) = dispatch_arms(path, source, allowed); + offending.extend(bad); } assert!( offending.is_empty(), - "设置页分组消息的 dispatch 里出现了兜底臂({offending:?}):\ + "设置页分组消息的 dispatch 本层出现了兜底臂({offending:?}):\ 任何 catch-all / binding 臂都会取消 `GroupMessage` 的穷尽性,\ 新变体漏接线时编译器不再报 E0004" ); @@ -108,16 +171,15 @@ fn the_group_dispatch_has_no_catch_all_arm() { /// `on_settings` 必须**恰好四条**臂,一条一条显式写全(不是「至少」)。 /// -/// 抓得住:有人删掉一条臂、或者把两条合到一起(`A(..) | B(..) =>` 这种也过不了上面那条前缀判据, -/// 这里是第二条独立判据)。 -/// 抓不住:臂的顺序(顺序不影响语义)。 +/// 抓得住:删掉一条臂、把两条合并、或者把 `match message` 改名(改名后扫不到臂 ⇒ 0 ≠ 4)。 +/// 抓不住:臂的顺序(顺序不影响语义)、每条臂接到了哪个 handler(类型对得上就编得过)。 #[test] fn the_group_dispatch_has_exactly_four_arms() { - let source = include_str!("../settings.rs"); - let arms = source - .lines() - .filter(|line| line.trim().starts_with("pages::settings::GroupMessage::")) - .count(); + let (arms, _) = dispatch_arms( + "app/update/settings.rs", + include_str!("../settings.rs"), + GROUP_ARMS, + ); assert_eq!( arms, 4, @@ -125,6 +187,60 @@ fn the_group_dispatch_has_exactly_four_arms() { ); } +/// **守门自身不误伤**:嵌套 match 与折行的 or-pattern 都不算兜底(r2 复审 P2-3 的探针)。 +/// +/// 这条是上一版判据假红的回归测试:复审往 handler 里加 `match true { true => …, false => … }` +/// 就让它 FAILED;现在这类写法必须放行。 +#[test] +fn the_guard_ignores_nested_matches_and_folded_or_patterns() { + let synthetic = "\ +impl App { + fn on_settings_java(&mut self, message: pages::settings::java::Message) -> Task { + match message { + pages::settings::java::Message::Probe => { + let nested = match true { true => 10u8, false => 11u8 }; + let _ = nested; + Task::none() + } + | pages::settings::java::Message::Retry => Task::none(), + } + } +} +"; + + let (arms, offending) = dispatch_arms( + "synthetic", + synthetic, + &["pages::settings::java::Message::"], + ); + assert!( + offending.is_empty(), + "正常代码被误判成兜底(这正是 r2 复审 P2-3 的假红):{offending:?}" + ); + assert_eq!(arms, 2, "嵌套 match 里的两条臂不算本层臂"); +} + +/// **守门仍然有牙**:`_ =>` 与 binding 臂 `other =>` 都要被本层判成兜底。 +#[test] +fn the_guard_still_catches_both_catch_all_shapes() { + for arm in [ + "_ => Task::none(),", + "other => { let _ = other; Task::none() }", + ] { + let synthetic = format!( + "impl App {{\n fn on_settings_java(&mut self, message: M) -> Task {{\n match message {{\n {arm}\n }}\n }}\n}}\n" + ); + let (_, offending) = dispatch_arms("synthetic", &synthetic, GROUP_ARMS); + assert_eq!( + offending.len(), + 1, + "`{arm}` 是兜底,必须被判红(实际 {offending:?})" + ); + } +} + +// --------------------------------------------------------------- 活的端到端证据 + /// **活的端到端证据**:一条组消息真的从外壳那条既有通道走到该组的 handler(r1 复审要求)。 /// /// 路径:`app::Message::Page` → `on_page` → `pages::Message::Settings` → `on_settings` → diff --git a/crates/app-iced/src/pages/settings/general.rs b/crates/app-iced/src/pages/settings/general.rs index 9fcd2ee..72e97a7 100644 --- a/crates/app-iced/src/pages/settings/general.rs +++ b/crates/app-iced/src/pages/settings/general.rs @@ -1,8 +1,13 @@ //! 设置页「General」分组:语言 / 主题 / 启动页 / 关闭按钮(演示行)。 //! -//! **这一组归 language+appearance 线**:语言那一行将来换真下拉(`pages::Message` 侧还需一条 -//! 「设置项改动」的消息,见 `pages/mod.rs` 的 `Message`),改这里 + 那一处消息即可, -//! `settings/mod.rs` 的注册表不用动。 +//! **这一组归 language+appearance 线**:语言那一行将来换真下拉时,消息加在本线的消息口 +//! `settings/language_appearance.rs` 的 `Message` 里,落点是 +//! `app/update/settings/language_appearance.rs`——**不要**去改 `pages/mod.rs` / `pages/message.rs` / +//! `app/update/page_messages.rs`(那三个正是 M101 收掉的共享锚点,改了就把本 mission 的目的掀掉); +//! `settings/mod.rs` 的注册表也不用动。 +//! +//! 要显示**页外真值**(例如「当前语言」的唯一来源是 `pages::Model.lang`)或首帧就要取数时, +//! 那两条仍有代价——见 `settings/mod.rs` 文档「仍然共享」小节里的第 **4、5** 条。 //! //! 分组元数据只有一份(这里的 [`GROUP`]):`settings/mod.rs` 的 `GROUPS` 直接引用它。 diff --git a/crates/app-iced/src/pages/settings/java.rs b/crates/app-iced/src/pages/settings/java.rs index f312af6..dac13c9 100644 --- a/crates/app-iced/src/pages/settings/java.rs +++ b/crates/app-iced/src/pages/settings/java.rs @@ -37,7 +37,8 @@ pub struct State {} pub enum Message { /// **只有测试构造它**:M101 r2 留的「活的端到端证据」——外壳消息 → /// `on_page` → `on_settings` → `on_settings_java` 这条路在运行时真的跑通,不是只靠编译期穷尽。 - /// java 线接第一个真控件时把这一支换成真变体(`tests/java_section.rs` 会提醒)。 + /// java 线接第一个真控件时把这一支换成真变体:`tests/java_section.rs` 的 + /// `the_java_group_is_still_demo_rows` 就是那条会提醒你的绊线(它断言 `body.is_none()`)。 #[cfg(test)] Ping, } diff --git a/crates/app-iced/src/pages/settings/mod.rs b/crates/app-iced/src/pages/settings/mod.rs index e73d94c..6fc2d7b 100644 --- a/crates/app-iced/src/pages/settings/mod.rs +++ b/crates/app-iced/src/pages/settings/mod.rs @@ -81,7 +81,7 @@ //! `Message::Page(pages::Message::Settings(GroupMessage::<线>(…)))` 回来——`Page` 这条既有通道就是 //! 异步结果的入口,`app::Message` / `app/update.rs` 都不用动。 //! -//! ### 仍然共享、本 mission 无法消除的点(四条线都可能碰到,只能靠 tower 串行合并) +//! ### 仍然共享的点(四条线都可能碰到,只能靠 tower 串行合并,或者由专人一次性做掉) //! //! 1. `crates/i18n/src/catalog/golden.rs` —— 每加一条文案 key 都要按声明顺序往金表里插一行 //! (`split_keeps_every_translation` 逐条比 key + 英/中译文)。**注意**:`catalog.rs` 的 `Key` @@ -90,6 +90,27 @@ //! 2. 本文件的 [`GROUPS`] —— 只有 ai+about 线**上第 5 组**时加一行(页面上多一组本来就是整页的变化)。 //! 3. `tests/mod.rs` —— ai+about 线新建 `tests/ai_section.rs` 时要加一行 `mod ai_section;` //! (测试文件不挂 `mod` 就不编译),以及「左列顺序」那条字面量断言会连带红,需要人确认。 +//! 4. **读页外真值**(r2 复审 P1 补的,上一版漏了)—— [`States`] 只有 `appearance` + `groups` 两格, +//! 组正文读不到 `pages::Model`,也读不到 presenter 的设置快照。最典型的是 language+appearance 线: +//! 「当前语言」的唯一来源是 `Model.lang`(`pages/model.rs`),General 分组要显示真值就得让值进 +//! `States`,那要改 `settings/state.rs` + `pages/mod.rs`(`render` 现在只拿 `model` 算 metrics, +//! **2 个共享文件**);或者反过来在 `app/state.rs` 用真值初始化 `Groups.language_appearance` +//! (**1 个共享文件**)。建议形状:**一次性**给 `States` 加一格 `model: &'a Model`(并让 +//! `render` 把 `model` 传下去),此后四条线都不必再碰这两个文件——这是「谁先撞谁付一次钱」的 +//! 那类代价,不是永久性的四路撞车。 +//! 5. **首帧取数 / 异步发起**(同上,r2 复审 P1 补)—— [`Body`] 是 +//! `fn(&Ui, States) -> Option`,**返回不了 `Task`**,而 `view` 是纯函数:一组想在自己 +//! 的首帧就发起探测(java 的「探测本机 JVM」、download 的「测源可用性」)有两条路: +//! ①在 `app/update.rs` 的 `Message::Boot` 分支里补一次首帧派发(**1 个共享文件**); +//! ②把 `Body` 的返回类型改成 `Option<(Element, Task)>` 或 `Option` 旁路 +//! (改本文件 + `form`,**1 个共享文件**,一次性)。不改的代价是:首屏显示演示值,等用户点一次 +//! 才变真值——四条线都能接受,就不必动。 +//! 6. **隐性代价:`Groups` 必须是 `Sync + Default`**(r2 复审 P3 指出)—— +//! [`demo_groups`] 用 `static LazyLock` 撑着 harness 路径,所以某个组的 `State` 里一旦 +//! 放了 `Rc` 这类非 `Sync` 字段,编译会在**本模块**(`settings/state.rs`)红。选择 `LazyLock` +//! 是为了让 `page()` / `page_with_appearance()` 的返回 `Element<'a>` 借得到一个 `'static` 的默认 +//! 状态;如果哪天必须放非 `Sync` 状态,改法是把默认状态改成「每个调用点自己造一个 `Groups`」 +//! (harness 侧改两处,同样是**一次性**代价)。 // 四条填页线的文件要能被**各自那条落点**(`app/update/settings/<线>.rs`)与状态架子 // (`state::Groups`)命名,所以是 `pub(crate)`:最小面,不是 crate 公开 API。 diff --git a/crates/app-iced/src/pages/settings/state.rs b/crates/app-iced/src/pages/settings/state.rs index 7310b0b..0e87ad6 100644 --- a/crates/app-iced/src/pages/settings/state.rs +++ b/crates/app-iced/src/pages/settings/state.rs @@ -73,6 +73,11 @@ pub struct States<'a> { /// /// `'static` 是有意的:那两条入口不持有任何组状态,而返回的 `Element<'a>` 会借走这份 `Groups`; /// 让它活得比调用方长,借用就不会把返回值钉在函数栈上。 +/// +/// **隐性代价(r2 复审 P3)**:`static` 要求 `Groups: Sync + Default`,于是某个组的 `State` 里一旦 +/// 放了 `Rc` 这类非 `Sync` 字段,编译会红在**本文件**(共享)而不是那一组自己的文件。真需要 +/// 非 `Sync` 状态时的改法:去掉这个 `static`,改成每个 harness 调用点自己造一份 `Groups` +/// (`pages/mod.rs` 的两个入口各一处,一次性代价),见 [`super`] 文档「仍然共享」第 6 条。 pub fn demo_groups() -> &'static Groups { static DEMO: LazyLock = LazyLock::new(Groups::default); &DEMO diff --git a/crates/app-iced/src/pages/settings/tests/appearance_section.rs b/crates/app-iced/src/pages/settings/tests/appearance_section.rs index 1741086..f0aeaf5 100644 --- a/crates/app-iced/src/pages/settings/tests/appearance_section.rs +++ b/crates/app-iced/src/pages/settings/tests/appearance_section.rs @@ -3,7 +3,8 @@ //! //! **归 language+appearance 线**:这是设置页唯一接了真控件的一组,真控件实现全在 //! `pages/appearance/section.rs`;本文件只守「注册表这一侧的接线」。 -//! 本组已经把 `body` 接上了,所以 `tests/mod.rs` 那条「只有一组有真控件」的绊线不用再动。 +//! 本组已经把 `body` 接上了;`tests/mod.rs` 那条「只有一组有真控件」的**聚合绞线已在 M101 r2 删除** +//! (被四条 per-group 断言完全蕴含——代价是「唯一性」从此没人守,那段说明写在 `tests/mod.rs` 同处)。 use super::super::appearance; use unlucky_design::icons::IconKind; @@ -45,6 +46,7 @@ fn the_appearance_group_keeps_its_icon() { fn the_appearance_group_keeps_a_real_control_body() { assert!( appearance::GROUP.body.is_some(), - "外观分组必须是挂了真控件的那一组(`body = Some(crate::pages::appearance::section)`)" + "外观分组必须是挂了真控件的那一组(`settings/appearance.rs` 的 `body: Some(appearance::body)`;\ + 具体渲染器由那一行源码 + 截图担保,函数指针相等性断言不了)" ); } diff --git a/crates/app-iced/src/pages/settings/tests/mod.rs b/crates/app-iced/src/pages/settings/tests/mod.rs index a166939..e258d5b 100644 --- a/crates/app-iced/src/pages/settings/tests/mod.rs +++ b/crates/app-iced/src/pages/settings/tests/mod.rs @@ -12,10 +12,11 @@ //! 判据(M100 报告里正面回答的那条):**给 java 加一条新断言,改几个文件?** —— 1 个 //! (`tests/java_section.rs`)。本文件不动。 //! -//! 但「java 线**接真控件**那天改几个文件」是另一回事,答案是 **3 个**(M100 r1 复审 p3-3 订正, -//! 我最初只写成 2 个):`settings/java.rs`(写 body)+ `tests/java_section.rs` 的 -//! `the_java_group_is_still_demo_rows`(它断言 `body.is_none()`,接上就红)+ 本文件末尾那条 -//! `only_the_appearance_group_carries_a_real_control_body`。download / general 同型。 +//! 但「java 线**接真控件**那天改几个文件」是另一回事,答案是 **2 个**(M101 r2 订正: +//! M100 之后曾是 3 个,第 3 处就是本文件末尾那条聚合绊线 +//! `only_the_appearance_group_carries_a_real_control_body`,它被四条 per-group 断言完全蕴含、 +//! 已在 M101 r2 删除):`settings/java.rs`(写 body)+ `tests/java_section.rs` 的 +//! `the_java_group_is_still_demo_rows`(它断言 `body.is_none()`,接上就红)。download / general 同型。 //! //! ## ai+about 线要额外改本文件的两处(M100 r1 复审 p3-4) //!