mod batch;
mod fixtures;
mod idempotency;
mod in_memory;
mod revision;
use std::fmt;
use turnframe_core::case::CaseRef;
use turnframe_core::command::CommandBatch;
use turnframe_core::flow::{WorkflowDefinition, WorkflowExecutor};
use turnframe_core::turn::ActorContext;
pub use self::batch::{
check_interrupted_batch_resumes_to_the_same_state, check_per_case_batch_is_all_or_nothing,
check_refused_command_leaves_the_case_byte_identical,
};
pub use self::idempotency::{
check_repeated_key_replays_the_outcome, check_repeated_key_with_another_command_is_refused,
};
pub use self::in_memory::{CONFORMANCE_ACCOUNT, CONFORMANCE_USER, InMemoryCase};
pub use self::revision::{
check_commit_reports_the_revision_it_reached, check_stale_expected_revision_is_a_conflict,
};
pub type WorkflowOf<F> = <F as ExecutorFactory>::Workflow;
pub type CommandOf<F> = <WorkflowOf<F> as WorkflowDefinition>::Command;
pub type BatchOf<F> = CommandBatch<CommandOf<F>>;
pub type SeedOf<F> = SeededCase<WorkflowOf<F>, <F as ExecutorFactory>::Executor>;
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("executor 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 failed_checks(&self) -> impl Iterator<Item = &'static str> {
self.failures().map(|failure| failure.check)
}
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 {} executor 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 struct SeededCase<W: WorkflowDefinition, E: WorkflowExecutor<W>> {
pub executor: E,
pub actor: ActorContext,
pub case_ref: CaseRef,
pub first: W::Command,
pub second: W::Command,
pub refused: W::Command,
}
impl<W: WorkflowDefinition, E: WorkflowExecutor<W>> SeededCase<W, E> {
pub fn new(
executor: E,
actor: ActorContext,
case_ref: CaseRef,
first: W::Command,
second: W::Command,
refused: W::Command,
) -> Self {
Self {
executor,
actor,
case_ref,
first,
second,
refused,
}
}
}
impl<W: WorkflowDefinition, E: WorkflowExecutor<W>> fmt::Debug for SeededCase<W, E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SeededCase")
.field("workflow", &self.case_ref.workflow)
.field("case_id", &self.case_ref.case_id)
.field("revision", &self.case_ref.expected_revision)
.finish_non_exhaustive()
}
}
#[async_trait::async_trait]
pub trait ExecutorFactory: Send + Sync {
type Workflow: WorkflowDefinition;
type Executor: WorkflowExecutor<Self::Workflow> + Send + Sync;
async fn seed(&self) -> Result<SeedOf<Self>, String>;
async fn interrupt_after(
&self,
seeded: &SeedOf<Self>,
batch: &BatchOf<Self>,
applied: usize,
) -> Result<(), String>;
}
pub const CHECK_COUNT: usize = 7;
pub async fn run_all<F: ExecutorFactory>(factory: &F) -> ConformanceReport {
let mut outcomes = Vec::with_capacity(CHECK_COUNT);
macro_rules! run {
($($check:path),* $(,)?) => {
$(
outcomes.push(CheckOutcome {
check: stringify!($check).rsplit("::").next().unwrap_or(stringify!($check)),
failure: $check(factory).await.err(),
});
)*
};
}
run!(
check_stale_expected_revision_is_a_conflict,
check_commit_reports_the_revision_it_reached,
check_repeated_key_replays_the_outcome,
check_repeated_key_with_another_command_is_refused,
check_per_case_batch_is_all_or_nothing,
check_interrupted_batch_resumes_to_the_same_state,
check_refused_command_leaves_the_case_byte_identical,
);
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);
assert_eq!(report.failed_checks().collect::<Vec<_>>(), vec!["check_b"]);
let rendered = report.to_string();
assert!(rendered.contains("1 of 2 executor conformance checks passed"));
assert!(rendered.contains("FAIL check_b: expected 1, got 2"));
assert_eq!(report.into_result().unwrap_err().check, "check_b");
}
#[test]
fn an_empty_report_passes() {
let report = ConformanceReport {
outcomes: Vec::new(),
};
assert!(report.passed());
assert!(report.into_result().is_ok());
}
}