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