use std::collections::HashMap;
use std::path::{Path, PathBuf};
use std::simd::{cmp::SimdPartialOrd, u32x8, Mask, Simd};
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::ast::{AstExtractor, FunctionInfo};
use crate::callgraph::scanner::{ProjectScanner, ScanConfig};
use crate::cfg::{CfgBuilder, CFGInfo};
use crate::error::{Result, BrrrError};
use crate::lang::LanguageRegistry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RiskLevel {
Low,
Medium,
High,
Critical,
}
impl RiskLevel {
#[must_use]
pub fn from_complexity(complexity: u32) -> Self {
match complexity {
0..=10 => Self::Low,
11..=20 => Self::Medium,
21..=50 => Self::High,
_ => Self::Critical,
}
}
#[must_use]
pub const fn description(&self) -> &'static str {
match self {
Self::Low => "Simple, low risk",
Self::Medium => "Moderate complexity, consider refactoring",
Self::High => "Complex, hard to test and maintain",
Self::Critical => "Critical complexity, refactor immediately",
}
}
#[must_use]
pub const fn color_code(&self) -> &'static str {
match self {
Self::Low => "\x1b[32m", Self::Medium => "\x1b[33m", Self::High => "\x1b[31m", Self::Critical => "\x1b[35m", }
}
}
impl std::fmt::Display for RiskLevel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Low => write!(f, "low"),
Self::Medium => write!(f, "medium"),
Self::High => write!(f, "high"),
Self::Critical => write!(f, "critical"),
}
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RiskLevelCounts {
pub low: usize,
pub medium: usize,
pub high: usize,
pub critical: usize,
}
impl RiskLevelCounts {
#[must_use]
pub fn count_simd(complexities: &[u32]) -> Self {
const LANES: usize = 8;
let threshold_low: u32x8 = Simd::splat(10);
let threshold_medium: u32x8 = Simd::splat(20);
let threshold_high: u32x8 = Simd::splat(50);
let mut low_count: usize = 0;
let mut medium_count: usize = 0;
let mut high_count: usize = 0;
let mut critical_count: usize = 0;
let chunks = complexities.len() / LANES;
let remainder = complexities.len() % LANES;
for chunk_idx in 0..chunks {
let offset = chunk_idx * LANES;
let values = u32x8::from_slice(&complexities[offset..offset + LANES]);
let le_10: Mask<i32, 8> = values.simd_le(threshold_low);
let le_20: Mask<i32, 8> = values.simd_le(threshold_medium);
let le_50: Mask<i32, 8> = values.simd_le(threshold_high);
low_count += le_10.to_bitmask().count_ones() as usize;
let is_medium = le_20 & !le_10;
medium_count += is_medium.to_bitmask().count_ones() as usize;
let is_high = le_50 & !le_20;
high_count += is_high.to_bitmask().count_ones() as usize;
let is_critical = !le_50;
critical_count += is_critical.to_bitmask().count_ones() as usize;
}
let tail_start = chunks * LANES;
for &c in &complexities[tail_start..tail_start + remainder] {
match c {
0..=10 => low_count += 1,
11..=20 => medium_count += 1,
21..=50 => high_count += 1,
_ => critical_count += 1,
}
}
Self {
low: low_count,
medium: medium_count,
high: high_count,
critical: critical_count,
}
}
#[must_use]
pub fn to_hashmap(&self) -> HashMap<String, usize> {
let mut map = HashMap::with_capacity(4);
if self.low > 0 {
map.insert("low".to_string(), self.low);
}
if self.medium > 0 {
map.insert("medium".to_string(), self.medium);
}
if self.high > 0 {
map.insert("high".to_string(), self.high);
}
if self.critical > 0 {
map.insert("critical".to_string(), self.critical);
}
map
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CyclomaticComplexity {
pub function_name: String,
pub file: PathBuf,
pub line: usize,
pub end_line: usize,
pub complexity: u32,
pub risk_level: RiskLevel,
pub decision_points: u32,
pub nodes: usize,
pub edges: usize,
}
impl CyclomaticComplexity {
fn from_cfg(cfg: &CFGInfo, file: &Path, line: usize, end_line: usize) -> Self {
let complexity = cfg.cyclomatic_complexity() as u32;
Self {
function_name: cfg.function_name.clone(),
file: file.to_path_buf(),
line,
end_line,
complexity,
risk_level: RiskLevel::from_complexity(complexity),
decision_points: cfg.decision_points as u32,
nodes: cfg.blocks.len(),
edges: cfg.edges.len(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FunctionComplexity {
pub name: String,
pub line: usize,
pub complexity: u32,
pub risk_level: RiskLevel,
}
impl From<&CyclomaticComplexity> for FunctionComplexity {
fn from(cc: &CyclomaticComplexity) -> Self {
Self {
name: cc.function_name.clone(),
line: cc.line,
complexity: cc.complexity,
risk_level: cc.risk_level,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityStats {
pub total_functions: usize,
pub average_complexity: f64,
pub max_complexity: u32,
pub min_complexity: u32,
pub median_complexity: u32,
pub risk_distribution: HashMap<String, usize>,
pub histogram: Vec<HistogramBucket>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HistogramBucket {
pub min: u32,
pub max: u32,
pub label: String,
pub count: usize,
}
impl ComplexityStats {
fn from_complexities(complexities: &[u32]) -> Self {
if complexities.is_empty() {
return Self {
total_functions: 0,
average_complexity: 0.0,
max_complexity: 0,
min_complexity: 0,
median_complexity: 0,
risk_distribution: HashMap::new(),
histogram: Vec::new(),
};
}
let total = complexities.len();
let sum: u64 = complexities.iter().map(|&c| u64::from(c)).sum();
let average = sum as f64 / total as f64;
let max = *complexities.iter().max().unwrap_or(&0);
let min = *complexities.iter().min().unwrap_or(&0);
let mut sorted = complexities.to_vec();
sorted.sort_unstable();
let median = if total % 2 == 0 {
(sorted[total / 2 - 1] + sorted[total / 2]) / 2
} else {
sorted[total / 2]
};
let risk_counts = RiskLevelCounts::count_simd(complexities);
let risk_distribution = risk_counts.to_hashmap();
let histogram = Self::build_histogram(complexities, max);
Self {
total_functions: total,
average_complexity: average,
max_complexity: max,
min_complexity: min,
median_complexity: median,
risk_distribution,
histogram,
}
}
fn build_histogram(complexities: &[u32], max: u32) -> Vec<HistogramBucket> {
let bucket_size = 5u32;
let num_buckets = ((max / bucket_size) + 1) as usize;
let mut buckets = Vec::with_capacity(num_buckets.min(20));
for i in 0..num_buckets.min(20) {
let min_val = (i as u32) * bucket_size + 1;
let max_val = min_val + bucket_size - 1;
let count = complexities.iter()
.filter(|&&c| c >= min_val && c <= max_val)
.count();
buckets.push(HistogramBucket {
min: min_val,
max: max_val,
label: format!("{}-{}", min_val, max_val),
count,
});
}
if max > 100 {
let overflow_count = complexities.iter().filter(|&&c| c > 100).count();
if overflow_count > 0 {
buckets.push(HistogramBucket {
min: 101,
max: u32::MAX,
label: "100+".to_string(),
count: overflow_count,
});
}
}
buckets
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ComplexityAnalysis {
pub path: PathBuf,
pub language: Option<String>,
pub functions: Vec<CyclomaticComplexity>,
#[serde(skip_serializing_if = "Option::is_none")]
pub violations: Option<Vec<CyclomaticComplexity>>,
pub stats: ComplexityStats,
#[serde(skip_serializing_if = "Option::is_none")]
pub threshold: Option<u32>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<AnalysisError>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AnalysisError {
pub file: PathBuf,
pub message: String,
}
pub fn analyze_complexity(
path: impl AsRef<Path>,
language: Option<&str>,
threshold: Option<u32>,
) -> Result<ComplexityAnalysis> {
let path = path.as_ref();
if !path.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("Path not found: {}", path.display()),
)));
}
if path.is_file() {
return analyze_file_complexity(path, threshold);
}
let path_str = path.to_str().ok_or_else(|| {
BrrrError::InvalidArgument("Invalid path encoding".to_string())
})?;
let scanner = ProjectScanner::new(path_str)?;
let config = if let Some(lang) = language {
ScanConfig::for_language(lang)
} else {
ScanConfig::default()
};
let scan_result = scanner.scan_with_config(&config)?;
if scan_result.files.is_empty() {
return Err(BrrrError::InvalidArgument(format!(
"No source files found in {} (filter: {:?})",
path.display(),
language
)));
}
debug!("Analyzing {} files for complexity", scan_result.files.len());
let results: Vec<(Vec<CyclomaticComplexity>, Vec<AnalysisError>)> = scan_result
.files
.par_iter()
.map(|file| analyze_file_functions(file, threshold))
.collect();
let mut all_functions = Vec::new();
let mut all_errors = Vec::new();
for (functions, errors) in results {
all_functions.extend(functions);
all_errors.extend(errors);
}
let complexities: Vec<u32> = all_functions.iter().map(|f| f.complexity).collect();
let stats = ComplexityStats::from_complexities(&complexities);
let violations = threshold.map(|t| {
all_functions
.iter()
.filter(|f| f.complexity > t)
.cloned()
.collect::<Vec<_>>()
});
Ok(ComplexityAnalysis {
path: path.to_path_buf(),
language: language.map(String::from),
functions: all_functions,
violations,
stats,
threshold,
errors: all_errors,
})
}
pub fn analyze_file_complexity(
file: impl AsRef<Path>,
threshold: Option<u32>,
) -> Result<ComplexityAnalysis> {
let file = file.as_ref();
if !file.exists() {
return Err(BrrrError::Io(std::io::Error::new(
std::io::ErrorKind::NotFound,
format!("File not found: {}", file.display()),
)));
}
if !file.is_file() {
return Err(BrrrError::InvalidArgument(format!(
"Expected a file, got directory: {}",
file.display()
)));
}
let (functions, errors) = analyze_file_functions(file, threshold);
let complexities: Vec<u32> = functions.iter().map(|f| f.complexity).collect();
let stats = ComplexityStats::from_complexities(&complexities);
let violations = threshold.map(|t| {
functions
.iter()
.filter(|f| f.complexity > t)
.cloned()
.collect::<Vec<_>>()
});
let registry = LanguageRegistry::global();
let language = registry
.detect_language(file)
.map(|l| l.name().to_string());
Ok(ComplexityAnalysis {
path: file.to_path_buf(),
language,
functions,
violations,
stats,
threshold,
errors,
})
}
fn analyze_file_functions(
file: &Path,
_threshold: Option<u32>,
) -> (Vec<CyclomaticComplexity>, Vec<AnalysisError>) {
let mut results = Vec::new();
let mut errors = Vec::new();
let module = match AstExtractor::extract_file(file) {
Ok(m) => m,
Err(e) => {
errors.push(AnalysisError {
file: file.to_path_buf(),
message: format!("Failed to parse file: {}", e),
});
return (results, errors);
}
};
let file_str = match file.to_str() {
Some(s) => s,
None => {
errors.push(AnalysisError {
file: file.to_path_buf(),
message: "Invalid file path encoding".to_string(),
});
return (results, errors);
}
};
for func in &module.functions {
let start_line = func.line_number;
let end_line = func.end_line_number.unwrap_or(start_line);
match analyze_function(file_str, &func.name, start_line, end_line) {
Ok(complexity) => results.push(complexity),
Err(e) => {
debug!("Failed to analyze function {}: {}", func.name, e);
if let Some(complexity) = estimate_complexity_from_function(func, file) {
results.push(complexity);
}
}
}
}
for class in &module.classes {
for method in &class.methods {
let qualified_name = format!("{}.{}", class.name, method.name);
let start_line = method.line_number;
let end_line = method.end_line_number.unwrap_or(start_line);
match analyze_function(file_str, &qualified_name, start_line, end_line) {
Ok(mut complexity) => {
complexity.function_name = qualified_name;
results.push(complexity);
}
Err(e) => {
debug!("Failed to analyze method {}: {}", qualified_name, e);
if let Some(mut complexity) = estimate_complexity_from_function(method, file) {
complexity.function_name = qualified_name;
results.push(complexity);
}
}
}
}
}
(results, errors)
}
fn analyze_function(
file: &str,
function_name: &str,
start_line: usize,
end_line: usize,
) -> Result<CyclomaticComplexity> {
let cfg = CfgBuilder::extract_from_file(file, function_name)?;
Ok(CyclomaticComplexity::from_cfg(&cfg, Path::new(file), start_line, end_line))
}
fn estimate_complexity_from_function(func: &FunctionInfo, file: &Path) -> Option<CyclomaticComplexity> {
let base_complexity = 1u32;
let start_line = func.line_number;
let end_line = func.end_line_number.unwrap_or(start_line);
Some(CyclomaticComplexity {
function_name: func.name.clone(),
file: file.to_path_buf(),
line: start_line,
end_line,
complexity: base_complexity,
risk_level: RiskLevel::from_complexity(base_complexity),
decision_points: 0,
nodes: 0,
edges: 0,
})
}
#[allow(dead_code)]
pub fn calculate_graph_complexity(cfg: &CFGInfo) -> u32 {
cfg.cyclomatic_complexity_graph() as u32
}
#[allow(dead_code)]
pub fn count_boolean_operators(condition: &str) -> u32 {
let and_count = condition.matches("&&").count() as u32;
let or_count = condition.matches("||").count() as u32;
let py_and_count = count_word_boundary("and", condition) as u32;
let py_or_count = count_word_boundary("or", condition) as u32;
and_count + or_count + py_and_count + py_or_count
}
fn count_word_boundary(word: &str, text: &str) -> usize {
let word_len = word.len();
let text_bytes = text.as_bytes();
let word_bytes = word.as_bytes();
let mut count = 0;
for i in 0..=text.len().saturating_sub(word_len) {
if &text_bytes[i..i + word_len] == word_bytes {
let before_ok = i == 0 || !text_bytes[i - 1].is_ascii_alphanumeric();
let after_ok = i + word_len >= text.len()
|| !text_bytes[i + word_len].is_ascii_alphanumeric();
if before_ok && after_ok {
count += 1;
}
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Write;
use tempfile::NamedTempFile;
fn create_temp_file(content: &str, extension: &str) -> NamedTempFile {
let mut file = tempfile::Builder::new()
.suffix(extension)
.tempfile()
.expect("Failed to create temp file");
file.write_all(content.as_bytes())
.expect("Failed to write to temp file");
file
}
#[test]
fn test_risk_level_classification() {
assert_eq!(RiskLevel::from_complexity(1), RiskLevel::Low);
assert_eq!(RiskLevel::from_complexity(10), RiskLevel::Low);
assert_eq!(RiskLevel::from_complexity(11), RiskLevel::Medium);
assert_eq!(RiskLevel::from_complexity(20), RiskLevel::Medium);
assert_eq!(RiskLevel::from_complexity(21), RiskLevel::High);
assert_eq!(RiskLevel::from_complexity(50), RiskLevel::High);
assert_eq!(RiskLevel::from_complexity(51), RiskLevel::Critical);
assert_eq!(RiskLevel::from_complexity(100), RiskLevel::Critical);
}
#[test]
fn test_risk_level_display() {
assert_eq!(RiskLevel::Low.to_string(), "low");
assert_eq!(RiskLevel::Medium.to_string(), "medium");
assert_eq!(RiskLevel::High.to_string(), "high");
assert_eq!(RiskLevel::Critical.to_string(), "critical");
}
#[test]
fn test_simple_function_complexity() {
let source = r#"
def simple():
return 42
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok(), "Analysis should succeed");
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
assert_eq!(analysis.functions[0].function_name, "simple");
assert_eq!(analysis.functions[0].complexity, 1);
assert_eq!(analysis.functions[0].risk_level, RiskLevel::Low);
}
#[test]
fn test_if_statement_complexity() {
let source = r#"
def with_if(x):
if x > 0:
return 1
return 0
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
assert_eq!(analysis.functions[0].complexity, 2); }
#[test]
fn test_if_elif_else_complexity() {
let source = r#"
def with_elif(x):
if x > 0:
return "positive"
elif x < 0:
return "negative"
else:
return "zero"
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
assert_eq!(analysis.functions[0].complexity, 3); }
#[test]
fn test_loop_complexity() {
let source = r#"
def with_loop(items):
total = 0
for item in items:
total += item
return total
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
assert_eq!(analysis.functions[0].complexity, 2); }
#[test]
fn test_nested_complexity() {
let source = r#"
def nested(x, items):
if x > 0:
for item in items:
if item > x:
return item
return None
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
assert_eq!(analysis.functions[0].complexity, 4);
}
#[test]
fn test_class_method_complexity() {
let source = r#"
class Calculator:
def add(self, a, b):
return a + b
def smart_divide(self, a, b):
if b == 0:
return None
return a / b
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 2);
let add = analysis.functions.iter().find(|f| f.function_name == "Calculator.add");
let divide = analysis.functions.iter().find(|f| f.function_name == "Calculator.smart_divide");
assert!(add.is_some(), "Should find add method");
assert!(divide.is_some(), "Should find smart_divide method");
assert_eq!(add.unwrap().complexity, 1);
assert_eq!(divide.unwrap().complexity, 2);
}
#[test]
fn test_threshold_filtering() {
let source = r#"
def simple():
return 1
def complex_func(x, y, z):
if x > 0:
if y > 0:
if z > 0:
return "all positive"
else:
return "z negative"
else:
return "y negative"
else:
return "x negative"
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), Some(2));
assert!(result.is_ok());
let analysis = result.unwrap();
assert!(analysis.violations.is_some());
let violations = analysis.violations.unwrap();
assert_eq!(violations.len(), 1);
assert_eq!(violations[0].function_name, "complex_func");
}
#[test]
fn test_statistics_calculation() {
let complexities = vec![1, 2, 3, 10, 15, 25];
let stats = ComplexityStats::from_complexities(&complexities);
assert_eq!(stats.total_functions, 6);
assert_eq!(stats.min_complexity, 1);
assert_eq!(stats.max_complexity, 25);
assert!((stats.average_complexity - 9.33).abs() < 0.1);
assert_eq!(*stats.risk_distribution.get("low").unwrap_or(&0), 4);
assert_eq!(*stats.risk_distribution.get("medium").unwrap_or(&0), 1);
assert_eq!(*stats.risk_distribution.get("high").unwrap_or(&0), 1);
}
#[test]
fn test_empty_file_analysis() {
let source = "# Just a comment\n";
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 0);
assert_eq!(analysis.stats.total_functions, 0);
}
#[test]
fn test_boolean_operator_counting() {
assert_eq!(count_boolean_operators("a && b"), 1);
assert_eq!(count_boolean_operators("a || b"), 1);
assert_eq!(count_boolean_operators("a && b && c"), 2);
assert_eq!(count_boolean_operators("a || b && c"), 2);
assert_eq!(count_boolean_operators("a and b"), 1);
assert_eq!(count_boolean_operators("a or b"), 1);
assert_eq!(count_boolean_operators("a and b and c"), 2);
assert_eq!(count_boolean_operators("android"), 0);
assert_eq!(count_boolean_operators("valor"), 0);
}
#[test]
fn test_word_boundary_matching() {
assert_eq!(count_word_boundary("and", "a and b"), 1);
assert_eq!(count_word_boundary("and", "android"), 0);
assert_eq!(count_word_boundary("and", "band"), 0);
assert_eq!(count_word_boundary("or", "a or b"), 1);
assert_eq!(count_word_boundary("or", "for"), 0);
assert_eq!(count_word_boundary("or", "order"), 0);
}
#[test]
fn test_try_except_complexity() {
let source = r#"
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
result = 0
except TypeError:
result = None
return result
"#;
let file = create_temp_file(source, ".py");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 1);
assert_eq!(analysis.functions[0].complexity, 3);
}
#[test]
fn test_typescript_complexity() {
let source = r#"
function simple(): number {
return 42;
}
function withIf(x: number): string {
if (x > 0) {
return "positive";
}
return "non-positive";
}
"#;
let file = create_temp_file(source, ".ts");
let result = analyze_file_complexity(file.path(), None);
assert!(result.is_ok());
let analysis = result.unwrap();
assert_eq!(analysis.functions.len(), 2);
}
#[test]
fn test_nonexistent_file() {
let result = analyze_file_complexity("/nonexistent/path/file.py", None);
assert!(result.is_err());
}
#[test]
fn test_histogram_generation() {
let complexities = vec![1, 2, 3, 5, 6, 7, 10, 11, 15, 20, 25, 30];
let stats = ComplexityStats::from_complexities(&complexities);
assert!(!stats.histogram.is_empty());
assert_eq!(stats.histogram[0].count, 4);
assert_eq!(stats.histogram[0].label, "1-5");
assert_eq!(stats.histogram[1].count, 3);
}
#[test]
fn test_simd_risk_level_empty() {
let counts = RiskLevelCounts::count_simd(&[]);
assert_eq!(counts.low, 0);
assert_eq!(counts.medium, 0);
assert_eq!(counts.high, 0);
assert_eq!(counts.critical, 0);
}
#[test]
fn test_simd_risk_level_single_element() {
let counts = RiskLevelCounts::count_simd(&[5]);
assert_eq!(counts.low, 1);
let counts = RiskLevelCounts::count_simd(&[15]);
assert_eq!(counts.medium, 1);
let counts = RiskLevelCounts::count_simd(&[30]);
assert_eq!(counts.high, 1);
let counts = RiskLevelCounts::count_simd(&[100]);
assert_eq!(counts.critical, 1);
}
#[test]
fn test_simd_risk_level_boundaries() {
assert_eq!(RiskLevelCounts::count_simd(&[10]).low, 1);
assert_eq!(RiskLevelCounts::count_simd(&[10]).medium, 0);
assert_eq!(RiskLevelCounts::count_simd(&[11]).medium, 1);
assert_eq!(RiskLevelCounts::count_simd(&[20]).medium, 1);
assert_eq!(RiskLevelCounts::count_simd(&[21]).high, 1);
assert_eq!(RiskLevelCounts::count_simd(&[50]).high, 1);
assert_eq!(RiskLevelCounts::count_simd(&[51]).critical, 1);
}
#[test]
fn test_simd_risk_level_tail_only() {
let complexities = vec![1, 5, 10, 15, 25, 55];
let counts = RiskLevelCounts::count_simd(&complexities);
assert_eq!(counts.low, 3); assert_eq!(counts.medium, 1); assert_eq!(counts.high, 1); assert_eq!(counts.critical, 1); }
#[test]
fn test_simd_risk_level_exact_8() {
let complexities = vec![1, 5, 10, 15, 20, 25, 50, 100];
let counts = RiskLevelCounts::count_simd(&complexities);
assert_eq!(counts.low, 3); assert_eq!(counts.medium, 2); assert_eq!(counts.high, 2); assert_eq!(counts.critical, 1); }
#[test]
fn test_simd_risk_level_16_elements() {
let complexities = vec![
1, 2, 3, 4, 5, 6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 18, ];
let counts = RiskLevelCounts::count_simd(&complexities);
assert_eq!(counts.low, 8);
assert_eq!(counts.medium, 8);
assert_eq!(counts.high, 0);
assert_eq!(counts.critical, 0);
}
#[test]
fn test_simd_risk_level_17_elements() {
let complexities = vec![
1, 2, 3, 4, 5, 6, 7, 10, 21, 22, 23, 24, 25, 26, 27, 30, 51, ];
let counts = RiskLevelCounts::count_simd(&complexities);
assert_eq!(counts.low, 8);
assert_eq!(counts.medium, 0);
assert_eq!(counts.high, 8);
assert_eq!(counts.critical, 1);
}
#[test]
fn test_simd_risk_level_large_mixed() {
let mut complexities = Vec::with_capacity(1000);
for i in 0..250 {
complexities.push(i % 10 + 1); }
for i in 0..250 {
complexities.push(i % 10 + 11); }
for i in 0..250 {
complexities.push(i % 30 + 21); }
for _ in 0..250 {
complexities.push(100); }
let counts = RiskLevelCounts::count_simd(&complexities);
assert_eq!(counts.low, 250);
assert_eq!(counts.medium, 250);
assert_eq!(counts.high, 250);
assert_eq!(counts.critical, 250);
}
#[test]
fn test_simd_matches_scalar() {
let complexities: Vec<u32> = (1..=100).collect();
let simd_counts = RiskLevelCounts::count_simd(&complexities);
let mut low = 0;
let mut medium = 0;
let mut high = 0;
let mut critical = 0;
for &c in &complexities {
match c {
0..=10 => low += 1,
11..=20 => medium += 1,
21..=50 => high += 1,
_ => critical += 1,
}
}
assert_eq!(simd_counts.low, low);
assert_eq!(simd_counts.medium, medium);
assert_eq!(simd_counts.high, high);
assert_eq!(simd_counts.critical, critical);
}
#[test]
fn test_simd_to_hashmap() {
let counts = RiskLevelCounts {
low: 10,
medium: 5,
high: 3,
critical: 1,
};
let map = counts.to_hashmap();
assert_eq!(map.get("low"), Some(&10));
assert_eq!(map.get("medium"), Some(&5));
assert_eq!(map.get("high"), Some(&3));
assert_eq!(map.get("critical"), Some(&1));
}
#[test]
fn test_simd_to_hashmap_omits_zeros() {
let counts = RiskLevelCounts {
low: 5,
medium: 0,
high: 0,
critical: 0,
};
let map = counts.to_hashmap();
assert_eq!(map.get("low"), Some(&5));
assert_eq!(map.get("medium"), None);
assert_eq!(map.get("high"), None);
assert_eq!(map.get("critical"), None);
}
}