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::{report::CheckResult, CheckType};
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
29pub async fn run(project_path: &Path) -> Result<CheckResult> {
30    let start = Instant::now();
31    let mut result = CheckResult::new(CheckType::Test);
32
33    // Run cargo test with comprehensive flags
34    let output = Command::new("cargo")
35        .current_dir(project_path)
36        .args(&["test", "--all-targets", "--all-features"])
37        .output()?;
38
39    result.set_duration(start.elapsed());
40
41    if !output.status.success() {
42        result.add_error("Tests failed");
43        result.add_suggestion("Fix failing tests before proceeding");
44
45        // Parse test failures
46        let stdout = String::from_utf8_lossy(&output.stdout);
47        let stderr = String::from_utf8_lossy(&output.stderr);
48
49        let mut failure_count = 0;
50        let mut in_failure = false;
51
52        for line in stdout.lines().chain(stderr.lines()) {
53            if line.starts_with("test ") && line.contains("FAILED") && failure_count < 5 {
54                result.add_error(format!("Test failure: {}", line.trim()));
55                failure_count += 1;
56            } else if line.starts_with("---- ") && line.contains("stdout ----") {
57                in_failure = true;
58            } else if in_failure && !line.trim().is_empty() && failure_count <= 5 {
59                result.add_context(format!("Test output: {}", line.trim()));
60                in_failure = false;
61            } else if line.contains("test result:") && line.contains("FAILED") {
62                result.add_error(line.trim().to_string());
63            }
64        }
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    } else {
73        // Parse successful test output
74        let stdout = String::from_utf8_lossy(&output.stdout);
75
76        for line in stdout.lines() {
77            if line.contains("test result: ok.") {
78                result.add_context(format!("Tests: {}", line.trim()));
79                break;
80            }
81        }
82
83        if result.context.is_empty() {
84            result.add_context("All tests passed");
85        }
86    }
87
88    Ok(result)
89}
90
91#[cfg(test)]
92#[allow(clippy::unwrap_used, clippy::expect_used)]
93mod tests {
94    use super::*;
95    use tempfile::TempDir;
96    use tokio::fs;
97
98    #[tokio::test]
99    async fn test_test_check_on_project_with_tests() {
100        let temp_dir = TempDir::new().unwrap();
101
102        // Create a basic Cargo.toml
103        let cargo_toml = r#"
104[package]
105name = "test"
106version = "0.1.0"
107edition = "2021"
108"#;
109        fs::write(temp_dir.path().join("Cargo.toml"), cargo_toml)
110            .await
111            .unwrap();
112
113        // Create src directory
114        fs::create_dir_all(temp_dir.path().join("src"))
115            .await
116            .unwrap();
117
118        // Create a lib.rs with tests
119        let lib_rs = r#"
120pub fn add(a: i32, b: i32) -> i32 {
121    a + b
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127    
128    #[test]
129    fn test_add() {
130        assert_eq!(add(2, 2), 4);
131    }
132}
133"#;
134        fs::write(temp_dir.path().join("src/lib.rs"), lib_rs)
135            .await
136            .unwrap();
137
138        let result = run(temp_dir.path()).await.unwrap();
139
140        // Should pass for working tests
141        assert!(result.passed);
142    }
143
144    #[test]
145    fn test_test_check_struct() {
146        assert_eq!(TestCheck::name(), "test");
147        assert!(!TestCheck::description().is_empty());
148    }
149}