memscope_rs/export/binary/
error_recovery.rs

1//! Simplified error recovery and diagnostic system
2//!
3//! This module provides essential error handling enhancements for the binary-to-JSON
4//! optimization system, focusing on the most critical recovery mechanisms without
5//! over-engineering.
6
7use crate::export::binary::{BinaryExportError, BinaryIndex, BinaryIndexBuilder};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10use std::time::{Duration, Instant, SystemTime};
11use tracing::{debug, error, info, warn};
12
13/// Simplified error recovery manager
14pub struct ErrorRecoveryManager {
15    /// Error statistics for trend analysis
16    error_stats: ErrorStatistics,
17
18    /// Recovery strategies configuration
19    recovery_config: RecoveryConfig,
20
21    /// Index corruption detection cache
22    corruption_cache: HashMap<PathBuf, CorruptionStatus>,
23}
24
25/// Error statistics for basic trend analysis
26#[derive(Debug, Clone, Default)]
27pub struct ErrorStatistics {
28    /// Total errors encountered
29    pub total_errors: u64,
30
31    /// Errors by type
32    pub errors_by_type: HashMap<String, u64>,
33
34    /// Successful recoveries
35    pub successful_recoveries: u64,
36
37    /// Failed recoveries
38    pub failed_recoveries: u64,
39
40    /// Index rebuilds performed
41    pub index_rebuilds: u64,
42
43    /// Last error timestamp
44    pub last_error_time: Option<SystemTime>,
45
46    /// Error rate (errors per hour)
47    pub error_rate: f64,
48}
49
50/// Recovery configuration
51#[derive(Debug, Clone)]
52pub struct RecoveryConfig {
53    /// Enable automatic index rebuilding
54    pub enable_auto_rebuild: bool,
55
56    /// Maximum retry attempts
57    pub max_retry_attempts: u32,
58
59    /// Retry delay
60    pub retry_delay: Duration,
61
62    /// Enable partial result returns
63    pub enable_partial_results: bool,
64
65    /// Corruption detection threshold
66    pub corruption_threshold: f64,
67}
68
69/// Index corruption status
70#[derive(Debug, Clone)]
71struct CorruptionStatus {
72    /// Whether the index is corrupted
73    is_corrupted: bool,
74
75    /// Last check timestamp
76    last_checked: SystemTime,
77
78    /// Corruption confidence (0.0 to 1.0)
79    #[allow(dead_code)]
80    confidence: f64,
81}
82
83/// Recovery attempt result
84#[derive(Debug, Clone)]
85pub struct RecoveryResult<T> {
86    /// The recovered result (if any)
87    pub result: Option<T>,
88
89    /// Whether recovery was successful
90    pub success: bool,
91
92    /// Recovery strategy used
93    pub strategy_used: RecoveryStrategy,
94
95    /// Number of attempts made
96    pub attempts_made: u32,
97
98    /// Time taken for recovery
99    pub recovery_time: Duration,
100
101    /// Partial results (if enabled)
102    pub partial_results: Vec<T>,
103}
104
105/// Recovery strategies
106#[derive(Debug, Clone, PartialEq)]
107pub enum RecoveryStrategy {
108    /// Retry the operation
109    Retry,
110
111    /// Rebuild corrupted index
112    RebuildIndex,
113
114    /// Fall back to legacy method
115    FallbackToLegacy,
116
117    /// Return partial results
118    PartialResults,
119
120    /// Skip corrupted records
121    SkipCorrupted,
122}
123
124impl Default for RecoveryConfig {
125    fn default() -> Self {
126        Self {
127            enable_auto_rebuild: true,
128            max_retry_attempts: 3,
129            retry_delay: Duration::from_millis(100),
130            enable_partial_results: true,
131            corruption_threshold: 0.7,
132        }
133    }
134}
135
136impl Default for ErrorRecoveryManager {
137    fn default() -> Self {
138        Self::new()
139    }
140}
141
142impl ErrorRecoveryManager {
143    /// Create a new error recovery manager
144    pub fn new() -> Self {
145        Self::with_config(RecoveryConfig::default())
146    }
147
148    /// Create a new error recovery manager with custom configuration
149    pub fn with_config(config: RecoveryConfig) -> Self {
150        Self {
151            error_stats: ErrorStatistics::default(),
152            recovery_config: config,
153            corruption_cache: HashMap::new(),
154        }
155    }
156
157    /// Attempt to recover from a binary export error
158    pub fn attempt_recovery<T, F>(&mut self, operation: F, context: &str) -> RecoveryResult<T>
159    where
160        F: Fn() -> Result<T, BinaryExportError>,
161    {
162        let start_time = Instant::now();
163        let mut attempts = 0;
164        let partial_results = Vec::new();
165
166        info!("Starting error recovery for: {}", context);
167
168        // First attempt - try the operation as-is
169        attempts += 1;
170        match operation() {
171            Ok(result) => {
172                self.error_stats.successful_recoveries += 1;
173                return RecoveryResult {
174                    result: Some(result),
175                    success: true,
176                    strategy_used: RecoveryStrategy::Retry,
177                    attempts_made: attempts,
178                    recovery_time: start_time.elapsed(),
179                    partial_results,
180                };
181            }
182            Err(error) => {
183                self.record_error(&error, context);
184                debug!("Initial attempt failed: {}", error);
185            }
186        }
187
188        // Retry with delay
189        for attempt in 1..=self.recovery_config.max_retry_attempts {
190            attempts += 1;
191
192            std::thread::sleep(self.recovery_config.retry_delay);
193
194            match operation() {
195                Ok(result) => {
196                    self.error_stats.successful_recoveries += 1;
197                    info!("Recovery successful after {} attempts", attempts);
198
199                    return RecoveryResult {
200                        result: Some(result),
201                        success: true,
202                        strategy_used: RecoveryStrategy::Retry,
203                        attempts_made: attempts,
204                        recovery_time: start_time.elapsed(),
205                        partial_results,
206                    };
207                }
208                Err(error) => {
209                    warn!("Retry attempt {} failed: {}", attempt, error);
210                    self.record_error(&error, context);
211                }
212            }
213        }
214
215        // All retries failed
216        self.error_stats.failed_recoveries += 1;
217        error!("All recovery attempts failed for: {}", context);
218
219        RecoveryResult {
220            result: None,
221            success: false,
222            strategy_used: RecoveryStrategy::Retry,
223            attempts_made: attempts,
224            recovery_time: start_time.elapsed(),
225            partial_results,
226        }
227    }
228
229    /// Detect and handle index corruption
230    pub fn check_index_corruption<P: AsRef<Path>>(
231        &mut self,
232        binary_path: P,
233    ) -> Result<bool, BinaryExportError> {
234        let binary_path = binary_path.as_ref();
235        let path_buf = binary_path.to_path_buf();
236
237        // Check cache first
238        if let Some(status) = self.corruption_cache.get(&path_buf) {
239            if status.last_checked.elapsed().unwrap_or(Duration::MAX) < Duration::from_secs(300) {
240                return Ok(status.is_corrupted);
241            }
242        }
243
244        debug!("Checking index corruption for: {:?}", binary_path);
245
246        // Simple corruption detection - try to build index
247        let builder = BinaryIndexBuilder::new();
248        let corruption_detected = match builder.build_index(binary_path) {
249            Ok(_) => false,
250            Err(BinaryExportError::CorruptedData(_)) => true,
251            Err(BinaryExportError::InvalidFormat) => true,
252            Err(_) => false, // Other errors are not corruption
253        };
254
255        // Update cache
256        self.corruption_cache.insert(
257            path_buf,
258            CorruptionStatus {
259                is_corrupted: corruption_detected,
260                last_checked: SystemTime::now(),
261                confidence: if corruption_detected { 0.9 } else { 0.1 },
262            },
263        );
264
265        if corruption_detected {
266            warn!("Index corruption detected for: {:?}", binary_path);
267        }
268
269        Ok(corruption_detected)
270    }
271
272    /// Attempt to rebuild a corrupted index
273    pub fn rebuild_index<P: AsRef<Path>>(
274        &mut self,
275        binary_path: P,
276    ) -> Result<BinaryIndex, BinaryExportError> {
277        let binary_path = binary_path.as_ref();
278
279        info!("Attempting to rebuild index for: {:?}", binary_path);
280
281        // Clear corruption cache for this file
282        self.corruption_cache.remove(&binary_path.to_path_buf());
283
284        // Attempt to rebuild
285        let builder = BinaryIndexBuilder::new();
286        match builder.build_index(binary_path) {
287            Ok(index) => {
288                self.error_stats.index_rebuilds += 1;
289                info!("Index successfully rebuilt for: {:?}", binary_path);
290
291                // Update cache with success
292                self.corruption_cache.insert(
293                    binary_path.to_path_buf(),
294                    CorruptionStatus {
295                        is_corrupted: false,
296                        last_checked: SystemTime::now(),
297                        confidence: 0.1,
298                    },
299                );
300
301                Ok(index)
302            }
303            Err(error) => {
304                error!("Failed to rebuild index for: {:?} - {}", binary_path, error);
305                Err(error)
306            }
307        }
308    }
309
310    /// Get error statistics
311    pub fn get_error_stats(&self) -> &ErrorStatistics {
312        &self.error_stats
313    }
314
315    /// Get mutable error statistics (for internal use)
316    pub fn get_error_stats_mut(&mut self) -> &mut ErrorStatistics {
317        &mut self.error_stats
318    }
319
320    /// Reset error statistics
321    pub fn reset_stats(&mut self) {
322        self.error_stats = ErrorStatistics::default();
323        info!("Error statistics reset");
324    }
325
326    /// Generate error report
327    pub fn generate_error_report(&self) -> ErrorReport {
328        let total_operations =
329            self.error_stats.successful_recoveries + self.error_stats.failed_recoveries;
330        let success_rate = if total_operations > 0 {
331            self.error_stats.successful_recoveries as f64 / total_operations as f64
332        } else {
333            0.0
334        };
335
336        ErrorReport {
337            total_errors: self.error_stats.total_errors,
338            success_rate,
339            most_common_error: self.get_most_common_error(),
340            index_rebuilds: self.error_stats.index_rebuilds,
341            error_rate: self.error_stats.error_rate,
342            recommendations: self.generate_recommendations(),
343        }
344    }
345
346    /// Update recovery configuration
347    pub fn update_config(&mut self, config: RecoveryConfig) {
348        self.recovery_config = config;
349        info!("Recovery configuration updated");
350    }
351
352    // Private helper methods
353
354    fn record_error(&mut self, error: &BinaryExportError, context: &str) {
355        self.error_stats.total_errors += 1;
356        self.error_stats.last_error_time = Some(SystemTime::now());
357
358        let error_type = match error {
359            BinaryExportError::Io(_) => "IO",
360            BinaryExportError::InvalidFormat => "InvalidFormat",
361            BinaryExportError::UnsupportedVersion(_) => "UnsupportedVersion",
362            BinaryExportError::CorruptedData(_) => "CorruptedData",
363            BinaryExportError::InvalidMagic { .. } => "InvalidMagic",
364            BinaryExportError::SerializationError(_) => "Serialization",
365            BinaryExportError::CompressionError(_) => "Compression",
366        };
367
368        *self
369            .error_stats
370            .errors_by_type
371            .entry(error_type.to_string())
372            .or_insert(0) += 1;
373
374        debug!("Recorded error: {} in context: {}", error_type, context);
375    }
376
377    fn get_most_common_error(&self) -> Option<String> {
378        self.error_stats
379            .errors_by_type
380            .iter()
381            .max_by_key(|(_, &count)| count)
382            .map(|(error_type, _)| error_type.clone())
383    }
384
385    fn generate_recommendations(&self) -> Vec<String> {
386        let mut recommendations = Vec::new();
387
388        if self.error_stats.total_errors > 10 {
389            recommendations
390                .push("High error rate detected. Consider checking file integrity.".to_string());
391        }
392
393        if self.error_stats.index_rebuilds > 3 {
394            recommendations.push(
395                "Frequent index rebuilds detected. Consider checking storage reliability."
396                    .to_string(),
397            );
398        }
399
400        if let Some(most_common) = self.get_most_common_error() {
401            match most_common.as_str() {
402                "IO" => recommendations
403                    .push("IO errors are common. Check disk space and permissions.".to_string()),
404                "CorruptedData" => recommendations
405                    .push("Data corruption detected. Consider backup verification.".to_string()),
406                "InvalidFormat" => recommendations
407                    .push("Format errors detected. Ensure file compatibility.".to_string()),
408                _ => {}
409            }
410        }
411
412        // Always provide at least one general recommendation if no specific ones were generated
413        if recommendations.is_empty() {
414            recommendations.push(
415                "Monitor error rates and enable detailed logging for better diagnostics."
416                    .to_string(),
417            );
418        }
419
420        recommendations
421    }
422}
423
424/// Error report summary
425#[derive(Debug, Clone)]
426pub struct ErrorReport {
427    /// Total errors encountered
428    pub total_errors: u64,
429
430    /// Recovery success rate
431    pub success_rate: f64,
432
433    /// Most common error type
434    pub most_common_error: Option<String>,
435
436    /// Number of index rebuilds
437    pub index_rebuilds: u64,
438
439    /// Error rate per hour
440    pub error_rate: f64,
441
442    /// Recommendations for improvement
443    pub recommendations: Vec<String>,
444}
445
446impl ErrorStatistics {
447    /// Calculate recovery success rate
448    pub fn recovery_success_rate(&self) -> f64 {
449        let total = self.successful_recoveries + self.failed_recoveries;
450        if total > 0 {
451            self.successful_recoveries as f64 / total as f64
452        } else {
453            0.0
454        }
455    }
456
457    /// Get error trend (increasing/decreasing)
458    pub fn error_trend(&self) -> ErrorTrend {
459        // Simplified trend analysis based on recent error rate
460        if self.error_rate > 10.0 {
461            ErrorTrend::Increasing
462        } else if self.error_rate == 0.0 {
463            ErrorTrend::Stable // No errors means stable, not decreasing
464        } else if self.error_rate < 1.0 {
465            ErrorTrend::Decreasing
466        } else {
467            ErrorTrend::Stable
468        }
469    }
470}
471
472/// Error trend analysis
473#[derive(Debug, Clone, PartialEq)]
474pub enum ErrorTrend {
475    Increasing,
476    Stable,
477    Decreasing,
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483
484    #[test]
485    fn test_error_recovery_manager_creation() {
486        let manager = ErrorRecoveryManager::new();
487        assert_eq!(manager.error_stats.total_errors, 0);
488        assert!(manager.recovery_config.enable_auto_rebuild);
489    }
490
491    #[test]
492    fn test_recovery_config() {
493        let config = RecoveryConfig {
494            enable_auto_rebuild: false,
495            max_retry_attempts: 5,
496            retry_delay: Duration::from_millis(200),
497            enable_partial_results: false,
498            corruption_threshold: 0.8,
499        };
500
501        let manager = ErrorRecoveryManager::with_config(config.clone());
502        assert_eq!(manager.recovery_config.max_retry_attempts, 5);
503        assert!(!manager.recovery_config.enable_auto_rebuild);
504    }
505
506    #[test]
507    fn test_successful_recovery() {
508        let mut manager = ErrorRecoveryManager::new();
509        let attempt_count = std::sync::Arc::new(std::sync::Mutex::new(0));
510        let attempt_count_clone = attempt_count.clone();
511
512        let result: RecoveryResult<String> = manager.attempt_recovery(
513            || {
514                let mut count = attempt_count_clone.lock().expect("Failed to acquire lock");
515                *count += 1;
516                if *count < 2 {
517                    Err(BinaryExportError::InvalidFormat)
518                } else {
519                    Ok("success".to_string())
520                }
521            },
522            "test operation",
523        );
524
525        assert!(result.success);
526        assert_eq!(result.result, Some("success".to_string()));
527        assert_eq!(result.attempts_made, 2);
528        assert_eq!(result.strategy_used, RecoveryStrategy::Retry);
529    }
530
531    #[test]
532    fn test_failed_recovery() {
533        let mut manager = ErrorRecoveryManager::with_config(RecoveryConfig {
534            max_retry_attempts: 2,
535            retry_delay: Duration::from_millis(1), // Fast test
536            ..Default::default()
537        });
538
539        let result: RecoveryResult<String> =
540            manager.attempt_recovery(|| Err(BinaryExportError::InvalidFormat), "test operation");
541
542        assert!(!result.success);
543        assert_eq!(result.result, None);
544        assert_eq!(result.attempts_made, 3); // Initial + 2 retries
545        assert_eq!(manager.error_stats.total_errors, 3);
546        assert_eq!(manager.error_stats.failed_recoveries, 1);
547    }
548
549    #[test]
550    fn test_error_statistics() {
551        let stats = ErrorStatistics {
552            successful_recoveries: 8,
553            failed_recoveries: 2,
554            error_rate: 5.0,
555            ..Default::default()
556        };
557
558        assert_eq!(stats.recovery_success_rate(), 0.8);
559        assert_eq!(stats.error_trend(), ErrorTrend::Stable);
560    }
561
562    #[test]
563    fn test_error_report_generation() {
564        let mut manager = ErrorRecoveryManager::new();
565
566        // Simulate some errors
567        manager.error_stats.total_errors = 10;
568        manager.error_stats.successful_recoveries = 8;
569        manager.error_stats.failed_recoveries = 2;
570        manager.error_stats.index_rebuilds = 1;
571        manager
572            .error_stats
573            .errors_by_type
574            .insert("IO".to_string(), 6);
575        manager
576            .error_stats
577            .errors_by_type
578            .insert("CorruptedData".to_string(), 4);
579
580        let report = manager.generate_error_report();
581
582        assert_eq!(report.total_errors, 10);
583        assert_eq!(report.success_rate, 0.8);
584        assert_eq!(report.most_common_error, Some("IO".to_string()));
585        assert_eq!(report.index_rebuilds, 1);
586        assert!(!report.recommendations.is_empty());
587    }
588
589    #[test]
590    fn test_recovery_strategies() {
591        assert_eq!(RecoveryStrategy::Retry, RecoveryStrategy::Retry);
592        assert_ne!(RecoveryStrategy::Retry, RecoveryStrategy::RebuildIndex);
593    }
594
595    #[test]
596    fn test_all_recovery_strategies() {
597        // Test all recovery strategy variants
598        let strategies = vec![
599            RecoveryStrategy::Retry,
600            RecoveryStrategy::RebuildIndex,
601            RecoveryStrategy::FallbackToLegacy,
602            RecoveryStrategy::PartialResults,
603            RecoveryStrategy::SkipCorrupted,
604        ];
605
606        for strategy in strategies {
607            // Each strategy should be equal to itself
608            assert_eq!(strategy, strategy);
609
610            // Test that we can format the strategy
611            let formatted = format!("{:?}", strategy);
612            assert!(!formatted.is_empty());
613        }
614    }
615
616    #[test]
617    fn test_binary_export_error_variants() {
618        // Test all BinaryExportError variants
619        let errors = vec![
620            BinaryExportError::InvalidFormat,
621            BinaryExportError::CorruptedData("test".to_string()),
622            BinaryExportError::Io(std::io::Error::new(std::io::ErrorKind::NotFound, "test")),
623            BinaryExportError::UnsupportedVersion(1),
624            BinaryExportError::SerializationError("test".to_string()),
625        ];
626
627        for error in errors {
628            // Each error should be formattable
629            let formatted = format!("{:?}", error);
630            assert!(!formatted.is_empty());
631
632            // Test error display
633            let display = format!("{}", error);
634            assert!(!display.is_empty());
635        }
636    }
637
638    #[test]
639    fn test_error_statistics_comprehensive() {
640        let mut stats = ErrorStatistics::default();
641
642        // Test initial state
643        assert_eq!(stats.total_errors, 0);
644        assert_eq!(stats.successful_recoveries, 0);
645        assert_eq!(stats.failed_recoveries, 0);
646        assert_eq!(stats.recovery_success_rate(), 0.0);
647        assert_eq!(stats.error_trend(), ErrorTrend::Stable);
648
649        // Test with various scenarios
650        stats.total_errors = 100;
651        stats.successful_recoveries = 80;
652        stats.failed_recoveries = 20;
653        assert_eq!(stats.recovery_success_rate(), 0.8);
654
655        // Test perfect success rate
656        stats.successful_recoveries = 100;
657        stats.failed_recoveries = 0;
658        assert_eq!(stats.recovery_success_rate(), 1.0);
659
660        // Test zero success rate
661        stats.successful_recoveries = 0;
662        stats.failed_recoveries = 100;
663        assert_eq!(stats.recovery_success_rate(), 0.0);
664
665        // Test error trend calculation
666        stats.error_rate = 15.0;
667        assert_eq!(stats.error_trend(), ErrorTrend::Increasing);
668
669        stats.error_rate = 0.5;
670        assert_eq!(stats.error_trend(), ErrorTrend::Decreasing); // Very low error rate is decreasing
671
672        stats.error_rate = 5.0;
673        assert_eq!(stats.error_trend(), ErrorTrend::Stable);
674    }
675
676    #[test]
677    fn test_error_trend_variants() {
678        // Test all ErrorTrend variants
679        let trends = vec![
680            ErrorTrend::Increasing,
681            ErrorTrend::Decreasing,
682            ErrorTrend::Stable,
683        ];
684
685        for trend in trends {
686            assert_eq!(trend, trend);
687            let formatted = format!("{:?}", trend);
688            assert!(!formatted.is_empty());
689        }
690    }
691
692    #[test]
693    fn test_recovery_config_edge_cases() {
694        // Test with extreme values
695        let extreme_config = RecoveryConfig {
696            enable_auto_rebuild: true,
697            max_retry_attempts: 0,                 // No retries
698            retry_delay: Duration::from_millis(0), // No delay
699            enable_partial_results: true,
700            corruption_threshold: 0.0, // Very strict
701        };
702
703        let manager = ErrorRecoveryManager::with_config(extreme_config);
704        assert_eq!(manager.recovery_config.max_retry_attempts, 0);
705        assert_eq!(manager.recovery_config.corruption_threshold, 0.0);
706
707        // Test with maximum values
708        let max_config = RecoveryConfig {
709            enable_auto_rebuild: false,
710            max_retry_attempts: 1000,
711            retry_delay: Duration::from_secs(60),
712            enable_partial_results: false,
713            corruption_threshold: 1.0, // Very lenient
714        };
715
716        let manager2 = ErrorRecoveryManager::with_config(max_config);
717        assert_eq!(manager2.recovery_config.max_retry_attempts, 1000);
718        assert_eq!(manager2.recovery_config.corruption_threshold, 1.0);
719    }
720
721    #[test]
722    fn test_recovery_with_different_error_types() {
723        let mut manager = ErrorRecoveryManager::new();
724
725        // Test recovery with IO error
726        let io_result: RecoveryResult<String> = manager.attempt_recovery(
727            || {
728                Err(BinaryExportError::Io(std::io::Error::new(
729                    std::io::ErrorKind::PermissionDenied,
730                    "access denied",
731                )))
732            },
733            "IO operation",
734        );
735        assert!(!io_result.success);
736
737        // Test recovery with corrupted data
738        let corruption_result: RecoveryResult<String> = manager.attempt_recovery(
739            || {
740                Err(BinaryExportError::CorruptedData(
741                    "test corruption".to_string(),
742                ))
743            },
744            "data read operation",
745        );
746        assert!(!corruption_result.success);
747
748        // Test recovery with version mismatch
749        let version_result: RecoveryResult<String> = manager.attempt_recovery(
750            || Err(BinaryExportError::UnsupportedVersion(99)),
751            "version check",
752        );
753        assert!(!version_result.success);
754
755        // Verify error statistics were updated
756        assert!(manager.error_stats.total_errors >= 3);
757        assert!(manager.error_stats.failed_recoveries >= 3);
758    }
759
760    #[test]
761    fn test_recovery_with_partial_results_enabled() {
762        let config = RecoveryConfig {
763            enable_partial_results: true,
764            max_retry_attempts: 1,
765            ..Default::default()
766        };
767        let mut manager = ErrorRecoveryManager::with_config(config);
768
769        let result: RecoveryResult<Vec<String>> = manager.attempt_recovery(
770            || {
771                Err(BinaryExportError::SerializationError(
772                    "test error".to_string(),
773                ))
774            },
775            "partial data operation",
776        );
777
778        // Even though it failed, partial results might be enabled
779        assert!(!result.success);
780        assert_eq!(result.strategy_used, RecoveryStrategy::Retry); // Default strategy
781    }
782
783    #[test]
784    fn test_error_report_with_empty_statistics() {
785        let manager = ErrorRecoveryManager::new();
786        let report = manager.generate_error_report();
787
788        assert_eq!(report.total_errors, 0);
789        assert_eq!(report.success_rate, 0.0);
790        assert_eq!(report.most_common_error, None);
791        assert_eq!(report.index_rebuilds, 0);
792        assert!(!report.recommendations.is_empty()); // Should still have general recommendations
793    }
794
795    #[test]
796    fn test_error_report_with_single_error_type() {
797        let mut manager = ErrorRecoveryManager::new();
798        manager.error_stats.total_errors = 5;
799        manager.error_stats.successful_recoveries = 3;
800        manager.error_stats.failed_recoveries = 2;
801        manager
802            .error_stats
803            .errors_by_type
804            .insert("SingleError".to_string(), 5);
805
806        let report = manager.generate_error_report();
807        assert_eq!(report.most_common_error, Some("SingleError".to_string()));
808        assert_eq!(report.success_rate, 0.6);
809    }
810
811    #[test]
812    fn test_error_report_recommendations() {
813        let mut manager = ErrorRecoveryManager::new();
814
815        // Simulate high failure rate
816        manager.error_stats.total_errors = 100;
817        manager.error_stats.successful_recoveries = 10;
818        manager.error_stats.failed_recoveries = 90;
819        manager
820            .error_stats
821            .errors_by_type
822            .insert("IO".to_string(), 80);
823        manager
824            .error_stats
825            .errors_by_type
826            .insert("CorruptedData".to_string(), 20);
827
828        let report = manager.generate_error_report();
829        assert!(!report.recommendations.is_empty());
830
831        // Should contain recommendations based on error patterns
832        let recommendations_text = report.recommendations.join(" ");
833        assert!(!recommendations_text.is_empty());
834    }
835
836    #[test]
837    fn test_concurrent_error_recovery() {
838        use std::sync::{Arc, Mutex};
839        use std::thread;
840
841        let manager = Arc::new(Mutex::new(ErrorRecoveryManager::new()));
842        let mut handles = vec![];
843
844        // Test concurrent recovery attempts
845        for i in 0..5 {
846            let manager_clone = manager.clone();
847            let handle = thread::spawn(move || {
848                let mut mgr = manager_clone.lock().expect("Failed to lock manager");
849                let result: RecoveryResult<String> = mgr.attempt_recovery(
850                    || {
851                        if i % 2 == 0 {
852                            Ok(format!("success_{}", i))
853                        } else {
854                            Err(BinaryExportError::InvalidFormat)
855                        }
856                    },
857                    &format!("operation_{}", i),
858                );
859                result.success
860            });
861            handles.push(handle);
862        }
863
864        let mut success_count = 0;
865        for handle in handles {
866            if handle.join().expect("Thread should complete") {
867                success_count += 1;
868            }
869        }
870
871        // Should have some successes and some failures
872        assert!(success_count >= 2); // At least the even-numbered operations should succeed
873
874        let final_manager = manager.lock().expect("Failed to lock manager");
875        assert!(final_manager.error_stats.total_errors >= 2); // At least the odd-numbered operations should fail
876    }
877
878    #[test]
879    fn test_recovery_result_properties() {
880        // Test successful recovery result
881        let success_result = RecoveryResult {
882            result: Some("test_data".to_string()),
883            success: true,
884            strategy_used: RecoveryStrategy::Retry,
885            attempts_made: 2,
886            recovery_time: Duration::from_millis(100),
887            partial_results: vec![],
888        };
889
890        assert!(success_result.success);
891        assert_eq!(success_result.result, Some("test_data".to_string()));
892        assert_eq!(success_result.attempts_made, 2);
893        assert_eq!(success_result.strategy_used, RecoveryStrategy::Retry);
894
895        // Test failed recovery result
896        let failed_result: RecoveryResult<String> = RecoveryResult {
897            result: None,
898            success: false,
899            strategy_used: RecoveryStrategy::RebuildIndex,
900            attempts_made: 3,
901            recovery_time: Duration::from_millis(500),
902            partial_results: vec![],
903        };
904
905        assert!(!failed_result.success);
906        assert_eq!(failed_result.result, None);
907        assert_eq!(failed_result.attempts_made, 3);
908        assert_eq!(failed_result.strategy_used, RecoveryStrategy::RebuildIndex);
909    }
910
911    #[test]
912    fn test_error_statistics_edge_cases() {
913        let mut stats = ErrorStatistics {
914            successful_recoveries: 0,
915            failed_recoveries: 0,
916            ..Default::default()
917        };
918
919        // Test division by zero scenarios
920        assert_eq!(stats.recovery_success_rate(), 0.0);
921
922        // Test with only successful recoveries
923        stats.successful_recoveries = 10;
924        stats.failed_recoveries = 0;
925        assert_eq!(stats.recovery_success_rate(), 1.0);
926
927        // Test with only failed recoveries
928        stats.successful_recoveries = 0;
929        stats.failed_recoveries = 10;
930        assert_eq!(stats.recovery_success_rate(), 0.0);
931
932        // Test error trend edge cases
933        stats.error_rate = 0.0;
934        assert_eq!(stats.error_trend(), ErrorTrend::Stable);
935
936        stats.error_rate = 100.0;
937        assert_eq!(stats.error_trend(), ErrorTrend::Increasing);
938    }
939
940    #[test]
941    fn test_recovery_manager_state_consistency() {
942        let mut manager = ErrorRecoveryManager::new();
943        let initial_errors = manager.error_stats.total_errors;
944
945        // Perform a failed recovery
946        let _result: RecoveryResult<String> =
947            manager.attempt_recovery(|| Err(BinaryExportError::InvalidFormat), "consistency test");
948
949        // Verify state was updated consistently
950        assert!(manager.error_stats.total_errors > initial_errors);
951        assert_eq!(manager.error_stats.failed_recoveries, 1);
952        assert_eq!(manager.error_stats.successful_recoveries, 0);
953
954        // Perform a successful recovery
955        let _result2: RecoveryResult<String> =
956            manager.attempt_recovery(|| Ok("success".to_string()), "consistency test 2");
957
958        // Verify state was updated consistently
959        assert_eq!(manager.error_stats.successful_recoveries, 1);
960        assert_eq!(manager.error_stats.failed_recoveries, 1);
961    }
962
963    #[test]
964    fn test_recovery_timing() {
965        let mut manager = ErrorRecoveryManager::with_config(RecoveryConfig {
966            retry_delay: Duration::from_millis(10),
967            max_retry_attempts: 2,
968            ..Default::default()
969        });
970
971        let start_time = std::time::Instant::now();
972        let _result: RecoveryResult<String> =
973            manager.attempt_recovery(|| Err(BinaryExportError::InvalidFormat), "timing test");
974        let elapsed = start_time.elapsed();
975
976        // Should have taken at least the retry delays
977        assert!(elapsed >= Duration::from_millis(20)); // 2 retries * 10ms delay
978    }
979}