use super::super::types::{
ActivationDisposition, BaselineAxis, BaselineQualification, EquivalenceAxis, ExecutionAxis,
FamilyAttribution, InconclusiveCause, IntendedRejection, MappingPosture, MaterializationAxis,
MutantId, MutationIdentity, MutationReport, MutationRun, MutationSite, MutationTarget,
PlanRefusal, PlannedDamage, PlannedRun, PressureLane, ProofPlan, ScopedInvocation,
SourceCoordinate,
};
use super::types::{
AdapterProfile, AnnouncedRoster, ArtifactManifestRefusal, BackendOutputId,
BackendVersionPosture, CompiledSuiteArtifactManifest, FamilyLookup, GrammarVersion,
MutationBackendInvocation, MutationSourceRevision, OwnerLookup, ReadingSource, UnparsedLine,
WrapOutcomeWord, WrapReading, WrapRefusal, WrappedBackend,
};
use crate::report::ForeignText;
use crate::runner::Selection;
use std::collections::{BTreeMap, BTreeSet};
const CONSOLE_GRAMMAR_VERSION: u32 = 1;
const WRAP_ACTIVATION: ActivationDisposition = ActivationDisposition::UnobservableUnderBackend;
const WRAP_EQUIVALENCE: EquivalenceAxis = EquivalenceAxis::NotAssessed;
const BASELINE_MARKER: &str = "Unmutated baseline";
const ROSTER_MARKER: &str = "Found";
const ROSTER_SUBJECT: &str = "mutant";
const BASELINE_QUALIFIED_WORD: &str = "ok";
const OUTCOME_WORDS: [(&str, WrapOutcomeWord); 5] = [
("caught", WrapOutcomeWord::Caught),
("missed", WrapOutcomeWord::Missed),
("unviable", WrapOutcomeWord::Unviable),
("timeout", WrapOutcomeWord::TimedOut),
("failed", WrapOutcomeWord::ToolFailed),
];
impl WrappedBackend {
#[must_use]
pub const fn spelling(self) -> &'static str {
match self {
Self::CargoMutants => "cargo-mutants",
}
}
}
enum LineReading<'line> {
Roster(u32),
Baseline(BaselineAxis),
Mutant {
word: WrapOutcomeWord,
coordinate: SourceCoordinate,
damage: String,
line: &'line str,
},
Unread,
}
#[must_use]
pub fn console_profile(version: BackendVersionPosture) -> AdapterProfile {
AdapterProfile::stated(
WrappedBackend::CargoMutants,
version,
ReadingSource::ConsoleStream,
GrammarVersion::adapter(CONSOLE_GRAMMAR_VERSION),
)
}
pub fn read_output(
text: &str,
version: BackendVersionPosture,
owner: OwnerLookup,
family_lookup: FamilyLookup,
) -> Result<WrapReading, WrapRefusal> {
let profile = console_profile(version);
let baseline = read_baseline(text)?;
let mut reports: Vec<MutationReport> = Vec::new();
let mut unparsed: Vec<UnparsedLine> = Vec::new();
let mut announced = AnnouncedRoster::Unstated;
for (ordinal, line) in text.lines().enumerate() {
match read_line(line) {
LineReading::Roster(found) => announced = AnnouncedRoster::Stated(found),
LineReading::Baseline(_) => {}
LineReading::Mutant {
word,
coordinate,
damage,
line: whole,
} => {
let target = targeted(&coordinate, damage.as_bytes(), owner, family_lookup);
reports.push(recorded(ordinal, word, target, whole, baseline)?);
}
LineReading::Unread => unparsed.push(UnparsedLine::unread(ordinal, line.as_bytes())),
}
}
WrapReading::read(
profile,
MutationRun::recorded(baseline, reports),
announced,
unparsed,
)
}
pub fn read_artifact(
text: &str,
invocation: MutationBackendInvocation,
sources: Vec<MutationSourceRevision>,
owner: OwnerLookup,
family_lookup: FamilyLookup,
) -> Result<CompiledSuiteArtifactManifest, ArtifactManifestRefusal> {
let reading = match invocation.backend() {
WrappedBackend::CargoMutants => read_output(
text,
BackendVersionPosture::Stated(invocation.version().clone()),
owner,
family_lookup,
),
}
.map_err(ArtifactManifestRefusal::Reading)?;
let mut supplied = BTreeMap::new();
for source in sources {
let file = source.file().to_owned();
if supplied.insert(file.clone(), source).is_some() {
return Err(ArtifactManifestRefusal::DuplicateSource(file));
}
}
let mut reported = BTreeSet::new();
for report in reading.run().reports() {
match report.target().site() {
MutationSite::Reported(coordinate) => {
reported.insert(coordinate.file());
}
MutationSite::Declared(_) => {
return Err(ArtifactManifestRefusal::MutationSiteNotReported);
}
}
}
for file in reported.iter().copied() {
if !supplied.contains_key(file) {
return Err(ArtifactManifestRefusal::ReportedSourceMissing(
file.to_owned(),
));
}
}
for file in supplied.keys() {
if !reported.contains(file.as_str()) {
return Err(ArtifactManifestRefusal::SourceNotReported(file.to_owned()));
}
}
Ok(CompiledSuiteArtifactManifest::recorded(
invocation,
BackendOutputId::derived(text.as_bytes()),
supplied.into_values().collect(),
reading,
))
}
fn read_baseline(text: &str) -> Result<BaselineQualification, WrapRefusal> {
for line in text.lines() {
if let LineReading::Baseline(axis) = read_line(line) {
return BaselineQualification::read(axis).map_err(WrapRefusal::BaselineNotQualified);
}
}
Err(WrapRefusal::BaselineNotStated)
}
fn recorded(
ordinal: usize,
word: WrapOutcomeWord,
target: MutationTarget,
line: &str,
baseline: BaselineQualification,
) -> Result<MutationReport, WrapRefusal> {
let axis = baseline.axis();
let cause = match word {
WrapOutcomeWord::Caught => {
return MutationReport::killed(
target,
axis,
MaterializationAxis::from(word),
WRAP_ACTIVATION,
ExecutionAxis::from(word),
IntendedRejection::ReportedByBackend {
stated: ForeignText::admitted(line.as_bytes()),
},
WRAP_EQUIVALENCE,
)
.map_err(|cause| WrapRefusal::KillNotLawful { ordinal, cause });
}
WrapOutcomeWord::Missed => InconclusiveCause::UnobservableAndUnrejected,
WrapOutcomeWord::Unviable | WrapOutcomeWord::ToolFailed => {
InconclusiveCause::NotMaterialized
}
WrapOutcomeWord::TimedOut => InconclusiveCause::WitnessIncomplete,
};
Ok(unlearned(target, axis, word, cause))
}
fn unlearned(
target: MutationTarget,
axis: BaselineAxis,
word: WrapOutcomeWord,
cause: InconclusiveCause,
) -> MutationReport {
MutationReport::inconclusive(
target,
axis,
MaterializationAxis::from(word),
WRAP_ACTIVATION,
ExecutionAxis::from(word),
cause,
WRAP_EQUIVALENCE,
)
}
fn targeted(
coordinate: &SourceCoordinate,
damage: &[u8],
owner: OwnerLookup,
family: FamilyLookup,
) -> MutationTarget {
let attribution = family(coordinate, damage).map_or(
FamilyAttribution::OutsideTheBank,
FamilyAttribution::Declared,
);
let posture = owner(coordinate).map_or(MappingPosture::OwnerUnmapped, MappingPosture::Mapped);
MutationTarget::pressed(
MutationIdentity::External(MutantId::over(coordinate, damage)),
attribution,
MutationSite::Reported(coordinate.clone()),
posture,
)
}
fn read_line(line: &str) -> LineReading<'_> {
let mut tokens = line.split_whitespace();
let Some(word) = tokens.next() else {
return LineReading::Unread;
};
let rest = tokens.collect::<Vec<&str>>();
if let Some(found) = roster_count(word, &rest) {
return LineReading::Roster(found);
}
if rest.join(" ").starts_with(BASELINE_MARKER) {
return LineReading::Baseline(baseline_axis(word));
}
mutant_reading(word, &rest, line)
}
fn roster_count(word: &str, rest: &[&str]) -> Option<u32> {
if word != ROSTER_MARKER {
return None;
}
let count = rest.first()?;
let subject = rest.get(1)?;
if !subject.starts_with(ROSTER_SUBJECT) {
return None;
}
count.parse::<u32>().ok()
}
fn baseline_axis(word: &str) -> BaselineAxis {
if word.eq_ignore_ascii_case(BASELINE_QUALIFIED_WORD) {
BaselineAxis::Qualified
} else {
BaselineAxis::Failed
}
}
fn mutant_reading<'line>(word: &str, rest: &[&str], line: &'line str) -> LineReading<'line> {
let Some(outcome) = outcome_word(word) else {
return LineReading::Unread;
};
let Some(coordinate_text) = rest.first() else {
return LineReading::Unread;
};
let Some(coordinate) = read_coordinate(coordinate_text) else {
return LineReading::Unread;
};
let damage = rest.get(1..).unwrap_or_default().join(" ");
if damage.is_empty() {
return LineReading::Unread;
}
LineReading::Mutant {
word: outcome,
coordinate,
damage,
line,
}
}
fn outcome_word(token: &str) -> Option<WrapOutcomeWord> {
OUTCOME_WORDS
.iter()
.find(|(spelling, _)| token.eq_ignore_ascii_case(spelling))
.map(|(_, word)| *word)
}
fn read_coordinate(token: &str) -> Option<SourceCoordinate> {
let body = token.strip_suffix(':').unwrap_or(token);
let mut fields = body.rsplitn(3, ':');
let column_text = fields.next()?;
let line_text = fields.next()?;
let file = fields.next()?;
let column = column_text.parse::<u32>().ok()?;
let line = line_text.parse::<u32>().ok()?;
SourceCoordinate::reported(file, line, column).ok()
}
#[must_use]
pub fn mutant_scoped(target: &MutationTarget) -> Selection {
match target.owning_claim() {
Some(claim) => Selection::ByClaim(BTreeSet::from([claim])),
None => Selection::All,
}
}
pub fn plan_pass(
targets: &[MutationTarget],
scope: ScopedInvocation,
) -> Result<ProofPlan, PlanRefusal> {
let budget = scope.budget();
let runs: Vec<PlannedRun> = targets
.iter()
.map(|target| {
PlannedRun::intended(
PressureLane::CompiledMutation,
target.identity(),
PlannedDamage::BackendChosen,
mutant_scoped(target),
budget,
)
})
.collect();
ProofPlan::planned(scope, runs)
}