1use super::{Task, TaskDependency, TaskGroup, TaskNode, Tasks};
10use crate::Result;
11use cuenv_task_graph::{GraphNode, TaskNodeData, TaskResolution, TaskResolver};
12use petgraph::graph::NodeIndex;
13use tracing::debug;
14
15impl TaskResolver<Task> for Tasks {
20 fn resolve(&self, name: &str) -> Option<TaskResolution<Task>> {
21 let node = self.resolve_path(name)?;
23 Some(self.node_to_resolution(name, node))
24 }
25}
26
27impl Tasks {
28 fn resolve_path(&self, path: &str) -> Option<&TaskNode> {
39 if let Some(task) = self.tasks.get(path) {
41 return Some(task);
42 }
43
44 let segments = parse_path_segments(path);
46 if segments.is_empty() {
47 return None;
48 }
49
50 let root_name = &segments[0];
52 let root_segment = match root_name {
53 PathSegment::Name(n) => n.as_str(),
54 PathSegment::Index(_) => return None, };
56
57 let mut current = self.tasks.get(root_segment)?;
58
59 for segment in &segments[1..] {
61 current = match (current, segment) {
62 (TaskNode::Group(group), PathSegment::Name(name)) => group.children.get(name)?,
64 (TaskNode::Sequence(steps), PathSegment::Index(idx)) => steps.get(*idx)?,
66 _ => return None,
68 };
69 }
70
71 Some(current)
72 }
73
74 fn node_to_resolution(&self, name: &str, node: &TaskNode) -> TaskResolution<Task> {
76 match node {
77 TaskNode::Task(task) => TaskResolution::Single(task.as_ref().clone()),
78 TaskNode::Sequence(steps) => {
79 let children: Vec<String> = (0..steps.len())
80 .map(|i| format!("{}[{}]", name, i))
81 .collect();
82 TaskResolution::Sequential { children }
83 }
84 TaskNode::Group(group) => {
85 let children: Vec<String> = group
86 .children
87 .keys()
88 .map(|k| format!("{}.{}", name, k))
89 .collect();
90 TaskResolution::Parallel {
91 children,
92 depends_on: group
93 .depends_on
94 .iter()
95 .map(|d| d.task_name().to_string())
96 .collect(),
97 }
98 }
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq)]
105enum PathSegment {
106 Name(String),
108 Index(usize),
110}
111
112fn parse_path_segments(path: &str) -> Vec<PathSegment> {
120 let mut segments = Vec::new();
121 let mut current_name = String::new();
122 let mut chars = path.chars().peekable();
123
124 while let Some(c) = chars.next() {
125 match c {
126 '.' => {
127 if !current_name.is_empty() {
128 segments.push(PathSegment::Name(current_name.clone()));
129 current_name.clear();
130 }
131 }
132 '[' => {
133 if !current_name.is_empty() {
134 segments.push(PathSegment::Name(current_name.clone()));
135 current_name.clear();
136 }
137 let mut index_str = String::new();
139 for c in chars.by_ref() {
140 if c == ']' {
141 break;
142 }
143 index_str.push(c);
144 }
145 if let Ok(idx) = index_str.parse::<usize>() {
146 segments.push(PathSegment::Index(idx));
147 }
148 }
149 _ => {
150 current_name.push(c);
151 }
152 }
153 }
154
155 if !current_name.is_empty() {
157 segments.push(PathSegment::Name(current_name));
158 }
159
160 segments
161}
162
163impl TaskNodeData for Task {
165 fn dependency_names(&self) -> impl Iterator<Item = &str> {
166 self.depends_on.iter().map(|d| d.task_name())
167 }
168
169 fn add_dependency(&mut self, dep: String) {
170 if !self.has_dependency(&dep) {
171 self.depends_on.push(TaskDependency::from_name(dep));
172 }
173 }
174}
175
176pub type TaskGraphNode = GraphNode<Task>;
178
179pub struct TaskGraph {
184 inner: cuenv_task_graph::TaskGraph<Task>,
186}
187
188impl TaskGraph {
189 #[must_use]
191 pub fn new() -> Self {
192 Self {
193 inner: cuenv_task_graph::TaskGraph::new(),
194 }
195 }
196
197 pub fn build_from_node(
199 &mut self,
200 name: &str,
201 node: &TaskNode,
202 all_tasks: &Tasks,
203 ) -> Result<Vec<NodeIndex>> {
204 match node {
205 TaskNode::Task(task) => {
206 let idx = self.add_task(name, task.as_ref().clone())?;
207 Ok(vec![idx])
208 }
209 TaskNode::Group(group) => self.build_parallel_group(name, group, all_tasks),
210 TaskNode::Sequence(steps) => self.build_sequential_list(name, steps, all_tasks),
211 }
212 }
213
214 fn build_sequential_list(
216 &mut self,
217 prefix: &str,
218 steps: &[TaskNode],
219 all_tasks: &Tasks,
220 ) -> Result<Vec<NodeIndex>> {
221 let mut nodes = Vec::new();
222 let mut previous: Option<NodeIndex> = None;
223
224 let child_names: Vec<String> = (0..steps.len())
226 .map(|i| format!("{}[{}]", prefix, i))
227 .collect();
228
229 for (i, step) in steps.iter().enumerate() {
230 let task_name = format!("{}[{}]", prefix, i);
231 let task_nodes = self.build_from_node(&task_name, step, all_tasks)?;
232
233 if let Some(prev) = previous
235 && let Some(first) = task_nodes.first()
236 {
237 self.inner.add_edge(prev, *first);
238 }
239
240 if let Some(last) = task_nodes.last() {
241 previous = Some(*last);
242 }
243
244 nodes.extend(task_nodes);
245 }
246
247 self.inner.register_group(prefix, child_names);
249
250 Ok(nodes)
251 }
252
253 fn build_parallel_group(
255 &mut self,
256 prefix: &str,
257 group: &TaskGroup,
258 all_tasks: &Tasks,
259 ) -> Result<Vec<NodeIndex>> {
260 let mut nodes = Vec::new();
261
262 let child_names: Vec<String> = group
264 .children
265 .keys()
266 .map(|name| format!("{}.{}", prefix, name))
267 .collect();
268
269 for (name, child_node) in &group.children {
270 let task_name = format!("{}.{}", prefix, name);
271 let task_nodes = self.build_from_node(&task_name, child_node, all_tasks)?;
272
273 if !group.depends_on.is_empty() {
275 for node_idx in &task_nodes {
276 if let Some(node) = self.inner.get_node_mut(*node_idx) {
277 for dep in &group.depends_on {
278 node.task.add_dependency(dep.task_name().to_string());
279 }
280 }
281 }
282 }
283
284 nodes.extend(task_nodes);
285 }
286
287 self.inner.register_group(prefix, child_names);
289
290 Ok(nodes)
291 }
292
293 pub fn add_task(&mut self, name: &str, task: Task) -> Result<NodeIndex> {
295 self.inner
296 .add_task(name, task)
297 .map_err(|e| crate::Error::configuration(e.to_string()))
298 }
299
300 pub fn add_dependency_edges(&mut self) -> Result<()> {
303 self.inner
304 .add_dependency_edges()
305 .map_err(|e| crate::Error::configuration(e.to_string()))
306 }
307
308 #[must_use]
310 pub fn has_cycles(&self) -> bool {
311 self.inner.has_cycles()
312 }
313
314 pub fn topological_sort(&self) -> Result<Vec<TaskGraphNode>> {
316 self.inner
317 .topological_sort()
318 .map_err(|e| crate::Error::configuration(e.to_string()))
319 }
320
321 pub fn get_parallel_groups(&self) -> Result<Vec<Vec<TaskGraphNode>>> {
323 self.inner
324 .get_parallel_groups()
325 .map_err(|e| crate::Error::configuration(e.to_string()))
326 }
327
328 #[must_use]
330 pub fn task_count(&self) -> usize {
331 self.inner.task_count()
332 }
333
334 #[must_use]
336 pub fn contains_task(&self, name: &str) -> bool {
337 self.inner.contains_task(name)
338 }
339
340 pub fn build_complete_graph(&mut self, tasks: &Tasks) -> Result<()> {
343 for (name, node) in tasks.tasks.iter() {
345 if let TaskNode::Task(task) = node {
346 self.add_task(name, task.as_ref().clone())?;
347 }
348 }
350
351 self.add_dependency_edges()
353 }
354
355 pub fn build_for_task(&mut self, task_name: &str, all_tasks: &Tasks) -> Result<()> {
360 debug!(
361 "Building graph for '{}' with tasks {:?}",
362 task_name,
363 all_tasks.list_tasks()
364 );
365
366 self.inner
367 .build_for_task_with_resolver(task_name, all_tasks)
368 .map_err(|e| crate::Error::configuration(e.to_string()))
369 }
370
371 pub fn add_output_ref_deps(
383 &mut self,
384 deps: &[(String, String)],
385 all_tasks: &Tasks,
386 ) -> Result<()> {
387 for (from, to) in deps {
388 if self.inner.get_node_index(from).is_none() {
392 continue;
393 }
394
395 if self.inner.get_node_index(to).is_none() {
398 self.inner
399 .build_for_task_with_resolver(to, all_tasks)
400 .map_err(|e| crate::Error::configuration(e.to_string()))?;
401 }
402
403 let from_idx = self.inner.get_node_index(from);
404 let to_idx = self.inner.get_node_index(to);
405
406 if let (Some(from_idx), Some(to_idx)) = (from_idx, to_idx) {
407 let already_exists = self
409 .inner
410 .get_task_mut(from)
411 .is_some_and(|d| d.has_dependency(to));
412
413 if !already_exists {
414 if let Some(from_data) = self.inner.get_task_mut(from) {
416 from_data.add_dependency(to.clone());
417 }
418 self.inner.add_edge(to_idx, from_idx);
420 }
421 }
422 }
423 Ok(())
424 }
425}
426
427impl Default for TaskGraph {
428 fn default() -> Self {
429 Self::new()
430 }
431}
432
433#[cfg(test)]
434#[path = "graph_advanced_tests.rs"]
435mod graph_advanced_tests;
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use crate::tasks::{TaskDependency, TaskGroup, TaskNode};
441 use crate::test_utils::create_task;
442 use std::collections::HashMap;
443
444 #[test]
445 fn test_task_graph_new() {
446 let graph = TaskGraph::new();
447 assert_eq!(graph.task_count(), 0);
448 }
449
450 #[test]
451 fn test_add_single_task() {
452 let mut graph = TaskGraph::new();
453 let task = create_task("test", vec![], vec![]);
454
455 let node = graph.add_task("test", task).unwrap();
456 assert!(graph.contains_task("test"));
457 assert_eq!(graph.task_count(), 1);
458
459 let task2 = create_task("test", vec![], vec![]);
461 let node2 = graph.add_task("test", task2).unwrap();
462 assert_eq!(node, node2);
463 assert_eq!(graph.task_count(), 1);
464 }
465
466 #[test]
467 fn test_task_dependencies() {
468 let mut graph = TaskGraph::new();
469
470 let task1 = create_task("task1", vec![], vec![]);
472 let task2 = create_task("task2", vec!["task1"], vec![]);
473 let task3 = create_task("task3", vec!["task1", "task2"], vec![]);
474
475 graph.add_task("task1", task1).unwrap();
476 graph.add_task("task2", task2).unwrap();
477 graph.add_task("task3", task3).unwrap();
478 graph.add_dependency_edges().unwrap();
479
480 assert_eq!(graph.task_count(), 3);
481 assert!(!graph.has_cycles());
482
483 let sorted = graph.topological_sort().unwrap();
484 assert_eq!(sorted.len(), 3);
485
486 let positions: HashMap<String, usize> = sorted
488 .iter()
489 .enumerate()
490 .map(|(i, node)| (node.name.clone(), i))
491 .collect();
492
493 assert!(positions["task1"] < positions["task2"]);
494 assert!(positions["task1"] < positions["task3"]);
495 assert!(positions["task2"] < positions["task3"]);
496 }
497
498 #[test]
499 fn test_cycle_detection() {
500 let mut graph = TaskGraph::new();
501
502 let task1 = create_task("task1", vec!["task3"], vec![]);
504 let task2 = create_task("task2", vec!["task1"], vec![]);
505 let task3 = create_task("task3", vec!["task2"], vec![]);
506
507 graph.add_task("task1", task1).unwrap();
508 graph.add_task("task2", task2).unwrap();
509 graph.add_task("task3", task3).unwrap();
510 graph.add_dependency_edges().unwrap();
511
512 assert!(graph.has_cycles());
513 assert!(graph.topological_sort().is_err());
514 }
515
516 #[test]
517 fn test_parallel_groups() {
518 let mut graph = TaskGraph::new();
519
520 let task1 = create_task("task1", vec![], vec![]);
526 let task2 = create_task("task2", vec![], vec![]);
527 let task3 = create_task("task3", vec!["task1"], vec![]);
528 let task4 = create_task("task4", vec!["task2"], vec![]);
529 let task5 = create_task("task5", vec!["task3", "task4"], vec![]);
530
531 graph.add_task("task1", task1).unwrap();
532 graph.add_task("task2", task2).unwrap();
533 graph.add_task("task3", task3).unwrap();
534 graph.add_task("task4", task4).unwrap();
535 graph.add_task("task5", task5).unwrap();
536 graph.add_dependency_edges().unwrap();
537
538 let groups = graph.get_parallel_groups().unwrap();
539
540 assert_eq!(groups.len(), 3);
542
543 assert_eq!(groups[0].len(), 2);
545
546 assert_eq!(groups[1].len(), 2);
548
549 assert_eq!(groups[2].len(), 1);
551 assert_eq!(groups[2][0].name, "task5");
552 }
553
554 #[test]
555 fn test_build_from_sequential_group() {
556 let mut graph = TaskGraph::new();
557 let tasks = Tasks::new();
558
559 let task1 = create_task("t1", vec![], vec![]);
560 let task2 = create_task("t2", vec![], vec![]);
561
562 let node = TaskNode::Sequence(vec![
563 TaskNode::Task(Box::new(task1)),
564 TaskNode::Task(Box::new(task2)),
565 ]);
566 let nodes = graph.build_from_node("seq", &node, &tasks).unwrap();
567 assert_eq!(nodes.len(), 2);
568
569 let sorted = graph.topological_sort().unwrap();
571 assert_eq!(sorted.len(), 2);
572 assert_eq!(sorted[0].name, "seq[0]");
573 assert_eq!(sorted[1].name, "seq[1]");
574 }
575
576 #[test]
577 fn test_build_from_parallel_group() {
578 let mut graph = TaskGraph::new();
579 let tasks = Tasks::new();
580
581 let task1 = create_task("t1", vec![], vec![]);
582 let task2 = create_task("t2", vec![], vec![]);
583
584 let mut parallel_tasks = HashMap::new();
585 parallel_tasks.insert("first".to_string(), TaskNode::Task(Box::new(task1)));
586 parallel_tasks.insert("second".to_string(), TaskNode::Task(Box::new(task2)));
587
588 let group = TaskGroup {
589 type_: "group".to_string(),
590 children: parallel_tasks,
591 depends_on: vec![],
592 description: None,
593 max_concurrency: None,
594 };
595
596 let node = TaskNode::Group(group);
597 let nodes = graph.build_from_node("par", &node, &tasks).unwrap();
598 assert_eq!(nodes.len(), 2);
599
600 assert!(!graph.has_cycles());
602
603 let groups = graph.get_parallel_groups().unwrap();
604 assert_eq!(groups.len(), 1); assert_eq!(groups[0].len(), 2); }
607
608 #[test]
609 fn test_three_way_cycle_detection() {
610 let mut graph = TaskGraph::new();
611
612 let task_a = create_task("task_a", vec!["task_c"], vec![]);
614 let task_b = create_task("task_b", vec!["task_a"], vec![]);
615 let task_c = create_task("task_c", vec!["task_b"], vec![]);
616
617 graph.add_task("task_a", task_a).unwrap();
618 graph.add_task("task_b", task_b).unwrap();
619 graph.add_task("task_c", task_c).unwrap();
620 graph.add_dependency_edges().unwrap();
621
622 assert!(graph.has_cycles());
624
625 assert!(graph.get_parallel_groups().is_err());
627 }
628
629 #[test]
634 fn output_ref_deps_adds_missing_target_and_edge() {
635 let mut tasks = Tasks::new();
640 let tmpdir = create_task("tmpdir", vec![], vec![]);
641 let work = create_task("work", vec![], vec![]);
642 tasks
643 .tasks
644 .insert("tmpdir".into(), TaskNode::Task(Box::new(tmpdir)));
645 tasks
646 .tasks
647 .insert("work".into(), TaskNode::Task(Box::new(work)));
648
649 let mut graph = TaskGraph::new();
650 graph.build_for_task("work", &tasks).unwrap();
651 assert!(graph.contains_task("work"));
652 assert!(!graph.contains_task("tmpdir"));
653
654 graph
655 .add_output_ref_deps(&[("work".into(), "tmpdir".into())], &tasks)
656 .unwrap();
657
658 assert!(graph.contains_task("tmpdir"));
660 let sorted = graph.topological_sort().unwrap();
661 let names: Vec<_> = sorted.iter().map(|n| n.name.as_str()).collect();
662 let pos_work = names.iter().position(|&n| n == "work").unwrap();
663 let pos_tmp = names.iter().position(|&n| n == "tmpdir").unwrap();
664 assert!(pos_tmp < pos_work, "tmpdir must precede work");
665
666 let work_data = graph.inner.get_task_mut("work").unwrap().clone();
668 assert!(work_data.has_dependency("tmpdir"));
669 }
670
671 #[test]
672 fn output_ref_deps_skips_when_source_not_in_graph() {
673 let mut tasks = Tasks::new();
676 let a = create_task("a", vec![], vec![]);
677 let b = create_task("b", vec![], vec![]);
678 tasks.tasks.insert("a".into(), TaskNode::Task(Box::new(a)));
679 tasks.tasks.insert("b".into(), TaskNode::Task(Box::new(b)));
680
681 let mut graph = TaskGraph::new();
682 graph.build_for_task("a", &tasks).unwrap();
683 assert!(graph.contains_task("a"));
684 assert!(!graph.contains_task("b"));
685
686 graph
687 .add_output_ref_deps(&[("x".into(), "b".into())], &tasks)
688 .unwrap();
689
690 assert!(graph.contains_task("a"));
692 assert!(!graph.contains_task("b"));
693 }
694
695 #[test]
696 fn output_ref_deps_no_duplicate_for_existing_dependency() {
697 let mut tasks = Tasks::new();
700 let tmpdir = create_task("tmpdir", vec![], vec![]);
701 let work = create_task("work", vec!["tmpdir"], vec![]);
702 tasks
703 .tasks
704 .insert("tmpdir".into(), TaskNode::Task(Box::new(tmpdir)));
705 tasks
706 .tasks
707 .insert("work".into(), TaskNode::Task(Box::new(work)));
708
709 let mut graph = TaskGraph::new();
710 graph.build_for_task("work", &tasks).unwrap();
711 graph.add_dependency_edges().unwrap();
713 let sorted1 = graph.topological_sort().unwrap();
714 let names1: Vec<_> = sorted1.iter().map(|n| n.name.as_str()).collect();
715 assert!(
716 names1.iter().position(|&n| n == "tmpdir").unwrap()
717 < names1.iter().position(|&n| n == "work").unwrap()
718 );
719
720 graph
722 .add_output_ref_deps(&[("work".into(), "tmpdir".into())], &tasks)
723 .unwrap();
724 let sorted2 = graph.topological_sort().unwrap();
725 let names2: Vec<_> = sorted2.iter().map(|n| n.name.as_str()).collect();
726 assert_eq!(names1, names2, "topology should remain unchanged");
727
728 let work_data = graph.inner.get_task_mut("work").unwrap().clone();
730 assert!(work_data.has_dependency("tmpdir"));
731 }
732
733 #[test]
734 fn test_self_dependency_cycle() {
735 let mut graph = TaskGraph::new();
736
737 let task = create_task("self_ref", vec!["self_ref"], vec![]);
739 graph.add_task("self_ref", task).unwrap();
740 graph.add_dependency_edges().unwrap();
741
742 assert!(graph.has_cycles());
743 assert!(graph.get_parallel_groups().is_err());
744 }
745
746 #[test]
747 fn test_complex_dependency_graph() {
748 let mut graph = TaskGraph::new();
749
750 let task_a = create_task("a", vec![], vec![]);
757 let task_b = create_task("b", vec!["a"], vec![]);
758 let task_c = create_task("c", vec!["a"], vec![]);
759 let task_d = create_task("d", vec!["b", "c"], vec![]);
760
761 graph.add_task("a", task_a).unwrap();
762 graph.add_task("b", task_b).unwrap();
763 graph.add_task("c", task_c).unwrap();
764 graph.add_task("d", task_d).unwrap();
765 graph.add_dependency_edges().unwrap();
766
767 assert!(!graph.has_cycles());
768 assert_eq!(graph.task_count(), 4);
769
770 let groups = graph.get_parallel_groups().unwrap();
771
772 assert_eq!(groups.len(), 3);
774 assert_eq!(groups[0].len(), 1); assert_eq!(groups[1].len(), 2); assert_eq!(groups[2].len(), 1); }
778
779 #[test]
780 fn test_missing_dependency() {
781 let mut graph = TaskGraph::new();
782
783 let task = create_task("dependent", vec!["missing"], vec![]);
785 graph.add_task("dependent", task).unwrap();
786
787 assert!(graph.add_dependency_edges().is_err());
789 }
790
791 #[test]
792 fn test_empty_graph() {
793 let graph = TaskGraph::new();
794
795 assert_eq!(graph.task_count(), 0);
796 assert!(!graph.has_cycles());
797
798 let groups = graph.get_parallel_groups().unwrap();
799 assert!(groups.is_empty());
800 }
801
802 #[test]
803 fn test_single_task_no_deps() {
804 let mut graph = TaskGraph::new();
805
806 let task = create_task("solo", vec![], vec![]);
807 graph.add_task("solo", task).unwrap();
808
809 assert_eq!(graph.task_count(), 1);
810 assert!(!graph.has_cycles());
811
812 let groups = graph.get_parallel_groups().unwrap();
813 assert_eq!(groups.len(), 1);
814 assert_eq!(groups[0].len(), 1);
815 }
816
817 #[test]
818 fn test_linear_chain() {
819 let mut graph = TaskGraph::new();
820
821 let task_a = create_task("a", vec![], vec![]);
823 let task_b = create_task("b", vec!["a"], vec![]);
824 let task_c = create_task("c", vec!["b"], vec![]);
825 let task_d = create_task("d", vec!["c"], vec![]);
826
827 graph.add_task("a", task_a).unwrap();
828 graph.add_task("b", task_b).unwrap();
829 graph.add_task("c", task_c).unwrap();
830 graph.add_task("d", task_d).unwrap();
831 graph.add_dependency_edges().unwrap();
832
833 assert!(!graph.has_cycles());
834 assert_eq!(graph.task_count(), 4);
835
836 let groups = graph.get_parallel_groups().unwrap();
837
838 assert_eq!(groups.len(), 4);
840 for group in &groups {
841 assert_eq!(group.len(), 1);
842 }
843 }
844
845 #[test]
846 fn test_group_as_dependency_parallel() {
847 let mut graph = TaskGraph::new();
848 let tasks = Tasks::new();
849
850 let deps_task = create_task("deps", vec![], vec![]);
852 let compile_task = create_task("compile", vec![], vec![]);
853
854 let mut parallel_tasks = HashMap::new();
855 parallel_tasks.insert("deps".to_string(), TaskNode::Task(Box::new(deps_task)));
856 parallel_tasks.insert(
857 "compile".to_string(),
858 TaskNode::Task(Box::new(compile_task)),
859 );
860
861 let build_group = TaskGroup {
862 type_: "group".to_string(),
863 children: parallel_tasks,
864 depends_on: vec![],
865 description: None,
866 max_concurrency: None,
867 };
868
869 let build_node = TaskNode::Group(build_group);
871 graph.build_from_node("build", &build_node, &tasks).unwrap();
872
873 let test_task = create_task("test", vec!["build"], vec![]);
875 graph.add_task("test", test_task).unwrap();
876
877 graph.add_dependency_edges().unwrap();
879
880 assert!(!graph.has_cycles());
881 assert_eq!(graph.task_count(), 3);
882
883 let sorted = graph.topological_sort().unwrap();
885 let positions: HashMap<String, usize> = sorted
886 .iter()
887 .enumerate()
888 .map(|(i, node)| (node.name.clone(), i))
889 .collect();
890
891 assert!(positions["build.deps"] < positions["test"]);
892 assert!(positions["build.compile"] < positions["test"]);
893 }
894
895 #[test]
896 fn test_group_as_dependency_sequential() {
897 let mut graph = TaskGraph::new();
898 let tasks = Tasks::new();
899
900 let task1 = create_task("s1", vec![], vec![]);
902 let task2 = create_task("s2", vec![], vec![]);
903
904 let setup_node = TaskNode::Sequence(vec![
906 TaskNode::Task(Box::new(task1)),
907 TaskNode::Task(Box::new(task2)),
908 ]);
909 graph.build_from_node("setup", &setup_node, &tasks).unwrap();
910
911 let run_task = create_task("run", vec!["setup"], vec![]);
913 graph.add_task("run", run_task).unwrap();
914
915 graph.add_dependency_edges().unwrap();
917
918 assert!(!graph.has_cycles());
919 assert_eq!(graph.task_count(), 3);
920
921 let sorted = graph.topological_sort().unwrap();
923 let positions: HashMap<String, usize> = sorted
924 .iter()
925 .enumerate()
926 .map(|(i, node)| (node.name.clone(), i))
927 .collect();
928
929 assert!(positions["setup[0]"] < positions["run"]);
930 assert!(positions["setup[1]"] < positions["run"]);
931 }
932
933 #[test]
934 fn test_nested_group_as_dependency() {
935 let mut graph = TaskGraph::new();
936 let tasks = Tasks::new();
937
938 let frontend_t1 = create_task("fe1", vec![], vec![]);
946 let frontend_t2 = create_task("fe2", vec![], vec![]);
947 let frontend_seq = TaskNode::Sequence(vec![
948 TaskNode::Task(Box::new(frontend_t1)),
949 TaskNode::Task(Box::new(frontend_t2)),
950 ]);
951
952 let backend_task = create_task("be", vec![], vec![]);
953
954 let mut parallel_tasks = HashMap::new();
955 parallel_tasks.insert("frontend".to_string(), frontend_seq);
956 parallel_tasks.insert(
957 "backend".to_string(),
958 TaskNode::Task(Box::new(backend_task)),
959 );
960
961 let build_group = TaskGroup {
962 type_: "group".to_string(),
963 children: parallel_tasks,
964 depends_on: vec![],
965 description: None,
966 max_concurrency: None,
967 };
968
969 let build_node = TaskNode::Group(build_group);
971 graph.build_from_node("build", &build_node, &tasks).unwrap();
972
973 let deploy_task = create_task("deploy", vec!["build"], vec![]);
975 graph.add_task("deploy", deploy_task).unwrap();
976
977 graph.add_dependency_edges().unwrap();
980
981 assert!(!graph.has_cycles());
982 assert_eq!(graph.task_count(), 4); let sorted = graph.topological_sort().unwrap();
986 let positions: HashMap<String, usize> = sorted
987 .iter()
988 .enumerate()
989 .map(|(i, node)| (node.name.clone(), i))
990 .collect();
991
992 assert!(positions["build.frontend[0]"] < positions["deploy"]);
993 assert!(positions["build.frontend[1]"] < positions["deploy"]);
994 assert!(positions["build.backend"] < positions["deploy"]);
995 }
996
997 #[test]
998 fn test_mixed_exact_and_group_dependencies() {
999 let mut graph = TaskGraph::new();
1000 let tasks = Tasks::new();
1001
1002 let lint_task = create_task("lint", vec![], vec![]);
1004 graph.add_task("lint", lint_task).unwrap();
1005
1006 let deps_task = create_task("deps", vec![], vec![]);
1008 let compile_task = create_task("compile", vec![], vec![]);
1009
1010 let mut parallel_tasks = HashMap::new();
1011 parallel_tasks.insert("deps".to_string(), TaskNode::Task(Box::new(deps_task)));
1012 parallel_tasks.insert(
1013 "compile".to_string(),
1014 TaskNode::Task(Box::new(compile_task)),
1015 );
1016
1017 let build_group = TaskGroup {
1018 type_: "group".to_string(),
1019 children: parallel_tasks,
1020 depends_on: vec![],
1021 description: None,
1022 max_concurrency: None,
1023 };
1024
1025 let build_node = TaskNode::Group(build_group);
1026 graph.build_from_node("build", &build_node, &tasks).unwrap();
1027
1028 let test_task = create_task("test", vec!["lint", "build"], vec![]);
1030 graph.add_task("test", test_task).unwrap();
1031
1032 graph.add_dependency_edges().unwrap();
1033
1034 assert!(!graph.has_cycles());
1035 assert_eq!(graph.task_count(), 4);
1036
1037 let sorted = graph.topological_sort().unwrap();
1039 let positions: HashMap<String, usize> = sorted
1040 .iter()
1041 .enumerate()
1042 .map(|(i, node)| (node.name.clone(), i))
1043 .collect();
1044
1045 assert!(positions["lint"] < positions["test"]);
1046 assert!(positions["build.deps"] < positions["test"]);
1047 assert!(positions["build.compile"] < positions["test"]);
1048 }
1049
1050 #[test]
1051 fn test_cycle_with_group_expansion() {
1052 let mut graph = TaskGraph::new();
1053 let tasks = Tasks::new();
1054
1055 let test_task = create_task("test", vec!["setup"], vec![]);
1060 graph.add_task("test", test_task).unwrap();
1061
1062 let task1 = create_task("s1", vec!["test"], vec![]);
1064 let task2 = create_task("s2", vec![], vec![]);
1065
1066 let setup_node = TaskNode::Sequence(vec![
1067 TaskNode::Task(Box::new(task1)),
1068 TaskNode::Task(Box::new(task2)),
1069 ]);
1070 graph.build_from_node("setup", &setup_node, &tasks).unwrap();
1071
1072 graph.add_dependency_edges().unwrap();
1073
1074 assert!(graph.has_cycles());
1076 assert!(graph.topological_sort().is_err());
1077 }
1078
1079 #[test]
1086 fn contract_diamond_dependency_executes_shared_dep_once() {
1087 let mut graph = TaskGraph::new();
1090
1091 let task_d = create_task("d", vec![], vec![]);
1092 let task_b = create_task("b", vec!["d"], vec![]);
1093 let task_c = create_task("c", vec!["d"], vec![]);
1094 let task_a = create_task("a", vec!["b", "c"], vec![]);
1095
1096 graph.add_task("d", task_d).unwrap();
1097 graph.add_task("b", task_b).unwrap();
1098 graph.add_task("c", task_c).unwrap();
1099 graph.add_task("a", task_a).unwrap();
1100 graph.add_dependency_edges().unwrap();
1101
1102 let sorted = graph.topological_sort().unwrap();
1103 let names: Vec<&str> = sorted.iter().map(|n| n.name.as_str()).collect();
1104
1105 let d_count = names.iter().filter(|&&n| n == "d").count();
1107 assert_eq!(
1108 d_count, 1,
1109 "Diamond dependency: shared task should appear exactly once"
1110 );
1111
1112 let d_pos = names.iter().position(|&n| n == "d").unwrap();
1114 let b_pos = names.iter().position(|&n| n == "b").unwrap();
1115 let c_pos = names.iter().position(|&n| n == "c").unwrap();
1116 let a_pos = names.iter().position(|&n| n == "a").unwrap();
1117
1118 assert!(d_pos < b_pos, "D must execute before B");
1119 assert!(d_pos < c_pos, "D must execute before C");
1120 assert!(b_pos < a_pos, "B must execute before A");
1121 assert!(c_pos < a_pos, "C must execute before A");
1122 }
1123
1124 #[test]
1125 fn contract_parallel_group_children_have_no_implicit_ordering() {
1126 let mut graph = TaskGraph::new();
1129 let tasks = Tasks::new();
1130
1131 let task1 = create_task("task1", vec![], vec![]);
1132 let task2 = create_task("task2", vec![], vec![]);
1133 let task3 = create_task("task3", vec![], vec![]);
1134
1135 let mut parallel_tasks = HashMap::new();
1136 parallel_tasks.insert("task1".to_string(), TaskNode::Task(Box::new(task1)));
1137 parallel_tasks.insert("task2".to_string(), TaskNode::Task(Box::new(task2)));
1138 parallel_tasks.insert("task3".to_string(), TaskNode::Task(Box::new(task3)));
1139
1140 let parallel = TaskGroup {
1141 type_: "group".to_string(),
1142 children: parallel_tasks,
1143 depends_on: vec![],
1144 description: None,
1145 max_concurrency: None,
1146 };
1147
1148 let parallel_node = TaskNode::Group(parallel);
1149 graph
1150 .build_from_node("parallel", ¶llel_node, &tasks)
1151 .unwrap();
1152 graph.add_dependency_edges().unwrap();
1153
1154 let groups = graph.get_parallel_groups().unwrap();
1156 assert_eq!(groups.len(), 1, "All tasks should be in one parallel group");
1157 assert_eq!(
1158 groups[0].len(),
1159 3,
1160 "All three tasks should be executable in parallel"
1161 );
1162 }
1163
1164 #[test]
1165 fn contract_sequential_group_children_have_strict_ordering() {
1166 let mut graph = TaskGraph::new();
1169 let tasks = Tasks::new();
1170
1171 let task1 = create_task("first", vec![], vec![]);
1172 let task2 = create_task("second", vec![], vec![]);
1173 let task3 = create_task("third", vec![], vec![]);
1174
1175 let seq_node = TaskNode::Sequence(vec![
1176 TaskNode::Task(Box::new(task1)),
1177 TaskNode::Task(Box::new(task2)),
1178 TaskNode::Task(Box::new(task3)),
1179 ]);
1180 graph.build_from_node("seq", &seq_node, &tasks).unwrap();
1181
1182 let sorted = graph.topological_sort().unwrap();
1184 let names: Vec<&str> = sorted.iter().map(|n| n.name.as_str()).collect();
1185
1186 let first_pos = names.iter().position(|&n| n == "seq[0]").unwrap();
1188 let second_pos = names.iter().position(|&n| n == "seq[1]").unwrap();
1189 let third_pos = names.iter().position(|&n| n == "seq[2]").unwrap();
1190
1191 assert!(first_pos < second_pos, "seq[0] must execute before seq[1]");
1192 assert!(second_pos < third_pos, "seq[1] must execute before seq[2]");
1193 }
1194
1195 #[test]
1196 fn contract_task_with_label_is_discoverable() {
1197 let task = create_task("build", vec![], vec!["ci", "fast"]);
1199 assert!(task.labels.contains(&"ci".to_string()));
1200 assert!(task.labels.contains(&"fast".to_string()));
1201 }
1202
1203 #[test]
1204 fn contract_topological_sort_is_deterministic() {
1205 let mut graph = TaskGraph::new();
1208
1209 let task_a = create_task("a", vec![], vec![]);
1210 let task_b = create_task("b", vec!["a"], vec![]);
1211 let task_c = create_task("c", vec!["a"], vec![]);
1212 let task_d = create_task("d", vec!["b", "c"], vec![]);
1213
1214 graph.add_task("a", task_a).unwrap();
1215 graph.add_task("b", task_b).unwrap();
1216 graph.add_task("c", task_c).unwrap();
1217 graph.add_task("d", task_d).unwrap();
1218 graph.add_dependency_edges().unwrap();
1219
1220 let sort1 = graph.topological_sort().unwrap();
1221 let sort2 = graph.topological_sort().unwrap();
1222 let sort3 = graph.topological_sort().unwrap();
1223
1224 let names1: Vec<&str> = sort1.iter().map(|n| n.name.as_str()).collect();
1225 let names2: Vec<&str> = sort2.iter().map(|n| n.name.as_str()).collect();
1226 let names3: Vec<&str> = sort3.iter().map(|n| n.name.as_str()).collect();
1227
1228 assert_eq!(names1, names2, "Topological sort should be deterministic");
1229 assert_eq!(names2, names3, "Topological sort should be deterministic");
1230 }
1231
1232 #[test]
1233 fn contract_cycle_detection_catches_all_cycle_types() {
1234 let mut graph1 = TaskGraph::new();
1241 let self_loop = create_task("self", vec!["self"], vec![]);
1242 graph1.add_task("self", self_loop).unwrap();
1243 graph1.add_dependency_edges().unwrap();
1244 assert!(graph1.has_cycles(), "Self-cycle must be detected");
1245
1246 let mut graph2 = TaskGraph::new();
1248 let a = create_task("a", vec!["b"], vec![]);
1249 let b = create_task("b", vec!["a"], vec![]);
1250 graph2.add_task("a", a).unwrap();
1251 graph2.add_task("b", b).unwrap();
1252 graph2.add_dependency_edges().unwrap();
1253 assert!(graph2.has_cycles(), "Two-node cycle must be detected");
1254
1255 let mut graph3 = TaskGraph::new();
1257 let x = create_task("x", vec!["y"], vec![]);
1258 let y = create_task("y", vec!["z"], vec![]);
1259 let z = create_task("z", vec!["x"], vec![]);
1260 graph3.add_task("x", x).unwrap();
1261 graph3.add_task("y", y).unwrap();
1262 graph3.add_task("z", z).unwrap();
1263 graph3.add_dependency_edges().unwrap();
1264 assert!(graph3.has_cycles(), "Three-node cycle must be detected");
1265 }
1266
1267 #[test]
1268 fn contract_missing_dependency_is_reported() {
1269 let mut graph = TaskGraph::new();
1272 let task = create_task("build", vec!["nonexistent"], vec![]);
1273 graph.add_task("build", task).unwrap();
1274
1275 let result = graph.add_dependency_edges();
1276 assert!(result.is_err(), "Missing dependency should be an error");
1277
1278 let err = result.unwrap_err().to_string();
1279 assert!(
1280 err.contains("nonexistent") || err.contains("not found"),
1281 "Error should mention the missing task name: {err}"
1282 );
1283 }
1284
1285 #[test]
1290 fn test_parse_path_segments_simple_name() {
1291 let segments = super::parse_path_segments("build");
1292 assert_eq!(segments, vec![super::PathSegment::Name("build".into())]);
1293 }
1294
1295 #[test]
1296 fn test_parse_path_segments_dotted() {
1297 let segments = super::parse_path_segments("build.frontend");
1298 assert_eq!(
1299 segments,
1300 vec![
1301 super::PathSegment::Name("build".into()),
1302 super::PathSegment::Name("frontend".into()),
1303 ]
1304 );
1305 }
1306
1307 #[test]
1308 fn test_parse_path_segments_indexed() {
1309 let segments = super::parse_path_segments("build[0]");
1310 assert_eq!(
1311 segments,
1312 vec![
1313 super::PathSegment::Name("build".into()),
1314 super::PathSegment::Index(0),
1315 ]
1316 );
1317 }
1318
1319 #[test]
1320 fn test_parse_path_segments_nested() {
1321 let segments = super::parse_path_segments("build.frontend[0]");
1322 assert_eq!(
1323 segments,
1324 vec![
1325 super::PathSegment::Name("build".into()),
1326 super::PathSegment::Name("frontend".into()),
1327 super::PathSegment::Index(0),
1328 ]
1329 );
1330 }
1331
1332 #[test]
1333 fn test_task_resolver_single_task() {
1334 use cuenv_task_graph::TaskResolver;
1335
1336 let task = create_task("build", vec![], vec![]);
1337 let mut tasks = Tasks::new();
1338 tasks
1339 .tasks
1340 .insert("build".into(), TaskNode::Task(Box::new(task)));
1341
1342 let resolution = tasks.resolve("build");
1343 assert!(resolution.is_some());
1344 match resolution.unwrap() {
1345 TaskResolution::Single(t) => assert_eq!(t.command, "echo build"),
1346 _ => panic!("Expected Single resolution"),
1347 }
1348 }
1349
1350 #[test]
1351 fn test_task_resolver_parallel_group() {
1352 use cuenv_task_graph::TaskResolver;
1353
1354 let frontend = create_task("frontend", vec![], vec![]);
1355 let backend = create_task("backend", vec![], vec![]);
1356
1357 let mut parallel_tasks = HashMap::new();
1358 parallel_tasks.insert("frontend".into(), TaskNode::Task(Box::new(frontend)));
1359 parallel_tasks.insert("backend".into(), TaskNode::Task(Box::new(backend)));
1360
1361 let group = TaskGroup {
1362 type_: "group".to_string(),
1363 children: parallel_tasks,
1364 depends_on: vec![TaskDependency::from_name("setup")],
1365 description: None,
1366 max_concurrency: None,
1367 };
1368
1369 let mut tasks = Tasks::new();
1370 tasks.tasks.insert("build".into(), TaskNode::Group(group));
1371
1372 let resolution = tasks.resolve("build");
1373 assert!(resolution.is_some());
1374 match resolution.unwrap() {
1375 TaskResolution::Parallel {
1376 children,
1377 depends_on,
1378 } => {
1379 assert_eq!(children.len(), 2);
1380 assert!(children.contains(&"build.frontend".to_string()));
1381 assert!(children.contains(&"build.backend".to_string()));
1382 assert_eq!(depends_on, vec!["setup"]);
1383 }
1384 _ => panic!("Expected Parallel resolution"),
1385 }
1386 }
1387
1388 #[test]
1389 fn test_task_resolver_sequential_group() {
1390 use cuenv_task_graph::TaskResolver;
1391
1392 let task1 = create_task("t1", vec![], vec![]);
1393 let task2 = create_task("t2", vec![], vec![]);
1394
1395 let seq = TaskNode::Sequence(vec![
1396 TaskNode::Task(Box::new(task1)),
1397 TaskNode::Task(Box::new(task2)),
1398 ]);
1399
1400 let mut tasks = Tasks::new();
1401 tasks.tasks.insert("build".into(), seq);
1402
1403 let resolution = tasks.resolve("build");
1404 assert!(resolution.is_some());
1405 match resolution.unwrap() {
1406 TaskResolution::Sequential { children } => {
1407 assert_eq!(children, vec!["build[0]", "build[1]"]);
1408 }
1409 _ => panic!("Expected Sequential resolution"),
1410 }
1411 }
1412
1413 #[test]
1414 fn test_task_resolver_nested_path() {
1415 use cuenv_task_graph::TaskResolver;
1416
1417 let task = create_task("fe", vec![], vec![]);
1418
1419 let mut parallel_tasks = HashMap::new();
1420 parallel_tasks.insert("frontend".into(), TaskNode::Task(Box::new(task)));
1421
1422 let group = TaskGroup {
1423 type_: "group".to_string(),
1424 children: parallel_tasks,
1425 depends_on: vec![],
1426 description: None,
1427 max_concurrency: None,
1428 };
1429
1430 let mut tasks = Tasks::new();
1431 tasks.tasks.insert("build".into(), TaskNode::Group(group));
1432
1433 let resolution = tasks.resolve("build.frontend");
1435 assert!(resolution.is_some());
1436 match resolution.unwrap() {
1437 TaskResolution::Single(t) => assert_eq!(t.command, "echo fe"),
1438 _ => panic!("Expected Single resolution"),
1439 }
1440 }
1441
1442 #[test]
1443 fn test_task_resolver_nonexistent() {
1444 use cuenv_task_graph::TaskResolver;
1445
1446 let tasks = Tasks::new();
1447 assert!(tasks.resolve("nonexistent").is_none());
1448 }
1449}