Files
Unlucky-Launcher/crates/app-iced/src/pages/settings/tests/mod.rs
T
AskaEth df331f6a60 feat(m175): Java 设置页接真运行时(扫描 / 手动路径 / 默认 JDK / 内存 / JVM 参数)
用户实测四条抱怨逐条对应:
- 「没有自动检测本机 Java」:首屏自动扫一次盘(托管根 + JAVA_HOME + PATH + 标准安装位置 +
  注册表),之后由「立即扫描」手动触发;扫描在后台线程里跑,带 done/total 进度与取消
- 「显示 Temurin 21.0.4 不知道是不是硬编码」:演示行与绊线测试一起删掉,列表来自真读盘
- 「没一个能改」:默认内存(MB,512~65536,范围与 schema 同源)与 JVM 参数可真改、真落盘、读回
- 「不能手动指定 Java 路径 / 不能改默认版本」:手动添加走严格校验(真跑 java -version),
  设为默认写 java.selected_path 并给出「为什么选它」(当前实例的版本门槛 + 每行满足与否)

core:java::manual(严格探测 + 厂商识别 + 名单持久化)、java::staged(候选收集 + 带进度与
取消的扫描)、java::proc(带超时且可打断的子进程执行:卡住的 java -version 会被 kill)。
i18n:catalog/settings/java.rs 55 条中英 + 金表按装配顺序插入。

复审 r1(p2-5items)全修:
- P2-1 「设为默认」不再清脏:只写 java.selected_path,内存/JVM 草稿一个字节都不动
- P2-2 错误文案全部走 i18n(三档「写不进去」/「路径太长」/「名单写不进去」各自一个 key),
  不再把 Rust 枚举名当人话
- P2-3 手动名单落盘有正面断言(加一条 → 从盘上新读一份 → 在)
- P2-4 版本号显示到小版本(17.0.13 / 17.0.9 可区分),解析不出来时给原始版本串
- P2-5 每候选 8 秒超时并 kill、取消打断正在跑的探测、扫描槽挂在 State 上、线程活着期间
  不许起第二个(界面显示「正在取消…」)

复审 r2(p2-2items)全修:
- P2-1 空态不再指向不存在的「Java 页」,改成真实存在的 `ull java install 21` 命令行入口
- P2-2 `auto_scan_due` 增加 `!report.cancelled`:取消过一次之后本进程不再自动扫,
  配置文件再变(存内存 / 改主题)也不会把取消无声撤销;手动「立即扫描」不受影响
- 顺带:proc 的 kill+wait 补「子进程真死了」的回归守卫(标记文件从未出现)、
  core 的 Schema 错误改成页面自己的 key(不再把中文 Display 直出给英文界面)、
  默认态 chip 用新的 `Chip::badge`(不再挂一条永远收不到的消息)
2026-11-08 19:52:04 +08:00

