Files
Unlucky-Launcher/crates/presenter/src/host/game/tests.rs
T
AskaEth c5009442c5 fix(presenter): 自动发现实例按盘上痕迹选运行目录 + 空态用例补上分支/传参(M273 r1)
r1 复审的两条 P2。

## P2-1:外部实例不再一律 isolated

`versions/<id>/` 里出现 `mods` / `saves` / `config` / `options.txt` 任一 ⇒ `isolated`
(gameDir = `versions/<id>`);否则 ⇒ `shared`(gameDir = 游戏根 `.minecraft`)。
判据只 `exists()`,只读,不改盘;写盘仍然只在用户第一次改设置时(`ensure_profile`)。

- 判据收在 `host/game/discovered.rs::base_mode()`,签名 `(env, id, version_id)`:
  **有档案的实例返回全局 `instances.mode`**(D20 原口径,一字不变),自动发现的按痕迹判。
- 派生运行目录的五处(启动 / mods / saves / addon install / addon pack)从传
  `global_mode` 改成传 `base_mode` ⇒ 「读到的存档/模组在哪」与磁贴上的 ISOLATION 同一份判据。
- `isolation_of(env, id, version_id)` 多收一个版本 id:实例文件优先,缺省 `base_mode`。
  于是懒创建档案(只写 launch 段、isolation 仍为空)之后,磁贴不会从「共用目录」翻回「独立目录」。
- 磁贴/详情:自动发现的实例多一句「运行目录由盘上已有的文件决定」(i18n 中英);
  按启动时再弹一条说清**这一次**用哪一边(`pages::instances::discovered_run_toast`,
  同文案在屏不重复叠加)。
- `dto::DISCOVERED_ID_PREFIX` / `is_discovered_instance` 成为「自动发现」的公开判据
  (不给 `InstanceSummary` 加字段:那要改 24 处结构体字面量,跨 agent / cli)。

## P2-2:空态用例的覆盖面

r1 复审实测:把 `!block.rows.is_empty()` 改成 `block.rows.is_empty()`(0 实例时反而没按钮),
当时三条用例全绿。现在:

- `detail_empty` 拆成 `empty_not_selected` / `empty_no_instances` 两个函数,分支各自可断言;
- `the_cta_lives_only_in_the_no_instances_branch`:CTA 只在「一个都没有」那一支、
  「没选中」那一支必须是 `Vec::new()`,且分派必须写成 `if has_instances {`(取反即红);
- `the_page_asks_the_empty_state_with_the_negated_row_count`:两条调用点(宽窗 + 窄窗)
  都必须传 `!block.rows.is_empty()`,条数也钉住(多一条即红)。
- 两个变异已自证:传参取反 ⇒ 后一条红;CTA 挪支 ⇒ 前一条红(红原文见交付报告)。

## P3

- `java::target_major()` 改读并集快照:只有自动发现实例的用户原来会拿到默认 JDK17 而不是
  该版本要求的 JDK(`profiles.json` 是空的)。有档案时顺序与旧写法一致。
- `instances()` 全量扫仍在 UI 线程——本轮不动,刷新时机与代价写进报告(塔另开一条)。

## 结构

`host/game.rs` 394 → 286 行:自动发现那一簇(痕迹判据 / 扫出来的版本 / 合成视图)搬到
`host/game/discovered.rs`(138 行),只为守 400 行硬线;`game::base_mode` 仍是同一条路径。

## 门禁

app-iced lib 973 passed / presenter all-targets 386 passed / i18n 36 passed /
fmt 0 / clippy -D warnings 0。
2026-11-12 14:21:30 +08:00

