Skip to main content

batuta/bug_hunter/
modes_hunt.rs

1//! BH-02: SBFL without failing tests (SBEST pattern).
2
3use super::types::*;
4use std::path::Path;
5
6/// BH-02: SBFL without failing tests (SBEST pattern)
7pub(super) fn run_hunt_mode(project_path: &Path, config: &HuntConfig, result: &mut HuntResult) {
8    // Look for crash logs, stack traces, or error reports
9    let crash_patterns = ["crash.log", "panic.log", "*.crash", "stack_trace.txt", "core.*"];
10
11    let mut stack_traces_found = Vec::new();
12
13    for pattern in crash_patterns {
14        if let Ok(entries) = glob::glob(&format!("{}/**/{}", project_path.display(), pattern)) {
15            for entry in entries.flatten() {
16                stack_traces_found.push(entry);
17            }
18        }
19    }
20
21    // Also check for recent panic messages in test output
22    let test_output = project_path.join("target/nextest/ci/junit.xml");
23    if test_output.exists() {
24        if let Ok(content) = std::fs::read_to_string(&test_output) {
25            if content.contains("panicked") || content.contains("FAILED") {
26                stack_traces_found.push(test_output);
27            }
28        }
29    }
30
31    if stack_traces_found.is_empty() {
32        // No crash data available, fall back to coverage-based analysis
33        analyze_coverage_hotspots(project_path, config, result);
34    } else {
35        for trace_file in stack_traces_found {
36            analyze_stack_trace(&trace_file, project_path, config, result);
37        }
38    }
39}
40
41/// Analyze coverage data for suspicious hotspots.
42pub(super) fn analyze_coverage_hotspots(
43    project_path: &Path,
44    config: &HuntConfig,
45    result: &mut HuntResult,
46) {
47    // Check custom coverage path first
48    if let Some(ref custom_path) = config.coverage_path {
49        if custom_path.exists() {
50            if let Ok(content) = std::fs::read_to_string(custom_path) {
51                parse_lcov_for_hotspots(&content, project_path, result);
52                return;
53            }
54        }
55    }
56
57    // Build list of paths to search
58    let mut lcov_paths: Vec<std::path::PathBuf> = vec![
59        // Project root (common location for cargo llvm-cov output)
60        project_path.join("lcov.info"),
61        // Standard target locations
62        project_path.join("target/coverage/lcov.info"),
63        project_path.join("target/llvm-cov/lcov.info"),
64        project_path.join("coverage/lcov.info"),
65    ];
66
67    // Check CARGO_TARGET_DIR for custom target locations
68    if let Ok(target_dir) = std::env::var("CARGO_TARGET_DIR") {
69        let target_path = std::path::Path::new(&target_dir);
70        lcov_paths.push(target_path.join("coverage/lcov.info"));
71        lcov_paths.push(target_path.join("llvm-cov/lcov.info"));
72    }
73
74    for lcov_path in &lcov_paths {
75        if lcov_path.exists() {
76            if let Ok(content) = std::fs::read_to_string(lcov_path) {
77                parse_lcov_for_hotspots(&content, project_path, result);
78                return;
79            }
80        }
81    }
82
83    // No coverage data available
84    let searched =
85        lcov_paths.iter().map(|p| p.display().to_string()).collect::<Vec<_>>().join(", ");
86    result.add_finding(
87        Finding::new(
88            "BH-HUNT-NOCOV",
89            project_path.join("target"),
90            1,
91            "No coverage data available",
92        )
93        .with_description(format!(
94            "Run `cargo llvm-cov --lcov --output-path lcov.info` or use --coverage-path. Searched: {}",
95            searched
96        ))
97        .with_severity(FindingSeverity::Info)
98        .with_category(DefectCategory::ConfigurationErrors)
99        .with_suspiciousness(0.1)
100        .with_discovered_by(HuntMode::Hunt),
101    );
102}
103
104/// Parse a single DA line from LCOV data, recording uncovered lines.
105pub(super) fn parse_lcov_da_line(
106    da: &str,
107    file: &str,
108    file_uncovered: &mut std::collections::HashMap<String, Vec<usize>>,
109) {
110    let Some((line_str, hits_str)) = da.split_once(',') else {
111        return;
112    };
113    let Ok(line_num) = line_str.parse::<usize>() else {
114        return;
115    };
116    let Ok(hits) = hits_str.parse::<usize>() else {
117        return;
118    };
119    if hits == 0 {
120        file_uncovered.entry(file.to_string()).or_default().push(line_num);
121    }
122}
123
124/// Report files with many uncovered lines as suspicious findings.
125pub(super) fn report_uncovered_hotspots(
126    file_uncovered: std::collections::HashMap<String, Vec<usize>>,
127    project_path: &Path,
128    result: &mut HuntResult,
129) {
130    let mut finding_id = 0;
131    for (file, lines) in file_uncovered {
132        if lines.len() <= 5 {
133            continue;
134        }
135        finding_id += 1;
136        let suspiciousness = (lines.len() as f64 / 100.0).min(0.8);
137        result.add_finding(
138            Finding::new(
139                format!("BH-COV-{:04}", finding_id),
140                project_path.join(&file),
141                lines[0],
142                format!("Low coverage region ({} uncovered lines)", lines.len()),
143            )
144            .with_description(format!(
145                "Lines {} are never executed; potential dead code or missing tests",
146                lines.iter().take(5).map(|l| l.to_string()).collect::<Vec<_>>().join(", ")
147            ))
148            .with_severity(FindingSeverity::Low)
149            .with_category(DefectCategory::LogicErrors)
150            .with_suspiciousness(suspiciousness)
151            .with_discovered_by(HuntMode::Hunt)
152            .with_evidence(FindingEvidence::sbfl("Coverage", suspiciousness)),
153        );
154    }
155}
156
157/// Parse LCOV data for coverage hotspots.
158pub(super) fn parse_lcov_for_hotspots(content: &str, project_path: &Path, result: &mut HuntResult) {
159    let mut current_file: Option<String> = None;
160    let mut file_uncovered: std::collections::HashMap<String, Vec<usize>> =
161        std::collections::HashMap::new();
162
163    for line in content.lines() {
164        if let Some(file) = line.strip_prefix("SF:") {
165            current_file = Some(file.to_string());
166        } else if let Some(da) = line.strip_prefix("DA:") {
167            if let Some(ref file) = current_file {
168                parse_lcov_da_line(da, file, &mut file_uncovered);
169            }
170        } else if line == "end_of_record" {
171            current_file = None;
172        }
173    }
174
175    report_uncovered_hotspots(file_uncovered, project_path, result);
176}
177
178/// Analyze a stack trace file.
179pub(super) fn analyze_stack_trace(
180    trace_file: &Path,
181    _project_path: &Path,
182    _config: &HuntConfig,
183    result: &mut HuntResult,
184) {
185    let content = match std::fs::read_to_string(trace_file) {
186        Ok(c) => c,
187        Err(_) => return,
188    };
189
190    let mut finding_id = 0;
191
192    // Look for panic locations
193    for line in content.lines() {
194        // Pattern: "at src/file.rs:123"
195        if let Some(at_pos) = line.find(" at ") {
196            let location = &line[at_pos + 4..];
197            if let Some(colon_pos) = location.rfind(':') {
198                let file = &location[..colon_pos];
199                if let Ok(line_num) = location[colon_pos + 1..].trim().parse::<usize>() {
200                    if file.ends_with(".rs") && !file.contains("/.cargo/") {
201                        finding_id += 1;
202                        result.add_finding(
203                            Finding::new(
204                                format!("BH-STACK-{:04}", finding_id),
205                                file,
206                                line_num,
207                                "Stack trace location",
208                            )
209                            .with_description(format!(
210                                "Found in stack trace: {}",
211                                trace_file.display()
212                            ))
213                            .with_severity(FindingSeverity::High)
214                            .with_category(DefectCategory::LogicErrors)
215                            .with_suspiciousness(0.85)
216                            .with_discovered_by(HuntMode::Hunt)
217                            .with_evidence(FindingEvidence::sbfl("StackTrace", 0.85)),
218                        );
219                    }
220                }
221            }
222        }
223    }
224}