fix(m168): 复审 r3 四项 —— 说准 panic 档、给两条修复补守卫、时限算术补洞
复审 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`)
This commit is contained in:
1 parent
b9009c0e61
commit
460da9330d
9 files changed
+231
-103
No files matched your search
@@ -39,9 +39,12 @@ fn cache_hit_never_touches_the_network() {
|
||||
|
||||
/// 守卫 ②:**sha256 不符时拒绝落盘**(正式路径上什么都不许留下)。
|
||||
///
|
||||
/// 抓得住:把复验换成「信引擎的」或把中间文件直接当成品换名——本用例会在 `cache_path` 存在
|
||||
/// 与「错误类别」两处同时红。
|
||||
/// 抓不住:引擎自身的流式校验(那是 `transfer` 的守卫)。
|
||||
/// **两层校验是互备的**(复审 r3 顺手项①:上一版这里写「只拆一层也会红」是假的,复审变异实测
|
||||
/// 单独拆掉引擎的 `with_sha256` 或单独短路本模块复验,本用例都**仍然绿**——另一层会接住)。
|
||||
/// 本用例真正抓的是**两层同时被拆**:那时坏字节会被当成字体返回、`cache_path` 也会落盘,断言在
|
||||
/// `expect_err` 处红(r1 的变异证明就是这么打的)。
|
||||
///
|
||||
/// 抓不住:单层的实现细节(比如 `verify::matches` 的大小写处理)——那要各自的单元用例。
|
||||
#[test]
|
||||
fn sha256_mismatch_is_refused_and_nothing_lands() {
|
||||
let dir = root("mismatch");
|
||||
@@ -187,14 +190,22 @@ fn a_trickling_candidate_is_cut_by_the_attempt_deadline() {
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
/// 守卫(M168 复审 r2 P2-3):**总时限**也会打断爬行,且文案说清「超时」、不落盘。
|
||||
/// 守卫(M168 复审 r3 P2-3):**最后一个候选拿整个剩余预算**(`is_last` 那一支)。
|
||||
///
|
||||
/// 抓得住:总预算被当成「每个候选各给一次」⇒ 本用例会跑到 600 × 100 ms 的爬行结束(≈60 s),
|
||||
/// `elapsed < 4 s` 红;也抓得住超时被归成其它类别(`class()` 必须是 `network`)。
|
||||
/// 抓不住:真网络上的取值合不合适(同上,见「未验」)。
|
||||
/// 形状是刻意挑的:**单候选** + `attempt = 300 ms` / `total = 5 s`。
|
||||
/// * 有 `is_last`:最后一个(也是唯一一个)候选拿到剩下的 ~5 s ⇒ 只在**总时限**处被打断,
|
||||
/// 耗时 ≈5 s;
|
||||
/// * 没有 `is_last`(一律 `attempt.min(remaining)`):300 ms 就切,耗时 ≈0.3 s。
|
||||
///
|
||||
/// 两种实现的解不同,于是这条用例能分辨它们——上一版给的是 `attempt=30 s > total=400 ms`,
|
||||
/// 两种实现同解,天生测不出差别(复审 r3 就是据此说 `is_last` 没人守)。
|
||||
///
|
||||
/// 抓得住:删掉 `is_last` 那一支(或把预算写成 `attempt_deadline.min(remaining)` 一视同仁)⇒
|
||||
/// `elapsed >= 3 s` 当场红;也抓得住服务端其实不发字节(`proof` 的两个断言)。
|
||||
/// 抓不住:真弱网下 60 s / 420 s 够不够(要真 CDN,见报告「未验」)。
|
||||
#[test]
|
||||
fn the_total_deadline_cuts_a_single_crawling_candidate() {
|
||||
let dir = root("deadline-total");
|
||||
fn the_last_candidate_gets_the_whole_remaining_budget() {
|
||||
let dir = root("deadline-last");
|
||||
let body = fake_font(b"not what the server sends");
|
||||
let (trickle_url, proof) = serve_trickle();
|
||||
let spec = FontSpec::new(vec![trickle_url], sha256(&body), body.len() as u64).expect("pin");
|
||||
@@ -203,24 +214,29 @@ fn the_total_deadline_cuts_a_single_crawling_candidate() {
|
||||
let error = fetch_blocking_with_limits(
|
||||
&dir,
|
||||
&spec,
|
||||
Duration::from_secs(30),
|
||||
Duration::from_millis(400),
|
||||
Duration::from_millis(300),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.expect_err("总时限必须打断爬行");
|
||||
.expect_err("爬行连接最终必须被总时限打断");
|
||||
let elapsed = started.elapsed();
|
||||
|
||||
assert_eq!(error.class(), "network", "超时属于网络档:{error}");
|
||||
assert!(
|
||||
error.message().contains("超时"),
|
||||
"文案要说得清是超时:{error}"
|
||||
);
|
||||
assert!(
|
||||
started.elapsed() < Duration::from_secs(4),
|
||||
"不该等到引擎的空闲超时:{:?}",
|
||||
started.elapsed()
|
||||
elapsed >= Duration::from_secs(3),
|
||||
"最后一个候选该拿整个剩余预算(≈5 s),而不是在 300 ms 处就被切:{elapsed:?}"
|
||||
);
|
||||
assert!(
|
||||
elapsed < Duration::from_secs(9),
|
||||
"不该超过总时限太多:{elapsed:?}"
|
||||
);
|
||||
assert!(!cache_path(&dir, &spec).exists(), "超时不许落盘");
|
||||
assert!(
|
||||
proof.dripped.load(Ordering::SeqCst) >= 1,
|
||||
"总时限也该掐在「有字节在动」的连接上,而不是一个不响的端口"
|
||||
"这条用例必须掐在「有字节在动」的连接上,而不是一个不响的端口"
|
||||
);
|
||||
std::fs::remove_dir_all(&dir).ok();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user