复审 P1-1(用户可见假文案):`LaunchEnd` 在 `failure == None`(跑起来之后崩的)时
临时造了 `Stage::Spawn + format!("exit {:?}")`,于是磁贴写「启动失败 · 启动游戏进程」、
详情页漏出 `exit Some(1)`、静默失败丢掉「exit 0 却没起来」,而同一时刻 Toast 说「崩溃」。
- 新增 `LaunchFailureCause{Stage|Crashed|SilentFailure}`(dto/launch_failure.rs),
`LaunchFailureCause::of(outcome, failure, exit_code)` 是唯一组装点;`InstanceRuntime::Failed{cause}`。
`LaunchFailureStage` 保持「只装装配阶段」(文档那句「崩溃是结局不是阶段」现在成立)。
- 界面:`InstancesFailed`(框架)+ `InstancesCrashed` + `InstancesSilentFailed`;
删掉 `InstancesFailedStage`。退出码按数字出词,不再有 Debug 文本。
- 用例:presenter 断言**枚举变体**(三种形状逐条)、app 断言**具体句子**
(「不许出现 Some(」「进程起来了不许说启动游戏进程」「静默失败要点出没起来」)。
mock 夹具与 launch_state_tests 的期望值同步;launch-error 三张图重拍。
复审 P2-1(`convert::failure_stage` 零覆盖):新增 `convert/stage_tests.rs` 5 条(三档 +
「既没回滚也没宣称干净」+ 非致命无阶段 + `failed_report_with` ⇒ NotStarted)。
变异实测:改成恒 `RolledBack` ⇒ 3 条红。
复审 P2-2(矮窗口用例的算式三个数全错、`body_height` 零测试):用例改成直接调
`frame::body_height`,chrome 从 `Ui::metrics` + `WidthClass::gutter` 现取,并把门槛与真实
chrome 绑死(chrome+MIN ⇒ Split、-1px ⇒ ScrollWhole);新增 `frame` 自己的 `body_height` 用例。
变异实测:`2.0 * gutter` → `gutter` ⇒ 2 条红。
复审 P2-3(源指纹闸门 / `Refused` 零覆盖):新增两条 core 用例——闸门(原地留着 prefs.json
+ 预先写好的同源收据 ⇒ Nothing 且收据未被重写,反证:源变了允许重放)、`Refused`
(`$schema` 1.7.0 ⇒ ReadOnly ⇒ 不动旧文件、不写收据、理由含「不可写」)。
变异实测:删闸门 ⇒ 1 条红;`can_save` 守卫改 `if false` ⇒ 1 条红。
顺手收:
- CLI `pack import` 那句「已整包回滚」也按三档出词(`RollbackStage` + `rollback_line`),
加 2 条单测。CLI 不依赖 presenter,判定暂与 presenter 各一份 → 已 file finding 建议下沉 core。
- 设置页 `status_text()` 抽出 applying 档,用例断言「正在安装…」且非错误色。
金表:新 key 加在 `golden.rs`(81 行),未动 `golden/tail.rs`(211 行),避免与 M162 的拆分冲突。
150 lines
6.2 KiB
Rust
150 lines
6.2 KiB
Rust
//! 启动失败/收尾的**文案**用例(从 `app/view/tests.rs` 拆出:那边 400 行到线)。
|
||
//!
|
||
//! 这里的断言都钉**具体句子**(不是「字符串非空」):磁贴与 Toast 必须说同一件事,
|
||
//! 「没起来」与「起来又崩」不许共用一句(M161 复审 P1-1)。
|
||
|
||
use super::*;
|
||
|
||
/// 失败后磁贴要留「重试」,且行/详情文案按「哪一步 + 原文」给。
|
||
#[test]
|
||
fn failed_instances_can_be_retried() {
|
||
// 进程**没起来**:说「哪一步 + 原文」。
|
||
let failed = instance(InstanceRuntime::Failed {
|
||
cause: unlucky_presenter::dto::LaunchFailureCause::Stage {
|
||
stage: unlucky_presenter::dto::LaunchFailureStage::Spawn,
|
||
detail: "os error 5".to_string(),
|
||
},
|
||
});
|
||
// 列表行是**单行**:只放「哪一步」(原文太长会被 `Wrapping::None` 裁成半截字)。
|
||
assert_eq!(
|
||
super::instances::runtime_status_row(&failed.runtime, Lang::En.catalog()).as_deref(),
|
||
Some("Launch failed · Starting the game process")
|
||
);
|
||
// 详情页给全文(阶段 + 原文)。
|
||
assert_eq!(
|
||
super::instances::runtime_status(&failed.runtime, Lang::En.catalog()).as_deref(),
|
||
Some("Launch failed · Starting the game process: os error 5")
|
||
);
|
||
assert!(
|
||
detail_of(&failed, Lang::En.catalog()).can_launch,
|
||
"失败后要能重试"
|
||
);
|
||
}
|
||
|
||
/// **「没起来」与「起来又崩」不许说成同一句**(复审 P1-1 的文案面)。
|
||
///
|
||
/// 抓得住:崩溃/静默失败被塞回 `LaunchFailureStage::Spawn`(磁贴会写「启动游戏进程失败」,
|
||
/// 而进程明明起来了);退出码漏成 `Some(1)` 这种 Debug 文本;静默失败丢掉
|
||
/// 「exit 0 却没跑起来」这条信息。断言的是**具体句子**,不是「字符串非空」。
|
||
#[test]
|
||
fn a_crash_after_start_is_not_worded_as_a_start_failure() {
|
||
use unlucky_presenter::dto::{LaunchFailureCause, LaunchFailureStage};
|
||
|
||
let line = |cause: LaunchFailureCause, lang: Lang| {
|
||
let runtime = InstanceRuntime::Failed { cause };
|
||
let row = super::instances::runtime_status_row(&runtime, lang.catalog()).expect("行文案");
|
||
let detail = super::instances::runtime_status(&runtime, lang.catalog()).expect("详情文案");
|
||
(row, detail)
|
||
};
|
||
|
||
// ① 没起来:说阶段。
|
||
let (row, detail) = line(
|
||
LaunchFailureCause::Stage {
|
||
stage: LaunchFailureStage::Spawn,
|
||
detail: "os error 5".to_string(),
|
||
},
|
||
Lang::En,
|
||
);
|
||
assert_eq!(row, "Launch failed · Starting the game process");
|
||
assert_eq!(
|
||
detail,
|
||
"Launch failed · Starting the game process: os error 5"
|
||
);
|
||
|
||
// ② 起来又崩:说结局 + 数字退出码(不许出现 `Some(1)`)。
|
||
let (row, detail) = line(LaunchFailureCause::Crashed { exit_code: Some(1) }, Lang::En);
|
||
assert_eq!(row, "Launch failed · the game crashed (exit 1)");
|
||
assert_eq!(detail, row, "结局类没有「原文」可加,行与详情同一句");
|
||
assert!(!detail.contains("Some("), "退出码不许漏 Debug:{detail}");
|
||
assert!(
|
||
!detail.contains("Starting the game process"),
|
||
"进程起来了,不许说「启动游戏进程」:{detail}"
|
||
);
|
||
|
||
// ③ 静默失败:exit 0 却没跑起来 —— 这条信息不能丢。
|
||
let (row, zh) = line(
|
||
LaunchFailureCause::SilentFailure { exit_code: Some(0) },
|
||
Lang::ZhCn,
|
||
);
|
||
assert_eq!(row, "启动失败 · 退出码 0,没起来");
|
||
assert_eq!(zh, row);
|
||
assert!(row.contains("没起来"), "{row}");
|
||
|
||
// ④ 平台没给退出码:只说崩溃,不编一个 0 出来。
|
||
let (row, _) = line(LaunchFailureCause::Crashed { exit_code: None }, Lang::En);
|
||
assert_eq!(row, "Launch failed · The game crashed");
|
||
}
|
||
|
||
///
|
||
/// 此前 `LaunchFinished.summary` 是 presenter 拼的中文,英文界面上弹的就是中文;
|
||
/// 现在 presenter 只给结构(结局 + 退出码 + 失败阶段),文案在这里按 key 现求。
|
||
///
|
||
/// 抓得住:①谁把中文串再写回 presenter(英文那一行会露中文);②失败时丢掉 `detail`
|
||
/// (原文是取证信息,不许翻也不许删);③「跑起来再崩」被说成「启动失败在某一步」。
|
||
#[test]
|
||
fn the_launch_toast_speaks_the_interface_language() {
|
||
use unlucky_presenter::dto::{
|
||
LaunchFailureStage, LaunchFailureView, LaunchFinished, LogOutcome,
|
||
};
|
||
|
||
let finished = |outcome, failure| LaunchFinished {
|
||
id: "survival".to_string(),
|
||
session: "20261031T120000-abcd12".to_string(),
|
||
outcome,
|
||
exit_code: Some(1),
|
||
uptime_ms: 12_500,
|
||
failure,
|
||
};
|
||
|
||
// 正常结束:只报结局 + 时长(`正常结束 · 12.5 秒` / `Finished normally · 12.5s`)。
|
||
let ok = finished(LogOutcome::Ok, None);
|
||
let en = super::instances::launch_toast(&ok, Lang::En);
|
||
let zh = super::instances::launch_toast(&ok, Lang::ZhCn);
|
||
assert_eq!(en, "Finished normally · 12.5s");
|
||
assert_eq!(zh, "正常结束 · 12.5 秒");
|
||
assert_ne!(en, zh, "切语言时提示语要变");
|
||
|
||
// 装配阶段失败:说清**哪一步**,并把原文原样带出来。
|
||
let failed = finished(
|
||
LogOutcome::Crashed,
|
||
Some(LaunchFailureView::new(
|
||
LaunchFailureStage::InstanceConfig,
|
||
"permission denied (os error 5)",
|
||
)),
|
||
);
|
||
let en = super::instances::launch_toast(&failed, Lang::En);
|
||
let zh = super::instances::launch_toast(&failed, Lang::ZhCn);
|
||
assert!(
|
||
en.starts_with("Launch failed · Reading the instance settings"),
|
||
"{en}"
|
||
);
|
||
assert!(zh.starts_with("启动失败 · 读实例设置"), "{zh}");
|
||
for text in [&en, &zh] {
|
||
assert!(
|
||
text.contains("permission denied (os error 5)"),
|
||
"原文(取证信息)不许丢:{text}"
|
||
);
|
||
}
|
||
// 英文界面里不许出现中文(反之亦然)——这条是本次债的核心。
|
||
assert!(!en.chars().any(is_cjk), "英文提示里混了中文:{en}");
|
||
assert!(
|
||
!zh.contains("Launch failed"),
|
||
"中文提示里混了英文前缀:{zh}"
|
||
);
|
||
}
|
||
|
||
/// 有没有中文字符(上面那条「不许混语言」的判据)。
|
||
fn is_cjk(ch: char) -> bool {
|
||
('\u{4e00}'..='\u{9fff}').contains(&ch)
|
||
}
|