use core::time::Duration;
use std::env::consts;
use serde::{Deserialize, Serialize};
use crate::discover::Plan;
use crate::exec::Session;
use crate::model::{Mutant, Outcome, Summary};
const SCHEMA_VERSION: &str = "3";
const TOP: usize = 20;
const HASH_WIDTH: usize = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, clap::ValueEnum)]
pub enum Redaction {
#[default]
Hashed,
Names,
Omitted,
}
impl Redaction {
fn apply(self, name: &str) -> Option<String> {
match self {
Self::Names => Some(name.to_owned()),
Self::Omitted => None,
Self::Hashed => {
let digest = blake3::hash(name.as_bytes()).to_hex();
Some(digest.get(..HASH_WIDTH).unwrap_or(&digest).to_owned())
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Bundle {
pub schema_version: String,
pub tool: Tool,
pub redaction: String,
pub host: Host,
pub config: Config,
pub run: Run,
pub population: Population,
pub outcomes: Outcomes,
#[serde(skip_serializing_if = "Option::is_none")]
pub build: Option<Build>,
#[serde(skip_serializing_if = "Option::is_none")]
pub phases: Option<Phases>,
#[serde(skip_serializing_if = "Option::is_none")]
pub durations: Option<Durations>,
pub binaries: Vec<Binary>,
pub mutators: Vec<Breakdown>,
pub packages: Vec<Breakdown>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Tool {
pub name: String,
pub version: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Host {
pub os: String,
pub arch: String,
pub cores: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub toolchain: Option<String>,
pub rustflags_set: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Config {
pub mutators: Vec<String>,
pub jobs: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub stall_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metered: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub unbounded: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub shard: Option<[u32; 2]>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Run {
pub wall_ms: u64,
pub fixed_ms: u64,
pub testing_ms: u64,
pub cpu_ms: u64,
pub effective_jobs: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub scratch_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Population {
pub files: usize,
pub mutants: usize,
pub packages: usize,
pub suppressed: usize,
pub idle_directives: usize,
pub sharded_out: usize,
pub settled_out: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Outcomes {
pub killed: u32,
pub survived: u32,
pub timeout: u32,
pub out_of_memory: u32,
pub flaky: u32,
pub unviable: u32,
pub ignored: u32,
pub uncovered: u32,
pub not_built: u32,
pub pending: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Build {
pub elapsed_ms: u64,
pub baseline_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub tests: Option<usize>,
pub withdrawn: usize,
pub widened: bool,
pub rounds: Vec<Round>,
pub withdrawals: Vec<Withdrawal>,
#[serde(skip_serializing_if = "Option::is_none")]
pub baseline_peak_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ordering_hints: Option<OrderingHints>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct OrderingHints {
pub offered: usize,
pub confirmed: usize,
pub rounds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Round {
pub elapsed_ms: u64,
pub withdrew: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Withdrawal {
pub code: String,
pub mutator: String,
pub mutants: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Phases {
pub copy: Phase,
pub preflight: Phase,
pub baseline: Phase,
#[serde(skip_serializing_if = "Option::is_none")]
pub census: Option<CensusPhase>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sweep: Option<SweepPhase>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Phase {
pub elapsed_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CensusPhase {
pub elapsed_ms: u64,
pub walked: usize,
pub binaries: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SweepPhase {
pub elapsed_ms: u64,
pub launches: usize,
pub probes: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Durations {
pub evaluated: usize,
pub min_ms: u64,
pub p50_ms: u64,
pub p90_ms: u64,
pub p99_ms: u64,
pub max_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Binary {
#[serde(skip_serializing_if = "Option::is_none")]
pub package: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub target: Option<String>,
pub baseline_ms: u64,
#[serde(skip_serializing_if = "Option::is_none")]
pub budget_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub peak_bytes: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ceiling_bytes: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Breakdown {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub mutants: usize,
pub cpu_ms: u64,
pub survivors: usize,
pub unviable: usize,
}
#[derive(Debug, Clone)]
pub struct Context<'a> {
pub cores: usize,
pub jobs: usize,
pub wall: Duration,
pub mutators: Vec<&'static str>,
pub shard: Option<(u32, u32)>,
pub scratch_bytes: Option<u64>,
pub redaction: Redaction,
pub version: &'a str,
}
#[must_use]
pub fn bundle(plan: &Plan, session: Option<&Session>, context: &Context<'_>) -> Bundle {
let summary = Summary::of(&plan.mutants);
let fixed = session.map_or(Duration::ZERO, |session| session.build + session.baseline_wall);
let testing = context.wall.saturating_sub(fixed);
let cpu: Duration = plan.mutants.iter().map(|mutant| Duration::from_millis(mutant.elapsed_ms)).sum();
let wall_ms = millis(context.wall);
let fixed_ms = millis(fixed);
let testing_ms = wall_ms.saturating_sub(fixed_ms);
Bundle {
schema_version: SCHEMA_VERSION.to_owned(),
tool: Tool {
name: "cargo-gamma".to_owned(),
version: context.version.to_owned(),
},
redaction: match context.redaction {
Redaction::Hashed => "hashed",
Redaction::Names => "names",
Redaction::Omitted => "omitted",
}
.to_owned(),
host: Host {
os: consts::OS.to_owned(),
arch: consts::ARCH.to_owned(),
cores: context.cores,
toolchain: redact_toolchain(crate::discover::toolchain(), context.redaction),
rustflags_set: crate::discover::rustflags().is_some(),
},
config: Config {
mutators: context.mutators.iter().map(|name| (*name).to_owned()).collect(),
jobs: context.jobs,
stall_ms: session.and_then(|session| session.stall).map(millis),
metered: session.map(|session| session.metered),
unbounded: session.and_then(|session| session.unbounded.clone()),
shard: context.shard.map(|(count, index)| [index, count]),
},
run: Run {
wall_ms,
fixed_ms,
testing_ms,
cpu_ms: millis(cpu),
effective_jobs: effective(cpu, testing),
scratch_bytes: context.scratch_bytes,
},
population: Population {
files: plan.files.len(),
mutants: plan.mutants.len(),
packages: plan.reach.len(),
suppressed: plan.suppressed,
idle_directives: plan.idle.len(),
sharded_out: plan.sharded_out,
settled_out: plan.settled_out,
},
outcomes: Outcomes {
killed: summary.killed,
survived: summary.survived,
timeout: summary.timeout,
out_of_memory: summary.out_of_memory,
flaky: summary.flaky,
unviable: summary.unviable,
ignored: summary.ignored,
uncovered: summary.uncovered,
not_built: summary.not_built,
pending: summary.pending,
},
build: session.map(build_of),
phases: session.map(phases_of),
durations: durations_of(&plan.mutants),
binaries: session.map(|session| binaries_of(session, context.redaction)).unwrap_or_default(),
mutators: breakdown(&plan.mutants, Redaction::Names, |mutant| mutant.mutator.to_string()),
packages: breakdown(&plan.mutants, context.redaction, |mutant| mutant.package.to_string()),
}
}
fn redact_toolchain(toolchain: Option<String>, redaction: Redaction) -> Option<String> {
let toolchain = toolchain?;
Some(
toolchain
.lines()
.filter_map(|line| {
let (key, value) = ["rustc=", "cargo=", "rustc_wrapper=", "rustc_workspace_wrapper="]
.into_iter()
.find_map(|key| line.strip_prefix(key).map(|value| (key, value)))?;
match redaction {
Redaction::Names => Some(line.to_owned()),
Redaction::Hashed => redaction.apply(value).map(|value| format!("{key}{value}")),
Redaction::Omitted => None,
}
})
.chain(
toolchain
.lines()
.filter(|line| {
!["rustc=", "cargo=", "rustc_wrapper=", "rustc_workspace_wrapper="]
.into_iter()
.any(|key| line.starts_with(key))
})
.map(str::to_owned),
)
.collect::<Vec<_>>()
.join("\n"),
)
}
pub fn to_json(bundle: &Bundle) -> crate::Result<String> {
serde_json::to_string_pretty(bundle)
.map_err(|cause| crate::error::error!("could not serialize the diagnostics bundle").caused_by(cause))
}
fn build_of(session: &Session) -> Build {
Build {
elapsed_ms: millis(session.build),
baseline_ms: millis(session.baseline_wall),
tests: session.tests,
withdrawn: session.withdrawn,
widened: session.widened,
rounds: session
.rounds_taken
.iter()
.map(|round| Round {
elapsed_ms: millis(round.elapsed),
withdrew: round.withdrew,
})
.collect(),
withdrawals: session
.census
.iter()
.map(|entry| Withdrawal {
code: entry.code.clone(),
mutator: entry.mutator.clone(),
mutants: entry.mutants,
})
.collect(),
baseline_peak_bytes: session.peak,
ordering_hints: (session.ordering.rounds > 0 || session.ordering.offered > 0).then_some(OrderingHints {
offered: session.ordering.offered,
confirmed: session.ordering.confirmed,
rounds: session.ordering.rounds,
}),
}
}
fn phases_of(session: &Session) -> Phases {
Phases {
copy: Phase {
elapsed_ms: millis(session.phases.copy),
},
preflight: Phase {
elapsed_ms: millis(session.phases.preflight),
},
baseline: Phase {
elapsed_ms: millis(session.baseline_wall),
},
census: session.phases.census.as_ref().map(|census| CensusPhase {
elapsed_ms: millis(census.elapsed),
walked: census.walked,
binaries: census.binaries,
}),
sweep: session.phases.sweep.as_ref().map(|sweep| SweepPhase {
elapsed_ms: millis(sweep.elapsed),
launches: sweep.launches,
probes: sweep.probes,
}),
}
}
fn binaries_of(session: &Session, redaction: Redaction) -> Vec<Binary> {
let mut binaries: Vec<&crate::exec::TestBinary> = session.binaries.iter().collect();
binaries.sort_by_key(|binary| core::cmp::Reverse(binary.baseline));
binaries
.into_iter()
.take(TOP)
.map(|binary| Binary {
package: redaction.apply(&binary.package),
target: redaction.apply(&binary.target),
baseline_ms: millis(binary.baseline),
budget_ms: binary.budget.map(millis),
peak_bytes: binary.peak,
ceiling_bytes: binary.memory,
})
.collect()
}
fn durations_of(mutants: &[Mutant]) -> Option<Durations> {
let mut spent: Vec<u64> = mutants
.iter()
.map(|mutant| mutant.elapsed_ms)
.filter(|elapsed| *elapsed > 0)
.collect();
if spent.is_empty() {
return None;
}
spent.sort_unstable();
Some(Durations {
evaluated: spent.len(),
min_ms: spent.first().copied().unwrap_or(0),
p50_ms: percentile(&spent, 0.50),
p90_ms: percentile(&spent, 0.90),
p99_ms: percentile(&spent, 0.99),
max_ms: spent.last().copied().unwrap_or(0),
})
}
fn breakdown(mutants: &[Mutant], redaction: Redaction, key: impl Fn(&Mutant) -> String) -> Vec<Breakdown> {
let mut buckets: crate::HashMap<String, Breakdown> = crate::HashMap::default();
for mutant in mutants {
let entry = buckets.entry(key(mutant)).or_insert_with(|| Breakdown {
name: None,
mutants: 0,
cpu_ms: 0,
survivors: 0,
unviable: 0,
});
entry.mutants += 1;
entry.cpu_ms = entry.cpu_ms.saturating_add(mutant.elapsed_ms);
match mutant.outcome {
Outcome::Survived => entry.survivors += 1,
Outcome::CompileError => entry.unviable += 1,
_other => {}
}
}
let mut rows: Vec<(String, Breakdown)> = buckets.into_iter().collect();
rows.sort_by(|(left_name, left), (right_name, right)| right.cpu_ms.cmp(&left.cpu_ms).then_with(|| left_name.cmp(right_name)));
rows.into_iter()
.take(TOP)
.map(|(name, row)| Breakdown {
name: redaction.apply(&name),
..row
})
.collect()
}
fn effective(cpu: Duration, testing: Duration) -> f64 {
if testing.is_zero() {
return 0.0;
}
(cpu.as_secs_f64() / testing.as_secs_f64() * 10.0).round() / 10.0
}
fn percentile(ascending: &[u64], fraction: f64) -> u64 {
if ascending.is_empty() {
return 0;
}
#[expect(clippy::cast_precision_loss, reason = "a population that large has other problems")]
#[expect(
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
reason = "a fraction of a non-negative length"
)]
let index = ((ascending.len() as f64 - 1.0) * fraction).round() as usize;
ascending.get(index).copied().unwrap_or(0)
}
fn millis(duration: Duration) -> u64 {
u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
}
#[cfg(test)]
#[cfg(not(miri))]
mod tests {
use super::*;
use crate::fixtures;
fn context() -> Context<'static> {
Context {
cores: 3,
jobs: 4,
wall: Duration::from_secs(100),
mutators: vec!["arith.add_to_sub"],
shard: None,
scratch_bytes: None,
redaction: Redaction::default(),
version: "0.0.0",
}
}
fn plan() -> Plan {
Plan {
skipped: Vec::new(),
digests: crate::HashMap::default(),
root: "/work/subject".into(),
files: Vec::new(),
mutants: Vec::new(),
suppressed: 0,
idle: Vec::new(),
sharded_out: 0,
settled_out: 0,
reach: crate::HashMap::default(),
specs: crate::HashMap::default(),
}
}
fn session_with(phases: crate::exec::Phases) -> Session {
Session {
baseline: Duration::from_millis(1500),
baseline_wall: Duration::from_millis(500),
tests: Some(12),
quiet: Duration::ZERO,
stall: None,
build: Duration::from_secs(7),
peak: None,
metered: false,
unbounded: None,
withdrawn: 0,
census: Vec::new(),
rounds: 1,
rounds_taken: Vec::new(),
binaries: Vec::new(),
scratch: "/work/scratch".into(),
filtered: 0,
widened: false,
ordering: crate::exec::OrderingHints::default(),
phases,
}
}
#[test]
fn a_run_that_built_something_carries_its_phase_timings() {
let phases = crate::exec::Phases {
copy: Duration::from_millis(12),
preflight: Duration::from_millis(340),
census: None,
sweep: Some(crate::exec::SweepCost {
elapsed: Duration::from_secs(42),
launches: 47,
probes: 12,
}),
};
let built = bundle(&plan(), Some(&session_with(phases)), &context());
let phases = built.phases.expect("a built run has phase timings");
assert_eq!(phases.copy.elapsed_ms, 12);
assert_eq!(phases.preflight.elapsed_ms, 340);
assert_eq!(phases.baseline.elapsed_ms, 500);
assert!(phases.census.is_none(), "no census ran, so the census phase is absent");
let sweep = phases.sweep.expect("the sweep ran");
assert_eq!(sweep.elapsed_ms, 42_000);
assert_eq!(sweep.launches, 47);
assert_eq!(sweep.probes, 12);
}
#[test]
fn a_run_that_built_nothing_has_no_phases_at_all() {
assert!(bundle(&plan(), None, &context()).phases.is_none());
}
#[test]
fn the_census_phase_is_present_only_when_a_census_ran() {
let without = bundle(&plan(), Some(&session_with(crate::exec::Phases::default())), &context());
assert!(without.phases.expect("phases").census.is_none(), "no census means no census phase");
let phases = crate::exec::Phases {
census: Some(crate::exec::CensusCost {
elapsed: Duration::from_secs(8),
walked: 1_681,
binaries: 30,
}),
..crate::exec::Phases::default()
};
let census = bundle(&plan(), Some(&session_with(phases)), &context())
.phases
.expect("phases")
.census
.expect("a census ran");
assert_eq!(census.elapsed_ms, 8_000);
assert_eq!(census.walked, 1_681);
assert_eq!(census.binaries, 30);
}
#[test]
fn the_baseline_phase_restates_the_build_baseline_exactly() {
let built = bundle(&plan(), Some(&session_with(crate::exec::Phases::default())), &context());
let baseline_phase = built.phases.expect("phases").baseline.elapsed_ms;
let build_baseline = built.build.expect("build").baseline_ms;
assert_eq!(baseline_phase, build_baseline);
}
#[test]
fn the_serialized_phases_use_camel_case_and_omit_the_phases_that_did_not_run() {
let phases = crate::exec::Phases {
copy: Duration::from_millis(1),
preflight: Duration::from_millis(2),
census: None,
sweep: Some(crate::exec::SweepCost {
elapsed: Duration::from_millis(3),
launches: 4,
probes: 5,
}),
};
let json = to_json(&bundle(&plan(), Some(&session_with(phases)), &context())).expect("json");
assert!(json.contains("\"phases\""), "{json}");
assert!(json.contains("\"elapsedMs\""), "{json}");
assert!(json.contains("\"launches\""), "{json}");
assert!(json.contains("\"probes\""), "{json}");
assert!(!json.contains("\"census\""), "an absent census must be omitted, not null: {json}");
assert!(!json.contains("\"walked\""), "{json}");
}
#[test]
fn the_bundle_carries_no_absolute_path_and_no_source_text() {
let json = to_json(&bundle(&plan(), None, &context())).expect("json");
assert!(!json.contains("/work/subject"), "{json}");
assert!(!json.contains("projectRoot"), "{json}");
}
#[test]
fn hashing_is_the_default_and_hides_the_name() {
assert_eq!(Redaction::default(), Redaction::Hashed);
let hashed = Redaction::Hashed.apply("secret-product").expect("hashed");
assert_ne!(hashed, "secret-product");
assert_eq!(hashed.len(), HASH_WIDTH);
}
#[test]
fn hashing_the_same_name_twice_gives_the_same_label() {
assert_eq!(Redaction::Hashed.apply("subject"), Redaction::Hashed.apply("subject"));
assert_ne!(Redaction::Hashed.apply("subject"), Redaction::Hashed.apply("other"));
}
#[test]
fn omitting_leaves_no_label_at_all() {
assert_eq!(Redaction::Omitted.apply("secret-product"), None);
assert_eq!(Redaction::Names.apply("secret-product").as_deref(), Some("secret-product"));
}
#[test]
fn the_bundle_says_which_schema_it_is() {
assert_eq!(bundle(&plan(), None, &context()).schema_version, "3");
}
#[test]
fn the_fixed_and_testing_split_adds_up_to_the_wall_time() {
let run = bundle(&plan(), None, &context()).run;
assert_eq!(run.wall_ms, 100_000);
assert_eq!(run.fixed_ms + run.testing_ms, run.wall_ms);
}
#[test]
fn fractional_milliseconds_keep_the_displayed_duration_split_consistent() {
let mut session = session_with(crate::exec::Phases::default());
session.build = Duration::from_micros(4_400);
session.baseline_wall = Duration::from_micros(2_500);
let context = Context {
wall: Duration::from_micros(10_100),
..context()
};
let run = bundle(&plan(), Some(&session), &context).run;
assert_eq!((run.wall_ms, run.fixed_ms, run.testing_ms), (10, 6, 4));
assert_eq!(run.fixed_ms + run.testing_ms, run.wall_ms);
}
#[test]
fn redacted_toolchains_hide_environment_derived_program_paths() {
let private = "/opt/acme/private-wrapper";
let toolchain = Some(format!(
"rustc=/toolchains/rustc\ncargo=/toolchains/cargo\nrustc_wrapper=\nrustc_workspace_wrapper={private}\nrustc 1.90.0\ncargo 1.90.0"
));
for redaction in [Redaction::Hashed, Redaction::Omitted] {
let redacted = redact_toolchain(toolchain.clone(), redaction).expect("toolchain");
assert!(!redacted.contains(private), "{redaction:?}: {redacted}");
assert!(redacted.contains("rustc 1.90.0"), "{redaction:?}: {redacted}");
}
}
#[test]
fn serialized_redacted_bundles_hide_workspace_wrapper_environment_paths() {
const CHILD: &str = "CARGO_GAMMA_DIAG_REDACTION_CHILD";
const PRIVATE: &str = "/opt/acme/private-wrapper";
if std::env::var_os(CHILD).is_some() {
for redaction in [Redaction::Hashed, Redaction::Omitted] {
let context = Context { redaction, ..context() };
let json = to_json(&bundle(&plan(), None, &context)).expect("serialized bundle");
assert!(!json.contains(PRIVATE), "{redaction:?}: {json}");
}
return;
}
let output = std::process::Command::new(std::env::current_exe().expect("test executable"))
.args([
"--exact",
"diag::bundle::tests::serialized_redacted_bundles_hide_workspace_wrapper_environment_paths",
"--nocapture",
])
.env(CHILD, "1")
.env("RUSTC_WORKSPACE_WRAPPER", PRIVATE)
.output()
.expect("diagnostic child process");
assert!(
output.status.success(),
"stdout:\n{}\nstderr:\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
}
#[test]
fn concurrent_baselines_use_wall_time_and_keep_cores_distinct_from_jobs() {
let mut session = session_with(crate::exec::Phases::default());
session.build = Duration::from_secs(7);
session.baseline = Duration::from_secs(5);
session.baseline_wall = Duration::from_secs(2);
let context = Context {
cores: 3,
jobs: 7,
wall: Duration::from_secs(20),
..context()
};
let bundle = bundle(&plan(), Some(&session), &context);
assert_eq!(
(bundle.run.wall_ms, bundle.run.fixed_ms, bundle.run.testing_ms),
(20_000, 9_000, 11_000)
);
assert_eq!(bundle.build.expect("build").baseline_ms, 2_000);
assert_eq!(bundle.phases.expect("phases").baseline.elapsed_ms, 2_000);
assert_eq!(bundle.host.cores, 3);
assert_eq!(bundle.config.jobs, 7);
}
#[test]
fn effective_jobs_is_cpu_over_the_testing_window() {
assert!((effective(Duration::from_secs(30), Duration::from_secs(10)) - 3.0).abs() < f64::EPSILON);
assert!(effective(Duration::from_secs(1), Duration::ZERO).abs() < f64::EPSILON);
}
#[test]
fn percentiles_come_from_the_ascending_list() {
let spent = [1_u64, 2, 3, 4, 5, 6, 7, 8, 9, 10];
assert_eq!(percentile(&spent, 0.50), 6);
assert_eq!(percentile(&spent, 0.90), 9);
assert_eq!(percentile(&spent, 0.0), 1);
assert_eq!(percentile(&[], 0.5), 0);
}
#[test]
fn a_run_that_measured_nothing_has_no_duration_distribution() {
assert!(durations_of(&[]).is_none());
}
#[test]
fn mutator_names_survive_redaction_while_package_names_do_not() {
let mut plan = plan();
plan.mutants = vec![mutant("subject", "arith.add_to_sub")];
let built = bundle(&plan, None, &context());
assert_eq!(built.mutators[0].name.as_deref(), Some("arith.add_to_sub"));
assert_ne!(built.packages[0].name.as_deref(), Some("subject"));
assert!(built.packages[0].name.is_some(), "the row still has to be groupable");
}
#[test]
fn the_breakdown_ranks_the_most_expensive_group_first() {
let mut plan = plan();
let mut cheap = mutant("cheap", "arith.add_to_sub");
let mut dear = mutant("dear", "relational.lt_to_le");
cheap.elapsed_ms = 10;
dear.elapsed_ms = 900;
plan.mutants = vec![cheap, dear];
let built = bundle(&plan, None, &context());
assert_eq!(built.mutators[0].name.as_deref(), Some("relational.lt_to_le"));
assert_eq!(built.mutators[0].cpu_ms, 900);
}
fn mutant(package: &str, mutator: &str) -> Mutant {
Mutant {
id: "id".to_owned().into(),
package: package.to_owned().into(),
mutator: mutator.to_owned().into(),
..fixtures::mutant()
}
}
}