use std::path::{Path, PathBuf};
use crate::party::{OutcomeRecord, OutcomeStatus, Results, Statement};
use crate::pipeline::{Error, load_clean_root, load_ixit, read_json, to_json_document};
use crate::run::RunReport;
#[derive(Debug)]
pub struct RunRequest<'a> {
pub root: &'a Path,
pub ixit: &'a Path,
pub out_dir: &'a Path,
pub sut_name: &'a str,
pub sut_version: &'a str,
pub filter: Option<&'a str>,
pub statement: Option<&'a Path>,
}
#[derive(Debug, Clone, Copy)]
pub enum RunWarning<'a> {
CarriedMeasurements {
count: usize,
measured_at: &'a str,
running_at: &'a str,
},
}
#[derive(Debug, Clone, Copy, Default)]
pub struct OutcomeCounts {
pub passed: usize,
pub failed: usize,
pub errored: usize,
pub not_applicable: usize,
}
#[derive(Debug)]
pub struct RunOutcome {
pub results: Results,
pub report: RunReport,
pub counts: OutcomeCounts,
pub results_path: PathBuf,
pub exceptions_path: PathBuf,
}
impl RunOutcome {
#[must_use]
pub fn is_clean(&self) -> bool {
self.counts.failed == 0 && self.counts.errored == 0
}
pub fn results_document(&self) -> Result<String, Error> {
to_json_document(&self.results, "serialize")
}
pub fn exceptions_document(&self) -> Result<String, Error> {
let entries: Vec<serde_json::Value> = self
.report
.exceptions
.iter()
.map(|(case, e)| serde_json::json!({ "case": case.to_string(), "exception": e }))
.collect();
to_json_document(&entries, "serialize")
}
}
#[must_use]
pub fn ixit_digest(ixit_text: &str) -> String {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
ixit_text.hash(&mut hasher);
format!("{:016x}", hasher.finish())
}
pub fn execute_run(
request: &RunRequest<'_>,
warn: &dyn Fn(RunWarning<'_>),
) -> Result<RunOutcome, 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));
}
let statement: Option<Statement> = match request.statement {
None => None,
Some(path) => Some(read_json(path, "statement")?),
};
let report = crate::run::execute(&set, &ixit, statement.as_ref())
.map_err(|e| Error::Instrument(format!("execution defect: {e}")))?;
let outcomes: Vec<OutcomeRecord> = report.records.iter().map(OutcomeRecord::from).collect();
let counts = tally(&outcomes);
let carried = carried_measurements(request, warn)?;
let results = Results {
sut: crate::party::Sut {
name: request.sut_name.to_owned(),
version: request.sut_version.to_owned(),
},
runner: crate::party::Runner {
name: "veredictum".to_owned(),
version: env!("CARGO_PKG_VERSION").to_owned(),
verification_pack_status: crate::party::VerificationPackStatus::Passed,
},
schedule_release: "cnf-2.0-w2".to_owned(),
tech_profile: tech_profile(statement.as_ref()),
ixit_digest: ixit_digest(&ixit_text),
restapi_specs_version: report.restapi_specs_version.clone(),
outcomes,
measurements: carried,
ambiguity_dispositions: Vec::new(),
};
results
.check_invariants()
.map_err(Error::RecordedInvariants)?;
Ok(RunOutcome {
results,
report,
counts,
results_path: request.out_dir.join("results.json"),
exceptions_path: request.out_dir.join("run-exceptions.json"),
})
}
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,
_ => counts.not_applicable += 1,
}
}
counts
}
fn tech_profile(statement: Option<&Statement>) -> crate::party::TechProfile {
crate::party::TechProfile {
its: crate::vocab::ItsName::ItsRest,
formats: statement
.and_then(|s| {
s.tech_profiles
.iter()
.find(|p| p.its == crate::vocab::ItsName::ItsRest)
})
.map_or_else(
|| crate::vocab::FormatName::ALL.to_vec(),
|p| p.formats.clone(),
),
}
}
fn carried_measurements(
request: &RunRequest<'_>,
warn: &dyn Fn(RunWarning<'_>),
) -> Result<Vec<crate::perf::Measurement>, Error> {
let prior_path = request.out_dir.join("results.json");
let prior = match std::fs::read_to_string(&prior_path) {
Ok(text) => match serde_json::from_str::<Results>(&text) {
Ok(prior) => Some(prior),
Err(e) => {
return Err(Error::Instrument(format!(
"runner defect: {} exists but does not parse as results.json ({e}) — \
its measurement records cannot be carried forward",
prior_path.display()
)));
}
},
Err(e) if e.kind() == std::io::ErrorKind::NotFound => None,
Err(e) => {
return Err(Error::Instrument(format!(
"runner defect: {} is unreadable ({e})",
prior_path.display()
)));
}
};
let Some(prior) = prior.filter(|prior| prior.sut.name == request.sut_name) else {
return Ok(Vec::new());
};
if prior.sut.version != request.sut_version && !prior.measurements.is_empty() {
warn(RunWarning::CarriedMeasurements {
count: prior.measurements.len(),
measured_at: &prior.sut.version,
running_at: request.sut_version,
});
}
Ok(prior.measurements)
}