use std::cell::RefCell;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Condvar, Mutex};
use std::time::{Duration, Instant};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use egui_kittest::wgpu::{WgpuTestRenderer, create_render_state, default_wgpu_setup};
use rsplot::egui;
use rsplot::{Frame, FrameLoader, ImageStack};
fn synth_frame(source: &str) -> Option<Frame> {
let (w, h) = source.split_once('x')?;
let w: u32 = w.parse().ok()?;
let h: u32 = h.parse().ok()?;
let data = (0..(w * h)).map(|i| i as f32).collect();
Some(Frame::new(w, h, data, Some(source.to_string())))
}
struct CountingLoader {
calls: AtomicUsize,
}
impl CountingLoader {
fn new() -> Self {
Self {
calls: AtomicUsize::new(0),
}
}
fn call_count(&self) -> usize {
self.calls.load(Ordering::SeqCst)
}
}
impl FrameLoader for CountingLoader {
fn load(&self, source: &str) -> Option<Frame> {
self.calls.fetch_add(1, Ordering::SeqCst);
synth_frame(source)
}
}
struct GatedLoader {
gate: Arc<(Mutex<bool>, Condvar)>,
}
impl GatedLoader {
fn new() -> (Self, Arc<(Mutex<bool>, Condvar)>) {
let gate = Arc::new((Mutex::new(false), Condvar::new()));
(Self { gate: gate.clone() }, gate)
}
}
impl FrameLoader for GatedLoader {
fn load(&self, source: &str) -> Option<Frame> {
let (lock, cv) = &*self.gate;
let mut released = lock.lock().unwrap();
while !*released {
released = cv.wait(released).unwrap();
}
synth_frame(source)
}
}
fn release(gate: &Arc<(Mutex<bool>, Condvar)>) {
let (lock, cv) = &**gate;
*lock.lock().unwrap() = true;
cv.notify_all();
}
fn harness_lazy(
loader: Arc<dyn FrameLoader>,
sources: Vec<String>,
) -> (Rc<RefCell<ImageStack>>, Harness<'static>) {
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut stack = ImageStack::new(&rs, 0);
stack.set_loader(loader);
stack.set_sources(sources);
let app = Rc::new(RefCell::new(stack));
let app_ui = app.clone();
let renderer = WgpuTestRenderer::from_render_state(rs.clone());
let harness = Harness::builder()
.with_size(egui::vec2(400.0, 400.0))
.with_pixels_per_point(1.0)
.renderer(renderer)
.build_ui(move |ui| {
app_ui.borrow_mut().ui(ui);
});
(app, harness)
}
fn step_until(
harness: &mut Harness<'static>,
app: &RefCell<ImageStack>,
pred: impl Fn(&ImageStack) -> bool,
) -> bool {
let deadline = Instant::now() + Duration::from_secs(5);
while Instant::now() < deadline {
harness.step();
if pred(&app.borrow()) {
return true;
}
std::thread::sleep(Duration::from_millis(5));
}
false
}
#[test]
fn lazy_load_fills_the_current_slot_in_the_background() {
let (loader, gate) = GatedLoader::new();
let (app, mut harness) = harness_lazy(
Arc::new(loader),
vec!["4x4".to_string(), "4x4".to_string(), "4x4".to_string()],
);
harness.step();
assert!(
!app.borrow().current_is_displayable(),
"an in-flight slot must stay empty (waiting overlay)"
);
release(&gate);
assert!(
step_until(&mut harness, &app, |s| s.current_is_displayable()),
"current slot was never filled by the background loader"
);
}
#[test]
fn lazy_load_failure_is_terminal_and_not_retried() {
let loader = Arc::new(CountingLoader::new());
let (app, mut harness) = harness_lazy(loader.clone(), vec!["bad".to_string()]);
let counting = loader.clone();
assert!(
step_until(&mut harness, &app, move |_| counting.call_count() >= 1),
"the failing load never ran"
);
harness.step();
assert!(!app.borrow().current_is_displayable());
let after_first = loader.call_count();
assert!(after_first >= 1, "the failing load should have run once");
for _ in 0..5 {
harness.step();
}
assert_eq!(
loader.call_count(),
after_first,
"a failed slot must not be retried"
);
}
#[test]
fn prefetch_loads_neighbours_without_navigating() {
let loader = Arc::new(CountingLoader::new());
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut stack = ImageStack::new(&rs, 0);
stack.set_loader(loader.clone());
stack.set_n_prefetch(1); stack.set_sources((0..5).map(|_| "2x2".to_string()).collect());
stack.set_current(2);
let app = Rc::new(RefCell::new(stack));
let app_ui = app.clone();
let renderer = WgpuTestRenderer::from_render_state(rs.clone());
let mut harness = Harness::builder()
.with_size(egui::vec2(400.0, 400.0))
.with_pixels_per_point(1.0)
.renderer(renderer)
.build_ui(move |ui| {
app_ui.borrow_mut().ui(ui);
});
let counting = loader.clone();
assert!(
step_until(&mut harness, &app, move |_| counting.call_count() >= 3),
"prefetch did not load the current slot and both neighbours"
);
for _ in 0..5 {
harness.step();
std::thread::sleep(Duration::from_millis(5));
}
assert_eq!(
loader.call_count(),
3,
"only the current slot and its two radius-1 neighbours should load"
);
assert_eq!(app.borrow().n_prefetch(), 1);
}
#[test]
fn lazy_table_shows_urls_and_remove_realigns() {
let rs = create_render_state(default_wgpu_setup());
rsplot::install(&rs);
let mut stack = ImageStack::new(&rs, 0);
stack.set_sources(vec![
"alpha::0".to_string(),
"beta::1".to_string(),
"gamma::2".to_string(),
]);
let app = Rc::new(RefCell::new(stack));
let app_ui = app.clone();
let renderer = WgpuTestRenderer::from_render_state(rs.clone());
let mut harness = Harness::builder()
.with_size(egui::vec2(500.0, 500.0))
.with_pixels_per_point(1.0)
.renderer(renderer)
.build_ui(move |ui| {
app_ui.borrow_mut().ui(ui);
});
harness.step();
let _ = harness.get_by_label("alpha::0");
let _ = harness.get_by_label("beta::1");
let _ = harness.get_by_label("gamma::2");
app.borrow_mut().remove_source(1);
harness.step();
assert_eq!(
app.borrow().sources(),
&["alpha::0".to_string(), "gamma::2".to_string()]
);
assert_eq!(app.borrow().len(), 2);
let _ = harness.get_by_label("alpha::0");
let _ = harness.get_by_label("gamma::2");
assert!(
harness.query_by_label("beta::1").is_none(),
"the removed URL must disappear from the table"
);
}
#[test]
fn navigating_to_a_new_slot_loads_it() {
let loader = Arc::new(CountingLoader::new());
let (app, mut harness) =
harness_lazy(loader.clone(), vec!["4x4".to_string(), "6x6".to_string()]);
assert!(step_until(&mut harness, &app, |s| s.current_is_displayable()));
app.borrow_mut().next_frame();
harness.step();
assert_eq!(app.borrow().current(), 1);
assert!(
step_until(&mut harness, &app, |s| s.current() == 1
&& s.current_is_displayable()),
"the newly-browsed slot was never loaded"
);
}