//! The shell: its parser (user/shparse.rs, compiled right into this test) //! and a scripted session of the real /bin/sh with its output in a pipe. #![no_std] #![no_main] #![feature(custom_test_frameworks)] #![test_runner(rustbelt::test_runner)] #![reexport_test_harness_main = "test_main"] extern crate alloc; #[path = "../user/shparse.rs"] mod shparse; use alloc::{string::String, sync::Arc, vec, vec::Vec}; use bootloader::{BootInfo, entry_point}; use core::panic::PanicInfo; use rustbelt::fs::{self, InodeKind, file::File}; use rustbelt::memory; use rustbelt::process::{self, ExitStatus, SpawnArgs}; use shparse::{Token, Word, parse, tokenize}; entry_point!(main); fn main(boot_info: &'static BootInfo) -> ! { rustbelt::init(); rustbelt::init_memory(boot_info); rustbelt::init_smp(); // all 4 CPUs run threads fs::init(); rustbelt::init_storage(); test_main(); rustbelt::hlt_loop(); } fn w(text: &str) -> Token { Token::Word(Word { text: String::from(text), quoted: false }) } fn q(text: &str) -> Token { Token::Word(Word { text: String::from(text), quoted: true }) } // --- tokenizer ----------------------------------------------------------- #[test_case] fn tokenizer_splits_words_and_operators() { // Operators need no spaces around them. assert_eq!( tokenize("ls -l|wc >>out.txt > out.txt &").unwrap().unwrap(); assert!(p.background); assert_eq!(p.commands.len(), 3); let names: Vec<&str> = p.commands.iter().map(|c| c.words[0].text.as_str()).collect(); assert_eq!(names, ["cat", "grep", "wc"]); assert_eq!(p.commands[0].stdin.as_ref().unwrap().text, "in.txt"); let (out, append) = p.commands[2].stdout.as_ref().unwrap(); assert_eq!((out.text.as_str(), *append), ("out.txt", true)); assert_eq!(p.commands[1].words.len(), 2); let p = parse("echo hi > f").unwrap().unwrap(); assert!(!p.background); assert_eq!(p.commands[0].stdout.as_ref().unwrap().1, false); assert_eq!(parse("# just a comment").unwrap(), None); assert_eq!(parse("").unwrap(), None); } #[test_case] fn parser_rejects_broken_lines() { for bad in ["| wc", "ls |", "ls | | wc", "ls >", "cat <", "ls & wc", "&", "> out"] { assert!(parse(bad).is_err(), "should fail: {}", bad); } } // --- drive letters ----------------------------------------------------------- const MOUNTS: &str = "/ ramfs\n/tmp ramfs\n/mnt/disk0 fat32\n/mnt/disk1 fat32\n"; #[test_case] fn drive_letters_are_assigned_in_mount_order() { let drives = shparse::assign_drives(MOUNTS); let summary: Vec<(char, &str, &str)> = drives.iter().map(|d| (d.letter, d.path.as_str(), d.fs.as_str())).collect(); // /tmp gets no letter: it's A:/tmp. assert_eq!(summary, [('A', "/", "ramfs"), ('B', "/mnt/disk0", "fat32"), ('C', "/mnt/disk1", "fat32")]); } #[test_case] fn drive_letters_translate_to_paths() { let drives = shparse::assign_drives(MOUNTS); let t = |s| shparse::translate(s, &drives); assert_eq!(t("B:").as_deref(), Some("/mnt/disk0")); assert_eq!(t("B:/").as_deref(), Some("/mnt/disk0")); assert_eq!(t("b:/notes/todo.txt").as_deref(), Some("/mnt/disk0/notes/todo.txt")); // any case assert_eq!(t("B:notes").as_deref(), Some("/mnt/disk0/notes")); assert_eq!(t("c:/x").as_deref(), Some("/mnt/disk1/x")); assert_eq!(t("A:").as_deref(), Some("/")); assert_eq!(t("A:/tmp/a.txt").as_deref(), Some("/tmp/a.txt")); assert_eq!(t("Z:/x"), None); // no such drive assert_eq!(t("readme.txt"), None); assert_eq!(t("/mnt/disk0"), None); assert_eq!(shparse::drive_letter("q:"), Some('Q')); } #[test_case] fn paths_display_with_drive_letters() { let drives = shparse::assign_drives(MOUNTS); let d = |s| shparse::display(s, &drives); assert_eq!(d("/"), "A:/"); assert_eq!(d("/tmp"), "A:/tmp"); assert_eq!(d("/mnt/disk0"), "B:/"); assert_eq!(d("/mnt/disk0/bin"), "B:/bin"); assert_eq!(d("/mnt/disk1/a/b"), "C:/a/b"); assert_eq!(d("/mnt/disk01"), "A:/mnt/disk01"); // not inside /mnt/disk0 assert_eq!(d("/mnt"), "A:/mnt"); } // --- the real shell -------------------------------------------------------- /// Run `/mnt/disk0/bin/sh script` with stdout in a pipe; return the output /// and how the shell ended. fn run_shell_script(script: &str) -> (String, ExitStatus) { let path = "/tmp/session.sh"; let _ = fs::remove(path, false); fs::create(path, InodeKind::File).unwrap().write_at(0, script.as_bytes()).unwrap(); let (reader, writer) = fs::pipe::new(); let mut spawn = SpawnArgs::kernel("sh"); spawn.args.push(String::from(path)); spawn.stdio[1] = Arc::new(writer); let sh = process::spawn_path("/mnt/disk0/bin/sh", spawn).unwrap(); let mut out = Vec::new(); let mut buf = [0u8; 256]; // EOF once the shell *and* every program it started have exited. while let Ok(n @ 1..) = reader.read(&mut buf) { out.extend_from_slice(&buf[..n]); } (String::from_utf8(out).unwrap(), process::wait(&sh)) } #[test_case] fn scripted_shell_session() { let (out, status) = run_shell_script( "cd B:\n\ pwd\n\ ls notes\n\ echo piped text | grep piped\n\ type b:notes/todo.txt | wc -l\n\ echo saved > A:/tmp/s.txt\n\ echo more >> A:/tmp/s.txt\n\ cat < /tmp/s.txt\n\ ver\n\ nosuchprogram\n\ exit 3\n\ echo not reached\n", ); // Each command is echoed after its prompt; the prompt shows the drive. for expected in [ "A:/ $ cd B:\n", "B:/ $ pwd\n/mnt/disk0\n", "B:/ $ ls notes\ntodo.txt\n", // into a pipe: one name per line "piped text\n", "wc -l\n2\n", "saved\nmore\n", "Rustbelt 0.21.0", "B:/ $ exit 3\n", ] { assert!(out.contains(expected), "missing {:?} in:\n{}", expected, out); } assert!(!out.contains("not reached")); assert!(!out.contains("command not found")); // that went to stderr assert_eq!(status, ExitStatus::Exited(3)); } #[test_case] fn shell_runs_background_jobs() { let (out, status) = run_shell_script("start sleep 250ms\njobs\nwait\n"); assert!(out.contains("sleep started in the background"), "{}", out); assert!(out.contains("running start sleep 250ms"), "{}", out); assert!(out.contains("exit code 0"), "{}", out); assert_eq!(status, ExitStatus::Exited(0)); // end of script: last status } /// Exited processes give their memory back (page tables, code, stacks): /// starting 20 more processes doesn't use up more frames. #[test_case] fn processes_give_their_memory_back() { let run_child = || { let mut spawn = SpawnArgs::kernel("child"); spawn.args.push(String::from("0")); let p = process::spawn_path("/mnt/disk0/bin/child", spawn).unwrap(); assert_eq!(process::wait(&p), ExitStatus::Exited(0)); }; run_child(); // warm up (e.g. caches filled on first use) let before = memory::frames_in_use(); for _ in 0..20 { run_child(); } assert_eq!(memory::frames_in_use(), before); } #[panic_handler] fn panic(info: &PanicInfo) -> ! { rustbelt::test_panic_handler(info) }