Unlucky Launcher v0.0.1-alpha
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
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 开发中。
This commit is contained in:
commit
bc41dfcf7d
517 files changed
+92844
No files matched your search
@@ -0,0 +1,256 @@
|
||||
//! [`super`] 的单测:数据根键与其余键的落点分流、dump 合并、`problems` 通道。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
|
||||
use super::{dump, get, set};
|
||||
use crate::cli::Globals;
|
||||
use crate::context::Ctx;
|
||||
|
||||
/// 一次性临时目录(`Drop` 时清理;cli 没有 dev-dependency 的 tempfile)。
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
fn new(tag: &str) -> Self {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| duration.as_nanos());
|
||||
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ull-config-test-{tag}-{}-{seq}-{nanos}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 正确的 `$schema`(写成真前缀,加载才不会降级)。
|
||||
fn schema_url() -> String {
|
||||
use unlucky_core::setting::keys::launcher_version;
|
||||
use unlucky_core::setting::schema::{SchemaId, SchemaRef};
|
||||
SchemaRef::new(SchemaId::LAUNCHER, launcher_version()).url()
|
||||
}
|
||||
|
||||
fn write_json(path: &Path, value: &Value) {
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent).expect("建目录");
|
||||
}
|
||||
std::fs::write(path, value.to_string()).expect("写配置");
|
||||
}
|
||||
|
||||
fn read_json(path: &Path) -> Value {
|
||||
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("读 {} 失败:{e}", path.display()));
|
||||
serde_json::from_slice(&bytes).expect("合法 JSON")
|
||||
}
|
||||
|
||||
fn ctx_with(globals: Globals, exe_dir: &Path) -> Ctx {
|
||||
Ctx::resolve_in(&globals, Some(exe_dir.to_path_buf())).expect("解析数据根")
|
||||
}
|
||||
|
||||
/// 便携模式:两个落点是同一份文件,`core.data_mode` 改了立刻是生效值。
|
||||
#[test]
|
||||
fn portable_mode_keeps_core_keys_in_one_file() {
|
||||
let temp = TempDir::new("portable");
|
||||
let ctx = ctx_with(Globals::default(), temp.path());
|
||||
assert_eq!(ctx.data.mode.as_str(), "portable");
|
||||
assert_eq!(
|
||||
ctx.setting_path_for("core.data_mode"),
|
||||
ctx.launcher_config(),
|
||||
"便携模式下两份路径本来就该重合"
|
||||
);
|
||||
|
||||
set(&ctx, "core.data_mode", "standard").expect("写 core.data_mode");
|
||||
let file = temp
|
||||
.path()
|
||||
.join(".ULL")
|
||||
.join("config")
|
||||
.join("launcher.json");
|
||||
assert!(file.is_file(), "应落在 {}", file.display());
|
||||
assert_eq!(read_json(&file)["core"]["data_mode"], json!("standard"));
|
||||
|
||||
let report = get(&ctx, Some("core.data_mode"), true).expect("读回来");
|
||||
// `core.*` 在 schema 里标了 `AiPolicy::sensitive()`(High),读路径默认掩码成
|
||||
// `<redacted>`,所以要看生效值得像用户那样加 `--show-secret`。
|
||||
assert_eq!(report.data["value"], json!("standard"));
|
||||
assert_eq!(
|
||||
report.data["path"],
|
||||
json!(file.display().to_string()),
|
||||
"read 与 write 必须同一份"
|
||||
);
|
||||
}
|
||||
|
||||
/// standard 模式:数据根键走便携目录那份(与解析同源),其余键走数据根。
|
||||
#[test]
|
||||
fn standard_mode_routes_core_keys_to_the_portable_file() {
|
||||
let temp = TempDir::new("standard");
|
||||
let exe = temp.path().join("exe");
|
||||
let portable = exe.join(".ULL").join("config").join("launcher.json");
|
||||
let launcher_home = temp.path().join("lhome");
|
||||
write_json(
|
||||
&portable,
|
||||
&json!({
|
||||
"$schema": schema_url(),
|
||||
"schemaVersion": 1,
|
||||
"core": {
|
||||
"data_mode": "standard",
|
||||
"launcher_home": launcher_home.display().to_string(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
let ctx = ctx_with(Globals::default(), &exe);
|
||||
assert_eq!(ctx.data.mode.as_str(), "standard");
|
||||
assert_eq!(ctx.data.launcher_home, launcher_home);
|
||||
assert_ne!(
|
||||
ctx.setting_path_for("core.data_mode"),
|
||||
ctx.launcher_config()
|
||||
);
|
||||
|
||||
let report = get(&ctx, Some("core.data_mode"), true).expect("读 core.data_mode");
|
||||
assert_eq!(
|
||||
report.data["value"],
|
||||
json!("standard"),
|
||||
"get 展示的必须是真正生效的值"
|
||||
);
|
||||
assert_eq!(report.data["path"], json!(portable.display().to_string()));
|
||||
|
||||
// 普通键仍写数据根。
|
||||
set(&ctx, "download.concurrency", "16").expect("写 download.concurrency");
|
||||
let core_file = ctx.launcher_config();
|
||||
assert_eq!(read_json(&core_file)["download"]["concurrency"], json!(16));
|
||||
|
||||
// 数据根键写便携目录那份,且不动数据根那份的 core.*。
|
||||
set(&ctx, "core.data_mode", "portable").expect("写 core.data_mode");
|
||||
assert_eq!(read_json(&portable)["core"]["data_mode"], json!("portable"));
|
||||
assert!(
|
||||
read_json(&core_file)["core"].get("data_mode").is_none(),
|
||||
"数据根那份不该被写入 core.data_mode"
|
||||
);
|
||||
}
|
||||
|
||||
/// `--data-dir` 模式:数据根由命令行钉死,core.* 仍落便携目录那份并给出提示。
|
||||
#[test]
|
||||
fn data_dir_flag_routes_core_keys_to_portable_and_says_so() {
|
||||
let temp = TempDir::new("datadir");
|
||||
let exe = temp.path().join("exe");
|
||||
std::fs::create_dir_all(&exe).expect("建 exe 目录");
|
||||
let data = temp.path().join("data");
|
||||
let globals = Globals {
|
||||
data_dir: Some(data.clone()),
|
||||
..Globals::default()
|
||||
};
|
||||
let ctx = ctx_with(globals, &exe);
|
||||
assert_eq!(ctx.data.mode.as_str(), "custom");
|
||||
assert_eq!(ctx.data.launcher_home, data.join(".ULL"));
|
||||
|
||||
let portable = exe.join(".ULL").join("config").join("launcher.json");
|
||||
set(&ctx, "core.data_mode", "standard").expect("写 core.data_mode");
|
||||
assert!(portable.is_file(), "应落在 {}", portable.display());
|
||||
assert!(!data
|
||||
.join(".ULL")
|
||||
.join("config")
|
||||
.join("launcher.json")
|
||||
.exists());
|
||||
|
||||
let report = get(&ctx, Some("core.data_mode"), true).expect("读回来");
|
||||
assert_eq!(report.data["value"], json!("standard"));
|
||||
assert!(
|
||||
report.human.iter().any(|line| line.contains("--data-dir")),
|
||||
"{:?}",
|
||||
report.human
|
||||
);
|
||||
}
|
||||
|
||||
/// 全量 dump:`core.*` 取便携目录那份的生效值,并标明两份文件与数据根。
|
||||
#[test]
|
||||
fn dump_merges_core_keys_from_the_portable_file() {
|
||||
let temp = TempDir::new("dump");
|
||||
let exe = temp.path().join("exe");
|
||||
let portable = exe.join(".ULL").join("config").join("launcher.json");
|
||||
let launcher_home = temp.path().join("home2");
|
||||
write_json(
|
||||
&portable,
|
||||
&json!({
|
||||
"$schema": schema_url(),
|
||||
"schemaVersion": 1,
|
||||
"core": {
|
||||
"data_mode": "standard",
|
||||
"launcher_home": launcher_home.display().to_string(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
// 数据根那份写一个普通键(默认值不落盘,dump 里只有文件里真有的键)。
|
||||
write_json(
|
||||
&launcher_home.join("config").join("launcher.json"),
|
||||
&json!({
|
||||
"$schema": schema_url(),
|
||||
"schemaVersion": 1,
|
||||
"download": { "concurrency": 4 },
|
||||
}),
|
||||
);
|
||||
let ctx = ctx_with(Globals::default(), &exe);
|
||||
|
||||
let report = dump(&ctx, true).expect("全量 dump");
|
||||
assert_eq!(
|
||||
report.data["config"]["core"]["data_mode"],
|
||||
json!("standard")
|
||||
);
|
||||
assert_eq!(
|
||||
report.data["core_path"],
|
||||
json!(portable.display().to_string())
|
||||
);
|
||||
assert_eq!(report.data["data_root"]["mode"], json!("standard"));
|
||||
assert_eq!(
|
||||
report.data["config"]["download"]["concurrency"],
|
||||
json!(4),
|
||||
"非 core.* 键仍取数据根那份"
|
||||
);
|
||||
}
|
||||
|
||||
/// 认不出的 `$schema` 前缀:值照读 + `problems` 里有 UnknownSchema,不再静默回落。
|
||||
#[test]
|
||||
fn unknown_schema_prefix_warns_and_still_shows_file_values() {
|
||||
let temp = TempDir::new("badschema");
|
||||
let exe = temp.path().join("exe");
|
||||
let portable = exe.join(".ULL").join("config").join("launcher.json");
|
||||
write_json(
|
||||
&portable,
|
||||
&json!({
|
||||
"$schema": "https://unlucky-launcher.local/schema/launcher/1.2.0.json",
|
||||
"schemaVersion": 1,
|
||||
"core": { "data_mode": "portable" },
|
||||
"download": { "concurrency": 32 },
|
||||
}),
|
||||
);
|
||||
let ctx = ctx_with(Globals::default(), &exe);
|
||||
|
||||
let report = get(&ctx, Some("download.concurrency"), false).expect("读普通键");
|
||||
assert_eq!(report.data["value"], json!(32), "值不该被回落成默认 8");
|
||||
let problems = report.data["problems"].as_array().expect("problems 数组");
|
||||
assert!(
|
||||
problems
|
||||
.iter()
|
||||
.any(|problem| problem.as_str().is_some_and(|p| p.contains("$schema"))),
|
||||
"{problems:?}"
|
||||
);
|
||||
assert!(
|
||||
report.human.iter().any(|line| line.contains("$schema")),
|
||||
"{:?}",
|
||||
report.human
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user