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` 现在无输出。
214 lines
7.8 KiB
Rust
214 lines
7.8 KiB
Rust
//! host/install_processors.rs 的单测(拆文件守 400 行上限)。
|
||
//!
|
||
//! 夹具在 [`super::fixture`]:这里只断言「计划怎么组」「找不到 Java 怎么说」
|
||
//! 「哪一步挂了怎么报」「进度文案长什么样」。
|
||
|
||
use std::path::PathBuf;
|
||
|
||
use unlucky_core::addon::loader::forge::InstallProfile;
|
||
use unlucky_core::addon::loader::processors::{ProcessorError, StepStatus};
|
||
use unlucky_core::addon::loader::LoaderKind as CoreLoaderKind;
|
||
use unlucky_core::download::DownloadErrorKind;
|
||
use unlucky_core::game::install::{FileFailure, FileOutcome, FileRole, FileStatus, InstallReport};
|
||
use unlucky_core::game::java::JavaRequirement;
|
||
|
||
use super::fixture::{input, installer, outcome, profile, Fake, Harness};
|
||
use super::{diagnostic, java_missing, libraries_failed, stage, step_line, StageInput};
|
||
|
||
#[test]
|
||
fn step_line_names_the_step_and_marks_the_outcome() {
|
||
let executed = step_line(&outcome(StepStatus::Executed), 9);
|
||
assert_eq!(
|
||
executed,
|
||
"installer 处理器 3/9:net.minecraftforge:installertools:1.3.0(1234 ms)"
|
||
);
|
||
let skipped = step_line(&outcome(StepStatus::Skipped), 9);
|
||
assert!(skipped.contains("产物已就绪"), "{skipped}");
|
||
}
|
||
|
||
#[test]
|
||
fn diagnostic_keeps_the_stable_code_and_the_hint() {
|
||
let failed = ProcessorError::failed("net.minecraftforge:installertools:1.3.0", "1", "boom")
|
||
.with_hint("确认 Java 版本与磁盘权限");
|
||
let text = diagnostic(&failed);
|
||
assert!(text.contains("processor_failed"), "{text}");
|
||
assert!(text.contains("退出码 1"), "{text}");
|
||
assert!(text.contains("boom"), "stderr 尾部要留:{text}");
|
||
assert!(text.contains("建议:确认 Java 版本"), "{text}");
|
||
|
||
let plan = diagnostic(&ProcessorError::plan("install profile 缺 `jar`"));
|
||
assert!(plan.contains("processor_plan"), "{plan}");
|
||
}
|
||
|
||
#[test]
|
||
fn java_missing_points_at_the_java_page() {
|
||
let managed = PathBuf::from("/tmp/.ULL/java");
|
||
let text = java_missing(&JavaRequirement::table(21, None), &managed);
|
||
assert!(text.contains("Java 21+"), "{text}");
|
||
assert!(text.contains("Java」页"), "{text}");
|
||
assert!(text.contains("JAVA_HOME"), "{text}");
|
||
assert!(text.contains(&managed.display().to_string()), "{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn libraries_failed_lists_at_most_three() {
|
||
let files: Vec<FileOutcome> = (0..4)
|
||
.map(|index| FileOutcome {
|
||
role: FileRole::Library,
|
||
name: format!("lib{index}.jar"),
|
||
dest: PathBuf::from(format!("lib{index}.jar")),
|
||
status: FileStatus::Failed,
|
||
bytes: 0,
|
||
source: None,
|
||
failure: Some(FileFailure {
|
||
kind: DownloadErrorKind::Network,
|
||
message: "连接超时".to_string(),
|
||
}),
|
||
})
|
||
.collect();
|
||
let report = InstallReport {
|
||
files,
|
||
version_json: None,
|
||
warnings: Vec::new(),
|
||
};
|
||
let text = libraries_failed(&report);
|
||
assert!(text.contains("4 个失败,共 4 个"), "{text}");
|
||
assert!(text.contains("lib0.jar"), "{text}");
|
||
assert!(!text.contains("lib3.jar"), "最多列三条:{text}");
|
||
}
|
||
|
||
#[test]
|
||
fn stage_skips_when_the_version_json_is_already_there() {
|
||
let h = Harness::new();
|
||
let installer = installer();
|
||
let profile = profile();
|
||
let mut input = input(&profile, &installer);
|
||
input.installed = true;
|
||
let fake = Fake::new();
|
||
let result = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel);
|
||
assert!(matches!(result, Ok(None)));
|
||
assert!(fake.calls().is_empty(), "已装完就不该再动网络/Java");
|
||
assert!(h.log().contains("跳过 installer processors"), "{}", h.log());
|
||
}
|
||
|
||
#[test]
|
||
fn stage_skips_an_empty_processor_list() {
|
||
let h = Harness::new();
|
||
let installer = installer();
|
||
let profile = InstallProfile::default();
|
||
let input = input(&profile, &installer);
|
||
let fake = Fake::new();
|
||
let result = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel);
|
||
assert!(matches!(result, Ok(None)), "没有 processor 就没这段");
|
||
assert!(fake.calls().is_empty());
|
||
}
|
||
|
||
#[test]
|
||
fn stage_reports_missing_installer_metadata() {
|
||
let h = Harness::new();
|
||
let input = StageInput {
|
||
kind: CoreLoaderKind::Forge,
|
||
mc_version: "1.20.1",
|
||
version_id: "1.20.1-forge-47.2.0",
|
||
profile: None,
|
||
installer: None,
|
||
installed: false,
|
||
};
|
||
let fake = Fake::new();
|
||
let err = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel).unwrap_err();
|
||
assert!(err.contains("install profile"), "{err}");
|
||
assert!(h.log().contains("失败:"), "{}", h.log());
|
||
}
|
||
|
||
#[test]
|
||
fn stage_builds_the_command_line_from_the_install_profile() {
|
||
let h = Harness::new();
|
||
let installer = installer();
|
||
let profile = profile();
|
||
let input = input(&profile, &installer);
|
||
let fake = Fake::new();
|
||
let run = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel)
|
||
.expect("stage 成功")
|
||
.expect("应当真的跑了 processors");
|
||
assert_eq!(run.executed(), 1);
|
||
let steps = fake.steps();
|
||
assert_eq!(steps.len(), 1);
|
||
let (index, label, args, classpath) = &steps[0];
|
||
assert_eq!(*index, 1);
|
||
assert_eq!(label, "net.minecraftforge:installertools:1.3.0");
|
||
assert_eq!(args, &vec!["--task".to_string(), "MCP_DATA".to_string()]);
|
||
assert_eq!(*classpath, 1, "jar 自己不在 -cp 里(拼命令行时才补)");
|
||
assert_eq!(fake.calls(), vec!["libraries", "java", "run"]);
|
||
let messages = h.messages();
|
||
assert!(
|
||
messages.iter().any(|m| m.contains("installer 处理器 1/1")),
|
||
"面板要看到当前处理器:{messages:?}"
|
||
);
|
||
assert!(
|
||
messages
|
||
.last()
|
||
.is_some_and(|m| m.contains("正在写入版本清单")),
|
||
"{messages:?}"
|
||
);
|
||
assert!(
|
||
h.log().contains("processors 完成:执行 1 步"),
|
||
"{}",
|
||
h.log()
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn stage_fails_loudly_without_a_usable_java() {
|
||
let h = Harness::new();
|
||
let installer = installer();
|
||
let profile = profile();
|
||
let input = input(&profile, &installer);
|
||
let mut fake = Fake::new();
|
||
fake.java = None;
|
||
let err = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel).unwrap_err();
|
||
assert!(err.contains("Java"), "{err}");
|
||
assert!(err.contains("托管目录"), "{err}");
|
||
assert_eq!(
|
||
fake.calls(),
|
||
vec!["libraries", "java"],
|
||
"没 Java 就不该起进程"
|
||
);
|
||
assert!(h.log().contains("失败:"), "{}", h.log());
|
||
}
|
||
|
||
#[test]
|
||
fn stage_fails_loudly_when_a_processor_exits_nonzero() {
|
||
let h = Harness::new();
|
||
let installer = installer();
|
||
let profile = profile();
|
||
let input = input(&profile, &installer);
|
||
let mut fake = Fake::new();
|
||
fake.run_error = Some(ProcessorError::failed(
|
||
"net.minecraftforge:installertools:1.3.0",
|
||
"1",
|
||
"java.lang.NoSuchMethodError",
|
||
));
|
||
let err = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel).unwrap_err();
|
||
assert!(err.contains("processor_failed"), "{err}");
|
||
assert!(err.contains("NoSuchMethodError"), "{err}");
|
||
let log = h.log();
|
||
assert!(log.contains("失败:"), "{log}");
|
||
assert!(log.contains("NoSuchMethodError"), "日志要能回溯:{log}");
|
||
}
|
||
|
||
#[test]
|
||
fn stage_reports_incomplete_library_downloads() {
|
||
let h = Harness::new();
|
||
let installer = installer();
|
||
let profile = profile();
|
||
let input = input(&profile, &installer);
|
||
let mut fake = Fake::new();
|
||
fake.libraries_ok = false;
|
||
let err = stage(&h.env, &h.engine, &input, &fake, &h.reporter(), &h.cancel).unwrap_err();
|
||
assert!(err.contains("依赖库没下全"), "{err}");
|
||
assert!(err.contains("连接超时"), "{err}");
|
||
assert_eq!(fake.calls(), vec!["libraries"], "库没下全就别去选 Java");
|
||
let progress = h.meter.lock().expect("进度锁").progress.clone();
|
||
assert!(progress.files_total >= 2, "依赖库要计入文件总数");
|
||
}
|