1#![allow(dead_code)]
5#![allow(clippy::missing_errors_doc)]
6#![allow(clippy::type_complexity)]
7
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet, VecDeque};
10use std::sync::{Arc, Mutex};
11use uuid::Uuid;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19pub struct NodeId(Uuid);
20
21impl NodeId {
22 #[must_use]
24 pub fn new() -> Self {
25 Self(Uuid::new_v4())
26 }
27
28 #[must_use]
30 pub const fn as_uuid(&self) -> &Uuid {
31 &self.0
32 }
33}
34
35impl Default for NodeId {
36 fn default() -> Self {
37 Self::new()
38 }
39}
40
41impl std::fmt::Display for NodeId {
42 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43 write!(f, "{}", self.0)
44 }
45}
46
47#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
49pub enum NodeStatus {
50 Pending,
52 Waiting,
54 Running,
56 Completed,
58 Failed(String),
60 Skipped,
62}
63
64impl NodeStatus {
65 #[must_use]
67 pub fn is_terminal(&self) -> bool {
68 matches!(self, Self::Completed | Self::Failed(_) | Self::Skipped)
69 }
70
71 #[must_use]
73 pub fn is_success(&self) -> bool {
74 matches!(self, Self::Completed)
75 }
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct WorkflowNode {
84 pub node_id: NodeId,
86 pub task_type: String,
88 pub inputs: HashMap<String, serde_json::Value>,
90 pub outputs: HashMap<String, serde_json::Value>,
92 pub parameters: HashMap<String, serde_json::Value>,
94 pub status: NodeStatus,
96}
97
98impl WorkflowNode {
99 #[must_use]
101 pub fn new(task_type: impl Into<String>) -> Self {
102 Self {
103 node_id: NodeId::new(),
104 task_type: task_type.into(),
105 inputs: HashMap::new(),
106 outputs: HashMap::new(),
107 parameters: HashMap::new(),
108 status: NodeStatus::Pending,
109 }
110 }
111
112 #[must_use]
114 pub fn with_input(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
115 self.inputs.insert(key.into(), value);
116 self
117 }
118
119 #[must_use]
121 pub fn with_parameter(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
122 self.parameters.insert(key.into(), value);
123 self
124 }
125
126 pub fn set_output(&mut self, key: impl Into<String>, value: serde_json::Value) {
128 self.outputs.insert(key.into(), value);
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
138pub struct WorkflowEdge {
139 pub from_node: NodeId,
141 pub to_node: NodeId,
143 pub data_type: String,
145 pub condition: Option<String>,
147}
148
149impl WorkflowEdge {
150 #[must_use]
152 pub fn new(from_node: NodeId, to_node: NodeId, data_type: impl Into<String>) -> Self {
153 Self {
154 from_node,
155 to_node,
156 data_type: data_type.into(),
157 condition: None,
158 }
159 }
160
161 #[must_use]
163 pub fn with_condition(
164 from_node: NodeId,
165 to_node: NodeId,
166 data_type: impl Into<String>,
167 condition: impl Into<String>,
168 ) -> Self {
169 Self {
170 from_node,
171 to_node,
172 data_type: data_type.into(),
173 condition: Some(condition.into()),
174 }
175 }
176}
177
178#[derive(Debug, thiserror::Error)]
184pub enum DagError {
185 #[error("Node not found: {0}")]
187 NodeNotFound(NodeId),
188
189 #[error("Cycle detected in DAG")]
191 CycleDetected,
192
193 #[error("Node already exists: {0}")]
195 DuplicateNode(NodeId),
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize, Default)]
202pub struct WorkflowDag {
203 pub nodes: HashMap<NodeId, WorkflowNode>,
205 pub edges: Vec<WorkflowEdge>,
207}
208
209impl WorkflowDag {
210 #[must_use]
212 pub fn new() -> Self {
213 Self::default()
214 }
215
216 pub fn add_node(&mut self, node: WorkflowNode) -> Result<NodeId, DagError> {
218 let id = node.node_id;
219 if self.nodes.contains_key(&id) {
220 return Err(DagError::DuplicateNode(id));
221 }
222 self.nodes.insert(id, node);
223 Ok(id)
224 }
225
226 pub fn add_edge(&mut self, edge: WorkflowEdge) -> Result<(), DagError> {
228 if !self.nodes.contains_key(&edge.from_node) {
229 return Err(DagError::NodeNotFound(edge.from_node));
230 }
231 if !self.nodes.contains_key(&edge.to_node) {
232 return Err(DagError::NodeNotFound(edge.to_node));
233 }
234 self.edges.push(edge);
235
236 if self.has_cycle() {
238 self.edges.pop();
239 return Err(DagError::CycleDetected);
240 }
241 Ok(())
242 }
243
244 #[must_use]
246 pub fn has_cycle(&self) -> bool {
247 let mut visited = HashSet::new();
248 let mut stack = HashSet::new();
249 for &id in self.nodes.keys() {
250 if self.dfs_cycle(id, &mut visited, &mut stack) {
251 return true;
252 }
253 }
254 false
255 }
256
257 fn dfs_cycle(
258 &self,
259 id: NodeId,
260 visited: &mut HashSet<NodeId>,
261 stack: &mut HashSet<NodeId>,
262 ) -> bool {
263 if stack.contains(&id) {
264 return true;
265 }
266 if visited.contains(&id) {
267 return false;
268 }
269 visited.insert(id);
270 stack.insert(id);
271 for edge in &self.edges {
272 if edge.from_node == id && self.dfs_cycle(edge.to_node, visited, stack) {
273 return true;
274 }
275 }
276 stack.remove(&id);
277 false
278 }
279
280 pub fn topological_sort(&self) -> Result<Vec<NodeId>, DagError> {
285 if self.has_cycle() {
286 return Err(DagError::CycleDetected);
287 }
288
289 let mut in_degree: HashMap<NodeId, usize> = self.nodes.keys().map(|&k| (k, 0)).collect();
291 for edge in &self.edges {
292 *in_degree.entry(edge.to_node).or_insert(0) += 1;
293 }
294
295 let mut queue: VecDeque<NodeId> = in_degree
296 .iter()
297 .filter(|(_, °)| deg == 0)
298 .map(|(&id, _)| id)
299 .collect();
300
301 let mut result = Vec::with_capacity(self.nodes.len());
302 while let Some(id) = queue.pop_front() {
303 result.push(id);
304 for edge in &self.edges {
305 if edge.from_node == id {
306 let deg = in_degree.entry(edge.to_node).or_insert(0);
307 *deg = deg.saturating_sub(1);
308 if *deg == 0 {
309 queue.push_back(edge.to_node);
310 }
311 }
312 }
313 }
314
315 Ok(result)
316 }
317
318 #[must_use]
320 pub fn predecessors(&self, node_id: NodeId) -> Vec<NodeId> {
321 self.edges
322 .iter()
323 .filter(|e| e.to_node == node_id)
324 .map(|e| e.from_node)
325 .collect()
326 }
327
328 #[must_use]
330 pub fn successors(&self, node_id: NodeId) -> Vec<NodeId> {
331 self.edges
332 .iter()
333 .filter(|e| e.from_node == node_id)
334 .map(|e| e.to_node)
335 .collect()
336 }
337}
338
339#[derive(Debug, Clone, Serialize, Deserialize)]
345pub struct DagRunStatus {
346 pub node_statuses: HashMap<NodeId, NodeStatus>,
348 pub succeeded: bool,
350 pub nodes_executed: usize,
352 pub nodes_failed: usize,
354}
355
356pub struct DagWorkflowEngine {
361 executor: Option<Arc<dyn Fn(&mut WorkflowNode) -> Result<(), String> + Send + Sync>>,
363 statuses: Arc<Mutex<HashMap<NodeId, NodeStatus>>>,
365}
366
367impl DagWorkflowEngine {
368 #[must_use]
370 pub fn new() -> Self {
371 Self {
372 executor: None,
373 statuses: Arc::new(Mutex::new(HashMap::new())),
374 }
375 }
376
377 #[must_use]
379 pub fn with_executor<F>(mut self, f: F) -> Self
380 where
381 F: Fn(&mut WorkflowNode) -> Result<(), String> + Send + Sync + 'static,
382 {
383 self.executor = Some(Arc::new(f));
384 self
385 }
386
387 pub fn execute(&self, dag: &mut WorkflowDag) -> Result<DagRunStatus, DagError> {
393 let order = dag.topological_sort()?;
394
395 let mut statuses: HashMap<NodeId, NodeStatus> = HashMap::new();
396 let mut nodes_executed = 0usize;
397 let mut nodes_failed = 0usize;
398
399 for node_id in &order {
400 let Some(node) = dag.nodes.get_mut(node_id) else {
401 continue;
402 };
403
404 node.status = NodeStatus::Running;
405 statuses.insert(*node_id, NodeStatus::Running);
406
407 let result = if let Some(ref exec) = self.executor {
408 exec(node)
409 } else {
410 Ok(())
411 };
412
413 match result {
414 Ok(()) => {
415 node.status = NodeStatus::Completed;
416 statuses.insert(*node_id, NodeStatus::Completed);
417 nodes_executed += 1;
418 }
419 Err(msg) => {
420 node.status = NodeStatus::Failed(msg.clone());
421 statuses.insert(*node_id, NodeStatus::Failed(msg));
422 nodes_failed += 1;
423 }
424 }
425 }
426
427 if let Ok(mut guard) = self.statuses.lock() {
429 guard.extend(statuses.clone());
430 }
431
432 let succeeded = nodes_failed == 0;
433
434 Ok(DagRunStatus {
435 node_statuses: statuses,
436 succeeded,
437 nodes_executed,
438 nodes_failed,
439 })
440 }
441
442 #[must_use]
444 pub fn node_status(&self, node_id: NodeId) -> Option<NodeStatus> {
445 self.statuses.lock().ok()?.get(&node_id).cloned()
446 }
447}
448
449impl Default for DagWorkflowEngine {
450 fn default() -> Self {
451 Self::new()
452 }
453}
454
455#[derive(Debug, Clone, Serialize, Deserialize)]
462pub struct WorkflowTemplate {
463 pub name: String,
465 pub description: String,
467 pub default_parameters: HashMap<String, serde_json::Value>,
469 node_specs: Vec<NodeSpec>,
471 edge_specs: Vec<(usize, usize, String)>,
473}
474
475#[derive(Debug, Clone, Serialize, Deserialize)]
477struct NodeSpec {
478 task_type: String,
479 parameters: HashMap<String, serde_json::Value>,
480}
481
482impl WorkflowTemplate {
483 #[must_use]
485 pub fn new(name: impl Into<String>, description: impl Into<String>) -> Self {
486 Self {
487 name: name.into(),
488 description: description.into(),
489 default_parameters: HashMap::new(),
490 node_specs: Vec::new(),
491 edge_specs: Vec::new(),
492 }
493 }
494
495 #[must_use]
497 pub fn with_default_parameter(
498 mut self,
499 key: impl Into<String>,
500 value: serde_json::Value,
501 ) -> Self {
502 self.default_parameters.insert(key.into(), value);
503 self
504 }
505
506 pub fn add_node(
508 &mut self,
509 task_type: impl Into<String>,
510 parameters: HashMap<String, serde_json::Value>,
511 ) -> usize {
512 self.node_specs.push(NodeSpec {
513 task_type: task_type.into(),
514 parameters,
515 });
516 self.node_specs.len() - 1
517 }
518
519 pub fn add_edge(&mut self, from_idx: usize, to_idx: usize, data_type: impl Into<String>) {
521 self.edge_specs.push((from_idx, to_idx, data_type.into()));
522 }
523
524 pub fn instantiate(
528 &self,
529 overrides: &HashMap<String, serde_json::Value>,
530 ) -> Result<WorkflowDag, DagError> {
531 let mut dag = WorkflowDag::new();
532 let mut ids: Vec<NodeId> = Vec::with_capacity(self.node_specs.len());
533
534 for spec in &self.node_specs {
535 let mut params = self.default_parameters.clone();
536 params.extend(spec.parameters.clone());
537 params.extend(overrides.clone());
538
539 let node = WorkflowNode {
540 node_id: NodeId::new(),
541 task_type: spec.task_type.clone(),
542 inputs: HashMap::new(),
543 outputs: HashMap::new(),
544 parameters: params,
545 status: NodeStatus::Pending,
546 };
547 let id = dag.add_node(node)?;
548 ids.push(id);
549 }
550
551 for &(from_idx, to_idx, ref dt) in &self.edge_specs {
552 let edge = WorkflowEdge::new(ids[from_idx], ids[to_idx], dt.clone());
553 dag.add_edge(edge)?;
554 }
555
556 Ok(dag)
557 }
558
559 #[must_use]
561 pub fn node_count(&self) -> usize {
562 self.node_specs.len()
563 }
564}
565
566#[derive(Debug, Clone, Serialize, Deserialize)]
572pub enum BranchType {
573 IfElse {
576 condition: String,
578 then_branch: NodeId,
580 else_branch: NodeId,
582 },
583 Switch {
586 key: String,
588 cases: HashMap<String, NodeId>,
590 default: Option<NodeId>,
592 },
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize)]
597pub struct BranchNode {
598 pub node_id: NodeId,
600 pub predecessor: NodeId,
602 pub branch_type: BranchType,
604}
605
606pub struct BranchEvaluator;
608
609impl BranchEvaluator {
610 #[must_use]
624 pub fn evaluate_condition(
625 condition: &str,
626 outputs: &HashMap<String, serde_json::Value>,
627 ) -> Option<bool> {
628 let parts: Vec<&str> = condition.trim().splitn(3, ' ').collect();
629 if parts.len() < 2 {
630 return None;
631 }
632
633 let key = parts[0];
634
635 if parts.len() == 2 {
637 return match parts[1] {
638 "exists" => Some(outputs.contains_key(key)),
639 "not_exists" => Some(!outputs.contains_key(key)),
640 _ => None,
641 };
642 }
643
644 let op = parts[1];
645 let rhs = parts[2];
646 let lhs_val = outputs.get(key)?;
647
648 match op {
649 "==" => {
650 let rhs_trimmed = rhs.trim_matches('"');
651 if let Some(s) = lhs_val.as_str() {
652 Some(s == rhs_trimmed)
653 } else if let Some(n) = lhs_val.as_f64() {
654 rhs.parse::<f64>()
655 .ok()
656 .map(|r| (n - r).abs() < f64::EPSILON)
657 } else if let Some(b) = lhs_val.as_bool() {
658 rhs.parse::<bool>().ok().map(|r| b == r)
659 } else {
660 None
661 }
662 }
663 "!=" => {
664 let eq = Self::evaluate_condition(&format!("{key} == {rhs}"), outputs)?;
665 Some(!eq)
666 }
667 ">" | ">=" | "<" | "<=" => {
668 let lhs_num = lhs_val.as_f64()?;
669 let rhs_num = rhs.parse::<f64>().ok()?;
670 Some(match op {
671 ">" => lhs_num > rhs_num,
672 ">=" => lhs_num >= rhs_num,
673 "<" => lhs_num < rhs_num,
674 "<=" => lhs_num <= rhs_num,
675 _ => return None,
676 })
677 }
678 _ => None,
679 }
680 }
681
682 #[must_use]
685 pub fn resolve_branch(
686 branch: &BranchNode,
687 predecessor_outputs: &HashMap<String, serde_json::Value>,
688 ) -> Vec<NodeId> {
689 match &branch.branch_type {
690 BranchType::IfElse {
691 condition,
692 then_branch,
693 else_branch,
694 } => {
695 let result =
696 Self::evaluate_condition(condition, predecessor_outputs).unwrap_or(false);
697 if result {
698 vec![*then_branch]
699 } else {
700 vec![*else_branch]
701 }
702 }
703 BranchType::Switch {
704 key,
705 cases,
706 default,
707 } => {
708 if let Some(val) = predecessor_outputs.get(key) {
709 let val_str = if let Some(s) = val.as_str() {
710 s.to_string()
711 } else {
712 val.to_string()
713 };
714 if let Some(&target) = cases.get(&val_str) {
715 return vec![target];
716 }
717 }
718 default.map_or_else(Vec::new, |d| vec![d])
719 }
720 }
721 }
722}
723
724impl WorkflowDag {
725 pub fn execute_with_branches(
735 &mut self,
736 branches: &HashMap<NodeId, BranchNode>,
737 executor: Option<&dyn Fn(&mut WorkflowNode) -> Result<(), String>>,
738 ) -> Result<DagRunStatus, DagError> {
739 let order = self.topological_sort()?;
740
741 let mut statuses: HashMap<NodeId, NodeStatus> = HashMap::new();
742 let mut nodes_executed = 0usize;
743 let mut nodes_failed = 0usize;
744 let mut skipped_nodes: HashSet<NodeId> = HashSet::new();
745
746 for &node_id in &order {
747 if skipped_nodes.contains(&node_id) {
749 if let Some(node) = self.nodes.get_mut(&node_id) {
750 node.status = NodeStatus::Skipped;
751 }
752 statuses.insert(node_id, NodeStatus::Skipped);
753 continue;
754 }
755
756 let Some(node) = self.nodes.get_mut(&node_id) else {
757 continue;
758 };
759
760 node.status = NodeStatus::Running;
761 statuses.insert(node_id, NodeStatus::Running);
762
763 let result = if let Some(exec) = &executor {
764 exec(node)
765 } else {
766 Ok(())
767 };
768
769 match result {
770 Ok(()) => {
771 node.status = NodeStatus::Completed;
772 statuses.insert(node_id, NodeStatus::Completed);
773 nodes_executed += 1;
774
775 if let Some(branch) = branches.get(&node_id) {
777 let predecessor_outputs = self
778 .nodes
779 .get(&branch.predecessor)
780 .map(|n| n.outputs.clone())
781 .unwrap_or_default();
782 let selected =
783 BranchEvaluator::resolve_branch(branch, &predecessor_outputs);
784
785 let all_successors = self.successors(node_id);
787 for succ_id in &all_successors {
788 if !selected.contains(succ_id) {
789 Self::collect_descendants_static(
790 &self.edges,
791 *succ_id,
792 &mut skipped_nodes,
793 );
794 skipped_nodes.insert(*succ_id);
795 }
796 }
797 }
798 }
799 Err(msg) => {
800 node.status = NodeStatus::Failed(msg.clone());
801 statuses.insert(node_id, NodeStatus::Failed(msg));
802 nodes_failed += 1;
803 }
804 }
805 }
806
807 let succeeded = nodes_failed == 0;
808 Ok(DagRunStatus {
809 node_statuses: statuses,
810 succeeded,
811 nodes_executed,
812 nodes_failed,
813 })
814 }
815
816 fn collect_descendants_static(
818 edges: &[WorkflowEdge],
819 node_id: NodeId,
820 result: &mut HashSet<NodeId>,
821 ) {
822 for edge in edges {
823 if edge.from_node == node_id && !result.contains(&edge.to_node) {
824 result.insert(edge.to_node);
825 Self::collect_descendants_static(edges, edge.to_node, result);
826 }
827 }
828 }
829
830 #[must_use]
832 pub fn descendants(&self, node_id: NodeId) -> HashSet<NodeId> {
833 let mut result = HashSet::new();
834 Self::collect_descendants_static(&self.edges, node_id, &mut result);
835 result
836 }
837}
838
839#[must_use]
847pub fn ingest_transcode() -> WorkflowTemplate {
848 let mut tmpl = WorkflowTemplate::new(
849 "ingest_transcode",
850 "Ingest a source file, probe it, transcode, then package for delivery",
851 )
852 .with_default_parameter("output_format", serde_json::json!("mp4"))
853 .with_default_parameter("preset", serde_json::json!("medium"));
854
855 let i0 = tmpl.add_node("ingest", HashMap::new());
856 let i1 = tmpl.add_node("probe", HashMap::new());
857 let i2 = tmpl.add_node("transcode", HashMap::new());
858 let i3 = tmpl.add_node("package", HashMap::new());
859
860 tmpl.add_edge(i0, i1, "raw_media");
861 tmpl.add_edge(i1, i2, "media_info");
862 tmpl.add_edge(i2, i3, "encoded_video");
863
864 tmpl
865}
866
867#[must_use]
871pub fn subtitle_burn() -> WorkflowTemplate {
872 let mut tmpl = WorkflowTemplate::new(
873 "subtitle_burn",
874 "Parse subtitle file and burn it into the video stream",
875 )
876 .with_default_parameter("subtitle_format", serde_json::json!("srt"))
877 .with_default_parameter("font_size", serde_json::json!(24));
878
879 let i0 = tmpl.add_node("ingest", HashMap::new());
880 let i1 = tmpl.add_node("subtitle_parse", HashMap::new());
881 let i2 = tmpl.add_node("burn_subtitles", HashMap::new());
882 let i3 = tmpl.add_node("encode", HashMap::new());
883 let i4 = tmpl.add_node("deliver", HashMap::new());
884
885 tmpl.add_edge(i0, i1, "raw_media");
886 tmpl.add_edge(i1, i2, "subtitle_events");
887 tmpl.add_edge(i2, i3, "filtered_video");
888 tmpl.add_edge(i3, i4, "encoded_video");
889
890 tmpl
891}
892
893#[must_use]
897pub fn audio_normalize() -> WorkflowTemplate {
898 let mut tmpl = WorkflowTemplate::new(
899 "audio_normalize",
900 "Analyze audio loudness and normalize to a target LUFS level",
901 )
902 .with_default_parameter("target_lufs", serde_json::json!(-23.0))
903 .with_default_parameter("true_peak_dbtp", serde_json::json!(-1.0));
904
905 let i0 = tmpl.add_node("ingest", HashMap::new());
906 let i1 = tmpl.add_node("audio_analyze", HashMap::new());
907 let i2 = tmpl.add_node("normalize", HashMap::new());
908 let i3 = tmpl.add_node("encode", HashMap::new());
909 let i4 = tmpl.add_node("deliver", HashMap::new());
910
911 tmpl.add_edge(i0, i1, "raw_audio");
912 tmpl.add_edge(i1, i2, "loudness_stats");
913 tmpl.add_edge(i2, i3, "normalized_audio");
914 tmpl.add_edge(i3, i4, "encoded_audio");
915
916 tmpl
917}
918
919#[cfg(test)]
924mod tests {
925 use super::*;
926
927 fn make_node(task_type: &str) -> WorkflowNode {
928 WorkflowNode::new(task_type)
929 }
930
931 #[test]
934 fn test_node_creation() {
935 let node = make_node("transcode");
936 assert_eq!(node.task_type, "transcode");
937 assert_eq!(node.status, NodeStatus::Pending);
938 assert!(node.inputs.is_empty());
939 assert!(node.outputs.is_empty());
940 assert!(node.parameters.is_empty());
941 }
942
943 #[test]
944 fn test_node_with_input_and_parameter() {
945 let node = make_node("encode")
946 .with_input("src", serde_json::json!("/tmp/in.mp4"))
947 .with_parameter("preset", serde_json::json!("slow"));
948 assert_eq!(node.inputs["src"], serde_json::json!("/tmp/in.mp4"));
949 assert_eq!(node.parameters["preset"], serde_json::json!("slow"));
950 }
951
952 #[test]
953 fn test_node_set_output() {
954 let mut node = make_node("transcode");
955 node.set_output("dst", serde_json::json!("/tmp/out.mp4"));
956 assert!(node.outputs.contains_key("dst"));
957 }
958
959 #[test]
960 fn test_node_status_terminal() {
961 assert!(!NodeStatus::Pending.is_terminal());
962 assert!(!NodeStatus::Running.is_terminal());
963 assert!(NodeStatus::Completed.is_terminal());
964 assert!(NodeStatus::Failed("err".to_string()).is_terminal());
965 assert!(NodeStatus::Skipped.is_terminal());
966 }
967
968 #[test]
971 fn test_edge_creation() {
972 let a = NodeId::new();
973 let b = NodeId::new();
974 let edge = WorkflowEdge::new(a, b, "video/mp4");
975 assert_eq!(edge.from_node, a);
976 assert_eq!(edge.to_node, b);
977 assert_eq!(edge.data_type, "video/mp4");
978 assert!(edge.condition.is_none());
979 }
980
981 #[test]
982 fn test_edge_with_condition() {
983 let a = NodeId::new();
984 let b = NodeId::new();
985 let edge = WorkflowEdge::with_condition(a, b, "audio/pcm", "bitrate > 128");
986 assert_eq!(edge.condition, Some("bitrate > 128".to_string()));
987 }
988
989 #[test]
992 fn test_dag_add_node_and_edge() {
993 let mut dag = WorkflowDag::new();
994 let a = dag
995 .add_node(make_node("ingest"))
996 .expect("should succeed in test");
997 let b = dag
998 .add_node(make_node("transcode"))
999 .expect("should succeed in test");
1000 dag.add_edge(WorkflowEdge::new(a, b, "raw_media"))
1001 .expect("should succeed in test");
1002
1003 assert_eq!(dag.nodes.len(), 2);
1004 assert_eq!(dag.edges.len(), 1);
1005 assert!(!dag.has_cycle());
1006 }
1007
1008 #[test]
1009 fn test_dag_cycle_detection() {
1010 let mut dag = WorkflowDag::new();
1011 let a = dag
1012 .add_node(make_node("a"))
1013 .expect("should succeed in test");
1014 let b = dag
1015 .add_node(make_node("b"))
1016 .expect("should succeed in test");
1017 dag.add_edge(WorkflowEdge::new(a, b, "x"))
1018 .expect("should succeed in test");
1019 let result = dag.add_edge(WorkflowEdge::new(b, a, "x"));
1020 assert!(matches!(result, Err(DagError::CycleDetected)));
1021 }
1022
1023 #[test]
1024 fn test_dag_topological_sort() {
1025 let mut dag = WorkflowDag::new();
1026 let a = dag
1027 .add_node(make_node("a"))
1028 .expect("should succeed in test");
1029 let b = dag
1030 .add_node(make_node("b"))
1031 .expect("should succeed in test");
1032 let c = dag
1033 .add_node(make_node("c"))
1034 .expect("should succeed in test");
1035 dag.add_edge(WorkflowEdge::new(a, b, "x"))
1036 .expect("should succeed in test");
1037 dag.add_edge(WorkflowEdge::new(b, c, "x"))
1038 .expect("should succeed in test");
1039
1040 let order = dag.topological_sort().expect("should succeed in test");
1041 let pos_a = order
1042 .iter()
1043 .position(|&x| x == a)
1044 .expect("should succeed in test");
1045 let pos_b = order
1046 .iter()
1047 .position(|&x| x == b)
1048 .expect("should succeed in test");
1049 let pos_c = order
1050 .iter()
1051 .position(|&x| x == c)
1052 .expect("should succeed in test");
1053 assert!(pos_a < pos_b && pos_b < pos_c);
1054 }
1055
1056 #[test]
1057 fn test_dag_predecessors_successors() {
1058 let mut dag = WorkflowDag::new();
1059 let a = dag
1060 .add_node(make_node("a"))
1061 .expect("should succeed in test");
1062 let b = dag
1063 .add_node(make_node("b"))
1064 .expect("should succeed in test");
1065 let c = dag
1066 .add_node(make_node("c"))
1067 .expect("should succeed in test");
1068 dag.add_edge(WorkflowEdge::new(a, c, "x"))
1069 .expect("should succeed in test");
1070 dag.add_edge(WorkflowEdge::new(b, c, "x"))
1071 .expect("should succeed in test");
1072
1073 let preds = dag.predecessors(c);
1074 assert_eq!(preds.len(), 2);
1075 assert!(preds.contains(&a));
1076 assert!(preds.contains(&b));
1077
1078 let succs = dag.successors(a);
1079 assert_eq!(succs.len(), 1);
1080 assert_eq!(succs[0], c);
1081 }
1082
1083 #[test]
1086 fn test_engine_execute_no_executor() {
1087 let mut dag = WorkflowDag::new();
1088 let a = dag
1089 .add_node(make_node("a"))
1090 .expect("should succeed in test");
1091 let b = dag
1092 .add_node(make_node("b"))
1093 .expect("should succeed in test");
1094 dag.add_edge(WorkflowEdge::new(a, b, "x"))
1095 .expect("should succeed in test");
1096
1097 let engine = DagWorkflowEngine::new();
1098 let result = engine.execute(&mut dag).expect("should succeed in test");
1099 assert!(result.succeeded);
1100 assert_eq!(result.nodes_executed, 2);
1101 assert_eq!(result.nodes_failed, 0);
1102 }
1103
1104 #[test]
1105 fn test_engine_execute_with_executor() {
1106 let mut dag = WorkflowDag::new();
1107 let a = dag
1108 .add_node(make_node("a"))
1109 .expect("should succeed in test");
1110 let b = dag
1111 .add_node(make_node("b"))
1112 .expect("should succeed in test");
1113 dag.add_edge(WorkflowEdge::new(a, b, "x"))
1114 .expect("should succeed in test");
1115
1116 let engine = DagWorkflowEngine::new().with_executor(|node| {
1117 node.set_output("done", serde_json::json!(true));
1118 Ok(())
1119 });
1120
1121 let result = engine.execute(&mut dag).expect("should succeed in test");
1122 assert!(result.succeeded);
1123 assert_eq!(result.nodes_executed, 2);
1124 assert!(dag.nodes[&a].outputs.contains_key("done"));
1126 }
1127
1128 #[test]
1129 fn test_engine_node_failure() {
1130 let mut dag = WorkflowDag::new();
1131 let a = dag
1132 .add_node(make_node("failing"))
1133 .expect("should succeed in test");
1134 dag.add_node(make_node("b"))
1135 .expect("should succeed in test"); let engine = DagWorkflowEngine::new().with_executor(|node| {
1138 if node.task_type == "failing" {
1139 Err("intentional failure".to_string())
1140 } else {
1141 Ok(())
1142 }
1143 });
1144
1145 let result = engine.execute(&mut dag).expect("should succeed in test");
1146 assert!(!result.succeeded);
1147 assert!(result.nodes_failed > 0);
1148 assert!(matches!(result.node_statuses[&a], NodeStatus::Failed(_)));
1149 }
1150
1151 #[test]
1152 fn test_engine_node_status_accessor() {
1153 let mut dag = WorkflowDag::new();
1154 let a = dag
1155 .add_node(make_node("a"))
1156 .expect("should succeed in test");
1157
1158 let engine = DagWorkflowEngine::new();
1159 engine.execute(&mut dag).expect("should succeed in test");
1160
1161 assert_eq!(engine.node_status(a), Some(NodeStatus::Completed));
1162 assert_eq!(engine.node_status(NodeId::new()), None);
1163 }
1164
1165 #[test]
1168 fn test_template_instantiate_ingest_transcode() {
1169 let tmpl = ingest_transcode();
1170 assert_eq!(tmpl.name, "ingest_transcode");
1171 assert_eq!(tmpl.node_count(), 4);
1172
1173 let dag = tmpl
1174 .instantiate(&HashMap::new())
1175 .expect("should succeed in test");
1176 assert_eq!(dag.nodes.len(), 4);
1177 assert_eq!(dag.edges.len(), 3);
1178 assert!(!dag.has_cycle());
1179 }
1180
1181 #[test]
1182 fn test_template_instantiate_subtitle_burn() {
1183 let tmpl = subtitle_burn();
1184 let dag = tmpl
1185 .instantiate(&HashMap::new())
1186 .expect("should succeed in test");
1187 assert_eq!(dag.nodes.len(), 5);
1188 assert_eq!(dag.edges.len(), 4);
1189 assert!(!dag.has_cycle());
1190 }
1191
1192 #[test]
1193 fn test_template_instantiate_audio_normalize() {
1194 let tmpl = audio_normalize();
1195 let dag = tmpl
1196 .instantiate(&HashMap::new())
1197 .expect("should succeed in test");
1198 assert_eq!(dag.nodes.len(), 5);
1199 assert_eq!(dag.edges.len(), 4);
1200 assert!(!dag.has_cycle());
1201 }
1202
1203 #[test]
1204 fn test_template_parameter_override() {
1205 let tmpl = ingest_transcode();
1206 let mut overrides = HashMap::new();
1207 overrides.insert("preset".to_string(), serde_json::json!("ultrafast"));
1208
1209 let dag = tmpl
1210 .instantiate(&overrides)
1211 .expect("should succeed in test");
1212 for node in dag.nodes.values() {
1214 assert_eq!(node.parameters["preset"], serde_json::json!("ultrafast"));
1215 }
1216 }
1217
1218 #[test]
1219 fn test_template_default_parameters() {
1220 let tmpl = audio_normalize();
1221 assert_eq!(
1222 tmpl.default_parameters["target_lufs"],
1223 serde_json::json!(-23.0)
1224 );
1225 assert_eq!(
1226 tmpl.default_parameters["true_peak_dbtp"],
1227 serde_json::json!(-1.0)
1228 );
1229 }
1230
1231 #[test]
1232 fn test_dag_error_node_not_found() {
1233 let mut dag = WorkflowDag::new();
1234 let a = dag
1235 .add_node(make_node("a"))
1236 .expect("should succeed in test");
1237 let ghost = NodeId::new();
1238 let result = dag.add_edge(WorkflowEdge::new(a, ghost, "x"));
1239 assert!(matches!(result, Err(DagError::NodeNotFound(_))));
1240 }
1241
1242 #[test]
1245 fn test_branch_evaluator_equality() {
1246 let mut outputs = HashMap::new();
1247 outputs.insert("codec".to_string(), serde_json::json!("h264"));
1248
1249 assert_eq!(
1250 BranchEvaluator::evaluate_condition("codec == h264", &outputs),
1251 Some(true)
1252 );
1253 assert_eq!(
1254 BranchEvaluator::evaluate_condition("codec == vp9", &outputs),
1255 Some(false)
1256 );
1257 assert_eq!(
1258 BranchEvaluator::evaluate_condition("codec != vp9", &outputs),
1259 Some(true)
1260 );
1261 }
1262
1263 #[test]
1264 fn test_branch_evaluator_numeric_comparison() {
1265 let mut outputs = HashMap::new();
1266 outputs.insert("bitrate".to_string(), serde_json::json!(5000.0));
1267
1268 assert_eq!(
1269 BranchEvaluator::evaluate_condition("bitrate > 3000", &outputs),
1270 Some(true)
1271 );
1272 assert_eq!(
1273 BranchEvaluator::evaluate_condition("bitrate < 3000", &outputs),
1274 Some(false)
1275 );
1276 assert_eq!(
1277 BranchEvaluator::evaluate_condition("bitrate >= 5000", &outputs),
1278 Some(true)
1279 );
1280 assert_eq!(
1281 BranchEvaluator::evaluate_condition("bitrate <= 5000", &outputs),
1282 Some(true)
1283 );
1284 }
1285
1286 #[test]
1287 fn test_branch_evaluator_exists() {
1288 let mut outputs = HashMap::new();
1289 outputs.insert("result".to_string(), serde_json::json!(42));
1290
1291 assert_eq!(
1292 BranchEvaluator::evaluate_condition("result exists", &outputs),
1293 Some(true)
1294 );
1295 assert_eq!(
1296 BranchEvaluator::evaluate_condition("missing not_exists", &outputs),
1297 Some(true)
1298 );
1299 assert_eq!(
1300 BranchEvaluator::evaluate_condition("result not_exists", &outputs),
1301 Some(false)
1302 );
1303 }
1304
1305 #[test]
1306 fn test_branch_evaluator_boolean() {
1307 let mut outputs = HashMap::new();
1308 outputs.insert("success".to_string(), serde_json::json!(true));
1309
1310 assert_eq!(
1311 BranchEvaluator::evaluate_condition("success == true", &outputs),
1312 Some(true)
1313 );
1314 assert_eq!(
1315 BranchEvaluator::evaluate_condition("success == false", &outputs),
1316 Some(false)
1317 );
1318 }
1319
1320 #[test]
1321 fn test_branch_evaluator_numeric_equality() {
1322 let mut outputs = HashMap::new();
1323 outputs.insert("count".to_string(), serde_json::json!(42.0));
1324
1325 assert_eq!(
1326 BranchEvaluator::evaluate_condition("count == 42", &outputs),
1327 Some(true)
1328 );
1329 }
1330
1331 #[test]
1332 fn test_branch_evaluator_invalid_expression() {
1333 let outputs = HashMap::new();
1334 assert_eq!(BranchEvaluator::evaluate_condition("", &outputs), None);
1335 assert_eq!(
1336 BranchEvaluator::evaluate_condition("single", &outputs),
1337 None
1338 );
1339 }
1340
1341 #[test]
1342 fn test_resolve_branch_if_else_true() {
1343 let then_id = NodeId::new();
1344 let else_id = NodeId::new();
1345 let pred_id = NodeId::new();
1346 let branch_id = NodeId::new();
1347
1348 let branch = BranchNode {
1349 node_id: branch_id,
1350 predecessor: pred_id,
1351 branch_type: BranchType::IfElse {
1352 condition: "quality > 90".to_string(),
1353 then_branch: then_id,
1354 else_branch: else_id,
1355 },
1356 };
1357
1358 let mut outputs = HashMap::new();
1359 outputs.insert("quality".to_string(), serde_json::json!(95.0));
1360
1361 let result = BranchEvaluator::resolve_branch(&branch, &outputs);
1362 assert_eq!(result, vec![then_id]);
1363 }
1364
1365 #[test]
1366 fn test_resolve_branch_if_else_false() {
1367 let then_id = NodeId::new();
1368 let else_id = NodeId::new();
1369 let pred_id = NodeId::new();
1370 let branch_id = NodeId::new();
1371
1372 let branch = BranchNode {
1373 node_id: branch_id,
1374 predecessor: pred_id,
1375 branch_type: BranchType::IfElse {
1376 condition: "quality > 90".to_string(),
1377 then_branch: then_id,
1378 else_branch: else_id,
1379 },
1380 };
1381
1382 let mut outputs = HashMap::new();
1383 outputs.insert("quality".to_string(), serde_json::json!(50.0));
1384
1385 let result = BranchEvaluator::resolve_branch(&branch, &outputs);
1386 assert_eq!(result, vec![else_id]);
1387 }
1388
1389 #[test]
1390 fn test_resolve_branch_switch() {
1391 let av1_id = NodeId::new();
1392 let h264_id = NodeId::new();
1393 let default_id = NodeId::new();
1394 let pred_id = NodeId::new();
1395 let branch_id = NodeId::new();
1396
1397 let mut cases = HashMap::new();
1398 cases.insert("av1".to_string(), av1_id);
1399 cases.insert("h264".to_string(), h264_id);
1400
1401 let branch = BranchNode {
1402 node_id: branch_id,
1403 predecessor: pred_id,
1404 branch_type: BranchType::Switch {
1405 key: "codec".to_string(),
1406 cases,
1407 default: Some(default_id),
1408 },
1409 };
1410
1411 let mut outputs = HashMap::new();
1412 outputs.insert("codec".to_string(), serde_json::json!("av1"));
1413 assert_eq!(
1414 BranchEvaluator::resolve_branch(&branch, &outputs),
1415 vec![av1_id]
1416 );
1417
1418 outputs.insert("codec".to_string(), serde_json::json!("h264"));
1419 assert_eq!(
1420 BranchEvaluator::resolve_branch(&branch, &outputs),
1421 vec![h264_id]
1422 );
1423
1424 outputs.insert("codec".to_string(), serde_json::json!("vp9"));
1425 assert_eq!(
1426 BranchEvaluator::resolve_branch(&branch, &outputs),
1427 vec![default_id]
1428 );
1429 }
1430
1431 #[test]
1432 fn test_resolve_branch_switch_no_default() {
1433 let av1_id = NodeId::new();
1434 let pred_id = NodeId::new();
1435 let branch_id = NodeId::new();
1436
1437 let mut cases = HashMap::new();
1438 cases.insert("av1".to_string(), av1_id);
1439
1440 let branch = BranchNode {
1441 node_id: branch_id,
1442 predecessor: pred_id,
1443 branch_type: BranchType::Switch {
1444 key: "codec".to_string(),
1445 cases,
1446 default: None,
1447 },
1448 };
1449
1450 let mut outputs = HashMap::new();
1451 outputs.insert("codec".to_string(), serde_json::json!("vp9"));
1452 assert!(BranchEvaluator::resolve_branch(&branch, &outputs).is_empty());
1453 }
1454
1455 #[test]
1456 fn test_execute_with_branches_if_else() {
1457 let mut dag = WorkflowDag::new();
1461 let probe = dag.add_node(make_node("probe")).expect("add node");
1462 let decision = dag.add_node(make_node("branch")).expect("add node");
1463 let high = dag.add_node(make_node("high_res")).expect("add node");
1464 let low = dag.add_node(make_node("low_res")).expect("add node");
1465
1466 dag.add_edge(WorkflowEdge::new(probe, decision, "media_info"))
1467 .expect("add edge");
1468 dag.add_edge(WorkflowEdge::new(decision, high, "video"))
1469 .expect("add edge");
1470 dag.add_edge(WorkflowEdge::new(decision, low, "video"))
1471 .expect("add edge");
1472
1473 let branch = BranchNode {
1474 node_id: decision,
1475 predecessor: probe,
1476 branch_type: BranchType::IfElse {
1477 condition: "resolution > 1080".to_string(),
1478 then_branch: high,
1479 else_branch: low,
1480 },
1481 };
1482
1483 let mut branches = HashMap::new();
1484 branches.insert(decision, branch);
1485
1486 let result = dag
1488 .execute_with_branches(
1489 &branches,
1490 Some(&|node: &mut WorkflowNode| {
1491 if node.task_type == "probe" {
1492 node.set_output("resolution", serde_json::json!(4000.0));
1493 }
1494 Ok(())
1495 }),
1496 )
1497 .expect("execute");
1498
1499 assert!(result.succeeded);
1500 assert_eq!(
1501 result.node_statuses.get(&high),
1502 Some(&NodeStatus::Completed)
1503 );
1504 assert_eq!(result.node_statuses.get(&low), Some(&NodeStatus::Skipped));
1505 }
1506
1507 #[test]
1508 fn test_descendants() {
1509 let mut dag = WorkflowDag::new();
1510 let a = dag.add_node(make_node("a")).expect("add node");
1511 let b = dag.add_node(make_node("b")).expect("add node");
1512 let c = dag.add_node(make_node("c")).expect("add node");
1513 let d = dag.add_node(make_node("d")).expect("add node");
1514
1515 dag.add_edge(WorkflowEdge::new(a, b, "x")).expect("edge");
1516 dag.add_edge(WorkflowEdge::new(b, c, "x")).expect("edge");
1517 dag.add_edge(WorkflowEdge::new(b, d, "x")).expect("edge");
1518
1519 let desc = dag.descendants(a);
1520 assert_eq!(desc.len(), 3);
1521 assert!(desc.contains(&b));
1522 assert!(desc.contains(&c));
1523 assert!(desc.contains(&d));
1524
1525 let desc_b = dag.descendants(b);
1526 assert_eq!(desc_b.len(), 2);
1527 assert!(!desc_b.contains(&a));
1528 }
1529
1530 #[test]
1531 fn test_dag_error_duplicate_node() {
1532 let mut dag = WorkflowDag::new();
1533 let node = make_node("a");
1534 let id = node.node_id;
1535 dag.add_node(node).expect("should succeed in test");
1536 let dup = WorkflowNode {
1537 node_id: id,
1538 task_type: "b".to_string(),
1539 inputs: HashMap::new(),
1540 outputs: HashMap::new(),
1541 parameters: HashMap::new(),
1542 status: NodeStatus::Pending,
1543 };
1544 assert!(matches!(dag.add_node(dup), Err(DagError::DuplicateNode(_))));
1545 }
1546}