1use super::time_format_error_to_io_error;
19use crate::console::{is_narrow, Console, Pager};
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, Scope};
25use endbasic_core::syms::{CallResult, 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) -> CallResult {
159 debug_assert_eq!(1, scope.nargs());
160 let target = scope.pop_string();
161
162 self.storage.borrow_mut().cd(&target)?;
163
164 Ok(())
165 }
166}
167
168pub struct DirCommand {
170 metadata: CallableMetadata,
171 console: Rc<RefCell<dyn Console>>,
172 storage: Rc<RefCell<Storage>>,
173}
174
175impl DirCommand {
176 pub fn new(console: Rc<RefCell<dyn Console>>, storage: Rc<RefCell<Storage>>) -> Rc<Self> {
178 Rc::from(Self {
179 metadata: CallableMetadataBuilder::new("DIR")
180 .with_syntax(&[
181 (&[], None),
182 (
183 &[SingularArgSyntax::RequiredValue(
184 RequiredValueSyntax {
185 name: Cow::Borrowed("path"),
186 vtype: ExprType::Text,
187 },
188 ArgSepSyntax::End,
189 )],
190 None,
191 ),
192 ])
193 .with_category(CATEGORY)
194 .with_description("Displays the list of files on the current or given path.")
195 .build(),
196 console,
197 storage,
198 })
199 }
200}
201
202#[async_trait(?Send)]
203impl Callable for DirCommand {
204 fn metadata(&self) -> &CallableMetadata {
205 &self.metadata
206 }
207
208 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
209 let path = if scope.nargs() == 0 {
210 "".to_owned()
211 } else {
212 debug_assert_eq!(1, scope.nargs());
213 scope.pop_string()
214 };
215
216 show_dir(&self.storage.borrow(), &mut *self.console.borrow_mut(), &path).await?;
217
218 Ok(())
219 }
220}
221
222pub struct MountCommand {
224 metadata: CallableMetadata,
225 console: Rc<RefCell<dyn Console>>,
226 storage: Rc<RefCell<Storage>>,
227}
228
229impl MountCommand {
230 pub fn new(console: Rc<RefCell<dyn Console>>, storage: Rc<RefCell<Storage>>) -> Rc<Self> {
232 Rc::from(Self {
233 metadata: CallableMetadataBuilder::new("MOUNT")
234 .with_syntax(&[
235 (&[], None),
236 (
237 &[
238 SingularArgSyntax::RequiredValue(
239 RequiredValueSyntax {
240 name: Cow::Borrowed("target"),
241 vtype: ExprType::Text,
242 },
243 ArgSepSyntax::Exactly(ArgSep::As),
244 ),
245 SingularArgSyntax::RequiredValue(
246 RequiredValueSyntax {
247 name: Cow::Borrowed("drive_name"),
248 vtype: ExprType::Text,
249 },
250 ArgSepSyntax::End,
251 ),
252 ],
253 None,
254 ),
255 ])
256 .with_category(CATEGORY)
257 .with_description(
258 "Lists the mounted drives or mounts a new drive.
259With no arguments, prints a list of mounted drives and their targets.
260With two arguments, mounts the drive_name$ to point to the target$. Drive names are specified \
261without a colon at the end, and targets are given in the form of a URI.",
262 )
263 .build(),
264 console,
265 storage,
266 })
267 }
268}
269
270#[async_trait(?Send)]
271impl Callable for MountCommand {
272 fn metadata(&self) -> &CallableMetadata {
273 &self.metadata
274 }
275
276 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
277 if scope.nargs() == 0 {
278 show_drives(&self.storage.borrow_mut(), &mut *self.console.borrow_mut())?;
279 Ok(())
280 } else {
281 debug_assert_eq!(2, scope.nargs());
282 let target = scope.pop_string();
283 let name = scope.pop_string();
284
285 self.storage.borrow_mut().mount(&name, &target)?;
286 Ok(())
287 }
288 }
289}
290
291pub struct PwdCommand {
293 metadata: CallableMetadata,
294 console: Rc<RefCell<dyn Console>>,
295 storage: Rc<RefCell<Storage>>,
296}
297
298impl PwdCommand {
299 pub fn new(console: Rc<RefCell<dyn Console>>, storage: Rc<RefCell<Storage>>) -> Rc<Self> {
301 Rc::from(Self {
302 metadata: CallableMetadataBuilder::new("PWD")
303 .with_syntax(&[(&[], None)])
304 .with_category(CATEGORY)
305 .with_description(
306 "Prints the current working location.
307If the EndBASIC path representing the current location is backed by a real path that is accessible \
308by the underlying operating system, displays such path as well.",
309 )
310 .build(),
311 console,
312 storage,
313 })
314 }
315}
316
317#[async_trait(?Send)]
318impl Callable for PwdCommand {
319 fn metadata(&self) -> &CallableMetadata {
320 &self.metadata
321 }
322
323 async fn exec(&self, scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
324 debug_assert_eq!(0, scope.nargs());
325
326 let storage = self.storage.borrow();
327 let cwd = storage.cwd();
328 let system_cwd = storage.system_path(&cwd).expect("cwd must return a valid path");
329
330 let console = &mut *self.console.borrow_mut();
331 console.print("")?;
332 console.print(&format!(" Working directory: {}", cwd))?;
333 match system_cwd {
334 Some(path) => console.print(&format!(" System location: {}", path.display()))?,
335 None => console.print(" No system location available")?,
336 }
337 console.print("")?;
338
339 Ok(())
340 }
341}
342
343pub struct UnmountCommand {
345 metadata: CallableMetadata,
346 storage: Rc<RefCell<Storage>>,
347}
348
349impl UnmountCommand {
350 pub fn new(storage: Rc<RefCell<Storage>>) -> Rc<Self> {
352 Rc::from(Self {
353 metadata: CallableMetadataBuilder::new("UNMOUNT")
354 .with_syntax(&[(
355 &[SingularArgSyntax::RequiredValue(
356 RequiredValueSyntax {
357 name: Cow::Borrowed("drive_name"),
358 vtype: ExprType::Text,
359 },
360 ArgSepSyntax::End,
361 )],
362 None,
363 )])
364 .with_category(CATEGORY)
365 .with_description(
366 "Unmounts the given drive.
367Drive names are specified without a colon at the end.",
368 )
369 .build(),
370 storage,
371 })
372 }
373}
374
375#[async_trait(?Send)]
376impl Callable for UnmountCommand {
377 fn metadata(&self) -> &CallableMetadata {
378 &self.metadata
379 }
380
381 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> CallResult {
382 debug_assert_eq!(1, scope.nargs());
383 let drive = scope.pop_string();
384
385 self.storage.borrow_mut().unmount(&drive)?;
386
387 Ok(())
388 }
389}
390
391pub fn add_all(
394 machine: &mut Machine,
395 console: Rc<RefCell<dyn Console>>,
396 storage: Rc<RefCell<Storage>>,
397) {
398 machine.add_callable(CdCommand::new(storage.clone()));
399 machine.add_callable(DirCommand::new(console.clone(), storage.clone()));
400 machine.add_callable(MountCommand::new(console.clone(), storage.clone()));
401 machine.add_callable(PwdCommand::new(console.clone(), storage.clone()));
402 machine.add_callable(UnmountCommand::new(storage));
403}
404
405#[cfg(test)]
406mod tests {
407 use crate::console::{CharsXY, Key};
408 use crate::storage::{DirectoryDriveFactory, DiskSpace, Drive, InMemoryDrive};
409 use crate::testutils::*;
410 use futures_lite::future::block_on;
411 use std::collections::BTreeMap;
412
413 #[test]
414 fn test_cd_ok() {
415 let mut t = Tester::default();
416 t.get_storage().borrow_mut().mount("other", "memory://").unwrap();
417 t.run("CD \"other:\"").check();
418 assert_eq!("OTHER:/", t.get_storage().borrow().cwd());
419 t.run("CD \"memory:/\"").check();
420 assert_eq!("MEMORY:/", t.get_storage().borrow().cwd());
421 }
422
423 #[test]
424 fn test_cd_errors() {
425 check_stmt_err("1:1: In call to CD: Drive 'A' is not mounted", "CD \"A:\"");
426 check_stmt_compilation_err("1:1: In call to CD: expected path$", "CD");
427 check_stmt_compilation_err("1:1: In call to CD: expected path$", "CD 2, 3");
428 check_stmt_compilation_err("1:1: In call to CD: 1:4: INTEGER is not a STRING", "CD 2");
429 }
430
431 #[test]
432 fn test_dir_current_empty() {
433 Tester::default()
434 .run("DIR")
435 .expect_prints([
436 "",
437 " Directory of MEMORY:/",
438 "",
439 " Modified Size Name",
440 " 0 file(s), 0 bytes",
441 "",
442 ])
443 .check();
444 }
445
446 #[test]
447 fn test_dir_with_disk_free() {
448 let mut other = InMemoryDrive::default();
449 other.fake_disk_quota = Some(DiskSpace::new(456, 0));
450 other.fake_disk_free = Some(DiskSpace::new(123, 0));
451
452 let mut t = Tester::default();
453 t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap();
454
455 t.run("DIR \"OTHER:/\"")
456 .expect_prints([
457 "",
458 " Directory of OTHER:/",
459 "",
460 " Modified Size Name",
461 " 0 file(s), 0 bytes",
462 " 123 of 456 bytes free",
463 "",
464 ])
465 .check();
466 }
467
468 #[test]
469 fn test_dir_current_entries_are_sorted() {
470 Tester::default()
471 .write_file("empty.bas", "")
472 .write_file("some other file.bas", "not empty\n")
473 .write_file("00AAA.BAS", "first\nfile\n")
474 .write_file("not a bas.txt", "")
475 .run("DIR")
476 .expect_prints([
477 "",
478 " Directory of MEMORY:/",
479 "",
480 " Modified Size Name",
481 " 2020-05-06 09:37 11 00AAA.BAS",
482 " 2020-05-06 09:37 0 empty.bas",
483 " 2020-05-06 09:37 0 not a bas.txt",
484 " 2020-05-06 09:37 10 some other file.bas",
485 "",
486 " 4 file(s), 21 bytes",
487 "",
488 ])
489 .expect_file("MEMORY:/empty.bas", "")
490 .expect_file("MEMORY:/some other file.bas", "not empty\n")
491 .expect_file("MEMORY:/00AAA.BAS", "first\nfile\n")
492 .expect_file("MEMORY:/not a bas.txt", "")
493 .check();
494 }
495
496 #[test]
497 fn test_dir_other_by_argument() {
498 let mut other = InMemoryDrive::default();
499 block_on(other.put("foo.bas", "hello")).unwrap();
500
501 let mut t = Tester::default().write_file("empty.bas", "");
502 t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap();
503
504 let mut prints = vec![
505 "",
506 " Directory of MEMORY:/",
507 "",
508 " Modified Size Name",
509 " 2020-05-06 09:37 0 empty.bas",
510 "",
511 " 1 file(s), 0 bytes",
512 "",
513 ];
514 t.run("DIR \"memory:\"")
515 .expect_prints(prints.clone())
516 .expect_file("MEMORY:/empty.bas", "")
517 .expect_file("OTHER:/foo.bas", "hello")
518 .check();
519
520 prints.extend([
521 "",
522 " Directory of OTHER:/",
523 "",
524 " Modified Size Name",
525 " 2020-05-06 09:37 5 foo.bas",
526 "",
527 " 1 file(s), 5 bytes",
528 "",
529 ]);
530 t.run("DIR \"other:/\"")
531 .expect_prints(prints)
532 .expect_file("MEMORY:/empty.bas", "")
533 .expect_file("OTHER:/foo.bas", "hello")
534 .check();
535 }
536
537 #[test]
538 fn test_dir_other_by_cwd() {
539 let mut other = InMemoryDrive::default();
540 block_on(other.put("foo.bas", "hello")).unwrap();
541
542 let mut t = Tester::default().write_file("empty.bas", "");
543 t.get_storage().borrow_mut().attach("other", "z://", Box::from(other)).unwrap();
544
545 let mut prints = vec![
546 "",
547 " Directory of MEMORY:/",
548 "",
549 " Modified Size Name",
550 " 2020-05-06 09:37 0 empty.bas",
551 "",
552 " 1 file(s), 0 bytes",
553 "",
554 ];
555 t.run("DIR")
556 .expect_prints(prints.clone())
557 .expect_file("MEMORY:/empty.bas", "")
558 .expect_file("OTHER:/foo.bas", "hello")
559 .check();
560
561 t.get_storage().borrow_mut().cd("other:/").unwrap();
562 prints.extend([
563 "",
564 " Directory of OTHER:/",
565 "",
566 " Modified Size Name",
567 " 2020-05-06 09:37 5 foo.bas",
568 "",
569 " 1 file(s), 5 bytes",
570 "",
571 ]);
572 t.run("DIR")
573 .expect_prints(prints)
574 .expect_file("MEMORY:/empty.bas", "")
575 .expect_file("OTHER:/foo.bas", "hello")
576 .check();
577 }
578
579 #[test]
580 fn test_dir_narrow_empty() {
581 let mut t = Tester::default();
582 t.get_console().borrow_mut().set_size_chars(CharsXY::new(10, 1));
583 t.run("DIR")
584 .expect_prints(["", " Directory of MEMORY:/", "", " 0 file(s)", ""])
585 .check();
586 }
587
588 #[test]
589 fn test_dir_narrow_some() {
590 let mut t = Tester::default().write_file("empty.bas", "");
591 t.get_console().borrow_mut().set_size_chars(CharsXY::new(10, 1));
592 t.run("DIR")
593 .expect_prints([
594 "",
595 " Directory of MEMORY:/",
596 "",
597 " empty.bas",
598 "",
599 " 1 file(s)",
600 "",
601 ])
602 .expect_file("MEMORY:/empty.bas", "")
603 .check();
604 }
605
606 #[test]
607 fn test_dir_paging() {
608 let t = Tester::default();
609 t.get_console().borrow_mut().set_interactive(true);
610 t.get_console().borrow_mut().set_size_chars(CharsXY { x: 80, y: 7 });
611 t.get_console().borrow_mut().add_input_keys(&[Key::NewLine]);
612 t.write_file("0.bas", "")
613 .write_file("1.bas", "")
614 .write_file("2.bas", "")
615 .write_file("3.bas", "")
616 .run("DIR")
617 .expect_prints([
618 "",
619 " Directory of MEMORY:/",
620 "",
621 " Modified Size Name",
622 " 2020-05-06 09:37 0 0.bas",
623 " 2020-05-06 09:37 0 1.bas",
624 " << Press any key for more; ESC or Ctrl+C to stop >> ",
625 " 2020-05-06 09:37 0 2.bas",
626 " 2020-05-06 09:37 0 3.bas",
627 "",
628 " 4 file(s), 0 bytes",
629 "",
630 ])
631 .expect_file("MEMORY:/0.bas", "")
632 .expect_file("MEMORY:/1.bas", "")
633 .expect_file("MEMORY:/2.bas", "")
634 .expect_file("MEMORY:/3.bas", "")
635 .check();
636 }
637
638 #[test]
639 fn test_dir_errors() {
640 check_stmt_compilation_err("1:1: In call to DIR: expected <> | <path$>", "DIR 2, 3");
641 check_stmt_compilation_err("1:1: In call to DIR: 1:5: INTEGER is not a STRING", "DIR 2");
642 }
643
644 #[test]
645 fn test_mount_list() {
646 let mut t = Tester::default();
647 let other = InMemoryDrive::default();
648 t.get_storage().borrow_mut().attach("o", "origin://", Box::from(other)).unwrap();
649
650 let mut prints = vec![
651 "",
652 " Name Target",
653 " MEMORY memory://",
654 " O origin://",
655 "",
656 " 2 drive(s)",
657 "",
658 ];
659 t.run("MOUNT").expect_prints(prints.clone()).check();
660
661 t.get_storage().borrow_mut().cd("o:").unwrap();
662 t.get_storage().borrow_mut().unmount("memory").unwrap();
663 prints.extend([
664 "",
665 " Name Target",
666 " O origin://",
667 "",
668 " 1 drive(s)",
669 "",
670 ]);
671 t.run("MOUNT").expect_prints(prints.clone()).check();
672 }
673
674 #[test]
675 fn test_mount_mount() {
676 let mut t = Tester::default();
677 t.run(r#"MOUNT "memory://" AS "abc""#).check();
678
679 let mut exp_info = BTreeMap::default();
680 exp_info.insert("MEMORY", "memory://");
681 exp_info.insert("ABC", "memory://");
682 assert_eq!(exp_info, t.get_storage().borrow().mounted());
683 }
684
685 #[test]
686 fn test_mount_errors() {
687 check_stmt_compilation_err(
688 "1:1: In call to MOUNT: expected <> | <target$ AS drive_name$>",
689 "MOUNT 1",
690 );
691 check_stmt_compilation_err(
692 "1:1: In call to MOUNT: expected <> | <target$ AS drive_name$>",
693 "MOUNT 1, 2, 3",
694 );
695
696 check_stmt_compilation_err(
697 "1:1: In call to MOUNT: 1:14: INTEGER is not a STRING",
698 r#"MOUNT "a" AS 1"#,
699 );
700 check_stmt_compilation_err(
701 "1:1: In call to MOUNT: 1:7: INTEGER is not a STRING",
702 r#"MOUNT 1 AS "a""#,
703 );
704
705 check_stmt_err(
706 "1:1: In call to MOUNT: Invalid drive name 'a:'",
707 r#"MOUNT "memory://" AS "a:""#,
708 );
709 check_stmt_err(
710 "1:1: In call to MOUNT: Mount URI must be of the form scheme://path",
711 r#"MOUNT "foo//bar" AS "a""#,
712 );
713 check_stmt_err(
714 "1:1: In call to MOUNT: Unknown mount scheme 'foo'",
715 r#"MOUNT "foo://bar" AS "a""#,
716 );
717 }
718
719 #[test]
720 fn test_pwd_without_system_path() {
721 let mut t = Tester::default();
722
723 t.run("PWD")
724 .expect_prints([
725 "",
726 " Working directory: MEMORY:/",
727 " No system location available",
728 "",
729 ])
730 .check();
731 }
732
733 #[test]
734 fn test_pwd_with_system_path() {
735 let dir = tempfile::tempdir().unwrap();
736 let dir = dir.path().canonicalize().unwrap();
737
738 let mut t = Tester::default();
739 {
740 let storage = t.get_storage();
741 let storage = &mut *storage.borrow_mut();
742 storage.register_scheme("file", Box::from(DirectoryDriveFactory::default()));
743 storage.mount("other", &format!("file://{}", dir.display())).unwrap();
744 storage.cd("other:/").unwrap();
745 }
746
747 t.run("PWD")
748 .expect_prints([
749 "",
750 " Working directory: OTHER:/",
751 &format!(" System location: {}", dir.join("").display()),
752 "",
753 ])
754 .check();
755 }
756
757 #[test]
758 fn test_unmount_ok() {
759 let mut t = Tester::default();
760 t.get_storage().borrow_mut().mount("other", "memory://").unwrap();
761 t.get_storage().borrow_mut().cd("other:").unwrap();
762 t.run("UNMOUNT \"memory\"").check();
763
764 let mut exp_info = BTreeMap::default();
765 exp_info.insert("OTHER", "memory://");
766 assert_eq!(exp_info, t.get_storage().borrow().mounted());
767 }
768
769 #[test]
770 fn test_unmount_errors() {
771 check_stmt_compilation_err("1:1: In call to UNMOUNT: expected drive_name$", "UNMOUNT");
772 check_stmt_compilation_err("1:1: In call to UNMOUNT: expected drive_name$", "UNMOUNT 2, 3");
773
774 check_stmt_compilation_err(
775 "1:1: In call to UNMOUNT: 1:9: INTEGER is not a STRING",
776 "UNMOUNT 1",
777 );
778
779 check_stmt_err("1:1: In call to UNMOUNT: Invalid drive name 'a:'", "UNMOUNT \"a:\"");
780 check_stmt_err("1:1: In call to UNMOUNT: Drive 'a' is not mounted", "UNMOUNT \"a\"");
781 }
782}