endbasic_std/storage/
cmds.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! File system interaction.
17
18use 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
34/// Category description for all symbols provided by this module.
35const 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
54/// Shows the contents of the given storage location.
55async 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
107/// Shows the mounted drives.
108fn 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
126/// The `CD` command.
127pub struct CdCommand {
128    metadata: CallableMetadata,
129    storage: Rc<RefCell<Storage>>,
130}
131
132impl CdCommand {
133    /// Creates a new `CD` command that changes the current location in `storage`.
134    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
168/// The `DIR` command.
169pub struct DirCommand {
170    metadata: CallableMetadata,
171    console: Rc<RefCell<dyn Console>>,
172    storage: Rc<RefCell<Storage>>,
173}
174
175impl DirCommand {
176    /// Creates a new `DIR` command that lists `storage` contents on the `console`.
177    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
222/// The `MOUNT` command.
223pub struct MountCommand {
224    metadata: CallableMetadata,
225    console: Rc<RefCell<dyn Console>>,
226    storage: Rc<RefCell<Storage>>,
227}
228
229impl MountCommand {
230    /// Creates a new `MOUNT` command.
231    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
291/// The `PWD` command.
292pub struct PwdCommand {
293    metadata: CallableMetadata,
294    console: Rc<RefCell<dyn Console>>,
295    storage: Rc<RefCell<Storage>>,
296}
297
298impl PwdCommand {
299    /// Creates a new `PWD` command that prints the current directory of `storage` to the `console`.
300    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
343/// The `UNMOUNT` command.
344pub struct UnmountCommand {
345    metadata: CallableMetadata,
346    storage: Rc<RefCell<Storage>>,
347}
348
349impl UnmountCommand {
350    /// Creates a new `UNMOUNT` command.
351    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
391/// Adds all file system manipulation commands for `storage` to the `machine`, using `console` to
392/// display information.
393pub 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}