Files
Unlucky-Launcher/crates/core/tests/launch/cmdline_snapshot.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

339 lines
13 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 命令行组装快照测试(`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");
}