fix(renderer): M97 r1 —— 降级/取证两条路改成可被变异测试抓到的纯函数
复审在 `git archive 72f20d8` 上做了变异:删掉 `respawn` 的
`.env("ICED_BACKEND", fallback.env_value())`,**327 passed / 0 failed**,那条名为
`retry_fallback_flips_the_tier_to_cpu` 的测试照样绿。
名字说它守住了「降级后档位翻转」,实际它只把 `"wgpu"` / `"tiny-skia"` 两个字面量喂给
`backend_from_env` 和 `effective_panel_alpha`,**`respawn` 一行都没执行**(它 `-> !`,
走 `process::exit`)。这和 M97 要修的那行常量日志是同一个结构:**在需要结果的地方验了旁边的东西**。
## 改了什么
1. `respawn` 的两组 env 抽成纯函数 `fallback_env(fallback)`,返回值是
**`[(&'static str, &'static str); 2]`**——数量由类型钉住(删一条直接编译不过,
比测试更早的闸门),键与值由测试钉住。
2. 子进程那一轮的决策抽成 `renderer::decision::fallback_round(kind)`,**兜底恒为 `None`**
(防无限递归重开),`boot_probe` 不再就地拼 `Decision`。
3. 失败分流抽成纯函数 `on_failure(decision, evidence)` → `Respawn(Kind) | Refuse`:
交互降级 / 取证拒绝两条路各自被测试钉住(判据表第 8 行 vs 第 9 行)。
4. 补上复审点名的两处 0 覆盖:`renderer::strict`(剥兜底)与 `renderer::run_once`
(传下去的是 `decision.primary`,且进程内只跑一次)。
5. 改名并改文档 `retry_fallback_flips_the_tier_to_cpu` →
`each_backend_value_maps_to_its_tier_and_alpha`,明确写「本测试**不**守翻转」,
并指到真正守它的那条。
## 变异验证(8/8 被抓到,全部在实现行上做,测试里的同一字面量不动)
| 变异 | 结果 |
|---|---|
| M1 删掉 `ICED_BACKEND` 那条(=复审的变异) | **编译期**红:`expected an array with a size of 2, found one with a size of 1` |
| M2 键打错 `ICED_BAKEND` | `fallback_env_carries_the_backend_and_the_gate` FAILED |
| M3 闸门键打错 `ULL_GPU_FALLBACK_TYPO` | 同上 FAILED |
| M4 闸门值写死(不跟 fallback 走) | 同上 FAILED |
| M5 `fallback_round` 又给兜底 | `fallback_round_never_degrades_again` FAILED |
| M6 取证启动改成也降级 | `evidence_start_refuses_instead_of_degrading` FAILED |
| M7 `strict` 忘了清兜底 | `strict_strips_the_fallback` FAILED |
| M8 `run_once` 传死值 | `run_once_passes_the_decision_primary` FAILED |
复原后 `cargo test -p unlucky-app-iced --lib` = 334 passed。第一次跑变异时 A/B/C 三个
「照样绿」是我脚本自己的 bug:`sed` 把测试期望里的同一字面量一起改了(自指式变异盲区),
改成只在函数体内替换才暴露真实结果——这条也记进报告。
## 顺带(p3-1)
`boot.rs` 模块文档里「字体栈不重算」是旧实现的残留:改成换进程之后,子进程会把字体栈、
配置、presenter 全部重算(实测首帧 493~531 ms),文档已按实际行为重写,并把判据表
第 8/9 行两条路的差异写进模块头部。
运行时行为未变(refactor 只做抽取),已复测:取证启动 + wgpu 不可用 → exit 1、无 PNG、
具名错误 + 「拒绝降级出图」;交互启动 → 具名错误 + 换进程降级 + `[probe] 本次是降级重开`。
This commit is contained in:
1 parent
31cdfd5bbd
commit
fa9dd170d3
4 files changed
+243
-29
No files matched your search
@@ -152,20 +152,26 @@ mod tests {
|
||||
/// 这是修之前做不到的一条:那时变量停在 `wgpu,tiny-skia`,静默回退到 tiny-skia 之后
|
||||
/// 判据仍然读 `Gpu` ⇒ 半透明在 CPU 渲染器上照开。现在 `app::boot` 重开前会改写这个值,
|
||||
/// 这条测试钉的是「改写成 tiny-skia 之后判据确实翻转」。
|
||||
/// **两个后端取值各自对应哪个档位、面板 α 各是多少。**
|
||||
///
|
||||
/// ⚠️ 这条**不**守「降级后档位翻转」——它只是把两个字面量喂给纯函数,`respawn` 一行都没执行。
|
||||
/// 复审 M97 r1 p2-1 就是靠这一点把 `respawn` 里的 `.env("ICED_BACKEND", …)` 删掉而测试全绿的。
|
||||
/// 真正的翻转由 `app::boot::tests::fallback_env_carries_the_backend_and_the_gate`
|
||||
/// (respawn 到底传了什么)钉住——那条红了才是「翻转坏了」。
|
||||
#[test]
|
||||
fn retry_fallback_flips_the_tier_to_cpu() {
|
||||
fn each_backend_value_maps_to_its_tier_and_alpha() {
|
||||
use crate::pages::appearance::alpha::effective_panel_alpha;
|
||||
|
||||
let before = backend_from_env(Backend::Auto, Some("wgpu"));
|
||||
assert_eq!(before, Backend::Gpu);
|
||||
assert_eq!(effective_panel_alpha(before, 0.55), 0.55);
|
||||
let gpu = backend_from_env(Backend::Auto, Some("wgpu"));
|
||||
assert_eq!(gpu, Backend::Gpu);
|
||||
assert_eq!(effective_panel_alpha(gpu, 0.55), 0.55);
|
||||
|
||||
let after = backend_from_env(Backend::Auto, Some("tiny-skia"));
|
||||
assert_eq!(after, Backend::Cpu);
|
||||
let cpu = backend_from_env(Backend::Auto, Some("tiny-skia"));
|
||||
assert_eq!(cpu, Backend::Cpu);
|
||||
assert_eq!(
|
||||
effective_panel_alpha(after, 0.55),
|
||||
effective_panel_alpha(cpu, 0.55),
|
||||
1.0,
|
||||
"D36:真降级到 CPU 后,面板 α 必须强制 1.0"
|
||||
"D36:CPU 档面板 α 强制 1.0"
|
||||
);
|
||||
}
|
||||
}
|
||||
+158
-20
@@ -1,13 +1,22 @@
|
||||
//! 启动:后端探测 → 字体求值 → `iced::application(..).run()`。
|
||||
//!
|
||||
//! ## M97:`run()` 为什么要重试
|
||||
//! ## M97:起步后端起不来时怎么办(两条路,策略不同,别合成一条)
|
||||
//!
|
||||
//! `ICED_BACKEND` 现在永远是**单项**(见 [`crate::renderer`] 的判据表)。单项的代价是
|
||||
//! iced 不再自己做回退,于是「wgpu 起不来」会变成 `iced::Error::GraphicsCreationFailed`
|
||||
//! ——**这正是我们要的**:那里面装着 wgpu 的具名错误(哪个 backend、卡在 adapter 还是 device)。
|
||||
//! 拿到它以后打印、然后换 `tiny-skia` 重开一次,安全网照样在,但降级不再是静默的。
|
||||
//!
|
||||
//! 重试只在起步项真的失败时发生,而且**只重试一次**;字体栈不重算(那是启动最贵的一段)。
|
||||
//! 拿到具名错误之后分两条路(判据表第 8 行 / 第 9 行):
|
||||
//!
|
||||
//! - **交互启动**:换一个**进程**用 `tiny-skia` 重开([`OnFailure::Respawn`])——安全网,用户还能用。
|
||||
//! 代价是子进程把启动流程**重跑一遍**(重新探测?不:`ULL_GPU_FALLBACK` 让它跳过探测;
|
||||
//! 但**字体栈、配置、presenter 都会重算**,实测首帧 531 ms)。这条代价只落在失败路径上。
|
||||
//! - **取证启动**(带 `--screenshot`,含 `--screen=gallery`):**拒绝出图**、退出码 1
|
||||
//! ([`OnFailure::Refuse`])——换了后端出的图会让报告撒谎。
|
||||
//!
|
||||
//! 为什么必须是「换进程」而不是「在本进程里再 `run()` 一次」:Windows 实测直接 panic
|
||||
//! (`iced_winit-0.14.1/src/lib.rs:81` → `Create event loop: RecreationAttempt`)——
|
||||
//! winit 一个进程只能建一次事件循环。猜测式重试别再加回来。
|
||||
|
||||
use std::time::Instant;
|
||||
|
||||
@@ -52,19 +61,16 @@ pub(crate) fn run(options: Options) -> iced::Result {
|
||||
}
|
||||
let flavor = options.capture_flavor().unwrap_or(SchemeFlavor::Light);
|
||||
|
||||
// 起步后端起不来时**换一个进程**重开(winit 不允许在本进程重建事件循环,见
|
||||
// `renderer::run_once` 的文档)。具名原因在换进程之前就打到同一个 stderr 上了。
|
||||
// 起步后端起不来时按判据表第 8/9 行分流(交互降级 vs 取证拒绝)。具名原因在分流之前
|
||||
// 就打到同一个 stderr 上了。
|
||||
match launch(&options, families, flavor, decision.primary) {
|
||||
Err(iced::Error::GraphicsCreationFailed(error)) => {
|
||||
renderer::report_failure(decision.primary, &error);
|
||||
match decision.fallback {
|
||||
// 交互启动:安全网优先,降级重开,用户还能用。
|
||||
Some(fallback) if options.screenshot.is_none() => {
|
||||
respawn(decision.primary, fallback)
|
||||
}
|
||||
// 取证启动(`--screenshot`):**拒绝降级出图**。图是要拿去下结论的,
|
||||
// 换了后端出的图会撒谎——以前就是这么把 CPU 截图当 GPU 结论用的。
|
||||
Some(_) => {
|
||||
match on_failure(decision, options.screenshot.as_ref()) {
|
||||
Some(OnFailure::Respawn(fallback)) => respawn(decision.primary, fallback),
|
||||
Some(OnFailure::Refuse) => {
|
||||
// 取证启动(`--screenshot`):**拒绝降级出图**。图是要拿去下结论的,
|
||||
// 换了后端出的图会撒谎——以前就是这么把 CPU 截图当 GPU 结论用的。
|
||||
eprintln!(
|
||||
"[gpu] 拒绝降级出图:本次是取证启动(--screenshot {}),换后端出的图会撒谎。",
|
||||
options
|
||||
@@ -84,6 +90,45 @@ pub(crate) fn run(options: Options) -> iced::Result {
|
||||
}
|
||||
}
|
||||
|
||||
/// 起步后端失败后的去向(判据表第 8 行 vs 第 9 行——**两条路策略不同,别合成一条**)。
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum OnFailure {
|
||||
/// 交互启动:安全网优先,换进程用这个后端重开。
|
||||
Respawn(Kind),
|
||||
/// 取证启动:红着退出,**不出图**。
|
||||
Refuse,
|
||||
}
|
||||
|
||||
/// 纯判据:起步后端失败时走哪条路。
|
||||
///
|
||||
/// `evidence` 非空 = 本次是取证启动(带 `--screenshot`,含 `--screen=gallery`),路径只用于日志。
|
||||
/// 返回 `None` = 没有兜底可用(`--renderer=cpu` / 未编 wgpu),原样把错误交回去。
|
||||
///
|
||||
/// 抽成纯函数只为一个理由:这条分支以前**没有任何测试**,`respawn` / `Refuse` 两条路可以被
|
||||
/// 静默改反(复审 M97 r1 p2-1 就是这么测出来的)。
|
||||
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、漏 ② 会无限重开),线上都极难看出来。
|
||||
fn fallback_env(fallback: Kind) -> [(&'static str, &'static str); 2] {
|
||||
[
|
||||
("ICED_BACKEND", fallback.env_value()),
|
||||
(renderer::FALLBACK_ENV, fallback.env_value()),
|
||||
]
|
||||
}
|
||||
|
||||
/// 安全网:起一个**新进程**用 `fallback` 后端重开,然后带着它的退出码退出。
|
||||
///
|
||||
/// 为什么必须是新进程:winit 一个进程只能建一次事件循环
|
||||
@@ -112,8 +157,7 @@ fn respawn(failed: Kind, fallback: Kind) -> ! {
|
||||
|
||||
let status = std::process::Command::new(exe)
|
||||
.args(std::env::args().skip(1))
|
||||
.env(renderer::FALLBACK_ENV, fallback.env_value())
|
||||
.env("ICED_BACKEND", fallback.env_value())
|
||||
.envs(fallback_env(fallback))
|
||||
.status();
|
||||
|
||||
// 退出码**透传**子进程的:上层脚本/CI 靠它判断这次到底是成功还是失败。
|
||||
@@ -179,13 +223,10 @@ fn launch(
|
||||
/// 渲染后端探测 + 写 `ICED_BACKEND`,返回判据产物(供 `run` 决定要不要换进程重开)。
|
||||
fn boot_probe(options: &Options) -> Decision {
|
||||
// 本进程是「上一步失败后重开」的那一次:别再探一次(会得到同一个结论,然后无限重开)。
|
||||
// 这一步的决策**兜底必须是 None**——由 `decision::fallback_round` 单点保证。
|
||||
if let Some(kind) = renderer::forced_fallback() {
|
||||
let decision = renderer::fallback_round(kind);
|
||||
std::env::set_var("ICED_BACKEND", kind.env_value());
|
||||
let decision = Decision {
|
||||
primary: kind,
|
||||
fallback: None,
|
||||
reason: "fallback-after-failure",
|
||||
};
|
||||
eprintln!(
|
||||
"[probe] 本次是降级重开({}={}):跳过探测 -> ICED_BACKEND={}",
|
||||
renderer::FALLBACK_ENV,
|
||||
@@ -231,3 +272,100 @@ fn default_font(families: Families) -> iced::Font {
|
||||
..iced::Font::DEFAULT
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::renderer::decision;
|
||||
|
||||
fn probe(device_type: &'static str) -> crate::Probe {
|
||||
crate::Probe {
|
||||
adapters: vec![crate::probe::AdapterSummary {
|
||||
name: "adapter".to_owned(),
|
||||
device_type,
|
||||
}],
|
||||
hardware: !matches!(device_type, "Cpu" | "Other"),
|
||||
}
|
||||
}
|
||||
|
||||
/// **降级重开到底传了什么**(M97 r1 p2-1 的守门)。
|
||||
///
|
||||
/// 抓得住:`fallback_env` 少写/写错任一条——两条都是静默失效型:
|
||||
/// 漏 `ICED_BACKEND` ⇒ 子进程又去试 wgpu(白开一次窗);漏 `ULL_GPU_FALLBACK` ⇒ **无限重开**。
|
||||
/// 抓不住:`Command` 本身没被建起来(那要真起进程,见 §「Windows 实测」)。
|
||||
#[test]
|
||||
fn fallback_env_carries_the_backend_and_the_gate() {
|
||||
for fallback in [Kind::TinySkia, Kind::Wgpu] {
|
||||
let env = fallback_env(fallback);
|
||||
assert_eq!(
|
||||
env.len(),
|
||||
2,
|
||||
"就这两条,多一条少一条都要在这里改文档:{env:?}"
|
||||
);
|
||||
assert!(
|
||||
env.contains(&("ICED_BACKEND", fallback.env_value())),
|
||||
"子进程必须直接用 {fallback:?} 跑,实际 {env:?}"
|
||||
);
|
||||
assert!(
|
||||
env.contains(&(renderer::FALLBACK_ENV, fallback.env_value())),
|
||||
"缺防无限重开的闸门 {FALLBACK_ENV:?},实际 {env:?}",
|
||||
FALLBACK_ENV = renderer::FALLBACK_ENV
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// 闸门的另一半:子进程那一轮的决策**不能再有兜底**,否则失败后继续 respawn ⇒ 无限递归。
|
||||
///
|
||||
/// 抓得住:`fallback_round` 的 `fallback` 被写成 `Some(..)`(自杀式循环)。
|
||||
/// 抓不住:`forced_fallback()` 没被 `boot_probe` 调用(那是环境变量的读,见下一条的说明)。
|
||||
#[test]
|
||||
fn fallback_round_never_degrades_again() {
|
||||
for kind in [Kind::TinySkia, Kind::Wgpu] {
|
||||
let decision = decision::fallback_round(kind);
|
||||
assert_eq!(decision.primary, kind);
|
||||
assert_eq!(
|
||||
decision.fallback, None,
|
||||
"降级重开那一次不许再有兜底,否则会无限递归重开"
|
||||
);
|
||||
assert_eq!(decision.reason, "fallback-after-failure");
|
||||
}
|
||||
}
|
||||
|
||||
/// 判据表第 8 行 vs 第 9 行:**交互降级 / 取证拒绝**,两条路的策略不同。
|
||||
///
|
||||
/// 抓得住:`--screenshot` 分支被改回「也降级」(那正是 M97 要杀的「取证工具自己换后端」)。
|
||||
/// 抓不住:`screenshot` 传进来的时机(那是 `run()` 的接线,见 Windows 实测记录)。
|
||||
#[test]
|
||||
fn evidence_start_refuses_instead_of_degrading() {
|
||||
let auto = decision::decide(crate::Backend::Auto, &probe("IntegratedGpu"), true);
|
||||
let evidence = std::path::PathBuf::from("out.png");
|
||||
|
||||
assert_eq!(
|
||||
on_failure(auto, Some(&evidence)),
|
||||
Some(OnFailure::Refuse),
|
||||
"取证启动 + wgpu 起不来 ⇒ 拒绝出图"
|
||||
);
|
||||
assert_eq!(
|
||||
on_failure(auto, None),
|
||||
Some(OnFailure::Respawn(Kind::TinySkia)),
|
||||
"交互启动 + wgpu 起不来 ⇒ 换进程降级"
|
||||
);
|
||||
}
|
||||
|
||||
/// 没有兜底可用的两行(强制 CPU / 未编 wgpu):失败就原样把错误交回去,不许 respawn。
|
||||
#[test]
|
||||
fn no_fallback_means_no_respawn() {
|
||||
let cpu = decision::decide(crate::Backend::Cpu, &probe("IntegratedGpu"), true);
|
||||
let evidence = std::path::PathBuf::from("out.png");
|
||||
assert_eq!(cpu.fallback, None);
|
||||
assert_eq!(on_failure(cpu, None), None);
|
||||
assert_eq!(
|
||||
on_failure(cpu, Some(&evidence)),
|
||||
None,
|
||||
"强制 CPU 也不该走到「拒绝降级」那条提示上(它压根没想上 GPU)"
|
||||
);
|
||||
|
||||
let not_compiled = decision::decide(crate::Backend::Auto, &probe("IntegratedGpu"), false);
|
||||
assert_eq!(on_failure(not_compiled, None), None);
|
||||
}
|
||||
}
|
||||
@@ -134,6 +134,20 @@ pub fn demote_after_preflight(decision: Decision) -> Decision {
|
||||
}
|
||||
}
|
||||
|
||||
/// 降级重开那一次的决策(判据表第 8 行的**子进程侧**,`boot_probe` 的第一分支用它)。
|
||||
///
|
||||
/// **`fallback` 必须是 `None`**:这一步的兜底就是「不再兜底」。若它非空,子进程再失败时
|
||||
/// 还会 respawn ⇒ **无限递归重开**(每次重开都像「刚刚启动」,这种自杀式循环极难诊断)。
|
||||
/// 所以这条不变量只在这里定义一次,并由 `decision::tests` 钉住,别在调用点另拼一个 `Decision`。
|
||||
#[must_use]
|
||||
pub fn fallback_round(kind: Kind) -> Decision {
|
||||
Decision {
|
||||
primary: kind,
|
||||
fallback: None,
|
||||
reason: "fallback-after-failure",
|
||||
}
|
||||
}
|
||||
|
||||
/// 取证通道(`--screen=gallery`)用:**剥掉兜底**。
|
||||
///
|
||||
/// 硬规则(M97 定):**取证工具宁可红着退出,也不能悄悄换一种后端继续出图**。
|
||||
@@ -294,6 +308,18 @@ mod tests {
|
||||
assert_eq!(demote_after_preflight(cpu), cpu, "强制 CPU 不许被预检改写");
|
||||
}
|
||||
|
||||
/// 取证通道的决策必须**剥掉兜底**(判据表第 9 行):`strict` 若忘了清 `fallback`,
|
||||
/// gallery 就能在 wgpu 挂掉时悄悄出 CPU 图——那是 M97 要杀的第九种「假绿」。
|
||||
#[test]
|
||||
fn strict_strips_the_fallback() {
|
||||
let gpu = decide(Backend::Gpu, &Probe::default(), true);
|
||||
assert_eq!(gpu.fallback, Some(Kind::TinySkia), "前提:普通决策有兜底");
|
||||
let strict = strict(gpu);
|
||||
assert_eq!(strict.fallback, None, "取证通道不许有兜底");
|
||||
assert_eq!(strict.primary, Kind::Wgpu, "剥的是兜底,不是起步项");
|
||||
assert_eq!(strict.reason, "gallery-strict");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn describe_names_the_backend_and_the_reason() {
|
||||
let hardware = probe(&["DiscreteGpu"]);
|
||||
|
||||
@@ -125,7 +125,7 @@ pub mod decision;
|
||||
pub mod log_bridge;
|
||||
pub mod preflight;
|
||||
|
||||
pub use decision::{decide, demote_after_preflight, strict, Decision, Kind};
|
||||
pub use decision::{decide, demote_after_preflight, fallback_round, strict, Decision, Kind};
|
||||
|
||||
/// 把 `iced::Error` 里的**具名**图形错误掏出来(`Display` 那句笼统的话不算原因)。
|
||||
///
|
||||
@@ -185,3 +185,47 @@ pub fn report_failure(backend: Kind, error: &(impl std::fmt::Display + std::fmt:
|
||||
eprintln!("[gpu] {} 初始化失败(具名):{error}", backend.name());
|
||||
eprintln!("[gpu] 失败详情(具名):{error:?}");
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{Backend, Probe};
|
||||
|
||||
/// `run_once` **必须**把决策里的起步后端传下去。
|
||||
///
|
||||
/// 抓得住:`run_once` 被改成传死值(例如恒 `Kind::Wgpu`)——那会让「强制 CPU」也去试 GPU。
|
||||
/// 抓不住:真 iced 启动成功/失败(那条只能真跑,见判据表第 6/8/9 行的实测记录)。
|
||||
#[test]
|
||||
fn run_once_passes_the_decision_primary() {
|
||||
for request in [Backend::Cpu, Backend::Gpu] {
|
||||
let decision = decide(request, &Probe::default(), true);
|
||||
let mut seen = None;
|
||||
let result = run_once(decision, |kind| {
|
||||
seen = Some(kind);
|
||||
Ok(())
|
||||
});
|
||||
assert!(result.is_ok(), "闭包返回 Ok 时 run_once 必须原样返回");
|
||||
assert_eq!(
|
||||
seen,
|
||||
Some(decision.primary),
|
||||
"run_once 传给 iced 的后端必须是决策里的起步项"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// `run_once` 只跑一次:失败也**不**在进程内重试(winit 只允许建一次事件循环)。
|
||||
///
|
||||
/// 抓得住:谁把「进程内重试」加回 `run_once`(会 panic:`RecreationAttempt`)。
|
||||
/// 抓不住:`respawn` 那条换进程的路(见 `app::boot::tests`)。
|
||||
#[test]
|
||||
fn run_once_never_retries_in_process() {
|
||||
let mut calls = 0;
|
||||
let _ = run_once(decide(Backend::Gpu, &Probe::default(), true), |_| {
|
||||
calls += 1;
|
||||
Err(iced::Error::WindowCreationFailed(Box::new(
|
||||
std::io::Error::other("测试用"),
|
||||
)))
|
||||
});
|
||||
assert_eq!(calls, 1, "进程内只许跑一次;重试只能换进程");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user