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
372impl Default for TaskGraph {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378#[cfg(test)]
379#[path = "graph_advanced_tests.rs"]
380mod graph_advanced_tests;
381
382#[cfg(test)]
383mod tests {
384 use super::*;
385 use crate::tasks::{TaskDependency, TaskGroup, TaskNode};
386 use crate::test_utils::create_task;
387 use std::collections::HashMap;
388
389 #[test]
390 fn test_task_graph_new() {
391 let graph = TaskGraph::new();
392 assert_eq!(graph.task_count(), 0);
393 }
394
395 #[test]
396 fn test_add_single_task() {
397 let mut graph = TaskGraph::new();
398 let task = create_task("test", vec![], vec![]);
399
400 let node = graph.add_task("test", task).unwrap();
401 assert!(graph.contains_task("test"));
402 assert_eq!(graph.task_count(), 1);
403
404 let task2 = create_task("test", vec![], vec![]);
406 let node2 = graph.add_task("test", task2).unwrap();
407 assert_eq!(node, node2);
408 assert_eq!(graph.task_count(), 1);
409 }
410
411 #[test]
412 fn test_task_dependencies() {
413 let mut graph = TaskGraph::new();
414
415 let task1 = create_task("task1", vec![], vec![]);
417 let task2 = create_task("task2", vec!["task1"], vec![]);
418 let task3 = create_task("task3", vec!["task1", "task2"], vec![]);
419
420 graph.add_task("task1", task1).unwrap();
421 graph.add_task("task2", task2).unwrap();
422 graph.add_task("task3", task3).unwrap();
423 graph.add_dependency_edges().unwrap();
424
425 assert_eq!(graph.task_count(), 3);
426 assert!(!graph.has_cycles());
427
428 let sorted = graph.topological_sort().unwrap();
429 assert_eq!(sorted.len(), 3);
430
431 let positions: HashMap<String, usize> = sorted
433 .iter()
434 .enumerate()
435 .map(|(i, node)| (node.name.clone(), i))
436 .collect();
437
438 assert!(positions["task1"] < positions["task2"]);
439 assert!(positions["task1"] < positions["task3"]);
440 assert!(positions["task2"] < positions["task3"]);
441 }
442
443 #[test]
444 fn test_cycle_detection() {
445 let mut graph = TaskGraph::new();
446
447 let task1 = create_task("task1", vec!["task3"], vec![]);
449 let task2 = create_task("task2", vec!["task1"], vec![]);
450 let task3 = create_task("task3", vec!["task2"], vec![]);
451
452 graph.add_task("task1", task1).unwrap();
453 graph.add_task("task2", task2).unwrap();
454 graph.add_task("task3", task3).unwrap();
455 graph.add_dependency_edges().unwrap();
456
457 assert!(graph.has_cycles());
458 assert!(graph.topological_sort().is_err());
459 }
460
461 #[test]
462 fn test_parallel_groups() {
463 let mut graph = TaskGraph::new();
464
465 let task1 = create_task("task1", vec![], vec![]);
471 let task2 = create_task("task2", vec![], vec![]);
472 let task3 = create_task("task3", vec!["task1"], vec![]);
473 let task4 = create_task("task4", vec!["task2"], vec![]);
474 let task5 = create_task("task5", vec!["task3", "task4"], vec![]);
475
476 graph.add_task("task1", task1).unwrap();
477 graph.add_task("task2", task2).unwrap();
478 graph.add_task("task3", task3).unwrap();
479 graph.add_task("task4", task4).unwrap();
480 graph.add_task("task5", task5).unwrap();
481 graph.add_dependency_edges().unwrap();
482
483 let groups = graph.get_parallel_groups().unwrap();
484
485 assert_eq!(groups.len(), 3);
487
488 assert_eq!(groups[0].len(), 2);
490
491 assert_eq!(groups[1].len(), 2);
493
494 assert_eq!(groups[2].len(), 1);
496 assert_eq!(groups[2][0].name, "task5");
497 }
498
499 #[test]
500 fn test_build_from_sequential_group() {
501 let mut graph = TaskGraph::new();
502 let tasks = Tasks::new();
503
504 let task1 = create_task("t1", vec![], vec![]);
505 let task2 = create_task("t2", vec![], vec![]);
506
507 let node = TaskNode::Sequence(vec![
508 TaskNode::Task(Box::new(task1)),
509 TaskNode::Task(Box::new(task2)),
510 ]);
511 let nodes = graph.build_from_node("seq", &node, &tasks).unwrap();
512 assert_eq!(nodes.len(), 2);
513
514 let sorted = graph.topological_sort().unwrap();
516 assert_eq!(sorted.len(), 2);
517 assert_eq!(sorted[0].name, "seq[0]");
518 assert_eq!(sorted[1].name, "seq[1]");
519 }
520
521 #[test]
522 fn test_build_from_parallel_group() {
523 let mut graph = TaskGraph::new();
524 let tasks = Tasks::new();
525
526 let task1 = create_task("t1", vec![], vec![]);
527 let task2 = create_task("t2", vec![], vec![]);
528
529 let mut parallel_tasks = HashMap::new();
530 parallel_tasks.insert("first".to_string(), TaskNode::Task(Box::new(task1)));
531 parallel_tasks.insert("second".to_string(), TaskNode::Task(Box::new(task2)));
532
533 let group = TaskGroup {
534 type_: "group".to_string(),
535 children: parallel_tasks,
536 depends_on: vec![],
537 description: None,
538 max_concurrency: None,
539 };
540
541 let node = TaskNode::Group(group);
542 let nodes = graph.build_from_node("par", &node, &tasks).unwrap();
543 assert_eq!(nodes.len(), 2);
544
545 assert!(!graph.has_cycles());
547
548 let groups = graph.get_parallel_groups().unwrap();
549 assert_eq!(groups.len(), 1); assert_eq!(groups[0].len(), 2); }
552
553 #[test]
554 fn test_three_way_cycle_detection() {
555 let mut graph = TaskGraph::new();
556
557 let task_a = create_task("task_a", vec!["task_c"], vec![]);
559 let task_b = create_task("task_b", vec!["task_a"], vec![]);
560 let task_c = create_task("task_c", vec!["task_b"], vec![]);
561
562 graph.add_task("task_a", task_a).unwrap();
563 graph.add_task("task_b", task_b).unwrap();
564 graph.add_task("task_c", task_c).unwrap();
565 graph.add_dependency_edges().unwrap();
566
567 assert!(graph.has_cycles());
569
570 assert!(graph.get_parallel_groups().is_err());
572 }
573
574 #[test]
575 fn test_self_dependency_cycle() {
576 let mut graph = TaskGraph::new();
577
578 let task = create_task("self_ref", vec!["self_ref"], vec![]);
580 graph.add_task("self_ref", task).unwrap();
581 graph.add_dependency_edges().unwrap();
582
583 assert!(graph.has_cycles());
584 assert!(graph.get_parallel_groups().is_err());
585 }
586
587 #[test]
588 fn test_complex_dependency_graph() {
589 let mut graph = TaskGraph::new();
590
591 let task_a = create_task("a", vec![], vec![]);
598 let task_b = create_task("b", vec!["a"], vec![]);
599 let task_c = create_task("c", vec!["a"], vec![]);
600 let task_d = create_task("d", vec!["b", "c"], vec![]);
601
602 graph.add_task("a", task_a).unwrap();
603 graph.add_task("b", task_b).unwrap();
604 graph.add_task("c", task_c).unwrap();
605 graph.add_task("d", task_d).unwrap();
606 graph.add_dependency_edges().unwrap();
607
608 assert!(!graph.has_cycles());
609 assert_eq!(graph.task_count(), 4);
610
611 let groups = graph.get_parallel_groups().unwrap();
612
613 assert_eq!(groups.len(), 3);
615 assert_eq!(groups[0].len(), 1); assert_eq!(groups[1].len(), 2); assert_eq!(groups[2].len(), 1); }
619
620 #[test]
621 fn test_missing_dependency() {
622 let mut graph = TaskGraph::new();
623
624 let task = create_task("dependent", vec!["missing"], vec![]);
626 graph.add_task("dependent", task).unwrap();
627
628 assert!(graph.add_dependency_edges().is_err());
630 }
631
632 #[test]
633 fn test_empty_graph() {
634 let graph = TaskGraph::new();
635
636 assert_eq!(graph.task_count(), 0);
637 assert!(!graph.has_cycles());
638
639 let groups = graph.get_parallel_groups().unwrap();
640 assert!(groups.is_empty());
641 }
642
643 #[test]
644 fn test_single_task_no_deps() {
645 let mut graph = TaskGraph::new();
646
647 let task = create_task("solo", vec![], vec![]);
648 graph.add_task("solo", task).unwrap();
649
650 assert_eq!(graph.task_count(), 1);
651 assert!(!graph.has_cycles());
652
653 let groups = graph.get_parallel_groups().unwrap();
654 assert_eq!(groups.len(), 1);
655 assert_eq!(groups[0].len(), 1);
656 }
657
658 #[test]
659 fn test_linear_chain() {
660 let mut graph = TaskGraph::new();
661
662 let task_a = create_task("a", vec![], vec![]);
664 let task_b = create_task("b", vec!["a"], vec![]);
665 let task_c = create_task("c", vec!["b"], vec![]);
666 let task_d = create_task("d", vec!["c"], vec![]);
667
668 graph.add_task("a", task_a).unwrap();
669 graph.add_task("b", task_b).unwrap();
670 graph.add_task("c", task_c).unwrap();
671 graph.add_task("d", task_d).unwrap();
672 graph.add_dependency_edges().unwrap();
673
674 assert!(!graph.has_cycles());
675 assert_eq!(graph.task_count(), 4);
676
677 let groups = graph.get_parallel_groups().unwrap();
678
679 assert_eq!(groups.len(), 4);
681 for group in &groups {
682 assert_eq!(group.len(), 1);
683 }
684 }
685
686 #[test]
687 fn test_group_as_dependency_parallel() {
688 let mut graph = TaskGraph::new();
689 let tasks = Tasks::new();
690
691 let deps_task = create_task("deps", vec![], vec![]);
693 let compile_task = create_task("compile", vec![], vec![]);
694
695 let mut parallel_tasks = HashMap::new();
696 parallel_tasks.insert("deps".to_string(), TaskNode::Task(Box::new(deps_task)));
697 parallel_tasks.insert(
698 "compile".to_string(),
699 TaskNode::Task(Box::new(compile_task)),
700 );
701
702 let build_group = TaskGroup {
703 type_: "group".to_string(),
704 children: parallel_tasks,
705 depends_on: vec![],
706 description: None,
707 max_concurrency: None,
708 };
709
710 let build_node = TaskNode::Group(build_group);
712 graph.build_from_node("build", &build_node, &tasks).unwrap();
713
714 let test_task = create_task("test", vec!["build"], vec![]);
716 graph.add_task("test", test_task).unwrap();
717
718 graph.add_dependency_edges().unwrap();
720
721 assert!(!graph.has_cycles());
722 assert_eq!(graph.task_count(), 3);
723
724 let sorted = graph.topological_sort().unwrap();
726 let positions: HashMap<String, usize> = sorted
727 .iter()
728 .enumerate()
729 .map(|(i, node)| (node.name.clone(), i))
730 .collect();
731
732 assert!(positions["build.deps"] < positions["test"]);
733 assert!(positions["build.compile"] < positions["test"]);
734 }
735
736 #[test]
737 fn test_group_as_dependency_sequential() {
738 let mut graph = TaskGraph::new();
739 let tasks = Tasks::new();
740
741 let task1 = create_task("s1", vec![], vec![]);
743 let task2 = create_task("s2", vec![], vec![]);
744
745 let setup_node = TaskNode::Sequence(vec![
747 TaskNode::Task(Box::new(task1)),
748 TaskNode::Task(Box::new(task2)),
749 ]);
750 graph.build_from_node("setup", &setup_node, &tasks).unwrap();
751
752 let run_task = create_task("run", vec!["setup"], vec![]);
754 graph.add_task("run", run_task).unwrap();
755
756 graph.add_dependency_edges().unwrap();
758
759 assert!(!graph.has_cycles());
760 assert_eq!(graph.task_count(), 3);
761
762 let sorted = graph.topological_sort().unwrap();
764 let positions: HashMap<String, usize> = sorted
765 .iter()
766 .enumerate()
767 .map(|(i, node)| (node.name.clone(), i))
768 .collect();
769
770 assert!(positions["setup[0]"] < positions["run"]);
771 assert!(positions["setup[1]"] < positions["run"]);
772 }
773
774 #[test]
775 fn test_nested_group_as_dependency() {
776 let mut graph = TaskGraph::new();
777 let tasks = Tasks::new();
778
779 let frontend_t1 = create_task("fe1", vec![], vec![]);
787 let frontend_t2 = create_task("fe2", vec![], vec![]);
788 let frontend_seq = TaskNode::Sequence(vec![
789 TaskNode::Task(Box::new(frontend_t1)),
790 TaskNode::Task(Box::new(frontend_t2)),
791 ]);
792
793 let backend_task = create_task("be", vec![], vec![]);
794
795 let mut parallel_tasks = HashMap::new();
796 parallel_tasks.insert("frontend".to_string(), frontend_seq);
797 parallel_tasks.insert(
798 "backend".to_string(),
799 TaskNode::Task(Box::new(backend_task)),
800 );
801
802 let build_group = TaskGroup {
803 type_: "group".to_string(),
804 children: parallel_tasks,
805 depends_on: vec![],
806 description: None,
807 max_concurrency: None,
808 };
809
810 let build_node = TaskNode::Group(build_group);
812 graph.build_from_node("build", &build_node, &tasks).unwrap();
813
814 let deploy_task = create_task("deploy", vec!["build"], vec![]);
816 graph.add_task("deploy", deploy_task).unwrap();
817
818 graph.add_dependency_edges().unwrap();
821
822 assert!(!graph.has_cycles());
823 assert_eq!(graph.task_count(), 4); let sorted = graph.topological_sort().unwrap();
827 let positions: HashMap<String, usize> = sorted
828 .iter()
829 .enumerate()
830 .map(|(i, node)| (node.name.clone(), i))
831 .collect();
832
833 assert!(positions["build.frontend[0]"] < positions["deploy"]);
834 assert!(positions["build.frontend[1]"] < positions["deploy"]);
835 assert!(positions["build.backend"] < positions["deploy"]);
836 }
837
838 #[test]
839 fn test_mixed_exact_and_group_dependencies() {
840 let mut graph = TaskGraph::new();
841 let tasks = Tasks::new();
842
843 let lint_task = create_task("lint", vec![], vec![]);
845 graph.add_task("lint", lint_task).unwrap();
846
847 let deps_task = create_task("deps", vec![], vec![]);
849 let compile_task = create_task("compile", vec![], vec![]);
850
851 let mut parallel_tasks = HashMap::new();
852 parallel_tasks.insert("deps".to_string(), TaskNode::Task(Box::new(deps_task)));
853 parallel_tasks.insert(
854 "compile".to_string(),
855 TaskNode::Task(Box::new(compile_task)),
856 );
857
858 let build_group = TaskGroup {
859 type_: "group".to_string(),
860 children: parallel_tasks,
861 depends_on: vec![],
862 description: None,
863 max_concurrency: None,
864 };
865
866 let build_node = TaskNode::Group(build_group);
867 graph.build_from_node("build", &build_node, &tasks).unwrap();
868
869 let test_task = create_task("test", vec!["lint", "build"], vec![]);
871 graph.add_task("test", test_task).unwrap();
872
873 graph.add_dependency_edges().unwrap();
874
875 assert!(!graph.has_cycles());
876 assert_eq!(graph.task_count(), 4);
877
878 let sorted = graph.topological_sort().unwrap();
880 let positions: HashMap<String, usize> = sorted
881 .iter()
882 .enumerate()
883 .map(|(i, node)| (node.name.clone(), i))
884 .collect();
885
886 assert!(positions["lint"] < positions["test"]);
887 assert!(positions["build.deps"] < positions["test"]);
888 assert!(positions["build.compile"] < positions["test"]);
889 }
890
891 #[test]
892 fn test_cycle_with_group_expansion() {
893 let mut graph = TaskGraph::new();
894 let tasks = Tasks::new();
895
896 let test_task = create_task("test", vec!["setup"], vec![]);
901 graph.add_task("test", test_task).unwrap();
902
903 let task1 = create_task("s1", vec!["test"], vec![]);
905 let task2 = create_task("s2", vec![], vec![]);
906
907 let setup_node = TaskNode::Sequence(vec![
908 TaskNode::Task(Box::new(task1)),
909 TaskNode::Task(Box::new(task2)),
910 ]);
911 graph.build_from_node("setup", &setup_node, &tasks).unwrap();
912
913 graph.add_dependency_edges().unwrap();
914
915 assert!(graph.has_cycles());
917 assert!(graph.topological_sort().is_err());
918 }
919
920 #[test]
927 fn contract_diamond_dependency_executes_shared_dep_once() {
928 let mut graph = TaskGraph::new();
931
932 let task_d = create_task("d", vec![], vec![]);
933 let task_b = create_task("b", vec!["d"], vec![]);
934 let task_c = create_task("c", vec!["d"], vec![]);
935 let task_a = create_task("a", vec!["b", "c"], vec![]);
936
937 graph.add_task("d", task_d).unwrap();
938 graph.add_task("b", task_b).unwrap();
939 graph.add_task("c", task_c).unwrap();
940 graph.add_task("a", task_a).unwrap();
941 graph.add_dependency_edges().unwrap();
942
943 let sorted = graph.topological_sort().unwrap();
944 let names: Vec<&str> = sorted.iter().map(|n| n.name.as_str()).collect();
945
946 let d_count = names.iter().filter(|&&n| n == "d").count();
948 assert_eq!(
949 d_count, 1,
950 "Diamond dependency: shared task should appear exactly once"
951 );
952
953 let d_pos = names.iter().position(|&n| n == "d").unwrap();
955 let b_pos = names.iter().position(|&n| n == "b").unwrap();
956 let c_pos = names.iter().position(|&n| n == "c").unwrap();
957 let a_pos = names.iter().position(|&n| n == "a").unwrap();
958
959 assert!(d_pos < b_pos, "D must execute before B");
960 assert!(d_pos < c_pos, "D must execute before C");
961 assert!(b_pos < a_pos, "B must execute before A");
962 assert!(c_pos < a_pos, "C must execute before A");
963 }
964
965 #[test]
966 fn contract_parallel_group_children_have_no_implicit_ordering() {
967 let mut graph = TaskGraph::new();
970 let tasks = Tasks::new();
971
972 let task1 = create_task("task1", vec![], vec![]);
973 let task2 = create_task("task2", vec![], vec![]);
974 let task3 = create_task("task3", vec![], vec![]);
975
976 let mut parallel_tasks = HashMap::new();
977 parallel_tasks.insert("task1".to_string(), TaskNode::Task(Box::new(task1)));
978 parallel_tasks.insert("task2".to_string(), TaskNode::Task(Box::new(task2)));
979 parallel_tasks.insert("task3".to_string(), TaskNode::Task(Box::new(task3)));
980
981 let parallel = TaskGroup {
982 type_: "group".to_string(),
983 children: parallel_tasks,
984 depends_on: vec![],
985 description: None,
986 max_concurrency: None,
987 };
988
989 let parallel_node = TaskNode::Group(parallel);
990 graph
991 .build_from_node("parallel", ¶llel_node, &tasks)
992 .unwrap();
993 graph.add_dependency_edges().unwrap();
994
995 let groups = graph.get_parallel_groups().unwrap();
997 assert_eq!(groups.len(), 1, "All tasks should be in one parallel group");
998 assert_eq!(
999 groups[0].len(),
1000 3,
1001 "All three tasks should be executable in parallel"
1002 );
1003 }
1004
1005 #[test]
1006 fn contract_sequential_group_children_have_strict_ordering() {
1007 let mut graph = TaskGraph::new();
1010 let tasks = Tasks::new();
1011
1012 let task1 = create_task("first", vec![], vec![]);
1013 let task2 = create_task("second", vec![], vec![]);
1014 let task3 = create_task("third", vec![], vec![]);
1015
1016 let seq_node = TaskNode::Sequence(vec![
1017 TaskNode::Task(Box::new(task1)),
1018 TaskNode::Task(Box::new(task2)),
1019 TaskNode::Task(Box::new(task3)),
1020 ]);
1021 graph.build_from_node("seq", &seq_node, &tasks).unwrap();
1022
1023 let sorted = graph.topological_sort().unwrap();
1025 let names: Vec<&str> = sorted.iter().map(|n| n.name.as_str()).collect();
1026
1027 let first_pos = names.iter().position(|&n| n == "seq[0]").unwrap();
1029 let second_pos = names.iter().position(|&n| n == "seq[1]").unwrap();
1030 let third_pos = names.iter().position(|&n| n == "seq[2]").unwrap();
1031
1032 assert!(first_pos < second_pos, "seq[0] must execute before seq[1]");
1033 assert!(second_pos < third_pos, "seq[1] must execute before seq[2]");
1034 }
1035
1036 #[test]
1037 fn contract_task_with_label_is_discoverable() {
1038 let task = create_task("build", vec![], vec!["ci", "fast"]);
1040 assert!(task.labels.contains(&"ci".to_string()));
1041 assert!(task.labels.contains(&"fast".to_string()));
1042 }
1043
1044 #[test]
1045 fn contract_topological_sort_is_deterministic() {
1046 let mut graph = TaskGraph::new();
1049
1050 let task_a = create_task("a", vec![], vec![]);
1051 let task_b = create_task("b", vec!["a"], vec![]);
1052 let task_c = create_task("c", vec!["a"], vec![]);
1053 let task_d = create_task("d", vec!["b", "c"], vec![]);
1054
1055 graph.add_task("a", task_a).unwrap();
1056 graph.add_task("b", task_b).unwrap();
1057 graph.add_task("c", task_c).unwrap();
1058 graph.add_task("d", task_d).unwrap();
1059 graph.add_dependency_edges().unwrap();
1060
1061 let sort1 = graph.topological_sort().unwrap();
1062 let sort2 = graph.topological_sort().unwrap();
1063 let sort3 = graph.topological_sort().unwrap();
1064
1065 let names1: Vec<&str> = sort1.iter().map(|n| n.name.as_str()).collect();
1066 let names2: Vec<&str> = sort2.iter().map(|n| n.name.as_str()).collect();
1067 let names3: Vec<&str> = sort3.iter().map(|n| n.name.as_str()).collect();
1068
1069 assert_eq!(names1, names2, "Topological sort should be deterministic");
1070 assert_eq!(names2, names3, "Topological sort should be deterministic");
1071 }
1072
1073 #[test]
1074 fn contract_cycle_detection_catches_all_cycle_types() {
1075 let mut graph1 = TaskGraph::new();
1082 let self_loop = create_task("self", vec!["self"], vec![]);
1083 graph1.add_task("self", self_loop).unwrap();
1084 graph1.add_dependency_edges().unwrap();
1085 assert!(graph1.has_cycles(), "Self-cycle must be detected");
1086
1087 let mut graph2 = TaskGraph::new();
1089 let a = create_task("a", vec!["b"], vec![]);
1090 let b = create_task("b", vec!["a"], vec![]);
1091 graph2.add_task("a", a).unwrap();
1092 graph2.add_task("b", b).unwrap();
1093 graph2.add_dependency_edges().unwrap();
1094 assert!(graph2.has_cycles(), "Two-node cycle must be detected");
1095
1096 let mut graph3 = TaskGraph::new();
1098 let x = create_task("x", vec!["y"], vec![]);
1099 let y = create_task("y", vec!["z"], vec![]);
1100 let z = create_task("z", vec!["x"], vec![]);
1101 graph3.add_task("x", x).unwrap();
1102 graph3.add_task("y", y).unwrap();
1103 graph3.add_task("z", z).unwrap();
1104 graph3.add_dependency_edges().unwrap();
1105 assert!(graph3.has_cycles(), "Three-node cycle must be detected");
1106 }
1107
1108 #[test]
1109 fn contract_missing_dependency_is_reported() {
1110 let mut graph = TaskGraph::new();
1113 let task = create_task("build", vec!["nonexistent"], vec![]);
1114 graph.add_task("build", task).unwrap();
1115
1116 let result = graph.add_dependency_edges();
1117 assert!(result.is_err(), "Missing dependency should be an error");
1118
1119 let err = result.unwrap_err().to_string();
1120 assert!(
1121 err.contains("nonexistent") || err.contains("not found"),
1122 "Error should mention the missing task name: {err}"
1123 );
1124 }
1125
1126 #[test]
1131 fn test_parse_path_segments_simple_name() {
1132 let segments = super::parse_path_segments("build");
1133 assert_eq!(segments, vec![super::PathSegment::Name("build".into())]);
1134 }
1135
1136 #[test]
1137 fn test_parse_path_segments_dotted() {
1138 let segments = super::parse_path_segments("build.frontend");
1139 assert_eq!(
1140 segments,
1141 vec![
1142 super::PathSegment::Name("build".into()),
1143 super::PathSegment::Name("frontend".into()),
1144 ]
1145 );
1146 }
1147
1148 #[test]
1149 fn test_parse_path_segments_indexed() {
1150 let segments = super::parse_path_segments("build[0]");
1151 assert_eq!(
1152 segments,
1153 vec![
1154 super::PathSegment::Name("build".into()),
1155 super::PathSegment::Index(0),
1156 ]
1157 );
1158 }
1159
1160 #[test]
1161 fn test_parse_path_segments_nested() {
1162 let segments = super::parse_path_segments("build.frontend[0]");
1163 assert_eq!(
1164 segments,
1165 vec![
1166 super::PathSegment::Name("build".into()),
1167 super::PathSegment::Name("frontend".into()),
1168 super::PathSegment::Index(0),
1169 ]
1170 );
1171 }
1172
1173 #[test]
1174 fn test_task_resolver_single_task() {
1175 use cuenv_task_graph::TaskResolver;
1176
1177 let task = create_task("build", vec![], vec![]);
1178 let mut tasks = Tasks::new();
1179 tasks
1180 .tasks
1181 .insert("build".into(), TaskNode::Task(Box::new(task)));
1182
1183 let resolution = tasks.resolve("build");
1184 assert!(resolution.is_some());
1185 match resolution.unwrap() {
1186 TaskResolution::Single(t) => assert_eq!(t.command, "echo build"),
1187 _ => panic!("Expected Single resolution"),
1188 }
1189 }
1190
1191 #[test]
1192 fn test_task_resolver_parallel_group() {
1193 use cuenv_task_graph::TaskResolver;
1194
1195 let frontend = create_task("frontend", vec![], vec![]);
1196 let backend = create_task("backend", vec![], vec![]);
1197
1198 let mut parallel_tasks = HashMap::new();
1199 parallel_tasks.insert("frontend".into(), TaskNode::Task(Box::new(frontend)));
1200 parallel_tasks.insert("backend".into(), TaskNode::Task(Box::new(backend)));
1201
1202 let group = TaskGroup {
1203 type_: "group".to_string(),
1204 children: parallel_tasks,
1205 depends_on: vec![TaskDependency::from_name("setup")],
1206 description: None,
1207 max_concurrency: None,
1208 };
1209
1210 let mut tasks = Tasks::new();
1211 tasks.tasks.insert("build".into(), TaskNode::Group(group));
1212
1213 let resolution = tasks.resolve("build");
1214 assert!(resolution.is_some());
1215 match resolution.unwrap() {
1216 TaskResolution::Parallel {
1217 children,
1218 depends_on,
1219 } => {
1220 assert_eq!(children.len(), 2);
1221 assert!(children.contains(&"build.frontend".to_string()));
1222 assert!(children.contains(&"build.backend".to_string()));
1223 assert_eq!(depends_on, vec!["setup"]);
1224 }
1225 _ => panic!("Expected Parallel resolution"),
1226 }
1227 }
1228
1229 #[test]
1230 fn test_task_resolver_sequential_group() {
1231 use cuenv_task_graph::TaskResolver;
1232
1233 let task1 = create_task("t1", vec![], vec![]);
1234 let task2 = create_task("t2", vec![], vec![]);
1235
1236 let seq = TaskNode::Sequence(vec![
1237 TaskNode::Task(Box::new(task1)),
1238 TaskNode::Task(Box::new(task2)),
1239 ]);
1240
1241 let mut tasks = Tasks::new();
1242 tasks.tasks.insert("build".into(), seq);
1243
1244 let resolution = tasks.resolve("build");
1245 assert!(resolution.is_some());
1246 match resolution.unwrap() {
1247 TaskResolution::Sequential { children } => {
1248 assert_eq!(children, vec!["build[0]", "build[1]"]);
1249 }
1250 _ => panic!("Expected Sequential resolution"),
1251 }
1252 }
1253
1254 #[test]
1255 fn test_task_resolver_nested_path() {
1256 use cuenv_task_graph::TaskResolver;
1257
1258 let task = create_task("fe", vec![], vec![]);
1259
1260 let mut parallel_tasks = HashMap::new();
1261 parallel_tasks.insert("frontend".into(), TaskNode::Task(Box::new(task)));
1262
1263 let group = TaskGroup {
1264 type_: "group".to_string(),
1265 children: parallel_tasks,
1266 depends_on: vec![],
1267 description: None,
1268 max_concurrency: None,
1269 };
1270
1271 let mut tasks = Tasks::new();
1272 tasks.tasks.insert("build".into(), TaskNode::Group(group));
1273
1274 let resolution = tasks.resolve("build.frontend");
1276 assert!(resolution.is_some());
1277 match resolution.unwrap() {
1278 TaskResolution::Single(t) => assert_eq!(t.command, "echo fe"),
1279 _ => panic!("Expected Single resolution"),
1280 }
1281 }
1282
1283 #[test]
1284 fn test_task_resolver_nonexistent() {
1285 use cuenv_task_graph::TaskResolver;
1286
1287 let tasks = Tasks::new();
1288 assert!(tasks.resolve("nonexistent").is_none());
1289 }
1290}