#![allow(clippy::return_self_not_must_use, clippy::must_use_candidate)]
use std::marker::PhantomData;
use bytes::Bytes;
use crate::RawMessage;
use crate::codec::Codec;
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
use crate::codec::DefaultCodec;
use crate::runtime::HandlerOutcome;
use serde::de::DeserializeOwned;
use std::fmt::Debug;
use super::coordinator::{Coordinator, Delivered, Outcome, Record, RecordedOptions, SlotRecord};
#[derive(Debug)]
pub struct SubscriberAssertions<'a> {
coordinator: &'a Coordinator,
scope_id: usize,
name: String,
}
impl<'a> SubscriberAssertions<'a> {
pub(crate) fn new(coordinator: &'a Coordinator, scope_id: usize, name: String) -> Self {
Self {
coordinator,
scope_id,
name,
}
}
fn with_records<R>(&self, f: impl FnOnce(&[&Record]) -> R) -> R {
self.coordinator.with_records(self.scope_id, &self.name, f)
}
fn with_last<R>(&self, what: &str, f: impl FnOnce(&Record) -> R) -> R {
self.with_records(|records| {
let last = records.last().unwrap_or_else(|| {
panic!(
"subscriber {:?} was not called, cannot assert {what}",
self.name
)
});
f(last)
})
}
fn with_sole_delivery<R>(&self, what: &str, f: impl FnOnce(&Delivered) -> R) -> R {
self.with_last(what, |record| match record.deliveries.as_slice() {
[only] => f(only),
batch => panic!(
"subscriber {:?} last received a batch of {} deliveries, so it has no single \
{what}; read the batch with received_raw()",
self.name,
batch.len(),
),
})
}
pub fn assert_called_once(self) -> Self {
let count = self.with_records(|records| records.len());
assert_eq!(
count, 1,
"subscriber {:?} was called {count} times, expected exactly once",
self.name,
);
self
}
pub fn assert_called(self, times: usize) -> Self {
let count = self.with_records(|records| records.len());
assert_eq!(
count, times,
"subscriber {:?} was called {count} times, expected {times}",
self.name,
);
self
}
#[must_use]
pub fn received_raw(&self) -> Vec<Bytes> {
self.with_records(|records| {
records
.iter()
.flat_map(|record| record.deliveries.iter().map(|one| one.raw.clone()))
.collect()
})
}
#[must_use]
pub fn batches_raw(&self) -> Vec<Vec<Bytes>> {
self.with_records(|records| {
records
.iter()
.map(|record| {
record
.deliveries
.iter()
.map(|one| one.raw.clone())
.collect()
})
.collect()
})
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
#[must_use]
pub fn batches<T: DeserializeOwned>(&self) -> Vec<Vec<T>> {
self.batches_with(&DefaultCodec::default())
}
#[must_use]
pub fn batches_with<T, C>(&self, codec: &C) -> Vec<Vec<T>>
where
T: DeserializeOwned,
C: Codec,
{
self.with_records(|records| {
records
.iter()
.map(|record| {
record
.deliveries
.iter()
.map(|one| {
codec.decode(&one.raw).unwrap_or_else(|err| {
panic!(
"subscriber {:?} received a payload that did not decode as \
{}: {err}",
self.name,
std::any::type_name::<T>(),
)
})
})
.collect()
})
.collect()
})
}
#[must_use]
pub fn outcomes(&self) -> Vec<Outcome> {
self.with_records(|records| records.iter().map(|record| record.outcome()).collect())
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
#[must_use]
pub fn received<T: DeserializeOwned>(&self) -> Vec<T> {
self.received_with(&DefaultCodec::default())
}
#[must_use]
pub fn received_with<T, C>(&self, codec: &C) -> Vec<T>
where
T: DeserializeOwned,
C: Codec,
{
self.with_records(|records| {
records
.iter()
.flat_map(|record| record.deliveries.iter())
.map(|one| {
codec.decode(&one.raw).unwrap_or_else(|err| {
panic!(
"subscriber {:?} received a payload that did not decode as {}: {err}",
self.name,
std::any::type_name::<T>(),
)
})
})
.collect()
})
}
pub fn assert_not_called(self) {
let count = self.with_records(|records| records.len());
assert_eq!(
count, 0,
"subscriber {:?} was called {count} times, expected never",
self.name,
);
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
pub fn with<T>(self, expected: &T) -> Self
where
T: DeserializeOwned + PartialEq + Debug,
{
self.with_codec(&DefaultCodec::default(), expected)
}
pub fn with_codec<T, C>(self, codec: &C, expected: &T) -> Self
where
T: DeserializeOwned + PartialEq + Debug,
C: Codec,
{
self.with_sole_delivery("received value", |one| {
let actual: T = codec.decode(&one.raw).unwrap_or_else(|err| {
panic!(
"subscriber {:?} received a payload that did not decode as {}: {err}",
self.name,
std::any::type_name::<T>(),
)
});
assert_eq!(
&actual, expected,
"subscriber {:?} received an unexpected value",
self.name
);
});
self
}
pub fn with_raw(self, bytes: &[u8]) -> Self {
self.with_sole_delivery("raw payload", |one| {
assert_eq!(
one.raw.as_ref(),
bytes,
"subscriber {:?} received unexpected raw bytes",
self.name,
);
});
self
}
#[allow(clippy::needless_pass_by_value)]
pub fn settled(self, outcome: HandlerOutcome) -> Self {
let expected = Some(outcome.outcome());
self.with_last("the settlement", |record| {
let mismatched = record
.deliveries
.iter()
.filter(|one| one.settle != expected)
.count();
assert_eq!(
mismatched,
0,
"subscriber {:?} settled {mismatched} of its last {} deliveries differently \
than expected",
self.name,
record.deliveries.len(),
);
});
self
}
pub fn assert_batch_sizes(self, sizes: &[usize]) -> Self {
self.with_records(|records| {
let batches: Vec<usize> = records.iter().map(|r| r.deliveries.len()).collect();
assert_eq!(
batches.as_slice(),
sizes,
"subscriber {:?} was handed batches of {batches:?}",
self.name,
);
});
self
}
pub fn panicked(self) -> Self {
self.with_last("a panic", |record| {
assert!(
record.panicked,
"subscriber {:?} did not panic on its last delivery",
self.name,
);
});
self
}
pub fn assert_outcome(self, expected: Outcome) -> Self {
self.with_last("the outcome", |record| {
assert_eq!(
record.outcome(),
expected,
"subscriber {:?} had an unexpected outcome",
self.name,
);
});
self
}
pub fn assert_last_failed_to_decode(self) {
self.with_last("a decode failure", |record| {
assert!(
record.decode_failed,
"subscriber {:?} decoded its last delivery successfully",
self.name,
);
});
}
}
#[derive(Debug)]
pub struct PublishedAssertions<T> {
name: String,
messages: Vec<RawMessage>,
options: Vec<RecordedOptions>,
_payload: PhantomData<fn() -> T>,
}
impl<T> PublishedAssertions<T> {
pub(crate) fn new(
name: String,
messages: Vec<RawMessage>,
options: Vec<RecordedOptions>,
) -> Self {
Self {
name,
messages,
options,
_payload: PhantomData,
}
}
pub(crate) fn captured(name: String, records: Vec<SlotRecord>) -> Self {
let (messages, options) = records
.into_iter()
.map(|record| (record.message, record.options))
.unzip();
Self {
name,
messages,
options,
_payload: PhantomData,
}
}
pub fn assert_called_once(self) -> Self {
let count = self.messages.len();
assert_eq!(
count, 1,
"channel {:?} was published to {count} times, expected exactly once",
self.name,
);
self
}
pub fn assert_called(self, times: usize) -> Self {
let count = self.messages.len();
assert_eq!(
count, times,
"channel {:?} was published to {count} times, expected {times}",
self.name,
);
self
}
pub fn assert_not_called(self) {
let count = self.messages.len();
assert_eq!(
count, 0,
"channel {:?} was published to {count} times, expected never",
self.name,
);
}
#[must_use]
pub fn messages(&self) -> &[RawMessage] {
&self.messages
}
#[must_use]
pub fn decoded_as<U>(self) -> PublishedAssertions<U> {
PublishedAssertions {
name: self.name,
messages: self.messages,
options: self.options,
_payload: PhantomData,
}
}
fn last_options(&self, what: &str) -> &RecordedOptions {
self.last(what);
self.options.last().unwrap_or_else(|| {
panic!(
"nothing the runtime published carries per-message options on {:?}, cannot \
assert {what}: they are recorded for a reply and for a slot publish \
(`tb.out::<Marker>()`), never for a message the test published itself",
self.name,
)
})
}
pub fn with_options<Options>(self, expected: &Options) -> Self
where
Options: PartialEq + Debug + 'static,
{
let recorded = self.last_options("the per-message options");
let actual = recorded.downcast::<Options>(&self.name).unwrap_or_else(|| {
panic!(
"channel {:?} published with the policy's default options, expected \
{expected:?}",
self.name,
)
});
assert_eq!(
actual, expected,
"channel {:?} published with unexpected per-message options",
self.name,
);
self
}
pub fn assert_options_default(self) -> Self {
let recorded = self.last_options("the per-message options");
assert!(
recorded.value.is_none(),
"channel {:?} published with per-message options set (`{}`), expected the policy's \
defaults",
self.name,
recorded.type_name,
);
self
}
fn last(&self, what: &str) -> &RawMessage {
self.messages.last().unwrap_or_else(|| {
panic!(
"nothing was published to {:?}, cannot assert {what}",
self.name
)
})
}
pub fn with_raw(self, bytes: &[u8]) -> Self {
assert_eq!(
self.last("the raw payload").payload(),
bytes,
"channel {:?} published unexpected raw bytes",
self.name,
);
self
}
pub fn with_header(self, name: &str, value: impl AsRef<[u8]>) -> Self {
let message = self.last("the published headers");
let actual = message.headers().get(name).unwrap_or_else(|| {
panic!(
"channel {:?} published a message without the {name:?} header",
self.name,
)
});
assert_eq!(
actual,
value.as_ref(),
"channel {:?} published an unexpected {name:?} header",
self.name,
);
self
}
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
impl<T> PublishedAssertions<T>
where
T: DeserializeOwned + PartialEq + Debug,
{
pub fn with(self, expected: &T) -> Self {
self.with_codec(&DefaultCodec::default(), expected)
}
}
impl<T> PublishedAssertions<T>
where
T: DeserializeOwned + PartialEq + Debug,
{
pub fn with_codec<C: Codec>(self, codec: &C, expected: &T) -> Self {
let actual: T = codec
.decode(self.last("the published value").payload())
.unwrap_or_else(|err| {
panic!(
"channel {:?} published a payload that did not decode as {}: {err}",
self.name,
std::any::type_name::<T>(),
)
});
assert_eq!(
&actual, expected,
"channel {:?} published an unexpected value",
self.name
);
self
}
}
#[cfg(any(feature = "json", feature = "cbor", feature = "msgpack"))]
impl<T: DeserializeOwned> PublishedAssertions<T> {
#[must_use]
pub fn decoded(&self) -> Vec<T> {
self.decoded_with(&DefaultCodec::default())
}
}
impl<T: DeserializeOwned> PublishedAssertions<T> {
#[must_use]
pub fn decoded_with<C: Codec>(&self, codec: &C) -> Vec<T> {
self.messages
.iter()
.map(|message| {
codec.decode(message.payload()).unwrap_or_else(|err| {
panic!(
"channel {:?} published a payload that did not decode as {}: {err}",
self.name,
std::any::type_name::<T>(),
)
})
})
.collect()
}
}
#[cfg(all(test, feature = "json"))]
mod tests {
use super::*;
use crate::codec::JsonCodec;
#[derive(Debug, PartialEq, serde::Deserialize)]
struct Order {
id: u64,
}
fn undecodable() -> PublishedAssertions<Order> {
PublishedAssertions::new(
"orders".to_owned(),
vec![RawMessage::new("orders", b"not json".as_slice())],
Vec::new(),
)
}
#[test]
#[should_panic(expected = "channel \"orders\" published a payload that did not decode")]
fn a_published_payload_that_does_not_decode_names_the_channel_and_type() {
undecodable().with_codec(&JsonCodec, &Order { id: 7 });
}
#[test]
#[should_panic(expected = "did not decode as")]
fn decoding_every_published_payload_reports_the_first_failure() {
let _ = undecodable().decoded_with(&JsonCodec);
}
#[test]
#[should_panic(expected = "without the \"x-app\" header")]
fn a_missing_published_header_names_itself() {
let bare = PublishedAssertions::<Order>::new(
"orders".to_owned(),
vec![RawMessage::new("orders", br#"{"id":7}"#.as_slice())],
Vec::new(),
);
bare.with_header("x-app", b"1");
}
#[test]
#[should_panic(expected = "was published to 1 times, expected 2")]
fn a_wrong_published_count_reports_both_numbers() {
let one = PublishedAssertions::<Order>::new(
"orders".to_owned(),
vec![RawMessage::new("orders", br#"{"id":7}"#.as_slice())],
Vec::new(),
);
one.assert_called(2);
}
}