pub mod cascade;
pub mod effort;
pub mod learning;
pub mod models;
pub mod reduction;
#[cfg(test)]
mod tests;
use crate::core::ComplexityMetrics;
use im::{HashMap, Vector};
use std::path::PathBuf;
pub use cascade::{CascadeCalculator, CascadeImpact};
pub use effort::{AdvancedEffortModel, EffortEstimate, EffortModel};
pub use learning::{ROILearningSystem, ROIOutcome};
pub use models::{ROIBreakdown, ROIComponent, ROI};
pub use reduction::{RiskReduction, RiskReductionModel};
use super::priority::TestTarget;
use super::RiskAnalyzer;
pub struct ROICalculator {
effort_model: Box<dyn EffortModel>,
risk_model: Box<dyn RiskReductionModel>,
cascade_calculator: CascadeCalculator,
learning_system: Option<ROILearningSystem>,
config: ROIConfig,
}
#[derive(Clone, Debug)]
pub struct ROIConfig {
pub cascade_weight: f64,
pub confidence_threshold: f64,
pub max_cascade_depth: usize,
pub learning_enabled: bool,
}
impl Default for ROIConfig {
fn default() -> Self {
Self {
cascade_weight: 0.5,
confidence_threshold: 0.1,
max_cascade_depth: 3,
learning_enabled: false,
}
}
}
impl ROICalculator {
pub fn new(risk_analyzer: RiskAnalyzer) -> Self {
Self {
effort_model: Box::new(AdvancedEffortModel::new()),
risk_model: Box::new(reduction::AdvancedRiskReductionModel::new(risk_analyzer)),
cascade_calculator: CascadeCalculator::new(),
learning_system: None,
config: ROIConfig::default(),
}
}
pub fn with_learning(mut self, learning_system: ROILearningSystem) -> Self {
self.learning_system = Some(learning_system);
self
}
pub fn calculate(&self, target: &TestTarget, context: &Context) -> ROI {
let effort = self.estimate_effort(target, context);
let direct_impact = self.calculate_direct_impact(target);
let cascade_impact = self.cascade_calculator.calculate(target, context);
let confidence = self.calculate_confidence(target, &effort, &direct_impact);
let module_multiplier = self.get_module_type_multiplier(target);
let total_impact = (direct_impact.percentage * module_multiplier)
+ (cascade_impact.total_risk_reduction * self.config.cascade_weight);
let adjusted_effort = self.adjust_effort_with_learning(effort.hours, target);
let dependency_factor = 1.0 + (target.dependents.len() as f64 * 0.1).min(1.0);
let complexity_weight = self.get_complexity_weight(target);
let raw_roi = if adjusted_effort > 0.0 {
(total_impact * dependency_factor * complexity_weight) / adjusted_effort
} else {
total_impact * dependency_factor * complexity_weight
};
let scaled_roi = if raw_roi > 20.0 {
10.0 + (raw_roi - 20.0).ln()
} else if raw_roi > 10.0 {
5.0 + (raw_roi - 10.0) * 0.5
} else {
raw_roi
};
let value = (scaled_roi * confidence).max(0.1);
ROI {
value,
effort: effort.clone(),
direct_impact: direct_impact.clone(),
cascade_impact: cascade_impact.clone(),
confidence,
breakdown: self.generate_breakdown(target, &effort, &direct_impact, &cascade_impact),
}
}
fn estimate_effort(&self, target: &TestTarget, _context: &Context) -> EffortEstimate {
self.effort_model.estimate(target)
}
fn calculate_direct_impact(&self, target: &TestTarget) -> RiskReduction {
self.risk_model.calculate(target)
}
fn calculate_confidence(
&self,
target: &TestTarget,
effort: &EffortEstimate,
_impact: &RiskReduction,
) -> f64 {
let complexity_confidence = match target.complexity.cyclomatic_complexity {
0..=5 => 0.9,
6..=10 => 0.8,
11..=20 => 0.7,
_ => 0.6,
};
let coverage_confidence = if target.current_coverage == 0.0 {
0.95
} else {
0.8
};
let effort_confidence = match effort.hours {
h if h <= 2.0 => 0.9,
h if h <= 5.0 => 0.8,
h if h <= 10.0 => 0.7,
_ => 0.6,
};
f64::max(
complexity_confidence * coverage_confidence * effort_confidence,
0.5,
)
}
fn adjust_effort_with_learning(&self, base_effort: f64, target: &TestTarget) -> f64 {
if let Some(ref learning) = self.learning_system {
learning.adjust_estimate(base_effort, target)
} else {
base_effort
}
}
fn get_module_type_multiplier(&self, target: &TestTarget) -> f64 {
use super::priority::ModuleType;
match target.module_type {
ModuleType::EntryPoint => 2.0, ModuleType::Core => 1.5, ModuleType::Api => 1.2, ModuleType::Model => 1.1, ModuleType::IO => 1.0, _ => 1.0, }
}
fn get_complexity_weight(&self, target: &TestTarget) -> f64 {
match (
target.complexity.cyclomatic_complexity,
target.complexity.cognitive_complexity,
) {
(1, 0..=1) => 0.1, (1, 2..=3) => 0.3, (2..=3, _) => 0.5, (4..=5, _) => 0.7, _ => 1.0, }
}
fn generate_breakdown(
&self,
target: &TestTarget,
effort: &EffortEstimate,
direct: &RiskReduction,
cascade: &CascadeImpact,
) -> ROIBreakdown {
let mut components = Vec::new();
components.push(ROIComponent {
name: "Direct Risk Reduction".to_string(),
value: direct.percentage,
weight: 1.0,
explanation: format!(
"Reduces risk from {:.1} to {:.1}",
target.current_risk,
target.current_risk - direct.absolute
),
});
components.push(ROIComponent {
name: "Cascade Impact".to_string(),
value: cascade.total_risk_reduction,
weight: self.config.cascade_weight,
explanation: if cascade.affected_modules.is_empty() && !target.dependents.is_empty() {
format!(
"Potentially affects {} dependent modules",
target.dependents.len()
)
} else if !cascade.affected_modules.is_empty() {
format!(
"Affects {} dependent modules (depth: {})",
cascade.affected_modules.len(),
cascade.propagation_depth
)
} else {
"No cascade impact detected".to_string()
},
});
components.push(ROIComponent {
name: "Effort Required".to_string(),
value: effort.hours,
weight: -1.0,
explanation: format!(
"{} test cases, {:.1} hours",
effort.test_cases, effort.hours
),
});
let module_multiplier = self.get_module_type_multiplier(target);
let dependency_factor = 1.0 + (target.dependents.len() as f64 * 0.1).min(1.0);
let complexity_weight = self.get_complexity_weight(target);
let formula = format!(
"ROI = ((Direct[{:.1}%] * {:.1}) + (Cascade[{:.1}%] * {:.1})) * DependencyFactor[{:.1}] * ComplexityWeight[{:.1}] / Effort[{:.1}h]",
direct.percentage,
module_multiplier,
cascade.total_risk_reduction,
self.config.cascade_weight,
dependency_factor,
complexity_weight,
effort.hours
);
let explanation = self.generate_explanation(target, effort, direct);
ROIBreakdown {
components,
formula,
explanation,
confidence_factors: vec![],
}
}
fn generate_explanation(
&self,
target: &TestTarget,
effort: &EffortEstimate,
impact: &RiskReduction,
) -> String {
let coverage_str = if target.current_coverage == 0.0 {
"completely untested".to_string()
} else {
format!("{:.0}% covered", target.current_coverage)
};
let complexity_str = format!(
"cyclomatic {} / cognitive {}",
target.complexity.cyclomatic_complexity, target.complexity.cognitive_complexity
);
let impact_str = if !target.dependents.is_empty() {
format!(" affecting {} dependent modules", target.dependents.len())
} else if !target.dependencies.is_empty() {
format!(" with {} dependencies", target.dependencies.len())
} else {
String::new()
};
format!(
"Currently {coverage_str} with {complexity_str}{impact_str}. \
Testing would reduce risk by {:.1}% with {:.1} hours effort ({} test cases)",
impact.percentage, effort.hours, effort.test_cases
)
}
}
#[derive(Clone, Debug)]
pub struct Context {
pub dependency_graph: DependencyGraph,
pub critical_paths: Vec<PathBuf>,
pub historical_data: Option<HistoricalData>,
}
#[derive(Clone, Debug)]
pub struct DependencyGraph {
pub nodes: HashMap<String, DependencyNode>,
pub edges: Vector<DependencyEdge>,
}
#[derive(Clone, Debug)]
pub struct DependencyNode {
pub id: String,
pub path: PathBuf,
pub risk: f64,
pub complexity: ComplexityMetrics,
}
#[derive(Clone, Debug)]
pub struct DependencyEdge {
pub from: String,
pub to: String,
pub weight: f64,
}
#[derive(Clone, Debug)]
pub struct HistoricalData {
pub change_frequency: HashMap<PathBuf, usize>,
pub bug_density: HashMap<PathBuf, f64>,
pub test_effectiveness: HashMap<PathBuf, f64>,
}