#![cfg(all(
feature = "testing",
feature = "memory",
feature = "json",
feature = "macros"
))]
use std::error::Error;
use std::future::Future;
use std::io;
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use ruststream::memory::{
ConnectedMemoryBroker, MemoryBroker, MemoryError, MemoryPublish, MemoryPublisher,
};
use ruststream::runtime::{AppInfo, HandlerOutcome, PublishExt, RustStream};
use ruststream::testing::{InProcess, TestApp, TestError};
use ruststream::{Broker, Outgoing, register_testable_broker, subscriber};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone)]
struct Counted {
bus: MemoryBroker,
connects: Arc<AtomicUsize>,
}
impl Counted {
fn new() -> Self {
Self {
bus: MemoryBroker::new(),
connects: Arc::new(AtomicUsize::new(0)),
}
}
fn connects(&self) -> usize {
self.connects.load(Ordering::SeqCst)
}
}
impl Broker for Counted {
type Error = MemoryError;
type Connected = ConnectedMemoryBroker;
fn connect(self) -> impl Future<Output = Result<Self::Connected, Self::Error>> + Send {
self.connects.fetch_add(1, Ordering::SeqCst);
self.bus.connect()
}
}
impl InProcess for Counted {
async fn connect_in_process(self) -> Result<Self::Connected, Self::Error> {
self.connects.fetch_add(1, Ordering::SeqCst);
self.bus.connect().await
}
}
register_testable_broker!(Counted);
#[derive(Debug, Outgoing, Serialize, Deserialize, PartialEq)]
#[outgoing(name = "orders")]
struct Order {
id: u64,
}
#[subscriber("orders")]
async fn take_order(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::ack()
}
#[derive(Debug, thiserror::Error)]
enum StartupError {
#[error("the configuration is missing")]
Missing,
#[error("{0} broker(s) were already connected when the state was produced")]
Connected(usize),
}
fn failing_app(broker: Counted) -> RustStream {
RustStream::new(AppInfo::new("failing", "0.1.0"))
.on_startup(async move |()| Err::<(), _>(StartupError::Missing))
.with_broker(broker, |b| {
b.include(take_order);
})
}
fn observing_app(broker: Counted) -> RustStream {
let observed = broker.clone();
RustStream::new(AppInfo::new("observing", "0.1.0"))
.on_startup(async move |()| match observed.connects() {
0 => Ok(()),
connected => Err(StartupError::Connected(connected)),
})
.with_broker(broker, |b| {
b.include(take_order);
})
}
async fn start(app: RustStream, live: bool) -> Result<TestApp<()>, TestError> {
if live {
TestApp::start_live(app).await
} else {
TestApp::start(app).await
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn a_failing_on_startup_connects_no_broker() {
for live in [false, true] {
let broker = Counted::new();
match start(failing_app(broker.clone()), live).await {
Err(TestError::Startup(source)) => assert_eq!(
source.to_string(),
"the configuration is missing",
"the hook's own error is the start's (live: {live})",
),
other => panic!(
"expected the start to fail (live: {live}), got {:?}",
other.map(|_| ())
),
}
assert_eq!(
broker.connects(),
0,
"a failed on_startup left a broker connected (live: {live})",
);
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn on_startup_runs_before_the_brokers_connect() -> Result<(), Box<dyn Error>> {
for live in [false, true] {
let broker = Counted::new();
let tb = start(observing_app(broker.clone()), live).await?;
assert_eq!(broker.connects(), 1, "live: {live}");
tb.broker::<Counted>()
.message(&Order { id: 1 })
.publish()
.await?;
tb.broker::<Counted>()
.subscriber("orders")
.assert_called_once();
tb.shutdown().await?;
}
Ok(())
}
fn announcing_app() -> RustStream {
RustStream::new(AppInfo::new("announcing", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.after_startup(MemoryPublish, async move |publisher: MemoryPublisher| {
publisher
.message(&Order { id: 0 })
.publish()
.await
.map_err(|err| io::Error::other(err.to_string()))
});
b.include(take_order);
})
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn after_startup_runs_once_the_subscriptions_are_open() -> Result<(), Box<dyn Error>> {
let tb = TestApp::start(announcing_app()).await?;
tb.settle().await?;
tb.broker::<MemoryBroker>()
.subscriber("orders")
.assert_called_once()
.with(&Order { id: 0 });
tb.shutdown().await?;
Ok(())
}