use super::graph::{DependencyGraph, GraphError};
use metrics::histogram;
use std::path::{Path, PathBuf};
use std::time::Instant;
use thread_utilities::{RapidMap, RapidSet};
use tracing::{info, warn};
#[derive(Debug, thiserror::Error)]
pub enum InvalidationError {
#[error("Circular dependency detected: {0:?}")]
CircularDependency(Vec<PathBuf>),
#[error("Graph error: {0}")]
Graph(String),
}
#[derive(Debug, Clone)]
pub struct InvalidationResult {
pub invalidated_files: Vec<PathBuf>,
pub analysis_order: Vec<PathBuf>,
pub circular_dependencies: Vec<Vec<PathBuf>>,
}
#[derive(Debug, Clone)]
pub struct InvalidationDetector {
graph: DependencyGraph,
}
impl InvalidationDetector {
pub fn new(graph: DependencyGraph) -> Self {
Self { graph }
}
pub fn compute_invalidation_set(&self, changed_files: &[PathBuf]) -> InvalidationResult {
let start = Instant::now();
info!(
"computing invalidation set for {} changed files",
changed_files.len()
);
let changed_set: RapidSet<PathBuf> = changed_files.iter().cloned().collect();
let affected = self.graph.find_affected_files(&changed_set);
let invalidated_files: Vec<PathBuf> = affected.iter().cloned().collect();
info!(
"found {} files affected by changes",
invalidated_files.len()
);
let result = match self.topological_sort(&invalidated_files) {
Ok(analysis_order) => {
info!("topological sort successful");
InvalidationResult {
invalidated_files,
analysis_order,
circular_dependencies: vec![],
}
}
Err(_) => {
warn!("circular dependencies detected");
let cycles = self.find_strongly_connected_components(&affected);
InvalidationResult {
invalidated_files,
analysis_order: vec![],
circular_dependencies: cycles,
}
}
};
let duration_ms = start.elapsed().as_micros() as f64 / 1000.0;
histogram!("invalidation_time_ms").record(duration_ms);
info!(
invalidated_count = result.invalidated_files.len(),
cycles = result.circular_dependencies.len(),
duration_ms = %format!("{:.2}", duration_ms),
"invalidation complete"
);
result
}
pub fn topological_sort(&self, files: &[PathBuf]) -> Result<Vec<PathBuf>, InvalidationError> {
let files_set: RapidSet<PathBuf> = files.iter().cloned().collect();
self.graph
.topological_sort(&files_set)
.map_err(|e| match e {
GraphError::CyclicDependency(path) => {
InvalidationError::CircularDependency(vec![path])
}
})
}
pub fn propagate_invalidation(&self, root: &Path) -> Vec<PathBuf> {
let root_set: RapidSet<PathBuf> = [root.to_path_buf()].into_iter().collect();
let affected: RapidSet<PathBuf> = self.graph.find_affected_files(&root_set);
affected.into_iter().collect()
}
fn find_strongly_connected_components(&self, files: &RapidSet<PathBuf>) -> Vec<Vec<PathBuf>> {
let mut state = TarjanState::new();
let mut sccs = Vec::new();
for file in files {
if !state.indices.contains_key(file) {
self.tarjan_dfs(file, &mut state, &mut sccs);
}
}
sccs.into_iter()
.filter(|scc| {
scc.len() > 1 || (scc.len() == 1 && self.has_self_loop(&scc[0]))
})
.collect()
}
fn tarjan_dfs(&self, v: &Path, state: &mut TarjanState, sccs: &mut Vec<Vec<PathBuf>>) {
let index = state.index_counter;
state.indices.insert(v.to_path_buf(), index);
state.lowlinks.insert(v.to_path_buf(), index);
state.index_counter += 1;
state.stack.push(v.to_path_buf());
state.on_stack.insert(v.to_path_buf());
let dependencies = self.graph.get_dependencies(v);
for edge in dependencies {
let dep = &edge.to;
if !state.indices.contains_key(dep) {
self.tarjan_dfs(dep, state, sccs);
let w_lowlink = *state.lowlinks.get(dep).unwrap();
let v_lowlink = state.lowlinks.get_mut(&v.to_path_buf()).unwrap();
*v_lowlink = (*v_lowlink).min(w_lowlink);
} else if state.on_stack.contains(dep) {
let w_index = *state.indices.get(dep).unwrap();
let v_lowlink = state.lowlinks.get_mut(&v.to_path_buf()).unwrap();
*v_lowlink = (*v_lowlink).min(w_index);
}
}
let v_index = *state.indices.get(&v.to_path_buf()).unwrap();
let v_lowlink = *state.lowlinks.get(&v.to_path_buf()).unwrap();
if v_lowlink == v_index {
let mut scc = Vec::new();
loop {
let w = state.stack.pop().unwrap();
state.on_stack.remove(&w);
scc.push(w.clone());
if w == v {
break;
}
}
sccs.push(scc);
}
}
fn has_self_loop(&self, file: &Path) -> bool {
let deps = self.graph.get_dependencies(file);
deps.iter().any(|edge| edge.to == file)
}
}
struct TarjanState {
index_counter: usize,
indices: RapidMap<PathBuf, usize>,
lowlinks: RapidMap<PathBuf, usize>,
stack: Vec<PathBuf>,
on_stack: RapidSet<PathBuf>,
}
impl TarjanState {
fn new() -> Self {
Self {
index_counter: 0,
indices: thread_utilities::get_map(),
lowlinks: thread_utilities::get_map(),
stack: Vec::new(),
on_stack: thread_utilities::get_set(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::types::{DependencyEdge, DependencyType};
#[test]
fn test_invalidation_detector_new() {
let graph = DependencyGraph::new();
let detector = InvalidationDetector::new(graph);
assert_eq!(detector.graph.node_count(), 0);
assert_eq!(detector.graph.edge_count(), 0);
}
#[test]
fn test_invalidation_detector_with_populated_graph() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
assert_eq!(detector.graph.node_count(), 2);
assert_eq!(detector.graph.edge_count(), 1);
}
#[test]
fn test_propagate_single_file_no_dependents() {
let mut graph = DependencyGraph::new();
graph.add_node(&PathBuf::from("isolated.rs"));
let detector = InvalidationDetector::new(graph);
let affected = detector.propagate_invalidation(&PathBuf::from("isolated.rs"));
assert_eq!(affected.len(), 1);
assert_eq!(affected[0], PathBuf::from("isolated.rs"));
}
#[test]
fn test_propagate_linear_chain() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let affected = detector.propagate_invalidation(&PathBuf::from("C"));
assert_eq!(affected.len(), 3);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(affected.contains(&PathBuf::from("C")));
}
#[test]
fn test_propagate_diamond_dependency() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("D"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let affected = detector.propagate_invalidation(&PathBuf::from("D"));
assert_eq!(affected.len(), 4);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(affected.contains(&PathBuf::from("C")));
assert!(affected.contains(&PathBuf::from("D")));
}
#[test]
fn test_propagate_respects_strong_dependencies_only() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import, ));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("B"),
DependencyType::Export, ));
let detector = InvalidationDetector::new(graph);
let affected = detector.propagate_invalidation(&PathBuf::from("B"));
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(
!affected.contains(&PathBuf::from("C")),
"Weak dependencies should not propagate invalidation"
);
}
#[test]
fn test_propagate_stops_at_frontier() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let affected = detector.propagate_invalidation(&PathBuf::from("B"));
assert_eq!(affected.len(), 2);
assert!(affected.contains(&PathBuf::from("A")));
assert!(affected.contains(&PathBuf::from("B")));
assert!(!affected.contains(&PathBuf::from("C")));
assert!(!affected.contains(&PathBuf::from("D")));
}
#[test]
fn test_propagate_unknown_file() {
let graph = DependencyGraph::new();
let detector = InvalidationDetector::new(graph);
let affected = detector.propagate_invalidation(&PathBuf::from("unknown.rs"));
assert_eq!(affected.len(), 1);
assert_eq!(affected[0], PathBuf::from("unknown.rs"));
}
#[test]
fn test_topological_sort_linear_chain() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let sorted = detector
.topological_sort(&[PathBuf::from("A"), PathBuf::from("B"), PathBuf::from("C")])
.unwrap();
assert_eq!(sorted.len(), 3);
let pos_a = sorted
.iter()
.position(|p| p == &PathBuf::from("A"))
.unwrap();
let pos_b = sorted
.iter()
.position(|p| p == &PathBuf::from("B"))
.unwrap();
let pos_c = sorted
.iter()
.position(|p| p == &PathBuf::from("C"))
.unwrap();
assert!(pos_c < pos_b, "C must come before B");
assert!(pos_b < pos_a, "B must come before A");
}
#[test]
fn test_topological_sort_diamond() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("D"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let sorted = detector
.topological_sort(&[
PathBuf::from("A"),
PathBuf::from("B"),
PathBuf::from("C"),
PathBuf::from("D"),
])
.unwrap();
assert_eq!(sorted.len(), 4);
let pos_a = sorted
.iter()
.position(|p| p == &PathBuf::from("A"))
.unwrap();
let pos_b = sorted
.iter()
.position(|p| p == &PathBuf::from("B"))
.unwrap();
let pos_c = sorted
.iter()
.position(|p| p == &PathBuf::from("C"))
.unwrap();
let pos_d = sorted
.iter()
.position(|p| p == &PathBuf::from("D"))
.unwrap();
assert!(pos_d < pos_b, "D must come before B");
assert!(pos_d < pos_c, "D must come before C");
assert!(pos_b < pos_a, "B must come before A");
assert!(pos_c < pos_a, "C must come before A");
}
#[test]
fn test_topological_sort_disconnected_components() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let sorted = detector
.topological_sort(&[
PathBuf::from("A"),
PathBuf::from("B"),
PathBuf::from("C"),
PathBuf::from("D"),
])
.unwrap();
assert_eq!(sorted.len(), 4);
let pos_a = sorted
.iter()
.position(|p| p == &PathBuf::from("A"))
.unwrap();
let pos_b = sorted
.iter()
.position(|p| p == &PathBuf::from("B"))
.unwrap();
let pos_c = sorted
.iter()
.position(|p| p == &PathBuf::from("C"))
.unwrap();
let pos_d = sorted
.iter()
.position(|p| p == &PathBuf::from("D"))
.unwrap();
assert!(pos_b < pos_a, "B must come before A");
assert!(pos_d < pos_c, "D must come before C");
}
#[test]
fn test_topological_sort_single_file() {
let graph = DependencyGraph::new();
let detector = InvalidationDetector::new(graph);
let sorted = detector
.topological_sort(&[PathBuf::from("only.rs")])
.unwrap();
assert_eq!(sorted, vec![PathBuf::from("only.rs")]);
}
#[test]
fn test_topological_sort_empty_set() {
let graph = DependencyGraph::new();
let detector = InvalidationDetector::new(graph);
let sorted = detector.topological_sort(&[]).unwrap();
assert!(sorted.is_empty());
}
#[test]
fn test_topological_sort_cycle_error() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("A"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.topological_sort(&[PathBuf::from("A"), PathBuf::from("B")]);
assert!(result.is_err());
match result.unwrap_err() {
InvalidationError::CircularDependency(cycle) => {
assert!(!cycle.is_empty(), "Cycle should contain file paths");
}
_ => panic!("Expected CircularDependency error"),
}
}
#[test]
fn test_topological_sort_self_loop() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("A"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.topological_sort(&[PathBuf::from("A")]);
assert!(result.is_err());
match result.unwrap_err() {
InvalidationError::CircularDependency(_) => {
}
_ => panic!("Expected CircularDependency error"),
}
}
#[test]
fn test_compute_invalidation_single_change() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("B")]);
assert_eq!(result.invalidated_files.len(), 2);
assert!(result.invalidated_files.contains(&PathBuf::from("A")));
assert!(result.invalidated_files.contains(&PathBuf::from("B")));
assert_eq!(result.analysis_order.len(), 2);
let pos_a = result
.analysis_order
.iter()
.position(|p| p == &PathBuf::from("A"))
.unwrap();
let pos_b = result
.analysis_order
.iter()
.position(|p| p == &PathBuf::from("B"))
.unwrap();
assert!(pos_b < pos_a, "B must come before A in analysis order");
assert!(result.circular_dependencies.is_empty());
}
#[test]
fn test_compute_invalidation_transitive() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("C")]);
assert_eq!(result.invalidated_files.len(), 3);
assert!(result.invalidated_files.contains(&PathBuf::from("A")));
assert!(result.invalidated_files.contains(&PathBuf::from("B")));
assert!(result.invalidated_files.contains(&PathBuf::from("C")));
assert_eq!(result.analysis_order.len(), 3);
let pos_a = result
.analysis_order
.iter()
.position(|p| p == &PathBuf::from("A"))
.unwrap();
let pos_b = result
.analysis_order
.iter()
.position(|p| p == &PathBuf::from("B"))
.unwrap();
let pos_c = result
.analysis_order
.iter()
.position(|p| p == &PathBuf::from("C"))
.unwrap();
assert!(pos_c < pos_b);
assert!(pos_b < pos_a);
assert!(result.circular_dependencies.is_empty());
}
#[test]
fn test_compute_invalidation_multiple_changes() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("D"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("C"), PathBuf::from("D")]);
assert_eq!(result.invalidated_files.len(), 4);
assert!(result.invalidated_files.contains(&PathBuf::from("A")));
assert!(result.invalidated_files.contains(&PathBuf::from("B")));
assert!(result.invalidated_files.contains(&PathBuf::from("C")));
assert!(result.invalidated_files.contains(&PathBuf::from("D")));
assert!(result.circular_dependencies.is_empty());
}
#[test]
fn test_compute_invalidation_empty_changes() {
let graph = DependencyGraph::new();
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[]);
assert!(result.invalidated_files.is_empty());
assert!(result.analysis_order.is_empty());
assert!(result.circular_dependencies.is_empty());
}
#[test]
fn test_compute_invalidation_unknown_files() {
let graph = DependencyGraph::new();
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("unknown.rs")]);
assert_eq!(result.invalidated_files.len(), 1);
assert!(
result
.invalidated_files
.contains(&PathBuf::from("unknown.rs"))
);
}
#[test]
fn test_compute_invalidation_with_cycle() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("A"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("A"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("A")]);
assert_eq!(result.invalidated_files.len(), 3);
assert!(!result.circular_dependencies.is_empty());
assert!(
result.circular_dependencies.iter().any(|cycle| {
cycle.contains(&PathBuf::from("A")) && cycle.contains(&PathBuf::from("B"))
}),
"Should detect cycle involving A and B"
);
}
#[test]
fn test_compute_invalidation_multiple_cycles() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("A"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("D"),
PathBuf::from("C"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("A"), PathBuf::from("C")]);
assert_eq!(result.circular_dependencies.len(), 2);
}
#[test]
fn test_compute_invalidation_partial_cycle() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("D"),
PathBuf::from("A"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let result = detector.compute_invalidation_set(&[PathBuf::from("B")]);
assert!(!result.circular_dependencies.is_empty());
let cycle = &result.circular_dependencies[0];
assert!(cycle.contains(&PathBuf::from("B")));
assert!(cycle.contains(&PathBuf::from("C")));
assert!(!cycle.contains(&PathBuf::from("A")));
assert!(!cycle.contains(&PathBuf::from("D")));
}
#[test]
fn test_find_scc_no_cycles() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let files: RapidSet<PathBuf> = [PathBuf::from("A"), PathBuf::from("B"), PathBuf::from("C")]
.into_iter()
.collect();
let sccs = detector.find_strongly_connected_components(&files);
assert!(sccs.is_empty());
}
#[test]
fn test_find_scc_simple_cycle() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("A"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let files: RapidSet<PathBuf> = [PathBuf::from("A"), PathBuf::from("B")]
.into_iter()
.collect();
let sccs = detector.find_strongly_connected_components(&files);
assert_eq!(sccs.len(), 1);
assert_eq!(sccs[0].len(), 2);
assert!(sccs[0].contains(&PathBuf::from("A")));
assert!(sccs[0].contains(&PathBuf::from("B")));
}
#[test]
fn test_find_scc_self_loop() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("A"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let files: RapidSet<PathBuf> = [PathBuf::from("A")].into_iter().collect();
let sccs = detector.find_strongly_connected_components(&files);
assert_eq!(sccs.len(), 1);
assert_eq!(sccs[0].len(), 1);
assert_eq!(sccs[0][0], PathBuf::from("A"));
}
#[test]
fn test_find_scc_multiple_cycles() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("A"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("D"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("D"),
PathBuf::from("C"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let files: RapidSet<PathBuf> = [
PathBuf::from("A"),
PathBuf::from("B"),
PathBuf::from("C"),
PathBuf::from("D"),
]
.into_iter()
.collect();
let sccs = detector.find_strongly_connected_components(&files);
assert_eq!(sccs.len(), 2);
}
#[test]
fn test_find_scc_nested_components() {
let mut graph = DependencyGraph::new();
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("B"),
PathBuf::from("C"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("C"),
PathBuf::from("B"),
DependencyType::Import,
));
graph.add_edge(DependencyEdge::new(
PathBuf::from("A"),
PathBuf::from("D"),
DependencyType::Import,
));
let detector = InvalidationDetector::new(graph);
let files: RapidSet<PathBuf> = [
PathBuf::from("A"),
PathBuf::from("B"),
PathBuf::from("C"),
PathBuf::from("D"),
]
.into_iter()
.collect();
let sccs = detector.find_strongly_connected_components(&files);
assert_eq!(sccs.len(), 1);
assert_eq!(sccs[0].len(), 2);
assert!(sccs[0].contains(&PathBuf::from("B")));
assert!(sccs[0].contains(&PathBuf::from("C")));
}
#[test]
fn test_large_graph_performance() {
let mut graph = DependencyGraph::new();
for i in 0..999 {
graph.add_edge(DependencyEdge::new(
PathBuf::from(format!("file_{}", i)),
PathBuf::from(format!("file_{}", i + 1)),
DependencyType::Import,
));
}
let detector = InvalidationDetector::new(graph);
let start = std::time::Instant::now();
let result = detector.compute_invalidation_set(&[PathBuf::from("file_500")]);
let duration = start.elapsed();
assert!(
duration.as_millis() < 50,
"Large graph processing took {}ms (expected < 50ms)",
duration.as_millis()
);
assert!(result.invalidated_files.len() >= 500);
}
#[test]
fn test_wide_fanout_performance() {
let mut graph = DependencyGraph::new();
for i in 0..100 {
graph.add_edge(DependencyEdge::new(
PathBuf::from(format!("dependent_{}", i)),
PathBuf::from("core.rs"),
DependencyType::Import,
));
}
let detector = InvalidationDetector::new(graph);
let start = std::time::Instant::now();
let result = detector.compute_invalidation_set(&[PathBuf::from("core.rs")]);
let duration = start.elapsed();
assert!(duration.as_millis() < 10);
assert_eq!(result.invalidated_files.len(), 101); }
}