fix(m162-r3): 金表条数与内容同源(堵住「少挂一段仍绿」的假绿)+ 补两条覆盖缺口
P2-1(r3 抓到的假绿):`golden::len()` 从「各段 len() 相加」改成 **`rows().count()`**。
r3 实测的变异是:删掉 `rows()` 里的 `chain(tail2::ROWS)` 后 `split_keeps_every_translation`
仍绿——因为条数来自一处手工聚合(仍含 tail2 ⇒ 459 == `Key::ALL.len()`)而内容只遍历
`rows()`(少了 213 行)。现在条数与内容同源,漏挂一段当场对不上。
- 变异复现(删 chain):`rows().count()=246 len()=246 Key::ALL.len()=459` ⇒ 用例
**FAILED**(`left: 459 / right: 246`,「key 条数与拆分前不一致」);
- 恢复后:`rows().count()=459 len()=459 Key::ALL.len()=459` ⇒ **ok**;
- 断言一字未改(没有放宽成「只比条数」或「只比前缀」)。
顺带三条:
1. `golden.rs` 的 `rows()` 文档补上 `tail2`(并写明「加段必须也 chain 上,条数与内容只能从
这一条序列导出」),与上一条同族。
2. `online/mod.rs` 模块表里 `[complete]` 的职责改成「按摘要认文件(`identity_of`)」,
与「补全循环已删、生产在 `import::complete`」一致;批量 POST 那段也点明循环位置。
3. 补两条 pack 用例走不到的覆盖(`import/complete.rs` 的 `#[cfg(test)] mod tests`):
- `an_entry_without_sha512_is_counted_not_guessed`:有 path、有 downloads、无 sha512 ⇒
`unfilled += 1` **且一次请求都不发**(mrpack 读者强制 sha512,这条是防御分支);
- `an_existing_download_list_is_never_extended`:已有直链时 `downloads` **逐字相等**
(长度 + 内容),不再用「包含」这种测不出重复追加的断言。
两条都验过能红:把「缺 sha512 不计数」与「总是追加直链」同时变异 ⇒ 两条 **FAILED**;
恢复后 **ok**。
登记项(r3 指出的 scan 注记)也顺手收了:`Completion::note` 在 `filled == 0` 时改说
「按 sha512 从 Modrinth 补全 sha1:一条也没补到(补不出 N 条)」,并且不再带
「其余条目照旧按『缺省保留』处置」(那是导入场景的尾缀,预检里没有这回事)。用例
`offline_with_nothing_filled_does_not_say_zero_items_were_filled` 与
`import_online::only_a_real_offline_stops_the_completion_round` 都钉住「真断网 + 一条没补到」
既不说「补全了 0 条」也不说「缺省保留」。
全仓行数(guards.yml::file-size 同口径):**0 个超限**。
This commit is contained in:
1 parent
ecd24f0608
commit
47ed270293
4 files changed
+216
-10
No files matched your search
@@ -48,12 +48,24 @@ impl Completion {
|
||||
if self.filled == 0 && !self.offline {
|
||||
return None;
|
||||
}
|
||||
let mut message = format!(
|
||||
"{lead}按 sha512 从 Modrinth 补全了 {} 条远端条目的 sha1(补不出 {} 条)",
|
||||
self.filled, self.unfilled
|
||||
);
|
||||
let mut message = if self.filled == 0 {
|
||||
// 一条也没补到(只可能是断网):说「补全了 0 条」读起来像成功。
|
||||
format!(
|
||||
"{lead}按 sha512 从 Modrinth 补全 sha1:一条也没补到(补不出 {} 条)",
|
||||
self.unfilled
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"{lead}按 sha512 从 Modrinth 补全了 {} 条远端条目的 sha1(补不出 {} 条)",
|
||||
self.filled, self.unfilled
|
||||
)
|
||||
};
|
||||
if self.offline {
|
||||
message.push_str(";补全因未联网提前停止,其余条目照旧按「缺省保留」处置");
|
||||
message.push_str(if self.filled == 0 {
|
||||
";补全因未联网提前停止"
|
||||
} else {
|
||||
";补全因未联网提前停止,其余条目照旧按「缺省保留」处置"
|
||||
});
|
||||
}
|
||||
Some(ImportIssue::new(keys::NOTE_REMOTE_COMPLETED, message))
|
||||
}
|
||||
@@ -133,3 +145,179 @@ pub(crate) async fn complete(request: &ImportRequest<'_>, plan: &mut ImportPlan)
|
||||
fn is_sha1(hex: &str) -> bool {
|
||||
hex.len() == 40 && hex.bytes().all(|byte| byte.is_ascii_hexdigit())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
//! 覆盖 **pack 用例走不到**的两条分支(r3 复审指出的缺口):
|
||||
//! `sha512` 缺失臂与「已有 downloads 不许追加第二个直链」。
|
||||
//!
|
||||
//! mrpack 的读者要求 `hashes.sha512` 必填,所以「有 path 但没有 sha512」在真实文件里进不来
|
||||
//! ——它是**防御分支**。直接构造 `ImportPlan` 是唯一能让它变红的办法(不是放宽断言)。
|
||||
|
||||
use std::path::Path;
|
||||
use std::sync::atomic::{AtomicU32, Ordering};
|
||||
|
||||
use super::{complete, Completion};
|
||||
use crate::addon::import::fetch::{
|
||||
FetchError, FetchFuture, FetchRequest, FetchedFile, ModrinthFileMeta, PackFetcher,
|
||||
};
|
||||
use crate::addon::import::plan::{ImportPlan, PlanRemote};
|
||||
use crate::addon::import::report::FileSource;
|
||||
use crate::addon::import::ImportRequest;
|
||||
use crate::addon::pack::{PackFile, PackManifest};
|
||||
use crate::download::blocking::block_on;
|
||||
|
||||
/// 只回答 `modrinth_file_by_sha512` 的假 fetcher(并记下被问了几次)。
|
||||
struct Lookup {
|
||||
meta: Result<ModrinthFileMeta, FetchError>,
|
||||
asked: AtomicU32,
|
||||
}
|
||||
|
||||
impl PackFetcher for Lookup {
|
||||
fn modrinth_file_by_sha512<'a>(
|
||||
&'a self,
|
||||
_sha512: &'a str,
|
||||
) -> FetchFuture<'a, Result<ModrinthFileMeta, FetchError>> {
|
||||
Box::pin(async move {
|
||||
self.asked.fetch_add(1, Ordering::SeqCst);
|
||||
match &self.meta {
|
||||
Ok(meta) => Ok(meta.clone()),
|
||||
Err(error) => Err(error.clone()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_file<'a>(
|
||||
&'a self,
|
||||
_request: FetchRequest<'a>,
|
||||
) -> FetchFuture<'a, Result<FetchedFile, FetchError>> {
|
||||
Box::pin(async move { Err(FetchError::unsupported("本用例不下载")) })
|
||||
}
|
||||
}
|
||||
|
||||
fn plan_with(file: PackFile) -> ImportPlan {
|
||||
ImportPlan {
|
||||
format: "modrinth".to_string(),
|
||||
manifest: PackManifest::default(),
|
||||
mc_version: "1.20.1".to_string(),
|
||||
loader: None,
|
||||
version_id: "1.20.1".to_string(),
|
||||
rejected: Vec::new(),
|
||||
notes: Vec::new(),
|
||||
overrides: Vec::new(),
|
||||
remote: vec![PlanRemote {
|
||||
file,
|
||||
path: Some("mods/no-sha1.jar".to_string()),
|
||||
source: FileSource::Modrinth,
|
||||
disabled: false,
|
||||
}],
|
||||
max_memory_mb: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn entry(sha512: Option<&str>, downloads: &[&str]) -> PackFile {
|
||||
let mut file = PackFile::with_path("mods/no-sha1.jar");
|
||||
file.downloads = downloads.iter().map(|url| (*url).to_string()).collect();
|
||||
if let Some(sha512) = sha512 {
|
||||
file.hashes.insert("sha512".to_string(), sha512.to_string());
|
||||
}
|
||||
file
|
||||
}
|
||||
|
||||
fn meta(sha1: &str, url: &str) -> ModrinthFileMeta {
|
||||
ModrinthFileMeta {
|
||||
sha1: Some(sha1.to_string()),
|
||||
size: Some(16),
|
||||
url: Some(url.to_string()),
|
||||
file_name: Some("no-sha1.jar".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_entry_without_sha512_is_counted_not_guessed() {
|
||||
// 防御分支:有 path、有 downloads、**没有 sha512** ⇒ 无从反查,如实计数、不发请求。
|
||||
let lookup = Lookup {
|
||||
meta: Ok(meta(&"a".repeat(40), "stub://from-source")),
|
||||
asked: AtomicU32::new(0),
|
||||
};
|
||||
let mut plan = plan_with(entry(None, &["stub://original"]));
|
||||
let request = ImportRequest::new(Path::new("pack.mrpack"), Path::new("instance"))
|
||||
.with_fetcher(&lookup);
|
||||
let summary = block_on(complete(&request, &mut plan));
|
||||
assert_eq!(summary.filled, 0);
|
||||
assert_eq!(summary.unfilled, 1, "缺 sha512 要计入「补不出」");
|
||||
assert_eq!(
|
||||
lookup.asked.load(Ordering::SeqCst),
|
||||
0,
|
||||
"没有摘要就不许发请求"
|
||||
);
|
||||
assert_eq!(plan.remote[0].file.sha1(), None, "补不出就是补不出,不许猜");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_existing_download_list_is_never_extended() {
|
||||
// 已有直链 ⇒ 站点给的直链**不许**再追加一条(列表必须逐字相同)。
|
||||
let lookup = Lookup {
|
||||
meta: Ok(meta(&"a".repeat(40), "stub://from-source")),
|
||||
asked: AtomicU32::new(0),
|
||||
};
|
||||
let mut plan = plan_with(entry(Some(&"c".repeat(128)), &["stub://original"]));
|
||||
let request = ImportRequest::new(Path::new("pack.mrpack"), Path::new("instance"))
|
||||
.with_fetcher(&lookup);
|
||||
let summary = block_on(complete(&request, &mut plan));
|
||||
assert_eq!(summary.filled, 1);
|
||||
assert_eq!(plan.remote[0].file.sha1(), Some("a".repeat(40).as_str()));
|
||||
assert_eq!(
|
||||
plan.remote[0].file.downloads,
|
||||
vec!["stub://original".to_string()],
|
||||
"已有直链时不许追加第二个(长度与内容都要逐字相同)"
|
||||
);
|
||||
assert_eq!(lookup.asked.load(Ordering::SeqCst), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_with_nothing_filled_does_not_say_zero_items_were_filled() {
|
||||
let note = Completion {
|
||||
filled: 0,
|
||||
unfilled: 3,
|
||||
offline: true,
|
||||
}
|
||||
.note("预检前")
|
||||
.expect("断网要留一条注记");
|
||||
assert!(
|
||||
note.message.contains("一条也没补到"),
|
||||
"断网且一条没补到时不许说「补全了 0 条」:{}",
|
||||
note.message
|
||||
);
|
||||
assert!(
|
||||
!note.message.contains("补全了 0 条"),
|
||||
"文案读起来像成功:{}",
|
||||
note.message
|
||||
);
|
||||
assert!(
|
||||
!note.message.contains("缺省保留"),
|
||||
"预检场景没有「缺省保留」这回事:{}",
|
||||
note.message
|
||||
);
|
||||
|
||||
// 真的补到过东西时,仍是原来那句(并带上「其余条目按缺省保留」)。
|
||||
let note = Completion {
|
||||
filled: 2,
|
||||
unfilled: 1,
|
||||
offline: true,
|
||||
}
|
||||
.note("")
|
||||
.expect("补到过就要留注记");
|
||||
assert!(note.message.contains("补全了 2 条"));
|
||||
assert!(note.message.contains("缺省保留"));
|
||||
|
||||
// 没补到、也没断网(例如所有摘要站点都不认得)⇒ 不占一行。
|
||||
assert!(Completion {
|
||||
filled: 0,
|
||||
unfilled: 2,
|
||||
offline: false
|
||||
}
|
||||
.note("")
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,8 @@
|
||||
//! `reqwest::Client`(与 `addon::import::fetch` 同一条纪律)。
|
||||
//!
|
||||
//! 没有 POST:Modrinth 的批量接口(`POST /v2/version_files`)需要 POST 请求体,而引擎的
|
||||
//! `DownloadRequest` 没有请求体字段。补全路径因此按**单摘要 GET** 逐条查(见 [`complete`]),
|
||||
//! `DownloadRequest` 没有请求体字段。补全路径因此按**单摘要 GET** 逐条查(循环在
|
||||
//! `crate::addon::import::complete`,本模块只用 [`identity_of`] 认文件),
|
||||
//! mrpack 里缺 `sha1` 的条目本来就是少数,不值得为它引入第二条 HTTP 路径。
|
||||
//!
|
||||
//! # 离线不是「空结果」
|
||||
@@ -43,7 +44,7 @@
|
||||
//! | [`transport`] | HTTP 出口 trait + 生产实现(走下载引擎)+ 错误分类 |
|
||||
//! | [`modrinth`] | 端点拼装与响应解析(纯逻辑,测试直接喂录制 JSON) |
|
||||
//! | [`download`] | 取下载地址之后那一步:引擎下载 + **sha1/sha512 复核** + 重试 + 不留半截文件 |
|
||||
//! | [`complete`] | 远端条目补全:缺 `sha1` 的 mrpack 条目按 `sha512` 反查 |
|
||||
//! | [`complete`] | 按摘要认文件([`identity_of`]):站点返回的版本里哪条才是我们要的那个 |
|
||||
|
||||
pub mod complete;
|
||||
pub mod download;
|
||||
|
||||
@@ -219,6 +219,16 @@ fn only_a_real_offline_stops_the_completion_round() {
|
||||
assert!(scan.is_ok(), "{:?}", scan.error);
|
||||
assert_eq!(scan.unknown_remote, 2, "没联网 ⇒ 两条都判不了");
|
||||
assert!(scan.conflicts.is_empty());
|
||||
// 断网且一条也没补到:注记不许说「补全了 0 条」(读起来像成功),也不许带「缺省保留」
|
||||
// ——那句是导入场景的尾缀,预检里没有这回事(r3 登记项)。
|
||||
let note = scan
|
||||
.notes
|
||||
.iter()
|
||||
.find(|note| note.message.contains("sha512"))
|
||||
.expect("断网要留一条补全注记");
|
||||
assert!(note.message.contains("一条也没补到"), "{}", note.message);
|
||||
assert!(!note.message.contains("补全了 0 条"), "{}", note.message);
|
||||
assert!(!note.message.contains("缺省保留"), "{}", note.message);
|
||||
}
|
||||
|
||||
/// 清单自带 `sha1` 时补全不许插手(也不许改写清单里的摘要)。
|
||||
|
||||
@@ -61,10 +61,12 @@ pub(super) const GOLDEN: &[(&str, &str, &str)] = &[
|
||||
("InstancesLaunchReset", "Reset to global", "恢复跟随全局"),
|
||||
];
|
||||
|
||||
/// 金表的**全量**行(三段:本文件 + [`tail`] + [`tail2`],顺序即 `Key::ALL`)。
|
||||
/// 金表的**全量**行(本文件 → [`tail`] → [`tail2`],顺序即 `Key::ALL`)。
|
||||
///
|
||||
/// 拆成两段只为守 400 行硬线(M157 加了 19 行启动参数文案后越线);对调用方是**一条**序列,
|
||||
/// 拆成三段只为守 400 行硬线(M157/M159 往金表追加文案后越线);对调用方是**一条**序列,
|
||||
/// `catalog/tests.rs` 的逐字节断言不用关心分段。
|
||||
///
|
||||
/// ⚠️ 加段时**必须**在这里也 chain 上:条数与内容都只能从这一条序列导出(见 [`len`])。
|
||||
pub(super) fn rows() -> impl Iterator<Item = &'static (&'static str, &'static str, &'static str)> {
|
||||
GOLDEN
|
||||
.iter()
|
||||
@@ -73,8 +75,13 @@ pub(super) fn rows() -> impl Iterator<Item = &'static (&'static str, &'static st
|
||||
}
|
||||
|
||||
/// 金表总行数(`Key::ALL` 的条数)。
|
||||
///
|
||||
/// **定义成 `rows().count()`,不是各段 `len()` 相加**:r3 复审实测过一条假绿——把
|
||||
/// [`rows`] 里的 `chain(tail2::ROWS)` 删掉,`catalog/tests.rs` 仍全绿,因为条数来自一处手工
|
||||
/// 聚合(仍有 tail2)而内容只遍历 `rows()`(少了 tail2),于是 213/459 行无人看守。
|
||||
/// 现在条数与内容同源:漏挂一段 ⇒ 条数当场对不上 ⇒ 用例必红。
|
||||
pub(super) fn len() -> usize {
|
||||
GOLDEN.len() + tail::ROWS.len() + tail2::ROWS.len()
|
||||
rows().count()
|
||||
}
|
||||
|
||||
mod tail;
|
||||
|
||||
Reference in new issue
Block a user