use std::fmt;
use turnframe_core::case::CaseRef;
use turnframe_core::command::{
AtomicityScope, CommandBatch, CommandEnvelope, CommandOrigin, IdempotencyKey, ResolutionChannel,
};
use turnframe_core::error::ExecutionError;
use turnframe_core::event::Commit;
use turnframe_core::flow::{WorkflowDefinition, WorkflowExecutor};
use turnframe_core::hash::{Digest, canonical_digest, derive_uuid};
use turnframe_core::ids::{BatchId, CaseRevision, CommandId, InteractionId, TurnId};
use turnframe_core::interaction::{ActionClass, InteractionKind};
use super::{ConformanceFailure, ExecutorFactory, SeedOf, SeededCase};
const TURN_DOMAIN: &str = "turnframe.test.executor.conformance.turn.v1";
pub(super) fn turn(label: &str) -> TurnId {
TurnId::from(derive_uuid(TURN_DOMAIN, &[label]))
}
pub(super) fn origin() -> CommandOrigin {
CommandOrigin::ConfirmedInteraction {
interaction_id: InteractionId::nil(),
payload_hash: Digest::of_bytes(b"turnframe.executor.conformance"),
interaction_kind: InteractionKind::ConfirmCommand,
action_class: ActionClass::AppliesOperation,
channel: ResolutionChannel::Click,
}
}
fn envelope<C: serde::Serialize + Clone>(
check: &'static str,
actor: &turnframe_core::turn::ActorContext,
turn_id: TurnId,
position: usize,
case_ref: &CaseRef,
command: C,
) -> Result<CommandEnvelope<C>, ConformanceFailure> {
let value = serde_json::to_value(&command).map_err(|error| {
ConformanceFailure::new(check, format!("a command did not serialize: {error}"))
})?;
let idempotency_key =
IdempotencyKey::derive(&actor.account_id, &turn_id, case_ref, &origin(), &value).map_err(
|error| {
ConformanceFailure::new(
check,
format!("an idempotency key could not be derived: {error}"),
)
},
)?;
Ok(CommandEnvelope {
command_id: CommandId::derive(
&turn_id,
turnframe_core::understanding::ActId::new(turnframe_core::understanding::UnitId(1), 1),
position,
),
turn_id,
actor: actor.clone(),
case_ref: case_ref.clone(),
idempotency_key,
origin: origin(),
command,
})
}
pub(super) fn batch<F: ExecutorFactory>(
check: &'static str,
seeded: &SeedOf<F>,
label: &str,
case_ref: &CaseRef,
commands: &[<F::Workflow as WorkflowDefinition>::Command],
) -> Result<CommandBatch<<F::Workflow as WorkflowDefinition>::Command>, ConformanceFailure> {
let turn_id = turn(label);
let mut envelopes = Vec::with_capacity(commands.len());
for (position, command) in commands.iter().enumerate() {
envelopes.push(envelope(
check,
&seeded.actor,
turn_id,
position,
case_ref,
command.clone(),
)?);
}
Ok(CommandBatch {
batch_id: BatchId::derive(&turn_id, &case_ref.key(), &AtomicityScope::PerCase),
scope: AtomicityScope::PerCase,
envelopes,
})
}
pub(super) async fn snapshot<W, E>(
check: &'static str,
what: &str,
seeded: &SeededCase<W, E>,
) -> Result<(Digest, CaseRevision), ConformanceFailure>
where
W: WorkflowDefinition,
E: WorkflowExecutor<W>,
{
let loaded = seeded
.executor
.load(&seeded.actor.account_id, &seeded.case_ref.case_id)
.await
.map_err(|error| {
ConformanceFailure::new(check, format!("{what}: load failed with {error:?}"))
})?;
let digest = canonical_digest(&loaded.value).map_err(|error| {
ConformanceFailure::new(check, format!("{what}: the state did not digest: {error}"))
})?;
Ok((digest, loaded.revision))
}
pub(super) async fn ensure_unchanged<W, E>(
check: &'static str,
what: &str,
seeded: &SeededCase<W, E>,
expected: &(Digest, CaseRevision),
) -> Result<(), ConformanceFailure>
where
W: WorkflowDefinition,
E: WorkflowExecutor<W>,
{
let actual = snapshot(check, what, seeded).await?;
ensure(
check,
actual.0 == expected.0 && actual.1 == expected.1,
format!(
"{what}: the case had to be untouched, expected revision {} state {}, \
found revision {} state {}",
expected.1,
expected.0.as_str(),
actual.1,
actual.0.as_str(),
),
)
}
pub(super) async fn seed<F: ExecutorFactory>(
check: &'static str,
factory: &F,
) -> Result<SeedOf<F>, ConformanceFailure> {
factory.seed().await.map_err(|detail| {
ConformanceFailure::new(check, format!("the factory could not seed: {detail}"))
})
}
pub(super) fn ensure(
check: &'static str,
condition: bool,
detail: impl Into<String>,
) -> Result<(), ConformanceFailure> {
if condition {
Ok(())
} else {
Err(ConformanceFailure::new(check, detail))
}
}
pub(super) fn ensure_eq<T: PartialEq + fmt::Debug>(
check: &'static str,
what: &str,
actual: &T,
expected: &T,
) -> Result<(), ConformanceFailure> {
ensure(
check,
actual == expected,
format!("{what}: expected {expected:?}, got {actual:?}"),
)
}
pub(super) fn ensure_commit<S, E>(
check: &'static str,
what: &str,
result: Result<Commit<S, E>, ExecutionError>,
) -> Result<Commit<S, E>, ConformanceFailure> {
result.map_err(|error| {
ConformanceFailure::new(check, format!("{what}: expected a commit, got {error:?}"))
})
}
pub(super) fn ensure_refused<S, E>(
check: &'static str,
what: &str,
result: Result<Commit<S, E>, ExecutionError>,
) -> Result<ExecutionError, ConformanceFailure> {
match result {
Err(error) => Ok(error),
Ok(commit) => Err(ConformanceFailure::new(
check,
format!(
"{what}: expected a refusal, got a commit at revision {}",
commit.new_revision
),
)),
}
}