1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NodeType {
14 DataSource,
15 Parameter,
16 Analysis,
17 Report,
18 Task,
19}
20
21impl NodeType {
22 pub fn as_str(&self) -> &'static str {
23 match self {
24 NodeType::DataSource => "data_source",
25 NodeType::Parameter => "parameter",
26 NodeType::Analysis => "analysis",
27 NodeType::Report => "report",
28 NodeType::Task => "task",
29 }
30 }
31
32 pub fn parse(s: &str) -> Option<Self> {
33 match s {
34 "data_source" => Some(NodeType::DataSource),
35 "parameter" => Some(NodeType::Parameter),
36 "analysis" => Some(NodeType::Analysis),
37 "report" => Some(NodeType::Report),
38 "task" => Some(NodeType::Task),
39 _ => None,
40 }
41 }
42
43 pub fn layer_order(&self) -> usize {
45 match self {
46 NodeType::DataSource => 0,
47 NodeType::Parameter => 1,
48 NodeType::Analysis => 2,
49 NodeType::Report => 3,
50 NodeType::Task => 4,
51 }
52 }
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum NodeStatus {
58 Valid,
59 Stale,
60 Pending,
61 Active,
62 Blocked,
63}
64
65impl NodeStatus {
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 NodeStatus::Valid => "valid",
69 NodeStatus::Stale => "stale",
70 NodeStatus::Pending => "pending",
71 NodeStatus::Active => "active",
72 NodeStatus::Blocked => "blocked",
73 }
74 }
75
76 pub fn parse(s: &str) -> Option<Self> {
77 match s {
78 "valid" => Some(NodeStatus::Valid),
79 "stale" => Some(NodeStatus::Stale),
80 "pending" => Some(NodeStatus::Pending),
81 "active" => Some(NodeStatus::Active),
82 "blocked" => Some(NodeStatus::Blocked),
83 _ => None,
84 }
85 }
86}
87
88#[derive(Debug, Clone)]
90pub struct DagNode {
91 pub id: usize,
92 pub node_type: NodeType,
93 pub status: NodeStatus,
94 pub depends_on: Vec<usize>,
96 pub tags: Vec<String>,
98}
99
100pub struct Dag {
102 pub nodes: Vec<DagNode>,
103 forward: Vec<Vec<usize>>,
105}
106
107#[derive(Debug, Clone, PartialEq)]
112pub enum DagError {
113 CycleDetected(Vec<usize>),
114 NodeNotFound(usize),
115 DuplicateEdge { from: usize, to: usize },
116}
117
118impl Dag {
123 pub fn new(nodes: Vec<DagNode>) -> Self {
126 let n = nodes.len();
127 let mut forward = vec![Vec::new(); n];
128 for node in &nodes {
129 for &dep in &node.depends_on {
130 if dep < n {
131 forward[dep].push(node.id);
132 }
133 }
134 }
135 Dag { nodes, forward }
136 }
137
138 pub fn node_count(&self) -> usize {
139 self.nodes.len()
140 }
141
142 pub fn edge_count(&self) -> usize {
143 self.nodes.iter().map(|n| n.depends_on.len()).sum()
144 }
145
146 pub fn upstream(&self, node: usize) -> &[usize] {
152 &self.nodes[node].depends_on
153 }
154
155 pub fn downstream(&self, node: usize) -> &[usize] {
157 &self.forward[node]
158 }
159
160 pub fn all_upstream(&self, node: usize) -> Vec<usize> {
162 let mut visited = vec![false; self.nodes.len()];
163 let mut queue = Vec::new();
164 let mut result = Vec::new();
165 for &dep in &self.nodes[node].depends_on {
166 if !visited[dep] {
167 visited[dep] = true;
168 queue.push(dep);
169 }
170 }
171 while let Some(current) = queue.pop() {
172 result.push(current);
173 for &dep in &self.nodes[current].depends_on {
174 if !visited[dep] {
175 visited[dep] = true;
176 queue.push(dep);
177 }
178 }
179 }
180 result
181 }
182
183 pub fn all_downstream(&self, node: usize) -> Vec<usize> {
185 let mut visited = vec![false; self.nodes.len()];
186 let mut queue = Vec::new();
187 let mut result = Vec::new();
188 for &child in &self.forward[node] {
189 if !visited[child] {
190 visited[child] = true;
191 queue.push(child);
192 }
193 }
194 while let Some(current) = queue.pop() {
195 result.push(current);
196 for &child in &self.forward[current] {
197 if !visited[child] {
198 visited[child] = true;
199 queue.push(child);
200 }
201 }
202 }
203 result
204 }
205
206 pub fn topological_sort(&self) -> Option<Vec<usize>> {
212 let n = self.nodes.len();
213 let mut in_degree = vec![0usize; n];
214 for node in &self.nodes {
215 in_degree[node.id] = node.depends_on.len();
216 }
217
218 let mut queue: Vec<usize> = (0..n).filter(|&i| in_degree[i] == 0).collect();
219 let mut order = Vec::with_capacity(n);
220
221 while let Some(u) = queue.pop() {
222 order.push(u);
223 for &v in &self.forward[u] {
224 in_degree[v] -= 1;
225 if in_degree[v] == 0 {
226 queue.push(v);
227 }
228 }
229 }
230
231 if order.len() == n {
232 Some(order)
233 } else {
234 None
235 }
236 }
237
238 pub fn detect_cycle(&self) -> Option<Vec<usize>> {
244 let n = self.nodes.len();
245 let mut color = vec![0u8; n];
247 let mut parent = vec![usize::MAX; n];
248
249 for start in 0..n {
250 if color[start] != 0 {
251 continue;
252 }
253 let mut stack = vec![(start, 0usize)]; color[start] = 1;
255
256 while let Some(&mut (u, ref mut ei)) = stack.last_mut() {
257 if *ei < self.forward[u].len() {
258 let v = self.forward[u][*ei];
259 *ei += 1;
260 if color[v] == 1 {
261 let mut cycle = vec![v, u];
263 let mut cur = u;
264 while cur != v {
265 cur = parent[cur];
266 if cur == usize::MAX {
267 break;
268 }
269 cycle.push(cur);
270 }
271 cycle.reverse();
272 return Some(cycle);
273 } else if color[v] == 0 {
274 color[v] = 1;
275 parent[v] = u;
276 stack.push((v, 0));
277 }
278 } else {
279 color[u] = 2;
280 stack.pop();
281 }
282 }
283 }
284 None
285 }
286
287 pub fn compute_depths(&self) -> Vec<usize> {
294 let n = self.nodes.len();
295 let mut depth = vec![0usize; n];
296
297 if let Some(order) = self.topological_sort() {
298 for &u in &order {
299 for &v in &self.forward[u] {
300 if depth[v] < depth[u] + 1 {
301 depth[v] = depth[u] + 1;
302 }
303 }
304 }
305 }
306 depth
307 }
308
309 pub fn critical_path(&self) -> Vec<usize> {
316 let depths = self.compute_depths();
317
318 let mut max_depth = 0;
320 let mut deepest = 0;
321 for (i, &d) in depths.iter().enumerate() {
322 if d > max_depth {
323 max_depth = d;
324 deepest = i;
325 }
326 }
327
328 let mut path = vec![deepest];
330 let mut current = deepest;
331 while !self.nodes[current].depends_on.is_empty() {
332 let mut best_dep = self.nodes[current].depends_on[0];
334 let mut best_depth = depths[best_dep];
335 for &dep in &self.nodes[current].depends_on[1..] {
336 if depths[dep] > best_depth {
337 best_depth = depths[dep];
338 best_dep = dep;
339 }
340 }
341 path.push(best_dep);
342 current = best_dep;
343 }
344 path.reverse();
345 path
346 }
347
348 pub fn assign_layers(&self) -> (Vec<usize>, usize) {
356 let depths = self.compute_depths();
357 let max = depths.iter().copied().max().unwrap_or(0);
358 (depths, max)
359 }
360
361 pub fn minimize_crossings(&self, layers: &[usize], max_layer: usize) -> Vec<Vec<usize>> {
365 let mut layer_nodes: Vec<Vec<usize>> = vec![Vec::new(); max_layer + 1];
366 for (i, &layer) in layers.iter().enumerate() {
367 layer_nodes[layer].push(i);
368 }
369
370 for layer in &mut layer_nodes {
372 layer.sort_by(|&a, &b| {
373 let ta = self.nodes[a].node_type.layer_order();
374 let tb = self.nodes[b].node_type.layer_order();
375 ta.cmp(&tb).then_with(|| {
376 let tag_a = self.nodes[a].tags.first().map(|s| s.as_str()).unwrap_or("");
378 let tag_b = self.nodes[b].tags.first().map(|s| s.as_str()).unwrap_or("");
379 tag_a.cmp(tag_b)
380 })
381 });
382 }
383
384 for _pass in 0..4 {
386 for l in 1..=max_layer {
388 let prev_positions = position_map(&layer_nodes[l - 1]);
389 layer_nodes[l].sort_by(|&a, &b| {
390 let bc_a = barycenter(&self.nodes[a].depends_on, &prev_positions);
391 let bc_b = barycenter(&self.nodes[b].depends_on, &prev_positions);
392 bc_a.partial_cmp(&bc_b)
393 .unwrap_or(core::cmp::Ordering::Equal)
394 });
395 }
396
397 for l in (0..max_layer).rev() {
399 let next_positions = position_map(&layer_nodes[l + 1]);
400 layer_nodes[l].sort_by(|&a, &b| {
401 let bc_a = barycenter(&self.forward[a], &next_positions);
402 let bc_b = barycenter(&self.forward[b], &next_positions);
403 bc_a.partial_cmp(&bc_b)
404 .unwrap_or(core::cmp::Ordering::Equal)
405 });
406 }
407 }
408
409 layer_nodes
410 }
411
412 pub fn layout(&self, width: f64, height: f64) -> Vec<(f64, f64)> {
415 let n = self.nodes.len();
416 if n == 0 {
417 return Vec::new();
418 }
419
420 let (layers, max_layer) = self.assign_layers();
421 let layer_nodes = self.minimize_crossings(&layers, max_layer);
422
423 let mut positions = vec![(0.0, 0.0); n];
424
425 let layer_count = layer_nodes.len();
426 let x_spacing = if layer_count > 1 {
427 width / (layer_count as f64 + 1.0)
428 } else {
429 width / 2.0
430 };
431
432 for (layer_idx, nodes_in_layer) in layer_nodes.iter().enumerate() {
433 let node_count = nodes_in_layer.len();
434 let y_spacing = if node_count > 1 {
435 height / (node_count as f64 + 1.0)
436 } else {
437 height / 2.0
438 };
439
440 for (pos_idx, &node_id) in nodes_in_layer.iter().enumerate() {
441 let x = x_spacing * (layer_idx as f64 + 1.0);
442 let y = if node_count > 1 {
443 y_spacing * (pos_idx as f64 + 1.0)
444 } else {
445 height / 2.0
446 };
447 positions[node_id] = (x, y);
448 }
449 }
450
451 positions
452 }
453
454 pub fn subgraph<F>(&self, predicate: F, depth: usize) -> Vec<usize>
462 where
463 F: Fn(&DagNode) -> bool,
464 {
465 let mut included = vec![false; self.nodes.len()];
466 let seeds: Vec<usize> = self
467 .nodes
468 .iter()
469 .filter(|n| predicate(n))
470 .map(|n| n.id)
471 .collect();
472
473 for &seed in &seeds {
474 included[seed] = true;
475 }
476
477 for &seed in &seeds {
479 self.expand_direction(seed, depth, true, &mut included);
480 }
481
482 for &seed in &seeds {
484 self.expand_direction(seed, depth, false, &mut included);
485 }
486
487 (0..self.nodes.len()).filter(|&i| included[i]).collect()
488 }
489
490 fn expand_direction(
491 &self,
492 start: usize,
493 max_depth: usize,
494 upstream: bool,
495 included: &mut [bool],
496 ) {
497 let mut frontier = vec![(start, 0usize)];
498 while let Some((node, d)) = frontier.pop() {
499 if d >= max_depth {
500 continue;
501 }
502 let neighbors = if upstream {
503 &self.nodes[node].depends_on
504 } else {
505 &self.forward[node]
506 };
507 for &next in neighbors {
508 if !included[next] {
509 included[next] = true;
510 frontier.push((next, d + 1));
511 }
512 }
513 }
514 }
515
516 pub fn impact_analysis(&self, node: usize) -> ImpactResult {
523 let affected = self.all_downstream(node);
524 let mut by_type = [0usize; 5];
525 for &a in &affected {
526 by_type[self.nodes[a].node_type.layer_order()] += 1;
527 }
528 ImpactResult {
529 source: node,
530 cascade_count: affected.len(),
531 affected_nodes: affected,
532 by_type,
533 }
534 }
535
536 pub fn orphans(&self) -> Vec<usize> {
541 self.nodes
542 .iter()
543 .filter(|n| n.depends_on.is_empty() && self.forward[n.id].is_empty())
544 .map(|n| n.id)
545 .collect()
546 }
547
548 pub fn validate(&self) -> Vec<String> {
555 let mut issues = Vec::new();
556
557 for node in &self.nodes {
559 for &dep in &node.depends_on {
560 if dep >= self.nodes.len() {
561 issues.push(format!(
562 "ERROR: Node {} depends on unknown node {}",
563 node.id, dep
564 ));
565 }
566 }
567 }
568
569 if let Some(cycle) = self.detect_cycle() {
571 let cycle_str: Vec<String> = cycle.iter().map(|i| i.to_string()).collect();
572 issues.push(format!("ERROR: Cycle detected: {}", cycle_str.join(" → ")));
573 }
574
575 for id in self.orphans() {
577 issues.push(format!("WARN: Orphan node {} has no connections", id));
578 }
579
580 for node in &self.nodes {
582 if node.node_type == NodeType::DataSource && !node.depends_on.is_empty() {
583 issues.push(format!(
584 "WARN: Data source {} has dependencies (unusual)",
585 node.id
586 ));
587 }
588 }
589
590 for node in &self.nodes {
592 if node.node_type == NodeType::Parameter {
593 for &dep in &node.depends_on {
594 if dep < self.nodes.len() && self.nodes[dep].node_type == NodeType::Report {
595 issues.push(format!(
596 "WARN: Parameter {} depends on report {} (unusual direction)",
597 node.id, dep
598 ));
599 }
600 }
601 }
602 }
603
604 issues
605 }
606
607 pub fn stale_nodes(&self) -> Vec<usize> {
613 let depths = self.compute_depths();
614 let mut stale: Vec<usize> = self
615 .nodes
616 .iter()
617 .filter(|n| n.status == NodeStatus::Stale)
618 .map(|n| n.id)
619 .collect();
620 stale.sort_by_key(|&id| depths[id]);
621 stale
622 }
623
624 pub fn plannable_tasks(&self) -> Vec<usize> {
633 self.nodes
634 .iter()
635 .filter(|n| n.node_type == NodeType::Task && n.status == NodeStatus::Pending)
636 .filter(|n| {
637 n.depends_on.iter().all(|&dep| {
638 dep < self.nodes.len() && self.nodes[dep].status == NodeStatus::Valid
639 })
640 })
641 .map(|n| n.id)
642 .collect()
643 }
644
645 pub fn blocked_tasks(&self) -> Vec<usize> {
650 self.nodes
651 .iter()
652 .filter(|n| n.node_type == NodeType::Task && n.status == NodeStatus::Pending)
653 .filter(|n| {
654 n.depends_on.iter().any(|&dep| {
655 dep >= self.nodes.len() || self.nodes[dep].status != NodeStatus::Valid
656 })
657 })
658 .map(|n| n.id)
659 .collect()
660 }
661
662 pub fn active_tasks(&self) -> Vec<usize> {
664 self.nodes
665 .iter()
666 .filter(|n| n.node_type == NodeType::Task && n.status == NodeStatus::Active)
667 .map(|n| n.id)
668 .collect()
669 }
670
671 pub fn parallel_groups(&self) -> Vec<Vec<usize>> {
674 let plannable = self.plannable_tasks();
675 if plannable.is_empty() {
676 return Vec::new();
677 }
678
679 let mut groups: Vec<Vec<usize>> = Vec::new();
680 let mut assigned = vec![false; self.nodes.len()];
681
682 for &task in &plannable {
683 if assigned[task] {
684 continue;
685 }
686 let mut group = vec![task];
687 assigned[task] = true;
688 let task_up: Vec<bool> = {
689 let up = self.all_upstream(task);
690 let mut v = vec![false; self.nodes.len()];
691 for &u in &up {
692 v[u] = true;
693 }
694 v
695 };
696 let task_down: Vec<bool> = {
697 let down = self.all_downstream(task);
698 let mut v = vec![false; self.nodes.len()];
699 for &d in &down {
700 v[d] = true;
701 }
702 v
703 };
704
705 for &other in &plannable {
706 if assigned[other] {
707 continue;
708 }
709 if task_up[other] || task_down[other] {
710 continue;
711 }
712 let mut conflicts = false;
714 for &member in &group {
715 let m_up = self.all_upstream(member);
716 let m_down = self.all_downstream(member);
717 if m_up.contains(&other) || m_down.contains(&other) {
718 conflicts = true;
719 break;
720 }
721 }
722 if !conflicts {
723 group.push(other);
724 assigned[other] = true;
725 }
726 }
727
728 groups.push(group);
729 }
730
731 groups
732 }
733
734 pub fn summarize(&self) -> String {
736 let mut by_type = [0usize; 5];
737 let mut by_status = [0usize; 5]; for node in &self.nodes {
739 by_type[node.node_type.layer_order()] += 1;
740 match node.status {
741 NodeStatus::Valid => by_status[0] += 1,
742 NodeStatus::Stale => by_status[1] += 1,
743 NodeStatus::Pending => by_status[2] += 1,
744 NodeStatus::Active => by_status[3] += 1,
745 NodeStatus::Blocked => by_status[4] += 1,
746 }
747 }
748
749 let type_names = ["data_source", "parameter", "analysis", "report", "task"];
750 let status_names = ["valid", "stale", "pending", "active", "blocked"];
751
752 let mut lines = Vec::new();
753 lines.push(format!(
754 "DAG: {} nodes, {} edges",
755 self.nodes.len(),
756 self.edge_count()
757 ));
758
759 let types_str: Vec<String> = type_names
760 .iter()
761 .zip(by_type.iter())
762 .filter(|(_, &c)| c > 0)
763 .map(|(n, c)| format!("{}={}", n, c))
764 .collect();
765 lines.push(format!("Types: {}", types_str.join(", ")));
766
767 let status_str: Vec<String> = status_names
768 .iter()
769 .zip(by_status.iter())
770 .filter(|(_, &c)| c > 0)
771 .map(|(n, c)| format!("{}={}", n, c))
772 .collect();
773 lines.push(format!("Status: {}", status_str.join(", ")));
774
775 let issues = self.validate();
776 let errors = issues.iter().filter(|i| i.starts_with("ERROR")).count();
777 let warns = issues.iter().filter(|i| i.starts_with("WARN")).count();
778 if errors > 0 {
779 lines.push(format!("Errors: {}", errors));
780 }
781 if warns > 0 {
782 lines.push(format!("Warnings: {}", warns));
783 }
784 for issue in &issues {
785 lines.push(format!(" {}", issue));
786 }
787
788 lines.join("\n")
789 }
790
791 pub fn count_crossings(&self, positions: &[(f64, f64)]) -> usize {
798 let mut edges: Vec<((f64, f64), (f64, f64))> = Vec::new();
799 for node in &self.nodes {
800 for &dep in &node.depends_on {
801 edges.push((positions[dep], positions[node.id]));
802 }
803 }
804
805 let mut crossings = 0;
806 for i in 0..edges.len() {
807 for j in (i + 1)..edges.len() {
808 if segments_intersect(edges[i].0, edges[i].1, edges[j].0, edges[j].1) {
809 crossings += 1;
810 }
811 }
812 }
813 crossings
814 }
815
816 pub fn find_paths(&self, source: usize, target: usize, max_paths: usize) -> Vec<Vec<usize>> {
823 let mut result = Vec::new();
824 let mut path = vec![source];
825 self.dfs_paths(source, target, &mut path, &mut result, max_paths);
826 result.sort_by_key(|p| p.len());
827 result
828 }
829
830 fn dfs_paths(
831 &self,
832 current: usize,
833 target: usize,
834 path: &mut Vec<usize>,
835 result: &mut Vec<Vec<usize>>,
836 max_paths: usize,
837 ) {
838 if result.len() >= max_paths {
839 return;
840 }
841 if current == target {
842 result.push(path.clone());
843 return;
844 }
845 for &next in &self.forward[current] {
846 if !path.contains(&next) {
847 path.push(next);
848 self.dfs_paths(next, target, path, result, max_paths);
849 path.pop();
850 }
851 }
852 }
853}
854
855pub struct ImpactResult {
860 pub source: usize,
861 pub cascade_count: usize,
862 pub affected_nodes: Vec<usize>,
863 pub by_type: [usize; 5],
865}
866
867fn position_map(layer: &[usize]) -> Vec<(usize, usize)> {
873 layer
874 .iter()
875 .enumerate()
876 .map(|(pos, &id)| (id, pos))
877 .collect()
878}
879
880fn barycenter(neighbors: &[usize], positions: &[(usize, usize)]) -> f64 {
882 if neighbors.is_empty() {
883 return f64::MAX;
884 }
885 let mut sum = 0.0;
886 let mut count = 0;
887 for &n in neighbors {
888 if let Some(&(_, pos)) = positions.iter().find(|&&(id, _)| id == n) {
889 sum += pos as f64;
890 count += 1;
891 }
892 }
893 if count == 0 {
894 f64::MAX
895 } else {
896 sum / count as f64
897 }
898}
899
900fn segments_intersect(a1: (f64, f64), a2: (f64, f64), b1: (f64, f64), b2: (f64, f64)) -> bool {
902 if a1 == b1 || a1 == b2 || a2 == b1 || a2 == b2 {
904 return false;
905 }
906
907 let d1 = cross_product(b1, b2, a1);
908 let d2 = cross_product(b1, b2, a2);
909 let d3 = cross_product(a1, a2, b1);
910 let d4 = cross_product(a1, a2, b2);
911
912 if ((d1 > 0.0 && d2 < 0.0) || (d1 < 0.0 && d2 > 0.0))
913 && ((d3 > 0.0 && d4 < 0.0) || (d3 < 0.0 && d4 > 0.0))
914 {
915 return true;
916 }
917
918 false
919}
920
921fn cross_product(o: (f64, f64), a: (f64, f64), b: (f64, f64)) -> f64 {
922 (a.0 - o.0) * (b.1 - o.1) - (a.1 - o.1) * (b.0 - o.0)
923}
924
925#[cfg(test)]
930mod tests {
931 use super::*;
932
933 fn make_node(id: usize, node_type: NodeType, depends_on: Vec<usize>) -> DagNode {
934 DagNode {
935 id,
936 node_type,
937 status: NodeStatus::Valid,
938 depends_on,
939 tags: Vec::new(),
940 }
941 }
942
943 fn make_linear_dag() -> Dag {
944 Dag::new(vec![
946 make_node(0, NodeType::DataSource, vec![]),
947 make_node(1, NodeType::Parameter, vec![0]),
948 make_node(2, NodeType::Analysis, vec![1]),
949 make_node(3, NodeType::Report, vec![2]),
950 ])
951 }
952
953 fn make_diamond_dag() -> Dag {
954 Dag::new(vec![
960 make_node(0, NodeType::DataSource, vec![]),
961 make_node(1, NodeType::Analysis, vec![0]),
962 make_node(2, NodeType::Analysis, vec![0]),
963 make_node(3, NodeType::Report, vec![1, 2]),
964 ])
965 }
966
967 fn make_complex_dag() -> Dag {
968 Dag::new(vec![
975 make_node(0, NodeType::DataSource, vec![]), make_node(1, NodeType::DataSource, vec![]), make_node(2, NodeType::Parameter, vec![]), make_node(3, NodeType::Analysis, vec![0]), make_node(4, NodeType::Analysis, vec![0, 1]), make_node(5, NodeType::Analysis, vec![2]), make_node(6, NodeType::Report, vec![3, 4]), make_node(7, NodeType::Report, vec![4, 5]), ])
984 }
985
986 #[test]
989 fn test_new_empty() {
990 let dag = Dag::new(vec![]);
991 assert_eq!(dag.node_count(), 0);
992 assert_eq!(dag.edge_count(), 0);
993 }
994
995 #[test]
996 fn test_new_counts() {
997 let dag = make_linear_dag();
998 assert_eq!(dag.node_count(), 4);
999 assert_eq!(dag.edge_count(), 3);
1000 }
1001
1002 #[test]
1003 fn test_forward_index() {
1004 let dag = make_diamond_dag();
1005 assert_eq!(dag.downstream(0), &[1, 2]);
1006 assert!(dag.downstream(3).is_empty());
1007 }
1008
1009 #[test]
1012 fn test_all_upstream_leaf() {
1013 let dag = make_linear_dag();
1014 let mut up = dag.all_upstream(3);
1015 up.sort();
1016 assert_eq!(up, vec![0, 1, 2]);
1017 }
1018
1019 #[test]
1020 fn test_all_upstream_root() {
1021 let dag = make_linear_dag();
1022 assert!(dag.all_upstream(0).is_empty());
1023 }
1024
1025 #[test]
1026 fn test_all_downstream_root() {
1027 let dag = make_linear_dag();
1028 let mut down = dag.all_downstream(0);
1029 down.sort();
1030 assert_eq!(down, vec![1, 2, 3]);
1031 }
1032
1033 #[test]
1034 fn test_all_downstream_leaf() {
1035 let dag = make_linear_dag();
1036 assert!(dag.all_downstream(3).is_empty());
1037 }
1038
1039 #[test]
1040 fn test_diamond_upstream() {
1041 let dag = make_diamond_dag();
1042 let mut up = dag.all_upstream(3);
1043 up.sort();
1044 assert_eq!(up, vec![0, 1, 2]);
1045 }
1046
1047 #[test]
1048 fn test_diamond_downstream() {
1049 let dag = make_diamond_dag();
1050 let mut down = dag.all_downstream(0);
1051 down.sort();
1052 assert_eq!(down, vec![1, 2, 3]);
1053 }
1054
1055 #[test]
1058 fn test_topo_sort_linear() {
1059 let dag = make_linear_dag();
1060 let order = dag.topological_sort().unwrap();
1061 for i in 0..order.len() - 1 {
1063 assert!(
1064 order.iter().position(|&x| x == i).unwrap()
1065 < order.iter().position(|&x| x == i + 1).unwrap()
1066 );
1067 }
1068 }
1069
1070 #[test]
1071 fn test_topo_sort_diamond() {
1072 let dag = make_diamond_dag();
1073 let order = dag.topological_sort().unwrap();
1074 assert_eq!(order[0], 0); assert_eq!(*order.last().unwrap(), 3); }
1077
1078 #[test]
1081 fn test_no_cycle() {
1082 let dag = make_linear_dag();
1083 assert!(dag.detect_cycle().is_none());
1084 }
1085
1086 #[test]
1087 fn test_detect_cycle() {
1088 let dag = Dag::new(vec![
1090 make_node(0, NodeType::Analysis, vec![2]),
1091 make_node(1, NodeType::Analysis, vec![0]),
1092 make_node(2, NodeType::Analysis, vec![1]),
1093 ]);
1094 let cycle = dag.detect_cycle();
1095 assert!(cycle.is_some());
1096 }
1097
1098 #[test]
1101 fn test_depths_linear() {
1102 let dag = make_linear_dag();
1103 assert_eq!(dag.compute_depths(), vec![0, 1, 2, 3]);
1104 }
1105
1106 #[test]
1107 fn test_depths_diamond() {
1108 let dag = make_diamond_dag();
1109 let depths = dag.compute_depths();
1110 assert_eq!(depths[0], 0);
1111 assert_eq!(depths[1], 1);
1112 assert_eq!(depths[2], 1);
1113 assert_eq!(depths[3], 2);
1114 }
1115
1116 #[test]
1119 fn test_critical_path_linear() {
1120 let dag = make_linear_dag();
1121 assert_eq!(dag.critical_path(), vec![0, 1, 2, 3]);
1122 }
1123
1124 #[test]
1125 fn test_critical_path_diamond() {
1126 let dag = make_diamond_dag();
1127 let path = dag.critical_path();
1128 assert_eq!(path.len(), 3); assert_eq!(path[0], 0);
1130 assert_eq!(*path.last().unwrap(), 3);
1131 }
1132
1133 #[test]
1136 fn test_layout_linear() {
1137 let dag = make_linear_dag();
1138 let pos = dag.layout(1000.0, 600.0);
1139 assert_eq!(pos.len(), 4);
1140 for i in 0..3 {
1142 assert!(pos[i].0 < pos[i + 1].0, "x should increase: {:?}", pos);
1143 }
1144 }
1145
1146 #[test]
1147 fn test_layout_diamond() {
1148 let dag = make_diamond_dag();
1149 let pos = dag.layout(1000.0, 600.0);
1150 assert!(pos[0].0 < pos[3].0);
1152 assert!((pos[1].0 - pos[2].0).abs() < 1.0);
1154 assert!((pos[1].1 - pos[2].1).abs() > 1.0);
1156 }
1157
1158 #[test]
1159 fn test_layout_empty() {
1160 let dag = Dag::new(vec![]);
1161 assert!(dag.layout(100.0, 100.0).is_empty());
1162 }
1163
1164 #[test]
1167 fn test_subgraph_single() {
1168 let dag = make_complex_dag();
1169 let sub = dag.subgraph(|n| n.id == 4, 0);
1170 assert_eq!(sub, vec![4]);
1171 }
1172
1173 #[test]
1174 fn test_subgraph_with_depth() {
1175 let dag = make_complex_dag();
1176 let mut sub = dag.subgraph(|n| n.id == 4, 1);
1177 sub.sort();
1178 assert!(sub.contains(&4));
1180 assert!(sub.contains(&0));
1181 assert!(sub.contains(&1));
1182 assert!(sub.contains(&6));
1183 assert!(sub.contains(&7));
1184 }
1185
1186 #[test]
1189 fn test_impact_root() {
1190 let dag = make_linear_dag();
1191 let impact = dag.impact_analysis(0);
1192 assert_eq!(impact.cascade_count, 3);
1193 assert_eq!(impact.by_type[NodeType::Parameter.layer_order()], 1);
1194 assert_eq!(impact.by_type[NodeType::Analysis.layer_order()], 1);
1195 assert_eq!(impact.by_type[NodeType::Report.layer_order()], 1);
1196 }
1197
1198 #[test]
1199 fn test_impact_leaf() {
1200 let dag = make_linear_dag();
1201 let impact = dag.impact_analysis(3);
1202 assert_eq!(impact.cascade_count, 0);
1203 }
1204
1205 #[test]
1206 fn test_impact_complex_param() {
1207 let dag = make_complex_dag();
1208 let impact = dag.impact_analysis(2); assert_eq!(impact.cascade_count, 2);
1211 }
1212
1213 #[test]
1216 fn test_orphans_none() {
1217 let dag = make_linear_dag();
1218 assert!(dag.orphans().is_empty());
1219 }
1220
1221 #[test]
1222 fn test_orphans_found() {
1223 let mut nodes = vec![
1224 make_node(0, NodeType::DataSource, vec![]),
1225 make_node(1, NodeType::Analysis, vec![0]),
1226 make_node(2, NodeType::Report, vec![]), ];
1228 nodes[2].depends_on = vec![];
1229 let dag = Dag::new(nodes);
1230 assert_eq!(dag.orphans(), vec![2]);
1231 }
1232
1233 #[test]
1236 fn test_find_paths_linear() {
1237 let dag = make_linear_dag();
1238 let paths = dag.find_paths(0, 3, 10);
1239 assert_eq!(paths.len(), 1);
1240 assert_eq!(paths[0], vec![0, 1, 2, 3]);
1241 }
1242
1243 #[test]
1244 fn test_find_paths_diamond() {
1245 let dag = make_diamond_dag();
1246 let paths = dag.find_paths(0, 3, 10);
1247 assert_eq!(paths.len(), 2); }
1249
1250 #[test]
1251 fn test_find_paths_no_path() {
1252 let dag = make_linear_dag();
1253 let paths = dag.find_paths(3, 0, 10); assert!(paths.is_empty());
1255 }
1256
1257 #[test]
1260 fn test_crossing_count_no_crossings() {
1261 let dag = make_linear_dag();
1262 let pos = dag.layout(1000.0, 600.0);
1263 assert_eq!(dag.count_crossings(&pos), 0);
1264 }
1265
1266 #[test]
1269 fn test_node_type_roundtrip() {
1270 for t in &[
1271 NodeType::DataSource,
1272 NodeType::Parameter,
1273 NodeType::Analysis,
1274 NodeType::Report,
1275 NodeType::Task,
1276 ] {
1277 assert_eq!(NodeType::parse(t.as_str()), Some(*t));
1278 }
1279 }
1280
1281 #[test]
1282 fn test_node_status_roundtrip() {
1283 for s in &[
1284 NodeStatus::Valid,
1285 NodeStatus::Stale,
1286 NodeStatus::Pending,
1287 NodeStatus::Active,
1288 NodeStatus::Blocked,
1289 ] {
1290 assert_eq!(NodeStatus::parse(s.as_str()), Some(*s));
1291 }
1292 }
1293
1294 #[test]
1297 fn test_validate_clean_dag() {
1298 let dag = make_linear_dag();
1299 let issues = dag.validate();
1300 assert!(issues.is_empty(), "linear DAG should have no issues");
1301 }
1302
1303 #[test]
1304 fn test_validate_orphan_warning() {
1305 let dag = Dag::new(vec![
1306 make_node(0, NodeType::DataSource, vec![]),
1307 make_node(1, NodeType::Analysis, vec![0]),
1308 make_node(2, NodeType::Report, vec![]),
1309 ]);
1310 let issues = dag.validate();
1311 assert!(issues.iter().any(|i| i.contains("Orphan")));
1312 }
1313
1314 #[test]
1315 fn test_validate_data_source_with_deps() {
1316 let dag = Dag::new(vec![
1317 make_node(0, NodeType::Analysis, vec![]),
1318 make_node(1, NodeType::DataSource, vec![0]),
1319 ]);
1320 let issues = dag.validate();
1321 assert!(issues
1322 .iter()
1323 .any(|i| i.contains("Data source") && i.contains("dependencies")));
1324 }
1325
1326 #[test]
1327 fn test_validate_param_depends_on_report() {
1328 let dag = Dag::new(vec![
1329 make_node(0, NodeType::Report, vec![]),
1330 make_node(1, NodeType::Parameter, vec![0]),
1331 ]);
1332 let issues = dag.validate();
1333 assert!(issues
1334 .iter()
1335 .any(|i| i.contains("Parameter") && i.contains("report")));
1336 }
1337
1338 #[test]
1341 fn test_stale_nodes_empty() {
1342 let dag = make_linear_dag();
1343 assert!(dag.stale_nodes().is_empty());
1344 }
1345
1346 #[test]
1347 fn test_stale_nodes_sorted_by_depth() {
1348 let dag = Dag::new(vec![
1349 make_node_with_status(0, NodeType::DataSource, vec![], NodeStatus::Stale),
1350 make_node_with_status(1, NodeType::Parameter, vec![0], NodeStatus::Valid),
1351 make_node_with_status(2, NodeType::Analysis, vec![1], NodeStatus::Stale),
1352 make_node_with_status(3, NodeType::Report, vec![2], NodeStatus::Stale),
1353 ]);
1354 let stale = dag.stale_nodes();
1355 assert_eq!(stale, vec![0, 2, 3], "shallowest stale node first");
1356 }
1357
1358 fn make_task_dag() -> Dag {
1361 Dag::new(vec![
1365 make_node_with_status(0, NodeType::DataSource, vec![], NodeStatus::Valid),
1366 make_node_with_status(1, NodeType::DataSource, vec![], NodeStatus::Valid),
1367 make_node_with_status(2, NodeType::Analysis, vec![0], NodeStatus::Valid),
1368 make_node_with_status(3, NodeType::Analysis, vec![1], NodeStatus::Valid),
1369 make_node_with_status(4, NodeType::Task, vec![2], NodeStatus::Pending), make_node_with_status(5, NodeType::Task, vec![3], NodeStatus::Pending), make_node_with_status(6, NodeType::Task, vec![4, 5], NodeStatus::Pending), ])
1373 }
1374
1375 fn make_node_with_status(
1376 id: usize,
1377 node_type: NodeType,
1378 depends_on: Vec<usize>,
1379 status: NodeStatus,
1380 ) -> DagNode {
1381 DagNode {
1382 id,
1383 node_type,
1384 status,
1385 depends_on,
1386 tags: Vec::new(),
1387 }
1388 }
1389
1390 #[test]
1391 fn test_plannable_tasks() {
1392 let dag = make_task_dag();
1393 let plannable = dag.plannable_tasks();
1394 assert_eq!(plannable, vec![4, 5]);
1396 }
1397
1398 #[test]
1399 fn test_blocked_tasks() {
1400 let dag = make_task_dag();
1401 let blocked = dag.blocked_tasks();
1402 assert_eq!(blocked, vec![6]);
1404 }
1405
1406 #[test]
1407 fn test_active_tasks() {
1408 let dag = Dag::new(vec![
1409 make_node_with_status(0, NodeType::DataSource, vec![], NodeStatus::Valid),
1410 make_node_with_status(1, NodeType::Task, vec![0], NodeStatus::Active),
1411 make_node_with_status(2, NodeType::Task, vec![0], NodeStatus::Pending),
1412 ]);
1413 assert_eq!(dag.active_tasks(), vec![1]);
1414 }
1415
1416 #[test]
1417 fn test_parallel_groups() {
1418 let dag = make_task_dag();
1419 let groups = dag.parallel_groups();
1420 assert_eq!(groups.len(), 1);
1422 let mut group = groups[0].clone();
1423 group.sort();
1424 assert_eq!(group, vec![4, 5]);
1425 }
1426
1427 #[test]
1428 fn test_parallel_groups_serial() {
1429 let dag = Dag::new(vec![
1431 make_node_with_status(0, NodeType::DataSource, vec![], NodeStatus::Valid),
1432 make_node_with_status(1, NodeType::Task, vec![0], NodeStatus::Pending), make_node_with_status(2, NodeType::Task, vec![0, 1], NodeStatus::Pending), ]);
1435 let plannable = dag.plannable_tasks();
1436 assert_eq!(plannable, vec![1]);
1438 }
1439
1440 #[test]
1443 fn test_summarize_basic() {
1444 let dag = make_linear_dag();
1445 let summary = dag.summarize();
1446 assert!(summary.contains("DAG: 4 nodes, 3 edges"));
1447 assert!(summary.contains("data_source=1"));
1448 assert!(summary.contains("valid=4"));
1449 }
1450
1451 #[test]
1452 fn test_summarize_with_warnings() {
1453 let dag = Dag::new(vec![
1454 make_node(0, NodeType::DataSource, vec![]),
1455 make_node(1, NodeType::Report, vec![]),
1456 ]);
1457 let summary = dag.summarize();
1458 assert!(summary.contains("Warnings:"));
1459 }
1460
1461 #[test]
1462 fn test_find_paths_complex_multiple() {
1463 let dag = make_complex_dag();
1465 let mut paths = dag.find_paths(0, 6, 10);
1466 paths.sort_by_key(|p| p.len());
1467 assert_eq!(paths.len(), 2, "two paths from src0 to r0");
1468 assert_eq!(paths[0].len(), 3, "shortest path has 3 nodes");
1469 for p in &paths {
1471 assert_eq!(*p.first().unwrap(), 0);
1472 assert_eq!(*p.last().unwrap(), 6);
1473 }
1474 }
1475
1476 #[test]
1477 fn test_impact_complex_data_source() {
1478 let dag = make_complex_dag();
1481 let impact0 = dag.impact_analysis(0);
1482 assert_eq!(impact0.source, 0);
1483 assert_eq!(
1484 impact0.cascade_count, 4,
1485 "src0 affects 4 nodes: a0, a1, r0, r1"
1486 );
1487
1488 let impact1 = dag.impact_analysis(1);
1489 assert_eq!(impact1.cascade_count, 3, "src1 affects 3 nodes: a1, r0, r1");
1490
1491 let impact2 = dag.impact_analysis(2);
1493 assert_eq!(impact2.cascade_count, 2, "param0 affects 2 nodes: a2, r1");
1494 }
1495
1496 #[test]
1497 fn test_layout_complex_layered() {
1498 let dag = make_complex_dag();
1500 let layout = dag.layout(800.0, 600.0);
1501 assert_eq!(layout.len(), 8, "all 8 nodes should have positions");
1502 let x_src0 = layout[0].0;
1505 let x_r0 = layout[6].0;
1506 assert!(
1507 x_src0 < x_r0,
1508 "data source should be left of report: x_src={}, x_report={}",
1509 x_src0,
1510 x_r0
1511 );
1512 }
1513}