Unlucky Launcher v0.1.0
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
单提交快照:main 只保留已发布版本的代码树,开发史在 dev。
This commit is contained in:
commit
5f8382565e
570 files changed
+104468
No files matched your search
@@ -0,0 +1,338 @@
|
||||
//! 命令行组装快照测试(`analyse/02` §4.1 的 33 项顺序表 + §4.2 默认值抑制)。
|
||||
//!
|
||||
//! 期望值是手工核对过的**完整 argv**:一旦组装顺序变了,这里必须一起改——
|
||||
//! 顺序本身有语义(后加的默认值会被先加的显式值抑制)。
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn legacy_forge_full_argv() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.mod_loader = ModLoaderKind::Forge;
|
||||
request.manifest.legacy_arguments = Some(
|
||||
"--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} \
|
||||
--assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} \
|
||||
--accessToken ${auth_session} --userType ${user_type} \
|
||||
--tweakClass net.minecraftforge.fml.common.launcher.FMLTweaker"
|
||||
.to_string(),
|
||||
);
|
||||
// 用户指定 GBK → 日志泵也必须用 GBK 解码(`analyse/02` §4.4)。
|
||||
request.user.jvm = vec!["-Dfile.encoding=GBK".to_string()];
|
||||
|
||||
let command = assemble(&request, &ctx).expect("组装应成功");
|
||||
assert_eq!(command.encoding, "GBK");
|
||||
assert_eq!(command.main_class, "net.minecraft.launchwrapper.Launch");
|
||||
assert_eq!(command.classpath.len(), 3, "主 jar 必须追加在末位");
|
||||
|
||||
assert_argv(
|
||||
&command,
|
||||
&[
|
||||
"C:/java/bin/java.exe",
|
||||
"-Xmx4096m",
|
||||
"-Xms1024m",
|
||||
"-XX:MetaspaceSize=64m",
|
||||
"-Dfile.encoding=GBK",
|
||||
"-Dsun.stdout.encoding=GBK",
|
||||
"-Dsun.stderr.encoding=GBK",
|
||||
"-Djava.rmi.server.useCodebaseOnly=true",
|
||||
"-Dcom.sun.jndi.rmi.object.trustURLCodebase=false",
|
||||
"-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=false",
|
||||
"-Dlog4j2.formatMsgNoLookups=true",
|
||||
"-Dlog4j.configurationFile=C:/mc/log4j2.xml",
|
||||
"-Dminecraft.client.jar=C:/mc/versions/1.12.2/1.12.2.jar",
|
||||
"-Dfml.ignoreInvalidMinecraftCertificates=true",
|
||||
"-Dfml.ignorePatchDiscrepancies=true",
|
||||
"-Djava.library.path=C:/mc/versions/1.12.2/natives-windows-x86_64",
|
||||
"-cp",
|
||||
"C:/mc/libraries/a.jar;C:/mc/libraries/b.jar;C:/mc/versions/1.12.2/1.12.2.jar",
|
||||
"net.minecraft.launchwrapper.Launch",
|
||||
"--username",
|
||||
"Steve",
|
||||
"--version",
|
||||
"1.12.2-Forge",
|
||||
"--gameDir",
|
||||
"C:/mc",
|
||||
"--assetsDir",
|
||||
"C:/mc/assets",
|
||||
"--assetIndex",
|
||||
"1.12",
|
||||
"--uuid",
|
||||
"0123456789abcdef0123456789abcdef",
|
||||
"--accessToken",
|
||||
"SECRET-TOKEN-123456",
|
||||
"--userType",
|
||||
"msa",
|
||||
"--tweakClass",
|
||||
"net.minecraftforge.fml.common.launcher.FMLTweaker",
|
||||
"--width",
|
||||
"854",
|
||||
"--height",
|
||||
"480",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn modern_fabric_full_argv_with_rules() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.instance.name = "1.20.1-Fabric".to_string();
|
||||
request.instance.version = GameVersion::parse("1.20.1");
|
||||
request.manifest.main_class =
|
||||
Some("net.fabricmc.loader.impl.launch.knot.KnotClient".to_string());
|
||||
request.manifest.legacy_arguments = None;
|
||||
request.manifest.jvm = vec![
|
||||
ArgumentEntry::plain(["-Djava.library.path=${natives_directory}"]),
|
||||
ArgumentEntry::plain(["-cp", "${classpath}"]),
|
||||
];
|
||||
request.manifest.game = vec![
|
||||
ArgumentEntry::plain(["--username", "${auth_player_name}"]),
|
||||
ArgumentEntry::plain(["--version", "${version_name}"]),
|
||||
ArgumentEntry::ruled(
|
||||
vec![Rule::allow().with_os_name("windows")],
|
||||
["-Dos.windows=true"],
|
||||
),
|
||||
ArgumentEntry::ruled(vec![Rule::allow().with_os_name("osx")], ["-Dos.macos=true"]),
|
||||
ArgumentEntry::ruled(
|
||||
vec![Rule::allow().with_feature("has_custom_resolution", true)],
|
||||
[
|
||||
"--width",
|
||||
"${resolution_width}",
|
||||
"--height",
|
||||
"${resolution_height}",
|
||||
],
|
||||
),
|
||||
// 先 allow 后 disallow:按“最后一个命中的 action”生效(`analyse/02` §4.7)。
|
||||
ArgumentEntry::ruled(
|
||||
vec![
|
||||
Rule::allow().with_os_name("windows"),
|
||||
Rule::disallow().with_feature("is_demo_user", true),
|
||||
],
|
||||
["--windows-only"],
|
||||
),
|
||||
];
|
||||
request.resolution = Some(Resolution {
|
||||
width: 1280,
|
||||
height: 720,
|
||||
});
|
||||
request.user.game = vec!["--fullscreen".to_string()];
|
||||
|
||||
let command = assemble(&request, &ctx).expect("组装应成功");
|
||||
// 主 jar 由组装器追加到最后,manifest 里的 `-cp` 只覆盖库。
|
||||
assert_eq!(command.classpath.len(), 3);
|
||||
|
||||
assert_argv(
|
||||
&command,
|
||||
&[
|
||||
"C:/java/bin/java.exe",
|
||||
"-Xmx4096m",
|
||||
"-Xms1024m",
|
||||
"-XX:MetaspaceSize=64m",
|
||||
"-Dfile.encoding=UTF-8",
|
||||
"-Dsun.stdout.encoding=UTF-8",
|
||||
"-Dsun.stderr.encoding=UTF-8",
|
||||
"-Djava.rmi.server.useCodebaseOnly=true",
|
||||
"-Dcom.sun.jndi.rmi.object.trustURLCodebase=false",
|
||||
"-Dcom.sun.jndi.cosnaming.object.trustURLCodebase=false",
|
||||
"-Dlog4j2.formatMsgNoLookups=true",
|
||||
"-Dlog4j.configurationFile=C:/mc/log4j2.xml",
|
||||
"-Dminecraft.client.jar=C:/mc/versions/1.12.2/1.12.2.jar",
|
||||
"-Djava.library.path=C:/mc/versions/1.12.2/natives-windows-x86_64",
|
||||
"-cp",
|
||||
"C:/mc/libraries/a.jar;C:/mc/libraries/b.jar;C:/mc/versions/1.12.2/1.12.2.jar",
|
||||
"net.fabricmc.loader.impl.launch.knot.KnotClient",
|
||||
"--username",
|
||||
"Steve",
|
||||
"--version",
|
||||
"1.20.1-Fabric",
|
||||
"-Dos.windows=true",
|
||||
"--width",
|
||||
"1280",
|
||||
"--height",
|
||||
"720",
|
||||
"--windows-only",
|
||||
"--fullscreen",
|
||||
],
|
||||
);
|
||||
assert!(!command.args.iter().any(|arg| arg == "-Dos.macos=true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_override_beats_defaults() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.java = JavaSelection::new("C:/java8/bin/java.exe", 8, 32);
|
||||
request.user.override_jvm = vec!["-Xmx8G".to_string(), "-XX:+UseSerialGC".to_string()];
|
||||
request.user.jvm = vec!["-Xmx2G".to_string()];
|
||||
request.optimization = OptimizationOptions {
|
||||
enabled: true,
|
||||
physical_memory_mb: Some(8192),
|
||||
};
|
||||
request.manifest.legacy_arguments = None;
|
||||
|
||||
let command = assemble(&request, &ctx).expect("组装应成功");
|
||||
let args = command.args;
|
||||
// override 的 -Xmx8G 生效,默认的 -Xmx4096m 被抑制。
|
||||
assert!(args.iter().any(|arg| arg == "-Xmx8G"));
|
||||
assert!(!args.iter().any(|arg| arg == "-Xmx4096m"));
|
||||
// 用户 JVM 参数里的 -Xmx2G 属于“默认值”,被 override 抑制。
|
||||
assert!(!args.iter().any(|arg| arg == "-Xmx2G"));
|
||||
// 用户显式挑了 GC → 不注入 G1 整组,但诊断关键项照旧。
|
||||
assert!(args.iter().any(|arg| arg == "-XX:+UseSerialGC"));
|
||||
assert!(!args.iter().any(|arg| arg == "-XX:+UseG1GC"));
|
||||
assert!(args
|
||||
.iter()
|
||||
.any(|arg| arg == "-XX:-OmitStackTraceInFastThrow"));
|
||||
// 32 位老 Java → PermSize,且不因内存充裕注入 CodeCache 组。
|
||||
assert!(args.iter().any(|arg| arg == "-XX:MetaspaceSize=64m"));
|
||||
assert!(!args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("-XX:ReservedCodeCacheSize")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn java8_uses_legacy_encoding_keys_and_keeps_xincgc() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.java = JavaSelection::new("C:/java8/bin/java.exe", 8, 32);
|
||||
request.user.jvm = vec!["-Xincgc".to_string()];
|
||||
request.manifest.legacy_arguments = None;
|
||||
let command = assemble(&request, &ctx).expect("组装应成功");
|
||||
assert!(command
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg == "-Dsun.stdout.encoding=UTF-8"));
|
||||
assert!(!command
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg == "-Dstdout.encoding=UTF-8"));
|
||||
// Java 8 上 -Xincgc 仍合法(只在 ≥9 上被禁,`analyse/02` §4.1 #33)。
|
||||
assert!(command.args.iter().any(|arg| arg == "-Xincgc"));
|
||||
|
||||
let mut modern = request.clone();
|
||||
modern.java = JavaSelection::new("C:/java21/bin/java.exe", 21, 64);
|
||||
let command = assemble(&modern, &ctx).expect("组装应成功");
|
||||
assert!(command
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg == "-Dstdout.encoding=UTF-8"));
|
||||
assert!(
|
||||
!command.args.iter().any(|arg| arg == "-Xincgc"),
|
||||
"Java ≥9 必须删掉 -Xincgc"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quickplay_and_graphics_backend_are_version_gated() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.manifest.legacy_arguments = None;
|
||||
request.quick_play = Some(QuickPlay::Multiplayer {
|
||||
address: "mc.example.com:25565".to_string(),
|
||||
});
|
||||
request.graphics_backend = Some("vulkan".to_string());
|
||||
|
||||
// 1.12.2:QuickPlay 退化成 --server/--port,--graphicsBackend 丢弃。
|
||||
let legacy = assemble(&request, &ctx).expect("组装应成功");
|
||||
let server = legacy
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|pair| pair[0] == "--server" && pair[1] == "mc.example.com");
|
||||
assert!(server, "老版本应退化为 --server");
|
||||
assert!(legacy
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|pair| pair[0] == "--port" && pair[1] == "25565"));
|
||||
assert!(!legacy.args.iter().any(|arg| arg == "--graphicsBackend"));
|
||||
assert!(legacy
|
||||
.warnings
|
||||
.contains(&AssemblyWarning::QuickPlayDegraded {
|
||||
address: "mc.example.com:25565".to_string()
|
||||
}));
|
||||
assert!(legacy
|
||||
.warnings
|
||||
.iter()
|
||||
.any(|warning| matches!(warning, AssemblyWarning::GraphicsBackendSkipped { .. })));
|
||||
|
||||
// 26.3:QuickPlay 直用,--graphicsBackend 允许。
|
||||
let mut modern = request.clone();
|
||||
modern.instance.version = GameVersion::parse("26.3");
|
||||
let modern = assemble(&modern, &ctx).expect("组装应成功");
|
||||
assert!(modern
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|pair| pair[0] == "--quickPlayMultiplayer" && pair[1] == "mc.example.com:25565"));
|
||||
assert!(modern
|
||||
.args
|
||||
.windows(2)
|
||||
.any(|pair| pair[0] == "--graphicsBackend" && pair[1] == "vulkan"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn assembly_fails_fast_on_missing_components() {
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.missing_components = vec![
|
||||
MissingComponent::named("org.lwjgl:lwjgl:3.3.1"),
|
||||
MissingComponent::at_path("net.minecraft:client", "C:/mc/versions/1.12.2/1.12.2.jar"),
|
||||
];
|
||||
let error = assemble(&request, &ctx).expect_err("缺组件必须启动前失败");
|
||||
match error {
|
||||
LaunchError::Assembly(AssemblyError::MissingComponents(items)) => {
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(items[0].name, "org.lwjgl:lwjgl:3.3.1");
|
||||
}
|
||||
other => panic!("错误类型不对:{other}"),
|
||||
}
|
||||
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.manifest.main_class = None;
|
||||
assert!(matches!(
|
||||
assemble(&request, &ctx),
|
||||
Err(LaunchError::Assembly(AssemblyError::MissingMainClass))
|
||||
));
|
||||
|
||||
let (mut request, ctx) = legacy_forge_request();
|
||||
request.classpath.clear();
|
||||
request.dirs.primary_jar = std::path::PathBuf::new();
|
||||
assert!(matches!(
|
||||
assemble(&request, &ctx),
|
||||
Err(LaunchError::Assembly(AssemblyError::EmptyClasspath))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn posix_gets_nice_and_colon_classpath() {
|
||||
let mut ctx = PlatformCtx::new(OsFamily::Linux, "x86_64");
|
||||
ctx.home_dir = Some(std::path::PathBuf::from("/home/aska"));
|
||||
let (mut request, _) = legacy_forge_request();
|
||||
request.nice = Some(5);
|
||||
request.wrapper = vec!["prime-run".to_string()];
|
||||
request.manifest.legacy_arguments = None;
|
||||
|
||||
let command = assemble(&request, &ctx).expect("组装应成功");
|
||||
assert_eq!(command.argv()[0], "nice");
|
||||
assert_eq!(command.argv()[1], "-n");
|
||||
assert_eq!(command.argv()[2], "5");
|
||||
assert_eq!(command.argv()[3], "prime-run");
|
||||
assert_eq!(command.argv()[4], "C:/java/bin/java.exe");
|
||||
assert!(command
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg
|
||||
== "C:/mc/libraries/a.jar:C:/mc/libraries/b.jar:C:/mc/versions/1.12.2/1.12.2.jar"));
|
||||
// 非 Windows 才注入 -Duser.home。
|
||||
assert!(command
|
||||
.args
|
||||
.iter()
|
||||
.any(|arg| arg.starts_with("-Duser.home=")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn placeholder_table_is_complete() {
|
||||
let (request, ctx) = legacy_forge_request();
|
||||
let map = unlucky_core::launch::placeholders::PlaceholderMap::from_request(&request, &ctx);
|
||||
for key in unlucky_core::launch::placeholders::PLACEHOLDER_KEYS {
|
||||
assert!(map.get(key).is_some(), "占位符 ${key} 未登记");
|
||||
}
|
||||
assert_eq!(map.get("primary_jar_name"), Some("1.12.2.jar"));
|
||||
assert_eq!(map.get("version_name"), Some("1.12.2-Forge"));
|
||||
// 未定义的占位符原样保留(便于发现清单漏项)。
|
||||
assert_eq!(map.render("${nope}"), "${nope}");
|
||||
assert_eq!(map.render("a${version_name}b"), "a1.12.2-Forgeb");
|
||||
}
|
||||
Reference in new issue
Block a user