rusty_stack/adapter/
legacy_adapter.rs1use super::{Adapter, AdapterError, AdapterOutput};
20use crate::core::types::ExecutorKind;
21use std::path::PathBuf;
22use std::process::Command;
23use std::time::Instant;
24
25pub struct LegacyAdapter {
50 scripts_dir: PathBuf,
52}
53
54impl LegacyAdapter {
55 pub fn new(scripts_dir: impl Into<PathBuf>) -> Self {
57 Self {
58 scripts_dir: scripts_dir.into(),
59 }
60 }
61
62 pub fn resolve_script_path(&self, component_id: &str) -> Option<PathBuf> {
67 let info = crate::platform::registry::get_component(component_id)?;
68 if info.installer_script.is_empty() {
69 return None;
70 }
71 Some(self.scripts_dir.join(&info.installer_script))
72 }
73
74 pub fn build_command(
78 &self,
79 component_id: &str,
80 version: &str,
81 ) -> Result<Command, AdapterError> {
82 let script_path = self.resolve_script_path(component_id).ok_or_else(|| {
83 AdapterError::AdapterNotFound {
84 component_id: component_id.to_string(),
85 }
86 })?;
87
88 let mut cmd = Command::new("bash");
89 cmd.arg(&script_path);
90 if !version.is_empty() {
91 cmd.arg(version);
92 }
93
94 Ok(cmd)
95 }
96}
97
98impl Adapter for LegacyAdapter {
99 fn execute(&self, component_id: &str, version: &str) -> Result<AdapterOutput, AdapterError> {
100 let start = Instant::now();
101
102 let script_path = self.resolve_script_path(component_id).ok_or_else(|| {
103 AdapterError::AdapterNotFound {
104 component_id: component_id.to_string(),
105 }
106 })?;
107
108 if !script_path.exists() {
110 return Err(AdapterError::InvocationError {
111 component_id: component_id.to_string(),
112 reason: format!("script not found: {}", script_path.display()),
113 });
114 }
115
116 let mut cmd = Command::new("bash");
118 cmd.arg(&script_path);
119 if !version.is_empty() {
120 cmd.arg(version);
121 }
122
123 let output = cmd.output().map_err(|e| AdapterError::InvocationError {
125 component_id: component_id.to_string(),
126 reason: format!("failed to execute script: {e}"),
127 })?;
128
129 let stdout = String::from_utf8_lossy(&output.stdout).to_string();
130 let stderr = String::from_utf8_lossy(&output.stderr).to_string();
131 let exit_code = output.status.code().unwrap_or(-1);
132 let duration_ms = start.elapsed().as_millis() as u64;
133
134 Ok(AdapterOutput {
135 component_id: component_id.to_string(),
136 executor_kind: ExecutorKind::LegacyScript,
137 stdout,
138 stderr,
139 exit_code,
140 duration_ms,
141 })
142 }
143
144 fn executor_kind(&self) -> ExecutorKind {
145 ExecutorKind::LegacyScript
146 }
147}
148
149#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn create_mock_script_dir() -> (tempfile::TempDir, PathBuf) {
163 let dir = tempfile::tempdir().unwrap();
164 let scripts_dir = dir.path().to_path_buf();
165
166 std::fs::write(
168 scripts_dir.join("install_pytorch_rocm.sh"),
169 "#!/bin/bash\necho \"PyTorch $1 installed successfully\"\nexit 0\n",
170 )
171 .unwrap();
172
173 std::fs::write(
174 scripts_dir.join("install_triton_multi.sh"),
175 "#!/bin/bash\necho \"Triton $1 installed successfully\"\nexit 0\n",
176 )
177 .unwrap();
178
179 std::fs::write(
180 scripts_dir.join("install_flash_attention_ck.sh"),
181 "#!/bin/bash\necho \"Flash Attention $1 installed successfully\"\nexit 0\n",
182 )
183 .unwrap();
184
185 std::fs::write(
187 scripts_dir.join("install_deepspeed.sh"),
188 "#!/bin/bash\necho \"Error: DeepSpeed $1 installation failed\" >&2\nexit 1\n",
189 )
190 .unwrap();
191
192 std::fs::write(
194 scripts_dir.join("install_vllm_multi.sh"),
195 "#!/bin/bash\necho \"vLLM $1 partially installed\"\necho \"Warning: some deps missing\" >&2\nexit 2\n",
196 )
197 .unwrap();
198
199 (dir, scripts_dir)
200 }
201
202 #[test]
207 fn test_legacy_adapter_forwards_version_argument() {
208 let (_dir, scripts_dir) = create_mock_script_dir();
209 let adapter = LegacyAdapter::new(&scripts_dir);
210
211 let result = adapter.execute("pytorch", "2.5.0").unwrap();
212 assert_eq!(result.exit_code, 0);
213 assert!(result.stdout.contains("2.5.0"));
214 assert!(result.stdout.contains("PyTorch"));
215 }
216
217 #[test]
218 fn test_legacy_adapter_forwards_correct_script_per_component() {
219 let (_dir, scripts_dir) = create_mock_script_dir();
220 let adapter = LegacyAdapter::new(&scripts_dir);
221
222 let result = adapter.execute("pytorch", "2.5.0").unwrap();
224 assert!(result.stdout.contains("PyTorch"));
225
226 let result = adapter.execute("triton", "3.1.0").unwrap();
228 assert!(result.stdout.contains("Triton"));
229 assert!(result.stdout.contains("3.1.0"));
230 }
231
232 #[test]
233 fn test_legacy_adapter_forwards_multiple_components() {
234 let (_dir, scripts_dir) = create_mock_script_dir();
235 let adapter = LegacyAdapter::new(&scripts_dir);
236
237 let result = adapter.execute("flash-attn", "2.6.0").unwrap();
239 assert!(result.stdout.contains("Flash Attention"));
240 assert!(result.stdout.contains("2.6.0"));
241 assert_eq!(result.exit_code, 0);
242 }
243
244 #[test]
249 fn test_legacy_adapter_captures_stdout() {
250 let (_dir, scripts_dir) = create_mock_script_dir();
251 let adapter = LegacyAdapter::new(&scripts_dir);
252
253 let result = adapter.execute("pytorch", "2.5.0").unwrap();
254 assert!(!result.stdout.is_empty());
255 assert!(result.stdout.contains("installed"));
256 }
257
258 #[test]
259 fn test_legacy_adapter_captures_stderr() {
260 let (_dir, scripts_dir) = create_mock_script_dir();
261 let adapter = LegacyAdapter::new(&scripts_dir);
262
263 let result = adapter.execute("deepspeed", "0.14.0").unwrap();
265 assert!(!result.stderr.is_empty());
266 assert!(result.stderr.contains("Error"));
267 }
268
269 #[test]
274 fn test_legacy_adapter_exit_code_success() {
275 let (_dir, scripts_dir) = create_mock_script_dir();
276 let adapter = LegacyAdapter::new(&scripts_dir);
277
278 let result = adapter.execute("pytorch", "2.5.0").unwrap();
279 assert_eq!(result.exit_code, 0);
280 assert!(result.is_success());
281 }
282
283 #[test]
284 fn test_legacy_adapter_exit_code_failure() {
285 let (_dir, scripts_dir) = create_mock_script_dir();
286 let adapter = LegacyAdapter::new(&scripts_dir);
287
288 let result = adapter.execute("deepspeed", "0.14.0").unwrap();
289 assert_eq!(result.exit_code, 1);
290 assert!(!result.is_success());
291 }
292
293 #[test]
294 fn test_legacy_adapter_exit_code_partial() {
295 let (_dir, scripts_dir) = create_mock_script_dir();
296 let adapter = LegacyAdapter::new(&scripts_dir);
297
298 let result = adapter.execute("vllm", "0.6.0").unwrap();
299 assert_eq!(result.exit_code, 2);
300 assert!(!result.is_success());
301 assert!(!result.stdout.is_empty());
302 assert!(!result.stderr.is_empty());
303 }
304
305 #[test]
310 fn test_resolve_script_path_known_component() {
311 let (_dir, scripts_dir) = create_mock_script_dir();
312 let adapter = LegacyAdapter::new(&scripts_dir);
313
314 let path = adapter.resolve_script_path("pytorch");
315 assert!(path.is_some());
316 let path = path.unwrap();
317 assert!(path.to_string_lossy().contains("install_pytorch_rocm.sh"));
318 }
319
320 #[test]
321 fn test_resolve_script_path_unknown_component() {
322 let (_dir, scripts_dir) = create_mock_script_dir();
323 let adapter = LegacyAdapter::new(&scripts_dir);
324
325 let path = adapter.resolve_script_path("nonexistent");
326 assert!(path.is_none());
327 }
328
329 #[test]
330 fn test_resolve_script_path_no_installer() {
331 let (_dir, scripts_dir) = create_mock_script_dir();
332 let adapter = LegacyAdapter::new(&scripts_dir);
333
334 let path = adapter.resolve_script_path("rocm-smi");
336 assert!(path.is_none());
337 }
338
339 #[test]
344 fn test_legacy_adapter_script_not_found() {
345 let dir = tempfile::tempdir().unwrap();
346 let adapter = LegacyAdapter::new(dir.path());
347
348 let result = adapter.execute("pytorch", "2.5.0");
350 assert!(result.is_err());
351 let err = result.unwrap_err();
352 assert!(matches!(err, AdapterError::InvocationError { .. }));
353 assert!(err.to_string().contains("script not found"));
354 }
355
356 #[test]
357 fn test_legacy_adapter_unknown_component() {
358 let dir = tempfile::tempdir().unwrap();
359 let adapter = LegacyAdapter::new(dir.path());
360
361 let result = adapter.execute("nonexistent", "1.0.0");
362 assert!(matches!(result, Err(AdapterError::AdapterNotFound { .. })));
363 }
364
365 #[test]
370 fn test_build_command_includes_version() {
371 let (_dir, scripts_dir) = create_mock_script_dir();
372 let adapter = LegacyAdapter::new(&scripts_dir);
373
374 let cmd = adapter.build_command("pytorch", "2.5.0").unwrap();
375 let formatted = format!("{cmd:?}");
376 assert!(formatted.contains("2.5.0"));
377 assert!(formatted.contains("install_pytorch_rocm.sh"));
378 }
379
380 #[test]
381 fn test_build_command_empty_version() {
382 let (_dir, scripts_dir) = create_mock_script_dir();
383 let adapter = LegacyAdapter::new(&scripts_dir);
384
385 let cmd = adapter.build_command("pytorch", "").unwrap();
387 let formatted = format!("{cmd:?}");
388 assert!(formatted.contains("install_pytorch_rocm.sh"));
389 }
390
391 #[test]
396 fn test_legacy_adapter_executor_kind() {
397 let dir = tempfile::tempdir().unwrap();
398 let adapter = LegacyAdapter::new(dir.path());
399 assert_eq!(adapter.executor_kind(), ExecutorKind::LegacyScript);
400 }
401
402 #[test]
407 fn test_legacy_adapter_tracks_duration() {
408 let (_dir, scripts_dir) = create_mock_script_dir();
409 let adapter = LegacyAdapter::new(&scripts_dir);
410
411 let result = adapter.execute("pytorch", "2.5.0").unwrap();
412 let _ = result.duration_ms;
414 }
415}