Skip to main content

entrenar/storage/preflight/checks/
environment.rs

1//! Environment preflight checks.
2
3use super::{CheckResult, CheckType, PreflightCheck};
4
5/// Compare available MB against a required threshold, returning a pass/fail `CheckResult`.
6fn check_mb_threshold(avail_mb: u64, required: u64, resource: &str) -> CheckResult {
7    if avail_mb >= required {
8        CheckResult::passed(format!("{avail_mb} MB {resource} available (minimum: {required} MB)"))
9    } else {
10        CheckResult::failed(format!(
11            "Only {avail_mb} MB {resource} available (minimum: {required} MB)"
12        ))
13    }
14}
15
16/// Parse the available disk space (MB) from `df -m .` output.
17#[cfg(unix)]
18fn parse_df_available_mb(stdout: &str) -> Option<u64> {
19    stdout
20        .lines()
21        .nth(1)
22        .and_then(|line| line.split_whitespace().nth(3))
23        .and_then(|s| s.parse::<u64>().ok())
24}
25
26/// Parse available memory (MB) from `free -m` output.
27#[cfg(unix)]
28fn parse_free_available_mb(stdout: &str) -> Option<u64> {
29    stdout.lines().nth(1).and_then(|line| {
30        let parts: Vec<&str> = line.split_whitespace().collect();
31        if parts.len() >= 7 {
32            parts[6].parse::<u64>().ok()
33        } else {
34            None
35        }
36    })
37}
38
39impl PreflightCheck {
40    // =========================================================================
41    // Built-in Environment Checks
42    // =========================================================================
43
44    /// Check available disk space
45    pub fn disk_space_mb(min_mb: u64) -> Self {
46        Self::new(
47            "disk_space",
48            CheckType::Environment,
49            format!("Ensures at least {min_mb} MB disk space available"),
50            move |_data, ctx| {
51                let required = ctx.min_disk_space_mb.unwrap_or(min_mb);
52
53                #[cfg(unix)]
54                {
55                    use std::process::Command;
56                    if let Ok(output) = Command::new("df").args(["-m", "."]).output() {
57                        let stdout = String::from_utf8_lossy(&output.stdout);
58                        if let Some(avail_mb) = parse_df_available_mb(&stdout) {
59                            return check_mb_threshold(avail_mb, required, "disk");
60                        }
61                    }
62                }
63
64                // Fallback: assume sufficient space
65                CheckResult::passed(format!("Disk space check passed (assumed >= {required} MB)"))
66            },
67        )
68    }
69
70    /// Check available memory
71    pub fn memory_mb(min_mb: u64) -> Self {
72        Self::new(
73            "memory",
74            CheckType::Environment,
75            format!("Ensures at least {min_mb} MB memory available"),
76            move |_data, ctx| {
77                let required = ctx.min_memory_mb.unwrap_or(min_mb);
78
79                #[cfg(unix)]
80                {
81                    use std::process::Command;
82                    if let Ok(output) = Command::new("free").args(["-m"]).output() {
83                        let stdout = String::from_utf8_lossy(&output.stdout);
84                        if let Some(avail_mb) = parse_free_available_mb(&stdout) {
85                            return check_mb_threshold(avail_mb, required, "memory");
86                        }
87                    }
88                }
89
90                // Fallback
91                CheckResult::passed(format!("Memory check passed (assumed >= {required} MB)"))
92            },
93        )
94    }
95
96    /// Check GPU availability
97    pub fn gpu_available() -> Self {
98        Self::new(
99            "gpu_available",
100            CheckType::Environment,
101            "Checks if GPU is available for training",
102            |_data, _ctx| {
103                // Check for NVIDIA GPU using nvidia-smi
104                #[cfg(unix)]
105                {
106                    use std::process::Command;
107                    let result = Command::new("nvidia-smi")
108                        .args(["--query-gpu=name", "--format=csv,noheader"])
109                        .output();
110
111                    if let Ok(output) = result {
112                        if output.status.success() {
113                            let gpu_name = String::from_utf8_lossy(&output.stdout);
114                            let gpu_name = gpu_name.trim();
115                            if !gpu_name.is_empty() {
116                                return CheckResult::passed(format!("GPU available: {gpu_name}"));
117                            }
118                        }
119                    }
120                }
121
122                CheckResult::warning("No GPU detected, training will use CPU")
123            },
124        )
125        .optional()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::storage::preflight::PreflightContext;
133
134    #[test]
135    fn test_disk_space_check_creation() {
136        let check = PreflightCheck::disk_space_mb(1000);
137        assert_eq!(check.name, "disk_space");
138        assert_eq!(check.check_type, CheckType::Environment);
139        assert!(check.description.contains("1000"));
140    }
141
142    #[test]
143    fn test_memory_check_creation() {
144        let check = PreflightCheck::memory_mb(512);
145        assert_eq!(check.name, "memory");
146        assert_eq!(check.check_type, CheckType::Environment);
147        assert!(check.description.contains("512"));
148    }
149
150    #[test]
151    fn test_gpu_available_check_creation() {
152        let check = PreflightCheck::gpu_available();
153        assert_eq!(check.name, "gpu_available");
154        assert_eq!(check.check_type, CheckType::Environment);
155        // Optional checks have required=false
156        assert!(!check.required);
157    }
158
159    #[test]
160    fn test_disk_space_check_runs() {
161        let check = PreflightCheck::disk_space_mb(1);
162        let ctx = PreflightContext::default();
163        let data: &[Vec<f64>] = &[];
164        let result = check.run(data, &ctx);
165        // Should either pass or use fallback
166        assert!(result.is_passed() || result.is_warning());
167    }
168
169    #[test]
170    fn test_memory_check_runs() {
171        let check = PreflightCheck::memory_mb(1);
172        let ctx = PreflightContext::default();
173        let data: &[Vec<f64>] = &[];
174        let result = check.run(data, &ctx);
175        // Should either pass or use fallback
176        assert!(result.is_passed() || result.is_warning());
177    }
178
179    #[test]
180    fn test_gpu_check_runs() {
181        let check = PreflightCheck::gpu_available();
182        let ctx = PreflightContext::default();
183        let data: &[Vec<f64>] = &[];
184        let result = check.run(data, &ctx);
185        // Should pass, warning, or fail (but not panic)
186        assert!(result.is_passed() || result.is_warning() || result.is_failed());
187    }
188
189    #[test]
190    fn test_disk_space_with_context_override() {
191        let check = PreflightCheck::disk_space_mb(1000);
192        let ctx = PreflightContext { min_disk_space_mb: Some(1), ..Default::default() };
193        let data: &[Vec<f64>] = &[];
194        let result = check.run(data, &ctx);
195        // With low threshold, should likely pass
196        assert!(result.is_passed() || result.is_warning());
197    }
198
199    #[test]
200    fn test_memory_with_context_override() {
201        let check = PreflightCheck::memory_mb(1000);
202        let ctx = PreflightContext { min_memory_mb: Some(1), ..Default::default() };
203        let data: &[Vec<f64>] = &[];
204        let result = check.run(data, &ctx);
205        // With low threshold, should likely pass
206        assert!(result.is_passed() || result.is_warning());
207    }
208}