#![cfg(all(
feature = "testing",
feature = "memory",
feature = "json",
feature = "macros"
))]
use std::convert::Infallible;
use std::future::{Future, ready};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use ruststream::memory::{MemoryBroker, Retaining};
use ruststream::runtime::{AppInfo, HandlerOutcome, RustStream};
use ruststream::testing::{TestApp, TestError};
use ruststream::{Broker, ConnectedBroker, Deserialized, subscriber};
use serde::{Deserialize, Serialize};
use tokio::sync::Notify;
use tokio::time::Instant;
#[derive(Serialize, Deserialize, PartialEq, Debug, schemars::JsonSchema)]
struct Order {
id: u64,
}
#[derive(Deserialized)]
struct Frame<'a>(&'a [u8]);
#[subscriber("orders")]
async fn handle_orders(order: &Order) -> HandlerOutcome {
let _ = order.id;
HandlerOutcome::ack()
}
#[subscriber("boom")]
async fn always_panics(order: &Order) -> HandlerOutcome {
assert!(order.id != 0, "boom on id 0");
HandlerOutcome::ack()
}
#[subscriber("frames")]
async fn ingest(frame: &Frame<'_>) -> HandlerOutcome {
let _ = frame.0.len();
HandlerOutcome::ack()
}
#[derive(Debug)]
struct Opaque;
#[derive(Debug)]
struct ConnectedOpaque;
#[derive(Debug, thiserror::Error)]
#[error("the opaque broker performs no I/O")]
struct OpaqueError;
impl Broker for Opaque {
type Error = OpaqueError;
type Connected = ConnectedOpaque;
fn connect(self) -> impl Future<Output = Result<ConnectedOpaque, OpaqueError>> {
ready(Ok(ConnectedOpaque))
}
}
impl ConnectedBroker for ConnectedOpaque {
type Error = OpaqueError;
type Closed = ();
fn shutdown(self) -> impl Future<Output = Result<(), OpaqueError>> {
ready(Ok(()))
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn an_app_with_a_broker_that_has_no_in_process_mode_does_not_start() {
let app = RustStream::new(AppInfo::new("svc", "0.1.0"))
.register_broker(Opaque)
.with_broker(MemoryBroker::new(), |b| {
b.include(handle_orders);
});
match TestApp::start(app).await {
Err(TestError::NoTransport(broker)) => {
assert!(broker.ends_with("Opaque"), "{broker}");
}
other => panic!(
"expected a missing in-process mode, got {:?}",
other.map(|_| ())
),
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[should_panic(expected = "no registered broker of type")]
async fn addressing_an_unregistered_broker_type_names_it() {
let app = RustStream::new(AppInfo::new("svc", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(handle_orders);
});
let tb = TestApp::start(app).await.expect("start");
let _ = tb.broker::<MemoryBroker<Retaining>>();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[should_panic(expected = "no registered broker of type")]
async fn a_mirror_state_addressing_an_unregistered_broker_type_names_it() {
let app = RustStream::new(AppInfo::new("svc", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(handle_orders);
});
let _ = TestApp::with_state(app, |brokers| {
let _ = brokers.broker::<MemoryBroker<Retaining>>();
})
.await;
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[should_panic(expected = "more than one broker of type")]
async fn a_mirror_state_addressing_a_duplicated_broker_type_names_it() {
let app = RustStream::new(AppInfo::new("svc", "0.1.0"))
.with_broker_labeled("a", MemoryBroker::new(), |b| {
b.include(handle_orders);
})
.with_broker_labeled("b", MemoryBroker::new(), |b| {
b.include(ingest);
});
let _ = TestApp::with_state(app, |brokers| {
let _ = brokers.broker::<MemoryBroker>();
})
.await;
}
#[derive(Default)]
struct Gate {
release: Notify,
drained: Mutex<Vec<u64>>,
}
impl Gate {
fn drained(&self) -> Vec<u64> {
self.drained
.lock()
.expect("the test holds no poisoned lock")
.clone()
}
}
#[subscriber("gated")]
async fn gated(order: &Order, ctx: &mut Context<'_, (), Arc<Gate>>) -> HandlerOutcome {
let id = order.id;
let gate = Arc::clone(ctx.state());
HandlerOutcome::ack().and_after(async move {
gate.release.notified().await;
gate.drained
.lock()
.expect("the test holds no poisoned lock")
.push(id);
})
}
#[tokio::test]
async fn drain_waits_for_a_still_pending_post_settle_continuation() {
let gate = Arc::new(Gate::default());
let state = Arc::clone(&gate);
let app = RustStream::new(AppInfo::new("svc", "0.1.0"))
.on_startup(async move |()| Ok::<_, Infallible>(state))
.with_broker(MemoryBroker::new(), |b| {
b.include(gated);
});
let tb = TestApp::start(app).await.expect("start");
tb.publish("gated", &Order { id: 3 })
.await
.expect("publish");
assert!(
gate.drained().is_empty(),
"the continuation must still be pending when the settle returns",
);
gate.release.notify_one();
tb.drain().await;
assert_eq!(gate.drained(), [3]);
tb.shutdown().await.expect("shutdown");
}
#[tokio::test(start_paused = true)]
async fn a_shutdown_timeout_the_loops_stay_inside_is_not_waited_out() {
const SHUTDOWN_TIMEOUT: Duration = Duration::from_secs(5);
let app = RustStream::new(AppInfo::new("svc", "0.1.0"))
.shutdown_timeout(SHUTDOWN_TIMEOUT)
.with_broker(MemoryBroker::new(), |b| {
b.include(handle_orders);
});
let tb = TestApp::start(app).await.expect("start");
tb.publish("orders", &Order { id: 1 })
.await
.expect("publish");
let before = Instant::now();
tb.shutdown().await.expect("shutdown");
assert!(
before.elapsed() < SHUTDOWN_TIMEOUT,
"the shutdown waited out its timeout: {:?}",
before.elapsed(),
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
#[should_panic(expected = "expected the service to be running")]
async fn assert_running_reports_why_the_service_stopped() {
let app = RustStream::new(AppInfo::new("svc", "0.1.0")).with_broker(MemoryBroker::new(), |b| {
b.include(always_panics);
});
let tb = TestApp::start(app).await.expect("start");
tb.publish("boom", &Order { id: 0 }).await.expect("publish");
tb.assert_running();
}