feat(m175): Java 设置页接真运行时(扫描 / 手动路径 / 默认 JDK / 内存 / JVM 参数)
用户实测四条抱怨逐条对应: - 「没有自动检测本机 Java」:首屏自动扫一次盘(托管根 + JAVA_HOME + PATH + 标准安装位置 + 注册表),之后由「立即扫描」手动触发;扫描在后台线程里跑,带 done/total 进度与取消 - 「显示 Temurin 21.0.4 不知道是不是硬编码」:演示行与绊线测试一起删掉,列表来自真读盘 - 「没一个能改」:默认内存(MB,512~65536,范围与 schema 同源)与 JVM 参数可真改、真落盘、读回 - 「不能手动指定 Java 路径 / 不能改默认版本」:手动添加走严格校验(真跑 java -version), 设为默认写 java.selected_path 并给出「为什么选它」(当前实例的版本门槛 + 每行满足与否) core:java::manual(严格探测 + 厂商识别 + 名单持久化)、java::staged(候选收集 + 带进度与 取消的扫描)、java::proc(带超时且可打断的子进程执行:卡住的 java -version 会被 kill)。 i18n:catalog/settings/java.rs 55 条中英 + 金表按装配顺序插入。 复审 r1(p2-5items)全修: - P2-1 「设为默认」不再清脏:只写 java.selected_path,内存/JVM 草稿一个字节都不动 - P2-2 错误文案全部走 i18n(三档「写不进去」/「路径太长」/「名单写不进去」各自一个 key), 不再把 Rust 枚举名当人话 - P2-3 手动名单落盘有正面断言(加一条 → 从盘上新读一份 → 在) - P2-4 版本号显示到小版本(17.0.13 / 17.0.9 可区分),解析不出来时给原始版本串 - P2-5 每候选 8 秒超时并 kill、取消打断正在跑的探测、扫描槽挂在 State 上、线程活着期间 不许起第二个(界面显示「正在取消…」) 复审 r2(p2-2items)全修: - P2-1 空态不再指向不存在的「Java 页」,改成真实存在的 `ull java install 21` 命令行入口 - P2-2 `auto_scan_due` 增加 `!report.cancelled`:取消过一次之后本进程不再自动扫, 配置文件再变(存内存 / 改主题)也不会把取消无声撤销;手动「立即扫描」不受影响 - 顺带:proc 的 kill+wait 补「子进程真死了」的回归守卫(标记文件从未出现)、 core 的 Schema 错误改成页面自己的 key(不再把中文 Display 直出给英文界面)、 默认态 chip 用新的 `Chip::badge`(不再挂一条永远收不到的消息)
This commit is contained in:
1 parent
30b61e2684
commit
df331f6a60
26 files changed
+4030
-85
No files matched your search
@@ -0,0 +1,242 @@
|
||||
//! 「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;
|
||||
Reference in new issue
Block a user