#![cfg(all(
feature = "macros",
feature = "memory",
feature = "json",
feature = "testing"
))]
mod common;
use std::future::Future;
use std::io;
use std::marker::PhantomData;
use std::num::NonZeroU32;
use std::sync::{Arc, Mutex};
use common::Order;
use ruststream::memory::{MemoryBroker, MemoryPublish};
use ruststream::runtime::{AppInfo, HandlerOutcome, RustStream};
use ruststream::testing::{Coordinator, InProcess, TestApp, TestableBroker};
use ruststream::{
Broker, BrokerMoves, Connected, ConnectedBroker, DeclareRetryError, DefaultPublish,
NamedCopies, OutgoingMessage, PairError, PublishPolicy, RawMessage, RetryDeclaration,
Subscribe, nonzero, register_testable_broker, subscriber,
};
type Declared = Arc<Mutex<Vec<(String, RetryDeclaration)>>>;
#[derive(Debug, Clone, Copy)]
struct Moved;
#[derive(Debug, Clone, Copy)]
struct Mapped;
#[derive(Debug, Clone, Copy)]
struct Unaddressed;
struct Bus<Answer> {
inner: MemoryBroker,
declared: Declared,
_answer: PhantomData<fn() -> Answer>,
}
impl<Answer> Bus<Answer> {
fn new(declared: &Declared) -> Self {
Self {
inner: MemoryBroker::new(),
declared: Arc::clone(declared),
_answer: PhantomData,
}
}
}
struct ConnectedBus<Answer> {
inner: Connected<MemoryBroker>,
declared: Declared,
_answer: PhantomData<fn() -> Answer>,
}
impl<Answer: Send + Sync + 'static> Broker for Bus<Answer> {
type Error = <MemoryBroker as Broker>::Error;
type Connected = ConnectedBus<Answer>;
async fn connect(self) -> Result<Self::Connected, Self::Error> {
Ok(ConnectedBus {
inner: self.inner.connect().await?,
declared: self.declared,
_answer: PhantomData,
})
}
}
impl<Answer: Send + Sync + 'static> ConnectedBroker for ConnectedBus<Answer> {
type Error = <Connected<MemoryBroker> as ConnectedBroker>::Error;
type Closed = ();
async fn shutdown(self) -> Result<Self::Closed, Self::Error> {
self.inner.shutdown().await?;
Ok(())
}
}
impl<Answer: Send + Sync + 'static> InProcess for Bus<Answer> {
fn connect_in_process(
self,
) -> impl Future<Output = Result<Self::Connected, Self::Error>> + Send {
self.connect()
}
}
impl<Answer: Send + Sync + 'static> TestableBroker for ConnectedBus<Answer> {
fn install_coordinator(&self, coordinator: Coordinator) {
self.inner.install_coordinator(coordinator);
}
fn inject(&self, message: OutgoingMessage<'_>) {
self.inner.inject(message);
}
fn published(&self, name: &str) -> Vec<RawMessage> {
self.inner.published(name)
}
}
register_testable_broker!(Bus<Moved>);
register_testable_broker!(Bus<Mapped>);
register_testable_broker!(Bus<Unaddressed>);
#[derive(Debug, Default, Clone, Copy)]
struct BusPublish;
impl<Answer: Send + Sync + 'static> PublishPolicy<ConnectedBus<Answer>> for BusPublish {
type Live = <MemoryPublish as PublishPolicy<Connected<MemoryBroker>>>::Live;
fn pair(
self,
connected: &ConnectedBus<Answer>,
) -> impl Future<Output = Result<Self::Live, PairError>> + Send {
MemoryPublish.pair(&connected.inner)
}
}
impl<Answer: Send + Sync + 'static> DefaultPublish for ConnectedBus<Answer> {
type Policy = BusPublish;
}
impl Subscribe for ConnectedBus<Moved> {
type Subscriber = <Connected<MemoryBroker> as Subscribe>::Subscriber;
type Copies = BrokerMoves;
fn subscribe(&self, name: &str) -> impl Future<Output = Result<Self::Subscriber, Self::Error>> {
self.inner.subscribe(name)
}
}
impl Subscribe for ConnectedBus<Unaddressed> {
type Subscriber = <Connected<MemoryBroker> as Subscribe>::Subscriber;
type Copies = NamedCopies;
fn subscribe(&self, name: &str) -> impl Future<Output = Result<Self::Subscriber, Self::Error>> {
self.inner.subscribe(name)
}
}
impl Subscribe for ConnectedBus<Mapped> {
type Subscriber = <Connected<MemoryBroker> as Subscribe>::Subscriber;
type Copies = BrokerMoves;
fn subscribe(&self, name: &str) -> impl Future<Output = Result<Self::Subscriber, Self::Error>> {
self.inner.subscribe(name)
}
fn declare_retry(
&self,
name: &str,
declaration: &RetryDeclaration,
) -> Result<(), DeclareRetryError> {
if declaration.max_attempts().is_none() || declaration.dead_letter().is_none() {
return Err(DeclareRetryError::Broker(Box::new(io::Error::other(
format!("subscription `{name}` needs the cap and the destination together"),
))));
}
self.declared
.lock()
.expect("declaration mutex poisoned")
.push((name.to_owned(), declaration.clone()));
Ok(())
}
}
#[subscriber("orders-workers")]
async fn moved(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::ack()
}
#[tokio::test]
async fn a_declaration_a_bare_name_carries_nowhere_refuses_to_start() {
let declared = Declared::default();
let app = RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(
Bus::<Moved>::new(&declared),
|b| {
b.include(moved)
.max_attempts(nonzero!(3u32))
.dead_letter("orders.dead");
},
);
let failed = TestApp::start(app)
.await
.expect_err("a declaration the broker maps nowhere must not start");
let message = failed.to_string();
assert!(message.contains("orders-workers"), "{message}");
assert!(message.contains("ConnectedBus"), "{message}");
assert!(
message.contains(
"moves a spent delivery itself and maps no retry declaration made over a bare \
subscription name"
),
"{message}"
);
assert!(
message.contains("declare the cap and the destination on this broker's own descriptor"),
"{message}"
);
assert_eq!(
*declared.lock().expect("declaration mutex poisoned"),
Vec::new(),
"the refusal comes from the default, which records nothing",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_broker_that_maps_a_bare_name_takes_both_halves() {
let declared = Declared::default();
let app = RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(
Bus::<Mapped>::new(&declared),
|b| {
b.include(moved)
.max_attempts(nonzero!(5u32))
.dead_letter("orders.dead");
},
);
let _tb = TestApp::start(app).await.expect("startup failed");
let recorded = declared.lock().expect("declaration mutex poisoned").clone();
assert_eq!(recorded.len(), 1, "{recorded:?}");
assert_eq!(recorded[0].0, "orders-workers");
assert_eq!(recorded[0].1.max_attempts().map(NonZeroU32::get), Some(5));
assert_eq!(recorded[0].1.dead_letter(), Some("orders.dead"));
}
#[tokio::test]
async fn a_broker_that_refuses_half_a_declaration_refuses_to_start() {
let declared = Declared::default();
let app = RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(
Bus::<Mapped>::new(&declared),
|b| {
b.include(moved).max_attempts(nonzero!(5u32));
},
);
let failed = TestApp::start(app)
.await
.expect_err("a declaration the broker rejects must not start");
let message = failed.to_string();
assert!(message.contains("orders-workers"), "{message}");
assert!(
message.contains("needs the cap and the destination together"),
"{message}"
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_bare_name_on_an_unaddressed_broker_declares_with_a_named_destination() {
let declared = Declared::default();
let app = RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(
Bus::<Unaddressed>::new(&declared),
|b| {
b.include(moved)
.max_attempts(nonzero!(3u32))
.dead_letter("orders.dead")
.out_retry(BusPublish)
.to("orders.retry");
},
);
let _tb = TestApp::start(app).await.expect("startup failed");
assert_eq!(
*declared.lock().expect("declaration mutex poisoned"),
Vec::new(),
"the runtime applies this one, so the broker is told nothing",
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_bare_name_that_declares_nothing_opens_where_the_broker_moves_deliveries() {
let declared = Declared::default();
let app = RustStream::new(AppInfo::new("declared", "0.1.0")).with_broker(
Bus::<Moved>::new(&declared),
|b| {
b.include(moved);
},
);
let _tb = TestApp::start(app).await.expect("startup failed");
}