use fallow_output::MutantStatus;
use rustc_hash::FxHashMap;
use super::super::protocol::Ready;
use super::{
Attempt, Job, JobContext, MutantOutcome, ScheduleConfig, ScheduleOutcome, ScheduledMutant,
retry_status,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CachedResult {
pub status: MutantStatus,
pub killed_by: Option<String>,
pub duration_ms: Option<u64>,
}
pub trait ResultLookup: std::fmt::Debug + Send + Sync {
fn key(
&self,
ready: &Ready,
mutant: &ScheduledMutant,
tests: &[String],
test_files: &[String],
) -> String;
fn get(&self, mutant_id: &str, key: &str) -> Option<CachedResult>;
}
pub trait OutcomeJournal: std::fmt::Debug + Send + Sync {
fn key(
&self,
ready: &Ready,
mutant: &ScheduledMutant,
tests: &[String],
test_files: &[String],
) -> String;
fn resumed(&self, mutant_id: &str, key: &str) -> Option<CachedResult>;
fn append(&self, mutant_id: &str, key: &str, result: &CachedResult);
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum StoredSource {
Cache,
Journal,
}
fn stored_result(
context: &JobContext<'_>,
key: Option<&String>,
mutant: &ScheduledMutant,
) -> Option<(CachedResult, StoredSource)> {
let key = key?;
let resumed = context
.journal
.and_then(|journal| journal.resumed(&mutant.id, key))
.map(|hit| (hit, StoredSource::Journal));
resumed.or_else(|| {
context
.cache
.and_then(|cache| cache.get(&mutant.id, key))
.map(|hit| (hit, StoredSource::Cache))
})
}
fn stored_outcome(
(hit, source): (CachedResult, StoredSource),
tests: &[String],
static_mutant: bool,
) -> MutantOutcome {
let mut covering_tests = tests.to_vec();
covering_tests.sort_unstable();
MutantOutcome {
status: hit.status,
killed_by: hit.killed_by,
covering_tests,
static_mutant,
duration_ms: hit.duration_ms,
retried: hit.status == MutantStatus::Survived,
attempts: Vec::new(),
cached: source == StoredSource::Cache,
resumed: source == StoredSource::Journal,
}
}
pub(super) fn job_key(
context: &JobContext<'_>,
mutant: &ScheduledMutant,
tests: &[String],
test_files: &[String],
) -> Option<String> {
match (context.cache, context.journal) {
(Some(cache), _) => Some(cache.key(context.ready, mutant, tests, test_files)),
(None, Some(journal)) => Some(journal.key(context.ready, mutant, tests, test_files)),
(None, None) => None,
}
}
pub(super) fn record_stored(
context: &JobContext<'_>,
key: Option<&String>,
mutant: &ScheduledMutant,
tests: &[String],
static_mutant: bool,
outcome: &mut ScheduleOutcome,
) -> bool {
let Some(stored) = stored_result(context, key, mutant) else {
return false;
};
let stored = stored_outcome(stored, tests, static_mutant);
if let (true, Some(key)) = (stored.resumed, key) {
outcome.cache_keys.insert(mutant.id.clone(), key.clone());
}
outcome.results.insert(mutant.id.clone(), stored);
true
}
fn journal_result(config: &ScheduleConfig, job: &Job, result: &CachedResult) {
if let (Some(journal), Some(key)) = (&config.journal, &job.cache_key) {
journal.append(&job.id, key, result);
}
}
pub(super) fn journal_first_run(config: &ScheduleConfig, job: &Job, attempt: &Attempt) {
if attempt.status != MutantStatus::Killed {
return;
}
journal_result(
config,
job,
&CachedResult {
status: MutantStatus::Killed,
killed_by: attempt.killed_by.clone(),
duration_ms: attempt.duration_ms,
},
);
}
pub(super) type FirstRuns = FxHashMap<String, (MutantStatus, Option<u64>)>;
pub(super) fn journal_retry(
config: &ScheduleConfig,
firsts: &FirstRuns,
job: &Job,
retry: &Attempt,
) {
let (first, first_ms) = firsts
.get(&job.id)
.copied()
.unwrap_or((MutantStatus::Survived, None));
let result = match retry_status(first, job, retry) {
MutantStatus::Killed => CachedResult {
status: MutantStatus::Killed,
killed_by: retry.killed_by.clone(),
duration_ms: retry.duration_ms,
},
MutantStatus::Survived if retry.dropped_tests.is_empty() => CachedResult {
status: MutantStatus::Survived,
killed_by: None,
duration_ms: first_ms,
},
_ => return,
};
journal_result(config, job, &result);
}