use anyhow::Result;
use std::path::{Path, PathBuf};
use crate::cli::enums::TdgOutputFormat;
use crate::tdg::formatters::{
format_comparison, format_human, format_json, format_markdown, format_project,
};
use crate::tdg::TdgAnalyzer;
#[derive(Debug, Clone)]
pub struct TdgAnalysisConfig {
pub path: PathBuf,
pub threshold: Option<f64>,
pub top_files: Option<usize>,
pub format: TdgOutputFormat,
pub include_components: bool,
pub output: Option<PathBuf>,
pub critical_only: bool,
pub verbose: bool,
}
fn is_test_source(path: &Path) -> bool {
if path.parent().is_some_and(|parent| {
parent.components().any(|c| {
matches!(
c.as_os_str().to_str(),
Some("tests" | "benches" | "examples" | "fuzz")
)
})
}) {
return true;
}
let Some(name) = path.file_name().and_then(|n| n.to_str()) else {
return false;
};
let stem = name.strip_suffix(".rs").unwrap_or(name);
stem == "tests"
|| stem == "test"
|| stem.starts_with("test_")
|| stem.starts_with("tests_")
|| stem.ends_with("_test")
|| stem.ends_with("_tests")
|| stem.contains("_test_")
|| stem.contains("_tests_")
}
async fn check_for_critical_defects(path: &Path, fail_on_critical: bool) -> Result<()> {
use crate::services::defect_detector::{RustDefectDetector, Severity};
use ignore::WalkBuilder;
let detector = RustDefectDetector::new();
let mut critical_defects_found = false;
let mut critical_count = 0;
crate::status_eprintln!("🔍 Checking for critical defects...");
for entry in WalkBuilder::new(path)
.follow_links(false)
.hidden(false)
.parents(true)
.ignore(true)
.git_ignore(true)
.git_global(true)
.git_exclude(true)
.build()
.filter_map(std::result::Result::ok)
{
let file_path = entry.path();
if !file_path.is_file() || file_path.extension() != Some(std::ffi::OsStr::new("rs")) {
continue;
}
if is_test_source(file_path) {
continue;
}
let content = match tokio::fs::read_to_string(file_path).await {
Ok(c) => c,
Err(_) => continue, };
let defects = detector.detect(&content, file_path);
for defect in &defects {
if defect.severity == Severity::Critical {
critical_defects_found = true;
critical_count += defect.instances.len();
if let Some(instance) = defect.instances.first() {
eprintln!(
"❌ CRITICAL DEFECT: {} in {}:{}:{}",
defect.name, instance.file, instance.line, instance.column
);
eprintln!(" Code: {}", instance.code_snippet);
}
}
}
}
if critical_defects_found {
if !fail_on_critical {
eprintln!(
"\n⚠️ {critical_count} critical defect(s) found (advisory). `analyze tdg` \
reports; `pmat analyze build-tdg` gates on them."
);
eprintln!(
" Run: pmat analyze defects --path {} --format text",
path.display()
);
return Ok(());
}
eprintln!(
"\n⛔ TDG ANALYSIS FAILED: Found {} critical defect(s)",
critical_count
);
eprintln!(" Critical defects must be fixed before deployment.");
eprintln!(
" Run: pmat analyze defects --path {} --format text",
path.display()
);
anyhow::bail!("TDG auto-fail: Critical defects detected")
}
crate::status_eprintln!("✅ No critical defects found");
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "check_compliance")]
pub async fn handle_analyze_tdg(config: TdgAnalysisConfig) -> Result<()> {
run_tdg_analysis(config, false).await
}
pub async fn handle_analyze_tdg_gated(config: TdgAnalysisConfig) -> Result<()> {
run_tdg_analysis(config, true).await
}
async fn run_tdg_analysis(config: TdgAnalysisConfig, enforce_threshold: bool) -> Result<()> {
crate::status_eprintln!("🔍 Starting TDG (Technical Debt Grading) analysis...");
let analyzer = TdgAnalyzer::new()?;
let threshold = config.threshold;
let top_files = config.top_files.unwrap_or(10);
if !enforce_threshold {
if let Some(t) = threshold {
if (t - 1.5).abs() > f64::EPSILON {
eprintln!(
"⚠️ --threshold {t} was not applied: `analyze tdg` reports every analysed \
file (see -n/--top-files and --critical-only). --threshold gates \
`analyze build-tdg` only."
);
}
}
}
if config.include_components
&& matches!(
config.format,
TdgOutputFormat::Json | TdgOutputFormat::Sarif
)
{
eprintln!(
"ℹ️ --include-components: -f json/sarif already carry every per-file component; \
the flag adds breakdown sections to -f table and -f markdown."
);
}
let (result, measured_score) = if config.path.is_dir() {
analyze_project_path(
&analyzer,
&config.path,
&config.format,
top_files,
config.critical_only,
config.include_components,
)
.await?
} else {
analyze_single_file(&analyzer, &config.path, &config.format).await?
};
write_or_print_result(&result, config.output).await?;
check_for_critical_defects(&config.path, enforce_threshold).await?;
crate::status_eprintln!("✅ TDG analysis complete");
if enforce_threshold {
enforce_tdg_threshold(measured_score, threshold.unwrap_or(2.0))?;
}
Ok(())
}
fn enforce_tdg_threshold(measured: Option<f32>, threshold: f64) -> Result<()> {
let Some(measured) = measured else {
anyhow::bail!(
"TDG gate failed: no file under this path could be graded, so there is no score to \
compare against the required minimum of {threshold:.1} (--threshold)"
);
};
eprintln!(
"🚦 TDG gate: measured {measured:.1}/100 against required minimum {threshold:.1} \
(--threshold, on the 0-100 scale where higher is better)"
);
if f64::from(measured) < threshold {
anyhow::bail!(
"TDG gate failed: score {measured:.1}/100 is below the required minimum of {threshold:.1} (--threshold)"
);
}
Ok(())
}
async fn analyze_project_path(
analyzer: &TdgAnalyzer,
path: &Path,
format: &TdgOutputFormat,
top_files: usize,
critical_only: bool,
include_components: bool,
) -> Result<(String, Option<f32>)> {
let mut project_score = analyzer.analyze_project(path).await?;
project_score.limit_to_worst_files(top_files);
if critical_only {
retain_critical_files(&mut project_score);
}
let average_score = project_score.average_score;
Ok((
format_project_result(&project_score, path, format, include_components)?,
average_score,
))
}
fn retain_critical_files(project_score: &mut crate::tdg::ProjectScore) {
project_score.files.retain(|file| file.has_critical_defects);
project_score.files_reported = project_score.files.len();
project_score.files_truncated = project_score.files_reported < project_score.total_files;
project_score.list_filter = Some("--critical-only".to_string());
}
async fn analyze_single_file(
analyzer: &TdgAnalyzer,
path: &Path,
format: &TdgOutputFormat,
) -> Result<(String, Option<f32>)> {
let score = analyzer.analyze_file(path).await?;
let total = Some(score.total);
Ok((format_file_result(&score, format)?, total))
}
fn format_project_result(
project_score: &crate::tdg::ProjectScore,
root: &Path,
format: &TdgOutputFormat,
include_components: bool,
) -> Result<String> {
let result = match format {
TdgOutputFormat::Table => {
let mut out = format_project(project_score);
if include_components {
out.push_str(&components_text(project_score));
}
out
}
TdgOutputFormat::Json => serde_json::to_string_pretty(project_score)?,
TdgOutputFormat::Markdown => project_markdown(project_score, include_components),
TdgOutputFormat::Sarif => {
let sarif = create_sarif_output(project_score, root);
serde_json::to_string_pretty(&sarif)?
}
};
Ok(result)
}
fn percent_of(count: usize, total: usize) -> f32 {
if total == 0 {
0.0
} else {
(count as f32 / total as f32) * 100.0
}
}
fn project_markdown(project: &crate::tdg::ProjectScore, include_components: bool) -> String {
use std::fmt::Write;
let mut out = String::new();
let w = &mut out;
let _ = writeln!(w, "# Project TDG Score Report\n");
let _ = match (project.average_score, project.average_grade) {
(Some(score), Some(grade)) => {
writeln!(w, "**Average Score:** {score:.1}/100 ({grade})")
}
_ => writeln!(w, "**Average Score:** not measured (no files analysed)"),
};
let _ = writeln!(w, "**Total Files:** {}", project.total_files);
if !project.ungraded_files.is_empty() {
let lines =
crate::tdg::formatters::ungraded::ungraded_markdown_lines(&project.ungraded_files);
let _ = writeln!(w, "{}\n", lines[0]);
for line in &lines[1..] {
let _ = writeln!(w, "{line}");
}
}
if project.files_truncated {
let via = project
.list_filter
.as_deref()
.map(|f| format!(" ({f})"))
.unwrap_or_default();
let _ = writeln!(
w,
"**Files Listed:** {} of {}{via}",
project.files_reported, project.total_files
);
}
if project.grade_capped {
let _ = writeln!(
w,
"**Grade Capped:** yes ({} F-grade file(s))",
project.f_grade_count
);
}
let _ = writeln!(w, "\n## Language Distribution\n");
let _ = writeln!(w, "| Language | Files | Share |");
let _ = writeln!(w, "|---|---:|---:|");
for (language, count) in &project.language_distribution {
let _ = writeln!(
w,
"| {} | {} | {:.1}% |",
language,
count,
percent_of(*count, project.total_files)
);
}
let _ = writeln!(w, "\n## Grade Distribution\n");
let _ = writeln!(w, "| Grade | Files | Share |");
let _ = writeln!(w, "|---|---:|---:|");
for (grade, count) in &project.grade_distribution {
let _ = writeln!(
w,
"| {} | {} | {:.1}% |",
grade,
count,
percent_of(*count, project.total_files)
);
}
let _ = writeln!(w, "\n## Files\n");
let _ = writeln!(w, "| File | Score | Grade |");
let _ = writeln!(w, "|---|---:|---|");
for file in &project.files {
let _ = writeln!(
w,
"| `{}` | {:.1} | {} |",
file_label(file),
file.total,
file.grade
);
}
if include_components {
let _ = writeln!(w, "\n## Component Breakdown\n");
let _ = writeln!(
w,
"| File | Structural | Semantic | Duplication | Coupling | Documentation | Consistency |"
);
let _ = writeln!(w, "|---|---:|---:|---:|---:|---:|---:|");
for file in &project.files {
let _ = writeln!(
w,
"| `{}` | {:.1} | {:.1} | {:.1} | {:.1} | {:.1} | {:.1} |",
file_label(file),
file.structural_complexity,
file.semantic_complexity,
file.duplication_ratio,
file.coupling_score,
file.doc_coverage,
file.consistency_score
);
}
}
out
}
fn components_text(project: &crate::tdg::ProjectScore) -> String {
use std::fmt::Write;
let mut out = String::new();
let _ = writeln!(
out,
"\nComponent Breakdown (--include-components; points earned per metric):"
);
if project.files.is_empty() {
let _ = writeln!(out, " (no files reported)");
return out;
}
for file in &project.files {
let _ = writeln!(
out,
" {:<48} structural {:>5.1} semantic {:>5.1} duplication {:>5.1} \
coupling {:>5.1} documentation {:>5.1} consistency {:>5.1}",
file_label(file),
file.structural_complexity,
file.semantic_complexity,
file.duplication_ratio,
file.coupling_score,
file.doc_coverage,
file.consistency_score
);
}
out
}
fn file_label(file: &crate::tdg::TdgScore) -> String {
file.file_path
.as_ref()
.map_or_else(|| "unknown".to_string(), |p| p.display().to_string())
}
fn format_file_result(score: &crate::tdg::TdgScore, format: &TdgOutputFormat) -> Result<String> {
let result = match format {
TdgOutputFormat::Table => format_human(score),
TdgOutputFormat::Json => format_json(score),
TdgOutputFormat::Markdown => format_markdown(score),
TdgOutputFormat::Sarif => {
let sarif = create_file_sarif_output(score);
serde_json::to_string_pretty(&sarif)?
}
};
Ok(result)
}
async fn write_or_print_result(result: &str, output_path: Option<PathBuf>) -> Result<()> {
if let Some(output_path) = output_path {
tokio::fs::write(&output_path, result).await?;
crate::status_eprintln!("📝 Results written to {}", output_path.display());
} else {
println!("{result}");
}
Ok(())
}
#[provable_contracts_macros::contract("pmat-core.yaml", equation = "path_exists")]
pub async fn handle_tdg_compare(
path1: PathBuf,
path2: PathBuf,
format: TdgOutputFormat,
output: Option<PathBuf>,
) -> Result<()> {
crate::status_eprintln!("🔍 Starting TDG comparison...");
let analyzer = TdgAnalyzer::new()?;
let comparison = analyzer.compare(&path1, &path2).await?;
let result = format_comparison_result(&comparison, &format)?;
write_or_print_result(&result, output).await?;
crate::status_eprintln!("✅ TDG comparison complete");
Ok(())
}
fn format_comparison_result(
comparison: &crate::tdg::Comparison,
format: &TdgOutputFormat,
) -> Result<String> {
let result = match format {
TdgOutputFormat::Table => format_comparison(comparison),
TdgOutputFormat::Json => serde_json::to_string_pretty(comparison)?,
TdgOutputFormat::Markdown => {
let mut md = format_comparison(comparison);
md.insert_str(0, "# TDG Comparison Report\n\n");
md
}
TdgOutputFormat::Sarif => {
anyhow::bail!("SARIF format is not supported for comparisons")
}
};
Ok(result)
}
fn sarif_level(total: f32) -> &'static str {
if total < 50.0 {
"error"
} else if total < 65.0 {
"warning"
} else if total < 75.0 {
"note"
} else {
"none"
}
}
fn sarif_location(uri: String) -> serde_json::Value {
serde_json::json!([{
"physicalLocation": {
"artifactLocation": { "uri": uri }
}
}])
}
fn sarif_score_properties(score: &crate::tdg::TdgScore) -> serde_json::Value {
serde_json::json!({
"tdg_score": score.total,
"grade": score.grade.to_string(),
"language": score.language.to_string(),
"confidence": score.confidence,
"structural_complexity": score.structural_complexity,
"semantic_complexity": score.semantic_complexity,
"duplication_ratio": score.duplication_ratio,
"coupling_score": score.coupling_score,
"doc_coverage": score.doc_coverage,
"consistency_score": score.consistency_score,
"has_contract_coverage": score.has_contract_coverage,
})
}
fn sarif_file_result(score: &crate::tdg::TdgScore) -> serde_json::Value {
let issues = score
.penalties_applied
.iter()
.map(|p| p.issue.as_str())
.collect::<Vec<_>>()
.join(", ");
let issues = if issues.is_empty() {
"none recorded".to_string()
} else {
issues
};
serde_json::json!({
"ruleId": "TDG001",
"level": sarif_level(score.total),
"message": {
"text": format!(
"File TDG score {:.1}/100 ({}). Issues: {}",
score.total, score.grade, issues
)
},
"locations": sarif_location(
score
.file_path
.as_ref()
.map_or_else(|| "unknown".to_string(), |p| p.display().to_string()),
),
"properties": sarif_score_properties(score),
})
}
fn sarif_project_result(project: &crate::tdg::ProjectScore, root: &Path) -> serde_json::Value {
let text = match (project.average_score, project.average_grade) {
(Some(score), Some(grade)) => format!(
"Project TDG score {score:.1}/100 ({grade}) over {} file(s).",
project.total_files
),
_ => format!(
"No analyzable files were found under {}; there is no project TDG score.",
root.display()
),
};
serde_json::json!({
"ruleId": "TDG000",
"level": match project.average_score {
Some(score) if project.total_files > 0 => sarif_level(score),
_ => "error",
},
"message": { "text": text },
"locations": sarif_location(root.display().to_string()),
"properties": {
"tdg_score": project.average_score,
"grade": project.average_grade.map(|g| g.to_string()),
"not_measured": project.not_measured,
"total_files": project.total_files,
"f_grade_count": project.f_grade_count,
"grade_capped": project.grade_capped,
},
})
}
fn sarif_rules() -> serde_json::Value {
serde_json::json!([
{
"id": "TDG000",
"name": "ProjectTechnicalDebtGrading",
"shortDescription": { "text": "Project-level Technical Debt Grading (TDG) score" },
"fullDescription": { "text": "The aggregate TDG score for the analyzed path. This is the same number reported by --format json, markdown and table." },
"help": { "text": "Raise the project score by improving the lowest-scoring files reported under TDG001." }
},
{
"id": "TDG001",
"name": "TechnicalDebtGrading",
"shortDescription": { "text": "Technical Debt Grading (TDG) quality assessment" },
"fullDescription": { "text": "Comprehensive code quality assessment using orthogonal metrics: structural complexity, semantic complexity, code duplication, coupling, documentation, and consistency." },
"help": { "text": "Review the specific issues identified in the TDG analysis and consider refactoring to improve code quality." }
}
])
}
fn sarif_document(
results: Vec<serde_json::Value>,
properties: serde_json::Value,
) -> serde_json::Value {
serde_json::json!({
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
"version": "2.1.0",
"runs": [{
"tool": {
"driver": {
"name": "pmat-tdg",
"informationUri": "https://github.com/paiml/paiml-mcp-agent-toolkit",
"version": env!("CARGO_PKG_VERSION"),
"rules": sarif_rules(),
}
},
"properties": properties,
"results": results
}]
})
}
pub(crate) fn create_sarif_output(
project: &crate::tdg::ProjectScore,
root: &Path,
) -> serde_json::Value {
let mut files: Vec<&crate::tdg::TdgScore> = project.files.iter().collect();
files.sort_by(|a, b| a.file_path.cmp(&b.file_path));
let mut results = vec![sarif_project_result(project, root)];
results.extend(files.into_iter().map(sarif_file_result));
sarif_document(
results,
serde_json::json!({
"total_files": project.total_files,
"average_score": project.average_score,
"average_grade": project.average_grade.map(|g| g.to_string()),
"not_measured": project.not_measured,
"f_grade_count": project.f_grade_count,
"grade_capped": project.grade_capped,
}),
)
}
pub(crate) fn create_file_sarif_output(score: &crate::tdg::TdgScore) -> serde_json::Value {
sarif_document(
vec![sarif_file_result(score)],
serde_json::json!({
"total_files": 1,
"average_score": score.total,
"average_grade": score.grade.to_string(),
}),
)
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
#[tokio::test]
async fn test_handle_analyze_tdg_file() -> Result<()> {
let mut temp_file = NamedTempFile::with_suffix(".rs")?;
writeln!(
temp_file,
r#"
/// A well-documented function
pub fn simple_function() -> i32 {{
42
}}
"#
)?;
let config = TdgAnalysisConfig {
path: temp_file.path().to_path_buf(),
threshold: Some(0.0),
top_files: Some(10),
format: TdgOutputFormat::Json,
include_components: false,
output: None,
critical_only: false,
verbose: false,
};
let result = handle_analyze_tdg(config).await;
assert!(result.is_ok());
Ok(())
}
#[test]
fn threshold_gate_fails_when_the_measured_score_is_below_it() {
let err = enforce_tdg_threshold(Some(85.0), 90.0)
.expect_err("85.0/100 must not satisfy a required minimum of 90.0");
let msg = err.to_string();
assert!(
msg.contains("85.0"),
"gate must state what it measured: {msg}"
);
assert!(
msg.contains("90.0"),
"gate must state what it required: {msg}"
);
}
#[test]
fn threshold_gate_passes_when_the_measured_score_meets_it() {
assert!(enforce_tdg_threshold(Some(85.0), 2.0).is_ok());
assert!(enforce_tdg_threshold(Some(85.0), 85.0).is_ok());
}
#[test]
fn threshold_gate_fails_when_nothing_was_measured() {
let err = enforce_tdg_threshold(None, 2.0)
.expect_err("a gate that measured nothing must not report a pass");
let msg = err.to_string();
assert!(
msg.contains("no file") && msg.contains("no score"),
"gate must say it measured nothing: {msg}"
);
}
#[tokio::test]
async fn build_tdg_gate_rejects_a_project_below_the_threshold() -> Result<()> {
let dir = tempfile::tempdir()?;
std::fs::write(
dir.path().join("lib.rs"),
"pub fn a() -> i32 { 1 }\npub fn b() -> i32 { 2 }\n",
)?;
let cfg = |threshold: f64| TdgAnalysisConfig {
path: dir.path().to_path_buf(),
threshold: Some(threshold),
top_files: Some(10),
format: TdgOutputFormat::Json,
include_components: false,
output: Some(dir.path().join("out.json")),
critical_only: false,
verbose: false,
};
assert!(
handle_analyze_tdg_gated(cfg(1000.0)).await.is_err(),
"--threshold 1000 must fail: no project scores above 100/100"
);
assert!(handle_analyze_tdg(cfg(1000.0)).await.is_ok());
Ok(())
}
fn project_with_unwraps_only_in_test_files() -> Result<tempfile::TempDir> {
let dir = tempfile::tempdir()?;
let src = dir.path().join("src");
std::fs::create_dir_all(&src)?;
std::fs::write(
src.join("lib.rs"),
"/// Documented.\npub fn add(a: i32, b: i32) -> i32 {\n a + b\n}\n",
)?;
std::fs::write(
src.join("tests.rs"),
"use super::*;\nfn helper() -> String {\n std::env::var(\"HOME\").unwrap()\n}\n",
)?;
std::fs::write(
src.join("lang_analyzer_tests_part4.rs"),
"fn helper() {\n let v: Option<i32> = Some(1);\n let _ = v.unwrap();\n}\n",
)?;
Ok(dir)
}
fn tdg_config(path: &Path, out: PathBuf) -> TdgAnalysisConfig {
TdgAnalysisConfig {
path: path.to_path_buf(),
threshold: None,
top_files: Some(10),
format: TdgOutputFormat::Json,
include_components: false,
output: Some(out),
critical_only: false,
verbose: false,
}
}
#[test]
fn test_module_files_are_not_production_defect_sources() {
assert!(is_test_source(Path::new(
"src/services/repo_score/scorers/hygiene_scorer/tests.rs"
)));
assert!(is_test_source(Path::new(
"src/services/lang_analyzer_tests_part4.rs"
)));
assert!(is_test_source(Path::new("tests/integration.rs")));
assert!(is_test_source(Path::new("src/foo_tests.rs")));
assert!(!is_test_source(Path::new("src/cli/handlers/latest.rs")));
assert!(!is_test_source(Path::new("src/services/contest.rs")));
assert!(!is_test_source(Path::new("src/tdg/analyzer.rs")));
}
#[tokio::test]
async fn analyze_tdg_does_not_exit_nonzero_for_unwraps_in_test_files() -> Result<()> {
let dir = project_with_unwraps_only_in_test_files()?;
let out = dir.path().join("out.json");
handle_analyze_tdg(tdg_config(dir.path(), out.clone())).await?;
handle_analyze_tdg_gated(tdg_config(dir.path(), out)).await?;
Ok(())
}
#[tokio::test]
async fn a_production_unwrap_fails_the_gate_but_only_the_gate() -> Result<()> {
let dir = tempfile::tempdir()?;
let src = dir.path().join("src");
std::fs::create_dir_all(&src)?;
std::fs::write(
src.join("lib.rs"),
"pub fn first(v: &[i32]) -> i32 {\n *v.first().unwrap()\n}\n",
)?;
handle_analyze_tdg(tdg_config(dir.path(), dir.path().join("a.json"))).await?;
let err = handle_analyze_tdg_gated(tdg_config(dir.path(), dir.path().join("b.json")))
.await
.expect_err("build-tdg must still auto-fail on a production .unwrap()");
assert!(
err.to_string().contains("Critical defects"),
"unexpected error: {err}"
);
Ok(())
}
fn graded(path: &str, total: f32, grade: crate::tdg::Grade) -> crate::tdg::TdgScore {
crate::tdg::TdgScore {
total,
grade,
file_path: Some(PathBuf::from(path)),
..Default::default()
}
}
fn with_critical_defects(path: &str, defects: usize) -> crate::tdg::TdgScore {
let mut score = crate::tdg::TdgScore {
file_path: Some(PathBuf::from(path)),
has_critical_defects: defects > 0,
critical_defects_count: defects,
..Default::default()
};
score.calculate_total();
score
}
#[test]
fn critical_only_keeps_nothing_when_no_file_is_critical() {
let mut project = crate::tdg::ProjectScore::aggregate(vec![
graded("src/a.rs", 98.0, crate::tdg::Grade::APlus),
graded("src/b.rs", 91.0, crate::tdg::Grade::A),
]);
retain_critical_files(&mut project);
assert!(
project.files.is_empty(),
"no F-grade file exists, so --critical-only must report none"
);
assert_eq!(project.files_reported, 0);
assert!(project.files_truncated, "the subset must be disclosed");
assert_eq!(project.total_files, 2);
}
#[test]
fn critical_only_keeps_exactly_the_files_with_critical_defects() {
let mut project = crate::tdg::ProjectScore::aggregate(vec![
graded("src/a.rs", 98.0, crate::tdg::Grade::APlus),
with_critical_defects("src/bad.rs", 5),
graded("src/b.rs", 91.0, crate::tdg::Grade::A),
]);
retain_critical_files(&mut project);
assert_eq!(project.files.len(), 1);
assert_eq!(
project.files[0].file_path,
Some(PathBuf::from("src/bad.rs"))
);
assert_eq!(project.files_reported, 1);
assert_eq!(project.total_files, 3);
}
#[test]
fn critical_only_keeps_a_single_defect_file_that_does_not_grade_f() {
let one = with_critical_defects("src/u1.rs", 1);
assert!(
one.grade != crate::tdg::Grade::F,
"fixture precondition: one defect must NOT grade F, else this test \
cannot distinguish the two predicates (got {})",
one.grade
);
let mut project = crate::tdg::ProjectScore::aggregate(vec![
graded("src/clean.rs", 98.0, crate::tdg::Grade::APlus),
one,
with_critical_defects("src/u5.rs", 5),
]);
retain_critical_files(&mut project);
let kept: Vec<_> = project
.files
.iter()
.map(|f| f.file_path.clone().unwrap())
.collect();
assert!(
kept.contains(&PathBuf::from("src/u1.rs")),
"a file whose own record says has_critical_defects must survive \
--critical-only; kept {kept:?}"
);
assert_eq!(kept.len(), 2, "the clean file must not survive: {kept:?}");
}
#[test]
fn critical_only_disclosure_names_critical_only_not_top_files() {
let mut project = crate::tdg::ProjectScore::aggregate(vec![
graded("src/clean.rs", 98.0, crate::tdg::Grade::APlus),
with_critical_defects("src/u1.rs", 1),
]);
retain_critical_files(&mut project);
assert_eq!(project.list_filter.as_deref(), Some("--critical-only"));
let rendered = crate::tdg::formatters::format_project(&project);
assert!(
rendered.contains("(--critical-only)"),
"the disclosure must name the filter that applied: {rendered}"
);
assert!(
!rendered.contains("(--top-files)"),
"--top-files was not passed: {rendered}"
);
}
fn two_file_project() -> crate::tdg::ProjectScore {
let mut a = graded("src/a.rs", 98.0, crate::tdg::Grade::APlus);
a.structural_complexity = 24.0;
a.semantic_complexity = 19.5;
a.duplication_ratio = 20.0;
a.coupling_score = 14.5;
a.doc_coverage = 9.0;
a.consistency_score = 10.0;
crate::tdg::ProjectScore::aggregate(vec![a, graded("src/b.rs", 91.0, crate::tdg::Grade::A)])
}
#[test]
fn markdown_is_not_the_box_drawing_table() {
let project = two_file_project();
let root = Path::new("/tmp/x");
let table = format_project_result(&project, root, &TdgOutputFormat::Table, false).unwrap();
let markdown =
format_project_result(&project, root, &TdgOutputFormat::Markdown, false).unwrap();
assert_ne!(
table, markdown,
"-f markdown must not re-emit the -f table rendering"
);
assert!(
!markdown.contains('\u{2500}') && !markdown.contains('\u{2502}'),
"markdown must not contain box-drawing characters: {markdown}"
);
assert!(markdown.starts_with("# Project TDG Score Report"));
assert!(
markdown.contains("| File | Score | Grade |"),
"markdown must carry a real pipe table: {markdown}"
);
assert!(markdown.contains("`src/a.rs`"));
}
#[test]
fn include_components_changes_the_human_renderings() {
let project = two_file_project();
let root = Path::new("/tmp/x");
for format in [TdgOutputFormat::Table, TdgOutputFormat::Markdown] {
let without = format_project_result(&project, root, &format, false).unwrap();
let with = format_project_result(&project, root, &format, true).unwrap();
assert_ne!(
without, with,
"--include-components must change -f {format:?} output"
);
assert!(
!without.contains("24.0"),
"components must be absent without the flag: {without}"
);
assert!(
with.contains("24.0") && with.contains("19.5"),
"components must be present with the flag: {with}"
);
}
}
}