Skip to main content

rusty_stack/core/
verification.rs

1//! Post-apply verification result types.
2//!
3//! This module implements:
4//! - [`VerificationCheck`] — a single named check with status and message.
5//! - [`VerificationResult`] — aggregate result for a component, containing
6//!   multiple checks and a computed `success` field.
7//!
8//! # Schema Completeness (VAL-CORE-016)
9//!
10//! A `VerificationResult` must contain:
11//! - `component_id` — identifies the verified component
12//! - `success` — `true` if all checks passed or only warned
13//! - `checks` — list of individual check results
14//!
15//! Success rules:
16//! - All checks pass → `success: true`
17//! - Any check fails → `success: false`
18//! - Warnings only → `success: true`
19
20use serde::{Deserialize, Serialize};
21use std::fmt;
22
23// ---------------------------------------------------------------------------
24// CheckStatus
25// ---------------------------------------------------------------------------
26
27/// Status of a single verification check.
28#[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// ---------------------------------------------------------------------------
53// VerificationCheck
54// ---------------------------------------------------------------------------
55
56/// A single verification check within a component's verification result.
57///
58/// Each check has a name (e.g., "binary_exists", "version_matches"),
59/// a status (passed/warning/failed), and an optional message with details.
60#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
61pub struct VerificationCheck {
62    /// Name of the check (e.g., "binary_exists").
63    pub name: String,
64    /// Result status of this check.
65    pub status: CheckStatus,
66    /// Human-readable details about the check outcome.
67    pub message: String,
68}
69
70impl VerificationCheck {
71    /// Create a passing check.
72    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    /// Create a warning check.
81    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    /// Create a failing check.
90    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// ---------------------------------------------------------------------------
100// VerificationResult
101// ---------------------------------------------------------------------------
102
103/// Aggregate verification result for a single component.
104///
105/// Contains the component ID, a list of individual checks, and a computed
106/// `success` field derived from the check statuses:
107/// - `success: true` if all checks passed or only warned
108/// - `success: false` if any check failed
109#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
110pub struct VerificationResult {
111    /// Component that was verified.
112    pub component_id: String,
113    /// Whether verification succeeded overall.
114    pub success: bool,
115    /// Individual check results.
116    pub checks: Vec<VerificationCheck>,
117}
118
119impl VerificationResult {
120    /// Create a new verification result from individual checks.
121    ///
122    /// The `success` field is automatically computed:
123    /// - `true` if no check has `CheckStatus::Failed`
124    /// - `false` if any check failed
125    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    /// Create a successful result with no checks.
135    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    /// Create a failed result with a single check.
144    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    /// Whether all checks passed (no warnings, no failures).
153    pub fn is_all_passed(&self) -> bool {
154        self.checks.iter().all(|c| c.status == CheckStatus::Passed)
155    }
156
157    /// Whether there are any warnings.
158    pub fn has_warnings(&self) -> bool {
159        self.checks.iter().any(|c| c.status == CheckStatus::Warning)
160    }
161
162    /// Number of checks.
163    pub fn check_count(&self) -> usize {
164        self.checks.len()
165    }
166}
167
168// ===========================================================================
169// Tests
170// ===========================================================================
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175
176    // =======================================================================
177    // VAL-CORE-016: VerificationResult schema completeness
178    // =======================================================================
179
180    #[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    // =======================================================================
241    // CheckStatus
242    // =======================================================================
243
244    #[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    // =======================================================================
273    // VerificationCheck
274    // =======================================================================
275
276    #[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    // =======================================================================
289    // Serde roundtrip
290    // =======================================================================
291
292    #[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        // Verify JSON structure
325        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}