rta_core/analyzers/
testing.rs1use crate::{analyzers::Analyzer, collector::RepositorySnapshot, model::TestingMaturity};
2
3pub struct TestingAnalyzer;
4
5impl Analyzer<TestingMaturity> for TestingAnalyzer {
6 fn analyze(&self, snapshot: &RepositorySnapshot) -> TestingMaturity {
7 let mut unit_test_files = 0;
8 let mut integration_test_files = 0;
9 let mut test_function_count = 0;
10
11 for file in snapshot.rust_files() {
12 let content = file.content.as_deref().unwrap_or_default();
13 let is_integration_test =
14 file.relative_path.starts_with("tests/") || file.relative_path.contains("/tests/");
15 let has_unit_tests = content.contains("#[cfg(test)]") || has_test_attribute(content);
16
17 if is_integration_test {
18 integration_test_files += 1;
19 test_function_count += count_test_functions(content);
20 } else if has_unit_tests {
21 unit_test_files += 1;
22 test_function_count += count_test_functions(content);
23 }
24 }
25
26 let has_tests = unit_test_files > 0 || integration_test_files > 0;
27 let testing_structure = if !has_tests {
28 "No Rust test structure detected.".to_string()
29 } else if integration_test_files > 0 && unit_test_files > 0 {
30 "Unit and integration testing structures detected.".to_string()
31 } else if integration_test_files > 0 {
32 "Integration tests detected; unit test coverage should be reviewed.".to_string()
33 } else {
34 "Unit tests detected; integration test coverage should be reviewed.".to_string()
35 };
36
37 let mut score = if has_tests { 55_i32 } else { 20_i32 };
38 score += (unit_test_files as i32 * 6).min(20);
39 score += (integration_test_files as i32 * 10).min(20);
40 score += (test_function_count as i32).min(15);
41
42 TestingMaturity {
43 has_tests,
44 unit_test_files,
45 integration_test_files,
46 test_function_count,
47 testing_structure,
48 score: score.clamp(0, 100) as u8,
49 }
50 }
51}
52
53fn count_test_functions(content: &str) -> usize {
54 content
55 .lines()
56 .map(str::trim)
57 .filter(|line| is_test_attribute(line))
58 .count()
59}
60
61fn has_test_attribute(content: &str) -> bool {
62 content.lines().map(str::trim).any(is_test_attribute)
63}
64
65fn is_test_attribute(line: &str) -> bool {
66 line == "#[test]" || line.starts_with("#[tokio::test")
67}
68
69#[cfg(test)]
70mod tests {
71 use std::path::PathBuf;
72
73 use crate::{
74 analyzers::Analyzer,
75 collector::{CargoManifest, FileSnapshot, RepositorySnapshot},
76 };
77
78 use super::TestingAnalyzer;
79
80 #[test]
81 fn empty_repository_has_low_testing_maturity() {
82 let report = TestingAnalyzer.analyze(&snapshot(Vec::new()));
83
84 assert!(!report.has_tests);
85 assert_eq!(report.score, 20);
86 }
87
88 #[test]
89 fn typical_repository_detects_unit_and_integration_tests() {
90 let report = TestingAnalyzer.analyze(&snapshot(vec![
91 file(
92 "src/lib.rs",
93 "#[cfg(test)]\nmod tests {\n#[test]\nfn it_works() {}\n}\n",
94 ),
95 file("tests/smoke.rs", "#[test]\nfn smoke() {}\n"),
96 ]));
97
98 assert!(report.has_tests);
99 assert_eq!(report.unit_test_files, 1);
100 assert_eq!(report.integration_test_files, 1);
101 assert_eq!(report.test_function_count, 2);
102 }
103
104 #[test]
105 fn extreme_test_count_caps_score_at_hundred() {
106 let mut content = String::new();
107 for index in 0..80 {
108 content.push_str(&format!("#[test]\nfn test_{index}() {{}}\n"));
109 }
110 let report = TestingAnalyzer.analyze(&snapshot(vec![
111 file(
112 "src/lib.rs",
113 "#[cfg(test)]\nmod tests {\n#[test]\nfn unit() {}\n}\n",
114 ),
115 file(
116 "src/core.rs",
117 "#[cfg(test)]\nmod tests {\n#[test]\nfn unit() {}\n}\n",
118 ),
119 file(
120 "src/api.rs",
121 "#[cfg(test)]\nmod tests {\n#[test]\nfn unit() {}\n}\n",
122 ),
123 file(
124 "src/db.rs",
125 "#[cfg(test)]\nmod tests {\n#[test]\nfn unit() {}\n}\n",
126 ),
127 file("tests/many.rs", &content),
128 file("tests/smoke.rs", "#[test]\nfn smoke() {}\n"),
129 ]));
130
131 assert_eq!(report.test_function_count, 85);
132 assert_eq!(report.score, 100);
133 }
134
135 #[test]
136 fn adversarial_commented_test_attribute_is_ignored() {
137 let report = TestingAnalyzer.analyze(&snapshot(vec![file(
138 "src/lib.rs",
139 "// #[test]\n// fn fake() {}\npub fn real() {}\n",
140 )]));
141
142 assert!(!report.has_tests);
143 assert_eq!(report.test_function_count, 0);
144 }
145
146 fn snapshot(files: Vec<FileSnapshot>) -> RepositorySnapshot {
147 RepositorySnapshot {
148 root: PathBuf::from("/tmp/repo"),
149 files,
150 manifests: Vec::<CargoManifest>::new(),
151 }
152 }
153
154 fn file(relative_path: &str, content: &str) -> FileSnapshot {
155 FileSnapshot {
156 path: PathBuf::from("/tmp/repo").join(relative_path),
157 relative_path: relative_path.into(),
158 extension: Some("rs".into()),
159 bytes: content.len() as u64,
160 lines: content.lines().count(),
161 content: Some(content.into()),
162 }
163 }
164}