fix(java): 把「这一页不存在」与「这一页读不懂」分开,解析失败不再被当成翻到尾巴(M280 r3)
r3 复审 p2-1items:r2 的吞判据按 `JavaErrorKind` 判,而 `Metadata` 这个桶里除了
404/410 与 200+空体,还装着**解析失败**(`parse_release_list`)⇒ 第二页起上游回
「看得懂的状态码 + 读不懂的内容」时,清单被静默截断且标成 `Complete`,用户看到的是
「这个 major 没有版本 X(可用:…)」,而 X 其实在上游。
## 修法:语义信号代替错误种类(复审判据里没选字面方案,选了改动面最小的等价实现)
`fetch_page` 的返回类型变成 `Result<Option<Vec<String>>, JavaError>`,`None` 只由
**新的纯函数 `page_outcome(status, body)`** 产生:404/410 或 200+空体。判据是状态码 +
体形状,**不匹配任何错误文案**。
- `page > 0` 的 `None` ⇒ 翻到尾巴:交回已取到的全部,`truncated = false`(r2 已做对,保留);
- `page == 0` 的 `None` ⇒ 这个大版本真没有 GA 清单:仍然 `Err`(与 r2 之前同口径,
只是判据从「错误种类」换成「哪一页不存在」);
- **任何页的解析失败 / 非 2xx 非 404 / 网络错误 ⇒ 一律 `Err`**(不再有「读不懂 ⇒ 没有更多」);
- 错误落在最后一页也是 `Err`,不可能再报 `Complete`。
`api/version.rs` 里新增 `page_outcome`(纯)+ `fetch_release_page`(HTTP 那一跳,判定全交给前者),
`fetch_version` 的闭包改调 `fetch_release_page`。只动本 mission scope 内的两个文件,
没有改 `net.rs`,也没有给跨 crate 匹配的 `JavaErrorKind` 加变体(cli / app-iced 的 match 不变)。
## 用例(离线;每条都有能变红的变异)
- `a_missing_page_after_a_full_page_ends_the_tail_cleanly`(判据 1):page0=20 条、page1=None
⇒ `Ok((20, false))`,逐条相等、`calls == 2`;
- `a_missing_page_is_404_410_or_an_empty_body_and_nothing_else`:404/410/200+空白 ⇒ `None`;
正常 JSON ⇒ `Some(names)`;`<html>502</html>` ⇒ `Err(Metadata)`;500/403/429/502 ⇒ `Err(Network)`;
- `a_parse_failure_on_a_later_page_is_an_error_not_the_end_of_the_list`(判据 2):夹具用**真的**
`parse_release_list("<html>502</html>").unwrap_err()` ⇒ 必须 `Err`,且报的是解析失败本身;
`Err` 分支在签名上就带不出部分结果;
- `an_error_on_the_last_allowed_page_is_never_reported_as_complete`(判据 3):错误落在
`page == MAX_PAGES-1` ⇒ 必须 `Err`,不许 `Ok((_, false))`;
- `a_missing_first_page_is_still_an_error`:第一页就不存在 ⇒ `Err`;
- `a_network_error_on_a_later_page_is_not_swallowed`:网络错误照抛。
变异(做完都还原,`git status` 干净):R3-A 把 page>0 的 None 当失败 ⇒ 判据 1 那条红;
R3-B 把 Metadata 重新并进吞掉的分支 ⇒ 判据 2/3 两条红(panic 原文就是 `(…20 条…, false)`,
即复审实测的那个 bug);R3-C 把 `page_outcome` 的解析失败并成 `None` ⇒ 底座那条红。
## 顺带两条(复审 P3)
- P3-1:报告里「钉住上游越界页就是 404」那句改成正确表述(夹具手写,钉的是「收到『这一页
不存在』要把已取到的交出去」);`--lib` 数字按 tip 重数(`aac0eef` 上是 880,本轮 +3 ⇒ 883)。
- P3-2:删掉 `catalog/tests.rs` 里冗余的否定式断言(紧邻的正向断言已蕴含它),
保留两条正向断言(`Adoptium 里可用` / `Oracle 镜像目录里可用`)。
门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-280):core --lib 883 / --test game 120 /
--test launch 91、cli --all-targets、fmt --check、clippy -D warnings 全部 EXIT=0。
This commit is contained in:
1 parent
aac0eef1b8
commit
a0aeccf303
3 files changed
+230
-62
No files matched your search
@@ -125,36 +125,45 @@ pub fn parse_release(
|
||||
binary.into_release(name, release.version_data)
|
||||
}
|
||||
|
||||
/// 翻页把该大版本的 GA release 名全部取回来(按页拼,直到「本页不满一页」或到 [`MAX_PAGES`])。
|
||||
/// 翻页把该大版本的 GA release 名全部取回来(按页拼,直到「这一页不存在」/「本页不满一页」/ 到 [`MAX_PAGES`])。
|
||||
///
|
||||
/// `fetch_page(page)` 由调用方给:真跑时是一发 HTTP,单测里是夹具闭包——所以「翻几页 / 什么时候
|
||||
/// 停 / 到上限了没有」这段判断**完全离线可测**。
|
||||
/// `fetch_page(page)` 由调用方给:真跑时是一发 HTTP(见 [`fetch_release_page`]),单测里是夹具闭包
|
||||
/// ——所以「翻几页 / 什么时候停 / 到上限了没有」这段判断**完全离线可测**。
|
||||
///
|
||||
/// 返回 `Ok(None)` 的语义是**「上游明确说没有这一页」**(404/410 或 200+空体),
|
||||
/// 与「这一页读不懂 / 网络坏了」是两件事:
|
||||
///
|
||||
/// * `Ok(None)` 在**第一页** ⇒ 「这个大版本真没有 GA 清单」,仍然 `Err`(与 r2 之前同口径);
|
||||
/// * `Ok(None)` 在**第二页及以后** ⇒「翻到尾巴」,把已取到的交出去,`truncated = false`;
|
||||
/// * `Err(..)` ⇒ **一律照抛**(含第二页及以后)。读不懂的响应一旦被当成「没有更多」,
|
||||
/// 用户会看到「这个 major 没有版本 X」,而 X 其实在上游——一句可证伪的假话。
|
||||
///
|
||||
/// 返回值第二项 = 是否**因为到上限**而停(还有没看到的页)。错误文案要用它决定说
|
||||
/// 「上游就这些」还是「只查了前 N 条」。
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// **第一页**取不到(网络 / 404 / 解析失败)时直接抛错——那才是「这个 major 真没有 GA 清单」。
|
||||
/// 第二页及以后取不到时**不抛**:上游对越界页回 HTTP 404 + 空体(2026-11-12 实测),
|
||||
/// 所以那是「翻到尾巴」的正常收尾(见下方注释)。
|
||||
/// 任何一页**读不懂**(解析失败 / 非 2xx 非 404 / 网络)都照抛,不吞。
|
||||
pub async fn collect_releases<F, Fut>(mut fetch_page: F) -> Result<(Vec<String>, bool), JavaError>
|
||||
where
|
||||
F: FnMut(usize) -> Fut,
|
||||
Fut: std::future::Future<Output = Result<Vec<String>, JavaError>>,
|
||||
Fut: std::future::Future<Output = Result<Option<Vec<String>>, JavaError>>,
|
||||
{
|
||||
let mut all: Vec<String> = Vec::new();
|
||||
for page in 0..MAX_PAGES {
|
||||
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));
|
||||
let Some(names) = fetch_page(page).await? else {
|
||||
if page == 0 {
|
||||
// 第一页就不存在 = 这个大版本真没有 GA 清单(不是「翻到尾巴」)。
|
||||
// 与 r2 之前的行为一致:那一条仍然报错,只是判据从「错误的种类」换成了
|
||||
// 「哪一页不存在」。
|
||||
return Err(JavaError::new(
|
||||
JavaErrorKind::Metadata,
|
||||
"Adoptium 没有该大版本的 GA 清单(HTTP 404 / 空响应)",
|
||||
));
|
||||
}
|
||||
Err(error) => return Err(error),
|
||||
// 「这一页不存在」= 翻到尾巴。上游对越界页回 404(2026-11-12 实测),
|
||||
// 某条 GA 线的条数恰好是 20 / 40 / 60 时就会撞上这一发;此时已取到的前几页必须交出去。
|
||||
return Ok((all, false));
|
||||
};
|
||||
let short_page = names.len() < PAGE_SIZE;
|
||||
all.extend(names);
|
||||
@@ -165,6 +174,60 @@ where
|
||||
Ok((all, true))
|
||||
}
|
||||
|
||||
/// 一页响应的判定:`Ok(None)` = 「上游明确说没有这一页」,`Err` = 读不懂或网络坏。
|
||||
///
|
||||
/// **判据是语义的**(状态码 + 体的形状),不匹配任何错误文案:
|
||||
///
|
||||
/// | 上游情形 | 结果 |
|
||||
/// | --- | --- |
|
||||
/// | 404 / 410(越界页实测就是这个) | `Ok(None)` |
|
||||
/// | 200 + 空体 | `Ok(None)`(等价于「没有这一页」) |
|
||||
/// | 其它非 2xx(5xx / 403 / 429…) | `Err(Network)` |
|
||||
/// | 200 + 不是预期 JSON | `Err(Metadata)` —— **不许**被当成「没有更多」 |
|
||||
/// | 200 + 正常清单 | `Ok(Some(names))` |
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// 非 2xx 且不是 404/410 时是 [`JavaErrorKind::Network`];体读不懂时是
|
||||
/// [`JavaErrorKind::Metadata`]。
|
||||
pub fn page_outcome(status: u16, body: &str) -> Result<Option<Vec<String>>, JavaError> {
|
||||
if status == 404 || status == 410 {
|
||||
return Ok(None);
|
||||
}
|
||||
if !(200..300).contains(&status) {
|
||||
return Err(JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
format!("Adoptium GA 清单返回 HTTP {status}"),
|
||||
));
|
||||
}
|
||||
if body.trim().is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
parse_release_list(body).map(Some)
|
||||
}
|
||||
|
||||
/// 取一页 GA 清单(HTTP 那一跳)。判定全交给 [`page_outcome`],这里只负责把响应取回来。
|
||||
async fn fetch_release_page(
|
||||
client: &reqwest::Client,
|
||||
url: &str,
|
||||
) -> Result<Option<Vec<String>>, JavaError> {
|
||||
let response = client.get(url).send().await.map_err(|error| {
|
||||
JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
format!("取 Adoptium GA 清单失败:{error}({url})"),
|
||||
)
|
||||
.with_hint("检查网络/代理;已装的 JDK 仍可用 `ull java list` 看到")
|
||||
})?;
|
||||
let status = response.status().as_u16();
|
||||
let body = response.text().await.map_err(|error| {
|
||||
JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
format!("读 Adoptium GA 清单响应失败:{error}({url})"),
|
||||
)
|
||||
})?;
|
||||
page_outcome(status, &body)
|
||||
}
|
||||
|
||||
/// 在枚举出来的 release 名里挑用户要的那一版(**不取最新、不取第一条**)。
|
||||
#[must_use]
|
||||
pub fn pick_release<'a>(releases: &'a [String], wanted: &str) -> Option<&'a String> {
|
||||
@@ -184,10 +247,7 @@ pub async fn fetch_version(
|
||||
) -> Result<JdkRelease, JavaError> {
|
||||
let (releases, truncated) = collect_releases(|page| {
|
||||
let url = releases_url(major, target, page);
|
||||
async move {
|
||||
let body = net::get_text(client, &url, "Adoptium GA 清单").await?;
|
||||
parse_release_list(&body)
|
||||
}
|
||||
async move { fetch_release_page(client, &url).await }
|
||||
})
|
||||
.await?;
|
||||
let Some(name) = pick_release(&releases, wanted).cloned() else {
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
//! **纯搬移**,既有断言一字未改。
|
||||
|
||||
use super::{
|
||||
collect_releases, parse_release, parse_release_list, pick_release, release_url, releases_url,
|
||||
MAX_PAGES, PAGE_SIZE,
|
||||
collect_releases, page_outcome, parse_release, parse_release_list, pick_release, release_url,
|
||||
releases_url, MAX_PAGES, PAGE_SIZE,
|
||||
};
|
||||
use crate::game::java::error::{JavaError, JavaErrorKind};
|
||||
use crate::game::java::target::AdoptiumTarget;
|
||||
@@ -124,11 +124,12 @@ fn page_one() -> Vec<String> {
|
||||
}
|
||||
|
||||
/// 假的上游:按 page 返回夹具(`pages` 用完之后一律空页)。
|
||||
fn two_pages(page: usize) -> Result<Vec<String>, JavaError> {
|
||||
fn two_pages(page: usize) -> Result<Option<Vec<String>>, JavaError> {
|
||||
match page {
|
||||
0 => Ok(page_zero()),
|
||||
1 => Ok(page_one()),
|
||||
_ => Ok(Vec::new()),
|
||||
0 => Ok(Some(page_zero())),
|
||||
1 => Ok(Some(page_one())),
|
||||
// 越界页:上游说「没有这一页」(404/410 或 200+空体)
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,9 +195,11 @@ async fn a_never_ending_catalog_stops_at_the_page_cap_and_says_so() {
|
||||
calls.set(calls.get() + 1);
|
||||
async move {
|
||||
let _ = page;
|
||||
Ok((0..PAGE_SIZE)
|
||||
.map(|index| format!("jdk-17.0.{}+1", index + 1))
|
||||
.collect::<Vec<_>>())
|
||||
Ok(Some(
|
||||
(0..PAGE_SIZE)
|
||||
.map(|index| format!("jdk-17.0.{}+1", index + 1))
|
||||
.collect::<Vec<_>>(),
|
||||
))
|
||||
}
|
||||
})
|
||||
.await
|
||||
@@ -206,64 +209,157 @@ async fn a_never_ending_catalog_stops_at_the_page_cap_and_says_so() {
|
||||
assert!(truncated, "到上限时必须标成截断,文案才能如实说");
|
||||
}
|
||||
|
||||
/// **越界页是 404 而不是空数组**(2026-11-12 实测):某条 GA 线的条数恰好是 20 的整数倍时,
|
||||
/// 最后一页是满页 ⇒ 会多要一页 ⇒ 那一页 404。这时必须把**已经取到的**交出去。
|
||||
/// **判据 1(别退回去)**:某条 GA 线的条数恰好是 20 的整数倍时,最后一页是满页 ⇒ 会多要一页
|
||||
/// ⇒ 上游对那一页说「没有这一页」(404/410 或 200+空体,见 [`page_outcome`])⇒
|
||||
/// 必须把**已经取到的**交出去,且 `truncated = false`。
|
||||
///
|
||||
/// 变异证明:把 `collect_releases` 里 `page > 0` 的分支去掉(改回无条件 `?`)
|
||||
/// ⇒ 本用例红(`Err(Metadata)` 被抛出去,前 20 条连同一句无关的 404 一起作废)。
|
||||
/// 它真正钉住的是「`collect_releases` 收到『这一页不存在』时要把已取到的交出去」;
|
||||
/// **不是**「上游越界页就是 404」——后者只能靠 curl 实测,夹具手写不出来
|
||||
/// (r3 复审 P3-1 的更正)。
|
||||
///
|
||||
/// 变异证明:把 `collect_releases` 里 `let Some(names) = … else { return Ok((all, false)) }`
|
||||
/// 的 `None` 分支改成 `return Err(…)`(即「没有这一页」也当失败)⇒ 本用例红。
|
||||
#[tokio::test]
|
||||
async fn a_404_on_the_page_after_a_full_page_ends_the_tail_cleanly() {
|
||||
async fn a_missing_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",
|
||||
)),
|
||||
0 => Ok(Some(page_zero())),
|
||||
// 越界页的语义信号:上游明确说「没有这一页」。
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let (releases, truncated) = result.expect("第一页拿到了就是拿到了,越界页的 404 不该作废它");
|
||||
let (releases, truncated) = result.expect("第一页拿到了就是拿到了,越界页不该作废它");
|
||||
assert_eq!(releases.len(), PAGE_SIZE, "正好一页,条数要精确");
|
||||
assert_eq!(releases, page_zero());
|
||||
assert_eq!(releases, page_zero(), "逐条相等(不是「大概 20 个」)");
|
||||
assert!(!truncated, "这是「翻到尾巴」,不是「到上限」");
|
||||
assert_eq!(
|
||||
calls.get(),
|
||||
2,
|
||||
"确实多要了第二页(这正是上游 404 的那一发)"
|
||||
"确实多要了第二页(这正是上游说 404 的那一发)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **反例**:第一页就 404 ⇒ 仍然是 Err(那才是「这个 major 没有 GA 清单」),不许被吞。
|
||||
/// **判据 1 的语义底座**:`page_outcome` 把「这一页不存在」与「这一页读不懂」分开,
|
||||
/// 判据是状态码 + 体的形状,**不匹配任何错误文案**。
|
||||
///
|
||||
/// 变异证明:把 `page > 0` 的判断改成 `page >= 0`(即第一页也吞)⇒ 本用例红。
|
||||
/// 变异证明:把 404/410 那一支删掉 ⇒ 第一条红;把空体那一支删掉 ⇒ 第三条红;
|
||||
/// 把解析失败并进 `Ok(None)` ⇒ 最后一条红。
|
||||
#[test]
|
||||
fn a_missing_page_is_404_410_or_an_empty_body_and_nothing_else() {
|
||||
assert_eq!(page_outcome(404, "").expect("404 = 没有这一页"), None);
|
||||
assert_eq!(
|
||||
page_outcome(410, "<html>gone</html>").expect("410 = 没有这一页"),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
page_outcome(200, " \n\t ").expect("200 + 空体 = 没有这一页"),
|
||||
None
|
||||
);
|
||||
// 正常清单:真的解析出名字(顺序即上游给的顺序)。
|
||||
assert_eq!(
|
||||
page_outcome(200, &list_body()).expect("应解析"),
|
||||
Some(vec![
|
||||
"jdk-21.0.13+9".to_string(),
|
||||
"jdk-21.0.12+8".to_string()
|
||||
])
|
||||
);
|
||||
// 读不懂 = 错误,**不许**被当成「没有这一页」。
|
||||
let parse_failure = page_outcome(200, "<html>502 Bad Gateway</html>").expect_err("HTML 应被拒");
|
||||
assert_eq!(parse_failure.kind(), JavaErrorKind::Metadata);
|
||||
// 其它非 2xx 是网络侧,同样不许被当成「没有这一页」。
|
||||
for status in [500u16, 403, 429, 502] {
|
||||
let error = page_outcome(status, "").expect_err("非 2xx 应报错");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Network, "status={status}");
|
||||
}
|
||||
}
|
||||
|
||||
/// **判据 2(r3 的 p2 本体)**:第二页**读不懂**(解析失败)⇒ 必须 `Err`,绝不许 `Ok`。
|
||||
///
|
||||
/// 夹具用的是**真的** `parse_release_list` 产出的错误(不是手写字符串),所以它跟着解析实现走。
|
||||
/// 返回类型是 `Result<(Vec<String>, bool), JavaError>`,`Err` 分支在类型上就**不可能**带出
|
||||
/// 部分结果——「不能既 Err 又把 all 交出去」由签名保证,用例断言的是「不许变成 Ok」。
|
||||
///
|
||||
/// 变异证明:在 `collect_releases` 里把 `Metadata` 错误重新并进「翻到尾巴」的分支
|
||||
/// (`Err(e) if e.kind() == Metadata => return Ok((all, false))`)⇒ 本用例红。
|
||||
#[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)",
|
||||
))
|
||||
async fn a_parse_failure_on_a_later_page_is_an_error_not_the_end_of_the_list() {
|
||||
let parse_failure = parse_release_list("<html>502 Bad Gateway</html>")
|
||||
.expect_err("HTML 不是 GA 清单,必须报错");
|
||||
assert_eq!(
|
||||
parse_failure.kind(),
|
||||
JavaErrorKind::Metadata,
|
||||
"先确认这一类错误确实落在 Metadata 桶里(这正是 r3 那条 p2 的入口)"
|
||||
);
|
||||
|
||||
let result = collect_releases(|page| {
|
||||
let error = parse_failure.clone();
|
||||
async move {
|
||||
match page {
|
||||
0 => Ok(Some(page_zero())),
|
||||
_ => Err(error),
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let error = result.expect_err("第一页 404 = 这个 major 真没有清单,必须报错");
|
||||
let error = result.expect_err("第二页读不懂 ⇒ 必须报错,不许静默截断");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||
assert!(error.message().contains("404"), "{}", error.message());
|
||||
assert!(
|
||||
error.message().contains("不是预期 JSON"),
|
||||
"报的必须是解析失败本身,不是一句无关的 404:{}",
|
||||
error.message()
|
||||
);
|
||||
}
|
||||
|
||||
/// 网络错误(非 Metadata)在任何一页都不许被当成「翻到尾巴」——那是真坏了。
|
||||
/// **判据 3**:错误落在**最后一页**(`page == MAX_PAGES - 1`)时同样不许报「清单完整」——
|
||||
/// 那是被错误截断,不是「上游就这些」。
|
||||
///
|
||||
/// 变异证明:同判据 2 的那个变异(把 `Metadata` 并进吞掉的分支)⇒ 本用例红
|
||||
/// (会拿到 `Ok((…, false))`,即文案会走 `Complete`)。
|
||||
#[tokio::test]
|
||||
async fn an_error_on_the_last_allowed_page_is_never_reported_as_complete() {
|
||||
let parse_failure =
|
||||
parse_release_list("<html>502</html>").expect_err("HTML 不是 GA 清单,必须报错");
|
||||
let full_page: Vec<String> = (0..PAGE_SIZE)
|
||||
.map(|index| format!("jdk-17.0.{}+1", index + 1))
|
||||
.collect();
|
||||
|
||||
let result = collect_releases(|page| {
|
||||
let error = parse_failure.clone();
|
||||
let full_page = full_page.clone();
|
||||
async move {
|
||||
if page + 1 == MAX_PAGES {
|
||||
Err(error)
|
||||
} else {
|
||||
Ok(Some(full_page))
|
||||
}
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let error = result.expect_err("最后一页读不懂也必须报错,不许报「完整」");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||
assert!(
|
||||
error.message().contains("不是预期 JSON"),
|
||||
"{}",
|
||||
error.message()
|
||||
);
|
||||
}
|
||||
|
||||
/// 网络错误在任何一页都不许被当成「翻到尾巴」——那是真坏了。
|
||||
///
|
||||
/// 变异证明:把 `fetch_page` 的错误改成一律 `Ok(None)` ⇒ 本用例红。
|
||||
#[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()),
|
||||
0 => Ok(Some(page_zero())),
|
||||
_ => Err(JavaError::new(
|
||||
JavaErrorKind::Network,
|
||||
"连不上 Adoptium API:connection reset",
|
||||
@@ -274,3 +370,20 @@ async fn a_network_error_on_a_later_page_is_not_swallowed() {
|
||||
let error = result.expect_err("网络错误要冒泡");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Network);
|
||||
}
|
||||
|
||||
/// **第一页就不存在**:那是「这个大版本真没有 GA 清单」,仍然报错(与 r2 之前同口径),
|
||||
/// 不许被当成「翻到尾巴」而返回空清单——否则用户会看到
|
||||
/// 「没有版本 X(上游一个都没列出来)」这种什么都说明不了的文案。
|
||||
///
|
||||
/// 变异证明:把 `collect_releases` 里 `if page == 0` 那一支删掉 ⇒ 本用例红。
|
||||
#[tokio::test]
|
||||
async fn a_missing_first_page_is_still_an_error() {
|
||||
let result = collect_releases(|_page| async { Ok::<_, JavaError>(None) }).await;
|
||||
let error = result.expect_err("第一页就不存在 = 这个 major 没有 GA 清单");
|
||||
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||
assert!(
|
||||
error.message().contains("没有该大版本的 GA 清单"),
|
||||
"{}",
|
||||
error.message()
|
||||
);
|
||||
}
|
||||
@@ -170,8 +170,8 @@ fn pin_refusals_are_distinct_and_each_explains_itself() {
|
||||
errors[0].message().contains("21.0.13+9"),
|
||||
"可用版本要列出来"
|
||||
);
|
||||
// P3-3:连接词由 `source` 自带,模板不再补空格 —— 两种拼法都必须读得通,
|
||||
// 且 Oracle 那句不许出现「镜像目录 里」这种多一个空格的样子。
|
||||
// P3-3:连接词由 `source` 自带,模板不再补空格 —— 两种拼法都必须读得通。
|
||||
// (正向断言已蕴含「没有多余空格」,所以不留反向断言:r3 复审 P3-2 判它冗余。)
|
||||
assert!(
|
||||
errors[0].message().contains("Adoptium 里可用"),
|
||||
"{}",
|
||||
@@ -182,11 +182,6 @@ fn pin_refusals_are_distinct_and_each_explains_itself() {
|
||||
"{}",
|
||||
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()
|
||||
|
||||
Reference in new issue
Block a user