#![cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod calculator_tests {
use super::super::calculator::{normalize_rps_percentage, PerfectionScoreCalculator};
use super::super::types::{CategoryScore, PerfectionScoreResult};
use std::fs;
use std::path::Path;
use tempfile::TempDir;
#[test]
fn test_calculator_new() {
let calc = PerfectionScoreCalculator::new();
assert!(!calc.fast_mode);
assert_eq!(calc.weights.tdg, 40);
}
#[test]
fn test_calculator_default() {
let calc = PerfectionScoreCalculator::default();
assert!(!calc.fast_mode);
}
#[test]
fn test_calculator_fast_mode_setter() {
let calc = PerfectionScoreCalculator::new().fast_mode(true);
assert!(calc.fast_mode);
let calc = PerfectionScoreCalculator::new().fast_mode(false);
assert!(!calc.fast_mode);
}
#[tokio::test]
async fn test_get_documentation_score_all_docs() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
fs::write(root.join("README.md"), "# Test Project").unwrap();
fs::write(root.join("CHANGELOG.md"), "# Changelog").unwrap();
fs::create_dir(root.join("docs")).unwrap();
fs::write(root.join("CONTRIBUTING.md"), "# Contributing").unwrap();
let calc = PerfectionScoreCalculator::new();
let (score, details) = calc.get_documentation_score(temp_dir.path()).await;
assert_eq!(score, 16.0 + 8.0 + 0.0 + 6.0);
assert!(details.contains("docs/ 0/25"), "{details}");
}
#[tokio::test]
async fn test_get_documentation_score_readme_only() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("README.md"), "# Test Project").unwrap();
let calc = PerfectionScoreCalculator::new();
let (score, _) = calc.get_documentation_score(temp_dir.path()).await;
assert_eq!(score, 16.0);
}
#[tokio::test]
async fn test_get_documentation_score_no_docs() {
let temp_dir = TempDir::new().unwrap();
let calc = PerfectionScoreCalculator::new();
let (score, details) = calc.get_documentation_score(temp_dir.path()).await;
assert_eq!(score, 0.0);
assert!(details.contains("README 0/40 (missing)"), "{details}");
}
#[tokio::test]
async fn test_get_documentation_score_lowercase_readme() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("readme.md"), "# Test").unwrap();
let calc = PerfectionScoreCalculator::new();
let (score, _) = calc.get_documentation_score(temp_dir.path()).await;
assert_eq!(score, 16.0);
}
#[tokio::test]
async fn test_performance_is_not_measured_from_a_benches_directory() {
let temp_dir = TempDir::new().unwrap();
fs::create_dir(temp_dir.path().join("benches")).unwrap();
fs::write(
temp_dir.path().join("Cargo.toml"),
"[dev-dependencies]\ncriterion = \"0.5\"\n",
)
.unwrap();
let calc = PerfectionScoreCalculator::new();
let err = calc
.get_performance_score(temp_dir.path())
.await
.expect_err("an empty benches/ dir is not a benchmark run");
assert!(err.contains("cargo bench"), "unhelpful reason: {err}");
}
#[tokio::test]
async fn test_performance_scores_criterion_comparisons() {
let temp_dir = TempDir::new().unwrap();
let criterion = temp_dir.path().join("target/criterion");
for (name, mean) in [("fast", -0.02), ("slow", 0.40), ("steady", 0.001)] {
let dir = criterion.join(name).join("change");
fs::create_dir_all(&dir).unwrap();
fs::write(
dir.join("estimates.json"),
format!("{{\"mean\":{{\"point_estimate\":{mean}}}}}"),
)
.unwrap();
}
let calc = PerfectionScoreCalculator::new();
let score = calc.get_performance_score(temp_dir.path()).await.unwrap();
assert!(
(score - (2.0 / 3.0 * 100.0)).abs() < 0.001,
"score was {score}"
);
}
#[tokio::test]
async fn test_mutation_is_not_measured_from_config_files() {
let temp_dir = TempDir::new().unwrap();
fs::write(temp_dir.path().join("mutants.toml"), "[mutants]").unwrap();
fs::create_dir(temp_dir.path().join(".mutants")).unwrap();
fs::write(
temp_dir.path().join("Cargo.toml"),
"[dev-dependencies]\ncargo-mutants = \"1.0\"\n",
)
.unwrap();
let calc = PerfectionScoreCalculator::new();
let err = calc
.get_mutation_score(temp_dir.path())
.await
.expect_err("configuration is not a mutation run");
assert!(err.contains("cargo mutants"), "unhelpful reason: {err}");
}
#[tokio::test]
async fn test_mutation_scores_cargo_mutants_outcomes() {
let temp_dir = TempDir::new().unwrap();
let out = temp_dir.path().join("mutants.out");
fs::create_dir_all(&out).unwrap();
fs::write(
out.join("outcomes.json"),
r#"{"outcomes":[
{"scenario":"Baseline","summary":"Success"},
{"scenario":{"Mutant":{}},"summary":"CaughtMutant"},
{"scenario":{"Mutant":{}},"summary":"CaughtMutant"},
{"scenario":{"Mutant":{}},"summary":"CaughtMutant"},
{"scenario":{"Mutant":{}},"summary":"MissedMutant"},
{"scenario":{"Mutant":{}},"summary":"Unviable"}
]}"#,
)
.unwrap();
let calc = PerfectionScoreCalculator::new();
let score = calc.get_mutation_score(temp_dir.path()).await.unwrap();
assert_eq!(score, 75.0);
}
#[tokio::test]
async fn test_get_coverage_score_from_cache() {
let temp_dir = TempDir::new().unwrap();
let metrics_dir = temp_dir.path().join(".pmat-metrics");
fs::create_dir_all(&metrics_dir).unwrap();
fs::write(metrics_dir.join("coverage.json"), r#"{"coverage": 85.5}"#).unwrap();
let calc = PerfectionScoreCalculator::new();
let score = calc.get_coverage_score(temp_dir.path()).await.unwrap();
assert_eq!(score, 85.5);
}
#[tokio::test]
async fn test_coverage_is_not_estimated_from_test_attribute_counts() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
fs::create_dir(root.join("src")).unwrap();
for i in 0..5_usize {
fs::write(
root.join("src").join(format!("mod_{i}.rs")),
format!("// Source file {i}\n\n#[test]\nfn test_{i}_0 () {{}}\n"),
)
.unwrap();
}
fs::write(root.join("Cargo.toml"), "[package]\nname = \"t\"\n").unwrap();
let calc = PerfectionScoreCalculator::new();
let err = calc
.get_coverage_score(root)
.await
.expect_err("test attributes are not coverage");
assert!(err.contains("llvm-cov"), "unhelpful reason: {err}");
}
#[tokio::test]
async fn test_get_coverage_score_empty_project() {
let temp_dir = TempDir::new().unwrap();
let calc = PerfectionScoreCalculator::new();
assert!(
calc.get_coverage_score(temp_dir.path()).await.is_err(),
"an empty project has no coverage, not 70%"
);
}
#[tokio::test]
async fn test_empty_files_do_not_move_the_total() {
fn bare_crate() -> TempDir {
let dir = TempDir::new().unwrap();
fs::create_dir(dir.path().join("src")).unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"t\"\n").unwrap();
fs::write(dir.path().join("src/lib.rs"), "//! x\n").unwrap();
dir
}
let bare = bare_crate();
let dressed = bare_crate();
fs::write(dressed.path().join("mutants.toml"), "").unwrap();
fs::create_dir(dressed.path().join(".mutants")).unwrap();
fs::create_dir(dressed.path().join("benches")).unwrap();
fs::write(
dressed.path().join("Cargo.toml"),
"[package]\nname = \"t\"\n\n[dev-dependencies]\ncriterion = \"0.5\"\n",
)
.unwrap();
let calc = PerfectionScoreCalculator::new().fast_mode(true);
let bare_result = calc.calculate(bare.path()).await.unwrap();
let dressed_result = calc.calculate(dressed.path()).await.unwrap();
let category = |r: &super::super::types::PerfectionScoreResult, name: &str| {
r.categories
.iter()
.find(|c| c.name == name)
.unwrap_or_else(|| panic!("missing category {name}"))
.earned_points
};
for name in ["Mutation Testing", "Performance", "Test Coverage"] {
assert_eq!(
category(&bare_result, name),
category(&dressed_result, name),
"{name} moved on four empty files"
);
assert_eq!(
category(&bare_result, name),
0.0,
"{name} was scored without evidence"
);
}
}
#[tokio::test]
async fn test_calculator_fast_mode_mutation_earns_nothing() {
let temp_dir = TempDir::new().unwrap();
let calc = PerfectionScoreCalculator::new().fast_mode(true);
let result = calc.calculate(temp_dir.path()).await.unwrap();
let mutation_cat = result
.categories
.iter()
.find(|c| c.name == "Mutation Testing")
.unwrap();
assert_eq!(
mutation_cat.earned_points, 0.0,
"an unmeasured category must not contribute points"
);
assert_eq!(
mutation_cat.max_points, 0,
"an unmeasured category must not sit in the denominator"
);
assert!(mutation_cat
.details
.as_ref()
.is_some_and(|d| d.contains("Not measured")));
let summed: u16 = result.categories.iter().map(|c| c.max_points).sum();
assert_eq!(summed, result.max_score);
assert!(
result.max_score <= 180,
"mutation must have left the denominator, got {}",
result.max_score
);
}
#[tokio::test]
async fn test_tdg_category_agrees_with_the_tdg_command() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
fs::create_dir(root.join("src")).unwrap();
fs::write(root.join("Cargo.toml"), "[package]\nname = \"awful\"\n").unwrap();
let mut src = String::from(
"// TODO: this whole file is a mess\n// FIXME: rewrite\n// HACK: do not ship\n",
);
src.push_str(
"pub fn monster(a: i32, b: i32, c: i32, d: i32) -> i32 {\n let mut r = 0;\n",
);
for i in 0..60 {
src.push_str(&format!(
" if a > {i} && b < {i} || c == {i} {{ r += {i}; }} else if d != {i} {{ r -= {i}; }}\n"
));
}
src.push_str(" r\n}\n");
for i in 0..40 {
src.push_str(&format!(
"pub fn bad{i}(s: &str) -> i32 {{ // TODO fix bad{i}\n let v: i32 = s.parse().unwrap();\n if v < 0 {{ panic!(\"negative\"); }}\n v\n}}\n"
));
}
fs::write(root.join("src/lib.rs"), src).unwrap();
let expected = crate::tdg::TdgAnalyzer::new()
.unwrap()
.analyze_project(root)
.await
.unwrap()
.average_score
.expect("the fixture has a gradable file");
let calc = PerfectionScoreCalculator::new();
let measured = calc.get_tdg_score(root).await.expect("a gradable tree");
assert!(
(measured - f64::from(expected)).abs() < 0.001,
"perfection-score says {measured}, `pmat tdg` says {expected}"
);
}
#[tokio::test]
async fn test_tdg_category_is_not_measured_without_source() {
let temp_dir = TempDir::new().unwrap();
let calc = PerfectionScoreCalculator::new();
let err = calc
.get_tdg_score(temp_dir.path())
.await
.expect_err("nothing to grade");
assert!(err.contains("gradable"), "unhelpful reason: {err}");
}
#[test]
fn test_category_score_in_calculator_context() {
let score = CategoryScore::new("Technical Debt Grade", 75.0, 40);
assert_eq!(score.earned_points, 30.0);
assert_eq!(score.grade, "C");
}
#[test]
fn test_rps_raw_points_normalize_to_category_fraction() {
let pct = normalize_rps_percentage(246.6, 289.0);
assert!((pct - 85.328).abs() < 0.01, "pct was {}", pct);
let score = CategoryScore::new("Rust Project Quality", pct, 30);
assert!(
(score.earned_points - 25.6).abs() < 0.01,
"earned was {}",
score.earned_points
);
}
#[test]
fn test_rps_perfect_input_earns_exactly_max() {
let pct = normalize_rps_percentage(289.0, 289.0);
assert_eq!(pct, 100.0);
let score = CategoryScore::new("Rust Project Quality", pct, 30);
assert_eq!(score.earned_points, 30.0);
}
#[test]
fn test_rps_normalize_degenerate_inputs() {
assert_eq!(normalize_rps_percentage(100.0, 0.0), 0.0);
assert_eq!(normalize_rps_percentage(-5.0, 289.0), 0.0);
assert_eq!(normalize_rps_percentage(300.0, 289.0), 100.0);
}
#[test]
fn test_category_never_exceeds_max_points() {
let score = CategoryScore::new("Rust Project Quality", 184.03, 30);
assert_eq!(score.earned_points, 30.0);
let perfect = CategoryScore::new("Rust Project Quality", 100.0, 30);
assert_eq!(
score.grade, perfect.grade,
"over-max raw must grade as the clamped 100%, not via overflow"
);
let negative = CategoryScore::new("Rust Project Quality", -10.0, 30);
assert_eq!(negative.earned_points, 0.0);
let zero = CategoryScore::new("Rust Project Quality", 0.0, 30);
assert_eq!(
negative.grade, zero.grade,
"negative raw must grade as the clamped 0%"
);
}
#[tokio::test(start_paused = true)]
async fn a_run_that_exceeds_its_budget_refuses_instead_of_grading_zero() {
let budget = std::time::Duration::from_secs(120);
let never = async {
tokio::time::sleep(budget * 10).await;
unreachable!("the backstop must fire first")
};
let err = super::super::calculator::guard_total(Path::new("/some/project"), budget, never)
.await
.expect_err("a run that measured nothing must not produce a score");
let text = err.to_string();
assert!(text.contains("measured nothing"), "{text}");
assert!(text.contains("120s"), "{text}");
assert!(!text.contains("Timed out"), "{text}");
}
#[tokio::test(start_paused = true)]
async fn the_backstop_passes_a_finished_run_through_untouched() {
let inner = async {
Ok(PerfectionScoreResult::new(vec![CategoryScore::new(
"Documentation",
80.0,
15,
)]))
};
let result = super::super::calculator::guard_total(
Path::new("/some/project"),
std::time::Duration::from_secs(120),
inner,
)
.await
.expect("an in-budget run must be returned as-is");
assert_eq!(result.categories.len(), 1);
assert_eq!(result.categories[0].earned_points, 12.0);
}
#[tokio::test(start_paused = true)]
async fn a_category_that_overruns_its_budget_is_not_measured() {
let slow = async {
tokio::time::sleep(std::time::Duration::from_secs(10_000)).await;
Ok(100.0)
};
let why = super::super::calculator::within_budget(slow)
.await
.expect_err("an overrunning category must not report a number");
assert!(why.contains("did not finish within"), "{why}");
let mut categories = Vec::new();
super::super::calculator::push_category(
&mut categories,
"Technical Debt Grade",
40,
Err(why),
);
assert_eq!(categories[0].max_points, 0);
assert_eq!(categories[0].grade, "N/A");
assert!(
categories[0]
.details
.as_deref()
.unwrap_or_default()
.contains("Not measured"),
"{:?}",
categories[0].details
);
}
#[tokio::test]
async fn four_empty_files_buy_no_documentation_points() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
for name in ["README.md", "CHANGELOG.md", "CONTRIBUTING.md"] {
fs::write(root.join(name), "").unwrap();
}
fs::create_dir(root.join("docs")).unwrap();
let (score, details) = PerfectionScoreCalculator::new()
.get_documentation_score(root)
.await;
assert_eq!(score, 0.0, "{details}");
assert!(details.contains("README 0/40 (empty)"), "{details}");
assert!(details.contains("docs/ 0/25 (0 non-empty"), "{details}");
}
#[tokio::test]
async fn written_documentation_outranks_empty_files() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
let readme = format!(
"# Project\n\n{}\n\n## Usage\n\n```sh\npmat perfection-score\n```\n",
"It does the thing, and here is a paragraph about how. ".repeat(6)
);
fs::write(root.join("README.md"), readme).unwrap();
fs::write(
root.join("CHANGELOG.md"),
format!(
"# Changelog\n\n## [1.2.3] - 2026-01-01\n\n{}\n",
"- fixed a thing that was broken in a way worth writing down. ".repeat(5)
),
)
.unwrap();
fs::create_dir(root.join("docs")).unwrap();
for i in 0..5 {
fs::write(
root.join("docs").join(format!("g{i}.md")),
"# Guide\nbody\n",
)
.unwrap();
}
fs::write(
root.join("CONTRIBUTING.md"),
format!(
"# Contributing\n\n{}\n",
"Run cargo test, then open a pull request. ".repeat(8)
),
)
.unwrap();
let (score, details) = PerfectionScoreCalculator::new()
.get_documentation_score(root)
.await;
assert_eq!(score, 100.0, "{details}");
}
#[tokio::test]
async fn substantive_but_unstructured_readme_earns_partial_credit() {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path();
fs::write(
root.join("README.md"),
"a wall of prose with no heading and no example. ".repeat(10),
)
.unwrap();
let (score, details) = PerfectionScoreCalculator::new()
.get_documentation_score(root)
.await;
assert_eq!(score, 28.0, "{details}");
assert!(details.contains("no structure"), "{details}");
}
}