`ull launch --dry-run` 在「环境准备」段走的是真解压:即使只打印命令行,也会在 `versions/<id>/natives/` 下建目录、解 dll、写戳记,把别人的版本树改脏(父目录 mtime 也变)。失败路径(缺 jar 报 missing_components)照样留下这些副作用。 - natives 新增 `preview()`:与 `extract()` 共用条目筛选与戳记口径,但零落盘 (无 create_dir_all / clear_natives / write_stamp),只清点「将写下几个原生库」; jar 打不开照样报错,预览不藏坏包。 - `NativesReport` 增 `planned` / `dir_exists`:预览报告自报家门,人读行与 JSON 说清「未落盘」「目录尚不存在」。 - `Launcher::dry_run` 改走 `preview_environment`,`spawn` 仍走 `prepare_environment`。 - 报告类型拆到 `natives/report.rs`(mod.rs 曾破 400 行红线)。 - 回归测试(natives_dry_run_cases.rs):有 jar / 无 jar / 空 jar / 已解压过四条 零落盘判据,用「节点类型 + 纳秒 mtime」快照逐字比较版本树,并带一条判据自检 (瞬时建删目录必须被快照看见,防假绿)。 - help:`--dry-run` 的承诺补成「不写盘、不解压 natives、不建目录」。 证据(外部版本树 versions/1.16.5 + 真 natives jar): 修复前 dry-run 后 versions/1.16.5 mtime 1794342830.6861426 → 1794342832.4544542, 新增 natives/ + lwjgl.dll + OpenAL.dll + .ull-natives.json;修复后同一场景快照逐字 不变(mtime 1794343730.6618167),JSON natives.planned=true / dir_exists=false。
179 lines
7.0 KiB
Rust
179 lines
7.0 KiB
Rust
//! natives 解压(`launch::natives`)的用例:解析正确性 / 排除项 / 幂等 / 时代参数 / 事件流。
|
||
//!
|
||
//! 假 jar 用 `zip` 的 Stored 模式现造(不依赖真下载),真机验收另有人工步骤。
|
||
|
||
use std::path::Path;
|
||
|
||
use unlucky_core::launch::natives::{self, NativeJar, NativesEra, NativesPlan, STAMP_FILE};
|
||
|
||
use crate::prelude::*;
|
||
|
||
fn plan(jars: Vec<NativeJar>) -> NativesPlan {
|
||
NativesPlan::new(jars)
|
||
}
|
||
|
||
#[test]
|
||
fn extracts_native_libraries_and_drops_noise() {
|
||
let root = temp_dir("natives-extract");
|
||
let jar = root.join("libraries/lwjgl-natives-windows.jar");
|
||
make_jar(&jar, &lwjgl_jar_entries());
|
||
let target = root.join("versions/1.16.5/natives");
|
||
|
||
let report =
|
||
natives::extract(&target, &plan(vec![NativeJar::new(&jar, "lwjgl")])).expect("解压应成功");
|
||
|
||
assert_eq!(report.files, 2, "只该解出两个原生库:{report:?}");
|
||
assert!(!report.up_to_date);
|
||
assert_eq!(report.jars, 1);
|
||
// META-INF/ 两条 + .sha1 + README.txt。
|
||
assert_eq!(report.ignored, 4, "噪音条目数:{report:?}");
|
||
assert_eq!(
|
||
std::fs::read(target.join("lwjgl.dll")).expect("读 dll"),
|
||
b"DLL-BYTES"
|
||
);
|
||
assert!(target.join("OpenAL.dll").is_file());
|
||
assert!(!target.join("META-INF").exists(), "META-INF/ 不该落地");
|
||
assert!(!target.join("lwjgl.dll.sha1").exists(), "签名文件不该落地");
|
||
assert!(!target.join("README.txt").exists(), "非原生库不该落地");
|
||
assert!(target.join(STAMP_FILE).is_file(), "应写戳记");
|
||
}
|
||
|
||
#[test]
|
||
fn second_run_is_idempotent_and_source_change_reextracts() {
|
||
let root = temp_dir("natives-idempotent");
|
||
let jar = root.join("natives-windows.jar");
|
||
make_jar(&jar, &[("lwjgl.dll", b"V1".as_slice())]);
|
||
let target = root.join("natives");
|
||
let extract_plan = plan(vec![NativeJar::new(&jar, "lwjgl")]);
|
||
|
||
let first = natives::extract(&target, &extract_plan).expect("首次解压");
|
||
assert_eq!(first.files, 1);
|
||
assert!(!first.up_to_date);
|
||
|
||
// 第二次:戳记命中,零写盘(文件 mtime 不变)。
|
||
let before = std::fs::metadata(target.join("lwjgl.dll"))
|
||
.expect("stat")
|
||
.modified()
|
||
.expect("mtime");
|
||
let second = natives::extract(&target, &extract_plan).expect("二次解压");
|
||
assert!(second.up_to_date, "应判定目录已就绪:{second:?}");
|
||
assert_eq!(second.files, 0);
|
||
assert_eq!(second.skipped, 1);
|
||
let after = std::fs::metadata(target.join("lwjgl.dll"))
|
||
.expect("stat")
|
||
.modified()
|
||
.expect("mtime");
|
||
assert_eq!(before, after, "幂等路径不该重写文件");
|
||
|
||
// 源 jar 变了(内容 + 长度都变):必须重解,且旧的原生库被清掉。
|
||
make_jar(&jar, &[("lwjgl.dll", b"V2-LONGER".as_slice())]);
|
||
let third = natives::extract(&target, &extract_plan).expect("三次解压");
|
||
assert!(!third.up_to_date, "源变了必须重解:{third:?}");
|
||
assert_eq!(
|
||
std::fs::read(target.join("lwjgl.dll")).expect("读 dll"),
|
||
b"V2-LONGER"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn stale_native_libraries_are_cleaned_before_reextract() {
|
||
let root = temp_dir("natives-clean");
|
||
let jar = root.join("natives-windows.jar");
|
||
make_jar(&jar, &[("lwjgl.dll", b"NEW".as_slice())]);
|
||
let target = root.join("natives");
|
||
std::fs::create_dir_all(&target).expect("建目录");
|
||
// 上一代遗留(旧版本 / 旧 loader 解出来的),必须清掉,否则两代 dll 混在 java.library.path 里。
|
||
std::fs::write(target.join("old-lwjgl.dll"), b"stale").expect("写旧 dll");
|
||
std::fs::write(target.join("keep-me.txt"), b"user file").expect("写用户文件");
|
||
|
||
natives::extract(&target, &plan(vec![NativeJar::new(&jar, "lwjgl")])).expect("解压");
|
||
|
||
assert!(!target.join("old-lwjgl.dll").exists(), "旧原生库应被清理");
|
||
assert!(target.join("keep-me.txt").is_file(), "非原生库文件不该被删");
|
||
assert!(target.join("lwjgl.dll").is_file());
|
||
}
|
||
|
||
#[test]
|
||
fn empty_plan_still_creates_directory_for_1_19() {
|
||
// 1.19+ 没有 natives 分类器:目录仍要存在(SharedLibraryExtractPath 指向它)。
|
||
let root = temp_dir("natives-empty");
|
||
let target = root.join("versions/1.21.4/natives");
|
||
let report = natives::extract(&target, &NativesPlan::default()).expect("建目录");
|
||
assert!(target.is_dir());
|
||
assert!(report.up_to_date);
|
||
assert_eq!(report.files, 0);
|
||
assert_eq!(report.jars, 0);
|
||
assert_eq!(
|
||
report.era,
|
||
NativesEra::ClasspathNative,
|
||
"默认时代(无 jar)"
|
||
);
|
||
}
|
||
|
||
#[test]
|
||
fn missing_directory_is_an_error_not_a_silent_pass() {
|
||
let plan = NativesPlan::new(vec![NativeJar::new("x.jar", "x")]);
|
||
let err = natives::extract(Path::new(""), &plan).expect_err("空目录必须报错");
|
||
assert!(err.message().contains("natives 目录"), "{err}");
|
||
}
|
||
|
||
#[test]
|
||
fn broken_jar_reports_zip_error() {
|
||
let root = temp_dir("natives-broken");
|
||
let jar = root.join("natives-broken.jar");
|
||
std::fs::write(&jar, b"not a zip at all").expect("写坏 jar");
|
||
let target = root.join("natives");
|
||
let err = natives::extract(&target, &plan(vec![NativeJar::new(&jar, "broken")]))
|
||
.expect_err("坏 jar 必须报错");
|
||
assert!(err.message().contains("zip"), "错误应点明 zip:{err}");
|
||
}
|
||
|
||
#[test]
|
||
fn era_detect_covers_three_generations() {
|
||
assert_eq!(NativesEra::detect(0, 3), NativesEra::Legacy);
|
||
assert_eq!(NativesEra::detect(9, 3), NativesEra::Lwjgl3);
|
||
assert_eq!(NativesEra::detect(9, 0), NativesEra::ClasspathNative);
|
||
assert_eq!(NativesEra::Legacy.as_str(), "legacy");
|
||
assert_eq!(NativesEra::Lwjgl3.as_str(), "lwjgl3");
|
||
assert_eq!(NativesEra::ClasspathNative.as_str(), "classpath_native");
|
||
}
|
||
|
||
/// 事件流:`natives_extracted` 落盘字段与接口约定一致(dir/files/skipped/custom)。
|
||
#[test]
|
||
fn natives_extracted_event_carries_report_fields() {
|
||
let root = temp_dir("natives-event");
|
||
let jar = root.join("natives-windows.jar");
|
||
make_jar(&jar, &[("lwjgl.dll", b"DLL".as_slice())]);
|
||
let target = root.join("natives");
|
||
let report =
|
||
natives::extract(&target, &plan(vec![NativeJar::new(&jar, "lwjgl")])).expect("解压");
|
||
|
||
let sessions = temp_sessions_root("natives-event-sessions");
|
||
let mut writer = SessionWriter::create(
|
||
&sessions,
|
||
SessionMeta::new("inst", "1.16.5", "1.16.5"),
|
||
Redactor::new(),
|
||
)
|
||
.expect("建会话");
|
||
writer.natives_extracted(&report).expect("写事件");
|
||
let path = writer.path().to_path_buf();
|
||
writer.finish(EndReason::Exit).expect("收尾");
|
||
|
||
let events = read_events(&path);
|
||
let event = events
|
||
.iter()
|
||
.find(|event| event["kind"] == "natives_extracted")
|
||
.expect("应有 natives_extracted 事件");
|
||
let payload = &event["payload"];
|
||
assert_eq!(payload["files"], 1);
|
||
assert_eq!(payload["skipped"], 0);
|
||
assert_eq!(payload["custom"], false);
|
||
assert_eq!(payload["jars"], 1);
|
||
assert_eq!(payload["up_to_date"], false);
|
||
assert_eq!(payload["era"], "lwjgl3");
|
||
assert!(payload["dir"]
|
||
.as_str()
|
||
.expect("dir 是字符串")
|
||
.ends_with("natives"));
|
||
}
|