use std::path::PathBuf;
use crate::bug_hunter::types::{CrashBucketingMode, FindingSeverity};
use super::*;
#[test]
fn test_crash_pattern_detection() {
assert_eq!(
CrashBucketer::detect_pattern("index out of bounds: 5 >= 3"),
RootCausePattern::IndexOutOfBounds
);
assert_eq!(
CrashBucketer::detect_pattern("called `Option::unwrap()` on a `None` value"),
RootCausePattern::UnwrapOnNone
);
assert_eq!(
CrashBucketer::detect_pattern("integer overflow"),
RootCausePattern::IntegerOverflow
);
}
#[test]
fn test_semantic_bucketing_dedup() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::Semantic);
for i in 0..3 {
bucketer.add_crash(CrashInfo {
id: format!("crash-{}", i),
file: PathBuf::from("src/lib.rs"),
line: 42,
message: "index out of bounds: the len is 5 but the index is 10".to_string(),
stack_trace: vec![],
});
}
let (total, buckets) = bucketer.stats();
assert_eq!(total, 3);
assert_eq!(buckets, 1);
let findings = bucketer.to_findings();
assert_eq!(findings.len(), 1);
}
#[test]
fn test_crash_pattern_assertion() {
assert_eq!(
CrashBucketer::detect_pattern("assertion failed: x > 0"),
RootCausePattern::AssertionFailed
);
}
#[test]
fn test_crash_pattern_divide_by_zero() {
assert_eq!(
CrashBucketer::detect_pattern("attempt to divide by zero"),
RootCausePattern::DivisionByZero
);
}
#[test]
fn test_crash_pattern_stack_overflow() {
assert_eq!(
CrashBucketer::detect_pattern("thread 'main' has overflowed its stack"),
RootCausePattern::StackOverflow
);
}
#[test]
fn test_crash_pattern_null_pointer() {
assert_eq!(
CrashBucketer::detect_pattern("null pointer dereference"),
RootCausePattern::NullPointerDeref
);
}
#[test]
fn test_crash_pattern_unknown() {
assert_eq!(
CrashBucketer::detect_pattern("some random error message"),
RootCausePattern::Unknown
);
}
#[test]
fn test_none_bucketing() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::None);
bucketer.add_crash(CrashInfo {
id: "crash-1".to_string(),
file: PathBuf::from("src/lib.rs"),
line: 42,
message: "error 1".to_string(),
stack_trace: vec![],
});
bucketer.add_crash(CrashInfo {
id: "crash-2".to_string(),
file: PathBuf::from("src/lib.rs"),
line: 42,
message: "error 2".to_string(),
stack_trace: vec![],
});
let (total, buckets) = bucketer.stats();
assert_eq!(total, 2);
assert_eq!(buckets, 2); }
#[test]
fn test_stack_trace_bucketing() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::StackTrace);
for i in 0..2 {
bucketer.add_crash(CrashInfo {
id: format!("crash-{}", i),
file: PathBuf::from("src/lib.rs"),
line: 42,
message: "error".to_string(),
stack_trace: vec![
StackFrame { function: "fn_a".to_string(), file: None, line: None },
StackFrame {
function: "fn_b".to_string(),
file: Some(PathBuf::from("src/lib.rs")),
line: Some(10),
},
StackFrame { function: "fn_c".to_string(), file: None, line: None },
],
});
}
let (total, buckets) = bucketer.stats();
assert_eq!(total, 2);
assert_eq!(buckets, 1, "Same top-3 frames should be 1 bucket");
}
#[test]
fn test_stack_trace_bucketing_different_frames() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::StackTrace);
bucketer.add_crash(CrashInfo {
id: "c1".to_string(),
file: PathBuf::from("a.rs"),
line: 1,
message: "err".to_string(),
stack_trace: vec![StackFrame { function: "fn_x".to_string(), file: None, line: None }],
});
bucketer.add_crash(CrashInfo {
id: "c2".to_string(),
file: PathBuf::from("b.rs"),
line: 2,
message: "err".to_string(),
stack_trace: vec![StackFrame { function: "fn_y".to_string(), file: None, line: None }],
});
let (total, buckets) = bucketer.stats();
assert_eq!(total, 2);
assert_eq!(buckets, 2, "Different frames should give different buckets");
}
#[test]
fn test_stack_trace_bucketing_empty_frames() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::StackTrace);
bucketer.add_crash(CrashInfo {
id: "c1".to_string(),
file: PathBuf::from("a.rs"),
line: 1,
message: "err".to_string(),
stack_trace: vec![],
});
let (total, buckets) = bucketer.stats();
assert_eq!(total, 1);
assert_eq!(buckets, 1);
}
#[test]
fn test_root_cause_pattern_display() {
assert_eq!(RootCausePattern::IndexOutOfBounds.to_string(), "index_out_of_bounds");
assert_eq!(RootCausePattern::NullPointerDeref.to_string(), "null_pointer_deref");
assert_eq!(RootCausePattern::IntegerOverflow.to_string(), "integer_overflow");
assert_eq!(RootCausePattern::DivisionByZero.to_string(), "division_by_zero");
assert_eq!(RootCausePattern::StackOverflow.to_string(), "stack_overflow");
assert_eq!(RootCausePattern::HeapOverflow.to_string(), "heap_overflow");
assert_eq!(RootCausePattern::UseAfterFree.to_string(), "use_after_free");
assert_eq!(RootCausePattern::DoubleFree.to_string(), "double_free");
assert_eq!(RootCausePattern::UnwrapOnNone.to_string(), "unwrap_on_none");
assert_eq!(RootCausePattern::AssertionFailed.to_string(), "assertion_failed");
assert_eq!(RootCausePattern::Unknown.to_string(), "unknown");
}
#[test]
fn test_detect_pattern_use_after_free() {
assert_eq!(
CrashBucketer::detect_pattern("use after free in allocator"),
RootCausePattern::UseAfterFree
);
}
#[test]
fn test_detect_pattern_double_free() {
assert_eq!(CrashBucketer::detect_pattern("double free detected"), RootCausePattern::DoubleFree);
}
#[test]
fn test_detect_pattern_heap_overflow() {
assert_eq!(
CrashBucketer::detect_pattern("heap buffer overflow"),
RootCausePattern::HeapOverflow
);
}
#[test]
fn test_detect_pattern_indexoutofbounds_single_word() {
assert_eq!(
CrashBucketer::detect_pattern("IndexOutOfBounds exception"),
RootCausePattern::IndexOutOfBounds
);
}
#[test]
fn test_detect_pattern_nullptr() {
assert_eq!(
CrashBucketer::detect_pattern("nullptr dereference"),
RootCausePattern::NullPointerDeref
);
}
#[test]
fn test_detect_pattern_unwrap_none_variant() {
assert_eq!(
CrashBucketer::detect_pattern("unwrap called on None value"),
RootCausePattern::UnwrapOnNone
);
}
#[test]
fn test_detect_pattern_division_by_zero_variant() {
assert_eq!(
CrashBucketer::detect_pattern("division by zero error"),
RootCausePattern::DivisionByZero
);
}
#[test]
fn test_detect_pattern_assert_keyword() {
assert_eq!(
CrashBucketer::detect_pattern("assert_eq failed: 1 != 2"),
RootCausePattern::AssertionFailed
);
}
#[test]
fn test_to_findings_content() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::Semantic);
bucketer.add_crash(CrashInfo {
id: "crash-1".to_string(),
file: PathBuf::from("src/parser.rs"),
line: 100,
message: "index out of bounds: len is 3 but index is 5".to_string(),
stack_trace: vec![],
});
bucketer.add_crash(CrashInfo {
id: "crash-2".to_string(),
file: PathBuf::from("src/parser.rs"),
line: 100,
message: "index out of bounds: len is 10 but index is 20".to_string(),
stack_trace: vec![],
});
let findings = bucketer.to_findings();
assert_eq!(findings.len(), 1);
let f = &findings[0];
assert!(f.id.starts_with("BH-CRASH-"));
assert_eq!(f.file, PathBuf::from("src/parser.rs"));
assert_eq!(f.line, 100);
assert_eq!(f.severity, FindingSeverity::High);
assert!((f.suspiciousness - 0.8).abs() < f64::EPSILON);
assert!(f.description.contains("2 occurrence(s)"));
}
#[test]
fn test_to_findings_empty_bucketer() {
let bucketer = CrashBucketer::new(CrashBucketingMode::Semantic);
let findings = bucketer.to_findings();
assert!(findings.is_empty());
}
#[test]
fn test_semantic_bucketing_different_files() {
let mut bucketer = CrashBucketer::new(CrashBucketingMode::Semantic);
bucketer.add_crash(CrashInfo {
id: "c1".to_string(),
file: PathBuf::from("src/a.rs"),
line: 10,
message: "index out of bounds".to_string(),
stack_trace: vec![],
});
bucketer.add_crash(CrashInfo {
id: "c2".to_string(),
file: PathBuf::from("src/b.rs"),
line: 20,
message: "index out of bounds".to_string(),
stack_trace: vec![],
});
let (total, buckets) = bucketer.stats();
assert_eq!(total, 2);
assert_eq!(buckets, 2);
}