Files
Unlucky-Launcher/crates/core/tests/launch/cmdline_snapshot.rs
T
AskaEth a5d1dec495
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
release: v0.2.4 里程碑快照(孤儿提交,无父;树 = dev 68362e5)
本批内容见 CHANGELOG.md 的 0.2.4 段。按 D41 用孤儿快照推 main,保留「公开史只有里程碑」的模型。
2026-11-11 21:41:38 +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");
}