use log;
use std::panic::{self, AssertUnwindSafe};
#[derive(Debug)]
pub enum PageResult<T> {
Ok(T),
Failed {
page_index: usize,
error: String,
},
}
pub fn process_pages_with_recovery<T, F>(pages: Vec<T>, stage_name: &str, mut op: F) -> Vec<T>
where
T: Send + Default + 'static,
F: FnMut(T) -> T,
{
let mut results = Vec::with_capacity(pages.len());
for (idx, page) in pages.into_iter().enumerate() {
let result = panic::catch_unwind(AssertUnwindSafe(|| op(page)));
match result {
Ok(processed) => {
results.push(processed);
}
Err(e) => {
let msg = if let Some(s) = e.downcast_ref::<&str>() {
s.to_string()
} else if let Some(s) = e.downcast_ref::<String>() {
s.clone()
} else {
"unknown panic".to_string()
};
log::error!(
"Stage '{}' failed on page {}: {}. Skipping page.",
stage_name,
idx + 1,
msg
);
results.push(Default::default());
}
}
}
results
}
#[derive(Debug, Default)]
pub struct PipelineErrors {
pub errors: Vec<(String, usize, String)>,
}
impl PipelineErrors {
pub fn record(&mut self, stage: &str, page_index: usize, error: &str) {
self.errors
.push((stage.to_string(), page_index, error.to_string()));
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn summary(&self) -> String {
if self.errors.is_empty() {
return "No errors".to_string();
}
self.errors
.iter()
.map(|(stage, page, msg)| format!("[{stage}] page {}: {msg}", page + 1))
.collect::<Vec<_>>()
.join("\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_pages_no_failure() {
let pages = vec![vec![1, 2], vec![3, 4]];
let result = process_pages_with_recovery(pages, "test", |mut p| {
p.push(99);
p
});
assert_eq!(result.len(), 2);
assert_eq!(result[0], vec![1, 2, 99]);
assert_eq!(result[1], vec![3, 4, 99]);
}
#[test]
fn test_process_pages_with_panic_recovery() {
let pages = vec![vec![1], vec![2], vec![3]];
let result = process_pages_with_recovery(pages, "test", |p| {
if p[0] == 2 {
panic!("simulated panic on page 2");
}
p
});
assert_eq!(result.len(), 3);
assert_eq!(result[0], vec![1]);
assert!(result[1].is_empty()); assert_eq!(result[2], vec![3]);
}
#[test]
fn test_pipeline_errors() {
let mut errors = PipelineErrors::default();
assert!(!errors.has_errors());
errors.record("Stage 3", 0, "Failed to detect tables");
errors.record("Stage 8", 2, "Header detection failed");
assert!(errors.has_errors());
assert_eq!(errors.errors.len(), 2);
let summary = errors.summary();
assert!(summary.contains("Stage 3"));
assert!(summary.contains("page 1"));
assert!(summary.contains("page 3"));
}
#[test]
fn test_pipeline_errors_empty_summary() {
let errors = PipelineErrors::default();
assert_eq!(errors.summary(), "No errors");
}
}