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` 现在无输出。
87 lines
3.1 KiB
Rust
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);
|
|
}
|