use std::path::Path;
use crate::party::{OutcomeRecord, OutcomeStatus, Results, Statement};
use crate::pipeline::conformance::OutcomeCounts;
use crate::pipeline::{Error, load_clean_root, load_ixit, read_json};
use crate::run::RunReport;
use crate::transcript::RunTranscript;
#[derive(Debug)]
pub struct ReplayRequest<'a> {
pub root: &'a Path,
pub ixit: &'a Path,
pub transcript: &'a Path,
pub statement: Option<&'a Path>,
pub filter: Option<&'a str>,
pub only: Option<&'a [String]>,
}
#[derive(Debug)]
pub struct ReplayOutcome {
pub results: Results,
pub report: RunReport,
pub counts: OutcomeCounts,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Divergence {
pub case: String,
pub format: Option<String>,
pub submitted: String,
pub rederived: String,
}
impl std::fmt::Display for Divergence {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let format = self
.format
.as_ref()
.map_or_else(String::new, |name| format!(" [{name}]"));
write!(
f,
"{}{format}: submitted {}, re-derived {}",
self.case, self.submitted, self.rederived
)
}
}
pub fn replay_run(
request: &ReplayRequest<'_>,
progress: &mut dyn FnMut(crate::run::Progress<'_>),
) -> Result<ReplayOutcome, Error> {
let loaded = load_clean_root(request.root)?;
let (ixit, ixit_text) = load_ixit(request.ixit)?;
let mut set = loaded.set;
if let Some(needle) = request.filter {
set.cases.retain(|(_, c)| c.id.as_str().contains(needle));
}
if let Some(ids) = request.only {
set.cases
.retain(|(_, c)| ids.iter().any(|id| id == c.id.as_str()));
}
let statement: Option<Statement> = match request.statement {
None => None,
Some(path) => Some(read_json(path, "statement")?),
};
let transcript: RunTranscript = read_json(request.transcript, "transcript")?;
let report = crate::run::replay(&set, &ixit, statement.as_ref(), &transcript, progress)
.map_err(|e| Error::Instrument(format!("replay defect: {e}")))?;
let outcomes: Vec<OutcomeRecord> = report.records.iter().map(OutcomeRecord::from).collect();
let counts = tally(&outcomes);
let results = Results {
sut: transcript.sut.clone(),
runner: crate::party::Runner {
name: "veredictum".to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
verification_pack_status: crate::party::VerificationPackStatus::Passed,
},
schedule_release: transcript.schedule_release.clone(),
tech_profile: crate::pipeline::conformance::tech_profile(statement.as_ref()),
ixit_digest: crate::pipeline::conformance::ixit_digest(&ixit_text),
restapi_specs_version: report.restapi_specs_version.clone(),
outcomes,
measurements: Vec::new(),
ambiguity_dispositions: Vec::new(),
};
results
.check_invariants()
.map_err(Error::RecordedInvariants)?;
Ok(ReplayOutcome {
results,
report,
counts,
})
}
#[must_use]
pub fn divergences(submitted: &Results, rederived: &Results) -> Vec<Divergence> {
let key = |outcome: &OutcomeRecord| {
(
outcome.case.to_string(),
outcome.format.map(|f| f.token().to_owned()),
)
};
let mut found = Vec::new();
for outcome in &submitted.outcomes {
let id = key(outcome);
match rederived
.outcomes
.iter()
.find(|candidate| key(candidate) == id)
{
None => found.push(Divergence {
case: id.0,
format: id.1,
submitted: describe(outcome),
rederived: String::from("no row: the replay never reached this case"),
}),
Some(again) => {
if describe(outcome) != describe(again) {
found.push(Divergence {
case: id.0,
format: id.1,
submitted: describe(outcome),
rederived: describe(again),
});
}
}
}
}
for outcome in &rederived.outcomes {
let id = key(outcome);
if !submitted
.outcomes
.iter()
.any(|candidate| key(candidate) == id)
{
found.push(Divergence {
case: id.0,
format: id.1,
submitted: String::from("no row: the record does not carry this case"),
rederived: describe(outcome),
});
}
}
found
}
fn describe(outcome: &OutcomeRecord) -> String {
format!(
"{} over {}/{} rows",
outcome.status.token(),
outcome.rows_driven,
outcome.rows_total
)
}
fn tally(outcomes: &[OutcomeRecord]) -> OutcomeCounts {
let mut counts = OutcomeCounts::default();
for outcome in outcomes {
match outcome.status {
OutcomeStatus::Passed => counts.passed += 1,
OutcomeStatus::Failed => counts.failed += 1,
OutcomeStatus::Errored => counts.errored += 1,
OutcomeStatus::Skipped | OutcomeStatus::NotApplicable => counts.not_applicable += 1,
}
}
counts
}