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)。
This commit is contained in:
1 parent
a4f096eea3
commit
4defc41e8d
64 files changed
+3819
-442
No files matched your search
@@ -0,0 +1,260 @@
|
||||
//! 导入**冲突清单**的视图模型(M160):分页、计数、逐项处置。
|
||||
//!
|
||||
//! # 为什么是分页而不是"一次画完"
|
||||
//!
|
||||
//! 冲突清单可能有几千条(一个用户改过很多配置的老实例)。`iced` 的每一行都是真实 widget,
|
||||
//! 一次画几千行会同时拖慢布局与滚动,而用户真正要看的永远只有"这一屏"。所以模型里只出
|
||||
//! **当前这一页**([`PAGE_SIZE`] 条),页面上的「上一页 / 下一页」改的是
|
||||
//! [`PackPageState::pick_page`]——数据本身留在 presenter 的 `ImportScanView` 里,页面不复制。
|
||||
//!
|
||||
//! # 三个入口如何体现在计数上
|
||||
//!
|
||||
//! | 用户点了 | 决策表 | 计数 |
|
||||
//! |---|---|---|
|
||||
//! | 全部保留 | [`ConflictDecisionsView::KeepAll`] | 覆盖 0 |
|
||||
//! | 全部覆盖 | [`ConflictDecisionsView::OverwriteAll`] | 覆盖 = 全部 |
|
||||
//! | 逐项 | `PerItem`(只列"覆盖"的那些,其余按保留) | 覆盖 = 点过的条数 |
|
||||
//!
|
||||
//! 页面显示的两个数直接来自 [`PackPageState::decisions`]、[`PackPageState::pick_choice`] 的计算,
|
||||
//! 与发给 host 的那份决策**同源**——不会出现"界面说覆盖 3 个、实际覆盖 0 个"。
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use unlucky_i18n::{Catalog, Key};
|
||||
use unlucky_presenter::dto::{
|
||||
human_bytes, ConflictChoiceView, ConflictReasonView, ConflictSourceView, ImportConflictView,
|
||||
ImportScanView,
|
||||
};
|
||||
|
||||
/// 一页画多少行(一次画几千行是性能陷阱;40 行在一屏内可读且不滚动太多)。
|
||||
pub const PAGE_SIZE: usize = 40;
|
||||
|
||||
/// 冲突清单块(导入前那一屏)。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Pick {
|
||||
/// 标题(`导入前:有 12 个文件已存在且内容不同`)
|
||||
pub title: String,
|
||||
/// 计数摘要(`保留 9 · 覆盖 3 · 覆盖将丢弃 2.4 MB`)
|
||||
pub summary: String,
|
||||
/// 缺省说明("没改动的一律按保留")
|
||||
pub default_note: &'static str,
|
||||
/// 判不了的远端条目(`Some` 才画那一行)
|
||||
pub unknown_note: Option<String>,
|
||||
/// **当前页**的行(最多 [`PAGE_SIZE`] 条)
|
||||
pub rows: Vec<PickRow>,
|
||||
/// 当前页(0 起)
|
||||
pub page: usize,
|
||||
/// 总页数(至少 1)
|
||||
pub pages: usize,
|
||||
/// 区间说明(`第 1-40 项,共 312 项`)
|
||||
pub range: String,
|
||||
/// 保留几条
|
||||
pub keep_count: usize,
|
||||
/// 覆盖几条
|
||||
pub overwrite_count: usize,
|
||||
/// 能不能翻页
|
||||
pub can_prev: bool,
|
||||
/// 能不能翻页
|
||||
pub can_next: bool,
|
||||
/// 三个入口的文案
|
||||
pub keep_all: &'static str,
|
||||
/// 三个入口的文案
|
||||
pub overwrite_all: &'static str,
|
||||
/// 逐项按钮(保留)的文案
|
||||
pub row_keep: &'static str,
|
||||
/// 逐项按钮(覆盖)的文案
|
||||
pub row_overwrite: &'static str,
|
||||
/// 翻页文案
|
||||
pub prev: &'static str,
|
||||
/// 翻页文案
|
||||
pub next: &'static str,
|
||||
/// 确认导入
|
||||
pub confirm: String,
|
||||
/// 取消导入
|
||||
pub cancel: &'static str,
|
||||
/// 「全部保留」这一档当前是否生效(整批档切到保留时高亮)
|
||||
pub keep_all_active: bool,
|
||||
/// 「全部覆盖」这一档当前是否生效
|
||||
pub overwrite_all_active: bool,
|
||||
}
|
||||
|
||||
/// 清单里的一行。
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PickRow {
|
||||
/// 实例内路径
|
||||
pub path: String,
|
||||
/// 来源(`包内 overrides/` / `清单 files[]`)
|
||||
pub source: String,
|
||||
/// 成因(`本来就在` / `你改过`)
|
||||
pub reason: String,
|
||||
/// 两侧体积(`你的:2.1 MB · 包内:1.8 MB`)
|
||||
pub sizes: String,
|
||||
/// 当前处置([`ConflictChoiceView::KeepUser`] = 保留)
|
||||
pub choice: ConflictChoiceView,
|
||||
/// 这一行按钮上写的字(点了就变成另一侧)
|
||||
pub toggle: &'static str,
|
||||
}
|
||||
|
||||
impl PickRow {
|
||||
/// 这一行是"覆盖"吗。
|
||||
#[must_use]
|
||||
pub const fn overwrites(&self) -> bool {
|
||||
matches!(self.choice, ConflictChoiceView::Overwrite)
|
||||
}
|
||||
}
|
||||
|
||||
/// 从预检结果 + 页面选择造出这一块(`None` = 没有待决冲突,页面不该画它)。
|
||||
#[must_use]
|
||||
pub fn pick_of(
|
||||
scan: &ImportScanView,
|
||||
page_number: usize,
|
||||
per_item: &BTreeMap<String, ConflictChoiceView>,
|
||||
batch_overwrite: bool,
|
||||
catalog: Catalog,
|
||||
) -> Option<Pick> {
|
||||
if scan.conflicts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let total = scan.conflicts.len();
|
||||
let pages = total.div_ceil(PAGE_SIZE).max(1);
|
||||
let page = page_number.min(pages - 1);
|
||||
let from = page * PAGE_SIZE;
|
||||
let to = (from + PAGE_SIZE).min(total);
|
||||
|
||||
let rows: Vec<PickRow> = scan.conflicts[from..to]
|
||||
.iter()
|
||||
.map(|conflict| row_of(conflict, per_item, batch_overwrite, catalog))
|
||||
.collect();
|
||||
|
||||
let overwrite_count = scan
|
||||
.conflicts
|
||||
.iter()
|
||||
.filter(|conflict| {
|
||||
effective(conflict.path.as_str(), per_item, batch_overwrite)
|
||||
== ConflictChoiceView::Overwrite
|
||||
})
|
||||
.count();
|
||||
let keep_count = total - overwrite_count;
|
||||
// 覆盖会丢掉多少:只算"你真选了覆盖"的那些,避免用最坏数字吓人。
|
||||
let lost = scan
|
||||
.conflicts
|
||||
.iter()
|
||||
.filter(|conflict| {
|
||||
effective(conflict.path.as_str(), per_item, batch_overwrite)
|
||||
== ConflictChoiceView::Overwrite
|
||||
})
|
||||
.map(|conflict| conflict.disk_size)
|
||||
.fold(0_u64, u64::saturating_add);
|
||||
|
||||
Some(Pick {
|
||||
title: catalog.render(Key::PackPickTitle, &[("count", &total.to_string())]),
|
||||
summary: catalog.render(
|
||||
Key::PackPickSummary,
|
||||
&[
|
||||
("keep", &keep_count.to_string()),
|
||||
("overwrite", &overwrite_count.to_string()),
|
||||
("bytes", &human_bytes(lost)),
|
||||
],
|
||||
),
|
||||
default_note: catalog.text(Key::PackPickDefaultNote),
|
||||
unknown_note: (scan.unknown_remote > 0).then(|| {
|
||||
catalog.render(
|
||||
Key::PackPickUnknownRemote,
|
||||
&[("count", &scan.unknown_remote.to_string())],
|
||||
)
|
||||
}),
|
||||
rows,
|
||||
page,
|
||||
pages,
|
||||
range: catalog.render(
|
||||
Key::PackPickPage,
|
||||
&[
|
||||
("from", &(from + 1).to_string()),
|
||||
("to", &to.to_string()),
|
||||
("total", &total.to_string()),
|
||||
],
|
||||
),
|
||||
keep_count,
|
||||
overwrite_count,
|
||||
can_prev: page > 0,
|
||||
can_next: page + 1 < pages,
|
||||
keep_all: catalog.text(Key::PackPickKeepAll),
|
||||
overwrite_all: catalog.text(Key::PackPickOverwriteAll),
|
||||
row_keep: catalog.text(Key::PackPickRowKeep),
|
||||
row_overwrite: catalog.text(Key::PackPickRowOverwrite),
|
||||
prev: catalog.text(Key::PackPickPrev),
|
||||
next: catalog.text(Key::PackPickNext),
|
||||
confirm: catalog.render(Key::PackPickConfirm, &[]),
|
||||
cancel: catalog.text(Key::PackPickCancel),
|
||||
keep_all_active: !batch_overwrite,
|
||||
overwrite_all_active: batch_overwrite,
|
||||
})
|
||||
}
|
||||
|
||||
/// 一条冲突的**有效**处置:逐项优先,其次整批档,缺省保留(与 core 侧同一条规则)。
|
||||
fn effective(
|
||||
path: &str,
|
||||
per_item: &BTreeMap<String, ConflictChoiceView>,
|
||||
batch_overwrite: bool,
|
||||
) -> ConflictChoiceView {
|
||||
if let Some(choice) = per_item.get(path) {
|
||||
return *choice;
|
||||
}
|
||||
if batch_overwrite {
|
||||
ConflictChoiceView::Overwrite
|
||||
} else {
|
||||
ConflictChoiceView::KeepUser
|
||||
}
|
||||
}
|
||||
|
||||
fn row_of(
|
||||
conflict: &ImportConflictView,
|
||||
per_item: &BTreeMap<String, ConflictChoiceView>,
|
||||
batch_overwrite: bool,
|
||||
catalog: Catalog,
|
||||
) -> PickRow {
|
||||
let choice = effective(conflict.path.as_str(), per_item, batch_overwrite);
|
||||
PickRow {
|
||||
path: conflict.path.clone(),
|
||||
source: source_label(conflict.source, catalog).to_owned(),
|
||||
reason: reason_label(conflict.reason, catalog).to_owned(),
|
||||
sizes: format!(
|
||||
"{} · {}",
|
||||
catalog.render(
|
||||
Key::PackPickDisk,
|
||||
&[("size", &human_bytes(conflict.disk_size))]
|
||||
),
|
||||
catalog.render(
|
||||
Key::PackPickIncoming,
|
||||
&[("size", &human_bytes(conflict.pack_size))]
|
||||
)
|
||||
),
|
||||
choice,
|
||||
// 按钮写"点了会变成什么":当前保留 ⇒ 按钮是「覆盖」。
|
||||
toggle: if matches!(choice, ConflictChoiceView::Overwrite) {
|
||||
catalog.text(Key::PackPickRowKeep)
|
||||
} else {
|
||||
catalog.text(Key::PackPickRowOverwrite)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// 来源的人读串。
|
||||
fn source_label(source: ConflictSourceView, catalog: Catalog) -> &'static str {
|
||||
match source {
|
||||
ConflictSourceView::Overrides => catalog.text(Key::PackPickSourceOverrides),
|
||||
ConflictSourceView::ClientOverrides => catalog.text(Key::PackPickSourceClientOverrides),
|
||||
ConflictSourceView::CurseForge | ConflictSourceView::Modrinth => {
|
||||
catalog.text(Key::PackPickSourceManifest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 成因的人读串(复用导入报告那三句,不另造一套说法)。
|
||||
fn reason_label(reason: ConflictReasonView, catalog: Catalog) -> &'static str {
|
||||
match reason {
|
||||
ConflictReasonView::UserModified => catalog.text(Key::PackConflictUserModified),
|
||||
ConflictReasonView::UserDeleted => catalog.text(Key::PackConflictUserDeleted),
|
||||
ConflictReasonView::PreExisting => catalog.text(Key::PackConflictPreExisting),
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@
|
||||
//! 这里**不测画法**(画法靠出图),测的是「决策」:哪些数字进了哪一行、哪些字段被截短、
|
||||
//! 哪一态该出现哪一块。所有断言都能在没有图形环境的情况下跑。
|
||||
|
||||
mod pick;
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
use unlucky_i18n::Lang;
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//! 冲突清单(M160)的视图模型守门:分页、计数、三个入口 → 决策表。
|
||||
//!
|
||||
//! 这里钉的是**不画**的那部分——一屏只出 `PAGE_SIZE` 行、翻页只换数据、计数与最终决策同源。
|
||||
//! 画法本身靠出图(`ULL_CONTENT_PACK=pick`)。
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use super::super::pick::{pick_of, PAGE_SIZE};
|
||||
use super::super::PackPageState;
|
||||
use super::catalog;
|
||||
use unlucky_presenter::dto::{
|
||||
ConflictChoiceView, ConflictDecisionsView, ConflictReasonView, ConflictSourceView,
|
||||
ImportConflictView, ImportScanView,
|
||||
};
|
||||
|
||||
/// `count` 条冲突的预检结果。
|
||||
fn scan(count: usize) -> ImportScanView {
|
||||
ImportScanView {
|
||||
target: std::path::PathBuf::from("instance"),
|
||||
format: None,
|
||||
pack_name: Some("Better MC".to_string()),
|
||||
pack_version: None,
|
||||
game_version: None,
|
||||
conflicts: (0..count)
|
||||
.map(|index| ImportConflictView {
|
||||
path: format!("config/row-{index:03}.toml"),
|
||||
source: ConflictSourceView::Overrides,
|
||||
reason: ConflictReasonView::PreExisting,
|
||||
disabled: false,
|
||||
pack_sha1: "aa".repeat(20),
|
||||
pack_size: 1_000,
|
||||
disk_sha1: "bb".repeat(20),
|
||||
disk_size: 2_000,
|
||||
base_sha1: None,
|
||||
})
|
||||
.collect(),
|
||||
unknown_remote: 0,
|
||||
lost_bytes: 0,
|
||||
incoming_bytes: 0,
|
||||
notes: Vec::new(),
|
||||
error: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pick(
|
||||
scan: &ImportScanView,
|
||||
page: usize,
|
||||
per_item: &BTreeMap<String, ConflictChoiceView>,
|
||||
batch: bool,
|
||||
) -> super::super::pick::Pick {
|
||||
pick_of(scan, page, per_item, batch, catalog()).expect("有冲突就该画这一块")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_the_current_page_is_drawn() {
|
||||
let scan = scan(312);
|
||||
let first = pick(&scan, 0, &BTreeMap::new(), false);
|
||||
assert_eq!(first.rows.len(), PAGE_SIZE, "一屏只许画一页的行");
|
||||
assert_eq!(first.pages, 8, "312 条 / 40 条一页 = 8 页");
|
||||
assert_eq!(first.page, 0);
|
||||
assert!(first.can_next, "还有下一页");
|
||||
assert!(!first.can_prev, "第一页没有上一页");
|
||||
assert_eq!(first.range, "Items 1-40 of 312");
|
||||
assert_eq!(first.rows[0].path, "config/row-000.toml");
|
||||
|
||||
let second = pick(&scan, 1, &BTreeMap::new(), false);
|
||||
assert_eq!(second.page, 1);
|
||||
assert_eq!(
|
||||
second.rows[0].path,
|
||||
format!("config/row-{PAGE_SIZE:03}.toml")
|
||||
);
|
||||
assert_eq!(second.range, "Items 41-80 of 312");
|
||||
assert!(second.can_prev && second.can_next);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_last_page_is_short_and_the_page_number_is_clamped() {
|
||||
let scan = scan(312);
|
||||
let last = pick(&scan, 7, &BTreeMap::new(), false);
|
||||
assert_eq!(last.rows.len(), 312 - 7 * PAGE_SIZE, "最后一页只剩尾巴");
|
||||
assert_eq!(last.range, "Items 281-312 of 312");
|
||||
assert!(!last.can_next);
|
||||
|
||||
// 页码越界(例如用户先在 300 条的清单上翻到第 6 页,再重扫一份 10 条的)⇒ 夹到最后一页。
|
||||
let clamped = pick(&scan, 99, &BTreeMap::new(), false);
|
||||
assert_eq!(clamped.page, 7);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_summary_counts_what_the_user_actually_picked() {
|
||||
let scan = scan(10);
|
||||
let keep_all = pick(&scan, 0, &BTreeMap::new(), false);
|
||||
assert_eq!(keep_all.keep_count, 10);
|
||||
assert_eq!(keep_all.overwrite_count, 0);
|
||||
assert!(keep_all.summary.contains("Keep 10"), "{}", keep_all.summary);
|
||||
assert!(keep_all.keep_all_active && !keep_all.overwrite_all_active);
|
||||
|
||||
let mut per_item = BTreeMap::new();
|
||||
per_item.insert(
|
||||
"config/row-001.toml".to_string(),
|
||||
ConflictChoiceView::Overwrite,
|
||||
);
|
||||
per_item.insert(
|
||||
"config/row-004.toml".to_string(),
|
||||
ConflictChoiceView::Overwrite,
|
||||
);
|
||||
let mixed = pick(&scan, 0, &per_item, false);
|
||||
assert_eq!(mixed.overwrite_count, 2);
|
||||
assert_eq!(mixed.keep_count, 8);
|
||||
// 丢弃量只算"真选了覆盖"的那两条(各 2 000 字节)。
|
||||
assert!(
|
||||
mixed.summary.contains("Overwrite 2") || mixed.summary.contains("overwrite 2"),
|
||||
"{}",
|
||||
mixed.summary
|
||||
);
|
||||
assert_eq!(
|
||||
mixed.rows[1].choice,
|
||||
ConflictChoiceView::Overwrite,
|
||||
"点过的那一行要显示成覆盖"
|
||||
);
|
||||
assert_eq!(mixed.rows[0].choice, ConflictChoiceView::KeepUser);
|
||||
|
||||
let all = pick(&scan, 0, &BTreeMap::new(), true);
|
||||
assert_eq!(all.overwrite_count, 10);
|
||||
assert!(all.overwrite_all_active && !all.keep_all_active);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_conflict_means_no_block() {
|
||||
let scan = scan(0);
|
||||
assert!(
|
||||
pick_of(&scan, 0, &BTreeMap::new(), false, catalog()).is_none(),
|
||||
"没有冲突就不该画这一屏(不打扰用户)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decisions_default_to_keep_all() {
|
||||
let page = PackPageState::default();
|
||||
assert_eq!(page.decisions(None), ConflictDecisionsView::KeepAll);
|
||||
// 逐项点一个"覆盖" ⇒ 逐项表里只有它,缺省档仍是保留。
|
||||
let mut page = PackPageState::default();
|
||||
page.pick_choices
|
||||
.insert("mods/a.jar".to_string(), ConflictChoiceView::Overwrite);
|
||||
let decisions = page.decisions(Some(&scan(3)));
|
||||
assert_eq!(decisions.default_choice(), ConflictChoiceView::KeepUser);
|
||||
assert_eq!(decisions.overwrite_count(), 1);
|
||||
assert_eq!(decisions.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn batch_overwrite_plus_a_kept_file_is_expressible() {
|
||||
// 全部覆盖之后单独放回一条:缺省档是覆盖、逐项里那条是保留。
|
||||
let mut page = PackPageState {
|
||||
pick_overwrite_all: true,
|
||||
..PackPageState::default()
|
||||
};
|
||||
assert_eq!(
|
||||
page.decisions(Some(&scan(3))),
|
||||
ConflictDecisionsView::OverwriteAll,
|
||||
"只点整批覆盖时不需要逐项表"
|
||||
);
|
||||
page.pick_choices
|
||||
.insert("mods/pin.jar".to_string(), ConflictChoiceView::KeepUser);
|
||||
let decisions = page.decisions(Some(&scan(3)));
|
||||
assert_eq!(decisions.default_choice(), ConflictChoiceView::Overwrite);
|
||||
assert_eq!(decisions.keep_count(), 1);
|
||||
assert_eq!(decisions.overwrite_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggling_flips_the_row_and_clearing_the_batch_drops_per_item_choices() {
|
||||
let mut page = PackPageState::default();
|
||||
let path = "mods/a.jar";
|
||||
assert_eq!(page.pick_choice(path), ConflictChoiceView::KeepUser);
|
||||
page.pick_choices
|
||||
.insert(path.to_string(), ConflictChoiceView::Overwrite);
|
||||
assert_eq!(page.pick_choice(path), ConflictChoiceView::Overwrite);
|
||||
|
||||
// 整批覆盖之后,那条逐项"保留"仍然优先于整批档。
|
||||
page.pick_overwrite_all = true;
|
||||
page.pick_choices
|
||||
.insert(path.to_string(), ConflictChoiceView::KeepUser);
|
||||
assert_eq!(page.pick_choice(path), ConflictChoiceView::KeepUser);
|
||||
assert_eq!(
|
||||
page.pick_choice("mods/other.jar"),
|
||||
ConflictChoiceView::Overwrite
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user