UI tests: wait for the sidebar's file-system checks before clicking
keyword_list_filters_renames_and_suggests failed intermittently under
full-suite load (1 photo instead of 2 after clicking travel|italy, or no
travel|italy row at all). No metadata was lost: at the failure every
photo still had its keywords, but the filter was `travel|france`.
Local's rows (Downloads, Home, ...) are added only once their is_dir
checks come back from fs_cached worker threads. Those start when the left
panel is first drawn, which in this test is right before the clicks.
Headless::busy() did not count them. Under load a row could land between
the frame that read a widget's rect and the frame that delivered the
press. The keyword rows then moved down 29 px and the click hit the row
above (travel|france), or the toggle click missed travel. Diagnostics
showed the click aimed at y=1307 and travel|italy drawn at 1365 after
the two Local rows landed. The same failure reproduces on 5cee0c0 (2 of
20 runs), so the thumbnail work did not cause it.
fs_cached now keeps a per-context count of checks in flight
(fs_cached_running), and busy() includes it, so settle() waits for them.
The keyword test settles after opening the left panel. The new test
settle_waits_for_file_system_checks holds a check open and fails without
the busy() change.
Full ui-egui lib runs with 4 test threads: 2 failures in 25 before the
change, none in the runs after it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
b4dc8c10ee
commit
e9931d6871
|
|
@ -169,9 +169,11 @@ impl Headless {
|
|||
}
|
||||
}
|
||||
|
||||
/// Is anything still in progress (renders, queued input)?
|
||||
/// Is anything still in progress (renders, file-system checks the sidebar waits for, queued
|
||||
/// input)?
|
||||
pub fn busy(&self) -> bool {
|
||||
self.app.renderer.in_flight() > 0
|
||||
|| crate::panels::left::fs_cached_running(&self.view.ctx) > 0
|
||||
|| self.app.merge.busy()
|
||||
|| self.app.scan.is_some()
|
||||
|| self.app.import.is_some()
|
||||
|
|
@ -534,6 +536,9 @@ mod tests {
|
|||
ex(&mut h, "photo.setMeta", json!({"ids": [vis[0], vis[1]], "addKeywords": ["travel|italy"]}));
|
||||
ex(&mut h, "photo.setMeta", json!({"ids": [vis[2]], "addKeywords": ["travel|france"]}));
|
||||
h.request("ui.set", json!({"leftPanel": true}), t);
|
||||
// Local's rows (Downloads, Home, …) appear when their background checks land and push
|
||||
// the keyword rows down: clicking before that hit the row above (`travel|france`)
|
||||
h.settle(SETTLE);
|
||||
let r = h.request("ui.clickWidget", json!({"id": "source:keyword:travel"}), t);
|
||||
assert_eq!(r["ok"], true, "{r}");
|
||||
assert_eq!(h.app.session.filter.keyword.as_deref(), Some("travel"));
|
||||
|
|
@ -560,6 +565,36 @@ mod tests {
|
|||
h.settle(SETTLE);
|
||||
}
|
||||
|
||||
/// The sidebar's file-system checks run on worker threads and add rows when they land, which
|
||||
/// moves every row below them: `busy()` counts them, so `settle` waits for them before a test
|
||||
/// reads widget positions (a click aimed at a stale rect hits the neighbouring row).
|
||||
#[test]
|
||||
fn settle_waits_for_file_system_checks() {
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
static RELEASE: AtomicBool = AtomicBool::new(false);
|
||||
fn slow_check(_: &str) -> bool {
|
||||
let t0 = Instant::now();
|
||||
while !RELEASE.load(Ordering::Acquire) && t0.elapsed() < Duration::from_secs(60) {
|
||||
std::thread::sleep(Duration::from_millis(1));
|
||||
}
|
||||
true
|
||||
}
|
||||
let mut h = demo([800.0, 600.0]);
|
||||
h.settle(SETTLE);
|
||||
let check = |h: &mut Headless| {
|
||||
let raw = HeadlessView::raw_input(h.size, 1.0, 0.0, vec![]);
|
||||
let mut got = None;
|
||||
h.view.run(raw, |ui| got = crate::panels::left::fs_cached(ui, "test-slow", "x", f64::INFINITY, slow_check));
|
||||
got
|
||||
};
|
||||
assert_eq!(check(&mut h), None, "the answer is worked out off the UI thread");
|
||||
assert!(h.busy(), "a pending file-system check is pending work");
|
||||
RELEASE.store(true, Ordering::Release);
|
||||
assert!(h.settle(SETTLE));
|
||||
assert!(!h.busy());
|
||||
assert_eq!(check(&mut h), Some(true));
|
||||
}
|
||||
|
||||
/// Photo > Rename Photos…: the dialog previews and renames the selection.
|
||||
#[test]
|
||||
fn rename_dialog_renames_the_selection() {
|
||||
|
|
|
|||
|
|
@ -335,6 +335,17 @@ fn list_subfolders(path: &str) -> Vec<(String, String)> {
|
|||
v
|
||||
}
|
||||
|
||||
/// How many [`fs_cached`] answers for `ctx` are being worked out right now. Rows appear (and
|
||||
/// the sidebar below them moves) when they land, so the headless driver counts them as pending
|
||||
/// work and waits for them before acting on widget positions.
|
||||
pub(crate) fn fs_cached_running(ctx: &egui::Context) -> usize {
|
||||
fs_running_counter(ctx).load(std::sync::atomic::Ordering::Acquire)
|
||||
}
|
||||
|
||||
fn fs_running_counter(ctx: &egui::Context) -> std::sync::Arc<std::sync::atomic::AtomicUsize> {
|
||||
ctx.data_mut(|d| d.get_temp_mut_or_default::<std::sync::Arc<std::sync::atomic::AtomicUsize>>(egui::Id::new("fs-cached-running")).clone())
|
||||
}
|
||||
|
||||
/// A file-system answer for `path` (`f(path)`), kept per `kind` and path and refreshed on a worker
|
||||
/// thread at most every `every` seconds: a folder on a sleeping NAS, a dropped share or a
|
||||
/// spinning-up drive never blocks a frame. `None` until the first answer arrives.
|
||||
|
|
@ -366,18 +377,24 @@ pub(crate) fn fs_cached<T: Clone + Send + 'static>(ui: &egui::Ui, kind: &'static
|
|||
due
|
||||
};
|
||||
if start {
|
||||
let (out, path, repaint) = (cell.clone(), path.to_string(), ui.ctx().clone());
|
||||
use std::sync::atomic::Ordering;
|
||||
let running = fs_running_counter(ui.ctx());
|
||||
running.fetch_add(1, Ordering::AcqRel);
|
||||
let (out, path, repaint, done) = (cell.clone(), path.to_string(), ui.ctx().clone(), running.clone());
|
||||
let work = move || {
|
||||
let v = f(&path);
|
||||
let mut e = out.lock().unwrap_or_else(std::sync::PoisonError::into_inner);
|
||||
e.value = Some(v);
|
||||
e.running = false;
|
||||
drop(e);
|
||||
// after the answer is stored: a frame that sees the count drop also sees the answer
|
||||
done.fetch_sub(1, Ordering::AcqRel);
|
||||
repaint.request_repaint();
|
||||
};
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
if std::thread::Builder::new().name("lc-fs-list".into()).spawn(work).is_err() {
|
||||
cell.lock().unwrap_or_else(std::sync::PoisonError::into_inner).running = false;
|
||||
running.fetch_sub(1, Ordering::AcqRel);
|
||||
}
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
work();
|
||||
|
|
|
|||
Loading…
Reference in New Issue