UI: seeded whole-window soak with memory and timing bounds

ui_soak_tests.rs drives the app through the egui test harness with seeded
random clicks, drags, keys (plain, Ctrl, Shift), typing and scrolling over the
sample deck. Every ten actions it checks that every document validates, the
selection is in range, undo, journal, thumbnail and texture caches stay
bounded, and no command or render panic was caught; at the end, that input
really reached the editor and the mean frame stays under 250 ms. Two seeds x
150 actions run in CI; DECKCRAFT_UI_SOAK_STEPS/SEED run it long.

Panics swallowed by the guards were invisible to tests: Session now counts
them (panics_caught) and deckcraft_render::panics_caught() counts render
panics drawn as blank. Textures::thumb_count exposes the thumbnail cache size.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Nikita Rakitin
2026-10-13 22:09:48 +03:00
co-authored by Cursor
parent 457406a7a5
commit e9315bc141
7 changed files with 241 additions and 5 deletions
+15
View File
@@ -257,6 +257,17 @@ impl Default for Renderer {
}
}
static PANICS: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
fn note_panic() {
PANICS.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
/// Render panics caught (and drawn as blank) since start; soak tests require it to stay 0.
pub fn panics_caught() -> u64 {
PANICS.load(std::sync::atomic::Ordering::Relaxed)
}
thread_local! {
static RENDERER: std::cell::RefCell<Renderer> = std::cell::RefCell::new(Renderer::default());
}
@@ -330,6 +341,7 @@ pub fn render_slide_drawn(pres: &Presentation, index: usize, opts: &RenderOpts)
}
Err(_) => {
log::error!("rendering slide {index} panicked; showing a blank slide");
note_panic();
RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
let (w, h) = output_size(pres, opts);
(Image { width: w as u32, height: h as u32, pixels: vec![255; w as usize * h as usize * 4] }, page)
@@ -354,6 +366,7 @@ pub fn render_slide_part(pres: &Presentation, index: usize, part: SlidePart, opt
Ok(img) => img,
Err(_) => {
log::error!("rendering part of slide {index} panicked; showing nothing");
note_panic();
RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
let (w, h) = output_size(pres, opts);
Image { width: w as u32, height: h as u32, pixels: vec![0; w as usize * h as usize * 4] }
@@ -385,6 +398,7 @@ pub fn render_blend(
Ok(img) => img,
Err(_) => {
log::error!("rendering a frame between slides {old} and {new} panicked; showing the new slide");
note_panic();
RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
render_slide(pres, new, opts)
}
@@ -397,6 +411,7 @@ pub fn render_layout(pres: &Presentation, master: usize, layout: Option<usize>,
Ok(img) => img,
Err(_) => {
log::error!("rendering a layout panicked; showing a blank image");
note_panic();
RENDERER.with(|r| *r.borrow_mut() = Renderer::default());
Image::default()
}