#![cfg(all(target_os = "windows", feature = "test-hooks"))]
use std::cell::Cell;
use std::rc::Rc;
use std::thread;
use std::time::{Duration, Instant};
use slate_framework::app::AppContext;
use slate_framework::app_state::window_state::WindowState;
use slate_framework::app_state::{AppState, RecoveryState};
use slate_framework::element::AnyElement;
use slate_framework::elements::Div;
use slate_framework::erased_view::ErasedView;
use slate_framework::executor::{Executor, RedrawRequester};
use slate_framework::view::{IntoAny, View};
use slate_platform::{
DefaultPlatform, Event, Platform, Window, WindowOptions, WindowRenderDelegate, wake_run_loop,
};
const TICK_INTERVAL: Duration = Duration::from_millis(10);
struct NoopView;
impl View for NoopView {
fn render(&mut self, _cx: &mut slate_framework::RenderCx) -> AnyElement {
Div::new().into_any()
}
}
#[test]
fn destroyed_reason_does_not_trigger_recovery() {
let _ = env_logger::builder().is_test(true).try_init();
let platform = DefaultPlatform::new();
let window = platform.create_window(WindowOptions {
title: "slate-callback-test".into(),
size: (1, 1),
min_size: None,
resizable: false,
visible: false,
position: Some((-32000, -32000)),
});
let redraw_requester = RedrawRequester::new(wake_run_loop);
let executor = Executor::new(redraw_requester.clone());
let runtime = slate_reactive::Runtime::new();
let cx = AppContext::new_for_test(runtime.clone(), executor.background.clone());
let window_id = window.id();
let state = Rc::new(AppState::new(
executor,
redraw_requester.clone(),
runtime.clone(),
));
{
state
.windows
.borrow_mut()
.insert(window_id, WindowState::new(window.clone(), runtime));
}
state.register_redraw_requester_for_test(window_id, redraw_requester);
let dyn_strong: Rc<dyn WindowRenderDelegate> = state.clone();
let dyn_weak = Rc::downgrade(&dyn_strong);
window.set_render_delegate(dyn_weak);
drop(dyn_strong);
let timeout = Duration::from_secs(3);
let start = Instant::now();
let initialized = Cell::new(false);
let fired = Cell::new(false);
let mut view_factory = |_cx: &AppContext| Box::new(NoopView) as Box<dyn ErasedView>;
platform.run(|event| {
if start.elapsed() > timeout {
platform.quit();
return;
}
let should_tick = match event {
Event::Resumed => {
if state
.init_surfaces(window_id, &mut view_factory, &cx, &platform)
.is_err()
{
platform.quit();
return;
}
initialized.set(true);
true
}
Event::Wake | Event::WindowRedrawRequested { .. } => true,
_ => false,
};
if !should_tick {
return;
}
state.dispatch_redraw(window_id);
if initialized.get() && !fired.get() {
let result = state.fire_renderer_device_lost_callback(
wgpu::DeviceLostReason::Destroyed,
"intentional drop for test".into(),
);
assert!(
!result,
"Destroyed reason should be filtered (return false)"
);
assert!(
!state.renderer_is_device_lost(),
"Device-lost flag should NOT be set for Destroyed reason"
);
assert!(
matches!(state.current_recovery_state(), RecoveryState::NotLost),
"Recovery state should remain NotLost for Destroyed reason"
);
fired.set(true);
platform.quit();
return;
}
thread::sleep(TICK_INTERVAL);
wake_run_loop();
});
assert!(fired.get(), "Test did not complete within timeout");
}
#[test]
fn unknown_reason_triggers_recovery_state_machine() {
let _ = env_logger::builder().is_test(true).try_init();
let platform = DefaultPlatform::new();
let window = platform.create_window(WindowOptions {
title: "slate-callback-test-unknown".into(),
size: (1, 1),
min_size: None,
resizable: false,
visible: false,
position: Some((-32000, -32000)),
});
let redraw_requester = RedrawRequester::new(wake_run_loop);
let executor = Executor::new(redraw_requester.clone());
let runtime = slate_reactive::Runtime::new();
let cx = AppContext::new_for_test(runtime.clone(), executor.background.clone());
let window_id = window.id();
let state = Rc::new(AppState::new(
executor,
redraw_requester.clone(),
runtime.clone(),
));
{
state
.windows
.borrow_mut()
.insert(window_id, WindowState::new(window.clone(), runtime));
}
state.register_redraw_requester_for_test(window_id, redraw_requester);
let dyn_strong: Rc<dyn WindowRenderDelegate> = state.clone();
let dyn_weak = Rc::downgrade(&dyn_strong);
window.set_render_delegate(dyn_weak);
drop(dyn_strong);
let timeout = Duration::from_secs(3);
let start = Instant::now();
let initialized = Cell::new(false);
let fired = Cell::new(false);
let recovery_engaged = Cell::new(false);
let mut view_factory = |_cx: &AppContext| Box::new(NoopView) as Box<dyn ErasedView>;
platform.run(|event| {
if start.elapsed() > timeout {
platform.quit();
return;
}
let should_tick = match event {
Event::Resumed => {
if state
.init_surfaces(window_id, &mut view_factory, &cx, &platform)
.is_err()
{
platform.quit();
return;
}
initialized.set(true);
true
}
Event::Wake | Event::WindowRedrawRequested { .. } => true,
_ => false,
};
if !should_tick {
return;
}
state.dispatch_redraw(window_id);
if initialized.get() && !fired.get() {
let result = state.fire_renderer_device_lost_callback(
wgpu::DeviceLostReason::Unknown,
"test-triggered device loss".into(),
);
assert!(result, "Unknown reason should trigger (return true)");
assert!(
state.renderer_is_device_lost(),
"Device-lost flag SHOULD be set for Unknown reason"
);
fired.set(true);
}
if fired.get() && !recovery_engaged.get() {
let recovery = state.current_recovery_state();
if !matches!(recovery, RecoveryState::NotLost) {
recovery_engaged.set(true);
println!("Recovery state machine engaged: {:?}", recovery);
platform.quit();
return;
}
}
thread::sleep(TICK_INTERVAL);
wake_run_loop();
});
assert!(fired.get(), "Callback was not fired within timeout");
assert!(
recovery_engaged.get(),
"Recovery state machine never engaged (never left NotLost state)"
);
}
#[test]
fn atomic_flag_visibility_across_threads() {
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
let flag = Arc::new(AtomicBool::new(false));
let flag_writer = Arc::clone(&flag);
let writer = std::thread::spawn(move || {
flag_writer.store(true, Ordering::Release);
});
writer.join().expect("writer thread panicked");
let value = flag.load(Ordering::Acquire);
assert!(value, "Atomic flag should be visible after Release store");
}