fix(app): LaunchEnd 也清 pending 并记失败原因,补早失败回归测试(M43 r3 两条 P2)
P2-1(本轮新引入的回归):`pending` 只在 LaunchStarted 清除,但 `launch::start`
的两条早失败出口(build_request 失败 / spawn 失败——没装 Java、版本 JSON 坏就是
这两条)只发 LaunchEnd → pending 永久残留 → `in_flight` 恒真 → 该实例本会话再也
启动不了,磁贴锁在「启动中」。修:LaunchEnd 分支先 `pending.remove`,早失败与
正常收尾都释放。
P2-2(failed 零 insert):上一轮只在 note 里声称「失败原因进 failed 表」,代码没
落地(唯一 insert 缺失,refresh 的 Failed 分支是死代码,test 6 还靠手动塞表绕过)。
修:LaunchEnd 非 Ok 时 `failed.insert(id, summary)`、Ok 时 remove;refresh_instances
的 Failed 分支从此由真实路径喂数据。
测试(123 → 124,全部走真实入口):
- 新增 `launch_end_before_launch_started_still_releases_the_instance`:mark_pending
→ handle(LaunchEnd Crashed) → pending 空、in_flight 假、refresh 后磁贴是
Failed{reason}(同时钉住 P2-1 与 P2-2)。
- test 6 重写:Busy 断言改走 `mark_pending`(start_launch 用的同一入口),Failed
断言改走 `handle(LaunchEnd Crashed)`,不再手动塞 failed 表。
- 所有测试的数据根加 `TempRoot` Drop 守卫,跑完不留临时目录。
This commit is contained in:
1 parent
7b2ff62ddc
commit
50b321ec64
3 files changed
+87
-17
No files matched your search
@@ -90,10 +90,19 @@ impl CoreHost {
|
||||
self.send(CoreEvent::InstanceRuntimeChanged { id, runtime });
|
||||
}
|
||||
HostTask::LaunchEnd(finished) => {
|
||||
// 收尾可能发生在 LaunchStarted 之前(build_request / spawn 失败只发
|
||||
// LaunchEnd):pending 必须在这里也清掉,否则 in_flight 恒真、该实例
|
||||
// 本会话内再也起不来。
|
||||
self.pending.remove(&finished.id);
|
||||
self.launches.retain(|live| live.instance != finished.id);
|
||||
let runtime = if finished.outcome == LogOutcome::Ok {
|
||||
self.failed.remove(&finished.id);
|
||||
InstanceRuntime::Idle
|
||||
} else {
|
||||
// 记住失败原因:随后的 refresh_instances 会重建快照,
|
||||
// 不记的话磁贴的「失败/重试」外观会被 Idle 盖掉。
|
||||
self.failed
|
||||
.insert(finished.id.clone(), finished.summary.clone());
|
||||
InstanceRuntime::Failed {
|
||||
reason: finished.summary.clone(),
|
||||
}
|
||||
|
||||
@@ -12,8 +12,17 @@ use super::*;
|
||||
use crate::dto::{LaunchFinished, LogOutcome};
|
||||
use crate::toast::ToastKind;
|
||||
|
||||
/// 建一个只有一条实例档案的数据根,返回 `(host, instance_id)`。
|
||||
fn host_with_instance(label: &str) -> (CoreHost, String) {
|
||||
/// 临时数据根守卫:测试结束(含断言 panic)时把目录删掉,不留空壳。
|
||||
struct TempRoot(std::path::PathBuf);
|
||||
|
||||
impl Drop for TempRoot {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 建一个只有一条实例档案的数据根,返回 `(host, instance_id, 守卫)`。
|
||||
fn host_with_instance(label: &str) -> (CoreHost, String, TempRoot) {
|
||||
let dir = std::env::temp_dir().join(format!(
|
||||
"ull-host-launch-{label}-{}-{}",
|
||||
std::process::id(),
|
||||
@@ -40,7 +49,7 @@ fn host_with_instance(label: &str) -> (CoreHost, String) {
|
||||
..ProfileList::default()
|
||||
};
|
||||
write_profiles(&env.layout, &profiles).expect("写 profiles");
|
||||
(CoreHost::headless(env), id)
|
||||
(CoreHost::headless(env), id, TempRoot(dir))
|
||||
}
|
||||
|
||||
fn live(instance: &str, session: &str, pid: u32) -> LiveLaunch {
|
||||
@@ -53,7 +62,7 @@ fn live(instance: &str, session: &str, pid: u32) -> LiveLaunch {
|
||||
|
||||
#[test]
|
||||
fn running_pid_is_per_instance_not_a_single_slot() {
|
||||
let (mut host, id) = host_with_instance("pid");
|
||||
let (mut host, id, _root) = host_with_instance("pid");
|
||||
host.launches.push(live("a", "s-a", 111));
|
||||
host.launches.push(live("b", "s-b", 222));
|
||||
|
||||
@@ -76,7 +85,7 @@ fn running_pid_is_per_instance_not_a_single_slot() {
|
||||
|
||||
#[test]
|
||||
fn duplicate_launch_is_rejected_before_spawning() {
|
||||
let (mut host, id) = host_with_instance("dup");
|
||||
let (mut host, id, _root) = host_with_instance("dup");
|
||||
let _ = host.drain_events();
|
||||
host.launches.push(live(&id, "s", 111));
|
||||
|
||||
@@ -101,9 +110,9 @@ fn duplicate_launch_is_rejected_before_spawning() {
|
||||
|
||||
#[test]
|
||||
fn pending_window_also_blocks_a_second_launch() {
|
||||
let (mut host, id) = host_with_instance("pending");
|
||||
// 模拟「launch::start 已调用、后台线程还没回报 LaunchStarted」的窗口。
|
||||
host.pending.insert(id.clone());
|
||||
let (mut host, id, _root) = host_with_instance("pending");
|
||||
// 走 start_launch 用的同一个入口,模拟「已受理、后台线程还没回报 LaunchStarted」。
|
||||
host.mark_pending(&id);
|
||||
assert!(host.in_flight(&id));
|
||||
|
||||
let _ = host.drain_events();
|
||||
@@ -121,8 +130,8 @@ fn pending_window_also_blocks_a_second_launch() {
|
||||
|
||||
#[test]
|
||||
fn launch_started_registers_and_launch_end_clears() {
|
||||
let (mut host, id) = host_with_instance("cycle");
|
||||
host.pending.insert(id.clone());
|
||||
let (mut host, id, _root) = host_with_instance("cycle");
|
||||
host.mark_pending(&id);
|
||||
|
||||
host.handle(HostTask::LaunchStarted {
|
||||
id: id.clone(),
|
||||
@@ -145,7 +154,7 @@ fn launch_started_registers_and_launch_end_clears() {
|
||||
|
||||
#[test]
|
||||
fn refresh_keeps_running_instances_running() {
|
||||
let (mut host, id) = host_with_instance("refresh-run");
|
||||
let (mut host, id, _root) = host_with_instance("refresh-run");
|
||||
host.launches.push(live(&id, "s", 4_242));
|
||||
let _ = host.drain_events();
|
||||
|
||||
@@ -164,9 +173,10 @@ fn refresh_keeps_running_instances_running() {
|
||||
|
||||
#[test]
|
||||
fn refresh_keeps_pending_as_busy_and_failed_as_failed() {
|
||||
let (mut host, id) = host_with_instance("refresh-busy");
|
||||
let (mut host, id, _root) = host_with_instance("refresh-busy");
|
||||
|
||||
host.pending.insert(id.clone());
|
||||
// Busy:走 start_launch 用的同一个入口(mark_pending),而不是直接改字段。
|
||||
host.mark_pending(&id);
|
||||
let pending = instances_from_events({
|
||||
host.refresh_instances();
|
||||
host.drain_events()
|
||||
@@ -182,8 +192,16 @@ fn refresh_keeps_pending_as_busy_and_failed_as_failed() {
|
||||
}
|
||||
));
|
||||
|
||||
host.pending.remove(&id);
|
||||
host.failed.insert(id.clone(), "崩溃(exit 1)".to_string());
|
||||
// Failed:走唯一真实写入路径 LaunchEnd(不是手动塞 failed 表)。
|
||||
host.handle(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id: id.clone(),
|
||||
session: "20261031T120000-dead01".to_string(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "崩溃(exit 1)".to_string(),
|
||||
})));
|
||||
assert!(!host.pending.contains(&id), "LaunchEnd 必须清 pending");
|
||||
assert!(!host.in_flight(&id), "收尾后该实例要能被重新启动");
|
||||
|
||||
let failed = instances_from_events({
|
||||
host.refresh_instances();
|
||||
host.drain_events()
|
||||
@@ -200,6 +218,42 @@ fn refresh_keeps_pending_as_busy_and_failed_as_failed() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn launch_end_before_launch_started_still_releases_the_instance() {
|
||||
let (mut host, id, _root) = host_with_instance("fail-early");
|
||||
// build_request / spawn 失败的现场:start_launch 已标 pending、后台线程只发 LaunchEnd。
|
||||
host.mark_pending(&id);
|
||||
assert!(host.in_flight(&id));
|
||||
|
||||
host.handle(HostTask::LaunchEnd(Box::new(LaunchFinished {
|
||||
id: id.clone(),
|
||||
session: String::new(),
|
||||
outcome: LogOutcome::Crashed,
|
||||
summary: "启动失败:找不到 Java".to_string(),
|
||||
})));
|
||||
|
||||
assert!(
|
||||
!host.pending.contains(&id),
|
||||
"LaunchEnd 早于 LaunchStarted 时也必须清 pending(否则该实例本会话再也起不来)"
|
||||
);
|
||||
assert!(!host.in_flight(&id), "失败后要能重试");
|
||||
|
||||
let failed = instances_from_events({
|
||||
host.refresh_instances();
|
||||
host.drain_events()
|
||||
})
|
||||
.into_iter()
|
||||
.find(|instance| instance.id == id)
|
||||
.expect("实例存在");
|
||||
assert_eq!(
|
||||
failed.runtime,
|
||||
InstanceRuntime::Failed {
|
||||
reason: "启动失败:找不到 Java".to_string()
|
||||
},
|
||||
"早失败也要在磁贴上留下「失败/重试」,不能被 Idle 盖掉"
|
||||
);
|
||||
}
|
||||
|
||||
/// 从事件队列里取最后一次 `InstancesChanged`。
|
||||
fn instances_from_events(events: Vec<AppEvent>) -> Vec<crate::dto::InstanceSummary> {
|
||||
events
|
||||
|
||||
@@ -255,6 +255,14 @@ impl CoreHost {
|
||||
self.pending.contains(key) || self.launches.iter().any(|live| live.instance == key)
|
||||
}
|
||||
|
||||
/// 标记「已受理、等后台线程回报」(`launch::start` 之后的窗口)。
|
||||
///
|
||||
/// 单测也走这个入口而不是直接改 `pending`,保证测的是真实状态流转。
|
||||
fn mark_pending(&mut self, id: &str) {
|
||||
self.pending.insert(id.to_string());
|
||||
self.failed.remove(id);
|
||||
}
|
||||
|
||||
fn start_launch(&mut self, id: &str) {
|
||||
if self.in_flight(id) {
|
||||
self.toast(ToastKind::Warn, "这个实例已经在启动/运行中");
|
||||
@@ -269,8 +277,7 @@ impl CoreHost {
|
||||
self.toast(ToastKind::Warn, "这个实例已经在启动/运行中");
|
||||
return;
|
||||
}
|
||||
self.pending.insert(instance_id.clone());
|
||||
self.failed.remove(&instance_id);
|
||||
self.mark_pending(&instance_id);
|
||||
if let Some(notice) = game::mode_change_notice(&self.env, &instance_id) {
|
||||
self.push_notice(notice);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user