chore(m197): 三处拆文件守 400 行硬线(复审 r1 后新增内容把三个文件顶过线)
- `core/src/setting/keys.rs` 396(原 400 → 我的 +15 到 415):`download.proxy_mode` / `proxy_url` / `gh_proxy` 三键搬进新 `keys/proxy.rs`(`specs()` + 一句「空串 ≠ 关」的守卫), 主表留一行 `.with_all(proxy::specs())`——与 `font` / `update` 同一种形状。 - `core/tests/download/engine_mock_test.rs` 407 → 364:404 那条用例整条搬进 `tests/download/engine_mock/not_found.rs`(`#[path]` 子模块,仍是 `engine_mock` 这个 target)。 - `pages/settings/java/downloads/tests.rs` 423 → 313:gh-proxy 两条守卫搬进 `downloads/tests/gh_proxy.rs`(`#[path = "tests/gh_proxy.rs"]`)。 纯搬移 + 一条新单测(`keys/proxy.rs` 的「空串 = 跟随内置默认,不是关」);行为不变。
This commit is contained in:
1 parent
a14b00aced
commit
7b79d5c397
6 files changed
+274
-191
No files matched your search
@@ -308,116 +308,6 @@ fn every_download_template_renders_without_leftover_placeholders() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --------------------------------------------------------------- P2-3:`download.gh_proxy` 的取值与真消费者
|
/// `download.gh_proxy` 的两条守卫(M197):取值映射 + 真消费者。拆到子模块守 400 行硬线。
|
||||||
|
#[path = "tests/gh_proxy.rs"]
|
||||||
/// 设置原文 → 三态:空 = 跟随内置默认,`off` 一族 = 关,其余 = 覆盖前缀。
|
mod gh_proxy;
|
||||||
///
|
|
||||||
/// 变异证明:删掉 `gh_proxy_from` 里那段 `matches!(lowered, "off" | …)` ⇒ 本用例红
|
|
||||||
/// (`off` 会被当成一个前缀 `off`,拼出 `offhttps://…` 这种 URL)。
|
|
||||||
#[test]
|
|
||||||
fn the_gh_proxy_setting_maps_to_three_states() {
|
|
||||||
use super::engine::gh_proxy_from;
|
|
||||||
use super::GhProxy;
|
|
||||||
|
|
||||||
assert_eq!(gh_proxy_from(""), GhProxy::Builtin, "空 = 跟随内置默认");
|
|
||||||
assert_eq!(gh_proxy_from(" "), GhProxy::Builtin);
|
|
||||||
for off in [
|
|
||||||
"off", "OFF", " none ", "direct", "Disable", "disabled", "no",
|
|
||||||
] {
|
|
||||||
assert_eq!(gh_proxy_from(off), GhProxy::Off, "{off} 应表示关掉");
|
|
||||||
}
|
|
||||||
assert_eq!(
|
|
||||||
gh_proxy_from(" https://gh.example/ "),
|
|
||||||
GhProxy::Prefix("https://gh.example/".to_string()),
|
|
||||||
"前后空白要去掉,值原样保留"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// **真消费者**:`download.gh_proxy` 落盘 → 引擎档案 → **候选链里真的出现该前缀**。
|
|
||||||
///
|
|
||||||
/// 三步都要成立才算「接了」:① 键读得出来;② 档案被覆盖;③ `expand()` 用它生成候选。
|
|
||||||
/// 只断言 ① 会漏掉「读了但没用」(本 mission 修的就是这一类),只断言 ② 会漏掉「expand 不认
|
|
||||||
/// 档案里的前缀」。
|
|
||||||
///
|
|
||||||
/// 变异证明:让 `engine_with` 忽略 `gh_proxy` 参数(`let profile = profile;`)⇒ ②③ 两条红;
|
|
||||||
/// 把 `gh_proxy_of_config` 写成恒 `Builtin` ⇒ ① 先红。
|
|
||||||
#[test]
|
|
||||||
fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
|
||||||
use super::engine::engine_with;
|
|
||||||
use super::{gh_proxy_of_config, GhProxy};
|
|
||||||
use unlucky_core::download::cache::now_ms;
|
|
||||||
use unlucky_core::download::region::{store_cached, RegionProfile};
|
|
||||||
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
|
||||||
use unlucky_core::download::ProgressEmitter;
|
|
||||||
use unlucky_core::setting::keys::launcher_schema;
|
|
||||||
use unlucky_core::setting::store::SettingStore;
|
|
||||||
|
|
||||||
let dir = std::env::temp_dir().join(format!("ull-gh-proxy-{}-{}", std::process::id(), line!()));
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
std::fs::create_dir_all(dir.join("cache")).expect("cache");
|
|
||||||
std::fs::create_dir_all(dir.join("config")).expect("config");
|
|
||||||
let config = dir.join("config").join("launcher.json");
|
|
||||||
|
|
||||||
// ① 键读得出来
|
|
||||||
let mut store = SettingStore::load(&config, launcher_schema());
|
|
||||||
store
|
|
||||||
.set(
|
|
||||||
"download.gh_proxy",
|
|
||||||
"https://gh.example/".to_string().into(),
|
|
||||||
)
|
|
||||||
.expect("schema 认识 download.gh_proxy");
|
|
||||||
store.save().expect("写盘");
|
|
||||||
assert_eq!(
|
|
||||||
gh_proxy_of_config(&config),
|
|
||||||
GhProxy::Prefix("https://gh.example/".to_string())
|
|
||||||
);
|
|
||||||
|
|
||||||
// region.json 钉成镜像优先:否则 gh-proxy 候选本来就不生成,测不出「设了有没有用」。
|
|
||||||
let cache = dir.join("cache");
|
|
||||||
store_cached(
|
|
||||||
&cache.join("region.json"),
|
|
||||||
&RegionProfile::mirror_first(now_ms()),
|
|
||||||
)
|
|
||||||
.expect("写 region.json");
|
|
||||||
|
|
||||||
let engine = engine_with(
|
|
||||||
&cache,
|
|
||||||
ProgressEmitter::silent(),
|
|
||||||
&gh_proxy_of_config(&config),
|
|
||||||
);
|
|
||||||
// ② 档案被覆盖
|
|
||||||
assert_eq!(
|
|
||||||
engine.region().gh_proxy.as_deref(),
|
|
||||||
Some("https://gh.example/"),
|
|
||||||
"设置值必须覆盖档案里的前缀"
|
|
||||||
);
|
|
||||||
|
|
||||||
// ③ 候选链里真的出现这个前缀
|
|
||||||
let request = DownloadRequest::new(
|
|
||||||
ResourceKind::Library,
|
|
||||||
"https://github.com/owner/repo/releases/download/v1/a.jar".to_string(),
|
|
||||||
dir.join("a.jar"),
|
|
||||||
);
|
|
||||||
let plan = engine.plan(&request, 0);
|
|
||||||
assert!(
|
|
||||||
plan.iter()
|
|
||||||
.any(|candidate| candidate.url.starts_with("https://gh.example/")),
|
|
||||||
"gh-proxy 候选没出现在候选链里:{:?}",
|
|
||||||
plan.iter()
|
|
||||||
.map(|candidate| candidate.url.clone())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
);
|
|
||||||
|
|
||||||
// 关掉那一档:一个代理候选都不生成(同一份 region.json,只换设置值)。
|
|
||||||
let off = engine_with(&cache, ProgressEmitter::silent(), &GhProxy::Off);
|
|
||||||
assert_eq!(off.region().gh_proxy, None, "off 必须把前缀清掉");
|
|
||||||
let plan = off.plan(&request, 0);
|
|
||||||
assert!(
|
|
||||||
!plan
|
|
||||||
.iter()
|
|
||||||
.any(|candidate| candidate.url.starts_with("https://gh.example/")),
|
|
||||||
"关掉之后不该再有 gh-proxy 候选"
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
//! `download.gh_proxy` 的两条守卫(M197):取值映射 + 「真的接上了」。
|
||||||
|
//!
|
||||||
|
//! 从 `downloads/tests.rs` 拆出来守 400 行硬线(拆法与 `agents::tests` 一致:断言跟着它守的
|
||||||
|
//! 东西走)。这里只说一件事:设置值必须**改变候选链**,而不只是被读出来。
|
||||||
|
|
||||||
|
// --------------------------------------------------------------- P2-3:`download.gh_proxy` 的取值与真消费者
|
||||||
|
|
||||||
|
/// 设置原文 → 三态:空 = 跟随内置默认,`off` 一族 = 关,其余 = 覆盖前缀。
|
||||||
|
///
|
||||||
|
/// 变异证明:删掉 `gh_proxy_from` 里那段 `matches!(lowered, "off" | …)` ⇒ 本用例红
|
||||||
|
/// (`off` 会被当成一个前缀 `off`,拼出 `offhttps://…` 这种 URL)。
|
||||||
|
#[test]
|
||||||
|
fn the_gh_proxy_setting_maps_to_three_states() {
|
||||||
|
use crate::pages::settings::java::downloads::engine::gh_proxy_from;
|
||||||
|
use crate::pages::settings::java::downloads::GhProxy;
|
||||||
|
|
||||||
|
assert_eq!(gh_proxy_from(""), GhProxy::Builtin, "空 = 跟随内置默认");
|
||||||
|
assert_eq!(gh_proxy_from(" "), GhProxy::Builtin);
|
||||||
|
for off in [
|
||||||
|
"off", "OFF", " none ", "direct", "Disable", "disabled", "no",
|
||||||
|
] {
|
||||||
|
assert_eq!(gh_proxy_from(off), GhProxy::Off, "{off} 应表示关掉");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
gh_proxy_from(" https://gh.example/ "),
|
||||||
|
GhProxy::Prefix("https://gh.example/".to_string()),
|
||||||
|
"前后空白要去掉,值原样保留"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// **真消费者**:`download.gh_proxy` 落盘 → 引擎档案 → **候选链里真的出现该前缀**。
|
||||||
|
///
|
||||||
|
/// 三步都要成立才算「接了」:① 键读得出来;② 档案被覆盖;③ `expand()` 用它生成候选。
|
||||||
|
/// 只断言 ① 会漏掉「读了但没用」(本 mission 修的就是这一类),只断言 ② 会漏掉「expand 不认
|
||||||
|
/// 档案里的前缀」。
|
||||||
|
///
|
||||||
|
/// 变异证明:让 `engine_with` 忽略 `gh_proxy` 参数(`let profile = profile;`)⇒ ②③ 两条红;
|
||||||
|
/// 把 `gh_proxy_of_config` 写成恒 `Builtin` ⇒ ① 先红。
|
||||||
|
#[test]
|
||||||
|
fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
||||||
|
use crate::pages::settings::java::downloads::engine::engine_with;
|
||||||
|
use crate::pages::settings::java::downloads::{gh_proxy_of_config, GhProxy};
|
||||||
|
use unlucky_core::download::cache::now_ms;
|
||||||
|
use unlucky_core::download::region::{store_cached, RegionProfile};
|
||||||
|
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
||||||
|
use unlucky_core::download::ProgressEmitter;
|
||||||
|
use unlucky_core::setting::keys::launcher_schema;
|
||||||
|
use unlucky_core::setting::store::SettingStore;
|
||||||
|
|
||||||
|
let dir = std::env::temp_dir().join(format!("ull-gh-proxy-{}-{}", std::process::id(), line!()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
std::fs::create_dir_all(dir.join("cache")).expect("cache");
|
||||||
|
std::fs::create_dir_all(dir.join("config")).expect("config");
|
||||||
|
let config = dir.join("config").join("launcher.json");
|
||||||
|
|
||||||
|
// ① 键读得出来
|
||||||
|
let mut store = SettingStore::load(&config, launcher_schema());
|
||||||
|
store
|
||||||
|
.set(
|
||||||
|
"download.gh_proxy",
|
||||||
|
"https://gh.example/".to_string().into(),
|
||||||
|
)
|
||||||
|
.expect("schema 认识 download.gh_proxy");
|
||||||
|
store.save().expect("写盘");
|
||||||
|
assert_eq!(
|
||||||
|
gh_proxy_of_config(&config),
|
||||||
|
GhProxy::Prefix("https://gh.example/".to_string())
|
||||||
|
);
|
||||||
|
|
||||||
|
// region.json 钉成镜像优先:否则 gh-proxy 候选本来就不生成,测不出「设了有没有用」。
|
||||||
|
let cache = dir.join("cache");
|
||||||
|
store_cached(
|
||||||
|
&cache.join("region.json"),
|
||||||
|
&RegionProfile::mirror_first(now_ms()),
|
||||||
|
)
|
||||||
|
.expect("写 region.json");
|
||||||
|
|
||||||
|
let engine = engine_with(
|
||||||
|
&cache,
|
||||||
|
ProgressEmitter::silent(),
|
||||||
|
&gh_proxy_of_config(&config),
|
||||||
|
);
|
||||||
|
// ② 档案被覆盖
|
||||||
|
assert_eq!(
|
||||||
|
engine.region().gh_proxy.as_deref(),
|
||||||
|
Some("https://gh.example/"),
|
||||||
|
"设置值必须覆盖档案里的前缀"
|
||||||
|
);
|
||||||
|
|
||||||
|
// ③ 候选链里真的出现这个前缀
|
||||||
|
let request = DownloadRequest::new(
|
||||||
|
ResourceKind::Library,
|
||||||
|
"https://github.com/owner/repo/releases/download/v1/a.jar".to_string(),
|
||||||
|
dir.join("a.jar"),
|
||||||
|
);
|
||||||
|
let plan = engine.plan(&request, 0);
|
||||||
|
assert!(
|
||||||
|
plan.iter()
|
||||||
|
.any(|candidate| candidate.url.starts_with("https://gh.example/")),
|
||||||
|
"gh-proxy 候选没出现在候选链里:{:?}",
|
||||||
|
plan.iter()
|
||||||
|
.map(|candidate| candidate.url.clone())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
|
||||||
|
// 关掉那一档:一个代理候选都不生成(同一份 region.json,只换设置值)。
|
||||||
|
let off = engine_with(&cache, ProgressEmitter::silent(), &GhProxy::Off);
|
||||||
|
assert_eq!(off.region().gh_proxy, None, "off 必须把前缀清掉");
|
||||||
|
let plan = off.plan(&request, 0);
|
||||||
|
assert!(
|
||||||
|
!plan
|
||||||
|
.iter()
|
||||||
|
.any(|candidate| candidate.url.starts_with("https://gh.example/")),
|
||||||
|
"关掉之后不该再有 gh-proxy 候选"
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
@@ -37,8 +37,10 @@ pub const JAVA_INSTALL_ROOT_UNSET: &str = "";
|
|||||||
/// 必须同一份集合(setting 层不依赖 game 层),一致性由 `tests/setting/java_install_keys.rs` 守。
|
/// 必须同一份集合(setting 层不依赖 game 层),一致性由 `tests/setting/java_install_keys.rs` 守。
|
||||||
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
pub const JDK_VENDORS: &[&str] = &["temurin", "oracle", "microsoft", "zulu", "termux"];
|
||||||
|
|
||||||
/// 拆到各自文件的键域:中文字体 CDN(M168)/ 自更新(M161)。主表各留一行登记。
|
/// 拆到各自文件的键域:中文字体 CDN(M168)/ 自更新(M161)/ 代理与 gh-proxy(M197)。
|
||||||
|
/// 主表各留一行登记。
|
||||||
pub mod font;
|
pub mod font;
|
||||||
|
pub mod proxy;
|
||||||
pub mod update;
|
pub mod update;
|
||||||
|
|
||||||
/// launcher.json 当前 schema 版本。
|
/// launcher.json 当前 schema 版本。
|
||||||
@@ -132,30 +134,9 @@ pub fn launcher_schema() -> SettingSchema {
|
|||||||
AiPolicy::audited(),
|
AiPolicy::audited(),
|
||||||
"镜像偏好:auto(健康度评分)/ official(官方源优先)/ mirror(镜像优先)",
|
"镜像偏好:auto(健康度评分)/ official(官方源优先)/ mirror(镜像优先)",
|
||||||
))
|
))
|
||||||
.with(SettingSpec::enumeration(
|
// M197:代理与 gh-proxy 三键(`download.proxy_mode` / `proxy_url` / `gh_proxy`)搬到
|
||||||
"download.proxy_mode",
|
// [`proxy`]——主表这一段的注释也一起搬过去,见那个模块的表格。
|
||||||
"none",
|
.with_all(proxy::specs())
|
||||||
PROXY_MODES,
|
|
||||||
AiPolicy::sensitive(),
|
|
||||||
"代理模式:none / system / manual",
|
|
||||||
))
|
|
||||||
.with(SettingSpec::text(
|
|
||||||
"download.proxy_url",
|
|
||||||
"",
|
|
||||||
512,
|
|
||||||
AiPolicy::sensitive(),
|
|
||||||
"manual 代理地址(如 http://127.0.0.1:7890)",
|
|
||||||
))
|
|
||||||
// M197:gh-proxy 前缀的用户覆盖。语义在 `core::download::region`:
|
|
||||||
// 空串 = 跟随内置默认(镜像表里的 gh-proxy.com,且只有镜像优先/国内判定才启用);
|
|
||||||
// `off` = 明确关掉;其余非空值 = 用这个前缀覆盖。
|
|
||||||
.with(SettingSpec::text(
|
|
||||||
"download.gh_proxy",
|
|
||||||
"",
|
|
||||||
256,
|
|
||||||
AiPolicy::sensitive(),
|
|
||||||
"GitHub 下载的加速前缀(gh-proxy)。留空 = 跟随内置默认(镜像表里的 gh-proxy.com,仅镜像优先/国内判定时启用);填 off 关闭;填一个 URL 前缀则覆盖它",
|
|
||||||
))
|
|
||||||
.with(SettingSpec::path(
|
.with(SettingSpec::path(
|
||||||
"java.selected_path",
|
"java.selected_path",
|
||||||
AiPolicy::sensitive(),
|
AiPolicy::sensitive(),
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
//! 代理与 gh-proxy 三键(M197 从 `keys.rs` 拆出:那边正好贴着 400 行硬线)。
|
||||||
|
//!
|
||||||
|
//! 三个键是一件事的两层:前两个是 **HTTP 代理**(整条请求走代理),第三个是 **gh-proxy 前缀**
|
||||||
|
//! (只把 GitHub 上的 URL 改写成 `<prefix><url>`,其余流量直连)。放在一起是因为用户问的往往是
|
||||||
|
//! 「我下不动 GitHub 怎么办」,答案要么在这三个里面、要么在镜像表里。
|
||||||
|
//!
|
||||||
|
//! | 键 | 语义 | 谁消费 |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | `download.proxy_mode` | `none` / `system` / `manual` | 引擎的 HTTP 客户端(`http::build_client`) |
|
||||||
|
//! | `download.proxy_url` | manual 模式的代理地址 | 同上 |
|
||||||
|
//! | `download.gh_proxy` | 空 = 跟随内置默认;`off` = 关;其它 = 覆盖前缀 | `app-iced` 的 JDK 下载作业(M197),其它下载走同一份 `region.json` |
|
||||||
|
//!
|
||||||
|
//! `download.gh_proxy` 的语义细节在 `core::download::region`:**空串不等于关**——空串是「没有
|
||||||
|
//! 覆盖」,仍按地区判定与内置镜像表走(国内判成镜像优先时用 `gh-proxy.com`)。要关必须写 `off`,
|
||||||
|
//! 因为「我不想用加速」和「我还没决定」是两件事。
|
||||||
|
|
||||||
|
use crate::setting::spec::{AiPolicy, SettingSpec};
|
||||||
|
|
||||||
|
use super::PROXY_MODES;
|
||||||
|
|
||||||
|
/// 三个键的完整登记项(主表一行 `.with_all(proxy::specs())`)。
|
||||||
|
#[must_use]
|
||||||
|
pub fn specs() -> Vec<SettingSpec> {
|
||||||
|
vec![
|
||||||
|
SettingSpec::enumeration(
|
||||||
|
"download.proxy_mode",
|
||||||
|
"none",
|
||||||
|
PROXY_MODES,
|
||||||
|
AiPolicy::sensitive(),
|
||||||
|
"代理模式:none / system / manual",
|
||||||
|
),
|
||||||
|
SettingSpec::text(
|
||||||
|
"download.proxy_url",
|
||||||
|
"",
|
||||||
|
512,
|
||||||
|
AiPolicy::sensitive(),
|
||||||
|
"manual 代理地址(如 http://127.0.0.1:7890)",
|
||||||
|
),
|
||||||
|
// M197:gh-proxy 前缀的用户覆盖(任务书那句「(默认值可改)」的落点)。
|
||||||
|
SettingSpec::text(
|
||||||
|
"download.gh_proxy",
|
||||||
|
"",
|
||||||
|
256,
|
||||||
|
AiPolicy::sensitive(),
|
||||||
|
"GitHub 下载的加速前缀(gh-proxy)。留空 = 跟随内置默认(镜像表里的 gh-proxy.com,仅镜像优先/国内判定时启用);填 off 关闭;填一个 URL 前缀则覆盖它",
|
||||||
|
),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
/// 三个键都在、且**空串 ≠ 关**这条语义写在描述里(M197 复审要求它与行为一致)。
|
||||||
|
///
|
||||||
|
/// 变异证明:把 `gh_proxy` 的描述改成「留空 = 关闭」⇒ 本用例红(实现里空串是「跟随内置默认」,
|
||||||
|
/// `downloads::engine::gh_proxy_from("")` 返回 `Builtin` 而不是 `Off`)。
|
||||||
|
#[test]
|
||||||
|
fn the_gh_proxy_description_says_empty_is_not_off() {
|
||||||
|
let specs = specs();
|
||||||
|
let ids: Vec<&str> = specs.iter().map(|spec| spec.key).collect();
|
||||||
|
assert_eq!(
|
||||||
|
ids,
|
||||||
|
vec![
|
||||||
|
"download.proxy_mode",
|
||||||
|
"download.proxy_url",
|
||||||
|
"download.gh_proxy"
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let gh = specs
|
||||||
|
.iter()
|
||||||
|
.find(|spec| spec.key == "download.gh_proxy")
|
||||||
|
.expect("gh_proxy 已登记");
|
||||||
|
assert!(gh.doc.contains("留空 = 跟随内置默认"), "{}", gh.doc);
|
||||||
|
assert!(gh.doc.contains("填 off 关闭"), "{}", gh.doc);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
//! `SourceFailed.kind` 的另一半(M197 复审 P2-2):**404 是「该源没有这个文件」**,不是「源坏了」。
|
||||||
|
//!
|
||||||
|
//! 两档在引擎里的后果不同:`NotFound` 走 `record_missing`(不冷却源),`ServerError` 走
|
||||||
|
//! `record_failure`。院校镜像只留最新补丁,请求旧补丁必然 404——判成「源坏了」会把唯一可用镜像
|
||||||
|
//! 冷却掉(M33/M34 实测)。
|
||||||
|
//!
|
||||||
|
//! 变异证明:把 `engine.rs` / `bytes.rs` 的 `kind: err.kind` 改成常量 `Network` ⇒ 本条与
|
||||||
|
//! `reports_progress_events_and_errors_when_all_sources_fail` 一起红。
|
||||||
|
//!
|
||||||
|
//! 拆成子模块:`engine_mock_test.rs` 加了这条之后 407 行,越过 400 行硬线(`guards.yml` 的
|
||||||
|
//! `file-size`),于是把整条用例搬出来。
|
||||||
|
|
||||||
|
use std::sync::Arc;
|
||||||
|
|
||||||
|
use unlucky_core::download::cache::ContentCache;
|
||||||
|
use unlucky_core::download::engine::DownloadEngine;
|
||||||
|
use unlucky_core::download::error::DownloadErrorKind;
|
||||||
|
use unlucky_core::download::health::HealthRegistry;
|
||||||
|
use unlucky_core::download::mirrors::MirrorTable;
|
||||||
|
use unlucky_core::download::progress::{ProgressEmitter, ProgressEvent};
|
||||||
|
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
||||||
|
|
||||||
|
use super::support::{temp_dir, MockResponse, MockServer};
|
||||||
|
use super::{direct_region, fast_config};
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_404_is_classified_as_not_found() {
|
||||||
|
let server = MockServer::start(vec![
|
||||||
|
MockResponse::status(404),
|
||||||
|
MockResponse::status(404),
|
||||||
|
MockResponse::status(404),
|
||||||
|
MockResponse::status(404),
|
||||||
|
])
|
||||||
|
.await;
|
||||||
|
let url = server.url("/gone.jar");
|
||||||
|
let dir = temp_dir("engine-404");
|
||||||
|
let (emitter, mut events) = ProgressEmitter::channel();
|
||||||
|
let engine = DownloadEngine::new(
|
||||||
|
fast_config(1),
|
||||||
|
Arc::new(MirrorTable::builtin()),
|
||||||
|
Arc::new(direct_region()),
|
||||||
|
Arc::new(HealthRegistry::in_memory()),
|
||||||
|
Arc::new(ContentCache::open(dir.join("cache"))),
|
||||||
|
)
|
||||||
|
.with_progress(emitter);
|
||||||
|
engine
|
||||||
|
.download(DownloadRequest::new(
|
||||||
|
ResourceKind::Library,
|
||||||
|
url.as_str(),
|
||||||
|
dir.join("gone.jar"),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.expect_err("只有 404 的源必须失败");
|
||||||
|
|
||||||
|
let mut seen: Vec<DownloadErrorKind> = Vec::new();
|
||||||
|
while let Ok(event) = events.try_recv() {
|
||||||
|
if let ProgressEvent::SourceFailed { kind, .. } = event {
|
||||||
|
seen.push(kind);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert!(!seen.is_empty(), "至少要有一条 SourceFailed");
|
||||||
|
assert!(
|
||||||
|
seen.iter().all(|kind| *kind == DownloadErrorKind::NotFound),
|
||||||
|
"404 必须分类成 NotFound,实际:{seen:?}"
|
||||||
|
);
|
||||||
|
server.stop();
|
||||||
|
}
|
||||||
@@ -359,56 +359,6 @@ async fn reports_progress_events_and_errors_when_all_sources_fail() {
|
|||||||
server.stop();
|
server.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `SourceFailed.kind` 的另一半:**404 不是「源坏了」**,是「该源没有这个文件」。
|
/// 「404 不是源坏了」的守卫(M197 P2-2 的第二档):拆到子模块守 400 行硬线。
|
||||||
///
|
#[path = "engine_mock/not_found.rs"]
|
||||||
/// 这两档在引擎里的后果完全不同(`NotFound` 走 `record_missing`,不冷却源;`ServerError` 走
|
mod not_found;
|
||||||
/// `record_failure`)。把两者混成一个常量最贵的地方就在这里:院校镜像只留最新补丁,请求旧补丁
|
|
||||||
/// 必然 404,判成「源坏了」会把唯一可用镜像冷却掉(M33/M34 实测)。
|
|
||||||
///
|
|
||||||
/// 变异证明:把 `engine.rs` / `bytes.rs` 两处 `kind: err.kind` 改成 `DownloadErrorKind::NotFound`
|
|
||||||
/// 常量 ⇒ 上面那条 500 用例红;改成 `Network` ⇒ 本条红。
|
|
||||||
#[tokio::test]
|
|
||||||
async fn a_404_is_classified_as_not_found_and_never_cools_the_source_down() {
|
|
||||||
let server = MockServer::start(vec![
|
|
||||||
MockResponse::status(404),
|
|
||||||
MockResponse::status(404),
|
|
||||||
MockResponse::status(404),
|
|
||||||
MockResponse::status(404),
|
|
||||||
])
|
|
||||||
.await;
|
|
||||||
let url = server.url("/gone.jar");
|
|
||||||
let dir = temp_dir("engine-404");
|
|
||||||
let cache = Arc::new(ContentCache::open(dir.join("cache")));
|
|
||||||
let (emitter, mut events) = ProgressEmitter::channel();
|
|
||||||
let engine = DownloadEngine::new(
|
|
||||||
fast_config(1),
|
|
||||||
Arc::new(MirrorTable::builtin()),
|
|
||||||
Arc::new(direct_region()),
|
|
||||||
Arc::new(HealthRegistry::in_memory()),
|
|
||||||
cache,
|
|
||||||
)
|
|
||||||
.with_progress(emitter);
|
|
||||||
|
|
||||||
let error = engine
|
|
||||||
.download(DownloadRequest::new(
|
|
||||||
ResourceKind::Library,
|
|
||||||
url.as_str(),
|
|
||||||
dir.join("gone.jar"),
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.expect_err("只有 404 的源必须失败");
|
|
||||||
assert_eq!(error.kind, DownloadErrorKind::AllSourcesFailed);
|
|
||||||
|
|
||||||
let mut seen: Vec<DownloadErrorKind> = Vec::new();
|
|
||||||
while let Ok(event) = events.try_recv() {
|
|
||||||
if let ProgressEvent::SourceFailed { kind, .. } = event {
|
|
||||||
seen.push(kind);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
assert!(!seen.is_empty(), "至少要有一条 SourceFailed");
|
|
||||||
assert!(
|
|
||||||
seen.iter().all(|kind| *kind == DownloadErrorKind::NotFound),
|
|
||||||
"404 必须分类成 NotFound,实际:{seen:?}"
|
|
||||||
);
|
|
||||||
server.stop();
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user