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::{CheckType, report::CheckResult};
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        handle_build_failure(&mut result, &output);
43    } else {
44        handle_build_success(&mut result, &output);
45    }
46
47    Ok(result)
48}
49
50/// Handle build failure output
51fn handle_build_failure(result: &mut CheckResult, output: &std::process::Output) {
52    result.add_error("Build failed");
53    result.add_suggestion("Fix compilation errors before proceeding");
54
55    let error_count = parse_build_errors(result, &output.stderr);
56
57    if error_count >= 3 {
58        result.add_error("... and more build errors (showing first 3)");
59    }
60
61    result.add_suggestion("Run 'cargo build' to see detailed error messages");
62    result.add_suggestion("Check for missing dependencies or syntax errors");
63}
64
65/// Parse build errors from stderr
66fn parse_build_errors(result: &mut CheckResult, stderr: &[u8]) -> usize {
67    let stderr = String::from_utf8_lossy(stderr);
68    let mut error_count = 0;
69
70    for line in stderr.lines() {
71        if line.starts_with("error") && error_count < 3 {
72            result.add_error(format!("Build: {}", line.trim()));
73            error_count += 1;
74        } else if line.trim().starts_with("-->") && error_count <= 3 {
75            result.add_context(format!("Location: {}", line.trim()));
76        }
77    }
78
79    error_count
80}
81
82/// Handle successful build output
83fn handle_build_success(result: &mut CheckResult, output: &std::process::Output) {
84    result.add_context("Project builds successfully in release mode");
85
86    // Check for warnings
87    let stderr = String::from_utf8_lossy(&output.stderr);
88    let warning_count = stderr
89        .lines()
90        .filter(|line| line.starts_with("warning:"))
91        .count();
92
93    if warning_count > 0 {
94        result.add_context(format!("Build completed with {} warnings", warning_count));
95    }
96}
97
98#[cfg(test)]
99#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
100mod tests {
101    use super::*;
102    use tempfile::TempDir;
103    use tokio::fs;
104
105    #[tokio::test]
106    async fn test_build_check_on_valid_project() {
107        let temp_dir = TempDir::new().unwrap();
108
109        // Create a basic Cargo.toml
110        let cargo_toml = r#"
111[package]
112name = "test"
113version = "0.1.0"
114edition = "2021"
115"#;
116        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
117            .await
118            .unwrap();
119
120        // Create src directory
121        fs::create_dir_all(temp_dir.path().join("src"))
122            .await
123            .unwrap();
124
125        // Create a valid main.rs
126        let main_rs = r#"fn main() {
127    println!("Hello, world!");
128}
129"#;
130        fs::write(temp_dir.path().join("src/main.rs"), main_rs)
131            .await
132            .unwrap();
133
134        let result = run(temp_dir.path()).await.unwrap();
135
136        // Should pass for valid code
137        assert!(result.passed);
138    }
139
140    #[test]
141    fn test_build_check_struct() {
142        assert_eq!(BuildCheck::name(), "build");
143        assert!(!BuildCheck::description().is_empty());
144    }
145}