1use 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
13pub struct ErrorRecoveryManager {
15 error_stats: ErrorStatistics,
17
18 recovery_config: RecoveryConfig,
20
21 corruption_cache: HashMap<PathBuf, CorruptionStatus>,
23}
24
25#[derive(Debug, Clone, Default)]
27pub struct ErrorStatistics {
28 pub total_errors: u64,
30
31 pub errors_by_type: HashMap<String, u64>,
33
34 pub successful_recoveries: u64,
36
37 pub failed_recoveries: u64,
39
40 pub index_rebuilds: u64,
42
43 pub last_error_time: Option<SystemTime>,
45
46 pub error_rate: f64,
48}
49
50#[derive(Debug, Clone)]
52pub struct RecoveryConfig {
53 pub enable_auto_rebuild: bool,
55
56 pub max_retry_attempts: u32,
58
59 pub retry_delay: Duration,
61
62 pub enable_partial_results: bool,
64
65 pub corruption_threshold: f64,
67}
68
69#[derive(Debug, Clone)]
71struct CorruptionStatus {
72 is_corrupted: bool,
74
75 last_checked: SystemTime,
77
78 #[allow(dead_code)]
80 confidence: f64,
81}
82
83#[derive(Debug, Clone)]
85pub struct RecoveryResult<T> {
86 pub result: Option<T>,
88
89 pub success: bool,
91
92 pub strategy_used: RecoveryStrategy,
94
95 pub attempts_made: u32,
97
98 pub recovery_time: Duration,
100
101 pub partial_results: Vec<T>,
103}
104
105#[derive(Debug, Clone, PartialEq)]
107pub enum RecoveryStrategy {
108 Retry,
110
111 RebuildIndex,
113
114 FallbackToLegacy,
116
117 PartialResults,
119
120 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 pub fn new() -> Self {
145 Self::with_config(RecoveryConfig::default())
146 }
147
148 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 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 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 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 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 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 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 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, };
254
255 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 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 self.corruption_cache.remove(&binary_path.to_path_buf());
283
284 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 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 pub fn get_error_stats(&self) -> &ErrorStatistics {
312 &self.error_stats
313 }
314
315 pub fn get_error_stats_mut(&mut self) -> &mut ErrorStatistics {
317 &mut self.error_stats
318 }
319
320 pub fn reset_stats(&mut self) {
322 self.error_stats = ErrorStatistics::default();
323 info!("Error statistics reset");
324 }
325
326 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 pub fn update_config(&mut self, config: RecoveryConfig) {
348 self.recovery_config = config;
349 info!("Recovery configuration updated");
350 }
351
352 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 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#[derive(Debug, Clone)]
426pub struct ErrorReport {
427 pub total_errors: u64,
429
430 pub success_rate: f64,
432
433 pub most_common_error: Option<String>,
435
436 pub index_rebuilds: u64,
438
439 pub error_rate: f64,
441
442 pub recommendations: Vec<String>,
444}
445
446impl ErrorStatistics {
447 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 pub fn error_trend(&self) -> ErrorTrend {
459 if self.error_rate > 10.0 {
461 ErrorTrend::Increasing
462 } else if self.error_rate == 0.0 {
463 ErrorTrend::Stable } else if self.error_rate < 1.0 {
465 ErrorTrend::Decreasing
466 } else {
467 ErrorTrend::Stable
468 }
469 }
470}
471
472#[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), ..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); 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 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 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 assert_eq!(strategy, strategy);
609
610 let formatted = format!("{:?}", strategy);
612 assert!(!formatted.is_empty());
613 }
614 }
615
616 #[test]
617 fn test_binary_export_error_variants() {
618 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 let formatted = format!("{:?}", error);
630 assert!(!formatted.is_empty());
631
632 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 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 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 stats.successful_recoveries = 100;
657 stats.failed_recoveries = 0;
658 assert_eq!(stats.recovery_success_rate(), 1.0);
659
660 stats.successful_recoveries = 0;
662 stats.failed_recoveries = 100;
663 assert_eq!(stats.recovery_success_rate(), 0.0);
664
665 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); stats.error_rate = 5.0;
673 assert_eq!(stats.error_trend(), ErrorTrend::Stable);
674 }
675
676 #[test]
677 fn test_error_trend_variants() {
678 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 let extreme_config = RecoveryConfig {
696 enable_auto_rebuild: true,
697 max_retry_attempts: 0, retry_delay: Duration::from_millis(0), enable_partial_results: true,
700 corruption_threshold: 0.0, };
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 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, };
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 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 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 let version_result: RecoveryResult<String> = manager.attempt_recovery(
750 || Err(BinaryExportError::UnsupportedVersion(99)),
751 "version check",
752 );
753 assert!(!version_result.success);
754
755 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 assert!(!result.success);
780 assert_eq!(result.strategy_used, RecoveryStrategy::Retry); }
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()); }
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 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 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 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 assert!(success_count >= 2); let final_manager = manager.lock().expect("Failed to lock manager");
875 assert!(final_manager.error_stats.total_errors >= 2); }
877
878 #[test]
879 fn test_recovery_result_properties() {
880 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 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 assert_eq!(stats.recovery_success_rate(), 0.0);
921
922 stats.successful_recoveries = 10;
924 stats.failed_recoveries = 0;
925 assert_eq!(stats.recovery_success_rate(), 1.0);
926
927 stats.successful_recoveries = 0;
929 stats.failed_recoveries = 10;
930 assert_eq!(stats.recovery_success_rate(), 0.0);
931
932 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 let _result: RecoveryResult<String> =
947 manager.attempt_recovery(|| Err(BinaryExportError::InvalidFormat), "consistency test");
948
949 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 let _result2: RecoveryResult<String> =
956 manager.attempt_recovery(|| Ok("success".to_string()), "consistency test 2");
957
958 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 assert!(elapsed >= Duration::from_millis(20)); }
979}