use std::path::PathBuf; use super::*; mod analysis; mod borders; mod charts; mod check; mod commands; mod filter; mod format; mod formula; mod handouts; mod independent; pub(crate) mod input; mod latency; mod layout; mod media; mod menus; mod motion; mod output; mod paint; mod playlist; mod print; mod refs; mod roundtrip; mod rules; mod sheet; pub(crate) mod show; mod slides; mod tools; mod workbook; /// The pinned LibreOffice commit the build must report. const PINNED_BUILD: &str = "8221e31"; /// The pinned build, or None when absent. pub(crate) fn install() -> Option { install_dir().ok() } /// A tiny flat-XML document; no zip needed. pub(crate) const FODT: &str = r#" Hello 世界 "#; /// A document whose button runs the Basic marker writer. fn macro_fodt(marker: &std::path::Path) -> String { let marker = marker.display().to_string().replace('\\', "/"); format!( r#" Sub Sneaky Open "{marker}" For Output As #1 Write #1, "ran" Close #1 End Sub Macro doc "# ) } /// Clicks (x, y) twips: left press and release. pub(crate) fn click(worker: &mut Worker, doc: &Doc, x: i32, y: i32) { for kind in [0, 1] { let event = Mouse { kind, x, y, count: 1, buttons: 1, modifiers: 0, }; worker.post_mouse(doc, event).expect("the click posts"); } } /// Polls past `from` until `done` or `wait`; rects pile up. pub(crate) fn poll_until( worker: &mut Worker, doc: &Doc, from: &Events, wait: std::time::Duration, done: impl Fn(&Events) -> bool, ) -> Events { let deadline = std::time::Instant::now() + wait; let mut seen = Events { rects: Vec::new(), all: false, states: Vec::new(), blocked: Vec::new(), ..from.clone() }; loop { let batch = worker .poll_events(doc, seen.seq, 250) .expect("the poll returns"); let mut rects = std::mem::take(&mut seen.rects); let mut states = std::mem::take(&mut seen.states); let mut blocked = std::mem::take(&mut seen.blocked); let all = seen.all || batch.all; rects.extend(batch.rects.iter().copied()); states.extend(batch.states.iter().cloned()); blocked.extend(batch.blocked.iter().cloned()); seen = Events { rects, all, states, blocked, ..batch }; if done(&seen) || std::time::Instant::now() >= deadline { return seen; } } } fn write(path: &Path, body: &str) { std::fs::write(path, body).expect("the fixture writes"); } #[test] fn the_worker_round_trips_a_document() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let profile = dir.path().join("配置 profile"); let mut worker = Worker::spawn(&install, &profile).expect("the worker spawns"); let build = worker.build_id().expect("the build id"); assert!(build.contains(PINNED_BUILD), "the pinned build: {build}"); let source = dir.path().join("note.fodt"); write(&source, FODT); let url = crate::file_url(&source); let doc = worker.load(&url).expect("the document loads"); let (width, height) = doc.size(&mut worker).expect("the size"); assert!(width > 0 || height > 0, "a nonzero document size"); let odt = dir.path().join("note.odt"); doc.save_as(&mut worker, &crate::file_url(&odt), "odt") .expect("the odt save"); assert!(odt.try_exists().expect("the odt exists")); let doc2 = worker .load(&crate::file_url(&odt)) .expect("the odt reloads"); let pdf = dir.path().join("note.pdf"); doc2.save_as(&mut worker, &crate::file_url(&pdf), "pdf") .expect("the pdf save"); let head = std::fs::read(&pdf).expect("the pdf reads"); assert!(head.starts_with(b"%PDF"), "a real PDF comes out"); worker.close_doc(doc2).expect("the second doc closes"); worker.close_doc(doc).expect("the first doc closes"); } #[test] fn a_missing_file_is_an_error_never_a_success() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let mut worker = Worker::spawn(&install, &dir.path().join("profile")).expect("the worker"); let missing = dir.path().join("gone.odt"); let outcome = worker.load(&crate::file_url(&missing)); assert!(outcome.is_err(), "LOK-09: the failure is an error"); } #[test] fn a_failed_check_refuses_every_later_request() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let profile = dir.path().join("profile"); // The self-check cannot write its sample over a folder. std::fs::create_dir_all(profile.join("lok-verify.fodt")).expect("the blocker"); let mut worker = Worker::launch(&install, &profile).expect("the worker"); assert!(worker.ready().is_err(), "the check fails"); assert!(worker.ready().is_err(), "and stays failed"); assert!(worker.load("private:factory/swriter").is_err(), "no document reaches it"); } /// Initializes the profile fully, the CLI way (registers Basic). fn seed_basic(install: &Path, profile: &Path) { #[cfg(target_os = "macos")] let soffice = install.parent().expect("the app dir").join("MacOS/soffice"); #[cfg(target_os = "linux")] let soffice = install.join("soffice.bin"); #[cfg(windows)] let soffice = install.join("soffice.exe"); let url = ipc::file_url_of(profile); let status = std::process::Command::new(&soffice) .args(["--headless", "--norestore", "--terminate_after_init"]) .arg(format!("-env:UserInstallation={url}")) .status() .expect("soffice runs"); assert!(status.success(), "the profile initializes"); } /// The macro module: writes its marker through the UCB. fn module(marker: &Path) -> String { format!( "\n\nSub Sneaky\n oSFA = createUnoService(\"com.sun.star.ucb.SimpleFileAccess\")\n oOut = oSFA.openFileWrite(\"{}\")\n oOut.writeString(\"ran\")\n oOut.closeOutput()\nEnd Sub\n\n", marker.display().to_string().replace('\\', "/") ) } const MACRO_URL: &str = "macro:///Standard.Module1.Sneaky"; /// 未验证: this backend never dispatches document events to Basic. #[test] fn a_macro_never_writes_its_marker() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let profile = dir.path().join("profile"); let marker = dir.path().join("macro-ran.txt"); seed_basic(&install, &profile); std::fs::write( profile.join("user/basic/Standard/Module1.xba"), module(&marker), ) .expect("the module writes"); let mut worker = Worker::spawn_dev(&install, &profile).expect("the worker"); let source = dir.path().join("macro.fodt"); std::fs::write(&source, macro_fodt(&marker)).expect("the macro fodt writes"); let doc = worker .load(&crate::file_url(&source)) .expect("the macro document loads"); std::thread::sleep(std::time::Duration::from_millis(800)); assert!( !marker.try_exists().expect("the marker check"), "LOK-11: loading ran no macro" ); let ran = worker.run_macro(MACRO_URL).expect("the macro channel"); assert!(ran != 0, "the build runs the library macro when asked"); assert!( marker.try_exists().expect("the marker check"), "the sample is real: it writes when run" ); worker.close_doc(doc).expect("the document closes"); } #[test] fn a_production_worker_refuses_run_macro() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let mut worker = Worker::spawn(&install, &dir.path().join("profile")).expect("the worker"); let refused = worker.run_macro(MACRO_URL); assert!( refused.is_err(), "LOK-11: the production worker keeps the gate" ); } #[cfg(target_os = "macos")] #[test] fn an_unpatched_build_is_rejected() { // A plain folder: a mounted .app would be registered. let frameworks = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("../../engines/lo-official/Contents/Frameworks"); if !frameworks.try_exists().expect("the folder check") { eprintln!("SKIP: run scripts/extract-official-lo.sh"); return; } let dir = tempfile::tempdir().expect("a temp dir"); let Err(error) = Worker::spawn(&frameworks, &dir.path().join("profile")) else { panic!("the unpatched build must be rejected"); }; assert_eq!(error.kind(), std::io::ErrorKind::InvalidData, "{error}"); assert!( error.to_string().contains("does not paint tiles (unpatched)"), "rejected for the tile check, not {error}" ); } #[test] fn shutdown_leaves_no_locks_or_temp() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let profile = dir.path().join("profile"); let mut worker = Worker::spawn(&install, &profile).expect("the worker"); let source = dir.path().join("note.fodt"); write(&source, FODT); let doc = worker .load(&crate::file_url(&source)) .expect("the document loads"); let lock = dir.path().join(".~lock.note.fodt#"); worker.close_doc(doc).expect("the document closes"); worker.shutdown().expect("the worker shuts down"); assert!( !lock.try_exists().expect("the doc lock check"), "no stale document lock" ); assert!( !profile .join(".lock") .try_exists() .expect("the profile lock check"), "no profile lock" ); let lu: Vec<_> = std::fs::read_dir(profile.join("tmp")) .expect("the temp dir lists") .filter_map(|entry| entry.ok()) .filter(|entry| entry.file_name().to_string_lossy().starts_with("lu")) .collect(); assert!(lu.is_empty(), "no kernel temp files: {lu:?}"); } #[test] fn each_load_is_its_own_session() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let mut worker = Worker::spawn(&install, &dir.path().join("profile")).expect("the worker spawns"); let source = dir.path().join("note.fodt"); write(&source, FODT); let url = crate::file_url(&source); // Freed native documents reuse addresses; sessions never repeat. let mut old = Vec::new(); for _ in 0..4 { let doc = worker.load(&url).expect("loads"); assert!(old.iter().all(|seen: &Doc| seen.id != doc.id), "a session came back"); worker.close_doc(doc.clone()).expect("closes"); old.push(doc); } for doc in &old { assert!(doc.size(&mut worker).is_err(), "a closed session answered"); } let doc = worker.load(&url).expect("loads again"); worker.post_key(&doc, 'x' as u32, 0).expect("a key"); let events = worker.poll_events(&doc, 0, 5_000).expect("events"); assert!(events.seq > 0, "the new session's events flow"); worker.close_doc(doc).expect("closes"); } #[test] fn a_document_belongs_to_its_worker() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; }; let dir = tempfile::tempdir().expect("a temp dir"); let source = dir.path().join("note.fodt"); write(&source, FODT); let url = crate::file_url(&source); let mut first = Worker::spawn(&install, &dir.path().join("one")).expect("a worker"); let mut second = Worker::spawn(&install, &dir.path().join("two")).expect("another worker"); let theirs = first.load(&url).expect("loads"); let ours = second.load(&url).expect("loads"); // Another worker's handle never reaches these documents. assert!(theirs.size(&mut second).is_err(), "another worker's document answered"); assert!(second.close_doc(theirs.clone()).is_err(), "another worker's document closed"); assert!(ours.size(&mut second).is_ok(), "its own document stays"); first.close_doc(theirs).expect("closes"); second.close_doc(ours).expect("closes"); } #[test] fn a_worker_that_cannot_start_says_why() { if install().is_none() { eprintln!("SKIP: no pinned LOK build"); return; } let dir = tempfile::tempdir().expect("a temp dir"); let Err(error) = Worker::spawn(&dir.path().join("no-install"), &dir.path().join("profile")) else { panic!("a worker started without a kernel"); }; // The worker's own reason, not only a closed pipe. assert!(error.to_string().contains("lok-worker:"), "{error}"); }