1use anyhow::{anyhow, Result};
6use std::path::{Path, PathBuf};
7
8use super::hero_image::HeroImageResult;
9use super::quality::{ComponentQuality, QualityGrade, Score, StackQualityReport};
10
11const SECTION_CHECKS: &[(&str, u32)] =
13 &[("installation", 3), ("usage", 3), ("license", 3), ("contributing", 3)];
14
15fn score_if_exists(path: &Path, file: &str, points: u32) -> u32 {
17 if path.join(file).exists() {
18 points
19 } else {
20 0
21 }
22}
23
24fn check_section_exists(content_lower: &str, section: &str) -> bool {
26 content_lower.contains(&format!("## {}", section))
27 || content_lower.contains(&format!("# {}", section))
28}
29
30fn extract_json_f64(value: &serde_json::Value, default: f64) -> f64 {
32 value.as_f64().unwrap_or(default)
33}
34
35fn run_command_score(dir: &Path, args: &[&str], points: u32) -> u32 {
37 use std::process::Command;
38
39 let result = Command::new("cargo").args(args).current_dir(dir).output();
40 if result.map(|o| o.status.success()).unwrap_or(false) {
41 points
42 } else {
43 0
44 }
45}
46
47pub struct QualityChecker {
49 workspace_root: PathBuf,
51 min_grade: QualityGrade,
53 strict: bool,
55}
56
57impl QualityChecker {
58 pub fn new(workspace_root: PathBuf) -> Self {
60 Self { workspace_root, min_grade: QualityGrade::AMinus, strict: false }
61 }
62
63 pub fn with_min_grade(mut self, grade: QualityGrade) -> Self {
65 self.min_grade = grade;
66 self
67 }
68
69 pub fn strict(mut self, strict: bool) -> Self {
71 self.strict = strict;
72 self
73 }
74
75 pub async fn check_component(&self, name: &str) -> Result<ComponentQuality> {
77 let path = self.find_component_path(name)?;
78
79 let rust_score = self.run_rust_project_score(&path).await?;
81
82 let (repo_score, readme_score) = self.run_repo_score(&path).await?;
84
85 let hero_image = HeroImageResult::detect(&path);
87
88 Ok(ComponentQuality::new(name, path, rust_score, repo_score, readme_score, hero_image))
89 }
90
91 pub async fn check_all(&self) -> Result<StackQualityReport> {
93 use crate::stack::PAIML_CRATES;
94
95 let mut components = Vec::new();
96
97 for crate_name in PAIML_CRATES {
98 match self.check_component(crate_name).await {
99 Ok(quality) => components.push(quality),
100 Err(e) => {
101 tracing::warn!("Failed to check {}: {}", crate_name, e);
103 }
104 }
105 }
106
107 Ok(StackQualityReport::from_components(components))
108 }
109
110 fn find_component_path(&self, name: &str) -> Result<PathBuf> {
112 let cargo_toml = self.workspace_root.join("Cargo.toml");
114 if cargo_toml.exists() {
115 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
116 if content.contains(&format!("name = \"{}\"", name)) {
117 return Ok(self.workspace_root.clone());
118 }
119 }
120 }
121
122 if let Some(parent) = self.workspace_root.parent() {
124 let sibling = parent.join(name);
125 if sibling.exists() && sibling.join("Cargo.toml").exists() {
126 return Ok(sibling);
127 }
128 }
129
130 Err(anyhow!("Could not find component: {}", name))
131 }
132
133 async fn run_rust_project_score(&self, path: &Path) -> Result<Score> {
135 use std::process::Command;
136
137 let output = Command::new("pmat")
138 .args(["rust-project-score", "--path"])
139 .arg(path)
140 .args(["--format", "json"])
141 .output();
142
143 match output {
144 Ok(output) if output.status.success() => {
145 if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&output.stdout) {
147 let earned = extract_json_f64(&json["total_earned"], 0.0);
149 let possible = extract_json_f64(&json["total_possible"], 134.0);
150 let percentage = extract_json_f64(&json["percentage"], 0.0);
151
152 let normalized_score = ((percentage / 100.0) * 114.0).round() as u32;
154 let grade = QualityGrade::from_rust_project_score(normalized_score);
155
156 return Ok(Score {
158 value: earned.round() as u32,
159 max: possible.round() as u32,
160 grade,
161 });
162 }
163 }
164 Ok(output) => {
165 let stderr = String::from_utf8_lossy(&output.stderr);
167 if !stderr.is_empty() {
168 tracing::debug!("pmat stderr: {}", stderr);
169 }
170 }
171 Err(e) => {
172 tracing::debug!("pmat not available: {}", e);
173 }
174 }
175
176 self.estimate_rust_score(path).await
178 }
179
180 async fn estimate_rust_score(&self, path: &Path) -> Result<Score> {
182 let mut score = 50u32; score += run_command_score(path, &["test", "--quiet"], 20);
186
187 score += run_command_score(path, &["clippy", "--quiet", "--", "-D", "warnings"], 15);
189
190 score += score_if_exists(path, "README.md", 10);
192
193 let cargo_toml = path.join("Cargo.toml");
195 if cargo_toml.exists() {
196 if let Ok(content) = std::fs::read_to_string(&cargo_toml) {
197 if content.contains("[package.metadata") || content.contains("documentation =") {
198 score += 5;
199 }
200 }
201 }
202
203 let grade = QualityGrade::from_rust_project_score(score);
204 Ok(Score::new(score, 114, grade))
205 }
206
207 async fn run_repo_score(&self, path: &Path) -> Result<(Score, Score)> {
209 use std::process::Command;
210
211 let output = Command::new("pmat")
212 .args(["repo-score", "--path"])
213 .arg(path)
214 .args(["--format", "json"])
215 .output();
216
217 match output {
218 Ok(output) if output.status.success() => {
219 if let Ok(json) = serde_json::from_slice::<serde_json::Value>(&output.stdout) {
220 let total = extract_json_f64(&json["total_score"], 0.0).round() as u32;
222
223 let readme = extract_json_f64(
225 &json["categories"]["documentation"]["score"],
226 0.0,
227 )
228 .round() as u32;
229
230 let repo_grade = QualityGrade::from_repo_score(total);
231 let readme_grade = QualityGrade::from_readme_score(readme);
232
233 return Ok((
234 Score::new(total, 110, repo_grade),
235 Score::new(readme, 20, readme_grade),
236 ));
237 }
238 }
239 Ok(output) => {
240 let stderr = String::from_utf8_lossy(&output.stderr);
241 if !stderr.is_empty() {
242 tracing::debug!("pmat repo-score stderr: {}", stderr);
243 }
244 }
245 Err(e) => {
246 tracing::debug!("pmat repo-score not available: {}", e);
247 }
248 }
249
250 self.estimate_repo_scores(path).await
252 }
253
254 async fn estimate_repo_scores(&self, path: &Path) -> Result<(Score, Score)> {
256 let mut repo_score = 40u32; let mut readme_score = 0u32;
258
259 let readme_path = path.join("README.md");
261 if readme_path.exists() {
262 repo_score += 10;
263 readme_score += 5; if let Ok(content) = std::fs::read_to_string(&readme_path) {
266 let content_lower = content.to_lowercase();
267
268 for &(section, points) in SECTION_CHECKS {
270 if check_section_exists(&content_lower, section) {
271 readme_score += points;
272 }
273 }
274 if content.len() > 500 {
275 readme_score += 3; }
277 }
278 }
279
280 repo_score += score_if_exists(path, "Makefile", 15);
282
283 repo_score += score_if_exists(path, ".github/workflows", 15);
285
286 if path.join(".pre-commit-config.yaml").exists()
288 || path.join(".git/hooks/pre-commit").exists()
289 {
290 repo_score += 10;
291 }
292
293 readme_score = readme_score.min(20);
294 let repo_grade = QualityGrade::from_repo_score(repo_score);
295 let readme_grade = QualityGrade::from_readme_score(readme_score);
296
297 Ok((Score::new(repo_score, 110, repo_grade), Score::new(readme_score, 20, readme_grade)))
298 }
299}
300
301#[cfg(test)]
302mod tests {
303 use super::*;
304 use std::path::PathBuf;
305
306 fn setup_test_dir(name: &str) -> PathBuf {
308 let dir = std::env::temp_dir().join(format!("{}_{}", name, std::process::id()));
309 let _ = std::fs::remove_dir_all(&dir);
310 std::fs::create_dir_all(&dir).expect("mkdir failed");
311 dir
312 }
313
314 fn cleanup_test_dir(dir: &Path) {
316 let _ = std::fs::remove_dir_all(dir);
317 }
318
319 #[test]
322 fn test_score_if_exists_present() {
323 let dir = setup_test_dir("test_qc_score_exists");
324 std::fs::write(dir.join("README.md"), "hello").expect("fs write failed");
325 assert_eq!(score_if_exists(&dir, "README.md", 10), 10);
326 cleanup_test_dir(&dir);
327 }
328
329 #[test]
330 fn test_score_if_exists_absent() {
331 let dir = setup_test_dir("test_qc_score_absent");
332 assert_eq!(score_if_exists(&dir, "README.md", 10), 0);
333 cleanup_test_dir(&dir);
334 }
335
336 #[test]
337 fn test_check_section_exists_h2() {
338 assert!(check_section_exists("## installation\nsome text", "installation"));
339 }
340
341 #[test]
342 fn test_check_section_exists_h1() {
343 assert!(check_section_exists("# usage\nsome text", "usage"));
344 }
345
346 #[test]
347 fn test_check_section_exists_missing() {
348 assert!(!check_section_exists("some random text", "installation"));
349 }
350
351 #[test]
352 fn test_extract_json_f64_present() {
353 let val = serde_json::json!(42.5);
354 assert_eq!(extract_json_f64(&val, 0.0), 42.5);
355 }
356
357 #[test]
358 fn test_extract_json_f64_null() {
359 let val = serde_json::json!(null);
360 assert_eq!(extract_json_f64(&val, 99.0), 99.0);
361 }
362
363 #[test]
364 fn test_extract_json_f64_string() {
365 let val = serde_json::json!("not a number");
366 assert_eq!(extract_json_f64(&val, 7.0), 7.0);
367 }
368
369 #[test]
370 fn test_extract_json_f64_integer() {
371 let val = serde_json::json!(100);
372 assert_eq!(extract_json_f64(&val, 0.0), 100.0);
373 }
374
375 #[test]
376 fn test_run_command_score_success() {
377 let dir = setup_test_dir("test_qc_cmd_score");
379 assert_eq!(run_command_score(&dir, &["--version"], 20), 20);
380 cleanup_test_dir(&dir);
381 }
382
383 #[test]
384 fn test_run_command_score_failure() {
385 let dir = setup_test_dir("test_qc_cmd_fail");
386 assert_eq!(run_command_score(&dir, &["false"], 20), 0);
387 cleanup_test_dir(&dir);
388 }
389
390 #[test]
391 fn test_run_command_score_not_found() {
392 let dir = setup_test_dir("test_qc_cmd_notfound");
393 assert_eq!(run_command_score(&dir, &["nonexistent_tool_xyz_abc"], 20), 0);
394 cleanup_test_dir(&dir);
395 }
396
397 #[test]
400 fn test_quality_checker_creation() {
401 let checker = QualityChecker::new(PathBuf::from("/tmp"));
402 assert_eq!(checker.min_grade, QualityGrade::AMinus);
403 assert!(!checker.strict);
404 }
405
406 #[test]
407 fn test_quality_checker_with_min_grade() {
408 let checker =
409 QualityChecker::new(PathBuf::from("/tmp")).with_min_grade(QualityGrade::APlus);
410 assert_eq!(checker.min_grade, QualityGrade::APlus);
411 }
412
413 #[test]
414 fn test_quality_checker_strict() {
415 let checker = QualityChecker::new(PathBuf::from("/tmp")).strict(true);
416 assert!(checker.strict);
417 }
418
419 #[test]
420 fn test_quality_checker_chaining() {
421 let checker = QualityChecker::new(PathBuf::from("/tmp"))
422 .with_min_grade(QualityGrade::AMinus)
423 .strict(true);
424 assert_eq!(checker.min_grade, QualityGrade::AMinus);
425 assert!(checker.strict);
426 }
427
428 #[test]
431 fn test_find_component_path_current_workspace() {
432 let temp_dir = setup_test_dir("test_quality_checker_workspace");
433 std::fs::write(
434 temp_dir.join("Cargo.toml"),
435 "[package]\nname = \"test-crate\"\nversion = \"1.0.0\"\n",
436 )
437 .expect("unexpected failure");
438 let checker = QualityChecker::new(temp_dir.clone());
439 let path = checker.find_component_path("test-crate").expect("unexpected failure");
440 assert_eq!(path, temp_dir);
441 cleanup_test_dir(&temp_dir);
442 }
443
444 #[test]
445 fn test_find_component_path_sibling() {
446 let temp_dir = setup_test_dir("test_quality_siblings");
447 let project_a = temp_dir.join("project-a");
448 let project_b = temp_dir.join("project-b");
449 std::fs::create_dir_all(&project_a).expect("mkdir failed");
450 std::fs::create_dir_all(&project_b).expect("mkdir failed");
451 std::fs::write(
452 project_a.join("Cargo.toml"),
453 "[package]\nname = \"project-a\"\nversion = \"1.0.0\"\n",
454 )
455 .expect("unexpected failure");
456 std::fs::write(
457 project_b.join("Cargo.toml"),
458 "[package]\nname = \"project-b\"\nversion = \"1.0.0\"\n",
459 )
460 .expect("unexpected failure");
461 let checker = QualityChecker::new(project_a.clone());
462 let path = checker.find_component_path("project-b").expect("unexpected failure");
463 assert_eq!(path, project_b);
464 cleanup_test_dir(&temp_dir);
465 }
466
467 #[test]
468 fn test_find_component_path_not_found() {
469 let temp_dir = setup_test_dir("test_quality_not_found");
470 let checker = QualityChecker::new(temp_dir.clone());
471 assert!(checker.find_component_path("nonexistent-crate").is_err());
472 cleanup_test_dir(&temp_dir);
473 }
474
475 #[test]
476 fn test_find_component_no_cargo_toml() {
477 let temp_dir = setup_test_dir("test_quality_no_cargo");
478 let checker = QualityChecker::new(temp_dir.clone());
479 assert!(checker.find_component_path("any-crate").is_err());
480 cleanup_test_dir(&temp_dir);
481 }
482
483 #[test]
484 fn test_find_component_cargo_toml_no_match() {
485 let temp_dir = setup_test_dir("test_quality_no_match");
486 std::fs::write(
487 temp_dir.join("Cargo.toml"),
488 "[package]\nname = \"other-crate\"\nversion = \"1.0.0\"\n",
489 )
490 .expect("unexpected failure");
491 let checker = QualityChecker::new(temp_dir.clone());
492 assert!(checker.find_component_path("wanted-crate").is_err());
494 cleanup_test_dir(&temp_dir);
495 }
496
497 #[tokio::test]
500 async fn test_estimate_repo_scores_empty_dir() {
501 let dir = setup_test_dir("test_qc_repo_empty");
502 let checker = QualityChecker::new(dir.clone());
503 let (repo, readme) =
504 checker.estimate_repo_scores(&dir).await.expect("async operation failed");
505 assert_eq!(repo.value, 40); assert_eq!(readme.value, 0);
507 cleanup_test_dir(&dir);
508 }
509
510 #[tokio::test]
511 async fn test_estimate_repo_scores_with_readme() {
512 let dir = setup_test_dir("test_qc_repo_readme");
513 std::fs::write(
515 dir.join("README.md"),
516 "# My Project\n\n## Installation\n\nRun cargo install.\n\n## Usage\n\nJust run it.\n\n## License\n\nMIT\n\n## Contributing\n\nPRs welcome.\n",
517 )
518 .expect("unexpected failure");
519 let checker = QualityChecker::new(dir.clone());
520 let (repo, readme) =
521 checker.estimate_repo_scores(&dir).await.expect("async operation failed");
522 assert!(repo.value > 40); assert_eq!(readme.value, 17);
524 cleanup_test_dir(&dir);
525 }
526
527 #[tokio::test]
528 async fn test_estimate_repo_scores_with_makefile_and_ci() {
529 let dir = setup_test_dir("test_qc_repo_mk_ci");
530 std::fs::write(dir.join("Makefile"), "all:\n\ttrue\n").expect("fs write failed");
531 std::fs::create_dir_all(dir.join(".github/workflows")).expect("mkdir failed");
532 let checker = QualityChecker::new(dir.clone());
533 let (repo, _) = checker.estimate_repo_scores(&dir).await.expect("async operation failed");
534 assert_eq!(repo.value, 40 + 15 + 15); cleanup_test_dir(&dir);
536 }
537
538 #[tokio::test]
539 async fn test_estimate_repo_scores_with_precommit() {
540 let dir = setup_test_dir("test_qc_repo_precommit");
541 std::fs::write(dir.join(".pre-commit-config.yaml"), "repos: []\n")
542 .expect("fs write failed");
543 let checker = QualityChecker::new(dir.clone());
544 let (repo, _) = checker.estimate_repo_scores(&dir).await.expect("async operation failed");
545 assert_eq!(repo.value, 40 + 10); cleanup_test_dir(&dir);
547 }
548
549 #[tokio::test]
550 async fn test_estimate_repo_scores_readme_partial_sections() {
551 let dir = setup_test_dir("test_qc_repo_partial");
552 std::fs::write(dir.join("README.md"), "# Proj\n\n## Installation\nstuff\n")
553 .expect("fs write failed");
554 let checker = QualityChecker::new(dir.clone());
555 let (_, readme) = checker.estimate_repo_scores(&dir).await.expect("async operation failed");
556 assert_eq!(readme.value, 8);
558 cleanup_test_dir(&dir);
559 }
560
561 #[tokio::test]
564 async fn test_estimate_rust_score_empty_dir() {
565 let dir = setup_test_dir("test_qc_rust_empty");
566 let checker = QualityChecker::new(dir.clone());
567 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
568 assert!(score.value >= 50, "score should be at least base: {}", score.value);
570 cleanup_test_dir(&dir);
571 }
572
573 #[tokio::test]
574 async fn test_estimate_rust_score_with_readme() {
575 let dir = setup_test_dir("test_qc_rust_readme");
576 std::fs::write(dir.join("README.md"), "# Hello").expect("fs write failed");
577 let checker = QualityChecker::new(dir.clone());
578 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
579 assert!(score.value >= 60, "score with README should be >= 60: {}", score.value);
581 cleanup_test_dir(&dir);
582 }
583
584 #[tokio::test]
585 async fn test_estimate_rust_score_with_metadata() {
586 let dir = setup_test_dir("test_qc_rust_meta");
587 std::fs::write(
588 dir.join("Cargo.toml"),
589 "[package]\nname = \"x\"\ndocumentation = \"https://docs.rs/x\"\n",
590 )
591 .expect("unexpected failure");
592 let checker = QualityChecker::new(dir.clone());
593 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
594 assert_eq!(score.value, 55); cleanup_test_dir(&dir);
596 }
597
598 #[tokio::test]
599 async fn test_estimate_rust_score_with_package_metadata() {
600 let dir = setup_test_dir("test_qc_rust_pkgmeta");
601 std::fs::write(
602 dir.join("Cargo.toml"),
603 "[package]\nname = \"x\"\n[package.metadata.foo]\nbar = true\n",
604 )
605 .expect("unexpected failure");
606 let checker = QualityChecker::new(dir.clone());
607 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
608 assert_eq!(score.value, 55); cleanup_test_dir(&dir);
610 }
611
612 #[tokio::test]
613 async fn test_estimate_rust_score_no_cargo_toml() {
614 let dir = setup_test_dir("test_qc_rust_nocargo");
615 let checker = QualityChecker::new(dir.clone());
616 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
617 assert!(score.value >= 50, "score should be at least base: {}", score.value);
619 cleanup_test_dir(&dir);
620 }
621
622 #[tokio::test]
623 async fn test_estimate_rust_score_with_readme_and_metadata() {
624 let dir = setup_test_dir("test_qc_rust_both");
625 std::fs::write(dir.join("README.md"), "# Project\n").expect("fs write failed");
626 std::fs::write(dir.join("Cargo.toml"), "[package]\nname = \"x\"\ndocumentation = \"y\"\n")
627 .expect("unexpected failure");
628 let checker = QualityChecker::new(dir.clone());
629 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
630 assert_eq!(score.value, 65); cleanup_test_dir(&dir);
632 }
633
634 #[tokio::test]
637 async fn test_run_rust_project_score_returns_valid() {
638 let dir = setup_test_dir("test_qc_pmat_fallback");
640 std::fs::write(dir.join("README.md"), "# Hi").expect("fs write failed");
641 let checker = QualityChecker::new(dir.clone());
642 let score = checker.run_rust_project_score(&dir).await.expect("async operation failed");
643 assert!(score.value > 0);
644 assert!(score.max > 0);
645 cleanup_test_dir(&dir);
646 }
647
648 #[tokio::test]
649 async fn test_run_repo_score_returns_valid() {
650 let dir = setup_test_dir("test_qc_repo_fallback");
652 std::fs::write(dir.join("README.md"), "# Project\n## Installation\nstuff\n")
653 .expect("fs write failed");
654 std::fs::write(dir.join("Makefile"), "all:\n").expect("fs write failed");
655 let checker = QualityChecker::new(dir.clone());
656 let (repo, readme) = checker.run_repo_score(&dir).await.expect("async operation failed");
657 assert!(repo.value > 0);
658 assert!(repo.max > 0);
659 assert!(readme.max > 0);
660 cleanup_test_dir(&dir);
661 }
662
663 #[tokio::test]
666 async fn test_check_component_self() {
667 let dir = setup_test_dir("test_qc_check_self");
668 std::fs::write(
669 dir.join("Cargo.toml"),
670 "[package]\nname = \"my-crate\"\nversion = \"0.1.0\"\n",
671 )
672 .expect("unexpected failure");
673 std::fs::write(dir.join("README.md"), "# My Crate\n## Usage\nstuff\n")
674 .expect("fs write failed");
675 let checker = QualityChecker::new(dir.clone());
676 let result = checker.check_component("my-crate").await.expect("async operation failed");
677 assert_eq!(result.name, "my-crate");
678 cleanup_test_dir(&dir);
679 }
680
681 #[tokio::test]
682 async fn test_check_component_not_found() {
683 let dir = setup_test_dir("test_qc_check_notfound");
684 let checker = QualityChecker::new(dir.clone());
685 let result = checker.check_component("nonexistent-xyz").await;
686 assert!(result.is_err());
687 cleanup_test_dir(&dir);
688 }
689
690 #[tokio::test]
693 async fn test_estimate_repo_scores_readme_length_bonus() {
694 let dir = setup_test_dir("test_qc_readme_len");
695 let long_content = format!("# Project\n\n## Installation\n\n{}\n", "x".repeat(600));
696 std::fs::write(dir.join("README.md"), &long_content).expect("fs write failed");
697 let checker = QualityChecker::new(dir.clone());
698 let (_, readme) = checker.estimate_repo_scores(&dir).await.expect("async operation failed");
699 assert_eq!(readme.value, 11);
701 cleanup_test_dir(&dir);
702 }
703
704 #[test]
707 fn test_check_section_exists_lowered_input() {
708 assert!(check_section_exists("## installation\n", "installation"));
710 }
711
712 #[test]
713 fn test_check_section_exists_no_hash() {
714 assert!(!check_section_exists("installation\n", "installation"));
715 }
716
717 #[tokio::test]
720 async fn test_estimate_repo_scores_git_hook() {
721 let dir = setup_test_dir("test_qc_githook");
722 std::fs::create_dir_all(dir.join(".git/hooks")).expect("mkdir failed");
723 std::fs::write(dir.join(".git/hooks/pre-commit"), "#!/bin/sh\n").expect("fs write failed");
724 let checker = QualityChecker::new(dir.clone());
725 let (repo, _) = checker.estimate_repo_scores(&dir).await.expect("async operation failed");
726 assert_eq!(repo.value, 40 + 10); cleanup_test_dir(&dir);
728 }
729
730 #[tokio::test]
733 async fn test_estimate_repo_scores_readme_score_capped_at_20() {
734 let dir = setup_test_dir("test_qc_readme_cap");
735 let long_content = format!(
737 "# Project\n\n## Installation\nstuff\n## Usage\nstuff\n## License\nMIT\n## Contributing\nYes\n\n{}\n",
738 "x".repeat(600)
739 );
740 std::fs::write(dir.join("README.md"), &long_content).expect("fs write failed");
741 let checker = QualityChecker::new(dir.clone());
742 let (_, readme) = checker.estimate_repo_scores(&dir).await.expect("async operation failed");
743 assert!(readme.value <= 20);
745 cleanup_test_dir(&dir);
746 }
747
748 #[test]
751 fn test_find_component_sibling_dir_no_cargo_toml() {
752 let temp_dir = setup_test_dir("test_quality_sibling_no_cargo");
753 let project_a = temp_dir.join("project-a");
754 let project_b = temp_dir.join("project-b");
755 std::fs::create_dir_all(&project_a).expect("mkdir failed");
756 std::fs::create_dir_all(&project_b).expect("mkdir failed");
757 std::fs::write(
758 project_a.join("Cargo.toml"),
759 "[package]\nname = \"project-a\"\nversion = \"1.0.0\"\n",
760 )
761 .expect("unexpected failure");
762 let checker = QualityChecker::new(project_a.clone());
764 assert!(checker.find_component_path("project-b").is_err());
765 cleanup_test_dir(&temp_dir);
766 }
767
768 #[tokio::test]
771 async fn test_estimate_repo_scores_full_infrastructure() {
772 let dir = setup_test_dir("test_qc_full_infra");
773 let long_content = format!(
775 "# Project\n\n## Installation\nstuff\n## Usage\nstuff\n## License\nMIT\n## Contributing\nYes\n\n{}\n",
776 "x".repeat(600)
777 );
778 std::fs::write(dir.join("README.md"), &long_content).expect("fs write failed");
779 std::fs::write(dir.join("Makefile"), "all:\n\ttrue\n").expect("fs write failed");
780 std::fs::create_dir_all(dir.join(".github/workflows")).expect("mkdir failed");
781 std::fs::write(dir.join(".pre-commit-config.yaml"), "repos: []\n")
782 .expect("fs write failed");
783 let checker = QualityChecker::new(dir.clone());
784 let (repo, readme) =
785 checker.estimate_repo_scores(&dir).await.expect("async operation failed");
786 assert_eq!(repo.value, 90);
788 assert_eq!(readme.value, 20); cleanup_test_dir(&dir);
790 }
791
792 #[tokio::test]
795 async fn test_check_component_sibling() {
796 let temp_dir = setup_test_dir("test_qc_check_sibling");
797 let project_a = temp_dir.join("project-a");
798 let project_b = temp_dir.join("project-b");
799 std::fs::create_dir_all(&project_a).expect("mkdir failed");
800 std::fs::create_dir_all(&project_b).expect("mkdir failed");
801 std::fs::write(
802 project_a.join("Cargo.toml"),
803 "[package]\nname = \"project-a\"\nversion = \"1.0.0\"\n",
804 )
805 .expect("unexpected failure");
806 std::fs::write(
807 project_b.join("Cargo.toml"),
808 "[package]\nname = \"project-b\"\nversion = \"1.0.0\"\n",
809 )
810 .expect("unexpected failure");
811 std::fs::write(project_b.join("README.md"), "# Project B\n").expect("fs write failed");
812 let checker = QualityChecker::new(project_a.clone());
813 let result = checker.check_component("project-b").await.expect("async operation failed");
814 assert_eq!(result.name, "project-b");
815 cleanup_test_dir(&temp_dir);
816 }
817
818 #[tokio::test]
821 async fn test_estimate_rust_score_cargo_toml_no_metadata() {
822 let dir = setup_test_dir("test_qc_rust_nometadata");
823 std::fs::write(dir.join("Cargo.toml"), "[package]\nname = \"x\"\nversion = \"0.1.0\"\n")
824 .expect("unexpected failure");
825 let checker = QualityChecker::new(dir.clone());
826 let score = checker.estimate_rust_score(&dir).await.expect("async operation failed");
827 assert_eq!(score.value, 50);
829 cleanup_test_dir(&dir);
830 }
831
832 #[test]
835 fn test_score_grade_assignment() {
836 let grade = QualityGrade::from_rust_project_score(100);
837 let score = Score::new(100, 114, grade);
838 assert_eq!(score.value, 100);
839 assert_eq!(score.max, 114);
840 }
841}