Files
Unlucky-Launcher/crates/core/tests/launch/exit_cases.rs
T
AskaEth 0522a41a52
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
release: v0.2.5
2026-11-12 12:06:59 +08:00

205 lines
7.1 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 退出分析测试:退出码语义 + `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"));
}