1use runmat_builtins::{CharArray, StringArray, Tensor, Value};
4use runmat_macros::runtime_builtin;
5
6use crate::builtins::common::fs::{expand_user_path, home_directory};
7use crate::builtins::common::path_state::{current_path_string, PATH_LIST_SEPARATOR};
8use crate::builtins::common::spec::{
9 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
10 ReductionNaN, ResidencyPolicy, ShapeRequirements,
11};
12use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
13
14use runmat_filesystem as vfs;
15use std::env;
16use std::io;
17use std::path::{Path, PathBuf};
18
19const DEFAULT_FILENAME: &str = "pathdef.m";
20const ERROR_ARG_TYPE: &str = "savepath: filename must be a character vector or string scalar";
21const ERROR_EMPTY_FILENAME: &str = "savepath: filename must not be empty";
22const MESSAGE_ID_CANNOT_WRITE: &str = "RunMat:savepath:cannotWriteFile";
23const MESSAGE_ID_CANNOT_RESOLVE: &str = "RunMat:savepath:cannotResolveFile";
24
25#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::savepath")]
26pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
27 name: "savepath",
28 op_kind: GpuOpKind::Custom("io"),
29 supported_precisions: &[],
30 broadcast: BroadcastSemantics::None,
31 provider_hooks: &[],
32 constant_strategy: ConstantStrategy::InlineLiteral,
33 residency: ResidencyPolicy::GatherImmediately,
34 nan_mode: ReductionNaN::Include,
35 two_pass_threshold: None,
36 workgroup_size: None,
37 accepts_nan_mode: false,
38 notes:
39 "Filesystem persistence executes on the host; GPU-resident filenames are gathered before writing pathdef.m.",
40};
41
42#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::savepath")]
43pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
44 name: "savepath",
45 shape: ShapeRequirements::Any,
46 constant_strategy: ConstantStrategy::InlineLiteral,
47 elementwise: None,
48 reduction: None,
49 emits_nan: false,
50 notes:
51 "Filesystem side-effects are not eligible for fusion; metadata registered for completeness.",
52};
53
54const BUILTIN_NAME: &str = "savepath";
55
56fn savepath_error(message: impl Into<String>) -> RuntimeError {
57 build_runtime_error(message)
58 .with_builtin(BUILTIN_NAME)
59 .build()
60}
61
62fn map_control_flow(err: RuntimeError) -> RuntimeError {
63 let identifier = err.identifier().map(str::to_string);
64 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
65 .with_builtin(BUILTIN_NAME)
66 .with_source(err);
67 if let Some(identifier) = identifier {
68 builder = builder.with_identifier(identifier);
69 }
70 builder.build()
71}
72
73#[runtime_builtin(
74 name = "savepath",
75 category = "io/repl_fs",
76 summary = "Persist the current MATLAB search path to pathdef.m with status outputs.",
77 keywords = "savepath,pathdef,search path,runmat path,persist path",
78 accel = "cpu",
79 suppress_auto_output = true,
80 type_resolver(crate::builtins::io::type_resolvers::savepath_type),
81 builtin_path = "crate::builtins::io::repl_fs::savepath"
82)]
83async fn savepath_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
84 let eval = evaluate(&args).await?;
85 if let Some(out_count) = crate::output_count::current_output_count() {
86 if out_count == 0 {
87 return Ok(Value::OutputList(Vec::new()));
88 }
89 return Ok(crate::output_count::output_list_with_padding(
90 out_count,
91 eval.outputs(),
92 ));
93 }
94 Ok(eval.first_output())
95}
96
97pub async fn evaluate(args: &[Value]) -> BuiltinResult<SavepathResult> {
99 let gathered = gather_arguments(args).await?;
100 let target = match gathered.len() {
101 0 => match default_target_path().await {
102 Ok(path) => path,
103 Err(err) => return Ok(SavepathResult::failure(err.message, err.message_id)),
104 },
105 1 => {
106 let raw = extract_filename(&gathered[0])?;
107 if raw.is_empty() {
108 return Err(savepath_error(ERROR_EMPTY_FILENAME));
109 }
110 match resolve_explicit_path(&raw).await {
111 Ok(path) => path,
112 Err(err) => return Ok(SavepathResult::failure(err.message, err.message_id)),
113 }
114 }
115 _ => return Err(savepath_error("savepath: too many input arguments")),
116 };
117
118 let path_string = current_path_string();
119 match persist_path(&target, &path_string).await {
120 Ok(()) => Ok(SavepathResult::success()),
121 Err(err) => Ok(SavepathResult::failure(err.message, err.message_id)),
122 }
123}
124
125#[derive(Debug, Clone)]
126pub struct SavepathResult {
127 status: f64,
128 message: String,
129 message_id: String,
130}
131
132impl SavepathResult {
133 fn success() -> Self {
134 Self {
135 status: 0.0,
136 message: String::new(),
137 message_id: String::new(),
138 }
139 }
140
141 fn failure(message: String, message_id: &'static str) -> Self {
142 Self {
143 status: 1.0,
144 message,
145 message_id: message_id.to_string(),
146 }
147 }
148
149 pub fn first_output(&self) -> Value {
150 Value::Num(self.status)
151 }
152
153 pub fn outputs(&self) -> Vec<Value> {
154 vec![
155 Value::Num(self.status),
156 char_array_value(&self.message),
157 char_array_value(&self.message_id),
158 ]
159 }
160
161 #[cfg(test)]
162 pub(crate) fn status(&self) -> f64 {
163 self.status
164 }
165
166 #[cfg(test)]
167 pub(crate) fn message(&self) -> &str {
168 &self.message
169 }
170
171 #[cfg(test)]
172 pub(crate) fn message_id(&self) -> &str {
173 &self.message_id
174 }
175}
176
177struct SavepathFailure {
178 message: String,
179 message_id: &'static str,
180}
181
182impl SavepathFailure {
183 fn new(message: String, message_id: &'static str) -> Self {
184 Self {
185 message,
186 message_id,
187 }
188 }
189
190 fn cannot_write(path: &Path, error: io::Error) -> Self {
191 Self::new(
192 format!(
193 "savepath: unable to write \"{}\": {}",
194 path.display(),
195 error
196 ),
197 MESSAGE_ID_CANNOT_WRITE,
198 )
199 }
200}
201
202async fn persist_path(target: &Path, path_string: &str) -> Result<(), SavepathFailure> {
203 if let Some(parent) = target.parent() {
204 if let Err(err) = vfs::create_dir_all_async(parent).await {
205 return Err(SavepathFailure::cannot_write(target, err));
206 }
207 }
208
209 let contents = build_pathdef_contents(path_string);
210 vfs::write_async(target, contents.as_bytes())
211 .await
212 .map_err(|err| SavepathFailure::cannot_write(target, err))
213}
214
215async fn default_target_path() -> Result<PathBuf, SavepathFailure> {
216 if let Ok(override_path) = env::var("RUNMAT_PATHDEF") {
217 if override_path.trim().is_empty() {
218 return Err(SavepathFailure::new(
219 "savepath: RUNMAT_PATHDEF is empty".to_string(),
220 MESSAGE_ID_CANNOT_RESOLVE,
221 ));
222 }
223 return resolve_explicit_path(&override_path).await;
224 }
225
226 let home = home_directory().ok_or_else(|| {
227 SavepathFailure::new(
228 "savepath: unable to determine default pathdef location".to_string(),
229 MESSAGE_ID_CANNOT_RESOLVE,
230 )
231 })?;
232 Ok(home.join(".runmat").join(DEFAULT_FILENAME))
233}
234
235async fn resolve_explicit_path(text: &str) -> Result<PathBuf, SavepathFailure> {
236 let expanded = match expand_user_path(text, "savepath") {
237 Ok(path) => path,
238 Err(err) => return Err(SavepathFailure::new(err, MESSAGE_ID_CANNOT_RESOLVE)),
239 };
240 let mut path = PathBuf::from(&expanded);
241 if path_should_be_directory(&path, text).await {
242 path.push(DEFAULT_FILENAME);
243 }
244 Ok(path)
245}
246
247async fn path_should_be_directory(path: &Path, original: &str) -> bool {
248 if original.ends_with(std::path::MAIN_SEPARATOR) || original.ends_with('/') {
249 return true;
250 }
251 if cfg!(windows) && original.ends_with('\\') {
252 return true;
253 }
254 match vfs::metadata_async(path).await {
255 Ok(metadata) => metadata.is_dir(),
256 Err(_) => false,
257 }
258}
259
260fn build_pathdef_contents(path_string: &str) -> String {
261 let mut contents = String::new();
262 contents.push_str("function p = pathdef\n");
263 contents.push_str("%PATHDEF Search path defaults generated by RunMat savepath.\n");
264 contents.push_str(
265 "% This file reproduces the MATLAB search path at the time savepath was called.\n",
266 );
267 if !path_string.is_empty() {
268 contents.push_str("%\n");
269 contents.push_str("% Directories on the saved path (in order):\n");
270 for entry in path_string.split(PATH_LIST_SEPARATOR) {
271 contents.push_str("% ");
272 contents.push_str(entry);
273 contents.push('\n');
274 }
275 }
276 contents.push('\n');
277 let escaped = path_string.replace('\'', "''");
278 contents.push_str("p = '");
279 contents.push_str(&escaped);
280 contents.push_str("';\n");
281 contents.push_str("end\n");
282 contents
283}
284
285fn extract_filename(value: &Value) -> BuiltinResult<String> {
286 match value {
287 Value::String(text) => Ok(text.clone()),
288 Value::StringArray(StringArray { data, .. }) => {
289 if data.len() != 1 {
290 Err(savepath_error(ERROR_ARG_TYPE))
291 } else {
292 Ok(data[0].clone())
293 }
294 }
295 Value::CharArray(chars) => {
296 if chars.rows != 1 {
297 return Err(savepath_error(ERROR_ARG_TYPE));
298 }
299 Ok(chars.data.iter().collect())
300 }
301 Value::Tensor(tensor) => tensor_to_string(tensor),
302 Value::GpuTensor(_) => Err(savepath_error(ERROR_ARG_TYPE)),
303 _ => Err(savepath_error(ERROR_ARG_TYPE)),
304 }
305}
306
307fn tensor_to_string(tensor: &Tensor) -> BuiltinResult<String> {
308 if tensor.shape.len() > 2 {
309 return Err(savepath_error(ERROR_ARG_TYPE));
310 }
311 if tensor.rows() > 1 {
312 return Err(savepath_error(ERROR_ARG_TYPE));
313 }
314
315 let mut text = String::with_capacity(tensor.data.len());
316 for &code in &tensor.data {
317 if !code.is_finite() {
318 return Err(savepath_error(ERROR_ARG_TYPE));
319 }
320 let rounded = code.round();
321 if (code - rounded).abs() > 1e-6 {
322 return Err(savepath_error(ERROR_ARG_TYPE));
323 }
324 let int_code = rounded as i64;
325 if !(0..=0x10FFFF).contains(&int_code) {
326 return Err(savepath_error(ERROR_ARG_TYPE));
327 }
328 let ch = char::from_u32(int_code as u32).ok_or_else(|| savepath_error(ERROR_ARG_TYPE))?;
329 text.push(ch);
330 }
331 Ok(text)
332}
333
334async fn gather_arguments(args: &[Value]) -> BuiltinResult<Vec<Value>> {
335 let mut gathered = Vec::with_capacity(args.len());
336 for value in args {
337 gathered.push(
338 gather_if_needed_async(value)
339 .await
340 .map_err(map_control_flow)?,
341 );
342 }
343 Ok(gathered)
344}
345
346fn char_array_value(text: &str) -> Value {
347 Value::CharArray(CharArray::new_row(text))
348}
349
350#[cfg(test)]
351pub(crate) mod tests {
352 use super::super::REPL_FS_TEST_LOCK;
353 use super::*;
354 use crate::builtins::common::path_state::{current_path_string, set_path_string};
355 use crate::builtins::common::test_support;
356 #[cfg(feature = "wgpu")]
357 use runmat_accelerate_api::AccelProvider;
358 use runmat_accelerate_api::HostTensorView;
359 use std::fs;
360 use tempfile::tempdir;
361
362 fn savepath_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
363 futures::executor::block_on(super::savepath_builtin(args))
364 }
365
366 fn evaluate(args: &[Value]) -> BuiltinResult<SavepathResult> {
367 futures::executor::block_on(super::evaluate(args))
368 }
369
370 struct PathGuard {
371 previous: String,
372 }
373
374 impl PathGuard {
375 fn new() -> Self {
376 Self {
377 previous: current_path_string(),
378 }
379 }
380 }
381
382 impl Drop for PathGuard {
383 fn drop(&mut self) {
384 set_path_string(&self.previous);
385 }
386 }
387
388 struct PathdefEnvGuard {
389 previous: Option<String>,
390 }
391
392 impl PathdefEnvGuard {
393 fn set(path: &Path) -> Self {
394 let previous = env::var("RUNMAT_PATHDEF").ok();
395 env::set_var("RUNMAT_PATHDEF", path.to_string_lossy().to_string());
396 Self { previous }
397 }
398
399 fn set_raw(value: &str) -> Self {
400 let previous = env::var("RUNMAT_PATHDEF").ok();
401 env::set_var("RUNMAT_PATHDEF", value);
402 Self { previous }
403 }
404 }
405
406 impl Drop for PathdefEnvGuard {
407 fn drop(&mut self) {
408 if let Some(ref value) = self.previous {
409 env::set_var("RUNMAT_PATHDEF", value);
410 } else {
411 env::remove_var("RUNMAT_PATHDEF");
412 }
413 }
414 }
415
416 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
417 #[test]
418 fn savepath_writes_to_default_location_with_env_override() {
419 let _lock = REPL_FS_TEST_LOCK
420 .lock()
421 .unwrap_or_else(|poison| poison.into_inner());
422 let _guard = PathGuard::new();
423
424 let temp = tempdir().expect("tempdir");
425 let target = temp.path().join("pathdef_default.m");
426 let _env_guard = PathdefEnvGuard::set(&target);
427
428 let path_a = temp.path().join("toolbox");
429 let path_b = temp.path().join("utils");
430 let path_string = format!(
431 "{}{}{}",
432 path_a.to_string_lossy(),
433 PATH_LIST_SEPARATOR,
434 path_b.to_string_lossy()
435 );
436 set_path_string(&path_string);
437
438 let eval = evaluate(&[]).expect("evaluate");
439 assert_eq!(eval.status(), 0.0);
440 assert!(eval.message().is_empty());
441 assert!(eval.message_id().is_empty());
442
443 let contents = fs::read_to_string(&target).expect("pathdef contents");
444 assert!(contents.contains("function p = pathdef"));
445 assert!(contents.contains(path_a.to_string_lossy().as_ref()));
446 assert!(contents.contains(path_b.to_string_lossy().as_ref()));
447 assert_eq!(current_path_string(), path_string);
448 }
449
450 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
451 #[test]
452 fn savepath_env_override_empty_returns_failure() {
453 let _lock = REPL_FS_TEST_LOCK
454 .lock()
455 .unwrap_or_else(|poison| poison.into_inner());
456 let _guard = PathGuard::new();
457
458 let _env_guard = PathdefEnvGuard::set_raw("");
459 set_path_string("");
460
461 let eval = evaluate(&[]).expect("evaluate");
462 assert_eq!(eval.status(), 1.0);
463 assert!(eval.message().contains("RUNMAT_PATHDEF is empty"));
464 assert_eq!(eval.message_id(), MESSAGE_ID_CANNOT_RESOLVE);
465 }
466
467 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
468 #[test]
469 fn savepath_accepts_explicit_filename_argument() {
470 let _lock = REPL_FS_TEST_LOCK
471 .lock()
472 .unwrap_or_else(|poison| poison.into_inner());
473 let _guard = PathGuard::new();
474
475 let temp = tempdir().expect("tempdir");
476 let target = temp.path().join("custom_pathdef.m");
477 set_path_string("");
478
479 let eval =
480 evaluate(&[Value::from(target.to_string_lossy().to_string())]).expect("evaluate");
481 assert_eq!(eval.status(), 0.0);
482 assert!(target.exists());
483 }
484
485 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
486 #[test]
487 fn savepath_appends_default_filename_for_directories() {
488 let _lock = REPL_FS_TEST_LOCK
489 .lock()
490 .unwrap_or_else(|poison| poison.into_inner());
491 let _guard = PathGuard::new();
492
493 let temp = tempdir().expect("tempdir");
494 let dir = temp.path().join("profile");
495 fs::create_dir_all(&dir).expect("create dir");
496 let expected = dir.join(DEFAULT_FILENAME);
497
498 let eval = evaluate(&[Value::from(dir.to_string_lossy().to_string())]).expect("evaluate");
499 assert_eq!(eval.status(), 0.0);
500 assert!(expected.exists());
501 }
502
503 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
504 #[test]
505 fn savepath_appends_default_filename_for_trailing_separator() {
506 let _lock = REPL_FS_TEST_LOCK
507 .lock()
508 .unwrap_or_else(|poison| poison.into_inner());
509 let _guard = PathGuard::new();
510
511 let temp = tempdir().expect("tempdir");
512 let dir = temp.path().join("profile_trailing");
513 let mut raw = dir.to_string_lossy().to_string();
514 raw.push(std::path::MAIN_SEPARATOR);
515
516 set_path_string("");
517 let eval = evaluate(&[Value::from(raw)]).expect("evaluate");
518 assert_eq!(eval.status(), 0.0);
519 assert!(dir.join(DEFAULT_FILENAME).exists());
520 }
521
522 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
523 #[test]
524 fn savepath_returns_failure_when_write_fails() {
525 let _lock = REPL_FS_TEST_LOCK
526 .lock()
527 .unwrap_or_else(|poison| poison.into_inner());
528 let _guard = PathGuard::new();
529
530 let temp = tempdir().expect("tempdir");
531 let target = temp.path().join("readonly_pathdef.m");
532 fs::write(&target, "locked").expect("write");
533 let mut perms = fs::metadata(&target).expect("metadata").permissions();
534 let original_perms = perms.clone();
535 perms.set_readonly(true);
536 fs::set_permissions(&target, perms).expect("set readonly");
537
538 let eval =
539 evaluate(&[Value::from(target.to_string_lossy().to_string())]).expect("evaluate");
540 assert_eq!(eval.status(), 1.0);
541 assert!(eval.message().contains("unable to write"));
542 assert_eq!(eval.message_id(), MESSAGE_ID_CANNOT_WRITE);
543
544 let _ = fs::set_permissions(&target, original_perms);
546 }
547
548 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
549 #[test]
550 fn savepath_outputs_vector_contains_message_and_id() {
551 let _lock = REPL_FS_TEST_LOCK
552 .lock()
553 .unwrap_or_else(|poison| poison.into_inner());
554 let _guard = PathGuard::new();
555
556 let temp = tempdir().expect("tempdir");
557 let target = temp.path().join("outputs_pathdef.m");
558 let eval =
559 evaluate(&[Value::from(target.to_string_lossy().to_string())]).expect("evaluate");
560 let outputs = eval.outputs();
561 assert_eq!(outputs.len(), 3);
562 assert!(matches!(outputs[0], Value::Num(0.0)));
563 assert!(matches!(outputs[1], Value::CharArray(ref ca) if ca.cols == 0));
564 assert!(matches!(outputs[2], Value::CharArray(ref ca) if ca.cols == 0));
565 }
566
567 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
568 #[test]
569 fn savepath_rejects_empty_filename() {
570 let _lock = REPL_FS_TEST_LOCK
571 .lock()
572 .unwrap_or_else(|poison| poison.into_inner());
573 let _guard = PathGuard::new();
574
575 let err = evaluate(&[Value::from(String::new())]).expect_err("expected error");
576 assert_eq!(err.message(), ERROR_EMPTY_FILENAME);
577 }
578
579 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
580 #[test]
581 fn savepath_rejects_non_string_input() {
582 let err = savepath_builtin(vec![Value::Num(1.0)]).expect_err("expected error");
583 assert!(err.message().contains("savepath"));
584 }
585
586 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
587 #[test]
588 fn savepath_accepts_string_array_scalar_argument() {
589 let _lock = REPL_FS_TEST_LOCK
590 .lock()
591 .unwrap_or_else(|poison| poison.into_inner());
592 let _guard = PathGuard::new();
593
594 let temp = tempdir().expect("tempdir");
595 let target = temp.path().join("string_array_pathdef.m");
596 let array = StringArray::new(vec![target.to_string_lossy().to_string()], vec![1])
597 .expect("string array");
598
599 set_path_string("");
600 let eval = evaluate(&[Value::StringArray(array)]).expect("evaluate");
601 assert_eq!(eval.status(), 0.0);
602 assert!(target.exists());
603 }
604
605 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
606 #[test]
607 fn savepath_rejects_multi_element_string_array() {
608 let array = StringArray::new(vec!["a".to_string(), "b".to_string()], vec![1, 2])
609 .expect("string array");
610 let err = extract_filename(&Value::StringArray(array)).expect_err("expected error");
611 assert_eq!(err.message(), ERROR_ARG_TYPE);
612 }
613
614 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
615 #[test]
616 fn savepath_rejects_multi_row_char_array() {
617 let chars = CharArray::new("abcd".chars().collect(), 2, 2).expect("char array");
618 let err = extract_filename(&Value::CharArray(chars)).expect_err("expected error");
619 assert_eq!(err.message(), ERROR_ARG_TYPE);
620 }
621
622 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
623 #[test]
624 fn savepath_rejects_tensor_with_fractional_codes() {
625 let tensor = Tensor::new(vec![65.5], vec![1, 1]).expect("tensor");
626 let err = extract_filename(&Value::Tensor(tensor)).expect_err("expected error");
627 assert_eq!(err.message(), ERROR_ARG_TYPE);
628 }
629
630 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
631 #[test]
632 fn savepath_supports_gpu_tensor_filename() {
633 let _lock = REPL_FS_TEST_LOCK
634 .lock()
635 .unwrap_or_else(|poison| poison.into_inner());
636 let _guard = PathGuard::new();
637
638 let temp = tempdir().expect("tempdir");
639 let target = temp.path().join("gpu_tensor_pathdef.m");
640 set_path_string("");
641
642 test_support::with_test_provider(|provider| {
643 let text = target.to_string_lossy().to_string();
644 let ascii: Vec<f64> = text.chars().map(|ch| ch as u32 as f64).collect();
645 let tensor = Tensor::new(ascii.clone(), vec![1, ascii.len()]).expect("tensor");
646 let view = HostTensorView {
647 data: &tensor.data,
648 shape: &tensor.shape,
649 };
650 let handle = provider.upload(&view).expect("upload");
651
652 let eval = evaluate(&[Value::GpuTensor(handle.clone())]).expect("evaluate");
653 assert_eq!(eval.status(), 0.0);
654
655 provider.free(&handle).expect("free");
656 });
657
658 assert!(target.exists());
659 }
660
661 #[cfg(feature = "wgpu")]
662 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
663 #[test]
664 fn savepath_supports_gpu_tensor_filename_with_wgpu_provider() {
665 let _lock = REPL_FS_TEST_LOCK
666 .lock()
667 .unwrap_or_else(|poison| poison.into_inner());
668 let _guard = PathGuard::new();
669
670 let temp = tempdir().expect("tempdir");
671 let target = temp.path().join("wgpu_tensor_pathdef.m");
672 set_path_string("");
673
674 let provider = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
675 runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
676 )
677 .expect("wgpu provider");
678
679 let text = target.to_string_lossy().to_string();
680 let ascii: Vec<f64> = text.chars().map(|ch| ch as u32 as f64).collect();
681 let tensor = Tensor::new(ascii.clone(), vec![1, ascii.len()]).expect("tensor");
682 let view = HostTensorView {
683 data: &tensor.data,
684 shape: &tensor.shape,
685 };
686 let handle = provider.upload(&view).expect("upload");
687
688 let eval = evaluate(&[Value::GpuTensor(handle.clone())]).expect("evaluate");
689 assert_eq!(eval.status(), 0.0);
690 assert!(target.exists());
691
692 provider.free(&handle).expect("free");
693 }
694}