#![cfg_attr(coverage_nightly, coverage(off))]
use crate::cli::RepoScoreOutputFormat;
use crate::services::repo_score::{
aggregator::ScoreAggregator, models::Grade, scorers::ScorerConfig, RepoScore,
};
use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_repo_score(
path: &Path,
format: RepoScoreOutputFormat,
verbose: bool,
failures_only: bool,
output: Option<&Path>,
update_badge: bool,
deep: bool,
) -> Result<()> {
if !path.exists() {
anyhow::bail!("Path not found: {}", path.display());
}
let config = build_scorer_config(verbose, failures_only, deep);
let aggregator = ScoreAggregator::new();
let score = aggregator
.aggregate(path, &config)
.await
.context("Failed to calculate repository score")?;
if update_badge {
update_readme_badge(path, &score)?;
}
let output_text = match format {
RepoScoreOutputFormat::Text => format_text(&score, verbose, failures_only),
RepoScoreOutputFormat::Json => format_json(&score, failures_only)?,
RepoScoreOutputFormat::Markdown => format_markdown(&score, failures_only),
RepoScoreOutputFormat::Yaml => format_yaml(&score, failures_only)?,
};
if let Some(output_path) = output {
fs::write(output_path, output_text)
.with_context(|| format!("Failed to write to {}", output_path.display()))?;
println!("Repository score written to: {}", output_path.display());
} else {
print!("{}", output_text);
}
Ok(())
}
fn build_scorer_config(verbose: bool, _failures_only: bool, deep: bool) -> ScorerConfig {
ScorerConfig {
verbose,
timeout_seconds: 300,
skip_slow_checks: false,
deep,
}
}
include!("repo_score_handlers_display.rs");
include!("repo_score_handlers_badge.rs");
include!("repo_score_handlers_tests.rs");
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod failures_only_is_a_display_filter_tests {
use super::*;
#[test]
fn scorer_config_does_not_vary_with_failures_only() {
let plain = build_scorer_config(false, false, false);
let filtered = build_scorer_config(false, true, false);
assert!(
!filtered.skip_slow_checks,
"--failures-only must not skip checks: a skipped check is scored as a pass"
);
assert_eq!(
format!("{plain:?}"),
format!("{filtered:?}"),
"a display filter must not change what is measured"
);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod failures_only_filters_the_report_tests {
use super::*;
use crate::services::repo_score::{
models::{CategoryScore, CategoryScores, ScoreMetadata, ScoreStatus},
RepoScore,
};
fn category(score: f64, max: f64, status: ScoreStatus) -> CategoryScore {
CategoryScore {
score,
max_score: max,
percentage: score / max * 100.0,
status,
subcategories: vec![],
findings: vec![],
}
}
fn mixed_score() -> RepoScore {
RepoScore {
total_score: 42.0,
grade: Grade::F,
categories: CategoryScores {
documentation: category(2.0, 20.0, ScoreStatus::Fail),
precommit_hooks: category(2.0, 20.0, ScoreStatus::Fail),
repository_hygiene: category(10.0, 10.0, ScoreStatus::Pass),
build_test_automation: category(2.0, 25.0, ScoreStatus::Fail),
continuous_integration: category(2.0, 20.0, ScoreStatus::Fail),
pmat_compliance: category(1.0, 5.0, ScoreStatus::Warning),
},
recommendations: vec![],
metadata: ScoreMetadata::new(std::path::PathBuf::from(".")),
}
}
#[test]
fn text_drops_the_passing_row_and_keeps_the_failing_ones() {
let score = mixed_score();
let plain = format_text(&score, false, false);
let filtered = format_text(&score, false, true);
assert_ne!(plain, filtered, "--failures-only changed nothing");
assert!(
plain.contains("Repository Hygiene"),
"control: the passing row is in the plain report:\n{plain}"
);
assert!(
!filtered.contains("Repository Hygiene"),
"the passing row survived --failures-only:\n{filtered}"
);
assert!(
filtered.contains("Documentation"),
"a failing row must survive:\n{filtered}"
);
}
#[test]
fn markdown_drops_the_passing_row() {
let score = mixed_score();
let plain = format_markdown(&score, false);
let filtered = format_markdown(&score, true);
assert_ne!(plain, filtered);
assert!(plain.contains("Repository Hygiene"));
assert!(!filtered.contains("Repository Hygiene"), "{filtered}");
}
#[test]
fn json_and_yaml_drop_the_passing_category() {
let score = mixed_score();
let doc: serde_json::Value =
serde_json::from_str(&format_json(&score, true).expect("json")).expect("parse");
assert!(
doc["categories"].get("repository_hygiene").is_none(),
"the passing category survived in JSON: {doc}"
);
assert!(
doc["categories"].get("documentation").is_some(),
"a failing category must survive in JSON: {doc}"
);
assert_eq!(doc["failures_only"], serde_json::Value::Bool(true));
let yaml = format_yaml(&score, true).expect("yaml");
assert!(!yaml.contains("repository_hygiene"), "{yaml}");
assert!(yaml.contains("documentation"), "{yaml}");
}
#[test]
fn the_total_is_the_measured_one_in_every_format() {
let score = mixed_score();
for report in [
format_text(&score, false, true),
format_markdown(&score, true),
] {
assert!(
report.contains("42.0"),
"the filtered report must still show the measured total:\n{report}"
);
}
let doc: serde_json::Value =
serde_json::from_str(&format_json(&score, true).expect("json")).expect("parse");
assert_eq!(doc["total_score"], 42.0);
}
}