1use super::types::{
4 CycleError, ImportDecl, ModuleGraph, ModuleInfo, ModulePath, ModuleRegistry,
5 ModuleResolutionResult,
6};
7use std::collections::{HashMap, HashSet, VecDeque};
8use std::path::PathBuf;
9use std::str::FromStr;
10
11#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ModulePathParseError(pub String);
18
19impl std::fmt::Display for ModulePathParseError {
20 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
21 write!(f, "invalid module path: {:?}", self.0)
22 }
23}
24
25impl std::error::Error for ModulePathParseError {}
26
27impl FromStr for ModulePath {
28 type Err = ModulePathParseError;
29
30 fn from_str(s: &str) -> Result<Self, Self::Err> {
31 if s.is_empty() {
32 return Err(ModulePathParseError(s.to_owned()));
33 }
34 let components: Vec<String> = s.split('.').map(|c| c.to_owned()).collect();
35 if components.iter().any(|c| c.is_empty()) {
36 return Err(ModulePathParseError(s.to_owned()));
37 }
38 Ok(Self { components })
39 }
40}
41
42impl ModulePath {
43 pub fn parse(s: &str) -> Option<Self> {
46 s.parse().ok()
47 }
48
49 pub fn to_file_path(&self) -> PathBuf {
54 let mut p: PathBuf = self.components.iter().collect();
55 p.set_extension("lean");
56 p
57 }
58
59 pub fn to_oxilean_file_path(&self) -> PathBuf {
61 let mut p: PathBuf = self.components.iter().collect();
62 p.set_extension("oxilean");
63 p
64 }
65}
66
67impl ModuleRegistry {
72 pub fn new(roots: Vec<PathBuf>) -> Self {
74 Self {
75 roots,
76 cache: HashMap::new(),
77 }
78 }
79
80 pub fn resolve(&mut self, path: &ModulePath) -> ModuleResolutionResult {
83 if let Some(info) = self.cache.get(path) {
84 return ModuleResolutionResult::Found(info.clone());
85 }
86
87 let lean_rel = path.to_file_path();
88 let oxilean_rel = path.to_oxilean_file_path();
89
90 for root in &self.roots {
91 let lean_abs = root.join(&lean_rel);
92 let oxilean_abs = root.join(&oxilean_rel);
93
94 let found = if lean_abs.exists() {
95 Some(lean_abs)
96 } else if oxilean_abs.exists() {
97 Some(oxilean_abs)
98 } else {
99 None
100 };
101
102 if let Some(file_path) = found {
103 let info = ModuleInfo {
104 path: file_path,
105 exports: Vec::new(),
106 dependencies: Vec::new(),
107 };
108 self.cache.insert(path.clone(), info.clone());
109 return ModuleResolutionResult::Found(info);
110 }
111 }
112
113 ModuleResolutionResult::NotFound(path.clone())
114 }
115
116 pub fn register(&mut self, path: ModulePath, info: ModuleInfo) {
118 self.cache.insert(path, info);
119 }
120}
121
122pub fn parse_import_decl(input: &str) -> Option<ImportDecl> {
136 let trimmed = input.trim();
137 let rest = trimmed.strip_prefix("import")?.trim_start();
138 if rest.is_empty() {
139 return None;
140 }
141
142 if let Some(paren_pos) = rest.find('(') {
144 let module_str = rest[..paren_pos].trim();
145 let module = ModulePath::parse(module_str)?;
146
147 let after_open = rest[paren_pos + 1..].trim_end_matches(|c: char| c.is_whitespace());
148 let inner = after_open.strip_suffix(')')?;
149 let selective: Vec<String> = inner
150 .split(',')
151 .map(|s| s.trim().to_owned())
152 .filter(|s| !s.is_empty())
153 .collect();
154
155 return Some(ImportDecl {
156 module,
157 alias: None,
158 selective,
159 });
160 }
161
162 let tokens: Vec<&str> = rest.split_whitespace().collect();
165 match tokens.as_slice() {
166 [module_str] => {
167 let module = ModulePath::parse(module_str)?;
168 Some(ImportDecl {
169 module,
170 alias: None,
171 selective: Vec::new(),
172 })
173 }
174 [module_str, "as", alias] => {
175 let module = ModulePath::parse(module_str)?;
176 Some(ImportDecl {
177 module,
178 alias: Some((*alias).to_owned()),
179 selective: Vec::new(),
180 })
181 }
182 _ => None,
183 }
184}
185
186pub fn parse_imports(source: &str) -> Vec<ImportDecl> {
190 source
191 .lines()
192 .filter_map(|line| parse_import_decl(line.trim()))
193 .collect()
194}
195
196pub fn build_module_graph(registry: &ModuleRegistry) -> ModuleGraph {
202 let mut graph = ModuleGraph::default();
203
204 for (module_path, info) in ®istry.cache {
205 graph.nodes.insert(module_path.clone(), info.clone());
206 for dep in &info.dependencies {
207 graph.edges.push((module_path.clone(), dep.clone()));
208 }
209 }
210
211 graph
212}
213
214pub fn direct_deps_of<'a>(graph: &'a ModuleGraph, module: &ModulePath) -> Vec<&'a ModulePath> {
220 graph
221 .edges
222 .iter()
223 .filter_map(|(from, to)| if from == module { Some(to) } else { None })
224 .collect()
225}
226
227pub fn transitive_deps(graph: &ModuleGraph, module: &ModulePath) -> Vec<ModulePath> {
231 let mut visited: HashSet<ModulePath> = HashSet::new();
232 let mut queue: VecDeque<ModulePath> = VecDeque::new();
233
234 for dep in direct_deps_of(graph, module) {
235 if !visited.contains(dep) {
236 visited.insert(dep.clone());
237 queue.push_back(dep.clone());
238 }
239 }
240
241 while let Some(current) = queue.pop_front() {
242 for dep in direct_deps_of(graph, ¤t) {
243 if !visited.contains(dep) {
244 visited.insert(dep.clone());
245 queue.push_back(dep.clone());
246 }
247 }
248 }
249
250 let mut result: Vec<ModulePath> = visited.into_iter().collect();
251 result.sort();
252 result
253}
254
255pub fn topological_sort(graph: &ModuleGraph) -> Result<Vec<ModulePath>, CycleError> {
265 let mut in_degree: HashMap<&ModulePath, usize> = HashMap::new();
267 let mut adjacency: HashMap<&ModulePath, Vec<&ModulePath>> = HashMap::new();
268
269 for node in graph.nodes.keys() {
270 in_degree.entry(node).or_insert(0);
271 adjacency.entry(node).or_default();
272 }
273
274 for (from, to) in &graph.edges {
275 *in_degree.entry(to).or_insert(0) += 1;
276 adjacency.entry(from).or_default().push(to);
277 adjacency.entry(to).or_default();
279 }
280
281 let mut queue: VecDeque<&ModulePath> = in_degree
283 .iter()
284 .filter_map(|(k, &v)| if v == 0 { Some(*k) } else { None })
285 .collect();
286
287 let mut queue_vec: Vec<&ModulePath> = queue.drain(..).collect();
289 queue_vec.sort();
290 let mut queue: VecDeque<&ModulePath> = queue_vec.into_iter().collect();
291
292 let mut sorted: Vec<ModulePath> = Vec::new();
293
294 while let Some(node) = queue.pop_front() {
295 sorted.push(node.clone());
296 if let Some(neighbors) = adjacency.get(node) {
297 let mut next_batch: Vec<&ModulePath> = Vec::new();
298 for &neighbor in neighbors {
299 let deg = in_degree.entry(neighbor).or_insert(0);
300 if *deg > 0 {
301 *deg -= 1;
302 if *deg == 0 {
303 next_batch.push(neighbor);
304 }
305 }
306 }
307 next_batch.sort();
308 for n in next_batch {
309 queue.push_back(n);
310 }
311 }
312 }
313
314 if sorted.len() != graph.nodes.len() {
315 let in_cycle: Vec<ModulePath> = graph
317 .nodes
318 .keys()
319 .filter(|n| !sorted.contains(n))
320 .cloned()
321 .collect();
322
323 let cycle = find_one_cycle(graph, &in_cycle);
325 return Err(CycleError { cycle });
326 }
327
328 Ok(sorted)
329}
330
331fn find_one_cycle(graph: &ModuleGraph, candidates: &[ModulePath]) -> Vec<ModulePath> {
333 let candidate_set: HashSet<&ModulePath> = candidates.iter().collect();
334 let mut visited: HashSet<&ModulePath> = HashSet::new();
335 let mut stack: Vec<&ModulePath> = Vec::new();
336
337 for start in candidates {
338 if !visited.contains(start) {
339 if let Some(cycle) = dfs_cycle(graph, start, &candidate_set, &mut visited, &mut stack) {
340 return cycle;
341 }
342 }
343 }
344
345 candidates.to_vec()
347}
348
349fn dfs_cycle<'a>(
350 graph: &'a ModuleGraph,
351 node: &'a ModulePath,
352 candidates: &HashSet<&'a ModulePath>,
353 visited: &mut HashSet<&'a ModulePath>,
354 stack: &mut Vec<&'a ModulePath>,
355) -> Option<Vec<ModulePath>> {
356 visited.insert(node);
357 stack.push(node);
358
359 for (from, to) in &graph.edges {
360 if from != node {
361 continue;
362 }
363 if !candidates.contains(to) {
364 continue;
365 }
366 if let Some(pos) = stack.iter().position(|&s| s == to) {
367 let cycle: Vec<ModulePath> = stack[pos..].iter().map(|&p| p.clone()).collect();
369 return Some(cycle);
370 }
371 if !visited.contains(to) {
372 if let Some(cycle) = dfs_cycle(graph, to, candidates, visited, stack) {
373 return Some(cycle);
374 }
375 }
376 }
377
378 stack.pop();
379 None
380}
381
382pub fn detect_cycles(graph: &ModuleGraph) -> Vec<CycleError> {
392 let sccs = tarjan_sccs(graph);
393 let mut errors: Vec<CycleError> = Vec::new();
394
395 for scc in sccs {
396 if scc.len() == 1 {
397 let node = &scc[0];
399 let self_loop = graph
400 .edges
401 .iter()
402 .any(|(from, to)| from == node && to == node);
403 if !self_loop {
404 continue;
405 }
406 }
407 let cycle = find_one_cycle(graph, &scc);
409 errors.push(CycleError { cycle });
410 }
411
412 errors
413}
414
415fn tarjan_sccs(graph: &ModuleGraph) -> Vec<Vec<ModulePath>> {
417 struct State<'a> {
418 index_counter: usize,
419 stack: Vec<&'a ModulePath>,
420 on_stack: HashSet<&'a ModulePath>,
421 index: HashMap<&'a ModulePath, usize>,
422 lowlink: HashMap<&'a ModulePath, usize>,
423 sccs: Vec<Vec<ModulePath>>,
424 }
425
426 fn strongconnect<'a>(v: &'a ModulePath, graph: &'a ModuleGraph, state: &mut State<'a>) {
427 let idx = state.index_counter;
428 state.index.insert(v, idx);
429 state.lowlink.insert(v, idx);
430 state.index_counter += 1;
431 state.stack.push(v);
432 state.on_stack.insert(v);
433
434 for (from, to) in &graph.edges {
435 if from != v {
436 continue;
437 }
438 if !graph.nodes.contains_key(to) {
439 continue;
440 }
441 if !state.index.contains_key(to) {
442 strongconnect(to, graph, state);
443 let ll_to = *state.lowlink.get(to).unwrap_or(&usize::MAX);
444 let ll_v = state.lowlink.get(v).copied().unwrap_or(usize::MAX);
445 state.lowlink.insert(v, ll_v.min(ll_to));
446 } else if state.on_stack.contains(to) {
447 let idx_to = *state.index.get(to).unwrap_or(&usize::MAX);
448 let ll_v = state.lowlink.get(v).copied().unwrap_or(usize::MAX);
449 state.lowlink.insert(v, ll_v.min(idx_to));
450 }
451 }
452
453 let ll_v = *state.lowlink.get(v).unwrap_or(&usize::MAX);
455 let idx_v = *state.index.get(v).unwrap_or(&usize::MAX);
456 if ll_v == idx_v {
457 let mut scc: Vec<ModulePath> = Vec::new();
458 while let Some(w) = state.stack.pop() {
459 state.on_stack.remove(w);
460 scc.push(w.clone());
461 if w == v {
462 break;
463 }
464 }
465 state.sccs.push(scc);
466 }
467 }
468
469 let mut state = State {
470 index_counter: 0,
471 stack: Vec::new(),
472 on_stack: HashSet::new(),
473 index: HashMap::new(),
474 lowlink: HashMap::new(),
475 sccs: Vec::new(),
476 };
477
478 let mut nodes: Vec<&ModulePath> = graph.nodes.keys().collect();
479 nodes.sort();
480
481 for node in nodes {
482 if !state.index.contains_key(node) {
483 strongconnect(node, graph, &mut state);
484 }
485 }
486
487 state.sccs
488}
489
490#[cfg(test)]
495mod tests {
496 use super::*;
497 use std::env;
498 use std::fs;
499
500 fn mp(s: &str) -> ModulePath {
503 ModulePath::parse(s).expect("valid module path")
504 }
505
506 fn info_with_deps(deps: &[&str]) -> ModuleInfo {
507 ModuleInfo {
508 path: PathBuf::from("dummy.lean"),
509 exports: Vec::new(),
510 dependencies: deps.iter().map(|s| mp(s)).collect(),
511 }
512 }
513
514 fn graph_from_edges(pairs: &[(&str, &str)]) -> ModuleGraph {
515 let mut g = ModuleGraph::default();
516 for &(from, to) in pairs {
517 let f = mp(from);
518 let t = mp(to);
519 g.nodes
520 .entry(f.clone())
521 .or_insert_with(|| info_with_deps(&[]));
522 g.nodes
523 .entry(t.clone())
524 .or_insert_with(|| info_with_deps(&[]));
525 g.edges.push((f, t));
526 }
527 g
528 }
529
530 #[test]
533 fn test_module_path_from_str_single() {
534 let p = ModulePath::parse("Std").expect("valid");
535 assert_eq!(p.components, vec!["Std"]);
536 }
537
538 #[test]
539 fn test_module_path_from_str_multi() {
540 let p = mp("Mathlib.Algebra.Ring");
541 assert_eq!(p.components, vec!["Mathlib", "Algebra", "Ring"]);
542 }
543
544 #[test]
545 fn test_module_path_from_str_empty_is_none() {
546 assert!(ModulePath::parse("").is_none());
547 }
548
549 #[test]
550 fn test_module_path_from_str_double_dot_is_none() {
551 assert!(ModulePath::parse("A..B").is_none());
552 }
553
554 #[test]
555 fn test_module_path_from_str_trailing_dot_is_none() {
556 assert!(ModulePath::parse("A.B.").is_none());
557 }
558
559 #[test]
562 fn test_to_file_path_single() {
563 let p = mp("Std");
564 assert_eq!(p.to_file_path(), PathBuf::from("Std.lean"));
565 }
566
567 #[test]
568 fn test_to_file_path_multi() {
569 let p = mp("Mathlib.Algebra.Ring");
570 assert_eq!(p.to_file_path(), PathBuf::from("Mathlib/Algebra/Ring.lean"));
571 }
572
573 #[test]
574 fn test_to_oxilean_file_path() {
575 let p = mp("Foo.Bar");
576 assert_eq!(p.to_oxilean_file_path(), PathBuf::from("Foo/Bar.oxilean"));
577 }
578
579 #[test]
582 fn test_to_string() {
583 let p = mp("A.B.C");
584 assert_eq!(p.to_string(), "A.B.C");
585 }
586
587 #[test]
588 fn test_display() {
589 let p = mp("X.Y");
590 assert_eq!(format!("{p}"), "X.Y");
591 }
592
593 #[test]
596 fn test_parse_import_bare() {
597 let decl = parse_import_decl("import Foo.Bar").expect("valid");
598 assert_eq!(decl.module, mp("Foo.Bar"));
599 assert!(decl.alias.is_none());
600 assert!(decl.selective.is_empty());
601 }
602
603 #[test]
604 fn test_parse_import_alias() {
605 let decl = parse_import_decl("import Foo.Bar as FB").expect("valid");
606 assert_eq!(decl.module, mp("Foo.Bar"));
607 assert_eq!(decl.alias.as_deref(), Some("FB"));
608 assert!(decl.selective.is_empty());
609 }
610
611 #[test]
612 fn test_parse_import_selective() {
613 let decl = parse_import_decl("import Foo.Bar (add, mul)").expect("valid");
614 assert_eq!(decl.module, mp("Foo.Bar"));
615 assert!(decl.alias.is_none());
616 assert_eq!(decl.selective, vec!["add", "mul"]);
617 }
618
619 #[test]
620 fn test_parse_import_leading_whitespace() {
621 let decl = parse_import_decl(" import Std").expect("valid");
622 assert_eq!(decl.module, mp("Std"));
623 }
624
625 #[test]
626 fn test_parse_import_not_import() {
627 assert!(parse_import_decl("def foo := 1").is_none());
628 }
629
630 #[test]
631 fn test_parse_import_empty_line() {
632 assert!(parse_import_decl("").is_none());
633 }
634
635 #[test]
638 fn test_parse_imports_multiple() {
639 let src = r#"
640import Std
641import Mathlib.Algebra.Ring as MAR
642def foo := 1
643import Foo (bar, baz)
644"#;
645 let imports = parse_imports(src);
646 assert_eq!(imports.len(), 3);
647 assert_eq!(imports[0].module, mp("Std"));
648 assert_eq!(imports[1].alias.as_deref(), Some("MAR"));
649 assert_eq!(imports[2].selective, vec!["bar", "baz"]);
650 }
651
652 #[test]
653 fn test_parse_imports_empty_source() {
654 assert!(parse_imports("").is_empty());
655 }
656
657 #[test]
660 fn test_registry_new_empty() {
661 let reg = ModuleRegistry::new(vec![]);
662 assert!(reg.cache.is_empty());
663 }
664
665 #[test]
666 fn test_registry_register_and_resolve() {
667 let mut reg = ModuleRegistry::new(vec![]);
668 let path = mp("Foo.Bar");
669 let info = ModuleInfo {
670 path: PathBuf::from("Foo/Bar.lean"),
671 exports: vec!["baz".to_owned()],
672 dependencies: vec![],
673 };
674 reg.register(path.clone(), info.clone());
675 match reg.resolve(&path) {
676 ModuleResolutionResult::Found(i) => assert_eq!(i.path, PathBuf::from("Foo/Bar.lean")),
677 other => panic!("expected Found, got {other:?}"),
678 }
679 }
680
681 #[test]
682 fn test_registry_not_found() {
683 let mut reg = ModuleRegistry::new(vec![]);
684 let path = mp("Nonexistent");
685 assert_eq!(reg.resolve(&path), ModuleResolutionResult::NotFound(path));
686 }
687
688 #[test]
689 fn test_registry_resolve_from_filesystem() {
690 let tmp = env::temp_dir().join("oxilean_test_registry_resolve");
691 fs::create_dir_all(&tmp).expect("create temp dir");
692 let module_dir = tmp.join("Foo");
693 fs::create_dir_all(&module_dir).expect("create Foo dir");
694 let lean_file = module_dir.join("Bar.lean");
695 fs::write(&lean_file, "-- empty").expect("write lean file");
696
697 let mut reg = ModuleRegistry::new(vec![tmp.clone()]);
698 let path = mp("Foo.Bar");
699 match reg.resolve(&path) {
700 ModuleResolutionResult::Found(info) => {
701 assert!(info.path.ends_with("Foo/Bar.lean"));
702 }
703 other => panic!("expected Found, got {other:?}"),
704 }
705
706 let _ = fs::remove_dir_all(&tmp);
708 }
709
710 #[test]
711 fn test_registry_resolve_oxilean_extension() {
712 let tmp = env::temp_dir().join("oxilean_test_registry_oxilean_ext");
713 fs::create_dir_all(&tmp).expect("create temp dir");
714 let module_dir = tmp.join("Ox");
715 fs::create_dir_all(&module_dir).expect("create dir");
716 let file = module_dir.join("Mod.oxilean");
717 fs::write(&file, "-- empty").expect("write file");
718
719 let mut reg = ModuleRegistry::new(vec![tmp.clone()]);
720 let path = mp("Ox.Mod");
721 match reg.resolve(&path) {
722 ModuleResolutionResult::Found(info) => {
723 assert!(info.path.ends_with("Ox/Mod.oxilean"));
724 }
725 other => panic!("expected Found, got {other:?}"),
726 }
727
728 let _ = fs::remove_dir_all(&tmp);
729 }
730
731 #[test]
732 fn test_registry_cache_hit() {
733 let tmp = env::temp_dir().join("oxilean_test_cache_hit");
734 fs::create_dir_all(&tmp).expect("create temp dir");
735 let file = tmp.join("Cache.lean");
736 fs::write(&file, "").expect("write");
737
738 let mut reg = ModuleRegistry::new(vec![tmp.clone()]);
739 let path = mp("Cache");
740
741 let r1 = reg.resolve(&path);
743 let _ = fs::remove_file(&file);
745 let r2 = reg.resolve(&path);
746
747 assert_eq!(r1, r2);
748 let _ = fs::remove_dir_all(&tmp);
749 }
750
751 #[test]
754 fn test_build_module_graph_empty() {
755 let reg = ModuleRegistry::new(vec![]);
756 let g = build_module_graph(®);
757 assert!(g.nodes.is_empty());
758 assert!(g.edges.is_empty());
759 }
760
761 #[test]
762 fn test_build_module_graph_nodes_and_edges() {
763 let mut reg = ModuleRegistry::new(vec![]);
764 reg.register(
765 mp("A"),
766 ModuleInfo {
767 path: PathBuf::from("A.lean"),
768 exports: vec![],
769 dependencies: vec![mp("B")],
770 },
771 );
772 reg.register(
773 mp("B"),
774 ModuleInfo {
775 path: PathBuf::from("B.lean"),
776 exports: vec![],
777 dependencies: vec![],
778 },
779 );
780 let g = build_module_graph(®);
781 assert_eq!(g.nodes.len(), 2);
782 assert!(g.edges.contains(&(mp("A"), mp("B"))));
783 }
784
785 #[test]
788 fn test_direct_deps_of_empty() {
789 let g = graph_from_edges(&[]);
790 let deps = direct_deps_of(&g, &mp("A"));
791 assert!(deps.is_empty());
792 }
793
794 #[test]
795 fn test_direct_deps_of_one() {
796 let g = graph_from_edges(&[("A", "B"), ("A", "C"), ("B", "C")]);
797 let deps = direct_deps_of(&g, &mp("A"));
798 let mut names: Vec<String> = deps.iter().map(|p| p.to_string()).collect();
799 names.sort();
800 assert_eq!(names, vec!["B", "C"]);
801 }
802
803 #[test]
806 fn test_transitive_deps_chain() {
807 let g = graph_from_edges(&[("A", "B"), ("B", "C")]);
809 let mut deps = transitive_deps(&g, &mp("A"));
810 deps.sort();
811 assert!(deps.contains(&mp("B")));
812 assert!(deps.contains(&mp("C")));
813 assert!(!deps.contains(&mp("A")));
814 }
815
816 #[test]
817 fn test_transitive_deps_diamond() {
818 let g = graph_from_edges(&[("A", "B"), ("A", "C"), ("B", "D"), ("C", "D")]);
820 let deps = transitive_deps(&g, &mp("A"));
821 assert!(deps.contains(&mp("B")));
822 assert!(deps.contains(&mp("C")));
823 assert!(deps.contains(&mp("D")));
824 assert_eq!(deps.len(), 3);
825 }
826
827 #[test]
830 fn test_topo_sort_empty() {
831 let g = ModuleGraph::default();
832 let sorted = topological_sort(&g).expect("no cycle");
833 assert!(sorted.is_empty());
834 }
835
836 #[test]
837 fn test_topo_sort_chain() {
838 let g = graph_from_edges(&[("A", "B"), ("B", "C")]);
842 let sorted = topological_sort(&g).expect("no cycle");
843 let pos_a = sorted
844 .iter()
845 .position(|p| p == &mp("A"))
846 .expect("A present");
847 let pos_b = sorted
848 .iter()
849 .position(|p| p == &mp("B"))
850 .expect("B present");
851 let pos_c = sorted
852 .iter()
853 .position(|p| p == &mp("C"))
854 .expect("C present");
855 assert!(pos_a < pos_b, "A should precede B; sorted={sorted:?}");
857 assert!(pos_b < pos_c, "B should precede C; sorted={sorted:?}");
858 }
859
860 #[test]
861 fn test_topo_sort_cycle_returns_err() {
862 let g = graph_from_edges(&[("A", "B"), ("B", "A")]);
864 assert!(topological_sort(&g).is_err());
865 }
866
867 #[test]
870 fn test_detect_cycles_none() {
871 let g = graph_from_edges(&[("A", "B"), ("B", "C")]);
872 assert!(detect_cycles(&g).is_empty());
873 }
874
875 #[test]
876 fn test_detect_cycles_simple() {
877 let g = graph_from_edges(&[("A", "B"), ("B", "A")]);
879 let cycles = detect_cycles(&g);
880 assert!(!cycles.is_empty());
881 let all_nodes: Vec<ModulePath> = cycles.into_iter().flat_map(|c| c.cycle).collect();
883 assert!(all_nodes.contains(&mp("A")) || all_nodes.contains(&mp("B")));
884 }
885
886 #[test]
887 fn test_detect_cycles_self_loop() {
888 let mut g = ModuleGraph::default();
889 g.nodes.insert(mp("A"), info_with_deps(&[]));
890 g.edges.push((mp("A"), mp("A")));
891 let cycles = detect_cycles(&g);
892 assert!(!cycles.is_empty());
893 }
894
895 #[test]
896 fn test_detect_cycles_two_distinct() {
897 let g = graph_from_edges(&[("A", "B"), ("B", "A"), ("C", "D"), ("D", "C")]);
899 let cycles = detect_cycles(&g);
900 assert!(cycles.len() >= 2);
902 }
903
904 #[test]
907 fn test_cycle_error_display() {
908 let err = CycleError {
909 cycle: vec![mp("A"), mp("B"), mp("C")],
910 };
911 let s = format!("{err}");
912 assert!(s.contains("A"));
913 assert!(s.contains("B"));
914 assert!(s.contains("C"));
915 }
916}