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 开发中。
339 lines
13 KiB
Rust
339 lines
13 KiB
Rust
//! 命令行组装快照测试(`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");
|
||
}
|