vtcode_eval/
test_quality.rs1use serde::{Deserialize, Serialize};
13
14#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
16pub struct TestQualitySummary {
17 pub total_tests: u64,
19 pub panic_only_tests: u64,
21 pub asymmetric_tests: u64,
23 pub structured_pbt_tests: u64,
25}
26
27impl TestQualitySummary {
28 pub fn panic_only_ratio(&self) -> f64 {
30 if self.total_tests == 0 {
31 return 0.0;
32 }
33 self.panic_only_tests as f64 / self.total_tests as f64
34 }
35
36 pub fn asymmetric_coverage(&self) -> f64 {
38 if self.total_tests == 0 {
39 return 0.0;
40 }
41 self.asymmetric_tests as f64 / self.total_tests as f64
42 }
43
44 pub fn structured_pbt_ratio(&self) -> f64 {
46 if self.total_tests == 0 {
47 return 0.0;
48 }
49 self.structured_pbt_tests as f64 / self.total_tests as f64
50 }
51
52 pub fn meets_agentic_testing_bar(&self) -> bool {
54 self.total_tests > 0 && self.panic_only_ratio() <= 0.5 && self.asymmetric_coverage() >= 0.2
55 }
56
57 pub fn merge(&mut self, other: &Self) {
59 self.total_tests = self.total_tests.saturating_add(other.total_tests);
60 self.panic_only_tests = self.panic_only_tests.saturating_add(other.panic_only_tests);
61 self.asymmetric_tests = self.asymmetric_tests.saturating_add(other.asymmetric_tests);
62 self.structured_pbt_tests = self.structured_pbt_tests.saturating_add(other.structured_pbt_tests);
63 }
64}
65
66pub fn analyze_test_source(source: &str) -> TestQualitySummary {
68 let mut summary = TestQualitySummary::default();
69 for block in split_test_blocks(source) {
70 summary.total_tests = summary.total_tests.saturating_add(1);
71 if is_panic_only(block) {
72 summary.panic_only_tests = summary.panic_only_tests.saturating_add(1);
73 }
74 if is_asymmetric(block) {
75 summary.asymmetric_tests = summary.asymmetric_tests.saturating_add(1);
76 }
77 if is_structured_pbt(block) {
78 summary.structured_pbt_tests = summary.structured_pbt_tests.saturating_add(1);
79 }
80 }
81 summary
82}
83
84fn split_test_blocks(source: &str) -> Vec<&str> {
85 let mut blocks = Vec::new();
86 let mut rest = source;
87 loop {
88 let attr_pos = ["#[test]", "#[tokio::test]", "#[rstest]"]
92 .iter()
93 .filter_map(|marker| rest.find(marker))
94 .min();
95 let pos = match attr_pos {
96 Some(pos) => pos,
97 None => match rest.find("proptest!") {
98 Some(pos) => pos,
99 None => break,
100 },
101 };
102 let next_attr = ["#[test]", "#[tokio::test]", "#[rstest]"]
103 .iter()
104 .filter_map(|marker| rest[pos + 1..].find(marker).map(|next| pos + 1 + next))
105 .min();
106 let next_prop = rest[pos + 1..].find("proptest!").map(|next| pos + 1 + next);
107 let next_test = match (next_attr, next_prop) {
108 (Some(attr), _) => Some(attr),
109 (None, prop) => prop,
110 };
111 let end = next_test.unwrap_or(rest.len());
112 blocks.push(&rest[pos..end]);
113 if next_test.is_none() {
114 break;
115 }
116 rest = &rest[end..];
117 }
118 blocks
119}
120
121fn is_panic_only(block: &str) -> bool {
122 !(block.contains("assert!") || block.contains("assert_") || block.contains("insta::") || block.contains("expect("))
126}
127
128fn is_asymmetric(block: &str) -> bool {
129 let lowered = block.to_ascii_lowercase();
130 if lowered.contains("both sides") {
131 return true;
132 }
133 let tokens: Vec<&str> = lowered
136 .split(|character: char| !character.is_ascii_alphanumeric())
137 .filter(|token| !token.is_empty())
138 .collect();
139 const STEMS: &[&str] = &[
140 "asymmetr",
141 "boundary",
142 "boundaries",
143 "revers",
144 "swap",
145 "transpos",
146 "palindrom",
147 "identical",
148 ];
149 const WORDS: &[&str] = &["order", "left", "right"];
150 tokens
151 .iter()
152 .any(|token| STEMS.iter().any(|stem| token.contains(stem)) || WORDS.contains(token))
153}
154
155fn is_structured_pbt(block: &str) -> bool {
156 block.contains("prop_oneof")
157 || block.contains("prop_compose")
158 || block.contains("Just(")
159 || block.contains("strategy")
160 || block.contains("prop::")
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166
167 #[test]
168 fn counts_oracle_backed_and_asymmetric_tests() {
169 let source = r#"
170 #[test]
171 fn transposed_order_is_rejected_asymmetric_boundary() {
172 assert_ne!(vec!["a", "b"], vec!["b", "a"]);
173 }
174 #[test]
175 fn never_panics_smoke() {
176 let _value = vec![1, 2, 3].len();
177 }
178 "#;
179 let summary = analyze_test_source(source);
180 assert_eq!(summary.total_tests, 2);
181 assert_eq!(summary.panic_only_tests, 1);
182 assert_eq!(summary.asymmetric_tests, 1);
183 assert!((summary.panic_only_ratio() - 0.5).abs() < f64::EPSILON);
184 assert!((summary.asymmetric_coverage() - 0.5).abs() < f64::EPSILON);
185 }
186
187 #[test]
188 fn detects_structured_proptest_strategies() {
189 let source = r#"
190 proptest! {
191 #[test]
192 fn order_swap_uses_structured_strategy(a in prop::option::of(Just(1))) {
193 prop_assert!(a.is_some());
194 }
195 }
196 "#;
197 let summary = analyze_test_source(source);
198 assert_eq!(summary.total_tests, 1);
199 assert_eq!(summary.panic_only_tests, 0);
200 assert_eq!(summary.structured_pbt_tests, 1);
201 assert!(summary.meets_agentic_testing_bar());
202 }
203
204 #[test]
205 fn empty_source_meets_no_bar_but_reports_zero_ratios() {
206 let summary = analyze_test_source("");
207 assert_eq!(summary.total_tests, 0);
208 assert_eq!(summary.panic_only_ratio(), 0.0);
209 assert_eq!(summary.asymmetric_coverage(), 0.0);
210 assert!(!summary.meets_agentic_testing_bar());
211 }
212
213 #[test]
214 fn prop_assume_and_panic_are_not_oracles() {
215 let assume_only = "#[test] fn filtered() { prop_assume!(true); }";
216 assert_eq!(analyze_test_source(assume_only).panic_only_tests, 1);
217 let panic_only = "#[test] fn todo() { panic!(\"todo\"); }";
218 assert_eq!(analyze_test_source(panic_only).panic_only_tests, 1);
219 let asserted = "#[test] fn checked() { assert!(true); }";
220 assert_eq!(analyze_test_source(asserted).panic_only_tests, 0);
221 }
222
223 #[test]
224 fn asymmetric_matching_uses_word_boundaries() {
225 assert_eq!(analyze_test_source("#[test] fn b() { let brightness = 1; assert!(true); }").asymmetric_tests, 0);
226 assert_eq!(analyze_test_source("#[test] fn b() { let leftover = 1; assert!(true); }").asymmetric_tests, 0);
227 assert_eq!(analyze_test_source("#[test] fn b() { fn render_border() {} assert!(true); }").asymmetric_tests, 0);
228 assert_eq!(
229 analyze_test_source("#[test] fn swapped() { assert_ne!(vec![\"a\"], vec![\"b\"]); }").asymmetric_tests,
230 1
231 );
232 }
233
234 #[test]
235 fn counts_tokio_tests() {
236 let source = "#[tokio::test] async fn async_case() { assert!(true); }";
237 assert_eq!(analyze_test_source(source).total_tests, 1);
238 }
239
240 #[test]
241 fn merge_sums_counters() {
242 let mut left = analyze_test_source("#[test] fn a() { assert!(true); }");
243 let right = analyze_test_source("#[test] fn b() { let _x = 1; }");
244 left.merge(&right);
245 assert_eq!(left.total_tests, 2);
246 assert_eq!(left.panic_only_tests, 1);
247 }
248}