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
@@ -46,11 +46,11 @@
|
||||
//! 面板在「进度未知」时本来就只画一行说明、不画条(`download/install.rs` 的既定口径)。
|
||||
//! 已登记为后续项:presenter 补 `futures-util` 之后换成字节级。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use unlucky_core::addon::online::{
|
||||
resolve, DependencyPlan, EngineArtifactFetcher, EngineTransport, Modrinth, PlannedNode,
|
||||
resolve, EngineArtifactFetcher, EngineTransport, Modrinth, PlannedNode,
|
||||
};
|
||||
use unlucky_core::download::blocking::block_on;
|
||||
use unlucky_core::game::install::instance::read_instance_config;
|
||||
@@ -63,6 +63,10 @@ use crate::dto::{
|
||||
|
||||
use super::super::env::HostEnv;
|
||||
use super::super::job::{HostTask, JobSender};
|
||||
mod collect;
|
||||
|
||||
pub(super) use collect::{collect, Placement, Ports};
|
||||
|
||||
use super::{downloads, plan};
|
||||
|
||||
/// 跑一次装入(同步;由 [`super::spawn_install`] 放进后台线程)。
|
||||
@@ -173,8 +177,15 @@ fn run(
|
||||
let client = Modrinth::new(&transport);
|
||||
block_on(resolve(&client, retry))
|
||||
});
|
||||
return collect(
|
||||
let fetcher = EngineArtifactFetcher::new(&core_engine);
|
||||
let title = |id: &str| plan::fetch_title(env, id);
|
||||
let ports = Ports {
|
||||
env,
|
||||
fetcher: &fetcher,
|
||||
title: &title,
|
||||
};
|
||||
return collect(
|
||||
&ports,
|
||||
request,
|
||||
dependency_plan,
|
||||
Placement {
|
||||
@@ -218,8 +229,15 @@ fn run(
|
||||
let client = Modrinth::new(&transport);
|
||||
block_on(resolve(&client, &resolve_request))
|
||||
};
|
||||
collect(
|
||||
let fetcher = EngineArtifactFetcher::new(&core_engine);
|
||||
let title = |id: &str| plan::fetch_title(env, id);
|
||||
let ports = Ports {
|
||||
env,
|
||||
fetcher: &fetcher,
|
||||
title: &title,
|
||||
};
|
||||
collect(
|
||||
&ports,
|
||||
request,
|
||||
dependency_plan,
|
||||
Placement {
|
||||
@@ -236,91 +254,6 @@ fn run(
|
||||
)
|
||||
}
|
||||
|
||||
/// 「装到哪 + 那次装入的根是谁」——总是一起出现,捆成一个参数(否则 `collect` 顶破 8 个)。
|
||||
struct Placement<'a> {
|
||||
/// 落盘目录(`mods/` / `resourcepacks/` / `shaderpacks/`)
|
||||
dir: &'a Path,
|
||||
/// 当初那次装入的根项目(id + 显示名);重试轮里由 `plan::remembered` 提供
|
||||
///
|
||||
/// **必须**带 id:重试那一轮 `plan.request.roots` 是上一轮的失败名单,不能拿它判「是不是根」
|
||||
/// (r2 复审 P2-2 就是这条:第一前置被贴上了根的标题)。
|
||||
root: plan::Root<'a>,
|
||||
}
|
||||
|
||||
/// 解析之后**两条入口共用**的尾巴:进度 → 逐个下载 → 收口。
|
||||
///
|
||||
/// 收口的两件事在同一个地方做,是因为它们必须同时发生:
|
||||
///
|
||||
/// | 结果 | 计划 | 可重试名单 |
|
||||
/// |---|---|---|
|
||||
/// | 有失败 | 记下(`plan::remember`,重试的输入) | 填(界面据此画「只重试这 N 个」) |
|
||||
/// | 全成功 | 忘掉(`plan::forget`) | 清空(界面**不渲染**那条入口) |
|
||||
fn collect(
|
||||
env: &HostEnv,
|
||||
request: &ContentInstallRequest,
|
||||
mut dependency_plan: DependencyPlan,
|
||||
placement: Placement<'_>,
|
||||
cancel: &AtomicBool,
|
||||
progress: &mut InstallProgressView,
|
||||
tx: &JobSender,
|
||||
epoch: u64,
|
||||
) -> Result<String, ContentFailure> {
|
||||
progress.stage = InstallStage::Downloading;
|
||||
progress.files_total = dependency_plan.nodes.len() as u64;
|
||||
progress.files_done = 0;
|
||||
progress.message = if dependency_plan.nodes.is_empty() {
|
||||
"没有要下载的文件".to_string()
|
||||
} else {
|
||||
format!(
|
||||
"{} 个文件(含 {} 个前置)",
|
||||
dependency_plan.nodes.len(),
|
||||
dependency_plan
|
||||
.nodes
|
||||
.iter()
|
||||
.filter(|node| !node.is_root())
|
||||
.count()
|
||||
)
|
||||
};
|
||||
let _ = tx.send(HostTask::Install {
|
||||
progress: Box::new(progress.clone()),
|
||||
epoch,
|
||||
});
|
||||
|
||||
let core_engine = super::super::engine::engine(env);
|
||||
let fetcher = EngineArtifactFetcher::new(&core_engine);
|
||||
// 下载阶段在 `downloads.rs`:可测(假下载器能驱动),失败进名单而不中断。
|
||||
let installed = downloads::download_stage(
|
||||
&mut dependency_plan,
|
||||
placement.dir,
|
||||
&fetcher,
|
||||
cancel,
|
||||
progress,
|
||||
tx,
|
||||
epoch,
|
||||
)?;
|
||||
|
||||
// 解析与下载两段的失败在这里一起收口:两档分成两句话(不合并成「下载失败」),
|
||||
// 并列出「可只重试这 N 个」。
|
||||
if let Some(failure) = plan::failure_of_plan(&dependency_plan, installed.len()) {
|
||||
// 「不兼容」那一档项目**存在**(站点只是没有匹配目标版本/加载器的条目),所以在这里
|
||||
// 现取一次名字——只在失败路径上发、一个项目一次 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.root.id,
|
||||
placement.root.title,
|
||||
&dependency_plan,
|
||||
);
|
||||
return Err(failure);
|
||||
}
|
||||
progress.retry.clear();
|
||||
plan::forget(env, &request.instance);
|
||||
Ok(plan::summary(&dependency_plan, &installed))
|
||||
}
|
||||
|
||||
/// 没有可重试的记录时那句话(**不**退化成「整单重装」)。
|
||||
const RETRY_LOST: &str =
|
||||
"没有可重试的记录:这次失败发生在别的实例上、或进程重启过。请重新装入那一次。";
|
||||
|
||||
Reference in new issue
Block a user