use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::time::{Duration, Instant};
use crate::cli_chains::action::Step;
use crate::cli_chains::corpus::Case;
use crate::cli_chains::model::Model;
use crate::cli_chains::transition::{self, Transition};
use super::observe::{Identities, ObservedState, observe};
use super::oracle::{Mismatch, judge_action, judge_state};
use super::scenario::{Invocation, RealResolver, Scenario};
use super::security;
#[derive(Debug, Clone)]
pub enum Expected {
Seed(Model),
Action(Box<Transition>),
}
impl Expected {
#[must_use]
pub fn next(&self) -> &Model {
match self {
Expected::Seed(model) => model,
Expected::Action(transition) => &transition.next,
}
}
}
#[derive(Debug, Clone)]
pub struct StepRecord {
pub index: usize,
pub step: Step,
pub setup: bool,
pub before: Model,
pub expected: Expected,
pub invocation: Option<Invocation>,
pub observed: Result<ObservedState, String>,
pub mismatches: Vec<Mismatch>,
}
#[derive(Debug)]
pub struct CaseRun<'c> {
pub case: &'c Case,
pub scenario_root: PathBuf,
pub service_tag: String,
pub github_base: String,
pub resolver: RealResolver,
pub initial: Result<ObservedState, String>,
pub records: Vec<StepRecord>,
pub history: Vec<String>,
pub elapsed: Duration,
}
impl CaseRun<'_> {
#[must_use]
pub fn divergence(&self) -> Option<&StepRecord> {
self.records
.iter()
.find(|record| !record.mismatches.is_empty())
}
#[must_use]
pub fn passed(&self) -> bool {
self.divergence().is_none() && self.records.len() == self.case.steps.len()
}
pub fn invocations(&self) -> impl Iterator<Item = &Invocation> {
self.records
.iter()
.filter_map(|record| record.invocation.as_ref())
}
#[must_use]
pub fn rejudge(&self, corrupt: &Corruption<'_>) -> Option<(usize, Vec<Mismatch>)> {
for record in &self.records {
let mismatches = match (&record.expected, &record.step) {
(Expected::Seed(model), _) => judge_state(model, &record.observed),
(Expected::Action(transition), Step::Run(action)) => {
let mut transition = (**transition).clone();
corrupt(record.index, &mut transition);
judge_action(
action,
&record.before,
&transition,
record
.invocation
.as_ref()
.expect("every action record holds its invocation"),
&record.observed,
&self.resolver,
)
}
(Expected::Action(_), Step::Seed(_)) => {
unreachable!("an action expectation always belongs to a run step")
}
};
if !mismatches.is_empty() {
return Some((record.index, mismatches));
}
}
None
}
}
pub type Corruption<'a> = dyn Fn(usize, &mut Transition) + Sync + 'a;
#[must_use]
pub fn execute(case: &Case) -> CaseRun<'_> {
execute_with(case, &|_, _| {})
}
#[must_use]
pub fn execute_with<'c>(case: &'c Case, corrupt: &Corruption<'_>) -> CaseRun<'c> {
let started = Instant::now();
let scenario = Scenario::new(case.id, case.installation);
let mut identities = Identities::default();
let initial = observe(&scenario, &mut identities);
let mut model = Model::fresh(case.installation);
let mut records = Vec::with_capacity(case.steps.len());
for (index, step) in case.steps.iter().enumerate() {
let (expected, invocation, observed, mut mismatches) = match step {
Step::Seed(seed) => {
let expected = transition::seed(&model, seed).unwrap_or_else(|problem| {
panic!(
"{}: step {} is not a reachable seed: {problem}",
case.id,
index + 1
)
});
let observed = scenario
.apply_seed(seed)
.and_then(|()| observe(&scenario, &mut identities));
let mismatches = judge_state(&expected, &observed);
(Expected::Seed(expected), None, observed, mismatches)
}
Step::Run(action) => {
let mut expected = transition::apply(&model, action);
corrupt(index, &mut expected);
let invocation = scenario.run_action(action);
let observed = observe(&scenario, &mut identities);
let mismatches = judge_action(
action,
&model,
&expected,
&invocation,
&observed,
&scenario.resolver,
);
(
Expected::Action(Box::new(expected)),
Some(invocation),
observed,
mismatches,
)
}
};
match security::collect(&scenario, invocation.as_ref()) {
Ok(fragments) => mismatches.extend(
security::findings(&fragments)
.into_iter()
.map(Mismatch::security),
),
Err(problem) => mismatches.push(Mismatch::security(problem)),
}
let next = expected.next().clone();
let record = StepRecord {
index,
step: step.clone(),
setup: index < case.setup_len,
before: std::mem::replace(&mut model, next),
expected,
invocation,
observed,
mismatches,
};
let diverged = !record.mismatches.is_empty();
records.push(record);
if diverged {
break;
}
}
CaseRun {
case,
scenario_root: scenario.root.clone(),
service_tag: scenario.tag.clone(),
github_base: scenario.github.base_url().to_string(),
resolver: scenario.resolver.clone(),
initial,
records,
history: scenario.github.seen(),
elapsed: started.elapsed(),
}
}
#[must_use]
pub fn execute_all<'c>(cases: &[&'c Case], workers: usize) -> Vec<CaseRun<'c>> {
let next = AtomicUsize::new(0);
let finished: Mutex<Vec<(usize, CaseRun<'c>)>> = Mutex::new(Vec::with_capacity(cases.len()));
std::thread::scope(|scope| {
for _ in 0..workers.max(1) {
scope.spawn(|| {
loop {
let index = next.fetch_add(1, Ordering::Relaxed);
let Some(case) = cases.get(index) else {
break;
};
let run = execute(case);
finished
.lock()
.expect("no worker panics while holding the lock")
.push((index, run));
}
});
}
});
let mut finished = finished.into_inner().expect("the workers are done");
finished.sort_by_key(|(index, _)| *index);
finished.into_iter().map(|(_, run)| run).collect()
}
#[must_use]
pub fn default_workers() -> usize {
std::thread::available_parallelism()
.map_or(2, std::num::NonZeroUsize::get)
.clamp(1, 8)
}