`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。
184 lines
7.4 KiB
Rust
184 lines
7.4 KiB
Rust
//! 测试共用设施。
|
||
//!
|
||
//! **crate 名只在本文件出现**(tower 定案 `unlucky_core`;若 M11 改包名,只改这里)。
|
||
|
||
use std::fs::File;
|
||
use std::io::Write;
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use zip::write::SimpleFileOptions;
|
||
use zip::{CompressionMethod, ZipWriter};
|
||
|
||
pub use unlucky_core::launch::args_table::{assemble, AssembledCommand, AssemblyWarning};
|
||
pub use unlucky_core::launch::argument::{ArgumentEntry, Rule};
|
||
pub use unlucky_core::launch::command_builder::{self, CommandBuilder};
|
||
pub use unlucky_core::launch::error::{AssemblyError, LaunchError, MissingComponent};
|
||
pub use unlucky_core::launch::exit_analysis::{self, ExitType};
|
||
pub use unlucky_core::launch::game_version::GameVersion;
|
||
pub use unlucky_core::launch::log_level::{self, Level};
|
||
pub use unlucky_core::launch::platform::{OsFamily, PlatformCtx};
|
||
pub use unlucky_core::launch::pump::{LineSink, LogLine, RingBuffer, RingBufferConfig, Stream};
|
||
pub use unlucky_core::launch::redact::{Redactor, TOKEN_PLACEHOLDER};
|
||
pub use unlucky_core::launch::request::*;
|
||
pub use unlucky_core::launch::session::{EndReason, SessionMeta, SessionSink, SessionWriter};
|
||
pub use unlucky_core::launch::session_index;
|
||
pub use unlucky_core::launch::window_detect::WindowDetector;
|
||
|
||
pub use unlucky_core::launch::adapter::{
|
||
build_request, AdapterOptions, AdapterWarning, LibraryInput, SnapshotInput,
|
||
};
|
||
pub use unlucky_core::launch::crash_scan::CrashSource;
|
||
pub use unlucky_core::launch::diagnosis_hook::{DiagnosisHook, DiagnosisRequest};
|
||
pub use unlucky_core::launch::launch_session::SessionHandle;
|
||
pub use unlucky_core::launch::launcher::{DryRun, Launcher, LauncherConfig};
|
||
pub use unlucky_core::launch::process::ProcessConfig;
|
||
pub use unlucky_core::launch::sink::{CompositeSink, CountingSink, FnSink};
|
||
|
||
/// 断言 argv 逐项相等,失败时打印全部实际值。
|
||
pub fn assert_argv(command: &AssembledCommand, expected: &[&str]) {
|
||
let actual = command.argv();
|
||
if actual.len() != expected.len() {
|
||
panic!(
|
||
"argv 长度 {} != 期望 {}\n实际:\n{:#?}",
|
||
actual.len(),
|
||
expected.len(),
|
||
actual
|
||
);
|
||
}
|
||
for (index, (actual, expected)) in actual.iter().zip(expected.iter()).enumerate() {
|
||
assert_eq!(actual, expected, "第 {index} 个参数不同");
|
||
}
|
||
}
|
||
|
||
/// 一个“1.12.2 + Forge”的基准请求(Windows/x86_64)。
|
||
pub fn legacy_forge_request() -> (LaunchRequest, PlatformCtx) {
|
||
let ctx = PlatformCtx::new(OsFamily::Windows, "x86_64");
|
||
let request = LaunchRequest {
|
||
instance: InstanceInfo {
|
||
id: "inst-1".to_string(),
|
||
name: "1.12.2-Forge".to_string(),
|
||
version: GameVersion::parse("1.12.2"),
|
||
version_type: "release".to_string(),
|
||
},
|
||
launcher: LauncherInfo {
|
||
name: "UnluckyLauncher".to_string(),
|
||
version: "0.1.0".to_string(),
|
||
},
|
||
java: JavaSelection::new("C:/java/bin/java.exe", 17, 64),
|
||
memory: MemoryOptions {
|
||
min_mb: Some(1024),
|
||
max_mb: Some(4096),
|
||
},
|
||
manifest: ManifestArgs {
|
||
main_class: Some("net.minecraft.launchwrapper.Launch".to_string()),
|
||
..ManifestArgs::default()
|
||
},
|
||
dirs: LaunchDirs {
|
||
game_dir: PathBuf::from("C:/mc"),
|
||
assets_root: PathBuf::from("C:/mc/assets"),
|
||
game_assets: PathBuf::from("C:/mc/assets/virtual/legacy"),
|
||
assets_index: "1.12".to_string(),
|
||
natives_dir: PathBuf::from("C:/mc/versions/1.12.2/natives-windows-x86_64"),
|
||
library_dir: PathBuf::from("C:/mc/libraries"),
|
||
primary_jar: PathBuf::from("C:/mc/versions/1.12.2/1.12.2.jar"),
|
||
},
|
||
classpath: vec![
|
||
PathBuf::from("C:/mc/libraries/a.jar"),
|
||
PathBuf::from("C:/mc/libraries/b.jar"),
|
||
],
|
||
auth: AuthPlaceholders {
|
||
player_name: "Steve".to_string(),
|
||
uuid: "0123456789abcdef0123456789abcdef".to_string(),
|
||
access_token: "SECRET-TOKEN-123456".to_string(),
|
||
user_type: "msa".to_string(),
|
||
user_properties: "{}".to_string(),
|
||
},
|
||
log4j: Log4jOptions {
|
||
enabled: true,
|
||
config_file: Some(PathBuf::from("C:/mc/log4j2.xml")),
|
||
debug: false,
|
||
},
|
||
// 大部分断言只关心“命令骨架”,优化参数会淹没差异;需要时单独打开。
|
||
optimization: OptimizationOptions {
|
||
enabled: false,
|
||
physical_memory_mb: None,
|
||
},
|
||
language: "zh-CN".to_string(),
|
||
features: FeatureSet {
|
||
has_custom_resolution: true,
|
||
..FeatureSet::default()
|
||
},
|
||
resolution: Some(Resolution {
|
||
width: 854,
|
||
height: 480,
|
||
}),
|
||
..LaunchRequest::default()
|
||
};
|
||
(request, ctx)
|
||
}
|
||
|
||
/// 一个唯一的临时目录(不自动清理,便于失败时排查;OS 会回收)。
|
||
pub fn temp_dir(name: &str) -> PathBuf {
|
||
let nanos = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.map(|value| value.as_nanos())
|
||
.unwrap_or_default();
|
||
let dir =
|
||
std::env::temp_dir().join(format!("ull-launch-{name}-{}-{nanos}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(&dir).expect("创建临时目录");
|
||
dir
|
||
}
|
||
|
||
/// 一个唯一的临时会话根目录(测试结束不自动清理,便于失败时排查;OS 会回收)。
|
||
pub fn temp_sessions_root(name: &str) -> PathBuf {
|
||
let nanos = std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.map(|value| value.as_nanos())
|
||
.unwrap_or_default();
|
||
let root = std::env::temp_dir().join(format!(
|
||
"ull-launch-test-{name}-{}-{nanos}",
|
||
std::process::id()
|
||
));
|
||
let _ = std::fs::remove_dir_all(&root);
|
||
root
|
||
}
|
||
|
||
/// 读回 jsonl 的每一行并解析成 JSON。
|
||
pub fn read_events(path: &std::path::Path) -> Vec<serde_json::Value> {
|
||
let text = std::fs::read_to_string(path).expect("读取会话文件");
|
||
text.lines()
|
||
.filter(|line| !line.trim().is_empty())
|
||
.map(|line| serde_json::from_str(line).expect("解析事件行"))
|
||
.collect()
|
||
}
|
||
|
||
/// 造一个假 jar(`zip` Stored 模式,不依赖真下载):`entries` 是(条目名, 内容)。
|
||
///
|
||
/// natives 用例共用(真解压与 `--dry-run` 预览两边都要现造 jar)。
|
||
pub fn make_jar(path: &Path, entries: &[(&str, &[u8])]) {
|
||
if let Some(parent) = path.parent() {
|
||
std::fs::create_dir_all(parent).expect("建 jar 目录");
|
||
}
|
||
let file = File::create(path).expect("建 jar");
|
||
let mut writer = ZipWriter::new(file);
|
||
let options = SimpleFileOptions::default().compression_method(CompressionMethod::Stored);
|
||
for (name, bytes) in entries {
|
||
writer.start_file(*name, options).expect("写条目");
|
||
writer.write_all(bytes).expect("写条目内容");
|
||
}
|
||
writer.finish().expect("收尾 jar");
|
||
}
|
||
|
||
/// 一份 1.16.5 风格的 natives jar 的条目集(2 个原生库 + 4 条该被排除的噪音)。
|
||
pub fn lwjgl_jar_entries() -> Vec<(&'static str, &'static [u8])> {
|
||
vec![
|
||
("lwjgl.dll", b"DLL-BYTES".as_slice()),
|
||
("OpenAL.dll", b"OPENAL-BYTES".as_slice()),
|
||
("META-INF/MANIFEST.MF", b"manifest".as_slice()),
|
||
("META-INF/LWJGL.SF", b"signature".as_slice()),
|
||
("lwjgl.dll.sha1", b"deadbeef".as_slice()),
|
||
("README.txt", b"noise".as_slice()),
|
||
]
|
||
}
|