1use std::collections::HashMap;
7
8use tcrm_task::tasks::{
9 async_tokio::spawner::TaskSpawner, event::TaskTerminateReason, state::TaskState,
10};
11use tokio::sync::mpsc;
12
13use crate::monitor::{
14 config::{TaskShell, TcrmTasks},
15 depend::{build_depend_map, check_circular_dependencies},
16 error::TaskMonitorError,
17};
18
19#[derive(Debug)]
104pub struct TaskMonitor {
105 pub tasks: TcrmTasks,
107 pub tasks_spawner: HashMap<String, TaskSpawner>,
109 pub dependencies: HashMap<String, Vec<String>>,
111 pub dependents: HashMap<String, Vec<String>>,
113 pub stdin_senders: HashMap<String, mpsc::Sender<String>>,
115}
116impl TaskMonitor {
117 pub fn new(mut tasks: TcrmTasks) -> Result<Self, TaskMonitorError> {
185 if tasks.is_empty() {
186 return Err(TaskMonitorError::ConfigParse(
187 "Task list cannot be empty".to_string(),
188 ));
189 }
190 let depen = build_depend_map(&tasks)?;
191 let dependencies = depen.dependencies;
192 let dependents = depen.dependents;
193 check_circular_dependencies(&dependencies)?;
194 shell_tasks(&mut tasks);
195
196 let mut tasks_spawner: HashMap<String, TaskSpawner> = HashMap::with_capacity(tasks.len());
198 let mut stdin_senders: HashMap<String, mpsc::Sender<String>> = HashMap::new();
199
200 for (task_name, task_spec) in &tasks {
201 let mut spawner = TaskSpawner::new(task_name.clone(), task_spec.config.clone());
202
203 if task_spec.config.enable_stdin.unwrap_or_default() {
205 let (stdin_tx, stdin_rx) = mpsc::channel::<String>(32);
207
208 spawner = spawner.set_stdin(stdin_rx);
210
211 stdin_senders.insert(task_name.clone(), stdin_tx);
213 }
214
215 tasks_spawner.insert(task_name.clone(), spawner);
216 }
217
218 Ok(Self {
219 tasks,
220 tasks_spawner,
221 dependencies,
222 dependents,
223 stdin_senders,
224 })
225 }
226
227 pub(crate) async fn terminate_dependencies_if_all_dependent_finished(
245 &mut self,
246 task_name: &str,
247 ) {
248 let Some(dependencies) = self.dependencies.get(task_name) else {
249 return;
250 };
251 for name in dependencies {
252 let Some(task) = self.tasks.get(name) else {
253 continue;
254 };
255 if !task.terminate_after_dependents_finished.unwrap_or_default() {
256 continue;
257 }
258
259 let Some(dependents) = self.dependents.get(name) else {
260 continue;
261 };
262
263 let mut all_finished = true;
264 for dep_name in dependents {
265 let Some(dep_spawner) = self.tasks_spawner.get(dep_name) else {
266 all_finished = false;
267 break;
268 };
269 let stopped = dep_spawner.get_state().await == TaskState::Finished;
270 if !stopped {
271 all_finished = false;
272 break;
273 }
274 }
275
276 if all_finished {
277 let Some(spawner) = self.tasks_spawner.get_mut(name) else {
278 continue;
279 };
280 match spawner
281 .send_terminate_signal(TaskTerminateReason::DependenciesFinished)
282 .await
283 {
284 Ok(()) => {}
285
286 #[allow(clippy::used_underscore_binding)]
287 Err(_e) => {
288 #[cfg(feature = "tracing")]
289 tracing::warn!(
290 error=%_e,
291 "Terminating dependencies failed",
292 );
293 }
294 }
295 }
296 }
297 }
298}
299
300fn shell_tasks(tasks: &mut TcrmTasks) {
306 for task_spec in tasks.values_mut() {
307 let default_shell = TaskShell::default();
309 let shell = task_spec.shell.as_ref().unwrap_or(&default_shell);
310
311 if *shell != TaskShell::None {
313 let original_command = std::mem::take(&mut task_spec.config.command);
314
315 match shell {
317 TaskShell::None => {
318 task_spec.config.command = original_command;
320 }
321 #[cfg(windows)]
322 TaskShell::Cmd => {
323 task_spec.config.command = "cmd".into();
324 let mut new_args = vec!["/C".into(), original_command];
325 if let Some(existing_args) = task_spec.config.args.take() {
326 new_args.extend(existing_args);
327 }
328 task_spec.config.args = Some(new_args);
329 }
330 #[cfg(windows)]
331 TaskShell::Powershell => {
332 task_spec.config.command = "powershell".into();
333 let mut new_args = vec!["-Command".into(), original_command];
334 if let Some(existing_args) = task_spec.config.args.take() {
335 new_args.extend(existing_args);
336 }
337 task_spec.config.args = Some(new_args);
338 }
339 #[cfg(unix)]
340 TaskShell::Bash => {
341 task_spec.config.command = "bash".into();
342 let mut new_args = vec!["-c".into(), original_command];
343 if let Some(existing_args) = task_spec.config.args.take() {
344 new_args.extend(existing_args);
345 }
346 task_spec.config.args = Some(new_args);
347 }
348 #[cfg(unix)]
349 TaskShell::Sh => {
350 task_spec.config.command = "sh".into();
351 let mut new_args = vec!["-c".into(), original_command];
352 if let Some(existing_args) = task_spec.config.args.take() {
353 new_args.extend(existing_args);
354 }
355 task_spec.config.args = Some(new_args);
356 }
357 #[cfg(unix)]
358 TaskShell::Zsh => {
359 task_spec.config.command = "zsh".into();
360 let mut new_args = vec!["-c".into(), original_command];
361 if let Some(existing_args) = task_spec.config.args.take() {
362 new_args.extend(existing_args);
363 }
364 task_spec.config.args = Some(new_args);
365 }
366 #[cfg(unix)]
367 TaskShell::Fish => {
368 task_spec.config.command = "fish".into();
369 let mut new_args = vec!["-c".into(), original_command];
370 if let Some(existing_args) = task_spec.config.args.take() {
371 new_args.extend(existing_args);
372 }
373 task_spec.config.args = Some(new_args);
374 }
375 TaskShell::Auto => {
376 #[cfg(windows)]
377 {
378 task_spec.config.command = "powershell".into();
379 let mut new_args = vec!["-Command".into(), original_command];
380 if let Some(existing_args) = task_spec.config.args.take() {
381 new_args.extend(existing_args);
382 }
383 task_spec.config.args = Some(new_args);
384 }
385 #[cfg(unix)]
386 {
387 task_spec.config.command = "bash".into();
388 let mut new_args = vec!["-c".into(), original_command];
389 if let Some(existing_args) = task_spec.config.args.take() {
390 new_args.extend(existing_args);
391 }
392 task_spec.config.args = Some(new_args);
393 }
394 }
395 }
396 }
397 }
398}
399
400#[cfg(test)]
401mod tests {
402
403 mod dependency_tests {
404 use std::collections::HashMap;
405
406 use tcrm_task::tasks::config::TaskConfig;
407
408 use crate::monitor::{
409 config::{TaskShell, TaskSpec, TcrmTasks},
410 error::TaskMonitorError,
411 tasks::TaskMonitor,
412 };
413
414 #[test]
415 fn test_valid_dependencies() {
416 let mut tasks: TcrmTasks = HashMap::new();
417
418 tasks.insert(
419 "taskA".to_string(),
420 TaskSpec::new(TaskConfig::new("echo").args(["A"])).shell(TaskShell::Auto),
421 );
422
423 tasks.insert(
424 "taskB".to_string(),
425 TaskSpec::new(TaskConfig::new("echo").args(["B"]))
426 .dependencies(["taskA"])
427 .shell(TaskShell::Auto),
428 );
429
430 tasks.insert(
431 "taskC".to_string(),
432 TaskSpec::new(TaskConfig::new("echo").args(["C"]))
433 .dependencies(["taskA"])
434 .shell(TaskShell::Auto),
435 );
436
437 let monitor = TaskMonitor::new(tasks);
438 assert!(monitor.is_ok());
439 }
440
441 #[test]
442 fn test_circular_dependency() {
443 let mut tasks = HashMap::new();
444
445 tasks.insert(
446 "taskA".to_string(),
447 TaskSpec::new(TaskConfig::new("echo").args(["A"]))
448 .dependencies(["taskB"])
449 .shell(TaskShell::Auto),
450 );
451
452 tasks.insert(
453 "taskB".to_string(),
454 TaskSpec::new(TaskConfig::new("echo").args(["B"]))
455 .dependencies(["taskC"])
456 .shell(TaskShell::Auto),
457 );
458
459 tasks.insert(
460 "taskC".to_string(),
461 TaskSpec::new(TaskConfig::new("echo").args(["C"]))
462 .dependencies(["taskA"])
463 .shell(TaskShell::Auto),
464 );
465
466 let monitor = TaskMonitor::new(tasks);
467 assert!(monitor.is_err());
468
469 match monitor.unwrap_err() {
470 TaskMonitorError::CircularDependency(task) => {
471 assert!(["taskA", "taskB", "taskC"].contains(&task.as_str()));
472 }
473 _ => panic!("Expected CircularDependency error"),
474 }
475 }
476
477 #[test]
478 fn test_missing_dependency() {
479 let mut tasks = HashMap::new();
480
481 tasks.insert(
482 "taskA".to_string(),
483 TaskSpec::new(TaskConfig::new("echo").args(["A"])).shell(TaskShell::Auto),
484 );
485
486 tasks.insert(
487 "taskC".to_string(),
488 TaskSpec::new(TaskConfig::new("echo").args(["C"]))
489 .dependencies(["nonexistent_task"])
490 .shell(TaskShell::Auto),
491 );
492
493 let monitor = TaskMonitor::new(tasks);
494 assert!(monitor.is_err());
495
496 match monitor.unwrap_err() {
497 TaskMonitorError::DependencyNotFound {
498 dependency_task_name,
499 task_name,
500 } => {
501 assert_eq!(dependency_task_name, "nonexistent_task");
502 assert_eq!(task_name, "taskC");
503 }
504 _ => panic!("Expected DependencyNotFound error"),
505 }
506 }
507
508 #[test]
509 fn test_complex_dependency_tree() {
510 let mut tasks = HashMap::new();
511
512 tasks.insert(
522 "task1".to_string(),
523 TaskSpec::new(TaskConfig::new("echo").args(["1"])),
524 );
525 tasks.insert(
526 "task2".to_string(),
527 TaskSpec::new(TaskConfig::new("echo").args(["2"])).dependencies(["task1"]),
528 );
529 tasks.insert(
530 "task3".to_string(),
531 TaskSpec::new(TaskConfig::new("echo").args(["3"])).dependencies(["task1"]),
532 );
533 tasks.insert(
534 "task4".to_string(),
535 TaskSpec::new(TaskConfig::new("echo").args(["4"])).dependencies(["task2"]),
536 );
537 tasks.insert(
538 "task5".to_string(),
539 TaskSpec::new(TaskConfig::new("echo").args(["5"])).dependencies(["task3"]),
540 );
541 tasks.insert(
542 "task6".to_string(),
543 TaskSpec::new(TaskConfig::new("echo").args(["6"])).dependencies(["task4", "task5"]),
544 );
545
546 let monitor = TaskMonitor::new(tasks).unwrap();
547
548 assert!(!monitor.dependencies.contains_key("task1")); assert_eq!(
551 monitor.dependencies.get("task2"),
552 Some(&vec!["task1".to_string()])
553 );
554 assert_eq!(
555 monitor.dependencies.get("task3"),
556 Some(&vec!["task1".to_string()])
557 );
558
559 let task6_deps = monitor.dependencies.get("task6").unwrap();
561 assert!(task6_deps.contains(&"task1".to_string()));
562 assert!(task6_deps.contains(&"task2".to_string()));
563 assert!(task6_deps.contains(&"task3".to_string()));
564 assert!(task6_deps.contains(&"task4".to_string()));
565 assert!(task6_deps.contains(&"task5".to_string()));
566 }
567
568 #[test]
569 fn test_multiple_independent_chains() {
570 let mut tasks = HashMap::new();
571
572 tasks.insert(
574 "A".to_string(),
575 TaskSpec::new(TaskConfig::new("echo").args(["A"])),
576 );
577 tasks.insert(
578 "B".to_string(),
579 TaskSpec::new(TaskConfig::new("echo").args(["B"])).dependencies(["A"]),
580 );
581
582 tasks.insert(
584 "X".to_string(),
585 TaskSpec::new(TaskConfig::new("echo").args(["X"])),
586 );
587 tasks.insert(
588 "Y".to_string(),
589 TaskSpec::new(TaskConfig::new("echo").args(["Y"])).dependencies(["X"]),
590 );
591 tasks.insert(
592 "Z".to_string(),
593 TaskSpec::new(TaskConfig::new("echo").args(["Z"])).dependencies(["Y"]),
594 );
595
596 let monitor = TaskMonitor::new(tasks).unwrap();
597 assert_eq!(monitor.tasks.len(), 5);
598
599 assert!(!monitor.dependencies.contains_key("A"));
601 assert!(!monitor.dependencies.contains_key("X"));
602 assert!(
603 monitor
604 .dependencies
605 .get("B")
606 .unwrap()
607 .contains(&"A".to_string())
608 );
609 assert!(
610 monitor
611 .dependencies
612 .get("Z")
613 .unwrap()
614 .contains(&"X".to_string())
615 );
616 assert!(
617 monitor
618 .dependencies
619 .get("Z")
620 .unwrap()
621 .contains(&"Y".to_string())
622 );
623 }
624 }
625
626 mod shell_tests {
627 use std::collections::HashMap;
628
629 use tcrm_task::tasks::config::TaskConfig;
630
631 use crate::monitor::{
632 config::{TaskShell, TaskSpec},
633 tasks::TaskMonitor,
634 };
635
636 #[test]
637 fn test_shell_command_transformation() {
638 let mut tasks = HashMap::new();
639
640 tasks.insert(
642 "echo_test".to_string(),
643 TaskSpec::new(TaskConfig::new("echo").args(["hello world"])).shell(TaskShell::Auto),
644 );
645
646 let monitor = TaskMonitor::new(tasks).unwrap();
647 let task = monitor.tasks.get("echo_test").unwrap();
648
649 #[cfg(windows)]
651 {
652 assert!(task.config.command == "cmd" || task.config.command == "powershell");
653 assert!(task.config.args.as_ref().unwrap().len() >= 2); }
655
656 #[cfg(unix)]
657 {
658 assert_eq!(task.config.command, "bash");
659 assert!(task.config.args.as_ref().unwrap().len() >= 2); }
661 }
662
663 #[test]
664 fn test_none_shell_preserves_original_command() {
665 let mut tasks = HashMap::new();
666 let original_command = "custom_executable";
667 let original_args = vec!["arg1".to_string(), "arg2".to_string()];
668
669 tasks.insert(
670 "raw_command".to_string(),
671 TaskSpec::new(TaskConfig::new(original_command).args(original_args.clone()))
672 .shell(TaskShell::None),
673 );
674
675 let monitor = TaskMonitor::new(tasks).unwrap();
676 let task = monitor.tasks.get("raw_command").unwrap();
677
678 assert_eq!(task.config.command, original_command);
680 assert_eq!(task.config.args.as_ref().unwrap(), &original_args);
681 }
682
683 #[cfg(windows)]
684 #[test]
685 fn test_windows_shell_argument_escaping() {
686 let mut tasks = HashMap::new();
687
688 tasks.insert(
690 "powershell_escape".to_string(),
691 TaskSpec::new(
692 TaskConfig::new("Write-Host").args(["test with spaces & special chars"]),
693 )
694 .shell(TaskShell::Powershell),
695 );
696
697 tasks.insert(
699 "cmd_escape".to_string(),
700 TaskSpec::new(TaskConfig::new("echo").args(["\"quoted string\" | more"]))
701 .shell(TaskShell::Cmd),
702 );
703
704 let monitor = TaskMonitor::new(tasks).unwrap();
705
706 let ps_task = monitor.tasks.get("powershell_escape").unwrap();
707 assert_eq!(ps_task.config.command, "powershell");
708 let ps_args = ps_task.config.args.as_ref().unwrap();
710 assert!(ps_args.len() >= 2);
711 assert!(
712 ps_args.contains(&"-Command".to_string()) || ps_args.contains(&"-c".to_string())
713 );
714
715 let cmd_task = monitor.tasks.get("cmd_escape").unwrap();
716 assert_eq!(cmd_task.config.command, "cmd");
717 let cmd_args = cmd_task.config.args.as_ref().unwrap();
719 assert!(cmd_args.len() >= 2);
720 assert!(cmd_args.contains(&"/c".to_string()) || cmd_args.contains(&"/C".to_string()));
721 }
722
723 #[cfg(unix)]
724 #[test]
725 fn test_unix_specific_shells() {
726 let mut tasks = HashMap::new();
727
728 tasks.insert(
730 "bash_task".to_string(),
731 TaskSpec::new(TaskConfig::new("echo").args(["test"])).shell(TaskShell::Bash),
732 );
733
734 let monitor = TaskMonitor::new(tasks).unwrap();
735 let bash_task = monitor.tasks.get("bash_task").unwrap();
736 assert_eq!(bash_task.config.command, "bash");
737 }
738 }
739
740 mod task_lifecycle_tests {
741 use std::collections::HashMap;
742
743 use tcrm_task::tasks::{config::TaskConfig, state::TaskState};
744
745 use crate::monitor::{config::TaskSpec, tasks::TaskMonitor};
746
747 #[test]
748 fn test_dependency_graph_construction() {
749 let mut tasks = HashMap::new();
750
751 tasks.insert(
753 "root".to_string(),
754 TaskSpec::new(TaskConfig::new("echo").args(["root task"])),
755 );
756
757 tasks.insert(
758 "dependent".to_string(),
759 TaskSpec::new(TaskConfig::new("echo").args(["dependent task"]))
760 .dependencies(["root"]),
761 );
762
763 let monitor = TaskMonitor::new(tasks).unwrap();
764
765 assert_eq!(monitor.tasks.len(), 2);
767 assert!(!monitor.dependencies.contains_key("root")); assert!(monitor.dependencies.contains_key("dependent"));
769
770 let root_dependents = monitor.dependents.get("root").unwrap();
772 assert!(root_dependents.contains(&"dependent".to_string()));
773
774 let dependent_deps = monitor.dependencies.get("dependent").unwrap();
775 assert!(dependent_deps.contains(&"root".to_string()));
776 }
777
778 #[test]
779 fn test_task_state_initialization() {
780 let mut tasks = HashMap::new();
781 tasks.insert(
782 "test_task".to_string(),
783 TaskSpec::new(TaskConfig::new("echo").args(["hello"])),
784 );
785
786 let monitor = TaskMonitor::new(tasks).unwrap();
787
788 for (_name, task) in &monitor.tasks {
790 assert_eq!(task.config.command, "echo");
793 assert_eq!(task.config.args.as_ref().unwrap()[0], "hello");
794 }
795 }
796
797 #[tokio::test]
798 async fn test_terminate_after_dependents_finished() {
799 let mut tasks = HashMap::new();
800
801 tasks.insert(
803 "parent".to_string(),
804 TaskSpec::new(TaskConfig::new("sleep").args(["10"]))
805 .terminate_after_dependents(true),
806 );
807
808 tasks.insert(
810 "child1".to_string(),
811 TaskSpec::new(TaskConfig::new("echo").args(["test"])).dependencies(["parent"]),
812 );
813
814 tasks.insert(
815 "child2".to_string(),
816 TaskSpec::new(TaskConfig::new("echo").args(["test"])).dependencies(["parent"]),
817 );
818
819 let mut monitor = TaskMonitor::new(tasks).unwrap();
820
821 assert!(monitor.dependencies.contains_key("child1"));
823 assert!(monitor.dependencies.contains_key("child2"));
824 assert!(monitor.dependents.contains_key("parent"));
825
826 let parent_spawner = monitor.tasks_spawner.get("parent").unwrap();
828 let initial_state = parent_spawner.get_state().await;
829 assert_ne!(initial_state, TaskState::Finished);
830
831 monitor
833 .terminate_dependencies_if_all_dependent_finished("child1")
834 .await;
835 monitor
836 .terminate_dependencies_if_all_dependent_finished("child2")
837 .await;
838 }
839 }
840
841 mod validation_tests {
842 use crate::monitor::{
843 config::{TaskShell, TaskSpec},
844 error::TaskMonitorError,
845 tasks::TaskMonitor,
846 };
847 use std::collections::HashMap;
848 use tcrm_task::tasks::config::TaskConfig;
849
850 #[test]
851 fn test_empty_command_validation() {
852 let mut tasks = HashMap::new();
854
855 tasks.insert(
857 "empty_command".to_string(),
858 TaskSpec::new(TaskConfig::new("")),
859 );
860
861 let monitor = TaskMonitor::new(tasks);
862 assert!(monitor.is_ok());
864 }
865
866 #[test]
867 fn test_empty_task_map_returns_config_parse_error() {
868 let tasks: HashMap<String, crate::monitor::config::TaskSpec> = HashMap::new();
870 let monitor = TaskMonitor::new(tasks);
871 match monitor {
872 Err(crate::monitor::error::TaskMonitorError::ConfigParse(_)) => {}
873 other => panic!("Expected ConfigParse error, got: {:?}", other),
874 }
875 }
876
877 #[test]
878 fn test_large_dependency_graph_validation() {
879 let mut tasks = HashMap::new();
881
882 tasks.insert(
884 "start".to_string(),
885 TaskSpec::new(TaskConfig::new("echo").args(["start"])),
886 );
887
888 for i in 1..100 {
889 let task_name = format!("task_{}", i);
890 let prev_task = if i == 1 {
891 "start".to_string()
892 } else {
893 format!("task_{}", i - 1)
894 };
895 tasks.insert(
896 task_name,
897 TaskSpec::new(TaskConfig::new("echo").args([&format!("task {}", i)]))
898 .dependencies([&prev_task]),
899 );
900 }
901
902 let start_time = std::time::Instant::now();
903 let monitor = TaskMonitor::new(tasks);
904 let duration = start_time.elapsed();
905
906 assert!(monitor.is_ok(), "Large dependency graph should be valid");
907 assert!(
908 duration.as_millis() < 1000,
909 "Validation should complete quickly even for large graphs"
910 );
911 }
912
913 #[test]
914 fn test_self_dependency_detection() {
915 let mut tasks = HashMap::new();
917
918 tasks.insert(
919 "self_dependent".to_string(),
920 TaskSpec::new(TaskConfig::new("echo").args(["hello"]))
921 .dependencies(["self_dependent"]),
922 );
923
924 let monitor = TaskMonitor::new(tasks);
925 assert!(monitor.is_err());
926
927 if let Err(TaskMonitorError::CircularDependency(task)) = monitor {
928 assert_eq!(task, "self_dependent");
929 } else {
930 panic!("Expected CircularDependency error for self-dependency");
931 }
932 }
933
934 #[test]
935 fn test_configuration_memory_efficiency() {
936 let mut tasks = HashMap::new();
938
939 for i in 0..1000 {
941 let task_name = format!("task_{}", i);
942 let config =
943 TaskConfig::new("echo").args([&format!("arg_{}", i), &format!("value_{}", i)]);
944
945 tasks.insert(
946 task_name,
947 TaskSpec::new(config)
948 .shell(if i % 2 == 0 {
949 TaskShell::Auto
950 } else {
951 TaskShell::None
952 })
953 .ignore_dependencies_error(i % 3 == 0),
954 );
955 }
956
957 let start_memory = get_memory_usage();
958 let monitor = TaskMonitor::new(tasks);
959 let end_memory = get_memory_usage();
960
961 assert!(monitor.is_ok());
962
963 let memory_diff = end_memory.saturating_sub(start_memory);
965 assert!(
966 memory_diff < 50_000_000,
967 "Memory usage should be reasonable for 1000 tasks"
968 ); }
970
971 fn get_memory_usage() -> usize {
972 0
976 }
977
978 #[test]
979 fn test_dependency_chain_depth_limits() {
980 let mut tasks = HashMap::new();
982
983 tasks.insert(
984 "root".to_string(),
985 TaskSpec::new(TaskConfig::new("echo").args(["root"])),
986 );
987
988 for i in 1..=500 {
989 let task_name = format!("deep_{}", i);
990 let prev_task = if i == 1 {
991 "root".to_string()
992 } else {
993 format!("deep_{}", i - 1)
994 };
995 tasks.insert(
996 task_name,
997 TaskSpec::new(TaskConfig::new("echo").args([&format!("deep {}", i)]))
998 .dependencies([&prev_task]),
999 );
1000 }
1001
1002 let monitor = TaskMonitor::new(tasks);
1003 assert!(
1004 monitor.is_ok(),
1005 "Deep dependency chains should be handled without stack overflow"
1006 );
1007 }
1008
1009 #[test]
1010 fn test_unicode_task_names_and_arguments() {
1011 let mut tasks = HashMap::new();
1013
1014 tasks.insert(
1015 "測試任務_🚀".to_string(),
1016 TaskSpec::new(TaskConfig::new("echo").args(["你好世界", "🌟✨", "Здравствуй мир"])),
1017 );
1018
1019 tasks.insert(
1020 "τεστ_задача_🎯".to_string(),
1021 TaskSpec::new(TaskConfig::new("echo").args(["العالم مرحبا"]))
1022 .dependencies(["測試任務_🚀"]),
1023 );
1024
1025 let monitor = TaskMonitor::new(tasks);
1026 assert!(
1027 monitor.is_ok(),
1028 "Unicode task names and arguments should be supported"
1029 );
1030
1031 let monitor = monitor.unwrap();
1033 assert!(monitor.tasks.contains_key("測試任務_🚀"));
1034 assert!(monitor.tasks.contains_key("τεστ_задача_🎯"));
1035 }
1036 }
1037}