release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
CI / fmt (rustfmt --check) (push) Waiting to run
CI / clippy (ubuntu-latest) (push) Waiting to run
CI / clippy (windows-latest) (push) Waiting to run
CI / test (ubuntu-latest) (push) Waiting to run
CI / test (windows-latest) (push) Waiting to run
CI / build release (ubuntu-latest) (push) Waiting to run
CI / build release (windows-latest) (push) Waiting to run
CI / runtime smoke (ubuntu x64, xvfb) (push) Waiting to run
CI / runtime smoke (macos arm64, probe) (push) Skipped
CI / update-e2e (windows x64) (push) Waiting to run
GPU evidence / GPU 判据自测(不需 GPU;跳过 ≠ 已验证) (push) Waiting to run
GPU evidence / GPU 证据 — 探测可用产物与硬件适配器 (push) Waiting to run
Guards / core 零 UI 守门 (push) Waiting to run
Guards / presenter 零 UI 守门 (push) Waiting to run
Guards / design 零第三方依赖守门 (push) Waiting to run
Guards / unsafe 落点守门(集合必须恰好等于期望) (push) Waiting to run
Guards / 文件行数 ≤400 守门 (push) Waiting to run
Guards / 依赖许可与来源(cargo-deny) (push) Waiting to run
Release guard / 发布标记语义守门(push tag → 稳定版) (push) Waiting to run
Signing status(播报,不是门禁) / 判据自测(三行/退出码/变异重放/接线) (push) Waiting to run
Signing status(播报,不是门禁) / 签名就绪度(播报) (push) Waiting to run
Release / 选槽位(默认四个:Windows 两档 + Linux 两档) (push) Waiting to run
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
This commit is contained in:
commit
a5d1dec495
1580 files changed
+320938
No files matched your search
@@ -0,0 +1,98 @@
|
||||
{
|
||||
"meta": {
|
||||
"description": "LiteLoader is a lightweight mod bootstrap designed to provide basic loader functionality for mods which don't need to modify game mechanics.",
|
||||
"authors": "Mumfrey",
|
||||
"url": "http://dl.liteloader.com",
|
||||
"updated": "2017-11-29T16:31:41+00:00",
|
||||
"updatedTime": 1511973101
|
||||
},
|
||||
"versions": {
|
||||
"1.7.10": {
|
||||
"repo": {
|
||||
"stream": "RELEASE",
|
||||
"type": "ivy",
|
||||
"url": "http://dl.liteloader.com/versions/",
|
||||
"classifier": "mcpnames"
|
||||
},
|
||||
"artefacts": {
|
||||
"com.mumfrey:liteloader": {
|
||||
"63ada46e033d0cb6782bada09ad5ca4e": {
|
||||
"tweakClass": "com.mumfrey.liteloader.launch.LiteLoaderTweaker",
|
||||
"libraries": [
|
||||
{ "name": "net.minecraft:launchwrapper:1.11" },
|
||||
{ "name": "org.ow2.asm:asm-all:5.0.3" }
|
||||
],
|
||||
"srcJar": "liteloader-1.7.10_04-mcpnames-sources.jar",
|
||||
"mcpJar": "liteloader-1.7.10_04-mcpnames.jar",
|
||||
"stream": "RELEASE",
|
||||
"file": "liteloader-1.7.10_04.jar",
|
||||
"version": "1.7.10_04",
|
||||
"md5": "63ada46e033d0cb6782bada09ad5ca4e",
|
||||
"timestamp": "1414368553"
|
||||
},
|
||||
"db7235aefd407ac1fde09a7baba50839": {
|
||||
"tweakClass": "com.mumfrey.liteloader.launch.LiteLoaderTweaker",
|
||||
"libraries": [
|
||||
{ "name": "net.minecraft:launchwrapper:1.9" },
|
||||
{
|
||||
"name": "org.ow2.asm:asm-all:5.0.3",
|
||||
"url": "http://repo.maven.apache.org/maven2/"
|
||||
}
|
||||
],
|
||||
"stream": "RELEASE",
|
||||
"file": "liteloader-1.7.10_00.jar",
|
||||
"version": "1.7.10_00",
|
||||
"md5": "db7235aefd407ac1fde09a7baba50839",
|
||||
"timestamp": "1404330030"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"1.12.2": {
|
||||
"repo": {
|
||||
"stream": "SNAPSHOT",
|
||||
"type": "m2",
|
||||
"url": "http://repo.mumfrey.com/content/repositories/snapshots/",
|
||||
"classifier": ""
|
||||
},
|
||||
"snapshots": {
|
||||
"libraries": [
|
||||
{ "name": "net.minecraft:launchwrapper:1.12" },
|
||||
{
|
||||
"name": "org.spongepowered:mixin:0.7.4-SNAPSHOT",
|
||||
"url": "https://repo.spongepowered.org/maven/"
|
||||
}
|
||||
],
|
||||
"com.mumfrey:liteloader": {
|
||||
"1420785ecbfed5aff4a586c5c9dd97eb": {
|
||||
"tweakClass": "com.mumfrey.liteloader.launch.LiteLoaderTweaker",
|
||||
"libraries": [
|
||||
{ "name": "net.minecraft:launchwrapper:1.12" },
|
||||
{ "name": "org.ow2.asm:asm-all:5.2" }
|
||||
],
|
||||
"stream": "SNAPSHOT",
|
||||
"file": "liteloader-1.12.2-SNAPSHOT.jar",
|
||||
"version": "1.12.2-SNAPSHOT",
|
||||
"build": "1.12.2-SNAPSHOT-r4CC2BB0-b4-4",
|
||||
"md5": "1420785ecbfed5aff4a586c5c9dd97eb",
|
||||
"timestamp": "1511880271",
|
||||
"lastSuccessfulBuild": 4
|
||||
},
|
||||
"latest": {
|
||||
"tweakClass": "com.mumfrey.liteloader.launch.LiteLoaderTweaker",
|
||||
"libraries": [
|
||||
{ "name": "net.minecraft:launchwrapper:1.12" },
|
||||
{ "name": "org.ow2.asm:asm-all:5.2" }
|
||||
],
|
||||
"stream": "SNAPSHOT",
|
||||
"file": "liteloader-1.12.2-SNAPSHOT.jar",
|
||||
"version": "1.12.2-SNAPSHOT",
|
||||
"md5": "1420785ecbfed5aff4a586c5c9dd97eb",
|
||||
"timestamp": "1511880271",
|
||||
"lastSuccessfulBuild": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
//! 边界与失败面:父版本缺失、组合不支持、数据源形状坏、取消。
|
||||
//!
|
||||
//! 网络层的失败映射(`DownloadError` → `loader_network`)由 `core::download` 的引擎测试
|
||||
//! 与既有加载器测试覆盖,这里**不联网**:外部源的真实可用性在 mission 报告里单独列
|
||||
//! (含探测结果),不混进单元测试冒充。
|
||||
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
use unlucky_core::addon::loader::liteloader::parse_versions;
|
||||
use unlucky_core::addon::loader::{
|
||||
LoaderErrorKind, LoaderInstallPlan, LoaderKind, LoaderPlanInput, LoaderSource,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::game::arguments::Environment;
|
||||
use unlucky_core::game::install::{execute_with_cancel, InstallErrorKind};
|
||||
use unlucky_core::game::layout::GameLayout;
|
||||
|
||||
use crate::support::{engine, liteloader_release, prefill, put_version, TempDir, VANILLA_1122};
|
||||
|
||||
/// 父版本不是原版、又没装在盘上 → **具名报错**,绝不默默改成叠原版。
|
||||
#[test]
|
||||
fn missing_loader_parent_is_a_named_error() {
|
||||
let temp = TempDir::new("missing-parent");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
let release = liteloader_release("1.12.2-forge-14.23.5.2860");
|
||||
|
||||
let error = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &release,
|
||||
environment: &Environment::host(),
|
||||
parent_installed: false,
|
||||
})
|
||||
.expect_err("父版本缺失必须报错");
|
||||
assert_eq!(error.kind(), LoaderErrorKind::NotFound);
|
||||
assert!(
|
||||
error.message().contains("1.12.2-forge-14.23.5.2860"),
|
||||
"错误里要点名缺的是哪个父版本:{}",
|
||||
error.message()
|
||||
);
|
||||
assert!(error.hint().is_some(), "要给出下一步怎么办");
|
||||
}
|
||||
|
||||
/// 父版本链:读盘定位原版版本 id(合成 loader id / 定位 jar 都用它)。
|
||||
#[test]
|
||||
fn parent_chain_walks_to_the_vanilla_root() {
|
||||
let temp = TempDir::new("parent-chain");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
put_version(
|
||||
&layout,
|
||||
"1.12.2-forge-14.23.5.2860",
|
||||
crate::support::FORGE_1122,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
unlucky_core::addon::loader::parent::root_version_id(&layout, "1.12.2-forge-14.23.5.2860")
|
||||
.expect("两层链应走得到根"),
|
||||
"1.12.2"
|
||||
);
|
||||
assert_eq!(
|
||||
unlucky_core::addon::loader::parent::root_version_id(&layout, "1.12.2").expect("根是自己"),
|
||||
"1.12.2"
|
||||
);
|
||||
|
||||
// 没装的父版本 → not_found(而不是猜一个原版版本号)。
|
||||
let error = unlucky_core::addon::loader::parent::root_version_id(&layout, "9.9.9-forge-1.0.0")
|
||||
.expect_err("缺父版本必须报错");
|
||||
assert_eq!(error.kind(), LoaderErrorKind::NotFound);
|
||||
}
|
||||
|
||||
/// 只有 LiteLoader 接受「父版本不是原版」;别家**具名拒绝**,不去发请求。
|
||||
#[test]
|
||||
fn only_liteloader_accepts_a_non_vanilla_parent() {
|
||||
let temp = TempDir::new("combo-guard");
|
||||
let engine = engine(temp.path());
|
||||
let source = LoaderSource::new(&engine, temp.path());
|
||||
let error = block_on(async {
|
||||
source
|
||||
.resolve_on(
|
||||
LoaderKind::Forge,
|
||||
"1.12.2",
|
||||
Some("14.23.5.2860"),
|
||||
Some("1.12.2-liteloader-1.12.2-SNAPSHOT"),
|
||||
)
|
||||
.await
|
||||
})
|
||||
.expect_err("Forge 叠在 LiteLoader 上必须被拒");
|
||||
assert_eq!(error.kind(), LoaderErrorKind::Unsupported);
|
||||
assert!(
|
||||
error
|
||||
.message()
|
||||
.contains("1.12.2-liteloader-1.12.2-SNAPSHOT"),
|
||||
"要说清是哪个组合:{}",
|
||||
error.message()
|
||||
);
|
||||
}
|
||||
|
||||
/// 版本表形状坏 / 空表:各自给具名大类,不当成「没有版本」糊过去。
|
||||
#[test]
|
||||
fn broken_version_table_is_reported_distinctly() {
|
||||
let shape = parse_versions(r#"{"versions": []}"#).expect_err("形状不符");
|
||||
assert_eq!(shape.kind(), LoaderErrorKind::Metadata);
|
||||
let empty = parse_versions(r#"{"meta": {}, "versions": {}}"#).expect_err("空表");
|
||||
assert_eq!(empty.kind(), LoaderErrorKind::NotFound);
|
||||
|
||||
// 版本段缺失(记录没有 version):只剩 latest 别名 → 仍当空表处理。
|
||||
let only_latest = r#"{"versions": {"1.7.10": {"artefacts": {"com.mumfrey:liteloader":
|
||||
{"latest": {"tweakClass": "x", "version": "", "file": "liteloader.jar"}}}}}}"#;
|
||||
let error = parse_versions(only_latest).expect_err("只有 http 别名时没有可用版本");
|
||||
assert_eq!(error.kind(), LoaderErrorKind::NotFound);
|
||||
}
|
||||
|
||||
/// 取消:执行段看标志就返回 `Cancelled`,**版本 JSON 不落盘**(不留半成品)。
|
||||
#[test]
|
||||
fn cancel_leaves_no_manifest() {
|
||||
let temp = TempDir::new("cancel");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
let release = liteloader_release("1.12.2");
|
||||
let plan = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &release,
|
||||
environment: &Environment::host(),
|
||||
parent_installed: true,
|
||||
})
|
||||
.expect("计划应可组");
|
||||
|
||||
let engine = engine(temp.path());
|
||||
prefill(&plan.install);
|
||||
let cancel = AtomicBool::new(true);
|
||||
let error = block_on(execute_with_cancel(
|
||||
&engine,
|
||||
&plan.install,
|
||||
4,
|
||||
None,
|
||||
Some(&cancel),
|
||||
))
|
||||
.expect_err("取消必须返回错误");
|
||||
assert_eq!(error.kind(), InstallErrorKind::Cancelled);
|
||||
assert!(
|
||||
!layout.version_json(&release.version_id).exists(),
|
||||
"取消之后不许留下版本清单"
|
||||
);
|
||||
}
|
||||
|
||||
/// 同一个 loader 版本 id 被两条父链请求:**具名报错**,且盘上那份清单一个字节都不动。
|
||||
///
|
||||
/// 为什么必须有这道门:`<原版>-liteloader-<ver>` 对「叠原版」与「叠 Forge」是同一个目录名,
|
||||
/// 直接覆盖会让已经登记、指向这个版本 id 的旧实例**静默换父链**(启动行为跟着变)。
|
||||
#[test]
|
||||
fn parent_collision_is_refused_instead_of_overwriting() {
|
||||
let temp = TempDir::new("parent-collision");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
put_version(
|
||||
&layout,
|
||||
"1.12.2-forge-14.23.5.2860",
|
||||
crate::support::FORGE_1122,
|
||||
);
|
||||
|
||||
let vanilla_release = liteloader_release("1.12.2");
|
||||
let existing = serde_json::json!({
|
||||
"id": vanilla_release.version_id,
|
||||
"inheritsFrom": "1.12.2",
|
||||
"mainClass": "net.minecraft.launchwrapper.Launch",
|
||||
"arguments": {"game": ["--tweakClass", "com.mumfrey.liteloader.launch.LiteLoaderTweaker"]},
|
||||
"libraries": []
|
||||
});
|
||||
put_version(
|
||||
&layout,
|
||||
&vanilla_release.version_id,
|
||||
&serde_json::to_string_pretty(&existing).expect("序列化"),
|
||||
);
|
||||
let before = std::fs::read_to_string(layout.version_json(&vanilla_release.version_id))
|
||||
.expect("读旧清单");
|
||||
|
||||
let forge_release = liteloader_release("1.12.2-forge-14.23.5.2860");
|
||||
assert_eq!(
|
||||
forge_release.version_id, vanilla_release.version_id,
|
||||
"两条父链撞同一个 id"
|
||||
);
|
||||
let error = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &forge_release,
|
||||
environment: &Environment::host(),
|
||||
parent_installed: true,
|
||||
})
|
||||
.expect_err("撞父链必须报错");
|
||||
assert_eq!(error.kind(), LoaderErrorKind::Unsupported);
|
||||
assert!(
|
||||
error.message().contains("1.12.2-forge-14.23.5.2860") && error.message().contains("1.12.2"),
|
||||
"要说清两条父链各是什么:{}",
|
||||
error.message()
|
||||
);
|
||||
|
||||
let after = std::fs::read_to_string(layout.version_json(&vanilla_release.version_id))
|
||||
.expect("读旧清单");
|
||||
assert_eq!(before, after, "报错之后盘上那份清单不许被改动");
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
//! 解析 → 计划 → 落盘 → 继承解析 → 启动参数顺序(两条父链:原版 / Forge)。
|
||||
|
||||
use unlucky_core::addon::loader::{LoaderInstallPlan, LoaderPlanInput};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::game::arguments::Environment;
|
||||
use unlucky_core::game::inherit::resolve;
|
||||
use unlucky_core::game::install::execute;
|
||||
use unlucky_core::game::layout::GameLayout;
|
||||
use unlucky_core::launch::command_builder::tokenize;
|
||||
|
||||
use crate::support::{
|
||||
engine, liteloader_release, prefill, put_version, read_version, TempDir, FORGE_1122,
|
||||
VANILLA_1122,
|
||||
};
|
||||
|
||||
/// 原版父链:装完能落盘,清单指向原版,`arguments.game` 带 tweakClass。
|
||||
#[test]
|
||||
fn installs_over_vanilla_and_writes_the_manifest() {
|
||||
let temp = TempDir::new("vanilla");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
|
||||
let release = liteloader_release("1.12.2");
|
||||
assert_eq!(release.version_id, "1.12.2-liteloader-1.12.2-SNAPSHOT");
|
||||
let plan = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &release,
|
||||
environment: &Environment::host(),
|
||||
parent_installed: true,
|
||||
})
|
||||
.expect("父版本已装,计划应可组");
|
||||
assert!(!plan.vanilla_install_needed);
|
||||
assert!(plan.install.client_jar().is_none(), "jar 归原版层");
|
||||
|
||||
// 三个库:launchwrapper(Mojang 库仓)+ asm-all(Central)+ 本体 jar(官方直链)。
|
||||
let names: Vec<&str> = plan.files().iter().map(|file| file.name.as_str()).collect();
|
||||
assert_eq!(names.len(), 3, "{names:?}");
|
||||
let loader_jar = plan
|
||||
.files()
|
||||
.iter()
|
||||
.find(|file| file.name == "com.mumfrey:liteloader:1.12.2-SNAPSHOT")
|
||||
.expect("缺本体 jar");
|
||||
assert!(
|
||||
loader_jar
|
||||
.url
|
||||
.ends_with("/1.12.2-SNAPSHOT/liteloader-1.12.2-20171128.144431-4-release.jar"),
|
||||
"本体直链:{}",
|
||||
loader_jar.url
|
||||
);
|
||||
let asm = plan
|
||||
.files()
|
||||
.iter()
|
||||
.find(|file| file.name.contains("asm-all"))
|
||||
.expect("缺 asm-all");
|
||||
assert!(
|
||||
asm.url.starts_with("https://repo1.maven.org/maven2/"),
|
||||
"asm-all 只能从 Central 取:{}",
|
||||
asm.url
|
||||
);
|
||||
|
||||
// 全部落点先铺好占位文件(库无 sha1/size → 存在即信),执行段只写版本 JSON。
|
||||
let engine = engine(temp.path());
|
||||
let prefilled = prefill(&plan.install);
|
||||
assert_eq!(prefilled, 3);
|
||||
let report = block_on(execute(&engine, &plan.install, 4, None)).expect("执行应成功");
|
||||
assert!(report.is_complete(), "{:?}", report.failures(3));
|
||||
|
||||
let written = read_version(&layout, &release.version_id);
|
||||
assert_eq!(written.inherits_from.as_deref(), Some("1.12.2"));
|
||||
assert_eq!(
|
||||
written.main_class.as_deref(),
|
||||
Some("net.minecraft.launchwrapper.Launch")
|
||||
);
|
||||
assert!(
|
||||
written.minecraft_arguments.is_none(),
|
||||
"loader 层不许写 minecraftArguments:那会顶掉父链"
|
||||
);
|
||||
}
|
||||
|
||||
/// Forge 父链:**父链的 FMLTweaker 必须还在**,LiteLoader 的 tweakClass 追加在它后面。
|
||||
///
|
||||
/// 这条是 M218 SURVEY-p1 的「准入条件」:靠 `--tweakClass` 生效,就要求继承时
|
||||
/// `arguments.game` 是追加、`minecraftArguments` 不被子层覆盖。启动参数顺序照
|
||||
/// `core::launch::game_args` 的第 24 项(legacy 子串)→ 第 25 项(`arguments.game`)复现,
|
||||
/// 真实 argv 由 `tests/launch/cmdline_snapshot.rs` 钉住。
|
||||
#[test]
|
||||
fn stacks_on_forge_and_keeps_the_forge_tweak_class() {
|
||||
let temp = TempDir::new("forge");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
put_version(&layout, "1.12.2-forge-14.23.5.2860", FORGE_1122);
|
||||
|
||||
let release = liteloader_release("1.12.2-forge-14.23.5.2860");
|
||||
assert!(!release.parent_is_vanilla());
|
||||
let plan = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &release,
|
||||
environment: &Environment::host(),
|
||||
parent_installed: true,
|
||||
})
|
||||
.expect("父版本已装,计划应可组");
|
||||
assert_eq!(plan.install.jar_id, "1.12.2", "jar 仍取原版那层");
|
||||
|
||||
let engine = engine(temp.path());
|
||||
prefill(&plan.install);
|
||||
let report = block_on(execute(&engine, &plan.install, 4, None)).expect("执行应成功");
|
||||
assert!(report.is_complete(), "{:?}", report.failures(3));
|
||||
|
||||
let installed = read_version(&layout, &release.version_id);
|
||||
let mut source = std::collections::BTreeMap::new();
|
||||
source.insert("1.12.2".to_string(), read_version(&layout, "1.12.2"));
|
||||
source.insert(
|
||||
"1.12.2-forge-14.23.5.2860".to_string(),
|
||||
read_version(&layout, "1.12.2-forge-14.23.5.2860"),
|
||||
);
|
||||
source.insert(release.version_id.clone(), installed);
|
||||
let resolved = resolve(&source, &source[&release.version_id]).expect("三层继承应可解析");
|
||||
|
||||
// ① 父链的 legacy 参数整串还在(子层没写 minecraftArguments)。
|
||||
let legacy = resolved
|
||||
.minecraft_arguments
|
||||
.as_deref()
|
||||
.expect("Forge 的 minecraftArguments 不许被子层顶掉");
|
||||
assert!(
|
||||
legacy.contains("net.minecraftforge.fml.common.launcher.FMLTweaker"),
|
||||
"Forge 的 tweakClass 丢了:{legacy}"
|
||||
);
|
||||
|
||||
// ② 启动顺序:legacy(第 24 项)在前,arguments.game(第 25 项)在后。
|
||||
let mut game: Vec<String> = tokenize(legacy);
|
||||
game.extend(
|
||||
resolved
|
||||
.arguments
|
||||
.as_ref()
|
||||
.expect("arguments 应保留")
|
||||
.expand(&Environment::host())
|
||||
.game,
|
||||
);
|
||||
let forge_at = game
|
||||
.iter()
|
||||
.position(|token| token.contains("FMLTweaker"))
|
||||
.expect("Forge tweakClass 不在游戏参数里");
|
||||
let liteloader_at = game
|
||||
.iter()
|
||||
.position(|token| token == "com.mumfrey.liteloader.launch.LiteLoaderTweaker")
|
||||
.expect("LiteLoader tweakClass 不在游戏参数里");
|
||||
assert!(
|
||||
forge_at < liteloader_at,
|
||||
"launchwrapper 按出现顺序应用 tweak:Forge 必须在前,实际 {game:?}"
|
||||
);
|
||||
assert_eq!(game[liteloader_at - 1], "--tweakClass");
|
||||
|
||||
// ③ 主类与库顺序:Forge 的库在前(classpath 先到先得),LiteLoader 的在后。
|
||||
assert_eq!(
|
||||
resolved.main_class.as_deref(),
|
||||
Some("net.minecraft.launchwrapper.Launch")
|
||||
);
|
||||
let libraries: Vec<&str> = resolved
|
||||
.libraries
|
||||
.as_deref()
|
||||
.expect("libraries")
|
||||
.iter()
|
||||
.map(|library| library.name.as_str())
|
||||
.collect();
|
||||
let forge_lib = libraries
|
||||
.iter()
|
||||
.position(|name| name.starts_with("net.minecraftforge:forge:"))
|
||||
.expect("缺 Forge 本体库");
|
||||
let liteloader_lib = libraries
|
||||
.iter()
|
||||
.position(|name| name.starts_with("com.mumfrey:liteloader:"))
|
||||
.expect("缺 LiteLoader 本体库");
|
||||
assert!(forge_lib < liteloader_lib, "{libraries:?}");
|
||||
}
|
||||
|
||||
/// **新式 args 的父链**:父版本用 `arguments.game`(没有 `minecraftArguments`)。
|
||||
///
|
||||
/// 现实里 LiteLoader 只到 1.12.2,碰不到这种父版本;但「追加语义」是这套路线的准入条件,
|
||||
/// 两种父版本形状都得钉住——否则哪天上游把老版本清单改成新式,我们会静默丢参数。
|
||||
/// 这里用一份**手工夹具**(`--launchTarget forgeclient` 是 Forge 1.13+ 的真实形态)说明:
|
||||
/// 父层参数在前、LiteLoader 的 tweakClass 在后,中间不互相覆盖。
|
||||
#[test]
|
||||
fn new_style_parent_args_are_appended_not_replaced() {
|
||||
const MODERN_PARENT: &str = r#"{
|
||||
"id": "1.12.2-modern-1.0",
|
||||
"inheritsFrom": "1.12.2",
|
||||
"mainClass": "net.minecraft.launchwrapper.Launch",
|
||||
"arguments": {"game": ["--launchTarget", "forgeclient"], "jvm": ["-Dmodern=1"]},
|
||||
"libraries": [{"name": "example:modern:1.0", "url": "https://example.invalid/"}]
|
||||
}"#;
|
||||
|
||||
let temp = TempDir::new("modern-args");
|
||||
let layout = GameLayout::new(temp.path());
|
||||
put_version(&layout, "1.12.2", VANILLA_1122);
|
||||
put_version(&layout, "1.12.2-modern-1.0", MODERN_PARENT);
|
||||
|
||||
let release = liteloader_release("1.12.2-modern-1.0");
|
||||
let plan = LoaderInstallPlan::build(LoaderPlanInput {
|
||||
layout: &layout,
|
||||
release: &release,
|
||||
environment: &Environment::host(),
|
||||
parent_installed: true,
|
||||
})
|
||||
.expect("计划应可组");
|
||||
let engine = engine(temp.path());
|
||||
prefill(&plan.install);
|
||||
let report = block_on(execute(&engine, &plan.install, 4, None)).expect("执行应成功");
|
||||
assert!(report.is_complete(), "{:?}", report.failures(3));
|
||||
|
||||
let mut source = std::collections::BTreeMap::new();
|
||||
source.insert("1.12.2".to_string(), read_version(&layout, "1.12.2"));
|
||||
source.insert(
|
||||
"1.12.2-modern-1.0".to_string(),
|
||||
read_version(&layout, "1.12.2-modern-1.0"),
|
||||
);
|
||||
source.insert(
|
||||
release.version_id.clone(),
|
||||
read_version(&layout, &release.version_id),
|
||||
);
|
||||
let resolved = resolve(&source, &source[&release.version_id]).expect("三层继承应可解析");
|
||||
|
||||
// 子层没写 legacy 串:继承结果里的 `minecraftArguments` 只能是祖父层(原版)那一份,
|
||||
// 里面绝不能出现 LiteLoader 的 tweakClass(写了就说明我们用了覆盖语义)。
|
||||
let legacy = resolved
|
||||
.minecraft_arguments
|
||||
.as_deref()
|
||||
.expect("原版那份 legacy 串应沿链继承下来");
|
||||
assert!(
|
||||
!legacy.contains("LiteLoaderTweaker"),
|
||||
"loader 层不许往 minecraftArguments 里写:{legacy}"
|
||||
);
|
||||
let game = resolved
|
||||
.arguments
|
||||
.as_ref()
|
||||
.expect("arguments")
|
||||
.expand(&Environment::host())
|
||||
.game;
|
||||
let assert_order = |needle: &str| {
|
||||
game.iter()
|
||||
.position(|token| token == needle)
|
||||
.unwrap_or_else(|| panic!("缺 `{needle}`:{game:?}"))
|
||||
};
|
||||
assert!(
|
||||
assert_order("forgeclient")
|
||||
< assert_order("com.mumfrey.liteloader.launch.LiteLoaderTweaker"),
|
||||
"父层参数必须在前:{game:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// LiteLoader 生效所依赖的**两条继承合并律**(M222 第一步核实,直接钉在合并函数上)。
|
||||
///
|
||||
/// 1. `arguments.game`:父在前、子在后**追加**(所以 tweakClass 能叠在父链参数之后);
|
||||
/// 2. `minecraftArguments`:**子覆盖父**(所以 loader 层绝不能写它,否则父链的 tweakClass
|
||||
/// 整串消失)。
|
||||
///
|
||||
/// 抓得住:有人把 `minecraftArguments` 改成追加(HMCL/原版语义不是这样),或把
|
||||
/// `arguments.game` 改成整体替换(LiteLoader/其它叠层加载器当场失效)。
|
||||
/// 抓不住:真实启动 argv 的顺序(那是 `launch::game_args` 的第 24/25 项,
|
||||
/// 由 `tests/launch/cmdline_snapshot.rs` 钉住;本 mission 没有改那一层)。
|
||||
#[test]
|
||||
fn merge_laws_liteloader_depends_on() {
|
||||
use unlucky_core::game::inherit::merge;
|
||||
use unlucky_core::game::manifest::VersionJson;
|
||||
|
||||
let parse = |json: &str| VersionJson::from_json_str(json).expect("夹具应可解析");
|
||||
|
||||
let parent = parse(
|
||||
r#"{"id":"p","minecraftArguments":"--tweakClass FMLTweaker","arguments":{"game":["--parent"]}}"#,
|
||||
);
|
||||
let child = parse(r#"{"id":"c","arguments":{"game":["--child"]}}"#);
|
||||
let merged = merge(&child, &parent);
|
||||
assert_eq!(
|
||||
merged.minecraft_arguments.as_deref(),
|
||||
Some("--tweakClass FMLTweaker"),
|
||||
"子没写就继承父(这条是「叠在 Forge 上」的前提)"
|
||||
);
|
||||
let game: Vec<String> = merged
|
||||
.arguments
|
||||
.as_ref()
|
||||
.expect("arguments")
|
||||
.expand(&Environment::host())
|
||||
.game;
|
||||
assert_eq!(
|
||||
game,
|
||||
["--parent", "--child"],
|
||||
"arguments.game 是父在前、子在后追加"
|
||||
);
|
||||
|
||||
// 子写了 minecraftArguments:整串替换(不是追加)。
|
||||
let loud_child = parse(r#"{"id":"c2","minecraftArguments":"--tweakClass LiteLoaderTweaker"}"#);
|
||||
let merged = merge(&loud_child, &parent);
|
||||
assert_eq!(
|
||||
merged.minecraft_arguments.as_deref(),
|
||||
Some("--tweakClass LiteLoaderTweaker"),
|
||||
"子覆盖父:这正是 loader 层不许写 minecraftArguments 的原因"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
//! LiteLoader 安装链路的集成测试(`tests/liteloader*` 这个 test target 的入口)。
|
||||
//!
|
||||
//! 判据链:**真实元数据夹具 → 解析 → 合成清单 → 计划 → 落盘 → 继承解析 → 启动参数顺序**。
|
||||
//! 全程不联网:LiteLoader 的库没有 sha1/size,`execute` 的跳过判据是「存在即信」,
|
||||
//! 所以先把每个待下文件按计划里的落点写一份占位字节,执行期就只做「跳过 + 写版本 JSON」。
|
||||
//!
|
||||
//! 夹具来源(可追溯):
|
||||
//! - `fixtures/liteloader-versions.json` = 2026-11 从 `https://dl.liteloader.com/versions/versions.json`
|
||||
//! 真取的响应(200 / 49129 字节 / 16 个原版版本)裁剪到两个原版版本,**字段名与嵌套结构原样**;
|
||||
//! - `metadata.xml` = 同一次探测里 `repo.mumfrey.com/…/liteloader/1.12.2-SNAPSHOT/maven-metadata.xml`
|
||||
//! 的原样响应(200 / 358 字节);
|
||||
//! - 原版 / Forge 1.12.2 的版本 JSON 是按官方清单形状写的最小夹具(Forge 那侧的
|
||||
//! `minecraftArguments` 与 `crates/core/src/addon/loader/tests/forge.rs` 的实测夹具一致)。
|
||||
//!
|
||||
//! 未覆盖(如实留档):真实下载(走 `core::download` 引擎的镜像/退避,已有 M13 的单测覆盖)、
|
||||
//! 真实 MC 启动、`dl.liteloader.com` / `repo.mumfrey.com` 的持续可用性。
|
||||
|
||||
mod guards;
|
||||
mod install;
|
||||
mod support;
|
||||
@@ -0,0 +1,157 @@
|
||||
//! 集成测试公用件:临时游戏目录、离线下载引擎、元数据夹具、父版本清单夹具。
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicUsize, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use unlucky_core::addon::loader::liteloader::{self, LiteLoaderEntry};
|
||||
use unlucky_core::addon::loader::LoaderRelease;
|
||||
use unlucky_core::download::backoff::BackoffPolicy;
|
||||
use unlucky_core::download::cache::ContentCache;
|
||||
use unlucky_core::download::chunked::ChunkedConfig;
|
||||
use unlucky_core::download::config::EngineConfig;
|
||||
use unlucky_core::download::engine::DownloadEngine;
|
||||
use unlucky_core::download::health::HealthRegistry;
|
||||
use unlucky_core::download::mirrors::MirrorTable;
|
||||
use unlucky_core::download::region::RegionProfile;
|
||||
use unlucky_core::game::layout::GameLayout;
|
||||
use unlucky_core::game::manifest::VersionJson;
|
||||
|
||||
/// 真实响应的裁剪版(见 `tests/liteloader.rs` 文件头的来源说明)。
|
||||
pub const VERSION_TABLE: &str = include_str!("fixtures/liteloader-versions.json");
|
||||
|
||||
/// 1.12.2-SNAPSHOT 的 maven-metadata(真实响应原样)。
|
||||
pub const METADATA_XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<metadata>
|
||||
<groupId>com.mumfrey</groupId>
|
||||
<artifactId>liteloader</artifactId>
|
||||
<version>1.12.2-SNAPSHOT</version>
|
||||
<versioning>
|
||||
<snapshot>
|
||||
<timestamp>20171128.144431</timestamp>
|
||||
<buildNumber>4</buildNumber>
|
||||
</snapshot>
|
||||
<lastUpdated>20171128144431</lastUpdated>
|
||||
</versioning>
|
||||
</metadata>"#;
|
||||
|
||||
/// 原版 1.12.2(老式 `minecraftArguments`,与官方清单同形)。
|
||||
pub const VANILLA_1122: &str = r#"{
|
||||
"id": "1.12.2",
|
||||
"type": "release",
|
||||
"mainClass": "net.minecraft.client.main.Main",
|
||||
"assets": "1.12",
|
||||
"assetIndex": {"id": "1.12"},
|
||||
"minecraftArguments": "--username ${auth_player_name} --version ${version_name}",
|
||||
"libraries": [
|
||||
{"name": "com.mojang:patchy:1.1", "url": "https://libraries.minecraft.net/"}
|
||||
]
|
||||
}"#;
|
||||
|
||||
/// Forge 1.12.2-14.23.5.2860(spec-0 的 version.json:主类 launchwrapper + FMLTweaker)。
|
||||
pub const FORGE_1122: &str = r#"{
|
||||
"id": "1.12.2-forge-14.23.5.2860",
|
||||
"inheritsFrom": "1.12.2",
|
||||
"mainClass": "net.minecraft.launchwrapper.Launch",
|
||||
"minecraftArguments": "--tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker",
|
||||
"libraries": [
|
||||
{"name": "net.minecraftforge:forge:1.12.2-14.23.5.2860", "url": "https://maven.minecraftforge.net/"}
|
||||
]
|
||||
}"#;
|
||||
|
||||
/// 唯一临时目录守卫(`drop` 时递归删除)。
|
||||
pub struct TempDir {
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl TempDir {
|
||||
pub fn new(tag: &str) -> Self {
|
||||
static COUNTER: AtomicUsize = AtomicUsize::new(0);
|
||||
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let stamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.map_or(0, |since| since.as_nanos());
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ull-liteloader-{tag}-{}-{stamp}-{unique}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = std::fs::remove_dir_all(&path);
|
||||
std::fs::create_dir_all(&path).expect("临时目录应可创建");
|
||||
Self { path }
|
||||
}
|
||||
|
||||
pub fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
/// 离线引擎:单次尝试、短超时、分块关闭(测试里只用来跑「全部跳过」的执行段)。
|
||||
pub fn engine(cache_root: &Path) -> DownloadEngine {
|
||||
let config = EngineConfig {
|
||||
max_attempts_per_source: 1,
|
||||
backoff: BackoffPolicy {
|
||||
base_ms: 1,
|
||||
max_ms: 2,
|
||||
jitter_ratio: 0.0,
|
||||
max_attempts: 1,
|
||||
},
|
||||
connect_timeout: Duration::from_secs(3),
|
||||
read_timeout: Duration::from_secs(3),
|
||||
chunked: ChunkedConfig::disabled(),
|
||||
..EngineConfig::default()
|
||||
};
|
||||
DownloadEngine::new(
|
||||
config,
|
||||
Arc::new(MirrorTable::builtin()),
|
||||
Arc::new(RegionProfile::direct(0)),
|
||||
Arc::new(HealthRegistry::in_memory()),
|
||||
Arc::new(ContentCache::open(cache_root.join("cache"))),
|
||||
)
|
||||
}
|
||||
|
||||
/// 解析夹具表里某个原版版本的条目。
|
||||
pub fn entries(mc_version: &str) -> Vec<LiteLoaderEntry> {
|
||||
let table = liteloader::parse_versions(VERSION_TABLE).expect("夹具版本表应可解析");
|
||||
table.get(mc_version).cloned().unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 取 1.12.2 的 SNAPSHOT 条目并合成 release(父版本由调用方给)。
|
||||
pub fn liteloader_release(parent_version_id: &str) -> LoaderRelease {
|
||||
let entries = entries("1.12.2");
|
||||
let entry = liteloader::default_entry(&entries).expect("1.12.2 应有一条版本");
|
||||
let url = liteloader::snapshot_artifact_url(entry, METADATA_XML).expect("快照直链");
|
||||
liteloader::build_release("1.12.2", parent_version_id, entry, &url).expect("合成清单")
|
||||
}
|
||||
|
||||
/// 把一份版本 JSON 写进 `versions/<id>/<id>.json`(模拟「父版本已装好」)。
|
||||
pub fn put_version(layout: &GameLayout, id: &str, json: &str) {
|
||||
let path = layout.version_json(id);
|
||||
std::fs::create_dir_all(path.parent().expect("版本目录")).expect("建版本目录");
|
||||
std::fs::write(&path, json).expect("写版本 JSON");
|
||||
}
|
||||
|
||||
/// 读盘上的版本 JSON。
|
||||
pub fn read_version(layout: &GameLayout, id: &str) -> VersionJson {
|
||||
let path = layout.version_json(id);
|
||||
let text = std::fs::read_to_string(&path)
|
||||
.unwrap_or_else(|error| panic!("读不到 {}:{error}", path.display()));
|
||||
VersionJson::from_json_str(&text).expect("版本 JSON 应可解析")
|
||||
}
|
||||
|
||||
/// 按计划把每个待下文件写成占位字节(`execute` 的跳过判据 = 无 sha1 且存在即信)。
|
||||
pub fn prefill(plan: &unlucky_core::game::install::InstallPlan) -> usize {
|
||||
for file in &plan.files {
|
||||
std::fs::create_dir_all(file.dest.parent().expect("文件父目录")).expect("建库目录");
|
||||
std::fs::write(&file.dest, b"placeholder").expect("写占位文件");
|
||||
}
|
||||
plan.files.len()
|
||||
}
|
||||
Reference in new issue
Block a user