Skip to main content

batuta/bug_hunter/localization/
scoring.rs

1//! Scoring types for fault localization.
2//!
3//! Contains `ScoredLocation`, `TestCoverage`, `SpectrumData` (SBFL),
4//! and `MutationData` (MBFL) used by the multi-channel localizer.
5
6use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use crate::bug_hunter::types::{ChannelWeights, SbflFormula};
10
11/// Location with multi-channel scores.
12#[derive(Debug, Clone)]
13pub struct ScoredLocation {
14    pub file: PathBuf,
15    pub line: usize,
16    /// SBFL spectrum score
17    pub spectrum_score: f64,
18    /// MBFL mutation score
19    pub mutation_score: f64,
20    /// Static analysis score
21    pub static_score: f64,
22    /// Semantic similarity score
23    pub semantic_score: f64,
24    /// PMAT quality score (BH-21)
25    pub quality_score: f64,
26    /// Final combined score
27    pub final_score: f64,
28}
29
30impl ScoredLocation {
31    pub fn new(file: PathBuf, line: usize) -> Self {
32        Self {
33            file,
34            line,
35            spectrum_score: 0.0,
36            mutation_score: 0.0,
37            static_score: 0.0,
38            semantic_score: 0.0,
39            quality_score: 0.0,
40            final_score: 0.0,
41        }
42    }
43
44    /// Compute final score using channel weights (BH-19, BH-21)
45    pub fn compute_final_score(&mut self, weights: &ChannelWeights) {
46        self.final_score = weights.combine(
47            self.spectrum_score,
48            self.mutation_score,
49            self.static_score,
50            self.semantic_score,
51            self.quality_score,
52        );
53    }
54}
55
56/// Coverage data for a single test.
57#[derive(Debug, Clone)]
58pub struct TestCoverage {
59    pub test_name: String,
60    pub passed: bool,
61    /// Lines executed: (file, line) -> execution count
62    pub executed_lines: HashMap<(PathBuf, usize), usize>,
63}
64
65/// SBFL spectrum data.
66#[derive(Debug, Default)]
67pub struct SpectrumData {
68    /// Lines covered by failing tests
69    pub failed_coverage: HashMap<(PathBuf, usize), usize>,
70    /// Lines covered by passing tests
71    pub passed_coverage: HashMap<(PathBuf, usize), usize>,
72    /// Total failing tests
73    pub total_failed: usize,
74    /// Total passing tests
75    pub total_passed: usize,
76}
77
78impl SpectrumData {
79    /// Compute SBFL score for a location using the specified formula.
80    pub fn compute_score(&self, file: &Path, line: usize, formula: SbflFormula) -> f64 {
81        let key = (file.to_path_buf(), line);
82        let ef = *self.failed_coverage.get(&key).unwrap_or(&0) as f64;
83        let ep = *self.passed_coverage.get(&key).unwrap_or(&0) as f64;
84        let nf = (self.total_failed as f64) - ef;
85
86        match formula {
87            SbflFormula::Tarantula => sbfl_tarantula(ef, ep, self.total_failed, self.total_passed),
88            SbflFormula::Ochiai => sbfl_ochiai(ef, ep, nf),
89            SbflFormula::DStar2 => sbfl_dstar(ef, ep, nf, 2),
90            SbflFormula::DStar3 => sbfl_dstar(ef, ep, nf, 3),
91        }
92    }
93}
94
95fn sbfl_tarantula(ef: f64, ep: f64, total_failed: usize, total_passed: usize) -> f64 {
96    let fail_ratio = if total_failed > 0 { ef / total_failed as f64 } else { 0.0 };
97    let pass_ratio = if total_passed > 0 { ep / total_passed as f64 } else { 0.0 };
98    let sum = fail_ratio + pass_ratio;
99    if sum > 0.0 {
100        fail_ratio / sum
101    } else {
102        0.0
103    }
104}
105
106fn sbfl_ochiai(ef: f64, ep: f64, nf: f64) -> f64 {
107    let denom = ((ef + nf) * (ef + ep)).sqrt();
108    if denom > 0.0 {
109        ef / denom
110    } else {
111        0.0
112    }
113}
114
115fn sbfl_dstar(ef: f64, ep: f64, nf: f64, power: u32) -> f64 {
116    let denom = ep + nf;
117    if denom > 0.0 {
118        ef.powi(power as i32) / denom
119    } else if ef > 0.0 {
120        f64::MAX
121    } else {
122        0.0
123    }
124}
125
126/// Mutation data for MBFL (BH-16).
127#[derive(Debug, Default)]
128pub struct MutationData {
129    /// Mutants at each location: (file, line) -> (total_mutants, killed_by_failing_tests)
130    pub mutants: HashMap<(PathBuf, usize), (usize, usize)>,
131}
132
133impl MutationData {
134    /// Compute MBFL score for a location.
135    /// Score = |killed_by_failing| / |total_mutants|
136    pub fn compute_score(&self, file: &Path, line: usize) -> f64 {
137        let key = (file.to_path_buf(), line);
138        if let Some((total, killed)) = self.mutants.get(&key) {
139            if *total > 0 {
140                return *killed as f64 / *total as f64;
141            }
142        }
143        0.0
144    }
145}