ferrous_forge/safety/checks/
build.rs

1//! Build checking with cargo build
2
3use crate::Result;
4use std::path::Path;
5use std::process::Command;
6use std::time::Instant;
7
8use super::SafetyCheck;
9use crate::safety::{report::CheckResult, CheckType};
10
11/// Build check implementation
12pub struct BuildCheck;
13
14impl SafetyCheck for BuildCheck {
15    async fn run(project_path: &Path) -> Result<CheckResult> {
16        run(project_path).await
17    }
18
19    fn name() -> &'static str {
20        "build"
21    }
22
23    fn description() -> &'static str {
24        "Ensures project builds successfully in release mode"
25    }
26}
27
28/// Run cargo build --release
29pub async fn run(project_path: &Path) -> Result<CheckResult> {
30    let start = Instant::now();
31    let mut result = CheckResult::new(CheckType::Build);
32
33    // Run cargo build --release
34    let output = Command::new("cargo")
35        .current_dir(project_path)
36        .args(&["build", "--release"])
37        .output()?;
38
39    result.set_duration(start.elapsed());
40
41    if !output.status.success() {
42        result.add_error("Build failed");
43        result.add_suggestion("Fix compilation errors before proceeding");
44
45        // Parse build errors
46        let stderr = String::from_utf8_lossy(&output.stderr);
47        let mut error_count = 0;
48
49        for line in stderr.lines() {
50            if line.starts_with("error") && error_count < 3 {
51                result.add_error(format!("Build: {}", line.trim()));
52                error_count += 1;
53            } else if line.trim().starts_with("-->") && error_count <= 3 {
54                result.add_context(format!("Location: {}", line.trim()));
55            }
56        }
57
58        if error_count >= 3 {
59            result.add_error("... and more build errors (showing first 3)");
60        }
61
62        result.add_suggestion("Run 'cargo build' to see detailed error messages");
63        result.add_suggestion("Check for missing dependencies or syntax errors");
64    } else {
65        result.add_context("Project builds successfully in release mode");
66
67        // Check for warnings
68        let stderr = String::from_utf8_lossy(&output.stderr);
69        let warning_count = stderr
70            .lines()
71            .filter(|line| line.starts_with("warning:"))
72            .count();
73
74        if warning_count > 0 {
75            result.add_context(format!("Build completed with {} warnings", warning_count));
76        }
77    }
78
79    Ok(result)
80}
81
82#[cfg(test)]
83#[allow(clippy::unwrap_used, clippy::expect_used)]
84mod tests {
85    use super::*;
86    use tempfile::TempDir;
87    use tokio::fs;
88
89    #[tokio::test]
90    async fn test_build_check_on_valid_project() {
91        let temp_dir = TempDir::new().unwrap();
92
93        // Create a basic Cargo.toml
94        let cargo_toml = r#"
95[package]
96name = "test"
97version = "0.1.0"
98edition = "2021"
99"#;
100        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
101            .await
102            .unwrap();
103
104        // Create src directory
105        fs::create_dir_all(temp_dir.path().join("src"))
106            .await
107            .unwrap();
108
109        // Create a valid main.rs
110        let main_rs = r#"fn main() {
111    println!("Hello, world!");
112}
113"#;
114        fs::write(temp_dir.path().join("src/main.rs"), main_rs)
115            .await
116            .unwrap();
117
118        let result = run(temp_dir.path()).await.unwrap();
119
120        // Should pass for valid code
121        assert!(result.passed);
122    }
123
124    #[test]
125    fn test_build_check_struct() {
126        assert_eq!(BuildCheck::name(), "build");
127        assert!(!BuildCheck::description().is_empty());
128    }
129}