1use 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
15pub struct ErrorRecoveryManager {
17 error_stats: ErrorStatistics,
19
20 recovery_config: RecoveryConfig,
22
23 corruption_cache: HashMap<PathBuf, CorruptionStatus>,
25}
26
27#[derive(Debug, Clone, Default)]
29pub struct ErrorStatistics {
30 pub total_errors: u64,
32
33 pub errors_by_type: HashMap<String, u64>,
35
36 pub successful_recoveries: u64,
38
39 pub failed_recoveries: u64,
41
42 pub index_rebuilds: u64,
44
45 pub last_error_time: Option<SystemTime>,
47
48 pub error_rate: f64,
50}
51
52#[derive(Debug, Clone)]
54pub struct RecoveryConfig {
55 pub enable_auto_rebuild: bool,
57
58 pub max_retry_attempts: u32,
60
61 pub retry_delay: Duration,
63
64 pub enable_partial_results: bool,
66
67 pub corruption_threshold: f64,
69}
70
71#[derive(Debug, Clone)]
73struct CorruptionStatus {
74 is_corrupted: bool,
76
77 last_checked: SystemTime,
79
80 #[allow(dead_code)]
82 confidence: f64,
83}
84
85#[derive(Debug, Clone)]
87pub struct RecoveryResult<T> {
88 pub result: Option<T>,
90
91 pub success: bool,
93
94 pub strategy_used: RecoveryStrategy,
96
97 pub attempts_made: u32,
99
100 pub recovery_time: Duration,
102
103 pub partial_results: Vec<T>,
105}
106
107#[derive(Debug, Clone, PartialEq)]
109pub enum RecoveryStrategy {
110 Retry,
112
113 RebuildIndex,
115
116 FallbackToLegacy,
118
119 PartialResults,
121
122 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 pub fn new() -> Self {
147 Self::with_config(RecoveryConfig::default())
148 }
149
150 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 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 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 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 self.error_stats.failed_recoveries += 1;
219 #[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 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 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 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, };
260
261 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 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 self.corruption_cache.remove(&binary_path.to_path_buf());
289
290 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 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 pub fn get_error_stats(&self) -> &ErrorStatistics {
324 &self.error_stats
325 }
326
327 pub fn get_error_stats_mut(&mut self) -> &mut ErrorStatistics {
329 &mut self.error_stats
330 }
331
332 pub fn reset_stats(&mut self) {
334 self.error_stats = ErrorStatistics::default();
335 info!("Error statistics reset");
336 }
337
338 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 pub fn update_config(&mut self, config: RecoveryConfig) {
360 self.recovery_config = config;
361 info!("Recovery configuration updated");
362 }
363
364 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 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#[derive(Debug, Clone)]
438pub struct ErrorReport {
439 pub total_errors: u64,
441
442 pub success_rate: f64,
444
445 pub most_common_error: Option<String>,
447
448 pub index_rebuilds: u64,
450
451 pub error_rate: f64,
453
454 pub recommendations: Vec<String>,
456}
457
458impl ErrorStatistics {
459 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 pub fn error_trend(&self) -> ErrorTrend {
471 if self.error_rate > 10.0 {
473 ErrorTrend::Increasing
474 } else if self.error_rate == 0.0 {
475 ErrorTrend::Stable } else if self.error_rate < 1.0 {
477 ErrorTrend::Decreasing
478 } else {
479 ErrorTrend::Stable
480 }
481 }
482}
483
484#[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), ..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); 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 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 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 assert_eq!(strategy, strategy);
621
622 let formatted = format!("{:?}", strategy);
624 assert!(!formatted.is_empty());
625 }
626 }
627
628 #[test]
629 fn test_binary_export_error_variants() {
630 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 let formatted = format!("{:?}", error);
642 assert!(!formatted.is_empty());
643
644 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 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 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 stats.successful_recoveries = 100;
669 stats.failed_recoveries = 0;
670 assert_eq!(stats.recovery_success_rate(), 1.0);
671
672 stats.successful_recoveries = 0;
674 stats.failed_recoveries = 100;
675 assert_eq!(stats.recovery_success_rate(), 0.0);
676
677 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); stats.error_rate = 5.0;
685 assert_eq!(stats.error_trend(), ErrorTrend::Stable);
686 }
687
688 #[test]
689 fn test_error_trend_variants() {
690 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 let extreme_config = RecoveryConfig {
708 enable_auto_rebuild: true,
709 max_retry_attempts: 0, retry_delay: Duration::from_millis(0), enable_partial_results: true,
712 corruption_threshold: 0.0, };
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 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, };
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 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 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 let version_result: RecoveryResult<String> = manager.attempt_recovery(
762 || Err(BinaryExportError::UnsupportedVersion(99)),
763 "version check",
764 );
765 assert!(!version_result.success);
766
767 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 assert!(!result.success);
792 assert_eq!(result.strategy_used, RecoveryStrategy::Retry); }
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()); }
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 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 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 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 assert!(success_count >= 2); let final_manager = manager.lock().expect("Failed to lock manager");
887 assert!(final_manager.error_stats.total_errors >= 2); }
889
890 #[test]
891 fn test_recovery_result_properties() {
892 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 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 assert_eq!(stats.recovery_success_rate(), 0.0);
933
934 stats.successful_recoveries = 10;
936 stats.failed_recoveries = 0;
937 assert_eq!(stats.recovery_success_rate(), 1.0);
938
939 stats.successful_recoveries = 0;
941 stats.failed_recoveries = 10;
942 assert_eq!(stats.recovery_success_rate(), 0.0);
943
944 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 let _result: RecoveryResult<String> =
959 manager.attempt_recovery(|| Err(BinaryExportError::InvalidFormat), "consistency test");
960
961 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 let _result2: RecoveryResult<String> =
968 manager.attempt_recovery(|| Ok("success".to_string()), "consistency test 2");
969
970 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 assert!(elapsed >= Duration::from_millis(20)); }
991}