1use std::collections::HashMap;
11use std::path::{Path, PathBuf};
12
13use super::types::{DefectCategory, Finding, FindingEvidence, FindingSeverity, HuntMode};
14use crate::tools;
15
16#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
21pub struct PmatFunctionInfo {
22 pub file_path: String,
23 pub function_name: String,
24 #[serde(default)]
25 pub start_line: usize,
26 #[serde(default)]
27 pub end_line: usize,
28 #[serde(default)]
29 pub tdg_score: f64,
30 #[serde(default)]
31 pub tdg_grade: String,
32 #[serde(default)]
33 pub complexity: u32,
34 #[serde(default)]
35 pub satd_count: u32,
36}
37
38pub type PmatQualityIndex = HashMap<PathBuf, Vec<PmatFunctionInfo>>;
40
41pub fn pmat_available() -> bool {
47 tools::ToolRegistry::detect().pmat.is_some()
48}
49
50fn run_pmat_query_raw(
56 project_path: &Path,
57 query: &str,
58 limit: usize,
59) -> Option<Vec<PmatFunctionInfo>> {
60 let limit_str = limit.to_string();
61 let args: Vec<&str> = vec!["query", query, "--format", "json", "--limit", &limit_str];
62 let output = tools::run_tool("pmat", &args, Some(project_path)).ok()?;
63 let results: Vec<PmatFunctionInfo> = serde_json::from_str(&output).ok()?;
64 Some(results)
65}
66
67pub fn build_quality_index(
71 project_path: &Path,
72 query: &str,
73 limit: usize,
74) -> Option<PmatQualityIndex> {
75 if !pmat_available() {
76 return None;
77 }
78
79 let results = run_pmat_query_raw(project_path, query, limit)?;
80 if results.is_empty() {
81 return None;
82 }
83
84 Some(index_from_results(results))
85}
86
87pub fn index_from_results(results: Vec<PmatFunctionInfo>) -> PmatQualityIndex {
89 let mut index: PmatQualityIndex = HashMap::new();
90 for result in results {
91 let path = PathBuf::from(&result.file_path);
92 index.entry(path).or_default().push(result);
93 }
94 for functions in index.values_mut() {
96 functions.sort_by_key(|f: &PmatFunctionInfo| f.start_line);
97 }
98 index
99}
100
101pub fn lookup_quality<'a>(
106 index: &'a PmatQualityIndex,
107 file: &Path,
108 line: usize,
109) -> Option<&'a PmatFunctionInfo> {
110 let functions = index.get(file)?;
111
112 if let Some(f) = functions.iter().find(|f| line >= f.start_line && line <= f.end_line) {
114 return Some(f);
115 }
116
117 functions.iter().min_by_key(|f| (f.start_line as isize - line as isize).unsigned_abs())
119}
120
121pub fn quality_adjusted_suspiciousness(base: f64, tdg_score: f64, weight: f64) -> f64 {
132 let quality_factor = 0.5 - (tdg_score / 100.0);
133 (base * (1.0 + weight * quality_factor)).clamp(0.0, 1.0)
134}
135
136pub fn apply_quality_weights(findings: &mut [Finding], index: &PmatQualityIndex, weight: f64) {
138 for finding in findings.iter_mut() {
139 if let Some(pmat) = lookup_quality(index, &finding.file, finding.line) {
140 let original = finding.suspiciousness;
141 finding.suspiciousness =
142 quality_adjusted_suspiciousness(original, pmat.tdg_score, weight);
143 finding.evidence.push(FindingEvidence::quality_metrics(
144 &pmat.tdg_grade,
145 pmat.tdg_score,
146 pmat.complexity,
147 ));
148 }
149 }
150}
151
152pub fn compute_regression_risk(pmat: &PmatFunctionInfo) -> f64 {
162 let tdg_factor = 1.0 - (pmat.tdg_score / 100.0);
163 let cx_factor = (pmat.complexity as f64 / 50.0).min(1.0);
164 let satd_factor = (pmat.satd_count as f64 / 5.0).min(1.0);
165 let risk: f64 = 0.5 * tdg_factor + 0.3 * cx_factor + 0.2 * satd_factor;
166 risk.clamp(0.0, 1.0)
167}
168
169pub fn apply_regression_risk(findings: &mut [Finding], index: &PmatQualityIndex) {
171 for finding in findings.iter_mut() {
172 if let Some(pmat) = lookup_quality(index, &finding.file, finding.line) {
173 finding.regression_risk = Some(compute_regression_risk(pmat));
174 }
175 }
176}
177
178pub fn scope_targets_by_quality(
184 project_path: &Path,
185 query: &str,
186 limit: usize,
187) -> Option<Vec<PathBuf>> {
188 if !pmat_available() {
189 return None;
190 }
191
192 let results = run_pmat_query_raw(project_path, query, limit)?;
193 if results.is_empty() {
194 return None;
195 }
196
197 let mut file_scores: HashMap<PathBuf, (f64, usize)> = HashMap::new();
199 for r in &results {
200 let path = PathBuf::from(&r.file_path);
201 let entry = file_scores.entry(path).or_insert((0.0, 0));
202 entry.0 += r.tdg_score;
203 entry.1 += 1;
204 }
205
206 let mut files: Vec<(PathBuf, f64)> =
208 file_scores.into_iter().map(|(path, (sum, count))| (path, sum / count as f64)).collect();
209 files.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
210
211 Some(files.into_iter().map(|(path, _)| path).collect())
212}
213
214pub fn generate_satd_findings(project_path: &Path, index: &PmatQualityIndex) -> Vec<Finding> {
220 let _ = project_path;
222 let mut findings = Vec::new();
223 let mut id_counter = 0u32;
224
225 for (file, functions) in index {
226 for func in functions {
227 if func.satd_count > 0 {
228 id_counter += 1;
229 let severity = match func.satd_count {
230 1 => FindingSeverity::Low,
231 2..=3 => FindingSeverity::Medium,
232 _ => FindingSeverity::High,
233 };
234 let suspiciousness = match func.satd_count {
235 1 => 0.3,
236 2..=3 => 0.5,
237 _ => 0.7,
238 };
239 findings.push(
240 Finding::new(
241 format!("BH-SATD-{:04}", id_counter),
242 file,
243 func.start_line,
244 format!(
245 "PMAT: {} SATD markers in `{}`",
246 func.satd_count, func.function_name
247 ),
248 )
249 .with_description(format!(
250 "Function `{}` (grade {}, complexity {}) has {} self-admitted technical debt markers",
251 func.function_name, func.tdg_grade, func.complexity, func.satd_count
252 ))
253 .with_severity(severity)
254 .with_category(DefectCategory::LogicErrors)
255 .with_suspiciousness(suspiciousness)
256 .with_discovered_by(HuntMode::Analyze)
257 .with_evidence(FindingEvidence::quality_metrics(
258 &func.tdg_grade,
259 func.tdg_score,
260 func.complexity,
261 )),
262 );
263 }
264 }
265 }
266
267 findings
268}
269
270#[cfg(test)]
275mod tests {
276 use super::*;
277
278 #[test]
283 fn test_pmat_available_returns_bool() {
284 let _ = pmat_available();
285 }
286
287 #[test]
292 fn test_quality_adjusted_high_quality_reduces() {
293 let adjusted = quality_adjusted_suspiciousness(0.5, 95.0, 0.5);
294 assert!(adjusted < 0.5, "High quality should reduce suspiciousness: got {}", adjusted);
295 }
296
297 #[test]
298 fn test_quality_adjusted_low_quality_boosts() {
299 let adjusted = quality_adjusted_suspiciousness(0.5, 20.0, 0.5);
300 assert!(adjusted > 0.5, "Low quality should boost suspiciousness: got {}", adjusted);
301 }
302
303 #[test]
304 fn test_quality_adjusted_baseline_no_change() {
305 let adjusted = quality_adjusted_suspiciousness(0.5, 50.0, 0.5);
307 assert!((adjusted - 0.5).abs() < 0.001);
308 }
309
310 #[test]
311 fn test_quality_adjusted_clamping() {
312 let adjusted = quality_adjusted_suspiciousness(0.9, 10.0, 1.0);
313 assert!(adjusted <= 1.0, "Should clamp to 1.0: got {}", adjusted);
314
315 let adjusted = quality_adjusted_suspiciousness(0.0, 10.0, 1.0);
316 assert!((adjusted - 0.0).abs() < 0.001);
317 }
318
319 #[test]
320 fn test_quality_adjusted_weight_zero() {
321 let adjusted = quality_adjusted_suspiciousness(0.5, 20.0, 0.0);
322 assert!((adjusted - 0.5).abs() < 0.001);
323 }
324
325 #[test]
330 fn test_index_from_results_groups_by_file() {
331 let results = vec![
332 PmatFunctionInfo {
333 file_path: "src/lib.rs".into(),
334 function_name: "alpha".into(),
335 start_line: 10,
336 end_line: 20,
337 tdg_score: 80.0,
338 tdg_grade: "B".into(),
339 complexity: 5,
340 satd_count: 0,
341 },
342 PmatFunctionInfo {
343 file_path: "src/lib.rs".into(),
344 function_name: "beta".into(),
345 start_line: 30,
346 end_line: 50,
347 tdg_score: 60.0,
348 tdg_grade: "C".into(),
349 complexity: 12,
350 satd_count: 2,
351 },
352 PmatFunctionInfo {
353 file_path: "src/main.rs".into(),
354 function_name: "main".into(),
355 start_line: 1,
356 end_line: 10,
357 tdg_score: 95.0,
358 tdg_grade: "A".into(),
359 complexity: 2,
360 satd_count: 0,
361 },
362 ];
363
364 let index = index_from_results(results);
365 assert_eq!(index.len(), 2);
366 assert_eq!(index[&PathBuf::from("src/lib.rs")].len(), 2);
367 assert_eq!(index[&PathBuf::from("src/main.rs")].len(), 1);
368 let lib_fns = &index[&PathBuf::from("src/lib.rs")];
369 assert!(lib_fns[0].start_line < lib_fns[1].start_line);
370 }
371
372 #[test]
377 fn test_lookup_quality_exact_span() {
378 let results = vec![PmatFunctionInfo {
379 file_path: "src/lib.rs".into(),
380 function_name: "process".into(),
381 start_line: 10,
382 end_line: 30,
383 tdg_score: 75.0,
384 tdg_grade: "C".into(),
385 complexity: 0,
386 satd_count: 0,
387 }];
388 let index = index_from_results(results);
389
390 let result = lookup_quality(&index, Path::new("src/lib.rs"), 20);
391 assert!(result.is_some());
392 assert_eq!(result.expect("operation failed").function_name, "process");
393 }
394
395 #[test]
396 fn test_lookup_quality_nearest_fallback() {
397 let results = vec![
398 PmatFunctionInfo {
399 file_path: "src/lib.rs".into(),
400 function_name: "alpha".into(),
401 start_line: 10,
402 end_line: 20,
403 tdg_score: 0.0,
404 tdg_grade: String::new(),
405 complexity: 0,
406 satd_count: 0,
407 },
408 PmatFunctionInfo {
409 file_path: "src/lib.rs".into(),
410 function_name: "beta".into(),
411 start_line: 50,
412 end_line: 70,
413 tdg_score: 0.0,
414 tdg_grade: String::new(),
415 complexity: 0,
416 satd_count: 0,
417 },
418 ];
419 let index = index_from_results(results);
420
421 let result = lookup_quality(&index, Path::new("src/lib.rs"), 35);
422 assert!(result.is_some());
423 }
424
425 #[test]
426 fn test_lookup_quality_no_file() {
427 let index: PmatQualityIndex = HashMap::new();
428 let result = lookup_quality(&index, Path::new("nonexistent.rs"), 10);
429 assert!(result.is_none());
430 }
431
432 #[test]
437 fn test_regression_risk_high_quality() {
438 let pmat = PmatFunctionInfo {
439 file_path: String::new(),
440 function_name: String::new(),
441 start_line: 0,
442 end_line: 0,
443 tdg_score: 95.0,
444 tdg_grade: "A".into(),
445 complexity: 3,
446 satd_count: 0,
447 };
448 let risk = compute_regression_risk(&pmat);
449 assert!(risk < 0.1, "High quality should have low risk: got {}", risk);
450 }
451
452 #[test]
453 fn test_regression_risk_low_quality() {
454 let pmat = PmatFunctionInfo {
455 file_path: String::new(),
456 function_name: String::new(),
457 start_line: 0,
458 end_line: 0,
459 tdg_score: 20.0,
460 tdg_grade: "F".into(),
461 complexity: 40,
462 satd_count: 8,
463 };
464 let risk = compute_regression_risk(&pmat);
465 assert!(risk > 0.7, "Low quality should have high risk: got {}", risk);
466 }
467
468 #[test]
469 fn test_regression_risk_clamped() {
470 let pmat = PmatFunctionInfo {
471 file_path: String::new(),
472 function_name: String::new(),
473 start_line: 0,
474 end_line: 0,
475 tdg_score: 0.0,
476 tdg_grade: "F".into(),
477 complexity: 100,
478 satd_count: 20,
479 };
480 let risk = compute_regression_risk(&pmat);
481 assert!(risk <= 1.0);
482 assert!(risk >= 0.0);
483 }
484
485 #[test]
490 fn test_generate_satd_findings_produces_findings() {
491 let results = vec![
492 PmatFunctionInfo {
493 file_path: "src/lib.rs".into(),
494 function_name: "messy_fn".into(),
495 start_line: 10,
496 end_line: 50,
497 tdg_score: 40.0,
498 tdg_grade: "D".into(),
499 complexity: 15,
500 satd_count: 3,
501 },
502 PmatFunctionInfo {
503 file_path: "src/lib.rs".into(),
504 function_name: "clean_fn".into(),
505 start_line: 60,
506 end_line: 70,
507 tdg_score: 95.0,
508 tdg_grade: "A".into(),
509 complexity: 2,
510 satd_count: 0,
511 },
512 ];
513 let index = index_from_results(results);
514 let findings = generate_satd_findings(Path::new("."), &index);
515
516 assert_eq!(findings.len(), 1, "Should only generate for functions with SATD");
517 assert!(findings[0].id.starts_with("BH-SATD-"));
518 assert!(findings[0].title.contains("messy_fn"));
519 assert_eq!(findings[0].severity, FindingSeverity::Medium);
520 }
521
522 #[test]
523 fn test_generate_satd_findings_severity_scaling() {
524 let results = vec![
525 PmatFunctionInfo {
526 file_path: "a.rs".into(),
527 function_name: "low".into(),
528 start_line: 1,
529 end_line: 10,
530 tdg_score: 0.0,
531 tdg_grade: String::new(),
532 complexity: 0,
533 satd_count: 1,
534 },
535 PmatFunctionInfo {
536 file_path: "b.rs".into(),
537 function_name: "high".into(),
538 start_line: 1,
539 end_line: 10,
540 tdg_score: 0.0,
541 tdg_grade: String::new(),
542 complexity: 0,
543 satd_count: 5,
544 },
545 ];
546 let index = index_from_results(results);
547 let findings = generate_satd_findings(Path::new("."), &index);
548
549 assert_eq!(findings.len(), 2);
550 let low_finding =
551 findings.iter().find(|f| f.title.contains("low")).expect("element not found");
552 let high_finding =
553 findings.iter().find(|f| f.title.contains("high")).expect("element not found");
554 assert_eq!(low_finding.severity, FindingSeverity::Low);
555 assert_eq!(high_finding.severity, FindingSeverity::High);
556 }
557
558 #[test]
559 fn test_generate_satd_findings_empty_index() {
560 let index: PmatQualityIndex = HashMap::new();
561 let findings = generate_satd_findings(Path::new("."), &index);
562 assert!(findings.is_empty());
563 }
564
565 #[test]
570 fn test_apply_quality_weights_adjusts_scores() {
571 let results = vec![PmatFunctionInfo {
572 file_path: "src/lib.rs".into(),
573 function_name: "buggy".into(),
574 start_line: 10,
575 end_line: 30,
576 tdg_score: 30.0,
577 tdg_grade: "D".into(),
578 complexity: 20,
579 satd_count: 0,
580 }];
581 let index = index_from_results(results);
582
583 let mut findings =
584 vec![Finding::new("F-001", "src/lib.rs", 15, "Test finding").with_suspiciousness(0.5)];
585
586 apply_quality_weights(&mut findings, &index, 0.5);
587
588 assert!(findings[0].suspiciousness > 0.5);
589 assert!(findings[0].evidence.iter().any(|e| matches!(
590 e.evidence_type,
591 crate::bug_hunter::types::EvidenceKind::QualityMetrics
592 )));
593 }
594
595 #[test]
600 fn test_apply_regression_risk_sets_risk() {
601 let results = vec![PmatFunctionInfo {
602 file_path: "src/lib.rs".into(),
603 function_name: "risky".into(),
604 start_line: 10,
605 end_line: 30,
606 tdg_score: 30.0,
607 tdg_grade: "D".into(),
608 complexity: 25,
609 satd_count: 4,
610 }];
611 let index = index_from_results(results);
612
613 let mut findings = vec![Finding::new("F-001", "src/lib.rs", 15, "Test finding")];
614
615 apply_regression_risk(&mut findings, &index);
616 assert!(findings[0].regression_risk.is_some());
617 assert!(findings[0].regression_risk.expect("unexpected failure") > 0.3);
618 }
619
620 #[test]
625 fn test_quality_adjusted_tdg_zero() {
626 let adjusted = quality_adjusted_suspiciousness(0.5, 0.0, 1.0);
628 assert!((adjusted - 0.75).abs() < 0.001);
630 }
631
632 #[test]
633 fn test_quality_adjusted_tdg_100() {
634 let adjusted = quality_adjusted_suspiciousness(0.5, 100.0, 1.0);
636 assert!((adjusted - 0.25).abs() < 0.001);
638 }
639
640 #[test]
641 fn test_quality_adjusted_tdg_over_100() {
642 let adjusted = quality_adjusted_suspiciousness(0.5, 150.0, 1.0);
645 assert!((adjusted - 0.0).abs() < 0.001);
646 }
647
648 #[test]
649 fn test_quality_adjusted_negative_tdg() {
650 let adjusted = quality_adjusted_suspiciousness(0.5, -50.0, 1.0);
653 assert!((adjusted - 1.0).abs() < 0.001);
654 }
655
656 #[test]
657 fn test_quality_adjusted_base_one() {
658 let adjusted = quality_adjusted_suspiciousness(1.0, 0.0, 1.0);
660 assert!((adjusted - 1.0).abs() < 0.001);
662 }
663
664 #[test]
665 fn test_quality_adjusted_large_weight() {
666 let adjusted = quality_adjusted_suspiciousness(0.5, 20.0, 5.0);
667 assert!((adjusted - 1.0).abs() < 0.001);
670 }
671
672 #[test]
673 fn test_quality_adjusted_negative_weight() {
674 let adjusted = quality_adjusted_suspiciousness(0.5, 20.0, -1.0);
676 assert!((adjusted - 0.35).abs() < 0.001);
679 }
680
681 #[test]
686 fn test_regression_risk_tdg_exactly_100() {
687 let pmat = PmatFunctionInfo {
688 file_path: String::new(),
689 function_name: String::new(),
690 start_line: 0,
691 end_line: 0,
692 tdg_score: 100.0,
693 tdg_grade: "A+".into(),
694 complexity: 0,
695 satd_count: 0,
696 };
697 let risk = compute_regression_risk(&pmat);
698 assert!((risk - 0.0).abs() < 0.001);
700 }
701
702 #[test]
703 fn test_regression_risk_tdg_zero_max_complexity_max_satd() {
704 let pmat = PmatFunctionInfo {
705 file_path: String::new(),
706 function_name: String::new(),
707 start_line: 0,
708 end_line: 0,
709 tdg_score: 0.0,
710 tdg_grade: "F".into(),
711 complexity: 100,
712 satd_count: 100,
713 };
714 let risk = compute_regression_risk(&pmat);
715 assert!((risk - 1.0).abs() < 0.001);
717 }
718
719 #[test]
720 fn test_regression_risk_complexity_at_threshold() {
721 let pmat = PmatFunctionInfo {
722 file_path: String::new(),
723 function_name: String::new(),
724 start_line: 0,
725 end_line: 0,
726 tdg_score: 50.0,
727 tdg_grade: "C".into(),
728 complexity: 50,
729 satd_count: 5,
730 };
731 let risk = compute_regression_risk(&pmat);
732 assert!((risk - 0.75).abs() < 0.001);
734 }
735
736 #[test]
737 fn test_regression_risk_complexity_below_threshold() {
738 let pmat = PmatFunctionInfo {
739 file_path: String::new(),
740 function_name: String::new(),
741 start_line: 0,
742 end_line: 0,
743 tdg_score: 50.0,
744 tdg_grade: "C".into(),
745 complexity: 25,
746 satd_count: 0,
747 };
748 let risk = compute_regression_risk(&pmat);
749 assert!((risk - 0.40).abs() < 0.001);
751 }
752
753 #[test]
758 fn test_apply_quality_weights_no_match() {
759 let index: PmatQualityIndex = HashMap::new();
760 let mut findings =
761 vec![Finding::new("F-001", "nonexistent.rs", 10, "Test").with_suspiciousness(0.5)];
762
763 apply_quality_weights(&mut findings, &index, 0.5);
764
765 assert!((findings[0].suspiciousness - 0.5).abs() < 0.001);
767 assert!(findings[0].evidence.is_empty());
768 }
769
770 #[test]
771 fn test_apply_quality_weights_empty_findings() {
772 let results = vec![PmatFunctionInfo {
773 file_path: "src/lib.rs".into(),
774 function_name: "f".into(),
775 start_line: 1,
776 end_line: 10,
777 tdg_score: 50.0,
778 tdg_grade: "C".into(),
779 complexity: 5,
780 satd_count: 0,
781 }];
782 let index = index_from_results(results);
783 let mut findings: Vec<Finding> = vec![];
784 apply_quality_weights(&mut findings, &index, 0.5);
785 assert!(findings.is_empty());
786 }
787
788 #[test]
789 fn test_apply_quality_weights_multiple_findings() {
790 let results = vec![
791 PmatFunctionInfo {
792 file_path: "src/lib.rs".into(),
793 function_name: "f1".into(),
794 start_line: 1,
795 end_line: 20,
796 tdg_score: 20.0,
797 tdg_grade: "F".into(),
798 complexity: 30,
799 satd_count: 0,
800 },
801 PmatFunctionInfo {
802 file_path: "src/lib.rs".into(),
803 function_name: "f2".into(),
804 start_line: 30,
805 end_line: 50,
806 tdg_score: 90.0,
807 tdg_grade: "A".into(),
808 complexity: 2,
809 satd_count: 0,
810 },
811 ];
812 let index = index_from_results(results);
813
814 let mut findings = vec![
815 Finding::new("F-001", "src/lib.rs", 10, "Low quality").with_suspiciousness(0.5),
816 Finding::new("F-002", "src/lib.rs", 40, "High quality").with_suspiciousness(0.5),
817 ];
818
819 apply_quality_weights(&mut findings, &index, 0.5);
820
821 assert!(findings[0].suspiciousness > 0.5);
823 assert!(findings[1].suspiciousness < 0.5);
824 }
825
826 #[test]
831 fn test_apply_regression_risk_no_match() {
832 let index: PmatQualityIndex = HashMap::new();
833 let mut findings = vec![Finding::new("F-001", "nonexistent.rs", 10, "Test")];
834 apply_regression_risk(&mut findings, &index);
835 assert!(findings[0].regression_risk.is_none());
836 }
837
838 #[test]
839 fn test_apply_regression_risk_empty_findings() {
840 let results = vec![PmatFunctionInfo {
841 file_path: "src/lib.rs".into(),
842 function_name: "f".into(),
843 start_line: 1,
844 end_line: 10,
845 tdg_score: 50.0,
846 tdg_grade: "C".into(),
847 complexity: 5,
848 satd_count: 0,
849 }];
850 let index = index_from_results(results);
851 let mut findings: Vec<Finding> = vec![];
852 apply_regression_risk(&mut findings, &index);
853 assert!(findings.is_empty());
854 }
855
856 #[test]
857 fn test_apply_regression_risk_multiple_findings() {
858 let results = vec![PmatFunctionInfo {
859 file_path: "src/lib.rs".into(),
860 function_name: "f".into(),
861 start_line: 1,
862 end_line: 50,
863 tdg_score: 30.0,
864 tdg_grade: "D".into(),
865 complexity: 40,
866 satd_count: 3,
867 }];
868 let index = index_from_results(results);
869
870 let mut findings = vec![
871 Finding::new("F-001", "src/lib.rs", 10, "First"),
872 Finding::new("F-002", "src/lib.rs", 20, "Second"),
873 ];
874
875 apply_regression_risk(&mut findings, &index);
876 assert!(findings[0].regression_risk.is_some());
877 assert!(findings[1].regression_risk.is_some());
878 assert!(
880 (findings[0].regression_risk.expect("unexpected failure")
881 - findings[1].regression_risk.expect("unexpected failure"))
882 .abs()
883 < 0.001
884 );
885 }
886
887 #[test]
892 fn test_lookup_quality_exact_start_boundary() {
893 let results = vec![PmatFunctionInfo {
894 file_path: "src/lib.rs".into(),
895 function_name: "bounded".into(),
896 start_line: 10,
897 end_line: 20,
898 tdg_score: 80.0,
899 tdg_grade: "B".into(),
900 complexity: 0,
901 satd_count: 0,
902 }];
903 let index = index_from_results(results);
904 let result = lookup_quality(&index, Path::new("src/lib.rs"), 10);
906 assert!(result.is_some());
907 assert_eq!(result.expect("operation failed").function_name, "bounded");
908 }
909
910 #[test]
911 fn test_lookup_quality_exact_end_boundary() {
912 let results = vec![PmatFunctionInfo {
913 file_path: "src/lib.rs".into(),
914 function_name: "bounded".into(),
915 start_line: 10,
916 end_line: 20,
917 tdg_score: 80.0,
918 tdg_grade: "B".into(),
919 complexity: 0,
920 satd_count: 0,
921 }];
922 let index = index_from_results(results);
923 let result = lookup_quality(&index, Path::new("src/lib.rs"), 20);
925 assert!(result.is_some());
926 assert_eq!(result.expect("operation failed").function_name, "bounded");
927 }
928
929 #[test]
930 fn test_lookup_quality_between_functions_nearest() {
931 let results = vec![
932 PmatFunctionInfo {
933 file_path: "src/lib.rs".into(),
934 function_name: "alpha".into(),
935 start_line: 10,
936 end_line: 20,
937 tdg_score: 0.0,
938 tdg_grade: String::new(),
939 complexity: 0,
940 satd_count: 0,
941 },
942 PmatFunctionInfo {
943 file_path: "src/lib.rs".into(),
944 function_name: "beta".into(),
945 start_line: 50,
946 end_line: 70,
947 tdg_score: 0.0,
948 tdg_grade: String::new(),
949 complexity: 0,
950 satd_count: 0,
951 },
952 ];
953 let index = index_from_results(results);
954
955 let result = lookup_quality(&index, Path::new("src/lib.rs"), 25);
957 assert!(result.is_some());
958 assert_eq!(result.expect("operation failed").function_name, "alpha");
959
960 let result = lookup_quality(&index, Path::new("src/lib.rs"), 45);
962 assert!(result.is_some());
963 assert_eq!(result.expect("operation failed").function_name, "beta");
964 }
965
966 #[test]
971 fn test_index_from_results_empty() {
972 let index = index_from_results(vec![]);
973 assert!(index.is_empty());
974 }
975
976 #[test]
977 fn test_index_from_results_unsorted_input() {
978 let results = vec![
979 PmatFunctionInfo {
980 file_path: "src/lib.rs".into(),
981 function_name: "last".into(),
982 start_line: 100,
983 end_line: 120,
984 tdg_score: 0.0,
985 tdg_grade: String::new(),
986 complexity: 0,
987 satd_count: 0,
988 },
989 PmatFunctionInfo {
990 file_path: "src/lib.rs".into(),
991 function_name: "first".into(),
992 start_line: 5,
993 end_line: 15,
994 tdg_score: 0.0,
995 tdg_grade: String::new(),
996 complexity: 0,
997 satd_count: 0,
998 },
999 PmatFunctionInfo {
1000 file_path: "src/lib.rs".into(),
1001 function_name: "middle".into(),
1002 start_line: 50,
1003 end_line: 60,
1004 tdg_score: 0.0,
1005 tdg_grade: String::new(),
1006 complexity: 0,
1007 satd_count: 0,
1008 },
1009 ];
1010 let index = index_from_results(results);
1011 let fns = &index[&PathBuf::from("src/lib.rs")];
1012 assert_eq!(fns.len(), 3);
1013 assert_eq!(fns[0].function_name, "first");
1015 assert_eq!(fns[1].function_name, "middle");
1016 assert_eq!(fns[2].function_name, "last");
1017 }
1018
1019 #[test]
1024 fn test_generate_satd_findings_medium_satd() {
1025 let results = vec![PmatFunctionInfo {
1026 file_path: "src/lib.rs".into(),
1027 function_name: "medium".into(),
1028 start_line: 1,
1029 end_line: 10,
1030 tdg_score: 50.0,
1031 tdg_grade: "C".into(),
1032 complexity: 10,
1033 satd_count: 2,
1034 }];
1035 let index = index_from_results(results);
1036 let findings = generate_satd_findings(Path::new("."), &index);
1037 assert_eq!(findings.len(), 1);
1038 assert_eq!(findings[0].severity, FindingSeverity::Medium);
1039 assert!((findings[0].suspiciousness - 0.5).abs() < 0.001);
1040 }
1041
1042 #[test]
1043 fn test_generate_satd_findings_high_satd() {
1044 let results = vec![PmatFunctionInfo {
1045 file_path: "src/lib.rs".into(),
1046 function_name: "terrible".into(),
1047 start_line: 1,
1048 end_line: 10,
1049 tdg_score: 10.0,
1050 tdg_grade: "F".into(),
1051 complexity: 50,
1052 satd_count: 10,
1053 }];
1054 let index = index_from_results(results);
1055 let findings = generate_satd_findings(Path::new("."), &index);
1056 assert_eq!(findings.len(), 1);
1057 assert_eq!(findings[0].severity, FindingSeverity::High);
1058 assert!((findings[0].suspiciousness - 0.7).abs() < 0.001);
1059 }
1060
1061 #[test]
1062 fn test_generate_satd_findings_description_format() {
1063 let results = vec![PmatFunctionInfo {
1064 file_path: "src/lib.rs".into(),
1065 function_name: "check_fn".into(),
1066 start_line: 5,
1067 end_line: 15,
1068 tdg_score: 60.0,
1069 tdg_grade: "C".into(),
1070 complexity: 8,
1071 satd_count: 1,
1072 }];
1073 let index = index_from_results(results);
1074 let findings = generate_satd_findings(Path::new("."), &index);
1075 assert_eq!(findings.len(), 1);
1076 let f = &findings[0];
1077 assert!(f.title.contains("check_fn"));
1078 assert!(f.title.contains("1 SATD"));
1079 assert!(f.description.contains("grade C"));
1080 assert!(f.description.contains("complexity 8"));
1081 }
1082
1083 #[test]
1084 fn test_generate_satd_findings_multiple_files() {
1085 let results = vec![
1086 PmatFunctionInfo {
1087 file_path: "src/a.rs".into(),
1088 function_name: "fn_a".into(),
1089 start_line: 1,
1090 end_line: 10,
1091 tdg_score: 0.0,
1092 tdg_grade: String::new(),
1093 complexity: 0,
1094 satd_count: 2,
1095 },
1096 PmatFunctionInfo {
1097 file_path: "src/b.rs".into(),
1098 function_name: "fn_b".into(),
1099 start_line: 1,
1100 end_line: 10,
1101 tdg_score: 0.0,
1102 tdg_grade: String::new(),
1103 complexity: 0,
1104 satd_count: 3,
1105 },
1106 PmatFunctionInfo {
1107 file_path: "src/c.rs".into(),
1108 function_name: "fn_c".into(),
1109 start_line: 1,
1110 end_line: 10,
1111 tdg_score: 0.0,
1112 tdg_grade: String::new(),
1113 complexity: 0,
1114 satd_count: 0,
1115 },
1116 ];
1117 let index = index_from_results(results);
1118 let findings = generate_satd_findings(Path::new("."), &index);
1119 assert_eq!(findings.len(), 2);
1121 }
1122
1123 #[test]
1124 fn test_generate_satd_findings_id_counter() {
1125 let results = vec![
1126 PmatFunctionInfo {
1127 file_path: "src/lib.rs".into(),
1128 function_name: "f1".into(),
1129 start_line: 1,
1130 end_line: 10,
1131 tdg_score: 0.0,
1132 tdg_grade: String::new(),
1133 complexity: 0,
1134 satd_count: 1,
1135 },
1136 PmatFunctionInfo {
1137 file_path: "src/lib.rs".into(),
1138 function_name: "f2".into(),
1139 start_line: 20,
1140 end_line: 30,
1141 tdg_score: 0.0,
1142 tdg_grade: String::new(),
1143 complexity: 0,
1144 satd_count: 1,
1145 },
1146 ];
1147 let index = index_from_results(results);
1148 let findings = generate_satd_findings(Path::new("."), &index);
1149 assert_eq!(findings.len(), 2);
1150 assert!(findings.iter().any(|f| f.id.contains("0001")));
1152 assert!(findings.iter().any(|f| f.id.contains("0002")));
1153 }
1154
1155 #[test]
1156 fn test_generate_satd_findings_category_and_mode() {
1157 let results = vec![PmatFunctionInfo {
1158 file_path: "src/lib.rs".into(),
1159 function_name: "f".into(),
1160 start_line: 1,
1161 end_line: 10,
1162 tdg_score: 0.0,
1163 tdg_grade: String::new(),
1164 complexity: 0,
1165 satd_count: 1,
1166 }];
1167 let index = index_from_results(results);
1168 let findings = generate_satd_findings(Path::new("."), &index);
1169 assert_eq!(findings.len(), 1);
1170 assert_eq!(findings[0].category, DefectCategory::LogicErrors);
1171 assert_eq!(findings[0].discovered_by, HuntMode::Analyze);
1172 }
1173
1174 #[test]
1179 fn test_build_quality_index_returns_option() {
1180 let result = build_quality_index(Path::new("."), "nonexistent_query_xyz", 1);
1184 let _ = result;
1186 }
1187
1188 #[test]
1189 fn test_scope_targets_by_quality_returns_option() {
1190 let result = scope_targets_by_quality(Path::new("."), "nonexistent_query_xyz", 1);
1192 let _ = result;
1193 }
1194
1195 #[test]
1196 fn test_scope_targets_by_quality_with_real_query() {
1197 let result = scope_targets_by_quality(Path::new("."), "cache", 5);
1199 if let Some(paths) = result {
1202 assert!(!paths.is_empty());
1204 }
1205 }
1206
1207 #[test]
1208 fn test_build_quality_index_with_real_query() {
1209 let result = build_quality_index(Path::new("."), "cache", 5);
1211 if let Some(index) = result {
1212 assert!(!index.is_empty());
1213 for functions in index.values() {
1215 for w in functions.windows(2) {
1216 assert!(w[0].start_line <= w[1].start_line);
1217 }
1218 }
1219 }
1220 }
1221}