228 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 设置页的**跨 section** 守门测试(M100:跨 section 的留在这里,section 自己的断言**新增**在
//! `tests/<section>_section.rs`)。
//!
//! ## 为什么这里只剩「跨 section」的断言
//!
//! 设置页正文接下来由四条线并行接(java / download / language+appearance / ai+about)。每条线要能
//! **只改自己的测试**就跑通,所以「某一组自己的行文案 / 图标 / body 现状」一律放在
//! `tests/<section>_section.rs`(M100 **新增**的 12 条,不是从本文件搬过去的——本文件在 `main`
//! 上就只有下面这 7 条跨 section 断言,一条 section 断言都没有);本文件只留**必须四组一起看**的
//! 断言——下放到任何单组文件里,都会变成「某组替其它三组说话」,四条线并行时必然互相踩。
//!
//! 判据(M100 报告里正面回答的那条):**给 java 加一条新断言,改几个文件?** —— 1 个
//! (`tests/java_section.rs`)。本文件不动。
//!
//! 但「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)
//!
//! 它加第 5 组时,除了 `settings/mod.rs` 的 `GROUPS`,**本文件还有两处必改**:
//! 1. 顶部 `mod ai_section;`(新测试文件要挂进来,否则永远不编译、永远不跑);
//! 2. [`the_split_kept_the_left_column_order`] 断言的是**字面量** `["General","Appearance",
//! "Java","Downloads"]`,加第 5 组必红——这是刻意的:左列顺序变了就是观感变了,要人确认。
//! 另外 `the_appearance_group_is_where_the_module_says_it_is` 依赖 `APPEARANCE_GROUP == 1`,
//! **把新组插在 Appearance 前面会连带红**(所以新组应该排最后)。
//!
//! ## 站位
//!
//! 本文件是 `pages::settings::tests`:`super` = `pages::settings`,`super::super` = `pages`。
//!
//! ## 命名
//!
//! 子模块带 `_section` 后缀,是为了不和 `use super::*` 带进来的分组模块(`settings::java` 等)
//! 撞名——撞名时显式 `mod` 会静默盖掉 glob,读代码的人会以为 `java` 指的是分组的那个。
mod appearance_section;
mod download_section;
mod general_section;
mod java_section;
mod scroll_fit;
mod update_section;
use super::super::layout::{self, frame_metrics, measure_cap_px, PaneKit, SIDE_NAV_WIDTH};
use super::*;
use crate::pages::appearance::{APPEARANCE_GROUP, GROUP_LABEL};
// --------------------------------------------------------------- 版面(不属于任何一组)
/// 守的是**版面契约**:分组导航走 drawer 240(D33),不是 rail 的 88。
///
/// 不能下放到分组:它不是「某一组」的属性,而是整个 supporting-pane 的几何。
#[test]
fn side_nav_reuses_the_drawer_width() {
assert_eq!(SIDE_NAV_WIDTH, 240.0);
let metrics = frame_metrics(PaneKit::Supporting, 1180.0);
assert_eq!(metrics.secondary, Some(240.0));
assert_ne!(metrics.secondary, Some(layout::rail_width()));
}
/// 守的是**行宽上限**(逐行 67 字符 ≤72)在所有宽度档下都成立。
///
/// 不能下放:它是四个分组共用的正文列,任何一组把列撑宽都会连带别人。
#[test]
fn form_column_never_exceeds_the_measure_cap() {
for width in [720.0, 840.0, 1180.0, 1920.0] {
let metrics = frame_metrics(PaneKit::Supporting, width);
assert!(
metrics.column <= measure_cap_px() + f32::EPSILON,
"{width} 宽下表单列 {:?} 超过行宽上限",
metrics.column
);
assert!(metrics.column >= layout::measure_floor_px());
}
}
/// 守的是**窄窗退化**:<720 时侧列消失、分组改为按组堆叠。
///
/// 不能下放:退化是整页行为,四组一起变(单组文件里没法验证「侧列没了」)。
#[test]
fn narrow_window_stacks_the_groups_instead_of_the_side_nav() {
let narrow = frame_metrics(PaneKit::Supporting, 700.0);
assert_eq!(narrow.layout, PageLayout::SingleColumn);
assert!(narrow.secondary.is_none(), "窄窗不许留侧列");
}
// --------------------------------------------------------------- 注册表(四组一起看)
/// 守的是**注册表的结构不变量**:每组都有名字、图标,且「有真控件分发」或「有演示行」至少占一样。
///
/// 下放不了:它遍历的是 `GROUPS`,单组文件拿不到其它组,而「新增一组忘了填 rows、又没有分发」
/// 正是这条要抓的。
///
/// M175 起 Java 组的 `rows` 是**空**的:它那三行演示值(`Temurin 21.0.4` / `4 GiB` /
/// `Automatic`)正是用户实测投诉的假数据源,真控件接上之后演示行就删干净了。所以判据从
/// 「必须有 rows」放宽成「必须有 rows **或** 有别的分发路径」——按下标分发的那几组
/// (general / update / assistant / java)在 `settings/mod.rs` 的 `form` 里各有 `else if`,
/// 本断言只对「既没有 rows 又不在这张名单里」的组报错。
#[test]
fn every_group_has_a_name_icon_and_rows() {
assert!(GROUPS.len() >= 3);
assert!(SELECTED < GROUPS.len());
// 按下标分发(`settings/mod.rs` 的 `form`):这些组的正文不在 `Group.rows` 里。
const INDEX_DISPATCHED: [&str; 4] = ["General", "Updates", "Assistant", "Java"];
for group in &GROUPS {
assert!(!group.name.is_empty());
assert!(!group.icon.icon().path.is_empty(), "{} 缺图标", group.name);
assert!(
!group.rows.is_empty() || INDEX_DISPATCHED.contains(&group.name),
"{} 既没有设置行,也不在按下标分发的名单里",
group.name
);
for (label, value) in group.rows {
assert!(!label.is_empty() && !value.is_empty());
}
}
}
/// M93:设置页「外观」分组的下标与 `pages::appearance` 声明的常量必须一致。
///
/// 抓得住:谁把 `GROUPS` 的顺序调了却忘了改 `APPEARANCE_GROUP`(那样真控件会画到别的分组上,
/// 而支持列显示的分组名也对不上)。
/// 抓不住:控件本身画得对不对(那由截图与 app 侧测试管)。
///
/// 下放不了:它同时读 `GROUPS`(注册表)与 `pages::appearance` 的常量,横跨两处;
/// 放进 `appearance_section.rs` 会让「外观那条线」替注册表顺序作证。
#[test]
fn the_appearance_group_is_where_the_module_says_it_is() {
assert_eq!(GROUPS[APPEARANCE_GROUP].name, GROUP_LABEL);
assert!(SELECTED < GROUPS.len(), "演示下标仍在范围内");
}
/// M99:拆分后**左列顺序**必须还是拆分前那四项(顺序变了就是观感变了,不是「纯搬移」)。
///
/// 抓得住:① 注册表顺序被调换(`GROUPS` 是四条线各自文件拼出来的,最容易在合并时错位);
/// ② 某组被误删/误加(长度不等)。
/// 抓不住:组内那些行还在不在、译文对不对(各组自己的文件与 `catalog` 金表各管一段)。
///
/// 下放不了:它是一句「四组顺序 = 这四个名字」的断言,任何单组文件都只能看到自己那一个名字。
#[test]
fn the_split_kept_the_left_column_order() {
let names: Vec<&str> = GROUPS.iter().map(|group| group.name).collect();
assert_eq!(
names,
vec![
"General",
"Appearance",
"Java",
"Downloads",
"Updates",
// M174:助手(AI)配置只追加在末尾
"Assistant",
]
);
}
/// M101 r2:`settings/mod.rs` 里**不许出现任何一组的 `State` 类型**(r1 复审 P1 的判据钉在这里)。
///
/// 抓得住:谁把状态漏斗拆回去——让 `view` / `form` 直接收 `appearance::State` 或某一组的 `State`
/// (那正是四条线「加一个字段就要改本文件」的形状,本次复审要求修掉的就是它)。
/// 抓不住:各组的**字段**(那些活在各组自己的文件里,本文件本来就不该知道,也就没有可断言的东西)。
///
/// **M106 的例外要读清楚**:General 已经接了真控件(语言行),但它**不走 `Group.body`**——
/// 那个槽位的签名是 `fn(&Ui, States) -> Option<Element>`,`States` 只有 `appearance` + `groups`
/// 两格,装不下「当前语言档位 + 生效语言」。它走 `settings::view` 的 `language` 形参 +
/// `GENERAL_GROUP` 那条按下标分发,`body` 仍是 `None`,所以本断言不动。
/// 判据没变:**`body: Some(..)` 仍然只有外观组**。
#[test]
fn the_settings_module_does_not_name_any_group_state() {
let source = include_str!("../mod.rs");
let forbidden = [
"appearance::State",
"java::State",
"download::State",
"language_appearance::State",
"ai_about::State",
];
let hits: Vec<&str> = forbidden
.into_iter()
.filter(|needle| source.contains(needle))
.collect();
assert!(
hits.is_empty(),
"`settings/mod.rs` 里出现了具体的组状态类型 {hits:?}:状态漏斗被打回去了——\
四条填页线又要改本文件。状态应该只在 `settings/state.rs` 的 `Groups` 与各组自己的\
`State` 定义里出现,本文件只转发 `States`"
);
}
///
/// 为什么能删:`tests/appearance_section.rs` 断言外观组 `body.is_some()`,
/// `tests/{general,java,download}_section.rs` 各自断言本组 `body.is_none()`——四条合起来与
/// 原来那条聚合断言等价(成员与顺序另有 [`the_split_kept_the_left_column_order`] 钉住)。
/// 删掉之后每条线「接真控件」只需改自己那 2 个文件,不必来本文件报到。
///
/// ## 删掉之后**抓不住什么**(tower 要求写明)
///
/// 那四条都只看**本组**,没有任何一条再断言「唯一性」——「四组里恰好一组挂了真控件」这件事
/// **现在没有守卫**。具体漏法:
///
/// | 情形 | 还红吗 |
/// |---|---|
/// | 某条线接上 `body` 却忘了翻自己那条 `is_none()` | **红**(本组断言还在) |
/// | 两条线的断言被同时改成 `is_some()`,页面上两组同时长真控件 | **绿着过去**(没人指认「唯一」) |
/// | 谁把 `body: Some(..)` 挂到一个不该有的组、又顺手改了它那条断言 | **绿着过去** |
///
/// 接受这个损失的理由:接真控件是**每组各自的一次性动作**,谁做谁翻自己那一条;而「唯一」在
/// 四条线并行的阶段是最容易假红的约束(一条线接上就替另外三条红)。想要回这条守卫,等四条线
/// 都落地后在这里加一条**计数**断言(`GROUPS.iter().filter(|g| g.body.is_some()).count()`),
/// 而不是现在钉死某个下标。
///
/// 这条 `#[test]` 本体已删;保留这段文档是为了让后来的人知道「这里原本有一条绊线」。
#[test]
fn every_line_can_be_wired_alone() {
// 判据是「每条线只改自己的文件」,不是任何一条具体断言:这里只钉住架子本身的两件事——
// ① 每组都有 `GROUP`(成员齐);② 演示下标合法。真正的 per-group 断言在各自文件里。
assert_eq!(
GROUPS.len(),
6,
"四条填页线 + M159 的更新组 + M174 的助手组"
);
assert!(SELECTED < GROUPS.len(), "演示下标仍在范围内");
}