use std::path::Path;
use std::process::Command;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifyStep {
Format,
Clippy,
Test,
Ratchets,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StepResult {
Pass,
Fail(String),
Skip(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifyReport {
pub format: StepResult,
pub clippy: StepResult,
pub test: StepResult,
pub ratchets: StepResult,
}
#[derive(Error, Debug)]
pub enum VerifyError {
#[error("Failed to execute cargo command: {0}")]
CommandExecution(String),
#[error("I/O error during verification: {0}")]
Io(#[from] std::io::Error),
}
impl VerifyReport {
pub fn is_success(&self) -> bool {
matches!(self.format, StepResult::Pass | StepResult::Skip(_))
&& matches!(self.clippy, StepResult::Pass | StepResult::Skip(_))
&& matches!(self.test, StepResult::Pass | StepResult::Skip(_))
&& matches!(self.ratchets, StepResult::Pass | StepResult::Skip(_))
}
}
pub fn run_all(target_dir: &Path) -> Result<VerifyReport, VerifyError> {
let format = run_format_check(target_dir)?;
let clippy = run_clippy(target_dir)?;
let test = run_tests(target_dir)?;
let ratchets = run_ratchets(target_dir)?;
Ok(VerifyReport {
format,
clippy,
test,
ratchets,
})
}
fn run_format_check(target_dir: &Path) -> Result<StepResult, VerifyError> {
let output = Command::new("cargo")
.arg("fmt")
.arg("--check")
.current_dir(target_dir)
.output()?;
if output.status.success() {
Ok(StepResult::Pass)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let message = format!("{}\n{}", stdout, stderr).trim().to_string();
Ok(StepResult::Fail(message))
}
}
fn run_clippy(target_dir: &Path) -> Result<StepResult, VerifyError> {
let output = Command::new("cargo")
.arg("clippy")
.arg("--all-targets")
.arg("--all-features")
.current_dir(target_dir)
.output()?;
if output.status.success() {
Ok(StepResult::Pass)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let message = format!("{}\n{}", stdout, stderr).trim().to_string();
Ok(StepResult::Fail(message))
}
}
fn run_tests(target_dir: &Path) -> Result<StepResult, VerifyError> {
let nextest_check = Command::new("cargo")
.arg("nextest")
.arg("--version")
.output();
match nextest_check {
Ok(output) if output.status.success() => {
let test_output = Command::new("cargo")
.arg("nextest")
.arg("run")
.current_dir(target_dir)
.output()?;
if test_output.status.success() {
Ok(StepResult::Pass)
} else {
let stderr = String::from_utf8_lossy(&test_output.stderr);
let stdout = String::from_utf8_lossy(&test_output.stdout);
let message = format!("{}\n{}", stdout, stderr).trim().to_string();
Ok(StepResult::Fail(message))
}
}
Ok(_) | Err(_) => {
Ok(StepResult::Skip("cargo-nextest not installed".to_string()))
}
}
}
fn run_ratchets(target_dir: &Path) -> Result<StepResult, VerifyError> {
let output = match Command::new("ratchets")
.arg("check")
.current_dir(target_dir)
.output()
{
Ok(output) => output,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
return Ok(StepResult::Skip("ratchets not installed".to_string()));
}
Err(e) => return Err(VerifyError::Io(e)),
};
if output.status.success() {
Ok(StepResult::Pass)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
let stdout = String::from_utf8_lossy(&output.stdout);
let message = format!("{}\n{}", stdout, stderr).trim().to_string();
Ok(StepResult::Fail(message))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_is_success_requires_ratchets() {
let report = VerifyReport {
format: StepResult::Pass,
clippy: StepResult::Pass,
test: StepResult::Pass,
ratchets: StepResult::Fail("budget exceeded".to_string()),
};
assert!(
!report.is_success(),
"a failing ratchets step must fail the report"
);
}
#[test]
fn test_is_success_allows_skipped_ratchets() {
let report = VerifyReport {
format: StepResult::Pass,
clippy: StepResult::Pass,
test: StepResult::Pass,
ratchets: StepResult::Skip("ratchets not installed".to_string()),
};
assert!(report.is_success(), "a skipped ratchets step must not fail");
}
}