Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
411 lines
15 KiB
Rust
411 lines
15 KiB
Rust
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<PathBuf> {
|
|
install_dir().ok()
|
|
}
|
|
|
|
/// A tiny flat-XML document; no zip needed.
|
|
pub(crate) const FODT: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
|
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
|
<office:body><office:text><text:p>Hello 世界</text:p></office:text></office:body>
|
|
</office:document>"#;
|
|
|
|
/// 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#"<?xml version="1.0" encoding="UTF-8"?>
|
|
<office:document xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"
|
|
xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"
|
|
xmlns:ooo="http://openoffice.org/2004/office"
|
|
xmlns:form="urn:oasis:names:tc:opendocument:xmlns:form:1.0"
|
|
xmlns:script="urn:oasis:names:tc:opendocument:xmlns:script:1.0"
|
|
xmlns:xlink="http://www.w3.org/1999/xlink" office:version="1.3" office:mimetype="application/vnd.oasis.opendocument.text">
|
|
<office:scripts>
|
|
<office:script script:language="ooo:Basic">
|
|
<ooo:libraries>
|
|
<ooo:library-embedded ooo:name="Standard">
|
|
<ooo:module ooo:name="Module1">
|
|
<ooo:source-code>Sub Sneaky
 Open "{marker}" For Output As #1
 Write #1, "ran"
 Close #1
End Sub</ooo:source-code>
|
|
</ooo:module>
|
|
</ooo:library-embedded>
|
|
</ooo:libraries>
|
|
</office:script>
|
|
</office:scripts>
|
|
<office:body>
|
|
<office:text>
|
|
<office:forms form:automatic-focus="false" form:apply-design-mode="false">
|
|
<form:form>
|
|
<form:button form:name="btn" form:control-implementation="ooo:com.sun.star.form.component.CommandButton" xml:id="btn1" form:id="btn1" form:label="Run">
|
|
<office:event-listeners>
|
|
<script:event-listener script:language="ooo:script" script:event-name="form:performaction" xlink:href="vnd.sun.star.script:Standard.Module1.Sneaky?language=Basic&location=document" xlink:type="simple"/>
|
|
</office:event-listeners>
|
|
</form:button>
|
|
</form:form>
|
|
</office:forms>
|
|
<text:p>Macro doc</text:p>
|
|
</office:text>
|
|
</office:body>
|
|
</office:document>"#
|
|
)
|
|
}
|
|
|
|
/// 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!(
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE script:module PUBLIC \"-//OpenOffice.org//DTD OfficeDocument 1.0//EN\" \"module.dtd\">\n<script:module xmlns:script=\"http://openoffice.org/2000/script\" script:name=\"Module1\" script:language=\"StarBasic\">Sub Sneaky\n oSFA = createUnoService(\"com.sun.star.ucb.SimpleFileAccess\")\n oOut = oSFA.openFileWrite(\"{}\")\n oOut.writeString(\"ran\")\n oOut.closeOutput()\nEnd Sub\n</script:module>\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}");
|
|
}
|