fix(content): 不兼容档在失败路径上取项目名 + 重试轮不再贴错根的标题 (M191 r2)
复审 r2 两条 p2,都修了,并按复审给的口径验过红。
**P2-1(续):不兼容档不再只有裸 id**
`FailureKind::Incompatible` 的项目**确实存在**(站点只是没有匹配目标版本/加载器的条目),
所以失败路径上值得现取一次名字——`plan::names_for_incompatible` 按失败项去重、一项一次 GET
(`Modrinth::project`),`fetch_title` 把任何失败都折成 `None`:
**拿名字失败不许影响安装结果**,取不到 / 拿到空标题就退回 id。只有这一档发请求;
「取不到」档 404 那部分项目根本不存在,再发一次只会再吃 404。
**P2-2(新):重试轮把根的标题贴到了前置头上**
根因是判「这条是不是根项目自己失败」用了 `plan.request.roots.first()`,而重试那一轮的 plan
来自 `retry_request()`,它的 `roots` 就是上一轮的失败名单——第一个是前置。
现在判据改成 **id 比对**(`Root{id,title}`,id 由 `remember` 一起记下来):
`label_of` 只在这条失败的 id 真的等于原始根 id 时才用根的标题。复审给的那个形状
(roots=["P7dR8mSH","1eAoo2KR"] + root_title="Sodium")现在画 `P7dR8mSH`,不再是
`Sodium (P7dR8mSH)`。
**守门(每条都真跑红过,且在拆完模块的最终代码上复跑)**
- 去掉不兼容档的名字来源 ⇒ `an_incompatible_item_shows_the_project_name_once_it_is_known` 红
(`left: "P7dR8mSH"` / `right: "Entity Culling (P7dR8mSH)"`)。
- `label_of` 换回 `roots.first()` ⇒ `the_retry_round_does_not_paste_the_root_title_onto_a_dependency` 红
(`left: ["Sodium (P7dR8mSH)","1eAoo2KR"]`,正是复审实测的那一行)。
- 取名字不降级(`expect`)⇒ `a_failed_name_lookup_falls_back_to_the_id_without_breaking_anything` 红。
**行数**:`plan.rs` 加完这套到 414 行、`install.rs` 到 403 行,都越过 400 硬线 ⇒
把「名字从哪来」整块(`Root`/`Names`/`names_for_incompatible`/`label_of`/`fetch_title`)
纯搬移到 `plan/labels.rs`(111 行):`plan.rs` 336、`install.rs` 388。
This commit is contained in:
1 parent
961ff453a0
commit
26e7b7333e
7 files changed
+313
-77
No files matched your search
@@ -41,7 +41,7 @@ fn retry_failure(instance: &str, catalog: Catalog) -> Option<RetryPanel> {
|
||||
..InstallProgressView::new()
|
||||
};
|
||||
// 三种可显示性各一条,与 host 侧 `plan::label_of` 的三种情况同形:
|
||||
// 根项目 = 请求里的标题、下载失败 = 文件名、解析 404 = 没有名字(只画 id)。
|
||||
// 根项目 = 请求里的标题、下载失败 = 文件名、不兼容档 = 失败路径上现取的项目名。
|
||||
progress.retry = vec![
|
||||
InstallFailureView::new(
|
||||
"AANobbMI",
|
||||
@@ -51,7 +51,7 @@ fn retry_failure(instance: &str, catalog: Catalog) -> Option<RetryPanel> {
|
||||
),
|
||||
InstallFailureView::new(
|
||||
"P7dR8mSH",
|
||||
"",
|
||||
"Entity Culling",
|
||||
InstallFailureKind::Incompatible,
|
||||
"没有匹配所选游戏版本/加载器(1.20.1 / fabric)的版本",
|
||||
),
|
||||
@@ -336,10 +336,10 @@ mod tests {
|
||||
.as_slice()
|
||||
)
|
||||
);
|
||||
// 拿不到名的那一档(解析 404 / 不兼容):只画 id,不编名字
|
||||
// 不兼容档:失败路径上现取的项目名(不是 id)
|
||||
assert_eq!(
|
||||
panel.items_of(unlucky_presenter::dto::InstallFailureKind::Incompatible),
|
||||
Some(["P7dR8mSH".to_string()].as_slice())
|
||||
Some(["Entity Culling (P7dR8mSH)".to_string()].as_slice())
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ fn failed_progress() -> unlucky_presenter::dto::InstallProgressView {
|
||||
),
|
||||
InstallFailureView::new(
|
||||
"P7dR8mSH",
|
||||
"fabric-api.jar",
|
||||
"Entity Culling",
|
||||
InstallFailureKind::Incompatible,
|
||||
"没有匹配的版本",
|
||||
),
|
||||
@@ -52,7 +52,7 @@ fn the_two_tiers_are_two_groups_with_two_labels() {
|
||||
// **P2-1 的守门**:有名字的画「名字 (id)」,没有名字的才只画 id
|
||||
assert_eq!(
|
||||
panel.items_of(InstallFailureKind::Incompatible),
|
||||
Some(["fabric-api.jar (P7dR8mSH)".to_string()].as_slice())
|
||||
Some(["Entity Culling (P7dR8mSH)".to_string()].as_slice())
|
||||
);
|
||||
assert_eq!(
|
||||
panel.items_of(InstallFailureKind::Unreachable),
|
||||
@@ -87,7 +87,7 @@ fn an_empty_tier_does_not_get_a_group() {
|
||||
};
|
||||
progress.retry = vec![InstallFailureView::new(
|
||||
"P7dR8mSH",
|
||||
"fabric-api.jar",
|
||||
"Entity Culling",
|
||||
InstallFailureKind::Incompatible,
|
||||
"没有匹配的版本",
|
||||
)];
|
||||
|
||||
@@ -161,14 +161,14 @@ fn run(
|
||||
// 「只重试这些」:输入是上一轮那份计划,**不**重新发起根项目安装。
|
||||
// 没有记录时(换了实例 / 进程重启过)回一条失败,而不是退化成「整单重装」。
|
||||
if request.retry {
|
||||
let Some((target_dir, title, previous)) = plan::remembered(env, &request.instance) else {
|
||||
let Some(saved) = plan::remembered(env, &request.instance) else {
|
||||
return Err(local(RETRY_LOST));
|
||||
};
|
||||
// 面板头继续显示那次的显示名(不是项目 id:r1 复审 N5)
|
||||
progress.version = title.clone();
|
||||
let present = plan::present_files(&target_dir);
|
||||
progress.version = saved.title.clone();
|
||||
let present = plan::present_files(&saved.dir);
|
||||
let core_engine = super::super::engine::engine(env);
|
||||
let dependency_plan = plan::retry_plan(&previous, present, |retry| {
|
||||
let dependency_plan = plan::retry_plan(&saved.plan, present, |retry| {
|
||||
let transport = EngineTransport::new(&core_engine);
|
||||
let client = Modrinth::new(&transport);
|
||||
block_on(resolve(&client, retry))
|
||||
@@ -178,8 +178,11 @@ fn run(
|
||||
request,
|
||||
dependency_plan,
|
||||
Placement {
|
||||
dir: &target_dir,
|
||||
title: &title,
|
||||
dir: &saved.dir,
|
||||
root: plan::Root {
|
||||
id: &saved.root,
|
||||
title: &saved.title,
|
||||
},
|
||||
},
|
||||
cancel,
|
||||
progress,
|
||||
@@ -221,7 +224,10 @@ fn run(
|
||||
dependency_plan,
|
||||
Placement {
|
||||
dir: &target_dir,
|
||||
title: &request.title,
|
||||
root: plan::Root {
|
||||
id: &request.project,
|
||||
title: &request.title,
|
||||
},
|
||||
},
|
||||
cancel,
|
||||
progress,
|
||||
@@ -230,12 +236,15 @@ fn run(
|
||||
)
|
||||
}
|
||||
|
||||
/// 「装到哪、那次叫什么」——这两样总是一起出现,捆成一个参数(否则 `collect` 顶破 8 个)。
|
||||
/// 「装到哪 + 那次装入的根是谁」——总是一起出现,捆成一个参数(否则 `collect` 顶破 8 个)。
|
||||
struct Placement<'a> {
|
||||
/// 落盘目录(`mods/` / `resourcepacks/` / `shaderpacks/`)
|
||||
dir: &'a Path,
|
||||
/// 那次装入的显示名(失败名单里根项目的可读名就是它)
|
||||
title: &'a str,
|
||||
/// 当初那次装入的根项目(id + 显示名);重试轮里由 `plan::remembered` 提供
|
||||
///
|
||||
/// **必须**带 id:重试那一轮 `plan.request.roots` 是上一轮的失败名单,不能拿它判「是不是根」
|
||||
/// (r2 复审 P2-2 就是这条:第一前置被贴上了根的标题)。
|
||||
root: plan::Root<'a>,
|
||||
}
|
||||
|
||||
/// 解析之后**两条入口共用**的尾巴:进度 → 逐个下载 → 收口。
|
||||
@@ -293,12 +302,16 @@ fn collect(
|
||||
// 解析与下载两段的失败在这里一起收口:两档分成两句话(不合并成「下载失败」),
|
||||
// 并列出「可只重试这 N 个」。
|
||||
if let Some(failure) = plan::failure_of_plan(&dependency_plan, installed.len()) {
|
||||
progress.retry = plan::retry_items(&dependency_plan, placement.title);
|
||||
// 「不兼容」那一档项目**存在**(站点只是没有匹配目标版本/加载器的条目),所以在这里
|
||||
// 现取一次名字——只在失败路径上发、一个项目一次 GET,取不到就退回 id(见 `names_for_incompatible`)。
|
||||
let names = plan::names_for_incompatible(&dependency_plan, |id| plan::fetch_title(env, id));
|
||||
progress.retry = plan::retry_items(&dependency_plan, placement.root, &names);
|
||||
plan::remember(
|
||||
env,
|
||||
&request.instance,
|
||||
placement.dir,
|
||||
placement.title,
|
||||
placement.root.id,
|
||||
placement.root.title,
|
||||
&dependency_plan,
|
||||
);
|
||||
return Err(failure);
|
||||
|
||||
@@ -43,8 +43,10 @@ use crate::dto::{ContentFailureKind, InstallFailureKind, InstallFailureView};
|
||||
|
||||
use super::sources::failure_of;
|
||||
|
||||
mod labels;
|
||||
mod remembered;
|
||||
|
||||
pub(super) use labels::{fetch_title, names_for_incompatible, Root};
|
||||
pub(super) use remembered::{forget, remember, remembered};
|
||||
|
||||
/// 扫目标目录(不递归)里已存在的文件名。
|
||||
@@ -126,18 +128,18 @@ fn kind_of(failure: &DependencyFailure) -> ContentFailureKind {
|
||||
///
|
||||
/// 档位**一一映射** core 的 [`FailureKind`]:两档在 DTO 上分开,界面才有两段文案。
|
||||
/// 顺序沿用计划里的顺序(`record_failure` 已按项目键去重,同一项目只出现一次)。
|
||||
///
|
||||
/// `root_title` 是**用户点的那次装入的显示名**(`ContentInstallRequest.title`,来自检索命中的
|
||||
/// 项目标题)。它只用来给「根项目自己失败」那条垫一个可读名(M191 r1 P2-1:面板原来只画
|
||||
/// `P7dR8mSH` 这种站点 id,用户拿到的是一串哈希)。
|
||||
#[must_use]
|
||||
pub(super) fn retry_items(plan: &DependencyPlan, root_title: &str) -> Vec<InstallFailureView> {
|
||||
pub(super) fn retry_items(
|
||||
plan: &DependencyPlan,
|
||||
root: labels::Root<'_>,
|
||||
names: &labels::Names,
|
||||
) -> Vec<InstallFailureView> {
|
||||
plan.failures
|
||||
.iter()
|
||||
.map(|failure| {
|
||||
InstallFailureView::new(
|
||||
failure.requested_as.clone(),
|
||||
label_of(plan, failure, root_title),
|
||||
labels::label_of(plan, failure, root, names),
|
||||
failure_kind_of(failure.kind),
|
||||
failure.message.clone(),
|
||||
)
|
||||
@@ -145,34 +147,6 @@ pub(super) fn retry_items(plan: &DependencyPlan, root_title: &str) -> Vec<Instal
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 一条失败的可读标签(**语言中立**:站点标题或文件名;拿不到就是空串)。
|
||||
///
|
||||
/// 两个来源,都在手边、零额外请求:
|
||||
///
|
||||
/// | 情况 | 标签 | 为什么 |
|
||||
/// |---|---|---|
|
||||
/// | 根项目自己失败 | 请求里的 `title` | 用户刚才点的就是它,界面其它地方用的也是这个名字 |
|
||||
/// | 计划里有这个节点(下载失败) | `node.file.filename` | 「哪个文件没下来」比项目名更具体 |
|
||||
/// | 都没有(解析阶段 404 / 不兼容) | 空 | 站点没给过名字;界面退回只画 id(合理兜底) |
|
||||
///
|
||||
/// **不许**拿 `failure.message` 顶:那是 core 的中文散文,画到界面上就是 i18n R2 的违规。
|
||||
#[must_use]
|
||||
fn label_of(plan: &DependencyPlan, failure: &DependencyFailure, root_title: &str) -> String {
|
||||
if plan
|
||||
.request
|
||||
.roots
|
||||
.first()
|
||||
.is_some_and(|root| root == &failure.requested_as)
|
||||
{
|
||||
return root_title.to_string();
|
||||
}
|
||||
plan.nodes
|
||||
.iter()
|
||||
.find(|node| node.requested_as == failure.requested_as)
|
||||
.map(|node| node.file.filename.clone())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// core 的档位 → DTO 的档位(**穷尽 match**:谁把两档合并,这条会报错或让用例红)。
|
||||
const fn failure_kind_of(kind: FailureKind) -> InstallFailureKind {
|
||||
match kind {
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
//! 「这条失败该显示什么名字」——**语言中立**的标签来源(M191 r1 P2-1 / r2 P2-1 续)。
|
||||
//!
|
||||
//! 从 `plan.rs` 拆出(加完失败路径上取名字那次 GET 之后父文件到 414 行,越过 400 行硬线)。
|
||||
//! 拆的口径是「一件事一个文件」:这里只管**名字从哪来**,判定与说话仍在 `plan.rs`。
|
||||
//!
|
||||
//! 三个来源,按可信度排(细节在 [`label_of`]):原始根项目的标题、失败文件的文件名、
|
||||
//! 失败路径上现取的项目名;都没有就留空,界面只画项目 id。
|
||||
//!
|
||||
//! **不许**拿 `DependencyFailure::message` 顶:那是 core 的中文散文,画到界面上就是 i18n R2 的违规。
|
||||
|
||||
use unlucky_core::addon::online::{
|
||||
DependencyFailure, DependencyPlan, EngineTransport, FailureKind, Modrinth,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
|
||||
use super::super::super::env::HostEnv;
|
||||
|
||||
/// 这一次装入的**原始根项目**(id + 显示名)。
|
||||
///
|
||||
/// 「是不是根」只能按 `id` 比(r2 复审 P2-2):重试那一轮的 plan 来自
|
||||
/// `DependencyPlan::retry_request()`,它的 `request.roots` **就是上一轮的失败名单**——
|
||||
/// 第一个是前置,拿它当「根」会把根的标题贴到前置头上。所以判据是「这条失败的 id 是不是
|
||||
/// 当初那次装入的根 id」,两个值都由 [`remembered`] / `ContentInstallRequest` 提供。
|
||||
#[derive(Clone, Copy)]
|
||||
pub(in crate::host::addon) struct Root<'a> {
|
||||
/// 当初那次装入的根项目 id(`ContentInstallRequest.project`)
|
||||
pub id: &'a str,
|
||||
/// 它的显示名(`ContentInstallRequest.title`;只配给 `id` 用)
|
||||
pub title: &'a str,
|
||||
}
|
||||
|
||||
/// 失败路径上现取的项目名(id → title)。取不到的项目不在表里。
|
||||
pub(in crate::host::addon) type Names = Vec<(String, String)>;
|
||||
|
||||
/// 为「不兼容」档现取项目名(**只在失败路径上**,一个项目一次 GET)。
|
||||
///
|
||||
/// 为什么只给这一档:`FailureKind::Incompatible` 是「站点**有这个项目**,但目标版本/加载器下
|
||||
/// 没有条目」——项目确实存在,所以这次 GET 有东西可拿。「取不到」档里 404 的那部分项目根本
|
||||
/// 不存在,给它发请求只会再吃一个 404,于是那一档仍显示 id(合理兜底)。
|
||||
///
|
||||
/// `lookup` 由调用方注入(生产是 `Modrinth::project`,用例是闭包):于是「取名字失败也要能降级」
|
||||
/// 这件事在没有网络的用例里也能钉住。取不到 / 拿到空串的项一律**不进**名单。
|
||||
pub(in crate::host::addon) fn names_for_incompatible(
|
||||
plan: &DependencyPlan,
|
||||
lookup: impl Fn(&str) -> Option<String>,
|
||||
) -> Names {
|
||||
plan.failures_of(FailureKind::Incompatible)
|
||||
.filter_map(|failure| {
|
||||
let title = lookup(&failure.requested_as)?;
|
||||
let title = title.trim();
|
||||
(!title.is_empty()).then(|| (failure.requested_as.clone(), title.to_string()))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// 一条失败的可读标签(**语言中立**:站点标题、文件名或现取的项目名;拿不到就是空串)。
|
||||
///
|
||||
/// 三个来源,**按可信度排**:
|
||||
///
|
||||
/// | 情况 | 标签 | 为什么 |
|
||||
/// |---|---|---|
|
||||
/// | 这条失败的 id == 原始根 id | 请求里的 `title` | 用户刚才点的就是它 |
|
||||
/// | 计划里有这个节点(下载失败) | `node.file.filename` | 「哪个文件没下来」比项目名更具体 |
|
||||
/// | 不兼容档 | [`names_for_incompatible`] 现取的名字 | 项目存在,一次 GET 拿得到 |
|
||||
/// | 其余(解析 404 等) | 空 | 项目不存在,显示 id 是合理兜底 |
|
||||
///
|
||||
/// **不许**拿 `failure.message` 顶:那是 core 的中文散文,画到界面上就是 i18n R2 的违规。
|
||||
#[must_use]
|
||||
pub(super) fn label_of(
|
||||
plan: &DependencyPlan,
|
||||
failure: &DependencyFailure,
|
||||
root: Root<'_>,
|
||||
names: &Names,
|
||||
) -> String {
|
||||
// ① 根项目自己失败:**按 id 比**,不看 `plan.request.roots`(重试轮里那是失败名单)
|
||||
if failure.requested_as == root.id {
|
||||
return root.title.to_string();
|
||||
}
|
||||
// ② 计划里有这个节点(下载失败):文件名最具体
|
||||
if let Some(node) = plan
|
||||
.nodes
|
||||
.iter()
|
||||
.find(|node| node.requested_as == failure.requested_as)
|
||||
{
|
||||
return node.file.filename.clone();
|
||||
}
|
||||
// ③ 不兼容档:失败路径上现取的名字;取不到就空 ⇒ 界面只画 id
|
||||
if failure.kind == FailureKind::Incompatible {
|
||||
return names
|
||||
.iter()
|
||||
.find(|(id, _)| id == &failure.requested_as)
|
||||
.map(|(_, title)| title.clone())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// 取一个项目的显示名(**只在失败路径上**发)。
|
||||
///
|
||||
/// 失败一律回 `None`:**拿名字失败不许影响安装结果**——这条是「只重试这些」的补充信息,
|
||||
/// 坏一次网络不该把「装了什么、缺了什么」这整件事变成一条错误。
|
||||
pub(in crate::host::addon) fn fetch_title(env: &HostEnv, id: &str) -> Option<String> {
|
||||
let core_engine = crate::host::engine::engine(env);
|
||||
let transport = EngineTransport::new(&core_engine);
|
||||
let client = Modrinth::new(&transport);
|
||||
let id = id.to_string();
|
||||
block_on(async move { client.project(&id).await })
|
||||
.ok()
|
||||
.map(|project| project.title)
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
//! 从 `plan.rs` 拆出(父文件加了 `label_of` 之后顶到 429 行,越过 400 行硬线)。
|
||||
//! 拆法与本仓既有 `-p1/-p2` 一致:**纯搬移**,一行不改语义;调用方路径不变
|
||||
//! (`plan.rs` 仍然 `pub(super) use` 这三个函数)。
|
||||
//!
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Mutex;
|
||||
@@ -12,7 +11,7 @@ use unlucky_core::addon::online::DependencyPlan;
|
||||
|
||||
use super::super::super::env::HostEnv;
|
||||
|
||||
/// 「上一次装入留下的计划」表(M191):`只重试这些` 的输入。
|
||||
/// 「上一次装入留下的计划」表的一行(M191):`只重试这些` 的输入。
|
||||
///
|
||||
/// ## 为什么要一张静态表(说清楚这笔债)
|
||||
///
|
||||
@@ -23,23 +22,45 @@ use super::super::super::env::HostEnv;
|
||||
///
|
||||
/// 键里带 `instance`:同一台机器上往两个实例装过东西,重试只该重试这个实例那一次的计划。
|
||||
/// 成功之后由 [`forget`] 清掉——「全成功」时不该留下任何可重试的东西。
|
||||
struct Remembered {
|
||||
///
|
||||
/// ## 为什么连**原始根项目 id** 一起记(r2 复审 P2-2)
|
||||
///
|
||||
/// 「这一条是不是根项目自己失败」不能靠 `plan.request.roots.first()` 判:重试那一轮的 plan 来自
|
||||
/// `DependencyPlan::retry_request()`,它的 `roots` 就是**上一轮的失败名单**——第一个是前置,
|
||||
/// 拿它跟根的显示名一比,就会把 `Sodium` 贴到一个前置头上。所以判据只能是「这个 id 是不是
|
||||
/// 当初那次装入的根 id」,那个 id 得跟计划一起记下来。
|
||||
struct Entry {
|
||||
host: PathBuf,
|
||||
instance: String,
|
||||
/// 落盘目录(重试要按它重新扫 `present`,且**不许**换目录)
|
||||
dir: PathBuf,
|
||||
/// 那次装入的显示名(重试时面板头与失败名单的可读名都沿用它,不再退回项目 id)
|
||||
/// 当初那次装入的根项目 id(`ContentInstallRequest.project`,判「是不是根」的唯一依据)
|
||||
root: String,
|
||||
/// 当初那次装入的显示名(只配给 `root` 用)
|
||||
title: String,
|
||||
plan: DependencyPlan,
|
||||
}
|
||||
|
||||
static REMEMBERED: Mutex<Vec<Remembered>> = Mutex::new(Vec::new());
|
||||
static REMEMBERED: Mutex<Vec<Entry>> = Mutex::new(Vec::new());
|
||||
|
||||
/// 取回的那一份记录(拆成结构体而不是四元组:调用点读得懂哪一个是哪个)。
|
||||
pub(in crate::host::addon) struct Remembered {
|
||||
/// 落盘目录
|
||||
pub dir: PathBuf,
|
||||
/// 当初那次装入的根项目 id
|
||||
pub root: String,
|
||||
/// 当初那次装入的显示名
|
||||
pub title: String,
|
||||
/// 上一轮的依赖计划(`retry_request()` 的来源)
|
||||
pub plan: DependencyPlan,
|
||||
}
|
||||
|
||||
/// 记下这一轮的计划(**覆盖**同一个 host + 实例的上一份)。
|
||||
pub(in crate::host::addon) fn remember(
|
||||
env: &HostEnv,
|
||||
instance: &str,
|
||||
dir: &Path,
|
||||
root: &str,
|
||||
title: &str,
|
||||
plan: &DependencyPlan,
|
||||
) {
|
||||
@@ -48,10 +69,11 @@ pub(in crate::host::addon) fn remember(
|
||||
.lock()
|
||||
.unwrap_or_else(|poison| poison.into_inner());
|
||||
table.retain(|item| !(item.host == host && item.instance == instance));
|
||||
table.push(Remembered {
|
||||
table.push(Entry {
|
||||
host,
|
||||
instance: instance.to_string(),
|
||||
dir: dir.to_path_buf(),
|
||||
root: root.to_string(),
|
||||
title: title.to_string(),
|
||||
plan: plan.clone(),
|
||||
});
|
||||
@@ -66,11 +88,8 @@ pub(in crate::host::addon) fn forget(env: &HostEnv, instance: &str) {
|
||||
table.retain(|item| !(item.host == host && item.instance == instance));
|
||||
}
|
||||
|
||||
/// 这个实例上一次失败的计划(+ 当时的落盘目录 + 显示名);`None` = 没有可重试的记录。
|
||||
pub(in crate::host::addon) fn remembered(
|
||||
env: &HostEnv,
|
||||
instance: &str,
|
||||
) -> Option<(PathBuf, String, DependencyPlan)> {
|
||||
/// 这个实例上一次失败的计划(+ 落盘目录 + 原始根 id 与显示名);`None` = 没有可重试的记录。
|
||||
pub(in crate::host::addon) fn remembered(env: &HostEnv, instance: &str) -> Option<Remembered> {
|
||||
let host = env.layout.home().to_path_buf();
|
||||
let table = REMEMBERED
|
||||
.lock()
|
||||
@@ -78,5 +97,10 @@ pub(in crate::host::addon) fn remembered(
|
||||
table
|
||||
.iter()
|
||||
.find(|item| item.host == host && item.instance == instance)
|
||||
.map(|item| (item.dir.clone(), item.title.clone(), item.plan.clone()))
|
||||
.map(|item| Remembered {
|
||||
dir: item.dir.clone(),
|
||||
root: item.root.clone(),
|
||||
title: item.title.clone(),
|
||||
plan: item.plan.clone(),
|
||||
})
|
||||
}
|
||||
@@ -5,7 +5,8 @@
|
||||
use super::{failure, node, plan};
|
||||
use crate::dto::{InstallFailureKind, InstallFailureView};
|
||||
use crate::host::addon::plan::{
|
||||
failure_of_plan, forget, remember, remembered, retry_items, retry_plan, retry_request,
|
||||
failure_of_plan, forget, names_for_incompatible, remember, remembered, retry_items, retry_plan,
|
||||
retry_request, Root,
|
||||
};
|
||||
use unlucky_core::addon::online::{DependencyPlan, FailureKind, OnlineErrorKind, PresentFile};
|
||||
|
||||
@@ -36,7 +37,14 @@ fn plan_with_two_tier_failures() -> DependencyPlan {
|
||||
/// 抓得住:把 `Incompatible` 并进 `Unreachable`(两档合成一档)⇒ 这条立刻红。
|
||||
#[test]
|
||||
fn the_retry_list_keeps_the_two_tiers_apart() {
|
||||
let items = retry_items(&plan_with_two_tier_failures(), "Sodium");
|
||||
let items = retry_items(
|
||||
&plan_with_two_tier_failures(),
|
||||
Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
},
|
||||
&Vec::new(),
|
||||
);
|
||||
assert_eq!(items.len(), 2);
|
||||
assert_eq!(
|
||||
items
|
||||
@@ -63,7 +71,15 @@ fn a_clean_plan_offers_nothing_to_retry() {
|
||||
vec![node("AANobbMI", "sodium.jar", 0, &[])],
|
||||
Vec::new(),
|
||||
);
|
||||
assert!(retry_items(&clean, "Sodium").is_empty());
|
||||
assert!(retry_items(
|
||||
&clean,
|
||||
Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
},
|
||||
&Vec::new(),
|
||||
)
|
||||
.is_empty());
|
||||
assert!(failure_of_plan(&clean, 1).is_none());
|
||||
}
|
||||
|
||||
@@ -116,17 +132,18 @@ fn the_last_failed_plan_is_remembered_per_instance() {
|
||||
|
||||
let plan = plan_with_two_tier_failures();
|
||||
let dir = root.join(".minecraft/mods");
|
||||
remember(&env, "survival", &dir, "Sodium", &plan);
|
||||
let (saved_dir, saved_title, saved) = remembered(&env, "survival").expect("记住了这一次");
|
||||
assert_eq!(saved_dir, dir);
|
||||
// 显示名跟着计划一起记住:重试时面板头与失败名单的可读名都沿用它(不再退回项目 id)
|
||||
assert_eq!(saved_title, "Sodium");
|
||||
assert_eq!(saved.failures.len(), 2);
|
||||
remember(&env, "survival", &dir, "AANobbMI", "Sodium", &plan);
|
||||
let saved = remembered(&env, "survival").expect("记住了这一次");
|
||||
assert_eq!(saved.dir, dir);
|
||||
// 原始根 id + 显示名跟着计划一起记住:重试时面板头用它,判「是不是根」也用它(r2 P2-2)
|
||||
assert_eq!(saved.root, "AANobbMI");
|
||||
assert_eq!(saved.title, "Sodium");
|
||||
assert_eq!(saved.plan.failures.len(), 2);
|
||||
// 别的实例不受影响(同一台机器上装过两个实例)
|
||||
assert!(remembered(&env, "modded").is_none());
|
||||
|
||||
// 重试请求从「记住的计划」里取:仍然只含失败项
|
||||
let request = retry_request(&saved, Vec::new());
|
||||
let request = retry_request(&saved.plan, Vec::new());
|
||||
assert_eq!(request.roots, vec!["P7dR8mSH", "1eAoo2KR"]);
|
||||
|
||||
forget(&env, "survival");
|
||||
@@ -140,7 +157,14 @@ fn the_last_failed_plan_is_remembered_per_instance() {
|
||||
#[test]
|
||||
fn the_dto_list_is_exactly_what_the_retry_request_asks_for() {
|
||||
let plan = plan_with_two_tier_failures();
|
||||
let items: Vec<InstallFailureView> = retry_items(&plan, "Sodium");
|
||||
let items: Vec<InstallFailureView> = retry_items(
|
||||
&plan,
|
||||
Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
},
|
||||
&Vec::new(),
|
||||
);
|
||||
let request = retry_request(&plan, Vec::new());
|
||||
assert_eq!(
|
||||
items
|
||||
@@ -198,7 +222,14 @@ fn the_retry_list_shows_a_readable_name_not_just_the_project_id() {
|
||||
// 根必须是请求里的那个键(`plan::request` 只放一个根)
|
||||
plan.request = unlucky_core::addon::online::ResolveRequest::single("AANobbMI");
|
||||
|
||||
let items = retry_items(&plan, "Sodium");
|
||||
let items = retry_items(
|
||||
&plan,
|
||||
Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
},
|
||||
&Vec::new(),
|
||||
);
|
||||
let lines: Vec<String> = items.iter().map(InstallFailureView::line).collect();
|
||||
assert_eq!(
|
||||
lines,
|
||||
@@ -213,3 +244,86 @@ fn the_retry_list_shows_a_readable_name_not_just_the_project_id() {
|
||||
assert!(!items[0].label.contains("下载"), "{}", items[0].label);
|
||||
assert!(!items[1].label.contains("失败"), "{}", items[1].label);
|
||||
}
|
||||
|
||||
/// **r2 P2-2 的守门**:重试那一轮,第一条失败项**不许**被贴成原根项目的名字。
|
||||
///
|
||||
/// 形状就是复审给的那个:重试轮的 plan 来自 `retry_request()`,它的 `roots` 是上一轮的失败名单
|
||||
/// (第一个是**前置**),而 `root` 记的是原始根 `AANobbMI / Sodium`。老实现拿 `roots.first()`
|
||||
/// 判「是不是根」⇒ 画出 `Sodium (P7dR8mSH)`;现在按 id 比 ⇒ 第一条是它自己的名字(或 id)。
|
||||
///
|
||||
/// 变异守门:把 `label_of` 的判据换回 `plan.request.roots.first()` ⇒ 这条红。
|
||||
#[test]
|
||||
fn the_retry_round_does_not_paste_the_root_title_onto_a_dependency() {
|
||||
let mut plan = plan_with_two_tier_failures();
|
||||
// 重试请求:roots = 上一轮的失败名单
|
||||
plan.request = unlucky_core::addon::online::ResolveRequest {
|
||||
roots: vec!["P7dR8mSH".to_string(), "1eAoo2KR".to_string()],
|
||||
..plan.request.clone()
|
||||
};
|
||||
// 重试轮里没有被安装的节点(上一轮装好的不会再进计划),于是标签只能来自 id
|
||||
let items = retry_items(
|
||||
&plan,
|
||||
Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
},
|
||||
&Vec::new(),
|
||||
);
|
||||
let lines: Vec<String> = items.iter().map(InstallFailureView::line).collect();
|
||||
assert_eq!(lines, vec!["P7dR8mSH".to_string(), "1eAoo2KR".to_string()]);
|
||||
assert!(
|
||||
!lines.iter().any(|line| line.contains("Sodium")),
|
||||
"重试轮把根的标题贴到前置上了:{lines:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// **r2 P2-1 续的守门**:不兼容档要显示项目名,不是 id。
|
||||
///
|
||||
/// 那一档的项目**确实存在**(站点只是没有匹配目标版本/加载器的条目),所以失败路径上值得
|
||||
/// 现取一次名字。变异守门:把这次获取去掉(`names` 传空)⇒ 第一条退回 `P7dR8mSH`,这条红。
|
||||
#[test]
|
||||
fn an_incompatible_item_shows_the_project_name_once_it_is_known() {
|
||||
let plan = plan_with_two_tier_failures();
|
||||
let root = Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
};
|
||||
// 生产路径:`names_for_incompatible` 用注入的查询取名字(这里模拟"站点有回应")
|
||||
let names = names_for_incompatible(&plan, |id| {
|
||||
(id == "P7dR8mSH").then(|| "Entity Culling".to_string())
|
||||
});
|
||||
assert_eq!(
|
||||
names,
|
||||
vec![("P7dR8mSH".to_string(), "Entity Culling".to_string())],
|
||||
"只给不兼容档取名字(404 那条不发请求)"
|
||||
);
|
||||
|
||||
let items = retry_items(&plan, root, &names);
|
||||
assert_eq!(items[0].line(), "Entity Culling (P7dR8mSH)");
|
||||
// 「取不到」那档不受影响:没有节点、没有名字 ⇒ 只画 id
|
||||
assert_eq!(items[1].line(), "1eAoo2KR");
|
||||
}
|
||||
|
||||
/// **降级**:取名字本身失败,不许把安装结果搞坏,也不许编名字——照旧画 id。
|
||||
///
|
||||
/// 变异守门:让 `project_title` 的错误往外冒(`?` / `expect`)⇒ 这条连编译都过不了,
|
||||
/// 或者(退回成「拿不到就 panic」)在这里红。
|
||||
#[test]
|
||||
fn a_failed_name_lookup_falls_back_to_the_id_without_breaking_anything() {
|
||||
let plan = plan_with_two_tier_failures();
|
||||
// 站点 404 / 没网 / 坏响应:`lookup` 全是 `None`
|
||||
let names = names_for_incompatible(&plan, |_| None);
|
||||
assert!(names.is_empty(), "{names:?}");
|
||||
let items = retry_items(
|
||||
&plan,
|
||||
Root {
|
||||
id: "AANobbMI",
|
||||
title: "Sodium",
|
||||
},
|
||||
&names,
|
||||
);
|
||||
assert_eq!(items[0].line(), "P7dR8mSH", "取不到名字就画 id,不编造");
|
||||
// 拿到空串(站点给了个空标题)同样不算名字
|
||||
let blank = names_for_incompatible(&plan, |_| Some(" ".to_string()));
|
||||
assert!(blank.is_empty(), "{blank:?}");
|
||||
}
|
||||
Reference in new issue
Block a user