use std::any::{Any, TypeId, type_name};
use std::fmt;
use std::future::Future;
use std::time::Duration;
use crate::runtime::{BoxError, BoxFuture, InProcessConnect};
use crate::{Broker, DefaultPublish, OutgoingMessage, PublishPolicy, Publisher, RawMessage};
use super::Coordinator;
#[diagnostic::on_unimplemented(
message = "`{Self}` has no in-process mode, so the test harness cannot run it",
label = "not an in-process broker",
note = "a broker crate implements `InProcess` under its `testing` feature: a service enables \
that feature in `[dev-dependencies]`, a broker author implements the trait there"
)]
pub trait InProcess: Broker<Connected: TestableBroker> {
fn connect_in_process(
self,
) -> impl Future<Output = Result<Self::Connected, Self::Error>> + Send;
}
#[diagnostic::on_unimplemented(
message = "the connected form `{Self}` cannot be driven by the test harness",
label = "no `TestableBroker` view",
note = "a broker's in-process mode implements `TestableBroker` on the connected form its \
`InProcess` transition produces"
)]
pub trait TestableBroker: Send + Sync {
fn install_coordinator(&self, coordinator: Coordinator);
fn inject(&self, message: OutgoingMessage<'_>);
fn published(&self, name: &str) -> Vec<RawMessage>;
fn routes(&self, destination: &str, subscriptions: &[&str]) -> Vec<usize> {
subscriptions
.iter()
.enumerate()
.filter(|(_, name)| **name == destination)
.map(|(position, _)| position)
.collect()
}
fn backlog(&self) -> Backlog {
Backlog::Missed
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Backlog {
Missed,
Delivered,
}
pub struct TestableRegistration {
broker: fn() -> TypeId,
connect: InProcessConnect,
downcast: fn(&dyn Any) -> Option<&dyn TestableBroker>,
live: LivePublish,
}
pub(crate) type LivePublish = for<'m> fn(
&'m (dyn Any + Send + Sync),
OutgoingMessage<'m>,
) -> BoxFuture<'m, Result<(), BoxError>>;
pub(crate) fn live_publish<'m, B>(
connected: &'m (dyn Any + Send + Sync),
msg: OutgoingMessage<'m>,
) -> BoxFuture<'m, Result<(), BoxError>>
where
B: Broker + 'static,
B::Connected: DefaultPublish,
<<B::Connected as DefaultPublish>::Policy as PublishPolicy<B::Connected>>::Live: Publisher,
{
let connected = connected.downcast_ref::<B::Connected>().ok_or_else(|| {
Box::from(format!(
"the live publisher of {} was handed another connected form",
type_name::<B>(),
)) as BoxError
});
Box::pin(async move {
let connected = connected?;
let publisher = <B::Connected as DefaultPublish>::Policy::default()
.pair(connected)
.await
.map_err(|err| Box::new(err) as BoxError)?;
let (name, payload, headers) = msg.into_parts();
publisher
.publish(
OutgoingMessage::new(name, payload).with_headers(headers),
None,
)
.await
.map_err(|err| Box::new(err) as BoxError)
})
}
impl fmt::Debug for TestableRegistration {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TestableRegistration")
.finish_non_exhaustive()
}
}
impl TestableRegistration {
#[must_use]
pub const fn of<B>() -> Self
where
B: InProcess + 'static,
B::Connected: DefaultPublish,
<<B::Connected as DefaultPublish>::Policy as PublishPolicy<B::Connected>>::Live: Publisher,
{
Self {
broker: TypeId::of::<B>,
connect: connect_erased::<B>,
downcast: testable_view::<B>,
live: live_publish::<B>,
}
}
pub(crate) fn registers(&self, broker: TypeId) -> bool {
(self.broker)() == broker
}
pub(crate) fn transition(&self) -> InProcessConnect {
self.connect
}
pub(crate) fn live(&self) -> LivePublish {
self.live
}
pub(crate) fn resolve<'a>(&self, any: &'a dyn Any) -> Option<&'a dyn TestableBroker> {
(self.downcast)(any)
}
}
fn connect_erased<B: InProcess + 'static>(
broker: Box<dyn Any + Send>,
) -> BoxFuture<'static, Result<Box<dyn Any + Send>, BoxError>> {
Box::pin(async move {
let broker = broker.downcast::<B>().map_err(|_| {
Box::from(format!(
"the in-process transition of {} was handed another broker",
type_name::<B>(),
)) as BoxError
})?;
let connected = broker
.connect_in_process()
.await
.map_err(|err| Box::new(err) as BoxError)?;
Ok(Box::new(connected) as Box<dyn Any + Send>)
})
}
fn testable_view<B: InProcess + 'static>(any: &dyn Any) -> Option<&dyn TestableBroker> {
any.downcast_ref::<B::Connected>()
.map(|connected| connected as &dyn TestableBroker)
}
inventory::collect!(TestableRegistration);
#[macro_export]
macro_rules! register_testable_broker {
($broker:ty) => {
$crate::inventory::submit! {
$crate::testing::TestableRegistration::of::<$broker>()
}
};
}
pub async fn expect_published<B: TestableBroker>(
broker: &B,
name: &str,
count: usize,
timeout: Duration,
) -> Vec<RawMessage> {
let deadline = tokio::time::Instant::now() + timeout;
loop {
let observed = broker.published(name);
if observed.len() >= count || tokio::time::Instant::now() >= deadline {
return observed;
}
tokio::time::sleep(Duration::from_millis(2)).await;
}
}