Skip to main content

reinhardt_tasks/
dag.rs

1//! Task Dependency Graph (DAG)
2//!
3//! Provides a Directed Acyclic Graph implementation for managing complex task dependencies.
4//! Unlike `TaskChain` which only supports linear execution, `TaskDAG` enables:
5//! - Complex dependency relationships
6//! - Detection of tasks ready for parallel execution
7//! - Cycle detection
8//! - Topological sorting for execution order
9//!
10//! # Examples
11//!
12//! ```rust
13//! use reinhardt_tasks::{TaskDAG, TaskId};
14//!
15//! let mut dag = TaskDAG::new();
16//!
17//! // Add tasks
18//! let task_a = TaskId::new();
19//! let task_b = TaskId::new();
20//! let task_c = TaskId::new();
21//!
22//! dag.add_task(task_a).unwrap();
23//! dag.add_task(task_b).unwrap();
24//! dag.add_task(task_c).unwrap();
25//!
26//! // Define dependencies: B depends on A, C depends on B
27//! dag.add_dependency(task_b, task_a).unwrap();
28//! dag.add_dependency(task_c, task_b).unwrap();
29//!
30//! // Get execution order
31//! let order = dag.topological_sort().unwrap();
32//! assert_eq!(order.len(), 3);
33//! ```
34
35use crate::{TaskError, TaskId, TaskResult};
36use serde::{Deserialize, Serialize};
37use std::collections::{HashMap, HashSet, VecDeque};
38
39/// Task node status within the DAG
40///
41/// # Examples
42///
43/// ```rust
44/// use reinhardt_tasks::TaskNodeStatus;
45///
46/// let status = TaskNodeStatus::Pending;
47/// assert_eq!(status, TaskNodeStatus::Pending);
48/// ```
49#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
50pub enum TaskNodeStatus {
51	/// Task is waiting for dependencies
52	Pending,
53	/// Task's dependencies are satisfied and ready to execute
54	Ready,
55	/// Task is currently executing
56	Running,
57	/// Task completed successfully
58	Completed,
59	/// Task failed during execution
60	Failed,
61}
62
63/// A node in the task dependency graph
64///
65/// # Examples
66///
67/// ```rust
68/// use reinhardt_tasks::{TaskNode, TaskId, TaskNodeStatus};
69///
70/// let task_id = TaskId::new();
71/// let node = TaskNode::new(task_id);
72/// assert_eq!(node.id(), task_id);
73/// assert_eq!(node.status(), TaskNodeStatus::Pending);
74/// ```
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct TaskNode {
77	/// Task identifier
78	id: TaskId,
79	/// IDs of tasks this node depends on
80	dependencies: Vec<TaskId>,
81	/// Current status of this task
82	status: TaskNodeStatus,
83}
84
85impl TaskNode {
86	/// Create a new task node
87	///
88	/// # Examples
89	///
90	/// ```rust
91	/// use reinhardt_tasks::{TaskNode, TaskId};
92	///
93	/// let task_id = TaskId::new();
94	/// let node = TaskNode::new(task_id);
95	/// ```
96	pub fn new(id: TaskId) -> Self {
97		Self {
98			id,
99			dependencies: Vec::new(),
100			status: TaskNodeStatus::Pending,
101		}
102	}
103
104	/// Get the task ID
105	///
106	/// # Examples
107	///
108	/// ```rust
109	/// use reinhardt_tasks::{TaskNode, TaskId};
110	///
111	/// let task_id = TaskId::new();
112	/// let node = TaskNode::new(task_id);
113	/// assert_eq!(node.id(), task_id);
114	/// ```
115	pub fn id(&self) -> TaskId {
116		self.id
117	}
118
119	/// Get the task dependencies
120	///
121	/// # Examples
122	///
123	/// ```rust
124	/// use reinhardt_tasks::{TaskNode, TaskId};
125	///
126	/// let node = TaskNode::new(TaskId::new());
127	/// assert_eq!(node.dependencies().len(), 0);
128	/// ```
129	pub fn dependencies(&self) -> &[TaskId] {
130		&self.dependencies
131	}
132
133	/// Get the task status
134	///
135	/// # Examples
136	///
137	/// ```rust
138	/// use reinhardt_tasks::{TaskNode, TaskId, TaskNodeStatus};
139	///
140	/// let node = TaskNode::new(TaskId::new());
141	/// assert_eq!(node.status(), TaskNodeStatus::Pending);
142	/// ```
143	pub fn status(&self) -> TaskNodeStatus {
144		self.status
145	}
146
147	/// Add a dependency
148	///
149	/// # Examples
150	///
151	/// ```rust
152	/// use reinhardt_tasks::{TaskNode, TaskId};
153	///
154	/// let mut node = TaskNode::new(TaskId::new());
155	/// let dep_id = TaskId::new();
156	/// node.add_dependency(dep_id);
157	/// assert_eq!(node.dependencies().len(), 1);
158	/// ```
159	pub fn add_dependency(&mut self, task_id: TaskId) {
160		if !self.dependencies.contains(&task_id) {
161			self.dependencies.push(task_id);
162		}
163	}
164
165	/// Set the task status
166	///
167	/// # Examples
168	///
169	/// ```rust
170	/// use reinhardt_tasks::{TaskNode, TaskId, TaskNodeStatus};
171	///
172	/// let mut node = TaskNode::new(TaskId::new());
173	/// node.set_status(TaskNodeStatus::Running);
174	/// assert_eq!(node.status(), TaskNodeStatus::Running);
175	/// ```
176	pub fn set_status(&mut self, status: TaskNodeStatus) {
177		self.status = status;
178	}
179
180	/// Remove a dependency from this node
181	pub(crate) fn remove_dependency(&mut self, task_id: TaskId) {
182		self.dependencies.retain(|&id| id != task_id);
183	}
184}
185
186/// Directed Acyclic Graph for task dependencies
187///
188/// Manages complex task dependencies and provides topological sorting for execution order.
189///
190/// # Examples
191///
192/// ```rust
193/// use reinhardt_tasks::{TaskDAG, TaskId};
194///
195/// let mut dag = TaskDAG::new();
196/// let task_a = TaskId::new();
197/// let task_b = TaskId::new();
198///
199/// dag.add_task(task_a).unwrap();
200/// dag.add_task(task_b).unwrap();
201/// dag.add_dependency(task_b, task_a).unwrap();
202///
203/// let order = dag.topological_sort().unwrap();
204/// assert_eq!(order.len(), 2);
205/// ```
206#[derive(Debug, Clone, Serialize, Deserialize)]
207pub struct TaskDAG {
208	/// Map of task IDs to task nodes
209	nodes: HashMap<TaskId, TaskNode>,
210	/// Adjacency list: task -> tasks that depend on it
211	dependents: HashMap<TaskId, Vec<TaskId>>,
212}
213
214impl TaskDAG {
215	/// Create a new empty task DAG
216	///
217	/// # Examples
218	///
219	/// ```rust
220	/// use reinhardt_tasks::TaskDAG;
221	///
222	/// let dag = TaskDAG::new();
223	/// assert_eq!(dag.task_count(), 0);
224	/// ```
225	pub fn new() -> Self {
226		Self {
227			nodes: HashMap::new(),
228			dependents: HashMap::new(),
229		}
230	}
231
232	/// Add a task to the DAG
233	///
234	/// # Examples
235	///
236	/// ```rust
237	/// use reinhardt_tasks::{TaskDAG, TaskId};
238	///
239	/// let mut dag = TaskDAG::new();
240	/// let task_id = TaskId::new();
241	/// dag.add_task(task_id).unwrap();
242	/// assert_eq!(dag.task_count(), 1);
243	/// ```
244	///
245	/// # Errors
246	///
247	/// Returns an error if the task already exists in the DAG.
248	pub fn add_task(&mut self, task_id: TaskId) -> TaskResult<()> {
249		if self.nodes.contains_key(&task_id) {
250			return Err(TaskError::ExecutionFailed(format!(
251				"Task {} already exists in DAG",
252				task_id
253			)));
254		}
255
256		self.nodes.insert(task_id, TaskNode::new(task_id));
257		self.dependents.insert(task_id, Vec::new());
258		Ok(())
259	}
260
261	/// Add a dependency between tasks
262	///
263	/// `task_id` depends on `depends_on`, meaning `depends_on` must complete before `task_id`.
264	///
265	/// # Examples
266	///
267	/// ```rust
268	/// use reinhardt_tasks::{TaskDAG, TaskId};
269	///
270	/// let mut dag = TaskDAG::new();
271	/// let task_a = TaskId::new();
272	/// let task_b = TaskId::new();
273	///
274	/// dag.add_task(task_a).unwrap();
275	/// dag.add_task(task_b).unwrap();
276	/// dag.add_dependency(task_b, task_a).unwrap();
277	/// ```
278	///
279	/// # Errors
280	///
281	/// Returns an error if:
282	/// - Either task doesn't exist in the DAG
283	/// - The dependency would create a cycle
284	pub fn add_dependency(&mut self, task_id: TaskId, depends_on: TaskId) -> TaskResult<()> {
285		// Validate both tasks exist
286		if !self.nodes.contains_key(&task_id) {
287			return Err(TaskError::TaskNotFound(task_id.to_string()));
288		}
289		if !self.nodes.contains_key(&depends_on) {
290			return Err(TaskError::TaskNotFound(depends_on.to_string()));
291		}
292
293		// Add dependency to the task node
294		if let Some(node) = self.nodes.get_mut(&task_id) {
295			node.add_dependency(depends_on);
296		}
297
298		// Add to dependents adjacency list
299		if let Some(deps) = self.dependents.get_mut(&depends_on)
300			&& !deps.contains(&task_id)
301		{
302			deps.push(task_id);
303		}
304
305		// Verify no cycles were created; roll back the edge on failure
306		if let Err(e) = self.detect_cycle() {
307			if let Some(node) = self.nodes.get_mut(&task_id) {
308				node.remove_dependency(depends_on);
309			}
310			if let Some(deps) = self.dependents.get_mut(&depends_on) {
311				deps.retain(|&id| id != task_id);
312			}
313			return Err(e);
314		}
315
316		Ok(())
317	}
318
319	/// Get the number of tasks in the DAG
320	///
321	/// # Examples
322	///
323	/// ```rust
324	/// use reinhardt_tasks::{TaskDAG, TaskId};
325	///
326	/// let mut dag = TaskDAG::new();
327	/// dag.add_task(TaskId::new()).unwrap();
328	/// dag.add_task(TaskId::new()).unwrap();
329	/// assert_eq!(dag.task_count(), 2);
330	/// ```
331	pub fn task_count(&self) -> usize {
332		self.nodes.len()
333	}
334
335	/// Get a task node by ID
336	///
337	/// # Examples
338	///
339	/// ```rust
340	/// use reinhardt_tasks::{TaskDAG, TaskId};
341	///
342	/// let mut dag = TaskDAG::new();
343	/// let task_id = TaskId::new();
344	/// dag.add_task(task_id).unwrap();
345	///
346	/// let node = dag.get_task(task_id);
347	/// assert!(node.is_some());
348	/// ```
349	pub fn get_task(&self, task_id: TaskId) -> Option<&TaskNode> {
350		self.nodes.get(&task_id)
351	}
352
353	/// Get tasks that are ready to execute (all dependencies satisfied)
354	///
355	/// # Examples
356	///
357	/// ```rust
358	/// use reinhardt_tasks::{TaskDAG, TaskId, TaskNodeStatus};
359	///
360	/// let mut dag = TaskDAG::new();
361	/// let task_a = TaskId::new();
362	/// let task_b = TaskId::new();
363	///
364	/// dag.add_task(task_a).unwrap();
365	/// dag.add_task(task_b).unwrap();
366	/// dag.add_dependency(task_b, task_a).unwrap();
367	///
368	/// let ready = dag.get_ready_tasks();
369	/// assert_eq!(ready.len(), 1); // Only task_a has no dependencies
370	/// ```
371	pub fn get_ready_tasks(&self) -> Vec<TaskId> {
372		self.nodes
373			.values()
374			.filter(|node| {
375				// Task is ready if it's pending and all dependencies are completed
376				node.status() == TaskNodeStatus::Pending
377					&& node
378						.dependencies()
379						.iter()
380						.all(|dep_id| match self.nodes.get(dep_id) {
381							Some(dep_node) => dep_node.status() == TaskNodeStatus::Completed,
382							None => false,
383						})
384			})
385			.map(|node| node.id())
386			.collect()
387	}
388
389	/// Mark a task as completed
390	///
391	/// # Examples
392	///
393	/// ```rust
394	/// use reinhardt_tasks::{TaskDAG, TaskId, TaskNodeStatus};
395	///
396	/// let mut dag = TaskDAG::new();
397	/// let task_id = TaskId::new();
398	/// dag.add_task(task_id).unwrap();
399	///
400	/// dag.mark_completed(task_id).unwrap();
401	/// assert_eq!(dag.get_task(task_id).unwrap().status(), TaskNodeStatus::Completed);
402	/// ```
403	///
404	/// # Errors
405	///
406	/// Returns an error if the task doesn't exist in the DAG.
407	pub fn mark_completed(&mut self, task_id: TaskId) -> TaskResult<()> {
408		let node = self
409			.nodes
410			.get_mut(&task_id)
411			.ok_or_else(|| TaskError::TaskNotFound(task_id.to_string()))?;
412
413		node.set_status(TaskNodeStatus::Completed);
414		Ok(())
415	}
416
417	/// Mark a task as failed
418	///
419	/// # Examples
420	///
421	/// ```rust
422	/// use reinhardt_tasks::{TaskDAG, TaskId, TaskNodeStatus};
423	///
424	/// let mut dag = TaskDAG::new();
425	/// let task_id = TaskId::new();
426	/// dag.add_task(task_id).unwrap();
427	///
428	/// dag.mark_failed(task_id).unwrap();
429	/// assert_eq!(dag.get_task(task_id).unwrap().status(), TaskNodeStatus::Failed);
430	/// ```
431	///
432	/// # Errors
433	///
434	/// Returns an error if the task doesn't exist in the DAG.
435	pub fn mark_failed(&mut self, task_id: TaskId) -> TaskResult<()> {
436		let node = self
437			.nodes
438			.get_mut(&task_id)
439			.ok_or_else(|| TaskError::TaskNotFound(task_id.to_string()))?;
440
441		node.set_status(TaskNodeStatus::Failed);
442		Ok(())
443	}
444
445	/// Mark a task as running
446	///
447	/// # Examples
448	///
449	/// ```rust
450	/// use reinhardt_tasks::{TaskDAG, TaskId, TaskNodeStatus};
451	///
452	/// let mut dag = TaskDAG::new();
453	/// let task_id = TaskId::new();
454	/// dag.add_task(task_id).unwrap();
455	///
456	/// dag.mark_running(task_id).unwrap();
457	/// assert_eq!(dag.get_task(task_id).unwrap().status(), TaskNodeStatus::Running);
458	/// ```
459	///
460	/// # Errors
461	///
462	/// Returns an error if the task doesn't exist in the DAG.
463	pub fn mark_running(&mut self, task_id: TaskId) -> TaskResult<()> {
464		let node = self
465			.nodes
466			.get_mut(&task_id)
467			.ok_or_else(|| TaskError::TaskNotFound(task_id.to_string()))?;
468
469		node.set_status(TaskNodeStatus::Running);
470		Ok(())
471	}
472
473	/// Perform topological sort using Kahn's algorithm
474	///
475	/// Returns an execution order that respects all dependencies.
476	///
477	/// # Examples
478	///
479	/// ```rust
480	/// use reinhardt_tasks::{TaskDAG, TaskId};
481	///
482	/// let mut dag = TaskDAG::new();
483	/// let task_a = TaskId::new();
484	/// let task_b = TaskId::new();
485	/// let task_c = TaskId::new();
486	///
487	/// dag.add_task(task_a).unwrap();
488	/// dag.add_task(task_b).unwrap();
489	/// dag.add_task(task_c).unwrap();
490	/// dag.add_dependency(task_b, task_a).unwrap();
491	/// dag.add_dependency(task_c, task_b).unwrap();
492	///
493	/// let order = dag.topological_sort().unwrap();
494	/// assert_eq!(order.len(), 3);
495	/// // task_a must come before task_b, task_b must come before task_c
496	/// let a_pos = order.iter().position(|&id| id == task_a).unwrap();
497	/// let b_pos = order.iter().position(|&id| id == task_b).unwrap();
498	/// let c_pos = order.iter().position(|&id| id == task_c).unwrap();
499	/// assert!(a_pos < b_pos);
500	/// assert!(b_pos < c_pos);
501	/// ```
502	///
503	/// # Errors
504	///
505	/// Returns an error if the graph contains a cycle.
506	pub fn topological_sort(&self) -> TaskResult<Vec<TaskId>> {
507		// Calculate in-degree for each node
508		let mut in_degree: HashMap<TaskId, usize> = HashMap::new();
509		for (task_id, node) in &self.nodes {
510			in_degree.insert(*task_id, node.dependencies().len());
511		}
512
513		// Queue of nodes with no dependencies
514		let mut queue: VecDeque<TaskId> = in_degree
515			.iter()
516			.filter(|(_, degree)| **degree == 0)
517			.map(|(task_id, _)| *task_id)
518			.collect();
519
520		let mut sorted = Vec::new();
521
522		while let Some(task_id) = queue.pop_front() {
523			sorted.push(task_id);
524
525			// Reduce in-degree for all dependents
526			if let Some(deps) = self.dependents.get(&task_id) {
527				for &dependent in deps {
528					if let Some(degree) = in_degree.get_mut(&dependent) {
529						*degree -= 1;
530						if *degree == 0 {
531							queue.push_back(dependent);
532						}
533					}
534				}
535			}
536		}
537
538		// If sorted doesn't include all nodes, there's a cycle
539		if sorted.len() != self.nodes.len() {
540			return Err(TaskError::ExecutionFailed(
541				"Cycle detected in task dependencies".to_string(),
542			));
543		}
544
545		Ok(sorted)
546	}
547
548	/// Detect if there's a cycle in the graph using iterative DFS
549	///
550	/// Uses an explicit stack instead of recursion to avoid stack overflow
551	/// on deeply nested dependency graphs.
552	///
553	/// # Errors
554	///
555	/// Returns an error if a cycle is detected.
556	fn detect_cycle(&self) -> TaskResult<()> {
557		let mut visited = HashSet::new();
558		let mut rec_stack = HashSet::new();
559
560		for &start_id in self.nodes.keys() {
561			if visited.contains(&start_id) {
562				continue;
563			}
564
565			// Explicit stack: (task_id, dependency_index, is_entering)
566			// is_entering=true means we are visiting this node for the first time
567			let mut stack: Vec<(TaskId, usize, bool)> = vec![(start_id, 0, true)];
568
569			while let Some((task_id, dep_idx, is_entering)) = stack.last_mut() {
570				if *is_entering {
571					visited.insert(*task_id);
572					rec_stack.insert(*task_id);
573					*is_entering = false;
574				}
575
576				let deps = self
577					.nodes
578					.get(task_id)
579					.map(|n| n.dependencies())
580					.unwrap_or(&[]);
581
582				if *dep_idx < deps.len() {
583					let dep_id = deps[*dep_idx];
584					*dep_idx += 1;
585
586					if rec_stack.contains(&dep_id) {
587						return Err(TaskError::ExecutionFailed(format!(
588							"Cycle detected: {} -> {}",
589							task_id, dep_id
590						)));
591					}
592
593					if !visited.contains(&dep_id) {
594						stack.push((dep_id, 0, true));
595					}
596				} else {
597					// All dependencies processed, backtrack
598					rec_stack.remove(task_id);
599					stack.pop();
600				}
601			}
602		}
603
604		Ok(())
605	}
606}
607
608impl Default for TaskDAG {
609	fn default() -> Self {
610		Self::new()
611	}
612}
613
614#[cfg(test)]
615mod tests {
616	use super::*;
617	use rstest::rstest;
618
619	#[rstest]
620	fn test_dag_creation() {
621		// Arrange & Act
622		let dag = TaskDAG::new();
623
624		// Assert
625		assert_eq!(dag.task_count(), 0);
626	}
627
628	#[rstest]
629	fn test_add_task() {
630		// Arrange
631		let mut dag = TaskDAG::new();
632		let task_id = TaskId::new();
633
634		// Act
635		dag.add_task(task_id).unwrap();
636
637		// Assert
638		assert_eq!(dag.task_count(), 1);
639		assert!(dag.get_task(task_id).is_some());
640	}
641
642	#[rstest]
643	fn test_add_duplicate_task() {
644		// Arrange
645		let mut dag = TaskDAG::new();
646		let task_id = TaskId::new();
647		dag.add_task(task_id).unwrap();
648
649		// Act
650		let result = dag.add_task(task_id);
651
652		// Assert
653		assert!(result.is_err());
654	}
655
656	#[rstest]
657	fn test_add_dependency() {
658		// Arrange
659		let mut dag = TaskDAG::new();
660		let task_a = TaskId::new();
661		let task_b = TaskId::new();
662		dag.add_task(task_a).unwrap();
663		dag.add_task(task_b).unwrap();
664
665		// Act
666		dag.add_dependency(task_b, task_a).unwrap();
667
668		// Assert
669		let node_b = dag.get_task(task_b).unwrap();
670		assert_eq!(node_b.dependencies().len(), 1);
671		assert_eq!(node_b.dependencies()[0], task_a);
672	}
673
674	#[rstest]
675	fn test_add_dependency_nonexistent_task() {
676		// Arrange
677		let mut dag = TaskDAG::new();
678		let task_a = TaskId::new();
679		let task_b = TaskId::new();
680		dag.add_task(task_a).unwrap();
681
682		// Act
683		let result = dag.add_dependency(task_a, task_b);
684
685		// Assert
686		assert!(result.is_err());
687	}
688
689	#[rstest]
690	fn test_cycle_detection() {
691		// Arrange
692		let mut dag = TaskDAG::new();
693		let task_a = TaskId::new();
694		let task_b = TaskId::new();
695		let task_c = TaskId::new();
696
697		dag.add_task(task_a).unwrap();
698		dag.add_task(task_b).unwrap();
699		dag.add_task(task_c).unwrap();
700
701		dag.add_dependency(task_b, task_a).unwrap();
702		dag.add_dependency(task_c, task_b).unwrap();
703
704		// Act - creating a cycle: a -> b -> c -> a
705		let result = dag.add_dependency(task_a, task_c);
706
707		// Assert
708		assert!(result.is_err());
709	}
710
711	#[rstest]
712	fn test_topological_sort_simple() {
713		// Arrange
714		let mut dag = TaskDAG::new();
715		let task_a = TaskId::new();
716		let task_b = TaskId::new();
717		let task_c = TaskId::new();
718
719		dag.add_task(task_a).unwrap();
720		dag.add_task(task_b).unwrap();
721		dag.add_task(task_c).unwrap();
722
723		// a -> b -> c
724		dag.add_dependency(task_b, task_a).unwrap();
725		dag.add_dependency(task_c, task_b).unwrap();
726
727		// Act
728		let order = dag.topological_sort().unwrap();
729
730		// Assert
731		assert_eq!(order.len(), 3);
732		let a_pos = order.iter().position(|&id| id == task_a).unwrap();
733		let b_pos = order.iter().position(|&id| id == task_b).unwrap();
734		let c_pos = order.iter().position(|&id| id == task_c).unwrap();
735		assert!(a_pos < b_pos);
736		assert!(b_pos < c_pos);
737	}
738
739	#[rstest]
740	fn test_topological_sort_diamond() {
741		// Arrange
742		let mut dag = TaskDAG::new();
743		let task_a = TaskId::new();
744		let task_b = TaskId::new();
745		let task_c = TaskId::new();
746		let task_d = TaskId::new();
747
748		dag.add_task(task_a).unwrap();
749		dag.add_task(task_b).unwrap();
750		dag.add_task(task_c).unwrap();
751		dag.add_task(task_d).unwrap();
752
753		// Diamond: a -> b, a -> c, b -> d, c -> d
754		dag.add_dependency(task_b, task_a).unwrap();
755		dag.add_dependency(task_c, task_a).unwrap();
756		dag.add_dependency(task_d, task_b).unwrap();
757		dag.add_dependency(task_d, task_c).unwrap();
758
759		// Act
760		let order = dag.topological_sort().unwrap();
761
762		// Assert
763		assert_eq!(order.len(), 4);
764		let a_pos = order.iter().position(|&id| id == task_a).unwrap();
765		let b_pos = order.iter().position(|&id| id == task_b).unwrap();
766		let c_pos = order.iter().position(|&id| id == task_c).unwrap();
767		let d_pos = order.iter().position(|&id| id == task_d).unwrap();
768
769		// a must come before b and c
770		assert!(a_pos < b_pos);
771		assert!(a_pos < c_pos);
772		// b and c must come before d
773		assert!(b_pos < d_pos);
774		assert!(c_pos < d_pos);
775	}
776
777	#[rstest]
778	fn test_get_ready_tasks() {
779		// Arrange
780		let mut dag = TaskDAG::new();
781		let task_a = TaskId::new();
782		let task_b = TaskId::new();
783		let task_c = TaskId::new();
784
785		dag.add_task(task_a).unwrap();
786		dag.add_task(task_b).unwrap();
787		dag.add_task(task_c).unwrap();
788
789		// a -> b -> c
790		dag.add_dependency(task_b, task_a).unwrap();
791		dag.add_dependency(task_c, task_b).unwrap();
792
793		// Assert - initially, only task_a should be ready
794		let ready = dag.get_ready_tasks();
795		assert_eq!(ready.len(), 1);
796		assert!(ready.contains(&task_a));
797
798		// Act - after marking a as completed, b should be ready
799		dag.mark_completed(task_a).unwrap();
800		let ready = dag.get_ready_tasks();
801
802		// Assert
803		assert_eq!(ready.len(), 1);
804		assert!(ready.contains(&task_b));
805
806		// Act - after marking b as completed, c should be ready
807		dag.mark_completed(task_b).unwrap();
808		let ready = dag.get_ready_tasks();
809
810		// Assert
811		assert_eq!(ready.len(), 1);
812		assert!(ready.contains(&task_c));
813	}
814
815	#[rstest]
816	fn test_mark_status() {
817		// Arrange
818		let mut dag = TaskDAG::new();
819		let task_id = TaskId::new();
820		dag.add_task(task_id).unwrap();
821
822		// Assert - initial status
823		assert_eq!(
824			dag.get_task(task_id).unwrap().status(),
825			TaskNodeStatus::Pending
826		);
827
828		// Act & Assert - running
829		dag.mark_running(task_id).unwrap();
830		assert_eq!(
831			dag.get_task(task_id).unwrap().status(),
832			TaskNodeStatus::Running
833		);
834
835		// Act & Assert - completed
836		dag.mark_completed(task_id).unwrap();
837		assert_eq!(
838			dag.get_task(task_id).unwrap().status(),
839			TaskNodeStatus::Completed
840		);
841	}
842
843	#[rstest]
844	fn test_mark_failed() {
845		// Arrange
846		let mut dag = TaskDAG::new();
847		let task_id = TaskId::new();
848		dag.add_task(task_id).unwrap();
849
850		// Act
851		dag.mark_failed(task_id).unwrap();
852
853		// Assert
854		assert_eq!(
855			dag.get_task(task_id).unwrap().status(),
856			TaskNodeStatus::Failed
857		);
858	}
859
860	#[rstest]
861	fn test_parallel_execution_detection() {
862		// Arrange
863		let mut dag = TaskDAG::new();
864		let task_a = TaskId::new();
865		let task_b = TaskId::new();
866		let task_c = TaskId::new();
867		let task_d = TaskId::new();
868
869		dag.add_task(task_a).unwrap();
870		dag.add_task(task_b).unwrap();
871		dag.add_task(task_c).unwrap();
872		dag.add_task(task_d).unwrap();
873
874		// a -> b, a -> c, (b,c) -> d
875		dag.add_dependency(task_b, task_a).unwrap();
876		dag.add_dependency(task_c, task_a).unwrap();
877		dag.add_dependency(task_d, task_b).unwrap();
878		dag.add_dependency(task_d, task_c).unwrap();
879
880		// Act - after completing a, both b and c should be ready
881		dag.mark_completed(task_a).unwrap();
882		let ready = dag.get_ready_tasks();
883
884		// Assert
885		assert_eq!(ready.len(), 2);
886		assert!(ready.contains(&task_b));
887		assert!(ready.contains(&task_c));
888	}
889
890	#[rstest]
891	fn test_deep_dependency_chain_does_not_stack_overflow() {
892		// Arrange - build a deep linear chain: t0 -> t1 -> t2 -> ... -> t999
893		// Iterative DFS handles this without stack overflow, while recursive
894		// DFS would fail for sufficiently deep chains.
895		let mut dag = TaskDAG::new();
896		let depth = 1000;
897		let mut task_ids = Vec::with_capacity(depth);
898
899		for _ in 0..depth {
900			let id = TaskId::new();
901			dag.add_task(id).unwrap();
902			task_ids.push(id);
903		}
904
905		for i in 1..depth {
906			dag.add_dependency(task_ids[i], task_ids[i - 1]).unwrap();
907		}
908
909		// Act - topological sort and cycle detection should succeed
910		let order = dag.topological_sort().unwrap();
911
912		// Assert
913		assert_eq!(order.len(), depth);
914		// Verify ordering: each task appears after its dependency
915		for i in 1..depth {
916			let dep_pos = order.iter().position(|&id| id == task_ids[i - 1]).unwrap();
917			let task_pos = order.iter().position(|&id| id == task_ids[i]).unwrap();
918			assert!(dep_pos < task_pos);
919		}
920	}
921
922	#[rstest]
923	fn test_cycle_detection_on_deep_chain_with_back_edge() {
924		// Arrange - build a deep chain and then add a back-edge to form a cycle
925		let mut dag = TaskDAG::new();
926		let depth = 500;
927		let mut task_ids = Vec::with_capacity(depth);
928
929		for _ in 0..depth {
930			let id = TaskId::new();
931			dag.add_task(id).unwrap();
932			task_ids.push(id);
933		}
934
935		for i in 1..depth {
936			dag.add_dependency(task_ids[i], task_ids[i - 1]).unwrap();
937		}
938
939		// Act - add a back-edge from the first to the last, creating a cycle
940		let result = dag.add_dependency(task_ids[0], task_ids[depth - 1]);
941
942		// Assert
943		assert!(result.is_err());
944	}
945
946	// Regression test for #756: iterative DFS cycle detection must handle chains
947	// of 10,000 nodes without a stack overflow. A naive recursive DFS would exhaust
948	// the default thread stack (typically 8 MiB) for chains this deep.
949	#[rstest]
950	fn test_deep_chain_10k_nodes_does_not_stack_overflow() {
951		// Arrange - linear chain: t0 -> t1 -> ... -> t9999
952		let mut dag = TaskDAG::new();
953		let depth = 10_000;
954		let mut task_ids = Vec::with_capacity(depth);
955
956		for _ in 0..depth {
957			let id = TaskId::new();
958			dag.add_task(id).unwrap();
959			task_ids.push(id);
960		}
961
962		for i in 1..depth {
963			dag.add_dependency(task_ids[i], task_ids[i - 1]).unwrap();
964		}
965
966		// Act - topological sort on the 10k-node chain
967		let order = dag.topological_sort().unwrap();
968
969		// Assert - all nodes present and ordering is preserved
970		assert_eq!(order.len(), depth);
971		for i in 1..depth {
972			let prev_pos = order.iter().position(|&id| id == task_ids[i - 1]).unwrap();
973			let curr_pos = order.iter().position(|&id| id == task_ids[i]).unwrap();
974			assert!(
975				prev_pos < curr_pos,
976				"task_ids[{}] must precede task_ids[{}]",
977				i - 1,
978				i
979			);
980		}
981	}
982
983	// Regression test for #756: cycle detection on a 10k-node chain with a back-edge
984	// must correctly detect the cycle using the iterative algorithm.
985	#[rstest]
986	fn test_deep_chain_10k_nodes_back_edge_detected() {
987		// Arrange - linear chain of 10,000 nodes
988		let mut dag = TaskDAG::new();
989		let depth = 10_000;
990		let mut task_ids = Vec::with_capacity(depth);
991
992		for _ in 0..depth {
993			let id = TaskId::new();
994			dag.add_task(id).unwrap();
995			task_ids.push(id);
996		}
997
998		for i in 1..depth {
999			dag.add_dependency(task_ids[i], task_ids[i - 1]).unwrap();
1000		}
1001
1002		// Act - add a back-edge from the first node to the last node
1003		let result = dag.add_dependency(task_ids[0], task_ids[depth - 1]);
1004
1005		// Assert - cycle must be detected
1006		assert!(
1007			result.is_err(),
1008			"back-edge cycle must be detected in 10k chain"
1009		);
1010	}
1011}