use std::{fmt, future::Future, thread, time::Duration};
pub use super::retry::redelivery_address;
#[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, HeaderMap, IncomingMessage, OutgoingMessage, Publisher, 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);
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;
super::in_process::run(factory).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 + Send + 'static,
MkBroker: Fn() -> B + Send + Sync + 'static,
Src: SubscriptionSource<Connected<B>> + Clone + Send + 'static,
Src::Subscriber: Send + 'static,
MkSrc: Fn(&str) -> Src,
SubscriberMessage<Src::Subscriber>: 'static,
Pub: Publisher + 'static,
MkPub: Fn(&Connected<B>) -> Pub,
{
let (make_broker, make_source, make_publisher) =
super::message_shape::publisher_carries(make_broker, make_source, make_publisher).await;
super::lifecycle::ladder(make_broker, make_source, make_publisher).await;
}
#[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: serde_json::Value = serde_json::from_str(
&build_spec(&app)
.to_json()
.expect("the generated document must serialize"),
)
.expect("the generated document is JSON");
let bindings = serde_json::json!({
"channel": source.channel_bindings(),
"operation": source.operation_bindings(),
"message": source.message_bindings(),
});
for (what, tree) in [
("the server description", &document),
("the subscription bindings", &bindings),
] {
assert!(
!super::message_shape::carries_secret(tree, 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.",
);
}
}
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:?}",
);
}