use std::any::Any;
use std::fmt;
use crate::OutgoingDestination;
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
use crate::codec::{Codec, DefaultCodec};
use crate::runtime::{HeadersUnset, PublishBuilder, PublishSink, Shutdown};
use crate::runtime::{MessageBody, UnnamedCodec, message_of};
use crate::{Lend, OutgoingMessage, RawMessage};
use super::assertions::{PublishedAssertions, SubscriberAssertions};
use super::attribution::{MessageOptions, PublishedEntry, attribute};
use super::broker::{LivePublish, TestableBroker};
use super::coordinator::Coordinator;
use super::harness::{BrokerEntry, Mode, TestError};
pub struct BrokerHandle<'a> {
pub(super) harness: Harness<'a>,
pub(super) scope_id: usize,
pub(super) transport: Transport<'a>,
pub(super) label: String,
}
#[derive(Clone, Copy)]
pub(super) struct Harness<'a> {
pub(super) coordinator: &'a Coordinator,
pub(super) shutdown: &'a Shutdown,
pub(super) mode: &'a Mode,
pub(super) brokers: &'a [BrokerEntry],
}
#[derive(Clone, Copy)]
pub(super) enum Transport<'a> {
InProcess(&'a dyn TestableBroker),
Live {
connected: &'a (dyn Any + Send + Sync),
publish: Option<LivePublish>,
},
}
impl fmt::Debug for BrokerHandle<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("BrokerHandle")
.field("broker", &self.label)
.finish_non_exhaustive()
}
}
pub struct InjectSink<'a>(pub(super) Target<'a>);
pub(super) enum Target<'a> {
Broker {
harness: Harness<'a>,
scope_id: usize,
transport: Transport<'a>,
label: String,
},
Ambiguous,
}
impl fmt::Debug for InjectSink<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InjectSink").finish_non_exhaustive()
}
}
impl PublishSink for InjectSink<'_> {
type Payload = Lend;
type Error = TestError;
type Options = ();
async fn send(
&mut self,
msg: OutgoingMessage<'_, &[u8]>,
_options: Option<&Self::Options>,
) -> Result<(), Self::Error> {
let Target::Broker {
harness,
scope_id,
transport,
label,
} = &self.0
else {
return Err(TestError::Ambiguous);
};
if harness.shutdown.is_cancelled() {
return Err(TestError::ShutDown);
}
let recorded = RawMessage::new(msg.name().to_owned(), msg.payload().to_vec())
.with_headers(msg.headers().clone());
let started = harness.coordinator.test_publish_started();
match transport {
Transport::InProcess(testable) => testable.inject(msg),
Transport::Live { connected, publish } => {
let publish = publish.ok_or_else(|| TestError::NoTransport(label.clone()))?;
publish(*connected, msg)
.await
.map_err(|err| TestError::Publish {
broker: label.clone(),
source: err,
})?;
}
}
harness
.coordinator
.record_published(*scope_id, recorded, started);
harness
.mode
.settle(harness.coordinator, harness.brokers)
.await
}
}
impl<'a> BrokerHandle<'a> {
pub fn message<'v, T>(
&self,
value: &'v T,
) -> PublishBuilder<InjectSink<'a>, MessageBody<'v, T>, UnnamedCodec, HeadersUnset, T::Form>
where
T: OutgoingDestination,
{
message_of(self.sink(), value, UnnamedCodec::new())
}
pub(super) fn sink(&self) -> InjectSink<'a> {
InjectSink(Target::Broker {
harness: self.harness,
scope_id: self.scope_id,
transport: self.transport,
label: self.label.clone(),
})
}
}
impl BrokerHandle<'_> {
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
pub async fn publish<T: serde::Serialize + Sync>(
&self,
name: &str,
value: &T,
) -> Result<(), TestError> {
let bytes = DefaultCodec::default()
.encode(value)
.map_err(|err| TestError::Encode(err.to_string()))?;
self.sink()
.send(OutgoingMessage::new(name, &bytes), None)
.await
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
pub async fn publish_with_headers<T, H>(
&self,
name: &str,
value: &T,
headers: &H,
) -> Result<(), TestError>
where
T: serde::Serialize + Sync,
H: serde::Serialize + Sync,
{
let bytes = DefaultCodec::default()
.encode(value)
.map_err(|err| TestError::Encode(err.to_string()))?;
let msg = OutgoingMessage::new(name, &bytes)
.with_typed_headers(headers)
.map_err(|err| TestError::Encode(err.to_string()))?;
self.sink().send(msg, None).await
}
#[must_use]
pub fn subscriber(&self, name: &str) -> SubscriberAssertions<'_> {
SubscriberAssertions::new(self.harness.coordinator, self.scope_id, name.to_owned())
}
#[must_use]
pub fn published<T>(&self, name: &str) -> PublishedAssertions<T> {
let recorded = self.harness.coordinator.published(self.scope_id, name);
let (messages, options) = match self.transport {
Transport::InProcess(testable) => {
let messages = testable.published(name);
let options = attribute(&messages, &recorded, testable.evicted(name));
(messages, options)
}
Transport::Live { .. } => recorded
.into_iter()
.map(|entry| match entry {
PublishedEntry::Test { message, .. } => (message, MessageOptions::Unrecorded),
PublishedEntry::Runtime(publish) => {
(publish.message, MessageOptions::Recorded(publish.options))
}
})
.unzip(),
};
PublishedAssertions::new(name.to_owned(), messages, options)
}
}
#[cfg(test)]
mod tests {
use super::{InjectSink, Target};
#[test]
fn the_injection_sink_names_itself_without_its_broker() {
let sink = InjectSink(Target::Ambiguous);
assert_eq!(format!("{sink:?}"), "InjectSink { .. }");
}
}