1use crate::model::*;
2use std::collections::{HashMap, HashSet, VecDeque};
3
4fn is_high_confidence(kind: &EdgeKind) -> bool {
9 matches!(
10 kind,
11 EdgeKind::Calls | EdgeKind::Extends | EdgeKind::Implements | EdgeKind::Embeds
12 )
13}
14
15const CALLABLE_KINDS: &[SymbolKind] = &[
21 SymbolKind::Function,
22 SymbolKind::Method,
23 SymbolKind::Class,
24 SymbolKind::Struct,
25];
26
27const HTTP_DECORATORS: &[&str] = &[
29 "get",
30 "post",
31 "put",
32 "delete",
33 "patch",
34 "app.route",
35 "router.get",
36 "router.post",
37 "api_view",
38 "route",
39];
40
41const CLI_DECORATORS: &[&str] = &["command", "subcommand", "clap", "parser"];
43
44pub fn detect_entry_points(
46 symbols: &[SymbolNode],
47 edges: &[Edge],
48 config: &FlowConfig,
49) -> Vec<EntryPoint> {
50 let excluded: HashSet<&str> = config
51 .excluded_entry_points
52 .iter()
53 .map(|s| s.as_str())
54 .collect();
55 let extra: HashSet<&str> = config
56 .extra_entry_points
57 .iter()
58 .map(|s| s.as_str())
59 .collect();
60
61 let has_incoming_calls: HashSet<&str> = edges
63 .iter()
64 .filter(|e| e.kind == EdgeKind::Calls)
65 .map(|e| e.target.as_str())
66 .collect();
67
68 let mut outgoing_count: HashMap<&str, usize> = HashMap::new();
70 for edge in edges {
71 if is_high_confidence(&edge.kind) {
72 *outgoing_count.entry(edge.source.as_str()).or_default() += 1;
73 }
74 }
75
76 let mut results: Vec<EntryPoint> = Vec::new();
77 let mut public_roots: Vec<EntryPoint> = Vec::new();
78
79 for sym in symbols {
80 let qn = sym.qualified_name.as_str();
81
82 if excluded.contains(qn) {
84 continue;
85 }
86
87 if let Some(ep) = classify_symbol(sym, &has_incoming_calls, &extra) {
89 if ep.kind == EntryPointKind::PublicRoot {
90 let mut ep_with_score = ep;
91 ep_with_score.confidence = 0.7;
93 public_roots.push(ep_with_score);
94 } else {
95 results.push(ep);
96 }
97 }
98 }
99
100 public_roots.sort_by(|a, b| {
102 let a_out = outgoing_count
103 .get(a.qualified_name.as_str())
104 .copied()
105 .unwrap_or(0);
106 let b_out = outgoing_count
107 .get(b.qualified_name.as_str())
108 .copied()
109 .unwrap_or(0);
110 b_out.cmp(&a_out)
111 });
112 public_roots.truncate(config.max_public_roots);
113 results.extend(public_roots);
114
115 results
116}
117
118fn classify_symbol(
119 sym: &SymbolNode,
120 has_incoming_calls: &HashSet<&str>,
121 extra: &HashSet<&str>,
122) -> Option<EntryPoint> {
123 let qn = &sym.qualified_name;
124 let name_lower = sym.name.to_lowercase();
125 let is_extra = extra.contains(qn.as_str());
126
127 if name_lower == "main" {
129 return Some(EntryPoint {
130 qualified_name: qn.clone(),
131 kind: EntryPointKind::Main,
132 confidence: 1.0,
133 });
134 }
135 if sym.decorators.iter().any(|d| d.contains("tokio::main")) {
136 return Some(EntryPoint {
137 qualified_name: qn.clone(),
138 kind: EntryPointKind::Main,
139 confidence: 1.0,
140 });
141 }
142
143 if sym.is_test || sym.kind == SymbolKind::Test {
145 return Some(EntryPoint {
146 qualified_name: qn.clone(),
147 kind: EntryPointKind::Test,
148 confidence: 1.0,
149 });
150 }
151 if name_lower.starts_with("test_") {
152 return Some(EntryPoint {
153 qualified_name: qn.clone(),
154 kind: EntryPointKind::Test,
155 confidence: 0.9,
156 });
157 }
158
159 for decorator in &sym.decorators {
161 let dec_lower = decorator.to_lowercase();
162 if HTTP_DECORATORS.iter().any(|h| dec_lower.contains(h)) {
163 return Some(EntryPoint {
164 qualified_name: qn.clone(),
165 kind: EntryPointKind::HttpHandler,
166 confidence: 1.0,
167 });
168 }
169 }
170
171 for decorator in &sym.decorators {
173 let dec_lower = decorator.to_lowercase();
174 if CLI_DECORATORS.iter().any(|c| dec_lower.contains(c)) {
175 return Some(EntryPoint {
176 qualified_name: qn.clone(),
177 kind: EntryPointKind::CliCommand,
178 confidence: 1.0,
179 });
180 }
181 }
182
183 if is_extra {
185 return Some(EntryPoint {
186 qualified_name: qn.clone(),
187 kind: EntryPointKind::PublicRoot,
188 confidence: 0.8,
189 });
190 }
191
192 if sym.is_exported
194 && sym.visibility == Visibility::Public
195 && CALLABLE_KINDS.contains(&sym.kind)
196 && !has_incoming_calls.contains(qn.as_str())
197 {
198 return Some(EntryPoint {
199 qualified_name: qn.clone(),
200 kind: EntryPointKind::PublicRoot,
201 confidence: 0.7,
202 });
203 }
204
205 None
206}
207
208pub fn brandes_betweenness(nodes: &HashSet<String>, edges: &[Edge]) -> HashMap<String, f64> {
216 let n = nodes.len();
217 if n < 2 {
218 return nodes.iter().map(|node| (node.clone(), 0.0)).collect();
219 }
220
221 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
223 for node in nodes {
224 adj.entry(node.as_str()).or_default();
225 }
226 for edge in edges {
227 if is_high_confidence(&edge.kind)
228 && nodes.contains(&edge.source)
229 && nodes.contains(&edge.target)
230 {
231 adj.entry(edge.source.as_str())
232 .or_default()
233 .push(edge.target.as_str());
234 }
235 }
236
237 let mut centrality: HashMap<String, f64> = nodes.iter().map(|n| (n.clone(), 0.0)).collect();
238
239 for source in nodes {
241 let mut stack: Vec<&str> = Vec::new();
242 let mut predecessors: HashMap<&str, Vec<&str>> = HashMap::new();
243 let mut sigma: HashMap<&str, f64> = HashMap::new();
244 let mut dist: HashMap<&str, i64> = HashMap::new();
245
246 for node in nodes {
247 sigma.insert(node.as_str(), 0.0);
248 dist.insert(node.as_str(), -1);
249 }
250 sigma.insert(source.as_str(), 1.0);
251 dist.insert(source.as_str(), 0);
252
253 let mut queue: VecDeque<&str> = VecDeque::new();
254 queue.push_back(source.as_str());
255
256 while let Some(v) = queue.pop_front() {
258 stack.push(v);
259 let d_v = dist[v];
260 if let Some(neighbors) = adj.get(v) {
261 for &w in neighbors {
262 if dist[w] < 0 {
264 dist.insert(w, d_v + 1);
265 queue.push_back(w);
266 }
267 if dist[w] == d_v + 1 {
269 *sigma.get_mut(w).unwrap() += sigma[v];
270 predecessors.entry(w).or_default().push(v);
271 }
272 }
273 }
274 }
275
276 let mut delta: HashMap<&str, f64> = nodes.iter().map(|n| (n.as_str(), 0.0)).collect();
278 while let Some(w) = stack.pop() {
279 if let Some(preds) = predecessors.get(w) {
280 for &v in preds {
281 let d = (sigma[v] / sigma[w]) * (1.0 + delta[w]);
282 *delta.get_mut(v).unwrap() += d;
283 }
284 }
285 if w != source.as_str() {
286 *centrality.get_mut(w).unwrap() += delta[w];
287 }
288 }
289 }
290
291 let norm = (n as f64 - 1.0) * (n as f64 - 2.0);
293 if norm > 0.0 {
294 for val in centrality.values_mut() {
295 *val /= norm;
296 }
297 }
298
299 centrality
300}
301
302pub fn enumerate_flows(
310 entry_points: &[EntryPoint],
311 edges: &[Edge],
312 config: &FlowConfig,
313) -> Vec<ExecutionFlow> {
314 let mut adj: HashMap<&str, Vec<&str>> = HashMap::new();
316 for edge in edges {
317 if is_high_confidence(&edge.kind) {
318 adj.entry(edge.source.as_str())
319 .or_default()
320 .push(edge.target.as_str());
321 }
322 }
323
324 let mut all_flows: Vec<ExecutionFlow> = Vec::new();
325
326 for ep in entry_points {
327 if all_flows.len() >= config.max_flows {
328 break;
329 }
330
331 let mut ep_flows: Vec<ExecutionFlow> = Vec::new();
332 let mut visit_count: usize = 0;
333 let mut truncated = false;
334
335 let mut stack: Vec<(String, Vec<String>, HashSet<String>)> = Vec::new();
338 let initial_visited: HashSet<String> = [ep.qualified_name.clone()].into_iter().collect();
339 stack.push((
340 ep.qualified_name.clone(),
341 vec![ep.qualified_name.clone()],
342 initial_visited,
343 ));
344
345 while let Some((node, path, visited)) = stack.pop() {
346 visit_count += 1;
347 if visit_count > config.visit_budget {
348 truncated = true;
349 break;
350 }
351 if all_flows.len() + ep_flows.len() >= config.max_flows {
352 break;
353 }
354
355 let neighbors: Vec<&str> = adj
356 .get(node.as_str())
357 .map(|v| v.as_slice())
358 .unwrap_or(&[])
359 .iter()
360 .filter(|&&n| !visited.contains(n))
361 .copied()
362 .collect();
363
364 if neighbors.is_empty() || path.len() >= config.max_depth {
365 ep_flows.push(ExecutionFlow {
367 entry: ep.qualified_name.clone(),
368 path: path.clone(),
369 depth: path.len(),
370 truncated: false,
371 });
372 } else {
373 for &neighbor in &neighbors {
374 let mut new_path = path.clone();
375 new_path.push(neighbor.to_string());
376 let mut new_visited = visited.clone();
377 new_visited.insert(neighbor.to_string());
378 stack.push((neighbor.to_string(), new_path, new_visited));
379 }
380 }
381 }
382
383 if truncated {
385 for flow in &mut ep_flows {
386 flow.truncated = true;
387 }
388 }
389
390 let remaining = config.max_flows - all_flows.len();
391 ep_flows.truncate(remaining);
392 all_flows.extend(ep_flows);
393 }
394
395 all_flows
396}
397
398#[cfg(test)]
403mod tests {
404 use super::*;
405
406 fn make_symbol(name: &str, qn: &str, kind: SymbolKind) -> SymbolNode {
411 SymbolNode {
412 name: name.into(),
413 qualified_name: qn.into(),
414 kind,
415 location: Location {
416 file: "src/lib.rs".into(),
417 line_start: 1,
418 line_end: 10,
419 col_start: 0,
420 col_end: 0,
421 },
422 visibility: Visibility::Public,
423 is_exported: true,
424 is_async: false,
425 is_test: false,
426 decorators: vec![],
427 signature: None,
428 }
429 }
430
431 fn make_edge(kind: EdgeKind, source: &str, target: &str) -> Edge {
432 Edge {
433 kind,
434 source: source.into(),
435 target: target.into(),
436 metadata: None,
437 }
438 }
439
440 #[test]
445 fn detect_main_entry_point() {
446 let sym = make_symbol("main", "src/main.rs::main", SymbolKind::Function);
447 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
448 assert_eq!(entries.len(), 1);
449 assert_eq!(entries[0].kind, EntryPointKind::Main);
450 assert_eq!(entries[0].confidence, 1.0);
451 }
452
453 #[test]
454 fn detect_tokio_main() {
455 let mut sym = make_symbol("main", "src/main.rs::main", SymbolKind::Function);
456 sym.decorators = vec!["tokio::main".into()];
457 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
458 assert!(entries.iter().any(|e| e.kind == EntryPointKind::Main));
459 }
460
461 #[test]
462 fn detect_test_entry_point() {
463 let mut sym = make_symbol("test_foo", "src/lib.rs::test_foo", SymbolKind::Function);
464 sym.is_test = true;
465 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
466 assert_eq!(entries.len(), 1);
467 assert_eq!(entries[0].kind, EntryPointKind::Test);
468 }
469
470 #[test]
471 fn detect_http_handler() {
472 let mut sym = make_symbol("handle", "src/api.rs::handle", SymbolKind::Function);
473 sym.decorators = vec!["Get".into()];
474 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
475 assert_eq!(entries.len(), 1);
476 assert_eq!(entries[0].kind, EntryPointKind::HttpHandler);
477 }
478
479 #[test]
480 fn detect_cli_command() {
481 let mut sym = make_symbol("run_cmd", "src/cli.rs::run_cmd", SymbolKind::Function);
482 sym.decorators = vec!["command".into()];
483 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
484 assert_eq!(entries.len(), 1);
485 assert_eq!(entries[0].kind, EntryPointKind::CliCommand);
486 }
487
488 #[test]
489 fn detect_public_root() {
490 let sym = make_symbol("init", "src/lib.rs::init", SymbolKind::Function);
491 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
492 assert!(entries.iter().any(|e| e.kind == EntryPointKind::PublicRoot));
493 }
494
495 #[test]
496 fn public_root_excluded_when_has_incoming_calls() {
497 let sym = make_symbol("helper", "src/lib.rs::helper", SymbolKind::Function);
498 let edge = make_edge(EdgeKind::Calls, "src/main.rs::main", "src/lib.rs::helper");
499 let entries = detect_entry_points(&[sym], &[edge], &FlowConfig::default());
500 assert!(!entries.iter().any(|e| e.kind == EntryPointKind::PublicRoot));
501 }
502
503 #[test]
504 fn public_root_capped_at_max() {
505 let config = FlowConfig {
506 max_public_roots: 2,
507 ..FlowConfig::default()
508 };
509 let syms: Vec<_> = (0..10)
510 .map(|i| {
511 make_symbol(
512 &format!("fn{i}"),
513 &format!("src/lib.rs::fn{i}"),
514 SymbolKind::Function,
515 )
516 })
517 .collect();
518 let entries = detect_entry_points(&syms, &[], &config);
519 let public_roots: Vec<_> = entries
520 .iter()
521 .filter(|e| e.kind == EntryPointKind::PublicRoot)
522 .collect();
523 assert!(public_roots.len() <= 2);
524 }
525
526 #[test]
527 fn excluded_entry_points_filtered() {
528 let sym = make_symbol("main", "src/main.rs::main", SymbolKind::Function);
529 let config = FlowConfig {
530 excluded_entry_points: vec!["src/main.rs::main".into()],
531 ..FlowConfig::default()
532 };
533 let entries = detect_entry_points(&[sym], &[], &config);
534 assert!(entries.is_empty());
535 }
536
537 #[test]
538 fn extra_entry_points_added() {
539 let sym = make_symbol("custom", "src/lib.rs::custom", SymbolKind::Function);
540 let edge = make_edge(EdgeKind::Calls, "src/main.rs::main", "src/lib.rs::custom");
541 let config = FlowConfig {
542 extra_entry_points: vec!["src/lib.rs::custom".into()],
543 ..FlowConfig::default()
544 };
545 let entries = detect_entry_points(&[sym], &[edge], &config);
546 assert!(!entries.is_empty());
547 }
548
549 #[test]
550 fn detect_python_dunder_main() {
551 let sym = make_symbol("main", "app.py::main", SymbolKind::Function);
552 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
553 assert!(entries.iter().any(|e| e.kind == EntryPointKind::Main));
554 }
555
556 #[test]
557 fn detect_test_prefix_python() {
558 let mut sym = make_symbol(
559 "test_login",
560 "test_auth.py::test_login",
561 SymbolKind::Function,
562 );
563 sym.is_test = false;
564 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
565 assert!(entries.iter().any(|e| e.kind == EntryPointKind::Test));
566 }
567
568 #[test]
569 fn public_root_excludes_non_callable_kinds() {
570 for kind in [
571 SymbolKind::Const,
572 SymbolKind::Enum,
573 SymbolKind::TypeAlias,
574 SymbolKind::Variable,
575 SymbolKind::Property,
576 SymbolKind::Interface,
577 SymbolKind::Trait,
578 ] {
579 let sym = make_symbol("MyType", "src/lib.rs::MyType", kind);
580 let entries = detect_entry_points(&[sym], &[], &FlowConfig::default());
581 assert!(
582 !entries.iter().any(|e| e.kind == EntryPointKind::PublicRoot),
583 "{kind:?} should not be classified as PublicRoot"
584 );
585 }
586 }
587
588 #[test]
593 fn brandes_linear_graph_center_has_highest_centrality() {
594 let edges = vec![
595 make_edge(EdgeKind::Calls, "a", "b"),
596 make_edge(EdgeKind::Calls, "b", "c"),
597 make_edge(EdgeKind::Calls, "c", "d"),
598 make_edge(EdgeKind::Calls, "d", "e"),
599 ];
600 let nodes: HashSet<String> = ["a", "b", "c", "d", "e"]
601 .iter()
602 .map(|s| s.to_string())
603 .collect();
604 let scores = brandes_betweenness(&nodes, &edges);
605 let c_score = scores.get("c").copied().unwrap_or(0.0);
606 let a_score = scores.get("a").copied().unwrap_or(0.0);
607 let e_score = scores.get("e").copied().unwrap_or(0.0);
608 assert!(
609 c_score > a_score,
610 "center should have higher betweenness than endpoints"
611 );
612 assert!(c_score > e_score);
613 for (_, &v) in &scores {
614 assert!(
615 v >= 0.0 && v <= 1.0,
616 "betweenness must be normalized to [0,1]"
617 );
618 }
619 }
620
621 #[test]
622 fn brandes_disconnected_nodes_have_zero_betweenness() {
623 let nodes: HashSet<String> = ["a", "b"].iter().map(|s| s.to_string()).collect();
624 let scores = brandes_betweenness(&nodes, &[]);
625 assert_eq!(*scores.get("a").unwrap_or(&0.0), 0.0);
626 assert_eq!(*scores.get("b").unwrap_or(&0.0), 0.0);
627 }
628
629 #[test]
630 fn brandes_diamond_graph_intermediaries_have_betweenness() {
631 let edges = vec![
632 make_edge(EdgeKind::Calls, "a", "b"),
633 make_edge(EdgeKind::Calls, "a", "c"),
634 make_edge(EdgeKind::Calls, "b", "d"),
635 make_edge(EdgeKind::Calls, "c", "d"),
636 ];
637 let nodes: HashSet<String> = ["a", "b", "c", "d"].iter().map(|s| s.to_string()).collect();
638 let scores = brandes_betweenness(&nodes, &edges);
639 let b = scores.get("b").copied().unwrap_or(0.0);
640 let c = scores.get("c").copied().unwrap_or(0.0);
641 assert!(b > 0.0, "intermediary b must have nonzero betweenness");
642 assert!(c > 0.0, "intermediary c must have nonzero betweenness");
643 assert!(
644 (b - c).abs() < 1e-9,
645 "symmetric intermediaries should have equal betweenness"
646 );
647 }
648
649 #[test]
650 fn brandes_single_node_no_division_by_zero() {
651 let nodes: HashSet<String> = ["a"].iter().map(|s| s.to_string()).collect();
652 let scores = brandes_betweenness(&nodes, &[]);
653 assert_eq!(*scores.get("a").unwrap_or(&0.0), 0.0);
654 }
655
656 #[test]
657 fn brandes_empty_graph() {
658 let nodes: HashSet<String> = HashSet::new();
659 let scores = brandes_betweenness(&nodes, &[]);
660 assert!(scores.is_empty());
661 }
662
663 #[test]
664 fn brandes_only_uses_high_confidence_edges() {
665 let edges = vec![
666 make_edge(EdgeKind::Calls, "a", "b"),
667 make_edge(EdgeKind::ImportsFrom, "a", "c"),
668 ];
669 let nodes: HashSet<String> = ["a", "b", "c"].iter().map(|s| s.to_string()).collect();
670 let scores = brandes_betweenness(&nodes, &edges);
671 assert_eq!(*scores.get("c").unwrap_or(&0.0), 0.0);
672 }
673
674 #[test]
675 fn brandes_normalization_directed() {
676 let edges = vec![
677 make_edge(EdgeKind::Calls, "a", "b"),
678 make_edge(EdgeKind::Calls, "b", "c"),
679 make_edge(EdgeKind::Calls, "c", "d"),
680 make_edge(EdgeKind::Calls, "d", "e"),
681 ];
682 let nodes: HashSet<String> = ["a", "b", "c", "d", "e"]
683 .iter()
684 .map(|s| s.to_string())
685 .collect();
686 let scores = brandes_betweenness(&nodes, &edges);
687 for &v in scores.values() {
688 assert!(v <= 1.0, "normalized score must not exceed 1.0");
689 }
690 let c_score = scores.get("c").copied().unwrap_or(0.0);
693 assert!(
694 (c_score - 1.0 / 3.0).abs() < 1e-9,
695 "center of 5-node directed linear graph should have betweenness 1/3, got {c_score}"
696 );
697 }
698
699 #[test]
704 fn enumerate_flows_linear_graph() {
705 let edges = vec![
706 make_edge(EdgeKind::Calls, "main", "a"),
707 make_edge(EdgeKind::Calls, "a", "b"),
708 ];
709 let entry_points = vec![EntryPoint {
710 qualified_name: "main".into(),
711 kind: EntryPointKind::Main,
712 confidence: 1.0,
713 }];
714 let flows = enumerate_flows(&entry_points, &edges, &FlowConfig::default());
715 assert_eq!(flows.len(), 1);
716 assert_eq!(flows[0].path, vec!["main", "a", "b"]);
717 assert_eq!(flows[0].depth, 3);
718 assert!(!flows[0].truncated);
719 }
720
721 #[test]
722 fn enumerate_flows_cycle_detection() {
723 let edges = vec![
724 make_edge(EdgeKind::Calls, "main", "a"),
725 make_edge(EdgeKind::Calls, "a", "b"),
726 make_edge(EdgeKind::Calls, "b", "a"),
727 ];
728 let entry_points = vec![EntryPoint {
729 qualified_name: "main".into(),
730 kind: EntryPointKind::Main,
731 confidence: 1.0,
732 }];
733 let flows = enumerate_flows(&entry_points, &edges, &FlowConfig::default());
734 for flow in &flows {
735 let unique: HashSet<&String> = flow.path.iter().collect();
736 assert_eq!(unique.len(), flow.path.len(), "no duplicates in flow path");
737 }
738 }
739
740 #[test]
741 fn enumerate_flows_depth_limit() {
742 let edges: Vec<Edge> = (0..25)
743 .map(|i| make_edge(EdgeKind::Calls, &format!("e{i}"), &format!("e{}", i + 1)))
744 .collect();
745 let entry_points = vec![EntryPoint {
746 qualified_name: "e0".into(),
747 kind: EntryPointKind::Main,
748 confidence: 1.0,
749 }];
750 let config = FlowConfig {
751 max_depth: 5,
752 ..FlowConfig::default()
753 };
754 let flows = enumerate_flows(&entry_points, &edges, &config);
755 for flow in &flows {
756 assert!(flow.path.len() <= 5, "flow depth must not exceed max_depth");
757 }
758 }
759
760 #[test]
761 fn enumerate_flows_global_cap() {
762 let edges: Vec<Edge> = (0..100)
763 .map(|i| make_edge(EdgeKind::Calls, "entry", &format!("a{i}")))
764 .collect();
765 let entry_points = vec![EntryPoint {
766 qualified_name: "entry".into(),
767 kind: EntryPointKind::Main,
768 confidence: 1.0,
769 }];
770 let config = FlowConfig {
771 max_flows: 10,
772 ..FlowConfig::default()
773 };
774 let flows = enumerate_flows(&entry_points, &edges, &config);
775 assert!(flows.len() <= 10, "global flow cap must be respected");
776 }
777
778 #[test]
779 fn enumerate_flows_visit_budget() {
780 let edges: Vec<Edge> = (0..1000)
781 .map(|i| make_edge(EdgeKind::Calls, "entry", &format!("a{i}")))
782 .collect();
783 let entry_points = vec![EntryPoint {
784 qualified_name: "entry".into(),
785 kind: EntryPointKind::Main,
786 confidence: 1.0,
787 }];
788 let config = FlowConfig {
789 visit_budget: 50,
790 ..FlowConfig::default()
791 };
792 let flows = enumerate_flows(&entry_points, &edges, &config);
793 assert!(flows.iter().any(|f| f.truncated) || flows.len() < 1000);
794 }
795
796 #[test]
797 fn enumerate_flows_only_high_confidence_edges() {
798 let edges = vec![
799 make_edge(EdgeKind::Calls, "main", "a"),
800 make_edge(EdgeKind::ImportsFrom, "a", "b"),
801 ];
802 let entry_points = vec![EntryPoint {
803 qualified_name: "main".into(),
804 kind: EntryPointKind::Main,
805 confidence: 1.0,
806 }];
807 let flows = enumerate_flows(&entry_points, &edges, &FlowConfig::default());
808 for flow in &flows {
809 assert!(!flow.path.contains(&"b".to_string()));
810 }
811 }
812
813 #[test]
814 fn enumerate_flows_multiple_entry_points_share_global_cap() {
815 let mut edges = Vec::new();
816 for i in 0..10 {
817 edges.push(make_edge(EdgeKind::Calls, "e1", &format!("a{i}")));
818 edges.push(make_edge(EdgeKind::Calls, "e2", &format!("b{i}")));
819 }
820 let entry_points = vec![
821 EntryPoint {
822 qualified_name: "e1".into(),
823 kind: EntryPointKind::Main,
824 confidence: 1.0,
825 },
826 EntryPoint {
827 qualified_name: "e2".into(),
828 kind: EntryPointKind::Test,
829 confidence: 1.0,
830 },
831 ];
832 let config = FlowConfig {
833 max_flows: 15,
834 ..FlowConfig::default()
835 };
836 let flows = enumerate_flows(&entry_points, &edges, &config);
837 assert!(
838 flows.len() <= 15,
839 "global cap must be shared across entry points"
840 );
841 let e1_flows = flows.iter().filter(|f| f.entry == "e1").count();
842 let e2_flows = flows.iter().filter(|f| f.entry == "e2").count();
843 assert!(
844 e1_flows > 0 && e2_flows > 0,
845 "both entry points should contribute flows"
846 );
847 }
848
849 #[test]
850 fn enumerate_flows_branching() {
851 let edges = vec![
852 make_edge(EdgeKind::Calls, "main", "a"),
853 make_edge(EdgeKind::Calls, "main", "b"),
854 make_edge(EdgeKind::Calls, "a", "c"),
855 make_edge(EdgeKind::Calls, "b", "c"),
856 ];
857 let entry_points = vec![EntryPoint {
858 qualified_name: "main".into(),
859 kind: EntryPointKind::Main,
860 confidence: 1.0,
861 }];
862 let flows = enumerate_flows(&entry_points, &edges, &FlowConfig::default());
863 assert_eq!(flows.len(), 2);
864 }
865}