1
use schemars::JsonSchema;
2
use serde::Deserialize;
3

            
4
use super::TaskContext;
5

            
6
/// This struct represents a task dependency. A task can depend on other tasks.
7
/// If a task depends on another task, the dependent task must be executed before
8
/// the dependent task.
9
#[derive(Debug, Deserialize, JsonSchema)]
10
pub struct TaskDependencyArgs {
11
  /// The name of the task to depend on
12
  pub name: String,
13
}
14

            
15
#[derive(Debug, Deserialize, JsonSchema)]
16
#[serde(untagged)]
17
/// A task dependency: either a task name string or an object with a `name` field.
18
pub enum TaskDependency {
19
  String(String),
20
  TaskDependency(Box<TaskDependencyArgs>),
21
}
22

            
23
impl TaskDependency {
24
8
  pub fn run(&self, context: &TaskContext) -> anyhow::Result<()> {
25
8
    run_task_by_name(context, self.resolve_name())
26
8
  }
27

            
28
106
  pub fn resolve_name(&self) -> &str {
29
106
    match self {
30
105
      TaskDependency::String(name) => name,
31
1
      TaskDependency::TaskDependency(args) => &args.name,
32
    }
33
106
  }
34
}
35

            
36
impl TaskDependencyArgs {
37
2
  pub fn execute(&self, context: &TaskContext) -> anyhow::Result<()> {
38
2
    run_task_by_name(context, &self.name)
39
2
  }
40
}
41

            
42
92
pub fn run_task_by_name(context: &TaskContext, task_name: &str) -> anyhow::Result<()> {
43
92
  assert!(!task_name.is_empty());
44

            
45
92
  if context.is_task_completed(task_name)? {
46
2
    log::trace!("Skipping completed task: {}", task_name);
47
2
    return Ok(());
48
90
  }
49

            
50
90
  if context.is_task_active(task_name)? {
51
    anyhow::bail!("Circular dependency detected - {}", task_name);
52
90
  }
53

            
54
90
  let task = context.task_root.tasks.get(task_name).ok_or_else(|| {
55
24
    anyhow::anyhow!(
56
      "Task '{}' not found. Run 'mk list' to see available tasks.",
57
      task_name
58
    )
59
24
  })?;
60

            
61
66
  log::trace!("Task: {:?}", task);
62

            
63
66
  context.mark_task_active(task_name)?;
64

            
65
66
  let result = {
66
66
    let mut child_context = TaskContext::from_context(context);
67
66
    child_context.set_current_task_name(task_name);
68
66
    child_context.emit_event(&serde_json::json!({
69
66
      "event": "task_started",
70
66
      "task": task_name,
71
66
    }))?;
72
66
    task.run(&mut child_context)
73
  };
74

            
75
66
  context.unmark_task_active(task_name)?;
76

            
77
66
  if result.is_ok() {
78
66
    context.mark_task_complete(task_name)?;
79
66
    context.emit_event(&serde_json::json!({
80
66
      "event": "task_finished",
81
66
      "task": task_name,
82
66
      "success": true,
83
66
    }))?;
84
  } else {
85
    context.emit_event(&serde_json::json!({
86
      "event": "task_finished",
87
      "task": task_name,
88
      "success": false,
89
    }))?;
90
  }
91

            
92
66
  result
93
92
}
94

            
95
#[cfg(test)]
96
mod test {
97
  use hashbrown::HashMap;
98
  use std::sync::Arc;
99

            
100
  use crate::schema::{
101
    Task,
102
    TaskRoot,
103
  };
104

            
105
  use super::*;
106

            
107
  #[test]
108
2
  fn test_task_dependency_1() -> anyhow::Result<()> {
109
2
    let yaml = "
110
2
      name: task1
111
2
    ";
112

            
113
2
    let task_dependency = serde_yaml::from_str::<TaskDependency>(yaml)?;
114
2
    if let TaskDependency::TaskDependency(args) = task_dependency {
115
2
      assert_eq!(args.name, "task1");
116
    } else {
117
      panic!("Expected TaskDependency::TaskDependency");
118
    }
119

            
120
2
    Ok(())
121
2
  }
122

            
123
  #[test]
124
2
  fn test_task_dependency_2() -> anyhow::Result<()> {
125
2
    let yaml = "\"task1\"";
126

            
127
2
    let task_dependency = serde_yaml::from_str::<TaskDependency>(yaml)?;
128
2
    if let TaskDependency::String(name) = task_dependency {
129
2
      assert_eq!(name, "task1");
130
    } else {
131
      panic!("Expected TaskDependency::TaskString");
132
    }
133

            
134
2
    Ok(())
135
2
  }
136

            
137
  #[test]
138
2
  fn test_task_dependency_3() -> anyhow::Result<()> {
139
2
    let yaml = "\"\"";
140

            
141
2
    let task_dependency = serde_yaml::from_str::<TaskDependency>(yaml)?;
142
2
    if let TaskDependency::String(name) = task_dependency {
143
2
      assert_eq!(name, "");
144
    } else {
145
      panic!("Expected TaskDependency::TaskString");
146
    }
147

            
148
2
    Ok(())
149
2
  }
150

            
151
  #[test]
152
2
  fn test_task_dependency_4() -> anyhow::Result<()> {
153
2
    let yaml = "
154
2
      name:
155
2
    ";
156

            
157
2
    let task_dependency = serde_yaml::from_str::<TaskDependencyArgs>(yaml)?;
158
2
    assert_eq!(task_dependency.name, "");
159

            
160
2
    Ok(())
161
2
  }
162

            
163
  #[test]
164
2
  fn test_task_dependency_5() -> anyhow::Result<()> {
165
2
    let yaml = "
166
2
      name: task_a
167
2
    ";
168

            
169
2
    let task_yaml = "
170
2
      commands:
171
2
        - command: echo 1
172
2
          verbose: false
173
2
    ";
174

            
175
2
    let task = serde_yaml::from_str::<Task>(task_yaml)?;
176
2
    let mut hm = HashMap::new();
177
2
    hm.insert("task_a".into(), task);
178

            
179
2
    let root = Arc::new(TaskRoot::from_hashmap(hm));
180
2
    let task_dependency = serde_yaml::from_str::<TaskDependencyArgs>(yaml)?;
181
2
    let result = task_dependency.execute(&TaskContext::empty_with_root(root));
182
2
    assert!(result.is_ok());
183

            
184
2
    Ok(())
185
2
  }
186

            
187
  #[test]
188
2
  fn test_task_dependency_6_shared_dependency_is_not_a_cycle() -> anyhow::Result<()> {
189
2
    let root_yaml = "
190
2
      tasks:
191
2
        root:
192
2
          commands:
193
2
            - command: echo root
194
2
              verbose: false
195
2
          depends_on:
196
2
            - left
197
2
            - right
198
2
        left:
199
2
          commands:
200
2
            - command: echo left
201
2
              verbose: false
202
2
          depends_on:
203
2
            - shared
204
2
        right:
205
2
          commands:
206
2
            - command: echo right
207
2
              verbose: false
208
2
          depends_on:
209
2
            - shared
210
2
        shared:
211
2
          commands:
212
2
            - command: echo shared
213
2
              verbose: false
214
2
    ";
215

            
216
2
    let root = Arc::new(serde_yaml::from_str::<TaskRoot>(root_yaml)?);
217
2
    let context = TaskContext::empty_with_root(root);
218
2
    let result = run_task_by_name(&context, "root");
219
2
    assert!(result.is_ok());
220
2
    assert!(context.is_task_completed("shared")?);
221

            
222
2
    Ok(())
223
2
  }
224
}