用户实测四条抱怨逐条对应: - 「没有自动检测本机 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`(不再挂一条永远收不到的消息)
164 lines
7.1 KiB
Rust
164 lines
7.1 KiB
Rust
//! M175 的 App 层用例:**「设为默认」不许吃掉未保存的内存 / JVM 草稿**(r1 复审 P2-1)。
|
||
//!
|
||
//! 为什么单独一个文件:`tests.rs` 加上它之后 446 行,越过 400 行硬线
|
||
//! (`app/components/gate.rs` 的 `every_source_file_stays_under_the_line_limit` 会判红)。
|
||
|
||
use super::bridged_app;
|
||
use crate::app::Message;
|
||
use crate::pages;
|
||
|
||
// --------------------------------------------------------------- M175 r1 P2-1:设为默认不许吃草稿
|
||
|
||
/// 一条 Java 分组消息 → 页面消息。
|
||
fn java(message: pages::settings::java::Message) -> Message {
|
||
Message::Page(pages::Message::Settings(
|
||
pages::settings::GroupMessage::Java(message),
|
||
))
|
||
}
|
||
|
||
/// 改内存 / JVM 草稿(还没保存)之后去点「设为默认」,**草稿必须还在**,而且仍算未保存。
|
||
///
|
||
/// 复审 r1 P2-1 的现场:旧代码在 `select_java_default` 里先 `dirty = false` 再 `hydrate`,
|
||
/// 于是只写了 `java.selected_path` 的一次操作把用户没保存的内存 / JVM 参数一起冲掉,
|
||
/// 一个字节都没落盘、也没有任何提示。
|
||
///
|
||
/// 变异证明:把 `dirty = false` 那一行加回 `select_java_default` ⇒ 这条红(内存回到 2048、
|
||
/// JVM 回到空、`dirty` 变假)。
|
||
#[test]
|
||
fn setting_the_default_jdk_keeps_unsaved_memory_and_jvm_drafts() {
|
||
let (mut app, path) = bridged_app("java-select");
|
||
|
||
let _ = app.update(java(pages::settings::java::Message::EditMemory(
|
||
"6144".to_string(),
|
||
)));
|
||
let _ = app.update(java(pages::settings::java::Message::EditJvm(
|
||
"-XX:+UseG1GC".to_string(),
|
||
)));
|
||
assert!(app.settings.java.dirty, "改过草稿就该是脏的");
|
||
assert_eq!(app.settings.java.saved_memory, 2048, "落盘真值还没变");
|
||
|
||
let _ = app.update(java(pages::settings::java::Message::SelectRuntime(
|
||
"C:/jdk-21/bin/java.exe".to_string(),
|
||
)));
|
||
|
||
assert_eq!(
|
||
app.settings.java.selected, "C:/jdk-21/bin/java.exe",
|
||
"设为默认必须真的写进去"
|
||
);
|
||
assert_eq!(
|
||
app.settings.java.memory, "6144",
|
||
"内存草稿被「设为默认」清掉了(P2-1 的数据丢失)"
|
||
);
|
||
assert_eq!(
|
||
app.settings.java.jvm, "-XX:+UseG1GC",
|
||
"JVM 草稿被「设为默认」清掉了(P2-1 的数据丢失)"
|
||
);
|
||
assert!(
|
||
app.settings.java.dirty,
|
||
"内存 / JVM 参数还没保存,不许标成已保存"
|
||
);
|
||
assert_eq!(app.settings.java.saved_memory, 2048, "落盘真值仍然没变");
|
||
std::fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
|
||
}
|
||
|
||
// --------------------------------------------------------------- r2 P2-2:取消之后不许自己重新起来
|
||
|
||
static HOLD: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
|
||
|
||
/// 一个会一直卡住的扫描作业(直到被取消)。
|
||
fn held_job(
|
||
_candidates: &[unlucky_core::game::java::staged::Candidate],
|
||
cancelled: &dyn Fn() -> bool,
|
||
_progress: &mut dyn FnMut(usize, usize),
|
||
) -> Vec<pages::settings::java::RuntimeRow> {
|
||
while !HOLD.load(std::sync::atomic::Ordering::SeqCst) && !cancelled() {
|
||
std::thread::sleep(std::time::Duration::from_millis(10));
|
||
}
|
||
Vec::new()
|
||
}
|
||
|
||
/// 取消过一次之后,**配置文件再变(心跳重读)也不会把扫描自己拉起来**,状态行仍是「已取消」。
|
||
///
|
||
/// 现场(r2 复审 P2-2):被取消的扫描 `running=false` / `cancelled=true` / `finished` 仍是
|
||
/// `false`,而 `auto_scan_due` 不看 `cancelled` ⇒ `clock.rs` 的 `reload_if_changed` 每变一次
|
||
/// `launcher.json`(存内存、改主题、改语言都算)就重新起一个扫描线程,用户的取消被无声撤销。
|
||
///
|
||
/// 变异证明:把 `auto_scan_due` 里的 `!report.cancelled` 去掉 ⇒ 这里 `auto_scan_java_if_due`
|
||
/// 会真的起第二个线程,`scanning` 变 `Some`、状态行不再是「已取消」⇒ 本条红。
|
||
#[test]
|
||
fn a_cancelled_scan_is_not_started_again_by_a_config_reload() {
|
||
use std::sync::atomic::Ordering;
|
||
use std::time::{Duration, Instant};
|
||
|
||
HOLD.store(false, Ordering::SeqCst);
|
||
let (mut app, path) = bridged_app("java-cancel");
|
||
|
||
// 真起一次扫描(注入卡住的作业),再取消,等线程退出:report 变成
|
||
// cancelled=true / finished=false —— 正是复审指认的那个形状。
|
||
let slot = app.settings.java.scan.clone();
|
||
assert!(slot.start_with(Vec::new(), Vec::new(), false, held_job));
|
||
slot.cancel();
|
||
let deadline = Instant::now() + Duration::from_secs(5);
|
||
while slot.report().running && Instant::now() < deadline {
|
||
std::thread::sleep(Duration::from_millis(10));
|
||
}
|
||
assert!(!slot.report().running, "取消之后线程必须退出");
|
||
assert!(slot.report().cancelled);
|
||
assert!(!slot.report().finished);
|
||
|
||
// 心跳那条路:把状态灌一次(`hydrate_java_scan`),再走「该自动扫就扫」的判据。
|
||
app.hydrate_java_scan();
|
||
assert!(app.settings.java.cancelled, "状态行要留着「已取消」");
|
||
assert!(app.settings.java.scanning.is_none());
|
||
|
||
let started = app.auto_scan_java_if_due(false);
|
||
assert!(
|
||
!started,
|
||
"取消过的扫描不许被自动重新拉起(P2-2:用户取消被无声撤销)"
|
||
);
|
||
assert!(
|
||
!slot.report().running,
|
||
"不许起第二个扫描线程:{:?}",
|
||
slot.report()
|
||
);
|
||
app.hydrate_java_scan();
|
||
assert!(
|
||
app.settings.java.scanning.is_none(),
|
||
"状态行不许回到「正在扫描」"
|
||
);
|
||
assert!(
|
||
app.settings.java.cancelled,
|
||
"取消标记要保持到下一次手动扫描为止"
|
||
);
|
||
|
||
HOLD.store(true, Ordering::SeqCst);
|
||
std::fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
|
||
}
|
||
|
||
// --------------------------------------------------------------- r2 P3-3:core 的中文串不许直出
|
||
|
||
/// 写盘被 schema 拒时,界面拿到的是**页面自己的 key**,而不是 `StoreError` 的中文 `Display`。
|
||
///
|
||
/// 现场(r2 复审 P3-3):`WriteError::Schema(format!("{error}"))` 里 `core::setting::StoreError`
|
||
/// 的 `Display` 是中文(`IO 失败 @ …` / `文件只读,拒绝保存: …`),原样渲染就是
|
||
/// `Not saved: IO 失败 @ …`。
|
||
///
|
||
/// 变异证明:`write_notice` 里把 `Schema(detail)` 映射成 `Notice::Unwritable(NotDirty)` ⇒ 本条红。
|
||
#[test]
|
||
fn a_schema_rejection_surfaces_as_a_page_key_not_the_core_sentence() {
|
||
let (mut app, path) = bridged_app("java-schema");
|
||
|
||
// `java.extra_args` 的 schema 是 `Text{max_len: 1024}`:本地校验(内存/引号)不管长度,
|
||
// 所以这一条会走到 `store.set` 的 schema 拒绝那一档。
|
||
let too_long = "-D".to_string() + &"x".repeat(2000);
|
||
let _ = app.update(java(pages::settings::java::Message::EditJvm(too_long)));
|
||
let _ = app.update(java(pages::settings::java::Message::Save));
|
||
|
||
assert_eq!(
|
||
app.settings.java.notice,
|
||
Some(pages::settings::java::Notice::WriteFailed),
|
||
"schema 拒绝要走页面的「写不进去」key,不能把 core 的中文句子原样上屏"
|
||
);
|
||
std::fs::remove_dir_all(path.parent().unwrap().parent().unwrap()).ok();
|
||
}
|