Skip to main content

rusty_stack/verification/
mod.rs

1//! Native Rust verification module.
2//!
3//! Ports the verification logic from:
4//! - `scripts/verify_installation.sh` → `full_verify()`
5//! - `scripts/enhanced_verify_installation.sh` → `enhanced_verify()`
6//! - `scripts/verify_and_build.sh` → `verify_and_build()`
7//!
8//! All verification uses `component_status.rs` detection — no subprocess
9//! calls to legacy shell scripts.
10
11use crate::component_status;
12use serde::Serialize;
13
14// ---------------------------------------------------------------------------
15// Public types
16// ---------------------------------------------------------------------------
17
18/// Result of a single verification check.
19#[derive(Debug, Clone, Serialize)]
20pub struct VerificationItem {
21    pub component_id: String,
22    pub label: String,
23    pub installed: bool,
24    pub category: String,
25}
26
27/// Result of a full verification run.
28#[derive(Debug, Clone, Serialize)]
29pub struct VerificationResult {
30    pub mode: String,
31    pub items: Vec<VerificationItem>,
32    pub passed: usize,
33    pub failed: usize,
34    pub total: usize,
35    pub all_passed: bool,
36}
37
38// ---------------------------------------------------------------------------
39// Component lists for each verification mode
40// ---------------------------------------------------------------------------
41
42/// Components checked by `verify_installation.sh` (basic/full mode).
43fn full_verify_components() -> Vec<(&'static str, &'static str)> {
44    vec![
45        ("rocm", "ROCm"),
46        ("pytorch", "PyTorch"),
47        ("triton", "Triton"),
48        ("mpi4py", "MPI4Py"),
49        ("deepspeed", "DeepSpeed"),
50        ("ml-stack-core", "ML Stack Core"),
51    ]
52}
53
54/// Components checked by `enhanced_verify_installation.sh` (all components).
55fn enhanced_verify_components() -> Vec<(&'static str, &'static str)> {
56    let mut components = full_verify_components();
57    components.extend(vec![
58        ("flash-attn", "Flash Attention"),
59        ("vllm", "vLLM"),
60        ("aiter", "AITER"),
61        ("onnx", "ONNX Runtime"),
62        ("bitsandbytes", "bitsandbytes"),
63        ("rocm-smi", "ROCm SMI"),
64        ("migraphx", "MIGraphX"),
65        ("pytorch-profiler", "PyTorch Profiler"),
66        ("wandb", "Weights & Biases"),
67        ("megatron", "Megatron-LM"),
68        ("comfyui", "ComfyUI"),
69        ("textgen", "text-generation-webui"),
70        ("vllm-studio", "vLLM Studio"),
71        ("llama-cpp", "llama.cpp (HIP)"),
72        ("basic-env", "Basic Environment"),
73        ("enhanced-env", "Enhanced Environment"),
74        ("permanent-env", "Permanent Environment"),
75    ]);
76    components
77}
78
79/// Components checked by `verify_and_build.sh` (build-critical components).
80fn build_verify_components() -> Vec<(&'static str, &'static str)> {
81    let mut components = full_verify_components();
82    components.extend(vec![
83        ("onnx", "ONNX Runtime"),
84        ("flash-attn", "Flash Attention"),
85        ("migraphx", "MIGraphX"),
86    ]);
87    components
88}
89
90// ---------------------------------------------------------------------------
91// Core verification logic
92// ---------------------------------------------------------------------------
93
94/// Run verification for a list of components using component_status detection.
95fn verify_components(component_list: &[(impl AsRef<str>, impl AsRef<str>)]) -> VerificationResult {
96    let python_candidates = component_status::python_interpreters();
97    let mut items = Vec::new();
98    let mut passed = 0usize;
99    let mut failed = 0usize;
100
101    for (id, label) in component_list {
102        let id_str = id.as_ref();
103        let label_str = label.as_ref();
104        let installed = component_status::is_component_installed_by_id(id_str, &python_candidates);
105        if installed {
106            passed += 1;
107        } else {
108            failed += 1;
109        }
110        items.push(VerificationItem {
111            component_id: id_str.to_string(),
112            label: label_str.to_string(),
113            installed,
114            category: category_for_component(id_str),
115        });
116    }
117
118    let total = items.len();
119    VerificationResult {
120        mode: "full".to_string(),
121        items,
122        passed,
123        failed,
124        total,
125        all_passed: failed == 0,
126    }
127}
128
129// ---------------------------------------------------------------------------
130// Public API
131// ---------------------------------------------------------------------------
132
133/// Full verification — equivalent to `verify_installation.sh`.
134///
135/// Checks core components: ROCm, PyTorch, Triton, MPI4Py, DeepSpeed, ML Stack Core.
136pub fn full_verify() -> VerificationResult {
137    let mut result = verify_components(&full_verify_components());
138    result.mode = "full".to_string();
139    result
140}
141
142/// Enhanced verification — equivalent to `enhanced_verify_installation.sh`.
143///
144/// Checks all components including ROCm tools, Python imports, and environment files.
145pub fn enhanced_verify() -> VerificationResult {
146    let mut result = verify_components(&enhanced_verify_components());
147    result.mode = "enhanced".to_string();
148    result
149}
150
151/// Verify and build — equivalent to `verify_and_build.sh`.
152///
153/// Checks core + build-critical components. Returns the verification result;
154/// the caller (CLI) is responsible for triggering rebuilds for failed components.
155pub fn verify_and_build() -> VerificationResult {
156    let mut result = verify_components(&build_verify_components());
157    result.mode = "build".to_string();
158    result
159}
160
161/// Get the list of failed component IDs from a verification result.
162pub fn failed_components(result: &VerificationResult) -> Vec<String> {
163    result
164        .items
165        .iter()
166        .filter(|item| !item.installed)
167        .map(|item| item.component_id.clone())
168        .collect()
169}
170
171// ---------------------------------------------------------------------------
172// Helpers
173// ---------------------------------------------------------------------------
174
175fn category_for_component(id: &str) -> String {
176    match id {
177        "rocm" | "rocm-smi" => "ROCm".to_string(),
178        "pytorch" | "triton" | "flash-attn" | "onnx" | "migraphx" | "pytorch-profiler" => {
179            "ML Framework".to_string()
180        }
181        "mpi4py" | "deepspeed" | "megatron" => "Training".to_string(),
182        "vllm" | "aiter" | "vllm-studio" => "Inference".to_string(),
183        "bitsandbytes" | "wandb" => "Utilities".to_string(),
184        "ml-stack-core" => "Core".to_string(),
185        "comfyui" | "textgen" => "Applications".to_string(),
186        "basic-env" | "enhanced-env" | "permanent-env" => "Environment".to_string(),
187        _ => "Other".to_string(),
188    }
189}
190
191// ---------------------------------------------------------------------------
192// Output formatting
193// ---------------------------------------------------------------------------
194
195/// Format verification result as human-readable text.
196pub fn format_result_human(result: &VerificationResult) -> String {
197    let mut output = String::new();
198    let mode_label = match result.mode.as_str() {
199        "full" => "Full Verification",
200        "enhanced" => "Enhanced Verification",
201        "build" => "Verify & Build",
202        _ => "Verification",
203    };
204
205    output.push_str(&format!(
206        "Rusty Stack {} — {} components checked\n",
207        mode_label, result.total
208    ));
209    output.push_str(&format!(
210        "  Passed: {}  |  Failed: {}  |  Result: {}\n\n",
211        result.passed,
212        result.failed,
213        if result.all_passed {
214            "ALL PASSED ✓"
215        } else {
216            "SOME FAILED ✗"
217        }
218    ));
219
220    for item in &result.items {
221        let status = if item.installed { "✓" } else { "✗" };
222        output.push_str(&format!(
223            "  {} {:30} [{}]\n",
224            status, item.label, item.category
225        ));
226    }
227
228    if !result.all_passed {
229        output.push_str("\nFailed components:\n");
230        for item in &result.items {
231            if !item.installed {
232                output.push_str(&format!("  - {} ({})\n", item.label, item.component_id));
233            }
234        }
235    }
236
237    output
238}
239
240/// Format verification result as JSON.
241pub fn format_result_json(result: &VerificationResult) -> String {
242    serde_json::to_string_pretty(result).unwrap_or_else(|e| {
243        format!(
244            "{{\"error\": \"failed to serialize verification result: {}\"}}",
245            e
246        )
247    })
248}
249
250// ---------------------------------------------------------------------------
251// Tests
252// ---------------------------------------------------------------------------
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn test_full_verify_returns_result() {
260        let result = full_verify();
261        assert_eq!(result.mode, "full");
262        assert!(
263            result.total > 0,
264            "Full verify should check at least 1 component"
265        );
266        assert_eq!(result.total, result.passed + result.failed);
267    }
268
269    #[test]
270    fn test_enhanced_verify_returns_more_components() {
271        let full = full_verify();
272        let enhanced = enhanced_verify();
273        assert!(
274            enhanced.total > full.total,
275            "Enhanced should check more components than full ({} > {})",
276            enhanced.total,
277            full.total
278        );
279    }
280
281    #[test]
282    fn test_verify_and_build_includes_build_components() {
283        let result = verify_and_build();
284        assert_eq!(result.mode, "build");
285        // Should include onnx, flash-attn, migraphx (build-critical)
286        let ids: Vec<&str> = result
287            .items
288            .iter()
289            .map(|i| i.component_id.as_str())
290            .collect();
291        assert!(ids.contains(&"onnx"), "build verify should include onnx");
292        assert!(
293            ids.contains(&"flash-attn"),
294            "build verify should include flash-attn"
295        );
296        assert!(
297            ids.contains(&"migraphx"),
298            "build verify should include migraphx"
299        );
300    }
301
302    #[test]
303    fn test_failed_components_extracts_failures() {
304        let result = VerificationResult {
305            mode: "test".to_string(),
306            items: vec![
307                VerificationItem {
308                    component_id: "rocm".to_string(),
309                    label: "ROCm".to_string(),
310                    installed: true,
311                    category: "ROCm".to_string(),
312                },
313                VerificationItem {
314                    component_id: "pytorch".to_string(),
315                    label: "PyTorch".to_string(),
316                    installed: false,
317                    category: "ML Framework".to_string(),
318                },
319            ],
320            passed: 1,
321            failed: 1,
322            total: 2,
323            all_passed: false,
324        };
325        let failed = failed_components(&result);
326        assert_eq!(failed, vec!["pytorch"]);
327    }
328
329    #[test]
330    fn test_format_human_output() {
331        let result = full_verify();
332        let text = format_result_human(&result);
333        assert!(
334            text.contains("Verification"),
335            "Human output should contain 'Verification'"
336        );
337        assert!(
338            text.contains("Passed:"),
339            "Human output should contain 'Passed:'"
340        );
341    }
342
343    #[test]
344    fn test_format_json_output() {
345        let result = full_verify();
346        let json = format_result_json(&result);
347        let parsed: serde_json::Value =
348            serde_json::from_str(&json).expect("JSON should be parseable");
349        assert!(parsed.get("mode").is_some(), "JSON should contain 'mode'");
350        assert!(parsed.get("items").is_some(), "JSON should contain 'items'");
351        assert!(
352            parsed.get("all_passed").is_some(),
353            "JSON should contain 'all_passed'"
354        );
355    }
356
357    #[test]
358    fn test_full_verify_includes_core_components() {
359        let result = full_verify();
360        let ids: Vec<&str> = result
361            .items
362            .iter()
363            .map(|i| i.component_id.as_str())
364            .collect();
365        assert!(ids.contains(&"rocm"), "full verify should include rocm");
366        assert!(
367            ids.contains(&"pytorch"),
368            "full verify should include pytorch"
369        );
370        assert!(ids.contains(&"triton"), "full verify should include triton");
371        assert!(ids.contains(&"mpi4py"), "full verify should include mpi4py");
372        assert!(
373            ids.contains(&"deepspeed"),
374            "full verify should include deepspeed"
375        );
376        assert!(
377            ids.contains(&"ml-stack-core"),
378            "full verify should include ml-stack-core"
379        );
380    }
381
382    #[test]
383    fn test_enhanced_verify_includes_all_categories() {
384        let result = enhanced_verify();
385        let categories: Vec<&str> = result.items.iter().map(|i| i.category.as_str()).collect();
386        assert!(
387            categories.contains(&"ROCm"),
388            "enhanced should include ROCm category"
389        );
390        assert!(
391            categories.contains(&"ML Framework"),
392            "enhanced should include ML Framework category"
393        );
394        assert!(
395            categories.contains(&"Environment"),
396            "enhanced should include Environment category"
397        );
398    }
399
400    #[test]
401    fn test_no_shell_subprocess_in_verification() {
402        // Verification uses component_status::is_component_installed_by_id which
403        // does detection via filesystem checks and python module probing — no bash
404        // subprocess to shell scripts. This test verifies the module compiles and
405        // runs without calling any shell verification scripts.
406        let result = full_verify();
407        assert!(result.total > 0);
408    }
409}