1use crate::config::DebtmapConfig;
24use crate::env::AnalysisEnv;
25use crate::errors::AnalysisError;
26use crate::io::traits::{Cache, CoverageData, CoverageLoader, FileCoverage, FileSystem};
27use crate::progress::implementations::{RecordingProgressSink, SilentProgressSink};
28use crate::progress::traits::{HasProgress, ProgressSink};
29use std::collections::HashMap;
30use std::path::{Path, PathBuf};
31use std::sync::{Arc, RwLock};
32
33#[derive(Clone)]
65pub struct DebtmapTestEnv {
66 files: Arc<RwLock<HashMap<PathBuf, String>>>,
67 coverage: Arc<RwLock<CoverageData>>,
68 cache: Arc<RwLock<HashMap<String, Vec<u8>>>>,
69 config: DebtmapConfig,
70 progress: Arc<dyn ProgressSink>,
71}
72
73impl DebtmapTestEnv {
74 pub fn new() -> Self {
82 Self {
83 files: Arc::new(RwLock::new(HashMap::new())),
84 coverage: Arc::new(RwLock::new(CoverageData::new())),
85 cache: Arc::new(RwLock::new(HashMap::new())),
86 config: DebtmapConfig::default(),
87 progress: Arc::new(SilentProgressSink),
88 }
89 }
90
91 pub fn with_recording_progress() -> (Self, Arc<RecordingProgressSink>) {
104 let recorder = Arc::new(RecordingProgressSink::new());
105 let env = Self {
106 files: Arc::new(RwLock::new(HashMap::new())),
107 coverage: Arc::new(RwLock::new(CoverageData::new())),
108 cache: Arc::new(RwLock::new(HashMap::new())),
109 config: DebtmapConfig::default(),
110 progress: recorder.clone(),
111 };
112 (env, recorder)
113 }
114
115 pub fn with_progress(self, progress: Arc<dyn ProgressSink>) -> Self {
128 Self { progress, ..self }
129 }
130
131 pub fn with_file(self, path: impl Into<PathBuf>, content: impl Into<String>) -> Self {
140 self.files
141 .write()
142 .expect("Lock poisoned")
143 .insert(path.into(), content.into());
144 self
145 }
146
147 pub fn with_files<'a>(mut self, files: impl IntoIterator<Item = (&'a str, &'a str)>) -> Self {
160 for (path, content) in files {
161 self = self.with_file(path, content);
162 }
163 self
164 }
165
166 pub fn with_coverage(self, path: impl Into<PathBuf>, coverage: FileCoverage) -> Self {
182 self.coverage
183 .write()
184 .expect("Lock poisoned")
185 .add_file_coverage(path.into(), coverage);
186 self
187 }
188
189 pub fn with_coverage_percentage(self, path: impl Into<PathBuf>, percentage: f64) -> Self {
201 let mut fc = FileCoverage::new();
202 let hit_lines = (percentage as usize).min(100);
204 for i in 1..=100 {
205 fc.add_line(i, if i <= hit_lines { 1 } else { 0 });
206 }
207 self.with_coverage(path, fc)
208 }
209
210 pub fn with_config(mut self, config: DebtmapConfig) -> Self {
222 self.config = config;
223 self
224 }
225
226 pub fn with_cache_entry(self, key: impl Into<String>, value: impl AsRef<[u8]>) -> Self {
235 self.cache
236 .write()
237 .expect("Lock poisoned")
238 .insert(key.into(), value.as_ref().to_vec());
239 self
240 }
241
242 pub fn has_file(&self, path: impl AsRef<Path>) -> bool {
244 self.files
245 .read()
246 .expect("Lock poisoned")
247 .contains_key(path.as_ref())
248 }
249
250 pub fn file_paths(&self) -> Vec<PathBuf> {
252 self.files
253 .read()
254 .expect("Lock poisoned")
255 .keys()
256 .cloned()
257 .collect()
258 }
259
260 pub fn clear_files(&self) {
262 self.files.write().expect("Lock poisoned").clear();
263 }
264
265 pub fn clear_coverage(&self) {
267 *self.coverage.write().expect("Lock poisoned") = CoverageData::new();
268 }
269
270 pub fn clear_cache(&self) {
272 self.cache.write().expect("Lock poisoned").clear();
273 }
274
275 pub fn reset(&self) {
277 self.clear_files();
278 self.clear_coverage();
279 self.clear_cache();
280 }
281}
282
283impl Default for DebtmapTestEnv {
284 fn default() -> Self {
285 Self::new()
286 }
287}
288
289impl std::fmt::Debug for DebtmapTestEnv {
290 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
291 let file_count = self.files.read().map(|f| f.len()).unwrap_or(0);
292 f.debug_struct("DebtmapTestEnv")
293 .field("file_count", &file_count)
294 .field("config", &self.config)
295 .finish_non_exhaustive()
296 }
297}
298
299impl AnalysisEnv for DebtmapTestEnv {
301 fn file_system(&self) -> &dyn FileSystem {
302 self
303 }
304
305 fn coverage_loader(&self) -> &dyn CoverageLoader {
306 self
307 }
308
309 fn cache(&self) -> &dyn Cache {
310 self
311 }
312
313 fn config(&self) -> &DebtmapConfig {
314 &self.config
315 }
316
317 fn with_config(self, config: DebtmapConfig) -> Self {
318 Self { config, ..self }
319 }
320}
321
322impl FileSystem for DebtmapTestEnv {
324 fn read_to_string(&self, path: &Path) -> Result<String, AnalysisError> {
325 self.files
326 .read()
327 .expect("Lock poisoned")
328 .get(path)
329 .cloned()
330 .ok_or_else(|| AnalysisError::io(format!("File not found: {}", path.display())))
331 }
332
333 fn write(&self, path: &Path, content: &str) -> Result<(), AnalysisError> {
334 self.files
335 .write()
336 .expect("Lock poisoned")
337 .insert(path.to_path_buf(), content.to_string());
338 Ok(())
339 }
340
341 fn exists(&self, path: &Path) -> bool {
342 self.files.read().expect("Lock poisoned").contains_key(path)
343 }
344
345 fn is_file(&self, path: &Path) -> bool {
346 self.exists(path)
347 }
348
349 fn is_dir(&self, path: &Path) -> bool {
350 let files = self.files.read().expect("Lock poisoned");
352 files.keys().any(|file_path| {
353 file_path
354 .parent()
355 .map(|p| p.starts_with(path))
356 .unwrap_or(false)
357 })
358 }
359
360 fn read_bytes(&self, path: &Path) -> Result<Vec<u8>, AnalysisError> {
361 self.read_to_string(path).map(|s| s.into_bytes())
362 }
363}
364
365impl CoverageLoader for DebtmapTestEnv {
367 fn load_lcov(&self, _path: &Path) -> Result<CoverageData, AnalysisError> {
368 Ok(self.coverage.read().expect("Lock poisoned").clone())
370 }
371
372 fn load_cobertura(&self, path: &Path) -> Result<CoverageData, AnalysisError> {
373 self.load_lcov(path)
375 }
376}
377
378impl HasProgress for DebtmapTestEnv {
380 fn progress(&self) -> &dyn ProgressSink {
381 &*self.progress
382 }
383}
384
385impl Cache for DebtmapTestEnv {
387 fn get(&self, key: &str) -> Option<Vec<u8>> {
388 self.cache.read().expect("Lock poisoned").get(key).cloned()
389 }
390
391 fn set(&self, key: &str, value: &[u8]) -> Result<(), AnalysisError> {
392 self.cache
393 .write()
394 .expect("Lock poisoned")
395 .insert(key.to_string(), value.to_vec());
396 Ok(())
397 }
398
399 fn invalidate(&self, key: &str) -> Result<(), AnalysisError> {
400 self.cache.write().expect("Lock poisoned").remove(key);
401 Ok(())
402 }
403
404 fn clear(&self) -> Result<(), AnalysisError> {
405 self.cache.write().expect("Lock poisoned").clear();
406 Ok(())
407 }
408}
409
410#[cfg(test)]
411mod tests {
412 use super::*;
413
414 #[test]
415 fn test_new_env_is_empty() {
416 let env = DebtmapTestEnv::new();
417 assert!(!env.has_file("any.rs"));
418 assert!(env.file_paths().is_empty());
419 }
420
421 #[test]
422 fn test_with_file() {
423 let env = DebtmapTestEnv::new().with_file("test.rs", "fn main() {}");
424
425 assert!(env.has_file("test.rs"));
426 let content = env
427 .file_system()
428 .read_to_string(Path::new("test.rs"))
429 .unwrap();
430 assert_eq!(content, "fn main() {}");
431 }
432
433 #[test]
434 fn test_with_files() {
435 let env = DebtmapTestEnv::new().with_files(vec![
436 ("a.rs", "fn a() {}"),
437 ("b.rs", "fn b() {}"),
438 ("c.rs", "fn c() {}"),
439 ]);
440
441 assert_eq!(env.file_paths().len(), 3);
442 assert!(env.has_file("a.rs"));
443 assert!(env.has_file("b.rs"));
444 assert!(env.has_file("c.rs"));
445 }
446
447 #[test]
448 fn test_file_not_found() {
449 let env = DebtmapTestEnv::new();
450 let result = env.file_system().read_to_string(Path::new("missing.rs"));
451 assert!(result.is_err());
452 }
453
454 #[test]
455 fn test_write_and_read() {
456 let env = DebtmapTestEnv::new();
457 env.file_system()
458 .write(Path::new("new.rs"), "fn new() {}")
459 .unwrap();
460
461 let content = env
462 .file_system()
463 .read_to_string(Path::new("new.rs"))
464 .unwrap();
465 assert_eq!(content, "fn new() {}");
466 }
467
468 #[test]
469 fn test_coverage_percentage() {
470 let env = DebtmapTestEnv::new().with_coverage_percentage("test.rs", 75.0);
471
472 let coverage = env.coverage_loader().load_lcov(Path::new("")).unwrap();
473 let pct = coverage.get_file_coverage(Path::new("test.rs")).unwrap();
474 assert!((pct - 75.0).abs() < 1.0);
475 }
476
477 #[test]
478 fn test_cache_operations() {
479 let env = DebtmapTestEnv::new();
480
481 env.cache().set("key", b"value").unwrap();
483 assert_eq!(env.cache().get("key"), Some(b"value".to_vec()));
484
485 env.cache().invalidate("key").unwrap();
487 assert!(env.cache().get("key").is_none());
488
489 env.cache().set("key1", b"v1").unwrap();
491 env.cache().set("key2", b"v2").unwrap();
492 env.cache().clear().unwrap();
493 assert!(env.cache().get("key1").is_none());
494 assert!(env.cache().get("key2").is_none());
495 }
496
497 #[test]
498 fn test_with_config() {
499 use crate::config::IgnoreConfig;
500
501 let config = DebtmapConfig {
502 ignore: Some(IgnoreConfig {
503 patterns: vec!["test".to_string()],
504 }),
505 ..Default::default()
506 };
507
508 let env = DebtmapTestEnv::new().with_config(config);
509 assert!(env.config().ignore.is_some());
510 }
511
512 #[test]
513 fn test_is_send_sync() {
514 fn assert_send_sync<T: Send + Sync>() {}
515 assert_send_sync::<DebtmapTestEnv>();
516 }
517
518 #[test]
519 fn test_is_clone() {
520 let env1 = DebtmapTestEnv::new().with_file("test.rs", "fn main() {}");
521 let env2 = env1.clone();
522
523 assert!(env1.has_file("test.rs"));
525 assert!(env2.has_file("test.rs"));
526 }
527
528 #[test]
529 fn test_reset() {
530 let env = DebtmapTestEnv::new()
531 .with_file("test.rs", "fn main() {}")
532 .with_coverage_percentage("test.rs", 50.0)
533 .with_cache_entry("key", b"value");
534
535 env.reset();
536
537 assert!(!env.has_file("test.rs"));
538 assert!(env.cache().get("key").is_none());
539 }
540
541 #[test]
542 fn test_analysis_env_trait() {
543 let env = DebtmapTestEnv::new().with_file("test.rs", "fn main() {}");
544
545 let _fs = env.file_system();
547 let _cl = env.coverage_loader();
548 let _cache = env.cache();
549 let _config = env.config();
550
551 let env2 = env.with_config(DebtmapConfig::default());
553 assert!(env2.has_file("test.rs"));
554 }
555
556 #[test]
557 fn test_is_dir() {
558 let env = DebtmapTestEnv::new()
559 .with_file("src/main.rs", "fn main() {}")
560 .with_file("src/lib.rs", "pub fn lib() {}");
561
562 assert!(env.file_system().is_dir(Path::new("src")));
563 assert!(!env.file_system().is_dir(Path::new("other")));
564 }
565
566 #[test]
567 fn test_read_bytes() {
568 let env = DebtmapTestEnv::new().with_file("test.rs", "fn main() {}");
569
570 let bytes = env.file_system().read_bytes(Path::new("test.rs")).unwrap();
571 assert_eq!(bytes, b"fn main() {}");
572 }
573
574 #[test]
575 fn test_has_progress_silent() {
576 let env = DebtmapTestEnv::new();
577
578 env.progress().start_stage("Test");
580 env.progress().report("Test", 0, 10);
581 env.progress().complete_stage("Test");
582 }
583
584 #[test]
585 fn test_with_recording_progress() {
586 let (env, recorder) = DebtmapTestEnv::with_recording_progress();
587
588 env.progress().start_stage("Analysis");
589 env.progress().report("Analysis", 5, 10);
590 env.progress().complete_stage("Analysis");
591
592 assert_eq!(recorder.stages(), vec!["Analysis"]);
593 assert_eq!(recorder.completed_stages(), vec!["Analysis"]);
594 assert_eq!(recorder.event_count(), 3);
595 }
596
597 #[test]
598 fn test_with_progress_custom() {
599 let recorder = Arc::new(RecordingProgressSink::new());
600 let env = DebtmapTestEnv::new().with_progress(recorder.clone());
601
602 env.progress().start_stage("Custom");
603 assert_eq!(recorder.stages(), vec!["Custom"]);
604 }
605}