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