1use super::graph::{DependencyGraph, GraphError};
19use metrics::histogram;
20use std::path::{Path, PathBuf};
21use std::time::Instant;
22use thread_utilities::{RapidMap, RapidSet};
23use tracing::{info, warn};
24
25#[derive(Debug, thiserror::Error)]
27pub enum InvalidationError {
28 #[error("Circular dependency detected: {0:?}")]
30 CircularDependency(Vec<PathBuf>),
31
32 #[error("Graph error: {0}")]
34 Graph(String),
35}
36
37#[derive(Debug, Clone)]
66pub struct InvalidationResult {
67 pub invalidated_files: Vec<PathBuf>,
69
70 pub analysis_order: Vec<PathBuf>,
73
74 pub circular_dependencies: Vec<Vec<PathBuf>>,
78}
79
80#[derive(Debug, Clone)]
108pub struct InvalidationDetector {
109 graph: DependencyGraph,
110}
111
112impl InvalidationDetector {
113 pub fn new(graph: DependencyGraph) -> Self {
129 Self { graph }
130 }
131
132 pub fn compute_invalidation_set(&self, changed_files: &[PathBuf]) -> InvalidationResult {
171 let start = Instant::now();
172 info!(
173 "computing invalidation set for {} changed files",
174 changed_files.len()
175 );
176
177 let changed_set: RapidSet<PathBuf> = changed_files.iter().cloned().collect();
179 let affected = self.graph.find_affected_files(&changed_set);
180 let invalidated_files: Vec<PathBuf> = affected.iter().cloned().collect();
181
182 info!(
183 "found {} files affected by changes",
184 invalidated_files.len()
185 );
186
187 let result = match self.topological_sort(&invalidated_files) {
189 Ok(analysis_order) => {
190 info!("topological sort successful");
192 InvalidationResult {
193 invalidated_files,
194 analysis_order,
195 circular_dependencies: vec![],
196 }
197 }
198 Err(_) => {
199 warn!("circular dependencies detected");
201 let cycles = self.find_strongly_connected_components(&affected);
202
203 InvalidationResult {
206 invalidated_files,
207 analysis_order: vec![],
208 circular_dependencies: cycles,
209 }
210 }
211 };
212
213 let duration_ms = start.elapsed().as_micros() as f64 / 1000.0;
214 histogram!("invalidation_time_ms").record(duration_ms);
215
216 info!(
217 invalidated_count = result.invalidated_files.len(),
218 cycles = result.circular_dependencies.len(),
219 duration_ms = %format!("{:.2}", duration_ms),
220 "invalidation complete"
221 );
222
223 result
224 }
225
226 pub fn topological_sort(&self, files: &[PathBuf]) -> Result<Vec<PathBuf>, InvalidationError> {
261 let files_set: RapidSet<PathBuf> = files.iter().cloned().collect();
263
264 self.graph
265 .topological_sort(&files_set)
266 .map_err(|e| match e {
267 GraphError::CyclicDependency(path) => {
268 InvalidationError::CircularDependency(vec![path])
269 }
270 })
271 }
272
273 pub fn propagate_invalidation(&self, root: &Path) -> Vec<PathBuf> {
300 let root_set: RapidSet<PathBuf> = [root.to_path_buf()].into_iter().collect();
302 let affected: RapidSet<PathBuf> = self.graph.find_affected_files(&root_set);
303 affected.into_iter().collect()
304 }
305
306 fn find_strongly_connected_components(&self, files: &RapidSet<PathBuf>) -> Vec<Vec<PathBuf>> {
322 let mut state = TarjanState::new();
324 let mut sccs = Vec::new();
325
326 for file in files {
328 if !state.indices.contains_key(file) {
329 self.tarjan_dfs(file, &mut state, &mut sccs);
330 }
331 }
332
333 sccs.into_iter()
335 .filter(|scc| {
336 scc.len() > 1 || (scc.len() == 1 && self.has_self_loop(&scc[0]))
338 })
339 .collect()
340 }
341
342 fn tarjan_dfs(&self, v: &Path, state: &mut TarjanState, sccs: &mut Vec<Vec<PathBuf>>) {
344 let index = state.index_counter;
346 state.indices.insert(v.to_path_buf(), index);
347 state.lowlinks.insert(v.to_path_buf(), index);
348 state.index_counter += 1;
349 state.stack.push(v.to_path_buf());
350 state.on_stack.insert(v.to_path_buf());
351
352 let dependencies = self.graph.get_dependencies(v);
354 for edge in dependencies {
355 let dep = &edge.to;
356 if !state.indices.contains_key(dep) {
357 self.tarjan_dfs(dep, state, sccs);
359
360 let w_lowlink = *state.lowlinks.get(dep).unwrap();
362 let v_lowlink = state.lowlinks.get_mut(&v.to_path_buf()).unwrap();
363 *v_lowlink = (*v_lowlink).min(w_lowlink);
364 } else if state.on_stack.contains(dep) {
365 let w_index = *state.indices.get(dep).unwrap();
367 let v_lowlink = state.lowlinks.get_mut(&v.to_path_buf()).unwrap();
368 *v_lowlink = (*v_lowlink).min(w_index);
369 }
370 }
371
372 let v_index = *state.indices.get(&v.to_path_buf()).unwrap();
374 let v_lowlink = *state.lowlinks.get(&v.to_path_buf()).unwrap();
375
376 if v_lowlink == v_index {
377 let mut scc = Vec::new();
378 loop {
379 let w = state.stack.pop().unwrap();
380 state.on_stack.remove(&w);
381 scc.push(w.clone());
382 if w == v {
383 break;
384 }
385 }
386 sccs.push(scc);
387 }
388 }
389
390 fn has_self_loop(&self, file: &Path) -> bool {
392 let deps = self.graph.get_dependencies(file);
393 deps.iter().any(|edge| edge.to == file)
394 }
395}
396
397struct TarjanState {
399 index_counter: usize,
400 indices: RapidMap<PathBuf, usize>,
401 lowlinks: RapidMap<PathBuf, usize>,
402 stack: Vec<PathBuf>,
403 on_stack: RapidSet<PathBuf>,
404}
405
406impl TarjanState {
407 fn new() -> Self {
408 Self {
409 index_counter: 0,
410 indices: thread_utilities::get_map(),
411 lowlinks: thread_utilities::get_map(),
412 stack: Vec::new(),
413 on_stack: thread_utilities::get_set(),
414 }
415 }
416}
417
418#[cfg(test)]
421mod tests {
422 use super::*;
423 use crate::incremental::types::{DependencyEdge, DependencyType};
424
425 #[test]
428 fn test_invalidation_detector_new() {
429 let graph = DependencyGraph::new();
430 let detector = InvalidationDetector::new(graph);
431
432 assert_eq!(detector.graph.node_count(), 0);
434 assert_eq!(detector.graph.edge_count(), 0);
435 }
436
437 #[test]
438 fn test_invalidation_detector_with_populated_graph() {
439 let mut graph = DependencyGraph::new();
440 graph.add_edge(DependencyEdge::new(
441 PathBuf::from("A"),
442 PathBuf::from("B"),
443 DependencyType::Import,
444 ));
445
446 let detector = InvalidationDetector::new(graph);
447 assert_eq!(detector.graph.node_count(), 2);
448 assert_eq!(detector.graph.edge_count(), 1);
449 }
450
451 #[test]
454 fn test_propagate_single_file_no_dependents() {
455 let mut graph = DependencyGraph::new();
456 graph.add_node(&PathBuf::from("isolated.rs"));
457
458 let detector = InvalidationDetector::new(graph);
459 let affected = detector.propagate_invalidation(&PathBuf::from("isolated.rs"));
460
461 assert_eq!(affected.len(), 1);
462 assert_eq!(affected[0], PathBuf::from("isolated.rs"));
463 }
464
465 #[test]
466 fn test_propagate_linear_chain() {
467 let mut graph = DependencyGraph::new();
468 graph.add_edge(DependencyEdge::new(
470 PathBuf::from("A"),
471 PathBuf::from("B"),
472 DependencyType::Import,
473 ));
474 graph.add_edge(DependencyEdge::new(
475 PathBuf::from("B"),
476 PathBuf::from("C"),
477 DependencyType::Import,
478 ));
479
480 let detector = InvalidationDetector::new(graph);
481 let affected = detector.propagate_invalidation(&PathBuf::from("C"));
482
483 assert_eq!(affected.len(), 3);
485 assert!(affected.contains(&PathBuf::from("A")));
486 assert!(affected.contains(&PathBuf::from("B")));
487 assert!(affected.contains(&PathBuf::from("C")));
488 }
489
490 #[test]
491 fn test_propagate_diamond_dependency() {
492 let mut graph = DependencyGraph::new();
493 graph.add_edge(DependencyEdge::new(
495 PathBuf::from("A"),
496 PathBuf::from("B"),
497 DependencyType::Import,
498 ));
499 graph.add_edge(DependencyEdge::new(
500 PathBuf::from("A"),
501 PathBuf::from("C"),
502 DependencyType::Import,
503 ));
504 graph.add_edge(DependencyEdge::new(
505 PathBuf::from("B"),
506 PathBuf::from("D"),
507 DependencyType::Import,
508 ));
509 graph.add_edge(DependencyEdge::new(
510 PathBuf::from("C"),
511 PathBuf::from("D"),
512 DependencyType::Import,
513 ));
514
515 let detector = InvalidationDetector::new(graph);
516 let affected = detector.propagate_invalidation(&PathBuf::from("D"));
517
518 assert_eq!(affected.len(), 4);
520 assert!(affected.contains(&PathBuf::from("A")));
521 assert!(affected.contains(&PathBuf::from("B")));
522 assert!(affected.contains(&PathBuf::from("C")));
523 assert!(affected.contains(&PathBuf::from("D")));
524 }
525
526 #[test]
527 fn test_propagate_respects_strong_dependencies_only() {
528 let mut graph = DependencyGraph::new();
529 graph.add_edge(DependencyEdge::new(
531 PathBuf::from("A"),
532 PathBuf::from("B"),
533 DependencyType::Import, ));
535 graph.add_edge(DependencyEdge::new(
536 PathBuf::from("C"),
537 PathBuf::from("B"),
538 DependencyType::Export, ));
540
541 let detector = InvalidationDetector::new(graph);
542 let affected = detector.propagate_invalidation(&PathBuf::from("B"));
543
544 assert!(affected.contains(&PathBuf::from("A")));
546 assert!(affected.contains(&PathBuf::from("B")));
547 assert!(
548 !affected.contains(&PathBuf::from("C")),
549 "Weak dependencies should not propagate invalidation"
550 );
551 }
552
553 #[test]
554 fn test_propagate_stops_at_frontier() {
555 let mut graph = DependencyGraph::new();
556 graph.add_edge(DependencyEdge::new(
558 PathBuf::from("A"),
559 PathBuf::from("B"),
560 DependencyType::Import,
561 ));
562 graph.add_edge(DependencyEdge::new(
563 PathBuf::from("C"),
564 PathBuf::from("D"),
565 DependencyType::Import,
566 ));
567
568 let detector = InvalidationDetector::new(graph);
569 let affected = detector.propagate_invalidation(&PathBuf::from("B"));
570
571 assert_eq!(affected.len(), 2);
573 assert!(affected.contains(&PathBuf::from("A")));
574 assert!(affected.contains(&PathBuf::from("B")));
575 assert!(!affected.contains(&PathBuf::from("C")));
576 assert!(!affected.contains(&PathBuf::from("D")));
577 }
578
579 #[test]
580 fn test_propagate_unknown_file() {
581 let graph = DependencyGraph::new();
582 let detector = InvalidationDetector::new(graph);
583 let affected = detector.propagate_invalidation(&PathBuf::from("unknown.rs"));
584
585 assert_eq!(affected.len(), 1);
587 assert_eq!(affected[0], PathBuf::from("unknown.rs"));
588 }
589
590 #[test]
593 fn test_topological_sort_linear_chain() {
594 let mut graph = DependencyGraph::new();
595 graph.add_edge(DependencyEdge::new(
597 PathBuf::from("A"),
598 PathBuf::from("B"),
599 DependencyType::Import,
600 ));
601 graph.add_edge(DependencyEdge::new(
602 PathBuf::from("B"),
603 PathBuf::from("C"),
604 DependencyType::Import,
605 ));
606
607 let detector = InvalidationDetector::new(graph);
608 let sorted = detector
609 .topological_sort(&[PathBuf::from("A"), PathBuf::from("B"), PathBuf::from("C")])
610 .unwrap();
611
612 assert_eq!(sorted.len(), 3);
613
614 let pos_a = sorted
616 .iter()
617 .position(|p| p == &PathBuf::from("A"))
618 .unwrap();
619 let pos_b = sorted
620 .iter()
621 .position(|p| p == &PathBuf::from("B"))
622 .unwrap();
623 let pos_c = sorted
624 .iter()
625 .position(|p| p == &PathBuf::from("C"))
626 .unwrap();
627
628 assert!(pos_c < pos_b, "C must come before B");
629 assert!(pos_b < pos_a, "B must come before A");
630 }
631
632 #[test]
633 fn test_topological_sort_diamond() {
634 let mut graph = DependencyGraph::new();
635 graph.add_edge(DependencyEdge::new(
637 PathBuf::from("A"),
638 PathBuf::from("B"),
639 DependencyType::Import,
640 ));
641 graph.add_edge(DependencyEdge::new(
642 PathBuf::from("A"),
643 PathBuf::from("C"),
644 DependencyType::Import,
645 ));
646 graph.add_edge(DependencyEdge::new(
647 PathBuf::from("B"),
648 PathBuf::from("D"),
649 DependencyType::Import,
650 ));
651 graph.add_edge(DependencyEdge::new(
652 PathBuf::from("C"),
653 PathBuf::from("D"),
654 DependencyType::Import,
655 ));
656
657 let detector = InvalidationDetector::new(graph);
658 let sorted = detector
659 .topological_sort(&[
660 PathBuf::from("A"),
661 PathBuf::from("B"),
662 PathBuf::from("C"),
663 PathBuf::from("D"),
664 ])
665 .unwrap();
666
667 assert_eq!(sorted.len(), 4);
668
669 let pos_a = sorted
670 .iter()
671 .position(|p| p == &PathBuf::from("A"))
672 .unwrap();
673 let pos_b = sorted
674 .iter()
675 .position(|p| p == &PathBuf::from("B"))
676 .unwrap();
677 let pos_c = sorted
678 .iter()
679 .position(|p| p == &PathBuf::from("C"))
680 .unwrap();
681 let pos_d = sorted
682 .iter()
683 .position(|p| p == &PathBuf::from("D"))
684 .unwrap();
685
686 assert!(pos_d < pos_b, "D must come before B");
688 assert!(pos_d < pos_c, "D must come before C");
689 assert!(pos_b < pos_a, "B must come before A");
690 assert!(pos_c < pos_a, "C must come before A");
691 }
692
693 #[test]
694 fn test_topological_sort_disconnected_components() {
695 let mut graph = DependencyGraph::new();
696 graph.add_edge(DependencyEdge::new(
698 PathBuf::from("A"),
699 PathBuf::from("B"),
700 DependencyType::Import,
701 ));
702 graph.add_edge(DependencyEdge::new(
703 PathBuf::from("C"),
704 PathBuf::from("D"),
705 DependencyType::Import,
706 ));
707
708 let detector = InvalidationDetector::new(graph);
709 let sorted = detector
710 .topological_sort(&[
711 PathBuf::from("A"),
712 PathBuf::from("B"),
713 PathBuf::from("C"),
714 PathBuf::from("D"),
715 ])
716 .unwrap();
717
718 assert_eq!(sorted.len(), 4);
719
720 let pos_a = sorted
722 .iter()
723 .position(|p| p == &PathBuf::from("A"))
724 .unwrap();
725 let pos_b = sorted
726 .iter()
727 .position(|p| p == &PathBuf::from("B"))
728 .unwrap();
729 let pos_c = sorted
730 .iter()
731 .position(|p| p == &PathBuf::from("C"))
732 .unwrap();
733 let pos_d = sorted
734 .iter()
735 .position(|p| p == &PathBuf::from("D"))
736 .unwrap();
737
738 assert!(pos_b < pos_a, "B must come before A");
739 assert!(pos_d < pos_c, "D must come before C");
740 }
741
742 #[test]
743 fn test_topological_sort_single_file() {
744 let graph = DependencyGraph::new();
745 let detector = InvalidationDetector::new(graph);
746 let sorted = detector
747 .topological_sort(&[PathBuf::from("only.rs")])
748 .unwrap();
749
750 assert_eq!(sorted, vec![PathBuf::from("only.rs")]);
751 }
752
753 #[test]
754 fn test_topological_sort_empty_set() {
755 let graph = DependencyGraph::new();
756 let detector = InvalidationDetector::new(graph);
757 let sorted = detector.topological_sort(&[]).unwrap();
758
759 assert!(sorted.is_empty());
760 }
761
762 #[test]
763 fn test_topological_sort_cycle_error() {
764 let mut graph = DependencyGraph::new();
765 graph.add_edge(DependencyEdge::new(
767 PathBuf::from("A"),
768 PathBuf::from("B"),
769 DependencyType::Import,
770 ));
771 graph.add_edge(DependencyEdge::new(
772 PathBuf::from("B"),
773 PathBuf::from("A"),
774 DependencyType::Import,
775 ));
776
777 let detector = InvalidationDetector::new(graph);
778 let result = detector.topological_sort(&[PathBuf::from("A"), PathBuf::from("B")]);
779
780 assert!(result.is_err());
781 match result.unwrap_err() {
782 InvalidationError::CircularDependency(cycle) => {
783 assert!(!cycle.is_empty(), "Cycle should contain file paths");
784 }
785 _ => panic!("Expected CircularDependency error"),
786 }
787 }
788
789 #[test]
790 fn test_topological_sort_self_loop() {
791 let mut graph = DependencyGraph::new();
792 graph.add_edge(DependencyEdge::new(
794 PathBuf::from("A"),
795 PathBuf::from("A"),
796 DependencyType::Import,
797 ));
798
799 let detector = InvalidationDetector::new(graph);
800 let result = detector.topological_sort(&[PathBuf::from("A")]);
801
802 assert!(result.is_err());
803 match result.unwrap_err() {
804 InvalidationError::CircularDependency(_) => {
805 }
807 _ => panic!("Expected CircularDependency error"),
808 }
809 }
810
811 #[test]
814 fn test_compute_invalidation_single_change() {
815 let mut graph = DependencyGraph::new();
816 graph.add_edge(DependencyEdge::new(
818 PathBuf::from("A"),
819 PathBuf::from("B"),
820 DependencyType::Import,
821 ));
822
823 let detector = InvalidationDetector::new(graph);
824 let result = detector.compute_invalidation_set(&[PathBuf::from("B")]);
825
826 assert_eq!(result.invalidated_files.len(), 2);
828 assert!(result.invalidated_files.contains(&PathBuf::from("A")));
829 assert!(result.invalidated_files.contains(&PathBuf::from("B")));
830
831 assert_eq!(result.analysis_order.len(), 2);
833 let pos_a = result
834 .analysis_order
835 .iter()
836 .position(|p| p == &PathBuf::from("A"))
837 .unwrap();
838 let pos_b = result
839 .analysis_order
840 .iter()
841 .position(|p| p == &PathBuf::from("B"))
842 .unwrap();
843 assert!(pos_b < pos_a, "B must come before A in analysis order");
844
845 assert!(result.circular_dependencies.is_empty());
847 }
848
849 #[test]
850 fn test_compute_invalidation_transitive() {
851 let mut graph = DependencyGraph::new();
852 graph.add_edge(DependencyEdge::new(
854 PathBuf::from("A"),
855 PathBuf::from("B"),
856 DependencyType::Import,
857 ));
858 graph.add_edge(DependencyEdge::new(
859 PathBuf::from("B"),
860 PathBuf::from("C"),
861 DependencyType::Import,
862 ));
863
864 let detector = InvalidationDetector::new(graph);
865 let result = detector.compute_invalidation_set(&[PathBuf::from("C")]);
866
867 assert_eq!(result.invalidated_files.len(), 3);
868 assert!(result.invalidated_files.contains(&PathBuf::from("A")));
869 assert!(result.invalidated_files.contains(&PathBuf::from("B")));
870 assert!(result.invalidated_files.contains(&PathBuf::from("C")));
871
872 assert_eq!(result.analysis_order.len(), 3);
874 let pos_a = result
875 .analysis_order
876 .iter()
877 .position(|p| p == &PathBuf::from("A"))
878 .unwrap();
879 let pos_b = result
880 .analysis_order
881 .iter()
882 .position(|p| p == &PathBuf::from("B"))
883 .unwrap();
884 let pos_c = result
885 .analysis_order
886 .iter()
887 .position(|p| p == &PathBuf::from("C"))
888 .unwrap();
889 assert!(pos_c < pos_b);
890 assert!(pos_b < pos_a);
891
892 assert!(result.circular_dependencies.is_empty());
893 }
894
895 #[test]
896 fn test_compute_invalidation_multiple_changes() {
897 let mut graph = DependencyGraph::new();
898 graph.add_edge(DependencyEdge::new(
900 PathBuf::from("A"),
901 PathBuf::from("C"),
902 DependencyType::Import,
903 ));
904 graph.add_edge(DependencyEdge::new(
905 PathBuf::from("B"),
906 PathBuf::from("D"),
907 DependencyType::Import,
908 ));
909
910 let detector = InvalidationDetector::new(graph);
911 let result = detector.compute_invalidation_set(&[PathBuf::from("C"), PathBuf::from("D")]);
912
913 assert_eq!(result.invalidated_files.len(), 4);
914 assert!(result.invalidated_files.contains(&PathBuf::from("A")));
915 assert!(result.invalidated_files.contains(&PathBuf::from("B")));
916 assert!(result.invalidated_files.contains(&PathBuf::from("C")));
917 assert!(result.invalidated_files.contains(&PathBuf::from("D")));
918
919 assert!(result.circular_dependencies.is_empty());
920 }
921
922 #[test]
923 fn test_compute_invalidation_empty_changes() {
924 let graph = DependencyGraph::new();
925 let detector = InvalidationDetector::new(graph);
926 let result = detector.compute_invalidation_set(&[]);
927
928 assert!(result.invalidated_files.is_empty());
929 assert!(result.analysis_order.is_empty());
930 assert!(result.circular_dependencies.is_empty());
931 }
932
933 #[test]
934 fn test_compute_invalidation_unknown_files() {
935 let graph = DependencyGraph::new();
936 let detector = InvalidationDetector::new(graph);
937 let result = detector.compute_invalidation_set(&[PathBuf::from("unknown.rs")]);
938
939 assert_eq!(result.invalidated_files.len(), 1);
941 assert!(
942 result
943 .invalidated_files
944 .contains(&PathBuf::from("unknown.rs"))
945 );
946 }
947
948 #[test]
949 fn test_compute_invalidation_with_cycle() {
950 let mut graph = DependencyGraph::new();
951 graph.add_edge(DependencyEdge::new(
953 PathBuf::from("A"),
954 PathBuf::from("B"),
955 DependencyType::Import,
956 ));
957 graph.add_edge(DependencyEdge::new(
958 PathBuf::from("B"),
959 PathBuf::from("A"),
960 DependencyType::Import,
961 ));
962 graph.add_edge(DependencyEdge::new(
963 PathBuf::from("C"),
964 PathBuf::from("A"),
965 DependencyType::Import,
966 ));
967
968 let detector = InvalidationDetector::new(graph);
969 let result = detector.compute_invalidation_set(&[PathBuf::from("A")]);
970
971 assert_eq!(result.invalidated_files.len(), 3);
973
974 assert!(!result.circular_dependencies.is_empty());
976 assert!(
977 result.circular_dependencies.iter().any(|cycle| {
978 cycle.contains(&PathBuf::from("A")) && cycle.contains(&PathBuf::from("B"))
979 }),
980 "Should detect cycle involving A and B"
981 );
982 }
983
984 #[test]
985 fn test_compute_invalidation_multiple_cycles() {
986 let mut graph = DependencyGraph::new();
987 graph.add_edge(DependencyEdge::new(
989 PathBuf::from("A"),
990 PathBuf::from("B"),
991 DependencyType::Import,
992 ));
993 graph.add_edge(DependencyEdge::new(
994 PathBuf::from("B"),
995 PathBuf::from("A"),
996 DependencyType::Import,
997 ));
998 graph.add_edge(DependencyEdge::new(
999 PathBuf::from("C"),
1000 PathBuf::from("D"),
1001 DependencyType::Import,
1002 ));
1003 graph.add_edge(DependencyEdge::new(
1004 PathBuf::from("D"),
1005 PathBuf::from("C"),
1006 DependencyType::Import,
1007 ));
1008
1009 let detector = InvalidationDetector::new(graph);
1010 let result = detector.compute_invalidation_set(&[PathBuf::from("A"), PathBuf::from("C")]);
1011
1012 assert_eq!(result.circular_dependencies.len(), 2);
1014 }
1015
1016 #[test]
1017 fn test_compute_invalidation_partial_cycle() {
1018 let mut graph = DependencyGraph::new();
1019 graph.add_edge(DependencyEdge::new(
1021 PathBuf::from("A"),
1022 PathBuf::from("B"),
1023 DependencyType::Import,
1024 ));
1025 graph.add_edge(DependencyEdge::new(
1026 PathBuf::from("B"),
1027 PathBuf::from("C"),
1028 DependencyType::Import,
1029 ));
1030 graph.add_edge(DependencyEdge::new(
1031 PathBuf::from("C"),
1032 PathBuf::from("B"),
1033 DependencyType::Import,
1034 ));
1035 graph.add_edge(DependencyEdge::new(
1036 PathBuf::from("D"),
1037 PathBuf::from("A"),
1038 DependencyType::Import,
1039 ));
1040
1041 let detector = InvalidationDetector::new(graph);
1042 let result = detector.compute_invalidation_set(&[PathBuf::from("B")]);
1043
1044 assert!(!result.circular_dependencies.is_empty());
1046 let cycle = &result.circular_dependencies[0];
1047 assert!(cycle.contains(&PathBuf::from("B")));
1048 assert!(cycle.contains(&PathBuf::from("C")));
1049 assert!(!cycle.contains(&PathBuf::from("A")));
1051 assert!(!cycle.contains(&PathBuf::from("D")));
1052 }
1053
1054 #[test]
1057 fn test_find_scc_no_cycles() {
1058 let mut graph = DependencyGraph::new();
1059 graph.add_edge(DependencyEdge::new(
1061 PathBuf::from("A"),
1062 PathBuf::from("B"),
1063 DependencyType::Import,
1064 ));
1065 graph.add_edge(DependencyEdge::new(
1066 PathBuf::from("B"),
1067 PathBuf::from("C"),
1068 DependencyType::Import,
1069 ));
1070
1071 let detector = InvalidationDetector::new(graph);
1072 let files: RapidSet<PathBuf> = [PathBuf::from("A"), PathBuf::from("B"), PathBuf::from("C")]
1073 .into_iter()
1074 .collect();
1075 let sccs = detector.find_strongly_connected_components(&files);
1076
1077 assert!(sccs.is_empty());
1079 }
1080
1081 #[test]
1082 fn test_find_scc_simple_cycle() {
1083 let mut graph = DependencyGraph::new();
1084 graph.add_edge(DependencyEdge::new(
1086 PathBuf::from("A"),
1087 PathBuf::from("B"),
1088 DependencyType::Import,
1089 ));
1090 graph.add_edge(DependencyEdge::new(
1091 PathBuf::from("B"),
1092 PathBuf::from("A"),
1093 DependencyType::Import,
1094 ));
1095
1096 let detector = InvalidationDetector::new(graph);
1097 let files: RapidSet<PathBuf> = [PathBuf::from("A"), PathBuf::from("B")]
1098 .into_iter()
1099 .collect();
1100 let sccs = detector.find_strongly_connected_components(&files);
1101
1102 assert_eq!(sccs.len(), 1);
1103 assert_eq!(sccs[0].len(), 2);
1104 assert!(sccs[0].contains(&PathBuf::from("A")));
1105 assert!(sccs[0].contains(&PathBuf::from("B")));
1106 }
1107
1108 #[test]
1109 fn test_find_scc_self_loop() {
1110 let mut graph = DependencyGraph::new();
1111 graph.add_edge(DependencyEdge::new(
1113 PathBuf::from("A"),
1114 PathBuf::from("A"),
1115 DependencyType::Import,
1116 ));
1117
1118 let detector = InvalidationDetector::new(graph);
1119 let files: RapidSet<PathBuf> = [PathBuf::from("A")].into_iter().collect();
1120 let sccs = detector.find_strongly_connected_components(&files);
1121
1122 assert_eq!(sccs.len(), 1);
1124 assert_eq!(sccs[0].len(), 1);
1125 assert_eq!(sccs[0][0], PathBuf::from("A"));
1126 }
1127
1128 #[test]
1129 fn test_find_scc_multiple_cycles() {
1130 let mut graph = DependencyGraph::new();
1131 graph.add_edge(DependencyEdge::new(
1133 PathBuf::from("A"),
1134 PathBuf::from("B"),
1135 DependencyType::Import,
1136 ));
1137 graph.add_edge(DependencyEdge::new(
1138 PathBuf::from("B"),
1139 PathBuf::from("A"),
1140 DependencyType::Import,
1141 ));
1142 graph.add_edge(DependencyEdge::new(
1143 PathBuf::from("C"),
1144 PathBuf::from("D"),
1145 DependencyType::Import,
1146 ));
1147 graph.add_edge(DependencyEdge::new(
1148 PathBuf::from("D"),
1149 PathBuf::from("C"),
1150 DependencyType::Import,
1151 ));
1152
1153 let detector = InvalidationDetector::new(graph);
1154 let files: RapidSet<PathBuf> = [
1155 PathBuf::from("A"),
1156 PathBuf::from("B"),
1157 PathBuf::from("C"),
1158 PathBuf::from("D"),
1159 ]
1160 .into_iter()
1161 .collect();
1162 let sccs = detector.find_strongly_connected_components(&files);
1163
1164 assert_eq!(sccs.len(), 2);
1165 }
1166
1167 #[test]
1168 fn test_find_scc_nested_components() {
1169 let mut graph = DependencyGraph::new();
1170 graph.add_edge(DependencyEdge::new(
1172 PathBuf::from("A"),
1173 PathBuf::from("B"),
1174 DependencyType::Import,
1175 ));
1176 graph.add_edge(DependencyEdge::new(
1177 PathBuf::from("B"),
1178 PathBuf::from("C"),
1179 DependencyType::Import,
1180 ));
1181 graph.add_edge(DependencyEdge::new(
1182 PathBuf::from("C"),
1183 PathBuf::from("B"),
1184 DependencyType::Import,
1185 ));
1186 graph.add_edge(DependencyEdge::new(
1187 PathBuf::from("A"),
1188 PathBuf::from("D"),
1189 DependencyType::Import,
1190 ));
1191
1192 let detector = InvalidationDetector::new(graph);
1193 let files: RapidSet<PathBuf> = [
1194 PathBuf::from("A"),
1195 PathBuf::from("B"),
1196 PathBuf::from("C"),
1197 PathBuf::from("D"),
1198 ]
1199 .into_iter()
1200 .collect();
1201 let sccs = detector.find_strongly_connected_components(&files);
1202
1203 assert_eq!(sccs.len(), 1);
1205 assert_eq!(sccs[0].len(), 2);
1206 assert!(sccs[0].contains(&PathBuf::from("B")));
1207 assert!(sccs[0].contains(&PathBuf::from("C")));
1208 }
1209
1210 #[test]
1213 fn test_large_graph_performance() {
1214 let mut graph = DependencyGraph::new();
1216 for i in 0..999 {
1217 graph.add_edge(DependencyEdge::new(
1218 PathBuf::from(format!("file_{}", i)),
1219 PathBuf::from(format!("file_{}", i + 1)),
1220 DependencyType::Import,
1221 ));
1222 }
1223
1224 let detector = InvalidationDetector::new(graph);
1225 let start = std::time::Instant::now();
1226 let result = detector.compute_invalidation_set(&[PathBuf::from("file_500")]);
1227 let duration = start.elapsed();
1228
1229 assert!(
1231 duration.as_millis() < 50,
1232 "Large graph processing took {}ms (expected < 50ms)",
1233 duration.as_millis()
1234 );
1235 assert!(result.invalidated_files.len() >= 500);
1236 }
1237
1238 #[test]
1239 fn test_wide_fanout_performance() {
1240 let mut graph = DependencyGraph::new();
1242 for i in 0..100 {
1243 graph.add_edge(DependencyEdge::new(
1244 PathBuf::from(format!("dependent_{}", i)),
1245 PathBuf::from("core.rs"),
1246 DependencyType::Import,
1247 ));
1248 }
1249
1250 let detector = InvalidationDetector::new(graph);
1251 let start = std::time::Instant::now();
1252 let result = detector.compute_invalidation_set(&[PathBuf::from("core.rs")]);
1253 let duration = start.elapsed();
1254
1255 assert!(duration.as_millis() < 10);
1256 assert_eq!(result.invalidated_files.len(), 101); }
1258}