use std::{fmt, future::Future, thread, time::Duration};
use super::helpers::unique_subject;
#[cfg(feature = "asyncapi")]
use crate::DescribeServer;
#[cfg(feature = "asyncapi")]
use crate::asyncapi::build_spec;
#[cfg(feature = "asyncapi")]
use crate::runtime::{AppInfo, RustStream};
use crate::{
AckError, Broker, Connected, ConnectedBroker, HeaderMap, IncomingMessage, OutgoingMessage,
Publisher, RedeliveryAddressed, Subscribe, Subscriber, SubscriptionSource,
testing::{Backlog, InProcess, TestableBroker},
};
use bytes::Bytes;
use futures::StreamExt;
use tokio::{runtime, sync::oneshot, time::timeout};
const DEFAULT_TIMEOUT: Duration = Duration::from_secs(2);
const NEGATIVE_WAIT: Duration = Duration::from_millis(100);
const REDELIVERY_DELAY: Duration = Duration::from_millis(200);
const REDELIVERY_TIMEOUT: Duration = Duration::from_secs(10);
type SubscriberMessage<S> = <S as Subscriber>::Message;
pub async fn run_suite<B, F>(factory: F)
where
B: InProcess,
B::Connected: Subscribe,
F: Fn() -> B,
{
let connect = async move |broker: B| {
broker
.connect_in_process()
.await
.expect("broker must connect in process before a suite scenario")
};
ordering(connect(factory()).await).await;
publish_before_subscribe(connect(factory()).await).await;
ack_consumes_delivery(connect(factory()).await).await;
nack_with_requeue_redelivers(connect(factory()).await).await;
nack_without_requeue_drops(connect(factory()).await).await;
headers_propagate(connect(factory()).await).await;
published_log_observes_publishes(connect(factory()).await).await;
}
#[derive(Debug, Clone)]
pub struct InProcessBroker<B>(B);
impl<B> InProcessBroker<B> {
#[must_use]
pub const fn new(broker: B) -> Self {
Self(broker)
}
}
impl<B: InProcess> Broker for InProcessBroker<B> {
type Error = B::Error;
type Connected = B::Connected;
fn connect(self) -> impl Future<Output = Result<Self::Connected, Self::Error>> + Send {
self.0.connect_in_process()
}
}
pub async fn lifecycle<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: SubscriptionSource<Connected<B>> + Clone + Send,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
SubscriberMessage<Src::Subscriber>: 'static,
Pub: Publisher + 'static,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.lifecycle");
let connected = make_broker()
.connect()
.await
.expect("broker must connect after synchronous construction");
let source = make_source(&subject);
let mut subscriber = source
.clone()
.subscribe(&connected)
.await
.expect("subscription source must open against the connected form");
let publisher = make_publisher(&connected);
let destination = subject.clone();
let publisher = on_foreign_runtime(async move || {
publisher
.publish(
OutgoingMessage::new(&destination, b"lifecycle".as_slice()),
None,
)
.await
.expect("publish after connect failed");
publisher
})
.await;
let mut stream = std::pin::pin!(subscriber.stream());
let msg = expect_next(&mut stream, "lifecycle").await;
assert_eq!(
msg.payload(),
b"lifecycle",
"subscription opened through SubscriptionSource must receive the publish",
);
let msg = if msg.supports_nack_after() {
on_foreign_runtime(async move || {
msg.nack_after(REDELIVERY_DELAY)
.await
.expect("a delayed nack the delivery offers must be accepted");
})
.await;
let again = timeout(REDELIVERY_DELAY + REDELIVERY_TIMEOUT, stream.next())
.await
.unwrap_or_else(|_| {
panic!(
"lifecycle: a delayed nack settled from another runtime never came back; the \
broker must run its internal tasks on the runtime it connected on",
)
})
.expect("lifecycle: stream ended before the delayed redelivery")
.unwrap_or_else(|err| panic!("lifecycle: stream yielded error: {err:?}"));
assert_eq!(
again.payload(),
b"lifecycle",
"a delayed nack must redeliver the same message",
);
again
} else {
msg
};
on_foreign_runtime(async move || match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
})
.await;
let _closed = connected
.shutdown()
.await
.expect("broker must shut down cleanly");
assert!(
publisher
.publish(
OutgoingMessage::new(&subject, b"post-shutdown".as_slice()),
None,
)
.await
.is_err(),
"publish through a handle aliasing the closed connection must error",
);
}
pub async fn redelivery_address<B, MkBroker, Src, MkSrc, Pub, MkPub>(
make_broker: MkBroker,
make_source: MkSrc,
make_publisher: MkPub,
) where
B: Broker,
MkBroker: Fn() -> B,
Src: RedeliveryAddressed<Connected<B>> + Clone + Send + Sync,
Src::Subscriber: Send,
MkSrc: Fn(&str) -> Src,
Pub: Publisher,
MkPub: Fn(&Connected<B>) -> Pub,
{
let subject = unique_subject("conformance.redelivery");
let connected = make_broker()
.connect()
.await
.expect("broker must connect after synchronous construction");
let source = make_source(&subject);
let address = source
.redelivery_address(&connected)
.await
.expect("reporting a redelivery address must not fail against a live connection");
let mut subscriber = source
.subscribe(&connected)
.await
.expect("subscription source must open against the connected form");
let publisher = make_publisher(&connected);
publisher
.publish(
OutgoingMessage::new(address.as_str(), b"redelivered".as_slice()),
None,
)
.await
.expect("publish to the reported redelivery address failed");
let mut stream = std::pin::pin!(subscriber.stream());
let msg = expect_next(&mut stream, "redelivery_address").await;
assert_eq!(
msg.payload(),
b"redelivered",
"a publish to the reported redelivery address must reach the subscription that reported \
it",
);
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
}
let _closed = connected
.shutdown()
.await
.expect("broker must shut down cleanly");
}
#[cfg(feature = "asyncapi")]
pub fn describes_without_credentials<B, Src>(broker: &B, source: &Src, secret: &str)
where
B: DescribeServer,
Src: SubscriptionSource<Connected<B>>,
{
assert!(
!secret.is_empty(),
"pass the password the broker was configured with; scanning for an empty string passes \
whatever the broker does",
);
let app = RustStream::new(AppInfo::new("conformance", "0.0.0"))
.server("broker", broker.describe_server());
let document = build_spec(&app)
.to_json()
.expect("the generated document must serialize");
let bindings = serde_json::to_string(&serde_json::json!({
"channel": source.channel_bindings(),
"operation": source.operation_bindings(),
"message": source.message_bindings(),
}))
.expect("a binding body is serialized once at construction, so it serializes again here");
for (what, text) in [
("the server description", &document),
("the subscription bindings", &bindings),
] {
assert!(
!text.contains(secret),
"{what} carries the broker's password. A published document is shared: describe the \
host with ServerSpec::from_url, which drops the userinfo, and keep credentials out \
of every binding body. Got: {text}",
);
}
}
async fn ordering<C: TestableBroker + Subscribe>(broker: C) {
let mut subscriber = Subscribe::subscribe(&broker, "conformance.ordering")
.await
.expect("subscribe failed");
for i in 0..10u32 {
broker.inject(OutgoingMessage::new(
"conformance.ordering",
i.to_be_bytes().as_slice(),
));
}
let mut stream = std::pin::pin!(subscriber.stream());
for expected in 0..10u32 {
let msg = expect_next(&mut stream, "ordering").await;
assert_eq!(
msg.payload(),
expected.to_be_bytes(),
"messages must be delivered in publish order",
);
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
}
}
broker.shutdown().await.expect("shutdown failed");
}
async fn publish_before_subscribe<C: TestableBroker + Subscribe>(broker: C) {
let backlog = broker.backlog();
broker.inject(OutgoingMessage::new(
"conformance.late",
b"before-subscribe".as_slice(),
));
let mut subscriber = Subscribe::subscribe(&broker, "conformance.late")
.await
.expect("subscribe failed");
broker.inject(OutgoingMessage::new(
"conformance.late",
b"after-subscribe".as_slice(),
));
let mut stream = std::pin::pin!(subscriber.stream());
if backlog == Backlog::Delivered {
let early = expect_next(&mut stream, "publish_before_subscribe").await;
assert_eq!(
early.payload(),
b"before-subscribe",
"the broker declares `Backlog::Delivered`: a subscription must first receive what was \
published to its name before it opened",
);
settle_ack(early).await;
}
let msg = expect_next(&mut stream, "publish_before_subscribe").await;
assert_eq!(
msg.payload(),
b"after-subscribe",
"the broker declares `Backlog::{backlog:?}`: {}",
match backlog {
Backlog::Missed =>
"a subscription must receive only messages published after it opened",
Backlog::Delivered => "the message published after the subscription opened comes next",
},
);
settle_ack(msg).await;
broker.shutdown().await.expect("shutdown failed");
}
async fn settle_ack<M: IncomingMessage>(msg: M) {
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
}
}
async fn ack_consumes_delivery<C: TestableBroker + Subscribe>(broker: C) {
let mut subscriber = Subscribe::subscribe(&broker, "conformance.ack")
.await
.expect("subscribe failed");
broker.inject(OutgoingMessage::new("conformance.ack", b"one".as_slice()));
let mut stream = std::pin::pin!(subscriber.stream());
let msg = expect_next(&mut stream, "ack_consumes_delivery").await;
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
}
expect_no_more(&mut stream, "ack_consumes_delivery").await;
broker.shutdown().await.expect("shutdown failed");
}
async fn nack_with_requeue_redelivers<C: TestableBroker + Subscribe>(broker: C) {
let mut subscriber = Subscribe::subscribe(&broker, "conformance.requeue")
.await
.expect("subscribe failed");
broker.inject(OutgoingMessage::new(
"conformance.requeue",
b"retry-me".as_slice(),
));
let mut stream = std::pin::pin!(subscriber.stream());
let first = expect_next(&mut stream, "nack_with_requeue first").await;
assert_eq!(first.payload(), b"retry-me");
let requeued = match first.nack(true).await {
Ok(()) => true,
Err(AckError::Unsupported) => false,
Err(other) => panic!("nack must succeed or be unsupported, got: {other:?}"),
};
if requeued {
let second = expect_next(&mut stream, "nack_with_requeue second").await;
assert_eq!(
second.payload(),
b"retry-me",
"nack(requeue=true) must redeliver the same payload",
);
match second.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
}
}
broker.shutdown().await.expect("shutdown failed");
}
async fn nack_without_requeue_drops<C: TestableBroker + Subscribe>(broker: C) {
let mut subscriber = Subscribe::subscribe(&broker, "conformance.drop")
.await
.expect("subscribe failed");
broker.inject(OutgoingMessage::new("conformance.drop", b"gone".as_slice()));
let mut stream = std::pin::pin!(subscriber.stream());
let msg = expect_next(&mut stream, "nack_without_requeue").await;
match msg.nack(false).await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("nack must succeed or be unsupported, got: {other:?}"),
}
expect_no_more(&mut stream, "nack_without_requeue").await;
broker.shutdown().await.expect("shutdown failed");
}
async fn headers_propagate<C: TestableBroker + Subscribe>(broker: C) {
let mut subscriber = Subscribe::subscribe(&broker, "conformance.headers")
.await
.expect("subscribe failed");
let mut headers = HeaderMap::new();
headers.insert("Content-Type", "application/json");
headers.insert("X-Tenant", Bytes::from_static(b"acme"));
broker.inject(
OutgoingMessage::new("conformance.headers", b"{}".as_slice()).with_headers(headers),
);
let mut stream = std::pin::pin!(subscriber.stream());
let msg = expect_next(&mut stream, "headers_propagate").await;
assert_eq!(msg.headers().content_type(), Some("application/json"));
assert_eq!(msg.headers().get("x-tenant"), Some(b"acme".as_slice()));
match msg.ack().await {
Ok(()) | Err(AckError::Unsupported) => {}
Err(other) => panic!("ack must succeed or be unsupported, got: {other:?}"),
}
broker.shutdown().await.expect("shutdown failed");
}
async fn published_log_observes_publishes<C: TestableBroker + Subscribe>(broker: C) {
broker.inject(OutgoingMessage::new(
"conformance.observe",
b"first".as_slice(),
));
broker.inject(OutgoingMessage::new(
"conformance.observe",
b"second".as_slice(),
));
let observed = broker.published("conformance.observe");
assert_eq!(
observed.len(),
2,
"the publish log must observe every publish",
);
assert_eq!(observed[0].payload(), b"first");
assert_eq!(observed[1].payload(), b"second");
broker.shutdown().await.expect("shutdown failed");
}
pub(crate) async fn on_foreign_runtime<Output>(
work: impl AsyncFnOnce() -> Output + Send + 'static,
) -> Output
where
Output: Send + 'static,
{
let (done, finished) = oneshot::channel();
thread::Builder::new()
.name("conformance-foreign-runtime".to_owned())
.spawn(move || {
let runtime = runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("a current-thread runtime for the foreign caller must build");
let output = runtime.block_on(work());
drop(runtime);
let _ = done.send(output);
})
.expect("the foreign caller's thread must start");
finished
.await
.expect("the work on the foreign runtime panicked; its message is above")
}
pub(crate) async fn expect_next<S, M, E>(stream: &mut S, label: &str) -> M
where
S: futures::Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
let item = timeout(DEFAULT_TIMEOUT, stream.next())
.await
.unwrap_or_else(|_| panic!("{label}: stream timed out"));
let item = item.unwrap_or_else(|| panic!("{label}: stream ended unexpectedly"));
item.unwrap_or_else(|err| panic!("{label}: stream yielded error: {err:?}"))
}
pub(crate) async fn expect_no_more<S, M, E>(stream: &mut S, label: &str)
where
S: futures::Stream<Item = Result<M, E>> + Unpin,
M: IncomingMessage,
E: fmt::Debug,
{
let result = timeout(NEGATIVE_WAIT, stream.next()).await;
assert!(
result.is_err(),
"{label}: expected no further deliveries within {NEGATIVE_WAIT:?}",
);
}