feat(m203): CLI 三条下载命令改吃 download.* 设置(并发/重试/gh-proxy)
`ull install` / `ull java` / `ull pack` 此前各自 `DownloadEngine::new(EngineConfig::default(), …)`: 用户在设置里配的 `download.concurrency` / `retry_limit` / `gh_proxy` 一概不生效;M193 把出厂并发 从 64 改成 8 之后,它们还静默跟着降了。现在三处都经 `crates/cli/src/download.rs::engine(ctx)`: - core:`shared/tuned.rs` 新增 `engine_with_tuning(cache_root, tune)` = `set_tuning` + `engine_for`, 由 `shared.rs` 重导出;`engine_for` 的签名与行为一字未改(presenter 的两步语义原样保留)。 放子模块是为了不动 `engine.rs`(那是 400 行零余量),`shared.rs` 仍 398 行。 - CLI:`download::tuning(ctx)` 用 `SettingStore::load(ctx.launcher_config(), launcher_schema())` 读 三个键(与 `ull config set` 同一份文件),默认值/夹取/回落在 `DownloadTuning::from_settings`; 键缺失或坏值静默回落出厂值,`engine()` 没有错误通道,安装不会因此中断。 - 顺带:`ull update` 复用 `install::build_engine`,也随之吃到设置。 - 测试:`crates/cli/src/download/tests.rs` 三条命令各一条(读 `config().max_concurrency` 与 `concurrency_probe()` 的真实值)+ 一条「缺失/越界/负数/类型不对 ⇒ 静默回落出厂值」; `shared/tuned.rs` 两条守住「灌调优与取引擎是一步」。变异实测:pack 换回裸 `EngineConfig::default()` ⇒ 只有 pack 那条红(left 8 / right 2)。
This commit is contained in:
1 parent
c97630d59e
commit
b31ca636d1
9 files changed
+368
-81
No files matched your search
@@ -0,0 +1,203 @@
|
||||
//! [`super`] 的单测(M203):`launcher.json` 的 `download.*` 真的进到三条命令拿到的引擎里。
|
||||
//!
|
||||
//! 断言读的是**引擎的真实字段**——`config().max_concurrency` 与 `Semaphore` 的
|
||||
//! `available_permits()`(`concurrency_probe`)——不是「函数被调用过」:后者在回落写错、
|
||||
//! 或者命令又自建了一个裸引擎时照样绿。
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
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,
|
||||
};
|
||||
use unlucky_core::download::DownloadEngine;
|
||||
use unlucky_core::setting::keys::launcher_schema;
|
||||
use unlucky_core::setting::store::SettingStore;
|
||||
|
||||
use crate::cli::Globals;
|
||||
use crate::context::Ctx;
|
||||
|
||||
/// 一次性临时目录(cli 没有 dev-dependency 的 tempfile;与 `commands/config/tests.rs` 同一手法)。
|
||||
struct TempDir(PathBuf);
|
||||
|
||||
impl TempDir {
|
||||
fn new(tag: &str) -> Self {
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let nanos = SystemTime::now()
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.map_or(0, |duration| duration.as_nanos());
|
||||
let seq = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
let path = std::env::temp_dir().join(format!(
|
||||
"ull-cli-download-{tag}-{}-{seq}-{nanos}",
|
||||
std::process::id()
|
||||
));
|
||||
std::fs::create_dir_all(&path).expect("建临时目录");
|
||||
Self(path)
|
||||
}
|
||||
|
||||
fn path(&self) -> &Path {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TempDir {
|
||||
fn drop(&mut self) {
|
||||
let _ = std::fs::remove_dir_all(&self.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// 数据根钉在临时目录里:`launcher.json` 落在 `<dir>/.ULL/config/launcher.json`。
|
||||
fn ctx_at(dir: &TempDir) -> Ctx {
|
||||
let globals = Globals {
|
||||
data_dir: Some(dir.path().to_path_buf()),
|
||||
..Globals::default()
|
||||
};
|
||||
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();
|
||||
std::fs::create_dir_all(path.parent().expect("配置目录")).expect("建配置目录");
|
||||
std::fs::write(&path, serde_json::to_vec(&document).expect("序列化"))
|
||||
.expect("写 launcher.json");
|
||||
}
|
||||
|
||||
/// 「引擎真的按这个并发跑」:配置字段 + 闸门许可数,两处都读真实值。
|
||||
fn assert_concurrency(engine: &DownloadEngine, expected: usize, case: &str) {
|
||||
assert_eq!(
|
||||
engine.config().max_concurrency,
|
||||
expected,
|
||||
"{case}:`EngineConfig` 里的并发"
|
||||
);
|
||||
assert_eq!(
|
||||
engine.concurrency_probe(),
|
||||
expected,
|
||||
"{case}:`Semaphore` 真实许可数"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`install` 吃设置**:`download.concurrency` 改了就变;同一次进程里再改一次也立刻变(不是只读一次)。
|
||||
///
|
||||
/// 抓得住的变异:`commands::install::build_engine` 换回裸 `EngineConfig::default()` ⇒ 3 / 5 两处红。
|
||||
/// 抓不住的:真实下载时「只有 3 条连接在飞」的时序(离线验不了,见报告「没验的部分」)。
|
||||
#[test]
|
||||
fn install_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
let dir = TempDir::new("install");
|
||||
let ctx = ctx_at(&dir);
|
||||
// 没有 launcher.json = 没配过 ⇒ 出厂值(这也是「回落」的第一档)。
|
||||
assert_concurrency(
|
||||
&crate::commands::install::build_engine(&ctx),
|
||||
DEFAULT_MAX_CONCURRENCY,
|
||||
"未配置",
|
||||
);
|
||||
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 3 } }));
|
||||
assert_concurrency(
|
||||
&crate::commands::install::build_engine(&ctx),
|
||||
3,
|
||||
"concurrency=3",
|
||||
);
|
||||
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 5 } }));
|
||||
assert_concurrency(
|
||||
&crate::commands::install::build_engine(&ctx),
|
||||
5,
|
||||
"concurrency=5",
|
||||
);
|
||||
}
|
||||
|
||||
/// **`java` 吃设置**:并发与每个源的尝试次数都进引擎。
|
||||
///
|
||||
/// 抓得住的变异:`commands::java::build_engine` 换回裸 `EngineConfig::default()` ⇒ 探针停在 8、次数停在 3,红。
|
||||
#[test]
|
||||
fn java_engine_takes_the_concurrency_and_retry_from_the_setting_store() {
|
||||
let dir = TempDir::new("java");
|
||||
let ctx = ctx_at(&dir);
|
||||
write_launcher_config(
|
||||
&ctx,
|
||||
json!({ "download": { "concurrency": 3, "retry_limit": 4 } }),
|
||||
);
|
||||
|
||||
let engine = crate::commands::java::build_engine(&ctx);
|
||||
assert_concurrency(&engine, 3, "concurrency=3");
|
||||
assert_eq!(
|
||||
engine.config().max_attempts_per_source,
|
||||
4,
|
||||
"retry_limit=4 要真的落到引擎上(出厂是 3)"
|
||||
);
|
||||
}
|
||||
|
||||
/// **`pack` 吃设置**(三条命令各有一条独立断言:把任意一处改回裸引擎,只有那一条红)。
|
||||
///
|
||||
/// 抓得住的变异:`commands::pack::build_engine` 换回裸 `EngineConfig::default()` ⇒ 2 处红。
|
||||
#[test]
|
||||
fn pack_engine_takes_the_concurrency_from_the_setting_store() {
|
||||
let dir = TempDir::new("pack");
|
||||
let ctx = ctx_at(&dir);
|
||||
write_launcher_config(&ctx, json!({ "download": { "concurrency": 2 } }));
|
||||
assert_concurrency(
|
||||
&crate::commands::pack::build_engine(&ctx),
|
||||
2,
|
||||
"concurrency=2",
|
||||
);
|
||||
}
|
||||
|
||||
/// **反向断言**:键缺失 / 越界 / 负数 / 类型不对 ⇒ 回落出厂值,而且回落是**静默的**。
|
||||
///
|
||||
/// 「静默」在这里是结构性的:`crate::download::engine` 的返回类型没有错误通道,坏设置只进
|
||||
/// `SettingStore::problems()`,调用方(安装流程)连一个 `?` 都不用加——`ull install` 不会因为一条
|
||||
/// 写坏的设置而中断,只是按出厂值跑。
|
||||
///
|
||||
/// 抓得住的变异:删掉 `DownloadTuning::from_settings` 的区间过滤 ⇒ 9_999 那条把许可数变成 64,红;
|
||||
/// 把 CLI 的读数改成 `expect`/报错 ⇒ 坏值文件这几档 panic 或编译不过,红。
|
||||
/// 抓不住的:`problems()` 的内容与措辞(那是 core 的 `setting` 模块,本用例只借它证明文件真的坏)。
|
||||
#[test]
|
||||
fn missing_or_illegal_values_fall_back_to_the_factory_defaults_silently() {
|
||||
let dir = TempDir::new("fallback");
|
||||
let ctx = ctx_at(&dir);
|
||||
assert_eq!(
|
||||
super::tuning(&ctx),
|
||||
DownloadTuning::default(),
|
||||
"没有 launcher.json 就该是出厂调优"
|
||||
);
|
||||
|
||||
for (case, document) in [
|
||||
(
|
||||
"越界",
|
||||
json!({ "download": { "concurrency": 9_999, "retry_limit": 99 } }),
|
||||
),
|
||||
(
|
||||
"负数",
|
||||
json!({ "download": { "concurrency": -4, "retry_limit": -1 } }),
|
||||
),
|
||||
(
|
||||
"类型不对",
|
||||
json!({ "download": { "concurrency": "八", "retry_limit": "很多" } }),
|
||||
),
|
||||
] {
|
||||
write_launcher_config(&ctx, document);
|
||||
// 先证明这份文件**真的**是坏的(否则「回落」只是键不存在,用例就成了空断言)。
|
||||
let store = SettingStore::load(ctx.launcher_config(), launcher_schema());
|
||||
assert!(
|
||||
!store.problems().is_empty(),
|
||||
"{case}:这份文件该被判为有问题"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
super::tuning(&ctx),
|
||||
DownloadTuning::default(),
|
||||
"{case}:坏值必须回落出厂调优"
|
||||
);
|
||||
let engine = crate::commands::install::build_engine(&ctx);
|
||||
assert_concurrency(&engine, DEFAULT_MAX_CONCURRENCY, case);
|
||||
assert_eq!(
|
||||
engine.config().max_attempts_per_source,
|
||||
DEFAULT_MAX_ATTEMPTS_PER_SOURCE,
|
||||
"{case}:尝试次数也回落出厂值"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user