主任务(逐文件覆盖/保留)
- 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)。
108 lines
4.2 KiB
Rust
108 lines
4.2 KiB
Rust
//! 测试用的**一次性临时根**(`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());
|
||
}
|
||
}
|
||
}
|
||
}
|