Skip to main content

ferrous_forge/safety/checks/
test.rs

1//! Test execution checking
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/// Test check implementation
12pub 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
28/// Run cargo test --all-targets --all-features
29///
30/// # Errors
31///
32/// Returns an error if the `cargo test` 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::Test);
36
37    // Run cargo test with comprehensive flags
38    // Disable the Ferrous Forge cargo wrapper to avoid recursive validation
39    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
56/// Handle test failure output
57fn 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
74/// Parse test failure output
75fn 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
96/// Handle successful test output
97fn 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        // Create a basic Cargo.toml
122        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        // Create src directory
133        fs::create_dir_all(temp_dir.path().join("src"))
134            .await
135            .unwrap();
136
137        // Create a lib.rs with tests
138        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        // Should pass for working tests
161        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}