use std::{cell::RefCell, sync::Arc};
use obs_proto::obs::v1::ObsEnvelope;
use crate::observer::{InMemoryHandle, InMemoryObserver, Observer};
thread_local! {
static TEST_HANDLE: RefCell<Option<InMemoryHandle>> = const { RefCell::new(None) };
}
tokio::task_local! {
static TEST_HANDLE_TASK: InMemoryHandle;
}
#[must_use]
pub fn install_thread_handle() -> (Arc<dyn Observer>, InMemoryHandle, TestHandleGuard) {
let observer = InMemoryObserver::new();
let handle = observer.handle();
let prev = TEST_HANDLE.with(|c| c.borrow_mut().replace(handle.clone()));
(Arc::new(observer), handle, TestHandleGuard { prev })
}
#[derive(Debug)]
pub struct TestHandleGuard {
prev: Option<InMemoryHandle>,
}
impl Drop for TestHandleGuard {
fn drop(&mut self) {
TEST_HANDLE.with(|c| {
*c.borrow_mut() = self.prev.take();
});
}
}
#[must_use]
pub fn new_async_pair() -> (Arc<dyn Observer>, InMemoryHandle) {
let observer = InMemoryObserver::new();
let handle = observer.handle();
(Arc::new(observer), handle)
}
pub async fn scoped_task_handle<F, R>(handle: InMemoryHandle, fut: F) -> R
where
F: std::future::Future<Output = R>,
{
TEST_HANDLE_TASK.scope(handle, fut).await
}
#[must_use]
pub fn render_envelope_payload_json(
env: &ObsEnvelope,
) -> Option<serde_json::Map<String, serde_json::Value>> {
if env.payload.is_empty() {
return None;
}
let registry = crate::registry::SchemaRegistry::from_link_section();
let schema = registry.lookup(env)?;
let mut map = serde_json::Map::new();
schema.render_json(&env.payload, &mut map).ok()?;
Some(map)
}
#[must_use]
pub fn snapshot_emitted() -> Vec<ObsEnvelope> {
if let Ok(h) = TEST_HANDLE_TASK.try_with(InMemoryHandle::clone) {
return h.snapshot();
}
TEST_HANDLE.with(|c| {
c.borrow()
.as_ref()
.map(InMemoryHandle::snapshot)
.unwrap_or_default()
})
}
#[must_use]
pub fn take_emitted() -> Vec<ObsEnvelope> {
if let Ok(h) = TEST_HANDLE_TASK.try_with(InMemoryHandle::clone) {
return h.drain();
}
TEST_HANDLE.with(|c| {
c.borrow()
.as_ref()
.map(InMemoryHandle::drain)
.unwrap_or_default()
})
}
#[must_use]
#[doc(hidden)]
pub fn any_emitted_matches(predicate: impl Fn(&ObsEnvelope) -> bool) -> bool {
snapshot_emitted().iter().any(predicate)
}
#[must_use]
#[doc(hidden)]
pub fn render_emitted(cap: usize) -> String {
let mut s = String::new();
for (i, env) in snapshot_emitted().iter().take(cap).enumerate() {
s.push_str(&format!(
" [{i}] {full} labels=",
i = i,
full = env.full_name
));
let mut keys: Vec<_> = env.labels.keys().collect();
keys.sort();
s.push('{');
for (j, k) in keys.iter().enumerate() {
if j > 0 {
s.push_str(", ");
}
if let Some(v) = env.labels.get(*k) {
s.push_str(&format!("{k}={v}"));
}
}
s.push_str("}\n");
}
s
}
#[doc(inline)]
pub use crate::assert_emitted;
#[macro_export]
macro_rules! assert_emitted {
($ty:ident { $($field:ident : $value:expr ,)* .. }) => {{
let __full_name_suffix = ::std::concat!(".", ::std::stringify!($ty));
let __pairs: &[(&'static str, ::std::string::String)] = &[
$((::std::stringify!($field), ::std::string::ToString::to_string(&$value))),*
];
let __ok = $crate::test::any_emitted_matches(|env| {
if !env.full_name.ends_with(__full_name_suffix) {
return false;
}
let __payload_json: ::std::option::Option<
$crate::__macro_deps::serde_json::Map<
String,
$crate::__macro_deps::serde_json::Value,
>,
> = $crate::test::render_envelope_payload_json(env);
for (k, v) in __pairs {
if let ::std::option::Option::Some(actual) = env.labels.get(*k)
&& actual == v
{
continue;
}
if let ::std::option::Option::Some(map) = __payload_json.as_ref()
&& let ::std::option::Option::Some(json_val) = map.get(*k)
{
let s = match json_val {
$crate::__macro_deps::serde_json::Value::String(s) => s.clone(),
other => other.to_string(),
};
if s == *v {
continue;
}
}
return false;
}
true
});
if !__ok {
let __captured = $crate::test::render_emitted(16);
::std::panic!(
"assert_emitted!: no envelope matched `{}` with the supplied label fields.\n\
Captured envelopes:\n{}",
::std::stringify!($ty),
__captured,
);
}
}};
($ty:ident { .. }) => {{
let __full_name_suffix = ::std::concat!(".", ::std::stringify!($ty));
let __ok = $crate::test::any_emitted_matches(|env| {
env.full_name.ends_with(__full_name_suffix)
});
if !__ok {
let __captured = $crate::test::render_emitted(16);
::std::panic!(
"assert_emitted!: no envelope matched `{}`.\n\
Captured envelopes:\n{}",
::std::stringify!($ty),
__captured,
);
}
}};
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
callsite::{EnabledOutcome, ObsCallsite},
observer::with_test_observer,
};
fn synthesize_envelope(full_name: &'static str, labels: &[(&str, &str)]) -> ObsEnvelope {
let mut env = ObsEnvelope {
full_name: full_name.to_string(),
..Default::default()
};
for (k, v) in labels {
env.labels.insert((*k).to_string(), (*v).to_string());
}
env
}
#[test]
fn test_assert_emitted_should_match_partial() {
let (observer, _handle, _g) = install_thread_handle();
with_test_observer(observer, || {
crate::observer::observer().emit_envelope(synthesize_envelope(
"myapp.v1.ObsRequestCompleted",
&[("route", "list_users"), ("status", "ok")],
));
assert_emitted!(ObsRequestCompleted {
route: "list_users",
..
});
});
}
#[test]
#[should_panic(expected = "no envelope matched")]
fn test_assert_emitted_should_panic_on_miss() {
let (observer, _handle, _g) = install_thread_handle();
with_test_observer(observer, || {
crate::observer::observer().emit_envelope(synthesize_envelope(
"myapp.v1.ObsRequestCompleted",
&[("route", "list_users")],
));
assert_emitted!(ObsRequestCompleted {
route: "different_route",
..
});
});
}
#[test]
fn test_assert_emitted_empty_body_should_match_by_type_only() {
let (observer, _handle, _g) = install_thread_handle();
with_test_observer(observer, || {
crate::observer::observer()
.emit_envelope(synthesize_envelope("myapp.v1.ObsHelloEmitted", &[]));
assert_emitted!(ObsHelloEmitted { .. });
});
}
#[test]
fn test_take_emitted_should_drain_thread_handle() {
let (observer, _handle, _g) = install_thread_handle();
with_test_observer(observer, || {
crate::observer::observer()
.emit_envelope(synthesize_envelope("a.v1.ObsX", &[("k", "v")]));
let drained = take_emitted();
assert_eq!(drained.len(), 1);
assert!(take_emitted().is_empty());
});
}
#[test]
fn test_callsite_helpers_compile_for_test_module() {
let cs = ObsCallsite::new("a.v1.ObsX", crate::Severity::Info, "m", "f", 1);
let _: EnabledOutcome = cs.enabled(0);
}
}