复审 r3(p2-4items / fix-then-merge)四项 + 顺手五条:
1. **P2-1 `catch_unwind` 在发布档是死代码,三处文档说了不成立的话**:
`Cargo.toml` 的 `[profile.release]` 是 `panic = "abort"`(只有 `fast` 是 `unwind`),发布走
`--release`。分两半写清楚:①**闸前移仍安全**——release 档 panic 是进程退出,不存在永久停在
`Downloading`;`dev`/`test`/`fast` 档 `catch_unwind` 真会写 `Failed`;②但「下载失败不崩」在
panic 这一支上对发布版**是反的**:注册/`load_font` panic ⇒ 启动器整体 abort,不是降级。
`install.rs::plan_with_offline` 的文档(新增两档对照表)、`worker.rs` 模块文档与 `run` 注释全部
按这个说法重写,不再用 `catch_unwind` 给「不崩」背书。
2. **P2-2 「启动不发起下载」这条修复没有守卫**:拆出纯函数
`fonts::resolve_with_faces(setting, faces)`(`resolve_with` 只是 `system_faces()` 的包装),
新增 `fonts::tests::resolving_fonts_never_starts_a_download`:给一张只有拉丁族的脸表
(一定判 `NeedsDownload`),断言调用后 `install::status()` **不是** `Downloading`。
变异(实跑):把 `install::after_probe(cjk_report.strategy)` 加回 `resolve_with_faces` ⇒
`求值字体**不许**发起下载(状态从 Unknown 变成了 Downloading { bytes: 17772300 })`,红。
3. **P2-3 时限算术有洞 + `is_last` 没守卫**:改 `ATTEMPT_DEADLINE=60 s` / `TOTAL_DEADLINE=420 s`
(原来 120/360 时,第一个候选吃掉 120 s 后最后一个只剩 240 s ⇒ 门槛 72.3 KiB/s,r1 实测
67 KiB/s 需要 259 s,仍然必失败)。文档按新数值重算成一张表:第一个候选瞬间失败 ⇒ 420 s ⇒
42.3 KiB/s;爬满 60 s 被切 ⇒ 360 s ⇒ 49.4 KiB/s,两者都低于实测最慢的 67 KiB/s。
守卫:把同解的旧用例换成 **单候选 + attempt 300 ms / total 5 s**——有 `is_last` 时跑到总时限
(≈5 s),没有时 300 ms 就切,两种实现可分辨。变异(实跑):删掉 `is_last` ⇒
`the_last_candidate_gets_the_whole_remaining_budget` 红在
`最后一个候选该拿整个剩余预算(≈5 s)…452.6035ms`。
4. **P2-4(b)(c)**:①`install::tests` 里三处判据用例改用 `plan_with_offline(..., false)`,把进程级
`OFFLINE` 从判据里摘出去(否则同进程里 `headless(screenshot_options())` 会把它置 true 且从不
还原 ⇒ 结果依赖执行顺序);②`fonts.rs` 注释改成真实机制——出图 harness 不经
`App::with_presenter`、压根没有 `set_offline`,它零流量靠的是「`resolve_with` 不触发」。
顺手:①`sha256_mismatch…` 的 doc 改成「两层校验互备,本用例抓的是两层同时拆」(原来说
「拆任意一层就红」是假的);②`font.rs` 文档「把 90 s 压成 300 ms」→ 60 s/420 s;③`install.rs` 与
`catalog/font_install.rs` 的「≈17 MB」→ 16.9;④`worker::start` 的裸 `thread::spawn` 换成
`Builder::spawn`,起不来就落 `Failed`(发布档 panic = abort,不能让它 panic);线程体抽成 `run()`。
验证(`CARGO_TARGET_DIR=C:/Temp/ull-target/wt-168`,取退出码、不截断):
- `cargo test -p unlucky-app-iced --lib` → **688 passed / 0 failed / 2 ignored / exit 0**
- `cargo test -p unlucky-core --all-targets` → **16 个 target / 1189 passed / 0 failed / exit 0**
- `cargo test -p unlucky-i18n` → 31 passed / 0 failed / exit 0
- `cargo clippy -p unlucky-i18n -p unlucky-core -p unlucky-app-iced --all-targets -- -D warnings` → exit 0
- `cargo fmt --all -- --check` 干净;全仓无文件 >400;本轮改动文件最大 378(`app-iced/src/fonts.rs`)
215 lines
6.9 KiB
Rust
215 lines
6.9 KiB
Rust
//! `fonts` 的单元测试(拆出来只为「src 文件 ≤400 行」的门禁)。
|
||
|
||
use super::cjk::CjkReport;
|
||
use super::*;
|
||
use unlucky_design::typography::WeightCoverage;
|
||
use unlucky_i18n::font::Strategy;
|
||
use unlucky_i18n::lang::Detection;
|
||
|
||
fn only(installed: &'static [&'static str]) -> impl Fn(&str) -> bool {
|
||
move |family| installed.contains(&family)
|
||
}
|
||
|
||
#[test]
|
||
fn windows_stack_falls_back_to_classic_segoe_when_variable_is_absent() {
|
||
// Win10 没有 Segoe UI Variable Text
|
||
let family = pick(
|
||
Platform::Windows,
|
||
FontFamily::Text,
|
||
&only(&["Segoe UI", "Microsoft YaHei UI"]),
|
||
);
|
||
assert_eq!(family, "Segoe UI");
|
||
}
|
||
|
||
#[test]
|
||
fn windows_stack_prefers_the_variable_face_on_win11() {
|
||
let family = pick(
|
||
Platform::Windows,
|
||
FontFamily::Text,
|
||
&only(&["Segoe UI Variable Text", "Segoe UI"]),
|
||
);
|
||
assert_eq!(family, "Segoe UI Variable Text");
|
||
}
|
||
|
||
#[test]
|
||
fn generic_family_names_are_never_reported_as_the_resolved_family() {
|
||
// 一个都没装:返回栈头,且栈头不是通用族名
|
||
let family = pick(Platform::Linux, FontFamily::Text, &only(&[]));
|
||
assert_eq!(family, "Roboto");
|
||
assert!(!is_generic(family));
|
||
}
|
||
|
||
#[test]
|
||
fn missing_cjk_fonts_do_not_change_the_latin_pick() {
|
||
// 装了苹方但没装 SF:macOS 栈应该选 Helvetica Neue 而不是中文族
|
||
let family = pick(
|
||
Platform::MacOs,
|
||
FontFamily::Text,
|
||
&only(&["PingFang SC", "Helvetica Neue"]),
|
||
);
|
||
assert_eq!(family, "Helvetica Neue");
|
||
}
|
||
|
||
#[test]
|
||
fn weight_reports_use_the_token_degradation_path() {
|
||
// 没有系统字面表时回落到令牌纯函数:Windows 栈里 Segoe UI Variable Text 是可变 300..700
|
||
let medium = weight_report(
|
||
Platform::Windows,
|
||
"Segoe UI Variable Text",
|
||
Weight::Medium,
|
||
None,
|
||
);
|
||
assert_eq!(medium.requested, 500);
|
||
assert_eq!(medium.quality, WeightMatch::Native);
|
||
|
||
// 手工构造一个「只有 400/700」的族(= 经典 Segoe UI),500 应该落到最近档
|
||
let faces = [
|
||
Face {
|
||
family: "Segoe UI".to_owned(),
|
||
weight: 400,
|
||
},
|
||
Face {
|
||
family: "Segoe UI".to_owned(),
|
||
weight: 700,
|
||
},
|
||
];
|
||
let medium = weight_report(Platform::Windows, "Segoe UI", Weight::Medium, Some(&faces));
|
||
assert_eq!(medium.available, 400, "400 比 700 更近");
|
||
assert_eq!(medium.quality, WeightMatch::Nearest);
|
||
|
||
// 只有 400 却要 700 → 合成加粗
|
||
let only_regular = [Face {
|
||
family: "Narrow".to_owned(),
|
||
weight: 400,
|
||
}];
|
||
let bold = weight_report(
|
||
Platform::Windows,
|
||
"Narrow",
|
||
Weight::Bold,
|
||
Some(&only_regular),
|
||
);
|
||
assert_eq!(bold.quality, WeightMatch::Synthetic);
|
||
|
||
// 原生有 500 就用 500
|
||
let has_medium = [Face {
|
||
family: "Noto".to_owned(),
|
||
weight: 500,
|
||
}];
|
||
assert_eq!(
|
||
weight_report(Platform::Linux, "Noto", Weight::Medium, Some(&has_medium)).quality,
|
||
WeightMatch::Native
|
||
);
|
||
|
||
let narrow = WeightCoverage::Static(&[400]);
|
||
assert_eq!(narrow.nearest(700), 400);
|
||
}
|
||
|
||
#[test]
|
||
fn describe_is_one_line_and_mentions_all_three_stacks() {
|
||
let resolved = Resolved {
|
||
platform: Platform::Windows,
|
||
families: [
|
||
FamilyReport {
|
||
stack: "text",
|
||
family: "Segoe UI",
|
||
installed: true,
|
||
},
|
||
FamilyReport {
|
||
stack: "display",
|
||
family: "Segoe UI Variable Display",
|
||
installed: false,
|
||
},
|
||
FamilyReport {
|
||
stack: "mono",
|
||
family: "Cascadia Mono",
|
||
installed: true,
|
||
},
|
||
],
|
||
weights: [
|
||
WeightReport {
|
||
requested: 400,
|
||
available: 400,
|
||
quality: WeightMatch::Native,
|
||
},
|
||
WeightReport {
|
||
requested: 500,
|
||
available: 400,
|
||
quality: WeightMatch::Nearest,
|
||
},
|
||
WeightReport {
|
||
requested: 700,
|
||
available: 400,
|
||
quality: WeightMatch::Synthetic,
|
||
},
|
||
],
|
||
cjk: CjkReport {
|
||
detection: Detection {
|
||
lang: unlucky_i18n::Lang::En,
|
||
raw: None,
|
||
origin: unlucky_i18n::lang::ORIGIN_DEFAULT,
|
||
},
|
||
strategy: Strategy::NotNeeded,
|
||
},
|
||
};
|
||
|
||
let line = resolved.describe();
|
||
assert_eq!(line.lines().count(), 1);
|
||
assert!(line.contains("text=\"Segoe UI\""), "{line}");
|
||
assert!(line.contains("未装机"), "{line}");
|
||
assert!(line.contains("w700=400(synthetic)"), "{line}");
|
||
assert!(line.contains("cjk=not-needed"), "英文环境要进日志:{line}");
|
||
assert!(resolved.has_synthetic());
|
||
assert_eq!(resolved.families().text, "Segoe UI");
|
||
}
|
||
|
||
#[test]
|
||
fn type_roles_pick_the_display_stack_for_big_text() {
|
||
use unlucky_design::typography::TypeRole;
|
||
|
||
assert_eq!(
|
||
TypeRole::TitleLarge.style().family,
|
||
FontFamily::Display,
|
||
"22px 标题走 Display 栈"
|
||
);
|
||
assert_eq!(TypeRole::BodyMedium.style().family, FontFamily::Text);
|
||
}
|
||
|
||
/// **求值字体不许发起下载**(复审 r3 P2-2:上一轮的修复没有任何用例守着)。
|
||
///
|
||
/// 用 `resolve_with_faces` 而不是 `resolve_with`:`system_faces()` 不可注入,拿不到「一台没有
|
||
/// 中文字体的机器」。这里给一张只有拉丁族的脸表 ⇒ 中文环境判出 `NeedsDownload`。
|
||
///
|
||
/// 抓得住:把 `install::after_probe(cjk_report.strategy)` 加回求值路径(那正是 r2 判为 bug 的写法)
|
||
/// ——状态槽会从 `NotNeeded`/`Unknown` 变成 `Downloading`,下面那条断言当场红。
|
||
/// 复审 r3 实测:加回去整套 `unlucky-app-iced --lib` **687 passed / 0 failed**(当时没有这条)。
|
||
/// 抓不住:`App` 构造之后 `log_cjk` 那一拍会不会触发(那是 `cjk_for` 的职责,且它本来就该触发)。
|
||
#[test]
|
||
fn resolving_fonts_never_starts_a_download() {
|
||
use unlucky_i18n::font::Strategy;
|
||
|
||
use crate::fonts::install;
|
||
|
||
// 只有拉丁族 ⇒ 中文环境一定判 needs-download,于是这条用例对「触发」是敏感的。
|
||
let latin: Vec<Face> = ["Segoe UI", "Consolas"]
|
||
.iter()
|
||
.map(|family| Face {
|
||
family: (*family).to_owned(),
|
||
weight: 400,
|
||
})
|
||
.collect();
|
||
|
||
let before = install::status();
|
||
let resolved = resolve_with_faces(Some("zh_cn"), Some(&latin));
|
||
let after = install::status();
|
||
|
||
assert_eq!(
|
||
resolved.cjk.strategy,
|
||
Strategy::NeedsDownload(&unlucky_i18n::font::NOTO_SANS_SC),
|
||
"这张脸表必须判出「需要下载」,否则下面的断言证明不了什么"
|
||
);
|
||
assert!(
|
||
!after.is_downloading(),
|
||
"求值字体**不许**发起下载(状态从 {before:?} 变成了 {after:?})"
|
||
);
|
||
}
|