use crate::cli::analysis_utilities::{QualityGateResults, QualityViolation};
use crate::cli::{QualityCheckType, QualityGateOutputFormat};
use anyhow::Result;
use serde_json::json;
use std::collections::HashMap;
use std::path::Path;
pub fn format_single_file_output(
single_file: &Path,
results: &QualityGateResults,
violations: &[QualityViolation],
format: QualityGateOutputFormat,
) -> Result<String> {
match format {
QualityGateOutputFormat::Json => Ok(serde_json::to_string_pretty(&json!({
"file": single_file,
"passed": results.passed,
"results": results,
"violations": violations,
}))?),
QualityGateOutputFormat::Summary
| QualityGateOutputFormat::Markdown
| QualityGateOutputFormat::Detailed
| QualityGateOutputFormat::Human
| QualityGateOutputFormat::Junit => {
Ok(format_single_file_summary(single_file, results, violations))
}
}
}
#[must_use]
pub fn format_single_file_summary(
file_path: &Path,
results: &QualityGateResults,
violations: &[QualityViolation],
) -> String {
let mut output = String::new();
output.push_str(&format!(
"# Quality Gate Report: {}\n\n",
file_path.display()
));
if results.passed {
output.push_str("✅ **Quality Gate: PASSED**\n\n");
} else {
output.push_str("❌ **Quality Gate: FAILED**\n\n");
}
add_summary_section(&mut output, results);
if !violations.is_empty() {
add_violations_section(&mut output, violations);
}
output
}
fn add_summary_section(output: &mut String, results: &QualityGateResults) {
output.push_str("## Summary\n\n");
output.push_str(&format!(
"- Total Violations: {}\n",
results.total_violations
));
output.push_str(&format!(
"- Complexity Issues: {}\n",
results.complexity_violations
));
output.push_str(&format!("- Dead Code: {}\n", results.dead_code_violations));
output.push_str(&format!(
"- Technical Debt (SATD): {}\n",
results.satd_violations
));
output.push_str(&format!(
"- Security Issues: {}\n",
results.security_violations
));
}
fn add_violations_section(output: &mut String, violations: &[QualityViolation]) {
output.push_str("\n## Violations\n\n");
let mut by_type: HashMap<String, Vec<&QualityViolation>> = HashMap::new();
for violation in violations {
by_type
.entry(violation.check_type.clone())
.or_default()
.push(violation);
}
for (check_type, type_violations) in by_type {
output.push_str(&format!(
"### {} ({})\n\n",
check_type.to_uppercase(),
type_violations.len()
));
for violation in type_violations {
add_violation_entry(output, violation);
}
output.push('\n');
}
}
fn add_violation_entry(output: &mut String, violation: &QualityViolation) {
let severity_icon = get_severity_icon(&violation.severity);
if let Some(line) = violation.line {
output.push_str(&format!(
"- {} Line {}: {}\n",
severity_icon, line, violation.message
));
} else {
output.push_str(&format!("- {} {}\n", severity_icon, violation.message));
}
}
fn get_severity_icon(severity: &str) -> &'static str {
match severity {
"error" => "🔴",
"warning" => "🟡",
_ => "🟢",
}
}
pub fn print_checks_to_run(checks: &[QualityCheckType]) {
eprintln!("\n📋 Checks to run:");
if checks.contains(&QualityCheckType::All) {
print_all_checks();
} else {
print_specific_checks(checks);
}
eprintln!();
}
fn print_all_checks() {
eprintln!(" ✓ Complexity analysis");
eprintln!(" ✓ Dead code detection");
eprintln!(" ✓ Self-admitted technical debt (SATD)");
eprintln!(" ✓ Security vulnerabilities");
eprintln!(" ✓ Code entropy");
eprintln!(" ✓ Duplicate code");
eprintln!(" ✓ Test coverage");
}
fn print_specific_checks(checks: &[QualityCheckType]) {
for check in checks {
let check_name = match check {
QualityCheckType::Complexity => "✓ Complexity analysis",
QualityCheckType::DeadCode => "✓ Dead code detection",
QualityCheckType::Satd => "✓ Self-admitted technical debt (SATD)",
QualityCheckType::Security => "✓ Security vulnerabilities",
QualityCheckType::Entropy => "✓ Code entropy",
QualityCheckType::Duplicates => "✓ Duplicate code",
QualityCheckType::Coverage => "✓ Test coverage",
_ => continue, };
eprintln!(" {check_name}");
}
}
#[derive(Debug, Clone)]
pub struct QualityCheckConfig<'a> {
pub project_path: &'a Path,
pub checks: &'a [QualityCheckType],
pub max_dead_code: f64,
pub min_entropy: f64,
pub max_complexity_p99: u32,
pub perf: bool,
}
pub async fn run_project_checks(
config: QualityCheckConfig<'_>,
violations: &mut Vec<QualityViolation>,
results: &mut QualityGateResults,
) -> Result<()> {
if config.checks.contains(&QualityCheckType::All) {
run_all_checks(
config.project_path,
config.max_dead_code,
config.min_entropy,
config.max_complexity_p99,
violations,
results,
config.perf,
)
.await?;
} else {
let checks_config = IndividualChecksConfig {
checks: config.checks,
project_path: config.project_path,
max_dead_code: config.max_dead_code,
min_entropy: config.min_entropy,
max_complexity_p99: config.max_complexity_p99,
perf: config.perf,
};
run_individual_checks(checks_config, violations, results).await?;
}
Ok(())
}
async fn run_all_checks(
project_path: &Path,
max_dead_code: f64,
min_entropy: f64,
max_complexity_p99: u32,
violations: &mut Vec<QualityViolation>,
results: &mut QualityGateResults,
perf: bool,
) -> Result<()> {
crate::cli::analysis_utilities::run_single_project_check(
&QualityCheckType::All,
project_path,
max_dead_code,
min_entropy,
max_complexity_p99,
violations,
results,
perf,
)
.await
}
struct IndividualChecksConfig<'a> {
checks: &'a [QualityCheckType],
project_path: &'a Path,
max_dead_code: f64,
min_entropy: f64,
max_complexity_p99: u32,
perf: bool,
}
async fn run_individual_checks(
config: IndividualChecksConfig<'_>,
violations: &mut Vec<QualityViolation>,
results: &mut QualityGateResults,
) -> Result<()> {
use std::time::Instant;
for check in config.checks {
let check_start = if config.perf {
Some(Instant::now())
} else {
None
};
crate::cli::analysis_utilities::run_single_project_check(
check,
config.project_path,
config.max_dead_code,
config.min_entropy,
config.max_complexity_p99,
violations,
results,
config.perf,
)
.await?;
if let Some(start) = check_start {
print_check_timing(check, start.elapsed().as_secs_f64());
}
}
Ok(())
}
fn print_check_timing(check: &QualityCheckType, elapsed_secs: f64) {
let check_name = match check {
QualityCheckType::Complexity => "Complexity",
QualityCheckType::DeadCode => "Dead code",
QualityCheckType::Satd => "SATD",
QualityCheckType::Security => "Security",
QualityCheckType::Entropy => "Entropy",
QualityCheckType::Duplicates => "Duplicates",
QualityCheckType::Coverage => "Coverage",
QualityCheckType::Sections => "Sections",
QualityCheckType::Provability => "Provability",
QualityCheckType::All => "All",
};
eprintln!(" ⏱️ {check_name} check: {elapsed_secs:.3}s");
}
pub fn format_qg_as_junit(violations: &[QualityViolation]) -> Result<String> {
use std::fmt::Write;
let mut output = String::new();
writeln!(&mut output, r#"<?xml version="1.0" encoding="UTF-8"?>"#)?;
writeln!(&mut output, r#"<testsuites name="Quality Gate">"#)?;
writeln!(
&mut output,
r#" <testsuite name="Quality Checks" tests="{}" failures="{}">"#,
violations.len(),
violations.len()
)?;
for violation in violations {
write_junit_test_case(&mut output, violation)?;
}
writeln!(&mut output, r" </testsuite>")?;
writeln!(&mut output, r"</testsuites>")?;
Ok(output)
}
fn write_junit_test_case(
output: &mut String,
violation: &QualityViolation,
) -> Result<(), std::fmt::Error> {
use std::fmt::Write;
writeln!(
output,
r#" <testcase name="{}" classname="{}">"#,
violation.message, violation.check_type
)?;
writeln!(
output,
r#" <failure message="{}" type="{}"/>"#,
violation.message, violation.severity
)?;
writeln!(output, r" </testcase>")?;
Ok(())
}
pub fn format_project_output(
results: &QualityGateResults,
violations: &[QualityViolation],
format: QualityGateOutputFormat,
) -> Result<String> {
match format {
QualityGateOutputFormat::Json => Ok(serde_json::to_string_pretty(&json!({
"passed": results.passed,
"results": results,
"violations": violations,
}))?),
QualityGateOutputFormat::Summary
| QualityGateOutputFormat::Markdown
| QualityGateOutputFormat::Detailed
| QualityGateOutputFormat::Human => Ok(format_project_summary(results, violations)),
QualityGateOutputFormat::Junit => format_qg_as_junit(violations),
}
}
fn format_project_summary(results: &QualityGateResults, violations: &[QualityViolation]) -> String {
let mut output = String::new();
output.push_str("# Quality Gate Results\n\n");
if results.passed {
output.push_str("## ✅ PASSED\n\n");
} else {
output.push_str("## ❌ FAILED\n\n");
}
output.push_str(&format!(
"Total violations: {}\n\n",
results.total_violations
));
if violations.is_empty() {
output.push_str("No violations found.\n");
} else {
output.push_str("## Violations by Type\n\n");
let grouped = group_violations_by_type(violations);
output.push_str(&format_violations_markdown(&grouped));
}
output
}
fn group_violations_by_type(
violations: &[QualityViolation],
) -> HashMap<String, Vec<&QualityViolation>> {
let mut grouped = HashMap::new();
for violation in violations {
grouped
.entry(violation.check_type.clone())
.or_insert_with(Vec::new)
.push(violation);
}
grouped
}
fn format_violations_markdown(grouped: &HashMap<String, Vec<&QualityViolation>>) -> String {
let mut output = String::new();
for (check_type, violations) in grouped {
output.push_str(&format!(
"### {} ({} violation{})\n\n",
check_type,
violations.len(),
if violations.len() == 1 { "" } else { "s" }
));
for violation in violations {
if let Some(line) = violation.line {
output.push_str(&format!(
"- **{}:{}**: {}\n",
violation.file, line, violation.message
));
} else {
output.push_str(&format!(
"- **{}**: {}\n",
violation.file, violation.message
));
}
}
output.push('\n');
}
output
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_results(passed: bool, total: usize) -> QualityGateResults {
QualityGateResults {
passed,
total_violations: total,
complexity_violations: total / 3,
dead_code_violations: total / 3,
satd_violations: total / 3,
entropy_violations: 0,
security_violations: 0,
duplicate_violations: 0,
coverage_violations: 0,
section_violations: 0,
provability_violations: 0,
provability_score: None,
violations: vec![],
}
}
fn create_test_violations() -> Vec<QualityViolation> {
vec![
QualityViolation {
check_type: "Complexity".to_string(),
severity: "high".to_string(),
file: "test.rs".to_string(),
line: Some(42),
message: "Function complexity exceeds threshold".to_string(),
},
QualityViolation {
check_type: "SATD".to_string(),
severity: "medium".to_string(),
file: "test.rs".to_string(),
line: Some(100),
message: "TODO comment found".to_string(),
},
]
}
#[test]
fn test_format_single_file_output_json() {
let file_path = PathBuf::from("test.rs");
let results = create_test_results(false, 6);
let violations = create_test_violations();
let output = format_single_file_output(
&file_path,
&results,
&violations,
QualityGateOutputFormat::Json,
)
.unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["passed"], false);
assert_eq!(parsed["file"], "test.rs");
assert!(parsed["violations"].is_array());
}
#[test]
fn test_format_single_file_output_summary() {
let file_path = PathBuf::from("test.rs");
let results = create_test_results(true, 0);
let violations = vec![];
let output = format_single_file_output(
&file_path,
&results,
&violations,
QualityGateOutputFormat::Summary,
)
.unwrap();
assert!(output.contains("Quality Gate Results"));
assert!(output.contains("✅ PASSED"));
assert!(output.contains("test.rs"));
}
#[test]
fn test_format_project_output_json() {
let results = create_test_results(false, 9);
let violations = create_test_violations();
let output =
format_project_output(&results, &violations, QualityGateOutputFormat::Json).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&output).unwrap();
assert_eq!(parsed["passed"], false);
assert_eq!(parsed["total_violations"], 9);
}
#[test]
fn test_format_project_output_summary() {
let results = create_test_results(true, 0);
let violations = vec![];
let output =
format_project_output(&results, &violations, QualityGateOutputFormat::Summary).unwrap();
assert!(output.contains("Quality Gate Results"));
assert!(output.contains("✅ PASSED"));
assert!(output.contains("Total violations: 0"));
}
#[test]
fn test_format_qg_as_junit() {
let violations = create_test_violations();
let output = format_qg_as_junit(&violations).unwrap();
assert!(output.contains("<?xml version"));
assert!(output.contains("<testsuites"));
assert!(output.contains("<testsuite"));
assert!(output.contains("<testcase"));
assert!(output.contains("<failure"));
assert!(output.contains("failures=\"2\""));
}
#[test]
fn test_format_qg_as_junit_empty() {
let violations = vec![];
let output = format_qg_as_junit(&violations).unwrap();
assert!(output.contains("failures=\"0\""));
assert!(!output.contains("<testcase"));
}
#[test]
fn test_group_violations_by_type() {
let violations = vec![
QualityViolation {
check_type: "Complexity".to_string(),
severity: "high".to_string(),
file: "a.rs".to_string(),
line: Some(1),
message: "Complex function".to_string(),
},
QualityViolation {
check_type: "Complexity".to_string(),
severity: "high".to_string(),
file: "b.rs".to_string(),
line: Some(2),
message: "Another complex function".to_string(),
},
QualityViolation {
check_type: "SATD".to_string(),
severity: "medium".to_string(),
file: "c.rs".to_string(),
line: Some(3),
message: "TODO found".to_string(),
},
];
let grouped = group_violations_by_type(&violations);
assert_eq!(grouped.len(), 2);
assert_eq!(grouped.get("Complexity").unwrap().len(), 2);
assert_eq!(grouped.get("SATD").unwrap().len(), 1);
}
#[test]
fn test_format_single_file_summary() {
let file_path = PathBuf::from("src/main.rs");
let results = create_test_results(false, 3);
let violations = create_test_violations();
let output = format_single_file_summary(&file_path, &results, &violations);
assert!(output.contains("Quality Gate Results for src/main.rs"));
assert!(output.contains("❌ FAILED"));
assert!(output.contains("Total violations: 3"));
assert!(output.contains("## Violations by Type"));
}
#[test]
fn test_format_project_summary() {
let results = create_test_results(true, 0);
let violations = vec![];
let output = format_project_summary(&results, &violations);
assert!(output.contains("Quality Gate Results"));
assert!(output.contains("✅ PASSED"));
assert!(output.contains("No violations found"));
}
#[test]
fn test_format_violations_markdown() {
let violations = create_test_violations();
let grouped = group_violations_by_type(&violations);
let output = format_violations_markdown(&grouped);
assert!(output.contains("### Complexity (1 violation)"));
assert!(output.contains("### SATD (1 violation)"));
assert!(output.contains("**test.rs:42**"));
assert!(output.contains("Function complexity exceeds threshold"));
}
#[test]
fn test_print_check_timing() {
print_check_timing(&QualityCheckType::Complexity, 1.234);
print_check_timing(&QualityCheckType::DeadCode, 0.567);
print_check_timing(&QualityCheckType::All, 5.678);
}
#[test]
fn test_write_junit_test_case() {
let mut output = String::new();
let violation = QualityViolation {
check_type: "Complexity".to_string(),
severity: "high".to_string(),
file: "test.rs".to_string(),
line: Some(42),
message: "Function too complex".to_string(),
};
write_junit_test_case(&mut output, &violation).unwrap();
assert!(output.contains("<testcase"));
assert!(output.contains("name=\"Function too complex\""));
assert!(output.contains("classname=\"Complexity\""));
assert!(output.contains("<failure"));
assert!(output.contains("type=\"high\""));
}
}
#[cfg(test)]
mod property_tests {
use proptest::prelude::*;
proptest! {
#[test]
fn basic_property_stability(_input in ".*") {
prop_assert!(true);
}
#[test]
fn module_consistency_check(_x in 0u32..1000) {
prop_assert!(_x < 1001);
}
}
}