use crate::assertion_analyzer::TestAssertion;
use crate::types::{AssertionStrength, AuditResult, TestId};
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use walkdir::WalkDir;
#[derive(Debug)]
pub struct FalsePositiveDetector {
test_dirs: Vec<PathBuf>,
critical_paths: Vec<CriticalPath>,
}
impl FalsePositiveDetector {
pub fn new(test_dirs: Vec<PathBuf>) -> Self {
let mut detector = Self {
test_dirs,
critical_paths: Vec::new(),
};
detector.add_ggen_critical_paths();
detector
}
fn add_ggen_critical_paths(&mut self) {
self.critical_paths = vec![
CriticalPath {
name: "RDF Parsing".to_string(),
code_paths: vec!["crates/ggen-rdf/src/parser.rs".into()],
test_patterns: vec!["rdf_parser", "parse_triple", "parse_rdf"],
required_strength: AssertionStrength::Strong,
},
CriticalPath {
name: "Ontology Projection".to_string(),
code_paths: vec!["crates/ggen-ontology/src/projection.rs".into()],
test_patterns: vec!["ontology", "project", "transform"],
required_strength: AssertionStrength::Strong,
},
CriticalPath {
name: "Code Generation".to_string(),
code_paths: vec!["crates/ggen-core/src/generator.rs".into()],
test_patterns: vec!["generate", "codegen", "emit"],
required_strength: AssertionStrength::Strong,
},
CriticalPath {
name: "ggen.toml Configuration".to_string(),
code_paths: vec!["crates/ggen-config/src/toml.rs".into()],
test_patterns: vec!["ggen_toml", "config", "parse_toml"],
required_strength: AssertionStrength::Strong,
},
];
}
#[must_use]
pub fn detect_execution_only_tests(&self, assertions: &[TestAssertion]) -> Vec<FalsePositive> {
assertions
.iter()
.filter(|a| matches!(a.assertion_strength, AssertionStrength::Weak))
.map(|a| FalsePositive {
test_id: a.test_id.clone(),
file_path: a.file_path.clone(),
reason: FalsePositiveReason::ExecutionOnly,
severity: Severity::Warning,
recommendation: format!(
"Replace weak assertions (is_ok, is_some) with value assertions (assert_eq!) that verify actual behavior in {}",
a.test_id.as_str()
),
})
.collect()
}
pub fn analyze_ggen_toml_tests(&self) -> AuditResult<Vec<FalsePositive>> {
let mut false_positives = Vec::new();
for test_dir in &self.test_dirs {
for entry in WalkDir::new(test_dir)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.path().extension().and_then(|s| s.to_str()) == Some("rs"))
{
let content = std::fs::read_to_string(entry.path())?;
if content.contains("ggen.toml")
|| content.contains("config")
|| content.contains("toml")
{
if self.has_only_execution_checks(&content) {
let test_id =
TestId::new(format!("ggen_toml_test_{}", entry.path().display()))?;
false_positives.push(FalsePositive {
test_id,
file_path: entry.path().to_path_buf(),
reason: FalsePositiveReason::GgenTomlNotValidated,
severity: Severity::Critical,
recommendation: "Add assertions that verify parsed TOML values (assert_eq!(config.field, expected_value)) instead of just checking is_ok()".to_string(),
});
}
}
}
}
Ok(false_positives)
}
fn has_only_execution_checks(&self, content: &str) -> bool {
let has_weak = content.contains("is_ok()") || content.contains("is_some()");
let has_strong = content.contains("assert_eq!") || content.contains("assert_ne!");
has_weak && !has_strong
}
#[must_use]
pub fn identify_critical_path_gaps(
&self, assertions: &[TestAssertion],
) -> Vec<CriticalPathGap> {
let mut gaps = Vec::new();
for critical_path in &self.critical_paths {
let covering_tests: Vec<&TestAssertion> = assertions
.iter()
.filter(|a| {
let test_name = a.test_id.as_str().to_lowercase();
critical_path
.test_patterns
.iter()
.any(|pattern| test_name.contains(pattern))
})
.collect();
let has_strong = covering_tests
.iter()
.any(|t| matches!(t.assertion_strength, AssertionStrength::Strong));
if !has_strong {
gaps.push(CriticalPathGap {
critical_path_name: critical_path.name.clone(),
code_paths: critical_path.code_paths.clone(),
weak_test_count: covering_tests.len(),
required_strength: critical_path.required_strength,
recommendation: format!(
"Add strong assertions (assert_eq! with expected values) to {} tests",
critical_path.name
),
});
}
}
gaps
}
pub fn generate_report(
&self, assertions: &[TestAssertion],
) -> AuditResult<FalsePositiveReport> {
let execution_only = self.detect_execution_only_tests(assertions);
let ggen_toml_issues = self.analyze_ggen_toml_tests()?;
let critical_gaps = self.identify_critical_path_gaps(assertions);
Ok(FalsePositiveReport {
total_tests_analyzed: assertions.len(),
execution_only_tests: execution_only,
ggen_toml_issues,
critical_path_gaps: critical_gaps,
overall_severity: self.calculate_overall_severity(assertions),
})
}
fn calculate_overall_severity(&self, assertions: &[TestAssertion]) -> Severity {
let weak_count = assertions
.iter()
.filter(|a| matches!(a.assertion_strength, AssertionStrength::Weak))
.count();
let weak_percentage = weak_count as f64 / assertions.len() as f64;
if weak_percentage > 0.5 {
Severity::Critical
} else if weak_percentage > 0.25 {
Severity::Error
} else {
Severity::Warning
}
}
}
#[derive(Debug, Clone)]
pub struct CriticalPath {
pub name: String,
pub code_paths: Vec<PathBuf>,
pub test_patterns: Vec<&'static str>,
pub required_strength: AssertionStrength,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FalsePositive {
pub test_id: TestId,
pub file_path: PathBuf,
pub reason: FalsePositiveReason,
pub severity: Severity,
pub recommendation: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum FalsePositiveReason {
ExecutionOnly,
GgenTomlNotValidated,
CriticalPathGap,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum Severity {
Warning,
Error,
Critical,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CriticalPathGap {
pub critical_path_name: String,
pub code_paths: Vec<PathBuf>,
pub weak_test_count: usize,
pub required_strength: AssertionStrength,
pub recommendation: String,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct FalsePositiveReport {
pub total_tests_analyzed: usize,
pub execution_only_tests: Vec<FalsePositive>,
pub ggen_toml_issues: Vec<FalsePositive>,
pub critical_path_gaps: Vec<CriticalPathGap>,
pub overall_severity: Severity,
}