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
Rust 原生单文件 Minecraft 启动器:CLI 完整可用(install/launch/accounts/java/diagnose/agent), MCP server 接入外部 Agent 工具,GUI 开发中。
206 lines
7.1 KiB
Rust
206 lines
7.1 KiB
Rust
//! 三模式数据目录解析:Portable / Standard / Custom + 平台落点 + 高级覆盖 + redirect 跟随。
|
|
|
|
use std::path::PathBuf;
|
|
|
|
use unlucky_core::platform::data_dir::{DataDirRequest, DataMode, ResolvedData, Resolver};
|
|
use unlucky_core::platform::error::PlatformError;
|
|
use unlucky_core::platform::paths::{standard_game_home, standard_launcher_home, MapEnv, Platform};
|
|
use unlucky_core::platform::redirect::{self, RedirectPointer, REDIRECT_SCHEMA_VERSION};
|
|
|
|
use crate::common::TempDir;
|
|
|
|
#[test]
|
|
fn portable_puts_dot_ull_and_minecraft_beside_exe() {
|
|
let env = MapEnv::new().with("APPDATA", "/roaming");
|
|
let resolver = Resolver::with_env(Platform::Windows, Some(PathBuf::from("/games/ULL")), &env);
|
|
assert_eq!(resolver.default_mode(), DataMode::Portable);
|
|
|
|
let resolved = resolver.resolve(&DataDirRequest::default()).expect("解析");
|
|
assert_eq!(resolved.mode, DataMode::Portable);
|
|
assert_eq!(resolved.launcher_home, PathBuf::from("/games/ULL/.ULL"));
|
|
assert_eq!(resolved.game_home, PathBuf::from("/games/ULL/.minecraft"));
|
|
assert!(resolved.redirected_from.is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn portable_without_exe_dir_errors() {
|
|
let env = MapEnv::new();
|
|
let resolver = Resolver::with_env(Platform::Linux, None, &env);
|
|
let err = resolver
|
|
.resolve(&DataDirRequest::default())
|
|
.expect_err("应当报错");
|
|
assert!(matches!(err, PlatformError::NoExecutableDir));
|
|
}
|
|
|
|
#[test]
|
|
fn standard_uses_per_os_locations() {
|
|
let windows = MapEnv::new().with("APPDATA", "/roaming");
|
|
assert_eq!(
|
|
standard_launcher_home(Platform::Windows, &windows),
|
|
Some(PathBuf::from("/roaming/ULL"))
|
|
);
|
|
assert_eq!(
|
|
standard_game_home(Platform::Windows, &windows),
|
|
Some(PathBuf::from("/roaming/.minecraft"))
|
|
);
|
|
|
|
let mac = MapEnv::new().with("HOME", "/Users/u");
|
|
assert_eq!(
|
|
standard_launcher_home(Platform::MacOs, &mac),
|
|
Some(PathBuf::from("/Users/u/Library/Application Support/ULL"))
|
|
);
|
|
assert_eq!(
|
|
standard_game_home(Platform::MacOs, &mac),
|
|
Some(PathBuf::from(
|
|
"/Users/u/Library/Application Support/minecraft"
|
|
))
|
|
);
|
|
|
|
// XDG 规范要求绝对路径;测试用 temp_dir(两种平台都算绝对路径)。
|
|
let xdg_root = std::env::temp_dir().join("ull-xdg");
|
|
let xdg_text = xdg_root.to_string_lossy().into_owned();
|
|
let linux = MapEnv::new()
|
|
.with("HOME", "/home/u")
|
|
.with("XDG_DATA_HOME", &xdg_text);
|
|
assert_eq!(
|
|
standard_launcher_home(Platform::Linux, &linux),
|
|
Some(xdg_root.join("ull"))
|
|
);
|
|
assert_eq!(
|
|
standard_game_home(Platform::Linux, &linux),
|
|
Some(PathBuf::from("/home/u/.minecraft"))
|
|
);
|
|
|
|
// 相对 XDG_DATA_HOME 不合法 → 回落 ~/.local/share/ull。
|
|
let relative_xdg = MapEnv::new()
|
|
.with("HOME", "/home/u")
|
|
.with("XDG_DATA_HOME", "relative/xdg");
|
|
assert_eq!(
|
|
standard_launcher_home(Platform::Linux, &relative_xdg),
|
|
Some(PathBuf::from("/home/u/.local/share/ull"))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn standard_falls_back_when_env_missing() {
|
|
let env = MapEnv::new()
|
|
.with("HOME", "/home/u")
|
|
.with("USERPROFILE", "/home/u");
|
|
assert_eq!(
|
|
standard_launcher_home(Platform::Linux, &env),
|
|
Some(PathBuf::from("/home/u/.local/share/ull"))
|
|
);
|
|
assert_eq!(
|
|
standard_launcher_home(Platform::Windows, &env),
|
|
Some(PathBuf::from("/home/u/AppData/Roaming/ULL"))
|
|
);
|
|
|
|
let empty = MapEnv::new();
|
|
assert_eq!(standard_launcher_home(Platform::Windows, &empty), None);
|
|
}
|
|
|
|
#[test]
|
|
fn standard_mode_resolution_and_macos_default() {
|
|
let env = MapEnv::new().with("HOME", "/Users/u");
|
|
let resolver = Resolver::with_env(Platform::MacOs, None, &env);
|
|
assert_eq!(resolver.default_mode(), DataMode::Standard);
|
|
|
|
let request = DataDirRequest {
|
|
mode: DataMode::Standard,
|
|
..DataDirRequest::default()
|
|
};
|
|
let resolved = resolver.resolve(&request).expect("解析");
|
|
assert_eq!(
|
|
resolved.launcher_home,
|
|
PathBuf::from("/Users/u/Library/Application Support/ULL")
|
|
);
|
|
assert_eq!(
|
|
resolved.game_home,
|
|
PathBuf::from("/Users/u/Library/Application Support/minecraft")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn custom_requires_absolute_existing_root_choice() {
|
|
let env = MapEnv::new();
|
|
let resolver = Resolver::with_env(Platform::Windows, Some(PathBuf::from("/games")), &env);
|
|
let root = std::env::temp_dir().join("ull-custom-root");
|
|
|
|
let ok = DataDirRequest {
|
|
mode: DataMode::Custom,
|
|
custom_root: Some(root.clone()),
|
|
..DataDirRequest::default()
|
|
};
|
|
let resolved = resolver.resolve(&ok).expect("解析");
|
|
assert_eq!(resolved.launcher_home, root.join(".ULL"));
|
|
assert_eq!(resolved.game_home, root.join(".minecraft"));
|
|
|
|
let relative = DataDirRequest {
|
|
mode: DataMode::Custom,
|
|
custom_root: Some(PathBuf::from("relative/dir")),
|
|
..DataDirRequest::default()
|
|
};
|
|
assert!(matches!(
|
|
resolver.resolve(&relative).expect_err("相对路径应报错"),
|
|
PlatformError::InvalidCustomRoot { .. }
|
|
));
|
|
|
|
let missing = DataDirRequest {
|
|
mode: DataMode::Custom,
|
|
custom_root: None,
|
|
..DataDirRequest::default()
|
|
};
|
|
assert!(resolver.resolve(&missing).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn advanced_overrides_split_launcher_and_game_home() {
|
|
let env = MapEnv::new();
|
|
let resolver = Resolver::with_env(Platform::Windows, Some(PathBuf::from("/games")), &env);
|
|
let request = DataDirRequest {
|
|
launcher_home: Some(PathBuf::from("/config/ULL")),
|
|
game_home: Some(PathBuf::from("/bigdisk/.minecraft")),
|
|
..DataDirRequest::default()
|
|
};
|
|
let resolved = resolver.resolve(&request).expect("解析");
|
|
assert_eq!(resolved.launcher_home, PathBuf::from("/config/ULL"));
|
|
assert_eq!(resolved.game_home, PathBuf::from("/bigdisk/.minecraft"));
|
|
assert!(!resolved.same_target(&ResolvedData {
|
|
mode: DataMode::Portable,
|
|
launcher_home: PathBuf::from("/other"),
|
|
game_home: PathBuf::from("/bigdisk/.minecraft"),
|
|
redirected_from: None,
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn effective_resolution_follows_redirect_pointer() {
|
|
let temp = TempDir::new("data-dir-redirect");
|
|
let exe_dir = temp.child("portable");
|
|
let moved_to = temp.child("after-move");
|
|
std::fs::create_dir_all(&exe_dir).expect("建目录");
|
|
|
|
let pointer = RedirectPointer {
|
|
schema_version: REDIRECT_SCHEMA_VERSION,
|
|
mode: DataMode::Custom,
|
|
launcher_home: moved_to.clone(),
|
|
game_home: moved_to.join(".minecraft"),
|
|
migrated_at_ms: 1_700_000_000_000,
|
|
};
|
|
redirect::write(&exe_dir.join(".ULL"), &pointer).expect("写指针");
|
|
|
|
let env = MapEnv::new();
|
|
let resolver = Resolver::with_env(Platform::Windows, Some(exe_dir.clone()), &env);
|
|
let resolved = resolver
|
|
.resolve_default(&DataDirRequest::default())
|
|
.expect("解析");
|
|
|
|
assert_eq!(resolved.launcher_home, moved_to);
|
|
assert_eq!(resolved.mode, DataMode::Portable);
|
|
assert_eq!(
|
|
resolved.redirected_from,
|
|
Some(exe_dir.join(".ULL")),
|
|
"旧目录要留在记录里,便于 UI 提示「数据已迁走」"
|
|
);
|
|
}
|