use std::fmt;
use std::fs::read_to_string;
use std::time::{Duration, Instant};
use anyhow::Context;
use camino::Utf8PathBuf;
use jiff::Timestamp;
use output::ScenarioOutput;
use serde::Serialize;
use serde::Serializer;
use serde::ser::SerializeStruct;
use tracing::warn;
use crate::console::{format_duration, plural};
use crate::exit_code::ExitCode;
use crate::process::Exit;
use crate::{Options, Result, Scenario, output};
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy, Serialize)]
pub enum Phase {
Check,
Build,
Test,
}
impl Phase {
pub fn name(self) -> &'static str {
match self {
Phase::Check => "check",
Phase::Build => "build",
Phase::Test => "test",
}
}
}
impl fmt::Display for Phase {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.pad(self.name())
}
}
#[derive(Debug, Serialize)]
#[allow(clippy::module_name_repetitions)]
pub struct LabOutcome {
pub outcomes: Vec<ScenarioOutcome>,
pub total_mutants: usize,
pub missed: usize,
pub caught: usize,
pub timeout: usize,
pub unviable: usize,
pub success: usize,
pub start_time: Timestamp,
pub end_time: Option<Timestamp>,
pub cargo_mutants_version: String,
}
impl LabOutcome {
pub fn new(start_time: Timestamp) -> LabOutcome {
LabOutcome {
outcomes: Vec::new(),
total_mutants: 0,
missed: 0,
caught: 0,
timeout: 0,
unviable: 0,
success: 0,
start_time,
end_time: None,
cargo_mutants_version: crate::VERSION.to_string(),
}
}
pub fn add(&mut self, outcome: ScenarioOutcome) {
if outcome.scenario.is_mutant() {
self.total_mutants += 1;
match outcome.summary() {
SummaryOutcome::CaughtMutant => self.caught += 1,
SummaryOutcome::MissedMutant => self.missed += 1,
SummaryOutcome::Timeout => self.timeout += 1,
SummaryOutcome::Unviable => self.unviable += 1,
SummaryOutcome::Success => self.success += 1,
SummaryOutcome::Failure => {
warn!("Unclassified failure for mutant {:?}", outcome.scenario);
}
}
}
self.outcomes.push(outcome);
}
pub fn exit_code(&self) -> ExitCode {
if self
.outcomes
.iter()
.any(|o| !o.scenario.is_mutant() && !o.success())
{
ExitCode::BaselineFailed
} else if self.timeout > 0 {
ExitCode::Timeout
} else if self.missed > 0 {
ExitCode::FoundProblems
} else {
ExitCode::Success
}
}
pub fn summary_string(&self, start_time: Instant, options: &Options) -> String {
let mut s = Vec::new();
s.push(format!("{} tested", plural(self.total_mutants, "mutant")));
if options.show_times {
s.push(format!(" in {}", format_duration(start_time.elapsed())));
}
s.push(": ".into());
let mut by_outcome: Vec<String> = Vec::new();
if self.missed != 0 {
by_outcome.push(format!("{} missed", self.missed));
}
if self.caught != 0 {
by_outcome.push(format!("{} caught", self.caught));
}
if self.unviable != 0 {
by_outcome.push(format!("{} unviable", self.unviable));
}
if self.timeout != 0 {
by_outcome.push(format!("{} timeouts", self.timeout));
}
if self.success != 0 {
by_outcome.push(format!("{} succeeded", self.success));
}
s.push(by_outcome.join(", "));
s.join("")
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
#[allow(clippy::module_name_repetitions)]
pub struct ScenarioOutcome {
output_dir: Utf8PathBuf,
log_path: Utf8PathBuf,
diff_path: Option<Utf8PathBuf>,
pub scenario: Scenario,
phase_results: Vec<PhaseResult>,
}
impl Serialize for ScenarioOutcome {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut ss = serializer.serialize_struct("Outcome", 5)?;
ss.serialize_field("scenario", &self.scenario)?;
ss.serialize_field("summary", &self.summary())?;
ss.serialize_field("log_path", &self.log_path)?;
ss.serialize_field("diff_path", &self.diff_path)?;
ss.serialize_field("phase_results", &self.phase_results)?;
ss.end()
}
}
impl ScenarioOutcome {
pub fn new(scenario_output: &ScenarioOutput, scenario: Scenario) -> ScenarioOutcome {
ScenarioOutcome {
output_dir: scenario_output.output_dir.clone(),
log_path: scenario_output.log_path().to_owned(),
diff_path: scenario_output.diff_path.clone(),
scenario,
phase_results: Vec::new(),
}
}
pub fn add_phase_result(&mut self, phase_result: PhaseResult) {
self.phase_results.push(phase_result);
}
pub fn get_log_content(&self) -> Result<String> {
read_to_string(self.output_dir.join(&self.log_path)).context("read log file")
}
pub fn last_phase(&self) -> Phase {
self.phase_results.last().unwrap().phase
}
pub fn last_phase_result(&self) -> Exit {
self.phase_results.last().unwrap().process_status
}
pub fn phase_results(&self) -> &[PhaseResult] {
&self.phase_results
}
pub fn phase_result(&self, phase: Phase) -> Option<&PhaseResult> {
self.phase_results.iter().find(|pr| pr.phase == phase)
}
pub fn should_show_logs(&self) -> bool {
!self.scenario.is_mutant() && !self.success()
}
pub fn success(&self) -> bool {
self.last_phase_result().is_success()
}
pub fn has_timeout(&self) -> bool {
self.phase_results
.iter()
.any(|pr| pr.process_status.is_timeout())
}
pub fn check_or_build_failed(&self) -> bool {
self.phase_results
.iter()
.any(|pr| pr.phase != Phase::Test && pr.process_status.is_failure())
}
pub fn mutant_caught(&self) -> bool {
self.scenario.is_mutant()
&& self.last_phase() == Phase::Test
&& self.last_phase_result().is_failure()
}
pub fn mutant_missed(&self) -> bool {
self.scenario.is_mutant()
&& self.last_phase() == Phase::Test
&& self.last_phase_result().is_success()
}
pub fn summary(&self) -> SummaryOutcome {
match self.scenario {
Scenario::Baseline => {
if self.has_timeout() {
SummaryOutcome::Timeout
} else if self.success() {
SummaryOutcome::Success
} else {
SummaryOutcome::Failure
}
}
Scenario::Mutant(_) => {
if self.check_or_build_failed() {
SummaryOutcome::Unviable
} else if self.has_timeout() {
SummaryOutcome::Timeout
} else if self.mutant_caught() {
SummaryOutcome::CaughtMutant
} else if self.mutant_missed() {
SummaryOutcome::MissedMutant
} else if self.success() {
SummaryOutcome::Success
} else {
SummaryOutcome::Failure
}
}
}
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct PhaseResult {
pub phase: Phase,
pub duration: Duration,
pub process_status: Exit,
pub argv: Vec<String>,
}
impl PhaseResult {
pub fn is_success(&self) -> bool {
self.process_status.is_success()
}
}
impl Serialize for PhaseResult {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let mut ss = serializer.serialize_struct("PhaseResult", 4)?;
ss.serialize_field("phase", &self.phase)?;
ss.serialize_field("duration", &self.duration.as_secs_f64())?;
ss.serialize_field("process_status", &self.process_status)?;
ss.serialize_field("argv", &self.argv)?;
ss.end()
}
}
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Hash)]
#[allow(clippy::module_name_repetitions)]
pub enum SummaryOutcome {
Success,
CaughtMutant,
MissedMutant,
Unviable,
Failure,
Timeout,
}
#[cfg(test)]
mod test {
use std::time::Duration;
use crate::process::Exit;
use super::{Phase, PhaseResult, Scenario, ScenarioOutcome};
#[test]
fn find_phase_result() {
let outcome = ScenarioOutcome {
output_dir: "output".into(),
log_path: "log".into(),
diff_path: Some("mutant.diff".into()),
scenario: Scenario::Baseline,
phase_results: vec![
PhaseResult {
phase: Phase::Build,
duration: Duration::from_secs(2),
process_status: Exit::Success,
argv: vec!["cargo".into(), "build".into()],
},
PhaseResult {
phase: Phase::Test,
duration: Duration::from_secs(3),
process_status: Exit::Success,
argv: vec!["cargo".into(), "test".into()],
},
],
};
assert_eq!(
outcome.phase_result(Phase::Build),
Some(&PhaseResult {
phase: Phase::Build,
duration: Duration::from_secs(2),
process_status: Exit::Success,
argv: vec!["cargo".into(), "build".into()],
})
);
assert_eq!(
outcome
.phase_result(Phase::Build)
.unwrap()
.duration
.as_secs(),
2
);
assert_eq!(
outcome
.phase_result(Phase::Test)
.unwrap()
.duration
.as_secs(),
3
);
assert_eq!(outcome.phase_result(Phase::Check), None);
}
}