test(addon): 给「失败路径取名字」的生产接线与降级补守卫 (M191 r3)
复审 r3 p2:那两处都只靠消费层用例撑着,生产接线上删掉/改坏也全绿。补两条能真红的守卫。 **接线层**:`install.rs` 里那次 `names_for_incompatible(..)` 原来不可驱动。现在下载器与 取名字器从 `Ports` 参数进来(生产 = 引擎 + `plan::fetch_title`,用例 = 假实现), 于是用例能驱动**生产那个 `collect`**:一份「一个文件下成功 + 一个不兼容前置 + 一条 404」的 计划跑完,断言不兼容那条拿到了名字、且**只**给不兼容档问过一次。 变异(把那一行短路成空名单)⇒ `the_failed_install_path_really_fetches_names_for_the_incompatible_items` 红:`left: "P7dR8mSH"` / `right: "Entity Culling (P7dR8mSH)"`。 **降级层**:把「一次查询 → 有名字/没名字」的折叠抽成 `title_from(query, id)` (`fetch_title` 只剩「建客户端 + 调它」)。参数化之后三种「没名字」都能离线钉住: 查询失败 / 空标题 / 全空白 ⇒ `None`,不 panic。 变异 ①`ok` 换 `expect` ⇒ `a_failed_or_blank_lookup_yields_no_name` 红 (panic:`带断网`);②去掉空串过滤 ⇒ 同一条红(空白档)。 **顺手**:`fetch_title` 真发一次 HTTP 的那一跳没进用例(本轮实测它确实取到了真数据 `P7dR8mSH → "Fabric API"`,但把网络调用放进 `cargo test` 会变脆,所以只把可坏的那一步——折叠—— 参数化测掉,报告里如实写「HTTP 这一跳仍无用例」)。 **行数**:`install.rs` 加到 415、`tests/retry.rs` 到 436,都越过 400 硬线 ⇒ 两步纯搬移: `collect` + `Ports` + `Placement` → `install/collect.rs`(117 行,`install.rs` 321); r3 用例 → `tests/retry/wire.rs`(115 行,父文件 332)。
This commit is contained in:
1 parent
26e7b7333e
commit
476bf92f97
5 files changed
+324
-94
No files matched your search
@@ -9,7 +9,8 @@
|
||||
//! **不许**拿 `DependencyFailure::message` 顶:那是 core 的中文散文,画到界面上就是 i18n R2 的违规。
|
||||
|
||||
use unlucky_core::addon::online::{
|
||||
DependencyFailure, DependencyPlan, EngineTransport, FailureKind, Modrinth,
|
||||
DependencyFailure, DependencyPlan, EngineTransport, FailureKind, Modrinth, OnlineError,
|
||||
ProjectDetail,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
|
||||
@@ -97,15 +98,76 @@ pub(super) fn label_of(
|
||||
|
||||
/// 取一个项目的显示名(**只在失败路径上**发)。
|
||||
///
|
||||
/// 失败一律回 `None`:**拿名字失败不许影响安装结果**——这条是「只重试这些」的补充信息,
|
||||
/// 坏一次网络不该把「装了什么、缺了什么」这整件事变成一条错误。
|
||||
/// 一次 GET,折成「有名字 / 没名字」的那一步在 [`title_from`](那里才是能坏的地方,因此那一步
|
||||
/// 单独可测;本函数只有「怎么发这一枪」)。
|
||||
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 })
|
||||
title_from(|id| block_on(async { client.project(id).await }), id)
|
||||
}
|
||||
|
||||
/// 把「一次项目查询」折成「有名字 / 没名字」——**降级就在这一层**(r3 复审 p2 要的守卫)。
|
||||
///
|
||||
/// 三种「没名字」都算 `None`,一个都不许往外冒:
|
||||
///
|
||||
/// | 情况 | 结果 |
|
||||
/// |---|---|
|
||||
/// | 查询失败(没网 / 404 / 坏响应) | `None`(**不是 panic,也不是错误**) |
|
||||
/// | 站点给了空标题 | `None` |
|
||||
/// | 站点给了全空白标题 | `None` |
|
||||
///
|
||||
/// 为什么单独抽出来:这条链路上唯一会「坏」的就是这一句折叠。抽成参数化的纯函数之后,
|
||||
/// 「查询失败仍退回 id」与「空白不算名字」都能在没有网络的用例里钉住(变异:`ok` 换 `expect`
|
||||
/// ⇒ 红;去掉空串过滤 ⇒ 红)。
|
||||
fn title_from(
|
||||
query: impl FnOnce(&str) -> Result<ProjectDetail, OnlineError>,
|
||||
id: &str,
|
||||
) -> Option<String> {
|
||||
query(id)
|
||||
.ok()
|
||||
.map(|project| project.title)
|
||||
.filter(|title| !title.trim().is_empty())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn detail(title: &str) -> ProjectDetail {
|
||||
ProjectDetail {
|
||||
id: "P7dR8mSH".to_string(),
|
||||
title: title.to_string(),
|
||||
..ProjectDetail::default()
|
||||
}
|
||||
}
|
||||
|
||||
/// **降级层**:查询失败 / 空标题 / 全空白 ⇒ 一律 `None`(界面退回只画 id),不 panic。
|
||||
///
|
||||
/// 抓得住:把 `.ok()` 换成 `expect`(查询失败即 panic)⇒ 第一条红;
|
||||
/// 去掉 `.filter(..)`(空白也算名字)⇒ 后两条红。
|
||||
#[test]
|
||||
fn a_failed_or_blank_lookup_yields_no_name() {
|
||||
// ① 查询失败(没网 / 404 / 坏响应)
|
||||
assert!(title_from(|_| Err(OnlineError::offline("断网")), "P7dR8mSH").is_none());
|
||||
// ② 站点给了空标题
|
||||
assert!(title_from(|_| Ok(detail("")), "P7dR8mSH").is_none());
|
||||
// ③ 站点给了全空白标题
|
||||
assert!(title_from(|_| Ok(detail(" ")), "P7dR8mSH").is_none());
|
||||
// ④ 正常情况:原样给出
|
||||
assert_eq!(
|
||||
title_from(|_| Ok(detail("Entity Culling")), "P7dR8mSH"),
|
||||
Some("Entity Culling".to_string())
|
||||
);
|
||||
// 传进去的 id 就是查询用的那个(不要拿别的键去问)
|
||||
let mut asked = String::new();
|
||||
let _ = title_from(
|
||||
|id| {
|
||||
asked.push_str(id);
|
||||
Ok(detail("X"))
|
||||
},
|
||||
"P7dR8mSH",
|
||||
);
|
||||
assert_eq!(asked, "P7dR8mSH");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user