1use super::time_format_error_to_io_error;
20use crate::MachineBuilder;
21use crate::console::{Console, Pager, is_narrow};
22use crate::storage::Storage;
23use async_trait::async_trait;
24use endbasic_core::{
25 ArgSep, ArgSepSyntax, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
26 ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
27};
28use std::borrow::Cow;
29use std::cell::RefCell;
30use std::cmp;
31use std::io;
32use std::rc::Rc;
33use std::str;
34use time::format_description;
35
36const CATEGORY: &str = "File system
38The EndBASIC storage subsystem is organized as a collection of drives, each identified by a \
39case-insensitive name. Drives can be backed by a multitude of file systems with different \
40behaviors, and their targets are specified as URIs. Special targets include: memory://, which \
41points to an in-memory read/write drive; and demos://, which points to a read-only drive with \
42sample programs. Other targets may be available such as file:// to access a local directory or \
43local:// to access web-local storage, depending on the context. The output of the MOUNT command \
44can help to identify which targets are available.
45All commands that operate with files take a path. Paths in EndBASIC can be of the form \
46FILENAME.EXT, in which case they refer to a file in the current drive; or DRIVE:/FILENAME.EXT and \
47DRIVE:FILENAME.EXT, in which case they refer to a file in the specified drive. Note that the \
48slash before the file name is currently optional because EndBASIC does not support directories \
49yet. Furthermore, if .EXT is missing, a .BAS extension is assumed.
50Be aware that the commands below must be invoked using proper EndBASIC syntax. In particular, \
51this means that path arguments must be double-quoted and multiple arguments have to be separated \
52by a comma (not a space). If you have used commands like CD, DIR, or MOUNT in other contexts, \
53this is likely to confuse you.
54See the \"Stored program\" help topic for information on how to load, modify, and save programs.";
55
56async fn show_dir(storage: &Storage, console: &mut dyn Console, path: &str) -> io::Result<()> {
58 let canonical_path = storage.make_canonical(path)?;
59 let files = storage.enumerate(path).await?;
60
61 let format = format_description::parse("[year]-[month]-[day] [hour]:[minute]")
62 .expect("Hardcoded format must be valid");
63 let show_narrow = is_narrow(&*console);
64
65 let mut pager = Pager::new(console)?;
66 pager.print("").await?;
67 pager.print(&format!(" Directory of {}", canonical_path)).await?;
68 pager.print("").await?;
69 if show_narrow {
70 let mut total_files = 0;
71 for name in files.dirents().keys() {
72 pager.print(&format!(" {}", name,)).await?;
73 total_files += 1;
74 }
75 if total_files > 0 {
76 pager.print("").await?;
77 }
78 pager.print(&format!(" {} file(s)", total_files)).await?;
79 } else {
80 let mut total_files = 0;
81 let mut total_bytes = 0;
82 pager.print(" Modified Size Name").await?;
83 for (name, details) in files.dirents() {
84 pager
85 .print(&format!(
86 " {} {:6} {}",
87 details.date.format(&format).map_err(time_format_error_to_io_error)?,
88 details.length,
89 name,
90 ))
91 .await?;
92 total_files += 1;
93 total_bytes += details.length;
94 }
95 if total_files > 0 {
96 pager.print("").await?;
97 }
98 pager.print(&format!(" {} file(s), {} bytes", total_files, total_bytes)).await?;
99 if let (Some(disk_quota), Some(disk_free)) = (files.disk_quota(), files.disk_free()) {
100 pager
101 .print(&format!(" {} of {} bytes free", disk_free.bytes, disk_quota.bytes))
102 .await?;
103 }
104 }
105 pager.print("").await?;
106 Ok(())
107}
108
109fn show_drives(storage: &Storage, console: &mut dyn Console) -> io::Result<()> {
111 let drive_info = storage.mounted();
112 let max_length = drive_info.keys().fold("Name".len(), |max, name| cmp::max(max, name.len()));
113
114 console.print("")?;
115 let filler = " ".repeat(max_length - "Name".len());
116 console.print(&format!(" Name{} Target", filler))?;
117 let num_drives = drive_info.len();
118 for (name, uri) in drive_info {
119 let filler = " ".repeat(max_length - name.len());
120 console.print(&format!(" {}{} {}", name, filler, uri))?;
121 }
122 console.print("")?;
123 console.print(&format!(" {} drive(s)", num_drives))?;
124 console.print("")?;
125 Ok(())
126}
127
128pub struct CdCommand {
130 metadata: Rc<CallableMetadata>,
131 storage: Rc<RefCell<Storage>>,
132}
133
134impl CdCommand {
135 pub fn new(storage: Rc<RefCell<Storage>>) -> Rc<Self> {
137 Rc::from(Self {
138 metadata: CallableMetadataBuilder::new("CD")
139 .with_syntax(&[(
140 &[SingularArgSyntax::RequiredValue(
141 RequiredValueSyntax { name: Cow::Borrowed("path"), vtype: ExprType::Text },
142 ArgSepSyntax::End,
143 )],
144 None,
145 )])
146 .with_category(CATEGORY)
147 .with_description("Changes the current path.")
148 .build(),
149 storage,
150 })
151 }
152}
153
154#[async_trait(?Send)]
155impl Callable for CdCommand {
156 fn metadata(&self) -> Rc<CallableMetadata> {
157 self.metadata.clone()
158 }
159
160 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
161 debug_assert_eq!(1, scope.nargs());
162 let target = scope.get_string(0);
163
164 self.storage.borrow_mut().cd(target)?;
165
166 Ok(())
167 }
168}
169
170pub struct CopyCommand {
172 metadata: Rc<CallableMetadata>,
173 storage: Rc<RefCell<Storage>>,
174}
175
176impl CopyCommand {
177 pub fn new(storage: Rc<RefCell<Storage>>) -> Rc<Self> {
179 Rc::from(Self {
180 metadata: CallableMetadataBuilder::new("COPY")
181 .with_async(true)
182 .with_syntax(&[(
183 &[
184 SingularArgSyntax::RequiredValue(
185 RequiredValueSyntax {
186 name: Cow::Borrowed("src"),
187 vtype: ExprType::Text,
188 },
189 ArgSepSyntax::Exactly(ArgSep::Long),
190 ),
191 SingularArgSyntax::RequiredValue(
192 RequiredValueSyntax {
193 name: Cow::Borrowed("dest"),
194 vtype: ExprType::Text,
195 },
196 ArgSepSyntax::End,
197 ),
198 ],
199 None,
200 )])
201 .with_category(CATEGORY)
202 .with_description(
203 "Copies src to dest.
204If dest is a path without a name, the target file given in dest will have the same name \
205as the source file in src.
206See the \"File system\" help topic for information on the path syntax.",
207 )
208 .build(),
209 storage,
210 })
211 }
212}
213
214#[async_trait(?Send)]
215impl Callable for CopyCommand {
216 fn metadata(&self) -> Rc<CallableMetadata> {
217 self.metadata.clone()
218 }
219
220 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
221 debug_assert_eq!(2, scope.nargs());
222 let src = scope.get_string(0).to_owned();
223 let dest = scope.get_string(1).to_owned();
224
225 let mut storage = self.storage.borrow_mut();
226 storage.copy(&src, &dest).await?;
227
228 Ok(())
229 }
230}
231
232pub struct DirCommand {
234 metadata: Rc<CallableMetadata>,
235 console: Rc<RefCell<dyn Console>>,
236 storage: Rc<RefCell<Storage>>,
237}
238
239impl DirCommand {
240 pub fn new(console: Rc<RefCell<dyn Console>>, storage: Rc<RefCell<Storage>>) -> Rc<Self> {
242 Rc::from(Self {
243 metadata: CallableMetadataBuilder::new("DIR")
244 .with_async(true)
245 .with_syntax(&[
246 (&[], None),
247 (
248 &[SingularArgSyntax::RequiredValue(
249 RequiredValueSyntax {
250 name: Cow::Borrowed("path"),
251 vtype: ExprType::Text,
252 },
253 ArgSepSyntax::End,
254 )],
255 None,
256 ),
257 ])
258 .with_category(CATEGORY)
259 .with_description("Displays the list of files on the current or given path.")
260 .build(),
261 console,
262 storage,
263 })
264 }
265}
266
267#[async_trait(?Send)]
268impl Callable for DirCommand {
269 fn metadata(&self) -> Rc<CallableMetadata> {
270 self.metadata.clone()
271 }
272
273 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
274 let path = if scope.nargs() == 0 {
275 ""
276 } else {
277 debug_assert_eq!(1, scope.nargs());
278 scope.get_string(0)
279 };
280
281 show_dir(&self.storage.borrow(), &mut *self.console.borrow_mut(), path).await?;
282
283 Ok(())
284 }
285}
286
287pub struct KillCommand {
289 metadata: Rc<CallableMetadata>,
290 storage: Rc<RefCell<Storage>>,
291}
292
293impl KillCommand {
294 pub fn new(storage: Rc<RefCell<Storage>>) -> Rc<Self> {
296 Rc::from(Self {
297 metadata: CallableMetadataBuilder::new("KILL")
298 .with_async(true)
299 .with_syntax(&[(
300 &[SingularArgSyntax::RequiredValue(
301 RequiredValueSyntax {
302 name: Cow::Borrowed("filename"),
303 vtype: ExprType::Text,
304 },
305 ArgSepSyntax::End,
306 )],
307 None,
308 )])
309 .with_category(CATEGORY)
310 .with_description(
311 "Deletes the given file.
312The filename must be a string and must be a valid EndBASIC path.
313See the \"File system\" help topic for information on the path syntax.",
314 )
315 .build(),
316 storage,
317 })
318 }
319}
320
321#[async_trait(?Send)]
322impl Callable for KillCommand {
323 fn metadata(&self) -> Rc<CallableMetadata> {
324 self.metadata.clone()
325 }
326
327 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
328 debug_assert_eq!(1, scope.nargs());
329 let name = scope.get_string(0).to_owned();
330
331 self.storage.borrow_mut().delete(&name).await?;
332
333 Ok(())
334 }
335}
336
337pub struct MountCommand {
339 metadata: Rc<CallableMetadata>,
340 console: Rc<RefCell<dyn Console>>,
341 storage: Rc<RefCell<Storage>>,
342}
343
344impl MountCommand {
345 pub fn new(console: Rc<RefCell<dyn Console>>, storage: Rc<RefCell<Storage>>) -> Rc<Self> {
347 Rc::from(Self {
348 metadata: CallableMetadataBuilder::new("MOUNT")
349 .with_syntax(&[
350 (&[], None),
351 (
352 &[
353 SingularArgSyntax::RequiredValue(
354 RequiredValueSyntax {
355 name: Cow::Borrowed("target"),
356 vtype: ExprType::Text,
357 },
358 ArgSepSyntax::Exactly(ArgSep::As),
359 ),
360 SingularArgSyntax::RequiredValue(
361 RequiredValueSyntax {
362 name: Cow::Borrowed("drive_name"),
363 vtype: ExprType::Text,
364 },
365 ArgSepSyntax::End,
366 ),
367 ],
368 None,
369 ),
370 ])
371 .with_category(CATEGORY)
372 .with_description(
373 "Lists the mounted drives or mounts a new drive.
374With no arguments, prints a list of mounted drives and their targets.
375With two arguments, mounts the drive_name$ to point to the target$. Drive names are specified \
376without a colon at the end, and targets are given in the form of a URI.",
377 )
378 .build(),
379 console,
380 storage,
381 })
382 }
383}
384
385#[async_trait(?Send)]
386impl Callable for MountCommand {
387 fn metadata(&self) -> Rc<CallableMetadata> {
388 self.metadata.clone()
389 }
390
391 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
392 if scope.nargs() == 0 {
393 show_drives(&self.storage.borrow(), &mut *self.console.borrow_mut())?;
394 Ok(())
395 } else {
396 debug_assert_eq!(2, scope.nargs());
397 let target = scope.get_string(0).to_owned();
398 let name = scope.get_string(1).to_owned();
399
400 self.storage.borrow_mut().mount(&name, &target)?;
401 Ok(())
402 }
403 }
404}
405
406pub struct PwdCommand {
408 metadata: Rc<CallableMetadata>,
409 console: Rc<RefCell<dyn Console>>,
410 storage: Rc<RefCell<Storage>>,
411}
412
413impl PwdCommand {
414 pub fn new(console: Rc<RefCell<dyn Console>>, storage: Rc<RefCell<Storage>>) -> Rc<Self> {
416 Rc::from(Self {
417 metadata: CallableMetadataBuilder::new("PWD")
418 .with_syntax(&[(&[], None)])
419 .with_category(CATEGORY)
420 .with_description(
421 "Prints the current working location.
422If the EndBASIC path representing the current location is backed by a real path that is accessible \
423by the underlying operating system, displays such path as well.",
424 )
425 .build(),
426 console,
427 storage,
428 })
429 }
430}
431
432#[async_trait(?Send)]
433impl Callable for PwdCommand {
434 fn metadata(&self) -> Rc<CallableMetadata> {
435 self.metadata.clone()
436 }
437
438 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
439 debug_assert_eq!(0, scope.nargs());
440
441 let storage = self.storage.borrow();
442 let cwd = storage.cwd();
443 let system_cwd = storage.system_path(&cwd).expect("cwd must return a valid path");
444
445 let console = &mut *self.console.borrow_mut();
446 console.print("")?;
447 console.print(&format!(" Working directory: {}", cwd))?;
448 match system_cwd {
449 Some(path) => console.print(&format!(" System location: {}", path.display()))?,
450 None => console.print(" No system location available")?,
451 }
452 console.print("")?;
453
454 Ok(())
455 }
456}
457
458pub struct UnmountCommand {
460 metadata: Rc<CallableMetadata>,
461 storage: Rc<RefCell<Storage>>,
462}
463
464impl UnmountCommand {
465 pub fn new(storage: Rc<RefCell<Storage>>) -> Rc<Self> {
467 Rc::from(Self {
468 metadata: CallableMetadataBuilder::new("UNMOUNT")
469 .with_syntax(&[(
470 &[SingularArgSyntax::RequiredValue(
471 RequiredValueSyntax {
472 name: Cow::Borrowed("drive_name"),
473 vtype: ExprType::Text,
474 },
475 ArgSepSyntax::End,
476 )],
477 None,
478 )])
479 .with_category(CATEGORY)
480 .with_description(
481 "Unmounts the given drive.
482Drive names are specified without a colon at the end.",
483 )
484 .build(),
485 storage,
486 })
487 }
488}
489
490#[async_trait(?Send)]
491impl Callable for UnmountCommand {
492 fn metadata(&self) -> Rc<CallableMetadata> {
493 self.metadata.clone()
494 }
495
496 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
497 debug_assert_eq!(1, scope.nargs());
498 let drive = scope.get_string(0).to_owned();
499
500 self.storage.borrow_mut().unmount(&drive)?;
501
502 Ok(())
503 }
504}
505
506pub fn add_all(
509 machine: &mut MachineBuilder,
510 console: Rc<RefCell<dyn Console>>,
511 storage: Rc<RefCell<Storage>>,
512) {
513 machine.add_callable(CdCommand::new(storage.clone()));
514 machine.add_callable(CopyCommand::new(storage.clone()));
515 machine.add_callable(DirCommand::new(console.clone(), storage.clone()));
516 machine.add_callable(KillCommand::new(storage.clone()));
517 machine.add_callable(MountCommand::new(console.clone(), storage.clone()));
518 machine.add_callable(PwdCommand::new(console.clone(), storage.clone()));
519 machine.add_callable(UnmountCommand::new(storage));
520}
521
522#[cfg(test)]
523mod tests {
524 use crate::console::{CharsXY, Key};
525 use crate::storage::{DirectoryDriveFactory, DiskSpace, Drive, InMemoryDrive};
526 use crate::testutils::*;
527 use futures_lite::future::block_on;
528 use std::collections::BTreeMap;
529
530 #[test]
531 fn test_cd_ok() {
532 let mut t = Tester::default();
533 t.get_storage().borrow_mut().mount("other", "memory://").unwrap();
534 t.run("CD \"other:\"").check();
535 assert_eq!("OTHER:/", t.get_storage().borrow().cwd());
536 t.run("CD \"memory:/\"").check();
537 assert_eq!("MEMORY:/", t.get_storage().borrow().cwd());
538 }
539
540 #[test]
541 fn test_cd_errors() {
542 check_stmt_err("1:1: Drive 'A' is not mounted", "CD \"A:\"");
543 check_stmt_compilation_err("1:1: CD expected path$", "CD");
544 check_stmt_compilation_err("1:1: CD expected path$", "CD 2, 3");
545 check_stmt_compilation_err("1:4: Expected STRING but found INTEGER", "CD 2");
546 }
547
548 #[test]
549 fn test_copy_ok() {
550 Tester::default()
551 .set_program(Some("foo.bas"), "Leave me alone")
552 .write_file("file1", "the content")
553 .run(r#"COPY "file1", "file2""#)
554 .expect_program(Some("foo.bas"), "Leave me alone")
555 .expect_file("MEMORY:/file1", "the content")
556 .expect_file("MEMORY:/file2", "the content")
557 .check();
558 }
559
560 #[test]
561 fn test_copy_deduce_target_name() {
562 let t = Tester::default();
563 t.get_storage().borrow_mut().mount("other", "memory://").unwrap();
564 t.set_program(Some("foo.bas"), "Leave me alone")
565 .write_file("file1.x", "the content")
566 .run(r#"COPY "file1.x", "OTHER:/""#)
567 .expect_program(Some("foo.bas"), "Leave me alone")
568 .expect_file("MEMORY:/file1.x", "the content")
569 .expect_file("OTHER:/file1.x", "the content")
570 .check();
571 }
572
573 #[test]
574 fn test_copy_errors() {
575 Tester::default()
576 .run(r#"COPY "foo""#)
577 .expect_compilation_err("1:1: COPY expected src$, dest$")
578 .check();
579
580 Tester::default()
581 .run(r#"COPY "memory:/", "foo.bar""#)
582 .expect_err("1:1: Missing file name in copy source path 'memory:/'")
583 .check();
584
585 Tester::default()
586 .run(r#"COPY "missing.txt", "new.txt""#)
587 .expect_err("1:1: Entry not found")
588 .check();
589
590 Tester::default()
591 .write_file("foo", "irrelevant")
592 .run(r#"COPY "foo", "missing:/""#)
593 .expect_err("1:1: Drive 'MISSING' is not mounted")
594 .expect_file("MEMORY:/foo", "irrelevant")
595 .check();
596
597 }
621
622 #[test]
623 fn test_dir_current_empty() {
624 Tester::default()
625 .run("DIR")
626 .expect_prints([
627 "",
628 " Directory of MEMORY:/",
629 "",
630 " Modified Size Name",
631 " 0 file(s), 0 bytes",
632 "",
633 ])
634 .check();
635 }
636
637 #[test]
638 fn test_dir_with_disk_free() {
639 let mut other = InMemoryDrive::default();
640 other.fake_disk_quota = Some(DiskSpace::new(456, 0));
641 other.fake_disk_free = Some(DiskSpace::new(123, 0));
642
643 let mut t = Tester::default();
644 t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap();
645
646 t.run("DIR \"OTHER:/\"")
647 .expect_prints([
648 "",
649 " Directory of OTHER:/",
650 "",
651 " Modified Size Name",
652 " 0 file(s), 0 bytes",
653 " 123 of 456 bytes free",
654 "",
655 ])
656 .check();
657 }
658
659 #[test]
660 fn test_dir_current_entries_are_sorted() {
661 Tester::default()
662 .write_file("empty.bas", "")
663 .write_file("some other file.bas", "not empty\n")
664 .write_file("00AAA.BAS", "first\nfile\n")
665 .write_file("not a bas.txt", "")
666 .run("DIR")
667 .expect_prints([
668 "",
669 " Directory of MEMORY:/",
670 "",
671 " Modified Size Name",
672 " 2020-05-06 09:37 11 00AAA.BAS",
673 " 2020-05-06 09:37 0 empty.bas",
674 " 2020-05-06 09:37 0 not a bas.txt",
675 " 2020-05-06 09:37 10 some other file.bas",
676 "",
677 " 4 file(s), 21 bytes",
678 "",
679 ])
680 .expect_file("MEMORY:/empty.bas", "")
681 .expect_file("MEMORY:/some other file.bas", "not empty\n")
682 .expect_file("MEMORY:/00AAA.BAS", "first\nfile\n")
683 .expect_file("MEMORY:/not a bas.txt", "")
684 .check();
685 }
686
687 #[test]
688 fn test_dir_other_by_argument() {
689 let mut other = InMemoryDrive::default();
690 block_on(other.put("foo.bas", b"hello")).unwrap();
691
692 let mut t = Tester::default().write_file("empty.bas", "");
693 t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap();
694
695 let mut prints = vec![
696 "",
697 " Directory of MEMORY:/",
698 "",
699 " Modified Size Name",
700 " 2020-05-06 09:37 0 empty.bas",
701 "",
702 " 1 file(s), 0 bytes",
703 "",
704 ];
705 t.run("DIR \"memory:\"")
706 .expect_prints(prints.clone())
707 .expect_file("MEMORY:/empty.bas", "")
708 .expect_file("OTHER:/foo.bas", "hello")
709 .check();
710
711 prints.extend([
712 "",
713 " Directory of OTHER:/",
714 "",
715 " Modified Size Name",
716 " 2020-05-06 09:37 5 foo.bas",
717 "",
718 " 1 file(s), 5 bytes",
719 "",
720 ]);
721 t.run("DIR \"other:/\"")
722 .expect_prints(prints)
723 .expect_file("MEMORY:/empty.bas", "")
724 .expect_file("OTHER:/foo.bas", "hello")
725 .check();
726 }
727
728 #[test]
729 fn test_dir_other_by_cwd() {
730 let mut other = InMemoryDrive::default();
731 block_on(other.put("foo.bas", b"hello")).unwrap();
732
733 let mut t = Tester::default().write_file("empty.bas", "");
734 t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap();
735
736 let mut prints = vec![
737 "",
738 " Directory of MEMORY:/",
739 "",
740 " Modified Size Name",
741 " 2020-05-06 09:37 0 empty.bas",
742 "",
743 " 1 file(s), 0 bytes",
744 "",
745 ];
746 t.run("DIR")
747 .expect_prints(prints.clone())
748 .expect_file("MEMORY:/empty.bas", "")
749 .expect_file("OTHER:/foo.bas", "hello")
750 .check();
751
752 t.get_storage().borrow_mut().cd("other:/").unwrap();
753 prints.extend([
754 "",
755 " Directory of OTHER:/",
756 "",
757 " Modified Size Name",
758 " 2020-05-06 09:37 5 foo.bas",
759 "",
760 " 1 file(s), 5 bytes",
761 "",
762 ]);
763 t.run("DIR")
764 .expect_prints(prints)
765 .expect_file("MEMORY:/empty.bas", "")
766 .expect_file("OTHER:/foo.bas", "hello")
767 .check();
768 }
769
770 #[test]
771 fn test_dir_narrow_empty() {
772 let mut t = Tester::default();
773 t.get_console().borrow_mut().set_size_chars(CharsXY::new(10, 1));
774 t.run("DIR")
775 .expect_prints(["", " Directory of MEMORY:/", "", " 0 file(s)", ""])
776 .check();
777 }
778
779 #[test]
780 fn test_dir_narrow_some() {
781 let mut t = Tester::default().write_file("empty.bas", "");
782 t.get_console().borrow_mut().set_size_chars(CharsXY::new(10, 1));
783 t.run("DIR")
784 .expect_prints([
785 "",
786 " Directory of MEMORY:/",
787 "",
788 " empty.bas",
789 "",
790 " 1 file(s)",
791 "",
792 ])
793 .expect_file("MEMORY:/empty.bas", "")
794 .check();
795 }
796
797 #[test]
798 fn test_dir_paging() {
799 let t = Tester::default();
800 t.get_console().borrow_mut().set_interactive(true);
801 t.get_console().borrow_mut().set_size_chars(CharsXY { x: 80, y: 7 });
802 t.get_console().borrow_mut().add_input_keys(&[Key::NewLine]);
803 t.write_file("0.bas", "")
804 .write_file("1.bas", "")
805 .write_file("2.bas", "")
806 .write_file("3.bas", "")
807 .run("DIR")
808 .expect_prints([
809 "",
810 " Directory of MEMORY:/",
811 "",
812 " Modified Size Name",
813 " 2020-05-06 09:37 0 0.bas",
814 " 2020-05-06 09:37 0 1.bas",
815 " << Press any key for more; ESC or Ctrl+C to stop >> ",
816 " 2020-05-06 09:37 0 2.bas",
817 " 2020-05-06 09:37 0 3.bas",
818 "",
819 " 4 file(s), 0 bytes",
820 "",
821 ])
822 .expect_file("MEMORY:/0.bas", "")
823 .expect_file("MEMORY:/1.bas", "")
824 .expect_file("MEMORY:/2.bas", "")
825 .expect_file("MEMORY:/3.bas", "")
826 .check();
827 }
828
829 #[test]
830 fn test_dir_errors() {
831 check_stmt_compilation_err("1:1: DIR expected <> | <path$>", "DIR 2, 3");
832 check_stmt_compilation_err("1:5: Expected STRING but found INTEGER", "DIR 2");
833 }
834
835 #[test]
836 fn test_kill_ok() {
837 for p in &["foo", "foo.bas"] {
838 Tester::default()
839 .set_program(Some(p), "Leave me alone")
840 .write_file("leave-me-alone.bas", "")
841 .write_file(p, "line 1\n line 2\n")
842 .run(format!(r#"KILL "{}""#, p))
843 .expect_program(Some(*p), "Leave me alone")
844 .expect_file("MEMORY:/leave-me-alone.bas", "")
845 .check();
846 }
847 }
848
849 #[test]
850 fn test_kill_errors() {
851 Tester::default()
852 .run("KILL 3")
853 .expect_compilation_err("1:6: Expected STRING but found INTEGER")
854 .check();
855
856 Tester::default()
857 .run(r#"KILL "a/b.bas""#)
858 .expect_err("1:1: Too many / separators in path 'a/b.bas'")
859 .check();
860
861 Tester::default()
862 .run(r#"KILL "drive:""#)
863 .expect_err("1:1: Missing file name in path 'drive:'")
864 .check();
865
866 Tester::default()
867 .run("KILL")
868 .expect_compilation_err("1:1: KILL expected filename$")
869 .check();
870
871 check_stmt_err("1:1: Entry not found", r#"KILL "missing-file""#);
872
873 Tester::default()
874 .write_file("no-automatic-extension.bas", "")
875 .run(r#"KILL "no-automatic-extension""#)
876 .expect_err("1:1: Entry not found")
877 .expect_file("MEMORY:/no-automatic-extension.bas", "")
878 .check();
879 }
880
881 #[test]
882 fn test_mount_list() {
883 let mut t = Tester::default();
884 let other = InMemoryDrive::default();
885 t.get_storage().borrow_mut().attach("o", "origin://", Box::from(other)).unwrap();
886
887 let mut prints = vec![
888 "",
889 " Name Target",
890 " MEMORY memory://",
891 " O origin://",
892 "",
893 " 2 drive(s)",
894 "",
895 ];
896 t.run("MOUNT").expect_prints(prints.clone()).check();
897
898 t.get_storage().borrow_mut().cd("o:").unwrap();
899 t.get_storage().borrow_mut().unmount("memory").unwrap();
900 prints.extend([
901 "",
902 " Name Target",
903 " O origin://",
904 "",
905 " 1 drive(s)",
906 "",
907 ]);
908 t.run("MOUNT").expect_prints(prints.clone()).check();
909 }
910
911 #[test]
912 fn test_mount_mount() {
913 let mut t = Tester::default();
914 t.run(r#"MOUNT "memory://" AS "abc""#).check();
915
916 let mut exp_info = BTreeMap::default();
917 exp_info.insert("MEMORY", "memory://");
918 exp_info.insert("ABC", "memory://");
919 assert_eq!(exp_info, t.get_storage().borrow().mounted());
920 }
921
922 #[test]
923 fn test_mount_errors() {
924 check_stmt_compilation_err("1:1: MOUNT expected <> | <target$ AS drive_name$>", "MOUNT 1");
925 check_stmt_compilation_err(
926 "1:1: MOUNT expected <> | <target$ AS drive_name$>",
927 "MOUNT 1, 2, 3",
928 );
929
930 check_stmt_compilation_err("1:14: Expected STRING but found INTEGER", r#"MOUNT "a" AS 1"#);
931 check_stmt_compilation_err("1:7: Expected STRING but found INTEGER", r#"MOUNT 1 AS "a""#);
932
933 check_stmt_err("1:1: Invalid drive name 'a:'", r#"MOUNT "memory://" AS "a:""#);
934 check_stmt_err(
935 "1:1: Mount URI must be of the form scheme://path",
936 r#"MOUNT "foo//bar" AS "a""#,
937 );
938 check_stmt_err("1:1: Unknown mount scheme 'foo'", r#"MOUNT "foo://bar" AS "a""#);
939 }
940
941 #[test]
942 fn test_pwd_without_system_path() {
943 let mut t = Tester::default();
944
945 t.run("PWD")
946 .expect_prints([
947 "",
948 " Working directory: MEMORY:/",
949 " No system location available",
950 "",
951 ])
952 .check();
953 }
954
955 #[test]
956 fn test_pwd_with_system_path() {
957 let dir = tempfile::tempdir().unwrap();
958 let dir = dir.path().canonicalize().unwrap();
959
960 let mut t = Tester::default();
961 {
962 let storage = t.get_storage();
963 let storage = &mut *storage.borrow_mut();
964 storage.register_scheme("file", Box::from(DirectoryDriveFactory::default()));
965 storage.mount("other", &format!("file://{}", dir.display())).unwrap();
966 storage.cd("other:/").unwrap();
967 }
968
969 t.run("PWD")
970 .expect_prints([
971 "",
972 " Working directory: OTHER:/",
973 &format!(" System location: {}", dir.join("").display()),
974 "",
975 ])
976 .check();
977 }
978
979 #[test]
980 fn test_unmount_ok() {
981 let mut t = Tester::default();
982 t.get_storage().borrow_mut().mount("other", "memory://").unwrap();
983 t.get_storage().borrow_mut().cd("other:").unwrap();
984 t.run("UNMOUNT \"memory\"").check();
985
986 let mut exp_info = BTreeMap::default();
987 exp_info.insert("OTHER", "memory://");
988 assert_eq!(exp_info, t.get_storage().borrow().mounted());
989 }
990
991 #[test]
992 fn test_unmount_errors() {
993 check_stmt_compilation_err("1:1: UNMOUNT expected drive_name$", "UNMOUNT");
994 check_stmt_compilation_err("1:1: UNMOUNT expected drive_name$", "UNMOUNT 2, 3");
995
996 check_stmt_compilation_err("1:9: Expected STRING but found INTEGER", "UNMOUNT 1");
997
998 check_stmt_err("1:1: Invalid drive name 'a:'", "UNMOUNT \"a:\"");
999 check_stmt_err("1:1: Drive 'a' is not mounted", "UNMOUNT \"a\"");
1000 }
1001}