entrenar/storage/preflight/checks/
environment.rs1use super::{CheckResult, CheckType, PreflightCheck};
4
5fn 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#[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#[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 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 CheckResult::passed(format!("Disk space check passed (assumed >= {required} MB)"))
66 },
67 )
68 }
69
70 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 CheckResult::passed(format!("Memory check passed (assumed >= {required} MB)"))
92 },
93 )
94 }
95
96 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 #[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 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 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 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 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 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 assert!(result.is_passed() || result.is_warning());
207 }
208}