fix(java): 越界页 404 不许作废已取到的清单 + 三处文档/文案(M280 r2)
r2 复审 p2-1items / fix-then-merge,只做指定的四项。
## P2-1 满页收尾会多打一页 404,把前几页一起丢掉
`collect_releases` 原来靠「本页不满 PAGE_SIZE」收尾;某条 GA 线的条数恰好是 20 的倍数时
最后一页是满页 ⇒ 会多要一页 ⇒ Adoptium 对越界页回 **HTTP 404 + 空体**(实测,不是空数组)
⇒ 那一页的 Metadata 错误把**已经取到的前几页**一起作废,整条 `--version` 路径失败。
- `page > 0` 的 Metadata(404 / 空响应)视为「翻到尾巴」:返回已取到的全部 + `truncated=false`;
- `page == 0` 的 404 仍然报错(那才是「这个 major 没有 GA 清单」);
- **网络错误**(`JavaErrorKind::Network`)在任何一页都不被吞,照样冒泡。
用例(全离线,注入夹具):
- `a_404_on_the_page_after_a_full_page_ends_the_tail_cleanly`:page0=Ok(20 条)、
page1=Err(Metadata 404) ⇒ 断言 `Ok((20 条, false))` 且 `calls == 2`(把「今天上游越界页就是 404」钉住);
- `a_404_on_the_first_page_is_still_an_error`:page0 就 404 ⇒ 必须 `Err`;
- `a_network_error_on_a_later_page_is_not_swallowed`:page1 网络错误 ⇒ 必须 `Err`。
## P3-1 `install/plan.rs` 文档与事实矛盾
改成如实描述:搬移的直接原因(400 行硬线)+ **三处语义变化**(签名接 `&InstallRequest`、
函数体改走 `chain_fetch` 与 `install` 共用同一份判据、`request.priority` 现在参与选择顺序)。
## P3-2 `api.rs` 头注释
补一句:latest 那条走本模块,「指定版本」走子模块 `version`(`feature_releases` 翻页枚举 →
`assets/release_name`),两者共用同一套响应结构。
## P3-3 Oracle 拒绝文案多一个空格
`source` 自带连接词(`Adoptium 里` / `Oracle 镜像目录里`),模板改成 `({source}可用:…)`。
真机:`(Oracle 镜像目录里可用:21.0.2 / 21.0.1 / 21)`、`(Adoptium 里可用:… 等 26 个)`。
用例补三条断言钉住这两种拼法,并断言 Oracle 那句不许出现「镜像目录 里」。
门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-280):core --lib 873 / --test game 120 /
--test launch 91、cli --all-targets、fmt --check、clippy -D warnings 全部 EXIT=0。
This commit is contained in:
1 parent
1353c044c3
commit
aac0eef1b8
6 files changed
+119
-9
No files matched your search
@@ -1,6 +1,8 @@
|
||||
//! Adoptium(Eclipse Temurin)版本查询:`api.adoptium.net` → 一个可下载的 JDK 归档。
|
||||
//!
|
||||
//! 只用 v3 的 `assets/latest/{major}/hotspot` 这一条接口(D13:自动下载只装最新 GA):
|
||||
//! **最新 GA**(`assets/latest/{major}/hotspot`,D13 的自动下载口径)走本模块;
|
||||
//! 「指定版本」走子模块 [`version`](`assets/feature_releases` 翻页枚举 → `assets/release_name`
|
||||
//! 取那一份),两者共用同一套响应结构。本模块只描述 latest 这条:
|
||||
//!
|
||||
//! ```text
|
||||
//! GET https://api.adoptium.net/v3/assets/latest/21/hotspot?os=windows&arch=x64&image_type=jdk&vendor=eclipse
|
||||
|
||||
@@ -135,7 +135,9 @@ pub fn parse_release(
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 任何一页取不到(网络 / 解析失败)时直接把该错误抛出去。
|
||||
/// **第一页**取不到(网络 / 404 / 解析失败)时直接抛错——那才是「这个 major 真没有 GA 清单」。
|
||||
/// 第二页及以后取不到时**不抛**:上游对越界页回 HTTP 404 + 空体(2026-11-12 实测),
|
||||
/// 所以那是「翻到尾巴」的正常收尾(见下方注释)。
|
||||
pub async fn collect_releases<F, Fut>(mut fetch_page: F) -> Result<(Vec<String>, bool), JavaError>
|
||||
where
|
||||
F: FnMut(usize) -> Fut,
|
||||
@@ -143,7 +145,17 @@ where
|
||||
{
|
||||
let mut all: Vec<String> = Vec::new();
|
||||
for page in 0..MAX_PAGES {
|
||||
let names = fetch_page(page).await?;
|
||||
let names = match fetch_page(page).await {
|
||||
Ok(names) => names,
|
||||
// 越界页不是空数组,而是 404(`net::get_text` 把它归成 Metadata)。某条 GA 线的条数
|
||||
// 恰好是 20 / 40 / 60 时,最后一页是满页 ⇒ 这里会多要一页 ⇒ 那一页 404。
|
||||
// 这时**必须把已经取到的前几页交出去**(否则整条 `--version` 路径被一句与用户请求
|
||||
// 无关的 404 打死);第一页的 404 仍然报错,走下面的 `Err(error) => return`。
|
||||
Err(error) if page > 0 && error.kind() == JavaErrorKind::Metadata => {
|
||||
return Ok((all, false));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
};
|
||||
let short_page = names.len() < PAGE_SIZE;
|
||||
all.extend(names);
|
||||
if short_page {
|
||||
|
||||
@@ -205,3 +205,72 @@ async fn a_never_ending_catalog_stops_at_the_page_cap_and_says_so() {
|
||||
assert_eq!(releases.len(), MAX_PAGES * PAGE_SIZE);
|
||||
assert!(truncated, "到上限时必须标成截断,文案才能如实说");
|
||||
}
|
||||
|
||||
/// **越界页是 404 而不是空数组**(2026-11-12 实测):某条 GA 线的条数恰好是 20 的整数倍时,
|
||||
/// 最后一页是满页 ⇒ 会多要一页 ⇒ 那一页 404。这时必须把**已经取到的**交出去。
|
||||
///
|
||||
/// 变异证明:把 `collect_releases` 里 `page > 0` 的分支去掉(改回无条件 `?`)
|
||||
/// ⇒ 本用例红(`Err(Metadata)` 被抛出去,前 20 条连同一句无关的 404 一起作废)。
|
||||
#[tokio::test]
|
||||
async fn a_404_on_the_page_after_a_full_page_ends_the_tail_cleanly() {
|
||||
let calls = std::cell::Cell::new(0usize);
|
||||
let result = collect_releases(|page| {
|
||||
calls.set(calls.get() + 1);
|
||||
async move {
|
||||
match page {
|
||||
0 => Ok(page_zero()),
|
||||
// 上游越界页的真实形态:HTTP 404 + 空体,被 `net::get_text` 归成 Metadata。
|
||||
_ => Err(JavaError::new(
|
||||
JavaErrorKind::Metadata,
|
||||
"Adoptium GA 清单 不存在(HTTP 404):https://api.adoptium.net/…&page=1",
|
||||
)),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let (releases, truncated) = result.expect("第一页拿到了就是拿到了,越界页的 404 不该作废它");
|
||||
assert_eq!(releases.len(), PAGE_SIZE, "正好一页,条数要精确");
|
||||
assert_eq!(releases, page_zero());
|
||||
assert!(!truncated, "这是「翻到尾巴」,不是「到上限」");
|
||||
assert_eq!(
|
||||
calls.get(),
|
||||
2,
|
||||
"确实多要了第二页(这正是上游 404 的那一发)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **反例**:第一页就 404 ⇒ 仍然是 Err(那才是「这个 major 没有 GA 清单」),不许被吞。
|
||||
///
|
||||
/// 变异证明:把 `page > 0` 的判断改成 `page >= 0`(即第一页也吞)⇒ 本用例红。
|
||||
#[tokio::test]
|
||||
async fn a_404_on_the_first_page_is_still_an_error() {
|
||||
let result = collect_releases(|_page| async {
|
||||
Err::<Vec<String>, JavaError>(JavaError::new(
|
||||
JavaErrorKind::Metadata,
|
||||
"Adoptium 没有 99 的 GA 产物(HTTP 404)",
|
||||
))
|
||||
})
|
||||
.await;
|
||||
|
||||
let error = result.expect_err("第一页 404 = 这个 major 真没有清单,必须报错");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||
assert!(error.message().contains("404"), "{}", error.message());
|
||||
}
|
||||
|
||||
/// 网络错误(非 Metadata)在任何一页都不许被当成「翻到尾巴」——那是真坏了。
|
||||
#[tokio::test]
|
||||
async fn a_network_error_on_a_later_page_is_not_swallowed() {
|
||||
let result = collect_releases(|page| async move {
|
||||
match page {
|
||||
0 => Ok(page_zero()),
|
||||
_ => Err(JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
"连不上 Adoptium API:connection reset",
|
||||
)),
|
||||
}
|
||||
})
|
||||
.await;
|
||||
let error = result.expect_err("网络错误要冒泡");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Network);
|
||||
}
|
||||
@@ -237,14 +237,16 @@ pub fn version_pin_error(
|
||||
let vendor_id = vendor.id();
|
||||
match vendor {
|
||||
JdkVendor::Temurin | JdkVendor::Oracle => {
|
||||
// `source` 自己带连接词:拉丁文名与中文名需要的空格不一样,
|
||||
// 模板里再补一个空格就会拼出「Oracle 镜像目录 里可用」。
|
||||
let source = if vendor == JdkVendor::Temurin {
|
||||
"Adoptium"
|
||||
"Adoptium 里"
|
||||
} else {
|
||||
"Oracle 镜像目录"
|
||||
"Oracle 镜像目录里"
|
||||
};
|
||||
JavaError::new(
|
||||
JavaErrorKind::Metadata,
|
||||
format!("{vendor_id} 的 Java {major} 没有版本 {wanted}({source} 里可用:{listed})"),
|
||||
format!("{vendor_id} 的 Java {major} 没有版本 {wanted}({source}可用:{listed})"),
|
||||
)
|
||||
.with_hint("不给 --version 就装该大版本最新的 GA;指定版本要写上面列出的形态")
|
||||
}
|
||||
|
||||
@@ -170,6 +170,23 @@ fn pin_refusals_are_distinct_and_each_explains_itself() {
|
||||
errors[0].message().contains("21.0.13+9"),
|
||||
"可用版本要列出来"
|
||||
);
|
||||
// P3-3:连接词由 `source` 自带,模板不再补空格 —— 两种拼法都必须读得通,
|
||||
// 且 Oracle 那句不许出现「镜像目录 里」这种多一个空格的样子。
|
||||
assert!(
|
||||
errors[0].message().contains("Adoptium 里可用"),
|
||||
"{}",
|
||||
errors[0].message()
|
||||
);
|
||||
assert!(
|
||||
errors[1].message().contains("Oracle 镜像目录里可用"),
|
||||
"{}",
|
||||
errors[1].message()
|
||||
);
|
||||
assert!(
|
||||
!errors[1].message().contains("镜像目录 里"),
|
||||
"多一个空格:{}",
|
||||
errors[1].message()
|
||||
);
|
||||
assert!(errors[2].message().contains("只内置了 21.0.13.0.2"));
|
||||
assert!(errors[2]
|
||||
.hint()
|
||||
|
||||
@@ -1,8 +1,16 @@
|
||||
//! `install` 的**只读取**规划段:`--dry-run` 用它算出「会装哪一版、打哪个 URL」。
|
||||
//!
|
||||
//! 从 `install.rs` 拆出来只为守 400 行硬线(M280 r1 给回退链加了守卫后那边到 401 行);
|
||||
//! 纯搬移,语义未变。与 `install/one.rs`(真装的一步)、`install/chain.rs`(判据与骨架)
|
||||
//! 同一个拆法。
|
||||
//! 从 `install.rs` 拆出来的直接原因是守 400 行硬线(M280 r1 给回退链加了守卫后那边到 401 行),
|
||||
//! 但**不是纯搬移**,三处语义变了,别照着「搬移」去读它:
|
||||
//!
|
||||
//! 1. 签名从 `plan(engine, location, major)` 变成 `plan(engine, location, &InstallRequest)`
|
||||
//! —— 版本这一维(`request.version`)现在参与选择;
|
||||
//! 2. 函数体不再自己写 `for` + `continue`,改成走 [`super::chain::chain_fetch`],
|
||||
//! **与 `install` 共用同一份判据**(取版本 + 「版本必须对得上」的兜底闸);
|
||||
//! 3. `request.priority`(设置里的 `java.vendorPriority`)现在参与选择顺序;旧实现用
|
||||
//! `vendor::order(&[], domestic)`,等于忽略这条设置。
|
||||
//!
|
||||
//! 与 `install/one.rs`(真装的一步)、`install/chain.rs`(判据与骨架)同一个拆法。
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
||||
Reference in new issue
Block a user