use super::*; fn parse(x: &[u8]) -> Ast { do_parse(x) .map_err(|(err, rest)| (err, String::from_utf8_lossy(&rest))) .unwrap() } const TIMEOUT_MS: u64 = 100; macro_rules! parse_test { ($l:expr, $r:expr $(,)?) => {{ let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { #[allow(unreachable_code, unused_variables)] let result = std::panic::catch_unwind(|| { let l = $l; let r = $r; if l != r { let mut left = Vec::new(); l.cdisplay(&mut left).unwrap(); let mut right = Vec::new(); r.cdisplay(&mut right).unwrap(); let left = String::from_utf8_lossy(&left); let right = String::from_utf8_lossy(&right); if left != right { panic!("parse equality error\nleft: {left}\nright: {right}") } } }); let _ = tx.send(result); }); match rx.recv_timeout(std::time::Duration::from_millis(TIMEOUT_MS)) { Ok(Ok(())) => (), Ok(Err(e)) => std::panic::resume_unwind(e), Err(_) => panic!("test timed out after {TIMEOUT_MS} ms"), } }}; } #[test] fn command_interp() { parse_test!( parse(br#""$(echo echo)""#), pipes([cmd([str([cmdp(pipes([cmd([ estr(b"echo"), estr(b"echo"), ])]))])])]), ) } #[test] fn string_concat() { parse_test!( parse(br#" foo'bar'"baz" "#), pipes([cmd([estr(b"foobarbaz")])]), ); } #[test] fn simple_string() { parse_test!(parse(b"foo"), pipes([cmd([estr(b"foo")])])); } #[test] fn simple_var() { parse_test!(parse(b"$foo"), pipes([cmd([str([var(b"foo")])])])); } #[test] fn ls_pipe_cat() { parse_test!( parse(b"ls | cat"), pipes([cmd([estr(b"ls")]), cmd([estr(b"cat")])]), ); } #[test] fn ls_pipe_cat_nospace() { parse_test!( parse(b"ls|cat"), pipes([cmd([estr(b"ls")]), cmd([estr(b"cat")])]), ); } #[test] fn unclosed_single_quote() { assert!(do_parse(b"x'").is_err()) } #[test] fn unclosed_double_quote() { assert!(do_parse(b"x\"").is_err()) } #[test] fn tilde() { parse_test!( parse(b"echo ~"), pipes([cmd([estr(b"echo"), str([var(b"HOME")])])]), ); } #[test] fn tilde2() { parse_test!( parse(b"echo ~/foo/bar"), pipes([cmd([ estr(b"echo"), str([var(b"HOME"), plain(b"/foo/bar")]), ])]), ); } #[test] fn tilde3() { parse_test!( parse(b"echo ~ "), pipes([cmd([estr(b"echo"), str([var(b"HOME")])])]), ); } #[test] fn tilde4() { parse_test!( parse(b"echo ~'x'"), pipes([cmd([estr(b"echo"), estr(b"~x")])]), ); } #[test] fn tilde5() { parse_test!( parse(b"echo ~$FOO"), pipes([cmd([estr(b"echo"), str([plain(b"~"), var(b"FOO")])])]), ); } #[test] fn tilde6() { parse_test!( parse(b"git rebase -i HEAD~10"), pipes([cmd([ estr(b"git"), estr(b"rebase"), estr(b"-i"), estr(b"HEAD~10"), ])]), ); } #[test] fn set_variable_in_fun() { parse_test!( parse(b"fun setter { set x = 1 }"), decl(estr(b"setter"), block([assign(estr(b"x"), estr(b"1"))])), ); } #[test] fn variable_with_defaults() { parse_test!( parse(b"${x:-y}"), pipes([cmd([str([var_default(b"x", estr(b"y"))])])]), ); } #[test] fn escape_newline() { parse_test!(parse(b"\"\\n\""), pipes([cmd([estr(b"\n")])])); } #[test] fn escape_carriage_return() { parse_test!(parse(b"\"\\r\""), pipes([cmd([estr(b"\r")])])); } #[test] fn escape_tab() { parse_test!(parse(b"\"\\t\""), pipes([cmd([estr(b"\t")])])); } #[test] fn escape_hex_1() { parse_test!(parse(b"\\x41"), pipes([cmd([estr(b"A")])])); } #[test] fn escape_hex_2() { parse_test!(parse(b"\\x0a"), pipes([cmd([estr(b"\n")])])); } #[test] fn pipe_on_new_line() { parse_test!( parse(b"cat file \n | cat"), pipes([cmd([estr(b"cat"), estr(b"file")]), cmd([estr(b"cat")])]), ); } #[test] fn semicolon() { parse_test!( parse(b"fun f { x ; y }"), decl( estr(b"f"), block([pipes([cmd([estr(b"x")])]), pipes([cmd([estr(b"y")])])]), ), ); } #[test] fn newline_separates_commands() { parse_test!( parse(b"fun f { x \n y }"), decl( estr(b"f"), block([pipes([cmd([estr(b"x")])]), pipes([cmd([estr(b"y")])])]), ), ); } #[test] fn newline_does_not_separate_pipes() { parse_test!( parse(b"fun f { x \n| y }"), decl( estr(b"f"), block([pipes([cmd([estr(b"x")]), cmd([estr(b"y")])])]), ), ); } #[test] fn simple_if() { parse_test!( parse(b"if cond { x }"), cond( pipes([cmd([estr(b"cond")]),]), block([pipes([cmd([estr(b"x")]),])]), block([]) ) ); } #[test] fn if_else() { parse_test!( parse(b"if cond { x } else { y }"), cond( pipes([cmd([estr(b"cond")]),]), block([pipes([cmd([estr(b"x")]),])]), block([pipes([cmd([estr(b"y")]),])]) ) ); } #[test] fn simple_while() { parse_test!(parse(b"while cond { x }"), todo!()); } #[test] fn multiline_string_1() { parse_test!( parse( br#"echo """ hello world""""# ), pipes([cmd([estr(b"echo"), estr(b"hello\nworld")]),]) ); } #[test] fn multiline_string_2() { parse_test!( parse( br#"echo """ line 1 $var line 3 """"# ), pipes([cmd([ estr(b"echo"), str([plain(b"line 1\n"), var(b"var"), plain(b"\nline 3\n")]) ]),]) ); } #[test] fn multiline_string_3() { parse_test!( parse( br#"echo ''' line 1 $var line 3 '''"# ), pipes([cmd([estr(b"echo"), estr(b"line 1\n$var\nline 3\n")]),]) ); } #[test] fn multiline_string_4() { parse_test!( parse( br#"echo foo""" text """ more text """foo "# ), pipes([cmd([estr(b"echo"), estr(b"text\n\"\"\"\nmore text\n")]),]) ); } #[test] fn exit_code() { parse_test!( parse(b"echo $?"), pipes([cmd([estr(b"echo"), str([var(b"?")])])]) ) } #[test] fn exit_code_2() { parse_test!( parse(b"echo \"$?\""), pipes([cmd([estr(b"echo"), str([var(b"?")])])]) ) } #[test] fn dollar_non_var() { parse_test!( parse(b"echo $_"), pipes([cmd([estr(b"echo"), estr(b"$_")]),]) ) } #[test] fn backslash_eof() { assert_eq!(do_parse(b"echo \\").unwrap_err().0, ParseError::Eof); } #[test] fn backslash_joins_lines() { parse_test!( parse(b"echo foo\\\nbar"), pipes([cmd([estr(b"echo"), estr(b"foobar")]),]) ) } fn combinations(choices: &[&[u8]], n: usize) -> impl Iterator> { struct CombinationGenerator<'a> { choices: &'a [&'a [u8]], indices: Vec, done: bool, } impl<'a> Iterator for CombinationGenerator<'a> { type Item = Vec; fn next(&mut self) -> Option { if self.done { return None; } // Build current combination let mut out = Vec::new(); for &i in &self.indices { out.extend_from_slice(self.choices[i]); } // Increment like a base-N counter for pos in (0..self.indices.len()).rev() { if self.indices[pos] + 1 < self.choices.len() { self.indices[pos] += 1; for j in pos + 1..self.indices.len() { self.indices[j] = 0; } return Some(out); } } // Last combination reached self.done = true; Some(out) } } CombinationGenerator { choices, indices: vec![0; n], done: n == 0 && choices.is_empty(), } } fn coerce(t: T) -> T { t } #[test] fn test_escape_bruteforce() { let words = [ coerce::<&'static [u8]>(b"\""), b"'", b"x", b"y", b"z", b"\\", b"|", b"$", b"{", b"}", b"'''", b"\"\"\"", ]; let quotes = [ StringDelimiter::None, StringDelimiter::Interp, StringDelimiter::Strict, StringDelimiter::InterpCustom(b"x".into()), StringDelimiter::StrictCustom(b"x".into()), ]; for quote in quotes { for phrase in combinations(&words[..], 5) { let mut x = Vec::new(); quote.write_opening_delimiter(&mut x); quote.escape(&phrase, &mut x); quote.write_closing_delimiter(&mut x); let s = ExpString::parse(&mut Cursor::new(&x, ParseMode::Command)) .map_err(|e| format!("{quote:?} escape {} failed: {e:?}", x.escape_ascii())) .unwrap(); assert_eq!(s.parts.len(), 1, "{}", x.escape_ascii()); let s = s.parts[0].clone(); let s = s.unwrap_boring(); let x = String::from_utf8(x).unwrap(); let s = String::from_utf8(s).unwrap(); let phrase = String::from_utf8(phrase).unwrap(); assert_eq!( phrase, s, "escape/parse roundtrip in StringDelimiter::{quote:?} failed: {phrase} -> {x} -> {s}" ); } } }