199 lines
9.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.
use super::*;
/// 版本 id → DTO 加载器的接口面(解析在 core,这里只验翻译没走样)。
///
/// 抓得住:显示路径把某个种类翻错/翻塌(例如 Quilt 写成 Forge)——`dto_kind` 的委派与
/// `dto` 的 `from_core` 都落在这一条上;以及 core 解析端退回老口径(`fabric-loader-…` 才认、
/// 自家 `1.20.4-fabric-0.16.5` 认不出)——后两条正是 M122 的 bug 面。
/// 抓不住:core 解析规则本身(真值表在 `unlucky_core::game::loader` 的测试里,那边才是
/// 真相源);也抓不住「版本号为空时不显示品牌名」那条(在 `LoaderInfo::badge` 与
/// app-iced 的 `loader_badge`,M120 已修,别在这里重复)。
#[test]
fn loader_parsing_covers_all_shapes() {
let cases = [
("1.20.4", LoaderKind::Vanilla, ""),
// 自家口径(生成端 `LoaderKind::version_id`):`<mc>-<kind>-<loader>`
("1.20.4-fabric-0.16.5", LoaderKind::Fabric, "0.16.5"),
("1.20.4-quilt-0.27.0", LoaderKind::Quilt, "0.27.0"),
("1.20.1-forge-47.2.0", LoaderKind::Forge, "47.2.0"),
("1.21.1-neoforge-21.1.80", LoaderKind::NeoForge, "21.1.80"),
// 老 HMCL 口径(存量实例还会带着它们)
("fabric-loader-0.16.5-1.20.1", LoaderKind::Fabric, "0.16.5"),
("quilt-loader-0.24.0-1.20.1", LoaderKind::Quilt, "0.24.0"),
// 第三方口径
("1.13.2-OptiFine_HD_U_E6", LoaderKind::OptiFine, "HD_U_E6"),
("1.12.2-LiteLoader", LoaderKind::LiteLoader, ""),
];
for (id, kind, version) in cases {
let info = loader_from_id(id);
assert_eq!(info.kind, kind, "{id}");
assert_eq!(info.version, version, "{id}");
assert!(!info.badge().is_empty());
}
}
/// 显示路径的**全量**锚:core 的每一种加载器,从自家版本 id 走到磁贴时仍然是**它自己**。
///
/// 清单取自 [`unlucky_core::game::LoaderKind::ALL`],**不手抄**:M126 r3 复审实测出的缺口正是
/// 「手抄的清单跟着漏」——core 加第八家、契约与 help 都补齐、只在 `dto_kind` 那一支写
/// `=> Vanilla`,三个 crate 全绿而磁贴静默显示「原版」。`ALL` 由 core 的宏表长出来,加一家
/// 自动进圈,测试作者不需要(也没法)记得补一行。
///
/// 抓得住:显示路径任何一环把种类翻塌成原版——`dto_kind` 又变回手写映射、`dto` 的
/// `from_core` 那一支接错、`label()` 被写成「原版」;**包括新加的那种**(那时 `ALL` 先变长)。
/// 抓不住:core 的 id 解析规则本身(真值表在 `unlucky_core::game::loader`);版本号为空的
/// 徽标判据(M120 的 `loader_badge`,本 crate 不渲染)。
#[test]
fn every_core_loader_kind_reaches_the_tile_as_itself() {
use unlucky_core::game::LoaderKind as Core;
for kind in Core::ALL {
let info = loader_from_id(&format!("1.20.4-{}-0.16.5", kind.as_str()));
assert_eq!(
info.kind.as_str(),
kind.as_str(),
"core 的 {kind:?} 走到磁贴时变成了 `{}`(徽标 `{}`)",
info.kind.as_str(),
info.badge()
);
assert_eq!(
info.badge().starts_with(LoaderKind::Vanilla.label()),
kind == Core::Vanilla,
"core 的 {kind:?} 在磁贴上的徽标是 `{}`",
info.badge()
);
}
}
/// 端到端(数据层):真装出来的四个加载器实例,走 [`instances`] 的快照各自带对加载器。
///
/// 造的是本启动器**真实产物**的形状:`profiles.json` 里 `versionId` 是自家 id,
/// `versions/<id>/<id>.json` 里 `inheritsFrom` 指向原版层(生成端见
/// `unlucky_core::addon::loader`)。原版版本号由此取自继承链根,不是从 id 里切的。
///
/// 抓得住:`describe_version` ↔ `loader_from_id` ↔ `instances` 这条链上任何一环把加载器
/// 丢掉(磁贴会显示「原版」——M122 的用户可见症状);以及继承链根取错(磁贴的版本号)。
/// 抓不住:实例页真出图(走 app-iced 的视图模型与 `loader_badge`,本 crate 不渲染);
/// 也抓不住「真下载安装了 Fabric/Quilt/Forge/NeoForge」——这里只伪造落盘结果。
#[test]
fn own_installed_instances_snapshot_carries_their_loader() {
let dir = std::env::temp_dir().join(format!("ull-host-game-own-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let env = HostEnv::at_root(&dir);
std::fs::create_dir_all(env.layout.config()).expect("config 目录");
let cases = [
(
"1.20.4-fabric-0.16.5",
"1.20.4",
LoaderKind::Fabric,
"Fabric 0.16.5",
),
(
"1.20.4-quilt-0.27.0",
"1.20.4",
LoaderKind::Quilt,
"Quilt 0.27.0",
),
(
"1.20.1-forge-47.2.0",
"1.20.1",
LoaderKind::Forge,
"Forge 47.2.0",
),
(
"1.21.1-neoforge-21.1.80",
"1.21.1",
LoaderKind::NeoForge,
"NeoForge 21.1.80",
),
];
for (version_id, mc_version, _, _) in cases {
for (id, inherits) in [(mc_version, None), (version_id, Some(mc_version))] {
let json = match inherits {
Some(parent) => format!(r#"{{"id":"{id}","inheritsFrom":"{parent}"}}"#),
None => format!(r#"{{"id":"{id}"}}"#),
};
let path = env.game.version_json(id);
std::fs::create_dir_all(path.parent().expect("版本目录")).expect("版本目录");
std::fs::write(&path, json).expect("写版本 JSON");
}
}
let profiles = ProfileList {
profiles: cases
.iter()
.map(
|(version_id, _, _, _)| unlucky_core::game::InstanceProfile {
id: unlucky_core::game::ProfileId::generate(),
name: format!("装好的 {version_id}"),
version_id: (*version_id).to_string(),
game_dir: None,
running_dir: None,
java_path: None,
created_at: None,
last_played_at: None,
extra: BTreeMap::new(),
},
)
.collect(),
..ProfileList::default()
};
unlucky_core::game::install::instance::write_profiles(&env.layout, &profiles).expect("写清单");
let snapshot = instances(&env);
// M273 之后 `versions/*` 也是实例来源:这个夹具为了造 `inheritsFrom` 还写了三个父版本目录
// (`1.20.4` / `1.20.1` / `1.21.1`)而**没有**给它们建档案,于是它们会作为「外部实例」出现。
// 这条用例守的是「这条链把加载器/版本号翻对」——所以只看它自己那四个版本,条数也在这一层断言;
// 「盘上有版本就出现在列表里」由 `tests_union.rs` 独占。
let snapshot: Vec<_> = snapshot
.into_iter()
.filter(|tile| cases.iter().any(|case| case.0 == tile.version_id))
.collect();
assert_eq!(snapshot.len(), cases.len());
for ((version_id, mc_version, kind, badge), tile) in cases.iter().zip(&snapshot) {
assert_eq!(tile.version_id.as_str(), *version_id);
assert_eq!(
tile.mc_version.as_str(),
*mc_version,
"{version_id} 的版本号取错"
);
assert_eq!(tile.loader.kind, *kind, "{version_id} 的加载器");
assert_eq!(tile.loader.badge().as_str(), *badge, "{version_id} 的徽标");
assert!(tile.version_present, "{version_id} 的载荷应当存在");
}
std::fs::remove_dir_all(&dir).ok();
}
#[test]
fn isolation_write_then_read_round_trips() {
let dir = std::env::temp_dir().join(format!("ull-host-game-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
let env = HostEnv::at_root(&dir);
std::fs::create_dir_all(env.layout.config()).expect("config 目录");
// 直接建一份 profiles.json(register 需要版本本体,这里只测隔离写读)
let profiles = ProfileList {
profiles: vec![unlucky_core::game::InstanceProfile {
id: unlucky_core::game::ProfileId::generate(),
name: "demo".to_string(),
version_id: "1.20.1".to_string(),
game_dir: None,
running_dir: None,
java_path: None,
created_at: None,
last_played_at: None,
extra: BTreeMap::new(),
}],
..ProfileList::default()
};
unlucky_core::game::install::instance::write_profiles(&env.layout, &profiles).expect("写清单");
let id = profiles.profiles[0].id.as_str().to_string();
// 有档案的实例:基底档 = 全局档(`isolated`),与 M273 之前一字不差。
assert_eq!(isolation_of(&env, &id, "1.20.1"), "isolated");
set_isolation(&env, &id, "shared").expect("切档位");
assert_eq!(isolation_of(&env, &id, "1.20.1"), "shared");
assert!(set_isolation(&env, &id, "nope").is_err());
std::fs::remove_dir_all(&dir).ok();
}