organizational_intelligence_plugin/
pmat.rs1use anyhow::{anyhow, Result};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::path::Path;
9use std::process::Command;
10use tracing::{debug, info, warn};
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FileTdgScore {
15 pub path: String,
16 pub score: f32,
17 pub grade: String,
18}
19
20#[derive(Debug, Clone)]
22pub struct TdgAnalysis {
23 pub file_scores: HashMap<String, f32>,
25 pub average_score: f32,
27 pub max_score: f32,
29}
30
31pub struct PmatIntegration;
33
34impl PmatIntegration {
35 pub fn analyze_tdg<P: AsRef<Path>>(repo_path: P) -> Result<TdgAnalysis> {
53 let path = repo_path.as_ref();
54 info!("Running pmat TDG analysis on {:?}", path);
55
56 if !Self::is_pmat_available() {
58 warn!("pmat command not found - TDG analysis unavailable");
59 return Err(anyhow!("pmat command not available in PATH"));
60 }
61
62 let output = Command::new("pmat")
64 .args(["analyze", "tdg", "--path"])
65 .arg(path)
66 .args(["--format", "json"])
67 .output()
68 .map_err(|e| anyhow!("Failed to execute pmat: {}", e))?;
69
70 if !output.status.success() {
71 let stderr = String::from_utf8_lossy(&output.stderr);
72 return Err(anyhow!("pmat tdg failed: {}", stderr));
73 }
74
75 let stdout = String::from_utf8_lossy(&output.stdout);
77 debug!("pmat output: {}", stdout);
78
79 Self::parse_tdg_output(&stdout)
80 }
81
82 fn is_pmat_available() -> bool {
84 Command::new("pmat")
85 .arg("--version")
86 .output()
87 .map(|output| output.status.success())
88 .unwrap_or(false)
89 }
90
91 fn parse_tdg_output(json_output: &str) -> Result<TdgAnalysis> {
93 #[derive(Deserialize)]
97 struct PmatFile {
98 file_path: String,
99 total: f32,
100 #[allow(dead_code)]
101 #[serde(default)]
102 grade: String,
103 }
104
105 #[derive(Deserialize)]
106 struct PmatOutput {
107 files: Vec<PmatFile>,
108 }
109
110 let parsed: PmatOutput = serde_json::from_str(json_output)
111 .map_err(|e| anyhow!("Failed to parse pmat JSON: {}", e))?;
112
113 let mut file_scores = HashMap::new();
114 let mut total_score = 0.0_f32;
115 let mut max_score = 0.0_f32;
116
117 for file in &parsed.files {
118 file_scores.insert(file.file_path.clone(), file.total);
119 total_score += file.total;
120 max_score = max_score.max(file.total);
121 }
122
123 let average_score = if parsed.files.is_empty() {
124 0.0
125 } else {
126 total_score / parsed.files.len() as f32
127 };
128
129 Ok(TdgAnalysis {
130 file_scores,
131 average_score,
132 max_score,
133 })
134 }
135
136 pub fn get_file_score(analysis: &TdgAnalysis, file_path: &str) -> Option<f32> {
146 analysis.file_scores.get(file_path).copied()
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn test_parse_tdg_output() {
156 let json = r#"{
157 "files": [
158 {"file_path": "src/main.rs", "total": 95.0, "grade": "APLus"},
159 {"file_path": "src/lib.rs", "total": 88.0, "grade": "A"}
160 ]
161 }"#;
162
163 let result = PmatIntegration::parse_tdg_output(json).unwrap();
164
165 assert_eq!(result.average_score, 91.5);
166 assert_eq!(result.max_score, 95.0);
167 assert_eq!(result.file_scores.len(), 2);
168 assert_eq!(result.file_scores.get("src/main.rs"), Some(&95.0));
169 }
170
171 #[test]
172 fn test_parse_empty_tdg_output() {
173 let json = r#"{
174 "files": []
175 }"#;
176
177 let result = PmatIntegration::parse_tdg_output(json).unwrap();
178
179 assert_eq!(result.average_score, 0.0);
180 assert_eq!(result.max_score, 0.0);
181 assert_eq!(result.file_scores.len(), 0);
182 }
183
184 #[test]
185 fn test_get_file_score() {
186 let mut file_scores = HashMap::new();
187 file_scores.insert("src/main.rs".to_string(), 95.0);
188 file_scores.insert("src/lib.rs".to_string(), 88.0);
189
190 let analysis = TdgAnalysis {
191 file_scores,
192 average_score: 91.5,
193 max_score: 95.0,
194 };
195
196 assert_eq!(
197 PmatIntegration::get_file_score(&analysis, "src/main.rs"),
198 Some(95.0)
199 );
200 assert_eq!(
201 PmatIntegration::get_file_score(&analysis, "nonexistent.rs"),
202 None
203 );
204 }
205
206 #[test]
208 #[ignore]
209 fn test_analyze_tdg_integration() {
210 let temp_dir = tempfile::TempDir::new().unwrap();
212
213 std::fs::write(
215 temp_dir.path().join("test.rs"),
216 "fn main() { println!(\"Hello\"); }",
217 )
218 .unwrap();
219
220 let result = PmatIntegration::analyze_tdg(temp_dir.path());
221
222 match result {
224 Ok(analysis) => {
225 assert!(analysis.average_score >= 0.0);
226 assert!(analysis.average_score <= 100.0);
227 }
228 Err(e) => {
229 assert!(e.to_string().contains("pmat"));
231 }
232 }
233 }
234
235 #[test]
236 fn test_parse_tdg_invalid_json() {
237 let invalid_json = "not valid json";
238
239 let result = PmatIntegration::parse_tdg_output(invalid_json);
240 assert!(result.is_err());
241 assert!(result.unwrap_err().to_string().contains("parse"));
242 }
243
244 #[test]
245 fn test_parse_tdg_single_file() {
246 let json = r#"{
247 "files": [
248 {"file_path": "src/single.rs", "total": 100.0, "grade": "APlusPlus"}
249 ]
250 }"#;
251
252 let result = PmatIntegration::parse_tdg_output(json).unwrap();
253
254 assert_eq!(result.average_score, 100.0);
255 assert_eq!(result.max_score, 100.0);
256 assert_eq!(result.file_scores.len(), 1);
257 }
258
259 #[test]
260 fn test_parse_tdg_multiple_files() {
261 let json = r#"{
262 "files": [
263 {"file_path": "file1.rs", "total": 90.0, "grade": "A"},
264 {"file_path": "file2.rs", "total": 85.0, "grade": "B"},
265 {"file_path": "file3.rs", "total": 95.0, "grade": "APLus"}
266 ]
267 }"#;
268
269 let result = PmatIntegration::parse_tdg_output(json).unwrap();
270
271 assert_eq!(result.average_score, 90.0);
272 assert_eq!(result.max_score, 95.0);
273 assert_eq!(result.file_scores.len(), 3);
274 }
275
276 #[test]
277 fn test_parse_tdg_with_zero_scores() {
278 let json = r#"{
279 "files": [
280 {"file_path": "bad1.rs", "total": 0.0, "grade": "F"},
281 {"file_path": "bad2.rs", "total": 0.0, "grade": "F"}
282 ]
283 }"#;
284
285 let result = PmatIntegration::parse_tdg_output(json).unwrap();
286
287 assert_eq!(result.average_score, 0.0);
288 assert_eq!(result.max_score, 0.0);
289 }
290
291 #[test]
292 fn test_parse_tdg_without_grade_field() {
293 let json = r#"{
294 "files": [
295 {"file_path": "src/main.rs", "total": 88.5}
296 ]
297 }"#;
298
299 let result = PmatIntegration::parse_tdg_output(json).unwrap();
300
301 assert_eq!(result.average_score, 88.5);
302 assert_eq!(result.max_score, 88.5);
303 }
304
305 #[test]
306 fn test_file_tdg_score_structure() {
307 let score = FileTdgScore {
308 path: "src/test.rs".to_string(),
309 score: 92.5,
310 grade: "A".to_string(),
311 };
312
313 assert_eq!(score.path, "src/test.rs");
314 assert_eq!(score.score, 92.5);
315 assert_eq!(score.grade, "A");
316 }
317
318 #[test]
319 fn test_file_tdg_score_clone() {
320 let original = FileTdgScore {
321 path: "src/test.rs".to_string(),
322 score: 92.5,
323 grade: "A".to_string(),
324 };
325
326 let cloned = original.clone();
327
328 assert_eq!(original.path, cloned.path);
329 assert_eq!(original.score, cloned.score);
330 assert_eq!(original.grade, cloned.grade);
331 }
332
333 #[test]
334 fn test_file_tdg_score_debug() {
335 let score = FileTdgScore {
336 path: "src/test.rs".to_string(),
337 score: 92.5,
338 grade: "A".to_string(),
339 };
340
341 let debug_str = format!("{:?}", score);
342 assert!(debug_str.contains("src/test.rs"));
343 assert!(debug_str.contains("92.5"));
344 assert!(debug_str.contains("A"));
345 }
346
347 #[test]
348 fn test_tdg_analysis_clone() {
349 let mut file_scores = HashMap::new();
350 file_scores.insert("file.rs".to_string(), 85.0);
351
352 let original = TdgAnalysis {
353 file_scores: file_scores.clone(),
354 average_score: 85.0,
355 max_score: 85.0,
356 };
357
358 let cloned = original.clone();
359
360 assert_eq!(original.average_score, cloned.average_score);
361 assert_eq!(original.max_score, cloned.max_score);
362 assert_eq!(original.file_scores.len(), cloned.file_scores.len());
363 }
364
365 #[test]
366 fn test_tdg_analysis_debug() {
367 let mut file_scores = HashMap::new();
368 file_scores.insert("file.rs".to_string(), 85.0);
369
370 let analysis = TdgAnalysis {
371 file_scores,
372 average_score: 85.0,
373 max_score: 85.0,
374 };
375
376 let debug_str = format!("{:?}", analysis);
377 assert!(debug_str.contains("85"));
378 }
379
380 #[test]
381 fn test_get_file_score_nonexistent() {
382 let analysis = TdgAnalysis {
383 file_scores: HashMap::new(),
384 average_score: 0.0,
385 max_score: 0.0,
386 };
387
388 assert_eq!(
389 PmatIntegration::get_file_score(&analysis, "missing.rs"),
390 None
391 );
392 }
393
394 #[test]
395 fn test_get_file_score_empty_analysis() {
396 let analysis = TdgAnalysis {
397 file_scores: HashMap::new(),
398 average_score: 0.0,
399 max_score: 0.0,
400 };
401
402 assert_eq!(PmatIntegration::get_file_score(&analysis, "any.rs"), None);
403 }
404
405 #[test]
406 fn test_parse_tdg_with_various_scores() {
407 let json = r#"{
408 "files": [
409 {"file_path": "low.rs", "total": 10.5, "grade": "F"},
410 {"file_path": "medium.rs", "total": 55.0, "grade": "C"},
411 {"file_path": "high.rs", "total": 99.9, "grade": "APlusPlus"}
412 ]
413 }"#;
414
415 let result = PmatIntegration::parse_tdg_output(json).unwrap();
416
417 assert!(result.average_score > 50.0 && result.average_score < 60.0);
418 assert_eq!(result.max_score, 99.9);
419 assert_eq!(
420 PmatIntegration::get_file_score(&result, "low.rs"),
421 Some(10.5)
422 );
423 }
424
425 #[test]
426 fn test_file_tdg_score_serialization() {
427 let score = FileTdgScore {
428 path: "src/test.rs".to_string(),
429 score: 92.5,
430 grade: "A".to_string(),
431 };
432
433 let json = serde_json::to_string(&score).unwrap();
434 let deserialized: FileTdgScore = serde_json::from_str(&json).unwrap();
435
436 assert_eq!(score.path, deserialized.path);
437 assert_eq!(score.score, deserialized.score);
438 assert_eq!(score.grade, deserialized.grade);
439 }
440
441 #[test]
442 fn test_parse_tdg_fractional_average() {
443 let json = r#"{
444 "files": [
445 {"file_path": "file1.rs", "total": 33.3, "grade": "D"},
446 {"file_path": "file2.rs", "total": 66.6, "grade": "B"},
447 {"file_path": "file3.rs", "total": 99.9, "grade": "APlusPlus"}
448 ]
449 }"#;
450
451 let result = PmatIntegration::parse_tdg_output(json).unwrap();
452
453 assert!((result.average_score - 66.6).abs() < 0.1);
455 }
456
457 #[test]
458 fn test_parse_tdg_with_long_file_paths() {
459 let long_path = "a/very/long/path/to/some/deeply/nested/directory/structure/file.rs";
460 let json = format!(
461 r#"{{
462 "files": [
463 {{"file_path": "{}", "total": 85.0, "grade": "A"}}
464 ]
465 }}"#,
466 long_path
467 );
468
469 let result = PmatIntegration::parse_tdg_output(&json).unwrap();
470
471 assert_eq!(
472 PmatIntegration::get_file_score(&result, long_path),
473 Some(85.0)
474 );
475 }
476
477 #[test]
478 fn test_is_pmat_available() {
479 let _available = PmatIntegration::is_pmat_available();
482 }
484}