fix(acceptance): M98 r1 —— [window] 绑定真 settings、语言维度补断言、boot.rs 拆分
复审 r1(reviewer-accscope-code,p2-2items / fix-then-merge)的两条阻塞 + 四条顺手的 p3。 ## p2-1 `[window]` 的三个窗口属性曾是格式串常量(U1:常量冒充观测结果) 第一版把 `resizable=true decorations=false transparent=false` 写在格式化串里,与交给 iced 的 `window::Settings` 没有任何绑定;定向变异把 `settings.decorations` 改成 `true`,**371 个测试全绿**, 而验收包里 `[caps]` 与 `[window]` 会互相矛盾。 - 新增 `acceptance::window::WindowFacts` + `from_settings()`:字段只能从**真实 settings** 抽出来, 没有手抄入口;`window_text()` 逐字段打印。 - 窗口设置收成单一产地 `app/boot/diagnostics::window_settings()`,`launch()` 与 `[window]`/`[caps]` 两行日志共用同一个 `&window::Settings`;`pages/harness.rs` 同样从自己那份 settings 取。 - 守门测试 `window_settings_match_what_the_acceptance_logs_claim`,含「from_settings 必须逐字段 搬运」的反向断言。变异实证:`window_settings` 的 `decorations → true` ⇒ 红并点名; `from_settings` 里写死 `decorations: false` ⇒ 红(第一次写测试时这条漏抓,补强后实测红)。 ## p2-2 语言维度空转、且没有任何结论断言 `lang::resolve` 是 setting → 平台源 → env,macOS 的 AppleLanguages 几乎恒有值 ⇒ `LANG=zh_CN` / `LANG=en_US` 两档同值,而原脚本只断言「`[i18n]` 这行存在」——整条链退化成 `source=default lang=en` 时核心集一条都不红。 - 脚本加两条结论断言:`^\[i18n\].*platform=applelanguages:[A-Za-z-]+` 与 ` source=applelanguages` (退化日志上实测红、健康日志上实测绿)。 - `REPORT.md` 语言行下注明:env 优先级低于平台源,正常 Mac 上两档同值,这一维验的是「平台源 真读到了 + 中文字体回退」,不是中英对照。 ## p3(复审 + 第二路自跑复核同源的三条) - `[perf] boot probe=0ms` → `probe=skipped`(没计时就不报数;降级重开那一轮改为**真计时**)。 变异实证:把 skipped 写成 0ms ⇒ `perf_lines_carry_measured_millis` 红。 - `[env] gpu=` 排序改成 `DiscreteGpu > IntegratedGpu > 其它`:本机原来报核显,而 iced 实际选的是 独显(现在报 `AMD Radeon RX 6600 XT [DiscreteGpu]`,与 `Selected:` 一致)。 - `[i18n] setting=<未设置>` → `<未接线>`(配置侧根本没读取点,写「未设置」等于把「我们没读」 说成「用户没设」)。 - `[backend] final=` 失败行:`refuse` / `none` 两条路是 `final=none`(什么都没跑起来),只有 `respawn` 才是兜底名;`pages/harness.rs` 不再写死 `via="refuse"`,改用共用的 `failure_via()`。 实跑负例:`[backend] final=none result=failed primary=wgpu fallback=tiny-skia via=refuse`。 - 脚本 `[tick]` 那处唯一未锚定的 `field()`:加 `^` 锚定、正则改 `clock=[a-z0-9-]+`(真值是 `idle-1hz`,旧 `[a-z]+` 把它截成 `idle`,靠前缀巧合通过);断言也改成 `clock=idle-1hz`。 - 脚本 `[backend] final` 取值改 `field_backend()`:优先 `result=started`(respawn 场景一条日志里 有父 failed + 子 started 两条,旧 `head -1` 会把「这次没跑起来」写进报告)。 - 脚本 `exec 3>&1` 全程留痕(终端提示也进 `logs/00-run.log`)、跑前清空 `$OUT/shots/`、 python3 缺失时在 REPORT 里写明「几何列未采」而不是留空。 - `renderer/mod.rs` 补一段依据:为什么「单项 ICED_BACKEND ⇒ 没有静默回退」成立(`iced_tiny_skia` 的 `with_backend` 只接受 `tiny-skia`),以及它依赖 iced 实现细节的两层缓解。 ## 结构 `app/boot.rs` 400 行零余量 → 拆成 `boot.rs`(248) + `boot/fallback.rs`(117,兜底/重开/退出码) + `boot/diagnostics.rs`(110,窗口设置与四段报告);`run()` 回到「探测 → 字体 → 打印 → 起 iced」。 `acceptance` 再把 `[window]` 拆成 `window.rs`(142)。 门禁:`cargo fmt --check` / `clippy --all-targets -- -D warnings` / `test`(373 lib + 6 integration)全绿。
This commit is contained in:
1 parent
f986981c12
commit
8153e54f87
10 files changed
+726
-385
No files matched your search
@@ -0,0 +1,117 @@
|
||||
//! 起步后端失败后的**兜底/重开**一簇:判据、进程环境交接、`respawn`、退出码透传。
|
||||
//!
|
||||
//! 从 `app/boot.rs` 拆出来的理由:这一簇**与启动编排零耦合**,但已有 5 条测试,而且它是
|
||||
//! M97 判据表第 8/9 行的落点。`boot.rs` 那边只负责「什么时候调它」。
|
||||
//!
|
||||
//! ## 为什么必须换进程(别改回进程内重试)
|
||||
//!
|
||||
//! Windows 实测本进程里再 `run()` 一次直接 panic
|
||||
//! (`iced_winit-0.14.1/src/lib.rs:81` → `Create event loop: RecreationAttempt`),winit 一个进程
|
||||
//! 只能建一次事件循环。
|
||||
|
||||
use crate::renderer::Decision;
|
||||
use crate::renderer::{self, acceptance, Kind};
|
||||
|
||||
/// 起步后端失败后的去向(判据表第 8 行 vs 第 9 行——**两条路策略不同,别合成一条**)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum OnFailure {
|
||||
/// 交互启动:安全网优先,换进程用这个后端重开。
|
||||
Respawn(Kind),
|
||||
/// 取证启动:红着退出,**不出图**。
|
||||
Refuse,
|
||||
}
|
||||
|
||||
/// 纯判据:起步后端失败时走哪条路。
|
||||
///
|
||||
/// `evidence` 非空 = 本次是取证启动(带 `--screenshot`);返回 `None` = 没有兜底可用
|
||||
/// (`--renderer=cpu` / 未编 wgpu),原样把错误交回去。抽成纯函数只为一个理由:这条分支以前
|
||||
/// **没有任何测试**,`respawn` / `Refuse` 两条路可以被静默改反(M97 r1 p2-1 就是这么测出来的)。
|
||||
pub(super) fn on_failure(
|
||||
decision: Decision,
|
||||
evidence: Option<&std::path::PathBuf>,
|
||||
) -> Option<OnFailure> {
|
||||
let fallback = decision.fallback?;
|
||||
Some(match evidence {
|
||||
Some(_) => OnFailure::Refuse,
|
||||
None => OnFailure::Respawn(fallback),
|
||||
})
|
||||
}
|
||||
|
||||
/// 降级重开要传给子进程的环境变量(**两条缺一不可**)。
|
||||
///
|
||||
/// ① `ICED_BACKEND`:让子进程直接用 `fallback` 跑。
|
||||
/// ② [`renderer::FALLBACK_ENV`]:**防无限重开的闸门**——子进程见它就必须跳过探测
|
||||
/// (`boot_probe` 的第一个分支),否则会算出同一个结论、再失败、再 respawn。
|
||||
///
|
||||
/// 抽成纯函数是为了让「漏传其中之一」变成**测试能抓到**的错误:两条都是静默失效型
|
||||
/// (漏 ① 重开又跑一遍 wgpu、漏 ② 无限重开),线上极难看出来。
|
||||
pub(super) fn fallback_env(fallback: Kind) -> [(&'static str, &'static str); 2] {
|
||||
[
|
||||
("ICED_BACKEND", fallback.env_value()),
|
||||
(renderer::FALLBACK_ENV, fallback.env_value()),
|
||||
]
|
||||
}
|
||||
|
||||
/// 安全网:起一个**新进程**用 `fallback` 后端重开,然后带着它的退出码退出。
|
||||
///
|
||||
/// 只在**交互启动**时走到这里(取证启动宁可红着退出也不换后端出图);子进程看到
|
||||
/// [`renderer::FALLBACK_ENV`] 必须跳过探测,否则无限递归重开(闸门在 `boot_probe` 入口)。
|
||||
pub(super) fn respawn(failed: Kind, fallback: Kind, error: &str) -> ! {
|
||||
let exe = std::env::current_exe().unwrap_or_else(|err| {
|
||||
eprintln!("[gpu] 降级重开失败:拿不到自身路径({err})");
|
||||
std::process::exit(exit_code(None));
|
||||
});
|
||||
eprintln!(
|
||||
"[gpu] 降级 → {}({} 已失败,原因见上两行;换一个进程重开:winit 不允许同进程建两次事件循环)。\
|
||||
下一次的日志从 `[probe] 本次是降级重开` 开始。",
|
||||
fallback.name(),
|
||||
failed.name()
|
||||
);
|
||||
// `[backend] final` 一行把「哪个后端失败了、去了哪、为什么」收在一起(可 grep 的单一结论行)。
|
||||
// 这里 `final=` 装的是**兜底**(进程即将带着它重开);`via=respawn` 说明它是换进程去的。
|
||||
acceptance::report_backend_failed(failed, Some(fallback), Some(fallback), "respawn", error);
|
||||
|
||||
let status = fallback_command(&exe, std::env::args_os().skip(1), fallback).status();
|
||||
|
||||
// 退出码**透传**子进程的:上层脚本/CI 靠它判断成败。写错的失效方式是「安静地给出一个错
|
||||
// 结论」——不崩、不静默降级、不无限重开,只是让 CI 把失败当成功。
|
||||
let code = match status {
|
||||
Ok(status) => exit_code(status.code()),
|
||||
Err(err) => {
|
||||
eprintln!("[gpu] 降级重开失败:{err}");
|
||||
exit_code(None)
|
||||
}
|
||||
};
|
||||
std::process::exit(code)
|
||||
}
|
||||
|
||||
/// 子进程退出码 → 本进程退出码。
|
||||
///
|
||||
/// `None` = 子进程被信号杀死(只有 Unix):走兜底码而**不是** panic、也不是 0——上层判据是
|
||||
/// 「非零 = 失败」,「拿不到退出码」和「退出码是某个非零值」对脚本是一回事。
|
||||
pub(super) fn exit_code(code: Option<i32>) -> i32 {
|
||||
/// 重开失败 / 拿不到退出码时的兜底码(与 `main` 的「参数或运行错误」一致)。
|
||||
const RESPAWN_FAILED: i32 = 2;
|
||||
code.unwrap_or(RESPAWN_FAILED)
|
||||
}
|
||||
|
||||
/// 构造降级重开的子进程命令(**可测**:program / argv / env 都能在测试里断言)。
|
||||
///
|
||||
/// `program` / `args` 是参数而不是在函数里读 `std::env`,唯一目的是让测试能喂假命令
|
||||
/// (`cmd /c exit 7` / `sh -c "exit 7"`)来验退出码透传——真用 `current_exe()` 就没法测。
|
||||
///
|
||||
/// 三路 stdio **显式**继承:子进程日志必须和父进程同一路输出,否则验收只抓到父进程那两行具名
|
||||
/// 错误。`Command` 没有 stdio 的 getter(stable 没有),所以这条只能靠真跑一次证明。
|
||||
pub(super) fn fallback_command(
|
||||
program: &std::path::Path,
|
||||
args: impl IntoIterator<Item = std::ffi::OsString>,
|
||||
fallback: Kind,
|
||||
) -> std::process::Command {
|
||||
let mut command = std::process::Command::new(program);
|
||||
command.args(args).envs(fallback_env(fallback));
|
||||
command
|
||||
.stdin(std::process::Stdio::inherit())
|
||||
.stdout(std::process::Stdio::inherit())
|
||||
.stderr(std::process::Stdio::inherit());
|
||||
command
|
||||
}
|
||||
Reference in new issue
Block a user