#![cfg(not(target_family = "wasm"))]
use std::{
sync::{LazyLock, Mutex, MutexGuard, OnceLock, atomic::Ordering::Relaxed},
thread,
time::Duration,
};
use egui_async::{
Bind,
bind::{CURR_FRAME, LAST_FRAME},
};
use tokio::runtime::{Builder, Runtime};
fn test_lock() -> MutexGuard<'static, ()> {
static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
match LOCK.get_or_init(|| Mutex::new(())).lock() {
Ok(g) => g,
Err(poisoned) => poisoned.into_inner(),
}
}
fn named_runtime(name: &str) -> Runtime {
Builder::new_multi_thread()
.worker_threads(1)
.thread_name(name)
.build()
.expect("failed to build runtime")
}
static GLOBAL: LazyLock<Runtime> = LazyLock::new(|| named_runtime("global-rt"));
fn default_thread() -> Option<String> {
let runtime = &*egui_async::bind::ASYNC_RUNTIME;
let task = runtime.spawn(async { thread::current().name().map(str::to_owned) });
runtime
.block_on(task)
.expect("task on built-in runtime failed")
}
type ThreadBind = Bind<Option<String>, ()>;
fn thread_of(b: &mut ThreadBind) -> Option<String> {
b.request(async { Ok(thread::current().name().map(str::to_owned)) });
for _ in 0..500 {
let curr = CURR_FRAME.load(Relaxed);
LAST_FRAME.store(curr, Relaxed);
CURR_FRAME.store(curr + 1.0, Relaxed);
if !b.is_pending() {
return b.take().and_then(Result::ok).flatten();
}
thread::sleep(Duration::from_millis(2));
}
None
}
#[test]
fn global_runtime_precedence() {
let _lock = test_lock();
let mut b = ThreadBind::new(true);
assert!(b.runtime().is_none());
assert_eq!(thread_of(&mut b), default_thread());
assert!(egui_async::set_global_runtime(GLOBAL.handle().clone()).is_ok());
assert_eq!(thread_of(&mut b).as_deref(), Some("global-rt"));
assert_eq!(
thread_of(&mut ThreadBind::new(true)).as_deref(),
Some("global-rt")
);
let other = named_runtime("other-rt");
assert!(egui_async::set_global_runtime(other.handle().clone()).is_err());
assert_eq!(thread_of(&mut b).as_deref(), Some("global-rt"));
let own = named_runtime("bind-rt");
let mut b = ThreadBind::new(true).with_runtime(own.handle().clone());
assert_eq!(thread_of(&mut b).as_deref(), Some("bind-rt"));
b.set_runtime(None);
assert_eq!(thread_of(&mut b).as_deref(), Some("global-rt"));
}
#[test]
fn bind_runtime_is_used_and_can_be_changed() {
let _lock = test_lock();
let first = named_runtime("first-rt");
let second = named_runtime("second-rt");
let mut b = ThreadBind::new(true).with_runtime(first.handle().clone());
assert!(b.runtime().is_some());
assert_eq!(thread_of(&mut b).as_deref(), Some("first-rt"));
b.clear();
assert_eq!(thread_of(&mut b).as_deref(), Some("first-rt"));
b.set_runtime(Some(second.handle().clone()));
assert_eq!(thread_of(&mut b).as_deref(), Some("second-rt"));
b.set_runtime(None);
assert!(b.runtime().is_none());
let global = thread_of(&mut b);
assert!(
global.as_deref() == Some("global-rt") || global == default_thread(),
"unexpected runtime thread: {global:?}"
);
}
#[test]
fn abort_works_on_bind_runtime() {
let _lock = test_lock();
let own = named_runtime("abort-rt");
let (started_tx, started_rx) = std::sync::mpsc::channel();
let (dropped_tx, dropped_rx) = std::sync::mpsc::channel::<()>();
let mut b: Bind<(), ()> = Bind::new(true).with_runtime(own.handle().clone());
b.set_abort(true);
b.request(async move {
let _dropped = dropped_tx;
started_tx.send(()).ok();
std::future::pending::<()>().await;
Ok(())
});
started_rx
.recv_timeout(Duration::from_secs(5))
.expect("task did not start");
b.abort();
assert_eq!(
dropped_rx.recv_timeout(Duration::from_secs(5)),
Err(std::sync::mpsc::RecvTimeoutError::Disconnected),
"task was not aborted"
);
assert!(b.is_idle());
}
#[test]
fn request_on_shut_down_runtime_returns_to_idle() {
let _lock = test_lock();
let handle = named_runtime("dead-rt").handle().clone();
let mut b = ThreadBind::new(true).with_runtime(handle);
assert_eq!(thread_of(&mut b), None);
assert!(b.is_idle());
assert!(b.read().is_none());
}