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> {
30 let start = Instant::now();
31 let mut result = CheckResult::new(CheckType::Build);
32
33 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
50fn 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
65fn 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
82fn handle_build_success(result: &mut CheckResult, output: &std::process::Output) {
84 result.add_context("Project builds successfully in release mode");
85
86 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 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 fs::create_dir_all(temp_dir.path().join("src"))
122 .await
123 .unwrap();
124
125 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 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}