Skip to main content

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