1use std::path::{Path, PathBuf};
28use std::process::{Command, Stdio};
29use std::time::Duration;
30use thiserror::Error;
31
32const DEFAULT_TIMEOUT_SECS: u64 = 3600;
38
39const LOW_VRAM_MB: u64 = 2048;
41
42const LOW_VRAM_TILE_SIZE: u32 = 128;
44
45const DEFAULT_GPU_TILE_SIZE: u32 = 400;
47
48#[derive(Debug, Error)]
50pub enum AiBridgeError {
51 #[error("Python virtual environment not found: {0}")]
52 VenvNotFound(PathBuf),
53
54 #[error("Tool not installed: {0:?}")]
55 ToolNotInstalled(AiTool),
56
57 #[error("Process failed: {0}")]
58 ProcessFailed(String),
59
60 #[error("Process timed out after {0:?}")]
61 Timeout(Duration),
62
63 #[error("GPU not available")]
64 GpuNotAvailable,
65
66 #[error("Out of memory")]
67 OutOfMemory,
68
69 #[error("All retries exhausted")]
70 RetriesExhausted,
71
72 #[error("IO error: {0}")]
73 IoError(#[from] std::io::Error),
74}
75
76pub type Result<T> = std::result::Result<T, AiBridgeError>;
77
78#[derive(Debug, Clone)]
80pub struct AiBridgeConfig {
81 pub venv_path: PathBuf,
83 pub gpu_config: GpuConfig,
85 pub timeout: Duration,
87 pub retry_config: RetryConfig,
89 pub log_level: LogLevel,
91}
92
93impl Default for AiBridgeConfig {
94 fn default() -> Self {
95 Self {
96 venv_path: PathBuf::from("./ai_venv"),
97 gpu_config: GpuConfig::default(),
98 timeout: Duration::from_secs(DEFAULT_TIMEOUT_SECS),
99 retry_config: RetryConfig::default(),
100 log_level: LogLevel::Info,
101 }
102 }
103}
104
105impl AiBridgeConfig {
106 #[must_use]
108 pub fn builder() -> AiBridgeConfigBuilder {
109 AiBridgeConfigBuilder::default()
110 }
111
112 #[must_use]
114 pub fn cpu_only() -> Self {
115 Self {
116 gpu_config: GpuConfig {
117 enabled: false,
118 ..Default::default()
119 },
120 ..Default::default()
121 }
122 }
123
124 #[must_use]
126 pub fn low_vram() -> Self {
127 Self {
128 gpu_config: GpuConfig {
129 enabled: true,
130 max_vram_mb: Some(LOW_VRAM_MB),
131 tile_size: Some(LOW_VRAM_TILE_SIZE),
132 ..Default::default()
133 },
134 ..Default::default()
135 }
136 }
137}
138
139#[derive(Debug, Default)]
141pub struct AiBridgeConfigBuilder {
142 config: AiBridgeConfig,
143}
144
145impl AiBridgeConfigBuilder {
146 #[must_use]
148 pub fn venv_path(mut self, path: impl Into<PathBuf>) -> Self {
149 self.config.venv_path = path.into();
150 self
151 }
152
153 #[must_use]
155 pub fn gpu_config(mut self, config: GpuConfig) -> Self {
156 self.config.gpu_config = config;
157 self
158 }
159
160 #[must_use]
162 pub fn gpu_enabled(mut self, enabled: bool) -> Self {
163 self.config.gpu_config.enabled = enabled;
164 self
165 }
166
167 #[must_use]
169 pub fn gpu_device(mut self, id: u32) -> Self {
170 self.config.gpu_config.device_id = Some(id);
171 self
172 }
173
174 #[must_use]
176 pub fn timeout(mut self, timeout: Duration) -> Self {
177 self.config.timeout = timeout;
178 self
179 }
180
181 #[must_use]
183 pub fn retry_config(mut self, config: RetryConfig) -> Self {
184 self.config.retry_config = config;
185 self
186 }
187
188 #[must_use]
190 pub fn max_retries(mut self, count: u32) -> Self {
191 self.config.retry_config.max_retries = count;
192 self
193 }
194
195 #[must_use]
197 pub fn log_level(mut self, level: LogLevel) -> Self {
198 self.config.log_level = level;
199 self
200 }
201
202 #[must_use]
204 pub fn build(self) -> AiBridgeConfig {
205 self.config
206 }
207}
208
209#[derive(Debug, Clone)]
211pub struct GpuConfig {
212 pub enabled: bool,
214 pub device_id: Option<u32>,
216 pub max_vram_mb: Option<u64>,
218 pub tile_size: Option<u32>,
220}
221
222impl Default for GpuConfig {
223 fn default() -> Self {
224 Self {
225 enabled: true,
226 device_id: None,
227 max_vram_mb: None,
228 tile_size: Some(DEFAULT_GPU_TILE_SIZE),
229 }
230 }
231}
232
233#[derive(Debug, Clone)]
235pub struct RetryConfig {
236 pub max_retries: u32,
238 pub retry_interval: Duration,
240 pub exponential_backoff: bool,
242}
243
244impl Default for RetryConfig {
245 fn default() -> Self {
246 Self {
247 max_retries: 3,
248 retry_interval: Duration::from_secs(5),
249 exponential_backoff: true,
250 }
251 }
252}
253
254#[derive(Debug, Clone, Copy, Default)]
256pub enum LogLevel {
257 #[default]
258 Info,
259 Debug,
260 Warn,
261 Error,
262}
263
264#[derive(Debug, Clone)]
266pub enum ProcessStatus {
267 Preparing,
269 Running { progress: f32 },
271 Completed { duration: Duration },
273 Failed { error: String, retries: u32 },
275 TimedOut,
277 Cancelled,
279}
280
281#[derive(Debug)]
283pub struct AiTaskResult {
284 pub processed_files: Vec<PathBuf>,
286 pub skipped_files: Vec<(PathBuf, String)>,
288 pub failed_files: Vec<(PathBuf, String)>,
290 pub duration: Duration,
292 pub gpu_stats: Option<GpuStats>,
294}
295
296#[derive(Debug, Clone)]
298pub struct GpuStats {
299 pub peak_vram_mb: u64,
301 pub avg_utilization: f32,
303}
304
305#[derive(Debug, Clone, Copy)]
307pub enum AiTool {
308 RealESRGAN,
309 YomiToku,
310}
311
312impl AiTool {
313 #[must_use]
315 pub fn module_name(&self) -> &str {
316 match self {
317 AiTool::RealESRGAN => "realesrgan",
318 AiTool::YomiToku => "yomitoku",
319 }
320 }
321}
322
323pub trait AiBridge {
325 fn new(config: AiBridgeConfig) -> Result<Self>
327 where
328 Self: Sized;
329
330 fn check_tool(&self, tool: AiTool) -> Result<bool>;
332
333 fn check_gpu(&self) -> Result<GpuStats>;
335
336 fn execute(
338 &self,
339 tool: AiTool,
340 input_files: &[PathBuf],
341 output_dir: &Path,
342 tool_options: &dyn std::any::Any,
343 ) -> Result<AiTaskResult>;
344
345 fn cancel(&self) -> Result<()>;
347}
348
349pub struct SubprocessBridge {
351 config: AiBridgeConfig,
352}
353
354impl SubprocessBridge {
355 #[allow(clippy::redundant_clone)] pub fn new(config: AiBridgeConfig) -> Result<Self> {
358 if !config.venv_path.exists() && !config.venv_path.to_string_lossy().contains("test") {
360 return Err(AiBridgeError::VenvNotFound(config.venv_path.clone()));
361 }
362
363 Ok(Self { config })
364 }
365
366 pub fn config(&self) -> &AiBridgeConfig {
368 &self.config
369 }
370
371 fn get_python_path(&self) -> PathBuf {
373 if cfg!(windows) {
374 self.config.venv_path.join("Scripts").join("python.exe")
375 } else {
376 self.config.venv_path.join("bin").join("python")
377 }
378 }
379
380 pub fn check_tool(&self, tool: AiTool) -> Result<bool> {
382 let python = self.get_python_path();
383
384 if !python.exists() {
385 return Ok(false);
386 }
387
388 let output = Command::new(&python)
389 .arg("-c")
390 .arg(format!("import {}", tool.module_name()))
391 .output();
392
393 match output {
394 Ok(o) => Ok(o.status.success()),
395 Err(_) => Ok(false),
396 }
397 }
398
399 pub fn check_gpu(&self) -> Result<GpuStats> {
401 let output = Command::new("nvidia-smi")
402 .args(["--query-gpu=memory.used", "--format=csv,noheader,nounits"])
403 .output()
404 .map_err(|_| AiBridgeError::GpuNotAvailable)?;
405
406 if !output.status.success() {
407 return Err(AiBridgeError::GpuNotAvailable);
408 }
409
410 let vram_str = String::from_utf8_lossy(&output.stdout);
411 let vram_mb: u64 = vram_str.trim().parse().unwrap_or(0);
412
413 Ok(GpuStats {
414 peak_vram_mb: vram_mb,
415 avg_utilization: 0.0,
416 })
417 }
418
419 pub fn execute(
427 &self,
428 tool: AiTool,
429 input_files: &[PathBuf],
430 output_dir: &Path,
431 tool_options: &dyn std::any::Any,
432 ) -> Result<AiTaskResult> {
433 let start_time = std::time::Instant::now();
434 let python = self.get_python_path();
435
436 let bridge_dir = self.config.venv_path.parent().unwrap_or(Path::new("."));
438 let bridge_script = match tool {
439 AiTool::RealESRGAN => bridge_dir.join("realesrgan_bridge.py"),
440 AiTool::YomiToku => bridge_dir.join("yomitoku_bridge.py"),
441 };
442
443 if !bridge_script.exists() {
445 return Err(AiBridgeError::ProcessFailed(format!(
446 "Bridge script not found: {}",
447 bridge_script.display()
448 )));
449 }
450
451 let mut processed = Vec::new();
452 let mut failed = Vec::new();
453
454 for input_file in input_files {
455 let mut last_error = None;
456
457 let output_filename = format!(
459 "{}_upscaled.{}",
460 input_file.file_stem().unwrap_or_default().to_string_lossy(),
461 input_file.extension().unwrap_or_default().to_string_lossy()
462 );
463 let output_path = output_dir.join(&output_filename);
464
465 for retry in 0..=self.config.retry_config.max_retries {
466 let mut cmd = Command::new(&python);
467 cmd.arg(&bridge_script);
468
469 match tool {
470 AiTool::RealESRGAN => {
471 cmd.arg("-i").arg(input_file);
472 cmd.arg("-o").arg(&output_path);
473
474 if let Some(opts) = tool_options.downcast_ref::<crate::RealEsrganOptions>() {
476 cmd.arg("-s").arg(opts.scale.to_string());
477 cmd.arg("-t").arg(opts.tile_size.to_string());
478 if let Some(gpu_id) = opts.gpu_id {
479 cmd.arg("-g").arg(gpu_id.to_string());
480 }
481 if !opts.fp16 {
482 cmd.arg("--fp32");
483 }
484 } else if let Some(tile) = self.config.gpu_config.tile_size {
485 cmd.arg("-t").arg(tile.to_string());
486 }
487
488 cmd.arg("--json");
489 }
490 AiTool::YomiToku => {
491 cmd.arg(input_file);
492 cmd.arg("--output").arg(output_dir);
493 cmd.arg("--json");
494 }
495 }
496
497 cmd.stdout(Stdio::piped());
498 cmd.stderr(Stdio::piped());
499
500 match cmd.output() {
501 Ok(output) if output.status.success() => {
502 processed.push(input_file.clone());
503 last_error = None;
504 break;
505 }
506 Ok(output) => {
507 let stderr = String::from_utf8_lossy(&output.stderr);
508 let stdout = String::from_utf8_lossy(&output.stdout);
509 last_error = Some(format!("stderr: {}, stdout: {}", stderr, stdout));
510
511 if stderr.contains("out of memory") || stderr.contains("CUDA error") {
512 return Err(AiBridgeError::OutOfMemory);
513 }
514 }
515 Err(e) => {
516 last_error = Some(e.to_string());
517 }
518 }
519
520 if retry < self.config.retry_config.max_retries {
522 let wait_time = if self.config.retry_config.exponential_backoff {
523 self.config.retry_config.retry_interval * 2_u32.pow(retry)
524 } else {
525 self.config.retry_config.retry_interval
526 };
527 std::thread::sleep(wait_time);
528 }
529 }
530
531 if let Some(error) = last_error {
532 failed.push((input_file.clone(), error));
533 }
534 }
535
536 Ok(AiTaskResult {
537 processed_files: processed,
538 skipped_files: vec![],
539 failed_files: failed,
540 duration: start_time.elapsed(),
541 gpu_stats: None,
542 })
543 }
544
545 pub fn execute_with_timeout(&self, args: &[String], timeout: Duration) -> Result<String> {
550 let python = self.get_python_path();
551
552 let mut cmd = Command::new(&python);
553 cmd.args(args);
554 cmd.stdout(Stdio::piped());
555 cmd.stderr(Stdio::piped());
556
557 let child = cmd
558 .spawn()
559 .map_err(|e| AiBridgeError::ProcessFailed(format!("Failed to spawn process: {}", e)))?;
560
561 let start = std::time::Instant::now();
563 let output = child
564 .wait_with_output()
565 .map_err(|e| AiBridgeError::ProcessFailed(format!("Process error: {}", e)))?;
566
567 if start.elapsed() > timeout {
568 return Err(AiBridgeError::Timeout(timeout));
569 }
570
571 if !output.status.success() {
572 let stderr = String::from_utf8_lossy(&output.stderr);
573 if stderr.contains("out of memory") || stderr.contains("CUDA error") {
574 return Err(AiBridgeError::OutOfMemory);
575 }
576 return Err(AiBridgeError::ProcessFailed(format!(
577 "Process exited with status {}: {}",
578 output.status, stderr
579 )));
580 }
581
582 Ok(String::from_utf8_lossy(&output.stdout).to_string())
583 }
584
585 pub fn cancel(&self) -> Result<()> {
587 Ok(())
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 #[test]
598 fn test_default_config() {
599 let config = AiBridgeConfig::default();
600
601 assert_eq!(config.venv_path, PathBuf::from("./ai_venv"));
602 assert!(config.gpu_config.enabled);
603 assert_eq!(config.timeout, Duration::from_secs(3600));
604 assert_eq!(config.retry_config.max_retries, 3);
605 }
606
607 #[test]
609 fn test_gpu_config_default() {
610 let config = GpuConfig::default();
611
612 assert!(config.enabled);
613 assert!(config.device_id.is_none());
614 assert!(config.max_vram_mb.is_none());
615 assert_eq!(config.tile_size, Some(400));
616 }
617
618 #[test]
619 fn test_retry_config_default() {
620 let config = RetryConfig::default();
621
622 assert_eq!(config.max_retries, 3);
623 assert_eq!(config.retry_interval, Duration::from_secs(5));
624 assert!(config.exponential_backoff);
625 }
626
627 #[test]
628 fn test_tool_module_names() {
629 assert_eq!(AiTool::RealESRGAN.module_name(), "realesrgan");
630 assert_eq!(AiTool::YomiToku.module_name(), "yomitoku");
631 }
632
633 #[test]
635 fn test_missing_venv_error() {
636 let config = AiBridgeConfig {
637 venv_path: PathBuf::from("/nonexistent/venv"),
638 ..Default::default()
639 };
640
641 let result = SubprocessBridge::new(config);
642 assert!(matches!(result, Err(AiBridgeError::VenvNotFound(_))));
643 }
644
645 #[test]
646 fn test_builder_pattern() {
647 let config = AiBridgeConfig::builder()
648 .venv_path("/custom/venv")
649 .gpu_enabled(false)
650 .timeout(Duration::from_secs(1800))
651 .max_retries(5)
652 .log_level(LogLevel::Debug)
653 .build();
654
655 assert_eq!(config.venv_path, PathBuf::from("/custom/venv"));
656 assert!(!config.gpu_config.enabled);
657 assert_eq!(config.timeout, Duration::from_secs(1800));
658 assert_eq!(config.retry_config.max_retries, 5);
659 assert!(matches!(config.log_level, LogLevel::Debug));
660 }
661
662 #[test]
663 fn test_cpu_only_preset() {
664 let config = AiBridgeConfig::cpu_only();
665
666 assert!(!config.gpu_config.enabled);
667 }
668
669 #[test]
670 fn test_low_vram_preset() {
671 let config = AiBridgeConfig::low_vram();
672
673 assert!(config.gpu_config.enabled);
674 assert_eq!(config.gpu_config.max_vram_mb, Some(2048));
675 assert_eq!(config.gpu_config.tile_size, Some(128));
676 }
677
678 #[test]
679 fn test_builder_gpu_device() {
680 let config = AiBridgeConfig::builder().gpu_device(1).build();
681
682 assert_eq!(config.gpu_config.device_id, Some(1));
683 }
684
685 #[test]
689 #[ignore = "requires external tool"]
690 fn test_bridge_initialization() {
691 let config = AiBridgeConfig {
692 venv_path: PathBuf::from("tests/fixtures/test_venv"),
693 ..Default::default()
694 };
695
696 let bridge = SubprocessBridge::new(config).unwrap();
697 assert!(bridge.check_tool(AiTool::RealESRGAN).is_ok());
698 }
699
700 #[test]
701 #[ignore = "requires external tool"]
702 fn test_check_gpu() {
703 let config = AiBridgeConfig::default();
704 let bridge = SubprocessBridge::new(config).unwrap();
705
706 let result = bridge.check_gpu();
707 match result {
709 Ok(stats) => {
710 eprintln!("GPU VRAM: {} MB", stats.peak_vram_mb);
712 }
713 Err(AiBridgeError::GpuNotAvailable) => {} Err(e) => panic!("Unexpected error: {:?}", e),
715 }
716 }
717
718 #[test]
720 #[ignore = "requires external tool"]
721 fn test_check_tool() {
722 let config = AiBridgeConfig {
723 venv_path: PathBuf::from("tests/fixtures/test_venv"),
724 ..Default::default()
725 };
726
727 let bridge = SubprocessBridge::new(config).unwrap();
728
729 let realesrgan_available = bridge.check_tool(AiTool::RealESRGAN).unwrap();
730 let yomitoku_available = bridge.check_tool(AiTool::YomiToku).unwrap();
731
732 assert!(realesrgan_available);
734 assert!(yomitoku_available);
735 }
736
737 #[test]
739 #[ignore = "requires external tool"]
740 fn test_execute_success() {
741 let config = AiBridgeConfig {
742 venv_path: PathBuf::from("tests/fixtures/test_venv"),
743 ..Default::default()
744 };
745 let bridge = SubprocessBridge::new(config).unwrap();
746 let temp_dir = tempfile::tempdir().unwrap();
747
748 let input_files = vec![PathBuf::from("tests/fixtures/test_image.png")];
749
750 let result = bridge
751 .execute(
752 AiTool::RealESRGAN,
753 &input_files,
754 temp_dir.path(),
755 &() as &dyn std::any::Any,
756 )
757 .unwrap();
758
759 assert_eq!(result.processed_files.len(), 1);
760 assert!(result.failed_files.is_empty());
761 }
762
763 #[test]
765 #[ignore = "requires external tool"]
766 fn test_execute_batch() {
767 let config = AiBridgeConfig {
768 venv_path: PathBuf::from("tests/fixtures/test_venv"),
769 ..Default::default()
770 };
771 let bridge = SubprocessBridge::new(config).unwrap();
772 let temp_dir = tempfile::tempdir().unwrap();
773
774 let input_files: Vec<_> = (1..=5)
775 .map(|i| PathBuf::from(format!("tests/fixtures/image_{}.png", i)))
776 .collect();
777
778 let result = bridge
779 .execute(
780 AiTool::RealESRGAN,
781 &input_files,
782 temp_dir.path(),
783 &() as &dyn std::any::Any,
784 )
785 .unwrap();
786
787 assert_eq!(result.processed_files.len(), 5);
788 }
789
790 #[test]
792 #[ignore = "requires external tool"]
793 fn test_timeout() {
794 let config = AiBridgeConfig {
795 venv_path: PathBuf::from("tests/fixtures/test_venv"),
796 timeout: Duration::from_millis(1), ..Default::default()
798 };
799 let bridge = SubprocessBridge::new(config).unwrap();
800 let temp_dir = tempfile::tempdir().unwrap();
801
802 let input_files = vec![PathBuf::from("tests/fixtures/large_image.png")];
803
804 let result = bridge.execute(
805 AiTool::RealESRGAN,
806 &input_files,
807 temp_dir.path(),
808 &() as &dyn std::any::Any,
809 );
810
811 assert!(matches!(result, Err(AiBridgeError::Timeout(_))));
812 }
813
814 #[test]
816 fn test_retry_config_exponential_backoff() {
817 let config = RetryConfig {
818 max_retries: 3,
819 retry_interval: Duration::from_secs(1),
820 exponential_backoff: true,
821 };
822
823 let base = config.retry_interval;
825 assert_eq!(base * 2_u32.pow(0), Duration::from_secs(1)); assert_eq!(base * 2_u32.pow(1), Duration::from_secs(2)); assert_eq!(base * 2_u32.pow(2), Duration::from_secs(4)); }
829
830 #[test]
832 fn test_cancel() {
833 let config = AiBridgeConfig {
834 venv_path: PathBuf::from("tests/fixtures/test_venv"),
835 ..Default::default()
836 };
837
838 if config.venv_path.exists() {
840 let bridge = SubprocessBridge::new(config).unwrap();
841 assert!(bridge.cancel().is_ok());
843 }
844 }
845
846 #[test]
848 fn test_process_status_variants() {
849 let preparing = ProcessStatus::Preparing;
850 let running = ProcessStatus::Running { progress: 0.5 };
851 let completed = ProcessStatus::Completed {
852 duration: Duration::from_secs(10),
853 };
854 let failed = ProcessStatus::Failed {
855 error: "Test error".to_string(),
856 retries: 2,
857 };
858 let timed_out = ProcessStatus::TimedOut;
859 let cancelled = ProcessStatus::Cancelled;
860
861 assert!(matches!(preparing, ProcessStatus::Preparing));
863 assert!(matches!(running, ProcessStatus::Running { progress: _ }));
864 assert!(matches!(completed, ProcessStatus::Completed { .. }));
865 assert!(matches!(failed, ProcessStatus::Failed { .. }));
866 assert!(matches!(timed_out, ProcessStatus::TimedOut));
867 assert!(matches!(cancelled, ProcessStatus::Cancelled));
868 }
869
870 #[test]
872 fn test_ai_task_result() {
873 let result = AiTaskResult {
874 processed_files: vec![PathBuf::from("test1.png"), PathBuf::from("test2.png")],
875 skipped_files: vec![(PathBuf::from("skip.png"), "Skipped reason".to_string())],
876 failed_files: vec![(PathBuf::from("fail.png"), "Error message".to_string())],
877 duration: Duration::from_secs(5),
878 gpu_stats: Some(GpuStats {
879 peak_vram_mb: 2048,
880 avg_utilization: 75.0,
881 }),
882 };
883
884 assert_eq!(result.processed_files.len(), 2);
885 assert_eq!(result.skipped_files.len(), 1);
886 assert_eq!(result.failed_files.len(), 1);
887 assert_eq!(result.duration, Duration::from_secs(5));
888 assert!(result.gpu_stats.is_some());
889 }
890
891 #[test]
893 fn test_gpu_stats() {
894 let stats = GpuStats {
895 peak_vram_mb: 4096,
896 avg_utilization: 85.5,
897 };
898
899 assert_eq!(stats.peak_vram_mb, 4096);
900 assert_eq!(stats.avg_utilization, 85.5);
901 }
902
903 #[test]
905 fn test_ai_task_result_no_gpu() {
906 let result = AiTaskResult {
907 processed_files: vec![PathBuf::from("test.png")],
908 skipped_files: vec![],
909 failed_files: vec![],
910 duration: Duration::from_secs(3),
911 gpu_stats: None,
912 };
913
914 assert_eq!(result.processed_files.len(), 1);
915 assert!(result.gpu_stats.is_none());
916 }
917
918 #[test]
920 fn test_error_display() {
921 let errors: Vec<(AiBridgeError, &str)> = vec![
922 (
923 AiBridgeError::VenvNotFound(PathBuf::from("/test")),
924 "environment",
925 ),
926 (AiBridgeError::GpuNotAvailable, "gpu"),
927 (AiBridgeError::OutOfMemory, "memory"),
928 (
929 AiBridgeError::ProcessFailed("test error".to_string()),
930 "failed",
931 ),
932 (AiBridgeError::Timeout(Duration::from_secs(60)), "timed out"),
933 ];
934
935 for (err, expected_substr) in errors {
936 let msg = err.to_string().to_lowercase();
937 assert!(
938 msg.contains(&expected_substr.to_lowercase()),
939 "Expected '{}' to contain '{}'",
940 msg,
941 expected_substr
942 );
943 }
944 }
945
946 #[test]
948 fn test_log_level_variants() {
949 assert!(matches!(LogLevel::Error, LogLevel::Error));
950 assert!(matches!(LogLevel::Warn, LogLevel::Warn));
951 assert!(matches!(LogLevel::Info, LogLevel::Info));
952 assert!(matches!(LogLevel::Debug, LogLevel::Debug));
953 }
954
955 #[test]
957 fn test_gpu_config_max_vram() {
958 let gpu_config = GpuConfig {
959 enabled: true,
960 device_id: Some(0),
961 max_vram_mb: Some(4096),
962 tile_size: Some(256),
963 };
964
965 assert_eq!(gpu_config.max_vram_mb, Some(4096));
966 assert_eq!(gpu_config.tile_size, Some(256));
967 }
968
969 #[test]
971 fn test_builder_retry_settings() {
972 let config = AiBridgeConfig::builder().max_retries(5).build();
973
974 assert_eq!(config.retry_config.max_retries, 5);
975 }
976
977 #[test]
979 fn test_retry_config_interval() {
980 let retry_config = RetryConfig {
981 max_retries: 3,
982 retry_interval: Duration::from_secs(10),
983 exponential_backoff: true,
984 };
985
986 assert_eq!(retry_config.retry_interval, Duration::from_secs(10));
987 }
988
989 #[test]
991 fn test_builder_chaining() {
992 let config = AiBridgeConfig::builder()
993 .venv_path("/custom/venv")
994 .gpu_enabled(true)
995 .gpu_device(0)
996 .timeout(Duration::from_secs(7200))
997 .max_retries(2)
998 .log_level(LogLevel::Warn)
999 .build();
1000
1001 assert_eq!(config.venv_path, PathBuf::from("/custom/venv"));
1002 assert!(config.gpu_config.enabled);
1003 assert_eq!(config.gpu_config.device_id, Some(0));
1004 assert_eq!(config.timeout, Duration::from_secs(7200));
1005 assert_eq!(config.retry_config.max_retries, 2);
1006 }
1007
1008 #[test]
1010 fn test_builder_gpu_config() {
1011 let gpu_config = GpuConfig {
1012 enabled: true,
1013 device_id: Some(1),
1014 max_vram_mb: Some(8192),
1015 tile_size: Some(512),
1016 };
1017
1018 let config = AiBridgeConfig::builder().gpu_config(gpu_config).build();
1019
1020 assert!(config.gpu_config.enabled);
1021 assert_eq!(config.gpu_config.device_id, Some(1));
1022 assert_eq!(config.gpu_config.max_vram_mb, Some(8192));
1023 assert_eq!(config.gpu_config.tile_size, Some(512));
1024 }
1025
1026 #[test]
1028 fn test_builder_retry_config() {
1029 let retry_config = RetryConfig {
1030 max_retries: 10,
1031 retry_interval: Duration::from_secs(30),
1032 exponential_backoff: false,
1033 };
1034
1035 let config = AiBridgeConfig::builder().retry_config(retry_config).build();
1036
1037 assert_eq!(config.retry_config.max_retries, 10);
1038 assert_eq!(config.retry_config.retry_interval, Duration::from_secs(30));
1039 assert!(!config.retry_config.exponential_backoff);
1040 }
1041
1042 #[test]
1044 fn test_process_status_progress() {
1045 let running_50 = ProcessStatus::Running { progress: 0.5 };
1046 let running_100 = ProcessStatus::Running { progress: 1.0 };
1047 let running_0 = ProcessStatus::Running { progress: 0.0 };
1048
1049 if let ProcessStatus::Running { progress } = running_50 {
1050 assert_eq!(progress, 0.5);
1051 }
1052 if let ProcessStatus::Running { progress } = running_100 {
1053 assert_eq!(progress, 1.0);
1054 }
1055 if let ProcessStatus::Running { progress } = running_0 {
1056 assert_eq!(progress, 0.0);
1057 }
1058 }
1059
1060 #[test]
1062 fn test_process_status_failed() {
1063 let failed = ProcessStatus::Failed {
1064 error: "Connection timeout".to_string(),
1065 retries: 3,
1066 };
1067
1068 if let ProcessStatus::Failed { error, retries } = failed {
1069 assert_eq!(error, "Connection timeout");
1070 assert_eq!(retries, 3);
1071 }
1072 }
1073
1074 #[test]
1076 fn test_process_status_completed() {
1077 let completed = ProcessStatus::Completed {
1078 duration: Duration::from_millis(1500),
1079 };
1080
1081 if let ProcessStatus::Completed { duration } = completed {
1082 assert_eq!(duration, Duration::from_millis(1500));
1083 }
1084 }
1085
1086 #[test]
1088 fn test_all_tool_module_names() {
1089 assert_eq!(AiTool::RealESRGAN.module_name(), "realesrgan");
1090 assert_eq!(AiTool::YomiToku.module_name(), "yomitoku");
1091 }
1092
1093 #[test]
1095 fn test_retry_config_linear() {
1096 let config = RetryConfig {
1097 max_retries: 5,
1098 retry_interval: Duration::from_secs(2),
1099 exponential_backoff: false,
1100 };
1101
1102 assert_eq!(config.max_retries, 5);
1103 assert_eq!(config.retry_interval, Duration::from_secs(2));
1104 assert!(!config.exponential_backoff);
1105 }
1106
1107 #[test]
1109 fn test_progress_status_sequence() {
1110 let statuses = [
1112 ProcessStatus::Preparing,
1113 ProcessStatus::Running { progress: 0.0 },
1114 ProcessStatus::Running { progress: 0.25 },
1115 ProcessStatus::Running { progress: 0.5 },
1116 ProcessStatus::Running { progress: 0.75 },
1117 ProcessStatus::Running { progress: 1.0 },
1118 ProcessStatus::Completed {
1119 duration: Duration::from_secs(10),
1120 },
1121 ];
1122
1123 assert!(matches!(statuses[0], ProcessStatus::Preparing));
1124 assert!(matches!(statuses[6], ProcessStatus::Completed { .. }));
1125
1126 let mut prev_progress = -1.0;
1128 for status in statuses.iter().skip(1).take(5) {
1129 if let ProcessStatus::Running { progress } = status {
1130 assert!(*progress > prev_progress);
1131 prev_progress = *progress;
1132 }
1133 }
1134 }
1135
1136 #[test]
1138 fn test_retries_exhausted_error() {
1139 let err = AiBridgeError::RetriesExhausted;
1140 let msg = err.to_string().to_lowercase();
1141 assert!(msg.contains("retries") || msg.contains("exhausted"));
1142 }
1143
1144 #[test]
1146 fn test_tool_not_installed_error() {
1147 let err = AiBridgeError::ToolNotInstalled(AiTool::RealESRGAN);
1148 let msg = err.to_string().to_lowercase();
1149 assert!(msg.contains("not installed") || msg.contains("tool"));
1150 }
1151
1152 #[test]
1154 fn test_batch_result_mixed_outcomes() {
1155 let result = AiTaskResult {
1156 processed_files: vec![PathBuf::from("success1.png"), PathBuf::from("success2.png")],
1157 skipped_files: vec![(PathBuf::from("skip.png"), "Already processed".to_string())],
1158 failed_files: vec![
1159 (PathBuf::from("fail1.png"), "Corrupted image".to_string()),
1160 (PathBuf::from("fail2.png"), "Out of memory".to_string()),
1161 ],
1162 duration: Duration::from_secs(120),
1163 gpu_stats: Some(GpuStats {
1164 peak_vram_mb: 3500,
1165 avg_utilization: 68.5,
1166 }),
1167 };
1168
1169 assert_eq!(result.processed_files.len(), 2);
1171 assert_eq!(result.skipped_files.len(), 1);
1172 assert_eq!(result.failed_files.len(), 2);
1173
1174 assert!(result.failed_files[0].1.contains("Corrupted"));
1176 assert!(result.failed_files[1].1.contains("memory"));
1177
1178 assert!(result.skipped_files[0].1.contains("Already"));
1180 }
1181
1182 #[test]
1184 fn test_gpu_config_disabled() {
1185 let config = AiBridgeConfig::cpu_only();
1186 assert!(!config.gpu_config.enabled);
1187 assert!(config.gpu_config.device_id.is_none());
1188 }
1189
1190 #[test]
1192 fn test_gpu_config_specific_device() {
1193 let gpu_config = GpuConfig {
1194 enabled: true,
1195 device_id: Some(2),
1196 max_vram_mb: Some(6144),
1197 tile_size: Some(384),
1198 };
1199
1200 assert!(gpu_config.enabled);
1201 assert_eq!(gpu_config.device_id, Some(2));
1202 assert_eq!(gpu_config.max_vram_mb, Some(6144));
1203 assert_eq!(gpu_config.tile_size, Some(384));
1204 }
1205
1206 #[test]
1208 fn test_exponential_backoff_edge_cases() {
1209 let config = RetryConfig {
1210 max_retries: 6,
1211 retry_interval: Duration::from_millis(100),
1212 exponential_backoff: true,
1213 };
1214
1215 let base = config.retry_interval;
1217 assert_eq!(base * 2_u32.pow(0), Duration::from_millis(100)); assert_eq!(base * 2_u32.pow(1), Duration::from_millis(200)); assert_eq!(base * 2_u32.pow(2), Duration::from_millis(400)); assert_eq!(base * 2_u32.pow(3), Duration::from_millis(800)); assert_eq!(base * 2_u32.pow(4), Duration::from_millis(1600)); assert_eq!(base * 2_u32.pow(5), Duration::from_millis(3200)); }
1224
1225 #[test]
1227 fn test_ai_tool_all_variants() {
1228 let tools = [AiTool::RealESRGAN, AiTool::YomiToku];
1229
1230 for tool in tools {
1231 let module_name = tool.module_name();
1232 assert!(!module_name.is_empty());
1233 }
1234 }
1235
1236 #[test]
1238 fn test_log_level_default() {
1239 let default_level = LogLevel::default();
1240 assert!(matches!(default_level, LogLevel::Info));
1241 }
1242
1243 #[test]
1245 fn test_builder_full_configuration() {
1246 let config = AiBridgeConfig::builder()
1247 .venv_path("/opt/ai/venv")
1248 .gpu_enabled(true)
1249 .gpu_device(1)
1250 .timeout(Duration::from_secs(1800))
1251 .max_retries(5)
1252 .log_level(LogLevel::Debug)
1253 .build();
1254
1255 assert_eq!(config.venv_path, PathBuf::from("/opt/ai/venv"));
1256 assert!(config.gpu_config.enabled);
1257 assert_eq!(config.gpu_config.device_id, Some(1));
1258 assert_eq!(config.timeout, Duration::from_secs(1800));
1259 assert_eq!(config.retry_config.max_retries, 5);
1260 assert!(matches!(config.log_level, LogLevel::Debug));
1261 }
1262
1263 #[test]
1265 fn test_process_status_failed_max_retries() {
1266 let failed = ProcessStatus::Failed {
1267 error: "Persistent failure".to_string(),
1268 retries: 10,
1269 };
1270
1271 if let ProcessStatus::Failed { error, retries } = failed {
1272 assert_eq!(retries, 10);
1273 assert!(error.contains("Persistent"));
1274 }
1275 }
1276
1277 #[test]
1279 fn test_gpu_stats_edge_values() {
1280 let zero_stats = GpuStats {
1282 peak_vram_mb: 0,
1283 avg_utilization: 0.0,
1284 };
1285 assert_eq!(zero_stats.peak_vram_mb, 0);
1286 assert_eq!(zero_stats.avg_utilization, 0.0);
1287
1288 let max_stats = GpuStats {
1290 peak_vram_mb: 48000, avg_utilization: 100.0,
1292 };
1293 assert_eq!(max_stats.peak_vram_mb, 48000);
1294 assert_eq!(max_stats.avg_utilization, 100.0);
1295 }
1296
1297 #[test]
1299 fn test_config_timeout_variations() {
1300 let short = AiBridgeConfig::builder()
1302 .timeout(Duration::from_millis(100))
1303 .build();
1304 assert_eq!(short.timeout, Duration::from_millis(100));
1305
1306 let long = AiBridgeConfig::builder()
1308 .timeout(Duration::from_secs(86400))
1309 .build();
1310 assert_eq!(long.timeout, Duration::from_secs(86400));
1311 }
1312
1313 #[test]
1316 fn test_config_debug_impl() {
1317 let config = AiBridgeConfig::builder().venv_path("/test").build();
1318 let debug_str = format!("{:?}", config);
1319 assert!(debug_str.contains("AiBridgeConfig"));
1320 assert!(debug_str.contains("test"));
1321 }
1322
1323 #[test]
1324 fn test_config_clone() {
1325 let original = AiBridgeConfig::builder()
1326 .venv_path("/cloned")
1327 .gpu_enabled(false)
1328 .max_retries(5)
1329 .build();
1330 let cloned = original.clone();
1331 assert_eq!(cloned.venv_path, original.venv_path);
1332 assert_eq!(cloned.gpu_config.enabled, original.gpu_config.enabled);
1333 assert_eq!(
1334 cloned.retry_config.max_retries,
1335 original.retry_config.max_retries
1336 );
1337 }
1338
1339 #[test]
1340 fn test_gpu_config_debug_impl() {
1341 let config = GpuConfig {
1342 enabled: true,
1343 device_id: Some(0),
1344 max_vram_mb: Some(4096),
1345 tile_size: Some(256),
1346 };
1347 let debug_str = format!("{:?}", config);
1348 assert!(debug_str.contains("GpuConfig"));
1349 assert!(debug_str.contains("4096"));
1350 }
1351
1352 #[test]
1353 fn test_gpu_config_clone() {
1354 let original = GpuConfig {
1355 enabled: true,
1356 device_id: Some(1),
1357 max_vram_mb: Some(8192),
1358 tile_size: Some(512),
1359 };
1360 let cloned = original.clone();
1361 assert_eq!(cloned.enabled, original.enabled);
1362 assert_eq!(cloned.device_id, original.device_id);
1363 assert_eq!(cloned.max_vram_mb, original.max_vram_mb);
1364 }
1365
1366 #[test]
1367 fn test_retry_config_debug_impl() {
1368 let config = RetryConfig {
1369 max_retries: 5,
1370 retry_interval: Duration::from_secs(10),
1371 exponential_backoff: true,
1372 };
1373 let debug_str = format!("{:?}", config);
1374 assert!(debug_str.contains("RetryConfig"));
1375 assert!(debug_str.contains("5"));
1376 }
1377
1378 #[test]
1379 fn test_retry_config_clone() {
1380 let original = RetryConfig {
1381 max_retries: 7,
1382 retry_interval: Duration::from_secs(30),
1383 exponential_backoff: false,
1384 };
1385 let cloned = original.clone();
1386 assert_eq!(cloned.max_retries, original.max_retries);
1387 assert_eq!(cloned.retry_interval, original.retry_interval);
1388 assert_eq!(cloned.exponential_backoff, original.exponential_backoff);
1389 }
1390
1391 #[test]
1392 fn test_process_status_debug_impl() {
1393 let status = ProcessStatus::Running { progress: 0.5 };
1394 let debug_str = format!("{:?}", status);
1395 assert!(debug_str.contains("Running"));
1396 assert!(debug_str.contains("0.5"));
1397 }
1398
1399 #[test]
1400 fn test_ai_task_result_debug_impl() {
1401 let result = AiTaskResult {
1402 processed_files: vec![PathBuf::from("test.png")],
1403 skipped_files: vec![],
1404 failed_files: vec![],
1405 duration: Duration::from_secs(1),
1406 gpu_stats: None,
1407 };
1408 let debug_str = format!("{:?}", result);
1409 assert!(debug_str.contains("AiTaskResult"));
1410 }
1411
1412 #[test]
1413 fn test_gpu_stats_debug_impl() {
1414 let stats = GpuStats {
1415 peak_vram_mb: 3000,
1416 avg_utilization: 75.0,
1417 };
1418 let debug_str = format!("{:?}", stats);
1419 assert!(debug_str.contains("GpuStats"));
1420 assert!(debug_str.contains("3000"));
1421 }
1422
1423 #[test]
1424 fn test_error_debug_impl() {
1425 let err = AiBridgeError::OutOfMemory;
1426 let debug_str = format!("{:?}", err);
1427 assert!(debug_str.contains("OutOfMemory"));
1428 }
1429
1430 #[test]
1431 fn test_ai_tool_debug_impl() {
1432 let tool = AiTool::RealESRGAN;
1433 let debug_str = format!("{:?}", tool);
1434 assert!(debug_str.contains("RealESRGAN"));
1435 }
1436
1437 #[test]
1438 fn test_ai_tool_clone() {
1439 let original = AiTool::YomiToku;
1440 let cloned = original;
1441 assert_eq!(cloned.module_name(), original.module_name());
1442 }
1443
1444 #[test]
1445 fn test_log_level_debug_impl() {
1446 let level = LogLevel::Debug;
1447 let debug_str = format!("{:?}", level);
1448 assert!(debug_str.contains("Debug"));
1449 }
1450
1451 #[test]
1452 fn test_log_level_clone() {
1453 let original = LogLevel::Warn;
1454 let cloned = original;
1455 assert!(matches!(cloned, LogLevel::Warn));
1456 }
1457
1458 #[test]
1459 fn test_error_io_conversion() {
1460 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "not found");
1461 let bridge_err: AiBridgeError = io_err.into();
1462 let msg = bridge_err.to_string().to_lowercase();
1463 assert!(msg.contains("io") || msg.contains("error"));
1464 }
1465
1466 #[test]
1467 fn test_builder_default_produces_valid_config() {
1468 let config = AiBridgeConfigBuilder::default().build();
1469 assert!(!config.venv_path.as_os_str().is_empty());
1470 assert!(config.timeout.as_secs() > 0);
1471 }
1472
1473 #[test]
1474 fn test_ai_task_result_all_empty() {
1475 let result = AiTaskResult {
1476 processed_files: vec![],
1477 skipped_files: vec![],
1478 failed_files: vec![],
1479 duration: Duration::ZERO,
1480 gpu_stats: None,
1481 };
1482
1483 assert!(result.processed_files.is_empty());
1484 assert!(result.skipped_files.is_empty());
1485 assert!(result.failed_files.is_empty());
1486 assert_eq!(result.duration, Duration::ZERO);
1487 }
1488
1489 #[test]
1490 fn test_path_types_in_result() {
1491 let result_abs = AiTaskResult {
1493 processed_files: vec![PathBuf::from("/absolute/path.png")],
1494 skipped_files: vec![],
1495 failed_files: vec![],
1496 duration: Duration::from_secs(1),
1497 gpu_stats: None,
1498 };
1499 assert!(result_abs.processed_files[0].is_absolute());
1500
1501 let result_rel = AiTaskResult {
1503 processed_files: vec![PathBuf::from("relative/path.png")],
1504 skipped_files: vec![],
1505 failed_files: vec![],
1506 duration: Duration::from_secs(1),
1507 gpu_stats: None,
1508 };
1509 assert!(result_rel.processed_files[0].is_relative());
1510 }
1511
1512 #[test]
1513 fn test_preset_configs_consistency() {
1514 let cpu = AiBridgeConfig::cpu_only();
1515 let low_vram = AiBridgeConfig::low_vram();
1516 let default_config = AiBridgeConfig::default();
1517
1518 assert!(!cpu.gpu_config.enabled);
1520
1521 assert!(low_vram.gpu_config.tile_size < default_config.gpu_config.tile_size);
1523
1524 assert!(low_vram.gpu_config.max_vram_mb.is_some());
1526 }
1527
1528 #[test]
1529 fn test_gpu_utilization_range() {
1530 for i in 0..=10 {
1531 let util = i as f32 * 10.0;
1532 let stats = GpuStats {
1533 peak_vram_mb: 1000,
1534 avg_utilization: util,
1535 };
1536 assert!(stats.avg_utilization >= 0.0 && stats.avg_utilization <= 100.0);
1537 }
1538 }
1539
1540 #[test]
1541 fn test_error_variants_all() {
1542 let errors: Vec<AiBridgeError> = vec![
1543 AiBridgeError::VenvNotFound(PathBuf::from("/test")),
1544 AiBridgeError::GpuNotAvailable,
1545 AiBridgeError::OutOfMemory,
1546 AiBridgeError::ProcessFailed("test".to_string()),
1547 AiBridgeError::Timeout(Duration::from_secs(60)),
1548 AiBridgeError::RetriesExhausted,
1549 AiBridgeError::ToolNotInstalled(AiTool::RealESRGAN),
1550 std::io::Error::other("io").into(),
1551 ];
1552
1553 for err in errors {
1554 let msg = err.to_string();
1555 assert!(!msg.is_empty());
1556 }
1557 }
1558
1559 #[test]
1560 fn test_process_status_all_variants() {
1561 let statuses = vec![
1562 ProcessStatus::Preparing,
1563 ProcessStatus::Running { progress: 0.5 },
1564 ProcessStatus::Completed {
1565 duration: Duration::from_secs(1),
1566 },
1567 ProcessStatus::Failed {
1568 error: "test".to_string(),
1569 retries: 1,
1570 },
1571 ProcessStatus::TimedOut,
1572 ProcessStatus::Cancelled,
1573 ];
1574
1575 for status in statuses {
1576 let debug_str = format!("{:?}", status);
1577 assert!(!debug_str.is_empty());
1578 }
1579 }
1580
1581 #[test]
1582 fn test_venv_path_extraction() {
1583 let path = PathBuf::from("/my/venv/path");
1584 let err = AiBridgeError::VenvNotFound(path.clone());
1585
1586 if let AiBridgeError::VenvNotFound(p) = err {
1587 assert_eq!(p, path);
1588 } else {
1589 panic!("Wrong error variant");
1590 }
1591 }
1592
1593 #[test]
1594 fn test_tool_not_installed_extraction() {
1595 let err = AiBridgeError::ToolNotInstalled(AiTool::YomiToku);
1596
1597 if let AiBridgeError::ToolNotInstalled(tool) = err {
1598 assert_eq!(tool.module_name(), "yomitoku");
1599 } else {
1600 panic!("Wrong error variant");
1601 }
1602 }
1603
1604 #[test]
1607 fn test_timeout_zero() {
1608 let config = AiBridgeConfig::builder()
1609 .timeout(Duration::from_secs(0))
1610 .build();
1611 assert_eq!(config.timeout, Duration::from_secs(0));
1612 }
1613
1614 #[test]
1615 fn test_timeout_max_value() {
1616 let config = AiBridgeConfig::builder()
1617 .timeout(Duration::from_secs(u64::MAX))
1618 .build();
1619 assert_eq!(config.timeout, Duration::from_secs(u64::MAX));
1620 }
1621
1622 #[test]
1623 fn test_max_retries_zero() {
1624 let config = AiBridgeConfig::builder().max_retries(0).build();
1625 assert_eq!(config.retry_config.max_retries, 0);
1626 }
1627
1628 #[test]
1629 fn test_max_retries_large() {
1630 let config = AiBridgeConfig::builder().max_retries(1000).build();
1631 assert_eq!(config.retry_config.max_retries, 1000);
1632 }
1633
1634 #[test]
1635 fn test_gpu_device_zero() {
1636 let config = AiBridgeConfig::builder().gpu_device(0).build();
1637 assert_eq!(config.gpu_config.device_id, Some(0));
1638 }
1639
1640 #[test]
1641 fn test_gpu_device_high_id() {
1642 let config = AiBridgeConfig::builder().gpu_device(15).build();
1643 assert_eq!(config.gpu_config.device_id, Some(15));
1644 }
1645
1646 #[test]
1647 fn test_progress_boundary_zero() {
1648 let status = ProcessStatus::Running { progress: 0.0 };
1649 if let ProcessStatus::Running { progress } = status {
1650 assert_eq!(progress, 0.0);
1651 }
1652 }
1653
1654 #[test]
1655 fn test_progress_boundary_one() {
1656 let status = ProcessStatus::Running { progress: 1.0 };
1657 if let ProcessStatus::Running { progress } = status {
1658 assert_eq!(progress, 1.0);
1659 }
1660 }
1661
1662 #[test]
1663 fn test_progress_boundary_negative() {
1664 let status = ProcessStatus::Running { progress: -0.1 };
1666 if let ProcessStatus::Running { progress } = status {
1667 assert!(progress < 0.0);
1668 }
1669 }
1670
1671 #[test]
1672 fn test_progress_boundary_over_one() {
1673 let status = ProcessStatus::Running { progress: 1.5 };
1675 if let ProcessStatus::Running { progress } = status {
1676 assert!(progress > 1.0);
1677 }
1678 }
1679
1680 #[test]
1681 fn test_duration_zero_completed() {
1682 let status = ProcessStatus::Completed {
1683 duration: Duration::from_secs(0),
1684 };
1685 if let ProcessStatus::Completed { duration } = status {
1686 assert_eq!(duration, Duration::ZERO);
1687 }
1688 }
1689
1690 #[test]
1691 fn test_duration_nanos() {
1692 let status = ProcessStatus::Completed {
1693 duration: Duration::from_nanos(1),
1694 };
1695 if let ProcessStatus::Completed { duration } = status {
1696 assert_eq!(duration.as_nanos(), 1);
1697 }
1698 }
1699
1700 #[test]
1701 fn test_retries_max_failed() {
1702 let status = ProcessStatus::Failed {
1703 error: "max".to_string(),
1704 retries: u32::MAX,
1705 };
1706 if let ProcessStatus::Failed { retries, .. } = status {
1707 assert_eq!(retries, u32::MAX);
1708 }
1709 }
1710
1711 #[test]
1712 fn test_timeout_error_zero_duration() {
1713 let err = AiBridgeError::Timeout(Duration::ZERO);
1714 let msg = err.to_string();
1715 assert!(msg.contains("0"));
1716 }
1717
1718 #[test]
1719 fn test_timeout_error_large_duration() {
1720 let err = AiBridgeError::Timeout(Duration::from_secs(86400 * 365));
1721 let msg = err.to_string();
1722 assert!(!msg.is_empty());
1723 }
1724}