pub mod collection;
pub mod partition;
pub mod reporting;
pub mod summary;
pub use collection::{AnalysisFailure, AnalysisResults, OperationType};
pub use partition::{ParPartitionResult, PartitionResult};
pub use reporting::{report_brief_summary, report_completion_summary};
pub use summary::ErrorSummary as AnalysisErrorSummary;
use std::fmt;
use std::io;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AnalysisError {
IoError {
message: String,
path: Option<PathBuf>,
},
ParseError {
message: String,
path: Option<PathBuf>,
line: Option<usize>,
},
ValidationError { message: String },
ConfigError {
message: String,
path: Option<PathBuf>,
},
CoverageError {
message: String,
path: Option<PathBuf>,
},
AnalysisFailure { message: String },
Other(String),
}
impl AnalysisError {
pub fn io(message: impl Into<String>) -> Self {
Self::IoError {
message: message.into(),
path: None,
}
}
pub fn io_with_path(message: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self::IoError {
message: message.into(),
path: Some(path.into()),
}
}
pub fn parse(message: impl Into<String>) -> Self {
Self::ParseError {
message: message.into(),
path: None,
line: None,
}
}
pub fn parse_with_context(
message: impl Into<String>,
path: impl Into<PathBuf>,
line: usize,
) -> Self {
Self::ParseError {
message: message.into(),
path: Some(path.into()),
line: Some(line),
}
}
pub fn parse_with_path(message: impl Into<String>, path: impl AsRef<std::path::Path>) -> Self {
Self::ParseError {
message: message.into(),
path: Some(path.as_ref().to_path_buf()),
line: None,
}
}
pub fn validation(message: impl Into<String>) -> Self {
Self::ValidationError {
message: message.into(),
}
}
pub fn validation_with_path(
message: impl Into<String>,
path: impl AsRef<std::path::Path>,
) -> Self {
Self::ValidationError {
message: format!("{} (path: {})", message.into(), path.as_ref().display()),
}
}
pub fn config(message: impl Into<String>) -> Self {
Self::ConfigError {
message: message.into(),
path: None,
}
}
pub fn config_with_path(message: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self::ConfigError {
message: message.into(),
path: Some(path.into()),
}
}
pub fn coverage(message: impl Into<String>) -> Self {
Self::CoverageError {
message: message.into(),
path: None,
}
}
pub fn coverage_with_path(message: impl Into<String>, path: impl Into<PathBuf>) -> Self {
Self::CoverageError {
message: message.into(),
path: Some(path.into()),
}
}
pub fn analysis(message: impl Into<String>) -> Self {
Self::AnalysisFailure {
message: message.into(),
}
}
pub fn other(message: impl Into<String>) -> Self {
Self::Other(message.into())
}
pub fn multi_file(errors: Vec<String>) -> Self {
let count = errors.len();
let message = if count == 1 {
errors.into_iter().next().unwrap_or_default()
} else {
format!("{} file errors:\n - {}", count, errors.join("\n - "))
};
Self::AnalysisFailure { message }
}
pub fn message(&self) -> &str {
match self {
Self::IoError { message, .. } => message,
Self::ParseError { message, .. } => message,
Self::ValidationError { message } => message,
Self::ConfigError { message, .. } => message,
Self::CoverageError { message, .. } => message,
Self::AnalysisFailure { message } => message,
Self::Other(message) => message,
}
}
pub fn path(&self) -> Option<&PathBuf> {
match self {
Self::IoError { path, .. } => path.as_ref(),
Self::ParseError { path, .. } => path.as_ref(),
Self::ConfigError { path, .. } => path.as_ref(),
Self::CoverageError { path, .. } => path.as_ref(),
_ => None,
}
}
pub fn category(&self) -> &'static str {
match self {
Self::IoError { .. } => "I/O",
Self::ParseError { .. } => "Parse",
Self::ValidationError { .. } => "Validation",
Self::ConfigError { .. } => "Config",
Self::CoverageError { .. } => "Coverage",
Self::AnalysisFailure { .. } => "Analysis",
Self::Other(_) => "Error",
}
}
pub fn is_retryable(&self) -> bool {
match self {
Self::IoError { message, .. } => {
let msg_lower = message.to_lowercase();
msg_lower.contains("resource busy")
|| msg_lower.contains("would block")
|| msg_lower.contains("timed out")
|| msg_lower.contains("timeout")
|| msg_lower.contains("interrupted")
|| msg_lower.contains("temporarily unavailable")
|| msg_lower.contains("connection reset")
|| msg_lower.contains("broken pipe")
|| msg_lower.contains("try again")
}
Self::CoverageError { message, .. } => {
let msg_lower = message.to_lowercase();
msg_lower.contains("connection")
|| msg_lower.contains("timeout")
|| msg_lower.contains("unavailable")
}
Self::Other(message) => {
let msg_lower = message.to_lowercase();
msg_lower.contains("index.lock")
|| msg_lower.contains("lock file")
|| msg_lower.contains("unable to lock")
|| msg_lower.contains("connection refused")
|| msg_lower.contains("network unreachable")
}
Self::ParseError { .. }
| Self::ValidationError { .. }
| Self::ConfigError { .. }
| Self::AnalysisFailure { .. } => false,
}
}
}
impl fmt::Display for AnalysisError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::IoError { message, path } => {
write!(f, "I/O error: {}", message)?;
if let Some(p) = path {
write!(f, " (path: {})", p.display())?;
}
Ok(())
}
Self::ParseError {
message,
path,
line,
} => {
write!(f, "Parse error: {}", message)?;
if let Some(p) = path {
write!(f, " in {}", p.display())?;
}
if let Some(l) = line {
write!(f, " at line {}", l)?;
}
Ok(())
}
Self::ValidationError { message } => {
write!(f, "Validation error: {}", message)
}
Self::ConfigError { message, path } => {
write!(f, "Config error: {}", message)?;
if let Some(p) = path {
write!(f, " (file: {})", p.display())?;
}
Ok(())
}
Self::CoverageError { message, path } => {
write!(f, "Coverage error: {}", message)?;
if let Some(p) = path {
write!(f, " (file: {})", p.display())?;
}
Ok(())
}
Self::AnalysisFailure { message } => {
write!(f, "Analysis error: {}", message)
}
Self::Other(message) => write!(f, "{}", message),
}
}
}
impl std::error::Error for AnalysisError {}
impl From<anyhow::Error> for AnalysisError {
fn from(err: anyhow::Error) -> Self {
let error_string = err.to_string();
if error_string.contains("I/O error") || error_string.contains("No such file") {
Self::io(error_string)
} else if error_string.contains("Parse error") || error_string.contains("syntax") {
Self::parse(error_string)
} else if error_string.contains("Config") || error_string.contains("configuration") {
Self::config(error_string)
} else if error_string.contains("Coverage") || error_string.contains("coverage") {
Self::coverage(error_string)
} else if error_string.contains("Validation") || error_string.contains("invalid") {
Self::validation(error_string)
} else {
Self::Other(error_string)
}
}
}
impl AnalysisError {
pub fn into_anyhow(self) -> anyhow::Error {
anyhow::Error::from(self)
}
}
impl From<io::Error> for AnalysisError {
fn from(err: io::Error) -> Self {
Self::io(err.to_string())
}
}
impl From<std::convert::Infallible> for AnalysisError {
fn from(infallible: std::convert::Infallible) -> Self {
match infallible {}
}
}
pub fn format_error_list(errors: &[AnalysisError]) -> String {
errors
.iter()
.enumerate()
.map(|(i, e)| format!(" {}. {}", i + 1, e))
.collect::<Vec<_>>()
.join("\n")
}
pub fn errors_to_anyhow(errors: Vec<AnalysisError>) -> anyhow::Error {
if errors.is_empty() {
anyhow::anyhow!("Unknown error (no errors provided)")
} else if errors.len() == 1 {
errors
.into_iter()
.next()
.expect("errors should have exactly one element")
.into()
} else {
anyhow::anyhow!("Multiple errors occurred:\n{}", format_error_list(&errors))
}
}
pub fn print_error_report(errors: &[AnalysisError]) {
if errors.is_empty() {
return;
}
let issue_count = if errors.len() == 1 {
"1 issue".to_string()
} else {
format!("{} issues", errors.len())
};
eprintln!("\nError: {} found:\n", issue_count);
for (i, error) in errors.iter().enumerate() {
eprintln!(" {}. {}", i + 1, error);
}
eprintln!("\nTip: Fix the issues above and run again.");
}
pub fn print_error_report_titled(title: &str, errors: &[AnalysisError]) {
if errors.is_empty() {
return;
}
let issue_count = if errors.len() == 1 {
"1 issue".to_string()
} else {
format!("{} issues", errors.len())
};
eprintln!("\n{}: {} found:\n", title, issue_count);
for (i, error) in errors.iter().enumerate() {
eprintln!(" {}. {}", i + 1, error);
}
eprintln!("\nTip: Fix the issues above and run again.");
}
pub fn format_error_report(errors: &[AnalysisError]) -> String {
if errors.is_empty() {
return String::new();
}
let issue_count = if errors.len() == 1 {
"1 issue".to_string()
} else {
format!("{} issues", errors.len())
};
let mut output = format!("Error: {} found:\n\n", issue_count);
output.push_str(&format_error_list(errors));
output.push_str("\n\nTip: Fix the issues above and run again.");
output
}
pub fn group_errors_by_category(
errors: &[AnalysisError],
) -> std::collections::HashMap<&'static str, Vec<&AnalysisError>> {
let mut groups: std::collections::HashMap<&'static str, Vec<&AnalysisError>> =
std::collections::HashMap::new();
for error in errors {
groups.entry(error.category()).or_default().push(error);
}
groups
}
pub fn print_grouped_error_report(errors: &[AnalysisError]) {
if errors.is_empty() {
return;
}
let groups = group_errors_by_category(errors);
let issue_count = if errors.len() == 1 {
"1 issue".to_string()
} else {
format!("{} issues", errors.len())
};
eprintln!("\nError: {} found:\n", issue_count);
for (category, category_errors) in groups {
eprintln!("[{}] {} error(s):", category, category_errors.len());
for error in category_errors {
eprintln!(" - {}", error.message());
if let Some(path) = error.path() {
eprintln!(" at {}", path.display());
}
}
eprintln!();
}
eprintln!("Tip: Fix the issues above and run again.");
}
#[derive(Debug, Clone)]
pub struct ErrorSummary {
pub total_count: usize,
pub by_category: std::collections::HashMap<String, usize>,
pub affected_files: Vec<PathBuf>,
pub errors: Vec<AnalysisError>,
}
impl ErrorSummary {
pub fn from_errors(errors: Vec<AnalysisError>) -> Self {
let mut by_category: std::collections::HashMap<String, usize> =
std::collections::HashMap::new();
let mut affected_files: std::collections::HashSet<PathBuf> =
std::collections::HashSet::new();
for error in &errors {
*by_category.entry(error.category().to_string()).or_insert(0) += 1;
if let Some(path) = error.path() {
affected_files.insert(path.clone());
}
}
Self {
total_count: errors.len(),
by_category,
affected_files: affected_files.into_iter().collect(),
errors,
}
}
pub fn has_errors(&self) -> bool {
self.total_count > 0
}
pub fn one_line_summary(&self) -> String {
if self.total_count == 0 {
"No errors".to_string()
} else {
let file_count = self.affected_files.len();
format!("{} error(s) in {} file(s)", self.total_count, file_count)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_io_error_creation() {
let err = AnalysisError::io("File not found");
assert_eq!(err.message(), "File not found");
assert_eq!(err.category(), "I/O");
assert!(err.path().is_none());
}
#[test]
fn test_io_error_with_path() {
let err = AnalysisError::io_with_path("Permission denied", "/etc/passwd");
assert_eq!(err.message(), "Permission denied");
assert_eq!(err.path().unwrap(), &PathBuf::from("/etc/passwd"));
}
#[test]
fn test_parse_error_with_context() {
let err = AnalysisError::parse_with_context("Unexpected token", "src/main.rs", 42);
assert!(err.to_string().contains("src/main.rs"));
assert!(err.to_string().contains("line 42"));
}
#[test]
fn test_validation_error() {
let err = AnalysisError::validation("Weights must sum to 1.0");
assert_eq!(err.category(), "Validation");
assert!(err.to_string().contains("Weights must sum"));
}
#[test]
fn test_anyhow_roundtrip() {
let original = AnalysisError::io("Test error");
let anyhow_err: anyhow::Error = original.clone().into();
let back: AnalysisError = anyhow_err.into();
assert!(back.to_string().contains("Test error"));
}
#[test]
fn test_format_error_list() {
let errors = vec![
AnalysisError::io("File not found"),
AnalysisError::parse("Invalid syntax"),
AnalysisError::validation("Invalid config"),
];
let formatted = format_error_list(&errors);
assert!(formatted.contains("1."));
assert!(formatted.contains("2."));
assert!(formatted.contains("3."));
}
#[test]
fn test_errors_to_anyhow_single() {
let errors = vec![AnalysisError::io("Single error")];
let result = errors_to_anyhow(errors);
assert!(result.to_string().contains("Single error"));
}
#[test]
fn test_errors_to_anyhow_multiple() {
let errors = vec![
AnalysisError::io("Error 1"),
AnalysisError::parse("Error 2"),
];
let result = errors_to_anyhow(errors);
let msg = result.to_string();
assert!(msg.contains("Multiple errors"));
assert!(msg.contains("Error 1"));
assert!(msg.contains("Error 2"));
}
#[test]
fn test_from_io_error() {
let io_err = io::Error::new(io::ErrorKind::NotFound, "file.txt not found");
let analysis_err: AnalysisError = io_err.into();
assert_eq!(analysis_err.category(), "I/O");
}
#[test]
fn test_format_error_report_single() {
let errors = vec![AnalysisError::config("Invalid threshold")];
let report = format_error_report(&errors);
assert!(report.contains("1 issue"));
assert!(report.contains("Invalid threshold"));
assert!(report.contains("Tip:"));
}
#[test]
fn test_format_error_report_multiple() {
let errors = vec![
AnalysisError::config("Error 1"),
AnalysisError::parse("Error 2"),
AnalysisError::io("Error 3"),
];
let report = format_error_report(&errors);
assert!(report.contains("3 issues"));
assert!(report.contains("1."));
assert!(report.contains("2."));
assert!(report.contains("3."));
}
#[test]
fn test_format_error_report_empty() {
let errors: Vec<AnalysisError> = vec![];
let report = format_error_report(&errors);
assert!(report.is_empty());
}
#[test]
fn test_group_errors_by_category() {
let errors = vec![
AnalysisError::config("Config 1"),
AnalysisError::config("Config 2"),
AnalysisError::parse("Parse 1"),
AnalysisError::io("IO 1"),
];
let groups = group_errors_by_category(&errors);
assert_eq!(groups.get("Config").map(|v| v.len()), Some(2));
assert_eq!(groups.get("Parse").map(|v| v.len()), Some(1));
assert_eq!(groups.get("I/O").map(|v| v.len()), Some(1));
}
#[test]
fn test_error_summary_from_errors() {
let errors = vec![
AnalysisError::io_with_path("Not found", "/path/to/file1.rs"),
AnalysisError::io_with_path("Permission denied", "/path/to/file2.rs"),
AnalysisError::config("Invalid threshold"),
];
let summary = ErrorSummary::from_errors(errors);
assert_eq!(summary.total_count, 3);
assert_eq!(summary.by_category.get("I/O"), Some(&2));
assert_eq!(summary.by_category.get("Config"), Some(&1));
assert_eq!(summary.affected_files.len(), 2);
assert!(summary.has_errors());
}
#[test]
fn test_error_summary_one_line() {
let errors = vec![
AnalysisError::io_with_path("Error", "/file1.rs"),
AnalysisError::io_with_path("Error", "/file2.rs"),
];
let summary = ErrorSummary::from_errors(errors);
let one_line = summary.one_line_summary();
assert!(one_line.contains("2 error(s)"));
assert!(one_line.contains("2 file(s)"));
}
#[test]
fn test_error_summary_empty() {
let summary = ErrorSummary::from_errors(vec![]);
assert!(!summary.has_errors());
assert_eq!(summary.one_line_summary(), "No errors");
}
#[test]
fn test_is_retryable_io_resource_busy() {
let err = AnalysisError::io("Resource busy - file locked");
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_io_timeout() {
let err = AnalysisError::io("Operation timed out");
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_io_interrupted() {
let err = AnalysisError::io("System call interrupted");
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_io_would_block() {
let err = AnalysisError::io("Would block on non-blocking read");
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_io_not_found() {
let err = AnalysisError::io("No such file or directory");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_io_permission_denied() {
let err = AnalysisError::io("Permission denied");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_parse_error() {
let err = AnalysisError::parse("Syntax error at line 5");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_validation_error() {
let err = AnalysisError::validation("Invalid configuration value");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_config_error() {
let err = AnalysisError::config("Missing required field");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_analysis_error() {
let err = AnalysisError::analysis("Algorithm failed");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_coverage_timeout() {
let err = AnalysisError::coverage("Connection timeout to coverage server");
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_coverage_parse_fail() {
let err = AnalysisError::coverage("Invalid coverage format");
assert!(!err.is_retryable());
}
#[test]
fn test_is_retryable_git_lock() {
let err = AnalysisError::other("Unable to create index.lock: File exists");
assert!(err.is_retryable());
}
#[test]
fn test_is_retryable_other_not_retryable() {
let err = AnalysisError::other("Unknown error occurred");
assert!(!err.is_retryable());
}
}