1use 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 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}