1use anyhow::Result;
8use std::path::Path;
9
10use crate::coverage_parser::{self, map_coverage_to_features, parse_coverage_reports};
11use crate::file_scanner::{list_files_recursive, list_files_recursive_with_changes};
12use crate::models::Feature;
13
14#[derive(Debug, Clone)]
16pub struct ScanConfig<'a> {
17 pub skip_changes: bool,
19
20 pub should_add_coverage: bool,
22
23 pub coverage_dir_override: Option<&'a Path>,
26
27 pub current_dir: &'a Path,
29
30 pub project_dir: Option<&'a Path>,
32}
33
34impl<'a> ScanConfig<'a> {
35 pub fn new(current_dir: &'a Path) -> Self {
37 Self {
38 skip_changes: false,
39 should_add_coverage: false,
40 coverage_dir_override: None,
41 current_dir,
42 project_dir: None,
43 }
44 }
45
46 pub fn skip_changes(mut self, skip: bool) -> Self {
48 self.skip_changes = skip;
49 self
50 }
51
52 pub fn with_coverage(mut self, should_add: bool) -> Self {
54 self.should_add_coverage = should_add;
55 self
56 }
57
58 pub fn coverage_dir(mut self, dir: &'a Path) -> Self {
60 self.coverage_dir_override = Some(dir);
61 self
62 }
63
64 pub fn project_dir(mut self, dir: &'a Path) -> Self {
66 self.project_dir = Some(dir);
67 self
68 }
69}
70
71pub fn scan_features(base_path: &Path, config: ScanConfig) -> Result<Vec<Feature>> {
97 let mut features = if config.skip_changes {
99 list_files_recursive(base_path)?
100 } else {
101 list_files_recursive_with_changes(base_path)?
102 };
103
104 if config.should_add_coverage {
106 add_coverage_to_features(
107 &mut features,
108 base_path,
109 config.coverage_dir_override,
110 config.current_dir,
111 config.project_dir,
112 );
113 }
114
115 Ok(features)
116}
117
118fn add_coverage_to_features(
123 features: &mut [Feature],
124 base_path: &Path,
125 coverage_dir_override: Option<&Path>,
126 current_dir: &Path,
127 project_dir: Option<&Path>,
128) {
129 let coverage_dirs = if let Some(override_dir) = coverage_dir_override {
130 vec![override_dir.to_path_buf()]
132 } else {
133 let mut dirs = vec![
138 base_path.join(".coverage"),
139 base_path.join("coverage"),
140 current_dir.join(".coverage"),
141 current_dir.join("coverage"),
142 ];
143
144 if let Some(proj_dir) = project_dir {
146 let proj_coverage = proj_dir.join(".coverage");
147 let proj_coverage_plain = proj_dir.join("coverage");
148
149 if !dirs.contains(&proj_coverage) {
151 dirs.push(proj_coverage);
152 }
153 if !dirs.contains(&proj_coverage_plain) {
154 dirs.push(proj_coverage_plain);
155 }
156 }
157
158 dirs
159 };
160
161 for coverage_dir in &coverage_dirs {
162 if let Ok(coverage_map) = parse_coverage_reports(coverage_dir, base_path)
164 && !coverage_map.is_empty()
165 {
166 let feature_coverage = map_coverage_to_features(features, coverage_map, base_path);
168 update_features_with_coverage(features, &feature_coverage);
169 break; }
171 }
172}
173
174fn update_features_with_coverage(
176 features: &mut [Feature],
177 feature_coverage: &std::collections::HashMap<String, coverage_parser::CoverageStats>,
178) {
179 for feature in features {
180 if let Some(coverage) = feature_coverage.get(&feature.path) {
181 if let Some(stats) = &mut feature.stats {
183 stats.coverage = Some(coverage.clone());
184 } else {
185 feature.stats = Some(crate::models::Stats {
186 files_count: None,
187 lines_count: None,
188 todos_count: None,
189 commits: std::collections::BTreeMap::new(),
190 coverage: Some(coverage.clone()),
191 });
192 }
193 }
194
195 update_features_with_coverage(&mut feature.features, feature_coverage);
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use std::path::PathBuf;
204
205 #[test]
206 fn test_scan_config_builder() {
207 let current_dir = PathBuf::from("/tmp");
208 let project_dir = PathBuf::from("/project");
209
210 let config = ScanConfig::new(¤t_dir)
211 .skip_changes(true)
212 .with_coverage(true)
213 .project_dir(&project_dir);
214
215 assert!(config.skip_changes);
216 assert!(config.should_add_coverage);
217 assert!(config.project_dir.is_some());
218 }
219
220 #[test]
221 fn test_scan_config_defaults() {
222 let current_dir = PathBuf::from("/tmp");
223 let config = ScanConfig::new(¤t_dir);
224
225 assert!(!config.skip_changes);
226 assert!(!config.should_add_coverage);
227 assert!(config.coverage_dir_override.is_none());
228 assert!(config.project_dir.is_none());
229 }
230
231 #[test]
232 fn test_scan_features_basic() {
233 let test_path = PathBuf::from("../../examples/tests_skip_changes/src");
234
235 if !test_path.exists() {
236 println!("Skipping test - test path does not exist");
237 return;
238 }
239
240 let current_dir = std::env::current_dir().unwrap();
241 let config = ScanConfig::new(¤t_dir).skip_changes(true);
242
243 let result = scan_features(&test_path, config);
244 assert!(result.is_ok(), "Failed to scan features");
245
246 let features = result.unwrap();
247 assert!(!features.is_empty(), "Should find at least one feature");
248 }
249
250 #[test]
251 fn test_scan_features_with_changes() {
252 let test_path = PathBuf::from("../../examples/tests-with-changes/src");
253
254 if !test_path.exists() {
255 println!("Skipping test - test path does not exist");
256 return;
257 }
258
259 let current_dir = std::env::current_dir().unwrap();
260 let config = ScanConfig::new(¤t_dir).skip_changes(false);
261
262 let result = scan_features(&test_path, config);
263 assert!(result.is_ok(), "Failed to scan features with changes");
264
265 let features = result.unwrap();
266 assert!(!features.is_empty(), "Should find at least one feature");
267
268 let has_changes = features.iter().any(|f| !f.changes.is_empty());
270 assert!(has_changes, "At least one feature should have git history");
271 }
272}