use std::cmp::Ordering;
use std::collections::{BinaryHeap, HashMap};
#[derive(Debug, Clone)]
pub struct ErrorCluster {
pub code: String,
pub count: usize,
pub representative: String,
pub sample_files: Vec<String>,
pub severity: f64,
}
impl ErrorCluster {
#[must_use]
pub fn new(code: impl Into<String>, representative: impl Into<String>) -> Self {
Self {
code: code.into(),
count: 1,
representative: representative.into(),
sample_files: Vec::new(),
severity: 1.0,
}
}
pub fn add_occurrence(&mut self, file: Option<&str>) {
self.count += 1;
if let Some(f) = file {
if self.sample_files.len() < 3 {
self.sample_files.push(f.to_string());
}
}
}
pub fn with_severity(mut self, severity: f64) -> Self {
self.severity = severity;
self
}
#[must_use]
pub fn priority_score(&self) -> f64 {
self.count as f64 * self.severity
}
}
#[derive(Debug, Clone)]
pub struct FailurePattern {
pub id: String,
pub error_code: String,
pub description: String,
pub affected_count: usize,
pub complexity: u8,
pub sample_code: Option<String>,
pub sample_error: Option<String>,
}
impl FailurePattern {
#[must_use]
pub fn new(id: impl Into<String>, error_code: impl Into<String>) -> Self {
Self {
id: id.into(),
error_code: error_code.into(),
description: String::new(),
affected_count: 0,
complexity: 5,
sample_code: None,
sample_error: None,
}
}
pub fn with_description(mut self, desc: impl Into<String>) -> Self {
self.description = desc.into();
self
}
pub fn with_affected_count(mut self, count: usize) -> Self {
self.affected_count = count;
self
}
pub fn with_complexity(mut self, complexity: u8) -> Self {
self.complexity = complexity;
self
}
pub fn with_sample_code(mut self, code: impl Into<String>) -> Self {
self.sample_code = Some(code.into());
self
}
pub fn with_sample_error(mut self, error: impl Into<String>) -> Self {
self.sample_error = Some(error.into());
self
}
}
#[derive(Debug, Clone)]
pub struct PrioritizedPattern {
pub pattern: FailurePattern,
pub priority: f64,
}
impl PartialEq for PrioritizedPattern {
fn eq(&self, other: &Self) -> bool {
self.priority == other.priority
}
}
impl Eq for PrioritizedPattern {}
impl PartialOrd for PrioritizedPattern {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for PrioritizedPattern {
fn cmp(&self, other: &Self) -> Ordering {
self.priority
.partial_cmp(&other.priority)
.unwrap_or(Ordering::Equal)
}
}
#[derive(Debug)]
pub struct HuntPlanner {
clusters: HashMap<String, ErrorCluster>,
priority_queue: BinaryHeap<PrioritizedPattern>,
processed: Vec<String>,
}
impl Default for HuntPlanner {
fn default() -> Self {
Self::new()
}
}
impl HuntPlanner {
#[must_use]
pub fn new() -> Self {
Self {
clusters: HashMap::new(),
priority_queue: BinaryHeap::new(),
processed: Vec::new(),
}
}
pub fn add_error(&mut self, code: &str, message: &str, file: Option<&str>, severity: f64) {
if let Some(cluster) = self.clusters.get_mut(code) {
cluster.add_occurrence(file);
} else {
let mut cluster = ErrorCluster::new(code, message).with_severity(severity);
if let Some(f) = file {
cluster.sample_files.push(f.to_string());
}
self.clusters.insert(code.to_string(), cluster);
}
}
pub fn build_priority_queue(&mut self) {
self.priority_queue.clear();
for cluster in self.clusters.values() {
if self.processed.contains(&cluster.code) {
continue;
}
let pattern = FailurePattern::new(format!("PAT-{}", cluster.code), &cluster.code)
.with_description(&cluster.representative)
.with_affected_count(cluster.count);
let priority = cluster.priority_score();
self.priority_queue
.push(PrioritizedPattern { pattern, priority });
}
}
#[must_use]
pub fn select_next_target(&mut self) -> Option<FailurePattern> {
if self.priority_queue.is_empty() {
self.build_priority_queue();
}
self.priority_queue.pop().map(|p| {
self.processed.push(p.pattern.error_code.clone());
p.pattern
})
}
#[must_use]
pub fn clusters(&self) -> &HashMap<String, ErrorCluster> {
&self.clusters
}
#[must_use]
pub fn top_clusters(&self, n: usize) -> Vec<&ErrorCluster> {
let mut clusters: Vec<_> = self.clusters.values().collect();
clusters.sort_by(|a, b| {
b.priority_score()
.partial_cmp(&a.priority_score())
.unwrap_or(Ordering::Equal)
});
clusters.into_iter().take(n).collect()
}
#[must_use]
pub fn total_errors(&self) -> usize {
self.clusters.values().map(|c| c.count).sum()
}
#[must_use]
pub fn unique_errors(&self) -> usize {
self.clusters.len()
}
pub fn clear(&mut self) {
self.clusters.clear();
self.priority_queue.clear();
self.processed.clear();
}
pub fn reset_processed(&mut self) {
self.processed.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_cluster_new() {
let cluster = ErrorCluster::new("E0308", "mismatched types");
assert_eq!(cluster.code, "E0308");
assert_eq!(cluster.count, 1);
}
#[test]
fn test_error_cluster_add_occurrence() {
let mut cluster = ErrorCluster::new("E0308", "mismatched types");
cluster.add_occurrence(Some("test.rs"));
assert_eq!(cluster.count, 2);
assert_eq!(cluster.sample_files.len(), 1);
}
#[test]
fn test_error_cluster_max_samples() {
let mut cluster = ErrorCluster::new("E0308", "mismatched types");
for i in 0..5 {
cluster.add_occurrence(Some(&format!("test{i}.rs")));
}
assert_eq!(cluster.sample_files.len(), 3);
}
#[test]
fn test_error_cluster_with_severity() {
let cluster = ErrorCluster::new("E0308", "mismatched types").with_severity(2.0);
assert!((cluster.severity - 2.0).abs() < f64::EPSILON);
}
#[test]
fn test_error_cluster_priority_score() {
let mut cluster = ErrorCluster::new("E0308", "mismatched types").with_severity(2.0);
cluster.add_occurrence(None);
cluster.add_occurrence(None);
assert!((cluster.priority_score() - 6.0).abs() < f64::EPSILON);
}
#[test]
fn test_failure_pattern_new() {
let pattern = FailurePattern::new("PAT-001", "E0308");
assert_eq!(pattern.id, "PAT-001");
assert_eq!(pattern.error_code, "E0308");
}
#[test]
fn test_failure_pattern_with_description() {
let pattern =
FailurePattern::new("PAT-001", "E0308").with_description("Type mismatch error");
assert_eq!(pattern.description, "Type mismatch error");
}
#[test]
fn test_failure_pattern_with_affected_count() {
let pattern = FailurePattern::new("PAT-001", "E0308").with_affected_count(10);
assert_eq!(pattern.affected_count, 10);
}
#[test]
fn test_failure_pattern_with_complexity() {
let pattern = FailurePattern::new("PAT-001", "E0308").with_complexity(8);
assert_eq!(pattern.complexity, 8);
}
#[test]
fn test_failure_pattern_with_sample_code() {
let pattern = FailurePattern::new("PAT-001", "E0308").with_sample_code("fn foo() {}");
assert_eq!(pattern.sample_code, Some("fn foo() {}".to_string()));
}
#[test]
fn test_failure_pattern_with_sample_error() {
let pattern =
FailurePattern::new("PAT-001", "E0308").with_sample_error("expected i32, found String");
assert_eq!(
pattern.sample_error,
Some("expected i32, found String".to_string())
);
}
#[test]
fn test_prioritized_pattern_ordering() {
let p1 = PrioritizedPattern {
pattern: FailurePattern::new("PAT-001", "E0308"),
priority: 10.0,
};
let p2 = PrioritizedPattern {
pattern: FailurePattern::new("PAT-002", "E0599"),
priority: 5.0,
};
assert!(p1 > p2);
}
#[test]
fn test_prioritized_pattern_equality() {
let p1 = PrioritizedPattern {
pattern: FailurePattern::new("PAT-001", "E0308"),
priority: 10.0,
};
let p2 = PrioritizedPattern {
pattern: FailurePattern::new("PAT-002", "E0599"),
priority: 10.0,
};
assert_eq!(p1, p2);
}
#[test]
fn test_hunt_planner_new() {
let planner = HuntPlanner::new();
assert!(planner.clusters().is_empty());
}
#[test]
fn test_hunt_planner_default() {
let planner = HuntPlanner::default();
assert!(planner.clusters().is_empty());
}
#[test]
fn test_hunt_planner_add_error() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", Some("test.rs"), 1.0);
assert_eq!(planner.unique_errors(), 1);
}
#[test]
fn test_hunt_planner_add_multiple_same_code() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", Some("test1.rs"), 1.0);
planner.add_error("E0308", "mismatched types", Some("test2.rs"), 1.0);
assert_eq!(planner.unique_errors(), 1);
assert_eq!(planner.total_errors(), 2);
}
#[test]
fn test_hunt_planner_add_different_codes() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", None, 1.0);
planner.add_error("E0599", "method not found", None, 1.0);
assert_eq!(planner.unique_errors(), 2);
}
#[test]
fn test_hunt_planner_select_next_target_empty() {
let mut planner = HuntPlanner::new();
assert!(planner.select_next_target().is_none());
}
#[test]
fn test_hunt_planner_select_next_target() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", None, 1.0);
let pattern = planner.select_next_target();
assert!(pattern.is_some());
assert_eq!(pattern.unwrap().error_code, "E0308");
}
#[test]
fn test_hunt_planner_select_highest_priority() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", None, 1.0);
planner.add_error("E0599", "method not found", None, 2.0);
let pattern = planner.select_next_target();
assert!(pattern.is_some());
assert_eq!(pattern.unwrap().error_code, "E0599");
}
#[test]
fn test_hunt_planner_top_clusters() {
let mut planner = HuntPlanner::new();
for _ in 0..10 {
planner.add_error("E0308", "mismatched types", None, 1.0);
}
for _ in 0..5 {
planner.add_error("E0599", "method not found", None, 1.0);
}
let top = planner.top_clusters(1);
assert_eq!(top.len(), 1);
assert_eq!(top[0].code, "E0308");
}
#[test]
fn test_hunt_planner_clear() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", None, 1.0);
planner.clear();
assert!(planner.clusters().is_empty());
}
#[test]
fn test_hunt_planner_reset_processed() {
let mut planner = HuntPlanner::new();
planner.add_error("E0308", "mismatched types", None, 1.0);
let _ = planner.select_next_target();
assert!(planner.select_next_target().is_none());
planner.reset_processed();
assert!(planner.select_next_target().is_some());
}
}