复审 p2-5items 的五项,全部按「先能红的用例,再改实现」: R1(默认目录可被不同实例隐式共用,产品缺陷):新增 `adopt/run_dir.rs`——登记前读**每个已有实例** 的 `gameDir`(现役设置 + M27 前档案的遗留字段),用「真实路径 + 拼回不存在的尾段 + 大小写归一」 (Windows/macOS)比较;撞上就顺延 `<base>-2/-3…`(上限 32,撞满 `adopt_no_free_run_dir`)并在 `notes` 点名撞的是哪个实例;**非空且无人认领**仍拒绝(那是用户数据,不偷偷换目录)。这样 `Better MC` 与 `Better-MC`(slug 同名)、Windows 上 `Foo` 与 `foo`(同目录)都不会共用一个运行目录。 大小写归一在 macOS 上是**保守**选择:大小写敏感卷上会多一次顺延,换来的是「绝不共用」在任何卷上成立。 R2(配置写面 junction 写穿到外部游戏根,产品缺陷):`run_dir::ensure_config_write_surface()` 在 `plan`(dry-run 也报)与 `adopt`(写盘前)各核一次——`config/`、`config/instances/`、 `profiles.json` 的真实落点必须在 `<.ULL>` 内,且**不得经链接落进游戏根**(错误按真实归属分类: 「逃出 .ULL」vs「落进游戏根」)。`.ULL` 本身在游戏根内是布局事实,附加判据在该布局下零判别力, 主判据仍然拦下一切跑出 `.ULL` 的落点——这不是给链接开后门。新码 `adopt_config_surface_unsafe`。 R3(dangling versions 被当不存在,产品缺陷):`scan` 的 `read_dir` `NotFound` 再三分—— `lstat(versions)` 看见入口(断链符号链接/目录联接/目标被移走)或游戏根解析失败(祖先断链)⇒ `adopt_versions_unreadable`;两者都不是才算「真没有」。`inspect` 也补了同族判据(条目存在但 不是可读目录 ⇒ `directory_unreadable`,不再混成「没装」)。 R4(强制链接测试逻辑反转,假绿):`fixture::link_gate(created, required) -> Run|Skip|Fail` 是纯 真值表,`link_guard_with()` 显式传参(`link_guard()` 只读环境变量);新增 `link_guard_executes_all_three_branches` 用 `catch_unwind` **执行** Fail/Skip 两个分支。 四处调用点统一走 guard;建链接/联接的夹具收进 `fixture.rs`。 R5(快照遗漏目录节点,假绿):新增 `tests/adopt/snapshot.rs`——全节点快照(根/目录/文件/链接 各记类型 + 适用 mtime;目录记 mtime、链接只记目标不跟随、Windows 用重解析点属性认 junction、 读盘失败直接 panic)。**CLI 测试用 `#[path]` 复用同一份**,判据只一处;新增 `fixture_cases` 五条工具自测,`plan`/dry-run 的零写盘判据改用 `snapshot_opt`(连空目录都不许出现)。 验证(本轮持久日志,`target/m221-r1/`,git 忽略、不入库): - `cargo fmt --all --check` 0;`cargo check -p unlucky-core -p unlucky-cli --all-targets` 0; - `cargo test -p unlucky-core --test adopt` **37 passed / 0 failed**(另跑一次 `ULL_ADOPT_TEST_SYMLINK_REQUIRE=1` 同样 37 passed); - `cargo test -p unlucky-cli --test adopt_test` **11 passed / 0 failed**; - `cargo clippy -p unlucky-core -p unlucky-cli --all-targets -- -D warnings` 0; - 四次**实现-only 变异**(缺省不查认领 / 配置写面不核 / versions 的 NotFound 当空 / 快照不记目录) 全部以**断言红**收场(不是编译错误、不是环境错误),每次还原后与基线清单逐文件一致,收尾再跑绿。 界限:只覆盖 core+cli 的定向目标(不等于 workspace 全量);没有启动真实 Minecraft;GUI 未接; 判据是检查那一刻的快照,TOCTOU 与非 junction 重解析点仍列边界。
102 lines
3.0 KiB
Rust
102 lines
3.0 KiB
Rust
//! 运行目录**碰撞**的 CLI 面:slug 同名 / 大小写不敏感不许让两个实例共用一个目录(复审 r1 的 R1)。
|
|
//!
|
|
//! 从 `tests/adopt_test.rs` 拆出来只为守 400 行硬线;用例体未改。
|
|
|
|
use std::path::Path;
|
|
|
|
use crate::support::{parse_json, run_cli, TempDir};
|
|
|
|
use crate::fixture::foreign_data_root;
|
|
|
|
/// **slug 碰撞的两个展示名不能绑定同一个物理运行目录**(R1 的 CLI 面)。
|
|
///
|
|
/// 复审 r1 的实跑:`Better MC` 与 `Better-MC` 都成功、两个 UUID、却指向同一个目录,
|
|
/// 随后 `launch --dry-run` 的 `--gameDir` 也是同一个。这条把它钉住。
|
|
///
|
|
/// 抓得住:
|
|
/// - 登记侧不查已有实例占用的目录(两个 `run_dir` 相同,第一条断言红);
|
|
/// - `launch` 的 `game_dir` 与登记的目录不一致(第三条断言红)。
|
|
///
|
|
/// 抓不住:`pack_dir` 的 slug 规则本身(既有行为,本 mission 不改)。
|
|
#[test]
|
|
fn colliding_slug_names_get_separate_run_dirs() {
|
|
let dir = TempDir::new("adopt-slug");
|
|
let root = dir.path();
|
|
foreign_data_root(root);
|
|
let dir_arg = dir.arg();
|
|
|
|
let first = run_cli(&[
|
|
"instances",
|
|
"adopt",
|
|
"1.20.1",
|
|
"--name",
|
|
"Better MC",
|
|
"--json",
|
|
"--data-dir",
|
|
dir_arg.as_str(),
|
|
]);
|
|
assert_eq!(
|
|
first.0, 0,
|
|
"第一次登记应成功:stdout={} stderr={}",
|
|
first.1, first.2
|
|
);
|
|
let first = parse_json(&first.1);
|
|
let second = run_cli(&[
|
|
"instances",
|
|
"adopt",
|
|
"1.20.1",
|
|
"--name",
|
|
"Better-MC",
|
|
"--json",
|
|
"--data-dir",
|
|
dir_arg.as_str(),
|
|
]);
|
|
assert_eq!(
|
|
second.0, 0,
|
|
"第二次登记应成功:stdout={} stderr={}",
|
|
second.1, second.2
|
|
);
|
|
let second = parse_json(&second.1);
|
|
|
|
let first_dir = first["data"]["instance"]["run_dir"]
|
|
.as_str()
|
|
.expect("run_dir A")
|
|
.to_string();
|
|
let second_dir = second["data"]["instance"]["run_dir"]
|
|
.as_str()
|
|
.expect("run_dir B")
|
|
.to_string();
|
|
assert_ne!(first_dir, second_dir, "两个实例不许共用运行目录");
|
|
assert!(
|
|
Path::new(&first_dir).is_dir() && Path::new(&second_dir).is_dir(),
|
|
"两个运行目录都要真实存在"
|
|
);
|
|
|
|
// 真启动计划(dry-run)也要分开:两条实例各自的 game_dir 不同。
|
|
for (value, expect) in [(&first, &first_dir), (&second, &second_dir)] {
|
|
let id = value["data"]["instance"]["id"]
|
|
.as_str()
|
|
.expect("instance id");
|
|
let args = [
|
|
"launch",
|
|
id,
|
|
"--dry-run",
|
|
"--json",
|
|
"--data-dir",
|
|
dir_arg.as_str(),
|
|
];
|
|
let (code, stdout, stderr) = run_cli(&args);
|
|
assert_eq!(
|
|
code, 0,
|
|
"stdout={stdout}
|
|
stderr={stderr}"
|
|
);
|
|
assert_eq!(
|
|
parse_json(&stdout)["data"]["game_dir"]
|
|
.as_str()
|
|
.expect("game_dir"),
|
|
expect.as_str()
|
|
);
|
|
}
|
|
}
|