1pub mod config;
18pub mod report;
19pub mod rule;
20pub mod rules;
21
22pub use config::ComplyConfig;
23#[allow(unused_imports)]
24pub use config::ProjectOverride;
25pub use report::{ComplyReport, ComplyReportFormat};
26pub use rule::StackComplianceRule;
27#[allow(unused_imports)]
28pub use rule::{FixResult, RuleResult};
29
30use crate::stack::PAIML_CRATES;
31use std::path::{Path, PathBuf};
32
33#[derive(Debug)]
37pub struct StackComplyEngine {
38 config: ComplyConfig,
40 rules: Vec<Box<dyn StackComplianceRule>>,
42 discovered_projects: Vec<ProjectInfo>,
44}
45
46#[derive(Debug, Clone)]
48pub struct ProjectInfo {
49 pub name: String,
51 pub path: PathBuf,
53 pub is_paiml_crate: bool,
55}
56
57impl StackComplyEngine {
58 pub fn new(config: ComplyConfig) -> Self {
60 let mut engine = Self { config, rules: Vec::new(), discovered_projects: Vec::new() };
61
62 engine.register_rule(Box::new(rules::MakefileRule::new()));
64 engine.register_rule(Box::new(rules::CargoTomlRule::new()));
65 engine.register_rule(Box::new(rules::CiWorkflowRule::new()));
66 engine.register_rule(Box::new(rules::DuplicationRule::new()));
67
68 engine
69 }
70
71 pub fn default_for_workspace(workspace: &Path) -> Self {
73 Self::new(ComplyConfig::default_for_workspace(workspace))
74 }
75
76 pub fn register_rule(&mut self, rule: Box<dyn StackComplianceRule>) {
78 self.rules.push(rule);
79 }
80
81 pub fn discover_projects(&mut self, workspace: &Path) -> anyhow::Result<&[ProjectInfo]> {
83 self.discovered_projects.clear();
84
85 for entry in
87 walkdir::WalkDir::new(workspace).max_depth(2).into_iter().filter_map(|e| e.ok())
88 {
89 let path = entry.path();
90 if path.file_name() == Some(std::ffi::OsStr::new("Cargo.toml")) {
91 if let Some(project) = self.parse_project(path)? {
92 self.discovered_projects.push(project);
93 }
94 }
95 }
96
97 Ok(&self.discovered_projects)
98 }
99
100 fn parse_project(&self, cargo_toml: &Path) -> anyhow::Result<Option<ProjectInfo>> {
102 let content = std::fs::read_to_string(cargo_toml)?;
103 let toml: toml::Value = toml::from_str(&content)?;
104
105 let name = toml
106 .get("package")
107 .and_then(|p| p.get("name"))
108 .and_then(|n| n.as_str())
109 .map(String::from);
110
111 match name {
112 Some(name) => {
113 let path = cargo_toml.parent().unwrap_or(Path::new(".")).to_path_buf();
114 let is_paiml_crate = PAIML_CRATES.contains(&name.as_str());
115
116 Ok(Some(ProjectInfo { name, path, is_paiml_crate }))
117 }
118 None => Ok(None),
119 }
120 }
121
122 pub fn check_all(&self) -> ComplyReport {
124 let mut report = ComplyReport::new();
125
126 for project in &self.discovered_projects {
127 if !project.is_paiml_crate && !self.config.include_external {
129 continue;
130 }
131
132 for rule in &self.rules {
133 if !self.is_rule_enabled(rule.id()) {
135 continue;
136 }
137
138 if self.has_rule_exemption(&project.name, rule.id()) {
140 report.add_exemption(&project.name, rule.id());
141 continue;
142 }
143
144 match rule.check(&project.path) {
145 Ok(result) => {
146 report.add_result(&project.name, rule.id(), result);
147 }
148 Err(e) => {
149 report.add_error(&project.name, rule.id(), e.to_string());
150 }
151 }
152 }
153 }
154
155 report.finalize();
156 report
157 }
158
159 pub fn check_rule(&self, rule_id: &str) -> ComplyReport {
161 let mut report = ComplyReport::new();
162
163 let rule = match self.rules.iter().find(|r| r.id() == rule_id) {
164 Some(r) => r,
165 None => {
166 report.add_global_error(format!("Unknown rule: {}", rule_id));
167 return report;
168 }
169 };
170
171 for project in &self.discovered_projects {
172 if !project.is_paiml_crate && !self.config.include_external {
173 continue;
174 }
175
176 if self.has_rule_exemption(&project.name, rule_id) {
177 report.add_exemption(&project.name, rule_id);
178 continue;
179 }
180
181 match rule.check(&project.path) {
182 Ok(result) => {
183 report.add_result(&project.name, rule_id, result);
184 }
185 Err(e) => {
186 report.add_error(&project.name, rule_id, e.to_string());
187 }
188 }
189 }
190
191 report.finalize();
192 report
193 }
194
195 pub fn fix_all(&self, dry_run: bool) -> ComplyReport {
197 let mut report = ComplyReport::new();
198
199 for project in &self.discovered_projects {
200 if !project.is_paiml_crate && !self.config.include_external {
201 continue;
202 }
203
204 for rule in &self.rules {
205 if !self.is_rule_enabled(rule.id()) || !rule.can_fix() {
206 continue;
207 }
208
209 if self.has_rule_exemption(&project.name, rule.id()) {
210 continue;
211 }
212
213 let check_result = match rule.check(&project.path) {
215 Ok(r) => r,
216 Err(e) => {
217 report.add_error(&project.name, rule.id(), e.to_string());
218 continue;
219 }
220 };
221
222 if check_result.passed {
223 report.add_result(&project.name, rule.id(), check_result);
224 continue;
225 }
226
227 if dry_run {
229 report.add_dry_run_fix(&project.name, rule.id(), &check_result.violations);
230 } else {
231 match rule.fix(&project.path) {
232 Ok(fix_result) => {
233 report.add_fix_result(&project.name, rule.id(), fix_result);
234 }
235 Err(e) => {
236 report.add_error(&project.name, rule.id(), e.to_string());
237 }
238 }
239 }
240 }
241 }
242
243 report.finalize();
244 report
245 }
246
247 fn is_rule_enabled(&self, rule_id: &str) -> bool {
249 if self.config.enabled_rules.is_empty() {
250 !self.config.disabled_rules.contains(&rule_id.to_string())
252 } else {
253 self.config.enabled_rules.contains(&rule_id.to_string())
254 }
255 }
256
257 fn has_rule_exemption(&self, project_name: &str, rule_id: &str) -> bool {
259 self.config
260 .project_overrides
261 .get(project_name)
262 .map(|o| o.exempt_rules.contains(&rule_id.to_string()))
263 .unwrap_or(false)
264 }
265
266 pub fn available_rules(&self) -> Vec<(&str, &str)> {
268 self.rules.iter().map(|r| (r.id(), r.description())).collect()
269 }
270
271 pub fn projects(&self) -> &[ProjectInfo] {
273 &self.discovered_projects
274 }
275}
276
277#[cfg(test)]
278mod tests {
279 use super::*;
280
281 #[test]
282 fn test_comply_engine_creation() {
283 let config = ComplyConfig::default();
284 let engine = StackComplyEngine::new(config);
285 assert!(!engine.rules.is_empty());
286 }
287
288 #[test]
289 fn test_available_rules() {
290 let engine = StackComplyEngine::new(ComplyConfig::default());
291 let rules = engine.available_rules();
292 assert!(rules.iter().any(|(id, _)| *id == "makefile-targets"));
293 assert!(rules.iter().any(|(id, _)| *id == "cargo-toml-consistency"));
294 assert!(rules.iter().any(|(id, _)| *id == "ci-workflow-parity"));
295 assert!(rules.iter().any(|(id, _)| *id == "code-duplication"));
296 }
297
298 #[test]
299 fn test_rule_enabled_check() {
300 let mut config = ComplyConfig::default();
301 config.disabled_rules.push("makefile-targets".to_string());
302 let engine = StackComplyEngine::new(config);
303 assert!(!engine.is_rule_enabled("makefile-targets"));
304 assert!(engine.is_rule_enabled("cargo-toml-consistency"));
305 }
306
307 #[test]
308 fn test_project_exemption() {
309 let mut config = ComplyConfig::default();
310 let mut override_config = ProjectOverride::default();
311 override_config.exempt_rules.push("makefile-targets".to_string());
312 config.project_overrides.insert("test-project".to_string(), override_config);
313
314 let engine = StackComplyEngine::new(config);
315 assert!(engine.has_rule_exemption("test-project", "makefile-targets"));
316 assert!(!engine.has_rule_exemption("test-project", "cargo-toml-consistency"));
317 assert!(!engine.has_rule_exemption("other-project", "makefile-targets"));
318 }
319
320 #[test]
321 fn test_default_for_workspace() {
322 let engine = StackComplyEngine::default_for_workspace(Path::new("."));
323 assert!(!engine.rules.is_empty());
324 }
325
326 #[test]
327 fn test_projects_empty_initially() {
328 let engine = StackComplyEngine::new(ComplyConfig::default());
329 assert!(engine.projects().is_empty());
330 }
331
332 #[test]
333 fn test_enabled_rules_explicit() {
334 let mut config = ComplyConfig::default();
335 config.enabled_rules.push("makefile-targets".to_string());
336 let engine = StackComplyEngine::new(config);
337 assert!(engine.is_rule_enabled("makefile-targets"));
338 assert!(!engine.is_rule_enabled("cargo-toml-consistency"));
339 }
340
341 #[test]
342 fn test_project_info_fields() {
343 let info = ProjectInfo {
344 name: "test-project".to_string(),
345 path: PathBuf::from("/path/to/project"),
346 is_paiml_crate: true,
347 };
348 assert_eq!(info.name, "test-project");
349 assert_eq!(info.path, PathBuf::from("/path/to/project"));
350 assert!(info.is_paiml_crate);
351 }
352
353 #[test]
354 fn test_check_rule_unknown() {
355 let engine = StackComplyEngine::new(ComplyConfig::default());
356 let report = engine.check_rule("nonexistent-rule");
357 assert!(!report.errors.is_empty());
358 }
359
360 #[test]
361 fn test_check_all_empty_projects() {
362 let engine = StackComplyEngine::new(ComplyConfig::default());
363 let report = engine.check_all();
364 assert_eq!(report.summary.total_projects, 0);
366 }
367
368 #[test]
369 fn test_fix_all_empty_projects() {
370 let engine = StackComplyEngine::new(ComplyConfig::default());
371 let report = engine.fix_all(true);
372 assert_eq!(report.summary.total_projects, 0);
373 }
374
375 #[test]
376 fn test_fix_all_dry_run() {
377 let engine = StackComplyEngine::new(ComplyConfig::default());
378 let report = engine.fix_all(true); assert_eq!(report.summary.total_projects, 0);
380 }
381
382 #[test]
383 fn test_fix_all_actual_run() {
384 let engine = StackComplyEngine::new(ComplyConfig::default());
385 let report = engine.fix_all(false); assert_eq!(report.summary.total_projects, 0);
387 }
388
389 #[test]
390 fn test_register_custom_rule() {
391 use crate::comply::rules::MakefileRule;
392 let mut engine = StackComplyEngine::new(ComplyConfig::default());
393 let initial_count = engine.rules.len();
394 engine.register_rule(Box::new(MakefileRule::new()));
395 assert_eq!(engine.rules.len(), initial_count + 1);
396 }
397
398 #[test]
399 fn test_project_info_clone() {
400 let info = ProjectInfo {
401 name: "test-project".to_string(),
402 path: PathBuf::from("/path/to/project"),
403 is_paiml_crate: true,
404 };
405 let cloned = info.clone();
406 assert_eq!(cloned.name, info.name);
407 assert_eq!(cloned.path, info.path);
408 assert_eq!(cloned.is_paiml_crate, info.is_paiml_crate);
409 }
410
411 #[test]
412 fn test_project_info_non_paiml() {
413 let info = ProjectInfo {
414 name: "third-party-lib".to_string(),
415 path: PathBuf::from("/external/lib"),
416 is_paiml_crate: false,
417 };
418 assert!(!info.is_paiml_crate);
419 }
420
421 #[test]
422 fn test_discover_projects_in_tempdir() {
423 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
424 let project_dir = tempdir.path().join("my-project");
425 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
426
427 let cargo_toml = r#"
429[package]
430name = "my-project"
431version = "0.1.0"
432"#;
433 std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
434
435 let mut engine = StackComplyEngine::new(ComplyConfig::default());
436 let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
437
438 assert_eq!(projects.len(), 1);
439 assert_eq!(projects[0].name, "my-project");
440 assert!(!projects[0].is_paiml_crate);
441 }
442
443 #[test]
444 fn test_discover_projects_paiml_crate() {
445 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
446 let project_dir = tempdir.path().join("trueno");
447 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
448
449 let cargo_toml = r#"
451[package]
452name = "trueno"
453version = "0.1.0"
454"#;
455 std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
456
457 let mut engine = StackComplyEngine::new(ComplyConfig::default());
458 let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
459
460 assert_eq!(projects.len(), 1);
461 assert!(projects[0].is_paiml_crate);
462 }
463
464 #[test]
465 fn test_discover_projects_multiple() {
466 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
467
468 for name in &["proj-a", "proj-b"] {
470 let project_dir = tempdir.path().join(name);
471 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
472 let cargo_toml = format!(
473 r#"
474[package]
475name = "{}"
476version = "0.1.0"
477"#,
478 name
479 );
480 std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
481 }
482
483 let mut engine = StackComplyEngine::new(ComplyConfig::default());
484 let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
485
486 assert_eq!(projects.len(), 2);
487 }
488
489 #[test]
490 fn test_discover_projects_no_name() {
491 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
492 let project_dir = tempdir.path().join("unnamed");
493 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
494
495 let cargo_toml = r#"
497[package]
498version = "0.1.0"
499"#;
500 std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
501
502 let mut engine = StackComplyEngine::new(ComplyConfig::default());
503 let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
504
505 assert_eq!(projects.len(), 0);
506 }
507
508 #[test]
509 fn test_discover_projects_clears_cache() {
510 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
511 let project_dir = tempdir.path().join("proj");
512 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
513 std::fs::write(
514 project_dir.join("Cargo.toml"),
515 "[package]\nname = \"proj\"\nversion = \"0.1.0\"",
516 )
517 .expect("unexpected failure");
518
519 let mut engine = StackComplyEngine::new(ComplyConfig::default());
520
521 engine.discover_projects(tempdir.path()).expect("unexpected failure");
523 assert_eq!(engine.projects().len(), 1);
524
525 let empty_dir = tempfile::tempdir().expect("tempdir creation failed");
527 engine.discover_projects(empty_dir.path()).expect("unexpected failure");
528 assert_eq!(engine.projects().len(), 0);
529 }
530
531 #[test]
532 fn test_check_all_with_include_external() {
533 let config = ComplyConfig { include_external: true, ..Default::default() };
534
535 let engine = StackComplyEngine::new(config);
536 let report = engine.check_all();
537 assert_eq!(report.summary.total_projects, 0);
538 }
539
540 #[test]
541 fn test_check_rule_with_discovered_projects() {
542 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
543 let project_dir = tempdir.path().join("trueno");
544 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
545 std::fs::write(
546 project_dir.join("Cargo.toml"),
547 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
548 )
549 .expect("unexpected failure");
550
551 let mut engine = StackComplyEngine::new(ComplyConfig::default());
552 engine.discover_projects(tempdir.path()).expect("unexpected failure");
553
554 let report = engine.check_rule("makefile-targets");
555 assert!(!report.results.is_empty() || !report.errors.is_empty());
557 }
558
559 #[test]
560 fn test_available_rules_descriptions() {
561 let engine = StackComplyEngine::new(ComplyConfig::default());
562 let rules = engine.available_rules();
563
564 for (id, desc) in &rules {
565 assert!(!id.is_empty());
566 assert!(!desc.is_empty());
567 }
568 }
569
570 #[test]
571 fn test_engine_debug() {
572 let engine = StackComplyEngine::new(ComplyConfig::default());
573 let debug_str = format!("{:?}", engine);
574 assert!(debug_str.contains("StackComplyEngine"));
575 }
576
577 #[test]
578 fn test_project_info_debug() {
579 let info = ProjectInfo {
580 name: "test".to_string(),
581 path: PathBuf::from("/test"),
582 is_paiml_crate: false,
583 };
584 let debug_str = format!("{:?}", info);
585 assert!(debug_str.contains("ProjectInfo"));
586 }
587
588 #[test]
593 fn test_check_all_with_paiml_project_no_makefile() {
594 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
595 let project_dir = tempdir.path().join("trueno");
596 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
597 std::fs::write(
598 project_dir.join("Cargo.toml"),
599 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
600 )
601 .expect("unexpected failure");
602
603 let mut engine = StackComplyEngine::new(ComplyConfig::default());
604 engine.discover_projects(tempdir.path()).expect("unexpected failure");
605
606 let report = engine.check_all();
607 assert_eq!(report.summary.total_projects, 1);
609 assert!(report.summary.total_checks > 0);
610 }
611
612 #[test]
613 fn test_check_all_skips_non_paiml_without_include_external() {
614 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
615 let project_dir = tempdir.path().join("some-lib");
616 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
617 std::fs::write(
618 project_dir.join("Cargo.toml"),
619 "[package]\nname = \"some-lib\"\nversion = \"0.1.0\"",
620 )
621 .expect("unexpected failure");
622
623 let config = ComplyConfig::default(); let mut engine = StackComplyEngine::new(config);
625 engine.discover_projects(tempdir.path()).expect("unexpected failure");
626
627 let report = engine.check_all();
628 assert_eq!(report.summary.total_projects, 0);
630 }
631
632 #[test]
633 fn test_check_all_includes_non_paiml_with_include_external() {
634 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
635 let project_dir = tempdir.path().join("external-lib");
636 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
637 std::fs::write(
638 project_dir.join("Cargo.toml"),
639 "[package]\nname = \"external-lib\"\nversion = \"0.1.0\"",
640 )
641 .expect("unexpected failure");
642
643 let config = ComplyConfig { include_external: true, ..Default::default() };
644 let mut engine = StackComplyEngine::new(config);
645 engine.discover_projects(tempdir.path()).expect("unexpected failure");
646
647 let report = engine.check_all();
648 assert_eq!(report.summary.total_projects, 1);
650 }
651
652 #[test]
653 fn test_check_all_with_disabled_rule() {
654 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
655 let project_dir = tempdir.path().join("trueno");
656 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
657 std::fs::write(
658 project_dir.join("Cargo.toml"),
659 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
660 )
661 .expect("unexpected failure");
662
663 let mut config = ComplyConfig::default();
664 config.disabled_rules.push("makefile-targets".to_string());
665 let mut engine = StackComplyEngine::new(config);
666 engine.discover_projects(tempdir.path()).expect("unexpected failure");
667
668 let report = engine.check_all();
669 let trueno_results = report.results.get("trueno");
671 if let Some(results) = trueno_results {
672 assert!(!results.contains_key("makefile-targets"));
673 }
674 }
675
676 #[test]
677 fn test_check_all_with_exemption() {
678 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
679 let project_dir = tempdir.path().join("trueno");
680 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
681 std::fs::write(
682 project_dir.join("Cargo.toml"),
683 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
684 )
685 .expect("unexpected failure");
686
687 let mut config = ComplyConfig::default();
688 let mut override_cfg = ProjectOverride::default();
689 override_cfg.exempt_rules.push("makefile-targets".to_string());
690 config.project_overrides.insert("trueno".to_string(), override_cfg);
691
692 let mut engine = StackComplyEngine::new(config);
693 engine.discover_projects(tempdir.path()).expect("unexpected failure");
694
695 let report = engine.check_all();
696 assert!(report
698 .exemptions
699 .iter()
700 .any(|e| e.project == "trueno" && e.rule == "makefile-targets"));
701 }
702
703 #[test]
708 fn test_fix_all_dry_run_with_paiml_project() {
709 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
710 let project_dir = tempdir.path().join("trueno");
711 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
712 std::fs::write(
713 project_dir.join("Cargo.toml"),
714 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
715 )
716 .expect("unexpected failure");
717 let mut engine = StackComplyEngine::new(ComplyConfig::default());
720 engine.discover_projects(tempdir.path()).expect("unexpected failure");
721
722 let report = engine.fix_all(true); assert_eq!(report.summary.total_projects, 1);
725 }
726
727 #[test]
728 fn test_fix_all_actual_with_paiml_project() {
729 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
730 let project_dir = tempdir.path().join("trueno");
731 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
732 std::fs::write(
733 project_dir.join("Cargo.toml"),
734 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
735 )
736 .expect("unexpected failure");
737
738 let mut engine = StackComplyEngine::new(ComplyConfig::default());
739 engine.discover_projects(tempdir.path()).expect("unexpected failure");
740
741 let report = engine.fix_all(false); assert_eq!(report.summary.total_projects, 1);
743 }
744
745 #[test]
746 fn test_fix_all_skips_non_paiml() {
747 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
748 let project_dir = tempdir.path().join("external");
749 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
750 std::fs::write(
751 project_dir.join("Cargo.toml"),
752 "[package]\nname = \"external\"\nversion = \"0.1.0\"",
753 )
754 .expect("unexpected failure");
755
756 let mut engine = StackComplyEngine::new(ComplyConfig::default());
757 engine.discover_projects(tempdir.path()).expect("unexpected failure");
758
759 let report = engine.fix_all(false);
760 assert_eq!(report.summary.total_projects, 0);
762 }
763
764 #[test]
765 fn test_fix_all_skips_disabled_rules() {
766 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
767 let project_dir = tempdir.path().join("trueno");
768 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
769 std::fs::write(
770 project_dir.join("Cargo.toml"),
771 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
772 )
773 .expect("unexpected failure");
774
775 let mut config = ComplyConfig::default();
776 config.disabled_rules.push("makefile-targets".to_string());
778 config.disabled_rules.push("cargo-toml-consistency".to_string());
779 config.disabled_rules.push("ci-workflow-parity".to_string());
780 config.disabled_rules.push("code-duplication".to_string());
781 let mut engine = StackComplyEngine::new(config);
782 engine.discover_projects(tempdir.path()).expect("unexpected failure");
783
784 let report = engine.fix_all(false);
785 assert_eq!(report.summary.total_checks, 0);
787 }
788
789 #[test]
790 fn test_fix_all_with_exemption() {
791 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
792 let project_dir = tempdir.path().join("trueno");
793 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
794 std::fs::write(
795 project_dir.join("Cargo.toml"),
796 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
797 )
798 .expect("unexpected failure");
799
800 let mut config = ComplyConfig::default();
801 let mut override_cfg = ProjectOverride::default();
802 override_cfg.exempt_rules.push("makefile-targets".to_string());
803 config.project_overrides.insert("trueno".to_string(), override_cfg);
804
805 let mut engine = StackComplyEngine::new(config);
806 engine.discover_projects(tempdir.path()).expect("unexpected failure");
807
808 let report = engine.fix_all(false);
809 assert_eq!(report.summary.total_projects, 0);
813 }
814
815 #[test]
816 fn test_fix_all_passing_project_with_makefile() {
817 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
818 let project_dir = tempdir.path().join("trueno");
819 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
820 std::fs::write(
821 project_dir.join("Cargo.toml"),
822 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
823 )
824 .expect("unexpected failure");
825
826 let makefile = r#"test-fast:
828 cargo nextest run --lib
829
830test:
831 cargo nextest run
832
833lint:
834 cargo clippy
835
836fmt:
837 cargo fmt
838
839coverage:
840 cargo llvm-cov
841"#;
842 std::fs::write(project_dir.join("Makefile"), makefile).expect("fs write failed");
843
844 let mut engine = StackComplyEngine::new(ComplyConfig::default());
845 engine.discover_projects(tempdir.path()).expect("unexpected failure");
846
847 let report = engine.fix_all(false);
848 assert_eq!(report.summary.total_projects, 1);
850 }
851
852 #[test]
857 fn test_check_rule_skips_non_paiml() {
858 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
859 let project_dir = tempdir.path().join("external");
860 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
861 std::fs::write(
862 project_dir.join("Cargo.toml"),
863 "[package]\nname = \"external\"\nversion = \"0.1.0\"",
864 )
865 .expect("unexpected failure");
866
867 let mut engine = StackComplyEngine::new(ComplyConfig::default());
868 engine.discover_projects(tempdir.path()).expect("unexpected failure");
869
870 let report = engine.check_rule("makefile-targets");
871 assert_eq!(report.summary.total_projects, 0);
873 }
874
875 #[test]
876 fn test_check_rule_with_exemption() {
877 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
878 let project_dir = tempdir.path().join("trueno");
879 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
880 std::fs::write(
881 project_dir.join("Cargo.toml"),
882 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
883 )
884 .expect("unexpected failure");
885
886 let mut config = ComplyConfig::default();
887 let mut override_cfg = ProjectOverride::default();
888 override_cfg.exempt_rules.push("makefile-targets".to_string());
889 config.project_overrides.insert("trueno".to_string(), override_cfg);
890
891 let mut engine = StackComplyEngine::new(config);
892 engine.discover_projects(tempdir.path()).expect("unexpected failure");
893
894 let report = engine.check_rule("makefile-targets");
895 assert!(report
896 .exemptions
897 .iter()
898 .any(|e| e.project == "trueno" && e.rule == "makefile-targets"));
899 }
900
901 #[test]
902 fn test_check_rule_includes_non_paiml_with_external() {
903 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
904 let project_dir = tempdir.path().join("ext-proj");
905 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
906 std::fs::write(
907 project_dir.join("Cargo.toml"),
908 "[package]\nname = \"ext-proj\"\nversion = \"0.1.0\"",
909 )
910 .expect("unexpected failure");
911
912 let config = ComplyConfig { include_external: true, ..Default::default() };
913 let mut engine = StackComplyEngine::new(config);
914 engine.discover_projects(tempdir.path()).expect("unexpected failure");
915
916 let report = engine.check_rule("makefile-targets");
917 assert!(report.summary.total_projects > 0 || !report.results.is_empty());
919 }
920
921 #[test]
926 fn test_parse_project_workspace_toml() {
927 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
929 let project_dir = tempdir.path().join("workspace");
930 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
931 let cargo_toml = r#"
932[workspace]
933members = ["crate-a", "crate-b"]
934"#;
935 std::fs::write(project_dir.join("Cargo.toml"), cargo_toml).expect("fs write failed");
936
937 let mut engine = StackComplyEngine::new(ComplyConfig::default());
938 let projects = engine.discover_projects(tempdir.path()).expect("unexpected failure");
939 assert_eq!(projects.len(), 0);
941 }
942
943 #[test]
944 fn test_check_all_reports_rule_errors() {
945 let tempdir = tempfile::tempdir().expect("tempdir creation failed");
948 let project_dir = tempdir.path().join("trueno");
949 std::fs::create_dir_all(&project_dir).expect("mkdir failed");
950 std::fs::write(
951 project_dir.join("Cargo.toml"),
952 "[package]\nname = \"trueno\"\nversion = \"0.1.0\"",
953 )
954 .expect("unexpected failure");
955
956 let mut engine = StackComplyEngine::new(ComplyConfig::default());
957 engine.discover_projects(tempdir.path()).expect("unexpected failure");
958
959 let report = engine.check_all();
960 assert_eq!(report.summary.total_projects, 1);
962 assert!(report.summary.total_checks > 0);
963 }
964}