1use hashbrown::HashMap;
2use indicatif::{
3 HumanDuration,
4 ProgressBar,
5 ProgressStyle,
6};
7use rand::Rng as _;
8use serde::Deserialize;
9
10use std::io::BufRead as _;
11use std::sync::mpsc::{
12 channel,
13 Receiver,
14 Sender,
15};
16use std::thread;
17use std::time::{
18 Duration,
19 Instant,
20};
21
22use super::{
23 is_shell_command,
24 CommandRunner,
25 Precondition,
26 Shell,
27 TaskContext,
28 TaskDependency,
29};
30use crate::defaults::default_verbose;
31use crate::run_shell_command;
32use crate::utils::{
33 deserialize_environment,
34 load_env_files,
35};
36
37#[derive(Debug, Default, Deserialize)]
41pub struct TaskArgs {
42 pub commands: Vec<CommandRunner>,
44
45 #[serde(default)]
47 pub preconditions: Vec<Precondition>,
48
49 #[serde(default)]
51 pub depends_on: Vec<TaskDependency>,
52
53 #[serde(default)]
55 pub labels: HashMap<String, String>,
56
57 #[serde(default)]
59 pub description: String,
60
61 #[serde(default, deserialize_with = "deserialize_environment")]
63 pub environment: HashMap<String, String>,
64
65 #[serde(default)]
67 pub env_file: Vec<String>,
68
69 #[serde(default)]
71 pub shell: Option<Shell>,
72
73 #[serde(default)]
76 pub parallel: Option<bool>,
77
78 #[serde(default)]
80 pub ignore_errors: Option<bool>,
81
82 #[serde(default)]
84 pub verbose: Option<bool>,
85}
86
87#[derive(Debug, Deserialize)]
88#[serde(untagged)]
89pub enum Task {
90 String(String),
91 Task(Box<TaskArgs>),
92}
93
94#[derive(Debug)]
95pub struct CommandResult {
96 index: usize,
97 success: bool,
98 message: String,
99}
100
101impl Task {
102 pub fn run(&self, context: &mut TaskContext) -> anyhow::Result<()> {
103 match self {
104 Task::String(command) => self.execute(context, command),
105 Task::Task(args) => args.run(context),
106 }
107 }
108
109 fn execute(&self, context: &mut TaskContext, command: &str) -> anyhow::Result<()> {
110 assert!(!command.is_empty());
111
112 TaskArgs {
113 commands: vec![CommandRunner::CommandRun(command.to_string())],
114 ..Default::default()
115 }
116 .run(context)
117 }
118}
119
120impl TaskArgs {
121 pub fn run(&self, context: &mut TaskContext) -> anyhow::Result<()> {
122 assert!(!self.commands.is_empty());
123
124 self.validate_parallel_commands()?;
126
127 let started = Instant::now();
128 let tick_interval = Duration::from_millis(80);
129
130 if let Some(shell) = &self.shell {
131 context.set_shell(shell);
132 }
133
134 if let Some(ignore_errors) = &self.ignore_errors {
135 context.set_ignore_errors(*ignore_errors);
136 }
137
138 if let Some(verbose) = &self.verbose {
139 context.set_verbose(*verbose);
140 }
141
142 if !context.is_nested {
144 let root_env = context.task_root.environment.clone();
145 let root_env_files = load_env_files(&context.task_root.env_file)?;
146 context.extend_env_vars(root_env);
147 context.extend_env_vars(root_env_files);
148 }
149
150 let defined_env = self.load_env(context)?;
152 let additional_env = self.load_env_file()?;
153
154 context.extend_env_vars(defined_env);
155 context.extend_env_vars(additional_env);
156
157 let mut rng = rand::thread_rng();
158 let pb_style =
161 ProgressStyle::with_template("{spinner:.green} [{prefix:.bold.dim}] {wide_msg:.cyan/blue} ")?
162 .tick_chars("⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏⦿");
163
164 let depends_on_pb = context.multi.add(ProgressBar::new(self.depends_on.len() as u64));
165
166 if !self.depends_on.is_empty() {
167 depends_on_pb.set_style(pb_style.clone());
168 depends_on_pb.set_message("Running task dependencies...");
169 depends_on_pb.enable_steady_tick(tick_interval);
170 for (i, dependency) in self.depends_on.iter().enumerate() {
171 thread::sleep(Duration::from_millis(rng.gen_range(40..300)));
172 depends_on_pb.set_prefix(format!("{}/{}", i + 1, self.depends_on.len()));
173 dependency.run(context)?;
174 depends_on_pb.inc(1);
175 }
176
177 let message = format!("Dependencies completed in {}.", HumanDuration(started.elapsed()));
178 if context.is_nested {
179 depends_on_pb.finish_and_clear();
180 } else {
181 depends_on_pb.finish_with_message(message);
182 }
183 }
184
185 let precondition_pb = context
186 .multi
187 .add(ProgressBar::new(self.preconditions.len() as u64));
188
189 if !self.preconditions.is_empty() {
190 precondition_pb.set_style(pb_style.clone());
191 precondition_pb.set_message("Running task precondition...");
192 precondition_pb.enable_steady_tick(tick_interval);
193 for (i, precondition) in self.preconditions.iter().enumerate() {
194 thread::sleep(Duration::from_millis(rng.gen_range(40..300)));
195 precondition_pb.set_prefix(format!("{}/{}", i + 1, self.preconditions.len()));
196 precondition.execute(context)?;
197 precondition_pb.inc(1);
198 }
199
200 let message = format!("Preconditions completed in {}.", HumanDuration(started.elapsed()));
201 if context.is_nested {
202 precondition_pb.finish_and_clear();
203 } else {
204 precondition_pb.finish_with_message(message);
205 }
206 }
207
208 if self.parallel.unwrap_or(false) {
209 self.execute_commands_parallel(context)?;
210 } else {
211 let command_pb = context.multi.add(ProgressBar::new(self.commands.len() as u64));
212 command_pb.set_style(pb_style);
213 command_pb.set_message("Running task command...");
214 command_pb.enable_steady_tick(tick_interval);
215 for (i, command) in self.commands.iter().enumerate() {
216 thread::sleep(Duration::from_millis(rng.gen_range(100..400)));
217 command_pb.set_prefix(format!("{}/{}", i + 1, self.commands.len()));
218 command.execute(context)?;
219 command_pb.inc(1);
220 }
221
222 let message = format!("Commands completed in {}.", HumanDuration(started.elapsed()));
223 if context.is_nested {
224 command_pb.finish_and_clear();
225 } else {
226 command_pb.finish_with_message(message);
227 }
228 }
229
230 Ok(())
231 }
232
233 fn validate_parallel_commands(&self) -> anyhow::Result<()> {
235 if !self.parallel.unwrap_or(false) {
236 return Ok(());
237 }
238
239 for command in &self.commands {
240 match command {
241 CommandRunner::LocalRun(local_run) if local_run.is_parallel_safe() => continue,
242 CommandRunner::LocalRun(_) => {
243 return Err(anyhow::anyhow!(
244 "Interactive local commands cannot be run in parallel"
245 ))
246 },
247 _ => {
248 return Err(anyhow::anyhow!(
249 "Parallel execution is only supported for non-interactive local commands"
250 ))
251 },
252 }
253 }
254 Ok(())
255 }
256
257 fn execute_commands_parallel(&self, context: &TaskContext) -> anyhow::Result<()> {
259 let (tx, rx): (Sender<CommandResult>, Receiver<CommandResult>) = channel();
260 let mut handles = vec![];
261 let command_count = self.commands.len();
262
263 let commands: Vec<_> = self.commands.to_vec();
265
266 let mut completed = 0;
268
269 for (i, command) in commands.into_iter().enumerate() {
270 let tx = tx.clone();
271 let context = context.clone();
272
273 let handle = thread::spawn(move || {
274 let result = match command.execute(&context) {
275 Ok(_) => CommandResult {
276 index: i,
277 success: true,
278 message: format!("Command {} completed successfully", i + 1),
279 },
280 Err(e) => CommandResult {
281 index: i,
282 success: false,
283 message: format!("Command {} failed: {}", i + 1, e),
284 },
285 };
286 tx.send(result).unwrap();
287 });
288
289 handles.push(handle);
290 }
291
292 let command_pb = context.multi.add(ProgressBar::new(command_count as u64));
294 command_pb.set_style(ProgressStyle::with_template(
295 "{spinner:.green} [{prefix:.bold.dim}] {wide_msg:.cyan/blue} ",
296 )?);
297 command_pb.set_prefix("?/?");
298 command_pb.set_message("Running task commands in parallel...");
299 command_pb.enable_steady_tick(Duration::from_millis(80));
300
301 let mut failures = Vec::new();
303
304 while completed < command_count {
305 match rx.recv() {
306 Ok(result) => {
307 let index = result.index;
308 if !result.success && !context.ignore_errors() {
309 failures.push(result.message);
310 }
311
312 completed += 1;
313 command_pb.set_prefix(format!("{}/{}", completed, command_count));
314 command_pb.inc(1);
315
316 command_pb.set_message(format!(
318 "Running task commands in parallel (completed {})",
319 index + 1
320 ));
321 },
322 Err(e) => {
323 command_pb.finish_with_message("Error receiving command results");
324 return Err(anyhow::anyhow!("Channel error: {}", e));
325 },
326 }
327 }
328
329 for handle in handles {
331 handle.join().unwrap();
332 }
333
334 if !failures.is_empty() {
335 command_pb.finish_with_message("Some commands failed");
336
337 failures.sort();
339 return Err(anyhow::anyhow!("Failed commands:\n{}", failures.join("\n")));
340 }
341
342 let message = "Commands completed in parallel";
343 if context.is_nested {
344 command_pb.finish_and_clear();
345 } else {
346 command_pb.finish_with_message(message);
347 }
348
349 Ok(())
350 }
351
352 fn load_env(&self, context: &TaskContext) -> anyhow::Result<HashMap<String, String>> {
353 let mut local_env: HashMap<String, String> = HashMap::new();
354 for (key, value) in &self.environment {
355 let value = self.get_env_value(context, value)?;
356 local_env.insert(key.clone(), value);
357 }
358
359 Ok(local_env)
360 }
361
362 fn load_env_file(&self) -> anyhow::Result<HashMap<String, String>> {
363 load_env_files(&self.env_file)
364 }
365
366 fn get_env_value(&self, context: &TaskContext, value_in: &str) -> anyhow::Result<String> {
367 if is_shell_command(value_in)? {
368 let verbose = self.verbose();
369 let mut cmd = self
370 .shell
371 .as_ref()
372 .map(|shell| shell.proc())
373 .unwrap_or_else(|| context.shell().proc());
374 let output = run_shell_command!(value_in, cmd, verbose);
375 Ok(output)
376 } else {
377 Ok(value_in.to_string())
378 }
379 }
380
381 fn verbose(&self) -> bool {
382 self.verbose.unwrap_or(default_verbose())
383 }
384}
385
386#[cfg(test)]
387mod test {
388 use super::*;
389
390 #[test]
391 fn test_task_1() -> anyhow::Result<()> {
392 {
393 let yaml = "
394 commands:
395 - command: echo \"Hello, World!\"
396 ignore_errors: false
397 verbose: false
398 depends_on:
399 - name: task1
400 description: This is a task
401 environment:
402 FOO: bar
403 env_file:
404 - test.env
405 - test2.env
406 ";
407
408 let task = serde_yaml::from_str::<Task>(yaml)?;
409
410 if let Task::Task(task) = &task {
411 if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
412 assert_eq!(local_run.command, "echo \"Hello, World!\"");
413 assert_eq!(local_run.work_dir, None);
414 assert_eq!(local_run.ignore_errors, Some(false));
415 assert_eq!(local_run.verbose, Some(false));
416 }
417
418 if let TaskDependency::TaskDependency(args) = &task.depends_on[0] {
419 assert_eq!(args.name, "task1");
420 }
421
422 assert_eq!(task.labels.len(), 0);
423 assert_eq!(task.description, "This is a task");
424 assert_eq!(task.environment.len(), 1);
425 assert_eq!(task.env_file.len(), 2);
426 } else {
427 panic!("Expected Task::Task");
428 }
429
430 Ok(())
431 }
432 }
433
434 #[test]
435 fn test_task_2() -> anyhow::Result<()> {
436 {
437 let yaml = "
438 commands:
439 - command: echo 'Hello, World!'
440 ignore_errors: false
441 verbose: false
442 description: This is a task
443 environment:
444 FOO: bar
445 BAR: foo
446 ";
447
448 let task = serde_yaml::from_str::<Task>(yaml)?;
449
450 if let Task::Task(task) = &task {
451 if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
452 assert_eq!(local_run.command, "echo 'Hello, World!'");
453 assert_eq!(local_run.work_dir, None);
454 assert_eq!(local_run.ignore_errors, Some(false));
455 assert_eq!(local_run.verbose, Some(false));
456 }
457
458 assert_eq!(task.description, "This is a task");
459 assert_eq!(task.depends_on.len(), 0);
460 assert_eq!(task.labels.len(), 0);
461 assert_eq!(task.env_file.len(), 0);
462 assert_eq!(task.environment.len(), 2);
463 } else {
464 panic!("Expected Task::Task");
465 }
466
467 Ok(())
468 }
469 }
470
471 #[test]
472 fn test_task_3() -> anyhow::Result<()> {
473 {
474 let yaml = "
475 commands:
476 - command: echo 'Hello, World!'
477 ";
478
479 let task = serde_yaml::from_str::<Task>(yaml)?;
480
481 if let Task::Task(task) = &task {
482 if let CommandRunner::LocalRun(local_run) = &task.commands[0] {
483 assert_eq!(local_run.command, "echo 'Hello, World!'");
484 assert_eq!(local_run.work_dir, None);
485 assert_eq!(local_run.ignore_errors, None);
486 assert_eq!(local_run.verbose, None);
487 }
488
489 assert_eq!(task.description.len(), 0);
490 assert_eq!(task.depends_on.len(), 0);
491 assert_eq!(task.labels.len(), 0);
492 assert_eq!(task.env_file.len(), 0);
493 assert_eq!(task.environment.len(), 0);
494 } else {
495 panic!("Expected Task::Task");
496 }
497
498 Ok(())
499 }
500 }
501
502 #[test]
503 fn test_task_4() -> anyhow::Result<()> {
504 {
505 let yaml = "
506 commands:
507 - container_command:
508 - echo
509 - Hello, World!
510 image: docker.io/library/hello-world:latest
511 ";
512
513 let task = serde_yaml::from_str::<Task>(yaml)?;
514
515 if let Task::Task(task) = &task {
516 if let CommandRunner::ContainerRun(container_run) = &task.commands[0] {
517 assert_eq!(container_run.container_command.len(), 2);
518 assert_eq!(container_run.container_command[0], "echo");
519 assert_eq!(container_run.container_command[1], "Hello, World!");
520 assert_eq!(container_run.image, "docker.io/library/hello-world:latest");
521 assert_eq!(container_run.mounted_paths, Vec::<String>::new());
522 assert_eq!(container_run.ignore_errors, None);
523 assert_eq!(container_run.verbose, None);
524 }
525
526 assert_eq!(task.description.len(), 0);
527 assert_eq!(task.depends_on.len(), 0);
528 assert_eq!(task.labels.len(), 0);
529 assert_eq!(task.env_file.len(), 0);
530 assert_eq!(task.environment.len(), 0);
531 } else {
532 panic!("Expected Task::Task");
533 }
534
535 Ok(())
536 }
537 }
538
539 #[test]
540 fn test_task_5() -> anyhow::Result<()> {
541 {
542 let yaml = "
543 commands:
544 - container_command:
545 - echo
546 - Hello, World!
547 image: docker.io/library/hello-world:latest
548 mounted_paths:
549 - /tmp
550 - /var/tmp
551 ";
552
553 let task = serde_yaml::from_str::<Task>(yaml)?;
554
555 if let Task::Task(task) = &task {
556 if let CommandRunner::ContainerRun(container_run) = &task.commands[0] {
557 assert_eq!(container_run.container_command.len(), 2);
558 assert_eq!(container_run.container_command[0], "echo");
559 assert_eq!(container_run.container_command[1], "Hello, World!");
560 assert_eq!(container_run.image, "docker.io/library/hello-world:latest");
561 assert_eq!(container_run.mounted_paths, vec!["/tmp", "/var/tmp"]);
562 assert_eq!(container_run.ignore_errors, None);
563 assert_eq!(container_run.verbose, None);
564 }
565
566 assert_eq!(task.description.len(), 0);
567 assert_eq!(task.depends_on.len(), 0);
568 assert_eq!(task.labels.len(), 0);
569 assert_eq!(task.env_file.len(), 0);
570 assert_eq!(task.environment.len(), 0);
571 } else {
572 panic!("Expected Task::Task");
573 }
574
575 Ok(())
576 }
577 }
578
579 #[test]
580 fn test_task_6() -> anyhow::Result<()> {
581 {
582 let yaml = "
583 commands:
584 - container_command:
585 - echo
586 - Hello, World!
587 image: docker.io/library/hello-world:latest
588 mounted_paths:
589 - /tmp
590 - /var/tmp
591 ignore_errors: true
592 ";
593
594 let task = serde_yaml::from_str::<Task>(yaml)?;
595
596 if let Task::Task(task) = &task {
597 if let CommandRunner::ContainerRun(container_run) = &task.commands[0] {
598 assert_eq!(container_run.container_command.len(), 2);
599 assert_eq!(container_run.container_command[0], "echo");
600 assert_eq!(container_run.container_command[1], "Hello, World!");
601 assert_eq!(container_run.image, "docker.io/library/hello-world:latest");
602 assert_eq!(container_run.mounted_paths, vec!["/tmp", "/var/tmp"]);
603 assert_eq!(container_run.ignore_errors, Some(true));
604 assert_eq!(container_run.verbose, None);
605 }
606
607 assert_eq!(task.description.len(), 0);
608 assert_eq!(task.depends_on.len(), 0);
609 assert_eq!(task.labels.len(), 0);
610 assert_eq!(task.env_file.len(), 0);
611 assert_eq!(task.environment.len(), 0);
612 } else {
613 panic!("Expected Task::Task");
614 }
615
616 Ok(())
617 }
618 }
619
620 #[test]
621 fn test_task_7() -> anyhow::Result<()> {
622 {
623 let yaml = "
624 commands:
625 - container_command:
626 - echo
627 - Hello, World!
628 image: docker.io/library/hello-world:latest
629 verbose: false
630 ";
631
632 let task = serde_yaml::from_str::<Task>(yaml)?;
633
634 if let Task::Task(task) = &task {
635 if let CommandRunner::ContainerRun(container_run) = &task.commands[0] {
636 assert_eq!(container_run.container_command.len(), 2);
637 assert_eq!(container_run.container_command[0], "echo");
638 assert_eq!(container_run.container_command[1], "Hello, World!");
639 assert_eq!(container_run.image, "docker.io/library/hello-world:latest");
640 assert_eq!(container_run.mounted_paths, Vec::<String>::new());
641 assert_eq!(container_run.ignore_errors, None);
642 assert_eq!(container_run.verbose, Some(false));
643 }
644
645 assert_eq!(task.description.len(), 0);
646 assert_eq!(task.depends_on.len(), 0);
647 assert_eq!(task.labels.len(), 0);
648 assert_eq!(task.env_file.len(), 0);
649 assert_eq!(task.environment.len(), 0);
650 } else {
651 panic!("Expected Task::Task");
652 }
653
654 Ok(())
655 }
656 }
657
658 #[test]
659 fn test_task_8() -> anyhow::Result<()> {
660 {
661 let yaml = "
662 commands:
663 - task: task1
664 ";
665
666 let task = serde_yaml::from_str::<Task>(yaml)?;
667
668 if let Task::Task(task) = &task {
669 if let CommandRunner::TaskRun(task_run) = &task.commands[0] {
670 assert_eq!(task_run.task, "task1");
671 assert_eq!(task_run.ignore_errors, None);
672 assert_eq!(task_run.verbose, None);
673 }
674
675 assert_eq!(task.description.len(), 0);
676 assert_eq!(task.depends_on.len(), 0);
677 assert_eq!(task.labels.len(), 0);
678 assert_eq!(task.env_file.len(), 0);
679 assert_eq!(task.environment.len(), 0);
680 } else {
681 panic!("Expected Task::Task");
682 }
683
684 Ok(())
685 }
686 }
687
688 #[test]
689 fn test_task_9() -> anyhow::Result<()> {
690 {
691 let yaml = "
692 commands:
693 - task: task1
694 verbose: true
695 ";
696
697 let task = serde_yaml::from_str::<Task>(yaml)?;
698
699 if let Task::Task(task) = &task {
700 if let CommandRunner::TaskRun(task_run) = &task.commands[0] {
701 assert_eq!(task_run.task, "task1");
702 assert_eq!(task_run.ignore_errors, None);
703 assert_eq!(task_run.verbose, Some(true));
704 }
705
706 assert_eq!(task.description.len(), 0);
707 assert_eq!(task.depends_on.len(), 0);
708 assert_eq!(task.labels.len(), 0);
709 assert_eq!(task.env_file.len(), 0);
710 assert_eq!(task.environment.len(), 0);
711 } else {
712 panic!("Expected Task::Task");
713 }
714
715 Ok(())
716 }
717 }
718
719 #[test]
720 fn test_task_10() -> anyhow::Result<()> {
721 {
722 let yaml = "
723 commands:
724 - task: task1
725 ignore_errors: true
726 ";
727
728 let task = serde_yaml::from_str::<Task>(yaml)?;
729
730 if let Task::Task(task) = &task {
731 if let CommandRunner::TaskRun(task_run) = &task.commands[0] {
732 assert_eq!(task_run.task, "task1");
733 assert_eq!(task_run.ignore_errors, Some(true));
734 assert_eq!(task_run.verbose, None);
735 }
736
737 assert_eq!(task.description.len(), 0);
738 assert_eq!(task.depends_on.len(), 0);
739 assert_eq!(task.labels.len(), 0);
740 assert_eq!(task.env_file.len(), 0);
741 assert_eq!(task.environment.len(), 0);
742 } else {
743 panic!("Expected Task::Task");
744 }
745
746 Ok(())
747 }
748 }
749
750 #[test]
751 fn test_task_11() -> anyhow::Result<()> {
752 {
753 let yaml = "
754 echo 'Hello, World!'
755 ";
756
757 let task = serde_yaml::from_str::<Task>(yaml)?;
758
759 if let Task::String(task) = &task {
760 assert_eq!(task, "echo 'Hello, World!'");
761 } else {
762 panic!("Expected Task::String");
763 }
764
765 Ok(())
766 }
767 }
768
769 #[test]
770 fn test_task_12() -> anyhow::Result<()> {
771 {
772 let yaml = "
773 'true'
774 ";
775
776 let task = serde_yaml::from_str::<Task>(yaml)?;
777
778 if let Task::String(task) = &task {
779 assert_eq!(task, "true");
780 } else {
781 panic!("Expected Task::String");
782 }
783
784 Ok(())
785 }
786 }
787
788 #[test]
789 fn test_task_13() -> anyhow::Result<()> {
790 {
791 let yaml = "
792 commands: []
793 environment:
794 FOO: bar
795 BAR: foo
796 KEY: 42
797 PIS: 3.14
798 ";
799
800 let task = serde_yaml::from_str::<Task>(yaml)?;
801
802 if let Task::Task(task) = &task {
803 assert_eq!(task.environment.len(), 4);
804 assert_eq!(task.environment.get("FOO").unwrap(), "bar");
805 assert_eq!(task.environment.get("BAR").unwrap(), "foo");
806 assert_eq!(task.environment.get("KEY").unwrap(), "42");
807 } else {
808 panic!("Expected Task::Task");
809 }
810
811 Ok(())
812 }
813 }
814
815 #[test]
816 fn test_parallel_interactive_rejected() -> anyhow::Result<()> {
817 let yaml = r#"
818 commands:
819 - command: "echo hello"
820 interactive: true
821 - command: "echo world"
822 parallel: true
823 "#;
824
825 let task = serde_yaml::from_str::<Task>(yaml)?;
826 let mut context = TaskContext::empty();
827
828 if let Task::Task(task) = task {
829 let result = task.run(&mut context);
830 assert!(result.is_err());
831 assert!(result
832 .unwrap_err()
833 .to_string()
834 .contains("Interactive local commands cannot be run in parallel"));
835 }
836
837 Ok(())
838 }
839
840 #[test]
841 fn test_parallel_non_interactive_accepted() -> anyhow::Result<()> {
842 let yaml = r#"
843 commands:
844 - command: "echo hello"
845 interactive: false
846 - command: "echo world"
847 parallel: true
848 "#;
849
850 let task = serde_yaml::from_str::<Task>(yaml)?;
851 let mut context = TaskContext::empty();
852
853 if let Task::Task(task) = task {
854 let result = task.run(&mut context);
855 assert!(result.is_ok());
856 }
857
858 Ok(())
859 }
860}