Skip to main content

runmat_runtime/builtins/io/repl_fs/
delete.rs

1//! MATLAB-compatible `delete` builtin for RunMat.
2
3use runmat_filesystem as vfs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use glob::{Pattern, PatternError};
8use runmat_builtins::{CellArray, CharArray, StringArray, Value};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::common::fs::{contains_wildcards, expand_user_path, path_to_string};
12use crate::builtins::common::spec::{
13    BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
14    ReductionNaN, ResidencyPolicy, ShapeRequirements,
15};
16use crate::{build_runtime_error, gather_if_needed_async, BuiltinResult, RuntimeError};
17
18const MESSAGE_ID_FILE_NOT_FOUND: &str = "RunMat:DELETE:FileNotFound";
19const MESSAGE_ID_IS_DIRECTORY: &str = "RunMat:delete:Directories";
20const MESSAGE_ID_OS_ERROR: &str = "RunMat:DELETE:PermissionDenied";
21const MESSAGE_ID_INVALID_PATTERN: &str = "RunMat:delete:InvalidPattern";
22const MESSAGE_ID_INVALID_INPUT: &str = "RunMat:delete:InvalidInput";
23const MESSAGE_ID_EMPTY_FILENAME: &str = "RunMat:delete:EmptyFilename";
24const MESSAGE_ID_INVALID_HANDLE: &str = "RunMat:delete:InvalidHandle";
25
26const ERR_FILENAME_ARG: &str =
27    "delete: filename must be a character vector, string scalar, string array, or cell array of character vectors";
28
29#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::delete")]
30pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
31    name: "delete",
32    op_kind: GpuOpKind::Custom("io"),
33    supported_precisions: &[],
34    broadcast: BroadcastSemantics::None,
35    provider_hooks: &[],
36    constant_strategy: ConstantStrategy::InlineLiteral,
37    residency: ResidencyPolicy::GatherImmediately,
38    nan_mode: ReductionNaN::Include,
39    two_pass_threshold: None,
40    workgroup_size: None,
41    accepts_nan_mode: false,
42    notes:
43        "Host-only filesystem operation. GPU-resident path values are gathered automatically before deletion.",
44};
45
46#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::delete")]
47pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
48    name: "delete",
49    shape: ShapeRequirements::Any,
50    constant_strategy: ConstantStrategy::InlineLiteral,
51    elementwise: None,
52    reduction: None,
53    emits_nan: false,
54    notes:
55        "Filesystem side-effects are executed immediately; metadata registered for completeness.",
56};
57
58const BUILTIN_NAME: &str = "delete";
59
60fn delete_error(message_id: &'static str, message: impl Into<String>) -> RuntimeError {
61    build_runtime_error(message)
62        .with_builtin(BUILTIN_NAME)
63        .with_identifier(message_id)
64        .build()
65}
66
67fn map_control_flow(err: RuntimeError) -> RuntimeError {
68    let identifier = err.identifier().map(str::to_string);
69    let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
70        .with_builtin(BUILTIN_NAME)
71        .with_source(err);
72    if let Some(identifier) = identifier {
73        builder = builder.with_identifier(identifier);
74    }
75    builder.build()
76}
77
78#[runtime_builtin(
79    name = "delete",
80    category = "io/repl_fs",
81    summary = "Remove files using MATLAB-compatible wildcard expansion, array inputs, and error diagnostics.",
82    keywords = "delete,remove file,wildcard delete,cleanup,temporary files,MATLAB delete",
83    accel = "cpu",
84    sink = true,
85    suppress_auto_output = true,
86    type_resolver(crate::builtins::io::type_resolvers::delete_type),
87    builtin_path = "crate::builtins::io::repl_fs::delete"
88)]
89async fn delete_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
90    if args.is_empty() {
91        return Err(delete_error(
92            MESSAGE_ID_INVALID_INPUT,
93            "delete: missing filename input",
94        ));
95    }
96    let gathered = gather_arguments(&args).await?;
97
98    if gathered.iter().all(is_handle_input) {
99        return delete_handles(&gathered);
100    }
101
102    if gathered.iter().any(contains_handle_input) {
103        return Err(delete_error(
104            MESSAGE_ID_INVALID_HANDLE,
105            "delete: cannot mix handle and filename inputs",
106        ));
107    }
108
109    let mut raw_targets = Vec::new();
110    for value in &gathered {
111        collect_targets(value, &mut raw_targets)?;
112    }
113
114    if raw_targets.is_empty() {
115        return Ok(Value::Num(0.0));
116    }
117
118    for raw in raw_targets {
119        delete_target(&raw).await?;
120    }
121
122    Ok(Value::Num(0.0))
123}
124
125async fn delete_target(raw: &str) -> BuiltinResult<()> {
126    let expanded = expand_user_path(raw, "delete")
127        .map_err(|msg| delete_error(MESSAGE_ID_INVALID_INPUT, msg))?;
128    if expanded.is_empty() {
129        return Err(delete_error(
130            MESSAGE_ID_EMPTY_FILENAME,
131            "delete: filename cannot be empty",
132        ));
133    }
134
135    if contains_wildcards(&expanded) {
136        delete_with_pattern(&expanded, raw).await
137    } else {
138        delete_single_path_async(&PathBuf::from(&expanded), raw).await
139    }
140}
141
142async fn delete_with_pattern(pattern: &str, display: &str) -> BuiltinResult<()> {
143    validate_wildcard_pattern(pattern, display)?;
144
145    if let Err(PatternError { msg, .. }) = Pattern::new(pattern) {
146        return Err(delete_error(
147            MESSAGE_ID_INVALID_PATTERN,
148            format!("delete: invalid wildcard pattern '{display}' ({msg})"),
149        ));
150    }
151
152    let paths = match glob::glob(pattern) {
153        Ok(iter) => iter,
154        Err(PatternError { msg, .. }) => {
155            return Err(delete_error(
156                MESSAGE_ID_INVALID_PATTERN,
157                format!("delete: invalid wildcard pattern '{display}' ({msg})"),
158            ))
159        }
160    };
161
162    let mut matches = Vec::new();
163    for entry in paths {
164        match entry {
165            Ok(path) => matches.push(path),
166            Err(err) => {
167                let problem_path = path_to_string(err.path());
168                return Err(delete_error(
169                    MESSAGE_ID_OS_ERROR,
170                    format!(
171                        "delete: unable to delete '{}' ({})",
172                        problem_path,
173                        err.error()
174                    ),
175                ));
176            }
177        }
178    }
179
180    if matches.is_empty() {
181        return Err(delete_error(
182            MESSAGE_ID_FILE_NOT_FOUND,
183            format!(
184                "delete: cannot delete '{}' because it does not exist",
185                display
186            ),
187        ));
188    }
189
190    for path in matches {
191        let display_path = path_to_string(&path);
192        delete_single_path_async(&path, &display_path).await?;
193    }
194    Ok(())
195}
196
197async fn delete_single_path_async(path: &Path, display: &str) -> BuiltinResult<()> {
198    match vfs::metadata_async(path).await {
199        Ok(meta) => {
200            if meta.is_dir() {
201                return Err(delete_error(
202                    MESSAGE_ID_IS_DIRECTORY,
203                    format!(
204                        "delete: cannot delete '{}' because it is a directory (use rmdir instead)",
205                        display
206                    ),
207                ));
208            }
209            vfs::remove_file_async(path).await.map_err(|err| {
210                delete_error(
211                    MESSAGE_ID_OS_ERROR,
212                    format!("delete: unable to delete '{}' ({})", display, err),
213                )
214            })
215        }
216        Err(err) => {
217            if err.kind() == io::ErrorKind::NotFound {
218                Err(delete_error(
219                    MESSAGE_ID_FILE_NOT_FOUND,
220                    format!(
221                        "delete: cannot delete '{}' because it does not exist",
222                        display
223                    ),
224                ))
225            } else {
226                Err(delete_error(
227                    MESSAGE_ID_OS_ERROR,
228                    format!("delete: unable to delete '{}' ({})", display, err),
229                ))
230            }
231        }
232    }
233}
234
235#[cfg(test)]
236fn delete_single_path(path: &Path, display: &str) -> BuiltinResult<()> {
237    futures::executor::block_on(delete_single_path_async(path, display))
238}
239
240fn validate_wildcard_pattern(pattern: &str, display: &str) -> BuiltinResult<()> {
241    if has_unbalanced(pattern, '[', ']') || has_unbalanced(pattern, '{', '}') {
242        return Err(delete_error(
243            MESSAGE_ID_INVALID_PATTERN,
244            format!("delete: invalid wildcard pattern '{display}'"),
245        ));
246    }
247    Ok(())
248}
249
250fn has_unbalanced(pattern: &str, open: char, close: char) -> bool {
251    let mut depth = 0usize;
252    let mut chars = pattern.chars();
253    while let Some(ch) = chars.next() {
254        if ch == '\\' {
255            // Skip escaped characters to avoid false positives
256            let _ = chars.next();
257            continue;
258        }
259        if ch == open {
260            depth += 1;
261        } else if ch == close {
262            if depth == 0 {
263                return true;
264            }
265            depth -= 1;
266        }
267    }
268    depth != 0
269}
270
271async fn gather_arguments(args: &[Value]) -> BuiltinResult<Vec<Value>> {
272    let mut out = Vec::with_capacity(args.len());
273    for value in args {
274        out.push(
275            gather_if_needed_async(value)
276                .await
277                .map_err(map_control_flow)?,
278        );
279    }
280    Ok(out)
281}
282
283fn collect_targets(value: &Value, targets: &mut Vec<String>) -> BuiltinResult<()> {
284    match value {
285        Value::String(text) => push_nonempty_target(text, targets),
286        Value::CharArray(array) => collect_char_array_targets(array, targets),
287        Value::StringArray(array) => collect_string_array_targets(array, targets),
288        Value::Cell(cell) => collect_cell_targets(cell, targets),
289        _ => Err(delete_error(MESSAGE_ID_INVALID_INPUT, ERR_FILENAME_ARG)),
290    }
291}
292
293fn collect_char_array_targets(array: &CharArray, targets: &mut Vec<String>) -> BuiltinResult<()> {
294    if array.rows == 0 || array.cols == 0 {
295        return Ok(());
296    }
297    for row in 0..array.rows {
298        let mut text = String::with_capacity(array.cols);
299        for col in 0..array.cols {
300            text.push(array.data[row * array.cols + col]);
301        }
302        let trimmed = text.trim_end().to_string();
303        if trimmed.is_empty() {
304            return Err(delete_error(
305                MESSAGE_ID_EMPTY_FILENAME,
306                "delete: filename cannot be empty",
307            ));
308        }
309        targets.push(trimmed);
310    }
311    Ok(())
312}
313
314fn collect_string_array_targets(
315    array: &StringArray,
316    targets: &mut Vec<String>,
317) -> BuiltinResult<()> {
318    for text in &array.data {
319        if text.is_empty() {
320            return Err(delete_error(
321                MESSAGE_ID_EMPTY_FILENAME,
322                "delete: filename cannot be empty",
323            ));
324        }
325        targets.push(text.clone());
326    }
327    Ok(())
328}
329
330fn collect_cell_targets(cell: &CellArray, targets: &mut Vec<String>) -> BuiltinResult<()> {
331    for handle in &cell.data {
332        let value = unsafe { &*handle.as_raw() };
333        collect_targets(value, targets)?;
334    }
335    Ok(())
336}
337
338fn delete_handles(values: &[Value]) -> BuiltinResult<Value> {
339    let mut mutated_last: Option<Value> = None;
340    let mut total = 0usize;
341    for value in values {
342        total += process_handle_value(value, &mut mutated_last)?;
343    }
344    if total == 1 {
345        Ok(mutated_last.unwrap_or(Value::Num(0.0)))
346    } else {
347        Ok(Value::Num(0.0))
348    }
349}
350
351fn process_handle_value(value: &Value, mutated_last: &mut Option<Value>) -> BuiltinResult<usize> {
352    match value {
353        Value::HandleObject(handle) => {
354            let mut invalid = handle.clone();
355            invalid.valid = false;
356            *mutated_last = Some(Value::HandleObject(invalid));
357            Ok(1)
358        }
359        Value::Listener(listener) => {
360            let mut invalid = listener.clone();
361            invalid.valid = false;
362            invalid.enabled = false;
363            *mutated_last = Some(Value::Listener(invalid));
364            Ok(1)
365        }
366        Value::Cell(cell) => {
367            let mut total = 0usize;
368            for handle in &cell.data {
369                let inner = unsafe { &*handle.as_raw() };
370                total += process_handle_value(inner, mutated_last)?;
371            }
372            Ok(total)
373        }
374        other => Err(delete_error(
375            MESSAGE_ID_INVALID_HANDLE,
376            format!("delete: unsupported handle input {other:?}"),
377        )),
378    }
379}
380
381fn is_handle_input(value: &Value) -> bool {
382    match value {
383        Value::HandleObject(_) | Value::Listener(_) => true,
384        Value::Cell(cell) => cell
385            .data
386            .iter()
387            .all(|ptr| is_handle_input(unsafe { &*ptr.as_raw() })),
388        _ => false,
389    }
390}
391
392fn contains_handle_input(value: &Value) -> bool {
393    match value {
394        Value::HandleObject(_) | Value::Listener(_) => true,
395        Value::Cell(cell) => cell
396            .data
397            .iter()
398            .any(|ptr| contains_handle_input(unsafe { &*ptr.as_raw() })),
399        _ => false,
400    }
401}
402
403fn push_nonempty_target(text: &str, targets: &mut Vec<String>) -> BuiltinResult<()> {
404    if text.is_empty() {
405        Err(delete_error(
406            MESSAGE_ID_EMPTY_FILENAME,
407            "delete: filename cannot be empty",
408        ))
409    } else {
410        targets.push(text.to_string());
411        Ok(())
412    }
413}
414
415#[cfg(test)]
416pub(crate) mod tests {
417    use super::super::REPL_FS_TEST_LOCK;
418    use super::*;
419    use runmat_builtins::{CharArray, StringArray, Value};
420    use std::fs::File;
421    use tempfile::tempdir;
422
423    fn delete_builtin(args: Vec<Value>) -> BuiltinResult<Value> {
424        futures::executor::block_on(super::delete_builtin(args))
425    }
426
427    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
428    #[test]
429    fn delete_removes_single_file() {
430        let _lock = REPL_FS_TEST_LOCK
431            .lock()
432            .unwrap_or_else(|poison| poison.into_inner());
433
434        let temp = tempdir().expect("temp dir");
435        let target = temp.path().join("single.txt");
436        File::create(&target).expect("create");
437
438        let result = delete_builtin(vec![Value::from(target.to_string_lossy().to_string())])
439            .expect("delete");
440        assert_eq!(result, Value::Num(0.0));
441        assert!(!target.exists());
442    }
443
444    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
445    #[test]
446    fn delete_removes_files_with_wildcard() {
447        let _lock = REPL_FS_TEST_LOCK
448            .lock()
449            .unwrap_or_else(|poison| poison.into_inner());
450
451        let temp = tempdir().expect("temp dir");
452        let file_a = temp.path().join("log-01.txt");
453        let file_b = temp.path().join("log-02.txt");
454        File::create(&file_a).expect("create a");
455        File::create(&file_b).expect("create b");
456
457        let pattern = temp.path().join("log-*.txt");
458        delete_builtin(vec![Value::from(pattern.to_string_lossy().to_string())]).expect("delete");
459        assert!(!file_a.exists());
460        assert!(!file_b.exists());
461    }
462
463    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
464    #[test]
465    fn delete_accepts_string_array() {
466        let _lock = REPL_FS_TEST_LOCK
467            .lock()
468            .unwrap_or_else(|poison| poison.into_inner());
469
470        let temp = tempdir().expect("temp dir");
471        let file_a = temp.path().join("stageA.dat");
472        let file_b = temp.path().join("stageB.dat");
473        File::create(&file_a).expect("create a");
474        File::create(&file_b).expect("create b");
475
476        let array = StringArray::new(
477            vec![
478                file_a.to_string_lossy().to_string(),
479                file_b.to_string_lossy().to_string(),
480            ],
481            vec![2],
482        )
483        .expect("string array");
484
485        delete_builtin(vec![Value::StringArray(array)]).expect("delete");
486        assert!(!file_a.exists());
487        assert!(!file_b.exists());
488    }
489
490    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
491    #[test]
492    fn delete_accepts_char_array() {
493        let _lock = REPL_FS_TEST_LOCK
494            .lock()
495            .unwrap_or_else(|poison| poison.into_inner());
496
497        let temp = tempdir().expect("temp dir");
498        let paths: Vec<_> = ["stageA.tmp", "stageB.tmp"]
499            .into_iter()
500            .map(|name| temp.path().join(name))
501            .collect();
502        let path_strings: Vec<String> = paths
503            .iter()
504            .map(|p| p.to_string_lossy().to_string())
505            .collect();
506        let max_len = path_strings.iter().map(|s| s.len()).max().unwrap();
507        let mut data: Vec<char> = Vec::with_capacity(path_strings.len() * max_len);
508
509        for (path, path_string) in paths.iter().zip(path_strings.iter()) {
510            File::create(path).expect("create file");
511            let mut chars: Vec<char> = path_string.chars().collect();
512            while chars.len() < max_len {
513                chars.push(' ');
514            }
515            data.extend(&chars);
516        }
517
518        let char_array = CharArray::new(data, path_strings.len(), max_len).expect("char array");
519        delete_builtin(vec![Value::CharArray(char_array)]).expect("delete");
520
521        for path in paths {
522            assert!(!path.exists(), "{path:?} should be removed");
523        }
524    }
525
526    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
527    #[test]
528    fn delete_accepts_cell_array_of_paths() {
529        let _lock = REPL_FS_TEST_LOCK
530            .lock()
531            .unwrap_or_else(|poison| poison.into_inner());
532
533        let temp = tempdir().expect("temp dir");
534        let file_a = temp.path().join("cellA.dat");
535        let file_b = temp.path().join("cellB.dat");
536        File::create(&file_a).expect("create cellA");
537        File::create(&file_b).expect("create cellB");
538
539        let cell_value = crate::make_cell(
540            vec![
541                Value::from(file_a.to_string_lossy().to_string()),
542                Value::from(file_b.to_string_lossy().to_string()),
543            ],
544            1,
545            2,
546        )
547        .expect("cell");
548
549        delete_builtin(vec![cell_value]).expect("delete");
550        assert!(!file_a.exists());
551        assert!(!file_b.exists());
552    }
553
554    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
555    #[test]
556    fn delete_empty_string_array_is_noop() {
557        let array = StringArray::new(Vec::<String>::new(), vec![0]).expect("empty array");
558        let result = delete_builtin(vec![Value::StringArray(array)]).expect("delete");
559        assert_eq!(result, Value::Num(0.0));
560    }
561
562    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
563    #[test]
564    fn delete_errors_on_empty_string_argument() {
565        let err = delete_builtin(vec![Value::from(String::new())]).expect_err("empty string");
566        assert_eq!(err.identifier(), Some(MESSAGE_ID_EMPTY_FILENAME));
567    }
568
569    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
570    #[test]
571    fn delete_errors_on_string_array_empty_element() {
572        let array =
573            StringArray::new(vec![String::new()], vec![1]).expect("single empty string element");
574        let err = delete_builtin(vec![Value::StringArray(array)]).expect_err("empty element");
575        assert_eq!(err.identifier(), Some(MESSAGE_ID_EMPTY_FILENAME));
576    }
577
578    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
579    #[test]
580    fn delete_errors_on_char_array_blank_row() {
581        let data = vec![' '; 4];
582        let char_array = CharArray::new(data, 1, 4).expect("char matrix");
583        let err = delete_builtin(vec![Value::CharArray(char_array)]).expect_err("blank row");
584        assert_eq!(err.identifier(), Some(MESSAGE_ID_EMPTY_FILENAME));
585    }
586
587    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
588    #[test]
589    fn delete_errors_on_invalid_pattern() {
590        let pattern = "{invalid*";
591        let err = futures::executor::block_on(delete_target(pattern))
592            .expect_err("invalid pattern should error");
593        assert_eq!(err.identifier(), Some(MESSAGE_ID_INVALID_PATTERN));
594    }
595
596    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
597    #[test]
598    fn delete_errors_on_missing_file() {
599        let _lock = REPL_FS_TEST_LOCK
600            .lock()
601            .unwrap_or_else(|poison| poison.into_inner());
602
603        let temp = tempdir().expect("temp dir");
604        let missing = temp.path().join("missing.txt");
605        let missing_str = missing.to_string_lossy().to_string();
606        let err = futures::executor::block_on(delete_target(&missing_str)).expect_err("error");
607        assert_eq!(err.identifier(), Some(MESSAGE_ID_FILE_NOT_FOUND));
608    }
609
610    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
611    #[test]
612    fn delete_errors_on_directory() {
613        let _lock = REPL_FS_TEST_LOCK
614            .lock()
615            .unwrap_or_else(|poison| poison.into_inner());
616
617        let temp = tempdir().expect("temp dir");
618        let dir = temp.path().join("dir");
619        std::fs::create_dir(&dir).expect("create dir");
620        let dir_display = dir.to_string_lossy().to_string();
621        let err = delete_single_path(&dir, &dir_display).expect_err("error");
622        assert_eq!(err.identifier(), Some(MESSAGE_ID_IS_DIRECTORY));
623    }
624
625    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
626    #[test]
627    fn delete_handle_returns_invalid_handle() {
628        let handle = futures::executor::block_on(crate::new_handle_object_builtin(
629            "ReplFsDeleteTestHandle".to_string(),
630        ))
631        .expect("handle");
632        let result = delete_builtin(vec![handle]).expect("delete handle");
633        match result {
634            Value::HandleObject(h) => {
635                assert!(!h.valid, "handle should be marked invalid");
636                let valid_value = futures::executor::block_on(crate::isvalid_builtin(
637                    Value::HandleObject(h.clone()),
638                ))
639                .expect("isvalid");
640                match valid_value {
641                    Value::Bool(flag) => assert!(!flag, "isvalid should report false after delete"),
642                    other => panic!("expected bool from isvalid, got {other:?}"),
643                }
644            }
645            other => panic!("expected handle result, got {other:?}"),
646        }
647    }
648
649    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
650    #[test]
651    fn delete_rejects_mixed_handle_and_filename() {
652        let handle = futures::executor::block_on(crate::new_handle_object_builtin(
653            "ReplFsDeleteTestHandle".to_string(),
654        ))
655        .expect("handle");
656        let err = delete_builtin(vec![
657            handle,
658            Value::from("mixed-handle-path.txt".to_string()),
659        ])
660        .expect_err("expected mixed error");
661        assert_eq!(err.identifier(), Some(MESSAGE_ID_INVALID_HANDLE));
662    }
663
664    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
665    #[test]
666    fn delete_accepts_cell_of_handles() {
667        let handle_a = futures::executor::block_on(crate::new_handle_object_builtin(
668            "ReplFsDeleteTestHandle".to_string(),
669        ))
670        .expect("handle");
671        let handle_b = futures::executor::block_on(crate::new_handle_object_builtin(
672            "ReplFsDeleteTestHandle".to_string(),
673        ))
674        .expect("handle");
675        let cell = crate::make_cell(vec![handle_a, handle_b], 1, 2).expect("cell of handles");
676        let result = delete_builtin(vec![cell]).expect("delete handles");
677        assert_eq!(result, Value::Num(0.0));
678    }
679
680    #[cfg(feature = "wgpu")]
681    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
682    #[test]
683    fn delete_runs_with_wgpu_provider_registered() {
684        let _lock = REPL_FS_TEST_LOCK
685            .lock()
686            .unwrap_or_else(|poison| poison.into_inner());
687
688        let _ = runmat_accelerate::backend::wgpu::provider::register_wgpu_provider(
689            runmat_accelerate::backend::wgpu::provider::WgpuProviderOptions::default(),
690        );
691
692        let temp = tempdir().expect("temp dir");
693        let path = temp.path().join("wgpu-file.txt");
694        File::create(&path).expect("create file");
695
696        delete_builtin(vec![Value::from(path.to_string_lossy().to_string())]).expect("delete");
697        assert!(!path.exists());
698    }
699}