Skip to main content

runmat_runtime/builtins/io/repl_fs/
savepath.rs

1//! MATLAB-compatible `savepath` builtin for persisting the session search path.
2
3use 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
97/// Evaluate `savepath` and expose all MATLAB-style outputs.
98pub 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        // Restore permissions so tempdir cleanup succeeds.
545        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}