1use serde::{Deserialize, Serialize};
21use std::fmt;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
29#[serde(rename_all = "lowercase")]
30pub enum CheckStatus {
31 Passed,
32 Warning,
33 Failed,
34}
35
36impl CheckStatus {
37 pub fn label(self) -> &'static str {
38 match self {
39 CheckStatus::Passed => "passed",
40 CheckStatus::Warning => "warning",
41 CheckStatus::Failed => "failed",
42 }
43 }
44}
45
46impl fmt::Display for CheckStatus {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 f.write_str(self.label())
49 }
50}
51
52#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct VerificationCheck {
62 pub name: String,
64 pub status: CheckStatus,
66 pub message: String,
68}
69
70impl VerificationCheck {
71 pub fn passed(name: impl Into<String>, message: impl Into<String>) -> Self {
73 Self {
74 name: name.into(),
75 status: CheckStatus::Passed,
76 message: message.into(),
77 }
78 }
79
80 pub fn warning(name: impl Into<String>, message: impl Into<String>) -> Self {
82 Self {
83 name: name.into(),
84 status: CheckStatus::Warning,
85 message: message.into(),
86 }
87 }
88
89 pub fn failed(name: impl Into<String>, message: impl Into<String>) -> Self {
91 Self {
92 name: name.into(),
93 status: CheckStatus::Failed,
94 message: message.into(),
95 }
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
110pub struct VerificationResult {
111 pub component_id: String,
113 pub success: bool,
115 pub checks: Vec<VerificationCheck>,
117}
118
119impl VerificationResult {
120 pub fn new(component_id: impl Into<String>, checks: Vec<VerificationCheck>) -> Self {
126 let success = !checks.iter().any(|c| c.status == CheckStatus::Failed);
127 Self {
128 component_id: component_id.into(),
129 success,
130 checks,
131 }
132 }
133
134 pub fn ok(component_id: impl Into<String>) -> Self {
136 Self {
137 component_id: component_id.into(),
138 success: true,
139 checks: vec![],
140 }
141 }
142
143 pub fn fail(component_id: impl Into<String>, reason: impl Into<String>) -> Self {
145 Self {
146 component_id: component_id.into(),
147 success: false,
148 checks: vec![VerificationCheck::failed("verification", reason)],
149 }
150 }
151
152 pub fn is_all_passed(&self) -> bool {
154 self.checks.iter().all(|c| c.status == CheckStatus::Passed)
155 }
156
157 pub fn has_warnings(&self) -> bool {
159 self.checks.iter().any(|c| c.status == CheckStatus::Warning)
160 }
161
162 pub fn check_count(&self) -> usize {
164 self.checks.len()
165 }
166}
167
168#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
181 fn test_verification_result_all_pass_success() {
182 let result = VerificationResult::new(
183 "pytorch",
184 vec![
185 VerificationCheck::passed("binary_exists", "Found at /usr/bin/python"),
186 VerificationCheck::passed("version_matches", "2.2.0 installed"),
187 VerificationCheck::passed("import_test", "import torch succeeded"),
188 ],
189 );
190 assert!(result.success, "All-pass should have success=true");
191 assert!(result.is_all_passed());
192 assert!(!result.has_warnings());
193 assert_eq!(result.check_count(), 3);
194 }
195
196 #[test]
197 fn test_verification_result_one_failure_not_success() {
198 let result = VerificationResult::new(
199 "triton",
200 vec![
201 VerificationCheck::passed("binary_exists", "Found"),
202 VerificationCheck::failed("import_test", "import triton failed with ImportError"),
203 ],
204 );
205 assert!(!result.success, "Any failure should have success=false");
206 assert!(!result.is_all_passed());
207 }
208
209 #[test]
210 fn test_verification_result_warnings_only_success() {
211 let result = VerificationResult::new(
212 "rocm",
213 vec![
214 VerificationCheck::passed("rocm_detect", "ROCm found at /opt/rocm"),
215 VerificationCheck::warning("version_old", "ROCm 7.0 is not the latest"),
216 ],
217 );
218 assert!(result.success, "Warnings-only should have success=true");
219 assert!(result.has_warnings());
220 assert!(!result.is_all_passed());
221 }
222
223 #[test]
224 fn test_verification_result_ok_empty() {
225 let result = VerificationResult::ok("empty-comp");
226 assert!(result.success);
227 assert!(result.checks.is_empty());
228 assert_eq!(result.check_count(), 0);
229 }
230
231 #[test]
232 fn test_verification_result_fail_single() {
233 let result = VerificationResult::fail("broken-comp", "installation failed");
234 assert!(!result.success);
235 assert_eq!(result.checks.len(), 1);
236 assert_eq!(result.checks[0].status, CheckStatus::Failed);
237 assert_eq!(result.checks[0].name, "verification");
238 }
239
240 #[test]
245 fn test_check_status_serde_roundtrip() {
246 let statuses = [
247 CheckStatus::Passed,
248 CheckStatus::Warning,
249 CheckStatus::Failed,
250 ];
251 for status in &statuses {
252 let json = serde_json::to_string(status).unwrap();
253 let back: CheckStatus = serde_json::from_str(&json).unwrap();
254 assert_eq!(*status, back);
255 }
256 }
257
258 #[test]
259 fn test_check_status_labels() {
260 assert_eq!(CheckStatus::Passed.label(), "passed");
261 assert_eq!(CheckStatus::Warning.label(), "warning");
262 assert_eq!(CheckStatus::Failed.label(), "failed");
263 }
264
265 #[test]
266 fn test_check_status_display() {
267 assert_eq!(format!("{}", CheckStatus::Passed), "passed");
268 assert_eq!(format!("{}", CheckStatus::Warning), "warning");
269 assert_eq!(format!("{}", CheckStatus::Failed), "failed");
270 }
271
272 #[test]
277 fn test_verification_check_constructors() {
278 let passed = VerificationCheck::passed("test", "all good");
279 assert_eq!(passed.status, CheckStatus::Passed);
280
281 let warning = VerificationCheck::warning("test", "be careful");
282 assert_eq!(warning.status, CheckStatus::Warning);
283
284 let failed = VerificationCheck::failed("test", "broke");
285 assert_eq!(failed.status, CheckStatus::Failed);
286 }
287
288 #[test]
293 fn test_verification_check_serde_roundtrip() {
294 let check = VerificationCheck::failed("import_test", "ModuleNotFoundError");
295 let json = serde_json::to_string(&check).unwrap();
296 let back: VerificationCheck = serde_json::from_str(&json).unwrap();
297 assert_eq!(check, back);
298 }
299
300 #[test]
301 fn test_verification_result_serde_roundtrip() {
302 let result = VerificationResult::new(
303 "pytorch",
304 vec![
305 VerificationCheck::passed("binary", "found"),
306 VerificationCheck::warning("version", "not latest"),
307 ],
308 );
309 let json = serde_json::to_string(&result).unwrap();
310 let back: VerificationResult = serde_json::from_str(&json).unwrap();
311 assert_eq!(result, back);
312 }
313
314 #[test]
315 fn test_verification_result_serializes_to_json() {
316 let result = VerificationResult::new(
317 "rocm",
318 vec![
319 VerificationCheck::passed("rocm_path", "/opt/rocm exists"),
320 VerificationCheck::passed("version", "7.2.1"),
321 ],
322 );
323 let json = serde_json::to_string(&result).unwrap();
324 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
326 assert_eq!(parsed["component_id"], "rocm");
327 assert_eq!(parsed["success"], true);
328 assert!(parsed["checks"].is_array());
329 assert_eq!(parsed["checks"].as_array().unwrap().len(), 2);
330 }
331
332 #[test]
333 fn test_verification_result_failure_serializes_correctly() {
334 let result = VerificationResult::new(
335 "triton",
336 vec![VerificationCheck::failed("import", "failed")],
337 );
338 let json = serde_json::to_string(&result).unwrap();
339 let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
340 assert_eq!(parsed["success"], false);
341 }
342}