Tests: the link-watch and place tests wait for the link scan instead of sleeping

The link-watch worker now signals a condition variable when it ends,
whether it stored its stamps or unwound, and Session::wait_link_scan
blocks until then. The place tests' idle helper waits on it once per look
in place of a 5 ms sleep under a 20 s wall-clock bound, and the engine's
link-watch and link-path tests wait on it in place of spinning under a
10 s bound. The link-watch tests date a rewritten file 2 s after the one
it replaces instead of sleeping 30 ms before writing it.
This commit is contained in:
dotCooCoo
2026-11-05 02:23:48 +07:00
committed by bflatastic
parent 08bd77054d
commit a5157654f4
4 changed files with 49 additions and 25 deletions
+31 -5
View File
@@ -17,7 +17,8 @@
//! update, or answering No, leaves the link modified until the file changes again.
use std::collections::{BTreeSet, HashMap, HashSet};
use std::sync::{Arc, Mutex, PoisonError};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use serde_json::{Value, json};
@@ -27,8 +28,21 @@ use crate::{Result, Session};
pub type Stamp = (u64, u128);
/// Stamps of a document's linked files taken on a worker thread: (document uid, [(file,
/// object ids, stamp)]).
pub type LinkScan = Arc<Mutex<Option<(u64, Vec<(String, Vec<u64>, Option<Stamp>)>)>>>;
/// object ids, stamp)]), the condition the worker signals when it ends, and whether it has.
pub type LinkScan = Arc<(Mutex<Option<(u64, Vec<(String, Vec<u64>, Option<Stamp>)>)>>, Condvar, AtomicBool)>;
/// Held by a look's worker: marks the look as ended and wakes [`Session::wait_link_scan`] when
/// dropped, whether the worker returned or unwound.
struct Ended(LinkScan);
impl Drop for Ended {
fn drop(&mut self) {
let (done, finished, ended) = &*self.0;
let _held = done.lock().unwrap_or_else(PoisonError::into_inner);
ended.store(true, Ordering::SeqCst);
finished.notify_all();
}
}
/// The stamp of the linked file stored as `path` in the document saved at `doc_path`; `None` when
/// there is no file, its path isn't followed ([`crate::cmd::fileio::document_path`]) or
@@ -79,12 +93,14 @@ impl Session {
let out = slot.clone();
let scope = crate::file_access::ScopeHandle::capture();
let spawned = std::thread::Builder::new().name("link-watch".into()).spawn(move || {
// Marks the look as ended when the worker returns or unwinds.
let _ended = Ended(out.clone());
let stamps = scope.run(|| {
crate::file_access::confine(crate::file_access::current().or(opened_by).as_ref(), || {
files.into_iter().map(|(p, ids)| (p.clone(), ids, stamp(&p, doc_path.as_deref()))).collect()
})
});
*out.lock().unwrap_or_else(PoisonError::into_inner) = Some((uid, stamps));
*out.0.lock().unwrap_or_else(PoisonError::into_inner) = Some((uid, stamps));
});
if spawned.is_ok() {
self.link_scan = Some(slot);
@@ -94,11 +110,21 @@ impl Session {
/// Apply a finished look (`None` while there is none, or it is still running): see
/// [`Session::apply_link_stamps`].
pub fn poll_link_scan(&mut self) -> Option<Result<Value>> {
let (uid, stamps) = self.link_scan.as_ref()?.lock().unwrap_or_else(PoisonError::into_inner).take()?;
let (uid, stamps) = self.link_scan.as_ref()?.0.lock().unwrap_or_else(PoisonError::into_inner).take()?;
self.link_scan = None;
Some(self.apply_link_stamps(uid, stamps, false))
}
/// Block until the look in flight has ended (at once when none is running), so that the next
/// [`Session::poll_link_scan`] applies its stamps. A look whose worker stopped without stamps
/// ends too, and the poll then returns `None`. For tests that drive the look step by step.
pub fn wait_link_scan(&self) {
let Some(scan) = &self.link_scan else { return };
let (done, finished, ended) = &**scan;
let guard = done.lock().unwrap_or_else(PoisonError::into_inner);
drop(finished.wait_while(guard, |stamps| stamps.is_none() && !ended.load(Ordering::SeqCst)).unwrap_or_else(PoisonError::into_inner));
}
/// Compare the stamps of document `uid`'s linked files with the ones last seen and act on
/// the changed ones as Update Links says (`update`: read them again regardless). Returns
/// `{updated: [ids], ask: [ids], modified: [ids]}`: read again, to offer (Ask When Modified),
+2 -5
View File
@@ -77,11 +77,8 @@ fn linked_files_on_other_computers_are_never_read() {
}
s.execute("links.updateChanged", &json!({})).unwrap();
s.start_link_scan();
let t0 = std::time::Instant::now();
while s.poll_link_scan().is_none() {
assert!(t0.elapsed().as_secs() < 10, "the look never finished");
std::thread::yield_now();
}
s.wait_link_scan();
assert!(s.poll_link_scan().is_some(), "no look ran");
});
assert_eq!(far_reads(&seen), Vec::<&String>::new());
// The watch kept no stamp of them (`//tmp/…` would have one had it been looked at).
+7 -10
View File
@@ -6,10 +6,13 @@ use serde_json::{Value, json};
use crate::tests_links::{BLUE, Folder, RED, centre_colour, place, png, session};
use crate::*;
/// Write `bytes` to `path` so its modification time moves even on coarse file systems.
/// Write `bytes` to `path`, dated 2 s after the file it replaces so its modification time moves
/// even on coarse file systems.
fn save(path: &str, bytes: &[u8]) {
std::thread::sleep(std::time::Duration::from_millis(30));
let stamp = std::fs::metadata(path).unwrap().modified().unwrap();
std::fs::write(path, bytes).unwrap();
let later = std::fs::FileTimes::new().set_modified(stamp + std::time::Duration::from_secs(2));
std::fs::OpenOptions::new().write(true).open(path).unwrap().set_times(later).unwrap();
}
fn colour(s: &Session) -> [u8; 3] {
@@ -19,14 +22,8 @@ fn colour(s: &Session) -> [u8; 3] {
/// One background look, waited for.
fn look(s: &mut Session) -> Value {
s.start_link_scan();
let t0 = std::time::Instant::now();
loop {
if let Some(r) = s.poll_link_scan() {
return r.unwrap();
}
assert!(t0.elapsed().as_secs() < 10, "the look never finished");
std::thread::yield_now();
}
s.wait_link_scan();
s.poll_link_scan().expect("a look ran").unwrap()
}
#[test]
+9 -5
View File
@@ -758,7 +758,8 @@ fn frame_at(app: &mut VectorcraftApp, ctx: &egui::Context, time: f64) -> std::ti
/// for) or until `done` → whether it was.
fn idle(app: &mut VectorcraftApp, ctx: &egui::Context, time: &mut f64, secs: f64, done: &dyn Fn(&VectorcraftApp) -> bool) -> bool {
let end = *time + secs;
let real_end = std::time::Instant::now() + std::time::Duration::from_secs(20);
// The look last waited for: one that a frame after the wait still holds ended without stamps.
let mut waited: Option<vectorcraft_engine::link_watch::LinkScan> = None;
loop {
let wait = frame_at(app, ctx, *time);
if done(app) {
@@ -769,10 +770,13 @@ fn idle(app: &mut VectorcraftApp, ctx: &egui::Context, time: &mut f64, secs: f64
*time = end;
return false;
}
// The look runs on a worker thread, in real time: the clock waits for it, else a busy
// machine's slow look outlasts the simulated seconds and the test fails at random.
if app.session.link_scan.is_some() && std::time::Instant::now() < real_end {
std::thread::sleep(std::time::Duration::from_millis(5));
// The look runs on a worker thread, in real time: the clock waits for it to finish, else a
// busy machine's slow look outlasts the simulated seconds and the test fails at random.
if let Some(scan) = app.session.link_scan.clone()
&& !waited.as_ref().is_some_and(|w| std::sync::Arc::ptr_eq(w, &scan))
{
app.session.wait_link_scan();
waited = Some(scan);
continue;
}
*time = next;