Files
Unlucky-Launcher/crates/core/tests/setting/dir_migration.rs
T
AskaEth bc41dfcf7d
CI / fmt (rustfmt --check) (push) Canceled after 0s
CI / clippy (ubuntu-latest) (push) Canceled after 0s
CI / clippy (windows-latest) (push) Canceled after 0s
CI / test (ubuntu-latest) (push) Canceled after 0s
CI / test (windows-latest) (push) Canceled after 0s
CI / build release (ubuntu-latest) (push) Canceled after 0s
CI / build release (windows-latest) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Unlucky Launcher v0.0.1-alpha
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent),
MCP server 接入外部 Agent 工具,GUI 开发中。
2026-10-31 21:54:10 +08:00

186 lines
6.9 KiB
Rust

//! 数据目录迁移:计划校验、逐分区搬动、收据 + 指针、失败逆序回滚。
use std::path::{Path, PathBuf};
use unlucky_core::platform::data_dir::{DataMode, ResolvedData};
use unlucky_core::platform::dir_migration::{self, DirMigrationOptions};
use unlucky_core::platform::error::PlatformError;
use unlucky_core::platform::layout::{LauncherLayout, Partition};
use unlucky_core::platform::receipt::{self, ReceiptKind};
use unlucky_core::platform::redirect;
use crate::common::TempDir;
fn resolved(home: &Path) -> ResolvedData {
ResolvedData {
mode: DataMode::Custom,
launcher_home: home.to_path_buf(),
game_home: home.join(".minecraft"),
redirected_from: None,
}
}
fn seed(home: &Path) {
let layout = LauncherLayout::new(home.to_path_buf());
layout.ensure().expect("建四分区");
std::fs::create_dir_all(layout.auth_data()).expect("auth 目录");
std::fs::create_dir_all(layout.sha1_cache("ab")).expect("缓存分片");
std::fs::write(layout.launcher_config(), b"{\"x\":1}").expect("写配置");
std::fs::write(layout.auth_data().join("token.json"), b"{}").expect("写凭据");
std::fs::write(layout.sha1_cache("ab").join("hash"), b"payload").expect("写缓存");
std::fs::write(layout.logs().join("launcher.log"), b"log").expect("写日志");
}
#[test]
fn plan_orders_partitions_and_skips_missing_ones() {
let temp = TempDir::new("dir-migration-plan");
seed(&temp.child("old/.ULL"));
let from = resolved(&temp.child("old/.ULL"));
let to = resolved(&temp.child("new/.ULL"));
let plan = dir_migration::plan(&from, &to, &DirMigrationOptions::default()).expect("计划");
let partitions: Vec<Partition> = plan.steps.iter().map(|step| step.partition).collect();
assert_eq!(
partitions,
vec![
Partition::Config,
Partition::Data,
Partition::Logs,
Partition::Cache
],
"配置最先、缓存最后"
);
let cache_off = DirMigrationOptions {
migrate_cache: false,
};
let plan = dir_migration::plan(&from, &to, &cache_off).expect("计划");
assert_eq!(plan.steps.len(), 3);
assert!(plan
.steps
.iter()
.all(|step| step.partition != Partition::Cache));
}
#[test]
fn execute_moves_everything_and_leaves_a_pointer() {
let temp = TempDir::new("dir-migration-run");
let old_home = temp.child("old/.ULL");
let new_home = temp.child("new/.ULL");
seed(&old_home);
let from = resolved(&old_home);
let to = resolved(&new_home);
let plan = dir_migration::plan(&from, &to, &DirMigrationOptions::default()).expect("计划");
let outcome = dir_migration::execute(&plan, 1_700_000_000_000).expect("执行");
assert_eq!(
outcome.moved,
vec![
Partition::Config,
Partition::Data,
Partition::Logs,
Partition::Cache
]
);
let moved = LauncherLayout::new(new_home.clone());
assert!(moved.launcher_config().is_file(), "配置要搬过去");
assert!(moved.auth_data().join("token.json").is_file());
assert!(moved.logs().join("launcher.log").is_file());
assert!(moved.sha1_cache("ab").join("hash").is_file());
assert!(!LauncherLayout::new(old_home.clone()).config().exists());
let stored = receipt::read(&outcome.receipt_path)
.expect("读收据")
.expect("有收据");
assert_eq!(stored.kind, ReceiptKind::DataDirMove);
assert_eq!(stored.source, old_home);
assert_eq!(stored.target, new_home);
assert_eq!(stored.applied_at_ms, 1_700_000_000_000);
let pointer = redirect::read(&old_home)
.expect("读指针")
.expect("旧目录要留指针");
assert_eq!(pointer.launcher_home, new_home);
assert_eq!(pointer.mode, DataMode::Custom);
assert_eq!(outcome.pointer_path, old_home.join("redirect.json"));
}
#[test]
fn refuses_same_source_and_target() {
let temp = TempDir::new("dir-migration-same");
seed(&temp.child("only/.ULL"));
let home = resolved(&temp.child("only/.ULL"));
let err = dir_migration::plan(&home, &home, &DirMigrationOptions::default())
.expect_err("源目标相同要拒绝");
assert!(
matches!(err, PlatformError::MigrationRefused { .. }),
"{err}"
);
}
#[test]
fn refuses_missing_source_and_non_empty_target() {
let temp = TempDir::new("dir-migration-refuse");
let from = resolved(&temp.child("gone/.ULL"));
let to = resolved(&temp.child("new/.ULL"));
assert!(matches!(
dir_migration::plan(&from, &to, &DirMigrationOptions::default()).expect_err("源不存在"),
PlatformError::MigrationRefused { .. }
));
seed(&temp.child("old/.ULL"));
let from = resolved(&temp.child("old/.ULL"));
std::fs::create_dir_all(to.launcher_home.clone()).expect("建目标");
std::fs::write(to.launcher_home.join("something"), b"x").expect("占位");
let err = dir_migration::plan(&from, &to, &DirMigrationOptions::default())
.expect_err("目标非空要拒绝");
assert!(err.to_string().contains("已有数据"), "{err}");
}
#[test]
fn rolls_back_moved_partitions_when_a_later_step_fails() {
let temp = TempDir::new("dir-migration-rollback");
let old_home = temp.child("old/.ULL");
let new_home = temp.child("new/.ULL");
seed(&old_home);
let from = resolved(&old_home);
let to = resolved(&new_home);
let plan = dir_migration::plan(&from, &to, &DirMigrationOptions::default()).expect("计划");
// 计划之后在目标侧造障碍:data 位置先占成普通文件 → 搬 data 必失败。
std::fs::create_dir_all(&new_home).expect("建目标根");
std::fs::write(new_home.join("data"), b"obstruction").expect("造障碍");
let err = dir_migration::execute(&plan, 42).expect_err("应当失败");
assert!(
matches!(err, PlatformError::MigrationRefused { .. }),
"{err}"
);
let old = LauncherLayout::new(old_home);
assert!(old.launcher_config().is_file(), "config 必须被回滚搬回来");
assert!(old.auth_data().join("token.json").is_file());
assert!(!old.redirect_path().exists(), "失败不该留指针");
assert!(!new_home.join("config").exists(), "目标侧不该残留已搬分区");
assert!(!new_home.join("data").is_dir(), "障碍文件保持原样");
}
#[test]
fn receipt_anti_replay_uses_source_target_signature() {
let temp = TempDir::new("dir-migration-receipt");
let receipt_path: PathBuf = temp.child("config/migration-receipt.json");
let receipt = receipt::MigrationReceipt::data_dir_move(
PathBuf::from("/old/.ULL"),
PathBuf::from("/new/.ULL"),
1_700_000_000_000,
);
receipt::write(&receipt_path, &receipt).expect("写收据");
let loaded = receipt::read(&receipt_path).expect("读").expect("有");
assert_eq!(
receipt::decide(Some(&loaded), &receipt.source_fingerprint),
receipt::ReplayDecision::SkipSameSource
);
}