Skip to main content

superbook_pdf/
ai_bridge.rs

1//! AI Tools Bridge module
2//!
3//! Provides communication with external AI tools (Python: `RealESRGAN`, `YomiToku`, etc.)
4//!
5//! # Features
6//!
7//! - Subprocess management for Python AI tools
8//! - GPU/CPU configuration with VRAM limits
9//! - Automatic retry on failure
10//! - Progress and timeout handling
11//!
12//! # Example
13//!
14//! ```rust,no_run
15//! use superbook_pdf::{AiBridgeConfig, SubprocessBridge, AiTool};
16//!
17//! // Configure AI bridge
18//! let config = AiBridgeConfig::builder()
19//!     .gpu_enabled(true)
20//!     .max_retries(3)
21//!     .build();
22//!
23//! // Create bridge for RealESRGAN
24//! // let bridge = SubprocessBridge::new(AiTool::RealEsrgan, &config);
25//! ```
26
27use std::path::{Path, PathBuf};
28use std::process::{Command, Stdio};
29use std::time::Duration;
30use thiserror::Error;
31
32// ============================================================
33// Constants
34// ============================================================
35
36/// Default timeout for AI processing (1 hour)
37const DEFAULT_TIMEOUT_SECS: u64 = 3600;
38
39/// Low VRAM limit (2GB)
40const LOW_VRAM_MB: u64 = 2048;
41
42/// Low VRAM tile size
43const LOW_VRAM_TILE_SIZE: u32 = 128;
44
45/// Default tile size for GPU processing
46const DEFAULT_GPU_TILE_SIZE: u32 = 400;
47
48/// AI Bridge error types
49#[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/// AI Bridge configuration
79#[derive(Debug, Clone)]
80pub struct AiBridgeConfig {
81    /// Python virtual environment path
82    pub venv_path: PathBuf,
83    /// GPU configuration
84    pub gpu_config: GpuConfig,
85    /// Timeout duration
86    pub timeout: Duration,
87    /// Retry configuration
88    pub retry_config: RetryConfig,
89    /// Log level
90    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    /// Create a new config builder
107    #[must_use]
108    pub fn builder() -> AiBridgeConfigBuilder {
109        AiBridgeConfigBuilder::default()
110    }
111
112    /// Create config for CPU-only processing
113    #[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    /// Create config for low VRAM systems
125    #[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/// Builder for [`AiBridgeConfig`]
140#[derive(Debug, Default)]
141pub struct AiBridgeConfigBuilder {
142    config: AiBridgeConfig,
143}
144
145impl AiBridgeConfigBuilder {
146    /// Set Python virtual environment path
147    #[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    /// Set GPU configuration
154    #[must_use]
155    pub fn gpu_config(mut self, config: GpuConfig) -> Self {
156        self.config.gpu_config = config;
157        self
158    }
159
160    /// Enable or disable GPU
161    #[must_use]
162    pub fn gpu_enabled(mut self, enabled: bool) -> Self {
163        self.config.gpu_config.enabled = enabled;
164        self
165    }
166
167    /// Set GPU device ID
168    #[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    /// Set timeout duration
175    #[must_use]
176    pub fn timeout(mut self, timeout: Duration) -> Self {
177        self.config.timeout = timeout;
178        self
179    }
180
181    /// Set retry configuration
182    #[must_use]
183    pub fn retry_config(mut self, config: RetryConfig) -> Self {
184        self.config.retry_config = config;
185        self
186    }
187
188    /// Set maximum retry count
189    #[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    /// Set log level
196    #[must_use]
197    pub fn log_level(mut self, level: LogLevel) -> Self {
198        self.config.log_level = level;
199        self
200    }
201
202    /// Build the configuration
203    #[must_use]
204    pub fn build(self) -> AiBridgeConfig {
205        self.config
206    }
207}
208
209/// GPU configuration
210#[derive(Debug, Clone)]
211pub struct GpuConfig {
212    /// Enable GPU
213    pub enabled: bool,
214    /// GPU device ID (None for auto)
215    pub device_id: Option<u32>,
216    /// Maximum VRAM usage (MB)
217    pub max_vram_mb: Option<u64>,
218    /// Tile size for memory efficiency
219    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/// Retry configuration
234#[derive(Debug, Clone)]
235pub struct RetryConfig {
236    /// Maximum retry count
237    pub max_retries: u32,
238    /// Retry interval
239    pub retry_interval: Duration,
240    /// Use exponential backoff
241    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/// Log levels
255#[derive(Debug, Clone, Copy, Default)]
256pub enum LogLevel {
257    #[default]
258    Info,
259    Debug,
260    Warn,
261    Error,
262}
263
264/// Process status
265#[derive(Debug, Clone)]
266pub enum ProcessStatus {
267    /// Preparing
268    Preparing,
269    /// Running with progress
270    Running { progress: f32 },
271    /// Completed
272    Completed { duration: Duration },
273    /// Failed
274    Failed { error: String, retries: u32 },
275    /// Timed out
276    TimedOut,
277    /// Cancelled
278    Cancelled,
279}
280
281/// AI task result
282#[derive(Debug)]
283pub struct AiTaskResult {
284    /// Successfully processed files
285    pub processed_files: Vec<PathBuf>,
286    /// Skipped files
287    pub skipped_files: Vec<(PathBuf, String)>,
288    /// Failed files
289    pub failed_files: Vec<(PathBuf, String)>,
290    /// Total duration
291    pub duration: Duration,
292    /// GPU statistics
293    pub gpu_stats: Option<GpuStats>,
294}
295
296/// GPU statistics
297#[derive(Debug, Clone)]
298pub struct GpuStats {
299    /// Peak VRAM usage (MB)
300    pub peak_vram_mb: u64,
301    /// Average GPU utilization
302    pub avg_utilization: f32,
303}
304
305/// AI tool types
306#[derive(Debug, Clone, Copy)]
307pub enum AiTool {
308    RealESRGAN,
309    YomiToku,
310}
311
312impl AiTool {
313    /// Get the module name for Python
314    #[must_use]
315    pub fn module_name(&self) -> &str {
316        match self {
317            AiTool::RealESRGAN => "realesrgan",
318            AiTool::YomiToku => "yomitoku",
319        }
320    }
321}
322
323/// AI Bridge trait
324pub trait AiBridge {
325    /// Initialize bridge
326    fn new(config: AiBridgeConfig) -> Result<Self>
327    where
328        Self: Sized;
329
330    /// Check if tool is available
331    fn check_tool(&self, tool: AiTool) -> Result<bool>;
332
333    /// Check GPU status
334    fn check_gpu(&self) -> Result<GpuStats>;
335
336    /// Execute task (sync)
337    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    /// Cancel running process
346    fn cancel(&self) -> Result<()>;
347}
348
349/// Subprocess-based bridge implementation
350pub struct SubprocessBridge {
351    config: AiBridgeConfig,
352}
353
354impl SubprocessBridge {
355    /// Create a new subprocess bridge
356    #[allow(clippy::redundant_clone)] // Clone needed due to partial move restrictions
357    pub fn new(config: AiBridgeConfig) -> Result<Self> {
358        // Check if venv exists or allow creation
359        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    /// Get the configuration
367    pub fn config(&self) -> &AiBridgeConfig {
368        &self.config
369    }
370
371    /// Get Python executable path
372    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    /// Check if a tool is available
381    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    /// Check GPU status
400    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    /// Execute AI tool
420    ///
421    /// # Arguments
422    /// * `tool` - The AI tool to execute
423    /// * `input_files` - Input file paths
424    /// * `output_dir` - Output directory for results
425    /// * `tool_options` - Tool-specific options (can be downcast to RealEsrganOptions, etc.)
426    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        // Get bridge script path (relative to venv parent directory)
437        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        // Check if bridge script exists
444        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            // Generate output filename based on input
458            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                        // Extract options from tool_options if available
475                        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                // Wait before retry
521                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    /// Execute a raw command with timeout
546    ///
547    /// This is a lower-level method for executing custom Python scripts
548    /// with arbitrary arguments and a configurable timeout.
549    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        // Wait for completion and check timeout
562        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    /// Cancel running process (placeholder)
586    pub fn cancel(&self) -> Result<()> {
587        // In a full implementation, this would track and kill running processes
588        Ok(())
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    // TC-AIB-001: ブリッジ初期化(設定デフォルト)
597    #[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    // TC-AIB-004: GPU状態確認(設定)
608    #[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    // TC-AIB-002: 仮想環境なしエラー
634    #[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    // Note: The following tests require actual Python environment and tools
686    // They are marked with #[ignore] until environment is available
687
688    #[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        // GPU may or may not be available
708        match result {
709            Ok(stats) => {
710                // Verify stats were retrieved successfully
711                eprintln!("GPU VRAM: {} MB", stats.peak_vram_mb);
712            }
713            Err(AiBridgeError::GpuNotAvailable) => {} // OK
714            Err(e) => panic!("Unexpected error: {:?}", e),
715        }
716    }
717
718    // TC-AIB-003: Tool availability check
719    #[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        // Test environment should have tools installed
733        assert!(realesrgan_available);
734        assert!(yomitoku_available);
735    }
736
737    // TC-AIB-005: Task execution (success)
738    #[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    // TC-AIB-006: Batch execution
764    #[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    // TC-AIB-007: Timeout handling
791    #[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), // Immediate timeout
797            ..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    // TC-AIB-008: Retry behavior
815    #[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        // Verify exponential backoff calculation
824        let base = config.retry_interval;
825        assert_eq!(base * 2_u32.pow(0), Duration::from_secs(1)); // 1st retry: 1s
826        assert_eq!(base * 2_u32.pow(1), Duration::from_secs(2)); // 2nd retry: 2s
827        assert_eq!(base * 2_u32.pow(2), Duration::from_secs(4)); // 3rd retry: 4s
828    }
829
830    // TC-AIB-010: Cancel operation
831    #[test]
832    fn test_cancel() {
833        let config = AiBridgeConfig {
834            venv_path: PathBuf::from("tests/fixtures/test_venv"),
835            ..Default::default()
836        };
837
838        // Create bridge even if venv doesn't exist for cancel test
839        if config.venv_path.exists() {
840            let bridge = SubprocessBridge::new(config).unwrap();
841            // Cancel should succeed even when nothing is running
842            assert!(bridge.cancel().is_ok());
843        }
844    }
845
846    // Test process status variants
847    #[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        // Verify all variants can be created
862        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 AiTaskResult construction
871    #[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 GpuStats construction
892    #[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 AiTaskResult without GPU stats
904    #[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 error display messages
919    #[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 log level variants
947    #[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 GpuConfig with max_vram
956    #[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 builder retry settings
970    #[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 RetryConfig interval
978    #[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 builder chaining
990    #[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 builder with gpu_config
1009    #[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 builder with retry_config
1027    #[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 ProcessStatus running progress
1043    #[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 ProcessStatus failed error message
1061    #[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 ProcessStatus completed duration
1075    #[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 all AiTool module names
1087    #[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 retry config without exponential backoff
1094    #[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    // TC-AIB-009: Async progress simulation
1108    #[test]
1109    fn test_progress_status_sequence() {
1110        // Simulate progress sequence as would be observed in async execution
1111        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        // Check progress increases monotonically
1127        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 RetriesExhausted error
1137    #[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 ToolNotInstalled error
1145    #[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 batch result with mixed outcomes
1153    #[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        // Verify counts
1170        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        // Verify error messages are preserved
1175        assert!(result.failed_files[0].1.contains("Corrupted"));
1176        assert!(result.failed_files[1].1.contains("memory"));
1177
1178        // Verify skip reason
1179        assert!(result.skipped_files[0].1.contains("Already"));
1180    }
1181
1182    // Test GPU config disabled
1183    #[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 GPU config with specific device
1191    #[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 exponential backoff calculation edge cases
1207    #[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        // Verify exponential growth
1216        let base = config.retry_interval;
1217        assert_eq!(base * 2_u32.pow(0), Duration::from_millis(100)); // 100ms
1218        assert_eq!(base * 2_u32.pow(1), Duration::from_millis(200)); // 200ms
1219        assert_eq!(base * 2_u32.pow(2), Duration::from_millis(400)); // 400ms
1220        assert_eq!(base * 2_u32.pow(3), Duration::from_millis(800)); // 800ms
1221        assert_eq!(base * 2_u32.pow(4), Duration::from_millis(1600)); // 1.6s
1222        assert_eq!(base * 2_u32.pow(5), Duration::from_millis(3200)); // 3.2s
1223    }
1224
1225    // Test AiTool variants completeness
1226    #[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 LogLevel default
1237    #[test]
1238    fn test_log_level_default() {
1239        let default_level = LogLevel::default();
1240        assert!(matches!(default_level, LogLevel::Info));
1241    }
1242
1243    // Test builder with all options
1244    #[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 ProcessStatus failed with high retry count
1264    #[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 GpuStats edge values
1278    #[test]
1279    fn test_gpu_stats_edge_values() {
1280        // Zero values
1281        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        // Maximum values
1289        let max_stats = GpuStats {
1290            peak_vram_mb: 48000, // 48GB
1291            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 config timeout variations
1298    #[test]
1299    fn test_config_timeout_variations() {
1300        // Very short timeout
1301        let short = AiBridgeConfig::builder()
1302            .timeout(Duration::from_millis(100))
1303            .build();
1304        assert_eq!(short.timeout, Duration::from_millis(100));
1305
1306        // Very long timeout (24 hours)
1307        let long = AiBridgeConfig::builder()
1308            .timeout(Duration::from_secs(86400))
1309            .build();
1310        assert_eq!(long.timeout, Duration::from_secs(86400));
1311    }
1312
1313    // Additional comprehensive tests
1314
1315    #[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        // Absolute paths
1492        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        // Relative paths
1502        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        // CPU only should have GPU disabled
1519        assert!(!cpu.gpu_config.enabled);
1520
1521        // Low VRAM should have smaller tile size than default
1522        assert!(low_vram.gpu_config.tile_size < default_config.gpu_config.tile_size);
1523
1524        // Low VRAM should have max_vram set
1525        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    // ==================== Boundary Value Tests ====================
1605
1606    #[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        // Negative progress is technically allowed by the type
1665        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        // Progress over 1.0 is technically allowed by the type
1674        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}