1use crate::core::fs_util::copy_file_cifs_safe;
3use async_trait::async_trait;
4use std::fmt;
5use std::path::Path;
6
7#[async_trait]
12pub trait Task: Send + Sync {
13 async fn execute(&self) -> TaskResult;
15 fn task_type(&self) -> &'static str;
17 fn task_id(&self) -> String;
19 fn estimated_duration(&self) -> Option<std::time::Duration> {
21 None
22 }
23 fn description(&self) -> String {
25 format!("{} task", self.task_type())
26 }
27}
28
29#[derive(Debug, Clone)]
34pub enum TaskResult {
35 Success(String),
37 Failed(String),
39 Cancelled,
41 PartialSuccess(String, String),
43}
44
45#[derive(Debug, Clone)]
50pub enum TaskStatus {
51 Pending,
53 Running,
55 Completed(TaskResult),
57 Failed(String),
59 Cancelled,
61}
62
63impl fmt::Display for TaskResult {
64 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
65 match self {
66 TaskResult::Success(msg) => write!(f, "✓ {}", msg),
67 TaskResult::Failed(msg) => write!(f, "✗ {}", msg),
68 TaskResult::Cancelled => write!(f, "⚠ Task cancelled"),
69 TaskResult::PartialSuccess(success, warn) => {
70 write!(f, "⚠ {} (warning: {})", success, warn)
71 }
72 }
73 }
74}
75
76impl fmt::Display for TaskStatus {
77 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78 match self {
79 TaskStatus::Pending => write!(f, "Pending"),
80 TaskStatus::Running => write!(f, "Running"),
81 TaskStatus::Completed(result) => write!(f, "Completed: {}", result),
82 TaskStatus::Failed(msg) => write!(f, "Failed: {}", msg),
83 TaskStatus::Cancelled => write!(f, "Cancelled"),
84 }
85 }
86}
87
88pub struct FileProcessingTask {
93 pub input_path: std::path::PathBuf,
95 pub output_path: Option<std::path::PathBuf>,
97 pub operation: ProcessingOperation,
99}
100
101#[derive(Debug, Clone)]
106pub enum ProcessingOperation {
107 ConvertFormat {
109 from: String,
111 to: String,
113 },
114 SyncSubtitle {
116 audio_path: std::path::PathBuf,
118 },
119 MatchFiles {
121 recursive: bool,
123 },
124 ValidateFormat,
126 CopyToVideoFolder {
128 source: std::path::PathBuf,
130 target: std::path::PathBuf,
132 },
133 MoveToVideoFolder {
135 source: std::path::PathBuf,
137 target: std::path::PathBuf,
139 },
140 CopyWithRename {
142 source: std::path::PathBuf,
144 target: std::path::PathBuf,
146 },
147 CreateBackup {
149 source: std::path::PathBuf,
151 backup: std::path::PathBuf,
153 },
154 RenameFile {
156 source: std::path::PathBuf,
158 target: std::path::PathBuf,
160 },
161}
162
163#[async_trait]
164impl Task for FileProcessingTask {
165 async fn execute(&self) -> TaskResult {
166 match &self.operation {
167 ProcessingOperation::ConvertFormat { from, to } => {
168 match self.convert_format(from, to).await {
169 Ok(path) => TaskResult::Success(format!(
170 "Successfully converted {} -> {}: {}",
171 from,
172 to,
173 path.display()
174 )),
175 Err(e) => TaskResult::Failed(format!(
176 "Conversion failed {}: {}",
177 self.input_path.display(),
178 e
179 )),
180 }
181 }
182 ProcessingOperation::SyncSubtitle { .. } => {
183 TaskResult::Failed("Sync functionality not implemented".to_string())
185 }
186 ProcessingOperation::MatchFiles { recursive } => {
187 match self.match_files(*recursive).await {
188 Ok(m) => TaskResult::Success(format!(
189 "File matching completed: found {} matches",
190 m.len()
191 )),
192 Err(e) => TaskResult::Failed(format!("Matching failed: {}", e)),
193 }
194 }
195 ProcessingOperation::ValidateFormat => match self.validate_format().await {
196 Ok(true) => TaskResult::Success(format!(
197 "Format validation passed: {}",
198 self.input_path.display()
199 )),
200 Ok(false) => TaskResult::Failed(format!(
201 "Format validation failed: {}",
202 self.input_path.display()
203 )),
204 Err(e) => TaskResult::Failed(format!("Validation error: {}", e)),
205 },
206 ProcessingOperation::CopyToVideoFolder { source, target } => {
207 match self.execute_copy_operation(source, target).await {
208 Ok(_) => TaskResult::Success(format!(
209 "Copied: {} -> {}",
210 source.display(),
211 target.display()
212 )),
213 Err(e) => TaskResult::Failed(format!("Copy failed: {}", e)),
214 }
215 }
216 ProcessingOperation::MoveToVideoFolder { source, target } => {
217 match self.execute_move_operation(source, target).await {
218 Ok(_) => TaskResult::Success(format!(
219 "Moved: {} -> {}",
220 source.display(),
221 target.display()
222 )),
223 Err(e) => TaskResult::Failed(format!("Move failed: {}", e)),
224 }
225 }
226 ProcessingOperation::CopyWithRename { source, target } => {
227 match self
228 .execute_copy_with_rename_operation(source, target)
229 .await
230 {
231 Ok(_) => TaskResult::Success(format!(
232 "Copied: {} -> {}",
233 source.display(),
234 target.display()
235 )),
236 Err(e) => TaskResult::Failed(format!("Copy failed: {}", e)),
237 }
238 }
239 ProcessingOperation::CreateBackup { source, backup } => {
240 match self.execute_create_backup_operation(source, backup).await {
241 Ok(_) => TaskResult::Success(format!(
242 "Backup created: {} -> {}",
243 source.display(),
244 backup.display()
245 )),
246 Err(e) => TaskResult::Failed(format!("Backup failed: {}", e)),
247 }
248 }
249 ProcessingOperation::RenameFile { source, target } => {
250 match self.execute_rename_file_operation(source, target).await {
251 Ok(_) => TaskResult::Success(format!(
252 "Renamed: {} -> {}",
253 source.display(),
254 target.display()
255 )),
256 Err(e) => TaskResult::Failed(format!("Rename failed: {}", e)),
257 }
258 }
259 }
260 }
261
262 fn task_type(&self) -> &'static str {
263 match &self.operation {
264 ProcessingOperation::ConvertFormat { .. } => "convert",
265 ProcessingOperation::SyncSubtitle { .. } => "sync",
266 ProcessingOperation::MatchFiles { .. } => "match",
267 ProcessingOperation::ValidateFormat => "validate",
268 ProcessingOperation::CopyToVideoFolder { .. } => "copy_to_video_folder",
269 ProcessingOperation::MoveToVideoFolder { .. } => "move_to_video_folder",
270 ProcessingOperation::CopyWithRename { .. } => "copy_with_rename",
271 ProcessingOperation::CreateBackup { .. } => "create_backup",
272 ProcessingOperation::RenameFile { .. } => "rename_file",
273 }
274 }
275
276 fn task_id(&self) -> String {
277 use std::collections::hash_map::DefaultHasher;
278 use std::hash::{Hash, Hasher};
279 let mut hasher = DefaultHasher::new();
280 self.input_path.hash(&mut hasher);
281 self.operation.hash(&mut hasher);
282 format!("{}_{:x}", self.task_type(), hasher.finish())
283 }
284
285 fn estimated_duration(&self) -> Option<std::time::Duration> {
286 if let Ok(meta) = std::fs::metadata(&self.input_path) {
287 let size_mb = meta.len() as f64 / 1_048_576.0;
288 let secs = match &self.operation {
289 ProcessingOperation::ConvertFormat { .. } => size_mb * 0.1,
290 ProcessingOperation::SyncSubtitle { .. } => size_mb * 0.5,
291 ProcessingOperation::MatchFiles { .. } => 2.0,
292 ProcessingOperation::ValidateFormat => size_mb * 0.05,
293 ProcessingOperation::CopyToVideoFolder { .. } => size_mb * 0.01, ProcessingOperation::MoveToVideoFolder { .. } => size_mb * 0.005, ProcessingOperation::CopyWithRename { .. } => size_mb * 0.01,
296 ProcessingOperation::CreateBackup { .. } => size_mb * 0.01,
297 ProcessingOperation::RenameFile { .. } => size_mb * 0.005,
298 };
299 Some(std::time::Duration::from_secs_f64(secs))
300 } else {
301 None
302 }
303 }
304
305 fn description(&self) -> String {
306 match &self.operation {
307 ProcessingOperation::ConvertFormat { from, to } => {
308 format!(
309 "Convert {} from {} to {}",
310 self.input_path.display(),
311 from,
312 to
313 )
314 }
315 ProcessingOperation::SyncSubtitle { audio_path } => format!(
316 "Sync subtitle {} with audio {}",
317 self.input_path.display(),
318 audio_path.display()
319 ),
320 ProcessingOperation::MatchFiles { recursive } => format!(
321 "Match files in {}{}",
322 self.input_path.display(),
323 if *recursive { " (recursive)" } else { "" }
324 ),
325 ProcessingOperation::ValidateFormat => {
326 format!("Validate format of {}", self.input_path.display())
327 }
328 ProcessingOperation::CopyToVideoFolder { source, target } => {
329 format!("Copy {} to {}", source.display(), target.display())
330 }
331 ProcessingOperation::MoveToVideoFolder { source, target } => {
332 format!("Move {} to {}", source.display(), target.display())
333 }
334 ProcessingOperation::CopyWithRename { source, target } => {
335 format!(
336 "CopyWithRename {} to {}",
337 source.display(),
338 target.display()
339 )
340 }
341 ProcessingOperation::CreateBackup { source, backup } => {
342 format!("CreateBackup {} to {}", source.display(), backup.display())
343 }
344 ProcessingOperation::RenameFile { source, target } => {
345 format!("Rename {} to {}", source.display(), target.display())
346 }
347 }
348 }
349}
350
351impl FileProcessingTask {
352 pub fn new(
354 input_path: std::path::PathBuf,
355 output_path: Option<std::path::PathBuf>,
356 operation: ProcessingOperation,
357 ) -> Self {
358 FileProcessingTask {
359 input_path,
360 output_path,
361 operation,
362 }
363 }
364
365 async fn execute_copy_operation(
367 &self,
368 source: &Path,
369 target: &Path,
370 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
371 if let Some(parent) = target.parent() {
373 std::fs::create_dir_all(parent)?;
374 }
375
376 let final_target = self.resolve_filename_conflict(target.to_path_buf()).await?;
378
379 copy_file_cifs_safe(source, &final_target)?;
381 Ok(())
382 }
383
384 async fn execute_move_operation(
386 &self,
387 source: &Path,
388 target: &Path,
389 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
390 if let Some(parent) = target.parent() {
392 std::fs::create_dir_all(parent)?;
393 }
394
395 let final_target = self.resolve_filename_conflict(target.to_path_buf()).await?;
397
398 std::fs::rename(source, &final_target)?;
400 Ok(())
401 }
402
403 async fn resolve_filename_conflict(
405 &self,
406 target: std::path::PathBuf,
407 ) -> Result<std::path::PathBuf, Box<dyn std::error::Error + Send + Sync>> {
408 if !target.exists() {
409 return Ok(target);
410 }
411
412 let file_stem = target
414 .file_stem()
415 .and_then(|s| s.to_str())
416 .unwrap_or("file");
417 let extension = target.extension().and_then(|s| s.to_str()).unwrap_or("");
418
419 let parent = target.parent().unwrap_or_else(|| std::path::Path::new("."));
420
421 for i in 1..1000 {
423 let new_name = if extension.is_empty() {
424 format!("{}.{}", file_stem, i)
425 } else {
426 format!("{}.{}.{}", file_stem, i, extension)
427 };
428 let new_path = parent.join(new_name);
429 if !new_path.exists() {
430 return Ok(new_path);
431 }
432 }
433
434 Err("Could not resolve filename conflict".into())
435 }
436
437 async fn execute_copy_with_rename_operation(
439 &self,
440 source: &Path,
441 target: &Path,
442 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
443 if let Some(parent) = target.parent() {
444 std::fs::create_dir_all(parent)?;
445 }
446 copy_file_cifs_safe(source, target)?;
447 Ok(())
448 }
449
450 async fn execute_create_backup_operation(
452 &self,
453 source: &Path,
454 backup: &Path,
455 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
456 if let Some(parent) = backup.parent() {
457 std::fs::create_dir_all(parent)?;
458 }
459 copy_file_cifs_safe(source, backup)?;
460 Ok(())
461 }
462
463 async fn execute_rename_file_operation(
465 &self,
466 source: &Path,
467 target: &Path,
468 ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
469 if let Some(parent) = target.parent() {
470 std::fs::create_dir_all(parent)?;
471 }
472 std::fs::rename(source, target)?;
473 Ok(())
474 }
475
476 async fn convert_format(&self, _from: &str, _to: &str) -> crate::Result<std::path::PathBuf> {
477 Ok(self.input_path.clone())
479 }
480
481 async fn sync_subtitle(
482 &self,
483 _audio_path: &std::path::Path,
484 ) -> crate::Result<crate::core::sync::SyncResult> {
485 Err(crate::error::SubXError::parallel_processing(
487 "sync_subtitle not implemented".to_string(),
488 ))
489 }
490
491 async fn match_files(&self, _recursive: bool) -> crate::Result<Vec<()>> {
492 Ok(Vec::new())
494 }
495
496 async fn validate_format(&self) -> crate::Result<bool> {
497 Ok(true)
499 }
500}
501
502impl std::hash::Hash for ProcessingOperation {
504 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
505 match self {
506 ProcessingOperation::ConvertFormat { from, to } => {
507 "convert".hash(state);
508 from.hash(state);
509 to.hash(state);
510 }
511 ProcessingOperation::SyncSubtitle { audio_path } => {
512 "sync".hash(state);
513 audio_path.hash(state);
514 }
515 ProcessingOperation::MatchFiles { recursive } => {
516 "match".hash(state);
517 recursive.hash(state);
518 }
519 ProcessingOperation::ValidateFormat => {
520 "validate".hash(state);
521 }
522 ProcessingOperation::CopyToVideoFolder { source, target } => {
523 "copy_to_video_folder".hash(state);
524 source.hash(state);
525 target.hash(state);
526 }
527 ProcessingOperation::MoveToVideoFolder { source, target } => {
528 "move_to_video_folder".hash(state);
529 source.hash(state);
530 target.hash(state);
531 }
532 ProcessingOperation::CopyWithRename { source, target } => {
533 "copy_with_rename".hash(state);
534 source.hash(state);
535 target.hash(state);
536 }
537 ProcessingOperation::CreateBackup { source, backup } => {
538 "create_backup".hash(state);
539 source.hash(state);
540 backup.hash(state);
541 }
542 ProcessingOperation::RenameFile { source, target } => {
543 "rename_file".hash(state);
544 source.hash(state);
545 target.hash(state);
546 }
547 }
548 }
549}
550
551#[cfg(test)]
552mod tests {
553 use super::*;
554 use std::time::Duration;
555 use tempfile::TempDir;
556
557 #[tokio::test]
558 async fn test_file_processing_task_validate_format() {
559 let tmp = TempDir::new().unwrap();
560 let test_file = tmp.path().join("test.srt");
561 tokio::fs::write(&test_file, "1\n00:00:01,000 --> 00:00:02,000\nTest\n")
562 .await
563 .unwrap();
564 let task = FileProcessingTask {
565 input_path: test_file.clone(),
566 output_path: None,
567 operation: ProcessingOperation::ValidateFormat,
568 };
569 let result = task.execute().await;
570 assert!(matches!(result, TaskResult::Success(_)));
571 }
572
573 #[tokio::test]
574 async fn test_file_processing_task_copy_with_rename() {
575 let tmp = TempDir::new().unwrap();
576 let src = tmp.path().join("orig.txt");
577 let dst = tmp.path().join("copy.txt");
578 tokio::fs::write(&src, b"hello").await.unwrap();
579 let task = FileProcessingTask::new(
580 src.clone(),
581 Some(dst.clone()),
582 ProcessingOperation::CopyWithRename {
583 source: src.clone(),
584 target: dst.clone(),
585 },
586 );
587 let result = task.execute().await;
588 assert!(matches!(result, TaskResult::Success(_)));
589 let data = tokio::fs::read(&dst).await.unwrap();
590 assert_eq!(data, b"hello");
591 }
592
593 #[tokio::test]
594 async fn test_file_processing_task_create_backup() {
595 let tmp = TempDir::new().unwrap();
596 let src = tmp.path().join("orig.txt");
597 let backup = tmp.path().join("orig.txt.bak");
598 tokio::fs::write(&src, b"backup").await.unwrap();
599 let task = FileProcessingTask::new(
600 src.clone(),
601 Some(backup.clone()),
602 ProcessingOperation::CreateBackup {
603 source: src.clone(),
604 backup: backup.clone(),
605 },
606 );
607 let result = task.execute().await;
608 assert!(matches!(result, TaskResult::Success(_)));
609 let data = tokio::fs::read(&backup).await.unwrap();
610 assert_eq!(data, b"backup");
611 }
612
613 #[tokio::test]
614 async fn test_file_processing_task_rename_file() {
615 let tmp = TempDir::new().unwrap();
616 let src = tmp.path().join("a.txt");
617 let dst = tmp.path().join("b.txt");
618 tokio::fs::write(&src, b"rename").await.unwrap();
619 let task = FileProcessingTask::new(
620 src.clone(),
621 Some(dst.clone()),
622 ProcessingOperation::RenameFile {
623 source: src.clone(),
624 target: dst.clone(),
625 },
626 );
627 let result = task.execute().await;
628 assert!(matches!(result, TaskResult::Success(_)));
629 assert!(!tokio::fs::metadata(&src).await.is_ok());
630 let data = tokio::fs::read(&dst).await.unwrap();
631 assert_eq!(data, b"rename");
632 }
633
634 #[tokio::test]
636 async fn test_task_lifecycle() {
637 let tmp = TempDir::new().unwrap();
638 let test_file = tmp.path().join("lifecycle.srt");
639 tokio::fs::write(
640 &test_file,
641 "1\n00:00:01,000 --> 00:00:02,000\nLifecycle test\n",
642 )
643 .await
644 .unwrap();
645
646 let task = FileProcessingTask {
647 input_path: test_file.clone(),
648 output_path: None,
649 operation: ProcessingOperation::ValidateFormat,
650 };
651
652 assert_eq!(task.task_type(), "validate");
654 assert!(!task.task_id().is_empty());
655 assert!(task.description().contains("Validate format"));
656 assert!(task.description().contains("lifecycle.srt"));
657 assert!(
658 task.estimated_duration().is_some(),
659 "Should estimate duration for existing file"
660 );
661
662 let result = task.execute().await;
664 assert!(matches!(result, TaskResult::Success(_)));
665 }
666
667 #[test]
669 fn test_task_result_display() {
670 let success = TaskResult::Success("Operation completed".to_string());
671 let failed = TaskResult::Failed("Operation failed".to_string());
672 let cancelled = TaskResult::Cancelled;
673 let partial =
674 TaskResult::PartialSuccess("Mostly worked".to_string(), "Minor issue".to_string());
675
676 assert_eq!(format!("{}", success), "✓ Operation completed");
677 assert_eq!(format!("{}", failed), "✗ Operation failed");
678 assert_eq!(format!("{}", cancelled), "⚠ Task cancelled");
679 assert_eq!(
680 format!("{}", partial),
681 "⚠ Mostly worked (warning: Minor issue)"
682 );
683 }
684
685 #[test]
687 fn test_task_status_display() {
688 let pending = TaskStatus::Pending;
689 let running = TaskStatus::Running;
690 let completed = TaskStatus::Completed(TaskResult::Success("Done".to_string()));
691 let failed = TaskStatus::Failed("Error occurred".to_string());
692 let cancelled = TaskStatus::Cancelled;
693
694 assert_eq!(format!("{}", pending), "Pending");
695 assert_eq!(format!("{}", running), "Running");
696 assert_eq!(format!("{}", completed), "Completed: ✓ Done");
697 assert_eq!(format!("{}", failed), "Failed: Error occurred");
698 assert_eq!(format!("{}", cancelled), "Cancelled");
699 }
700
701 #[tokio::test]
703 async fn test_format_conversion_task() {
704 let tmp = TempDir::new().unwrap();
705 let input_file = tmp.path().join("input.srt");
706 let output_file = tmp.path().join("output.ass");
707
708 let srt_content = r#"1
71000:00:01,000 --> 00:00:03,000
711First subtitle
712
7132
71400:00:04,000 --> 00:00:06,000
715Second subtitle
716"#;
717
718 tokio::fs::write(&input_file, srt_content).await.unwrap();
719
720 let task = FileProcessingTask {
721 input_path: input_file.clone(),
722 output_path: Some(output_file.clone()),
723 operation: ProcessingOperation::ConvertFormat {
724 from: "srt".to_string(),
725 to: "ass".to_string(),
726 },
727 };
728
729 let result = task.execute().await;
730 assert!(matches!(result, TaskResult::Success(_)));
731
732 assert!(tokio::fs::metadata(&input_file).await.is_ok());
735 }
736
737 #[tokio::test]
739 async fn test_file_matching_task() {
740 let tmp = TempDir::new().unwrap();
741 let video_file = tmp.path().join("movie.mkv");
742 let subtitle_file = tmp.path().join("movie.srt");
743
744 tokio::fs::write(&video_file, b"fake video content")
746 .await
747 .unwrap();
748 tokio::fs::write(&subtitle_file, "1\n00:00:01,000 --> 00:00:02,000\nTest\n")
749 .await
750 .unwrap();
751
752 let task = FileProcessingTask {
753 input_path: tmp.path().to_path_buf(),
754 output_path: None,
755 operation: ProcessingOperation::MatchFiles { recursive: false },
756 };
757
758 let result = task.execute().await;
759 assert!(matches!(result, TaskResult::Success(_)));
760 }
761
762 #[tokio::test]
764 async fn test_sync_subtitle_task() {
765 let tmp = TempDir::new().unwrap();
766 let audio_file = tmp.path().join("audio.wav");
767 let subtitle_file = tmp.path().join("subtitle.srt");
768
769 tokio::fs::write(&audio_file, b"fake audio content")
770 .await
771 .unwrap();
772 tokio::fs::write(&subtitle_file, "1\n00:00:01,000 --> 00:00:02,000\nTest\n")
773 .await
774 .unwrap();
775
776 let task = FileProcessingTask {
777 input_path: subtitle_file.clone(),
778 output_path: None,
779 operation: ProcessingOperation::SyncSubtitle {
780 audio_path: audio_file,
781 },
782 };
783
784 let result = task.execute().await;
785 assert!(matches!(result, TaskResult::Failed(_)));
787 }
788
789 #[tokio::test]
791 async fn test_task_error_handling() {
792 let tmp = TempDir::new().unwrap();
794 let test_file = tmp.path().join("test.srt");
795
796 let task = FileProcessingTask {
797 input_path: test_file,
798 output_path: None,
799 operation: ProcessingOperation::SyncSubtitle {
800 audio_path: tmp.path().join("audio.wav"),
801 },
802 };
803
804 let result = task.execute().await;
805 assert!(matches!(result, TaskResult::Failed(_)));
806 }
807
808 #[tokio::test]
810 async fn test_task_timeout() {
811 use async_trait::async_trait;
812
813 struct SlowTask {
814 duration: Duration,
815 }
816
817 #[async_trait]
818 impl Task for SlowTask {
819 async fn execute(&self) -> TaskResult {
820 tokio::time::sleep(self.duration).await;
821 TaskResult::Success("Slow task completed".to_string())
822 }
823 fn task_type(&self) -> &'static str {
824 "slow"
825 }
826 fn task_id(&self) -> String {
827 "slow_task_1".to_string()
828 }
829 fn estimated_duration(&self) -> Option<Duration> {
830 Some(self.duration)
831 }
832 }
833
834 let slow_task = SlowTask {
835 duration: Duration::from_millis(100),
836 };
837
838 assert_eq!(
840 slow_task.estimated_duration(),
841 Some(Duration::from_millis(100))
842 );
843
844 let start = std::time::Instant::now();
846 let result = slow_task.execute().await;
847 let elapsed = start.elapsed();
848
849 assert!(matches!(result, TaskResult::Success(_)));
850 assert!(elapsed >= Duration::from_millis(90)); }
852
853 #[test]
855 fn test_processing_operation_variants() {
856 let convert_op = ProcessingOperation::ConvertFormat {
857 from: "srt".to_string(),
858 to: "ass".to_string(),
859 };
860
861 let sync_op = ProcessingOperation::SyncSubtitle {
862 audio_path: std::path::PathBuf::from("audio.wav"),
863 };
864
865 let match_op = ProcessingOperation::MatchFiles { recursive: true };
866 let validate_op = ProcessingOperation::ValidateFormat;
867
868 assert!(format!("{:?}", convert_op).contains("ConvertFormat"));
870 assert!(format!("{:?}", sync_op).contains("SyncSubtitle"));
871 assert!(format!("{:?}", match_op).contains("MatchFiles"));
872 assert!(format!("{:?}", validate_op).contains("ValidateFormat"));
873
874 let convert_clone = convert_op.clone();
876 assert!(format!("{:?}", convert_clone).contains("ConvertFormat"));
877 }
878
879 #[tokio::test]
881 async fn test_custom_task_implementation() {
882 use async_trait::async_trait;
883
884 struct CustomTask {
885 id: String,
886 should_succeed: bool,
887 }
888
889 #[async_trait]
890 impl Task for CustomTask {
891 async fn execute(&self) -> TaskResult {
892 if self.should_succeed {
893 TaskResult::Success(format!("Custom task {} succeeded", self.id))
894 } else {
895 TaskResult::Failed(format!("Custom task {} failed", self.id))
896 }
897 }
898
899 fn task_type(&self) -> &'static str {
900 "custom"
901 }
902
903 fn task_id(&self) -> String {
904 self.id.clone()
905 }
906
907 fn description(&self) -> String {
908 format!("Custom task with ID: {}", self.id)
909 }
910
911 fn estimated_duration(&self) -> Option<Duration> {
912 Some(Duration::from_millis(1))
913 }
914 }
915
916 let success_task = CustomTask {
918 id: "success_1".to_string(),
919 should_succeed: true,
920 };
921
922 assert_eq!(success_task.task_type(), "custom");
923 assert_eq!(success_task.task_id(), "success_1");
924 assert_eq!(success_task.description(), "Custom task with ID: success_1");
925 assert_eq!(
926 success_task.estimated_duration(),
927 Some(Duration::from_millis(1))
928 );
929
930 let result = success_task.execute().await;
931 assert!(matches!(result, TaskResult::Success(_)));
932
933 let fail_task = CustomTask {
935 id: "fail_1".to_string(),
936 should_succeed: false,
937 };
938
939 let result = fail_task.execute().await;
940 assert!(matches!(result, TaskResult::Failed(_)));
941 }
942}