Files
Unlucky-Launcher/crates/core/src/addon/temp.rs
T
worker-modpackpick 4defc41e8d feat(m160): 整合包逐文件覆盖/保留决策 + 导入前冲突清单 + 限额口径统一
主任务(逐文件覆盖/保留)
- core: 新增 import/scan.rs —— 只读预检产出「冲突清单」(目标已存在且 SHA-1 不同才进),
  逐条给路径/来源/两侧体积与哈希;用户删过的、内容相同的、被叠层遮蔽的一律不打扰。
- core: 新增 import/decision.rs —— ConflictDecisions(整批档 + 逐项表,逐项优先),
  缺省档恒为 KeepUser;ImportRequest.conflict_policy 改由它承载,CLI 调用点不变。
- core: phase.rs 的 settle 按 path 取决策;非冲突路径不受决策影响。
- presenter: ImportScanView / ImportConflictView / ConflictDecisionsView(全部保留 /
  全部覆盖 / 逐项{default,items})+ PackCommand::Scan / PackEvent::Scan;host 新增只读
  spawn_scan,且新实例路径不再提前建目录(target_of 只算路径,prepare_target 才建)。
- app-iced: 导入前冲突清单页(分页 40 条/页、三入口、计数摘要、确认/取消、两行式行布局);
  预检干净时直接往下导,不打扰用户。出图见 C:/Temp/m160-shots/{pick-light,pick-dark}.png。
- i18n: 新增 18 条 en/zh 文案 + 更新「两者都留」那条口径说明。

限额口径统一
- pack/archive/limits.rs 成为唯一来源:PACK_MAX_ENTRIES/UNCOMPRESSED_BYTES/ENTRY_BYTES
  (20k / 512 MiB / 64 MiB);读侧 PackLimits::default 与写侧 WriteLimits::production 同源。
- 写侧不再直接用 zip::ZIP64_BYTES_THR;新增单条目闸,于是「能导出的包一定能导入」。
  不等式在 pack/tests/limits.rs 里用 const 断言钉死。

测试临时物泄漏(塔台追加项)
- addon/temp.rs:TempRoot —— Drop 即删(panic 展开同样删),Deref/AsRef 到落点路径,
  under()/at() 支持"子树落点、删父根"。形状照 core::update::tests::support::TempDir。
- addon::pack 与 addon::tests 两族全部改用 TempRoot(含 fixtures::Fixture 持守卫、
  multimc/fabric_instance 等返回守卫);4 GiB 稀疏文件的语义一字未改,只改"谁负责删"。
- 自证:pack/tests/mod.rs 三条用例(drop 删 / panic 也删 / 子树删父根)。

测试
- import_decisions.rs(11 例):预检清单、不打扰、逐项决策后的最终文件集、基线记包内哈希。
- roundtrip_three.rs(2 例):叠层 + 冲突夹具 import→export→import 三轮文件集逐字节一致,
  以及「选保留就按设计分叉」的反向证据。
- pack/tests/limits.rs(5 例):单条/总量边界、读侧用同一批常量、写出即可读回。

证据:cargo fmt --check 干净;cargo clippy --workspace --all-targets -D warnings 干净;
cargo test --workspace --all-targets -j 4 → 2507 passed / 0 failed(51 个 test binary)。
2026-11-07 13:52:43 +08:00

108 lines
4.2 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.
//! 测试用的**一次性临时根**(`Drop` 即删;panic 展开时同样删)。
//!
//! # 为什么它必须在 `addon` 里而不是某个测试目录里
//!
//! `addon::pack` 与 `addon::tests` 两族用例都建临时目录,而 `pack/tests/fixtures.rs` 被**两边
//! 同时**通过 `#[path]` 编译(同一份手造包只维护一处)。守卫因此要放在两边的公共祖先上,否则
//! `fixtures.rs` 里的 `super::TempRoot` 在其中一侧解析不到。
//!
//! # 为什么不能靠"系统会清 `%TEMP%`"
//!
//! 那种清理要等时间或外部工具,而 `zip64` 那族用例每个要造 4 GiB 的**稀疏文件**验阈值:
//! M160 r2 实测 `%TEMP%/ull-pack-*` 堆到 **1651 个目录 / 110 GB**,把整台机器(含别的 agent
//! 的构建)一起顶死。所以测试自己负责收尾,并且有自证用例
//! (`pack/tests/mod.rs` 的 `the_temp_root_is_gone_once_dropped` /
//! `a_panicking_test_still_removes_its_temp_root`)。
//!
//! # 形状来自仓库既有写法
//!
//! 与 [`crate::update::tests::support::TempDir`] 同一套 `new` / `join` / `Drop` 三件套
//! (不引 `tempfile`、不新增范式),只多了两件事:`Deref<Target = Path>` 让调用点能直接当
//! `&Path` 用;[`TempRoot::under`] 让"子树落点、删父目录"这件事不留下空壳。
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
/// 一次性临时根目录(**`Drop` 即删**)。
pub(crate) struct TempRoot {
/// 真正要删的根([`Self::under`] 造的子树也删它,不留空壳父目录)。
root: PathBuf,
/// 本夹具的落点(通常等于 `root`;`under` 时是 `root/<sub>`)。
path: PathBuf,
}
impl TempRoot {
/// 建一个新的临时根(同名残留先清掉:`COUNTER` 在同一进程内唯一,跨进程靠 pid)。
pub(crate) fn new(tag: &str) -> Self {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let unique = COUNTER.fetch_add(1, Ordering::Relaxed);
let root =
std::env::temp_dir().join(format!("ull-pack-{}-{tag}-{unique}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("建临时根");
Self {
path: root.clone(),
root,
}
}
/// 在临时根下开一个子目录当落点(`Drop` 删的是**根**,父目录不会剩空壳)。
pub(crate) fn under(tag: &str, sub: &str) -> Self {
let base = Self::new(tag);
let path = base.path.join(sub);
std::fs::create_dir_all(&path).expect("建临时子目录");
Self {
root: base.root.clone(),
path,
}
}
/// 建一个临时根,并让落点直接是根内的某个文件(`Drop` 删的仍是**根**)。
///
/// 给"夹具函数要返回一个包路径、而包路径必须活到测试用完"的场景用:返回值 `Deref` 到那个
/// 文件,根由它持有。`root.parent()` 拿得到根目录。
pub(crate) fn at(tag: &str, file: &str) -> Self {
let mut base = Self::new(tag);
base.path = base.path.join(file);
base
}
/// 本夹具的落点。
pub(crate) fn path(&self) -> &Path {
&self.path
}
/// 落点下的一条路径(`&str` 与 `Path` 都收,免得把调用点写成两套)。
pub(crate) fn join(&self, name: impl AsRef<Path>) -> PathBuf {
self.path.join(name)
}
}
impl Deref for TempRoot {
type Target = Path;
fn deref(&self) -> &Path {
&self.path
}
}
impl AsRef<Path> for TempRoot {
/// 让 `&TempRoot` 能直接喂给 `impl AsRef<Path>` 形态的 API(`fs::read_dir` / `remove_dir_all`
/// / `PackArchive::open_default` …)。`Deref` 只解决"要 `&Path`"的场景,两者都要。
fn as_ref(&self) -> &Path {
&self.path
}
}
impl Drop for TempRoot {
fn drop(&mut self) {
// 尽力删:删不掉留一行日志,别在析构里 panic(二次 panic 会 abort)。
if let Err(error) = std::fs::remove_dir_all(&self.root) {
if error.kind() != std::io::ErrorKind::NotFound {
eprintln!("[temp] 删 {} 失败:{error}", self.root.display());
}
}
}
}