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