1use super::{Adapter, AdapterError, AdapterOutput};
20use crate::core::types::ExecutorKind;
21use std::time::Instant;
22
23pub type RustAdapterFn =
33 Box<dyn Fn(&str, &str) -> Result<(String, String), (i32, String, String)> + Send + Sync>;
34
35pub struct RustAdapter {
59 executor: RustAdapterFn,
61 #[allow(dead_code)]
63 component_id: String,
64}
65
66impl RustAdapter {
67 pub fn new(executor: RustAdapterFn) -> Self {
69 Self {
70 executor,
71 component_id: String::new(),
72 }
73 }
74
75 pub fn with_id(component_id: impl Into<String>, executor: RustAdapterFn) -> Self {
77 Self {
78 executor,
79 component_id: component_id.into(),
80 }
81 }
82
83 pub fn simple_success(message_prefix: &str) -> Self {
85 let prefix = message_prefix.to_string();
86 Self::new(Box::new(move |id, version| {
87 Ok((format!("{prefix}: {id} {version} installed"), String::new()))
88 }))
89 }
90
91 pub fn always_fails(exit_code: i32, message: &str) -> Self {
93 let msg = message.to_string();
94 Self::new(Box::new(move |_id, _version| {
95 Err((exit_code, String::new(), msg.clone()))
96 }))
97 }
98}
99
100impl Adapter for RustAdapter {
101 fn execute(&self, component_id: &str, version: &str) -> Result<AdapterOutput, AdapterError> {
102 let start = Instant::now();
103
104 match (self.executor)(component_id, version) {
105 Ok((stdout, stderr)) => Ok(AdapterOutput {
106 component_id: component_id.to_string(),
107 executor_kind: ExecutorKind::Rust,
108 stdout,
109 stderr,
110 exit_code: 0,
111 duration_ms: start.elapsed().as_millis() as u64,
112 }),
113 Err((exit_code, stdout, stderr)) => Ok(AdapterOutput {
114 component_id: component_id.to_string(),
115 executor_kind: ExecutorKind::Rust,
116 stdout,
117 stderr: stderr.clone(),
118 exit_code,
119 duration_ms: start.elapsed().as_millis() as u64,
120 }),
121 }
122 }
123
124 fn executor_kind(&self) -> ExecutorKind {
125 ExecutorKind::Rust
126 }
127}
128
129pub struct ShellOutputFormat;
138
139impl ShellOutputFormat {
140 pub fn success(component_id: &str, version: &str) -> String {
144 let display = crate::platform::registry::display_name(component_id);
145 format!("{display} {version} installed successfully")
146 }
147
148 pub fn failure(component_id: &str, reason: &str) -> String {
152 let display = crate::platform::registry::display_name(component_id);
153 format!("Error: {display} installation failed: {reason}")
154 }
155
156 pub fn partial(component_id: &str, details: &str) -> String {
160 let display = crate::platform::registry::display_name(component_id);
161 format!("Warning: {display} partially installed: {details}")
162 }
163
164 pub fn status(component_id: &str, status: &str) -> String {
168 let display = crate::platform::registry::display_name(component_id);
169 format!("{display} {status}")
170 }
171}
172
173#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
186 fn test_rust_adapter_success() {
187 let adapter = RustAdapter::new(Box::new(|id, version| {
188 Ok((format!("{id}:{version}:ok"), String::new()))
189 }));
190
191 let result = adapter.execute("pytorch", "2.5.0").unwrap();
192 assert_eq!(result.exit_code, 0);
193 assert_eq!(result.stdout, "pytorch:2.5.0:ok");
194 assert!(result.stderr.is_empty());
195 assert_eq!(result.executor_kind, ExecutorKind::Rust);
196 }
197
198 #[test]
199 fn test_rust_adapter_failure() {
200 let adapter = RustAdapter::new(Box::new(|_id, _version| {
201 Err((1, String::new(), "installation error".to_string()))
202 }));
203
204 let result = adapter.execute("pytorch", "2.5.0").unwrap();
205 assert_eq!(result.exit_code, 1);
206 assert_eq!(result.stderr, "installation error");
207 assert!(!result.is_success());
208 }
209
210 #[test]
211 fn test_rust_adapter_partial_failure() {
212 let adapter = RustAdapter::new(Box::new(|_id, _version| {
213 Err((2, "partial output".to_string(), "some warnings".to_string()))
214 }));
215
216 let result = adapter.execute("triton", "3.1.0").unwrap();
217 assert_eq!(result.exit_code, 2);
218 assert_eq!(result.stdout, "partial output");
219 assert_eq!(result.stderr, "some warnings");
220 }
221
222 #[test]
223 fn test_rust_adapter_no_shell_subprocess() {
224 let adapter = RustAdapter::new(Box::new(|id, version| {
227 let result = format!("computed: {id}@{version}");
229 Ok((result, String::new()))
230 }));
231
232 let result = adapter.execute("test", "1.0.0").unwrap();
233 assert_eq!(result.stdout, "computed: test@1.0.0");
234 assert_eq!(result.executor_kind, ExecutorKind::Rust);
235 }
236
237 #[test]
242 fn test_rust_adapter_simple_success() {
243 let adapter = RustAdapter::simple_success("mock");
244 let result = adapter.execute("pytorch", "2.5.0").unwrap();
245 assert_eq!(result.exit_code, 0);
246 assert!(result.stdout.contains("pytorch"));
247 assert!(result.stdout.contains("2.5.0"));
248 }
249
250 #[test]
251 fn test_rust_adapter_always_fails() {
252 let adapter = RustAdapter::always_fails(42, "always fails");
253 let result = adapter.execute("test", "1.0.0").unwrap();
254 assert_eq!(result.exit_code, 42);
255 assert_eq!(result.stderr, "always fails");
256 }
257
258 #[test]
259 fn test_rust_adapter_with_id() {
260 let adapter = RustAdapter::with_id(
261 "pytorch",
262 Box::new(|id, v| Ok((format!("{id}:{v}"), String::new()))),
263 );
264 let result = adapter.execute("pytorch", "2.5.0").unwrap();
265 assert!(result.stdout.contains("pytorch"));
266 }
267
268 #[test]
273 fn test_shell_output_format_success() {
274 let output = ShellOutputFormat::success("pytorch", "2.5.0");
275 assert!(output.contains("PyTorch"));
276 assert!(output.contains("2.5.0"));
277 assert!(output.contains("installed successfully"));
278 }
279
280 #[test]
281 fn test_shell_output_format_failure() {
282 let output = ShellOutputFormat::failure("pytorch", "build error");
283 assert!(output.contains("PyTorch"));
284 assert!(output.contains("Error:"));
285 assert!(output.contains("build error"));
286 }
287
288 #[test]
289 fn test_shell_output_format_partial() {
290 let output = ShellOutputFormat::partial("triton", "missing optional deps");
291 assert!(output.contains("Triton"));
292 assert!(output.contains("Warning:"));
293 assert!(output.contains("missing optional deps"));
294 }
295
296 #[test]
297 fn test_shell_output_format_status() {
298 let output = ShellOutputFormat::status("rocm", "detected");
299 assert!(output.contains("ROCm"));
300 assert!(output.contains("detected"));
301 }
302
303 #[test]
308 fn test_rust_adapter_tracks_duration() {
309 let adapter = RustAdapter::new(Box::new(|_id, _version| {
310 std::thread::sleep(std::time::Duration::from_millis(1));
312 Ok(("done".to_string(), String::new()))
313 }));
314
315 let result = adapter.execute("test", "1.0.0").unwrap();
316 assert!(result.duration_ms > 0, "Duration should be non-zero");
317 }
318
319 #[test]
324 fn test_rust_adapter_executor_kind() {
325 let adapter = RustAdapter::simple_success("test");
326 assert_eq!(adapter.executor_kind(), ExecutorKind::Rust);
327 }
328}