ferrous_forge/safety/checks/
build.rs1use 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
11pub 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
28pub async fn run(project_path: &Path) -> Result<CheckResult> {
34 let start = Instant::now();
35 let mut result = CheckResult::new(CheckType::Build);
36
37 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
54fn 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
69fn 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
86fn handle_build_success(result: &mut CheckResult, output: &std::process::Output) {
88 result.add_context("Project builds successfully in release mode");
89
90 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 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 fs::create_dir_all(temp_dir.path().join("src"))
126 .await
127 .unwrap();
128
129 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 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}