1use std::path::Path;
4use std::process::Command;
5use thiserror::Error;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum VerifyStep {
10 Format,
11 Clippy,
12 Test,
13 Ratchets,
14}
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum StepResult {
19 Pass,
20 Fail(String),
21 Skip(String),
22}
23
24#[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#[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 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
53pub 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
68fn 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
86fn 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
105fn run_tests(target_dir: &Path) -> Result<StepResult, VerifyError> {
107 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 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 Ok(StepResult::Skip("cargo-nextest not installed".to_string()))
134 }
135 }
136}
137
138fn 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}