1use runmat_filesystem as vfs;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use glob::Pattern;
8use runmat_builtins::{CharArray, Value};
9use runmat_macros::runtime_builtin;
10
11use crate::builtins::common::fs::{contains_wildcards, expand_user_path};
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_OS_ERROR: &str = "RunMat:movefile:OSError";
19const MESSAGE_ID_SOURCE_NOT_FOUND: &str = "RunMat:movefile:FileDoesNotExist";
20const MESSAGE_ID_DEST_EXISTS: &str = "RunMat:movefile:DestinationExists";
21const MESSAGE_ID_DEST_MISSING: &str = "RunMat:movefile:DestinationNotFound";
22const MESSAGE_ID_DEST_NOT_DIR: &str = "RunMat:movefile:DestinationNotDirectory";
23const MESSAGE_ID_EMPTY_SOURCE: &str = "RunMat:movefile:EmptySource";
24const MESSAGE_ID_EMPTY_DEST: &str = "RunMat:movefile:EmptyDestination";
25const MESSAGE_ID_PATTERN_ERROR: &str = "RunMat:movefile:InvalidPattern";
26
27const ERR_SOURCE_ARG: &str = "movefile: source must be a character vector or string scalar";
28const ERR_DEST_ARG: &str = "movefile: destination must be a character vector or string scalar";
29const ERR_FLAG_ARG: &str =
30 "movefile: flag must be the character 'f' supplied as a char vector or string scalar";
31
32#[runmat_macros::register_gpu_spec(builtin_path = "crate::builtins::io::repl_fs::movefile")]
33pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
34 name: "movefile",
35 op_kind: GpuOpKind::Custom("io"),
36 supported_precisions: &[],
37 broadcast: BroadcastSemantics::None,
38 provider_hooks: &[],
39 constant_strategy: ConstantStrategy::InlineLiteral,
40 residency: ResidencyPolicy::GatherImmediately,
41 nan_mode: ReductionNaN::Include,
42 two_pass_threshold: None,
43 workgroup_size: None,
44 accepts_nan_mode: false,
45 notes:
46 "Host-only filesystem builtin. GPU-resident path and flag arguments are gathered automatically before moving files.",
47};
48
49#[runmat_macros::register_fusion_spec(builtin_path = "crate::builtins::io::repl_fs::movefile")]
50pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
51 name: "movefile",
52 shape: ShapeRequirements::Any,
53 constant_strategy: ConstantStrategy::InlineLiteral,
54 elementwise: None,
55 reduction: None,
56 emits_nan: false,
57 notes: "Filesystem side-effects materialise immediately; metadata registered for completeness.",
58};
59
60const BUILTIN_NAME: &str = "movefile";
61
62fn movefile_error(message: impl Into<String>) -> RuntimeError {
63 build_runtime_error(message)
64 .with_builtin(BUILTIN_NAME)
65 .build()
66}
67
68fn map_control_flow(err: RuntimeError) -> RuntimeError {
69 let identifier = err.identifier().map(str::to_string);
70 let mut builder = build_runtime_error(format!("{BUILTIN_NAME}: {}", err.message()))
71 .with_builtin(BUILTIN_NAME)
72 .with_source(err);
73 if let Some(identifier) = identifier {
74 builder = builder.with_identifier(identifier);
75 }
76 builder.build()
77}
78
79#[runtime_builtin(
80 name = "movefile",
81 category = "io/repl_fs",
82 summary = "Move or rename files and folders with MATLAB-compatible status, message, and message ID outputs.",
83 keywords = "movefile,rename,move file,filesystem,status,message,messageid,force,overwrite",
84 accel = "cpu",
85 suppress_auto_output = true,
86 type_resolver(crate::builtins::io::type_resolvers::movefile_type),
87 builtin_path = "crate::builtins::io::repl_fs::movefile"
88)]
89async fn movefile_builtin(args: Vec<Value>) -> crate::BuiltinResult<Value> {
90 let eval = evaluate(&args).await?;
91 if let Some(out_count) = crate::output_count::current_output_count() {
92 if out_count == 0 {
93 return Ok(Value::OutputList(Vec::new()));
94 }
95 return Ok(crate::output_count::output_list_with_padding(
96 out_count,
97 eval.outputs(),
98 ));
99 }
100 Ok(eval.first_output())
101}
102
103pub async fn evaluate(args: &[Value]) -> BuiltinResult<MovefileResult> {
105 let gathered = gather_arguments(args).await?;
106 match gathered.len() {
107 0 | 1 => Err(movefile_error("movefile: not enough input arguments")),
108 2 => move_operation(&gathered[0], &gathered[1], false).await,
109 3 => {
110 let force = parse_force_flag(&gathered[2])?;
111 move_operation(&gathered[0], &gathered[1], force).await
112 }
113 _ => Err(movefile_error("movefile: too many input arguments")),
114 }
115}
116
117#[derive(Debug, Clone)]
118pub struct MovefileResult {
119 status: f64,
120 message: String,
121 message_id: String,
122}
123
124impl MovefileResult {
125 fn success() -> Self {
126 Self {
127 status: 1.0,
128 message: String::new(),
129 message_id: String::new(),
130 }
131 }
132
133 fn failure(message: String, message_id: &str) -> Self {
134 Self {
135 status: 0.0,
136 message,
137 message_id: message_id.to_string(),
138 }
139 }
140
141 fn empty_source() -> Self {
142 Self::failure(
143 "Source file or folder name must not be empty.".to_string(),
144 MESSAGE_ID_EMPTY_SOURCE,
145 )
146 }
147
148 fn empty_destination() -> Self {
149 Self::failure(
150 "Destination file or folder name must not be empty.".to_string(),
151 MESSAGE_ID_EMPTY_DEST,
152 )
153 }
154
155 fn source_not_found(display: &str) -> Self {
156 Self::failure(
157 format!("Source \"{}\" does not exist.", display),
158 MESSAGE_ID_SOURCE_NOT_FOUND,
159 )
160 }
161
162 fn destination_exists(display: &str) -> Self {
163 Self::failure(
164 format!(
165 "Cannot move to \"{}\": destination already exists.",
166 display
167 ),
168 MESSAGE_ID_DEST_EXISTS,
169 )
170 }
171
172 fn destination_missing(display: &str) -> Self {
173 Self::failure(
174 format!(
175 "Destination folder \"{}\" must exist when moving multiple sources.",
176 display
177 ),
178 MESSAGE_ID_DEST_MISSING,
179 )
180 }
181
182 fn destination_not_directory(display: &str) -> Self {
183 Self::failure(
184 format!("Destination \"{}\" must refer to a folder.", display),
185 MESSAGE_ID_DEST_NOT_DIR,
186 )
187 }
188
189 fn glob_pattern_error(pattern: &str, err: &str) -> Self {
190 Self::failure(
191 format!("Invalid source pattern \"{}\": {}", pattern, err),
192 MESSAGE_ID_PATTERN_ERROR,
193 )
194 }
195
196 fn os_error(source: &str, target: &str, err: &io::Error) -> Self {
197 Self::failure(
198 format!("Unable to move \"{}\" to \"{}\": {}", source, target, err),
199 MESSAGE_ID_OS_ERROR,
200 )
201 }
202
203 pub fn first_output(&self) -> Value {
204 Value::Num(self.status)
205 }
206
207 pub fn outputs(&self) -> Vec<Value> {
208 vec![
209 Value::Num(self.status),
210 char_array_value(&self.message),
211 char_array_value(&self.message_id),
212 ]
213 }
214
215 #[cfg(test)]
216 pub(crate) fn status(&self) -> f64 {
217 self.status
218 }
219
220 #[cfg(test)]
221 pub(crate) fn message(&self) -> &str {
222 &self.message
223 }
224
225 #[cfg(test)]
226 pub(crate) fn message_id(&self) -> &str {
227 &self.message_id
228 }
229}
230
231async fn move_operation(
232 source: &Value,
233 destination: &Value,
234 force: bool,
235) -> BuiltinResult<MovefileResult> {
236 let source_raw = extract_path(source, ERR_SOURCE_ARG)?;
237 if source_raw.is_empty() {
238 return Ok(MovefileResult::empty_source());
239 }
240
241 let destination_raw = extract_path(destination, ERR_DEST_ARG)?;
242 if destination_raw.is_empty() {
243 return Ok(MovefileResult::empty_destination());
244 }
245
246 let source_expanded = expand_user_path(&source_raw, "movefile").map_err(movefile_error)?;
247 let destination_expanded =
248 expand_user_path(&destination_raw, "movefile").map_err(movefile_error)?;
249
250 if contains_wildcards(&source_expanded) {
251 Ok(move_with_pattern(&source_expanded, &destination_expanded, force).await)
252 } else {
253 Ok(move_single_source(&source_expanded, &destination_expanded, force).await)
254 }
255}
256
257async fn move_single_source(source: &str, destination: &str, force: bool) -> MovefileResult {
258 let source_path = PathBuf::from(source);
259 if vfs::metadata_async(&source_path).await.is_err() {
260 return MovefileResult::source_not_found(source);
261 }
262
263 let destination_path = PathBuf::from(destination);
264 if destination_path == source_path {
265 return MovefileResult::success();
266 }
267
268 let destination_meta = vfs::metadata_async(&destination_path).await.ok();
269 let mut target_path = destination_path.clone();
270 let mut remove_target = false;
271 let mut remove_is_dir = false;
272
273 if let Some(meta) = &destination_meta {
274 if meta.is_dir() {
275 let Some(name) = source_path.file_name() else {
276 return MovefileResult::os_error(
277 source,
278 destination,
279 &io::Error::other("Cannot determine source file name"),
280 );
281 };
282 target_path = destination_path.join(name);
283 if target_path == source_path {
284 return MovefileResult::success();
285 }
286 match vfs::metadata_async(&target_path).await {
287 Ok(existing) => {
288 if !force {
289 return MovefileResult::destination_exists(&path_to_display(&target_path));
290 }
291 remove_target = true;
292 remove_is_dir = existing.is_dir();
293 }
294 Err(err) => {
295 if err.kind() != io::ErrorKind::NotFound {
296 return MovefileResult::os_error(
297 source,
298 &path_to_display(&target_path),
299 &err,
300 );
301 }
302 }
303 }
304 } else if !force {
305 if destination_path == source_path {
306 return MovefileResult::success();
307 }
308 return MovefileResult::destination_exists(destination);
309 } else {
310 remove_target = true;
311 remove_is_dir = meta.is_dir();
312 }
313 }
314
315 let source_display = path_to_display(&source_path);
316 let target_display = path_to_display(&target_path);
317 let plan = vec![MovePlanEntry::new(
318 source_path,
319 source_display,
320 target_path,
321 target_display,
322 remove_target,
323 remove_is_dir,
324 )];
325
326 match execute_plan(&plan).await {
327 Ok(()) => MovefileResult::success(),
328 Err(err) => MovefileResult::os_error(&err.source_display, &err.target_display, &err.error),
329 }
330}
331
332async fn move_with_pattern(pattern: &str, destination: &str, force: bool) -> MovefileResult {
333 let pattern_path = Path::new(pattern);
334 let (base_dir, name_pattern) = match pattern_path.file_name() {
335 Some(name) => (
336 pattern_path.parent().unwrap_or_else(|| Path::new(".")),
337 name,
338 ),
339 None => {
340 return MovefileResult::glob_pattern_error(pattern, "pattern has no file name");
341 }
342 };
343 let matcher = match Pattern::new(&name_pattern.to_string_lossy()) {
344 Ok(matcher) => matcher,
345 Err(err) => return MovefileResult::glob_pattern_error(pattern, err.msg),
346 };
347
348 let mut matches = Vec::new();
349 let entries = match vfs::read_dir_async(base_dir).await {
350 Ok(entries) => entries,
351 Err(err) => {
352 let display = path_to_display(base_dir);
353 return MovefileResult::os_error(&display, destination, &err);
354 }
355 };
356
357 for entry in entries {
358 let file_name = entry.file_name().to_string_lossy();
359 if matcher.matches(&file_name) {
360 matches.push(entry.path().to_path_buf());
361 }
362 }
363
364 if matches.is_empty() {
365 return MovefileResult::source_not_found(pattern);
366 }
367
368 let destination_path = PathBuf::from(destination);
369 let destination_meta = match vfs::metadata_async(&destination_path).await {
370 Ok(meta) => meta,
371 Err(_) => return MovefileResult::destination_missing(destination),
372 };
373
374 if !destination_meta.is_dir() {
375 return MovefileResult::destination_not_directory(destination);
376 }
377
378 let mut plan = Vec::with_capacity(matches.len());
379 for source_path in matches {
380 let display_source = path_to_display(&source_path);
381 if vfs::metadata_async(&source_path).await.is_err() {
382 return MovefileResult::source_not_found(&display_source);
383 }
384 let Some(name) = source_path.file_name() else {
385 return MovefileResult::os_error(
386 &display_source,
387 destination,
388 &io::Error::other("Cannot determine source name"),
389 );
390 };
391 let target_path = destination_path.join(name);
392 if target_path == source_path {
393 continue;
394 }
395 let target_display = path_to_display(&target_path);
396 match vfs::metadata_async(&target_path).await {
397 Ok(existing) => {
398 if !force {
399 return MovefileResult::destination_exists(&target_display);
400 }
401 plan.push(MovePlanEntry::new(
402 source_path.clone(),
403 display_source.clone(),
404 target_path.clone(),
405 target_display,
406 true,
407 existing.is_dir(),
408 ));
409 }
410 Err(err) => {
411 if err.kind() != io::ErrorKind::NotFound {
412 return MovefileResult::os_error(&display_source, &target_display, &err);
413 }
414 plan.push(MovePlanEntry::new(
415 source_path.clone(),
416 display_source,
417 target_path.clone(),
418 target_display,
419 false,
420 false,
421 ));
422 }
423 }
424 }
425
426 match execute_plan(&plan).await {
427 Ok(()) => MovefileResult::success(),
428 Err(err) => MovefileResult::os_error(&err.source_display, &err.target_display, &err.error),
429 }
430}
431
432#[derive(Debug, Clone)]
433struct MovePlanEntry {
434 source_path: PathBuf,
435 source_display: String,
436 target_path: PathBuf,
437 target_display: String,
438 remove_target: bool,
439 remove_is_dir: bool,
440}
441
442impl MovePlanEntry {
443 fn new(
444 source_path: PathBuf,
445 source_display: String,
446 target_path: PathBuf,
447 target_display: String,
448 remove_target: bool,
449 remove_is_dir: bool,
450 ) -> Self {
451 Self {
452 source_path,
453 source_display,
454 target_path,
455 target_display,
456 remove_target,
457 remove_is_dir,
458 }
459 }
460}
461
462struct MoveError {
463 source_display: String,
464 target_display: String,
465 error: io::Error,
466}
467
468async fn execute_plan(plan: &[MovePlanEntry]) -> Result<(), MoveError> {
469 for entry in plan {
470 if entry.remove_target {
471 let result = if entry.remove_is_dir {
472 vfs::remove_dir_all_async(&entry.target_path).await
473 } else {
474 vfs::remove_file_async(&entry.target_path).await
475 };
476 if let Err(err) = result {
477 if err.kind() != io::ErrorKind::NotFound {
478 return Err(MoveError {
479 source_display: entry.source_display.clone(),
480 target_display: entry.target_display.clone(),
481 error: err,
482 });
483 }
484 }
485 }
486
487 if let Err(err) = vfs::rename_async(&entry.source_path, &entry.target_path).await {
488 return Err(MoveError {
489 source_display: entry.source_display.clone(),
490 target_display: entry.target_display.clone(),
491 error: err,
492 });
493 }
494 }
495
496 Ok(())
497}
498
499fn parse_force_flag(value: &Value) -> BuiltinResult<bool> {
500 let text = extract_path(value, ERR_FLAG_ARG)?;
501 if text.eq_ignore_ascii_case("f") {
502 Ok(true)
503 } else {
504 Err(movefile_error(ERR_FLAG_ARG))
505 }
506}
507
508fn extract_path(value: &Value, error_message: &str) -> BuiltinResult<String> {
509 match value {
510 Value::String(text) => Ok(text.clone()),
511 Value::CharArray(array) => {
512 if array.rows == 1 {
513 Ok(array.data.iter().collect())
514 } else {
515 Err(movefile_error(error_message))
516 }
517 }
518 Value::StringArray(array) => {
519 if array.data.len() == 1 {
520 Ok(array.data[0].clone())
521 } else {
522 Err(movefile_error(error_message))
523 }
524 }
525 _ => Err(movefile_error(error_message)),
526 }
527}
528
529async fn gather_arguments(args: &[Value]) -> BuiltinResult<Vec<Value>> {
530 let mut out = Vec::with_capacity(args.len());
531 for value in args {
532 out.push(
533 gather_if_needed_async(value)
534 .await
535 .map_err(map_control_flow)?,
536 );
537 }
538 Ok(out)
539}
540
541fn char_array_value(text: &str) -> Value {
542 Value::CharArray(CharArray::new_row(text))
543}
544
545fn path_to_display(path: &Path) -> String {
546 path.display().to_string()
547}
548
549#[cfg(test)]
550pub(crate) mod tests {
551 use super::super::REPL_FS_TEST_LOCK;
552 use super::*;
553 use std::fs::{self, File};
554 use tempfile::tempdir;
555
556 fn evaluate(args: &[Value]) -> BuiltinResult<MovefileResult> {
557 futures::executor::block_on(super::evaluate(args))
558 }
559
560 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
561 #[test]
562 fn movefile_renames_file() {
563 let _lock = REPL_FS_TEST_LOCK
564 .lock()
565 .unwrap_or_else(|poison| poison.into_inner());
566
567 let temp = tempdir().expect("temp dir");
568 let source = temp.path().join("source.txt");
569 let dest = temp.path().join("dest.txt");
570 File::create(&source).expect("create source");
571
572 let eval = evaluate(&[
573 Value::from(source.to_string_lossy().to_string()),
574 Value::from(dest.to_string_lossy().to_string()),
575 ])
576 .expect("movefile");
577 assert_eq!(eval.status(), 1.0);
578 assert!(!source.exists());
579 assert!(dest.exists());
580 }
581
582 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
583 #[test]
584 fn movefile_moves_into_existing_directory() {
585 let _lock = REPL_FS_TEST_LOCK
586 .lock()
587 .unwrap_or_else(|poison| poison.into_inner());
588
589 let temp = tempdir().expect("temp dir");
590 let source = temp.path().join("report.txt");
591 let dest_dir = temp.path().join("reports");
592 fs::create_dir(&dest_dir).expect("create dest dir");
593 File::create(&source).expect("create source");
594
595 let eval = evaluate(&[
596 Value::from(source.to_string_lossy().to_string()),
597 Value::from(dest_dir.to_string_lossy().to_string()),
598 ])
599 .expect("movefile");
600 assert_eq!(eval.status(), 1.0);
601 assert!(dest_dir.join("report.txt").exists());
602 }
603
604 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
605 #[test]
606 fn movefile_force_overwrites_existing_file() {
607 let _lock = REPL_FS_TEST_LOCK
608 .lock()
609 .unwrap_or_else(|poison| poison.into_inner());
610
611 let temp = tempdir().expect("temp dir");
612 let source = temp.path().join("draft.txt");
613 let dest = temp.path().join("final.txt");
614 File::create(&source).expect("create source");
615 File::create(&dest).expect("create dest");
616
617 let eval = evaluate(&[
618 Value::from(source.to_string_lossy().to_string()),
619 Value::from(dest.to_string_lossy().to_string()),
620 Value::from("f"),
621 ])
622 .expect("movefile");
623 assert_eq!(eval.status(), 1.0);
624 assert!(!source.exists());
625 assert!(dest.exists());
626 }
627
628 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
629 #[test]
630 fn movefile_without_force_preserves_existing_file() {
631 let _lock = REPL_FS_TEST_LOCK
632 .lock()
633 .unwrap_or_else(|poison| poison.into_inner());
634
635 let temp = tempdir().expect("temp dir");
636 let source = temp.path().join("draft.txt");
637 let dest = temp.path().join("final.txt");
638 File::create(&source).expect("create source");
639 File::create(&dest).expect("create dest");
640
641 let eval = evaluate(&[
642 Value::from(source.to_string_lossy().to_string()),
643 Value::from(dest.to_string_lossy().to_string()),
644 ])
645 .expect("movefile");
646 assert_eq!(eval.status(), 0.0);
647 assert_eq!(eval.message_id(), MESSAGE_ID_DEST_EXISTS);
648 assert!(eval.message().contains("destination already exists."));
649 assert!(source.exists());
650 assert!(dest.exists());
651 }
652
653 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
654 #[test]
655 fn movefile_moves_multiple_files_with_wildcard() {
656 let _lock = REPL_FS_TEST_LOCK
657 .lock()
658 .unwrap_or_else(|poison| poison.into_inner());
659
660 let temp = tempdir().expect("temp dir");
661 let dest_dir = temp.path().join("logs");
662 fs::create_dir(&dest_dir).expect("create dest dir");
663 let file_a = temp.path().join("a.log");
664 let file_b = temp.path().join("b.log");
665 File::create(&file_a).expect("create a");
666 File::create(&file_b).expect("create b");
667
668 let pattern = temp.path().join("*.log");
669 let eval = evaluate(&[
670 Value::from(pattern.to_string_lossy().to_string()),
671 Value::from(dest_dir.to_string_lossy().to_string()),
672 ])
673 .expect("movefile");
674 assert_eq!(eval.status(), 1.0);
675 assert!(dest_dir.join("a.log").exists());
676 assert!(dest_dir.join("b.log").exists());
677 }
678
679 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
680 #[test]
681 fn movefile_reports_missing_source() {
682 let _lock = REPL_FS_TEST_LOCK
683 .lock()
684 .unwrap_or_else(|poison| poison.into_inner());
685
686 let temp = tempdir().expect("temp dir");
687 let source = temp.path().join("missing.txt");
688 let dest = temp.path().join("dest.txt");
689
690 let eval = evaluate(&[
691 Value::from(source.to_string_lossy().to_string()),
692 Value::from(dest.to_string_lossy().to_string()),
693 ])
694 .expect("movefile");
695 assert_eq!(eval.status(), 0.0);
696 assert_eq!(eval.message_id(), MESSAGE_ID_SOURCE_NOT_FOUND);
697 assert!(eval.message().contains("does not exist"));
698 }
699
700 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
701 #[test]
702 fn movefile_outputs_char_arrays() {
703 let _lock = REPL_FS_TEST_LOCK
704 .lock()
705 .unwrap_or_else(|poison| poison.into_inner());
706
707 let temp = tempdir().expect("temp dir");
708 let source = temp.path().join("source.txt");
709 let dest = temp.path().join("dest.txt");
710 File::create(&source).expect("create source");
711
712 let eval = evaluate(&[
713 Value::from(source.to_string_lossy().to_string()),
714 Value::from(dest.to_string_lossy().to_string()),
715 ])
716 .expect("movefile");
717 let outputs = eval.outputs();
718 assert_eq!(outputs.len(), 3);
719 assert!(matches!(outputs[0], Value::Num(1.0)));
720 assert!(matches!(outputs[1], Value::CharArray(ref ca) if ca.cols == 0));
721 assert!(matches!(outputs[2], Value::CharArray(ref ca) if ca.cols == 0));
722 }
723
724 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
725 #[test]
726 fn movefile_rejects_invalid_flag() {
727 let _lock = REPL_FS_TEST_LOCK
728 .lock()
729 .unwrap_or_else(|poison| poison.into_inner());
730
731 let err = evaluate(&[Value::from("a"), Value::from("b"), Value::Num(1.0)])
732 .expect_err("expected error");
733 assert_eq!(err.message(), ERR_FLAG_ARG);
734 }
735
736 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
737 #[test]
738 fn movefile_force_flag_accepts_uppercase_char_array() {
739 let _lock = REPL_FS_TEST_LOCK
740 .lock()
741 .unwrap_or_else(|poison| poison.into_inner());
742
743 let temp = tempdir().expect("temp dir");
744 let source = temp.path().join("draft.txt");
745 let dest = temp.path().join("final.txt");
746 File::create(&source).expect("create source");
747 File::create(&dest).expect("create dest");
748
749 let eval = evaluate(&[
750 Value::from(source.to_string_lossy().to_string()),
751 Value::from(dest.to_string_lossy().to_string()),
752 Value::CharArray(CharArray::new_row("F")),
753 ])
754 .expect("movefile");
755 assert_eq!(eval.status(), 1.0);
756 }
757
758 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
759 #[test]
760 fn movefile_same_path_is_success() {
761 let _lock = REPL_FS_TEST_LOCK
762 .lock()
763 .unwrap_or_else(|poison| poison.into_inner());
764
765 let temp = tempdir().expect("temp dir");
766 let source = temp.path().join("note.txt");
767 File::create(&source).expect("create source");
768
769 let eval = evaluate(&[
770 Value::from(source.to_string_lossy().to_string()),
771 Value::from(source.to_string_lossy().to_string()),
772 ])
773 .expect("movefile");
774 assert_eq!(eval.status(), 1.0);
775 assert!(source.exists());
776 }
777
778 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
779 #[test]
780 fn movefile_moving_into_same_directory_is_success() {
781 let _lock = REPL_FS_TEST_LOCK
782 .lock()
783 .unwrap_or_else(|poison| poison.into_inner());
784
785 let temp = tempdir().expect("temp dir");
786 let dir = temp.path().join("docs");
787 fs::create_dir(&dir).expect("create dir");
788 let source = dir.join("readme.txt");
789 File::create(&source).expect("create source");
790
791 let eval = evaluate(&[
792 Value::from(source.to_string_lossy().to_string()),
793 Value::from(dir.to_string_lossy().to_string()),
794 ])
795 .expect("movefile");
796 assert_eq!(eval.status(), 1.0);
797 assert!(dir.join("readme.txt").exists());
798 }
799
800 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
801 #[test]
802 fn movefile_reports_empty_source() {
803 let _lock = REPL_FS_TEST_LOCK
804 .lock()
805 .unwrap_or_else(|poison| poison.into_inner());
806
807 let eval = evaluate(&[Value::from(""), Value::from("dest.txt")]).expect("movefile");
808 assert_eq!(eval.status(), 0.0);
809 assert_eq!(eval.message_id(), MESSAGE_ID_EMPTY_SOURCE);
810 }
811
812 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
813 #[test]
814 fn movefile_reports_empty_destination() {
815 let _lock = REPL_FS_TEST_LOCK
816 .lock()
817 .unwrap_or_else(|poison| poison.into_inner());
818
819 let eval = evaluate(&[Value::from("source.txt"), Value::from("")]).expect("movefile");
820 assert_eq!(eval.status(), 0.0);
821 assert_eq!(eval.message_id(), MESSAGE_ID_EMPTY_DEST);
822 }
823
824 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
825 #[test]
826 fn movefile_requires_existing_destination_directory_for_pattern() {
827 let _lock = REPL_FS_TEST_LOCK
828 .lock()
829 .unwrap_or_else(|poison| poison.into_inner());
830
831 let temp = tempdir().expect("temp dir");
832 let file = temp.path().join("file.log");
833 File::create(&file).expect("create file");
834 let pattern = temp.path().join("*.log");
835 let dest = temp.path().join("missing");
836
837 let eval = evaluate(&[
838 Value::from(pattern.to_string_lossy().to_string()),
839 Value::from(dest.to_string_lossy().to_string()),
840 ])
841 .expect("movefile");
842 assert_eq!(eval.status(), 0.0);
843 assert_eq!(eval.message_id(), MESSAGE_ID_DEST_MISSING);
844 }
845
846 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
847 #[test]
848 fn movefile_requires_directory_destination_for_pattern() {
849 let _lock = REPL_FS_TEST_LOCK
850 .lock()
851 .unwrap_or_else(|poison| poison.into_inner());
852
853 let temp = tempdir().expect("temp dir");
854 let file = temp.path().join("file.log");
855 let dest_file = temp.path().join("dest.log");
856 File::create(&file).expect("create file");
857 File::create(&dest_file).expect("create dest");
858 let pattern = temp.path().join("*.log");
859
860 let eval = evaluate(&[
861 Value::from(pattern.to_string_lossy().to_string()),
862 Value::from(dest_file.to_string_lossy().to_string()),
863 ])
864 .expect("movefile");
865 assert_eq!(eval.status(), 0.0);
866 assert_eq!(eval.message_id(), MESSAGE_ID_DEST_NOT_DIR);
867 }
868
869 #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test::wasm_bindgen_test)]
870 #[test]
871 fn movefile_reports_invalid_pattern() {
872 let _lock = REPL_FS_TEST_LOCK
873 .lock()
874 .unwrap_or_else(|poison| poison.into_inner());
875
876 let eval = evaluate(&[
877 Value::from("[*.txt"), Value::from("dest"),
879 ])
880 .expect("movefile");
881 assert_eq!(eval.status(), 0.0);
882 assert_eq!(eval.message_id(), MESSAGE_ID_PATTERN_ERROR);
883 }
884}