1use super::super::priority::{ModuleType, TestTarget};
2
3pub trait EffortModel: Send + Sync {
4 fn estimate(&self, target: &TestTarget) -> EffortEstimate;
5 fn explain(&self, estimate: &EffortEstimate) -> String;
6}
7
8#[derive(Clone, Debug)]
9pub struct EffortEstimate {
10 pub hours: f64,
11 pub test_cases: usize,
12 pub complexity: ComplexityLevel,
13 pub breakdown: EffortBreakdown,
14}
15
16#[derive(Clone, Debug, PartialEq)]
17pub enum ComplexityLevel {
18 Trivial,
19 Simple,
20 Moderate,
21 Complex,
22 VeryComplex,
23}
24
25#[derive(Clone, Debug)]
26pub struct EffortBreakdown {
27 pub base: f64,
28 pub setup: f64,
29 pub mocking: f64,
30 pub understanding: f64,
31}
32
33pub struct AdvancedEffortModel {
34 base_rates: EffortRates,
35 complexity_factors: ComplexityFactors,
36}
37
38#[derive(Clone, Debug)]
39struct EffortRates {
40 per_test_case: f64,
41 per_dependency: f64,
42 cognitive_penalty: f64,
43}
44
45impl Default for EffortRates {
46 fn default() -> Self {
47 Self {
48 per_test_case: 0.25,
49 per_dependency: 0.15,
50 cognitive_penalty: 0.1,
51 }
52 }
53}
54
55#[derive(Clone, Debug)]
56struct ComplexityFactors {
57 cyclomatic_base: f64,
58 cognitive_weight: f64,
59 nesting_penalty: f64,
60}
61
62impl Default for ComplexityFactors {
63 fn default() -> Self {
64 Self {
65 cyclomatic_base: 1.0,
66 cognitive_weight: 0.1,
67 nesting_penalty: 0.2,
68 }
69 }
70}
71
72impl Default for AdvancedEffortModel {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl AdvancedEffortModel {
79 pub fn new() -> Self {
80 Self {
81 base_rates: EffortRates::default(),
82 complexity_factors: ComplexityFactors::default(),
83 }
84 }
85
86 fn calculate_base_effort(&self, target: &TestTarget) -> f64 {
87 let min_cases = (target.complexity.cyclomatic_complexity + 1) as f64;
88 let cognitive_factor = (target.complexity.cognitive_complexity as f64 / 7.0).max(1.0);
89 let case_hours = min_cases * self.base_rates.per_test_case;
90
91 let complexity_multiplier = self.complexity_factors.cyclomatic_base
93 + (target.complexity.cognitive_complexity as f64
94 * self.complexity_factors.cognitive_weight)
95 + ((target.complexity.cyclomatic_complexity as f64 / 10.0)
96 * self.complexity_factors.nesting_penalty);
97
98 case_hours * cognitive_factor * complexity_multiplier.max(1.0)
99 }
100
101 fn estimate_setup_effort(&self, target: &TestTarget) -> f64 {
102 let dependency_count = target.dependencies.len();
103 let module_factor = match target.module_type {
104 ModuleType::EntryPoint => 2.0,
105 ModuleType::IO => 1.5,
106 ModuleType::Api => 1.3,
107 ModuleType::Core => 1.0,
108 _ => 0.5,
109 };
110
111 let dependency_effort = dependency_count as f64 * self.base_rates.per_dependency;
113
114 let base_effort = match dependency_count {
115 0 => 0.0,
116 1..=3 => 0.5 * module_factor,
117 4..=7 => 1.0 * module_factor,
118 8..=12 => 1.5 * module_factor,
119 _ => 2.0 * module_factor,
120 };
121
122 base_effort + dependency_effort
123 }
124
125 fn estimate_mocking_effort(&self, target: &TestTarget) -> f64 {
126 let external_deps = self.count_external_dependencies(target);
127 self.calculate_mocking_hours(external_deps)
128 }
129
130 fn count_external_dependencies(&self, target: &TestTarget) -> usize {
131 const EXTERNAL_MARKERS: &[&str] = &["io", "net", "fs", "db", "http"];
132
133 target
134 .dependencies
135 .iter()
136 .filter(|dep| EXTERNAL_MARKERS.iter().any(|marker| dep.contains(marker)))
137 .count()
138 }
139
140 fn calculate_mocking_hours(&self, external_deps: usize) -> f64 {
141 match external_deps {
142 0 => 0.0,
143 1 => 0.5,
144 2 => 1.0,
145 3 => 1.5,
146 _ => 2.0 + (external_deps as f64 - 3.0) * 0.25,
147 }
148 }
149
150 fn estimate_understanding_effort(&self, target: &TestTarget) -> f64 {
151 let cognitive = target.complexity.cognitive_complexity;
152 let lines = target.lines;
153
154 let cognitive_hours = match cognitive {
155 0..=7 => 0.0,
156 8..=15 => 0.5,
157 16..=30 => 1.0,
158 31..=50 => 2.0,
159 _ => 3.0,
160 };
161
162 let size_factor = match lines {
163 0..=50 => 1.0,
164 51..=100 => 1.2,
165 101..=200 => 1.5,
166 201..=500 => 2.0,
167 _ => 2.5,
168 };
169
170 let cognitive_penalty = if cognitive > 30 {
172 self.base_rates.cognitive_penalty * ((cognitive - 30) as f64 / 10.0)
173 } else {
174 0.0
175 };
176
177 cognitive_hours * size_factor + cognitive_penalty
178 }
179
180 fn estimate_test_cases(&self, target: &TestTarget) -> usize {
181 let min_cases = target.complexity.cyclomatic_complexity + 1;
182
183 let edge_cases = match target.module_type {
184 ModuleType::Api | ModuleType::IO => 3,
185 ModuleType::Core | ModuleType::EntryPoint => 2,
186 _ => 1,
187 };
188
189 let error_cases = if !target.dependencies.is_empty() {
190 (target.dependencies.len() / 2).max(1) as u32
191 } else {
192 0
193 };
194
195 (min_cases + edge_cases + error_cases) as usize
196 }
197
198 fn categorize_complexity(&self, hours: f64) -> ComplexityLevel {
199 match hours {
200 h if h <= 0.5 => ComplexityLevel::Trivial,
201 h if h <= 2.0 => ComplexityLevel::Simple,
202 h if h <= 5.0 => ComplexityLevel::Moderate,
203 h if h <= 10.0 => ComplexityLevel::Complex,
204 _ => ComplexityLevel::VeryComplex,
205 }
206 }
207}
208
209impl EffortModel for AdvancedEffortModel {
210 fn estimate(&self, target: &TestTarget) -> EffortEstimate {
211 let base = self.calculate_base_effort(target);
212 let setup = self.estimate_setup_effort(target);
213 let mocking = self.estimate_mocking_effort(target);
214 let understanding = self.estimate_understanding_effort(target);
215
216 let total_hours = base + setup + mocking + understanding;
217
218 EffortEstimate {
219 hours: total_hours,
220 test_cases: self.estimate_test_cases(target),
221 complexity: self.categorize_complexity(total_hours),
222 breakdown: EffortBreakdown {
223 base,
224 setup,
225 mocking,
226 understanding,
227 },
228 }
229 }
230
231 fn explain(&self, estimate: &EffortEstimate) -> String {
232 format!(
233 "Estimated effort: {:.1} hours ({} test cases)\n\
234 - Base testing: {:.1}h\n\
235 - Setup/teardown: {:.1}h\n\
236 - Mocking dependencies: {:.1}h\n\
237 - Understanding code: {:.1}h\n\
238 Complexity level: {:?}",
239 estimate.hours,
240 estimate.test_cases,
241 estimate.breakdown.base,
242 estimate.breakdown.setup,
243 estimate.breakdown.mocking,
244 estimate.breakdown.understanding,
245 estimate.complexity
246 )
247 }
248}