Skip to main content

rust_bucket/
verify.rs

1// Verification and validation logic
2
3use std::path::Path;
4use std::process::Command;
5use thiserror::Error;
6
7/// Verification step types
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VerifyStep {
10    Format,
11    Clippy,
12    Test,
13    Ratchets,
14}
15
16/// Result of running a verification step
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum StepResult {
19    Pass,
20    Fail(String),
21    Skip(String),
22}
23
24/// Report containing results of all verification steps
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct VerifyReport {
27    pub format: StepResult,
28    pub clippy: StepResult,
29    pub test: StepResult,
30    pub ratchets: StepResult,
31}
32
33/// Errors that can occur during verification
34#[derive(Error, Debug)]
35pub enum VerifyError {
36    #[error("Failed to execute cargo command: {0}")]
37    CommandExecution(String),
38
39    #[error("I/O error during verification: {0}")]
40    Io(#[from] std::io::Error),
41}
42
43impl VerifyReport {
44    /// Check if all verification steps passed or were skipped
45    pub fn is_success(&self) -> bool {
46        matches!(self.format, StepResult::Pass | StepResult::Skip(_))
47            && matches!(self.clippy, StepResult::Pass | StepResult::Skip(_))
48            && matches!(self.test, StepResult::Pass | StepResult::Skip(_))
49            && matches!(self.ratchets, StepResult::Pass | StepResult::Skip(_))
50    }
51}
52
53/// Run all verification steps on the target directory
54pub fn run_all(target_dir: &Path) -> Result<VerifyReport, VerifyError> {
55    let format = run_format_check(target_dir)?;
56    let clippy = run_clippy(target_dir)?;
57    let test = run_tests(target_dir)?;
58    let ratchets = run_ratchets(target_dir)?;
59
60    Ok(VerifyReport {
61        format,
62        clippy,
63        test,
64        ratchets,
65    })
66}
67
68/// Run cargo fmt --check
69fn run_format_check(target_dir: &Path) -> Result<StepResult, VerifyError> {
70    let output = Command::new("cargo")
71        .arg("fmt")
72        .arg("--check")
73        .current_dir(target_dir)
74        .output()?;
75
76    if output.status.success() {
77        Ok(StepResult::Pass)
78    } else {
79        let stderr = String::from_utf8_lossy(&output.stderr);
80        let stdout = String::from_utf8_lossy(&output.stdout);
81        let message = format!("{}\n{}", stdout, stderr).trim().to_string();
82        Ok(StepResult::Fail(message))
83    }
84}
85
86/// Run cargo clippy --all-targets --all-features
87fn run_clippy(target_dir: &Path) -> Result<StepResult, VerifyError> {
88    let output = Command::new("cargo")
89        .arg("clippy")
90        .arg("--all-targets")
91        .arg("--all-features")
92        .current_dir(target_dir)
93        .output()?;
94
95    if output.status.success() {
96        Ok(StepResult::Pass)
97    } else {
98        let stderr = String::from_utf8_lossy(&output.stderr);
99        let stdout = String::from_utf8_lossy(&output.stdout);
100        let message = format!("{}\n{}", stdout, stderr).trim().to_string();
101        Ok(StepResult::Fail(message))
102    }
103}
104
105/// Run cargo nextest run
106fn run_tests(target_dir: &Path) -> Result<StepResult, VerifyError> {
107    // First check if cargo-nextest is available
108    let nextest_check = Command::new("cargo")
109        .arg("nextest")
110        .arg("--version")
111        .output();
112
113    match nextest_check {
114        Ok(output) if output.status.success() => {
115            // cargo-nextest is available, run tests
116            let test_output = Command::new("cargo")
117                .arg("nextest")
118                .arg("run")
119                .current_dir(target_dir)
120                .output()?;
121
122            if test_output.status.success() {
123                Ok(StepResult::Pass)
124            } else {
125                let stderr = String::from_utf8_lossy(&test_output.stderr);
126                let stdout = String::from_utf8_lossy(&test_output.stdout);
127                let message = format!("{}\n{}", stdout, stderr).trim().to_string();
128                Ok(StepResult::Fail(message))
129            }
130        }
131        Ok(_) | Err(_) => {
132            // cargo-nextest not installed
133            Ok(StepResult::Skip("cargo-nextest not installed".to_string()))
134        }
135    }
136}
137
138/// Run `ratchets check`
139///
140/// Skips (rather than fails) when the `ratchets` binary is not on `PATH`, since
141/// it is installed separately from cargo and may be absent outside the
142/// devcontainer.
143fn run_ratchets(target_dir: &Path) -> Result<StepResult, VerifyError> {
144    let output = match Command::new("ratchets")
145        .arg("check")
146        .current_dir(target_dir)
147        .output()
148    {
149        Ok(output) => output,
150        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
151            return Ok(StepResult::Skip("ratchets not installed".to_string()));
152        }
153        Err(e) => return Err(VerifyError::Io(e)),
154    };
155
156    if output.status.success() {
157        Ok(StepResult::Pass)
158    } else {
159        let stderr = String::from_utf8_lossy(&output.stderr);
160        let stdout = String::from_utf8_lossy(&output.stdout);
161        let message = format!("{}\n{}", stdout, stderr).trim().to_string();
162        Ok(StepResult::Fail(message))
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_is_success_requires_ratchets() {
172        let report = VerifyReport {
173            format: StepResult::Pass,
174            clippy: StepResult::Pass,
175            test: StepResult::Pass,
176            ratchets: StepResult::Fail("budget exceeded".to_string()),
177        };
178        assert!(
179            !report.is_success(),
180            "a failing ratchets step must fail the report"
181        );
182    }
183
184    #[test]
185    fn test_is_success_allows_skipped_ratchets() {
186        let report = VerifyReport {
187            format: StepResult::Pass,
188            clippy: StepResult::Pass,
189            test: StepResult::Pass,
190            ratchets: StepResult::Skip("ratchets not installed".to_string()),
191        };
192        assert!(report.is_success(), "a skipped ratchets step must not fail");
193    }
194}