ferrous_forge/safety/checks/
test.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 TestCheck;
13
14impl SafetyCheck for TestCheck {
15 async fn run(project_path: &Path) -> Result<CheckResult> {
16 run(project_path).await
17 }
18
19 fn name() -> &'static str {
20 "test"
21 }
22
23 fn description() -> &'static str {
24 "Runs the complete test suite"
25 }
26}
27
28pub async fn run(project_path: &Path) -> Result<CheckResult> {
34 let start = Instant::now();
35 let mut result = CheckResult::new(CheckType::Test);
36
37 let output = Command::new("cargo")
40 .current_dir(project_path)
41 .env("FERROUS_FORGE_ENABLED", "0")
42 .args(["test", "--all-targets", "--all-features"])
43 .output()?;
44
45 result.set_duration(start.elapsed());
46
47 if !output.status.success() {
48 handle_test_failures(&mut result, &output);
49 } else {
50 handle_test_success(&mut result, &output);
51 }
52
53 Ok(result)
54}
55
56fn handle_test_failures(result: &mut CheckResult, output: &std::process::Output) {
58 result.add_error("Tests failed");
59 result.add_suggestion("Fix failing tests before proceeding");
60
61 let stdout = String::from_utf8_lossy(&output.stdout);
62 let stderr = String::from_utf8_lossy(&output.stderr);
63
64 let failure_count = parse_test_failures(result, &stdout, &stderr);
65
66 if failure_count >= 5 {
67 result.add_error("... and more test failures (showing first 5)");
68 }
69
70 result.add_suggestion("Run 'cargo test' to see detailed test output");
71 result.add_suggestion("Check test logic and fix failing assertions");
72}
73
74fn parse_test_failures(result: &mut CheckResult, stdout: &str, stderr: &str) -> usize {
76 let mut failure_count = 0;
77 let mut in_failure = false;
78
79 for line in stdout.lines().chain(stderr.lines()) {
80 if line.starts_with("test ") && line.contains("FAILED") && failure_count < 5 {
81 result.add_error(format!("Test failure: {}", line.trim()));
82 failure_count += 1;
83 } else if line.starts_with("---- ") && line.contains("stdout ----") {
84 in_failure = true;
85 } else if in_failure && !line.trim().is_empty() && failure_count <= 5 {
86 result.add_context(format!("Test output: {}", line.trim()));
87 in_failure = false;
88 } else if line.contains("test result:") && line.contains("FAILED") {
89 result.add_error(line.trim().to_string());
90 }
91 }
92
93 failure_count
94}
95
96fn handle_test_success(result: &mut CheckResult, output: &std::process::Output) {
98 let stdout = String::from_utf8_lossy(&output.stdout);
99
100 for line in stdout.lines() {
101 if line.contains("test result: ok.") {
102 result.add_context(format!("Tests: {}", line.trim()));
103 return;
104 }
105 }
106
107 result.add_context("All tests passed");
108}
109
110#[cfg(test)]
111#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
112mod tests {
113 use super::*;
114 use tempfile::TempDir;
115 use tokio::fs;
116
117 #[tokio::test]
118 async fn test_test_check_on_project_with_tests() {
119 let temp_dir = TempDir::new().unwrap();
120
121 let cargo_toml = r#"
123[package]
124name = "test"
125version = "0.1.0"
126edition = "2021"
127"#;
128 fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
129 .await
130 .unwrap();
131
132 fs::create_dir_all(temp_dir.path().join("src"))
134 .await
135 .unwrap();
136
137 let lib_rs = r#"
139pub fn add(a: i32, b: i32) -> i32 {
140 a + b
141}
142
143#[cfg(test)]
144#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
145mod tests {
146 use super::*;
147
148 #[test]
149 fn test_add() {
150 assert_eq!(add(2, 2), 4);
151 }
152}
153"#;
154 fs::write(temp_dir.path().join("src/lib.rs"), lib_rs)
155 .await
156 .unwrap();
157
158 let result = run(temp_dir.path()).await.unwrap();
159
160 assert!(result.passed);
162 }
163
164 #[test]
165 fn test_test_check_struct() {
166 assert_eq!(TestCheck::name(), "test");
167 assert!(!TestCheck::description().is_empty());
168 }
169}