Unlucky Launcher v0.2.2
CI / runtime smoke (macos arm64, probe) (push) Skipped
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
CI / runtime smoke (ubuntu x64, xvfb) (push) Canceled after 0s
Guards / core 零 UI 守门 (push) Canceled after 0s
Guards / presenter 零 UI 守门 (push) Canceled after 0s
Guards / design 零第三方依赖守门 (push) Canceled after 0s
Guards / unsafe 落点守门(集合必须恰好一个文件) (push) Canceled after 0s
Guards / 文件行数 ≤400 守门 (push) Canceled after 0s
Guards / 依赖许可与来源(cargo-deny) (push) Canceled after 0s
Release / 选槽位(默认两个 Windows) (push) Canceled after 0s
Release / build (${{ matrix.slot }}) (push) Canceled after 0s
Release / GitHub Release (push) Canceled after 0s

This commit is contained in:
AskaEth committed 2026-11-07 08:46:11 +08:00
commit 1105a86448
1128 files changed
+217164

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");
}