Files
Unlucky-Launcher/crates/core/tests/diagnose/engine_rank.rs
T
AskaEth 8a8e5ca0c0
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
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
Unlucky Launcher v0.2.0
首个 iced + Material 3 Expressive 版本的源码快照。
完整历史与日常提交在 dev 分支;main 只放阶段性快照。
2026-11-05 14:22:11 +08:00

234 lines
8.5 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.
//! 引擎行为:主因排序 / 抑制关系 / 去重 / 内置结论 / 动作物化。
use crate::prelude::*;
#[test]
fn root_cause_outranks_secondary_error() {
// 一条日志里既 OOM 又 NoClassDefFoundError:OOM 是根因(priority 80 vs 40)。
let text = "\
java.lang.OutOfMemoryError: Java heap space
java.lang.NoClassDefFoundError: net/minecraftforge/fml/loading/FMLEnvironment
";
let report = analyze_text(SignalOrigin::Stdout, text);
assert_eq!(
report.primary().expect("命中").rule_id,
"memory.out_of_memory"
);
assert!(
ids(&report).contains(&"integrity.no_class_def_found"),
"低级错误也应给出(哪怕是次生):{:?}",
ids(&report)
);
}
#[test]
fn suppress_moves_rule_into_secondary() {
// 32 位 Java 分配失败 + 次生 OutOfMemoryError
let text = "\
Could not reserve enough space for 4194304KB object heap
java.lang.OutOfMemoryError: Java heap space
";
let report = analyze_text(SignalOrigin::Stdout, text);
assert_eq!(report.primary().expect("命中").rule_id, "jvm.32bit");
let secondary = report
.secondary
.iter()
.find(|finding| finding.rule_id == "memory.out_of_memory")
.expect("OOM 应被降级为次生结论而不是丢弃");
assert_eq!(secondary.suppressed_by.as_deref(), Some("jvm.32bit"));
assert!(
!ids(&report).contains(&"memory.out_of_memory"),
"被抑制的结论不应出现在主因列表"
);
}
#[test]
fn same_rule_keeps_best_evidence_source() {
// 同一条文本在两个来源出现:只留一条结论,且优先崩溃报告。
let mut input = DiagnosisInput::new();
input.push_text(
SignalOrigin::Stdout,
"java.lang.OutOfMemoryError: Java heap space",
);
input.push_text(
SignalOrigin::CrashReport,
"---- Minecraft Crash Report ----\njava.lang.OutOfMemoryError: Java heap space",
);
let report = diagnoser().analyze(&input);
let hits = report
.findings
.iter()
.filter(|finding| finding.rule_id == "memory.out_of_memory")
.count();
assert_eq!(hits, 1, "同一规则只应报一次");
let finding = &report.findings[0];
assert_eq!(finding.evidence[0].origin, SignalOrigin::CrashReport);
}
#[test]
fn access_violation_exit_is_diagnosed() {
let exit = ExitSignal::classify(Some(-1_073_741_819), Some(90_000), 0, "");
let mut input = DiagnosisInput::new();
input.exit = Some(exit);
let report = diagnoser().analyze(&input);
let finding = report
.findings
.iter()
.find(|finding| finding.rule_id == "signal.access_violation")
.expect("0xC0000005 应产出内置结论");
assert_eq!(finding.category, RuleCategory::Graphics);
let action = &finding.materialized_actions()[0];
assert_eq!(action.kind, FixActionKind::ChangeGraphics);
assert_eq!(action.tool, "graphics.set");
assert_eq!(
action.args.get("update_driver").map(String::as_str),
Some("true")
);
}
#[test]
fn silent_failure_is_marked_on_exit_zero() {
// analyse/02 §7.10-10:exit 0 + 存活 <30s + 有错误行 → 静默失败,HMCL 会判 NORMAL 放过。
let exit = ExitSignal::classify(Some(0), Some(4_200), 3, "java.lang.Exception: boom");
assert_eq!(exit.exit_type, ExitType::Normal);
assert!(exit.silent_failure);
let mut input = DiagnosisInput::new();
input.exit = Some(exit);
let report = diagnoser().analyze(&input);
assert!(
ids(&report).contains(&"signal.silent_failure"),
"静默失败应单独打标:{:?}",
ids(&report)
);
// 低置信 → 交 AI 深化(PlanDoc/06 §2.3)
assert!(report.needs_more_info());
}
#[test]
fn empty_input_asks_for_more_information() {
let report = diagnoser().analyze(&DiagnosisInput::new());
assert!(report.findings.is_empty());
assert!(report.needs_more_info());
}
#[test]
fn conflict_rule_materializes_capture_groups() {
let text = read_sample("samples/fabric/mod_resolution_conflict.log");
let report = analyze_text(SignalOrigin::Stdout, &text);
let primary = report.primary().expect("命中");
assert_eq!(primary.rule_id, "fabric.mod_resolution_conflict");
let action = &primary.materialized_actions()[0];
assert_eq!(action.kind, FixActionKind::RemoveMod);
assert_eq!(action.tool, "mod.remove");
assert_eq!(
action.args.get("mod_ids").map(String::as_str),
Some("sodium,sodium-extra"),
"mod_ids 应已代入命名捕获组"
);
}
/// 回归(review P2-1):OptiFine/Forge 兼容规则最后一个 alternative 必须是**前缀语义**,
/// 既收裸 `(Reflector.java)` 也收真实的带行号形态 `(Reflector.java:455)`。
#[test]
fn optifine_forge_matches_both_frame_forms() {
for frame in [
"\tTRANSFORMER/net.optifine/net.optifine.reflect.Reflector.<clinit>(Reflector.java:455)",
"\tTRANSFORMER/net.optifine/net.optifine.reflect.Reflector.<clinit>(Reflector.java)",
] {
let report = analyze_text(SignalOrigin::Stdout, frame);
assert!(
ids(&report).contains(&"conflict.optifine_forge"),
"真实栈帧形态应命中:{frame}(实际命中:{:?})",
ids(&report)
);
}
}
#[test]
fn too_old_java_converts_class_file_version() {
let text = read_sample("samples/jvm/too_old_java.log");
let report = analyze_text(SignalOrigin::Stdout, &text);
let primary = report.primary().expect("命中");
assert_eq!(primary.rule_id, "jvm.too_old_java");
let action = &primary.materialized_actions()[0];
assert_eq!(
action.args.get("required_major").map(String::as_str),
Some("17"),
"class file version 61 应换算成 Java 17"
);
assert_eq!(class_file_to_java_major(61), Some(17));
assert_eq!(class_file_to_java_major(52), Some(8));
assert_eq!(class_file_to_java_major(40), None);
}
#[test]
fn fix_actions_are_deduplicated_across_findings() {
// 一条日志同时命中 OOM(adjust_memory)与 32bit(adjust_memory + change_java)
let text = "\
Could not reserve enough space for 4194304KB object heap
java.lang.OutOfMemoryError: Java heap space
";
let report = analyze_text(SignalOrigin::Stdout, text);
let actions = report.fix_actions();
assert!(!actions.is_empty());
let mut unique = actions.clone();
unique.sort_by(|a, b| a.tool.cmp(b.tool).then(a.args.len().cmp(&b.args.len())));
unique.dedup();
assert_eq!(unique.len(), actions.len(), "fix_actions 应当已去重");
}
#[test]
fn record_matches_launch_diagnosis_payload() {
let text = read_sample("samples/memory/out_of_memory.log");
let report = analyze_text(SignalOrigin::Stdout, &text);
let record = report.to_record();
assert_eq!(record.source, "rules");
assert!(!record.candidates.is_empty());
let candidate = &record.candidates[0];
assert_eq!(candidate.id, "memory.out_of_memory");
assert_eq!(candidate.severity, "critical");
assert_eq!(candidate.category, "memory");
assert_eq!(candidate.message_key, "diagnose.cause.memory.out_of_memory");
assert!(!candidate.message.is_empty(), "M16 需要渲染后的中文结论");
assert!(!candidate.evidence.is_empty());
assert_eq!(candidate.fix_actions[0].tool, "memory.set");
}
#[test]
fn hs_err_report_feeds_builtin_findings() {
let report = hs_err::parse(&read_sample("samples/hs_err/access_violation.log"));
let mut input = DiagnosisInput::new();
input.push_hs_err(report);
let analysis = diagnoser().analyze(&input);
assert_eq!(
analysis.primary().expect("命中").rule_id,
"hs_err.access_violation"
);
// graphics.graphics_driver 会被内置结论 suppress 成次生(同一条证据、不同粒度)
assert!(
analysis
.secondary
.iter()
.any(|finding| finding.rule_id == "graphics.graphics_driver"),
"hs_err 原文也应命中图形规则,但被降级:{:?}",
analysis
.secondary
.iter()
.map(|finding| finding.rule_id.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn file_scope_rules_survive_very_long_logs() {
// >1 MiB:首尾窗口扫描,尾部窗口里的签名仍应命中。
let mut text = String::from("noise line\n").repeat(120_000);
text.push_str("java.lang.OutOfMemoryError: Java heap space\n");
assert!(text.len() > 1024 * 1024);
let report = analyze_text(SignalOrigin::LatestLog, &text);
assert!(
ids(&report).contains(&"memory.out_of_memory"),
"超长日志的尾部窗口应被扫描"
);
}