test(launch): M298 用例搬到 dialog/tests/m298.rs(守 400 行单文件门禁)
This commit is contained in:
1 parent
97768b7e3f
commit
aa25583806
2 files changed
+196
-185
No files matched your search
@@ -0,0 +1,192 @@
|
||||
//! M298 的收尾失败 / 并发守卫用例(从 `dialog/tests.rs` 拆出:那个文件贴 400 行硬线)。
|
||||
//!
|
||||
//! 共用父模块的夹具:`headless()`(注入式 `App`)、`report()`(一份现场)、`slot_guard()`
|
||||
//! (`crash_report` 的现场是进程级单槽,凡 publish 的用例都要拿它)。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_core::launch::crash_report::{self, CrashReport, ReportContext, SessionAttribution};
|
||||
use unlucky_presenter::dto::{LaunchFailureStage, LaunchFailureView, LaunchFinished, LogOutcome};
|
||||
|
||||
use super::{headless, report, slot_guard};
|
||||
|
||||
/// 一次启动的现场上下文(与 presenter 里那份同形:实例 / 运行目录 / 日志目录)。
|
||||
fn context() -> ReportContext {
|
||||
ReportContext::new(
|
||||
"survival",
|
||||
"生存",
|
||||
"1.20.4",
|
||||
std::path::Path::new("/instances/survival"),
|
||||
std::path::Path::new("/logs"),
|
||||
std::time::SystemTime::now(),
|
||||
"os=windows",
|
||||
)
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------ M298:收尾失败必须弹
|
||||
|
||||
/// **真收尾失败必须弹出**(M298 修的那条静默漏弹)。
|
||||
///
|
||||
/// 出口形状取自 `crates/presenter/src/host/launch.rs` 的 `finish()` 失败分支,逐字段对齐:
|
||||
/// 进程**起来过**(有会话)、`finish()` 报错 ⇒ `outcome = SilentFail` + `failure = Some(Spawn)`。
|
||||
/// 现场由 core 的 `CrashReport::before_start` 登记,会话归因 = 收尾事件里那个会话。
|
||||
///
|
||||
/// ⚠️ 这一条出口**拿不到退出码**:`finish()` 只在「等进程出错」或「看门狗放弃等待」时失败,
|
||||
/// 两种都没有 `ExitStatus`(`pending_ms` 那条路径进程还在跑)。所以「exit_code 有值」在本出口上
|
||||
/// 不成立,判据改用「报告带会话 + 进程起来过」;带退出码的那条出口见下一条用例。
|
||||
///
|
||||
/// **变异证明**:把 `CrashReport::before_start` 的 `session` 改回写死空串(本 mission 之前的
|
||||
/// 写法),`open_failure_dialog` 的并发守卫成立并 `return` ⇒ 本用例第一段立刻红。
|
||||
#[test]
|
||||
fn a_finish_failure_pops_the_dialog_with_its_own_session() {
|
||||
let _slot = slot_guard();
|
||||
let mut app = headless();
|
||||
let log = PathBuf::from("/sessions/game/9f2c.jsonl");
|
||||
crash_report::publish(CrashReport::before_start(
|
||||
&context(),
|
||||
"spawn",
|
||||
"会话写入器不可用(锁中毒)",
|
||||
"spawn",
|
||||
Some(SessionAttribution {
|
||||
id: "9f2c",
|
||||
log: Some(log.as_path()),
|
||||
}),
|
||||
));
|
||||
let finished = LaunchFinished {
|
||||
id: "survival".to_string(),
|
||||
session: "9f2c".to_string(),
|
||||
outcome: LogOutcome::SilentFail,
|
||||
exit_code: None,
|
||||
uptime_ms: 0,
|
||||
failure: Some(LaunchFailureView::new(
|
||||
LaunchFailureStage::Spawn,
|
||||
"会话写入器不可用(锁中毒)",
|
||||
)),
|
||||
};
|
||||
app.open_failure_dialog(&finished);
|
||||
assert!(
|
||||
app.state.dialogs.current().is_some(),
|
||||
"真收尾失败必须弹框——用户原话是「找半天没找到崩溃日志」"
|
||||
);
|
||||
let card = app.dialog_card.as_ref().expect("卡片");
|
||||
assert_eq!(
|
||||
card.report.session, finished.session,
|
||||
"弹的是**这一场**的现场"
|
||||
);
|
||||
// 现场带着会话 ⇒ 事件流路径必须出现在可复制的路径行里(不是「进程没起来」那一套)。
|
||||
let modal = app.dialog_modal().expect("模型");
|
||||
let paths = modal.card.as_ref().expect("卡片视图").paths.clone();
|
||||
assert!(
|
||||
paths.iter().any(|(_, path)| path.ends_with("9f2c.jsonl")),
|
||||
"会话 JSONL 必须可复制:{paths:?}"
|
||||
);
|
||||
crash_report::clear();
|
||||
}
|
||||
|
||||
/// 带**退出码**的那条出口(真崩溃)照旧要弹:报告的 `session` 与收尾事件同源。
|
||||
///
|
||||
/// **变异证明**:把 `CrashReport::collect` 里的 `session: session.session_id.clone()` 改成空串,
|
||||
/// 本用例红(并发守卫把"自己这一场"判成别人的)。
|
||||
#[test]
|
||||
fn a_crashed_launch_with_an_exit_code_pops_with_its_evidence() {
|
||||
let _slot = slot_guard();
|
||||
let mut app = headless();
|
||||
crash_report::publish(report(true, vec!["Exception in thread main".to_string()]));
|
||||
let finished = LaunchFinished {
|
||||
id: "survival".to_string(),
|
||||
session: "9f2c".to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
exit_code: Some(1),
|
||||
uptime_ms: 1_000,
|
||||
failure: None,
|
||||
};
|
||||
app.open_failure_dialog(&finished);
|
||||
assert!(app.state.dialogs.current().is_some(), "真崩溃必须弹框");
|
||||
let modal = app.dialog_modal().expect("模型");
|
||||
assert!(
|
||||
modal.body_text().contains("退出码 1"),
|
||||
"正文必须带退出码:{}",
|
||||
modal.body_text()
|
||||
);
|
||||
crash_report::clear();
|
||||
}
|
||||
|
||||
/// **并发守卫的原意不许被松掉**:登记的是另一场 ⇒ 不许拿它解释这一场。
|
||||
///
|
||||
/// 这条与上一条是一对:修「自己这一场被误判」不能靠删守卫(删了这条就红)。
|
||||
#[test]
|
||||
fn a_report_from_another_session_is_never_reused() {
|
||||
let _slot = slot_guard();
|
||||
let mut app = headless();
|
||||
crash_report::publish(CrashReport::before_start(
|
||||
&context(),
|
||||
"spawn",
|
||||
"另一场的现场",
|
||||
"spawn",
|
||||
Some(SessionAttribution {
|
||||
id: "aaaa",
|
||||
log: None,
|
||||
}),
|
||||
));
|
||||
let finished = LaunchFinished {
|
||||
id: "survival".to_string(),
|
||||
session: "9f2c".to_string(),
|
||||
outcome: LogOutcome::SilentFail,
|
||||
exit_code: None,
|
||||
uptime_ms: 0,
|
||||
failure: Some(LaunchFailureView::new(LaunchFailureStage::Spawn, "x")),
|
||||
};
|
||||
app.open_failure_dialog(&finished);
|
||||
assert!(
|
||||
app.state.dialogs.current().is_none(),
|
||||
"A 场的现场不许拿来解释 B 场"
|
||||
);
|
||||
crash_report::clear();
|
||||
}
|
||||
|
||||
/// 没有会话的两个出口(装配失败 / spawn 失败)**照旧要弹**:那里空串是事实,守卫不该拦它。
|
||||
///
|
||||
/// 抓得住:为修「收尾失败被误判」而把守卫整条删掉 / 或把空串也编一个 id 出来(后者会让
|
||||
/// 装配失败的现场指向一个不存在的会话)。
|
||||
#[test]
|
||||
fn a_session_less_failure_still_pops() {
|
||||
let _slot = slot_guard();
|
||||
for (stage, detail) in [
|
||||
(LaunchFailureStage::VersionManifest, "缺 1.20.4.json"),
|
||||
(LaunchFailureStage::Spawn, "javaw.exe 不存在"),
|
||||
] {
|
||||
let mut app = headless();
|
||||
crash_report::publish(CrashReport::before_start(
|
||||
&context(),
|
||||
stage.slug(),
|
||||
detail,
|
||||
stage.slug(),
|
||||
None,
|
||||
));
|
||||
let finished = LaunchFinished {
|
||||
id: "survival".to_string(),
|
||||
session: String::new(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
exit_code: None,
|
||||
uptime_ms: 0,
|
||||
failure: Some(LaunchFailureView::new(stage, detail)),
|
||||
};
|
||||
app.open_failure_dialog(&finished);
|
||||
assert!(
|
||||
app.state.dialogs.current().is_some(),
|
||||
"{stage:?} 出口必须弹框(这条出口的 session 为空是事实)"
|
||||
);
|
||||
let card = app.dialog_card.as_ref().expect("卡片");
|
||||
assert!(card.report.session.is_empty(), "{stage:?}");
|
||||
assert_eq!(
|
||||
card.report.exit_code, None,
|
||||
"{stage:?}: 没有退出码就是 None"
|
||||
);
|
||||
assert_eq!(
|
||||
card.report.stage.as_deref(),
|
||||
Some(stage.slug()),
|
||||
"{stage:?}"
|
||||
);
|
||||
crash_report::clear();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user