use std::cell::Cell;
use std::collections::HashMap;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU64, Ordering};
use logwise::{ContextToken, Detail, Dispatch, EventRef, Interest, Metadata, Privacy};
use some_executor::task::{Configuration, Task};
#[cfg(not(target_arch = "wasm32"))]
use std::thread;
#[cfg_attr(target_arch = "wasm32", allow(dead_code))]
#[derive(Clone, Debug)]
struct Seen {
name: &'static str,
fields: Vec<(&'static str, Privacy, Detail, bool)>,
}
static EVENTS: Mutex<Vec<Seen>> = Mutex::new(Vec::new());
static WATCHED: Mutex<ContextToken> = Mutex::new(ContextToken::NONE);
static PARENTS: Mutex<Option<HashMap<u64, ContextToken>>> = Mutex::new(None);
static NEXT: AtomicU64 = AtomicU64::new(1);
static WANTED: Mutex<Interest> = Mutex::new(Interest::NONE);
static GENERATION: AtomicU64 = AtomicU64::new(0);
thread_local! {
static CURRENT: Cell<ContextToken> = const { Cell::new(ContextToken::NONE) };
}
struct Recorder;
impl Dispatch for Recorder {
fn generation(&self) -> usize {
GENERATION.load(Ordering::Acquire) as usize
}
fn interest(&self, _metadata: &'static Metadata) -> Interest {
*WANTED.lock().unwrap()
}
fn emit(&self, event: EventRef<'_>) {
if event.metadata.event_name != "app_window.main_thread.submission_overran" {
return;
}
let fields = event
.metadata
.fields
.iter()
.zip(event.fields.iter())
.map(|(meta, got)| (meta.name, meta.privacy, meta.detail, got.is_some()))
.collect();
EVENTS.lock().unwrap().push(Seen {
name: event.metadata.event_name,
fields,
});
}
fn capture_context(&self) -> ContextToken {
CURRENT.with(Cell::get)
}
fn create_context(&self, parent: ContextToken, _name: &'static str) -> ContextToken {
let id = NEXT.fetch_add(1, Ordering::Relaxed);
if parent == *WATCHED.lock().unwrap() {
PARENTS
.lock()
.unwrap()
.get_or_insert_with(HashMap::new)
.insert(id, parent);
}
ContextToken::from_parts(id, 0)
}
fn enter_context(&self, context: ContextToken) -> ContextToken {
CURRENT.with(|current| current.replace(context))
}
fn exit_context(&self, previous: ContextToken) {
CURRENT.with(|current| current.set(previous));
}
}
static RECORDER: Recorder = Recorder;
fn parent_of(context: ContextToken) -> Option<ContextToken> {
PARENTS
.lock()
.unwrap()
.get_or_insert_with(HashMap::new)
.get(&context.into_parts().0)
.copied()
}
fn want(interest: Interest) {
*WANTED.lock().unwrap() = interest;
GENERATION.fetch_add(1, Ordering::Release);
}
fn fail_loudly() {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
previous(info);
std::process::exit(1);
}));
}
fn install() {
fail_loudly();
logwise::install_dispatcher(&RECORDER).expect("install dispatcher");
want(Interest::NONE);
}
async fn check_context_contract() {
let outer = logwise::context::capture();
let caller = logwise::context::child(ContextToken::NONE, "test.caller");
*WATCHED.lock().unwrap() = caller;
let (first_tx, first_rx) = r#continue::continuation();
let (second_tx, second_rx) = r#continue::continuation();
{
let _entered = logwise::context::enter(caller);
assert_eq!(
logwise::context::capture(),
caller,
"the test thread should be in its own context"
);
app_window::application::submit_to_main_thread("probe".to_string(), move || {
first_tx.send(logwise::context::capture());
});
app_window::application::submit_to_main_thread("probe2".to_string(), move || {
second_tx.send(logwise::context::capture());
});
assert_eq!(
logwise::context::capture(),
caller,
"submitting work must not disturb the caller's context"
);
}
assert_eq!(
logwise::context::capture(),
outer,
"the guard should restore the context that was entered before it"
);
let observed = first_rx.await;
let after = second_rx.await;
assert!(
!observed.is_none(),
"main-thread work should run in a context, not the null token"
);
assert_ne!(
observed, caller,
"it should be its own context, not the caller's"
);
assert_eq!(
parent_of(observed),
Some(caller),
"the main-thread context should descend from the submitting one"
);
assert_ne!(
after, observed,
"each submission gets its own context; the main thread did not keep the last one"
);
assert_eq!(parent_of(after), Some(caller));
*WATCHED.lock().unwrap() = ContextToken::NONE;
#[cfg(not(target_arch = "wasm32"))]
check_field_gating().await;
logwise::log!("app_window logwise context contract: OK");
}
#[cfg(not(target_arch = "wasm32"))]
async fn check_field_gating() {
EVENTS.lock().unwrap().clear();
want(Interest::CORE_SUPPORT.union(Interest::DETAIL_SUPPORT));
let (done_tx, done_rx) = r#continue::continuation();
app_window::application::submit_to_main_thread("slow".to_string(), move || {
let start = wasm_lite_std::time::Instant::now();
while start.elapsed() < wasm_lite_std::time::Duration::from_millis(25) {
std::hint::spin_loop();
}
done_tx.send(());
});
done_rx.await;
let deadline =
wasm_lite_std::time::Instant::now() + wasm_lite_std::time::Duration::from_secs(5);
let overran = loop {
let events = EVENTS.lock().unwrap().clone();
if let Some(seen) = events
.iter()
.find(|seen| seen.name == "app_window.main_thread.submission_overran")
{
break seen.clone();
}
assert!(
wasm_lite_std::time::Instant::now() < deadline,
"a 25ms main-thread closure should report an overrun; saw {events:?}"
);
wasm_lite_std::sleep_async(wasm_lite_std::time::Duration::from_millis(5)).await;
};
let overran = &overran;
for (name, privacy, detail, materialized) in &overran.fields {
match (privacy, detail) {
(Privacy::SupportSafe, Detail::Core) => assert!(
materialized,
"{name} is support-safe core and should be present"
),
(Privacy::LocalOnly, _) => assert!(
!materialized,
"{name} is local-only and must be withheld from a support view"
),
_ => {}
}
}
assert!(
overran
.fields
.iter()
.any(|(name, ..)| *name == "duration_ms"),
"the overrun should say how long it took: {overran:?}"
);
want(Interest::NONE);
}
#[cfg(not(target_arch = "wasm32"))]
fn main() {
install();
app_window::application::main(|| {
thread::spawn(|| {
let task = Task::without_notifications(
"logwise_context_test".to_string(),
Configuration::default(),
async {
check_context_contract().await;
std::process::exit(0);
},
);
task.spawn_static_current();
});
});
}
#[cfg(target_arch = "wasm32")]
wasm_lite::test_main!();
#[cfg(target_arch = "wasm32")]
#[wasm_lite::wasm_lite_test]
fn wasm_main() {
wasm_lite_std::async_doctest!(async {
install();
assert!(app_window::application::is_main_thread());
let (done, wait) = r#continue::continuation();
app_window::application::main(move || {
let task = Task::without_notifications(
"logwise_context_test".to_string(),
Configuration::default(),
async move {
check_context_contract().await;
done.send(());
},
);
task.spawn_static_current();
});
wait.await;
});
}