D4:`pick` 的排序键从「来源序」改成「版本距离 → 来源序」,全不满足时选最接近要求的那个 (托管 25 与 JAVA_HOME/扫盘 17 之间选 17,而不是按 rank 选托管)。新增 `select::decide`: 不符且**没有人显式指定 Java** 时 `ull launch` 直接报 `java_mismatch` 不 spawn(`--java` / 实例 `javaPath` 是豁免,只告警)——1.12.2 配 Java 17/25 必然起不来,警告没有信息量。 `JavaMismatch` + `check` 拆到 `mismatch.rs` 并加 `advice()`(mapping.rs 408→282 行)。 D6:`LaunchSession::finish` 的等进程改成**有界**。判据在 `startup_watch.rs`(纯逻辑): ①窗口从未出现且**连续 N 秒没有任何日志**(还在打日志就说明还在加载,慢加载不被判死); ②日志出现关闭标记后 N 秒仍未退出(实机「关窗后 java.exe 仍存活」的形状)。 到点只**停止等待、绝不杀进程**:写 `session_end(still_running)` + 索引 + `launcher.log`, 返回 `LaunchError::Watchdog`,CLI 翻成 `launch_timeout`。两个时长可配 (默认 300s/60s;`ULL_LAUNCH_STARTUP_TIMEOUT_SECS` / `ULL_LAUNCH_SHUTDOWN_GRACE_SECS`, 生效值每次启动写进 launcher.log 与人读输出)。 D10:把「被终止」与「应用错误」分开。①我们自己杀的:`tree_kill` **送达即登记** `kill_registry`(pid 登记簿,取走语义、上限 64),覆盖 GUI 从别的线程 `kill_tree` 的路径, `finish` 据此判 `killed`;②外部强杀:Windows 上「退出码 1 + 零错误输出 + 无崩溃/JVM 关键字」 判 `killed` 并标 `terminated_externally`(任务管理器与 `taskkill /F` 都是 `TerminateProcess(h, 1)`,应用真崩给的是 NTSTATUS 异常码)。 顺带:`SessionHandle` 拆 `session_handle.rs`(launch_session.rs 422→343 行)、 `NativesReport::to_json` 落 core(cli/launch.rs 腾出空间,仍 399 行)。 用例:select 距离/取舍、startup_watch 状态机、watchdog_cases(4 条真进程)、 exit_cases(外部终止与「我们杀的」区分)、pipeline 跨线程 kill。
205 lines
7.1 KiB
Rust
205 lines
7.1 KiB
Rust
//! 退出分析测试:退出码语义 + `exit 0` 静默失败(`analyse/02` §6.3 §7.2 §7.10-10)。
|
||
|
||
use crate::prelude::*;
|
||
|
||
fn line(stream: Stream, text: &str) -> LogLine {
|
||
LogLine::new(stream, text)
|
||
}
|
||
|
||
fn analyze(
|
||
code: i32,
|
||
uptime_ms: u64,
|
||
lines: &[LogLine],
|
||
posix: bool,
|
||
) -> unlucky_core::launch::exit_analysis::ExitAnalysis {
|
||
let mut signals = exit_analysis::ExitSignals::new(code, uptime_ms, lines);
|
||
signals.is_posix = posix;
|
||
exit_analysis::analyze(&signals)
|
||
}
|
||
|
||
#[test]
|
||
fn clean_long_run_is_normal() {
|
||
let lines = vec![line(
|
||
Stream::Stdout,
|
||
"[12:00:00] [Render thread/INFO]: started",
|
||
)];
|
||
let analysis = analyze(0, 120_000, &lines, true);
|
||
assert_eq!(analysis.exit_type, ExitType::Normal);
|
||
assert!(analysis.silent_failure.is_none());
|
||
assert!(!analysis.needs_diagnosis);
|
||
}
|
||
|
||
#[test]
|
||
fn exit_zero_short_lived_is_silent_failure() {
|
||
let lines = vec![line(Stream::Stdout, "Stopping!")];
|
||
let analysis = analyze(0, 8_000, &lines, true);
|
||
assert_eq!(analysis.exit_type, ExitType::Normal);
|
||
let silent = analysis.silent_failure.expect("应标记静默失败");
|
||
assert!(silent.short_lived);
|
||
assert!(!silent.error_output);
|
||
assert!(analysis.needs_diagnosis, "静默失败必须进诊断流水线");
|
||
}
|
||
|
||
#[test]
|
||
fn exit_zero_with_error_output_is_silent_failure() {
|
||
let lines = vec![
|
||
line(Stream::Stdout, "[12:00:00] [main/INFO]: loading"),
|
||
line(
|
||
Stream::Stderr,
|
||
"java.lang.RuntimeException: mixin apply failed",
|
||
),
|
||
];
|
||
let analysis = analyze(0, 90_000, &lines, true);
|
||
assert_eq!(analysis.exit_type, ExitType::Normal);
|
||
let silent = analysis.silent_failure.expect("应标记静默失败");
|
||
assert!(silent.error_output);
|
||
assert!(!silent.short_lived);
|
||
assert!(silent.describe().contains("错误特征"));
|
||
assert_eq!(analysis.error_lines, 1);
|
||
}
|
||
|
||
#[test]
|
||
fn jvm_keywords_take_precedence() {
|
||
let lines = vec![line(
|
||
Stream::Stderr,
|
||
"Error occurred during initialization of VM",
|
||
)];
|
||
let analysis = analyze(1, 500, &lines, true);
|
||
assert_eq!(analysis.exit_type, ExitType::JvmError);
|
||
assert_eq!(analysis.jvm_keywords.len(), 1);
|
||
assert!(analysis.needs_diagnosis);
|
||
}
|
||
|
||
#[test]
|
||
fn nonzero_without_keywords_is_application_error() {
|
||
let lines = vec![line(Stream::Stdout, "[12:00:00] [main/INFO]: hi")];
|
||
let analysis = analyze(1, 40_000, &lines, true);
|
||
assert_eq!(analysis.exit_type, ExitType::ApplicationError);
|
||
assert!(analysis.needs_diagnosis);
|
||
}
|
||
|
||
#[test]
|
||
fn crash_keywords_promote_exit_zero() {
|
||
let lines = vec![line(
|
||
Stream::Stdout,
|
||
"[12:00:00] [Render thread/ERROR]: Crash report saved to ./crash-reports/crash-1.txt",
|
||
)];
|
||
let analysis = analyze(0, 60_000, &lines, true);
|
||
assert_eq!(analysis.exit_type, ExitType::ApplicationError);
|
||
assert_eq!(analysis.crash_keywords.len(), 1);
|
||
assert!(
|
||
analysis.silent_failure.is_none(),
|
||
"已判异常就不再是静默失败"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn posix_signals_are_recognized() {
|
||
assert_eq!(analyze(137, 60_000, &[], true).exit_type, ExitType::SigKill);
|
||
assert_eq!(analyze(143, 60_000, &[], true).exit_type, ExitType::SigTerm);
|
||
// Windows 上 137 没有信号语义,按应用错误处理。
|
||
assert_eq!(
|
||
analyze(137, 60_000, &[], false).exit_type,
|
||
ExitType::ApplicationError
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn windows_terminated_code_with_no_output_is_not_an_application_error() {
|
||
// D10:1.12.2 挂死后被任务管理器 / `taskkill /F` 结束 —— 退出码 1,零错误输出。
|
||
let analysis = analyze(1, 60_000, &[], false);
|
||
assert_eq!(analysis.exit_type, ExitType::Killed);
|
||
assert!(analysis.terminated_externally);
|
||
assert!(
|
||
analysis.summary().contains("疑似被外部结束"),
|
||
"{}",
|
||
analysis.summary()
|
||
);
|
||
// 同样的退出码带错误输出 → 仍是应用错误(不能把所有 1 都洗成「被结束」)。
|
||
let lines = vec![line(
|
||
Stream::Stderr,
|
||
"Exception in thread \"main\" java.lang.NullPointerException",
|
||
)];
|
||
let analysis = analyze(1, 60_000, &lines, false);
|
||
assert_eq!(analysis.exit_type, ExitType::ApplicationError);
|
||
assert!(!analysis.terminated_externally);
|
||
// POSIX 上 1 不是信号痕迹:不套这条判据(137/143 才是)。
|
||
let analysis = analyze(1, 60_000, &[], true);
|
||
assert_eq!(analysis.exit_type, ExitType::ApplicationError);
|
||
assert!(!analysis.terminated_externally);
|
||
}
|
||
|
||
#[test]
|
||
fn a_deliberate_kill_is_reported_as_killed_not_suspected() {
|
||
// 「我们杀的」有登记作据,不该被说成「疑似」。
|
||
let mut signals = exit_analysis::ExitSignals::new(1, 60_000, &[]);
|
||
signals.killed = true;
|
||
let analysis = exit_analysis::analyze(&signals);
|
||
assert_eq!(analysis.exit_type, ExitType::Killed);
|
||
assert!(!analysis.terminated_externally);
|
||
}
|
||
|
||
#[test]
|
||
fn windows_ntstatus_is_explained() {
|
||
let analysis = analyze(-1_073_741_819, 60_000, &[], false);
|
||
assert_eq!(analysis.exit_type, ExitType::ApplicationError);
|
||
assert_eq!(
|
||
analysis.windows_status.as_deref(),
|
||
Some("0xC0000005 ACCESS_VIOLATION")
|
||
);
|
||
|
||
let dll = analyze(-1_073_741_515, 60_000, &[], false);
|
||
assert_eq!(
|
||
dll.windows_status.as_deref(),
|
||
Some("0xC0000135 DLL_NOT_FOUND")
|
||
);
|
||
|
||
// 0xC000013A = STATUS_CONTROL_C_EXIT(控制台 Ctrl+C / 关窗口),十进制 -1073741510。
|
||
let control_c = analyze(-1_073_741_510, 60_000, &[], false);
|
||
assert_eq!(
|
||
control_c.windows_status.as_deref(),
|
||
Some("0xC000013A CONTROL_C_EXIT")
|
||
);
|
||
// 回归:-1073741743 其实是 0xC0000051,不得被当成 CONTROL_C_EXIT。
|
||
assert!(analyze(-1_073_741_743, 60_000, &[], false)
|
||
.windows_status
|
||
.is_none());
|
||
}
|
||
|
||
#[test]
|
||
fn killed_and_interrupted_override_exit_code() {
|
||
let mut signals = exit_analysis::ExitSignals::new(1, 5_000, &[]);
|
||
signals.killed = true;
|
||
assert_eq!(exit_analysis::analyze(&signals).exit_type, ExitType::Killed);
|
||
|
||
let mut signals = exit_analysis::ExitSignals::new(1, 5_000, &[]);
|
||
signals.interrupted = true;
|
||
assert_eq!(
|
||
exit_analysis::analyze(&signals).exit_type,
|
||
ExitType::Interrupted
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn exit_type_strings_match_contract() {
|
||
// 与 M15 刻意保持一致的字符串集合(`launch-接口约定.md` §3)。
|
||
assert_eq!(ExitType::Normal.as_str(), "normal");
|
||
assert_eq!(ExitType::ApplicationError.as_str(), "application_error");
|
||
assert_eq!(ExitType::JvmError.as_str(), "jvm_error");
|
||
assert_eq!(ExitType::SigKill.as_str(), "sigkill");
|
||
assert_eq!(ExitType::SigTerm.as_str(), "sigterm");
|
||
assert_eq!(ExitType::Killed.as_str(), "killed");
|
||
assert_eq!(ExitType::Interrupted.as_str(), "interrupted");
|
||
assert!(!ExitType::Normal.is_abnormal());
|
||
assert!(ExitType::JvmError.is_abnormal());
|
||
}
|
||
|
||
#[test]
|
||
fn summary_is_human_readable() {
|
||
let analysis = analyze(-1_073_741_819, 61_500, &[], false);
|
||
let summary = analysis.summary();
|
||
assert!(summary.contains("-1073741819"));
|
||
assert!(summary.contains("61.5s"));
|
||
assert!(summary.contains("ACCESS_VIOLATION"));
|
||
}
|