1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
//! EXTREME TDD: TDG Score Normalization Tests
//!
//! These tests enforce that TDG scores are properly normalized to 0-100 range
//! regardless of component values, entropy contribution, or complexity analysis.
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod red_phase_tests {
use crate::tdg::{Grade, TdgScore};
/// RED TEST: TDG total score MUST always be in 0-100 range
///
/// This test will FAIL until normalization is properly implemented in calculate_total()
#[test]
fn red_test_tdg_total_must_be_normalized_to_0_100() {
let mut score = TdgScore::default();
// Test 1: Default score should be in range
score.calculate_total();
assert!(
score.total >= 0.0 && score.total <= 100.0,
"Default TDG total {} is outside 0-100 range",
score.total
);
// Test 2: All components at max should still normalize to 100
score.structural_complexity = 25.0;
score.semantic_complexity = 20.0;
score.duplication_ratio = 20.0;
score.coupling_score = 15.0;
score.doc_coverage = 10.0;
score.consistency_score = 10.0;
score.entropy_score = 0.0; // Should not break normalization
score.calculate_total();
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} with max components is outside 0-100 range",
score.total
);
assert!(
(score.total - 100.0).abs() < 0.01,
"TDG total {} should be ~100 when all components at max",
score.total
);
// Test 3: All components at 0 should give 0
score.structural_complexity = 0.0;
score.semantic_complexity = 0.0;
score.duplication_ratio = 0.0;
score.coupling_score = 0.0;
score.doc_coverage = 0.0;
score.consistency_score = 0.0;
score.entropy_score = 0.0;
score.calculate_total();
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} with zero components is outside 0-100 range",
score.total
);
assert!(
score.total.abs() < 0.01,
"TDG total {} should be ~0 when all components at 0",
score.total
);
}
/// RED TEST: Entropy score MUST NOT break 0-100 normalization
///
/// When entropy is added, the total must remain in 0-100 range
#[test]
fn red_test_entropy_must_not_break_normalization() {
let mut score = TdgScore::default();
// Test with various entropy values
for entropy in [0.0, 5.0, 10.0, 15.0, 20.0, 50.0] {
score.structural_complexity = 25.0;
score.semantic_complexity = 20.0;
score.duplication_ratio = 20.0;
score.coupling_score = 15.0;
score.doc_coverage = 10.0;
score.consistency_score = 10.0;
score.entropy_score = entropy;
score.calculate_total();
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} with entropy {} is outside 0-100 range",
score.total,
entropy
);
}
}
/// RED TEST: Component scores MUST be clamped to their weight limits
///
/// Individual components should never exceed their designated weight
#[test]
fn red_test_components_must_respect_weight_limits() {
// Try to set components beyond their limits
let mut score = TdgScore {
structural_complexity: 50.0, // Max should be ~25
semantic_complexity: 40.0, // Max should be ~20
duplication_ratio: 40.0, // Max should be ~20
coupling_score: 30.0, // Max should be ~15
doc_coverage: 20.0, // Max should be ~10
consistency_score: 20.0, // Max should be ~10
entropy_score: 100.0, // Should have a reasonable limit
..Default::default()
};
score.calculate_total();
// After normalization, total should still be in range
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} with excessive components is outside 0-100 range",
score.total
);
}
/// RED TEST: Grade MUST match the normalized score
///
/// Grade calculation should work correctly after normalization
#[test]
fn red_test_grade_must_match_normalized_score() {
// Enable contract coverage so this test validates raw grade mapping
// (the contract cap is tested separately in test_tdg_score_contract_coverage_*)
let mut score = TdgScore {
has_contract_coverage: true,
..Default::default()
};
// Test various score levels
let test_cases = vec![
(100.0, Grade::APLus),
(95.0, Grade::APLus),
(90.0, Grade::A),
(85.0, Grade::AMinus),
(75.0, Grade::B),
(60.0, Grade::C),
(50.0, Grade::D),
(40.0, Grade::F),
];
for (target_total, expected_grade) in test_cases {
// Set components to achieve target (simplified)
let component_value = target_total / 6.0; // 6 main components
score.structural_complexity = component_value * 25.0 / (100.0 / 6.0);
score.semantic_complexity = component_value * 20.0 / (100.0 / 6.0);
score.duplication_ratio = component_value * 20.0 / (100.0 / 6.0);
score.coupling_score = component_value * 15.0 / (100.0 / 6.0);
score.doc_coverage = component_value * 10.0 / (100.0 / 6.0);
score.consistency_score = component_value * 10.0 / (100.0 / 6.0);
score.entropy_score = 0.0;
score.calculate_total();
// Verify total is in range
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} targeting {} is outside 0-100 range",
score.total,
target_total
);
// Grade should be correctly calculated from normalized score
assert_eq!(
score.grade, expected_grade,
"Grade {:?} doesn't match expected {:?} for score {}",
score.grade, expected_grade, score.total
);
}
}
/// RED TEST: High complexity values must be properly bounded
///
/// Even with extreme penalty deductions, score should stay in range
#[test]
fn red_test_extreme_complexity_stays_in_bounds() {
// Simulate extreme penalty scenario where all components go negative
let mut score = TdgScore {
structural_complexity: -10.0, // Penalties exceeded starting value
semantic_complexity: -5.0,
duplication_ratio: -8.0,
coupling_score: -3.0,
doc_coverage: -2.0,
consistency_score: -4.0,
entropy_score: -1.0,
..Default::default()
};
score.calculate_total();
// Even with negative components, total must be clamped to 0-100
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} with negative components is outside 0-100 range",
score.total
);
assert!(
score.total >= 0.0,
"TDG total {} should be clamped to minimum of 0",
score.total
);
}
/// RED TEST: Entropy contribution must be balanced with other metrics
///
/// Entropy should have proportional weight, not dominate the score
#[test]
fn red_test_entropy_weight_is_balanced() {
// Set all non-entropy components to perfect
let mut score = TdgScore {
structural_complexity: 25.0,
semantic_complexity: 20.0,
duplication_ratio: 20.0,
coupling_score: 15.0,
doc_coverage: 10.0,
consistency_score: 10.0,
..Default::default()
};
// Test with max entropy
score.entropy_score = 100.0; // Unreasonably high
score.calculate_total();
// Total should still be in range (entropy should be weighted/clamped)
assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} with max entropy is outside 0-100 range",
score.total
);
// Entropy should not contribute more than ~10-15% of total
let total_without_entropy = 100.0;
let max_acceptable_total = 115.0; // Allow some buffer
assert!(
score.total <= max_acceptable_total,
"TDG total {} suggests entropy has too much weight (base was {})",
score.total,
total_without_entropy
);
}
}
#[cfg_attr(coverage_nightly, coverage(off))]
#[cfg(test)]
mod property_tests {
use crate::tdg::TdgScore;
use proptest::prelude::*;
proptest! {
/// Property: Any combination of component values must produce total in 0-100
#[test]
fn prop_any_components_produce_valid_total(
structural in -50.0f32..150.0,
semantic in -50.0f32..150.0,
duplication in -50.0f32..150.0,
coupling in -50.0f32..150.0,
doc in -50.0f32..150.0,
consistency in -50.0f32..150.0,
entropy in -50.0f32..150.0,
) {
let mut score = TdgScore {
structural_complexity: structural,
semantic_complexity: semantic,
duplication_ratio: duplication,
coupling_score: coupling,
doc_coverage: doc,
consistency_score: consistency,
..Default::default()
};
score.entropy_score = entropy;
score.calculate_total();
prop_assert!(
score.total >= 0.0 && score.total <= 100.0,
"TDG total {} is outside 0-100 range with components: struct={}, sem={}, dup={}, coup={}, doc={}, cons={}, ent={}",
score.total, structural, semantic, duplication, coupling, doc, consistency, entropy
);
}
/// Property: Grade must always be valid for the total score
#[test]
fn prop_grade_always_matches_total(
structural in 0.0f32..30.0,
semantic in 0.0f32..25.0,
duplication in 0.0f32..25.0,
coupling in 0.0f32..20.0,
doc in 0.0f32..15.0,
consistency in 0.0f32..15.0,
) {
let mut score = TdgScore {
structural_complexity: structural,
semantic_complexity: semantic,
duplication_ratio: duplication,
coupling_score: coupling,
doc_coverage: doc,
consistency_score: consistency,
..Default::default()
};
score.entropy_score = 0.0;
score.calculate_total();
// Verify grade matches the score range, accounting for
// CB-1400 contract coverage cap (A/A+ → A- without contracts)
let raw_grade = crate::tdg::Grade::from_score(score.total);
let expected_grade = if !score.has_contract_coverage && raw_grade < crate::tdg::Grade::AMinus {
crate::tdg::Grade::AMinus
} else {
raw_grade
};
prop_assert_eq!(score.grade, expected_grade);
}
}
}