jellyflow-runtime 0.2.0

Headless store, rules, schema, profile, and change pipeline for Jellyflow.
Documentation
use std::fmt;

use serde::{Deserialize, Serialize};

use super::scenario::ConformanceTraceEvent;

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformanceRunReport {
    pub scenario: String,
    pub actual_trace: Vec<ConformanceTraceEvent>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub mismatches: Vec<ConformanceTraceMismatch>,
}

impl ConformanceRunReport {
    pub fn new(
        scenario: impl Into<String>,
        actual_trace: Vec<ConformanceTraceEvent>,
        expected_trace: &[ConformanceTraceEvent],
    ) -> Self {
        let mismatches = trace_mismatches(expected_trace, &actual_trace);
        Self {
            scenario: scenario.into(),
            actual_trace,
            mismatches,
        }
    }

    pub fn is_match(&self) -> bool {
        self.mismatches.is_empty()
    }

    pub fn actual_trace(&self) -> &[ConformanceTraceEvent] {
        &self.actual_trace
    }

    pub fn mismatches(&self) -> &[ConformanceTraceMismatch] {
        &self.mismatches
    }
}

impl fmt::Display for ConformanceRunReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.is_match() {
            return write!(
                f,
                "conformance scenario `{}` matched {} trace events",
                self.scenario,
                self.actual_trace.len()
            );
        }

        writeln!(
            f,
            "conformance trace mismatch for scenario `{}` ({} mismatch(es))",
            self.scenario,
            self.mismatches.len()
        )?;
        for mismatch in self.mismatches.iter().take(8) {
            writeln!(
                f,
                "  [{}] expected: {:?}; actual: {:?}",
                mismatch.index, mismatch.expected, mismatch.actual
            )?;
        }
        if self.mismatches.len() > 8 {
            writeln!(f, "  ... {} more", self.mismatches.len() - 8)?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConformanceSuiteReport {
    pub suite: String,
    pub scenario_reports: Vec<ConformanceRunReport>,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub errors: Vec<ConformanceRunError>,
}

impl ConformanceSuiteReport {
    pub fn is_match(&self) -> bool {
        self.errors.is_empty()
            && self
                .scenario_reports
                .iter()
                .all(ConformanceRunReport::is_match)
    }

    pub fn failed_scenarios(&self) -> usize {
        self.errors.len()
            + self
                .scenario_reports
                .iter()
                .filter(|report| !report.is_match())
                .count()
    }

    pub fn scenario_count(&self) -> usize {
        self.scenario_reports.len() + self.errors.len()
    }
}

impl fmt::Display for ConformanceSuiteReport {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        if self.is_match() {
            return write!(
                f,
                "conformance suite `{}` matched {} scenario(s)",
                self.suite,
                self.scenario_count()
            );
        }

        writeln!(
            f,
            "conformance suite `{}` failed: {} scenario(s), {} execution error(s)",
            self.suite,
            self.failed_scenarios(),
            self.errors.len()
        )?;
        for report in self
            .scenario_reports
            .iter()
            .filter(|report| !report.is_match())
            .take(8)
        {
            writeln!(
                f,
                "  scenario `{}` mismatched {} trace event(s)",
                report.scenario,
                report.mismatches.len()
            )?;
        }
        for error in self.errors.iter().take(8) {
            writeln!(
                f,
                "  scenario `{}` errored at action {} ({}): {}",
                error.scenario, error.action_index, error.action_kind, error.message
            )?;
        }
        Ok(())
    }
}

#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ConformanceTraceMismatch {
    pub index: usize,
    pub expected: Option<ConformanceTraceEvent>,
    pub actual: Option<ConformanceTraceEvent>,
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
#[error(
    "conformance scenario `{scenario}` failed at action {action_index} ({action_kind}): {message}"
)]
pub struct ConformanceRunError {
    pub scenario: String,
    pub action_index: usize,
    pub action_kind: String,
    pub message: String,
}

fn trace_mismatches(
    expected: &[ConformanceTraceEvent],
    actual: &[ConformanceTraceEvent],
) -> Vec<ConformanceTraceMismatch> {
    let len = expected.len().max(actual.len());
    (0..len)
        .filter_map(|index| {
            let expected = expected.get(index);
            let actual = actual.get(index);
            (expected != actual).then(|| ConformanceTraceMismatch {
                index,
                expected: expected.cloned(),
                actual: actual.cloned(),
            })
        })
        .collect()
}