fix(m203): 给第 4 条吃设置的路径(ull update)补打点,并清掉 KEY_GH_PROXY 的腐化 doc
复审 r1 P2-1(p2-1items / fix-then-merge):`ull update` 复用 `install::build_engine` 后成了第 4 条 吃设置的路径,但没有任何用例钉它——把 update 换回裸引擎,四条 download::tests 全绿。按 pack 的完全 同形接缝补上: - `commands/update.rs` 新增 `pub(crate) fn engine(ctx)`(`job()` 改调它),doc 写明这条路径上并发 不可观测(自更新只下一个 artifact),随设置变的是每源重试次数与 gh-proxy 前缀; - `download/tests.rs` 新增 `update_engine_takes_the_concurrency_from_the_setting_store`: 写 concurrency=4 ⇒ 断言 `config().max_concurrency` 与 `concurrency_probe()` 都是 4。 顺带收掉复审的三条观察: 1. 新增 `the_gh_proxy_setting_reaches_the_candidate_chain`:镜像优先地区档案下,出厂(空串 = 跟随 内置)确实生成 gh-proxy 候选;手写 `gh_proxy: "off"` 后候选里没有 gh-proxy(读的是 `plan()` 的 候选链,不是配置值;两段式,避免「本来就没有候选」的白绿)。 2. 测试写侧改用 `ctx.setting_path_for(KEY_CONCURRENCY)`(`ull config set` 用的那个表达式),不再与 实现读侧的 `launcher_config()` 共用同一个表达式——实现改读别的文件时用例会红而不是一起漂移。 3. `core/download/config.rs` 的 `KEY_GH_PROXY` doc 已腐化(写「暂未登记、M197 才登记、别删这条弱 断言」,而该键在 main 上就已登记、本文件根本没有针对该常量的断言):改成事实,并保留「弱在哪」 ——常量值与 schema 登记没被断言绑在一起,改名只会让宿主侧静默退化成 Builtin。 变异验证(只改实现侧,逐次还原): - update::engine 换回裸 `EngineConfig::default()` ⇒ 只有 update 那条红(left 8 / right 4),另五条绿; - `download::tuning` 不读 `KEY_GH_PROXY`(传 None)⇒ 只有 gh_proxy 那条红(off 档仍有 `https://gh-proxy.com/...` 候选),另五条绿; - `gh_proxy_override("")` 返回 Off ⇒ gh_proxy 那条的「出厂该套内置前缀」与回落用例的 `tuning == default` 同时红(两处都该红)。
This commit is contained in:
1 parent
b31ca636d1
commit
7995f62908
4 files changed
+95
-19
No files matched your search
@@ -34,8 +34,10 @@
|
||||
//! 去掉 `--dest` 会把它真改名换掉(`crates/cli/tests/update_e2e_test.rs` 的注释写了这件事)。
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use serde_json::json;
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
use unlucky_core::update::{default_source, Applied, CheckOutcome, Job, UpdateError, Version};
|
||||
|
||||
use crate::cli::{UpdateApplyArgs, UpdateCheckArgs};
|
||||
@@ -53,9 +55,19 @@ fn current_version() -> Result<Version, CliError> {
|
||||
.map_err(|error| CliError::internal("bad_version", format!("编译期版本号不合法:{error}")))
|
||||
}
|
||||
|
||||
/// 自更新的下载引擎:与 `install` / `java` / `pack` 同一条装配(参数来自
|
||||
/// [`crate::download`],设置读数与落点在那边)。
|
||||
///
|
||||
/// 自更新只下**一个** artifact(`update::github` 的 `fetch_artifact` → 单次 `engine.download`),
|
||||
/// 所以并发的差别在这条路径上不可观测;真正随设置变的是**每源重试次数**与**gh-proxy 前缀**——
|
||||
/// 那正是「设置说真话」的两维。单独留这个接缝是为了让用例能逐命令打点(变异时只有它红)。
|
||||
pub(crate) fn engine(ctx: &Ctx) -> Arc<DownloadEngine> {
|
||||
super::install::build_engine(ctx)
|
||||
}
|
||||
|
||||
/// 造一个内核 job(源 = GitHub / `ULL_UPDATE_DIR`,暂存 = 数据根缓存,目标 = 本进程 exe)。
|
||||
fn job(ctx: &Ctx, dest: Option<PathBuf>) -> Job {
|
||||
let engine = super::install::build_engine(ctx);
|
||||
let engine = engine(ctx);
|
||||
let job = Job::for_process(default_source(engine), &ctx.layout().cache());
|
||||
match dest {
|
||||
Some(dest) => job.with_dest(dest),
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
//! CLI 侧的下载引擎:把 CLI 自己的 `launcher.json` 接到 core 的下载调优上(M203)。
|
||||
//!
|
||||
//! 三条下载命令(`install` / `java` / `pack`)此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
|
||||
//! `install` / `java` / `pack` 三条命令此前各自 `DownloadEngine::new(EngineConfig::default(), …)`:
|
||||
//! 用户在设置里配的 `download.concurrency` / `download.retry_limit` / `download.gh_proxy` 一概不生效。
|
||||
//! M193 把出厂并发从 64 改成 8 之后,它们还**静默**跟着降了——「设置项说假话」的同期病灶
|
||||
//! (`docs/download-settings.md` §3.1 记着这三条 ❌)。
|
||||
//! (`docs/download-settings.md` §3.1 记着这三条 ❌)。第四条路径 `update`(自更新)复用 `install`
|
||||
//! 的装配,也随之吃设置;它只下一个 artifact,所以并发在那条路径上不可观测。
|
||||
//!
|
||||
//! 现在三处一律经 [`engine`] 取引擎(进程内共享,见 `core::download::shared`):
|
||||
//! 现在四条一律经 [`engine`] 取引擎(进程内共享,见 `core::download::shared`):
|
||||
//!
|
||||
//! ```text
|
||||
//! <数据根>/config/launcher.json(与 `ull config set` 同一份) → setting store → DownloadTuning
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! [`super`] 的单测(M203):`launcher.json` 的 `download.*` 真的进到三条命令拿到的引擎里。
|
||||
//! [`super`] 的单测(M203):`launcher.json` 的 `download.*` 真的进到四条命令拿到的引擎里
|
||||
//! (`install` / `java` / `pack` / `update`——`update` 复用 `install` 的装配,自更新只下一个
|
||||
//! artifact,所以并发在那条路径上不可观测,随设置变的是每源重试次数与 gh-proxy 前缀)。
|
||||
//!
|
||||
//! 断言读的是**引擎的真实字段**——`config().max_concurrency` 与 `Semaphore` 的
|
||||
//! `available_permits()`(`concurrency_probe`)——不是「函数被调用过」:后者在回落写错、
|
||||
//! 或者命令又自建了一个裸引擎时照样绿。
|
||||
//! 或者命令又自建了一个裸引擎时照样绿。gh-proxy 那一条断言的是**候选链**(真行为),不是配置值。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@@ -10,8 +12,9 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use unlucky_core::download::config::{
|
||||
DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY,
|
||||
DownloadTuning, DEFAULT_MAX_ATTEMPTS_PER_SOURCE, DEFAULT_MAX_CONCURRENCY, KEY_CONCURRENCY,
|
||||
};
|
||||
use unlucky_core::download::types::{DownloadRequest, ResourceKind};
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
use unlucky_core::setting::keys::launcher_schema;
|
||||
use unlucky_core::setting::store::SettingStore;
|
||||
@@ -57,13 +60,17 @@ fn ctx_at(dir: &TempDir) -> Ctx {
|
||||
Ctx::resolve(&globals).expect("解析数据根")
|
||||
}
|
||||
|
||||
/// 直接写 `launcher.json`——**与 `ull config set` 的落点是同一份文件**(`ctx.launcher_config()`),
|
||||
/// 所以这条用例测的就是用户手改/命令改之后 CLI 读到的值。
|
||||
fn write_launcher_config(ctx: &Ctx, document: Value) {
|
||||
let path = ctx.launcher_config();
|
||||
/// 直接写 `launcher.json`——落点用 [`Ctx::setting_path_for`](`ull config set` 用的那个表达式),
|
||||
/// **不是**实现读侧用的 `ctx.launcher_config()`。
|
||||
///
|
||||
/// 两个表达式今天指向同一份文件(`download.*` 不是数据根键 ⇒ 回落 `launcher_config()`),但写侧
|
||||
/// 走「用户改配置那条路」的 API,实现哪天改读别的文件时这条用例会红,而不是跟着一起漂移。
|
||||
fn write_launcher_config(ctx: &Ctx, document: Value) -> PathBuf {
|
||||
let path = ctx.setting_path_for(KEY_CONCURRENCY);
|
||||
std::fs::create_dir_all(path.parent().expect("配置目录")).expect("建配置目录");
|
||||
std::fs::write(&path, serde_json::to_vec(&document).expect("序列化"))
|
||||
.expect("写 launcher.json");
|
||||
path
|
||||
}
|
||||
|
||||
/// 「引擎真的按这个并发跑」:配置字段 + 闸门许可数,两处都读真实值。
|
||||
@@ -131,7 +138,7 @@ fn java_engine_takes_the_concurrency_and_retry_from_the_setting_store() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **`pack` 吃设置**(三条命令各有一条独立断言:把任意一处改回裸引擎,只有那一条红)。
|
||||
/// **`pack` 吃设置**(四条命令各有一条独立断言:把任意一处改回裸引擎,只有那一条红)。
|
||||
///
|
||||
/// 抓得住的变异:`commands::pack::build_engine` 换回裸 `EngineConfig::default()` ⇒ 2 处红。
|
||||
#[test]
|
||||
@@ -146,6 +153,62 @@ fn pack_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
);
|
||||
}
|
||||
|
||||
/// **`update` 吃设置**(第 4 条路径,`commands/update.rs::engine`):自更新也只拿出厂值是不行的。
|
||||
///
|
||||
/// 这条路径上「并发」实际不可观测(自更新只下**一个** artifact,闸门开 1 还是 8 都只有一条连接),
|
||||
/// 但引擎参数是同一处装配出来的,用并发打点最省事;真正随设置变的是每源重试次数与 gh-proxy 前缀。
|
||||
/// 抓得住的变异:`commands::update::engine` 换回裸 `EngineConfig::default()` ⇒ 4 ≠ 8 处红,另五条绿。
|
||||
#[test]
|
||||
fn update_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
let dir = TempDir::new("update");
|
||||
let ctx = ctx_at(&dir);
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 4 } }));
|
||||
assert_concurrency(&crate::commands::update::engine(&ctx), 4, "concurrency=4");
|
||||
}
|
||||
|
||||
/// **`gh_proxy` 的 CLI 侧闭环**:手写 `"off"` ⇒ 候选链里真的没有 gh-proxy(读的是 `plan()`,不是配置值)。
|
||||
///
|
||||
/// 两段式:先在镜像优先的地区档案下确认**出厂**(空串 = 跟随内置)确实生成 gh-proxy 候选,再把它
|
||||
/// 改成 `off` 确认候选消失——只断言后者的话,「本来就没有候选」会让用例白绿。
|
||||
/// 抓得住的变异:`download::tuning` 不读 `KEY_GH_PROXY`(传 `None`)⇒ 第二段仍有 gh-proxy 候选,红;
|
||||
/// 把空串当成关(`gh_proxy_override("")` 返回 `Off`)⇒ 第一段没有候选,红(回落用例的
|
||||
/// `tuning == default` 同时红——两处都该红,实测如此)。
|
||||
/// 抓不住的:真实网络下这个前缀能不能通(离线只验候选链的**生成**)。
|
||||
#[test]
|
||||
fn the_gh_proxy_setting_reaches_the_candidate_chain() {
|
||||
use unlucky_core::download::cache::now_ms;
|
||||
use unlucky_core::download::region::{store_cached, RegionProfile};
|
||||
|
||||
let dir = TempDir::new("ghproxy");
|
||||
let ctx = ctx_at(&dir);
|
||||
// gh-proxy 只在「镜像优先」的地区档案下生成候选:先把该数据根的 region.json 钉住。
|
||||
let cache_root = ctx.layout().cache();
|
||||
store_cached(
|
||||
&cache_root.join("region.json"),
|
||||
&RegionProfile::mirror_first(now_ms()),
|
||||
)
|
||||
.expect("地区档案");
|
||||
let url = "https://github.com/o/r/releases/download/v1/a.jar";
|
||||
let chain = |ctx: &Ctx| -> Vec<String> {
|
||||
let request =
|
||||
DownloadRequest::new(ResourceKind::Other, url, ctx.layout().cache().join("a.jar"));
|
||||
crate::download::engine(ctx)
|
||||
.plan(&request, 0)
|
||||
.into_iter()
|
||||
.map(|candidate| candidate.url)
|
||||
.collect()
|
||||
};
|
||||
let has_gh_proxy = |chain: &[String]| chain.iter().any(|url| url.contains("gh-proxy"));
|
||||
|
||||
// 出厂(未配置):空串 = 跟随内置默认 ⇒ 有 gh-proxy 候选。
|
||||
assert!(has_gh_proxy(&chain(&ctx)), "出厂该套内置前缀");
|
||||
// 写 off:候选里不该再有 gh-proxy(其余候选仍在)。
|
||||
write_launcher_config(&ctx, json!({ "download": { "gh_proxy": "off" } }));
|
||||
let off = chain(&ctx);
|
||||
assert!(!has_gh_proxy(&off), "off 档还有 gh-proxy 候选:{off:?}");
|
||||
assert!(!off.is_empty(), "关掉加速不等于没有候选:{off:?}");
|
||||
}
|
||||
|
||||
/// **反向断言**:键缺失 / 越界 / 负数 / 类型不对 ⇒ 回落出厂值,而且回落是**静默的**。
|
||||
///
|
||||
/// 「静默」在这里是结构性的:`crate::download::engine` 的返回类型没有错误通道,坏设置只进
|
||||
@@ -179,9 +242,9 @@ fn missing_or_illegal_values_fall_back_to_the_factory_defaults_silently() {
|
||||
json!({ "download": { "concurrency": "八", "retry_limit": "很多" } }),
|
||||
),
|
||||
] {
|
||||
write_launcher_config(&ctx, document);
|
||||
let written = write_launcher_config(&ctx, document);
|
||||
// 先证明这份文件**真的**是坏的(否则「回落」只是键不存在,用例就成了空断言)。
|
||||
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
|
||||
let store = SettingStore::load(&written, launcher_schema());
|
||||
assert!(
|
||||
!store.problems().is_empty(),
|
||||
"{case}:这份文件该被判为有问题"
|
||||
|
||||
Reference in new issue
Block a user