mod commit_bundle;
mod conversations;
mod events;
mod fixtures;
mod interactions;
mod journal;
mod outbox;
mod replay;
use std::fmt;
use crate::stores::Stores;
pub use self::commit_bundle::{
check_commit_bundle_applies_all, check_commit_bundle_atomic_on_invalid_item,
check_commit_bundle_rejects_foreign_account_items,
check_commit_bundle_restores_modified_records,
};
pub use self::conversations::check_conversation_turn_persistence;
pub use self::events::{
check_event_append_ordering_and_get_by_ids, check_event_redaction_is_audited_and_idempotent,
check_event_redaction_preserves_identity_and_order,
check_event_stream_cursor_pages_exactly_once, check_revision_read_truncates_inside_a_revision,
};
pub use self::interactions::{
check_administrative_invalidation_ignores_the_revision,
check_answered_blocking_card_is_remembered, check_begin_resolution_cas,
check_blocking_interaction_conflict, check_blocking_interaction_replace,
check_cross_tenant_isolation, check_finish_resolution_idempotent, check_interaction_expiry,
check_revision_invalidation_respects_independence,
};
pub use self::journal::{
check_awaiting_confirmation_is_never_resumed, check_journal_idempotency_replay,
check_pending_for_turn_after_partial_write,
};
pub use self::outbox::{
check_outbox_claim_exclusivity_and_reschedule, check_refused_settlement_writes_nothing,
};
pub use self::replay::check_replay_put_get;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("conformance check `{check}` failed: {detail}")]
pub struct ConformanceFailure {
pub check: &'static str,
pub detail: String,
}
impl ConformanceFailure {
#[must_use]
pub fn new(check: &'static str, detail: impl Into<String>) -> Self {
Self {
check,
detail: detail.into(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CheckOutcome {
pub check: &'static str,
pub failure: Option<ConformanceFailure>,
}
impl CheckOutcome {
#[must_use]
pub fn passed(&self) -> bool {
self.failure.is_none()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ConformanceReport {
pub outcomes: Vec<CheckOutcome>,
}
impl ConformanceReport {
#[must_use]
pub fn passed(&self) -> bool {
self.outcomes.iter().all(CheckOutcome::passed)
}
pub fn failures(&self) -> impl Iterator<Item = &ConformanceFailure> {
self.outcomes
.iter()
.filter_map(|outcome| outcome.failure.as_ref())
}
pub fn into_result(self) -> Result<(), ConformanceFailure> {
match self.outcomes.into_iter().find_map(|o| o.failure) {
Some(failure) => Err(failure),
None => Ok(()),
}
}
}
impl fmt::Display for ConformanceReport {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let failed = self.failures().count();
writeln!(
f,
"{} of {} conformance checks passed",
self.outcomes.len() - failed,
self.outcomes.len()
)?;
for outcome in &self.outcomes {
match &outcome.failure {
None => writeln!(f, " pass {}", outcome.check)?,
Some(failure) => writeln!(f, " FAIL {}: {}", outcome.check, failure.detail)?,
}
}
Ok(())
}
}
pub trait StoreFactory: Send + Sync {
fn build(&self) -> Stores;
}
impl<F> StoreFactory for F
where
F: Fn() -> Stores + Send + Sync,
{
fn build(&self) -> Stores {
self()
}
}
pub const CHECK_COUNT: usize = 25;
pub async fn run_all(factory: &dyn StoreFactory) -> ConformanceReport {
let mut outcomes = Vec::new();
macro_rules! run {
($($check:path),* $(,)?) => {
$(
let stores = factory.build();
outcomes.push(CheckOutcome {
check: stringify!($check).rsplit("::").next().unwrap_or(stringify!($check)),
failure: $check(&stores).await.err(),
});
)*
};
}
run!(
check_blocking_interaction_conflict,
check_blocking_interaction_replace,
check_cross_tenant_isolation,
check_begin_resolution_cas,
check_finish_resolution_idempotent,
check_revision_invalidation_respects_independence,
check_interaction_expiry,
check_answered_blocking_card_is_remembered,
check_administrative_invalidation_ignores_the_revision,
check_journal_idempotency_replay,
check_pending_for_turn_after_partial_write,
check_awaiting_confirmation_is_never_resumed,
check_event_append_ordering_and_get_by_ids,
check_event_stream_cursor_pages_exactly_once,
check_revision_read_truncates_inside_a_revision,
check_event_redaction_preserves_identity_and_order,
check_event_redaction_is_audited_and_idempotent,
check_outbox_claim_exclusivity_and_reschedule,
check_refused_settlement_writes_nothing,
check_replay_put_get,
check_conversation_turn_persistence,
check_commit_bundle_applies_all,
check_commit_bundle_atomic_on_invalid_item,
check_commit_bundle_restores_modified_records,
check_commit_bundle_rejects_foreign_account_items,
);
ConformanceReport { outcomes }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn report_renders_and_keeps_the_first_failure() {
let report = ConformanceReport {
outcomes: vec![
CheckOutcome {
check: "check_a",
failure: None,
},
CheckOutcome {
check: "check_b",
failure: Some(ConformanceFailure::new("check_b", "expected 1, got 2")),
},
],
};
assert!(!report.passed());
assert_eq!(report.failures().count(), 1);
let rendered = report.to_string();
assert!(rendered.contains("1 of 2 conformance checks passed"));
assert!(rendered.contains("FAIL check_b: expected 1, got 2"));
assert_eq!(
report.into_result().unwrap_err().check,
"check_b",
"into_result keeps the first failure"
);
}
#[test]
fn empty_report_passes() {
let report = ConformanceReport {
outcomes: Vec::new(),
};
assert!(report.passed());
assert!(report.into_result().is_ok());
}
}