Files
Unlucky-Launcher/crates/presenter/src/host/logs_tests.rs
T
AskaEth f159529936 feat(presenter): 新建 unlucky-presenter,把 host/状态/DTO/设置桥从 app 迁出(M85 第 1 块)
D26(iced 重写)的关键一次性动作:UI 框架无关的宿主层独立成 crate,
Slint 版(crates/app)与 iced 版(crates/app-iced)共用,Slint 退役后不再重写一遍。

- 新建 crates/presenter(package `unlucky-presenter` / lib `unlucky_presenter`),
  workspace 登记 + crate 文档头写清「这层为什么存在」、对外表面、UI 侧允许/禁止、
  iced 接线骨架、以及哪些东西留在了 crates/app 及为什么。
- `git mv`(保留文件历史)迁入:host/(25 文件)、dto/(11 文件)、state.rs(+tests)、
  prefs.rs、settings_bridge.rs、toast.rs、router.rs、dialogs.rs、wizard.rs、
  mock.rs、mock_data.rs、backend.rs。模块路径与公开 API 名称不变(`host::install` 等),
  内部实现零改动。
- 新增 `theme`:把原本混在 ui_bind/theme_runtime 里的**纯判据**(CONFIG_POLL_TICKS /
  SYSTEM_THEME_POLL_TICKS / TILE_REGENERATE_STEP / BackgroundKey / tile_viewport /
  system_theme_value / 两个 poll 判据)抽出来,附 5 个单测;app 侧改成调用它们。
- crates/app 改为消费者:删掉迁走的文件,`crate::host|state|dto|prefs|settings_bridge|
  toast|router|dialogs|wizard|mock|mock_data|backend` 全部改写为 `unlucky_presenter::*`
  (64 处),backend.rs 的 config_path/build_backend 提为 pub。
- crates/app-iced 只登记 unlucky-presenter 依赖,本 mission 不接线。
- prefs.rs 的 2 处 slint 文档引用改写;`grep -rn slint crates/presenter` 现在无输出。
2026-11-02 09:29:31 +08:00

87 lines
3.1 KiB
Rust

//! host/logs.rs 的单测(拆文件守 400 行上限)。
use super::*;
#[test]
fn time_display_and_outcomes() {
assert_eq!(display_time("2026-10-29T19:22:45.123Z"), "10-29 19:22");
assert_eq!(display_time("nope"), "nope");
let mut entry = SessionIndexEntry {
v: 1,
session_id: "s".to_string(),
instance_id: String::new(),
instance_name: String::new(),
game_version: String::new(),
started_at: String::new(),
ended_at: "2026-10-29T19:22:45Z".to_string(),
duration_ms: 1000,
exit_code: Some(0),
exit_type: Some("normal".to_string()),
silent_failure: false,
window_detected: true,
event_count: 1,
log_path: "s.jsonl".to_string(),
crash_found: false,
diagnosis_top: None,
};
assert_eq!(outcome_of(&entry), LogOutcome::Ok);
entry.exit_code = Some(1);
assert_eq!(outcome_of(&entry), LogOutcome::Crashed);
entry.silent_failure = true;
assert_eq!(outcome_of(&entry), LogOutcome::SilentFail);
}
#[test]
fn lines_map_levels_and_skip_non_line_events() {
let stdout = serde_json::json!({"kind":"stdout","payload":{"line":"hi","level":"info"}});
assert_eq!(line_of(&stdout).unwrap().level, LogLevel::Info);
let stderr = serde_json::json!({"kind":"stderr","payload":{"line":"boom","level":"error"}});
assert_eq!(line_of(&stderr).unwrap().level, LogLevel::Error);
let other = serde_json::json!({"kind":"cmd_built","payload":{"argv":[]}});
assert!(line_of(&other).is_none());
}
#[test]
fn tail_only_consumes_complete_lines() {
let dir = std::env::temp_dir().join(format!("ull-host-tail-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).expect("目录");
let path = dir.join("s.jsonl");
std::fs::write(
&path,
"{\"kind\":\"stdout\",\"payload\":{\"line\":\"one\"}}\n",
)
.expect("写");
let mut tail = Tail::default();
tail.attach(path.clone(), "s");
let (session, lines) = tail.poll().expect("首拍有新行");
assert_eq!(session, "s");
assert_eq!(lines.len(), 1);
assert!(tail.poll().is_none(), "没有新增就不该再产出");
// 半行不消费
let mut text = std::fs::read_to_string(&path).expect("读");
text.push_str("{\"kind\":\"stdout\",\"payload\":{\"line\":\"tw");
std::fs::write(&path, text).expect("写");
assert!(tail.poll().is_none(), "半个 JSON 行不能解析");
let mut text = std::fs::read_to_string(&path).expect("读");
text.push_str("o\"}}\n");
std::fs::write(&path, text).expect("写");
let (_, lines) = tail.poll().expect("补全后应产出");
assert_eq!(lines[0].text, "two");
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn crash_import_reports_missing_file() {
let err = analyze_crash_file("D:/definitely/missing/hs_err_pid1.log").unwrap_err();
assert!(err.contains("找不到崩溃日志"), "{err}");
}
#[test]
fn confidence_percent_is_rounded_and_clamped() {
assert_eq!(confidence_percent(0.923), 92);
assert_eq!(confidence_percent(-1.0), 0);
assert_eq!(confidence_percent(2.0), 100);
}