#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod tests {
use super::super::lint_hotspot_handlers::{FileSummary, SeverityDistribution, ViolationDetail};
use proptest::prelude::*;
use std::collections::HashMap;
use std::path::PathBuf;
fn arb_file_path() -> impl Strategy<Value = PathBuf> {
prop::string::string_regex("src/[a-zA-Z0-9_/]+\\.rs")
.unwrap()
.prop_map(PathBuf::from)
}
fn arb_lint_name() -> impl Strategy<Value = String> {
prop_oneof![
Just("clippy::pedantic".to_string()),
Just("clippy::nursery".to_string()),
Just("clippy::complexity".to_string()),
Just("clippy::style".to_string()),
Just("clippy::perf".to_string()),
Just("unused_variables".to_string()),
Just("dead_code".to_string()),
prop::string::string_regex("clippy::[a-z_]+").unwrap(),
]
}
fn arb_severity() -> impl Strategy<Value = String> {
prop_oneof![
Just("error".to_string()),
Just("warning".to_string()),
Just("help".to_string()),
Just("note".to_string()),
]
}
prop_compose! {
fn arb_violation_detail()
(
file in arb_file_path(),
line in 1u32..1000,
column in 1u32..120,
end_line in 1u32..1000,
end_column in 1u32..120,
lint_name in arb_lint_name(),
message in "[a-zA-Z0-9 .,!?-]+",
severity in arb_severity(),
has_suggestion in any::<bool>(),
suggestion in "[a-zA-Z0-9 .,!?-]+",
machine_applicable in any::<bool>(),
)
-> ViolationDetail
{
ViolationDetail {
file,
line,
column,
end_line: end_line.max(line),
end_column: if end_line > line { end_column } else { end_column.max(column) },
lint_name,
message,
severity,
suggestion: if has_suggestion { Some(suggestion) } else { None },
machine_applicable: machine_applicable && has_suggestion,
}
}
}
prop_compose! {
fn arb_severity_distribution()
(
error in 0usize..100,
warning in 0usize..500,
suggestion in 0usize..200,
note in 0usize..100,
)
-> SeverityDistribution
{
SeverityDistribution {
error,
warning,
suggestion,
note,
}
}
}
prop_compose! {
fn arb_file_summary()
(
errors in 0usize..100,
warnings in 0usize..500,
sloc in 1usize..5000,
suggestions in 0usize..200,
notes in 0usize..100,
)
-> FileSummary
{
let total_violations = errors + warnings + suggestions + notes;
let defect_density = calculate_defect_density(total_violations, sloc);
FileSummary {
total_violations,
errors,
warnings,
sloc,
defect_density,
}
}
}
proptest! {
#[test]
fn defect_density_always_valid(
total_violations in 0usize..10000,
sloc in 1usize..10000,
) {
let density = calculate_defect_density(total_violations, sloc);
prop_assert!(density >= 0.0, "Density should be non-negative");
prop_assert!(density.is_finite(), "Density should be finite");
let expected = (total_violations as f64 / sloc as f64) * 100.0;
prop_assert!((density - expected).abs() < f64::EPSILON);
}
#[test]
fn empty_summary_zero_density(_dummy in 0u8..1) {
let summary = FileSummary {
total_violations: 0,
errors: 0,
warnings: 0,
sloc: 100,
defect_density: 0.0,
};
prop_assert_eq!(summary.total_violations, 0);
prop_assert_eq!(summary.defect_density, 0.0);
let calculated_density = calculate_defect_density(summary.total_violations, summary.sloc);
prop_assert_eq!(calculated_density, 0.0);
}
#[test]
fn severity_counts_consistent_with_violations(
violations in prop::collection::vec(arb_violation_detail(), 0..100)
) {
let mut severity_counts = SeverityDistribution::default();
for v in &violations {
match v.severity.as_str() {
"error" => severity_counts.error += 1,
"warning" => severity_counts.warning += 1,
"help" | "suggestion" => severity_counts.suggestion += 1,
_ => severity_counts.note += 1,
}
}
let actual_errors = violations.iter().filter(|v| v.severity == "error").count();
let actual_warnings = violations.iter().filter(|v| v.severity == "warning").count();
let actual_suggestions = violations.iter()
.filter(|v| v.severity == "help" || v.severity == "suggestion")
.count();
let actual_notes = violations.iter()
.filter(|v| !["error", "warning", "help", "suggestion"].contains(&v.severity.as_str()))
.count();
prop_assert_eq!(severity_counts.error, actual_errors);
prop_assert_eq!(severity_counts.warning, actual_warnings);
prop_assert_eq!(severity_counts.suggestion, actual_suggestions);
prop_assert_eq!(severity_counts.note, actual_notes);
}
#[test]
fn top_lints_correctly_sorted(
violations in prop::collection::hash_map(
arb_lint_name(),
1usize..100,
1..20
),
limit in 1usize..10,
) {
let top_lints = get_top_lints(&violations, limit);
prop_assert!(top_lints.len() <= limit);
prop_assert!(top_lints.len() <= violations.len());
for i in 1..top_lints.len() {
prop_assert!(
top_lints[i-1].1 >= top_lints[i].1,
"Top lints not sorted: {} ({}) should come before {} ({})",
top_lints[i-1].0, top_lints[i-1].1,
top_lints[i].0, top_lints[i].1
);
}
for (lint_name, count) in &top_lints {
prop_assert_eq!(violations.get(lint_name), Some(count));
}
}
#[test]
fn file_summary_density_consistent(summary in arb_file_summary()) {
let calculated_density = calculate_defect_density(summary.total_violations, summary.sloc);
prop_assert!(
(summary.defect_density - calculated_density).abs() < 0.001,
"Density mismatch: {} vs calculated {}",
summary.defect_density,
calculated_density
);
prop_assert!(
summary.total_violations >= summary.errors + summary.warnings,
"Total violations {} should be >= errors {} + warnings {}",
summary.total_violations,
summary.errors,
summary.warnings
);
}
#[test]
fn find_hotspot_returns_max_density(
file_summaries in prop::collection::hash_map(
arb_file_path(),
arb_file_summary(),
1..10
)
) {
let mut max_density = 0.0;
let mut hotspot_path = None;
for (path, summary) in &file_summaries {
if summary.sloc == 0 || summary.total_violations == 0 {
continue;
}
let density = calculate_defect_density(summary.total_violations, summary.sloc);
if density > max_density {
max_density = density;
hotspot_path = Some(path.clone());
}
}
if let Some(expected_path) = hotspot_path {
let summary = file_summaries.get(&expected_path).unwrap();
prop_assert!(summary.total_violations > 0);
prop_assert!(summary.sloc > 0);
let density = calculate_defect_density(summary.total_violations, summary.sloc);
prop_assert!(
(density - max_density).abs() < 0.001,
"Density {} should match max density {}",
density,
max_density
);
for other_summary in file_summaries.values() {
if other_summary.total_violations > 0 && other_summary.sloc > 0 {
let other_density = calculate_defect_density(
other_summary.total_violations,
other_summary.sloc
);
prop_assert!(
density >= other_density || (density - other_density).abs() < 0.001,
"Found higher density: {} > {}",
other_density,
density
);
}
}
}
}
#[test]
fn machine_applicable_requires_suggestion(
mut violation in arb_violation_detail()
) {
if violation.machine_applicable {
prop_assert!(violation.suggestion.is_some(),
"Machine applicable violation must have suggestion");
}
violation.suggestion = None;
violation.machine_applicable = false;
prop_assert!(!violation.machine_applicable);
}
#[test]
fn violation_line_ranges_valid(violation in arb_violation_detail()) {
prop_assert!(violation.line > 0, "Line must be positive");
prop_assert!(violation.column > 0, "Column must be positive");
prop_assert!(violation.end_line >= violation.line,
"End line must be >= start line");
if violation.end_line == violation.line {
prop_assert!(violation.end_column >= violation.column,
"End column must be >= start column on same line");
}
}
#[test]
fn zero_sloc_handled_safely(total_violations in 0usize..1000) {
let density = calculate_defect_density(total_violations, 0);
prop_assert_eq!(density, 0.0, "Zero SLOC should produce zero density");
}
}
fn calculate_defect_density(total_violations: usize, sloc: usize) -> f64 {
if sloc == 0 {
0.0
} else {
(total_violations as f64 / sloc as f64) * 100.0
}
}
fn get_top_lints(violations: &HashMap<String, usize>, limit: usize) -> Vec<(String, usize)> {
let mut sorted: Vec<_> = violations.iter().map(|(k, v)| (k.clone(), *v)).collect();
sorted.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
sorted.truncate(limit);
sorted
}
}