//! 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 { 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(); }