CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
178 lines
6.6 KiB
Rust
178 lines
6.6 KiB
Rust
//! `i18n::font` 的单测(拆出来只为守 400 行硬线)。
|
||
|
||
use std::cell::Cell;
|
||
|
||
use super::*;
|
||
|
||
fn only(installed: &'static [&'static str]) -> impl Fn(&str) -> bool {
|
||
move |family| installed.contains(&family)
|
||
}
|
||
|
||
/// 假下载器:记录调用次数,避免「悄悄联网」这种事在单测里被放过。
|
||
struct FakeFetcher {
|
||
calls: Cell<usize>,
|
||
result: Result<Vec<u8>, FetchError>,
|
||
seen: Cell<Option<&'static str>>,
|
||
}
|
||
|
||
impl FakeFetcher {
|
||
fn new(result: Result<Vec<u8>, FetchError>) -> Self {
|
||
FakeFetcher {
|
||
calls: Cell::new(0),
|
||
result,
|
||
seen: Cell::new(None),
|
||
}
|
||
}
|
||
}
|
||
|
||
impl Fetcher for FakeFetcher {
|
||
fn fetch(&self, font: &'static CjkFont) -> Result<Vec<u8>, FetchError> {
|
||
self.calls.set(self.calls.get() + 1);
|
||
self.seen.set(Some(font.family));
|
||
self.result.clone()
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn english_never_wants_a_cjk_font() {
|
||
// 抓得住:D28 的核心承诺——英文环境不加载/不下载中文字体(D28 说「零额外体积/零网络」)。
|
||
// 抓不住:程序真的没去读那个字体文件(那是加载器的行为,本 crate 只管判据)。
|
||
assert_eq!(decide(Lang::En, &only(&[])), Strategy::NotNeeded);
|
||
assert_eq!(
|
||
decide(Lang::En, &only(&["Microsoft YaHei"])),
|
||
Strategy::NotNeeded,
|
||
"装了中文字体也不该进入中文分支"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn chinese_prefers_the_first_installed_candidate() {
|
||
// 抓得住:候选顺序(YaHei UI > YaHei > SimSun…)被调换,或命中判断写成了「装了任意字面就 ok」。
|
||
// 抓不住:命中的族是否真有简体字形覆盖(族名存在 ≠ cmap 覆盖,见模块文档)。
|
||
assert_eq!(
|
||
decide(
|
||
Lang::ZhCn,
|
||
&only(&["Segoe UI", "Microsoft YaHei", "SimSun"])
|
||
),
|
||
Strategy::System {
|
||
family: "Microsoft YaHei"
|
||
}
|
||
);
|
||
assert_eq!(
|
||
decide(Lang::ZhCn, &only(&["PingFang SC"])),
|
||
Strategy::System {
|
||
family: "PingFang SC"
|
||
}
|
||
);
|
||
assert_eq!(
|
||
decide(Lang::ZhCn, &only(&["Noto Sans CJK SC", "Noto Sans SC"])),
|
||
Strategy::System {
|
||
family: "Noto Sans CJK SC"
|
||
}
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn chinese_without_cjk_fonts_emits_the_download_signal() {
|
||
// 抓得住:中文环境 + 系统无 CJK 时**给信号**(而不是静默回英文、也不是自己联网)。
|
||
// 抓不住:信号之后 UI 有没有把提示画出来(页面侧 / 截图验收)。
|
||
let strategy = decide(Lang::ZhCn, &only(&["Segoe UI", "Roboto", "Cascadia Mono"]));
|
||
let Strategy::NeedsDownload(font) = strategy else {
|
||
panic!("应当要求下载:{strategy:?}");
|
||
};
|
||
assert_eq!(font.family, "Noto Sans SC");
|
||
assert_eq!(font.license, "OFL-1.1");
|
||
assert_eq!(strategy.describe(), "cjk=needs-download(Noto Sans SC)");
|
||
}
|
||
|
||
/// pin 的形态守卫(M168):URL 走 https、哈希是 64 位小写十六进制、字节数在大小上限内。
|
||
///
|
||
/// 抓得住:①有人把 URL 改成 `@main` 这类会漂移的引用(不是不行,但那样哈希必须跟着变,
|
||
/// 这条断言至少逼他看见自己在动什么);②手抄哈希抄错一位(下一个人只会在真机上看到
|
||
/// 「校验失败」然后降级);③把三个静态子集塞进来却没改字节数。
|
||
/// 抓不住:CDN 上今天还发不发得出这份字节(要真联网,报告里写明未验)。
|
||
#[test]
|
||
fn the_pinned_source_is_well_formed() {
|
||
let Source::Pinned {
|
||
urls,
|
||
sha256,
|
||
bytes,
|
||
} = NOTO_SANS_SC.source;
|
||
assert!(!urls.is_empty(), "至少要有一个 URL");
|
||
for url in urls {
|
||
assert!(url.starts_with("https://"), "字体必须走 https:{url}");
|
||
assert!(
|
||
!url.contains("@main"),
|
||
"钉 commit 而不是会漂移的分支:{url}"
|
||
);
|
||
}
|
||
assert_eq!(sha256.len(), 64, "{sha256}");
|
||
assert!(
|
||
sha256.bytes().all(|byte| byte.is_ascii_hexdigit()),
|
||
"{sha256}"
|
||
);
|
||
assert_eq!(sha256, sha256.to_ascii_lowercase(), "哈希用小写:{sha256}");
|
||
assert!(bytes > 0 && bytes < 32 * 1024 * 1024, "{bytes}");
|
||
}
|
||
|
||
#[test]
|
||
fn download_fonts_cover_the_required_weights() {
|
||
// 抓得住:将来换了字体却没覆盖 400/500/700(M3 Expressive 的 emphasized 档要 700)。
|
||
// 抓不住:字重是可变轴还是静态子集(那要看下载 mission 选的文件,本 crate 只有元数据)。
|
||
for weight in REQUIRED_WEIGHTS {
|
||
assert!(
|
||
NOTO_SANS_SC.weights.contains(weight),
|
||
"缺 {weight} 档:{:?}",
|
||
NOTO_SANS_SC.weights
|
||
);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn ensure_only_touches_the_fetcher_when_download_is_needed() {
|
||
// 抓得住:英文环境/系统已有字体时**仍然调了下载器**(= 偷跑网络请求);
|
||
// 也抓得住下载失败被吞掉(错误必须原样透出)。
|
||
// 抓不住:真实现的网络行为(本 crate 没有网络实现,这正是不联网的证据)。
|
||
let idle = FakeFetcher::new(Ok(vec![1, 2, 3]));
|
||
assert_eq!(ensure(Strategy::NotNeeded, &idle), Ok(Fetched::NothingToDo));
|
||
assert_eq!(
|
||
ensure(Strategy::System { family: "SimSun" }, &idle),
|
||
Ok(Fetched::AlreadySystem { family: "SimSun" })
|
||
);
|
||
assert_eq!(ensure(Strategy::Unprobeable, &idle), Ok(Fetched::Unknown));
|
||
assert_eq!(idle.calls.get(), 0, "这三种情况都不许联网");
|
||
|
||
let ok = FakeFetcher::new(Ok(vec![9; 4]));
|
||
assert_eq!(
|
||
ensure(Strategy::NeedsDownload(&NOTO_SANS_SC), &ok),
|
||
Ok(Fetched::Downloaded(vec![9; 4]))
|
||
);
|
||
assert_eq!(ok.calls.get(), 1);
|
||
assert_eq!(ok.seen.get(), Some("Noto Sans SC"));
|
||
|
||
let broken = FakeFetcher::new(Err(FetchError::Network("超时".to_owned())));
|
||
assert_eq!(
|
||
ensure(Strategy::NeedsDownload(&NOTO_SANS_SC), &broken),
|
||
Err(FetchError::Network("超时".to_owned()))
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn strategy_describe_is_one_line_for_every_case() {
|
||
// 抓得住:日志片段里混进换行(启动日志是按行解析的)。
|
||
// 抓不住:这些片段是否真的被打印(app-iced 的启动路径在那边测)。
|
||
let cases = [
|
||
Strategy::NotNeeded,
|
||
Strategy::System { family: "SimSun" },
|
||
Strategy::NeedsDownload(&NOTO_SANS_SC),
|
||
Strategy::Unprobeable,
|
||
];
|
||
for case in cases {
|
||
let line = case.describe();
|
||
assert_eq!(line.lines().count(), 1, "{line}");
|
||
assert!(line.starts_with("cjk="), "{line}");
|
||
}
|
||
assert_eq!(Strategy::NotNeeded.describe(), "cjk=not-needed");
|
||
assert_eq!(Strategy::Unprobeable.describe(), "cjk=unprobeable");
|
||
}
|