Skip to main content

rusty_stack/bootstrap/
mod.rs

1//! Native Rust bootstrap module.
2//!
3//! Ports the three bootstrap scripts to native Rust:
4//! - `scripts/install.sh` → `install_sh_clone_and_build()`
5//! - `scripts/run_rusty_stack.sh` → `tui_launcher()`
6//! - `scripts/enhanced_setup_environment.sh` → `setup_environment()`
7//!
8//! # Validation Assertions
9//!
10//! - **VAL-VBA-012**: install.sh equivalent — clone and build
11//! - **VAL-VBA-013**: run_rusty_stack.sh equivalent — TUI launcher
12//! - **VAL-VBA-014**: enhanced_setup_environment.sh — GPU filtering
13//! - **VAL-VBA-015**: enhanced_setup_environment.sh — system dependency checking
14//! - **VAL-VBA-016**: enhanced_setup_environment.sh — environment file creation
15//! - **VAL-VBA-017**: enhanced_setup_environment.sh — ROCm optimization
16
17pub mod env_setup;
18pub mod install;
19pub mod launcher;
20
21// Re-export key types
22pub use env_setup::{
23    check_system_dependency, create_env_file, detect_correct_gpu_arch, detect_discrete_gpus,
24    DependencyCheckResult, EnvFileResult, GpuArchInfo, GpuFilterResult, SystemDependency,
25};
26pub use install::{clone_or_update_repo, install_sh, InstallConfig, InstallResult};
27pub use launcher::{launch_tui, BuildMode, LaunchResult};
28
29// ---------------------------------------------------------------------------
30// Tests
31// ---------------------------------------------------------------------------
32
33#[cfg(test)]
34mod tests {
35    use super::*;
36
37    // --- VAL-VBA-012: install.sh equivalent — clone and build ---
38
39    #[test]
40    fn test_install_config_defaults() {
41        let config = InstallConfig::default();
42        assert_eq!(
43            config.repo_url,
44            "https://github.com/scooter-lacroix/Stan-s-ML-Stack.git"
45        );
46        assert!(!config.skip_build);
47        assert_eq!(config.branch, "main");
48    }
49
50    #[test]
51    fn test_install_config_custom() {
52        let config = InstallConfig {
53            repo_dir: "/tmp/test-repo".to_string(),
54            repo_url: "https://example.com/repo.git".to_string(),
55            skip_build: true,
56            branch: "develop".to_string(),
57        };
58        assert_eq!(config.repo_dir, "/tmp/test-repo");
59        assert!(config.skip_build);
60        assert_eq!(config.branch, "develop");
61    }
62
63    #[test]
64    fn test_install_result_success() {
65        let result = InstallResult::success(
66            "/tmp/repo".to_string(),
67            "/tmp/repo/rusty-stack/target/release/rusty-stack".to_string(),
68        );
69        assert!(result.success);
70        assert!(result.binary_path.is_some());
71        assert!(result.errors.is_empty());
72    }
73
74    #[test]
75    fn test_install_result_failure() {
76        let result = InstallResult::failure("Build failed".to_string());
77        assert!(!result.success);
78        assert!(result.binary_path.is_none());
79        assert!(!result.errors.is_empty());
80    }
81
82    // --- VAL-VBA-013: run_rusty_stack.sh equivalent — TUI launcher ---
83
84    #[test]
85    fn test_build_mode_default() {
86        let mode = BuildMode::default();
87        assert_eq!(mode, BuildMode::Release);
88    }
89
90    #[test]
91    fn test_launch_result_success() {
92        let result = LaunchResult::success("/path/to/rusty-stack".to_string());
93        assert!(result.success);
94        assert!(result.binary_path.is_some());
95    }
96
97    #[test]
98    fn test_launch_result_failure() {
99        let result = LaunchResult::failure("Build failed".to_string());
100        assert!(!result.success);
101        assert!(result.binary_path.is_none());
102    }
103
104    // --- VAL-VBA-014: GPU filtering ---
105
106    #[test]
107    fn test_gpu_arch_info_default() {
108        let info = GpuArchInfo::default();
109        assert_eq!(info.gpu_arch, "gfx1100");
110        assert_eq!(info.hsa_override_gfx_version, "11.0.0");
111    }
112
113    #[test]
114    fn test_gpu_arch_info_gfx1101() {
115        let info = GpuArchInfo::from_arch("gfx1101");
116        assert_eq!(info.gpu_arch, "gfx1101");
117        assert_eq!(info.hsa_override_gfx_version, "11.0.1");
118    }
119
120    #[test]
121    fn test_gpu_arch_info_gfx1102() {
122        let info = GpuArchInfo::from_arch("gfx1102");
123        assert_eq!(info.gpu_arch, "gfx1102");
124        assert_eq!(info.hsa_override_gfx_version, "11.0.2");
125    }
126
127    #[test]
128    fn test_gpu_arch_info_gfx1030() {
129        let info = GpuArchInfo::from_arch("gfx1030");
130        assert_eq!(info.gpu_arch, "gfx1030");
131        assert_eq!(info.hsa_override_gfx_version, "10.3.0");
132    }
133
134    #[test]
135    fn test_gpu_arch_info_gfx1200() {
136        let info = GpuArchInfo::from_arch("gfx1200");
137        assert_eq!(info.gpu_arch, "gfx1200");
138        assert_eq!(info.hsa_override_gfx_version, "12.0.0");
139    }
140
141    #[test]
142    fn test_gpu_arch_info_unknown_falls_back() {
143        let info = GpuArchInfo::from_arch("gfx9999");
144        assert_eq!(info.gpu_arch, "gfx9999");
145        assert_eq!(info.hsa_override_gfx_version, "11.0.0"); // default fallback
146    }
147
148    #[test]
149    fn test_gpu_filter_result_no_gpus() {
150        let result = GpuFilterResult {
151            discrete_gpu_indices: vec![],
152            hip_visible_devices: "0".to_string(),
153            gpu_count: 0,
154        };
155        assert!(result.discrete_gpu_indices.is_empty());
156        // Should still have a default device
157        assert_eq!(result.hip_visible_devices, "0");
158    }
159
160    #[test]
161    fn test_gpu_filter_result_with_gpus() {
162        let result = GpuFilterResult {
163            discrete_gpu_indices: vec![0, 1],
164            hip_visible_devices: "0,1".to_string(),
165            gpu_count: 2,
166        };
167        assert_eq!(result.discrete_gpu_indices.len(), 2);
168        assert_eq!(result.hip_visible_devices, "0,1");
169    }
170
171    // --- VAL-VBA-015: System dependency checking ---
172
173    #[test]
174    fn test_system_dependency_list() {
175        let deps = SystemDependency::required_packages();
176        assert!(deps.contains(&"git"), "git should be required");
177        assert!(deps.contains(&"cmake"), "cmake should be required");
178        assert!(
179            deps.contains(&"python3-dev"),
180            "python3-dev should be required"
181        );
182        assert!(
183            deps.contains(&"build-essential"),
184            "build-essential should be required"
185        );
186    }
187
188    #[test]
189    fn test_dependency_check_result_installed() {
190        let result = DependencyCheckResult {
191            package: "git".to_string(),
192            installed: true,
193            mapped_name: "git".to_string(),
194        };
195        assert!(result.installed);
196    }
197
198    #[test]
199    fn test_dependency_check_result_missing() {
200        let result = DependencyCheckResult {
201            package: "nonexistent-pkg".to_string(),
202            installed: false,
203            mapped_name: "nonexistent-pkg".to_string(),
204        };
205        assert!(!result.installed);
206    }
207
208    // --- VAL-VBA-016: Environment file creation ---
209
210    #[test]
211    fn test_env_file_result_success() {
212        let result = EnvFileResult::success("/home/user/.mlstack_env".to_string());
213        assert!(result.success);
214        assert!(result.path.is_some());
215        assert!(result.errors.is_empty());
216    }
217
218    #[test]
219    fn test_env_file_result_failure() {
220        let result = EnvFileResult::failure("Permission denied".to_string());
221        assert!(!result.success);
222        assert!(result.path.is_none());
223    }
224
225    #[test]
226    fn test_env_file_content_has_required_variables() {
227        let content = env_setup::generate_env_file_content(
228            "0",         // hip_visible_devices
229            "/opt/rocm", // rocm_path
230            "7.2.0",     // rocm_version
231            "latest",    // rocm_channel
232            "gfx1100",   // gpu_arch
233            "11.0.0",    // hsa_override_gfx_version
234            "/usr/bin/python3",
235        );
236        // Must contain all required variable exports
237        assert!(
238            content.contains("HIP_VISIBLE_DEVICES"),
239            "env file must set HIP_VISIBLE_DEVICES"
240        );
241        assert!(
242            content.contains("CUDA_VISIBLE_DEVICES"),
243            "env file must set CUDA_VISIBLE_DEVICES"
244        );
245        assert!(
246            content.contains("PYTORCH_ROCM_DEVICE"),
247            "env file must set PYTORCH_ROCM_DEVICE"
248        );
249        assert!(content.contains("ROCM_HOME"), "env file must set ROCM_HOME");
250        assert!(
251            content.contains("ROCM_VERSION"),
252            "env file must set ROCM_VERSION"
253        );
254        assert!(
255            content.contains("ROCM_CHANNEL"),
256            "env file must set ROCM_CHANNEL"
257        );
258        assert!(content.contains("GPU_ARCH"), "env file must set GPU_ARCH");
259        assert!(
260            content.contains("LD_LIBRARY_PATH"),
261            "env file must set LD_LIBRARY_PATH"
262        );
263        assert!(content.contains("PATH"), "env file must update PATH");
264    }
265
266    #[test]
267    fn test_env_file_content_is_shell_sourceable() {
268        let content = env_setup::generate_env_file_content(
269            "0",
270            "/opt/rocm",
271            "7.2.0",
272            "latest",
273            "gfx1100",
274            "11.0.0",
275            "/usr/bin/python3",
276        );
277        // The content should be valid shell syntax — test with bash -n
278        // (We can't actually run bash -n in unit tests, but we can check structure)
279        assert!(
280            content.starts_with("# ML Stack Environment File"),
281            "env file must have header comment"
282        );
283        assert!(
284            content.contains("export "),
285            "env file must have export statements"
286        );
287        // No bash-specific syntax errors: should not contain unmatched quotes
288        let export_count = content.matches("export ").count();
289        assert!(
290            export_count >= 10,
291            "should have at least 10 export statements, got {export_count}"
292        );
293    }
294
295    // --- VAL-VBA-017: ROCm optimization ---
296
297    #[test]
298    fn test_rocm_optimization_hsa_override() {
299        let content = env_setup::generate_env_file_content(
300            "0",
301            "/opt/rocm",
302            "7.2.0",
303            "latest",
304            "gfx1100",
305            "11.0.0",
306            "/usr/bin/python3",
307        );
308        assert!(
309            content.contains("HSA_OVERRIDE_GFX_VERSION"),
310            "must set HSA_OVERRIDE_GFX_VERSION"
311        );
312        assert!(
313            content.contains("11.0.0"),
314            "must contain the correct HSA_OVERRIDE_GFX_VERSION value"
315        );
316    }
317
318    #[test]
319    fn test_rocm_optimization_miopen_settings() {
320        let content = env_setup::generate_env_file_content(
321            "0",
322            "/opt/rocm",
323            "7.2.0",
324            "latest",
325            "gfx1100",
326            "11.0.0",
327            "/usr/bin/python3",
328        );
329        assert!(
330            content.contains("MIOPEN_DEBUG_CONV_IMPLICIT_GEMM"),
331            "must set MIOPEN_DEBUG_CONV_IMPLICIT_GEMM"
332        );
333        assert!(
334            content.contains("MIOPEN_FIND_MODE"),
335            "must set MIOPEN_FIND_MODE"
336        );
337        assert!(
338            content.contains("MIOPEN_FIND_ENFORCE"),
339            "must set MIOPEN_FIND_ENFORCE"
340        );
341    }
342
343    #[test]
344    fn test_rocm_optimization_performance_vars() {
345        let content = env_setup::generate_env_file_content(
346            "0",
347            "/opt/rocm",
348            "7.2.0",
349            "latest",
350            "gfx1100",
351            "11.0.0",
352            "/usr/bin/python3",
353        );
354        assert!(
355            content.contains("HSA_ENABLE_SDMA"),
356            "must set HSA_ENABLE_SDMA"
357        );
358        assert!(
359            content.contains("GPU_MAX_HEAP_SIZE"),
360            "must set GPU_MAX_HEAP_SIZE"
361        );
362        assert!(
363            content.contains("GPU_MAX_ALLOC_PERCENT"),
364            "must set GPU_MAX_ALLOC_PERCENT"
365        );
366    }
367
368    #[test]
369    fn test_rocm_optimization_pytorch_vars() {
370        let content = env_setup::generate_env_file_content(
371            "0",
372            "/opt/rocm",
373            "7.2.0",
374            "latest",
375            "gfx1100",
376            "11.0.0",
377            "/usr/bin/python3",
378        );
379        assert!(
380            !content.contains("TORCH_CUDA_ARCH_LIST"),
381            "must NOT set TORCH_CUDA_ARCH_LIST (Stage 2: no CUDA env leakage)"
382        );
383        assert!(
384            content.contains("PYTORCH_ALLOC_CONF"),
385            "must set PYTORCH_ALLOC_CONF"
386        );
387        assert!(
388            content.contains("PYTORCH_HIP_ALLOC_CONF"),
389            "must set PYTORCH_HIP_ALLOC_CONF"
390        );
391    }
392
393    #[test]
394    fn test_rocm_optimization_mpi_vars() {
395        let content = env_setup::generate_env_file_content(
396            "0",
397            "/opt/rocm",
398            "7.2.0",
399            "latest",
400            "gfx1100",
401            "11.0.0",
402            "/usr/bin/python3",
403        );
404        assert!(
405            !content.contains("opal_cuda_support"),
406            "must NOT set any OMPI CUDA-opal MCA var (Stage 2: CUDA opal path is for CUDA-built MPI; ROCm uses the ROCm accelerator path)"
407        );
408        assert!(content.contains("OMPI_MCA_pml"), "must set OMPI MCA PML");
409    }
410
411    #[test]
412    fn test_rocm_optimization_channel_selection() {
413        let content = env_setup::generate_env_file_content(
414            "0",
415            "/opt/rocm",
416            "7.0.0",
417            "legacy",
418            "gfx1030",
419            "10.3.0",
420            "/usr/bin/python3",
421        );
422        assert!(content.contains("ROCM_CHANNEL"), "must set ROCM_CHANNEL");
423        assert!(content.contains("legacy"), "must contain the channel value");
424    }
425
426    // --- Integration: detect_correct_gpu_arch mapping ---
427
428    #[test]
429    fn test_gpu_arch_from_marketing_name_7900_xtx() {
430        let arch = detect_correct_gpu_arch(Some("Radeon RX 7900 XTX"));
431        assert_eq!(arch.gpu_arch, "gfx1100");
432    }
433
434    #[test]
435    fn test_gpu_arch_from_marketing_name_7800_xt() {
436        let arch = detect_correct_gpu_arch(Some("Radeon RX 7800 XT"));
437        assert_eq!(arch.gpu_arch, "gfx1101");
438    }
439
440    #[test]
441    fn test_gpu_arch_from_marketing_name_7600() {
442        let arch = detect_correct_gpu_arch(Some("Radeon RX 7600"));
443        assert_eq!(arch.gpu_arch, "gfx1102");
444    }
445
446    #[test]
447    fn test_gpu_arch_from_marketing_name_9070_xt() {
448        let arch = detect_correct_gpu_arch(Some("Radeon RX 9070 XT"));
449        assert_eq!(arch.gpu_arch, "gfx1200");
450    }
451
452    #[test]
453    fn test_gpu_arch_from_marketing_name_none() {
454        let arch = detect_correct_gpu_arch(None);
455        assert_eq!(arch.gpu_arch, "gfx1100"); // default fallback
456    }
457
458    #[test]
459    fn test_gpu_arch_from_marketing_name_unknown() {
460        let arch = detect_correct_gpu_arch(Some("Unknown GPU"));
461        assert_eq!(arch.gpu_arch, "gfx1100"); // default fallback
462    }
463
464    // --- Integration: detect_discrete_gpus filtering ---
465
466    #[test]
467    fn test_is_integrated_gpu_filtering() {
468        // These should be identified as integrated GPUs
469        assert!(env_setup::is_integrated_gpu_name("Raphael"));
470        assert!(env_setup::is_integrated_gpu_name("AMD Ryzen Graphics"));
471        assert!(env_setup::is_integrated_gpu_name("Integrated Graphics"));
472        assert!(env_setup::is_integrated_gpu_name("AMD Radeon Graphics"));
473        // These should NOT be identified as integrated
474        assert!(!env_setup::is_integrated_gpu_name("Radeon RX 7900 XTX"));
475        assert!(!env_setup::is_integrated_gpu_name("Radeon RX 7800 XT"));
476    }
477}