1use serde::Serialize;
2
3use crate::extractor::FileAnalysis;
4
5#[derive(Debug, Clone, Default, Serialize)]
7pub struct ProjectMetrics {
8 pub mock_density_avg: f64,
9 pub mock_class_max: usize,
10 pub parameterized_ratio: f64,
11 pub pbt_ratio: f64,
12 pub assertion_density_avg: f64,
13 pub contract_coverage: f64,
14 pub test_source_ratio: f64,
15}
16
17pub fn compute_metrics(analyses: &[FileAnalysis], source_file_count: usize) -> ProjectMetrics {
18 let total_files = analyses.len();
19 let total_functions: usize = analyses.iter().map(|a| a.functions.len()).sum();
20
21 if total_functions == 0 {
22 let pbt_ratio = if total_files > 0 {
23 analyses.iter().filter(|a| a.has_pbt_import).count() as f64 / total_files as f64
24 } else {
25 0.0
26 };
27 let contract_coverage = if total_files > 0 {
28 analyses.iter().filter(|a| a.has_contract_import).count() as f64 / total_files as f64
29 } else {
30 0.0
31 };
32 let test_source_ratio = if source_file_count > 0 {
33 total_files as f64 / source_file_count as f64
34 } else {
35 0.0
36 };
37 return ProjectMetrics {
38 pbt_ratio,
39 contract_coverage,
40 test_source_ratio,
41 ..Default::default()
42 };
43 }
44
45 let all_funcs: Vec<_> = analyses.iter().flat_map(|a| &a.functions).collect();
46
47 let mock_density_avg = all_funcs
48 .iter()
49 .map(|f| f.analysis.mock_count)
50 .sum::<usize>() as f64
51 / total_functions as f64;
52 let mock_class_max = all_funcs
53 .iter()
54 .map(|f| f.analysis.mock_classes.len())
55 .max()
56 .unwrap_or(0);
57 let total_parameterized: usize = analyses.iter().map(|a| a.parameterized_count).sum();
58 let parameterized_ratio = total_parameterized as f64 / total_functions as f64;
59 let pbt_ratio =
60 analyses.iter().filter(|a| a.has_pbt_import).count() as f64 / total_files as f64;
61 let assertion_density_avg = all_funcs
62 .iter()
63 .map(|f| f.analysis.assertion_count)
64 .sum::<usize>() as f64
65 / total_functions as f64;
66 let contract_coverage =
67 analyses.iter().filter(|a| a.has_contract_import).count() as f64 / total_files as f64;
68 let test_source_ratio = if source_file_count > 0 {
69 total_files as f64 / source_file_count as f64
70 } else {
71 0.0
72 };
73
74 ProjectMetrics {
75 mock_density_avg,
76 mock_class_max,
77 parameterized_ratio,
78 pbt_ratio,
79 assertion_density_avg,
80 contract_coverage,
81 test_source_ratio,
82 }
83}
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88 use crate::extractor::{TestAnalysis, TestFunction};
89
90 fn make_func(
91 name: &str,
92 file: &str,
93 assertion_count: usize,
94 mock_count: usize,
95 mock_classes: Vec<String>,
96 ) -> TestFunction {
97 TestFunction {
98 name: name.to_string(),
99 file: file.to_string(),
100 line: 1,
101 end_line: 10,
102 analysis: TestAnalysis {
103 assertion_count,
104 mock_count,
105 mock_classes,
106 ..Default::default()
107 },
108 }
109 }
110
111 fn make_analysis(
112 file: &str,
113 functions: Vec<TestFunction>,
114 has_pbt: bool,
115 has_contract: bool,
116 parameterized: usize,
117 ) -> FileAnalysis {
118 FileAnalysis {
119 file: file.to_string(),
120 functions,
121 has_pbt_import: has_pbt,
122 has_contract_import: has_contract,
123 has_error_test: false,
124 has_relational_assertion: false,
125 parameterized_count: parameterized,
126 }
127 }
128
129 #[test]
130 fn empty_analyses_returns_all_zeros() {
131 let m = compute_metrics(&[], 0);
132 assert_eq!(m.mock_density_avg, 0.0);
133 assert_eq!(m.mock_class_max, 0);
134 assert_eq!(m.parameterized_ratio, 0.0);
135 assert_eq!(m.pbt_ratio, 0.0);
136 assert_eq!(m.assertion_density_avg, 0.0);
137 assert_eq!(m.contract_coverage, 0.0);
138 assert_eq!(m.test_source_ratio, 0.0);
139 }
140
141 #[test]
142 fn single_file_single_function_correct_values() {
143 let funcs = vec![make_func("test_a", "test.py", 3, 2, vec!["Db".into()])];
144 let analyses = vec![make_analysis("test.py", funcs, true, true, 1)];
145 let m = compute_metrics(&analyses, 5);
146 assert_eq!(m.mock_density_avg, 2.0);
147 assert_eq!(m.mock_class_max, 1);
148 assert_eq!(m.parameterized_ratio, 1.0); assert_eq!(m.pbt_ratio, 1.0); assert_eq!(m.assertion_density_avg, 3.0);
151 assert_eq!(m.contract_coverage, 1.0); assert!((m.test_source_ratio - 0.2).abs() < f64::EPSILON); }
154
155 #[test]
156 fn multiple_files_proper_aggregation() {
157 let funcs1 = vec![
158 make_func("test_a", "a.py", 2, 4, vec!["Db".into(), "Api".into()]),
159 make_func("test_b", "a.py", 1, 0, vec![]),
160 ];
161 let funcs2 = vec![make_func("test_c", "b.py", 3, 2, vec!["Cache".into()])];
162 let analyses = vec![
163 make_analysis("a.py", funcs1, true, false, 1),
164 make_analysis("b.py", funcs2, false, true, 0),
165 ];
166 let m = compute_metrics(&analyses, 4);
167 assert_eq!(m.mock_density_avg, 2.0);
169 assert_eq!(m.mock_class_max, 2);
171 assert!((m.parameterized_ratio - 1.0 / 3.0).abs() < 0.001);
173 assert_eq!(m.pbt_ratio, 0.5);
175 assert_eq!(m.assertion_density_avg, 2.0);
177 assert_eq!(m.contract_coverage, 0.5);
179 assert_eq!(m.test_source_ratio, 0.5);
181 }
182
183 #[test]
184 fn zero_source_files_test_source_ratio_zero() {
185 let funcs = vec![make_func("test_a", "test.py", 1, 0, vec![])];
186 let analyses = vec![make_analysis("test.py", funcs, false, false, 0)];
187 let m = compute_metrics(&analyses, 0);
188 assert_eq!(m.test_source_ratio, 0.0);
189 }
190
191 #[test]
192 fn zero_functions_all_ratios_zero() {
193 let analyses = vec![make_analysis("test.py", vec![], false, false, 0)];
194 let m = compute_metrics(&analyses, 5);
195 assert_eq!(m.mock_density_avg, 0.0);
196 assert_eq!(m.mock_class_max, 0);
197 assert_eq!(m.parameterized_ratio, 0.0);
198 assert_eq!(m.assertion_density_avg, 0.0);
199 assert_eq!(m.pbt_ratio, 0.0); assert_eq!(m.contract_coverage, 0.0); }
203
204 #[test]
205 fn mock_class_max_is_per_function_max() {
206 let funcs = vec![
207 make_func(
208 "test_a",
209 "test.py",
210 1,
211 3,
212 vec!["A".into(), "B".into(), "C".into()],
213 ),
214 make_func("test_b", "test.py", 1, 1, vec!["D".into()]),
215 ];
216 let analyses = vec![make_analysis("test.py", funcs, false, false, 0)];
217 let m = compute_metrics(&analyses, 1);
218 assert_eq!(m.mock_class_max, 3); }
220
221 #[test]
222 fn parameterized_ratio_sums_across_files() {
223 let funcs1 = vec![
224 make_func("test_a", "a.py", 1, 0, vec![]),
225 make_func("test_b", "a.py", 1, 0, vec![]),
226 ];
227 let funcs2 = vec![make_func("test_c", "b.py", 1, 0, vec![])];
228 let analyses = vec![
229 make_analysis("a.py", funcs1, false, false, 1),
230 make_analysis("b.py", funcs2, false, false, 1),
231 ];
232 let m = compute_metrics(&analyses, 1);
233 assert!((m.parameterized_ratio - 2.0 / 3.0).abs() < 0.001);
235 }
236
237 #[test]
238 fn pbt_contract_count_files_with_import() {
239 let analyses = vec![
240 make_analysis(
241 "a.py",
242 vec![make_func("t", "a.py", 1, 0, vec![])],
243 true,
244 false,
245 0,
246 ),
247 make_analysis(
248 "b.py",
249 vec![make_func("t", "b.py", 1, 0, vec![])],
250 false,
251 true,
252 0,
253 ),
254 make_analysis(
255 "c.py",
256 vec![make_func("t", "c.py", 1, 0, vec![])],
257 true,
258 true,
259 0,
260 ),
261 ];
262 let m = compute_metrics(&analyses, 1);
263 assert!((m.pbt_ratio - 2.0 / 3.0).abs() < 0.001);
265 assert!((m.contract_coverage - 2.0 / 3.0).abs() < 0.001);
267 }
268}