fix(java): Temurin 指定版本要翻页 + 三层「不静默回落」补上守卫(M280 r1)
复审 r1 判 p2-2items / fix-then-merge。两条都在这轮修掉。
## P2-1 Temurin 只读第一页 ⇒ 上游有的版本被报「没有」
`page_size` 上限 20 是上游硬约束,但 8/11/17 线各有 26 条 GA(实测 17/ga page=0 回
20 条、page=1 回 6 条)。原来只读第一页,`17.0.4` 这种「上游有、第一页没有」的版本
被报成「没有这个版本」,而且「等 20 个」是我们的窗口而不是上游总数。
- `releases_url(major, target, page)` 带页码;`collect_releases(fetch_page)` 是**纯**骨架
(分页由闭包注入),翻到「本页不满一页」为止,上限 `MAX_PAGES = 5`;
- `fetch_version` 用 `pick_release(&releases, wanted)` 挑(不取最新、不取第一条);
- 文案:`catalog::VersionListScope::{Complete, Truncated}` +
`available_versions_text(.., scope)`。完整时说「等 N 个」(= 上游总数),
到上限时才说「只查了前 N 条,后面可能还有」——不再把窗口当全集。
- 真机复现(复审给的命令):`17 --version 17.0.4` 现在命中 `17.0.4+8`;
`17 --version 17` 命中第二页最后一条 `17+35`;`17 --version 17.0.99` 报「等 26 个」。
## P2-2 三层保证一条守卫都没有(复审的变异 A/B/C)
现在每条都有能变红的用例:
| 守卫 | 位置 | 用例 |
| --- | --- | --- |
| Temurin 命中而非取最新 | `api/version.rs::pick_release` | `the_pinned_version_wins_over_the_newest_entry` + 两页用例 |
| 兜底闸(版本必须对上) | `catalog::version_mismatch` | `catalog/tests.rs::the_version_gate_rejects_...` |
| 装配层的「记失败继续走链」 | `install/chain.rs::resolve_one` / `chain_fetch` | `a_release_that_is_not_...`、`a_mismatch_records_a_failure_and_the_chain_moves_on`、`nothing_matching_...` |
| 钉死一家不换家 | `install()` 全链(`--vendor zulu --version`) | `a_pinned_vendor_that_cannot_pin_is_tried_alone`(断言「试过 1」+ `latest=true` + 一个字节没落盘) |
`install` 的循环与 `--dry-run` 的 `plan` 现在**共用同一份判据**(`resolve_one`):
`install` 逐家调它,`plan` 用 `chain_fetch` 驱动整条链。
## 拆分(守 400 行硬线)
- `catalog.rs` 400 → `catalog.rs` 278 + `catalog/tests.rs` 229(既有 7 条用例搬走,
唯一差异是新 API 的 `VersionListScope::Complete` 参数)
- `install.rs` → `install/plan.rs`(`plan` 段落;文档与签名原样,函数体改成走
`chain_fetch`,这正是「同一份判据」的要求)+ `install/chain.rs`(新骨架与守卫,
`install.rs` 361 行)
- `api/version.rs` 417 → `api/version.rs` 214 + `api/version/tests.rs` 207
## 门禁(CARGO_TARGET_DIR=C:/Temp/ull-target/wt-280)
core --lib 870 / --test game 120 / --test launch 91、cli --all-targets、fmt --check、
clippy -D warnings 全部 EXIT=0。
This commit is contained in:
1 parent
9fded45d9e
commit
1353c044c3
11 files changed
+849
-310
No files matched your search
@@ -59,7 +59,8 @@ pub const fn general() -> &'static str {
|
|||||||
--no-color 关闭颜色(v1 输出不含 ANSI,参数保留以兼容脚本)
|
--no-color 关闭颜色(v1 输出不含 ANSI,参数保留以兼容脚本)
|
||||||
--quiet 静默人类可读输出(错误与提示仍进 stderr)
|
--quiet 静默人类可读输出(错误与提示仍进 stderr)
|
||||||
--help, -h 帮助(有位置参数时按该命令给帮助)
|
--help, -h 帮助(有位置参数时按该命令给帮助)
|
||||||
--version, -V 版本 + 系统摘要
|
--version, -V 版本 + 系统摘要(只在子命令**之前**生效:`ull --version`;
|
||||||
|
子命令里的 `--version` 是它自己的旗标,如 `java install <major> --version V`)
|
||||||
|
|
||||||
退出码: 0 成功 / 1 用户错误 / 2 内部错误 / 3 部分成功(仅 pack import)"
|
退出码: 0 成功 / 1 用户错误 / 2 内部错误 / 3 部分成功(仅 pack import)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,13 +13,21 @@
|
|||||||
//! (openapi 里 `page_size` 的 `maximum` 就是 20)把前者翻成后者。同一份响应的 release 名
|
//! (openapi 里 `page_size` 的 `maximum` 就是 20)把前者翻成后者。同一份响应的 release 名
|
||||||
//! 列表同时是「这个大版本有哪些版本」的唯一来源——版本不存在时就靠它列给人看。
|
//! 列表同时是「这个大版本有哪些版本」的唯一来源——版本不存在时就靠它列给人看。
|
||||||
//!
|
//!
|
||||||
|
//! **必须翻页**(M280 r1):`page_size` 上限 20 是上游硬约束,而 8 / 11 / 17 线各有 26 条 GA
|
||||||
|
//! (2026-11-12 实测:`17/ga` page=0 回 20 条、page=1 回 6 条)。只读第一页会把
|
||||||
|
//! `17.0.4+8` 这种「上游有、第一页没有」的版本报成「没有这个版本」——文案还会把窗口当总数。
|
||||||
|
//! 所以这里按 `page` 翻到「本页不满一页」为止,上限 [`MAX_PAGES`] 页;真到上限就把清单标成
|
||||||
|
//! [`VersionListScope::Truncated`],错误文案里如实写「只查了前 N 条」。
|
||||||
|
//!
|
||||||
//! 这里**只有取**:匹配用 [`matches_version`],匹配不上时把可用清单交给
|
//! 这里**只有取**:匹配用 [`matches_version`],匹配不上时把可用清单交给
|
||||||
//! [`version_pin_error`] 生成错误。**不换家、不回落 latest**(那是 `install` / `source` 的事)。
|
//! [`version_pin_error`] 生成错误。**不换家、不回落 latest**(那是 `install` / `source` 的事)。
|
||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use super::{ApiBinary, ApiVersion, ADOPTIUM_API_BASE};
|
use super::{ApiBinary, ApiVersion, ADOPTIUM_API_BASE};
|
||||||
use crate::game::java::catalog::{canonical_version, matches_version, version_pin_error};
|
use crate::game::java::catalog::{
|
||||||
|
canonical_version, matches_version, version_pin_error, VersionListScope,
|
||||||
|
};
|
||||||
use crate::game::java::error::{JavaError, JavaErrorKind};
|
use crate::game::java::error::{JavaError, JavaErrorKind};
|
||||||
use crate::game::java::net;
|
use crate::game::java::net;
|
||||||
use crate::game::java::release::JdkRelease;
|
use crate::game::java::release::JdkRelease;
|
||||||
@@ -29,11 +37,14 @@ use crate::game::java::vendor::JdkVendor;
|
|||||||
/// `feature_releases` 的每页条数(Adoptium 允许的上限)。
|
/// `feature_releases` 的每页条数(Adoptium 允许的上限)。
|
||||||
pub const PAGE_SIZE: usize = 20;
|
pub const PAGE_SIZE: usize = 20;
|
||||||
|
|
||||||
/// 某大版本的 GA 清单地址(新 → 旧)。
|
/// 最多翻几页(正常最多 2 页;5 页 = 100 条,纯防御,别让上游的异常响应把内存吃掉)。
|
||||||
|
pub const MAX_PAGES: usize = 5;
|
||||||
|
|
||||||
|
/// 某大版本第 `page` 页的 GA 清单地址(新 → 旧,`page` 从 0 起)。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn releases_url(major: u32, target: AdoptiumTarget) -> String {
|
pub fn releases_url(major: u32, target: AdoptiumTarget, page: usize) -> String {
|
||||||
format!(
|
format!(
|
||||||
"{ADOPTIUM_API_BASE}/assets/feature_releases/{major}/ga?os={}&arch={}&image_type=jdk&page=0&page_size={PAGE_SIZE}&sort_order=DESC&vendor=eclipse",
|
"{ADOPTIUM_API_BASE}/assets/feature_releases/{major}/ga?os={}&arch={}&image_type=jdk&page={page}&page_size={PAGE_SIZE}&sort_order=DESC&vendor=eclipse",
|
||||||
target.os, target.arch
|
target.os, target.arch
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -114,6 +125,40 @@ pub fn parse_release(
|
|||||||
binary.into_release(name, release.version_data)
|
binary.into_release(name, release.version_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 翻页把该大版本的 GA release 名全部取回来(按页拼,直到「本页不满一页」或到 [`MAX_PAGES`])。
|
||||||
|
///
|
||||||
|
/// `fetch_page(page)` 由调用方给:真跑时是一发 HTTP,单测里是夹具闭包——所以「翻几页 / 什么时候
|
||||||
|
/// 停 / 到上限了没有」这段判断**完全离线可测**。
|
||||||
|
///
|
||||||
|
/// 返回值第二项 = 是否**因为到上限**而停(还有没看到的页)。错误文案要用它决定说
|
||||||
|
/// 「上游就这些」还是「只查了前 N 条」。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 任何一页取不到(网络 / 解析失败)时直接把该错误抛出去。
|
||||||
|
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>>,
|
||||||
|
{
|
||||||
|
let mut all: Vec<String> = Vec::new();
|
||||||
|
for page in 0..MAX_PAGES {
|
||||||
|
let names = fetch_page(page).await?;
|
||||||
|
let short_page = names.len() < PAGE_SIZE;
|
||||||
|
all.extend(names);
|
||||||
|
if short_page {
|
||||||
|
return Ok((all, false));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok((all, true))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 在枚举出来的 release 名里挑用户要的那一版(**不取最新、不取第一条**)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn pick_release<'a>(releases: &'a [String], wanted: &str) -> Option<&'a String> {
|
||||||
|
releases.iter().find(|name| matches_version(wanted, name))
|
||||||
|
}
|
||||||
|
|
||||||
/// 查一次「某大版本的指定版本」。
|
/// 查一次「某大版本的指定版本」。
|
||||||
///
|
///
|
||||||
/// # Errors
|
/// # Errors
|
||||||
@@ -125,22 +170,30 @@ pub async fn fetch_version(
|
|||||||
target: AdoptiumTarget,
|
target: AdoptiumTarget,
|
||||||
wanted: &str,
|
wanted: &str,
|
||||||
) -> Result<JdkRelease, JavaError> {
|
) -> Result<JdkRelease, JavaError> {
|
||||||
let list = net::get_text(client, &releases_url(major, target), "Adoptium GA 清单").await?;
|
let (releases, truncated) = collect_releases(|page| {
|
||||||
let releases = parse_release_list(&list)?;
|
let url = releases_url(major, target, page);
|
||||||
let picked = releases
|
async move {
|
||||||
.iter()
|
let body = net::get_text(client, &url, "Adoptium GA 清单").await?;
|
||||||
.find(|name| matches_version(wanted, name))
|
parse_release_list(&body)
|
||||||
.cloned();
|
}
|
||||||
let Some(name) = picked else {
|
})
|
||||||
|
.await?;
|
||||||
|
let Some(name) = pick_release(&releases, wanted).cloned() else {
|
||||||
let versions = releases
|
let versions = releases
|
||||||
.iter()
|
.iter()
|
||||||
.map(|name| canonical_version(name).to_string())
|
.map(|name| canonical_version(name).to_string())
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
let scope = if truncated {
|
||||||
|
VersionListScope::Truncated
|
||||||
|
} else {
|
||||||
|
VersionListScope::Complete
|
||||||
|
};
|
||||||
return Err(version_pin_error(
|
return Err(version_pin_error(
|
||||||
JdkVendor::Temurin,
|
JdkVendor::Temurin,
|
||||||
major,
|
major,
|
||||||
wanted,
|
wanted,
|
||||||
&versions,
|
&versions,
|
||||||
|
scope,
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let body = net::get_text(client, &release_url(&name, target), "Adoptium 指定版本产物").await?;
|
let body = net::get_text(client, &release_url(&name, target), "Adoptium 指定版本产物").await?;
|
||||||
@@ -158,88 +211,4 @@ pub async fn fetch_version(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests;
|
||||||
use super::{parse_release, parse_release_list, release_url, releases_url};
|
|
||||||
use crate::game::java::error::JavaErrorKind;
|
|
||||||
use crate::game::java::target::AdoptiumTarget;
|
|
||||||
|
|
||||||
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
|
||||||
|
|
||||||
/// 真机 `feature_releases/21/ga` 响应的裁剪(2026-11-12,只留两条与用到的字段)。
|
|
||||||
fn list_body() -> String {
|
|
||||||
r#"[
|
|
||||||
{"release_name":"jdk-21.0.13+9","version_data":{"major":21,"semver":"21.0.13+9.0.LTS"},"binaries":[]},
|
|
||||||
{"release_name":"jdk-21.0.12+8","version_data":{"major":21,"semver":"21.0.12+8.0.LTS"},"binaries":[]}
|
|
||||||
]"#
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 真机 `assets/release_name/eclipse/jdk-21.0.13+9` 响应的裁剪。
|
|
||||||
fn release_body() -> String {
|
|
||||||
r#"{
|
|
||||||
"release_name": "jdk-21.0.13+9",
|
|
||||||
"version_data": {"major": 21, "semver": "21.0.13+9.0.LTS", "build": 9},
|
|
||||||
"binaries": [
|
|
||||||
{
|
|
||||||
"os": "windows", "architecture": "x64", "image_type": "jdk", "jvm_impl": "hotspot",
|
|
||||||
"package": {
|
|
||||||
"name": "OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip",
|
|
||||||
"link": "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.13%2B9/OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip",
|
|
||||||
"checksum": "a4dbaa05fca4bd89d84584e58c459bd732efebb1722af80ab13c6516832004f9",
|
|
||||||
"size": 205421281
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}"#
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn urls_target_the_versioned_endpoints_never_latest() {
|
|
||||||
let releases = releases_url(21, WINDOWS);
|
|
||||||
assert!(releases.starts_with("https://api.adoptium.net/v3/assets/feature_releases/21/ga?"));
|
|
||||||
assert!(releases.contains("page_size=20"));
|
|
||||||
assert!(releases.contains("sort_order=DESC"));
|
|
||||||
assert!(!releases.contains("latest"));
|
|
||||||
let release = release_url("jdk-21.0.13+9", WINDOWS);
|
|
||||||
assert!(release
|
|
||||||
.starts_with("https://api.adoptium.net/v3/assets/release_name/eclipse/jdk-21.0.13+9?"));
|
|
||||||
assert!(release.contains("os=windows") && release.contains("arch=x64"));
|
|
||||||
assert!(!release.contains("latest"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn release_list_keeps_the_upstream_order_and_names() {
|
|
||||||
assert_eq!(
|
|
||||||
parse_release_list(&list_body()).expect("应解析"),
|
|
||||||
vec!["jdk-21.0.13+9".to_string(), "jdk-21.0.12+8".to_string()]
|
|
||||||
);
|
|
||||||
let error = parse_release_list("<html>502</html>").expect_err("HTML 应被拒");
|
|
||||||
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn release_body_becomes_the_pinned_archive() {
|
|
||||||
let release = parse_release(&release_body(), "jdk-21.0.13+9", WINDOWS).expect("应解析");
|
|
||||||
assert_eq!(release.version, "21.0.13+9", "版本串取自 release 名");
|
|
||||||
assert_eq!(release.semver.as_deref(), Some("21.0.13+9.0.LTS"));
|
|
||||||
assert_eq!(release.major, 21);
|
|
||||||
assert_eq!(
|
|
||||||
release.archive.name,
|
|
||||||
"OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip"
|
|
||||||
);
|
|
||||||
assert_eq!(release.archive.size, 205_421_281);
|
|
||||||
assert!(release.archive.url.contains("temurin21-binaries"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn other_platform_has_no_pinned_archive() {
|
|
||||||
let error = parse_release(
|
|
||||||
&release_body(),
|
|
||||||
"jdk-21.0.13+9",
|
|
||||||
AdoptiumTarget::new("windows", "x86-32"),
|
|
||||||
)
|
|
||||||
.expect_err("x86-32 应没有条目");
|
|
||||||
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
//! `api::version` 的单测:端点形状、两页夹具下的翻页、按版本挑而不是挑最新。
|
||||||
|
//!
|
||||||
|
//! 从 `version.rs` 拆出来只为守 400 行硬线(M280 r1 加了 P2-1/P2-2 的守卫后那边到 417 行);
|
||||||
|
//! **纯搬移**,既有断言一字未改。
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
collect_releases, 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;
|
||||||
|
|
||||||
|
const WINDOWS: AdoptiumTarget = AdoptiumTarget::new("windows", "x64");
|
||||||
|
|
||||||
|
/// 真机 `feature_releases/21/ga` 响应的裁剪(2026-11-12,只留两条与用到的字段)。
|
||||||
|
fn list_body() -> String {
|
||||||
|
r#"[
|
||||||
|
{"release_name":"jdk-21.0.13+9","version_data":{"major":21,"semver":"21.0.13+9.0.LTS"},"binaries":[]},
|
||||||
|
{"release_name":"jdk-21.0.12+8","version_data":{"major":21,"semver":"21.0.12+8.0.LTS"},"binaries":[]}
|
||||||
|
]"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 真机 `assets/release_name/eclipse/jdk-21.0.13+9` 响应的裁剪。
|
||||||
|
fn release_body() -> String {
|
||||||
|
r#"{
|
||||||
|
"release_name": "jdk-21.0.13+9",
|
||||||
|
"version_data": {"major": 21, "semver": "21.0.13+9.0.LTS", "build": 9},
|
||||||
|
"binaries": [
|
||||||
|
{
|
||||||
|
"os": "windows", "architecture": "x64", "image_type": "jdk", "jvm_impl": "hotspot",
|
||||||
|
"package": {
|
||||||
|
"name": "OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip",
|
||||||
|
"link": "https://github.com/adoptium/temurin21-binaries/releases/download/jdk-21.0.13%2B9/OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip",
|
||||||
|
"checksum": "a4dbaa05fca4bd89d84584e58c459bd732efebb1722af80ab13c6516832004f9",
|
||||||
|
"size": 205421281
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}"#
|
||||||
|
.to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn urls_target_the_versioned_endpoints_never_latest() {
|
||||||
|
let releases = releases_url(21, WINDOWS, 0);
|
||||||
|
assert!(releases.starts_with("https://api.adoptium.net/v3/assets/feature_releases/21/ga?"));
|
||||||
|
assert!(releases.contains("page_size=20"));
|
||||||
|
assert!(releases.contains("page=0"));
|
||||||
|
// 页码真的进 URL(翻页不是装饰)。
|
||||||
|
assert!(releases_url(21, WINDOWS, 1).contains("page=1"));
|
||||||
|
assert!(releases.contains("sort_order=DESC"));
|
||||||
|
assert!(!releases.contains("latest"));
|
||||||
|
let release = release_url("jdk-21.0.13+9", WINDOWS);
|
||||||
|
assert!(release
|
||||||
|
.starts_with("https://api.adoptium.net/v3/assets/release_name/eclipse/jdk-21.0.13+9?"));
|
||||||
|
assert!(release.contains("os=windows") && release.contains("arch=x64"));
|
||||||
|
assert!(!release.contains("latest"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_list_keeps_the_upstream_order_and_names() {
|
||||||
|
assert_eq!(
|
||||||
|
parse_release_list(&list_body()).expect("应解析"),
|
||||||
|
vec!["jdk-21.0.13+9".to_string(), "jdk-21.0.12+8".to_string()]
|
||||||
|
);
|
||||||
|
let error = parse_release_list("<html>502</html>").expect_err("HTML 应被拒");
|
||||||
|
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_body_becomes_the_pinned_archive() {
|
||||||
|
let release = parse_release(&release_body(), "jdk-21.0.13+9", WINDOWS).expect("应解析");
|
||||||
|
assert_eq!(release.version, "21.0.13+9", "版本串取自 release 名");
|
||||||
|
assert_eq!(release.semver.as_deref(), Some("21.0.13+9.0.LTS"));
|
||||||
|
assert_eq!(release.major, 21);
|
||||||
|
assert_eq!(
|
||||||
|
release.archive.name,
|
||||||
|
"OpenJDK21U-jdk_x64_windows_hotspot_21.0.13_9.zip"
|
||||||
|
);
|
||||||
|
assert_eq!(release.archive.size, 205_421_281);
|
||||||
|
assert!(release.archive.url.contains("temurin21-binaries"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn other_platform_has_no_pinned_archive() {
|
||||||
|
let error = parse_release(
|
||||||
|
&release_body(),
|
||||||
|
"jdk-21.0.13+9",
|
||||||
|
AdoptiumTarget::new("windows", "x86-32"),
|
||||||
|
)
|
||||||
|
.expect_err("x86-32 应没有条目");
|
||||||
|
assert_eq!(error.kind(), JavaErrorKind::Metadata);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 真机 `17/ga` 的两页形状(2026-11-12 实测 26 条:page=0 满 20 条、page=1 只有 6 条)。
|
||||||
|
/// 目标版本 `jdk-17.0.4+8` 在**第二页**,第一页 20 条里没有它。
|
||||||
|
fn page_zero() -> Vec<String> {
|
||||||
|
// 17.0.21 … 17.0.6 共 16 条 + 4 条补丁变体,凑满一页 20 条;
|
||||||
|
// 这一页里**没有** 17.0.4(它只在第二页)。
|
||||||
|
let mut names: Vec<String> = (0..16)
|
||||||
|
.map(|index| format!("jdk-17.0.{}+9", 21 - index))
|
||||||
|
.collect();
|
||||||
|
for filler in ["17.0.5.1+1", "17.0.5+11", "17.0.6.1+1", "17.0.10.1+1"] {
|
||||||
|
names.push(format!("jdk-{filler}"));
|
||||||
|
}
|
||||||
|
assert_eq!(names.len(), PAGE_SIZE);
|
||||||
|
assert!(
|
||||||
|
pick_release(&names, "17.0.4").is_none(),
|
||||||
|
"第一页不许有 17.0.4"
|
||||||
|
);
|
||||||
|
names
|
||||||
|
}
|
||||||
|
|
||||||
|
fn page_one() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"jdk-17.0.4.1+1".to_string(),
|
||||||
|
"jdk-17.0.4+8".to_string(),
|
||||||
|
"jdk-17.0.3+7".to_string(),
|
||||||
|
"jdk-17+35".to_string(),
|
||||||
|
"jdk-16.0.2+7".to_string(),
|
||||||
|
"jdk-16+36".to_string(),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 假的上游:按 page 返回夹具(`pages` 用完之后一律空页)。
|
||||||
|
fn two_pages(page: usize) -> Result<Vec<String>, JavaError> {
|
||||||
|
match page {
|
||||||
|
0 => Ok(page_zero()),
|
||||||
|
1 => Ok(page_one()),
|
||||||
|
_ => Ok(Vec::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P2-1 守卫**:`17.0.4` 只在第二页出现,必须翻到第二页才命中。
|
||||||
|
///
|
||||||
|
/// 变异证明(复审实测过的那个):把 `collect_releases` 的循环改成「只取第一页」
|
||||||
|
/// ⇒ 本用例红(`pick_release` 找不到 `jdk-17.0.4+8`)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_version_on_the_second_page_is_still_found() {
|
||||||
|
let pages: std::cell::RefCell<Vec<usize>> = std::cell::RefCell::new(Vec::new());
|
||||||
|
let (releases, truncated) = collect_releases(|page| {
|
||||||
|
pages.borrow_mut().push(page);
|
||||||
|
async move { two_pages(page) }
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("两页都应取到");
|
||||||
|
|
||||||
|
assert_eq!(*pages.borrow(), vec![0, 1], "应该正好翻两页");
|
||||||
|
assert_eq!(releases.len(), PAGE_SIZE + page_one().len());
|
||||||
|
assert!(!truncated, "第二页不满一页 ⇒ 清单是完整的");
|
||||||
|
assert_eq!(
|
||||||
|
pick_release(&releases, "17.0.4").map(String::as_str),
|
||||||
|
Some("jdk-17.0.4+8"),
|
||||||
|
"第二页那一版必须能被选中"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pick_release(&releases, "17.0.4.1").map(String::as_str),
|
||||||
|
Some("jdk-17.0.4.1+1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P2-2 守卫 1**:列表里同时有目标与更新的版本时,必须选**目标**,不许取第一条/最新。
|
||||||
|
///
|
||||||
|
/// 变异证明(复审的变异 B):把 `pick_release` 换成 `releases.first()`
|
||||||
|
/// ⇒ 本用例第一条红(会拿到 `jdk-17.0.21+9`)。
|
||||||
|
#[test]
|
||||||
|
fn the_pinned_version_wins_over_the_newest_entry() {
|
||||||
|
let releases = vec![
|
||||||
|
"jdk-17.0.21+9".to_string(),
|
||||||
|
"jdk-17.0.12+7".to_string(),
|
||||||
|
"jdk-17.0.4+8".to_string(),
|
||||||
|
];
|
||||||
|
assert_eq!(
|
||||||
|
pick_release(&releases, "17.0.4").map(String::as_str),
|
||||||
|
Some("jdk-17.0.4+8"),
|
||||||
|
"点名 17.0.4 时不许拿最新的 17.0.21+9"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
pick_release(&releases, "17.0.12+7").map(String::as_str),
|
||||||
|
Some("jdk-17.0.12+7")
|
||||||
|
);
|
||||||
|
assert_eq!(pick_release(&releases, "17.0.99"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 上限兜底:上游每页都满(异常情况)时,翻到 [`MAX_PAGES`] 页就停,并如实标「还有更多」。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `collect_releases` 的 `for page in 0..MAX_PAGES` 改成 `loop`
|
||||||
|
/// ⇒ 夹具永远满页,本用例挂死/超时(红)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_never_ending_catalog_stops_at_the_page_cap_and_says_so() {
|
||||||
|
let calls = std::cell::Cell::new(0usize);
|
||||||
|
let (releases, truncated) = collect_releases(|page| {
|
||||||
|
calls.set(calls.get() + 1);
|
||||||
|
async move {
|
||||||
|
let _ = page;
|
||||||
|
Ok((0..PAGE_SIZE)
|
||||||
|
.map(|index| format!("jdk-17.0.{}+1", index + 1))
|
||||||
|
.collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("每页都成功");
|
||||||
|
assert_eq!(calls.get(), MAX_PAGES, "翻满上限就停");
|
||||||
|
assert_eq!(releases.len(), MAX_PAGES * PAGE_SIZE);
|
||||||
|
assert!(truncated, "到上限时必须标成截断,文案才能如实说");
|
||||||
|
}
|
||||||
@@ -174,9 +174,29 @@ pub fn version_mismatch(wanted: Option<&str>, found: &str) -> Option<String> {
|
|||||||
(!matches_version(wanted, found)).then(|| format!("返回的版本是 {found},不是请求的 {wanted}"))
|
(!matches_version(wanted, found)).then(|| format!("返回的版本是 {found},不是请求的 {wanted}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 可用版本清单的完整度:决定错误文案里那句「还有没有更多」怎么说。
|
||||||
|
///
|
||||||
|
/// 上游的版本清单有三种形态:Oracle 目录页一次就能看全、Microsoft 只有一份快照、
|
||||||
|
/// Zulu/Termux 压根没有清单([`Complete`]);Temurin 要走分页,翻到上限还没翻完时
|
||||||
|
/// 只能如实说「只查了前 N 条」——**不许把窗口当全集**。
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum VersionListScope {
|
||||||
|
/// 上游的清单已经全看到了。
|
||||||
|
Complete,
|
||||||
|
/// 只看了上游的前若干条(Temurin 翻页到上限)。
|
||||||
|
Truncated,
|
||||||
|
}
|
||||||
|
|
||||||
/// 可用版本列表的人读形态(错误里列给人看;超过 `limit` 条就收口成「等 N 个」)。
|
/// 可用版本列表的人读形态(错误里列给人看;超过 `limit` 条就收口成「等 N 个」)。
|
||||||
|
///
|
||||||
|
/// `scope` 只影响尾巴那句:`Complete` 时「等 N 个」就是上游总数,`Truncated` 时补一句
|
||||||
|
/// 「只查了前 N 条」,因为这时 N 是**我们的窗口**而不是上游的总数。
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn available_versions_text(versions: &[String], limit: usize) -> String {
|
pub fn available_versions_text(
|
||||||
|
versions: &[String],
|
||||||
|
limit: usize,
|
||||||
|
scope: VersionListScope,
|
||||||
|
) -> String {
|
||||||
if versions.is_empty() {
|
if versions.is_empty() {
|
||||||
return "(上游一个都没列出来)".to_string();
|
return "(上游一个都没列出来)".to_string();
|
||||||
}
|
}
|
||||||
@@ -186,10 +206,16 @@ pub fn available_versions_text(versions: &[String], limit: usize) -> String {
|
|||||||
.map(String::as_str)
|
.map(String::as_str)
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" / ");
|
.join(" / ");
|
||||||
if versions.len() > limit {
|
let listed = if versions.len() > limit {
|
||||||
format!("{shown} 等 {} 个", versions.len())
|
format!("{shown} 等 {} 个", versions.len())
|
||||||
} else {
|
} else {
|
||||||
shown
|
shown
|
||||||
|
};
|
||||||
|
match scope {
|
||||||
|
VersionListScope::Complete => listed,
|
||||||
|
VersionListScope::Truncated => {
|
||||||
|
format!("{listed}(只查了前 {} 条,后面可能还有)", versions.len())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,8 +230,9 @@ pub fn version_pin_error(
|
|||||||
major: u32,
|
major: u32,
|
||||||
wanted: &str,
|
wanted: &str,
|
||||||
available: &[String],
|
available: &[String],
|
||||||
|
scope: VersionListScope,
|
||||||
) -> JavaError {
|
) -> JavaError {
|
||||||
let listed = available_versions_text(available, 10);
|
let listed = available_versions_text(available, 10, scope);
|
||||||
let try_other = "要这个版本换一家:`--vendor temurin`(Adoptium 全量补丁)或 `--vendor oracle`";
|
let try_other = "要这个版本换一家:`--vendor temurin`(Adoptium 全量补丁)或 `--vendor oracle`";
|
||||||
let vendor_id = vendor.id();
|
let vendor_id = vendor.id();
|
||||||
match vendor {
|
match vendor {
|
||||||
@@ -248,153 +275,4 @@ pub fn version_pin_error(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests;
|
||||||
use super::{choices, default_major, find, vendors_for, BASELINE_MAJOR};
|
|
||||||
use crate::game::java::vendor::{ids, JdkVendor};
|
|
||||||
|
|
||||||
use super::{available_versions_text, matches_version, version_pin_error};
|
|
||||||
|
|
||||||
/// 目录必须覆盖用户点名的三档(8 / 17 / 21),并钉住基准版本。
|
|
||||||
///
|
|
||||||
/// 变异证明:从 `CHOICES` 里删掉 8 那一项 ⇒ 本用例红(`find(8)` 为 `None`)。
|
|
||||||
#[test]
|
|
||||||
fn the_catalog_covers_eight_seventeen_and_twenty_one() {
|
|
||||||
for major in [8, 17, 21] {
|
|
||||||
assert!(find(major).is_some(), "目录里必须有 Java {major}");
|
|
||||||
}
|
|
||||||
assert_eq!(BASELINE_MAJOR, 17, "D13 的基准版本是 17");
|
|
||||||
assert!(find(BASELINE_MAJOR).expect("基准版本在目录里").baseline);
|
|
||||||
assert_eq!(
|
|
||||||
choices().len(),
|
|
||||||
5,
|
|
||||||
"目录是 8/11/17/21/25 五档,改动要同步本断言与 UI 文案"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
choices()
|
|
||||||
.windows(2)
|
|
||||||
.all(|pair| pair[0].major < pair[1].major),
|
|
||||||
"目录必须升序(UI 直接按这个顺序画)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 「8 只能由 Temurin / Zulu 承接」:Oracle 从 9 起、MS 已停发 8。
|
|
||||||
///
|
|
||||||
/// 变异证明:把 `vendors_for` 里的 `supports` 过滤删掉 ⇒ 本用例红(8 会列出 oracle/microsoft)。
|
|
||||||
#[test]
|
|
||||||
fn java_eight_is_only_served_by_the_vendors_that_still_ship_it() {
|
|
||||||
assert_eq!(ids(&vendors_for(8, &[], true)), vec!["temurin", "zulu"]);
|
|
||||||
assert_eq!(
|
|
||||||
ids(&vendors_for(21, &[], true)),
|
|
||||||
vec!["temurin", "oracle", "microsoft", "zulu"],
|
|
||||||
"21 四家都在(国内默认序)"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
!vendors_for(8, &[], true).contains(&JdkVendor::Microsoft),
|
|
||||||
"微软已停发 8"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 用户优先级要真的排在最前面(与 `install` 的链同源)。
|
|
||||||
#[test]
|
|
||||||
fn the_user_priority_leads_the_vendor_list() {
|
|
||||||
let priority = vec![JdkVendor::Zulu];
|
|
||||||
assert_eq!(ids(&vendors_for(17, &priority, true))[0], "zulu");
|
|
||||||
assert_eq!(ids(&vendors_for(17, &priority, true)).len(), 4);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 「该装哪个」对齐到目录里的一档:向上取整,没有上下文给基准,超出目录给基准。
|
|
||||||
#[test]
|
|
||||||
fn the_default_major_rounds_up_into_the_catalog() {
|
|
||||||
assert_eq!(default_major(None), 17);
|
|
||||||
assert_eq!(default_major(Some(8)), 8, "1.12.2 → 8");
|
|
||||||
assert_eq!(default_major(Some(16)), 17, "1.17 要 16 → 目录里的 17");
|
|
||||||
assert_eq!(default_major(Some(17)), 17);
|
|
||||||
assert_eq!(default_major(Some(21)), 21, "1.20.5+ → 21");
|
|
||||||
assert_eq!(default_major(Some(25)), 25, "26.1 → 25");
|
|
||||||
assert_eq!(default_major(Some(99)), 17, "超出目录时退回基准");
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 四家的版本串用同一把尺子量(`matches_version` 是唯一判据)。
|
|
||||||
///
|
|
||||||
/// 变异证明:把 `matches_version` 换成 `starts_with` ⇒ 本用例红
|
|
||||||
/// (`21.0.13` 会命中 MS 的 `21.0.13.0.2`,那是另一个版本)。
|
|
||||||
#[test]
|
|
||||||
fn version_matching_is_shared_and_never_prefix_fuzzy() {
|
|
||||||
// Temurin:公共部分 ↔ 带构建号;semver 与 release 名两种写法都算同一份。
|
|
||||||
assert!(matches_version("21.0.13", "21.0.13+9"));
|
|
||||||
assert!(matches_version("21.0.13+9", "jdk-21.0.13+9"));
|
|
||||||
assert!(matches_version("21.0.13+9", "21.0.13+9.0.LTS"));
|
|
||||||
assert!(matches_version(" 21.0.13 ", "21.0.13"));
|
|
||||||
// Oracle:目录名就是三段,公共部分一致即命中。
|
|
||||||
assert!(matches_version("21.0.2", "21.0.2"));
|
|
||||||
// 少一位、多一段都不是同一个版本(不许前缀近似)。
|
|
||||||
assert!(!matches_version("21.0", "21.0.13"));
|
|
||||||
assert!(!matches_version("21.0.13", "21.0.13.0.2"));
|
|
||||||
assert!(!matches_version("21.0.13+8", "21.0.13+9"));
|
|
||||||
// 用户点名了构建号、候选没给 → 不算命中。
|
|
||||||
assert!(!matches_version("21.0.13+9", "21.0.13"));
|
|
||||||
assert!(!matches_version("", "21.0.13"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 可用版本列表:给几条就列几条,多了收口(错误文案不能无限长)。
|
|
||||||
#[test]
|
|
||||||
fn available_versions_text_caps_the_list() {
|
|
||||||
let versions: Vec<String> = ["21.0.13", "21.0.12", "21.0.11"]
|
|
||||||
.iter()
|
|
||||||
.map(|text| (*text).to_string())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(
|
|
||||||
available_versions_text(&versions, 10),
|
|
||||||
"21.0.13 / 21.0.12 / 21.0.11"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
available_versions_text(&versions, 2),
|
|
||||||
"21.0.13 / 21.0.12 等 3 个"
|
|
||||||
);
|
|
||||||
assert!(available_versions_text(&[], 5).contains("一个都没列出来"));
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 四家的「这家不能按你要的版本装」文案必须**互不相同**、各自说清原因,且给下一步。
|
|
||||||
///
|
|
||||||
/// 变异证明:把某两家共用同一句「不支持指定版本」⇒ 本用例红。
|
|
||||||
#[test]
|
|
||||||
fn pin_refusals_are_distinct_and_each_explains_itself() {
|
|
||||||
let snapshot = vec!["21.0.13.0.2".to_string()];
|
|
||||||
let listed = vec!["21.0.13+9".to_string()];
|
|
||||||
let errors = [
|
|
||||||
version_pin_error(JdkVendor::Temurin, 21, "21.0.5", &listed),
|
|
||||||
version_pin_error(JdkVendor::Oracle, 21, "21.0.5", &["21.0.2".to_string()]),
|
|
||||||
version_pin_error(JdkVendor::Microsoft, 21, "21.0.5", &snapshot),
|
|
||||||
version_pin_error(JdkVendor::Zulu, 21, "21.0.5", &[]),
|
|
||||||
version_pin_error(JdkVendor::Termux, 21, "21.0.5", &[]),
|
|
||||||
];
|
|
||||||
for (index, left) in errors.iter().enumerate() {
|
|
||||||
for right in errors.iter().skip(index + 1) {
|
|
||||||
assert_ne!(
|
|
||||||
left.message(),
|
|
||||||
right.message(),
|
|
||||||
"每家的拒绝理由必须各说各的"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
left.message().contains("21.0.5"),
|
|
||||||
"错误里要带上用户要的版本"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
assert!(
|
|
||||||
errors[0].message().contains("21.0.13+9"),
|
|
||||||
"可用版本要列出来"
|
|
||||||
);
|
|
||||||
assert!(errors[2].message().contains("只内置了 21.0.13.0.2"));
|
|
||||||
assert!(errors[2]
|
|
||||||
.hint()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.contains("--vendor microsoft"));
|
|
||||||
assert!(errors[3].message().contains("latest=true"));
|
|
||||||
assert!(errors[3]
|
|
||||||
.hint()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.contains("--vendor temurin"));
|
|
||||||
assert!(errors[4].message().contains("系统包"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
//! `catalog` 的单测:目录、版本判据、拒绝文案、兜底闸。
|
||||||
|
//!
|
||||||
|
//! 从 `catalog.rs` 拆出来只为守 400 行硬线(M280 r1 要在这里加「兜底闸」与「清单被截断」
|
||||||
|
//! 的用例,留在主文件会到 470+);**纯搬移**,既有断言一字未改。
|
||||||
|
|
||||||
|
use super::{
|
||||||
|
available_versions_text, choices, default_major, find, matches_version, vendors_for,
|
||||||
|
version_mismatch, version_pin_error, VersionListScope, BASELINE_MAJOR,
|
||||||
|
};
|
||||||
|
use crate::game::java::vendor::{ids, JdkVendor};
|
||||||
|
|
||||||
|
/// 目录必须覆盖用户点名的三档(8 / 17 / 21),并钉住基准版本。
|
||||||
|
///
|
||||||
|
/// 变异证明:从 `CHOICES` 里删掉 8 那一项 ⇒ 本用例红(`find(8)` 为 `None`)。
|
||||||
|
#[test]
|
||||||
|
fn the_catalog_covers_eight_seventeen_and_twenty_one() {
|
||||||
|
for major in [8, 17, 21] {
|
||||||
|
assert!(find(major).is_some(), "目录里必须有 Java {major}");
|
||||||
|
}
|
||||||
|
assert_eq!(BASELINE_MAJOR, 17, "D13 的基准版本是 17");
|
||||||
|
assert!(find(BASELINE_MAJOR).expect("基准版本在目录里").baseline);
|
||||||
|
assert_eq!(
|
||||||
|
choices().len(),
|
||||||
|
5,
|
||||||
|
"目录是 8/11/17/21/25 五档,改动要同步本断言与 UI 文案"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
choices()
|
||||||
|
.windows(2)
|
||||||
|
.all(|pair| pair[0].major < pair[1].major),
|
||||||
|
"目录必须升序(UI 直接按这个顺序画)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「8 只能由 Temurin / Zulu 承接」:Oracle 从 9 起、MS 已停发 8。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `vendors_for` 里的 `supports` 过滤删掉 ⇒ 本用例红(8 会列出 oracle/microsoft)。
|
||||||
|
#[test]
|
||||||
|
fn java_eight_is_only_served_by_the_vendors_that_still_ship_it() {
|
||||||
|
assert_eq!(ids(&vendors_for(8, &[], true)), vec!["temurin", "zulu"]);
|
||||||
|
assert_eq!(
|
||||||
|
ids(&vendors_for(21, &[], true)),
|
||||||
|
vec!["temurin", "oracle", "microsoft", "zulu"],
|
||||||
|
"21 四家都在(国内默认序)"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!vendors_for(8, &[], true).contains(&JdkVendor::Microsoft),
|
||||||
|
"微软已停发 8"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 用户优先级要真的排在最前面(与 `install` 的链同源)。
|
||||||
|
#[test]
|
||||||
|
fn the_user_priority_leads_the_vendor_list() {
|
||||||
|
let priority = vec![JdkVendor::Zulu];
|
||||||
|
assert_eq!(ids(&vendors_for(17, &priority, true))[0], "zulu");
|
||||||
|
assert_eq!(ids(&vendors_for(17, &priority, true)).len(), 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 「该装哪个」对齐到目录里的一档:向上取整,没有上下文给基准,超出目录给基准。
|
||||||
|
#[test]
|
||||||
|
fn the_default_major_rounds_up_into_the_catalog() {
|
||||||
|
assert_eq!(default_major(None), 17);
|
||||||
|
assert_eq!(default_major(Some(8)), 8, "1.12.2 → 8");
|
||||||
|
assert_eq!(default_major(Some(16)), 17, "1.17 要 16 → 目录里的 17");
|
||||||
|
assert_eq!(default_major(Some(17)), 17);
|
||||||
|
assert_eq!(default_major(Some(21)), 21, "1.20.5+ → 21");
|
||||||
|
assert_eq!(default_major(Some(25)), 25, "26.1 → 25");
|
||||||
|
assert_eq!(default_major(Some(99)), 17, "超出目录时退回基准");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 四家的版本串用同一把尺子量(`matches_version` 是唯一判据)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `matches_version` 换成 `starts_with` ⇒ 本用例红
|
||||||
|
/// (`21.0.13` 会命中 MS 的 `21.0.13.0.2`,那是另一个版本)。
|
||||||
|
#[test]
|
||||||
|
fn version_matching_is_shared_and_never_prefix_fuzzy() {
|
||||||
|
// Temurin:公共部分 ↔ 带构建号;semver 与 release 名两种写法都算同一份。
|
||||||
|
assert!(matches_version("21.0.13", "21.0.13+9"));
|
||||||
|
assert!(matches_version("21.0.13+9", "jdk-21.0.13+9"));
|
||||||
|
assert!(matches_version("21.0.13+9", "21.0.13+9.0.LTS"));
|
||||||
|
assert!(matches_version(" 21.0.13 ", "21.0.13"));
|
||||||
|
// Oracle:目录名就是三段,公共部分一致即命中。
|
||||||
|
assert!(matches_version("21.0.2", "21.0.2"));
|
||||||
|
// 少一位、多一段都不是同一个版本(不许前缀近似)。
|
||||||
|
assert!(!matches_version("21.0", "21.0.13"));
|
||||||
|
assert!(!matches_version("21.0.13", "21.0.13.0.2"));
|
||||||
|
assert!(!matches_version("21.0.13+8", "21.0.13+9"));
|
||||||
|
// 用户点名了构建号、候选没给 → 不算命中。
|
||||||
|
assert!(!matches_version("21.0.13+9", "21.0.13"));
|
||||||
|
assert!(!matches_version("", "21.0.13"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 可用版本列表:给几条就列几条,多了收口(错误文案不能无限长)。
|
||||||
|
#[test]
|
||||||
|
fn available_versions_text_caps_the_list() {
|
||||||
|
let versions: Vec<String> = ["21.0.13", "21.0.12", "21.0.11"]
|
||||||
|
.iter()
|
||||||
|
.map(|text| (*text).to_string())
|
||||||
|
.collect();
|
||||||
|
assert_eq!(
|
||||||
|
available_versions_text(&versions, 10, VersionListScope::Complete),
|
||||||
|
"21.0.13 / 21.0.12 / 21.0.11"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
available_versions_text(&versions, 2, VersionListScope::Complete),
|
||||||
|
"21.0.13 / 21.0.12 等 3 个"
|
||||||
|
);
|
||||||
|
assert!(available_versions_text(&[], 5, VersionListScope::Complete).contains("一个都没列出来"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 四家的「这家不能按你要的版本装」文案必须**互不相同**、各自说清原因,且给下一步。
|
||||||
|
///
|
||||||
|
/// 变异证明:把某两家共用同一句「不支持指定版本」⇒ 本用例红。
|
||||||
|
#[test]
|
||||||
|
fn pin_refusals_are_distinct_and_each_explains_itself() {
|
||||||
|
let snapshot = vec!["21.0.13.0.2".to_string()];
|
||||||
|
let listed = vec!["21.0.13+9".to_string()];
|
||||||
|
let errors = [
|
||||||
|
version_pin_error(
|
||||||
|
JdkVendor::Temurin,
|
||||||
|
21,
|
||||||
|
"21.0.5",
|
||||||
|
&listed,
|
||||||
|
VersionListScope::Complete,
|
||||||
|
),
|
||||||
|
version_pin_error(
|
||||||
|
JdkVendor::Oracle,
|
||||||
|
21,
|
||||||
|
"21.0.5",
|
||||||
|
&["21.0.2".to_string()],
|
||||||
|
VersionListScope::Complete,
|
||||||
|
),
|
||||||
|
version_pin_error(
|
||||||
|
JdkVendor::Microsoft,
|
||||||
|
21,
|
||||||
|
"21.0.5",
|
||||||
|
&snapshot,
|
||||||
|
VersionListScope::Complete,
|
||||||
|
),
|
||||||
|
version_pin_error(
|
||||||
|
JdkVendor::Zulu,
|
||||||
|
21,
|
||||||
|
"21.0.5",
|
||||||
|
&[],
|
||||||
|
VersionListScope::Complete,
|
||||||
|
),
|
||||||
|
version_pin_error(
|
||||||
|
JdkVendor::Termux,
|
||||||
|
21,
|
||||||
|
"21.0.5",
|
||||||
|
&[],
|
||||||
|
VersionListScope::Complete,
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (index, left) in errors.iter().enumerate() {
|
||||||
|
for right in errors.iter().skip(index + 1) {
|
||||||
|
assert_ne!(
|
||||||
|
left.message(),
|
||||||
|
right.message(),
|
||||||
|
"每家的拒绝理由必须各说各的"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
left.message().contains("21.0.5"),
|
||||||
|
"错误里要带上用户要的版本"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
errors[0].message().contains("21.0.13+9"),
|
||||||
|
"可用版本要列出来"
|
||||||
|
);
|
||||||
|
assert!(errors[2].message().contains("只内置了 21.0.13.0.2"));
|
||||||
|
assert!(errors[2]
|
||||||
|
.hint()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("--vendor microsoft"));
|
||||||
|
assert!(errors[3].message().contains("latest=true"));
|
||||||
|
assert!(errors[3]
|
||||||
|
.hint()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("--vendor temurin"));
|
||||||
|
assert!(errors[4].message().contains("系统包"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **兜底闸(P2-2 守卫 2)**:实际拿到的版本与请求不符时必须给出原因,相符/没钉版本时必须放行。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `version_mismatch` 改成恒 `None`(复审的变异 A)⇒ 本用例第一条红。
|
||||||
|
#[test]
|
||||||
|
fn the_version_gate_rejects_a_different_version_and_lets_everything_else_through() {
|
||||||
|
let reason = version_mismatch(Some("21.0.12"), "21.0.13+9")
|
||||||
|
.expect("要 21.0.12、拿到 21.0.13+9 ⇒ 必须拒绝");
|
||||||
|
assert!(reason.contains("21.0.13+9"), "{reason}");
|
||||||
|
assert!(reason.contains("不是请求的 21.0.12"), "{reason}");
|
||||||
|
|
||||||
|
// 同一个版本(公共部分一致、构建号一致)⇒ 放行。
|
||||||
|
assert_eq!(version_mismatch(Some("21.0.12"), "21.0.12+8"), None);
|
||||||
|
assert_eq!(version_mismatch(Some("21.0.12+8"), "jdk-21.0.12+8"), None);
|
||||||
|
// 没钉版本 ⇒ 恒放行(不许对默认路径误报)。
|
||||||
|
assert_eq!(version_mismatch(None, "21.0.13+9"), None);
|
||||||
|
// 构建号不同 ⇒ 拒绝。
|
||||||
|
assert!(version_mismatch(Some("21.0.12+8"), "21.0.12+9").is_some());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **清单被截断时不许把窗口当全集(P2-1 文案)**:`Complete` 说「等 N 个」,
|
||||||
|
/// `Truncated` 必须补一句「只查了前 N 条」。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `Truncated` 分支改成与 `Complete` 相同 ⇒ 本用例第二条红。
|
||||||
|
#[test]
|
||||||
|
fn a_truncated_list_never_pretends_to_be_the_whole_catalog() {
|
||||||
|
let versions: Vec<String> = (0..20).map(|index| format!("17.0.{}", index + 1)).collect();
|
||||||
|
let complete = available_versions_text(&versions, 2, VersionListScope::Complete);
|
||||||
|
let truncated = available_versions_text(&versions, 2, VersionListScope::Truncated);
|
||||||
|
assert_eq!(complete, "17.0.1 / 17.0.2 等 20 个");
|
||||||
|
assert_ne!(complete, truncated);
|
||||||
|
assert!(
|
||||||
|
truncated.contains("只查了前 20 条"),
|
||||||
|
"截断时必须如实说:{truncated}"
|
||||||
|
);
|
||||||
|
// 完整清单的拒绝文案里不许出现「只查了」。
|
||||||
|
let error = version_pin_error(
|
||||||
|
JdkVendor::Temurin,
|
||||||
|
17,
|
||||||
|
"17.0.99",
|
||||||
|
&versions,
|
||||||
|
VersionListScope::Complete,
|
||||||
|
);
|
||||||
|
assert!(!error.message().contains("只查了"), "{}", error.message());
|
||||||
|
}
|
||||||
@@ -42,9 +42,13 @@ use crate::download::{DownloadEngine, DownloadRequest, ResourceKind};
|
|||||||
|
|
||||||
use one::install_one;
|
use one::install_one;
|
||||||
|
|
||||||
|
mod chain;
|
||||||
mod one;
|
mod one;
|
||||||
|
mod plan;
|
||||||
mod termux_install;
|
mod termux_install;
|
||||||
|
|
||||||
|
use chain::resolve_one;
|
||||||
|
|
||||||
/// 归档缓存目录名(在安装根下;托管目录扫描跳过它)。
|
/// 归档缓存目录名(在安装根下;托管目录扫描跳过它)。
|
||||||
pub use super::location::CACHE_DIR;
|
pub use super::location::CACHE_DIR;
|
||||||
|
|
||||||
@@ -232,6 +236,7 @@ pub async fn install(
|
|||||||
request.major,
|
request.major,
|
||||||
wanted,
|
wanted,
|
||||||
&[],
|
&[],
|
||||||
|
catalog::VersionListScope::Complete,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
@@ -285,20 +290,20 @@ pub async fn install(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let release = match source::fetch(engine, vendor, request.major, target, wanted).await {
|
// 取版本 + 兜底闸(版本必须真是点名的那一版,任何一家都不许回落 latest):
|
||||||
|
// 判据在 `chain::resolve_one` 里,`--dry-run` 的 `plan` 走同一个函数。
|
||||||
|
let release = match resolve_one(wanted, || {
|
||||||
|
source::fetch(engine, vendor, request.major, target, wanted)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
Ok(release) => release,
|
Ok(release) => release,
|
||||||
Err(error) => {
|
Err(reason) => {
|
||||||
chain.failed(vendor, error.message().to_string());
|
|
||||||
reasons.push(format!("{}:{}", vendor.id(), error.message()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
// 兜底闸:装成的那一版必须真是用户点名的那一版(任何一家都不许回落 latest)。
|
|
||||||
if let Some(reason) = catalog::version_mismatch(wanted, &release.version) {
|
|
||||||
chain.failed(vendor, reason.clone());
|
chain.failed(vendor, reason.clone());
|
||||||
reasons.push(format!("{}:{reason}", vendor.id()));
|
reasons.push(format!("{}:{reason}", vendor.id()));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
};
|
||||||
match install_one(engine, location, &release).await {
|
match install_one(engine, location, &release).await {
|
||||||
Ok(prepared) => {
|
Ok(prepared) => {
|
||||||
chain.hit(vendor, format!("命中 {}", release.summary()));
|
chain.hit(vendor, format!("命中 {}", release.summary()));
|
||||||
@@ -336,57 +341,15 @@ pub async fn install(
|
|||||||
// 钉了版本时给一句能照着做的:哪几家真能按版本装(默认路径的提示一个字都不动)。
|
// 钉了版本时给一句能照着做的:哪几家真能按版本装(默认路径的提示一个字都不动)。
|
||||||
Err(if wanted.is_some() {
|
Err(if wanted.is_some() {
|
||||||
error.with_hint(
|
error.with_hint(
|
||||||
"能按版本装的是 temurin / oracle;microsoft 只认它内置的快照,zulu 上游只提供 latest。\
|
"能按版本装的是 `--vendor temurin` 与 `--vendor oracle`;\
|
||||||
`ull java vendors` 看各发行版的可用性",
|
microsoft 只认它内置的那份快照,zulu 上游只提供 latest",
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
error.with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME")
|
error.with_hint("`ull java vendors` 看各发行版的可用性;或手动装 JDK 后设 JAVA_HOME")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 只做规划不落盘:返回将要安装的版本、目标目录与「是否已经装好」。
|
pub use plan::plan;
|
||||||
///
|
|
||||||
/// `--dry-run` 用它:`request.version` 与 `--vendor` 都参与选择(钉了版本就把这一维钉死),
|
|
||||||
/// 所以「最后会打哪个 URL、装哪个版本串」在下载之前就能看见——这也是不联网时
|
|
||||||
/// 唯一能验证版本选择逻辑的输出。
|
|
||||||
///
|
|
||||||
/// # Errors
|
|
||||||
///
|
|
||||||
/// 与 [`install`] 的查询段相同(平台不支持 / 网络失败 / 该家没有这个版本)。
|
|
||||||
pub async fn plan(
|
|
||||||
engine: &DownloadEngine,
|
|
||||||
location: &JavaInstallRoot,
|
|
||||||
request: &InstallRequest,
|
|
||||||
) -> Result<(JdkRelease, PathBuf, bool), JavaError> {
|
|
||||||
let target = AdoptiumTarget::host()?;
|
|
||||||
let domestic = engine.region().is_domestic();
|
|
||||||
let wanted = request.version.as_deref();
|
|
||||||
let mut reasons: Vec<String> = Vec::new();
|
|
||||||
for vendor in chain_order(request, domestic) {
|
|
||||||
let release = match source::fetch(engine, vendor, request.major, target, wanted).await {
|
|
||||||
Ok(release) => release,
|
|
||||||
Err(error) => {
|
|
||||||
reasons.push(format!("{}:{}", vendor.id(), error.message()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(reason) = catalog::version_mismatch(wanted, &release.version) {
|
|
||||||
reasons.push(format!("{}:{reason}", vendor.id()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let root = location.jdk_dir(release.major, release.vendor, &release.version);
|
|
||||||
let ready = extract::is_ready(&root, release.sha256());
|
|
||||||
return Ok((release, root, ready));
|
|
||||||
}
|
|
||||||
Err(JavaError::new(
|
|
||||||
JavaErrorKind::Metadata,
|
|
||||||
format!(
|
|
||||||
"没有可用的 Java {}({} )",
|
|
||||||
request.major,
|
|
||||||
reasons.join(";")
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn majors_text(vendor: JdkVendor) -> String {
|
fn majors_text(vendor: JdkVendor) -> String {
|
||||||
vendor
|
vendor
|
||||||
|
|||||||
@@ -0,0 +1,220 @@
|
|||||||
|
//! `install` 回退链的骨架:一次尝试的判据(取到 / 版本不符 / 失败)与「按顺序试、命中即停」。
|
||||||
|
//!
|
||||||
|
//! 单独成模块有两个理由:
|
||||||
|
//!
|
||||||
|
//! 1. `install.rs` 贴着 400 行硬线(M280 r1 加了这些守卫后到 440+);
|
||||||
|
//! 2. 这段是**纯判断**——真正的「取版本」由闭包注入,所以「版本不符 ⇒ 记失败继续走链」
|
||||||
|
//! 与「钉死一家时只试它一家」都能**离线**测。复审的变异 A(把
|
||||||
|
//! `catalog::version_mismatch` 改成恒 `None`)就是靠这里的用例变红的:没有它,
|
||||||
|
//! 「绝不把 latest 当用户要的版本装下去」只有代码、没有守卫。
|
||||||
|
|
||||||
|
use std::future::Future;
|
||||||
|
|
||||||
|
use super::super::catalog;
|
||||||
|
use super::super::error::JavaError;
|
||||||
|
use super::super::release::JdkRelease;
|
||||||
|
use super::super::vendor::JdkVendor;
|
||||||
|
|
||||||
|
/// 一次尝试的结果:`Ok` = 这一版就是用户要的;`Err` = 一句话原因(进降级记录)。
|
||||||
|
pub(super) type Attempt = Result<JdkRelease, String>;
|
||||||
|
|
||||||
|
/// 一次尝试的**判据**:取不到 → 原因;取到了但版本不是用户点名的那一版 → 拒绝。
|
||||||
|
///
|
||||||
|
/// `fetch` 由调用方给(真跑是 `source::fetch`,单测是夹具),所以这道闸离线可测。
|
||||||
|
/// `wanted = None`(没钉版本)时恒放行——默认路径不受任何影响。
|
||||||
|
pub(super) async fn resolve_one<F, Fut>(wanted: Option<&str>, fetch: F) -> Attempt
|
||||||
|
where
|
||||||
|
F: FnOnce() -> Fut,
|
||||||
|
Fut: Future<Output = Result<JdkRelease, JavaError>>,
|
||||||
|
{
|
||||||
|
match fetch().await {
|
||||||
|
Err(error) => Err(error.message().to_string()),
|
||||||
|
Ok(release) => match catalog::version_mismatch(wanted, &release.version) {
|
||||||
|
Some(reason) => Err(reason),
|
||||||
|
None => Ok(release),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 回退链骨架:按顺序试,第一个通过的即命中;失败与版本不符都变成一条原因继续下一家。
|
||||||
|
///
|
||||||
|
/// 返回 `(命中的发行版与版本, 每家失败的原因)`。`plan`(`--dry-run`)用它;`install` 还要记
|
||||||
|
/// `ChainLog` 与「不支持该大版本」的跳过原因,所以自己驱动循环,但**唯一入口仍是
|
||||||
|
/// [`resolve_one`]**——两边的判据只有一份。
|
||||||
|
pub(super) async fn chain_fetch<F, Fut>(
|
||||||
|
vendors: &[JdkVendor],
|
||||||
|
wanted: Option<&str>,
|
||||||
|
mut fetch: F,
|
||||||
|
) -> (Option<(JdkVendor, JdkRelease)>, Vec<(JdkVendor, String)>)
|
||||||
|
where
|
||||||
|
F: FnMut(JdkVendor) -> Fut,
|
||||||
|
Fut: Future<Output = Result<JdkRelease, JavaError>>,
|
||||||
|
{
|
||||||
|
let mut failures: Vec<(JdkVendor, String)> = Vec::new();
|
||||||
|
for candidate in vendors {
|
||||||
|
let vendor = *candidate;
|
||||||
|
match resolve_one(wanted, || fetch(vendor)).await {
|
||||||
|
Ok(release) => return (Some((vendor, release)), failures),
|
||||||
|
Err(reason) => failures.push((vendor, reason)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(None, failures)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use crate::download::RegionProfile;
|
||||||
|
use crate::download::{
|
||||||
|
ContentCache, DownloadEngine, EngineConfig, HealthRegistry, MirrorTable,
|
||||||
|
};
|
||||||
|
use crate::game::java::error::JavaError;
|
||||||
|
use crate::game::java::install::{self, InstallRequest, JavaInstallRoot};
|
||||||
|
use crate::game::java::release::{JdkArchive, JdkRelease};
|
||||||
|
use crate::game::java::vendor::JdkVendor;
|
||||||
|
|
||||||
|
use super::{chain_fetch, resolve_one};
|
||||||
|
|
||||||
|
fn release(version: &str) -> JdkRelease {
|
||||||
|
JdkRelease {
|
||||||
|
vendor: JdkVendor::Temurin,
|
||||||
|
major: 21,
|
||||||
|
version: version.to_string(),
|
||||||
|
semver: None,
|
||||||
|
release_name: format!("jdk-{version}"),
|
||||||
|
archive: JdkArchive {
|
||||||
|
name: format!("jdk-{version}.zip"),
|
||||||
|
url: "https://example.invalid/x.zip".to_string(),
|
||||||
|
sha256: "a".repeat(64),
|
||||||
|
size: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn temp_dir(tag: &str) -> std::path::PathBuf {
|
||||||
|
let dir = std::env::temp_dir().join(format!("ull-javapin-{tag}-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(&dir).expect("建临时目录");
|
||||||
|
dir
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P2-2 守卫(兜底闸)**:拿到的版本不是点名的那一版 ⇒ 拒绝,原因里带双方版本号。
|
||||||
|
///
|
||||||
|
/// 变异证明(复审的变异 A):把 `catalog::version_mismatch` 改成恒 `None`
|
||||||
|
/// ⇒ 本用例第一条红(`expect_err` 拿到 `Ok`)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_release_that_is_not_the_requested_version_is_rejected() {
|
||||||
|
let refused = resolve_one(Some("21.0.12"), || async { Ok(release("21.0.13+9")) })
|
||||||
|
.await
|
||||||
|
.expect_err("要 21.0.12、供应商回 latest 21.0.13+9 ⇒ 必须拒绝");
|
||||||
|
assert!(refused.contains("21.0.13+9"), "{refused}");
|
||||||
|
assert!(refused.contains("不是请求的 21.0.12"), "{refused}");
|
||||||
|
|
||||||
|
// 相符 ⇒ 放行(公共部分一致也算相符)。
|
||||||
|
assert!(
|
||||||
|
resolve_one(Some("21.0.12"), || async { Ok(release("21.0.12+8")) })
|
||||||
|
.await
|
||||||
|
.is_ok()
|
||||||
|
);
|
||||||
|
// 没钉版本 ⇒ 恒放行(默认路径不许误报)。
|
||||||
|
assert!(resolve_one(None, || async { Ok(release("21.0.13+9")) })
|
||||||
|
.await
|
||||||
|
.is_ok());
|
||||||
|
// 真正取不到 ⇒ 原因原样带出来。
|
||||||
|
let failed = resolve_one(Some("21.0.12"), || async {
|
||||||
|
Err::<JdkRelease, JavaError>(JavaError::new(
|
||||||
|
crate::game::java::JavaErrorKind::Network,
|
||||||
|
"连不上",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect_err("取不到就得报错");
|
||||||
|
assert_eq!(failed, "连不上");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P2-2 守卫(继续走链)**:第一家回落的 latest 必须被记成失败,第二家(真给了那一版)命中。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `chain_fetch` 的 `Err(reason) => failures.push(..)` 改成直接返回
|
||||||
|
/// ⇒ 本用例红(命中变成第一家那个不符的版本)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_mismatch_records_a_failure_and_the_chain_moves_on() {
|
||||||
|
let vendors = [JdkVendor::Temurin, JdkVendor::Oracle];
|
||||||
|
let (hit, failures) = chain_fetch(&vendors, Some("21.0.12"), |vendor| async move {
|
||||||
|
match vendor {
|
||||||
|
// 第一家没按版本取(就是复审担心的那种 bug:静默回落 latest)。
|
||||||
|
JdkVendor::Temurin => Ok(release("21.0.13+9")),
|
||||||
|
_ => Ok(release("21.0.12+8")),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let (vendor, picked) = hit.expect("第二家应命中");
|
||||||
|
assert_eq!(vendor, JdkVendor::Oracle, "命中不许是第一家");
|
||||||
|
assert_eq!(picked.version, "21.0.12+8");
|
||||||
|
assert_eq!(failures.len(), 1);
|
||||||
|
assert_eq!(failures[0].0, JdkVendor::Temurin);
|
||||||
|
assert!(failures[0].1.contains("不是请求的 21.0.12"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 全都不符 ⇒ 没有命中,每家一条原因(命令据此退出非 0 并列出全部原因)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn nothing_matching_means_no_hit_and_every_reason_is_kept() {
|
||||||
|
let vendors = [JdkVendor::Temurin, JdkVendor::Oracle];
|
||||||
|
let (hit, failures) = chain_fetch(&vendors, Some("21.0.12"), |_vendor| async {
|
||||||
|
Ok(release("21.0.13+9"))
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
assert!(hit.is_none(), "一家都没给对版本 ⇒ 不许命中");
|
||||||
|
assert_eq!(failures.len(), 2);
|
||||||
|
assert!(failures
|
||||||
|
.iter()
|
||||||
|
.all(|(_, reason)| reason.contains("不是请求的")));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **P2-2 守卫(不换家)**:钉死一家、而这家不支持指定版本 ⇒ `install` 只试它一家
|
||||||
|
/// (错误里的「试过 N」必须是 1),一个请求都不发。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `source::pinned` 里 Zulu 的拒绝改成回落 `fetch_latest`
|
||||||
|
/// ⇒ 本用例红(错误里不再有 `latest=true`,且「试过」不再是 1 条原因)。
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_pinned_vendor_that_cannot_pin_is_tried_alone() {
|
||||||
|
let dir = temp_dir("pinned-alone");
|
||||||
|
let cache = Arc::new(ContentCache::open(dir.join("cache")));
|
||||||
|
let engine = DownloadEngine::new(
|
||||||
|
EngineConfig::default(),
|
||||||
|
Arc::new(MirrorTable::builtin()),
|
||||||
|
Arc::new(RegionProfile::direct(0)),
|
||||||
|
Arc::new(HealthRegistry::in_memory()),
|
||||||
|
cache,
|
||||||
|
);
|
||||||
|
let location = JavaInstallRoot::custom(dir.join("java-managed"));
|
||||||
|
let request = InstallRequest::with_vendor(21, JdkVendor::Zulu)
|
||||||
|
.with_version(Some("21.0.13".to_string()));
|
||||||
|
|
||||||
|
let error = install::install(&engine, &location, request)
|
||||||
|
.await
|
||||||
|
.expect_err("zulu 上游只有 latest ⇒ 必须报错");
|
||||||
|
assert!(
|
||||||
|
error.message().contains("试过 1"),
|
||||||
|
"只许试点名的那一家:{}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error.message().contains("latest=true"),
|
||||||
|
"{}",
|
||||||
|
error.message()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.hint()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.contains("--vendor temurin"),
|
||||||
|
"要给出能做的下一步:{:?}",
|
||||||
|
error.hint()
|
||||||
|
);
|
||||||
|
// 一个字节都没落盘(连目录都不该建出来)。
|
||||||
|
assert!(!dir.join("java-managed").exists());
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
//! `install` 的**只读取**规划段:`--dry-run` 用它算出「会装哪一版、打哪个 URL」。
|
||||||
|
//!
|
||||||
|
//! 从 `install.rs` 拆出来只为守 400 行硬线(M280 r1 给回退链加了守卫后那边到 401 行);
|
||||||
|
//! 纯搬移,语义未变。与 `install/one.rs`(真装的一步)、`install/chain.rs`(判据与骨架)
|
||||||
|
//! 同一个拆法。
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use super::super::error::{JavaError, JavaErrorKind};
|
||||||
|
use super::super::extract;
|
||||||
|
use super::super::location::JavaInstallRoot;
|
||||||
|
use super::super::release::JdkRelease;
|
||||||
|
use super::super::source;
|
||||||
|
use super::super::target::AdoptiumTarget;
|
||||||
|
use super::{chain::chain_fetch, chain_order, InstallRequest};
|
||||||
|
use crate::download::DownloadEngine;
|
||||||
|
|
||||||
|
/// 只做规划不落盘:返回将要安装的版本、目标目录与「是否已经装好」。
|
||||||
|
///
|
||||||
|
/// `--dry-run` 用它:`request.version` 与 `--vendor` 都参与选择(钉了版本就把这一维钉死),
|
||||||
|
/// 所以「最后会打哪个 URL、装哪个版本串」在下载之前就能看见——这也是不联网时
|
||||||
|
/// 唯一能验证版本选择逻辑的输出。
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
///
|
||||||
|
/// 与 [`install`] 的查询段相同(平台不支持 / 网络失败 / 该家没有这个版本)。
|
||||||
|
pub async fn plan(
|
||||||
|
engine: &DownloadEngine,
|
||||||
|
location: &JavaInstallRoot,
|
||||||
|
request: &InstallRequest,
|
||||||
|
) -> Result<(JdkRelease, PathBuf, bool), JavaError> {
|
||||||
|
let target = AdoptiumTarget::host()?;
|
||||||
|
let domestic = engine.region().is_domestic();
|
||||||
|
let wanted = request.version.as_deref();
|
||||||
|
let candidates = chain_order(request, domestic);
|
||||||
|
let (hit, failures) = chain_fetch(&candidates, wanted, |vendor| {
|
||||||
|
source::fetch(engine, vendor, request.major, target, wanted)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
match hit {
|
||||||
|
Some((_, release)) => {
|
||||||
|
let root = location.jdk_dir(release.major, release.vendor, &release.version);
|
||||||
|
let ready = extract::is_ready(&root, release.sha256());
|
||||||
|
Ok((release, root, ready))
|
||||||
|
}
|
||||||
|
None => Err(JavaError::new(
|
||||||
|
JavaErrorKind::Metadata,
|
||||||
|
format!(
|
||||||
|
"没有可用的 Java {}({} )",
|
||||||
|
request.major,
|
||||||
|
failures
|
||||||
|
.iter()
|
||||||
|
.map(|(vendor, reason)| format!("{}:{reason}", vendor.id()))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(";")
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
//! 3. **GitHub 侧没有任何资产**(`microsoft/openjdk` 的 release `assets` 全空),
|
//! 3. **GitHub 侧没有任何资产**(`microsoft/openjdk` 的 release `assets` 全空),
|
||||||
//! 所以 gh-proxy 对这条链毫无用处——`domestic_mirrors` 实测为空数组。
|
//! 所以 gh-proxy 对这条链毫无用处——`domestic_mirrors` 实测为空数组。
|
||||||
|
|
||||||
use super::catalog::{matches_version, version_pin_error};
|
use super::catalog::{matches_version, version_pin_error, VersionListScope};
|
||||||
use super::error::{JavaError, JavaErrorKind};
|
use super::error::{JavaError, JavaErrorKind};
|
||||||
use super::net;
|
use super::net;
|
||||||
use super::release::{parse_sha256_text, JdkArchive, JdkRelease};
|
use super::release::{parse_sha256_text, JdkArchive, JdkRelease};
|
||||||
@@ -187,6 +187,7 @@ pub async fn fetch_version(
|
|||||||
major,
|
major,
|
||||||
wanted,
|
wanted,
|
||||||
&[known.to_string()],
|
&[known.to_string()],
|
||||||
|
VersionListScope::Complete,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
fetch_latest(client, table, major, target).await
|
fetch_latest(client, table, major, target).await
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
//!
|
//!
|
||||||
//! 「存在不信状态码」的理由见 `oracle.rs` 头:华为云对任意不存在的路径都回 200 + 门户 HTML。
|
//! 「存在不信状态码」的理由见 `oracle.rs` 头:华为云对任意不存在的路径都回 200 + 门户 HTML。
|
||||||
|
|
||||||
use super::super::catalog::{matches_version, version_pin_error};
|
use super::super::catalog::{matches_version, version_pin_error, VersionListScope};
|
||||||
use super::super::error::{JavaError, JavaErrorKind};
|
use super::super::error::{JavaError, JavaErrorKind};
|
||||||
use super::super::vendor::JdkVendor;
|
use super::super::vendor::JdkVendor;
|
||||||
|
|
||||||
@@ -102,7 +102,15 @@ pub(super) fn select_version(
|
|||||||
.iter()
|
.iter()
|
||||||
.find(|version| matches_version(wanted, version))
|
.find(|version| matches_version(wanted, version))
|
||||||
.cloned()
|
.cloned()
|
||||||
.ok_or_else(|| version_pin_error(JdkVendor::Oracle, major, wanted, &available))
|
.ok_or_else(|| {
|
||||||
|
version_pin_error(
|
||||||
|
JdkVendor::Oracle,
|
||||||
|
major,
|
||||||
|
wanted,
|
||||||
|
&available,
|
||||||
|
VersionListScope::Complete,
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 「目录页里没有这个大版本」。
|
/// 「目录页里没有这个大版本」。
|
||||||
|
|||||||
@@ -82,9 +82,13 @@ async fn pinned(
|
|||||||
}
|
}
|
||||||
// 上游只给最新一份(Zulu)/ 由系统包管理器定版本(Termux):明确报错,
|
// 上游只给最新一份(Zulu)/ 由系统包管理器定版本(Termux):明确报错,
|
||||||
// 绝不换成别的版本假装装成了用户点名的那一版。
|
// 绝不换成别的版本假装装成了用户点名的那一版。
|
||||||
JdkVendor::Zulu | JdkVendor::Termux => {
|
JdkVendor::Zulu | JdkVendor::Termux => Err(catalog::version_pin_error(
|
||||||
Err(catalog::version_pin_error(vendor, major, wanted, &[]))
|
vendor,
|
||||||
}
|
major,
|
||||||
|
wanted,
|
||||||
|
&[],
|
||||||
|
catalog::VersionListScope::Complete,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user