用户原话:「我们内置了那么多镜像源 甚至用户不能手动选择指定版本下载。」
「自动下载 jdk17 作为基准版本,后续在设置中提供多版本可供用户下载。」
- core:新增 `game::java::catalog`(8/11/17/21/25 五档 × 能承接的发行版 × 「该装哪档」),
数据全部来自 `JdkVendor::supports` 与 `vendor::order`,不复制第二份真相。
- progress:`SourceFailed` 补一个结构化的 `kind`(在这之前只有中文 `reason`,双语界面
要么放弃语言、要么放弃「哪个源为什么退避」这条用户可见信息)。
- app-iced:`downloads` 模块真下(后台线程 + `ProgressEmitter` + 快照),三态(选择列表 /
下载中 / 失败)都能说出「命中的源 / 退避原因 / 试过哪些源」;成功后重扫并(在
`java.auto_install` 开着且没有默认 JDK 时)把新装的那份设为默认。
- 三个设置键做成可改 UI:`java.installRoot` / `java.vendorPriority` / `java.auto_install`,
各自带「现在生效什么、从哪一层来」(数据目录模式 / 地区默认序),改完回读落盘值。
- `State::satisfied` 换成带方向的 `Fit`:1.12.2 + Java 21 不再被说成「低于要求」。
- i18n:删掉 `SettingsJavaEmptyBody` / `SettingsJavaAddHint` 的 markdown 反引号;
`SettingsJavaNeverScannedBody` 改成不与首屏自动扫矛盾的表述。
- 出图三张(选择列表 / 下载中 / 失败)在 DevDocs/artifacts/m197/。
顺带拆文件守 400 行:`golden/tail.rs` 的 M174 段搬到 `golden/tail_ai.rs`,
`pages/settings/java/{context,prefs,prefs_store,downloads,download_view}` 各自成模块。
246 lines
8.8 KiB
Rust
246 lines
8.8 KiB
Rust
//! 「Java」分组的真数据源断言(M175):扫描 / 手动路径 / 内存 / JVM 参数 / 真命令行。
|
||
//!
|
||
//! 每一条都**能变红**(报告的「变异证明」逐条给了改哪一行会红)。四条守卫:
|
||
//!
|
||
//! | 守卫 | 断言 | 改回什么会红 |
|
||
//! |---|---|---|
|
||
//! | 列表来自真扫盘 | [`the_runtime_list_comes_from_the_disk`] | 把行换成写死的字符串 / 不读候选表 |
|
||
//! | 非法路径不污染设置 | [`an_invalid_manual_path_is_refused_and_writes_nothing`] | 删掉 `manual::probe_manual` 的校验 |
|
||
//! | 内存越界被拒 | [`the_memory_bounds_are_enforced_before_writing`] | 删掉范围判断 / 允许非数字 |
|
||
//! | 内存真进命令行 | [`the_saved_memory_reaches_the_real_command_line`] | 把 `-Xmx` 的来源改回常量 |
|
||
//!
|
||
//! 前三条不依赖本机装没装 JDK(假的 `bin/java` 空文件就够:它扫得到、但答不出版本),
|
||
//! 第四条(`tests/cmdline.rs`)走 core 的**真组装链**(`launch::args_table::assemble`)——
|
||
//! 不是读配置回显。落盘 / 读回 / 版本门槛 / 截图种子也在那个文件里(同一族「真值」断言)。
|
||
|
||
#[path = "tests/cmdline.rs"]
|
||
mod cmdline;
|
||
|
||
use std::path::{Path, PathBuf};
|
||
|
||
use super::state::{Refusal, RuntimeRow, State, MAX_MEMORY_MB, MIN_MEMORY_MB};
|
||
use super::store;
|
||
|
||
/// 每个用例一个临时数据根(`<dir>/config/launcher.json` 的形状与真实数据根一致)。
|
||
pub(super) fn root(tag: &str) -> PathBuf {
|
||
let dir = std::env::temp_dir().join(format!(
|
||
"ull-java-page-{tag}-{}-{}",
|
||
std::process::id(),
|
||
std::time::SystemTime::now()
|
||
.duration_since(std::time::UNIX_EPOCH)
|
||
.map(|d| d.as_nanos())
|
||
.unwrap_or(0)
|
||
));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
std::fs::create_dir_all(dir.join("config")).expect("config");
|
||
dir
|
||
}
|
||
|
||
pub(super) fn config(dir: &Path) -> PathBuf {
|
||
dir.join("config").join("launcher.json")
|
||
}
|
||
|
||
/// 造一个「看起来像 JDK」的目录:`bin/java` 是空文件(扫得到、但 `-version` 答不出)。
|
||
pub(super) fn fake_jdk(dir: &Path, name: &str) -> PathBuf {
|
||
let jdk = dir.join(name);
|
||
std::fs::create_dir_all(jdk.join("bin")).expect("bin");
|
||
std::fs::write(jdk.join("bin").join(java_bin()), b"").expect("写 java");
|
||
jdk
|
||
}
|
||
|
||
pub(super) fn java_bin() -> &'static str {
|
||
if cfg!(windows) {
|
||
"java.exe"
|
||
} else {
|
||
"java"
|
||
}
|
||
}
|
||
|
||
// --------------------------------------------------------------- 守卫①:列表来自真扫盘
|
||
|
||
/// 扫描列表必须来自**盘上真有的目录**:造两个 → 两项;删一个 → 一项。
|
||
///
|
||
/// 变异证明:把 `store::start` 里的候选表换成一个写死的两行常量,删目录之后仍是两项 ⇒ 红。
|
||
#[test]
|
||
fn the_runtime_list_comes_from_the_disk() {
|
||
let dir = root("scan");
|
||
let one = fake_jdk(&dir, "jdk-a");
|
||
let two = fake_jdk(&dir, "jdk-b");
|
||
|
||
let found =
|
||
unlucky_core::game::java::staged::scan_roots(&[], &[one.clone(), two.clone()], false);
|
||
assert_eq!(found.len(), 2, "两个假 JDK 目录必须扫出两项:{found:?}");
|
||
|
||
std::fs::remove_dir_all(&one).expect("删 jdk-a");
|
||
let after = unlucky_core::game::java::staged::scan_roots(&[], &[one, two], false);
|
||
assert_eq!(after.len(), 1, "删掉一个之后必须少一项:{after:?}");
|
||
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
/// 扫描结论灌进页面状态之后,**行数与路径**都来自那次扫描(不是常量)。
|
||
#[test]
|
||
fn applying_a_scan_report_replaces_every_row() {
|
||
let mut state = State::default();
|
||
let report = store::ScanReport {
|
||
running: false,
|
||
done: 2,
|
||
total: 2,
|
||
finished: true,
|
||
cancelled: false,
|
||
runtimes: vec![
|
||
RuntimeRow {
|
||
binary: "C:/jdk-a/bin/java.exe".to_string(),
|
||
version: String::new(),
|
||
major: None,
|
||
vendor: None,
|
||
source: "manual",
|
||
},
|
||
RuntimeRow {
|
||
binary: "C:/jdk-b/bin/java.exe".to_string(),
|
||
version: String::new(),
|
||
major: Some(21),
|
||
vendor: Some("Temurin".to_string()),
|
||
source: "system",
|
||
},
|
||
],
|
||
};
|
||
assert!(state.apply_scan(&report), "第一次灌结论必须报告「变了」");
|
||
assert_eq!(state.runtimes.len(), 2);
|
||
assert!(state.scanned, "扫完之后「未检测到」与「还没扫」必须能分开");
|
||
assert_eq!(state.runtimes[1].summary(), "Java 21 · Temurin");
|
||
}
|
||
|
||
// --------------------------------------------------------------- 守卫②:非法路径不污染设置
|
||
|
||
/// 非法路径被拒、**设置与名单一个字节都不写**。
|
||
///
|
||
/// 变异证明:删掉 `store::add_manual` 里的 `manual::probe_manual`(改成只看 `is_dir()`),
|
||
/// 「不存在的路径」就会被加进名单 ⇒ 红。
|
||
#[test]
|
||
fn an_invalid_manual_path_is_refused_and_writes_nothing() {
|
||
let dir = root("manual");
|
||
let file = config(&dir);
|
||
|
||
assert_eq!(
|
||
store::add_manual(&file, "").err(),
|
||
Some(Refusal::AddEmpty),
|
||
"空输入要被拒"
|
||
);
|
||
assert_eq!(
|
||
store::add_manual(&file, "Z:/definitely/not/here").err(),
|
||
Some(Refusal::AddMissing),
|
||
"不存在的路径要被拒"
|
||
);
|
||
let empty = dir.join("empty-jdk");
|
||
std::fs::create_dir_all(&empty).expect("空目录");
|
||
assert_eq!(
|
||
store::add_manual(&file, &empty.display().to_string()).err(),
|
||
Some(Refusal::AddNotJava),
|
||
"目录里没有 bin/java 要被拒"
|
||
);
|
||
let broken = fake_jdk(&dir, "broken-jdk");
|
||
assert_eq!(
|
||
store::add_manual(&file, &broken.display().to_string()).err(),
|
||
Some(Refusal::AddNoVersion),
|
||
"跑不出 java -version 要被拒"
|
||
);
|
||
|
||
assert!(
|
||
store::load_manual(&file).is_empty(),
|
||
"被拒的路径不许进名单:{:?}",
|
||
store::load_manual(&file)
|
||
);
|
||
let snapshot = store::load(&file);
|
||
assert!(
|
||
snapshot.selected.is_empty(),
|
||
"被拒的路径不许污染 java.selected_path:{snapshot:?}"
|
||
);
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
// --------------------------------------------------------------- 守卫③:内存越界被拒
|
||
|
||
/// 内存必须落在 schema 登记的区间里,非数字也要拒,而且**不写盘**。
|
||
///
|
||
/// 变异证明:删掉 `State::validate` 里的范围判断(只留 `parse`),128 与 999999 会落盘 ⇒ 红;
|
||
/// 把 `parse::<u32>` 换成 `unwrap_or(2048)`,`"abc"` 不再报错 ⇒ 红。
|
||
#[test]
|
||
fn the_memory_bounds_are_enforced_before_writing() {
|
||
let mut state = State {
|
||
storage: true,
|
||
..State::default()
|
||
};
|
||
|
||
state.memory = (MIN_MEMORY_MB - 1).to_string();
|
||
assert_eq!(
|
||
state.validate().err(),
|
||
Some(Refusal::MemoryRange),
|
||
"低于下限"
|
||
);
|
||
state.memory = (MAX_MEMORY_MB + 1).to_string();
|
||
assert_eq!(
|
||
state.validate().err(),
|
||
Some(Refusal::MemoryRange),
|
||
"高于上限"
|
||
);
|
||
state.memory = "abc".to_string();
|
||
assert_eq!(
|
||
state.validate().err(),
|
||
Some(Refusal::MemoryNotNumber),
|
||
"非数字"
|
||
);
|
||
state.memory = " ".to_string();
|
||
assert_eq!(state.validate().err(), Some(Refusal::MemoryEmpty), "空");
|
||
state.memory = "4096".to_string();
|
||
assert!(state.validate().is_ok());
|
||
state.jvm = "-XX:+UseG1GC".to_string();
|
||
assert!(state.validate().is_ok());
|
||
state.jvm = "-Xss\"512k".to_string();
|
||
assert_eq!(
|
||
state.validate().err(),
|
||
Some(Refusal::JvmQuotes),
|
||
"引号没配对"
|
||
);
|
||
|
||
// 真的不写盘:一次越界保存之后,文件里还是出厂默认值。
|
||
let dir = root("memory");
|
||
let file = config(&dir);
|
||
let state = State {
|
||
storage: true,
|
||
memory: "128".to_string(),
|
||
..State::default()
|
||
};
|
||
assert!(store::save(&file, &state).is_err(), "越界必须写不进去");
|
||
assert_eq!(
|
||
store::load(&file).memory_mb,
|
||
super::state::DEFAULT_MEMORY_MB,
|
||
"被拒的保存不许落盘"
|
||
);
|
||
std::fs::remove_dir_all(&dir).ok();
|
||
}
|
||
|
||
/// 两个数字常量必须与 schema 登记的一致(否则 UI 放行的值会被 schema 拒)。
|
||
///
|
||
/// 变异证明:把 `MIN_MEMORY_MB` 改成 256(core 的 `launch_overrides` 那一档),这条立刻红。
|
||
#[test]
|
||
fn the_memory_bounds_mirror_the_schema() {
|
||
use unlucky_core::setting::keys::launcher_schema;
|
||
use unlucky_core::setting::spec::ValueKind;
|
||
let schema = launcher_schema();
|
||
let kind = schema
|
||
.spec("java.default_memory_mb")
|
||
.map(|spec| &spec.kind)
|
||
.expect("schema 里必须有 java.default_memory_mb");
|
||
let ValueKind::Int { min, max } = kind else {
|
||
panic!("java.default_memory_mb 应该是 Int:{kind:?}");
|
||
};
|
||
assert_eq!(*min, i64::from(MIN_MEMORY_MB));
|
||
assert_eq!(*max, i64::from(MAX_MEMORY_MB));
|
||
}
|
||
#[path = "tests/runtime.rs"]
|
||
mod runtime;
|
||
|
||
#[path = "tests/prefs.rs"]
|
||
mod prefs;
|