diff --git a/crates/printf/src/lib.rs b/crates/printf/src/lib.rs index b43f3c5f5..f23038287 100644 --- a/crates/printf/src/lib.rs +++ b/crates/printf/src/lib.rs @@ -6,8 +6,10 @@ mod printf_impl; pub use printf_impl::{Error, FormatString, sprintf_locale}; pub mod locale; -pub use locale::{C_LOCALE, EN_US_LOCALE, Locale}; +pub use locale::{C_LOCALE, Locale}; +#[cfg(test)] +pub use locale::EN_US_LOCALE; #[cfg(test)] mod tests; diff --git a/crates/printf/src/locale.rs b/crates/printf/src/locale.rs index bdb13effb..0afbb69b6 100644 --- a/crates/printf/src/locale.rs +++ b/crates/printf/src/locale.rs @@ -118,86 +118,91 @@ pub fn separator_count(&self, digits_count: usize) -> usize { group_repeat: true, }; -#[test] -fn test_apply_grouping() { - let input = "123456789"; - let mut result: String; +#[cfg(test)] +mod tests { + use super::{C_LOCALE, EN_US_LOCALE, Locale}; - // en_US has commas. - assert_eq!(EN_US_LOCALE.thousands_sep, Some(',')); - result = EN_US_LOCALE.apply_grouping(input); - assert_eq!(result, "123,456,789"); + #[test] + fn test_apply_grouping() { + let input = "123456789"; + let mut result: String; - // Test weird locales. - let input: &str = "1234567890123456"; - let mut locale: Locale = C_LOCALE; - locale.thousands_sep = Some('!'); + // en_US has commas. + assert_eq!(EN_US_LOCALE.thousands_sep, Some(',')); + result = EN_US_LOCALE.apply_grouping(input); + assert_eq!(result, "123,456,789"); - locale.grouping = [5, 3, 1, 0]; - locale.group_repeat = false; - result = locale.apply_grouping(input); - assert_eq!(result, "1234567!8!901!23456"); + // Test weird locales. + let input: &str = "1234567890123456"; + let mut locale: Locale = C_LOCALE; + locale.thousands_sep = Some('!'); - // group_repeat doesn't matter because trailing group is 0 - locale.grouping = [5, 3, 1, 0]; - locale.group_repeat = true; - result = locale.apply_grouping(input); - assert_eq!(result, "1234567!8!901!23456"); + locale.grouping = [5, 3, 1, 0]; + locale.group_repeat = false; + result = locale.apply_grouping(input); + assert_eq!(result, "1234567!8!901!23456"); - locale.grouping = [5, 3, 1, 2]; - locale.group_repeat = false; - result = locale.apply_grouping(input); - assert_eq!(result, "12345!67!8!901!23456"); + // group_repeat doesn't matter because trailing group is 0 + locale.grouping = [5, 3, 1, 0]; + locale.group_repeat = true; + result = locale.apply_grouping(input); + assert_eq!(result, "1234567!8!901!23456"); - locale.grouping = [5, 3, 1, 2]; - locale.group_repeat = true; - result = locale.apply_grouping(input); - assert_eq!(result, "1!23!45!67!8!901!23456"); -} + locale.grouping = [5, 3, 1, 2]; + locale.group_repeat = false; + result = locale.apply_grouping(input); + assert_eq!(result, "12345!67!8!901!23456"); -#[test] -#[should_panic] -fn test_thousands_grouping_length_panics_if_no_sep() { - // We should panic if we try to group with no thousands separator. - assert_eq!(C_LOCALE.thousands_sep, None); - C_LOCALE.apply_grouping("123"); -} - -#[test] -fn test_thousands_grouping_length() { - fn validate_grouping_length_hint(locale: Locale, mut input: &str) { - loop { - let expected = locale.separator_count(input.len()) + input.len(); - let actual = locale.apply_grouping(input).len(); - assert_eq!(expected, actual); - if input.is_empty() { - break; - } - input = &input[1..]; - } + locale.grouping = [5, 3, 1, 2]; + locale.group_repeat = true; + result = locale.apply_grouping(input); + assert_eq!(result, "1!23!45!67!8!901!23456"); } - validate_grouping_length_hint(EN_US_LOCALE, "123456789"); + #[test] + #[should_panic] + fn test_thousands_grouping_length_panics_if_no_sep() { + // We should panic if we try to group with no thousands separator. + assert_eq!(C_LOCALE.thousands_sep, None); + C_LOCALE.apply_grouping("123"); + } - // Test weird locales. - let input = "1234567890123456"; - let mut locale: Locale = C_LOCALE; - locale.thousands_sep = Some('!'); + #[test] + fn test_thousands_grouping_length() { + fn validate_grouping_length_hint(locale: Locale, mut input: &str) { + loop { + let expected = locale.separator_count(input.len()) + input.len(); + let actual = locale.apply_grouping(input).len(); + assert_eq!(expected, actual); + if input.is_empty() { + break; + } + input = &input[1..]; + } + } - locale.grouping = [5, 3, 1, 0]; - locale.group_repeat = false; - validate_grouping_length_hint(locale, input); + validate_grouping_length_hint(EN_US_LOCALE, "123456789"); - // group_repeat doesn't matter because trailing group is 0 - locale.grouping = [5, 3, 1, 0]; - locale.group_repeat = true; - validate_grouping_length_hint(locale, input); + // Test weird locales. + let input = "1234567890123456"; + let mut locale: Locale = C_LOCALE; + locale.thousands_sep = Some('!'); - locale.grouping = [5, 3, 1, 2]; - locale.group_repeat = false; - validate_grouping_length_hint(locale, input); + locale.grouping = [5, 3, 1, 0]; + locale.group_repeat = false; + validate_grouping_length_hint(locale, input); - locale.grouping = [5, 3, 1, 2]; - locale.group_repeat = true; - validate_grouping_length_hint(locale, input); + // group_repeat doesn't matter because trailing group is 0 + locale.grouping = [5, 3, 1, 0]; + locale.group_repeat = true; + validate_grouping_length_hint(locale, input); + + locale.grouping = [5, 3, 1, 2]; + locale.group_repeat = false; + validate_grouping_length_hint(locale, input); + + locale.grouping = [5, 3, 1, 2]; + locale.group_repeat = true; + validate_grouping_length_hint(locale, input); + } } diff --git a/src/abbrs.rs b/src/abbrs.rs index 755307971..0f420ceae 100644 --- a/src/abbrs.rs +++ b/src/abbrs.rs @@ -7,8 +7,6 @@ use once_cell::sync::Lazy; use crate::parse_constants::SourceRange; -#[cfg(test)] -use crate::tests::prelude::*; use pcre2::utf32::Regex; static ABBRS: Lazy> = Lazy::new(|| Mutex::new(Default::default())); @@ -122,7 +120,6 @@ fn matches_position(&self, position: Position) -> bool { #[derive(Debug, Eq, PartialEq)] pub struct Replacer { /// The string to use to replace the incoming token, either literal or as a function name. - /// Exposed for testing. pub replacement: WString, /// If true, treat 'replacement' as the name of a function. @@ -274,41 +271,180 @@ pub fn abbrs_match(token: &wstr, position: Position, cmd: &wstr) -> Vec { + let result = abbrs_match(L!($token), $position, L!("")); + assert_eq!(result, vec![]); + }; + ($token:expr, $position:expr, $expected:expr) => { + let result = abbrs_match(L!($token), $position, L!("")); + assert_eq!( + result + .into_iter() + .map(|a| a.replacement) + .collect::>(), + vec![L!($expected).to_owned()] + ); + }; + } - abbrs_g.rename(L!("gc"), L!("gcc")); - assert!(abbrs_g.has_name(L!("gcc"))); - assert!(!abbrs_g.has_name(L!("gc"))); + let cmd = Position::Command; + abbr_expand_1!("", cmd); + abbr_expand_1!("nothing", cmd); - assert!(!abbrs_g.erase(L!("gc"))); - assert!(abbrs_g.erase(L!("gcc"))); - assert!(!abbrs_g.erase(L!("gcc"))); - }) + abbr_expand_1!("gc", cmd, "git checkout"); + abbr_expand_1!("foo", cmd, "bar"); + + let expand_abbreviation_in_command = + |cmdline: &wstr, cursor_pos: Option| -> Option { + let replacement = reader_expand_abbreviation_at_cursor( + cmdline, + cursor_pos.unwrap_or(cmdline.len()), + &parser, + )?; + let mut cmdline_expanded = cmdline.to_owned(); + let mut colors = vec![HighlightSpec::new(); cmdline.len()]; + apply_edit( + &mut cmdline_expanded, + &mut colors, + &Edit::new(replacement.range.into(), replacement.text), + ); + Some(cmdline_expanded) + }; + + macro_rules! validate { + ($cmdline:expr, $cursor:expr) => {{ + let actual = expand_abbreviation_in_command(L!($cmdline), $cursor); + assert_eq!(actual, None); + }}; + ($cmdline:expr, $cursor:expr, $expected:expr) => {{ + let actual = expand_abbreviation_in_command(L!($cmdline), $cursor); + assert_eq!(actual, Some(L!($expected).to_owned())); + }}; + } + + validate!("just a command", Some(3)); + validate!("gc somebranch", Some(0), "git checkout somebranch"); + + validate!( + "gc somebranch", + Some("gc".chars().count()), + "git checkout somebranch" + ); + + // Space separation. + validate!( + "gx somebranch", + Some("gc".chars().count()), + "git checkout somebranch" + ); + + validate!( + "echo hi ; gc somebranch", + Some("echo hi ; g".chars().count()), + "echo hi ; git checkout somebranch" + ); + + validate!( + "echo (echo (echo (echo (gc ", + Some("echo (echo (echo (echo (gc".chars().count()), + "echo (echo (echo (echo (git checkout " + ); + + // If commands should be expanded. + validate!("if gc", None, "if git checkout"); + + // Others should not be. + validate!("of gc", None); + + // Other decorations generally should be. + validate!("command gc", None, "command git checkout"); + + // yin/yang expands everywhere. + validate!("command yin", None, "command yang"); + } + + #[test] + #[serial] + fn rename_abbrs() { + let _cleanup = test_init(); + + with_abbrs_mut(|abbrs_g| { + let mut add = |name: &wstr, repl: &wstr, position: Position| { + abbrs_g.add(Abbreviation { + name: name.into(), + key: name.into(), + regex: None, + commands: vec![], + replacement: repl.into(), + replacement_is_function: false, + position, + set_cursor_marker: None, + from_universal: false, + }) + }; + add(L!("gc"), L!("git checkout"), Position::Command); + add(L!("foo"), L!("bar"), Position::Command); + add(L!("gx"), L!("git checkout"), Position::Command); + add(L!("yin"), L!("yang"), Position::Anywhere); + + assert!(!abbrs_g.has_name(L!("gcc"))); + assert!(abbrs_g.has_name(L!("gc"))); + + abbrs_g.rename(L!("gc"), L!("gcc")); + assert!(abbrs_g.has_name(L!("gcc"))); + assert!(!abbrs_g.has_name(L!("gc"))); + + assert!(!abbrs_g.erase(L!("gc"))); + assert!(abbrs_g.erase(L!("gcc"))); + assert!(!abbrs_g.erase(L!("gcc"))); + }) + } } diff --git a/src/ast.rs b/src/ast.rs index e11191ba9..a234d075a 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -17,8 +17,6 @@ SourceRange, StatementDecoration, token_type_user_presentable_description, }; use crate::parse_tree::ParseToken; -#[cfg(test)] -use crate::tests::prelude::*; use crate::tokenizer::{ TOK_ACCEPT_UNFINISHED, TOK_ARGUMENT_LIST, TOK_CONTINUE_AFTER_ERROR, TOK_SHOW_COMMENTS, TokFlags, TokenType, Tokenizer, TokenizerError, variable_assignment_equals_pos, @@ -2827,11 +2825,116 @@ fn keyword_for_token(tok: TokenType, token: &wstr) -> ParseKeyword { ParseKeyword::from(&unescape_keyword(tok, token)[..]) } -#[test] -#[serial] -fn test_ast_parse() { - let _cleanup = test_init(); - let src = L!("echo"); - let ast = parse(src, ParseTreeFlags::empty(), None); - assert!(!ast.any_error); +#[cfg(test)] +mod tests { + use super::{Node, is_same_node}; + use crate::ast; + use crate::parse_constants::ParseTreeFlags; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + + #[test] + #[serial] + fn test_ast_parse() { + let _cleanup = test_init(); + let src = L!("echo"); + let ast = ast::parse(src, ParseTreeFlags::empty(), None); + assert!(!ast.any_error); + } + + // TODO use 'indoc' but that fails on windows: + // 0 [main] rustc 550 child_info_fork::abort: address space needed by 'indoc-1058d1a3f55eac1a.dll' (0x400000) is already occupied + // error: could not exec the linker `x86_64-pc-cygwin-gcc` + const FISH_FUNC: &str = { + r#" +function stuff --description 'Stuff' + set -l log "/tmp/chaos_log.(random)" + set -x PATH /custom/bin $PATH + + echo "[$USER] Hooray" | tee -a $log 2>/dev/null + + time if test (count $argv) -eq 0 + echo "No targets specified" >> $log 2>&1 + return 1 + end + + for target in $argv + command bash -c "echo" >> $log 2> /dev/null + switch $status + case 0 + echo "Success" | tee -a $log + case '*' + echo "Failure" >> $log + end + end + set_color green +end +"# + }; + + #[test] + fn test_is_same_node() { + // is_same_node is pretty subtle! Let's check it. + let src = WString::from_str(FISH_FUNC); + let ast = ast::parse(&src, Default::default(), None); + assert!(!ast.errored()); + let all_nodes: Vec<&dyn Node> = ast.walk().collect(); + for i in 0..all_nodes.len() { + for j in 0..all_nodes.len() { + let same = is_same_node(all_nodes[i], all_nodes[j]); + if i == j { + assert!(same, "Node {} should be the same as itself", i); + } else { + assert!(!same, "Node {} should not be the same as node {}", i, j); + } + } + } + } +} + +// Run with cargo +nightly bench --features=benchmark +#[cfg(feature = "benchmark")] +#[cfg(test)] +mod bench { + extern crate test; + use crate::ast; + use crate::wchar::prelude::*; + use test::Bencher; + + // Return a long string suitable for benchmarking. + fn generate_fish_script() -> WString { + let mut buff = WString::new(); + let s = &mut buff; + + for i in 0..1000 { + // command with args and redirections + sprintf!(=> s, + "echo arg%d arg%d > out%d.txt 2> err%d.txt\n", + i, i + 1, i, i + ); + + // simple block + sprintf!(=> s, "begin\n echo inside block %d\nend\n", i ); + + // conditional + sprintf!(=> s, "if test %d\n echo even\nelse\n echo odd\nend\n", i % 2); + + // loop + sprintf!(=> s, "for x in a b c\n echo $x %d\nend\n", i); + + // pipeline + sprintf!(=> s, "echo foo%d | grep f | wc -l\n", i); + } + + buff + } + + #[bench] + fn bench_ast_construction(b: &mut Bencher) { + let src = generate_fish_script(); + b.bytes = (src.len() * 4) as u64; // 4 bytes per character + b.iter(|| { + let _ast = ast::parse(&src, Default::default(), None); + }); + } } diff --git a/src/autoload.rs b/src/autoload.rs index 78e51429b..3803fd079 100644 --- a/src/autoload.rs +++ b/src/autoload.rs @@ -7,8 +7,6 @@ use crate::env::Environment; use crate::io::IoChain; use crate::parser::Parser; -#[cfg(test)] -use crate::tests::prelude::*; use crate::wchar::{L, WString, wstr}; use crate::wchar_ext::WExt; use crate::wutil::{FileId, INVALID_FILE_ID, file_id_for_path}; @@ -209,7 +207,6 @@ pub fn clear(&mut self) { } /// Invalidate any underlying cache. - /// This is exposed for testing. #[cfg(test)] fn invalidate_cache(&mut self) { *self.cache = AutoloadFileCache::with_dirs(self.cache.dirs().to_owned()); @@ -484,15 +481,21 @@ fn locate_asset(&self, cmd: &wstr, asset_dir: AssetDir) -> Option { let cmd = wcs2zstring(&sprintf!($fmt $(, $arg)*)); let status = unsafe { libc::system(cmd.as_ptr()) }; @@ -500,102 +503,103 @@ macro_rules! run { }; } - fn touch_file(path: &wstr) { - use nix::sys::stat::Mode; - use std::io::Write; + fn touch_file(path: &wstr) { + use nix::sys::stat::Mode; + use std::io::Write; - let mut file = wopen_cloexec( - path, - OFlag::O_RDWR | OFlag::O_CREAT, - Mode::from_bits_truncate(0o666), - ) - .unwrap(); - file.write_all(b"Hello").unwrap(); + let mut file = wopen_cloexec( + path, + OFlag::O_RDWR | OFlag::O_CREAT, + Mode::from_bits_truncate(0o666), + ) + .unwrap(); + file.write_all(b"Hello").unwrap(); + } + + let mut t1 = "/tmp/fish_test_autoload.XXXXXX\0".as_bytes().to_vec(); + let p1 = charptr2wcstring(unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }); + let mut t2 = "/tmp/fish_test_autoload.XXXXXX\0".as_bytes().to_vec(); + let p2 = charptr2wcstring(unsafe { libc::mkdtemp(t2.as_mut_ptr().cast()) }); + + let paths = &[p1.clone(), p2.clone()]; + let mut autoload = Autoload::new(L!("test_var")); + assert!(autoload.resolve_command_impl(L!("file1"), paths).is_none()); + assert!( + autoload + .resolve_command_impl(L!("nothing"), paths) + .is_none() + ); + assert!(autoload.get_autoloaded_commands().is_empty()); + + run!("touch %s/file1.fish", p1); + run!("touch %s/file2.fish", p2); + autoload.invalidate_cache(); + + assert!(!autoload.autoload_in_progress(L!("file1"))); + assert!(matches!( + autoload.resolve_command_impl(L!("file1"), paths), + AutoloadResult::Path(_) + )); + assert!(matches!( + autoload.resolve_command_impl(L!("file1"), paths), + AutoloadResult::Pending + )); + assert!(autoload.autoload_in_progress(L!("file1"))); + assert!(autoload.get_autoloaded_commands() == vec![L!("file1")]); + autoload.mark_autoload_finished(L!("file1")); + assert!(!autoload.autoload_in_progress(L!("file1"))); + assert!(autoload.get_autoloaded_commands() == vec![L!("file1")]); + + assert!(matches!( + autoload.resolve_command_impl(L!("file1"), paths), + AutoloadResult::Loaded + )); + assert!( + autoload + .resolve_command_impl(L!("nothing"), paths) + .is_none() + ); + assert!(autoload.resolve_command_impl(L!("file2"), paths).is_some()); + assert!(matches!( + autoload.resolve_command_impl(L!("file2"), paths), + AutoloadResult::Pending + )); + autoload.mark_autoload_finished(L!("file2")); + assert!(matches!( + autoload.resolve_command_impl(L!("file2"), paths), + AutoloadResult::Loaded + )); + assert!((autoload.get_autoloaded_commands() == vec![L!("file1"), L!("file2")])); + + autoload.clear(); + assert!(autoload.resolve_command_impl(L!("file1"), paths).is_some()); + autoload.mark_autoload_finished(L!("file1")); + assert!(matches!( + autoload.resolve_command_impl(L!("file1"), paths), + AutoloadResult::Loaded + )); + assert!( + autoload + .resolve_command_impl(L!("nothing"), paths) + .is_none() + ); + assert!(autoload.resolve_command_impl(L!("file2"), paths).is_some()); + assert!(matches!( + autoload.resolve_command_impl(L!("file2"), paths), + AutoloadResult::Pending + )); + autoload.mark_autoload_finished(L!("file2")); + + assert!(matches!( + autoload.resolve_command_impl(L!("file1"), paths), + AutoloadResult::Loaded + )); + touch_file(&sprintf!("%s/file1.fish", p1)); + autoload.invalidate_cache(); + assert!(autoload.resolve_command_impl(L!("file1"), paths).is_some()); + autoload.mark_autoload_finished(L!("file1")); + + run!(L!("rm -Rf %s"), p1); + run!(L!("rm -Rf %s"), p2); } - - let mut t1 = "/tmp/fish_test_autoload.XXXXXX\0".as_bytes().to_vec(); - let p1 = charptr2wcstring(unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }); - let mut t2 = "/tmp/fish_test_autoload.XXXXXX\0".as_bytes().to_vec(); - let p2 = charptr2wcstring(unsafe { libc::mkdtemp(t2.as_mut_ptr().cast()) }); - - let paths = &[p1.clone(), p2.clone()]; - let mut autoload = Autoload::new(L!("test_var")); - assert!(autoload.resolve_command_impl(L!("file1"), paths).is_none()); - assert!( - autoload - .resolve_command_impl(L!("nothing"), paths) - .is_none() - ); - assert!(autoload.get_autoloaded_commands().is_empty()); - - run!("touch %s/file1.fish", p1); - run!("touch %s/file2.fish", p2); - autoload.invalidate_cache(); - - assert!(!autoload.autoload_in_progress(L!("file1"))); - assert!(matches!( - autoload.resolve_command_impl(L!("file1"), paths), - AutoloadResult::Path(_) - )); - assert!(matches!( - autoload.resolve_command_impl(L!("file1"), paths), - AutoloadResult::Pending - )); - assert!(autoload.autoload_in_progress(L!("file1"))); - assert!(autoload.get_autoloaded_commands() == vec![L!("file1")]); - autoload.mark_autoload_finished(L!("file1")); - assert!(!autoload.autoload_in_progress(L!("file1"))); - assert!(autoload.get_autoloaded_commands() == vec![L!("file1")]); - - assert!(matches!( - autoload.resolve_command_impl(L!("file1"), paths), - AutoloadResult::Loaded - )); - assert!( - autoload - .resolve_command_impl(L!("nothing"), paths) - .is_none() - ); - assert!(autoload.resolve_command_impl(L!("file2"), paths).is_some()); - assert!(matches!( - autoload.resolve_command_impl(L!("file2"), paths), - AutoloadResult::Pending - )); - autoload.mark_autoload_finished(L!("file2")); - assert!(matches!( - autoload.resolve_command_impl(L!("file2"), paths), - AutoloadResult::Loaded - )); - assert!((autoload.get_autoloaded_commands() == vec![L!("file1"), L!("file2")])); - - autoload.clear(); - assert!(autoload.resolve_command_impl(L!("file1"), paths).is_some()); - autoload.mark_autoload_finished(L!("file1")); - assert!(matches!( - autoload.resolve_command_impl(L!("file1"), paths), - AutoloadResult::Loaded - )); - assert!( - autoload - .resolve_command_impl(L!("nothing"), paths) - .is_none() - ); - assert!(autoload.resolve_command_impl(L!("file2"), paths).is_some()); - assert!(matches!( - autoload.resolve_command_impl(L!("file2"), paths), - AutoloadResult::Pending - )); - autoload.mark_autoload_finished(L!("file2")); - - assert!(matches!( - autoload.resolve_command_impl(L!("file1"), paths), - AutoloadResult::Loaded - )); - touch_file(&sprintf!("%s/file1.fish", p1)); - autoload.invalidate_cache(); - assert!(autoload.resolve_command_impl(L!("file1"), paths).is_some()); - autoload.mark_autoload_finished(L!("file1")); - - run!(L!("rm -Rf %s"), p1); - run!(L!("rm -Rf %s"), p2); } diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index ab2e2d29d..3f1fac5ff 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -48,9 +48,6 @@ pub mod ulimit; pub mod wait; -#[cfg(test)] -mod tests; - mod prelude { pub use super::shared::*; pub use libc::c_int; diff --git a/src/builtins/path.rs b/src/builtins/path.rs index 7bdf41186..b20c5eb09 100644 --- a/src/builtins/path.rs +++ b/src/builtins/path.rs @@ -533,21 +533,6 @@ fn find_extension(path: &wstr) -> Option { } } -#[test] -fn test_find_extension() { - let cases = [ - (L!("foo.wmv"), Some(3)), - (L!("verylongfilename.wmv"), Some("verylongfilename".len())), - (L!("foo"), None), - (L!(".foo"), None), - (L!("./foo.wmv"), Some(5)), - ]; - - for (f, ext_idx) in cases { - assert_eq!(find_extension(f), ext_idx); - } -} - fn path_extension(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> BuiltinResult { let mut opts = Options::default(); let mut optind = 0; @@ -1012,3 +997,24 @@ pub fn path(parser: &Parser, streams: &mut IoStreams, args: &mut [&wstr]) -> Bui let args = &mut args[1..]; return subcmd(parser, streams, args); } + +#[cfg(test)] +mod tests { + use super::find_extension; + use crate::wchar::prelude::*; + + #[test] + fn test_find_extension() { + let cases = [ + (L!("foo.wmv"), Some(3)), + (L!("verylongfilename.wmv"), Some("verylongfilename".len())), + (L!("foo"), None), + (L!(".foo"), None), + (L!("./foo.wmv"), Some(5)), + ]; + + for (f, ext_idx) in cases { + assert_eq!(find_extension(f), ext_idx); + } + } +} diff --git a/src/builtins/string.rs b/src/builtins/string.rs index 79937b9f6..d2ea05165 100644 --- a/src/builtins/string.rs +++ b/src/builtins/string.rs @@ -17,6 +17,9 @@ mod trim; mod unescape; +#[cfg(test)] +mod test_helpers; + macro_rules! string_error { ( $streams:expr, diff --git a/src/builtins/string/escape.rs b/src/builtins/string/escape.rs index 1b509bb22..e1c34f938 100644 --- a/src/builtins/string/escape.rs +++ b/src/builtins/string/escape.rs @@ -63,3 +63,27 @@ fn handle( } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "escape"], STATUS_CMD_ERROR, ""); + validate!(["string", "escape", ""], STATUS_CMD_OK, "''\n"); + validate!(["string", "escape", "-n", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "escape", "a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "escape", "\x07"], STATUS_CMD_OK, "\\cg\n"); + validate!(["string", "escape", "\"x\""], STATUS_CMD_OK, "'\"x\"'\n"); + validate!(["string", "escape", "hello world"], STATUS_CMD_OK, "'hello world'\n"); + validate!(["string", "escape", "-n", "hello world"], STATUS_CMD_OK, "hello\\ world\n"); + validate!(["string", "escape", "hello", "world"], STATUS_CMD_OK, "hello\nworld\n"); + validate!(["string", "escape", "-n", "~"], STATUS_CMD_OK, "\\~\n"); + } +} diff --git a/src/builtins/string/join.rs b/src/builtins/string/join.rs index 57761cff0..45c5a943f 100644 --- a/src/builtins/string/join.rs +++ b/src/builtins/string/join.rs @@ -97,3 +97,28 @@ fn handle( } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "join"], STATUS_INVALID_ARGS, ""); + validate!(["string", "join", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "join", "", "", "", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "join", "", "a", "b", "c"], STATUS_CMD_OK, "abc\n"); + validate!(["string", "join", ".", "fishshell", "com"], STATUS_CMD_OK, "fishshell.com\n"); + validate!(["string", "join", "/", "usr"], STATUS_CMD_ERROR, "usr\n"); + validate!(["string", "join", "/", "usr", "local", "bin"], STATUS_CMD_OK, "usr/local/bin\n"); + validate!(["string", "join", "...", "3", "2", "1"], STATUS_CMD_OK, "3...2...1\n"); + validate!(["string", "join", "-q"], STATUS_INVALID_ARGS, ""); + validate!(["string", "join", "-q", "."], STATUS_CMD_ERROR, ""); + validate!(["string", "join", "-q", ".", "."], STATUS_CMD_ERROR, ""); + } +} diff --git a/src/builtins/string/length.rs b/src/builtins/string/length.rs index dc4529cef..940463b4d 100644 --- a/src/builtins/string/length.rs +++ b/src/builtins/string/length.rs @@ -76,3 +76,28 @@ fn handle( } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "length"], STATUS_CMD_ERROR, ""); + validate!(["string", "length", ""], STATUS_CMD_ERROR, "0\n"); + validate!(["string", "length", "", "", ""], STATUS_CMD_ERROR, "0\n0\n0\n"); + validate!(["string", "length", "a"], STATUS_CMD_OK, "1\n"); + + validate!(["string", "length", "\u{2008A}"], STATUS_CMD_OK, "1\n"); + validate!(["string", "length", "um", "dois", "três"], STATUS_CMD_OK, "2\n4\n4\n"); + validate!(["string", "length", "um", "dois", "três"], STATUS_CMD_OK, "2\n4\n4\n"); + validate!(["string", "length", "-q"], STATUS_CMD_ERROR, ""); + validate!(["string", "length", "-q", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "length", "-q", "a"], STATUS_CMD_OK, ""); + } +} diff --git a/src/builtins/string/match.rs b/src/builtins/string/match.rs index 584512030..7c40f99e7 100644 --- a/src/builtins/string/match.rs +++ b/src/builtins/string/match.rs @@ -409,3 +409,125 @@ fn report_matches(&mut self, arg: &wstr, streams: &mut IoStreams) { } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS}; + use crate::future_feature_flags::{FeatureFlag, scoped_test}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "match"], STATUS_INVALID_ARGS, ""); + validate!(["string", "match", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "match", "*", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "match", "**", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "match", "*", "xyzzy"], STATUS_CMD_OK, "xyzzy\n"); + validate!(["string", "match", "**", "plugh"], STATUS_CMD_OK, "plugh\n"); + validate!(["string", "match", "a*b", "axxb"], STATUS_CMD_OK, "axxb\n"); + validate!(["string", "match", "a*", "axxb"], STATUS_CMD_OK, "axxb\n"); + validate!(["string", "match", "*a", "xxa"], STATUS_CMD_OK, "xxa\n"); + validate!(["string", "match", "*a*", "axa"], STATUS_CMD_OK, "axa\n"); + validate!(["string", "match", "*a*", "xax"], STATUS_CMD_OK, "xax\n"); + validate!(["string", "match", "*a*", "bxa"], STATUS_CMD_OK, "bxa\n"); + validate!(["string", "match", "*a", "a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "match", "a*", "a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "match", "a*b*c", "axxbyyc"], STATUS_CMD_OK, "axxbyyc\n"); + validate!(["string", "match", "\\*", "*"], STATUS_CMD_OK, "*\n"); + validate!(["string", "match", "a*\\", "abc\\"], STATUS_CMD_OK, "abc\\\n"); + + validate!(["string", "match", "a*b", "axxbc"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "*b", "bbba"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "0x[0-9a-fA-F][0-9a-fA-F]", "0xbad"], STATUS_CMD_ERROR, ""); + + validate!(["string", "match", "-a", "*", "ab", "cde"], STATUS_CMD_OK, "ab\ncde\n"); + validate!(["string", "match", "*", "ab", "cde"], STATUS_CMD_OK, "ab\ncde\n"); + validate!(["string", "match", "-n", "*d*", "cde"], STATUS_CMD_OK, "1 3\n"); + validate!(["string", "match", "-n", "*x*", "cde"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "-q", "a*", "b", "c"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "-q", "a*", "b", "a"], STATUS_CMD_OK, ""); + + validate!(["string", "match", "-r"], STATUS_INVALID_ARGS, ""); + validate!(["string", "match", "-r", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "-r", "", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "match", "-r", ".", "a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "match", "-r", ".*", ""], STATUS_CMD_OK, "\n"); + validate!(["string", "match", "-r", "a*b", "b"], STATUS_CMD_OK, "b\n"); + validate!(["string", "match", "-r", "a*b", "aab"], STATUS_CMD_OK, "aab\n"); + validate!(["string", "match", "-r", "-i", "a*b", "Aab"], STATUS_CMD_OK, "Aab\n"); + validate!(["string", "match", "-r", "-a", "a[bc]", "abadac"], STATUS_CMD_OK, "ab\nac\n"); + validate!(["string", "match", "-r", "a", "xaxa", "axax"], STATUS_CMD_OK, "a\na\n"); + validate!(["string", "match", "-r", "-a", "a", "xaxa", "axax"], STATUS_CMD_OK, "a\na\na\na\n"); + validate!(["string", "match", "-r", "a[bc]", "abadac"], STATUS_CMD_OK, "ab\n"); + validate!(["string", "match", "-r", "-q", "a[bc]", "abadac"], STATUS_CMD_OK, ""); + validate!(["string", "match", "-r", "-q", "a[bc]", "ad"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "-r", "(a+)b(c)", "aabc"], STATUS_CMD_OK, "aabc\naa\nc\n"); + validate!(["string", "match", "-r", "-a", "(a)b(c)", "abcabc"], STATUS_CMD_OK, "abc\na\nc\nabc\na\nc\n"); + validate!(["string", "match", "-r", "(a)b(c)", "abcabc"], STATUS_CMD_OK, "abc\na\nc\n"); + validate!(["string", "match", "-r", "(a|(z))(bc)", "abc"], STATUS_CMD_OK, "abc\na\nbc\n"); + validate!(["string", "match", "-r", "-n", "a", "ada", "dad"], STATUS_CMD_OK, "1 1\n2 1\n"); + validate!(["string", "match", "-r", "-n", "-a", "a", "bacadae"], STATUS_CMD_OK, "2 1\n4 1\n6 1\n"); + validate!(["string", "match", "-r", "-n", "(a).*(b)", "a---b"], STATUS_CMD_OK, "1 5\n1 1\n5 1\n"); + validate!(["string", "match", "-r", "-n", "(a)(b)", "ab"], STATUS_CMD_OK, "1 2\n1 1\n2 1\n"); + validate!(["string", "match", "-r", "-n", "(a)(b)", "abab"], STATUS_CMD_OK, "1 2\n1 1\n2 1\n"); + validate!(["string", "match", "-r", "-n", "-a", "(a)(b)", "abab"], STATUS_CMD_OK, "1 2\n1 1\n2 1\n3 2\n3 1\n4 1\n"); + validate!(["string", "match", "-r", "*", ""], STATUS_INVALID_ARGS, ""); + validate!(["string", "match", "-r", "-a", "a*", "b"], STATUS_CMD_OK, "\n\n"); + validate!(["string", "match", "-r", "foo\\Kbar", "foobar"], STATUS_CMD_OK, "bar\n"); + validate!(["string", "match", "-r", "(foo)\\Kbar", "foobar"], STATUS_CMD_OK, "bar\nfoo\n"); + } + + #[test] + #[serial] + #[rustfmt::skip] + fn test_qmark_noglob_true() { + scoped_test(FeatureFlag::qmark_noglob, true, || { + validate!(["string", "match", "a*b?c", "axxb?c"], STATUS_CMD_OK, "axxb?c\n"); + validate!(["string", "match", "*?", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "*?", "ab"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "?*", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "?*", "ab"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "a*\\?", "abc?"], STATUS_CMD_ERROR, ""); + + validate!(["string", "match", "?", "?"], STATUS_CMD_OK, "?\n"); + validate!(["string", "match", "a??b", "axxb"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "a??b", "a??b"], STATUS_CMD_OK, "a??b\n"); + validate!(["string", "match", "-i", "a??B", "axxb"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "-i", "a??b", "A??b"], STATUS_CMD_OK, "A??b\n"); + validate!(["string", "match", "a*\\?", "abc\\?"], STATUS_CMD_OK, "abc\\?\n"); + + validate!(["string", "match", "?", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "?", "ab"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "??", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "?a", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "a?", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "a??B", "axxb"], STATUS_CMD_ERROR, ""); + }); + } + + #[test] + #[serial] + #[rustfmt::skip] + fn test_qmark_glob() { + scoped_test(FeatureFlag::qmark_noglob, false, || { + validate!(["string", "match", "a*b?c", "axxbyc"], STATUS_CMD_OK, "axxbyc\n"); + validate!(["string", "match", "*?", "a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "match", "*?", "ab"], STATUS_CMD_OK, "ab\n"); + validate!(["string", "match", "?*", "a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "match", "?*", "ab"], STATUS_CMD_OK, "ab\n"); + validate!(["string", "match", "a*\\?", "abc?"], STATUS_CMD_OK, "abc?\n"); + + validate!(["string", "match", "?", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "?", "ab"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "??", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "?a", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "a?", "a"], STATUS_CMD_ERROR, ""); + validate!(["string", "match", "a??B", "axxb"], STATUS_CMD_ERROR, ""); + }); + } +} diff --git a/src/builtins/string/replace.rs b/src/builtins/string/replace.rs index 99da54172..a406a358d 100644 --- a/src/builtins/string/replace.rs +++ b/src/builtins/string/replace.rs @@ -275,3 +275,64 @@ fn replace<'a>(&self, arg: Cow<'a, wstr>) -> Result<(bool, Cow<'a, wstr>), pcre2 } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "replace", ""], STATUS_INVALID_ARGS, ""); + validate!(["string", "replace", "", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "replace", "", "", ""], STATUS_CMD_ERROR, "\n"); + validate!(["string", "replace", "", "", " "], STATUS_CMD_ERROR, " \n"); + validate!(["string", "replace", "a", "b", ""], STATUS_CMD_ERROR, "\n"); + validate!(["string", "replace", "a", "b", "a"], STATUS_CMD_OK, "b\n"); + validate!(["string", "replace", "a", "b", "xax"], STATUS_CMD_OK, "xbx\n"); + validate!(["string", "replace", "a", "b", "xax", "axa"], STATUS_CMD_OK, "xbx\nbxa\n"); + validate!(["string", "replace", "bar", "x", "red barn"], STATUS_CMD_OK, "red xn\n"); + validate!(["string", "replace", "x", "bar", "red xn"], STATUS_CMD_OK, "red barn\n"); + validate!(["string", "replace", "--", "x", "-", "xyz"], STATUS_CMD_OK, "-yz\n"); + validate!(["string", "replace", "--", "y", "-", "xyz"], STATUS_CMD_OK, "x-z\n"); + validate!(["string", "replace", "--", "z", "-", "xyz"], STATUS_CMD_OK, "xy-\n"); + validate!(["string", "replace", "-i", "z", "X", "_Z_"], STATUS_CMD_OK, "_X_\n"); + validate!(["string", "replace", "-a", "a", "A", "aaa"], STATUS_CMD_OK, "AAA\n"); + validate!(["string", "replace", "-i", "a", "z", "AAA"], STATUS_CMD_OK, "zAA\n"); + validate!(["string", "replace", "-q", "x", ">x<", "x"], STATUS_CMD_OK, ""); + validate!(["string", "replace", "-a", "x", "", "xxx"], STATUS_CMD_OK, "\n"); + validate!(["string", "replace", "-a", "***", "_", "*****"], STATUS_CMD_OK, "_**\n"); + validate!(["string", "replace", "-a", "***", "***", "******"], STATUS_CMD_OK, "******\n"); + validate!(["string", "replace", "-a", "a", "b", "xax", "axa"], STATUS_CMD_OK, "xbx\nbxb\n"); + + validate!(["string", "replace", "-r"], STATUS_INVALID_ARGS, ""); + validate!(["string", "replace", "-r", ""], STATUS_INVALID_ARGS, ""); + validate!(["string", "replace", "-r", "", ""], STATUS_CMD_ERROR, ""); + validate!(["string", "replace", "-r", "", "", ""], STATUS_CMD_OK, "\n"); // pcre2 behavior + validate!(["string", "replace", "-r", "", "", " "], STATUS_CMD_OK, " \n"); // pcre2 behavior + validate!(["string", "replace", "-r", "a", "b", ""], STATUS_CMD_ERROR, "\n"); + validate!(["string", "replace", "-r", "a", "b", "a"], STATUS_CMD_OK, "b\n"); + validate!(["string", "replace", "-r", ".", "x", "abc"], STATUS_CMD_OK, "xbc\n"); + validate!(["string", "replace", "-r", ".", "", "abc"], STATUS_CMD_OK, "bc\n"); + validate!(["string", "replace", "-r", "(\\w)(\\w)", "$2$1", "ab"], STATUS_CMD_OK, "ba\n"); + validate!(["string", "replace", "-r", "(\\w)", "$1$1", "ab"], STATUS_CMD_OK, "aab\n"); + validate!(["string", "replace", "-r", "-a", ".", "x", "abc"], STATUS_CMD_OK, "xxx\n"); + validate!(["string", "replace", "-r", "-a", "(\\w)", "$1$1", "ab"], STATUS_CMD_OK, "aabb\n"); + validate!(["string", "replace", "-r", "-a", ".", "", "abc"], STATUS_CMD_OK, "\n"); + validate!(["string", "replace", "-r", "a", "x", "bc", "cd", "de"], STATUS_CMD_ERROR, "bc\ncd\nde\n"); + validate!(["string", "replace", "-r", "a", "x", "aba", "caa"], STATUS_CMD_OK, "xba\ncxa\n"); + validate!(["string", "replace", "-r", "-a", "a", "x", "aba", "caa"], STATUS_CMD_OK, "xbx\ncxx\n"); + validate!(["string", "replace", "-r", "-i", "A", "b", "xax"], STATUS_CMD_OK, "xbx\n"); + validate!(["string", "replace", "-r", "-i", "[a-z]", ".", "1A2B"], STATUS_CMD_OK, "1.2B\n"); + validate!(["string", "replace", "-r", "A", "b", "xax"], STATUS_CMD_ERROR, "xax\n"); + validate!(["string", "replace", "-r", "a", "$1", "a"], STATUS_INVALID_ARGS, ""); + validate!(["string", "replace", "-r", "(a)", "$2", "a"], STATUS_INVALID_ARGS, ""); + validate!(["string", "replace", "-r", "*", ".", "a"], STATUS_INVALID_ARGS, ""); + validate!(["string", "replace", "-ra", "x", "\\c"], STATUS_CMD_ERROR, ""); + validate!(["string", "replace", "-r", "^(.)", "\t$1", "abc", "x"], STATUS_CMD_OK, "\tabc\n\tx\n"); + } +} diff --git a/src/builtins/string/split.rs b/src/builtins/string/split.rs index 26831c392..536f2b170 100644 --- a/src/builtins/string/split.rs +++ b/src/builtins/string/split.rs @@ -278,3 +278,43 @@ fn handle( }; } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "split"], STATUS_INVALID_ARGS, ""); + validate!(["string", "split", ":"], STATUS_CMD_ERROR, ""); + validate!(["string", "split", ".", "www.ch.ic.ac.uk"], STATUS_CMD_OK, "www\nch\nic\nac\nuk\n"); + validate!(["string", "split", "..", "...."], STATUS_CMD_OK, "\n\n\n"); + validate!(["string", "split", "-m", "x", "..", "...."], STATUS_INVALID_ARGS, ""); + validate!(["string", "split", "-m1", "..", "...."], STATUS_CMD_OK, "\n..\n"); + validate!(["string", "split", "-m0", "/", "/usr/local/bin/fish"], STATUS_CMD_ERROR, "/usr/local/bin/fish\n"); + validate!(["string", "split", "-m2", ":", "a:b:c:d", "e:f:g:h"], STATUS_CMD_OK, "a\nb\nc:d\ne\nf\ng:h\n"); + validate!(["string", "split", "-m1", "-r", "/", "/usr/local/bin/fish"], STATUS_CMD_OK, "/usr/local/bin\nfish\n"); + validate!(["string", "split", "-r", ".", "www.ch.ic.ac.uk"], STATUS_CMD_OK, "www\nch\nic\nac\nuk\n"); + validate!(["string", "split", "--", "--", "a--b---c----d"], STATUS_CMD_OK, "a\nb\n-c\n\nd\n"); + validate!(["string", "split", "-r", "..", "...."], STATUS_CMD_OK, "\n\n\n"); + validate!(["string", "split", "-r", "--", "--", "a--b---c----d"], STATUS_CMD_OK, "a\nb-\nc\n\nd\n"); + validate!(["string", "split", "", ""], STATUS_CMD_ERROR, "\n"); + validate!(["string", "split", "", "a"], STATUS_CMD_ERROR, "a\n"); + validate!(["string", "split", "", "ab"], STATUS_CMD_OK, "a\nb\n"); + validate!(["string", "split", "", "abc"], STATUS_CMD_OK, "a\nb\nc\n"); + validate!(["string", "split", "-m1", "", "abc"], STATUS_CMD_OK, "a\nbc\n"); + validate!(["string", "split", "-r", "", ""], STATUS_CMD_ERROR, "\n"); + validate!(["string", "split", "-r", "", "a"], STATUS_CMD_ERROR, "a\n"); + validate!(["string", "split", "-r", "", "ab"], STATUS_CMD_OK, "a\nb\n"); + validate!(["string", "split", "-r", "", "abc"], STATUS_CMD_OK, "a\nb\nc\n"); + validate!(["string", "split", "-r", "-m1", "", "abc"], STATUS_CMD_OK, "ab\nc\n"); + validate!(["string", "split", "-q"], STATUS_INVALID_ARGS, ""); + validate!(["string", "split", "-q", ":"], STATUS_CMD_ERROR, ""); + validate!(["string", "split", "-q", "x", "axbxc"], STATUS_CMD_OK, ""); + } +} diff --git a/src/builtins/string/sub.rs b/src/builtins/string/sub.rs index f905d07b0..1765739cb 100644 --- a/src/builtins/string/sub.rs +++ b/src/builtins/string/sub.rs @@ -114,3 +114,42 @@ fn handle( } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "sub"], STATUS_CMD_ERROR, ""); + validate!(["string", "sub", "abcde"], STATUS_CMD_OK, "abcde\n"); + validate!(["string", "sub", "-l", "x", "abcde"], STATUS_INVALID_ARGS, ""); + validate!(["string", "sub", "-s", "x", "abcde"], STATUS_INVALID_ARGS, ""); + validate!(["string", "sub", "-l0", "abcde"], STATUS_CMD_OK, "\n"); + validate!(["string", "sub", "-l2", "abcde"], STATUS_CMD_OK, "ab\n"); + validate!(["string", "sub", "-l5", "abcde"], STATUS_CMD_OK, "abcde\n"); + validate!(["string", "sub", "-l6", "abcde"], STATUS_CMD_OK, "abcde\n"); + validate!(["string", "sub", "-l-1", "abcde"], STATUS_INVALID_ARGS, ""); + validate!(["string", "sub", "-s0", "abcde"], STATUS_INVALID_ARGS, ""); + validate!(["string", "sub", "-s1", "abcde"], STATUS_CMD_OK, "abcde\n"); + validate!(["string", "sub", "-s5", "abcde"], STATUS_CMD_OK, "e\n"); + validate!(["string", "sub", "-s6", "abcde"], STATUS_CMD_OK, "\n"); + validate!(["string", "sub", "-s-1", "abcde"], STATUS_CMD_OK, "e\n"); + validate!(["string", "sub", "-s-5", "abcde"], STATUS_CMD_OK, "abcde\n"); + validate!(["string", "sub", "-s-6", "abcde"], STATUS_CMD_OK, "abcde\n"); + validate!(["string", "sub", "-s1", "-l0", "abcde"], STATUS_CMD_OK, "\n"); + validate!(["string", "sub", "-s1", "-l1", "abcde"], STATUS_CMD_OK, "a\n"); + validate!(["string", "sub", "-s2", "-l2", "abcde"], STATUS_CMD_OK, "bc\n"); + validate!(["string", "sub", "-s-1", "-l1", "abcde"], STATUS_CMD_OK, "e\n"); + validate!(["string", "sub", "-s-1", "-l2", "abcde"], STATUS_CMD_OK, "e\n"); + validate!(["string", "sub", "-s-3", "-l2", "abcde"], STATUS_CMD_OK, "cd\n"); + validate!(["string", "sub", "-s-3", "-l4", "abcde"], STATUS_CMD_OK, "cde\n"); + validate!(["string", "sub", "-q"], STATUS_CMD_ERROR, ""); + validate!(["string", "sub", "-q", "abcde"], STATUS_CMD_OK, ""); + } +} diff --git a/src/builtins/string/test_helpers.rs b/src/builtins/string/test_helpers.rs new file mode 100644 index 000000000..41cae2743 --- /dev/null +++ b/src/builtins/string/test_helpers.rs @@ -0,0 +1,33 @@ +use super::string; +use crate::builtins::shared::BuiltinResultExt; +use crate::io::IoChain; +use crate::io::{IoStreams, OutputStream, StringOutputStream}; +use crate::tests::prelude::*; +use crate::wchar::prelude::*; + +#[macro_export] +macro_rules! validate { + ( [$($argv:expr),*], $expected_rc:expr, $expected_out:expr ) => { + { + use $crate::common::escape; + use $crate::wchar::prelude::*; + use $crate::builtins::string::test_helpers::string_test; + let (actual_out, actual_rc) = string_test(vec![$(L!($argv)),*]); + assert_eq!(escape(L!($expected_out)), escape(&actual_out)); + assert_eq!($expected_rc, actual_rc); + } + }; +} + +pub fn string_test(mut args: Vec<&wstr>) -> (WString, libc::c_int) { + let parser = TestParser::new(); + let mut outs = OutputStream::String(StringOutputStream::new()); + let mut errs = OutputStream::Null; + let io_chain = IoChain::new(); + let mut streams = IoStreams::new(&mut outs, &mut errs, &io_chain); + streams.stdin_is_directly_redirected = false; // read from argv instead of stdin + + let rc = string(&parser, &mut streams, args.as_mut_slice()); + + (outs.contents().to_owned(), rc.builtin_status_code()) +} diff --git a/src/builtins/string/trim.rs b/src/builtins/string/trim.rs index 9680532f5..2caf0d189 100644 --- a/src/builtins/string/trim.rs +++ b/src/builtins/string/trim.rs @@ -99,3 +99,40 @@ fn handle( } } } + +#[cfg(test)] +mod tests { + use crate::builtins::shared::{STATUS_CMD_ERROR, STATUS_CMD_OK}; + use crate::tests::prelude::*; + use crate::validate; + + #[test] + #[serial] + #[rustfmt::skip] + fn plain() { + let _cleanup = test_init(); + validate!(["string", "trim"], STATUS_CMD_ERROR, ""); + validate!(["string", "trim", ""], STATUS_CMD_ERROR, "\n"); + validate!(["string", "trim", " "], STATUS_CMD_OK, "\n"); + validate!(["string", "trim", " \x0C\n\r\t"], STATUS_CMD_OK, "\n"); + validate!(["string", "trim", " a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "a "], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", " a "], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-l", " a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-l", "a "], STATUS_CMD_ERROR, "a \n"); + validate!(["string", "trim", "-l", " a "], STATUS_CMD_OK, "a \n"); + validate!(["string", "trim", "-r", " a"], STATUS_CMD_ERROR, " a\n"); + validate!(["string", "trim", "-r", "a "], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-r", " a "], STATUS_CMD_OK, " a\n"); + validate!(["string", "trim", "-c", ".", " a"], STATUS_CMD_ERROR, " a\n"); + validate!(["string", "trim", "-c", ".", "a "], STATUS_CMD_ERROR, "a \n"); + validate!(["string", "trim", "-c", ".", " a "], STATUS_CMD_ERROR, " a \n"); + validate!(["string", "trim", "-c", ".", ".a"], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-c", ".", "a."], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-c", ".", ".a."], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-c", "\\/", "/a\\"], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-c", "\\/", "a/"], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-c", "\\/", "\\a/"], STATUS_CMD_OK, "a\n"); + validate!(["string", "trim", "-c", "", ".a."], STATUS_CMD_ERROR, ".a.\n"); + } +} diff --git a/src/builtins/test.rs b/src/builtins/test.rs index 83be97c3f..d842ca985 100644 --- a/src/builtins/test.rs +++ b/src/builtins/test.rs @@ -1089,3 +1089,185 @@ pub fn test(parser: &Parser, streams: &mut IoStreams, argv: &mut [&wstr]) -> Bui Err(STATUS_CMD_ERROR) } } + +#[cfg(test)] +mod tests { + use super::test as builtin_test; + use crate::builtins::prelude::*; + use crate::io::{IoChain, OutputStream}; + use crate::tests::prelude::*; + + fn run_one_test_test_mbracket(expected: i32, lst: &[&str], bracket: bool) -> bool { + let parser = TestParser::new(); + let mut argv = Vec::new(); + if bracket { + argv.push(L!("[").to_owned()); + } else { + argv.push(L!("test").to_owned()); + } + for s in lst { + argv.push(WString::from_str(s)); + } + if bracket { + argv.push(L!("]").to_owned()) + }; + + // Convert to &[&wstr]. + let mut argv = argv.iter().map(|s| s.as_ref()).collect::>(); + let mut out = OutputStream::Null; + let mut err = OutputStream::Null; + let io_chain = IoChain::new(); + let mut streams = IoStreams::new(&mut out, &mut err, &io_chain); + + let result = builtin_test(&parser, &mut streams, &mut argv).builtin_status_code(); + + if result != expected { + eprintf!( + "expected builtin_test() to return %s, got %s\n", + expected.to_string(), + result.to_string() + ); + } + result == expected + } + + fn run_test_test(expected: i32, lst: &[&str]) -> bool { + let nobracket = run_one_test_test_mbracket(expected, lst, false); + let bracket = run_one_test_test_mbracket(expected, lst, true); + assert_eq!(nobracket, bracket); + nobracket + } + + fn test_test_brackets() { + // Ensure [ knows it needs a ]. + let parser = TestParser::new(); + + let mut out = OutputStream::Null; + let mut err = OutputStream::Null; + let io_chain = IoChain::new(); + let mut streams = IoStreams::new(&mut out, &mut err, &io_chain); + + let args1 = &mut [L!("["), L!("foo")]; + assert_eq!( + builtin_test(&parser, &mut streams, args1), + Err(STATUS_INVALID_ARGS) + ); + + let args2 = &mut [L!("["), L!("foo"), L!("]")]; + assert_eq!(builtin_test(&parser, &mut streams, args2), Ok(SUCCESS)); + + let args3 = &mut [L!("["), L!("foo"), L!("]"), L!("bar")]; + assert_eq!( + builtin_test(&parser, &mut streams, args3), + Err(STATUS_INVALID_ARGS) + ); + } + + #[rustfmt::skip] + fn test_test() { + assert!(run_test_test(0, &["5", "-ne", "6"])); + assert!(run_test_test(0, &["5", "-eq", "5"])); + assert!(run_test_test(0, &["0", "-eq", "0"])); + assert!(run_test_test(0, &["-1", "-eq", "-1"])); + assert!(run_test_test(0, &["1", "-ne", "-1"])); + assert!(run_test_test(1, &[" 2 ", "-ne", "2"])); + assert!(run_test_test(0, &[" 2", "-eq", "2"])); + assert!(run_test_test(0, &["2 ", "-eq", "2"])); + assert!(run_test_test(0, &[" 2 ", "-eq", "2"])); + assert!(run_test_test(2, &[" 2x", "-eq", "2"])); + assert!(run_test_test(2, &["", "-eq", "0"])); + assert!(run_test_test(2, &["", "-ne", "0"])); + assert!(run_test_test(2, &[" ", "-eq", "0"])); + assert!(run_test_test(2, &[" ", "-ne", "0"])); + assert!(run_test_test(2, &["x", "-eq", "0"])); + assert!(run_test_test(2, &["x", "-ne", "0"])); + assert!(run_test_test(1, &["-1", "-ne", "-1"])); + assert!(run_test_test(0, &["abc", "!=", "def"])); + assert!(run_test_test(1, &["abc", "=", "def"])); + assert!(run_test_test(0, &["5", "-le", "10"])); + assert!(run_test_test(0, &["10", "-le", "10"])); + assert!(run_test_test(1, &["20", "-le", "10"])); + assert!(run_test_test(0, &["-1", "-le", "0"])); + assert!(run_test_test(1, &["0", "-le", "-1"])); + assert!(run_test_test(0, &["15", "-ge", "10"])); + assert!(run_test_test(0, &["15", "-ge", "10"])); + assert!(run_test_test(1, &["!", "15", "-ge", "10"])); + assert!(run_test_test(0, &["!", "!", "15", "-ge", "10"])); + + assert!(run_test_test(0, &[ + "(", "-d", "/", ")", + "-o", + "(", "!", "-d", "/", ")", + ])); + + assert!(run_test_test(0, &["0", "-ne", "1", "-a", "0", "-eq", "0"])); + assert!(run_test_test(0, &["0", "-ne", "1", "-a", "-n", "5"])); + assert!(run_test_test(0, &["-n", "5", "-a", "10", "-gt", "5"])); + assert!(run_test_test(0, &["-n", "3", "-a", "-n", "5"])); + + // Test precedence: + // '0 == 0 || 0 == 1 && 0 == 2' + // should be evaluated as: + // '0 == 0 || (0 == 1 && 0 == 2)' + // and therefore true. If it were + // '(0 == 0 || 0 == 1) && 0 == 2' + // it would be false. + assert!(run_test_test(0, &["0", "=", "0", "-o", "0", "=", "1", "-a", "0", "=", "2"])); + assert!(run_test_test(0, &["-n", "5", "-o", "0", "=", "1", "-a", "0", "=", "2"])); + assert!(run_test_test(1, &["(", "0", "=", "0", "-o", "0", "=", "1", ")", "-a", "0", "=", "2"])); + assert!(run_test_test(0, &["0", "=", "0", "-o", "(", "0", "=", "1", "-a", "0", "=", "2", ")"])); + + // A few lame tests for permissions; these need to be a lot more complete. + assert!(run_test_test(0, &["-e", "/bin/ls"])); + assert!(run_test_test(1, &["-e", "/bin/ls_not_a_path"])); + assert!(run_test_test(0, &["-x", "/bin/ls"])); + assert!(run_test_test(1, &["-x", "/bin/ls_not_a_path"])); + assert!(run_test_test(0, &["-d", "/bin/"])); + assert!(run_test_test(1, &["-d", "/bin/ls"])); + + // This failed at one point. + assert!(run_test_test(1, &["-d", "/bin", "-a", "5", "-eq", "3"])); + assert!(run_test_test(0, &["-d", "/bin", "-o", "5", "-eq", "3"])); + assert!(run_test_test(0,&["-d", "/bin", "-a", "!", "5", "-eq", "3"])); + + // We didn't properly handle multiple "just strings" either. + assert!(run_test_test(0, &["foo"])); + assert!(run_test_test(0, &["foo", "-a", "bar"])); + + // These should be errors. + assert!(run_test_test(1, &["foo", "bar"])); + assert!(run_test_test(1, &["foo", "bar", "baz"])); + + // This crashed. + assert!(run_test_test(1, &["1", "=", "1", "-a", "=", "1"])); + + // Make sure we can treat -S as a parameter instead of an operator. + // https://github.com/fish-shell/fish-shell/issues/601 + assert!(run_test_test(0, &["-S", "=", "-S"])); + assert!(run_test_test(1, &["!", "!", "!", "A"])); + + // Verify that 1. doubles are treated as doubles, and 2. integers that cannot be represented as + // doubles are still treated as integers. + assert!(run_test_test(0, &["4611686018427387904", "-eq", "4611686018427387904"])); + assert!(run_test_test(0, &["4611686018427387904.0", "-eq", "4611686018427387904.0"])); + assert!(run_test_test(0, &["4611686018427387904.00000000000000001", "-eq", "4611686018427387904.0"])); + assert!(run_test_test(1, &["4611686018427387904", "-eq", "4611686018427387905"])); + assert!(run_test_test(0, &["-4611686018427387904", "-ne", "4611686018427387904"])); + assert!(run_test_test(0, &["-4611686018427387904", "-le", "4611686018427387904"])); + assert!(run_test_test(1, &["-4611686018427387904", "-ge", "4611686018427387904"])); + assert!(run_test_test(1, &["4611686018427387904", "-gt", "4611686018427387904"])); + assert!(run_test_test(0, &["4611686018427387904", "-ge", "4611686018427387904"])); + + // test out-of-range numbers + assert!(run_test_test(2, &["99999999999999999999999999", "-ge", "1"])); + assert!(run_test_test(2, &["1", "-eq", "-99999999999999999999999999.9"])); + } + + #[test] + #[serial] + fn test_test_builtin() { + let _cleanup = test_init(); + test_test_brackets(); + test_test(); + } +} diff --git a/src/builtins/tests/mod.rs b/src/builtins/tests/mod.rs deleted file mode 100644 index b25db0384..000000000 --- a/src/builtins/tests/mod.rs +++ /dev/null @@ -1,2 +0,0 @@ -mod string_tests; -mod test_tests; diff --git a/src/builtins/tests/string_tests.rs b/src/builtins/tests/string_tests.rs deleted file mode 100644 index cbe4181f0..000000000 --- a/src/builtins/tests/string_tests.rs +++ /dev/null @@ -1,329 +0,0 @@ -use crate::io::IoChain; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; - -#[test] -#[serial] -fn test_string() { - let _cleanup = test_init(); - use crate::builtins::shared::{ - BuiltinResultExt, STATUS_CMD_ERROR, STATUS_CMD_OK, STATUS_INVALID_ARGS, - }; - use crate::builtins::string::string; - use crate::common::escape; - use crate::future_feature_flags::{FeatureFlag, scoped_test}; - use crate::io::{IoStreams, OutputStream, StringOutputStream}; - use crate::tests::prelude::*; - use crate::wchar::prelude::*; - - // avoid 1.3k L!()'s - macro_rules! test_cases { - ([$($x:expr),*], $rc:expr, $out:expr) => { (vec![$(L!($x)),*], $rc, L!($out)) }; - [$($x:tt),* $(,)?] => { [$(test_cases!$x),*] }; - } - - // TODO: these should be individual tests, not all in one, port when we can run these with `cargo test` - fn string_test(mut args: Vec<&wstr>, expected_rc: i32, expected_out: &wstr) { - let parser = TestParser::new(); - let mut outs = OutputStream::String(StringOutputStream::new()); - let mut errs = OutputStream::Null; - let io_chain = IoChain::new(); - let mut streams = IoStreams::new(&mut outs, &mut errs, &io_chain); - streams.stdin_is_directly_redirected = false; // read from argv instead of stdin - - let rc = string(&parser, &mut streams, args.as_mut_slice()); - - let actual = escape(outs.contents()); - let expected = escape(expected_out); - assert_eq!( - expected, actual, - "string builtin returned unexpected output" - ); - - // Check return code after so we get a chance to identify the difference first - assert_eq!( - expected_rc, - rc.builtin_status_code(), - "string builtin returned unexpected return code" - ); - } - - #[rustfmt::skip] - let tests = test_cases![ - (["string", "escape"], STATUS_CMD_ERROR, ""), - (["string", "escape", ""], STATUS_CMD_OK, "''\n"), - (["string", "escape", "-n", ""], STATUS_CMD_OK, "\n"), - (["string", "escape", "a"], STATUS_CMD_OK, "a\n"), - (["string", "escape", "\x07"], STATUS_CMD_OK, "\\cg\n"), - (["string", "escape", "\"x\""], STATUS_CMD_OK, "'\"x\"'\n"), - (["string", "escape", "hello world"], STATUS_CMD_OK, "'hello world'\n"), - (["string", "escape", "-n", "hello world"], STATUS_CMD_OK, "hello\\ world\n"), - (["string", "escape", "hello", "world"], STATUS_CMD_OK, "hello\nworld\n"), - (["string", "escape", "-n", "~"], STATUS_CMD_OK, "\\~\n"), - - (["string", "join"], STATUS_INVALID_ARGS, ""), - (["string", "join", ""], STATUS_CMD_ERROR, ""), - (["string", "join", "", "", "", ""], STATUS_CMD_OK, "\n"), - (["string", "join", "", "a", "b", "c"], STATUS_CMD_OK, "abc\n"), - (["string", "join", ".", "fishshell", "com"], STATUS_CMD_OK, "fishshell.com\n"), - (["string", "join", "/", "usr"], STATUS_CMD_ERROR, "usr\n"), - (["string", "join", "/", "usr", "local", "bin"], STATUS_CMD_OK, "usr/local/bin\n"), - (["string", "join", "...", "3", "2", "1"], STATUS_CMD_OK, "3...2...1\n"), - (["string", "join", "-q"], STATUS_INVALID_ARGS, ""), - (["string", "join", "-q", "."], STATUS_CMD_ERROR, ""), - (["string", "join", "-q", ".", "."], STATUS_CMD_ERROR, ""), - - (["string", "length"], STATUS_CMD_ERROR, ""), - (["string", "length", ""], STATUS_CMD_ERROR, "0\n"), - (["string", "length", "", "", ""], STATUS_CMD_ERROR, "0\n0\n0\n"), - (["string", "length", "a"], STATUS_CMD_OK, "1\n"), - - (["string", "length", "\u{2008A}"], STATUS_CMD_OK, "1\n"), - (["string", "length", "um", "dois", "três"], STATUS_CMD_OK, "2\n4\n4\n"), - (["string", "length", "um", "dois", "três"], STATUS_CMD_OK, "2\n4\n4\n"), - (["string", "length", "-q"], STATUS_CMD_ERROR, ""), - (["string", "length", "-q", ""], STATUS_CMD_ERROR, ""), - (["string", "length", "-q", "a"], STATUS_CMD_OK, ""), - - (["string", "match"], STATUS_INVALID_ARGS, ""), - (["string", "match", ""], STATUS_CMD_ERROR, ""), - (["string", "match", "", ""], STATUS_CMD_OK, "\n"), - (["string", "match", "*", ""], STATUS_CMD_OK, "\n"), - (["string", "match", "**", ""], STATUS_CMD_OK, "\n"), - (["string", "match", "*", "xyzzy"], STATUS_CMD_OK, "xyzzy\n"), - (["string", "match", "**", "plugh"], STATUS_CMD_OK, "plugh\n"), - (["string", "match", "a*b", "axxb"], STATUS_CMD_OK, "axxb\n"), - (["string", "match", "a*", "axxb"], STATUS_CMD_OK, "axxb\n"), - (["string", "match", "*a", "xxa"], STATUS_CMD_OK, "xxa\n"), - (["string", "match", "*a*", "axa"], STATUS_CMD_OK, "axa\n"), - (["string", "match", "*a*", "xax"], STATUS_CMD_OK, "xax\n"), - (["string", "match", "*a*", "bxa"], STATUS_CMD_OK, "bxa\n"), - (["string", "match", "*a", "a"], STATUS_CMD_OK, "a\n"), - (["string", "match", "a*", "a"], STATUS_CMD_OK, "a\n"), - (["string", "match", "a*b*c", "axxbyyc"], STATUS_CMD_OK, "axxbyyc\n"), - (["string", "match", "\\*", "*"], STATUS_CMD_OK, "*\n"), - (["string", "match", "a*\\", "abc\\"], STATUS_CMD_OK, "abc\\\n"), - - (["string", "match", "a*b", "axxbc"], STATUS_CMD_ERROR, ""), - (["string", "match", "*b", "bbba"], STATUS_CMD_ERROR, ""), - (["string", "match", "0x[0-9a-fA-F][0-9a-fA-F]", "0xbad"], STATUS_CMD_ERROR, ""), - - (["string", "match", "-a", "*", "ab", "cde"], STATUS_CMD_OK, "ab\ncde\n"), - (["string", "match", "*", "ab", "cde"], STATUS_CMD_OK, "ab\ncde\n"), - (["string", "match", "-n", "*d*", "cde"], STATUS_CMD_OK, "1 3\n"), - (["string", "match", "-n", "*x*", "cde"], STATUS_CMD_ERROR, ""), - (["string", "match", "-q", "a*", "b", "c"], STATUS_CMD_ERROR, ""), - (["string", "match", "-q", "a*", "b", "a"], STATUS_CMD_OK, ""), - - (["string", "match", "-r"], STATUS_INVALID_ARGS, ""), - (["string", "match", "-r", ""], STATUS_CMD_ERROR, ""), - (["string", "match", "-r", "", ""], STATUS_CMD_OK, "\n"), - (["string", "match", "-r", ".", "a"], STATUS_CMD_OK, "a\n"), - (["string", "match", "-r", ".*", ""], STATUS_CMD_OK, "\n"), - (["string", "match", "-r", "a*b", "b"], STATUS_CMD_OK, "b\n"), - (["string", "match", "-r", "a*b", "aab"], STATUS_CMD_OK, "aab\n"), - (["string", "match", "-r", "-i", "a*b", "Aab"], STATUS_CMD_OK, "Aab\n"), - (["string", "match", "-r", "-a", "a[bc]", "abadac"], STATUS_CMD_OK, "ab\nac\n"), - (["string", "match", "-r", "a", "xaxa", "axax"], STATUS_CMD_OK, "a\na\n"), - (["string", "match", "-r", "-a", "a", "xaxa", "axax"], STATUS_CMD_OK, "a\na\na\na\n"), - (["string", "match", "-r", "a[bc]", "abadac"], STATUS_CMD_OK, "ab\n"), - (["string", "match", "-r", "-q", "a[bc]", "abadac"], STATUS_CMD_OK, ""), - (["string", "match", "-r", "-q", "a[bc]", "ad"], STATUS_CMD_ERROR, ""), - (["string", "match", "-r", "(a+)b(c)", "aabc"], STATUS_CMD_OK, "aabc\naa\nc\n"), - (["string", "match", "-r", "-a", "(a)b(c)", "abcabc"], STATUS_CMD_OK, "abc\na\nc\nabc\na\nc\n"), - (["string", "match", "-r", "(a)b(c)", "abcabc"], STATUS_CMD_OK, "abc\na\nc\n"), - (["string", "match", "-r", "(a|(z))(bc)", "abc"], STATUS_CMD_OK, "abc\na\nbc\n"), - (["string", "match", "-r", "-n", "a", "ada", "dad"], STATUS_CMD_OK, "1 1\n2 1\n"), - (["string", "match", "-r", "-n", "-a", "a", "bacadae"], STATUS_CMD_OK, "2 1\n4 1\n6 1\n"), - (["string", "match", "-r", "-n", "(a).*(b)", "a---b"], STATUS_CMD_OK, "1 5\n1 1\n5 1\n"), - (["string", "match", "-r", "-n", "(a)(b)", "ab"], STATUS_CMD_OK, "1 2\n1 1\n2 1\n"), - (["string", "match", "-r", "-n", "(a)(b)", "abab"], STATUS_CMD_OK, "1 2\n1 1\n2 1\n"), - (["string", "match", "-r", "-n", "-a", "(a)(b)", "abab"], STATUS_CMD_OK, "1 2\n1 1\n2 1\n3 2\n3 1\n4 1\n"), - (["string", "match", "-r", "*", ""], STATUS_INVALID_ARGS, ""), - (["string", "match", "-r", "-a", "a*", "b"], STATUS_CMD_OK, "\n\n"), - (["string", "match", "-r", "foo\\Kbar", "foobar"], STATUS_CMD_OK, "bar\n"), - (["string", "match", "-r", "(foo)\\Kbar", "foobar"], STATUS_CMD_OK, "bar\nfoo\n"), - (["string", "replace"], STATUS_INVALID_ARGS, ""), - (["string", "replace", ""], STATUS_INVALID_ARGS, ""), - (["string", "replace", "", ""], STATUS_CMD_ERROR, ""), - (["string", "replace", "", "", ""], STATUS_CMD_ERROR, "\n"), - (["string", "replace", "", "", " "], STATUS_CMD_ERROR, " \n"), - (["string", "replace", "a", "b", ""], STATUS_CMD_ERROR, "\n"), - (["string", "replace", "a", "b", "a"], STATUS_CMD_OK, "b\n"), - (["string", "replace", "a", "b", "xax"], STATUS_CMD_OK, "xbx\n"), - (["string", "replace", "a", "b", "xax", "axa"], STATUS_CMD_OK, "xbx\nbxa\n"), - (["string", "replace", "bar", "x", "red barn"], STATUS_CMD_OK, "red xn\n"), - (["string", "replace", "x", "bar", "red xn"], STATUS_CMD_OK, "red barn\n"), - (["string", "replace", "--", "x", "-", "xyz"], STATUS_CMD_OK, "-yz\n"), - (["string", "replace", "--", "y", "-", "xyz"], STATUS_CMD_OK, "x-z\n"), - (["string", "replace", "--", "z", "-", "xyz"], STATUS_CMD_OK, "xy-\n"), - (["string", "replace", "-i", "z", "X", "_Z_"], STATUS_CMD_OK, "_X_\n"), - (["string", "replace", "-a", "a", "A", "aaa"], STATUS_CMD_OK, "AAA\n"), - (["string", "replace", "-i", "a", "z", "AAA"], STATUS_CMD_OK, "zAA\n"), - (["string", "replace", "-q", "x", ">x<", "x"], STATUS_CMD_OK, ""), - (["string", "replace", "-a", "x", "", "xxx"], STATUS_CMD_OK, "\n"), - (["string", "replace", "-a", "***", "_", "*****"], STATUS_CMD_OK, "_**\n"), - (["string", "replace", "-a", "***", "***", "******"], STATUS_CMD_OK, "******\n"), - (["string", "replace", "-a", "a", "b", "xax", "axa"], STATUS_CMD_OK, "xbx\nbxb\n"), - - (["string", "replace", "-r"], STATUS_INVALID_ARGS, ""), - (["string", "replace", "-r", ""], STATUS_INVALID_ARGS, ""), - (["string", "replace", "-r", "", ""], STATUS_CMD_ERROR, ""), - (["string", "replace", "-r", "", "", ""], STATUS_CMD_OK, "\n"), // pcre2 behavior - (["string", "replace", "-r", "", "", " "], STATUS_CMD_OK, " \n"), // pcre2 behavior - (["string", "replace", "-r", "a", "b", ""], STATUS_CMD_ERROR, "\n"), - (["string", "replace", "-r", "a", "b", "a"], STATUS_CMD_OK, "b\n"), - (["string", "replace", "-r", ".", "x", "abc"], STATUS_CMD_OK, "xbc\n"), - (["string", "replace", "-r", ".", "", "abc"], STATUS_CMD_OK, "bc\n"), - (["string", "replace", "-r", "(\\w)(\\w)", "$2$1", "ab"], STATUS_CMD_OK, "ba\n"), - (["string", "replace", "-r", "(\\w)", "$1$1", "ab"], STATUS_CMD_OK, "aab\n"), - (["string", "replace", "-r", "-a", ".", "x", "abc"], STATUS_CMD_OK, "xxx\n"), - (["string", "replace", "-r", "-a", "(\\w)", "$1$1", "ab"], STATUS_CMD_OK, "aabb\n"), - (["string", "replace", "-r", "-a", ".", "", "abc"], STATUS_CMD_OK, "\n"), - (["string", "replace", "-r", "a", "x", "bc", "cd", "de"], STATUS_CMD_ERROR, "bc\ncd\nde\n"), - (["string", "replace", "-r", "a", "x", "aba", "caa"], STATUS_CMD_OK, "xba\ncxa\n"), - (["string", "replace", "-r", "-a", "a", "x", "aba", "caa"], STATUS_CMD_OK, "xbx\ncxx\n"), - (["string", "replace", "-r", "-i", "A", "b", "xax"], STATUS_CMD_OK, "xbx\n"), - (["string", "replace", "-r", "-i", "[a-z]", ".", "1A2B"], STATUS_CMD_OK, "1.2B\n"), - (["string", "replace", "-r", "A", "b", "xax"], STATUS_CMD_ERROR, "xax\n"), - (["string", "replace", "-r", "a", "$1", "a"], STATUS_INVALID_ARGS, ""), - (["string", "replace", "-r", "(a)", "$2", "a"], STATUS_INVALID_ARGS, ""), - (["string", "replace", "-r", "*", ".", "a"], STATUS_INVALID_ARGS, ""), - (["string", "replace", "-ra", "x", "\\c"], STATUS_CMD_ERROR, ""), - (["string", "replace", "-r", "^(.)", "\t$1", "abc", "x"], STATUS_CMD_OK, "\tabc\n\tx\n"), - - (["string", "split"], STATUS_INVALID_ARGS, ""), - (["string", "split", ":"], STATUS_CMD_ERROR, ""), - (["string", "split", ".", "www.ch.ic.ac.uk"], STATUS_CMD_OK, "www\nch\nic\nac\nuk\n"), - (["string", "split", "..", "...."], STATUS_CMD_OK, "\n\n\n"), - (["string", "split", "-m", "x", "..", "...."], STATUS_INVALID_ARGS, ""), - (["string", "split", "-m1", "..", "...."], STATUS_CMD_OK, "\n..\n"), - (["string", "split", "-m0", "/", "/usr/local/bin/fish"], STATUS_CMD_ERROR, "/usr/local/bin/fish\n"), - (["string", "split", "-m2", ":", "a:b:c:d", "e:f:g:h"], STATUS_CMD_OK, "a\nb\nc:d\ne\nf\ng:h\n"), - (["string", "split", "-m1", "-r", "/", "/usr/local/bin/fish"], STATUS_CMD_OK, "/usr/local/bin\nfish\n"), - (["string", "split", "-r", ".", "www.ch.ic.ac.uk"], STATUS_CMD_OK, "www\nch\nic\nac\nuk\n"), - (["string", "split", "--", "--", "a--b---c----d"], STATUS_CMD_OK, "a\nb\n-c\n\nd\n"), - (["string", "split", "-r", "..", "...."], STATUS_CMD_OK, "\n\n\n"), - (["string", "split", "-r", "--", "--", "a--b---c----d"], STATUS_CMD_OK, "a\nb-\nc\n\nd\n"), - (["string", "split", "", ""], STATUS_CMD_ERROR, "\n"), - (["string", "split", "", "a"], STATUS_CMD_ERROR, "a\n"), - (["string", "split", "", "ab"], STATUS_CMD_OK, "a\nb\n"), - (["string", "split", "", "abc"], STATUS_CMD_OK, "a\nb\nc\n"), - (["string", "split", "-m1", "", "abc"], STATUS_CMD_OK, "a\nbc\n"), - (["string", "split", "-r", "", ""], STATUS_CMD_ERROR, "\n"), - (["string", "split", "-r", "", "a"], STATUS_CMD_ERROR, "a\n"), - (["string", "split", "-r", "", "ab"], STATUS_CMD_OK, "a\nb\n"), - (["string", "split", "-r", "", "abc"], STATUS_CMD_OK, "a\nb\nc\n"), - (["string", "split", "-r", "-m1", "", "abc"], STATUS_CMD_OK, "ab\nc\n"), - (["string", "split", "-q"], STATUS_INVALID_ARGS, ""), - (["string", "split", "-q", ":"], STATUS_CMD_ERROR, ""), - (["string", "split", "-q", "x", "axbxc"], STATUS_CMD_OK, ""), - - (["string", "sub"], STATUS_CMD_ERROR, ""), - (["string", "sub", "abcde"], STATUS_CMD_OK, "abcde\n"), - (["string", "sub", "-l", "x", "abcde"], STATUS_INVALID_ARGS, ""), - (["string", "sub", "-s", "x", "abcde"], STATUS_INVALID_ARGS, ""), - (["string", "sub", "-l0", "abcde"], STATUS_CMD_OK, "\n"), - (["string", "sub", "-l2", "abcde"], STATUS_CMD_OK, "ab\n"), - (["string", "sub", "-l5", "abcde"], STATUS_CMD_OK, "abcde\n"), - (["string", "sub", "-l6", "abcde"], STATUS_CMD_OK, "abcde\n"), - (["string", "sub", "-l-1", "abcde"], STATUS_INVALID_ARGS, ""), - (["string", "sub", "-s0", "abcde"], STATUS_INVALID_ARGS, ""), - (["string", "sub", "-s1", "abcde"], STATUS_CMD_OK, "abcde\n"), - (["string", "sub", "-s5", "abcde"], STATUS_CMD_OK, "e\n"), - (["string", "sub", "-s6", "abcde"], STATUS_CMD_OK, "\n"), - (["string", "sub", "-s-1", "abcde"], STATUS_CMD_OK, "e\n"), - (["string", "sub", "-s-5", "abcde"], STATUS_CMD_OK, "abcde\n"), - (["string", "sub", "-s-6", "abcde"], STATUS_CMD_OK, "abcde\n"), - (["string", "sub", "-s1", "-l0", "abcde"], STATUS_CMD_OK, "\n"), - (["string", "sub", "-s1", "-l1", "abcde"], STATUS_CMD_OK, "a\n"), - (["string", "sub", "-s2", "-l2", "abcde"], STATUS_CMD_OK, "bc\n"), - (["string", "sub", "-s-1", "-l1", "abcde"], STATUS_CMD_OK, "e\n"), - (["string", "sub", "-s-1", "-l2", "abcde"], STATUS_CMD_OK, "e\n"), - (["string", "sub", "-s-3", "-l2", "abcde"], STATUS_CMD_OK, "cd\n"), - (["string", "sub", "-s-3", "-l4", "abcde"], STATUS_CMD_OK, "cde\n"), - (["string", "sub", "-q"], STATUS_CMD_ERROR, ""), - (["string", "sub", "-q", "abcde"], STATUS_CMD_OK, ""), - - (["string", "trim"], STATUS_CMD_ERROR, ""), - (["string", "trim", ""], STATUS_CMD_ERROR, "\n"), - (["string", "trim", " "], STATUS_CMD_OK, "\n"), - (["string", "trim", " \x0C\n\r\t"], STATUS_CMD_OK, "\n"), - (["string", "trim", " a"], STATUS_CMD_OK, "a\n"), - (["string", "trim", "a "], STATUS_CMD_OK, "a\n"), - (["string", "trim", " a "], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-l", " a"], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-l", "a "], STATUS_CMD_ERROR, "a \n"), - (["string", "trim", "-l", " a "], STATUS_CMD_OK, "a \n"), - (["string", "trim", "-r", " a"], STATUS_CMD_ERROR, " a\n"), - (["string", "trim", "-r", "a "], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-r", " a "], STATUS_CMD_OK, " a\n"), - (["string", "trim", "-c", ".", " a"], STATUS_CMD_ERROR, " a\n"), - (["string", "trim", "-c", ".", "a "], STATUS_CMD_ERROR, "a \n"), - (["string", "trim", "-c", ".", " a "], STATUS_CMD_ERROR, " a \n"), - (["string", "trim", "-c", ".", ".a"], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-c", ".", "a."], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-c", ".", ".a."], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-c", "\\/", "/a\\"], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-c", "\\/", "a/"], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-c", "\\/", "\\a/"], STATUS_CMD_OK, "a\n"), - (["string", "trim", "-c", "", ".a."], STATUS_CMD_ERROR, ".a.\n"), - ]; - - for (cmd, expected_status, expected_stdout) in tests { - string_test(cmd, expected_status, expected_stdout); - } - - #[rustfmt::skip] - let qmark_noglob_tests = test_cases![ - (["string", "match", "a*b?c", "axxb?c"], STATUS_CMD_OK, "axxb?c\n"), - (["string", "match", "*?", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "*?", "ab"], STATUS_CMD_ERROR, ""), - (["string", "match", "?*", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "?*", "ab"], STATUS_CMD_ERROR, ""), - (["string", "match", "a*\\?", "abc?"], STATUS_CMD_ERROR, ""), - - (["string", "match", "?", "?"], STATUS_CMD_OK, "?\n"), - (["string", "match", "a??b", "axxb"], STATUS_CMD_ERROR, ""), - (["string", "match", "a??b", "a??b"], STATUS_CMD_OK, "a??b\n"), - (["string", "match", "-i", "a??B", "axxb"], STATUS_CMD_ERROR, ""), - (["string", "match", "-i", "a??b", "A??b"], STATUS_CMD_OK, "A??b\n"), - (["string", "match", "a*\\?", "abc\\?"], STATUS_CMD_OK, "abc\\?\n"), - - (["string", "match", "?", ""], STATUS_CMD_ERROR, ""), - (["string", "match", "?", "ab"], STATUS_CMD_ERROR, ""), - (["string", "match", "??", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "?a", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "a?", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "a??B", "axxb"], STATUS_CMD_ERROR, ""), - ]; - - scoped_test(FeatureFlag::qmark_noglob, true, || { - for (cmd, expected_status, expected_stdout) in qmark_noglob_tests { - string_test(cmd, expected_status, expected_stdout); - } - }); - - #[rustfmt::skip] - let qmark_glob_tests = test_cases![ - (["string", "match", "a*b?c", "axxbyc"], STATUS_CMD_OK, "axxbyc\n"), - (["string", "match", "*?", "a"], STATUS_CMD_OK, "a\n"), - (["string", "match", "*?", "ab"], STATUS_CMD_OK, "ab\n"), - (["string", "match", "?*", "a"], STATUS_CMD_OK, "a\n"), - (["string", "match", "?*", "ab"], STATUS_CMD_OK, "ab\n"), - (["string", "match", "a*\\?", "abc?"], STATUS_CMD_OK, "abc?\n"), - - (["string", "match", "?", ""], STATUS_CMD_ERROR, ""), - (["string", "match", "?", "ab"], STATUS_CMD_ERROR, ""), - (["string", "match", "??", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "?a", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "a?", "a"], STATUS_CMD_ERROR, ""), - (["string", "match", "a??B", "axxb"], STATUS_CMD_ERROR, ""), - ]; - - scoped_test(FeatureFlag::qmark_noglob, false, || { - for (cmd, expected_status, expected_stdout) in qmark_glob_tests { - string_test(cmd, expected_status, expected_stdout); - } - }); -} diff --git a/src/builtins/tests/test_tests.rs b/src/builtins/tests/test_tests.rs deleted file mode 100644 index 3dab58322..000000000 --- a/src/builtins/tests/test_tests.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::builtins::prelude::*; -use crate::builtins::test::test as builtin_test; -use crate::io::{IoChain, OutputStream}; -use crate::tests::prelude::*; - -fn run_one_test_test_mbracket(expected: i32, lst: &[&str], bracket: bool) -> bool { - let parser = TestParser::new(); - let mut argv = Vec::new(); - if bracket { - argv.push(L!("[").to_owned()); - } else { - argv.push(L!("test").to_owned()); - } - for s in lst { - argv.push(WString::from_str(s)); - } - if bracket { - argv.push(L!("]").to_owned()) - }; - - // Convert to &[&wstr]. - let mut argv = argv.iter().map(|s| s.as_ref()).collect::>(); - let mut out = OutputStream::Null; - let mut err = OutputStream::Null; - let io_chain = IoChain::new(); - let mut streams = IoStreams::new(&mut out, &mut err, &io_chain); - - let result = builtin_test(&parser, &mut streams, &mut argv).builtin_status_code(); - - if result != expected { - eprintf!( - "expected builtin_test() to return %s, got %s\n", - expected.to_string(), - result.to_string() - ); - } - result == expected -} - -fn run_test_test(expected: i32, lst: &[&str]) -> bool { - let nobracket = run_one_test_test_mbracket(expected, lst, false); - let bracket = run_one_test_test_mbracket(expected, lst, true); - assert_eq!(nobracket, bracket); - nobracket -} - -fn test_test_brackets() { - // Ensure [ knows it needs a ]. - let parser = TestParser::new(); - - let mut out = OutputStream::Null; - let mut err = OutputStream::Null; - let io_chain = IoChain::new(); - let mut streams = IoStreams::new(&mut out, &mut err, &io_chain); - - let args1 = &mut [L!("["), L!("foo")]; - assert_eq!( - builtin_test(&parser, &mut streams, args1), - Err(STATUS_INVALID_ARGS) - ); - - let args2 = &mut [L!("["), L!("foo"), L!("]")]; - assert_eq!(builtin_test(&parser, &mut streams, args2), Ok(SUCCESS)); - - let args3 = &mut [L!("["), L!("foo"), L!("]"), L!("bar")]; - assert_eq!( - builtin_test(&parser, &mut streams, args3), - Err(STATUS_INVALID_ARGS) - ); -} - -#[rustfmt::skip] -fn test_test() { - assert!(run_test_test(0, &["5", "-ne", "6"])); - assert!(run_test_test(0, &["5", "-eq", "5"])); - assert!(run_test_test(0, &["0", "-eq", "0"])); - assert!(run_test_test(0, &["-1", "-eq", "-1"])); - assert!(run_test_test(0, &["1", "-ne", "-1"])); - assert!(run_test_test(1, &[" 2 ", "-ne", "2"])); - assert!(run_test_test(0, &[" 2", "-eq", "2"])); - assert!(run_test_test(0, &["2 ", "-eq", "2"])); - assert!(run_test_test(0, &[" 2 ", "-eq", "2"])); - assert!(run_test_test(2, &[" 2x", "-eq", "2"])); - assert!(run_test_test(2, &["", "-eq", "0"])); - assert!(run_test_test(2, &["", "-ne", "0"])); - assert!(run_test_test(2, &[" ", "-eq", "0"])); - assert!(run_test_test(2, &[" ", "-ne", "0"])); - assert!(run_test_test(2, &["x", "-eq", "0"])); - assert!(run_test_test(2, &["x", "-ne", "0"])); - assert!(run_test_test(1, &["-1", "-ne", "-1"])); - assert!(run_test_test(0, &["abc", "!=", "def"])); - assert!(run_test_test(1, &["abc", "=", "def"])); - assert!(run_test_test(0, &["5", "-le", "10"])); - assert!(run_test_test(0, &["10", "-le", "10"])); - assert!(run_test_test(1, &["20", "-le", "10"])); - assert!(run_test_test(0, &["-1", "-le", "0"])); - assert!(run_test_test(1, &["0", "-le", "-1"])); - assert!(run_test_test(0, &["15", "-ge", "10"])); - assert!(run_test_test(0, &["15", "-ge", "10"])); - assert!(run_test_test(1, &["!", "15", "-ge", "10"])); - assert!(run_test_test(0, &["!", "!", "15", "-ge", "10"])); - - assert!(run_test_test(0, &[ - "(", "-d", "/", ")", - "-o", - "(", "!", "-d", "/", ")", - ])); - - assert!(run_test_test(0, &["0", "-ne", "1", "-a", "0", "-eq", "0"])); - assert!(run_test_test(0, &["0", "-ne", "1", "-a", "-n", "5"])); - assert!(run_test_test(0, &["-n", "5", "-a", "10", "-gt", "5"])); - assert!(run_test_test(0, &["-n", "3", "-a", "-n", "5"])); - - // Test precedence: - // '0 == 0 || 0 == 1 && 0 == 2' - // should be evaluated as: - // '0 == 0 || (0 == 1 && 0 == 2)' - // and therefore true. If it were - // '(0 == 0 || 0 == 1) && 0 == 2' - // it would be false. - assert!(run_test_test(0, &["0", "=", "0", "-o", "0", "=", "1", "-a", "0", "=", "2"])); - assert!(run_test_test(0, &["-n", "5", "-o", "0", "=", "1", "-a", "0", "=", "2"])); - assert!(run_test_test(1, &["(", "0", "=", "0", "-o", "0", "=", "1", ")", "-a", "0", "=", "2"])); - assert!(run_test_test(0, &["0", "=", "0", "-o", "(", "0", "=", "1", "-a", "0", "=", "2", ")"])); - - // A few lame tests for permissions; these need to be a lot more complete. - assert!(run_test_test(0, &["-e", "/bin/ls"])); - assert!(run_test_test(1, &["-e", "/bin/ls_not_a_path"])); - assert!(run_test_test(0, &["-x", "/bin/ls"])); - assert!(run_test_test(1, &["-x", "/bin/ls_not_a_path"])); - assert!(run_test_test(0, &["-d", "/bin/"])); - assert!(run_test_test(1, &["-d", "/bin/ls"])); - - // This failed at one point. - assert!(run_test_test(1, &["-d", "/bin", "-a", "5", "-eq", "3"])); - assert!(run_test_test(0, &["-d", "/bin", "-o", "5", "-eq", "3"])); - assert!(run_test_test(0,&["-d", "/bin", "-a", "!", "5", "-eq", "3"])); - - // We didn't properly handle multiple "just strings" either. - assert!(run_test_test(0, &["foo"])); - assert!(run_test_test(0, &["foo", "-a", "bar"])); - - // These should be errors. - assert!(run_test_test(1, &["foo", "bar"])); - assert!(run_test_test(1, &["foo", "bar", "baz"])); - - // This crashed. - assert!(run_test_test(1, &["1", "=", "1", "-a", "=", "1"])); - - // Make sure we can treat -S as a parameter instead of an operator. - // https://github.com/fish-shell/fish-shell/issues/601 - assert!(run_test_test(0, &["-S", "=", "-S"])); - assert!(run_test_test(1, &["!", "!", "!", "A"])); - - // Verify that 1. doubles are treated as doubles, and 2. integers that cannot be represented as - // doubles are still treated as integers. - assert!(run_test_test(0, &["4611686018427387904", "-eq", "4611686018427387904"])); - assert!(run_test_test(0, &["4611686018427387904.0", "-eq", "4611686018427387904.0"])); - assert!(run_test_test(0, &["4611686018427387904.00000000000000001", "-eq", "4611686018427387904.0"])); - assert!(run_test_test(1, &["4611686018427387904", "-eq", "4611686018427387905"])); - assert!(run_test_test(0, &["-4611686018427387904", "-ne", "4611686018427387904"])); - assert!(run_test_test(0, &["-4611686018427387904", "-le", "4611686018427387904"])); - assert!(run_test_test(1, &["-4611686018427387904", "-ge", "4611686018427387904"])); - assert!(run_test_test(1, &["4611686018427387904", "-gt", "4611686018427387904"])); - assert!(run_test_test(0, &["4611686018427387904", "-ge", "4611686018427387904"])); - - // test out-of-range numbers - assert!(run_test_test(2, &["99999999999999999999999999", "-ge", "1"])); - assert!(run_test_test(2, &["1", "-eq", "-99999999999999999999999999.9"])); -} - -#[test] -#[serial] -fn test_test_builtin() { - let _cleanup = test_init(); - test_test_brackets(); - test_test(); -} diff --git a/src/common.rs b/src/common.rs index a02be686a..af1e3ed74 100644 --- a/src/common.rs +++ b/src/common.rs @@ -2032,3 +2032,344 @@ macro_rules! env_stack_set_from_env { } }}; } + +/// The highest character number of character to try and escape. +#[cfg(test)] +pub const ESCAPE_TEST_CHAR: usize = 4000; + +#[cfg(test)] +mod tests { + use super::{ + ENCODE_DIRECT_BASE, ENCODE_DIRECT_END, ESCAPE_TEST_CHAR, EscapeFlags, EscapeStringStyle, + ScopeGuard, ScopedCell, ScopedRefCell, UnescapeStringStyle, bytes2wcstring, escape_string, + truncate_at_nul, unescape_string, wcs2bytes, + }; + use crate::util::{get_rng_seed, get_seeded_rng}; + use crate::wchar::{L, WString, wstr}; + use rand::{Rng, RngCore}; + + #[test] + fn test_escape_string() { + let regex = |input| escape_string(input, EscapeStringStyle::Regex); + + // plain text should not be needlessly escaped + assert_eq!(regex(L!("hello world!")), L!("hello world!")); + + // all the following are intended to be ultimately matched literally - even if they + // don't look like that's the intent - so we escape them. + assert_eq!(regex(L!(".ext")), L!("\\.ext")); + assert_eq!(regex(L!("{word}")), L!("\\{word\\}")); + assert_eq!(regex(L!("hola-mundo")), L!("hola\\-mundo")); + assert_eq!( + regex(L!("$17.42 is your total?")), + L!("\\$17\\.42 is your total\\?") + ); + assert_eq!( + regex(L!("not really escaped\\?")), + L!("not really escaped\\\\\\?") + ); + } + + #[test] + pub fn test_unescape_sane() { + const TEST_CASES: &[(&wstr, &wstr)] = &[ + (L!("abcd"), L!("abcd")), + (L!("'abcd'"), L!("abcd")), + (L!("'abcd\\n'"), L!("abcd\\n")), + (L!("\"abcd\\n\""), L!("abcd\\n")), + (L!("\"abcd\\n\""), L!("abcd\\n")), + (L!("\\143"), L!("c")), + (L!("'\\143'"), L!("\\143")), + (L!("\\n"), L!("\n")), // \n normally becomes newline + ]; + + for (input, expected) in TEST_CASES { + let Some(output) = unescape_string(input, UnescapeStringStyle::default()) else { + panic!("Failed to unescape string {input:?}"); + }; + + assert_eq!( + output, *expected, + "In unescaping {input:?}, expected {expected:?} but got {output:?}\n" + ); + } + } + + #[test] + fn test_escape_var() { + const TEST_CASES: &[(&wstr, &wstr)] = &[ + (L!(" a"), L!("_20_a")), + (L!("a B "), L!("a_20_42_20_")), + (L!("a b "), L!("a_20_b_20_")), + (L!(" B"), L!("_20_42_")), + (L!(" f"), L!("_20_f")), + (L!(" 1"), L!("_20_31_")), + (L!("a\nghi_"), L!("a_0A_ghi__")), + ]; + + for (input, expected) in TEST_CASES { + let output = escape_string(input, EscapeStringStyle::Var); + + assert_eq!( + output, *expected, + "In escaping {input:?} with style var, expected {expected:?} but got {output:?}\n" + ); + } + } + + fn escape_test(escape_style: EscapeStringStyle, unescape_style: UnescapeStringStyle) { + let seed: u128 = 92348567983274852905629743984572; + let mut rng = get_seeded_rng(seed); + + let mut random_string = WString::new(); + let mut escaped_string; + for _ in 0..(ESCAPE_TEST_COUNT as u32) { + random_string.clear(); + let length = rng.gen_range(0..=(2 * ESCAPE_TEST_LENGTH)); + for _ in 0..length { + random_string + .push(char::from_u32((rng.next_u32() % ESCAPE_TEST_CHAR as u32) + 1).unwrap()); + } + + escaped_string = escape_string(&random_string, escape_style); + let Some(unescaped_string) = unescape_string(&escaped_string, unescape_style) else { + let slice = escaped_string.as_char_slice(); + panic!("Failed to unescape string {slice:?}"); + }; + assert_eq!( + random_string, unescaped_string, + "Escaped and then unescaped string {random_string:?}, but got back a different string {unescaped_string:?}. The intermediate escape looked like {escaped_string:?}." + ); + } + } + + #[test] + fn test_escape_random_script() { + escape_test(EscapeStringStyle::default(), UnescapeStringStyle::default()); + } + + #[test] + fn test_escape_random_var() { + escape_test(EscapeStringStyle::Var, UnescapeStringStyle::Var); + } + + #[test] + fn test_escape_random_url() { + escape_test(EscapeStringStyle::Url, UnescapeStringStyle::Url); + } + + #[test] + fn test_escape_no_printables() { + // Verify that ESCAPE_NO_PRINTABLES also escapes backslashes so we don't regress on issue #3892. + let random_string = L!("line 1\\n\nline 2").to_owned(); + let escaped_string = escape_string( + &random_string, + EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED), + ); + let Some(unescaped_string) = + unescape_string(&escaped_string, UnescapeStringStyle::default()) + else { + panic!("Failed to unescape string <{escaped_string}>"); + }; + + assert_eq!( + random_string, unescaped_string, + "Escaped and then unescaped string '{random_string}', but got back a different string '{unescaped_string}'" + ); + } + + /// The number of tests to run. + const ESCAPE_TEST_COUNT: usize = 20_000; + /// The average length of strings to unescape. + const ESCAPE_TEST_LENGTH: usize = 100; + + /// Helper to convert a narrow string to a sequence of hex digits. + fn bytes2hex(input: &[u8]) -> String { + let mut output = "".to_string(); + for byte in input { + output += &format!("0x{:2X} ", *byte); + } + output + } + + /// Test wide/narrow conversion by creating random strings and verifying that the original + /// string comes back through double conversion. + #[test] + fn test_convert() { + let seed = get_rng_seed(); + let mut rng = get_seeded_rng(seed); + let mut origin = Vec::new(); + + for _ in 0..ESCAPE_TEST_COUNT { + let length: usize = rng.gen_range(0..=(2 * ESCAPE_TEST_LENGTH)); + origin.resize(length, 0); + rng.fill_bytes(&mut origin); + + let w = bytes2wcstring(&origin[..]); + let n = wcs2bytes(&w); + assert_eq!( + origin, + n, + "Conversion cycle of string:\n{:4} chars: {}\n\ + produced different string:\n\ + {:4} chars: {}\n + Use this seed to reproduce: {}", + origin.len(), + &bytes2hex(&origin), + n.len(), + &bytes2hex(&n), + seed, + ); + } + } + + /// Verify correct behavior with embedded nulls. + #[test] + fn test_convert_nulls() { + let input = L!("AAA\0BBB"); + let out_str = wcs2bytes(input); + assert_eq!( + input.chars().collect::>(), + std::str::from_utf8(&out_str) + .unwrap() + .chars() + .collect::>() + ); + + let out_wstr = bytes2wcstring(&out_str); + assert_eq!(input, &out_wstr); + } + + /// Verify that ASCII narrow->wide conversions are correct. + #[test] + fn test_convert_ascii() { + let mut s = vec![b'\0'; 4096]; + for (i, c) in s.iter_mut().enumerate() { + *c = u8::try_from(i % 10).unwrap() + b'0'; + } + + // Test a variety of alignments. + for left in 0..16 { + for right in 0..16 { + let len = s.len() - left - right; + let input = &s[left..left + len]; + let wide = bytes2wcstring(input); + let narrow = wcs2bytes(&wide); + assert_eq!(narrow, input); + } + } + + // Put some non-ASCII bytes in and ensure it all still works. + for i in 0..s.len() { + let saved = s[i]; + s[i] = 0xF7; + assert_eq!(wcs2bytes(&bytes2wcstring(&s)), s); + s[i] = saved; + } + } + + /// fish uses the private-use range to encode bytes that are not valid UTF-8. + /// If the input decodes to these private-use codepoints, + /// then fish should also use the direct encoding for those bytes. + /// Verify that characters in the private use area are correctly round-tripped. See #7723. + #[test] + fn test_convert_private_use() { + for c in ENCODE_DIRECT_BASE..ENCODE_DIRECT_END { + // A `char` represents an Unicode scalar value, which takes up at most 4 bytes when encoded in UTF-8. + // TODO MSRV(1.92?) replace 4 by `char::MAX_LEN_UTF8` once that's available in our MSRV. + // https://doc.rust-lang.org/std/primitive.char.html#associatedconstant.MAX_LEN_UTF8 + let mut converted = [0_u8; 4]; + let s = c.encode_utf8(&mut converted).as_bytes(); + // Ask fish to decode this via bytes2wcstring. + // bytes2wcstring should notice that the decoded form collides with its private use + // and encode it directly. + let ws = bytes2wcstring(s); + + // Each byte should be encoded directly, and round tripping should work. + assert_eq!(ws.len(), s.len()); + assert_eq!(wcs2bytes(&ws), s); + } + } + + #[test] + fn test_scoped_cell() { + let cell = ScopedCell::new(42); + + { + let _guard = cell.scoped_mod(|x| *x += 1); + assert_eq!(cell.get(), 43); + } + + assert_eq!(cell.get(), 42); + } + + #[test] + fn test_scoped_refcell() { + #[derive(Debug, PartialEq, Clone)] + struct Data { + x: i32, + y: i32, + } + + let cell = ScopedRefCell::new(Data { x: 1, y: 2 }); + + { + let _guard = cell.scoped_set(10, |d| &mut d.x); + assert_eq!(cell.borrow().x, 10); + } + assert_eq!(cell.borrow().x, 1); + + { + let _guard = cell.scoped_replace(Data { x: 42, y: 99 }); + assert_eq!(*cell.borrow(), Data { x: 42, y: 99 }); + } + assert_eq!(*cell.borrow(), Data { x: 1, y: 2 }); + } + + #[test] + fn test_scope_guard() { + let relaxed = std::sync::atomic::Ordering::Relaxed; + let counter = std::sync::atomic::AtomicUsize::new(0); + { + let guard = ScopeGuard::new(123, |arg| { + assert_eq!(arg, 123); + counter.fetch_add(1, relaxed); + }); + assert_eq!(counter.load(relaxed), 0); + std::mem::drop(guard); + assert_eq!(counter.load(relaxed), 1); + } + // commit also invokes the callback. + { + let guard = ScopeGuard::new(123, |arg| { + assert_eq!(arg, 123); + counter.fetch_add(1, relaxed); + }); + assert_eq!(counter.load(relaxed), 1); + ScopeGuard::commit(guard); + assert_eq!(counter.load(relaxed), 2); + } + } + + #[test] + fn test_truncate_at_nul() { + assert_eq!(truncate_at_nul(L!("abc\0def")), L!("abc")); + assert_eq!(truncate_at_nul(L!("abc")), L!("abc")); + assert_eq!(truncate_at_nul(L!("\0abc")), L!("")); + } +} + +#[cfg(feature = "benchmark")] +#[cfg(test)] +mod bench { + extern crate test; + use crate::common::bytes2wcstring; + use test::Bencher; + + #[bench] + fn bench_convert_ascii(b: &mut Bencher) { + let s: [u8; 128 * 1024] = std::array::from_fn(|i| b'0' + u8::try_from(i % 10).unwrap()); + b.bytes = u64::try_from(s.len()).unwrap(); + b.iter(|| bytes2wcstring(&s)); + } +} diff --git a/src/complete.rs b/src/complete.rs index 10a2da511..b5242ed14 100644 --- a/src/complete.rs +++ b/src/complete.rs @@ -2577,3 +2577,700 @@ pub struct CompletionRequestOptions { /// If set, we do not require a prefix match pub fuzzy_match: bool, } + +#[cfg(test)] +mod tests { + use super::{ + CompleteFlags, CompleteOptionType, CompletionMode, CompletionRequestOptions, complete, + complete_add, complete_add_wrapper, complete_get_wrap_targets, complete_remove_wrapper, + sort_and_prioritize, + }; + use crate::abbrs::{self, Abbreviation, with_abbrs_mut}; + use crate::env::{EnvMode, Environment}; + use crate::io::IoChain; + use crate::operation_context::{ + EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT, OperationContext, no_cancel, + }; + use crate::reader::completion_apply_to_command_line; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + use crate::wcstringutil::join_strings; + use std::collections::HashMap; + use std::ffi::CString; + + /// Joins a std::vector via commas. + fn comma_join(lst: Vec) -> WString { + join_strings(&lst, ',') + } + + #[test] + #[serial] + fn test_complete() { + let _cleanup = test_init(); + let vars = PwdEnvironment { + parent: TestEnvironment { + vars: HashMap::from([ + (WString::from_str("Foo1"), WString::new()), + (WString::from_str("Foo2"), WString::new()), + (WString::from_str("Foo3"), WString::new()), + (WString::from_str("Bar1"), WString::new()), + (WString::from_str("Bar2"), WString::new()), + (WString::from_str("Bar3"), WString::new()), + (WString::from_str("alpha"), WString::new()), + (WString::from_str("ALPHA!"), WString::new()), + (WString::from_str("gamma1"), WString::new()), + (WString::from_str("GAMMA2"), WString::new()), + (WString::from_str("SOMEDIR"), L!("/").to_owned()), + (WString::from_str("SOMEVAR"), WString::new()), + ]), + }, + }; + + let parser = TestParser::new(); + let ctx = OperationContext::test_only_foreground(&parser, &vars, Box::new(no_cancel)); + + let do_complete = + |cmd: &wstr, flags: CompletionRequestOptions| complete(cmd, flags, &ctx).0; + + let mut completions = do_complete(L!("$"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + assert_eq!( + completions + .into_iter() + .map(|c| c.completion.to_string()) + .collect::>(), + [ + "alpha", "ALPHA!", "Bar1", "Bar2", "Bar3", "Foo1", "Foo2", "Foo3", "gamma1", + "GAMMA2", "PWD", "SOMEDIR", "SOMEVAR", + ] + .into_iter() + .map(|s| s.to_owned()) + .collect::>() + ); + + // Smartcase test. Lowercase inputs match both lowercase and uppercase. + let mut completions = do_complete(L!("$a"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + + assert_eq!(completions.len(), 2); + assert_eq!(completions[0].completion, L!("$ALPHA!")); + assert_eq!(completions[1].completion, L!("lpha")); + + let mut completions = do_complete(L!("$F"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + assert_eq!(completions.len(), 3); + assert_eq!(completions[0].completion, L!("oo1")); + assert_eq!(completions[1].completion, L!("oo2")); + assert_eq!(completions[2].completion, L!("oo3")); + + completions = do_complete(L!("$1"), CompletionRequestOptions::default()); + sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); + assert_eq!(completions, vec![]); + + let fuzzy_options = CompletionRequestOptions { + fuzzy_match: true, + ..Default::default() + }; + let mut completions = do_complete(L!("$1"), fuzzy_options); + sort_and_prioritize(&mut completions, fuzzy_options); + assert_eq!(completions.len(), 3); + assert_eq!(completions[0].completion, L!("$Bar1")); + assert_eq!(completions[1].completion, L!("$Foo1")); + assert_eq!(completions[2].completion, L!("$gamma1")); + + std::fs::create_dir_all("test/complete_test").unwrap(); + std::fs::write("test/complete_test/has space", []).unwrap(); + std::fs::write("test/complete_test/bracket[abc]", []).unwrap(); + #[cfg(not(cygwin))] + // Backslashes and colons are not legal filename characters on WIN32/CYGWIN + { + std::fs::write(r"test/complete_test/gnarlybracket\[abc]", []).unwrap(); + std::fs::write(r"test/complete_test/colon:abc", []).unwrap(); + } + std::fs::write(r"test/complete_test/equal=abc", []).unwrap(); + // On MSYS, the executable bit cannot be set manually, is set automatically + // based on the file content/type. So make it a shell script + std::fs::write("test/complete_test/testfile", "#!/bin/sh").unwrap(); + let testfile = CString::new("test/complete_test/testfile").unwrap(); + assert_eq!(unsafe { libc::chmod(testfile.as_ptr(), 0o700,) }, 0); + std::fs::create_dir_all("test/complete_test/foo1").unwrap(); + std::fs::create_dir_all("test/complete_test/foo2").unwrap(); + std::fs::create_dir_all("test/complete_test/foo3").unwrap(); + + completions = do_complete( + L!("echo (test/complete_test/testfil"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("e")); + + completions = do_complete( + L!("echo (ls test/complete_test/testfil"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("e")); + + completions = do_complete( + L!("echo (command ls test/complete_test/testfil"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("e")); + + // Completing after spaces - see #2447 + completions = do_complete( + L!("echo (ls test/complete_test/has\\ "), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("space")); + + #[cfg(not(cygwin))] + // Backslashes and colons are not legal filename characters on WIN32/CYGWIN + { + completions = do_complete( + L!(": test/complete_test/colon:"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("abc")); + } + + macro_rules! unique_completion_applies_as { + ( $cmdline:expr, $completion_result:expr, $applied:expr $(,)? ) => { + let cmdline = L!($cmdline); + let completions = do_complete(cmdline, CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!( + completions[0].completion, + L!($completion_result), + "completion mismatch" + ); + let mut cursor = cmdline.len(); + let newcmdline = completion_apply_to_command_line( + &ctx, + &completions[0].completion, + completions[0].flags, + cmdline, + &mut cursor, + /*append_only=*/ false, + /*is_unique=*/ true, + ); + assert_eq!(newcmdline, L!($applied), "apply result mismatch"); + }; + } + + unique_completion_applies_as!( + "touch test/complete_test/{testfi", + r"le", + "touch test/complete_test/{testfile", + ); + + // Brackets - see #5831 + unique_completion_applies_as!( + "touch test/complete_test/bracket[", + "test/complete_test/bracket[abc]", + "touch 'test/complete_test/bracket[abc]' ", + ); + unique_completion_applies_as!( + "echo (ls test/complete_test/bracket[", + "test/complete_test/bracket[abc]", + "echo (ls 'test/complete_test/bracket[abc]' ", + ); + #[cfg(not(cygwin))] + // Backslashes are not legal filename characters on WIN32/CYGWIN + { + unique_completion_applies_as!( + r"touch test/complete_test/gnarlybracket\\[", + r"test/complete_test/gnarlybracket\[abc]", + r"touch 'test/complete_test/gnarlybracket\\[abc]' ", + ); + unique_completion_applies_as!( + r"a=test/complete_test/bracket[", + r"test/complete_test/bracket[abc]", + r"a='test/complete_test/bracket[abc]' ", + ); + } + + #[cfg(not(cygwin))] + // Colons are not legal filename characters on WIN32/CYGWIN + { + unique_completion_applies_as!( + r"touch test/complete_test/colon", + r":abc", + r"touch test/complete_test/colon:abc ", + ); + unique_completion_applies_as!( + r"touch test/complete_test/colon:", + r"abc", + r"touch test/complete_test/colon:abc ", + ); + unique_completion_applies_as!( + r#"touch "test/complete_test/colon:"#, + r"abc", + r#"touch "test/complete_test/colon:abc" "#, + ); + } + + unique_completion_applies_as!("echo $SOMEV", r"AR", "echo $SOMEVAR "); + unique_completion_applies_as!("echo $SOMED", r"IR", "echo $SOMEDIR/"); + unique_completion_applies_as!(r#"echo "$SOMED"#, r"IR", r#"echo "$SOMEDIR/"#); + + // #8820 + let mut cursor_pos = 11; + let newcmdline = completion_apply_to_command_line( + &ctx, + L!("Debug/"), + CompleteFlags::REPLACES_TOKEN | CompleteFlags::NO_SPACE, + L!("mv debug debug"), + &mut cursor_pos, + true, + /*is_unique=*/ false, + ); + assert_eq!(newcmdline, L!("mv debug Debug/")); + + // Add a function and test completing it in various ways. + parser.eval(L!("function scuttlebutt; end"), &IoChain::new()); + + // Complete a function name. + completions = do_complete(L!("echo (scuttlebut"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("t")); + + // But not with the command prefix. + completions = do_complete( + L!("echo (command scuttlebut"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + + // Not with the builtin prefix. + let completions = do_complete( + L!("echo (builtin scuttlebut"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + + // Not after a redirection. + let completions = do_complete( + L!("echo hi > scuttlebut"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + + // Trailing spaces (#1261). + let no_files = CompletionMode { + no_files: true, + ..Default::default() + }; + complete_add( + L!("foobarbaz").into(), + false, + WString::new(), + CompleteOptionType::ArgsOnly, + no_files, + vec![], + L!("qux").into(), + WString::new(), + CompleteFlags::AUTO_SPACE, + ); + let completions = do_complete(L!("foobarbaz "), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("qux")); + + // Don't complete variable names in single quotes (#1023). + let completions = do_complete(L!("echo '$Foo"), CompletionRequestOptions::default()); + assert_eq!(completions, vec![]); + let completions = do_complete(L!("echo \\$Foo"), CompletionRequestOptions::default()); + assert_eq!(completions, vec![]); + + // File completions. + let completions = do_complete( + L!("cat test/complete_test/te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete( + L!("echo sup > test/complete_test/te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete( + L!("echo sup > test/complete_test/te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + + parser.pushd("test/complete_test"); + let completions = do_complete(L!("cat te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + assert!(!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN))); + assert!( + !(completions[0] + .flags + .contains(CompleteFlags::DUPLICATES_ARGUMENT)) + ); + let completions = do_complete(L!("cat testfile te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + assert!( + completions[0] + .flags + .contains(CompleteFlags::DUPLICATES_ARGUMENT) + ); + let completions = do_complete(L!("cat testfile TE"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("testfile")); + assert!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN)); + assert!( + completions[0] + .flags + .contains(CompleteFlags::DUPLICATES_ARGUMENT) + ); + let completions = do_complete( + L!("something --abc=te"), + CompletionRequestOptions::default(), + ); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete(L!("something -abc=te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete(L!("something abc=te"), CompletionRequestOptions::default()); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("stfile")); + let completions = do_complete( + L!("something abc=stfile"), + CompletionRequestOptions::default(), + ); + assert_eq!(&completions, &[]); + let completions = do_complete(L!("something abc=stfile"), fuzzy_options); + assert_eq!(completions.len(), 1); + assert_eq!(completions[0].completion, L!("abc=testfile")); + + // Zero escapes can cause problems. See issue #1631. + let completions = do_complete(L!("cat foo\\0"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + let completions = do_complete(L!("cat foo\\0bar"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + let completions = do_complete(L!("cat \\0"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + let mut completions = do_complete(L!("cat te\\0"), CompletionRequestOptions::default()); + assert_eq!(&completions, &[]); + + parser.popd(); + completions.clear(); + + // Test abbreviations. + parser.eval( + L!("function testabbrsonetwothreefour; end"), + &IoChain::new(), + ); + with_abbrs_mut(|abbrset| { + abbrset.add(Abbreviation::new( + L!("somename").into(), + L!("testabbrsonetwothreezero").into(), + L!("expansion").into(), + abbrs::Position::Command, + false, + )) + }); + + let completions = complete( + L!("testabbrsonetwothree"), + CompletionRequestOptions::default(), + &parser.context(), + ) + .0; + assert_eq!(completions.len(), 2); + // Abbreviations should not have a space after them. + assert_eq!(completions[0].completion, L!("zero")); + assert!(completions[0].flags.contains(CompleteFlags::NO_SPACE)); + with_abbrs_mut(|abbrset| { + abbrset.erase(L!("testabbrsonetwothreezero")); + }); + assert_eq!(completions[1].completion, L!("four")); + assert!(!completions[1].flags.contains(CompleteFlags::NO_SPACE)); + + // Test wraps. + assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty()); + complete_add_wrapper(L!("wrapper1").into(), L!("wrapper2").into()); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper1"))), + L!("wrapper2") + ); + complete_add_wrapper(L!("wrapper2").into(), L!("wrapper3").into()); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper1"))), + L!("wrapper2") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper2"))), + L!("wrapper3") + ); + complete_add_wrapper(L!("wrapper3").into(), L!("wrapper1").into()); // loop! + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper1"))), + L!("wrapper2") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper2"))), + L!("wrapper3") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper3"))), + L!("wrapper1") + ); + complete_remove_wrapper(L!("wrapper1").into(), L!("wrapper2")); + assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty()); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper2"))), + L!("wrapper3") + ); + assert_eq!( + comma_join(complete_get_wrap_targets(L!("wrapper3"))), + L!("wrapper1") + ); + + // Test cd wrapping chain + parser.pushd("test/complete_test"); + + complete_add_wrapper(L!("cdwrap1").into(), L!("cd").into()); + complete_add_wrapper(L!("cdwrap2").into(), L!("cdwrap1").into()); + + let mut cd_compl = do_complete(L!("cd "), CompletionRequestOptions::default()); + sort_and_prioritize(&mut cd_compl, CompletionRequestOptions::default()); + + let mut cdwrap1_compl = do_complete(L!("cdwrap1 "), CompletionRequestOptions::default()); + sort_and_prioritize(&mut cdwrap1_compl, CompletionRequestOptions::default()); + + let mut cdwrap2_compl = do_complete(L!("cdwrap2 "), CompletionRequestOptions::default()); + sort_and_prioritize(&mut cdwrap2_compl, CompletionRequestOptions::default()); + + let min_compl_size = cd_compl + .len() + .min(cdwrap1_compl.len().min(cdwrap2_compl.len())); + + assert_eq!(cd_compl.len(), min_compl_size); + assert_eq!(cdwrap1_compl.len(), min_compl_size); + assert_eq!(cdwrap2_compl.len(), min_compl_size); + for i in 0..min_compl_size { + assert_eq!(cd_compl[i].completion, cdwrap1_compl[i].completion); + assert_eq!(cdwrap1_compl[i].completion, cdwrap2_compl[i].completion); + } + + complete_remove_wrapper(L!("cdwrap1").into(), L!("cd")); + complete_remove_wrapper(L!("cdwrap2").into(), L!("cdwrap1")); + parser.popd(); + } + + // Testing test_autosuggest_suggest_special, in particular for properly handling quotes and + // backslashes. + #[test] + #[serial] + fn test_autosuggest_suggest_special() { + let _cleanup = test_init(); + let parser = TestParser::new(); + macro_rules! perform_one_autosuggestion_cd_test { + ($command:literal, $expected:literal, $vars:expr) => { + let mut comps = complete( + L!($command), + CompletionRequestOptions::autosuggest(), + &OperationContext::background($vars, EXPANSION_LIMIT_BACKGROUND), + ) + .0; + + let expects_error = $expected == ""; + + assert_eq!(expects_error, comps.is_empty()); + if !expects_error { + sort_and_prioritize(&mut comps, CompletionRequestOptions::default()); + let suggestion = &comps[0]; + assert_eq!(suggestion.completion, L!($expected)); + } + }; + } + + macro_rules! perform_one_completion_cd_test { + ($command:literal, $expected:literal) => { + let mut comps = complete( + L!($command), + CompletionRequestOptions::default(), + &OperationContext::foreground( + &parser, + Box::new(no_cancel), + EXPANSION_LIMIT_DEFAULT, + ), + ) + .0; + + let expects_error = $expected == ""; + + assert_eq!(expects_error, comps.is_empty()); + if !expects_error { + sort_and_prioritize(&mut comps, CompletionRequestOptions::default()); + let suggestion = &comps[0]; + assert_eq!(suggestion.completion, L!($expected)); + } + }; + } + + std::fs::create_dir_all("test/autosuggest_test/0foobar").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/1foo bar").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/2foo bar").unwrap(); + // Cygwin disallows backslashes in filenames. + #[cfg(not(cygwin))] + std::fs::create_dir_all("test/autosuggest_test/3foo\\bar").unwrap(); + // a path with a single quote + std::fs::create_dir_all("test/autosuggest_test/4foo'bar").unwrap(); + // a path with a double quote + std::fs::create_dir_all("test/autosuggest_test/5foo\"bar").unwrap(); + // This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia` + // test below. + // Fake out the home directory + parser.vars().set_one( + L!("HOME"), + EnvMode::LOCAL | EnvMode::EXPORT, + L!("test/test-home").to_owned(), + ); + std::fs::create_dir_all("test/test-home/test_autosuggest_suggest_special/").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi4").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi42").unwrap(); + std::fs::create_dir_all("test/autosuggest_test/start/unique2/.hiddenDir/moreStuff") + .unwrap(); + + // Ensure symlink don't cause us to chase endlessly. + // Symbolic link is complicated on Windows/Cygwin (see winsymlinks). The behavior + // depends on the env var CYGWIN (or MSYS). Currently, the default is to copy + // the target, which will fail with recursive symlinks + #[cfg(not(cygwin))] + { + std::fs::create_dir_all("test/autosuggest_test/has_loop/loopy").unwrap(); + let _ = std::fs::remove_file("test/autosuggest_test/has_loop/loopy/loop"); + std::os::unix::fs::symlink("../loopy", "test/autosuggest_test/has_loop/loopy/loop") + .unwrap(); + } + + let wd = "test/autosuggest_test"; + + let mut vars = PwdEnvironment::default(); + vars.parent.vars.insert( + L!("HOME").into(), + parser.vars().get(L!("HOME")).unwrap().as_string(), + ); + + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/2", "foo bar/", &vars); + #[cfg(not(cygwin))] + // Windows does not allow backslashes in filenames + { + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/3", "foo\\bar/", &vars); + } + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/5", "foo\"bar/", &vars); + + vars.parent + .vars + .insert(L!("AUTOSUGGEST_TEST_LOC").to_owned(), WString::from_str(wd)); + perform_one_autosuggestion_cd_test!("cd $AUTOSUGGEST_TEST_LOC/0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd ~/test_autosuggest_suggest_specia", "l/", &vars); + + perform_one_autosuggestion_cd_test!( + "cd test/autosuggest_test/start/", + "unique2/unique3/", + &vars + ); + + #[cfg(not(cygwin))] + // We skipped the creation of `loopy/loop` above + perform_one_autosuggestion_cd_test!( + "cd test/autosuggest_test/has_loop/", + "loopy/loop/", + &vars + ); + + parser.pushd(wd); + perform_one_autosuggestion_cd_test!("cd 0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd '0", "foobar/", &vars); + perform_one_autosuggestion_cd_test!("cd 1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '1", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"2", "foo bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '2", "foo bar/", &vars); + #[cfg(not(cygwin))] + // Windows does not allow backslashes in filenames + { + perform_one_autosuggestion_cd_test!("cd 3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"3", "foo\\bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '3", "foo\\bar/", &vars); + } + perform_one_autosuggestion_cd_test!("cd 4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '4", "foo'bar/", &vars); + perform_one_autosuggestion_cd_test!("cd 5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd \"5", "foo\"bar/", &vars); + perform_one_autosuggestion_cd_test!("cd '5", "foo\"bar/", &vars); + + // A single quote should defeat tilde expansion. + perform_one_autosuggestion_cd_test!( + "cd '~/test_autosuggest_suggest_specia'", + "", + &vars + ); + + // Don't crash on ~ (issue #2696). Note this is cwd dependent. + std::fs::create_dir_all("~absolutelynosuchuser/path1/path2/").unwrap(); + perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchus", "er/path1/path2/", &vars); + perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchuser/", "path1/path2/", &vars); + perform_one_completion_cd_test!("cd ~absolutelynosuchus", "er/"); + perform_one_completion_cd_test!("cd ~absolutelynosuchuser/", "path1/"); + + parser + .vars() + .remove(L!("HOME"), EnvMode::LOCAL | EnvMode::EXPORT); + parser.popd(); + } + + #[test] + #[serial] + fn test_autosuggestion_ignores() { + let _cleanup = test_init(); + // Testing scenarios that should produce no autosuggestions + macro_rules! perform_one_autosuggestion_should_ignore_test { + ($command:literal) => { + let comps = complete( + L!($command), + CompletionRequestOptions::autosuggest(), + &OperationContext::empty(), + ) + .0; + assert_eq!(&comps, &[]); + }; + } + // Do not do file autosuggestions immediately after certain statement terminators - see #1631. + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST|"); + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST&"); + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST#comment"); + perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST;"); + } +} diff --git a/src/editable_line.rs b/src/editable_line.rs index 773dbc6de..9050d976c 100644 --- a/src/editable_line.rs +++ b/src/editable_line.rs @@ -364,3 +364,80 @@ pub fn range_of_line_at_cursor(buffer: &wstr, cursor: usize) -> Range { pub fn line_at_cursor(buffer: &wstr, cursor: usize) -> &wstr { &buffer[range_of_line_at_cursor(buffer, cursor)] } + +#[cfg(test)] +mod tests { + use crate::{ + editable_line::{Edit, EditableLine}, + wchar::prelude::*, + }; + + #[test] + fn test_undo() { + let mut line = EditableLine::default(); + + let insert = |line: &EditableLine| line.position()..line.position(); + + assert!(!line.undo()); // nothing to undo + assert!(line.text().is_empty()); + assert_eq!(line.position(), 0); + line.push_edit(Edit::new(0..0, L!("a b c").to_owned()), true); + assert_eq!(line.text(), L!("a b c").to_owned()); + assert_eq!(line.position(), 5); + line.set_position(2); + line.push_edit(Edit::new(2..3, L!("B").to_owned()), true); // replacement right of cursor + assert_eq!(line.text(), L!("a B c").to_owned()); + line.undo(); + assert_eq!(line.text(), L!("a b c").to_owned()); + assert_eq!(line.position(), 2); + line.redo(); + assert_eq!(line.text(), L!("a B c").to_owned()); + assert_eq!(line.position(), 3); + + assert!(!line.redo()); // nothing to redo + + line.push_edit(Edit::new(0..2, L!("").to_owned()), true); // deletion left of cursor + assert_eq!(line.text(), L!("B c").to_owned()); + assert_eq!(line.position(), 1); + line.undo(); + assert_eq!(line.text(), L!("a B c").to_owned()); + assert_eq!(line.position(), 3); + line.redo(); + assert_eq!(line.text(), L!("B c").to_owned()); + assert_eq!(line.position(), 1); + + line.push_edit(Edit::new(0..line.len(), L!("a b c").to_owned()), true); // replacement left and right of cursor + assert_eq!(line.text(), L!("a b c").to_owned()); + assert_eq!(line.position(), 5); + + // Undo coalesced edits + line.push_edit(Edit::new(0..line.len(), L!("").to_owned()), false); + line.push_edit(Edit::new(insert(&line), L!("a").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!("b").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!("c").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!(" ").to_owned()), true); + line.undo(); + line.undo(); + line.redo(); + assert_eq!(line.text(), L!("abc").to_owned()); + // This removes the space insertion from the history, but does not coalesce with the first edit. + line.push_edit(Edit::new(insert(&line), L!("d").to_owned()), true); + line.push_edit(Edit::new(insert(&line), L!("e").to_owned()), true); + assert_eq!(line.text(), L!("abcde").to_owned()); + line.undo(); + assert_eq!(line.text(), L!("abc").to_owned()); + } + + #[test] + fn test_undo_group() { + let mut line = EditableLine::default(); + line.begin_edit_group(); + line.push_edit(Edit::new(0..0, L!("a").to_owned()), true); + line.end_edit_group(); + line.begin_edit_group(); + line.push_edit(Edit::new(1..1, L!("b").to_owned()), true); + line.end_edit_group(); + line.undo(); + assert_eq!(line.text(), "a"); + } +} diff --git a/src/env/environment.rs b/src/env/environment.rs index 983cc517f..bcaa83138 100644 --- a/src/env/environment.rs +++ b/src/env/environment.rs @@ -127,8 +127,7 @@ pub struct EnvDyn { } impl EnvDyn { - // Exposed for testing. - pub fn new(inner: Box) -> Self { + fn new(inner: Box) -> Self { Self { inner } } } @@ -842,3 +841,95 @@ pub fn env_init(paths: Option<&ConfigPaths>, do_uvars: bool, default_paths: bool } } } + +#[cfg(test)] +mod tests { + use super::{EnvMode, EnvStack, Environment}; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + + #[test] + #[serial] + fn test_env_snapshot() { + let _cleanup = test_init(); + std::fs::create_dir_all("test/fish_env_snapshot_test/").unwrap(); + let parser = TestParser::new(); + let vars = parser.vars(); + parser.pushd("test/fish_env_snapshot_test/"); + vars.push(true); + let before_pwd = vars.get(L!("PWD")).unwrap().as_string(); + vars.set_one( + L!("test_env_snapshot_var"), + EnvMode::default(), + L!("before").to_owned(), + ); + let snapshot = vars.snapshot(); + vars.set_one(L!("PWD"), EnvMode::default(), L!("/newdir").to_owned()); + vars.set_one( + L!("test_env_snapshot_var"), + EnvMode::default(), + L!("after").to_owned(), + ); + vars.set_one( + L!("test_env_snapshot_var_2"), + EnvMode::default(), + L!("after").to_owned(), + ); + + // vars should be unaffected by the snapshot + assert_eq!(vars.get(L!("PWD")).unwrap().as_string(), L!("/newdir")); + assert_eq!( + vars.get(L!("test_env_snapshot_var")).unwrap().as_string(), + L!("after") + ); + assert_eq!( + vars.get(L!("test_env_snapshot_var_2")).unwrap().as_string(), + L!("after") + ); + + // snapshot should have old values of vars + assert_eq!(snapshot.get(L!("PWD")).unwrap().as_string(), before_pwd); + assert_eq!( + snapshot + .get(L!("test_env_snapshot_var")) + .unwrap() + .as_string(), + L!("before") + ); + assert_eq!(snapshot.get(L!("test_env_snapshot_var_2")), None); + + // snapshots see global var changes except for perproc like PWD + vars.set_one( + L!("test_env_snapshot_var_3"), + EnvMode::GLOBAL, + L!("reallyglobal").to_owned(), + ); + assert_eq!( + vars.get(L!("test_env_snapshot_var_3")).unwrap().as_string(), + L!("reallyglobal") + ); + assert_eq!( + snapshot + .get(L!("test_env_snapshot_var_3")) + .unwrap() + .as_string(), + L!("reallyglobal") + ); + + vars.pop(); + parser.popd(); + } + + // Can't push/pop from globals. + #[test] + #[should_panic] + fn test_no_global_push() { + EnvStack::globals().push(true); + } + + #[test] + #[should_panic] + fn test_no_global_pop() { + EnvStack::globals().pop(); + } +} diff --git a/src/env/environment_impl.rs b/src/env/environment_impl.rs index 3ef6f4b1e..585c5188e 100644 --- a/src/env/environment_impl.rs +++ b/src/env/environment_impl.rs @@ -1155,23 +1155,29 @@ pub fn lock(&self) -> EnvMutexGuard<'_, T> { unsafe impl Sync for EnvMutex {} unsafe impl Send for EnvMutex {} -#[test] -fn test_colon_split() { - assert_eq!(colon_split(&[L!("foo")]), &[L!("foo")]); - assert_eq!( - colon_split(&[L!("foo:bar:baz")]), - &[L!("foo"), L!("bar"), L!("baz")] - ); - assert_eq!( - colon_split(&[L!("foo:bar"), L!("baz")]), - &[L!("foo"), L!("bar"), L!("baz")] - ); - assert_eq!( - colon_split(&[L!("foo:bar"), L!("baz")]), - &[L!("foo"), L!("bar"), L!("baz")] - ); - assert_eq!( - colon_split(&[L!("1:"), L!("2:"), L!(":3:")]), - &[L!("1"), L!(""), L!("2"), L!(""), L!(""), L!("3"), L!("")] - ); +#[cfg(test)] +mod tests { + use super::colon_split; + use crate::wchar::prelude::*; + + #[test] + fn test_colon_split() { + assert_eq!(colon_split(&[L!("foo")]), &[L!("foo")]); + assert_eq!( + colon_split(&[L!("foo:bar:baz")]), + &[L!("foo"), L!("bar"), L!("baz")] + ); + assert_eq!( + colon_split(&[L!("foo:bar"), L!("baz")]), + &[L!("foo"), L!("bar"), L!("baz")] + ); + assert_eq!( + colon_split(&[L!("foo:bar"), L!("baz")]), + &[L!("foo"), L!("bar"), L!("baz")] + ); + assert_eq!( + colon_split(&[L!("1:"), L!("2:"), L!(":3:")]), + &[L!("1"), L!(""), L!("2"), L!(""), L!(""), L!("3"), L!("")] + ); + } } diff --git a/src/env/var.rs b/src/env/var.rs index f66c115ac..8889732af 100644 --- a/src/env/var.rs +++ b/src/env/var.rs @@ -289,3 +289,67 @@ pub fn is_read_only(name: &wstr) -> bool { false } } + +#[cfg(test)] +mod tests { + use super::{EnvMode, EnvVar, EnvVarFlags}; + use crate::env::environment::{EnvStack, Environment}; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + use std::{ + mem::MaybeUninit, + time::{SystemTime, UNIX_EPOCH}, + }; + + /// Helper for test_timezone_env_vars(). + fn return_timezone_hour(tstamp: SystemTime, timezone: &wstr) -> libc::c_int { + let vars = EnvStack::globals().create_child(true /* dispatches_var_changes */); + + vars.set_one(L!("TZ"), EnvMode::EXPORT, timezone.to_owned()); + + let _var = vars.get(L!("TZ")); + + #[allow(deprecated)] + let tstamp: libc::time_t = tstamp + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + .try_into() + .unwrap(); + let mut local_time = MaybeUninit::uninit(); + unsafe { libc::localtime_r(&tstamp, local_time.as_mut_ptr()) }; + let local_time = unsafe { local_time.assume_init() }; + local_time.tm_hour + } + + /// Verify that setting TZ calls tzset() in the current shell process. + fn test_timezone_env_vars() { + // Confirm changing the timezone affects fish's idea of the local time. + let tstamp = SystemTime::now(); + + let first_tstamp = return_timezone_hour(tstamp, L!("UTC-1")); + let second_tstamp = return_timezone_hour(tstamp, L!("UTC-2")); + let delta = second_tstamp - first_tstamp; + assert!(delta == 1 || delta == -23); + } + + // Verify that setting special env vars have the expected effect on the current shell process. + #[test] + #[serial] + fn test_env_vars() { + let _cleanup = test_init(); + test_timezone_env_vars(); + // TODO: Add tests for the locale vars. + + let v1 = EnvVar::new(L!("abc").to_owned(), EnvVarFlags::EXPORT); + let v2 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::EXPORT); + let v3 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::empty()); + let v4 = EnvVar::new_vec( + vec![L!("abc").to_owned(), L!("def").to_owned()], + EnvVarFlags::EXPORT, + ); + assert_eq!(v1, v2); + assert_ne!(v1, v3); + assert_ne!(v1, v4); + } +} diff --git a/src/env_universal_common.rs b/src/env_universal_common.rs index 1f31041e3..b07e6bafe 100644 --- a/src/env_universal_common.rs +++ b/src/env_universal_common.rs @@ -28,9 +28,8 @@ pub struct CallbackData { pub type CallbackDataList = Vec; // List of fish universal variable formats. -// This is exposed for testing. #[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum UvarFormat { +enum UvarFormat { fish_2_x, fish_3_0, future, @@ -144,8 +143,7 @@ pub fn initialize(&mut self) -> Option { } /// Initialize a this uvars for a given path. - /// This is exposed for testing only. - pub fn initialize_at_path(&mut self, path: WString) -> Option { + fn initialize_at_path(&mut self, path: WString) -> Option { if path.is_empty() { return None; } @@ -230,9 +228,8 @@ pub fn sync(&mut self) -> (bool, Option) { } /// Populate a variable table `out_vars` from a `s` string. - /// This is exposed for testing only. /// Return the format of the file that we read. - pub fn populate_variables(s: &[u8], out_vars: &mut VarTable) -> UvarFormat { + fn populate_variables(s: &[u8], out_vars: &mut VarTable) -> UvarFormat { // Decide on the format. let format = Self::format_for_contents(s); @@ -268,9 +265,9 @@ pub fn populate_variables(s: &[u8], out_vars: &mut VarTable) -> UvarFormat { format } - /// Guess a file format. Exposed for testing only. + /// Guess a file format. /// Return the format corresponding to file contents `s`. - pub fn format_for_contents(s: &[u8]) -> UvarFormat { + fn format_for_contents(s: &[u8]) -> UvarFormat { // Walk over leading comments, looking for one like '# version' let iter = LineIterator::new(s); for line in iter { @@ -310,8 +307,8 @@ pub fn format_for_contents(s: &[u8]) -> UvarFormat { return UvarFormat::fish_2_x; } - /// Serialize a variable list. Exposed for testing only. - pub fn serialize_with_vars(vars: &VarTable) -> Vec { + /// Serialize a variable list. + fn serialize_with_vars(vars: &VarTable) -> Vec { let mut contents = vec![]; contents.extend_from_slice(SAVE_MSG); contents.extend_from_slice(b"# VERSION: "); @@ -336,7 +333,6 @@ pub fn serialize_with_vars(vars: &VarTable) -> Vec { contents } - /// Exposed for testing only. #[cfg(test)] pub fn is_ok_to_save(&self) -> bool { self.ok_to_save @@ -805,3 +801,347 @@ fn skip_spaces(mut s: &wstr) -> &wstr { } s } + +#[cfg(test)] +mod tests { + use crate::common::ENCODE_DIRECT_BASE; + use crate::common::char_offset; + use crate::common::wcs2osstring; + use crate::env::{EnvVar, EnvVarFlags, VarTable}; + use crate::env_universal_common::{EnvUniversal, UvarFormat}; + use crate::reader::fake_scoped_reader; + use crate::tests::prelude::*; + use crate::threads::{iothread_drain_all, iothread_perform}; + use crate::wchar::prelude::*; + use crate::wutil::{INVALID_FILE_ID, file_id_for_path}; + + const UVARS_PER_THREAD: usize = 8; + const UVARS_TEST_PATH: &wstr = L!("test/fish_uvars_test/varsfile.txt"); + + fn test_universal_helper(x: usize) { + let _cleanup = test_init(); + let mut uvars = EnvUniversal::new(); + uvars.initialize_at_path(UVARS_TEST_PATH.to_owned()); + + for j in 0..UVARS_PER_THREAD { + let key = sprintf!("key_%d_%d", x, j); + let val = sprintf!("val_%d_%d", x, j); + uvars.set(&key, EnvVar::new(val, EnvVarFlags::empty())); + let (synced, _) = uvars.sync(); + assert!( + synced, + "Failed to sync universal variables after modification" + ); + } + + // Last step is to delete the first key. + uvars.remove(&sprintf!("key_%d_%d", x, 0)); + let (synced, _) = uvars.sync(); + assert!(synced, "Failed to sync universal variables after deletion"); + } + + #[test] + #[serial] + fn test_universal() { + let _cleanup = test_init(); + let _ = std::fs::remove_dir_all("test/fish_uvars_test/"); + std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + let parser = TestParser::new(); + + let mut reader = fake_scoped_reader(&parser); + + let threads = 1; + for i in 0..threads { + iothread_perform(move || test_universal_helper(i)); + } + iothread_drain_all(&mut reader); + + let mut uvars = EnvUniversal::new(); + uvars.initialize_at_path(UVARS_TEST_PATH.to_owned()); + + for i in 0..threads { + for j in 0..UVARS_PER_THREAD { + let key = sprintf!("key_%d_%d", i, j); + let expected_val = if j == 0 { + None + } else { + Some(EnvVar::new( + sprintf!("val_%d_%d", i, j), + EnvVarFlags::empty(), + )) + }; + let var = uvars.get(&key); + assert_eq!(var, expected_val); + } + } + + std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); + } + + #[test] + #[serial] + fn test_universal_output() { + let _cleanup = test_init(); + let flag_export = EnvVarFlags::EXPORT; + let flag_pathvar = EnvVarFlags::PATHVAR; + + let mut vars = VarTable::new(); + vars.insert( + L!("varA").to_owned(), + EnvVar::new_vec( + vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], + EnvVarFlags::empty(), + ), + ); + vars.insert( + L!("varB").to_owned(), + EnvVar::new_vec(vec![L!("ValB1").to_owned()], flag_export), + ); + vars.insert( + L!("varC").to_owned(), + EnvVar::new_vec(vec![L!("ValC1").to_owned()], EnvVarFlags::empty()), + ); + vars.insert( + L!("varD").to_owned(), + EnvVar::new_vec(vec![L!("ValD1").to_owned()], flag_export | flag_pathvar), + ); + vars.insert( + L!("varE").to_owned(), + EnvVar::new_vec( + vec![L!("ValE1").to_owned(), L!("ValE2").to_owned()], + flag_pathvar, + ), + ); + vars.insert( + L!("varF").to_owned(), + EnvVar::new_vec( + vec![WString::from_chars([char_offset(ENCODE_DIRECT_BASE, 0xfc)])], + EnvVarFlags::empty(), + ), + ); + + let text = EnvUniversal::serialize_with_vars(&vars); + let expected = concat!( + "# This file contains fish universal variable definitions.\n", + "# VERSION: 3.0\n", + "SETUVAR varA:ValA1\\x1eValA2\n", + "SETUVAR --export varB:ValB1\n", + "SETUVAR varC:ValC1\n", + "SETUVAR --export --path varD:ValD1\n", + "SETUVAR --path varE:ValE1\\x1eValE2\n", + "SETUVAR varF:\\xfc\n", + ) + .as_bytes(); + assert_eq!(text, expected); + } + + #[test] + #[serial] + fn test_universal_parsing() { + let _cleanup = test_init(); + let input = concat!( + "# This file contains fish universal variable definitions.\n", + "# VERSION: 3.0\n", + "SETUVAR varA:ValA1\\x1eValA2\n", + "SETUVAR --export varB:ValB1\n", + "SETUVAR --nonsenseflag varC:ValC1\n", + "SETUVAR --export --path varD:ValD1\n", + "SETUVAR --path --path varE:ValE1\\x1eValE2\n", + ) + .as_bytes(); + + let flag_export = EnvVarFlags::EXPORT; + let flag_pathvar = EnvVarFlags::PATHVAR; + + let mut vars = VarTable::new(); + + vars.insert( + L!("varA").to_owned(), + EnvVar::new_vec( + vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], + EnvVarFlags::empty(), + ), + ); + vars.insert( + L!("varB").to_owned(), + EnvVar::new_vec(vec![L!("ValB1").to_owned()], flag_export), + ); + vars.insert( + L!("varC").to_owned(), + EnvVar::new_vec(vec![L!("ValC1").to_owned()], EnvVarFlags::empty()), + ); + vars.insert( + L!("varD").to_owned(), + EnvVar::new_vec(vec![L!("ValD1").to_owned()], flag_export | flag_pathvar), + ); + vars.insert( + L!("varE").to_owned(), + EnvVar::new_vec( + vec![L!("ValE1").to_owned(), L!("ValE2").to_owned()], + flag_pathvar, + ), + ); + + let mut parsed_vars = VarTable::new(); + EnvUniversal::populate_variables(input, &mut parsed_vars); + assert_eq!(vars, parsed_vars); + } + + #[test] + #[serial] + fn test_universal_parsing_legacy() { + let _cleanup = test_init(); + let input = concat!( + "# This file contains fish universal variable definitions.\n", + "SET varA:ValA1\\x1eValA2\n", + "SET_EXPORT varB:ValB1\n", + ) + .as_bytes(); + + let mut vars = VarTable::new(); + vars.insert( + L!("varA").to_owned(), + EnvVar::new_vec( + vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], + EnvVarFlags::empty(), + ), + ); + vars.insert( + L!("varB").to_owned(), + EnvVar::new(L!("ValB1").to_owned(), EnvVarFlags::EXPORT), + ); + + let mut parsed_vars = VarTable::new(); + EnvUniversal::populate_variables(input, &mut parsed_vars); + assert_eq!(vars, parsed_vars); + } + + #[test] + #[serial] + fn test_universal_callbacks() { + let _cleanup = test_init(); + std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + let mut uvars1 = EnvUniversal::new(); + let mut uvars2 = EnvUniversal::new(); + let mut callbacks = uvars1 + .initialize_at_path(UVARS_TEST_PATH.to_owned()) + .unwrap_or_default(); + callbacks.append( + &mut uvars2 + .initialize_at_path(UVARS_TEST_PATH.to_owned()) + .unwrap_or_default(), + ); + + macro_rules! sync { + ($uvars:expr) => { + let (_, cb_opt) = $uvars.sync(); + if let Some(mut cb) = cb_opt { + callbacks.append(&mut cb); + } + }; + } + + let noflags = EnvVarFlags::empty(); + + // Put some variables into both. + uvars1.set(L!("alpha"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("beta"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("delta"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("epsilon"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("lambda"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("kappa"), EnvVar::new(L!("1").to_owned(), noflags)); // + uvars1.set(L!("omicron"), EnvVar::new(L!("1").to_owned(), noflags)); // + + sync!(uvars1); + sync!(uvars2); + + // Change uvars1. + uvars1.set(L!("alpha"), EnvVar::new(L!("2").to_owned(), noflags)); // changes value + uvars1.set( + L!("beta"), + EnvVar::new(L!("1").to_owned(), EnvVarFlags::EXPORT), + ); // changes export + uvars1.remove(L!("delta")); // erases value + uvars1.set(L!("epsilon"), EnvVar::new(L!("1").to_owned(), noflags)); // changes nothing + sync!(uvars1); + + // Change uvars2. It should treat its value as correct and ignore changes from uvars1. + uvars2.set(L!("lambda"), EnvVar::new(L!("1").to_owned(), noflags)); // same value + uvars2.set(L!("kappa"), EnvVar::new(L!("2").to_owned(), noflags)); // different value + + // Now see what uvars2 sees. + callbacks.clear(); + sync!(uvars2); + + // Sort them to get them in a predictable order. + callbacks.sort_by(|a, b| a.key.cmp(&b.key)); + + // Should see exactly three changes. + assert_eq!(callbacks.len(), 3); + assert_eq!(callbacks[0].key, L!("alpha")); + assert_eq!(callbacks[0].val.as_ref().unwrap().as_string(), L!("2")); + assert_eq!(callbacks[1].key, L!("beta")); + assert_eq!(callbacks[1].val.as_ref().unwrap().as_string(), L!("1")); + assert_eq!(callbacks[2].key, L!("delta")); + assert_eq!(callbacks[2].val, None); + std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); + } + + #[test] + #[serial] + fn test_universal_formats() { + let _cleanup = test_init(); + macro_rules! validate { + ( $version_line:literal, $expected_format:expr ) => { + assert_eq!( + EnvUniversal::format_for_contents($version_line), + $expected_format + ); + }; + } + validate!(b"# VERSION: 3.0", UvarFormat::fish_3_0); + validate!(b"# version: 3.0", UvarFormat::fish_2_x); + validate!(b"# blah blahVERSION: 3.0", UvarFormat::fish_2_x); + validate!(b"stuff\n# blah blahVERSION: 3.0", UvarFormat::fish_2_x); + validate!(b"# blah\n# VERSION: 3.0", UvarFormat::fish_3_0); + validate!(b"# blah\n#VERSION: 3.0", UvarFormat::fish_3_0); + validate!(b"# blah\n#VERSION:3.0", UvarFormat::fish_3_0); + validate!(b"# blah\n#VERSION:3.1", UvarFormat::future); + } + + #[test] + #[serial] + fn test_universal_ok_to_save() { + let _cleanup = test_init(); + // Ensure we don't try to save after reading from a newer fish. + std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); + let contents = b"# VERSION: 99999.99\n"; + std::fs::write(wcs2osstring(UVARS_TEST_PATH), contents).unwrap(); + + let before_id = file_id_for_path(UVARS_TEST_PATH); + assert_ne!( + before_id, INVALID_FILE_ID, + "UVARS_TEST_PATH should be readable" + ); + + let mut uvars = EnvUniversal::new(); + uvars + .initialize_at_path(UVARS_TEST_PATH.to_owned()) + .unwrap_or_default(); + assert!(!uvars.is_ok_to_save(), "Should not be OK to save"); + uvars.sync(); + assert!(!uvars.is_ok_to_save(), "Should still not be OK to save"); + uvars.set( + L!("SOMEVAR"), + EnvVar::new(L!("SOMEVALUE").to_owned(), EnvVarFlags::empty()), + ); + + // Ensure file is same. + let after_id = file_id_for_path(UVARS_TEST_PATH); + assert_eq!( + before_id, after_id, + "UVARS_TEST_PATH should not have changed", + ); + std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); + } +} diff --git a/src/expand.rs b/src/expand.rs index 56773040c..fe9e45af0 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -1591,3 +1591,466 @@ pub struct ExpandResult { /// $status. pub status: libc::c_int, } + +#[cfg(test)] +mod tests { + use crate::abbrs::Abbreviation; + use crate::abbrs::{self}; + use crate::abbrs::{with_abbrs, with_abbrs_mut}; + use crate::complete::{CompletionList, CompletionReceiver}; + use crate::env::{EnvMode, EnvStackSetResult}; + use crate::expand::{ExpandResultCode, expand_to_receiver}; + use crate::operation_context::{EXPANSION_LIMIT_DEFAULT, no_cancel}; + use crate::parse_constants::ParseErrorList; + use crate::tests::prelude::*; + use crate::wildcard::ANY_STRING; + use crate::{ + expand::{ExpandFlags, expand_string}, + operation_context::OperationContext, + wchar::prelude::*, + }; + use std::collections::HashSet; + use std::collections::hash_map::RandomState; + + fn expand_test_impl( + input: &wstr, + flags: ExpandFlags, + expected: Vec, + error_message: Option<&str>, + ) { + let parser = TestParser::new(); + let mut output = CompletionList::new(); + let mut errors = ParseErrorList::new(); + let pwd = PwdEnvironment::default(); + let ctx = OperationContext::test_only_foreground(&parser, &pwd, Box::new(no_cancel)); + + if expand_string( + input.to_owned(), + &mut output, + flags, + &ctx, + Some(&mut errors), + ) == ExpandResultCode::error + { + assert_ne!( + errors, + vec![], + "Bug: Parse error reported but no error text found." + ); + panic!( + "{}", + errors[0].describe(input, ctx.parser().is_interactive()) + ); + } + + let expected_set: HashSet = HashSet::from_iter(expected); + let output_set = HashSet::from_iter(output.into_iter().map(|c| c.completion)); + assert_eq!( + expected_set, + output_set, + "{}", + error_message.unwrap_or("expand mismatch") + ); + } + + // Test globbing and other parameter expansion. + #[test] + #[serial] + fn test_expand() { + let _cleanup = test_init(); + let parser = TestParser::new(); + /// Perform parameter expansion and test if the output equals the zero-terminated parameter list /// supplied. + /// + /// \param in the string to expand + /// \param flags the flags to send to expand_string + /// \param ... A zero-terminated parameter list of values to test. + /// After the zero terminator comes one more arg, a string, which is the error + /// message to print if the test fails. + macro_rules! expand_test { + ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? )) => { + expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], None) + }; + ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? ), $error:literal) => { + expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], Some($error)) + }; + ($input:expr, $flags:expr, $expected:expr) => { + expand_test_impl(L!($input), $flags, vec![ $expected.into() ], None) + }; + ($input:expr, $flags:expr, $expected:expr, $error:literal) => { + expand_test_impl(L!($input), $flags, vec![ $expected.into() ], Some($error)) + }; + } + + // Testing parameter expansion + let noflags = ExpandFlags::default(); + + expand_test!("foo", noflags, "foo", "Strings do not expand to themselves"); + + expand_test!( + "a{b,c,d}e", + noflags, + ("abe", "ace", "ade"), + "Bracket expansion is broken" + ); + expand_test!( + "a*", + ExpandFlags::SKIP_WILDCARDS, + "a*", + "Cannot skip wildcard expansion" + ); + expand_test!( + "/bin/l\\0", + ExpandFlags::FOR_COMPLETIONS, + (), + "Failed to handle null escape in expansion" + ); + expand_test!( + "foo\\$bar", + ExpandFlags::SKIP_VARIABLES, + "foo$bar", + "Failed to handle dollar sign in variable-skipping expansion" + ); + + // bb + // x + // bar + // baz + // xxx + // yyy + // bax + // xxx + // lol + // nub + // q + // zzz + // .foo + // aaa + // aaa2 + // x + std::fs::create_dir_all("test/fish_expand_test/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/bb/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/baz/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/bax/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/lol/nub/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/aaa/").unwrap(); + std::fs::create_dir_all("test/fish_expand_test/aaa2/").unwrap(); + std::fs::write("test/fish_expand_test/.foo", []).unwrap(); + std::fs::write("test/fish_expand_test/bb/x", []).unwrap(); + std::fs::write("test/fish_expand_test/bar", []).unwrap(); + std::fs::write("test/fish_expand_test/bax/xxx", []).unwrap(); + std::fs::write("test/fish_expand_test/baz/xxx", []).unwrap(); + std::fs::write("test/fish_expand_test/baz/yyy", []).unwrap(); + std::fs::write("test/fish_expand_test/lol/nub/q", []).unwrap(); + std::fs::write("test/fish_expand_test/lol/nub/zzz", []).unwrap(); + std::fs::write("test/fish_expand_test/aaa2/x", []).unwrap(); + + // This is checking that .* does NOT match . and .. + // (https://github.com/fish-shell/fish-shell/issues/270). But it does have to match literal + // components (e.g. "./*" has to match the same as "*". + expand_test!( + "test/fish_expand_test/.*", + noflags, + "test/fish_expand_test/.foo", + "Expansion not correctly handling dotfiles" + ); + + expand_test!( + "test/fish_expand_test/./.*", + noflags, + "test/fish_expand_test/./.foo", + "Expansion not correctly handling literal path components in dotfiles" + ); + + expand_test!( + "test/fish_expand_test/*/xxx", + noflags, + ( + "test/fish_expand_test/bax/xxx", + "test/fish_expand_test/baz/xxx" + ), + "Glob did the wrong thing 1" + ); + + expand_test!( + "test/fish_expand_test/*z/xxx", + noflags, + "test/fish_expand_test/baz/xxx", + "Glob did the wrong thing 2" + ); + + expand_test!( + "test/fish_expand_test/**z/xxx", + noflags, + "test/fish_expand_test/baz/xxx", + "Glob did the wrong thing 3" + ); + + expand_test!( + "test/fish_expand_test////baz/xxx", + noflags, + "test/fish_expand_test////baz/xxx", + "Glob did the wrong thing 3" + ); + + expand_test!( + "test/fish_expand_test/b**", + noflags, + ( + "test/fish_expand_test/bb", + "test/fish_expand_test/bb/x", + "test/fish_expand_test/bar", + "test/fish_expand_test/bax", + "test/fish_expand_test/bax/xxx", + "test/fish_expand_test/baz", + "test/fish_expand_test/baz/xxx", + "test/fish_expand_test/baz/yyy" + ), + "Glob did the wrong thing 4" + ); + + // A trailing slash should only produce directories. + expand_test!( + "test/fish_expand_test/b*/", + noflags, + ( + "test/fish_expand_test/bb/", + "test/fish_expand_test/baz/", + "test/fish_expand_test/bax/" + ), + "Glob did the wrong thing 5" + ); + + expand_test!( + "test/fish_expand_test/b**/", + noflags, + ( + "test/fish_expand_test/bb/", + "test/fish_expand_test/baz/", + "test/fish_expand_test/bax/" + ), + "Glob did the wrong thing 6" + ); + + expand_test!( + "test/fish_expand_test/**/q", + noflags, + "test/fish_expand_test/lol/nub/q", + "Glob did the wrong thing 7" + ); + + expand_test!( + "test/fish_expand_test/BA", + ExpandFlags::FOR_COMPLETIONS, + ( + "test/fish_expand_test/bar", + "test/fish_expand_test/bax/", + "test/fish_expand_test/baz/" + ), + "Case insensitive test did the wrong thing" + ); + + expand_test!( + "test/fish_expand_test/BA", + ExpandFlags::FOR_COMPLETIONS, + ( + "test/fish_expand_test/bar", + "test/fish_expand_test/bax/", + "test/fish_expand_test/baz/" + ), + "Case insensitive test did the wrong thing" + ); + + expand_test!( + "test/fish_expand_test/bb/yyy", + ExpandFlags::FOR_COMPLETIONS, + (), /* nothing! */ + "Wrong fuzzy matching 1" + ); + + expand_test!( + "test/fish_expand_test/bb/x", + ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH, + "", + // we just expect the empty string since this is an exact match + "Wrong fuzzy matching 2" + ); + + // Some vswprintfs refuse to append ANY_STRING in a format specifiers, so don't use + // format_string here. + let fuzzy_comp = ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH; + let any_str_str = ANY_STRING.to_string(); + expand_test!( + "test/fish_expand_test/b/xx*", + fuzzy_comp, + ( + (String::from("test/fish_expand_test/bax/xx") + &any_str_str), + (String::from("test/fish_expand_test/baz/xx") + &any_str_str) + ), + "Wrong fuzzy matching 3" + ); + + expand_test!( + "test/fish_expand_test/*/nu/zz", + fuzzy_comp, + (format!("test/fish_expand_test/{any_str_str}/nub/zzz")), + "Glob did not expand correctly with more than one path item after the *" + ); + + expand_test!( + "test/fish_expand_test/b/yyy", + fuzzy_comp, + "test/fish_expand_test/baz/yyy", + "Wrong fuzzy matching 4" + ); + + expand_test!( + "test/fish_expand_test/aa/x", + fuzzy_comp, + "test/fish_expand_test/aaa2/x", + "Wrong fuzzy matching 5" + ); + + expand_test!( + "test/fish_expand_test/aaa/x", + fuzzy_comp, + (), + "Wrong fuzzy matching 6 - shouldn't remove valid directory names (#3211)" + ); + + // Dotfiles + expand_test!( + "test/fish_expand_test/.*", + noflags, + "test/fish_expand_test/.foo", + "" + ); + + // Literal path components in dotfiles. + expand_test!( + "test/fish_expand_test/./.*", + noflags, + "test/fish_expand_test/./.foo", + "" + ); + + parser.pushd("test/fish_expand_test"); + + expand_test!( + "b/xx", + fuzzy_comp, + ("bax/xxx", "baz/xxx"), + "Wrong fuzzy matching 5" + ); + + // multiple slashes with fuzzy matching - #3185 + expand_test!("l///n", fuzzy_comp, "lol///nub/", "Wrong fuzzy matching 6"); + + parser.popd(); + } + + #[test] + #[serial] + fn test_expand_overflow() { + let _cleanup = test_init(); + // Testing overflowing expansions + // Ensure that we have sane limits on number of expansions - see #7497. + + // Make a list of 64 elements, then expand it cartesian-style 64 times. + // This is far too large to expand. + let vals: Vec = (1..=64).map(|i| i.to_wstring()).collect(); + let expansion = WString::from_str(&str::repeat("$bigvar", 64)); + + let parser = TestParser::new(); + parser.vars().push(true); + let set = parser.vars().set(L!("bigvar"), EnvMode::LOCAL, vals); + assert_eq!(set, EnvStackSetResult::Ok); + + let mut errors = ParseErrorList::new(); + let ctx = + OperationContext::foreground(&parser, Box::new(no_cancel), EXPANSION_LIMIT_DEFAULT); + + // We accept only 1024 completions. + let mut output = CompletionReceiver::new(1024); + + let res = expand_to_receiver( + expansion, + &mut output, + ExpandFlags::default(), + &ctx, + Some(&mut errors), + ); + assert_ne!(errors, vec![]); + assert_eq!(res, ExpandResultCode::error); + + parser.vars().pop(); + } + + #[test] + #[serial] + fn test_abbreviations() { + let _cleanup = test_init(); + // Testing abbreviations + + with_abbrs_mut(|abbrset| { + abbrset.add(Abbreviation::new( + L!("gc").to_owned(), + L!("gc").to_owned(), + L!("git checkout").to_owned(), + abbrs::Position::Command, + false, + )); + abbrset.add(Abbreviation::new( + L!("foo").to_owned(), + L!("foo").to_owned(), + L!("bar").to_owned(), + abbrs::Position::Command, + false, + )); + abbrset.add(Abbreviation::new( + L!("gx").to_owned(), + L!("gx").to_owned(), + L!("git checkout").to_owned(), + abbrs::Position::Command, + false, + )); + abbrset.add(Abbreviation::new( + L!("yin").to_owned(), + L!("yin").to_owned(), + L!("yang").to_owned(), + abbrs::Position::Anywhere, + false, + )); + }); + + // Helper to expand an abbreviation, enforcing we have no more than one result. + let abbr_expand_1 = |token, pos| -> Option { + let result = with_abbrs(|abbrset| abbrset.r#match(token, pos, L!(""))); + if result.is_empty() { + return None; + } + assert_eq!( + &result[1..], + &[], + "abbreviation expansion for {token} returned more than 1 result" + ); + Some(result.into_iter().next().unwrap().replacement) + }; + + let cmd = abbrs::Position::Command; + assert!( + abbr_expand_1(L!(""), cmd).is_none(), + "Unexpected success with empty abbreviation" + ); + assert!( + abbr_expand_1(L!("nothing"), cmd).is_none(), + "Unexpected success with missing abbreviation" + ); + + assert_eq!( + abbr_expand_1(L!("gc"), cmd), + Some(L!("git checkout").into()) + ); + + assert_eq!(abbr_expand_1(L!("foo"), cmd), Some(L!("bar").into())); + } +} diff --git a/src/fallback.rs b/src/fallback.rs index 5a5499c4c..fac44c1e7 100644 --- a/src/fallback.rs +++ b/src/fallback.rs @@ -162,21 +162,26 @@ pub fn new(mut chars: std::iter::Map, Canonicalize>) -> Self { lhs.cmp(rhs) } -#[test] -fn test_wcscasecmp() { +#[cfg(test)] +mod tests { + use super::wcscasecmp; + use crate::wchar::prelude::*; use std::cmp::Ordering; - // Comparison with empty - assert_eq!(wcscasecmp(L!("a"), L!("")), Ordering::Greater); - assert_eq!(wcscasecmp(L!(""), L!("a")), Ordering::Less); - assert_eq!(wcscasecmp(L!(""), L!("")), Ordering::Equal); + #[test] + fn test_wcscasecmp() { + // Comparison with empty + assert_eq!(wcscasecmp(L!("a"), L!("")), Ordering::Greater); + assert_eq!(wcscasecmp(L!(""), L!("a")), Ordering::Less); + assert_eq!(wcscasecmp(L!(""), L!("")), Ordering::Equal); - // Basic comparison - assert_eq!(wcscasecmp(L!("A"), L!("a")), Ordering::Equal); - assert_eq!(wcscasecmp(L!("B"), L!("a")), Ordering::Greater); - assert_eq!(wcscasecmp(L!("A"), L!("B")), Ordering::Less); + // Basic comparison + assert_eq!(wcscasecmp(L!("A"), L!("a")), Ordering::Equal); + assert_eq!(wcscasecmp(L!("B"), L!("a")), Ordering::Greater); + assert_eq!(wcscasecmp(L!("A"), L!("B")), Ordering::Less); - // Multi-byte comparison - assert_eq!(wcscasecmp(L!("İ"), L!("i\u{307}")), Ordering::Equal); - assert_eq!(wcscasecmp(L!("ia"), L!("İa")), Ordering::Less); + // Multi-byte comparison + assert_eq!(wcscasecmp(L!("İ"), L!("i\u{307}")), Ordering::Equal); + assert_eq!(wcscasecmp(L!("ia"), L!("İa")), Ordering::Less); + } } diff --git a/src/fd_monitor.rs b/src/fd_monitor.rs index fe3408eae..f108822dc 100644 --- a/src/fd_monitor.rs +++ b/src/fd_monitor.rs @@ -449,3 +449,248 @@ fn drop(&mut self) { } } } + +#[cfg(test)] +mod tests { + #[cfg(not(target_has_atomic = "64"))] + use portable_atomic::AtomicU64; + use std::fs::File; + use std::io::Write; + use std::os::fd::{AsRawFd, IntoRawFd, OwnedFd}; + #[cfg(target_has_atomic = "64")] + use std::sync::atomic::AtomicU64; + use std::sync::atomic::{AtomicUsize, Ordering}; + use std::sync::{Arc, Barrier, Mutex}; + use std::thread; + use std::time::Duration; + + use errno::errno; + + use crate::fd_monitor::{FdEventSignaller, FdMonitor}; + use crate::fd_readable_set::{FdReadableSet, Timeout}; + use crate::fds::{AutoCloseFd, AutoClosePipes, make_autoclose_pipes}; + use crate::tests::prelude::*; + + /// Helper to make an item which counts how many times its callback was invoked. + /// + /// This could be structured differently to avoid the `Mutex` on `writer`, but it's not worth it + /// since this is just used for test purposes. + struct ItemMaker { + pub length_read: AtomicUsize, + pub total_calls: AtomicUsize, + item_id: AtomicU64, + pub always_close: bool, + pub writer: Mutex>, + } + + impl ItemMaker { + pub fn insert_new_into(monitor: &FdMonitor) -> Arc { + Self::insert_new_into2(monitor, |_| {}) + } + + pub fn insert_new_into2(monitor: &FdMonitor, config: F) -> Arc { + let pipes = make_autoclose_pipes().expect("fds exhausted!"); + + let mut result = ItemMaker { + length_read: 0.into(), + total_calls: 0.into(), + item_id: 0.into(), + always_close: false, + writer: Mutex::new(Some(File::from(pipes.write))), + }; + + config(&mut result); + + let result = Arc::new(result); + let callback = { + let result = Arc::clone(&result); + move |fd: &mut AutoCloseFd| result.callback(fd) + }; + let fd = AutoCloseFd::new(pipes.read.into_raw_fd()); + let item_id = monitor.add(fd, Box::new(callback)); + result.item_id.store(u64::from(item_id), Ordering::Relaxed); + + result + } + + fn callback(&self, fd: &mut AutoCloseFd) { + let mut buf = [0u8; 1024]; + let res = nix::unistd::read(&fd, &mut buf); + let amt = res.expect("read error!"); + self.length_read.fetch_add(amt, Ordering::Relaxed); + let was_closed = amt == 0; + + self.total_calls.fetch_add(1, Ordering::Relaxed); + if was_closed || self.always_close { + fd.close(); + } + } + + /// Write 42 bytes to our write end. + fn write42(&self) { + let buf = [0u8; 42]; + let mut writer = self.writer.lock().expect("Mutex poisoned!"); + writer + .as_mut() + .unwrap() + .write_all(&buf) + .expect("Error writing 42 bytes to pipe!"); + } + } + + #[test] + #[serial] + fn fd_monitor_items() { + let _cleanup = test_init(); + let monitor = FdMonitor::new(); + + // Item which will never receive data or be called. + let item_never = ItemMaker::insert_new_into(&monitor); + + // Item which should get exactly 42 bytes. + let item42 = ItemMaker::insert_new_into(&monitor); + + // Item which should get 42 bytes then get notified it is closed. + let item42_then_close = ItemMaker::insert_new_into(&monitor); + + // Item which should get a callback exactly once. + let item_oneshot = ItemMaker::insert_new_into2(&monitor, |item| { + item.always_close = true; + }); + + item42.write42(); + item42_then_close.write42(); + *item42_then_close.writer.lock().expect("Mutex poisoned!") = None; + item_oneshot.write42(); + + // May need to loop here to ensure our fd_monitor gets scheduled. See #7699. + for _ in 0..100 { + std::thread::sleep(Duration::from_millis(84)); + if item_oneshot.total_calls.load(Ordering::Relaxed) > 0 { + break; + } + } + + drop(monitor); + + assert_eq!(item_never.length_read.load(Ordering::Relaxed), 0); + + assert_eq!(item42.length_read.load(Ordering::Relaxed), 42); + + assert_eq!(item42_then_close.length_read.load(Ordering::Relaxed), 42); + assert_eq!(item42_then_close.total_calls.load(Ordering::Relaxed), 2); + + assert_eq!(item_oneshot.length_read.load(Ordering::Relaxed), 42); + assert_eq!(item_oneshot.total_calls.load(Ordering::Relaxed), 1); + } + + #[test] + fn test_fd_event_signaller() { + let sema = FdEventSignaller::new(); + assert!(!sema.try_consume()); + assert!(!sema.poll(false)); + + // Post once. + sema.post(); + assert!(sema.poll(false)); + assert!(sema.poll(false)); + assert!(sema.try_consume()); + assert!(!sema.poll(false)); + assert!(!sema.try_consume()); + + // Posts are coalesced. + sema.post(); + sema.post(); + sema.post(); + assert!(sema.poll(false)); + assert!(sema.poll(false)); + assert!(sema.try_consume()); + assert!(!sema.poll(false)); + assert!(!sema.try_consume()); + } + + // A helper function which calls poll() or selects() on a file descriptor in the background, + // and then invokes the `bad_action` function on the file descriptor while the poll/select is + // waiting. The function returns Result: either the number of readable file descriptors + // or the error code from poll/select. + #[cfg(test)] + fn do_something_bad_during_select(bad_action: F) -> Result + where + F: FnOnce(OwnedFd) -> Option, + { + let AutoClosePipes { + read: read_fd, + write: write_fd, + } = make_autoclose_pipes().expect("Failed to create pipe"); + let raw_read_fd = read_fd.as_raw_fd(); + + // Try to ensure that the thread will be scheduled by waiting until it is. + let barrier = Arc::new(Barrier::new(2)); + let barrier_clone = Arc::clone(&barrier); + + let select_thread = thread::spawn(move || -> Result { + let mut fd_set = FdReadableSet::new(); + fd_set.add(raw_read_fd); + + barrier_clone.wait(); + + // Timeout after 500 msec. + // macOS will eagerly return EBADF if the fd is closed; Linux will hit the timeout. + let timeout = Timeout::Duration(Duration::from_millis(500)); + let ret = fd_set.check_readable(timeout); + if ret < 0 { Err(errno().0) } else { Ok(ret) } + }); + + barrier.wait(); + thread::sleep(Duration::from_millis(100)); + let read_fd = bad_action(read_fd); + + let result = select_thread.join().expect("Select thread panicked"); + // Ensure these stay alive until after thread is joined. + drop(read_fd); + drop(write_fd); + result + } + + #[test] + fn test_close_during_select_ebadf() { + use crate::common::{WSL, is_windows_subsystem_for_linux as is_wsl}; + let close_it = |read_fd: OwnedFd| { + drop(read_fd); + None + }; + let result = do_something_bad_during_select(close_it); + + // WSLv1 does not error out with EBADF if the fd is closed mid-select. + // This is OK because we do not _depend_ on this behavior; the only + // true requirement is that we don't panic in the handling code above. + assert!( + is_wsl(WSL::V1) || matches!(result, Err(libc::EBADF) | Ok(1)), + "select/poll should have failed with EBADF or marked readable" + ); + } + + #[test] + fn test_dup2_during_select_ebadf() { + // Make a random file descriptor that we can dup2 stdin to. + let AutoClosePipes { + read: pipe_read, + write: pipe_write, + } = make_autoclose_pipes().expect("Failed to create pipe"); + + let dup2_it = |read_fd: OwnedFd| { + // We are going to dup2 stdin to this fd, which should cause select/poll to fail. + assert!(read_fd.as_raw_fd() > 0, "fd should be valid and not stdin"); + unsafe { libc::dup2(pipe_read.as_raw_fd(), read_fd.as_raw_fd()) }; + Some(read_fd) + }; + let result = do_something_bad_during_select(dup2_it); + assert!( + matches!(result, Err(libc::EBADF) | Ok(0) | Ok(1)), + "select/poll should have failed with EBADF or timed out or the fd should be ready" + ); + // Ensure these stay alive until after thread is joined. + drop(pipe_read); + drop(pipe_write); + } +} diff --git a/src/fds.rs b/src/fds.rs index 7a1362664..228e91a97 100644 --- a/src/fds.rs +++ b/src/fds.rs @@ -1,8 +1,6 @@ use crate::common::wcs2zstring; use crate::flog::FLOG; use crate::signal::signal_check_cancel; -#[cfg(test)] -use crate::tests::prelude::*; use crate::wchar::prelude::*; use crate::wutil::perror; use libc::{EINTR, F_GETFD, F_GETFL, F_SETFD, F_SETFL, FD_CLOEXEC, O_NONBLOCK, c_int}; @@ -337,25 +335,33 @@ pub fn make_fd_blocking(fd: RawFd) -> Result<(), io::Error> { Ok(()) } -#[test] -#[serial] -fn test_pipes() { - let _cleanup = test_init(); - // Here we just test that each pipe has CLOEXEC set and is in the high range. - // Note pipe creation may fail due to fd exhaustion; don't fail in that case. - let mut pipes = vec![]; - for _i in 0..10 { - if let Ok(pipe) = make_autoclose_pipes() { - pipes.push(pipe); +#[cfg(test)] +mod tests { + use super::{FIRST_HIGH_FD, make_autoclose_pipes}; + use crate::tests::prelude::*; + use libc::{F_GETFD, FD_CLOEXEC}; + use std::os::fd::AsRawFd; + + #[test] + #[serial] + fn test_pipes() { + let _cleanup = test_init(); + // Here we just test that each pipe has CLOEXEC set and is in the high range. + // Note pipe creation may fail due to fd exhaustion; don't fail in that case. + let mut pipes = vec![]; + for _i in 0..10 { + if let Ok(pipe) = make_autoclose_pipes() { + pipes.push(pipe); + } } - } - for pipe in pipes { - for fd in [&pipe.read, &pipe.write] { - let fd = fd.as_raw_fd(); - assert!(fd >= FIRST_HIGH_FD); - let flags = unsafe { libc::fcntl(fd, F_GETFD, 0) }; - assert!(flags >= 0); - assert!(flags & FD_CLOEXEC != 0); + for pipe in pipes { + for fd in [&pipe.read, &pipe.write] { + let fd = fd.as_raw_fd(); + assert!(fd >= FIRST_HIGH_FD); + let flags = unsafe { libc::fcntl(fd, F_GETFD, 0) }; + assert!(flags >= 0); + assert!(flags & FD_CLOEXEC != 0); + } } } } diff --git a/src/future_feature_flags.rs b/src/future_feature_flags.rs index 70fe48539..905dd2e05 100644 --- a/src/future_feature_flags.rs +++ b/src/future_feature_flags.rs @@ -273,48 +273,54 @@ pub fn scoped_test(flag: FeatureFlag, value: bool, test_fn: impl FnOnce()) { }); } -#[test] -fn test_feature_flags() { - let f = Features::new(); - f.set_from_string(L!("stderr-nocaret,nonsense")); - assert!(f.test(FeatureFlag::stderr_nocaret)); - f.set_from_string(L!("stderr-nocaret,no-stderr-nocaret,nonsense")); - assert!(f.test(FeatureFlag::stderr_nocaret)); +#[cfg(test)] +mod tests { + use super::{FeatureFlag, Features, METADATA, scoped_test, set, test}; + use crate::wchar::prelude::*; - // Ensure every metadata is represented once. - let mut counts: [usize; METADATA.len()] = [0; METADATA.len()]; - for md in METADATA { - counts[md.flag as usize] += 1; - } - for count in counts { - assert_eq!(count, 1); + #[test] + fn test_feature_flags() { + let f = Features::new(); + f.set_from_string(L!("stderr-nocaret,nonsense")); + assert!(f.test(FeatureFlag::stderr_nocaret)); + f.set_from_string(L!("stderr-nocaret,no-stderr-nocaret,nonsense")); + assert!(f.test(FeatureFlag::stderr_nocaret)); + + // Ensure every metadata is represented once. + let mut counts: [usize; METADATA.len()] = [0; METADATA.len()]; + for md in METADATA { + counts[md.flag as usize] += 1; + } + for count in counts { + assert_eq!(count, 1); + } + + assert_eq!( + METADATA[FeatureFlag::stderr_nocaret as usize].name, + L!("stderr-nocaret") + ); } - assert_eq!( - METADATA[FeatureFlag::stderr_nocaret as usize].name, - L!("stderr-nocaret") - ); -} - -#[test] -fn test_scoped() { - scoped_test(FeatureFlag::qmark_noglob, true, || { - assert!(test(FeatureFlag::qmark_noglob)); - }); - - set(FeatureFlag::qmark_noglob, true); - - scoped_test(FeatureFlag::qmark_noglob, false, || { - assert!(!test(FeatureFlag::qmark_noglob)); - }); - - set(FeatureFlag::qmark_noglob, false); -} - -#[test] -#[should_panic] -fn test_nested_scopes_not_supported() { - scoped_test(FeatureFlag::qmark_noglob, true, || { - scoped_test(FeatureFlag::qmark_noglob, false, || {}); - }); + #[test] + fn test_scoped() { + scoped_test(FeatureFlag::qmark_noglob, true, || { + assert!(test(FeatureFlag::qmark_noglob)); + }); + + set(FeatureFlag::qmark_noglob, true); + + scoped_test(FeatureFlag::qmark_noglob, false, || { + assert!(!test(FeatureFlag::qmark_noglob)); + }); + + set(FeatureFlag::qmark_noglob, false); + } + + #[test] + #[should_panic] + fn test_nested_scopes_not_supported() { + scoped_test(FeatureFlag::qmark_noglob, true, || { + scoped_test(FeatureFlag::qmark_noglob, false, || {}); + }); + } } diff --git a/src/highlight/file_tester.rs b/src/highlight/file_tester.rs index cc676acd5..897415620 100644 --- a/src/highlight/file_tester.rs +++ b/src/highlight/file_tester.rs @@ -416,3 +416,406 @@ pub fn fs_is_case_insensitive( // Other platforms don’t have _PC_CASE_SENSITIVE. false } + +#[cfg(test)] +mod tests { + use super::{FileTester, IsErr, IsFile, PathFlags, is_potential_path}; + use crate::env::EnvStack; + use crate::operation_context::{EXPANSION_LIMIT_DEFAULT, OperationContext}; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + + use crate::common::charptr2wcstring; + use crate::redirection::RedirectionMode; + use std::fs::{self, File, Permissions, create_dir_all}; + use std::os::unix::fs::PermissionsExt; + + struct TempDir { + basepath: WString, + ctx: OperationContext<'static>, + } + + impl TempDir { + fn new() -> TempDir { + let mut t1 = *b"/tmp/fish_file_tester_dir.XXXXXX\0"; + let basepath_narrow = unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }; + assert!(!basepath_narrow.is_null(), "mkdtemp failed"); + let basepath: WString = charptr2wcstring(basepath_narrow); + TempDir { + basepath, + ctx: OperationContext::empty(), + } + } + + fn filepath(&self, name: &str) -> String { + let mut result = self.basepath.to_string(); + result.push('/'); + result.push_str(name); + result + } + + fn file_tester(&self) -> FileTester<'_> { + FileTester::new(self.basepath.clone(), &self.ctx) + } + } + + impl Drop for TempDir { + fn drop(&mut self) { + let _ = std::fs::remove_dir_all(self.basepath.to_string()); + } + } + + #[test] + fn test_ispath() { + let temp = TempDir::new(); + let tester = temp.file_tester(); + + let file_path = temp.filepath("file.txt"); + File::create(file_path).unwrap(); + + let result = tester.test_path(L!("file.txt"), false); + assert!(result); + + let result = tester.test_path(L!("file.txt"), true); + assert!(result); + + let result = tester.test_path(L!("fi"), false); + assert!(!result); + + let result = tester.test_path(L!("fi"), true); + assert!(result); + + let result = tester.test_path(L!("file.txt-more"), false); + assert!(!result); + + let result = tester.test_path(L!("file.txt-more"), true); + assert!(!result); + + let result = tester.test_path(L!("ffiledfk.txt"), false); + assert!(!result); + + let result = tester.test_path(L!("ffiledfk.txt"), true); + assert!(!result); + + // Directories are also files. + let dir_path = temp.filepath("somedir"); + create_dir_all(dir_path).unwrap(); + + let result = tester.test_path(L!("somedir"), false); + assert!(result); + + let result = tester.test_path(L!("somedir"), true); + assert!(result); + + let result = tester.test_path(L!("some"), false); + assert!(!result); + + let result = tester.test_path(L!("some"), true); + assert!(result); + } + + #[test] + fn test_iscdpath() { + let temp = TempDir::new(); + let tester = temp.file_tester(); + + // Note cd (unlike file paths) should report IsErr for invalid cd paths, + // rather than IsFile(false). + + let dir_path = temp.filepath("somedir"); + create_dir_all(dir_path).unwrap(); + + let result = tester.test_cd_path(L!("somedir"), false); + assert_eq!(result, Ok(IsFile(true))); + + let result = tester.test_cd_path(L!("somedir"), true); + assert_eq!(result, Ok(IsFile(true))); + + let result = tester.test_cd_path(L!("some"), false); + assert_eq!(result, Err(IsErr)); + + let result = tester.test_cd_path(L!("some"), true); + assert_eq!(result, Ok(IsFile(true))); + + let result = tester.test_cd_path(L!("notdir"), false); + assert_eq!(result, Err(IsErr)); + + let result = tester.test_cd_path(L!("notdir"), true); + assert_eq!(result, Err(IsErr)); + } + + #[test] + fn test_redirections() { + // Note we use is_ok and is_err since we don't care about the IsFile part. + let temp = TempDir::new(); + let tester = temp.file_tester(); + let file_path = temp.filepath("file.txt"); + File::create(&file_path).unwrap(); + + let dir_path = temp.filepath("somedir"); + create_dir_all(&dir_path).unwrap(); + + // Normal redirection. + let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::input); + assert!(result); + + // Can't redirect from a missing file + let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::input); + assert!(!result); + let result = + tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::input); + assert!(!result); + + // Can't redirect from a directory. + let result = tester.test_redirection_target(L!("somedir"), RedirectionMode::input); + assert!(!result); + + // Can't redirect from an unreadable file. + #[cfg(not(cygwin))] // Can't mark a file write-only on MSYS, this may work on true Cygwin + { + fs::set_permissions(&file_path, Permissions::from_mode(0o200)).unwrap(); + let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::input); + assert!(!result); + fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap(); + } + + // try_input syntax highlighting reports an error even though the command will succeed. + let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::try_input); + assert!(result); + let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::try_input); + assert!(!result); + let result = + tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::try_input); + assert!(!result); + + // Test write redirections. + // Overwrite an existing file. + let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::overwrite); + assert!(result); + + // Append to an existing file. + let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::append); + assert!(result); + + // Write to a missing file. + let result = tester.test_redirection_target(L!("newfile.txt"), RedirectionMode::overwrite); + assert!(result); + + // No-clobber write to existing file should fail. + let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::noclob); + assert!(!result); + + // No-clobber write to missing file should succeed. + let result = tester.test_redirection_target(L!("unique.txt"), RedirectionMode::noclob); + assert!(result); + + let write_modes = &[ + RedirectionMode::overwrite, + RedirectionMode::append, + RedirectionMode::noclob, + ]; + + // Can't write to a directory. + for mode in write_modes { + assert!( + !tester.test_redirection_target(L!("somedir"), *mode), + "Should not be able to write to a directory with mode {:?}", + mode + ); + } + + // Can't write without write permissions. + fs::set_permissions(&file_path, Permissions::from_mode(0o400)).unwrap(); // Read-only. + for mode in write_modes { + assert!( + !tester.test_redirection_target(L!("file.txt"), *mode), + "Should not be able to write to a read-only file with mode {:?}", + mode + ); + } + fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap(); // Restore permissions. + + // Writing into a directory without write permissions (loop through all modes). + #[cfg(not(cygwin))] // Can't mark a file write-only on MSYS, this may work on true Cygwin + { + fs::set_permissions(&dir_path, Permissions::from_mode(0o500)).unwrap(); // Read and execute, no write. + for mode in write_modes { + assert!( + !tester.test_redirection_target(L!("somedir/newfile.txt"), *mode), + "Should not be able to create/write in a read-only directory with mode {:?}", + mode + ); + } + fs::set_permissions(&dir_path, Permissions::from_mode(0o700)).unwrap(); // Restore permissions. + } + + // Test fd redirections. + assert!(tester.test_redirection_target(L!("-"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("0"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("1"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("2"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("3"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("500"), RedirectionMode::fd)); + + // We are base 10, despite the leading 0. + assert!(tester.test_redirection_target(L!("000"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("01"), RedirectionMode::fd)); + assert!(tester.test_redirection_target(L!("07"), RedirectionMode::fd)); + + // Invalid fd redirections. + assert!(!tester.test_redirection_target(L!("0x2"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("0x3F"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("0F"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("-1"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("-0009"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("--"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("derp"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("123boo"), RedirectionMode::fd)); + assert!(!tester.test_redirection_target(L!("18446744073709551616"), RedirectionMode::fd)); + } + + #[test] + #[serial] + fn test_is_potential_path() { + let _cleanup = test_init(); + // Directories + std::fs::create_dir_all("test/is_potential_path_test/alpha/").unwrap(); + std::fs::create_dir_all("test/is_potential_path_test/beta/").unwrap(); + + // Files + std::fs::write("test/is_potential_path_test/aardvark", []).unwrap(); + std::fs::write("test/is_potential_path_test/gamma", []).unwrap(); + + let wd = L!("test/is_potential_path_test/").to_owned(); + let wds = [L!(".").to_owned(), wd]; + + let vars = EnvStack::new(); + let ctx = OperationContext::background(&vars, EXPANSION_LIMIT_DEFAULT); + + let path_require_dir = PathFlags { + require_dir: true, + ..Default::default() + }; + + assert!(is_potential_path( + L!("al"), + true, + &wds[..], + &ctx, + path_require_dir + )); + + assert!(is_potential_path( + L!("alpha/"), + true, + &wds[..], + &ctx, + path_require_dir + )); + assert!(is_potential_path( + L!("aard"), + true, + &wds[..], + &ctx, + PathFlags::default() + )); + assert!(!is_potential_path( + L!("aard"), + false, + &wds[..], + &ctx, + PathFlags::default() + )); + assert!(!is_potential_path( + L!("alp/"), + true, + &wds[..], + &ctx, + PathFlags { + require_dir: true, + for_cd: true, + ..Default::default() + } + )); + + assert!(!is_potential_path( + L!("balpha/"), + true, + &wds[..], + &ctx, + path_require_dir + )); + assert!(!is_potential_path( + L!("aard"), + true, + &wds[..], + &ctx, + path_require_dir + )); + assert!(!is_potential_path( + L!("aarde"), + true, + &wds[..], + &ctx, + path_require_dir + )); + assert!(!is_potential_path( + L!("aarde"), + true, + &wds[..], + &ctx, + PathFlags::default() + )); + + assert!(is_potential_path( + L!("test/is_potential_path_test/aardvark"), + true, + &wds[..], + &ctx, + PathFlags::default() + )); + assert!(is_potential_path( + L!("test/is_potential_path_test/al"), + true, + &wds[..], + &ctx, + path_require_dir + )); + assert!(is_potential_path( + L!("test/is_potential_path_test/aardv"), + true, + &wds[..], + &ctx, + PathFlags::default() + )); + + assert!(!is_potential_path( + L!("test/is_potential_path_test/aardvark"), + true, + &wds[..], + &ctx, + path_require_dir + )); + assert!(!is_potential_path( + L!("test/is_potential_path_test/al/"), + true, + &wds[..], + &ctx, + PathFlags::default() + )); + assert!(!is_potential_path( + L!("test/is_potential_path_test/ar"), + true, + &wds[..], + &ctx, + PathFlags::default() + )); + assert!(is_potential_path( + L!("/usr"), + true, + &wds[..], + &ctx, + path_require_dir + )); + } +} diff --git a/src/highlight/highlight.rs b/src/highlight/highlight.rs index 3efb19994..46aca5a56 100644 --- a/src/highlight/highlight.rs +++ b/src/highlight/highlight.rs @@ -1259,3 +1259,575 @@ pub struct HighlightSpec { pub valid_path: bool, pub force_underline: bool, } + +#[cfg(test)] +mod tests { + use super::{HighlightColorResolver, HighlightRole, HighlightSpec, highlight_shell}; + use crate::common::ScopeGuard; + use crate::env::EnvMode; + use crate::future_feature_flags::{self, FeatureFlag}; + use crate::operation_context::{EXPANSION_LIMIT_BACKGROUND, OperationContext}; + use crate::tests::prelude::*; + use crate::text_face::UnderlineStyle; + use crate::wchar::prelude::*; + use libc::PATH_MAX; + + // Helper to return a string whose length greatly exceeds PATH_MAX. + fn get_overlong_path() -> String { + let path_max = usize::try_from(PATH_MAX).unwrap(); + let mut longpath = String::with_capacity(path_max * 2 + 10); + while longpath.len() <= path_max * 2 { + longpath += "/overlong"; + } + longpath + } + + #[test] + #[serial] + fn test_highlighting() { + let _cleanup = test_init(); + let parser = TestParser::new(); + // Testing syntax highlighting + parser.pushd("test/fish_highlight_test/"); + let _popd = ScopeGuard::new((), |_| parser.popd()); + std::fs::create_dir_all("dir").unwrap(); + std::fs::create_dir_all("cdpath-entry/dir-in-cdpath").unwrap(); + std::fs::write("foo", []).unwrap(); + std::fs::write("bar", []).unwrap(); + + // Here are the components of our source and the colors we expect those to be. + #[derive(Debug)] + struct HighlightComponent<'a> { + text: &'a str, + color: HighlightSpec, + nospace: bool, + } + + macro_rules! component { + ( ( $text:expr, $color:expr) ) => { + HighlightComponent { + text: $text, + color: $color, + nospace: false, + } + }; + ( ( $text:literal, $color:expr, ns ) ) => { + HighlightComponent { + text: $text, + color: $color, + nospace: true, + } + }; + } + + macro_rules! validate { + ( $($comp:tt),* $(,)? ) => { + let components = [ + $( + component!($comp), + )* + ]; + let vars = parser.vars(); + // Generate the text. + let mut text = WString::new(); + let mut expected_colors = vec![]; + for comp in &components { + if !text.is_empty() && !comp.nospace { + text.push(' '); + expected_colors.push(HighlightSpec::new()); + } + text.push_str(comp.text); + expected_colors.resize(text.len(), comp.color); + } + assert_eq!(text.len(), expected_colors.len()); + + let mut colors = vec![]; + highlight_shell( + &text, + &mut colors, + &OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND), + true, /* io_ok */ + Some(text.len()), + ); + assert_eq!(colors.len(), expected_colors.len()); + + for (i, c) in text.chars().enumerate() { + // Hackish space handling. We don't care about the colors in spaces. + if c == ' ' { + continue; + } + + assert_eq!(colors[i], expected_colors[i], "Failed at position {i}, char {c}"); + } + }; + } + + let mut param_valid_path = HighlightSpec::with_fg(HighlightRole::param); + param_valid_path.valid_path = true; + + let saved_flag = future_feature_flags::test(FeatureFlag::ampersand_nobg_in_token); + future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, true); + let _restore_saved_flag = ScopeGuard::new((), |_| { + future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, saved_flag); + }); + + let fg = HighlightSpec::with_fg; + + // Verify variables and wildcards in commands using /bin/cat. + let vars = parser.vars(); + vars.set_one( + L!("CDPATH"), + EnvMode::LOCAL, + L!("./cdpath-entry").to_owned(), + ); + + vars.set_one( + L!("VARIABLE_IN_COMMAND"), + EnvMode::LOCAL, + L!("a").to_owned(), + ); + vars.set_one( + L!("VARIABLE_IN_COMMAND2"), + EnvMode::LOCAL, + L!("at").to_owned(), + ); + + let _cleanup = ScopeGuard::new((), |_| { + vars.remove(L!("VARIABLE_IN_COMMAND"), EnvMode::default()); + vars.remove(L!("VARIABLE_IN_COMMAND2"), EnvMode::default()); + }); + + validate!( + ("echo", fg(HighlightRole::command)), + ("./foo", param_valid_path), + ("&", fg(HighlightRole::statement_terminator)), + ); + + validate!( + ("command", fg(HighlightRole::keyword)), + ("echo", fg(HighlightRole::command)), + ("abc", fg(HighlightRole::param)), + ("foo", param_valid_path), + ("&", fg(HighlightRole::statement_terminator)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("foo&bar", fg(HighlightRole::param)), + ("foo", fg(HighlightRole::param), ns), + ("&", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + ("&>", fg(HighlightRole::redirection)), + ); + + validate!( + ("if command", fg(HighlightRole::keyword)), + ("ls", fg(HighlightRole::command)), + ("; ", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + ("abc", fg(HighlightRole::param)), + ("; ", fg(HighlightRole::statement_terminator)), + ("/bin/definitely_not_a_command", fg(HighlightRole::error)), + ("; ", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("if", fg(HighlightRole::keyword)), + ("true", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("else", fg(HighlightRole::keyword)), + ("true", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + // Verify that cd shows errors for non-directories. + validate!( + ("cd", fg(HighlightRole::command)), + ("dir", param_valid_path), + ); + + validate!( + ("cd", fg(HighlightRole::command)), + ("foo", fg(HighlightRole::error)), + ); + + validate!( + ("cd", fg(HighlightRole::command)), + ("--help", fg(HighlightRole::option)), + ("-h", fg(HighlightRole::option)), + ("definitely_not_a_directory", fg(HighlightRole::error)), + ); + + validate!( + ("cd", fg(HighlightRole::command)), + ("dir-in-cdpath", param_valid_path), + ); + + // Command substitutions. + validate!( + ("echo", fg(HighlightRole::command)), + ("param1", fg(HighlightRole::param)), + ("-l", fg(HighlightRole::option)), + ("--", fg(HighlightRole::option)), + ("-l", fg(HighlightRole::param)), + ("(", fg(HighlightRole::operat)), + ("ls", fg(HighlightRole::command)), + ("-l", fg(HighlightRole::option)), + ("--", fg(HighlightRole::option)), + ("-l", fg(HighlightRole::param)), + ("param2", fg(HighlightRole::param)), + (")", fg(HighlightRole::operat)), + ("|", fg(HighlightRole::statement_terminator)), + ("cat", fg(HighlightRole::command)), + ); + validate!( + ("true", fg(HighlightRole::command)), + ("$(", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (")", fg(HighlightRole::operat)), + ); + validate!( + ("true", fg(HighlightRole::command)), + ("\"before", fg(HighlightRole::quote)), + ("$(", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + ("param1", fg(HighlightRole::param)), + (")", fg(HighlightRole::operat)), + ("after\"", fg(HighlightRole::quote)), + ("param2", fg(HighlightRole::param)), + ); + validate!( + ("true", fg(HighlightRole::command)), + ("\"", fg(HighlightRole::error)), + ("unclosed quote", fg(HighlightRole::quote)), + ("$(", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (")", fg(HighlightRole::operat)), + ); + + // Redirections substitutions. + validate!( + ("echo", fg(HighlightRole::command)), + ("param1", fg(HighlightRole::param)), + // Input redirection. + ("<", fg(HighlightRole::redirection)), + ("/bin/echo", fg(HighlightRole::redirection)), + // Output redirection to a valid fd. + ("1>&2", fg(HighlightRole::redirection)), + // Output redirection to an invalid fd. + ("2>&", fg(HighlightRole::redirection)), + ("LO", fg(HighlightRole::error)), + // Just a param, not a redirection. + ("test/blah", fg(HighlightRole::param)), + // Input redirection from directory. + ("<", fg(HighlightRole::redirection)), + ("test/", fg(HighlightRole::error)), + // Output redirection to an invalid path. + ("3>", fg(HighlightRole::redirection)), + ("/not/a/valid/path/nope", fg(HighlightRole::error)), + // Output redirection to directory. + ("3>", fg(HighlightRole::redirection)), + ("test/nope/", fg(HighlightRole::error)), + // Redirections to overflow fd. + ("99999999999999999999>&2", fg(HighlightRole::error)), + ("2>&", fg(HighlightRole::redirection)), + ("99999999999999999999", fg(HighlightRole::error)), + // Output redirection containing a command substitution. + ("4>", fg(HighlightRole::redirection)), + ("(", fg(HighlightRole::operat)), + ("echo", fg(HighlightRole::command)), + ("test/somewhere", fg(HighlightRole::param)), + (")", fg(HighlightRole::operat)), + // Just another param. + ("param2", fg(HighlightRole::param)), + ); + + validate!( + ("for", fg(HighlightRole::keyword)), + ("x", fg(HighlightRole::param)), + ("in", fg(HighlightRole::keyword)), + ("set-by-for-1", fg(HighlightRole::param)), + ("set-by-for-2", fg(HighlightRole::param)), + (";", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + (">", fg(HighlightRole::redirection)), + ("$x", fg(HighlightRole::redirection)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("set", fg(HighlightRole::command)), + ("x", fg(HighlightRole::param)), + ("set-by-set", fg(HighlightRole::param)), + (";", fg(HighlightRole::statement_terminator)), + ("echo", fg(HighlightRole::command)), + (">", fg(HighlightRole::redirection)), + ("$x", fg(HighlightRole::redirection)), + ("2>", fg(HighlightRole::redirection)), + ("$totally_not_x", fg(HighlightRole::error)), + ("<", fg(HighlightRole::redirection)), + ("$x_but_its_an_impostor", fg(HighlightRole::error)), + ); + + validate!( + ("x", fg(HighlightRole::param), ns), + ("=", fg(HighlightRole::operat), ns), + ("set-by-variable-override", fg(HighlightRole::param), ns), + ("echo", fg(HighlightRole::command)), + (">", fg(HighlightRole::redirection)), + ("$x", fg(HighlightRole::redirection)), + ); + + validate!( + ("end", fg(HighlightRole::error)), + (";", fg(HighlightRole::statement_terminator)), + ("if", fg(HighlightRole::keyword)), + ("end", fg(HighlightRole::error)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("'", fg(HighlightRole::error)), + ("single_quote", fg(HighlightRole::quote)), + ("$stuff", fg(HighlightRole::quote)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("\"", fg(HighlightRole::error)), + ("double_quote", fg(HighlightRole::quote)), + ("$stuff", fg(HighlightRole::operat)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("$foo", fg(HighlightRole::operat)), + ("\"", fg(HighlightRole::quote)), + ("$bar", fg(HighlightRole::operat)), + ("\"", fg(HighlightRole::quote)), + ("$baz[", fg(HighlightRole::operat)), + ("1 2..3", fg(HighlightRole::param)), + ("]", fg(HighlightRole::operat)), + ); + + validate!( + ("for", fg(HighlightRole::keyword)), + ("i", fg(HighlightRole::param)), + ("in", fg(HighlightRole::keyword)), + ("1 2 3", fg(HighlightRole::param)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("$$foo[", fg(HighlightRole::operat)), + ("1", fg(HighlightRole::param)), + ("][", fg(HighlightRole::operat)), + ("2", fg(HighlightRole::param)), + ("]", fg(HighlightRole::operat)), + ("[3]", fg(HighlightRole::param)), // two dollar signs, so last one is not an expansion + ); + + validate!( + ("cat", fg(HighlightRole::command)), + ("/dev/null", param_valid_path), + ("|", fg(HighlightRole::statement_terminator)), + // This is bogus, but we used to use "less" here and that doesn't have to be installed. + ("cat", fg(HighlightRole::command)), + ("2>", fg(HighlightRole::redirection)), + ); + + // Highlight path-prefixes only at the cursor. + validate!( + ("cat", fg(HighlightRole::command)), + ("/dev/nu", fg(HighlightRole::param)), + ("/dev/nu", param_valid_path), + ); + + validate!( + ("if", fg(HighlightRole::keyword)), + ("true", fg(HighlightRole::command)), + ("&&", fg(HighlightRole::operat)), + ("false", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("or", fg(HighlightRole::operat)), + ("false", fg(HighlightRole::command)), + ("||", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("and", fg(HighlightRole::operat)), + ("not", fg(HighlightRole::operat)), + ("!", fg(HighlightRole::operat)), + ("true", fg(HighlightRole::command)), + (";", fg(HighlightRole::statement_terminator)), + ("end", fg(HighlightRole::keyword)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("%self", fg(HighlightRole::operat)), + ("not%self", fg(HighlightRole::param)), + ("self%not", fg(HighlightRole::param)), + ); + + validate!( + ("false", fg(HighlightRole::command)), + ("&|", fg(HighlightRole::statement_terminator)), + ("true", fg(HighlightRole::command)), + ); + + validate!( + ("HOME", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + (".", fg(HighlightRole::param), ns), + ("VAR1", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + ("VAL1", fg(HighlightRole::param), ns), + ("VAR", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + ("false", fg(HighlightRole::command)), + ("|&", fg(HighlightRole::error)), + ("true", fg(HighlightRole::command)), + ("stuff", fg(HighlightRole::param)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), // ( + (")", fg(HighlightRole::error)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("stuff", fg(HighlightRole::param)), + ("# comment", fg(HighlightRole::comment)), + ); + + validate!( + ("echo", fg(HighlightRole::command)), + ("--", fg(HighlightRole::option)), + ("-s", fg(HighlightRole::param)), + ); + + // Overlong paths don't crash (#7837). + let overlong = get_overlong_path(); + validate!( + ("touch", fg(HighlightRole::command)), + (&overlong, fg(HighlightRole::param)), + ); + + validate!( + ("a", fg(HighlightRole::param)), + ("=", fg(HighlightRole::operat), ns), + ); + + // Highlighting works across escaped line breaks (#8444). + validate!( + ("echo", fg(HighlightRole::command)), + ("$FISH_\\\n", fg(HighlightRole::operat)), + ("VERSION", fg(HighlightRole::operat), ns), + ); + + validate!( + ("/bin/ca", fg(HighlightRole::command), ns), + ("*", fg(HighlightRole::operat), ns) + ); + + validate!( + ("/bin/c", fg(HighlightRole::command), ns), + ("*", fg(HighlightRole::operat), ns) + ); + + validate!( + ("/bin/c", fg(HighlightRole::command), ns), + ("{$VARIABLE_IN_COMMAND}", fg(HighlightRole::operat), ns), + ("*", fg(HighlightRole::operat), ns) + ); + + validate!( + ("/bin/c", fg(HighlightRole::command), ns), + ("$VARIABLE_IN_COMMAND2", fg(HighlightRole::operat), ns) + ); + + validate!(("$EMPTY_VARIABLE", fg(HighlightRole::error))); + validate!(("\"$EMPTY_VARIABLE\"", fg(HighlightRole::error))); + + validate!( + ("echo", fg(HighlightRole::command)), + ("\\UFDFD", fg(HighlightRole::escape)), + ); + validate!( + ("echo", fg(HighlightRole::command)), + ("\\U10FFFF", fg(HighlightRole::escape)), + ); + validate!( + ("echo", fg(HighlightRole::command)), + ("\\U110000", fg(HighlightRole::error)), + ); + + validate!( + (">", fg(HighlightRole::error)), + ("echo", fg(HighlightRole::error)), + ); + } + + /// Tests that trailing spaces after a command don't inherit the underline formatting of the + /// command. + #[test] + #[serial] + #[allow(clippy::needless_range_loop)] + fn test_trailing_spaces_after_command() { + let _cleanup = test_init(); + let parser = TestParser::new(); + let vars = parser.vars(); + + // First, set up fish_color_command to include underline + vars.set_one( + L!("fish_color_command"), + EnvMode::LOCAL, + L!("--underline").to_owned(), + ); + + // Prepare a command with trailing spaces for highlighting + let text = L!("echo ").to_owned(); // Command 'echo' followed by 3 spaces + let mut colors = vec![]; + highlight_shell( + &text, + &mut colors, + &OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND), + true, /* io_ok */ + Some(text.len()), + ); + + // Verify we have the right number of colors + assert_eq!(colors.len(), text.len()); + + // Create a resolver to check actual RGB colors with their attributes + let mut resolver = HighlightColorResolver::new(); + + // Check that 'echo' is underlined + for i in 0..4 { + let face = resolver.resolve_spec(&colors[i], vars); + assert_eq!( + face.style.underline_style(), + Some(UnderlineStyle::Single), + "Character at position {} of 'echo' should be underlined", + i + ); + } + + // Check that trailing spaces are NOT underlined + for i in 4..text.len() { + let face = resolver.resolve_spec(&colors[i], vars); + assert_eq!( + face.style.underline_style(), + None, + "Trailing space at position {} should NOT be underlined", + i + ); + } + } +} diff --git a/src/highlight/mod.rs b/src/highlight/mod.rs index ced381ab1..f11770168 100644 --- a/src/highlight/mod.rs +++ b/src/highlight/mod.rs @@ -3,6 +3,3 @@ mod highlight; pub use file_tester::is_potential_path; pub use highlight::*; - -#[cfg(test)] -mod tests; diff --git a/src/highlight/tests.rs b/src/highlight/tests.rs deleted file mode 100644 index a36626a90..000000000 --- a/src/highlight/tests.rs +++ /dev/null @@ -1,971 +0,0 @@ -use crate::common::ScopeGuard; -use crate::env::EnvMode; -use crate::future_feature_flags::{self, FeatureFlag}; -use crate::highlight::HighlightColorResolver; -use crate::tests::prelude::*; -use crate::text_face::UnderlineStyle; -use crate::wchar::prelude::*; -use crate::{ - env::EnvStack, - highlight::file_tester::{PathFlags, is_potential_path}, - highlight::{HighlightRole, HighlightSpec, highlight_shell}, - operation_context::{EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT, OperationContext}, -}; -use libc::PATH_MAX; - -// Helper to return a string whose length greatly exceeds PATH_MAX. -fn get_overlong_path() -> String { - let path_max = usize::try_from(PATH_MAX).unwrap(); - let mut longpath = String::with_capacity(path_max * 2 + 10); - while longpath.len() <= path_max * 2 { - longpath += "/overlong"; - } - longpath -} - -#[test] -#[serial] -fn test_is_potential_path() { - let _cleanup = test_init(); - // Directories - std::fs::create_dir_all("test/is_potential_path_test/alpha/").unwrap(); - std::fs::create_dir_all("test/is_potential_path_test/beta/").unwrap(); - - // Files - std::fs::write("test/is_potential_path_test/aardvark", []).unwrap(); - std::fs::write("test/is_potential_path_test/gamma", []).unwrap(); - - let wd = L!("test/is_potential_path_test/").to_owned(); - let wds = [L!(".").to_owned(), wd]; - - let vars = EnvStack::new(); - let ctx = OperationContext::background(&vars, EXPANSION_LIMIT_DEFAULT); - - let path_require_dir = PathFlags { - require_dir: true, - ..Default::default() - }; - - assert!(is_potential_path( - L!("al"), - true, - &wds[..], - &ctx, - path_require_dir - )); - - assert!(is_potential_path( - L!("alpha/"), - true, - &wds[..], - &ctx, - path_require_dir - )); - assert!(is_potential_path( - L!("aard"), - true, - &wds[..], - &ctx, - PathFlags::default() - )); - assert!(!is_potential_path( - L!("aard"), - false, - &wds[..], - &ctx, - PathFlags::default() - )); - assert!(!is_potential_path( - L!("alp/"), - true, - &wds[..], - &ctx, - PathFlags { - require_dir: true, - for_cd: true, - ..Default::default() - } - )); - - assert!(!is_potential_path( - L!("balpha/"), - true, - &wds[..], - &ctx, - path_require_dir - )); - assert!(!is_potential_path( - L!("aard"), - true, - &wds[..], - &ctx, - path_require_dir - )); - assert!(!is_potential_path( - L!("aarde"), - true, - &wds[..], - &ctx, - path_require_dir - )); - assert!(!is_potential_path( - L!("aarde"), - true, - &wds[..], - &ctx, - PathFlags::default() - )); - - assert!(is_potential_path( - L!("test/is_potential_path_test/aardvark"), - true, - &wds[..], - &ctx, - PathFlags::default() - )); - assert!(is_potential_path( - L!("test/is_potential_path_test/al"), - true, - &wds[..], - &ctx, - path_require_dir - )); - assert!(is_potential_path( - L!("test/is_potential_path_test/aardv"), - true, - &wds[..], - &ctx, - PathFlags::default() - )); - - assert!(!is_potential_path( - L!("test/is_potential_path_test/aardvark"), - true, - &wds[..], - &ctx, - path_require_dir - )); - assert!(!is_potential_path( - L!("test/is_potential_path_test/al/"), - true, - &wds[..], - &ctx, - PathFlags::default() - )); - assert!(!is_potential_path( - L!("test/is_potential_path_test/ar"), - true, - &wds[..], - &ctx, - PathFlags::default() - )); - assert!(is_potential_path( - L!("/usr"), - true, - &wds[..], - &ctx, - path_require_dir - )); -} - -#[test] -#[serial] -fn test_highlighting() { - let _cleanup = test_init(); - let parser = TestParser::new(); - // Testing syntax highlighting - parser.pushd("test/fish_highlight_test/"); - let _popd = ScopeGuard::new((), |_| parser.popd()); - std::fs::create_dir_all("dir").unwrap(); - std::fs::create_dir_all("cdpath-entry/dir-in-cdpath").unwrap(); - std::fs::write("foo", []).unwrap(); - std::fs::write("bar", []).unwrap(); - - // Here are the components of our source and the colors we expect those to be. - #[derive(Debug)] - struct HighlightComponent<'a> { - text: &'a str, - color: HighlightSpec, - nospace: bool, - } - - macro_rules! component { - ( ( $text:expr, $color:expr) ) => { - HighlightComponent { - text: $text, - color: $color, - nospace: false, - } - }; - ( ( $text:literal, $color:expr, ns ) ) => { - HighlightComponent { - text: $text, - color: $color, - nospace: true, - } - }; - } - - macro_rules! validate { - ( $($comp:tt),* $(,)? ) => { - let components = [ - $( - component!($comp), - )* - ]; - let vars = parser.vars(); - // Generate the text. - let mut text = WString::new(); - let mut expected_colors = vec![]; - for comp in &components { - if !text.is_empty() && !comp.nospace { - text.push(' '); - expected_colors.push(HighlightSpec::new()); - } - text.push_str(comp.text); - expected_colors.resize(text.len(), comp.color); - } - assert_eq!(text.len(), expected_colors.len()); - - let mut colors = vec![]; - highlight_shell( - &text, - &mut colors, - &OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND), - true, /* io_ok */ - Some(text.len()), - ); - assert_eq!(colors.len(), expected_colors.len()); - - for (i, c) in text.chars().enumerate() { - // Hackish space handling. We don't care about the colors in spaces. - if c == ' ' { - continue; - } - - assert_eq!(colors[i], expected_colors[i], "Failed at position {i}, char {c}"); - } - }; - } - - let mut param_valid_path = HighlightSpec::with_fg(HighlightRole::param); - param_valid_path.valid_path = true; - - let saved_flag = future_feature_flags::test(FeatureFlag::ampersand_nobg_in_token); - future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, true); - let _restore_saved_flag = ScopeGuard::new((), |_| { - future_feature_flags::set(FeatureFlag::ampersand_nobg_in_token, saved_flag); - }); - - let fg = HighlightSpec::with_fg; - - // Verify variables and wildcards in commands using /bin/cat. - let vars = parser.vars(); - vars.set_one( - L!("CDPATH"), - EnvMode::LOCAL, - L!("./cdpath-entry").to_owned(), - ); - - vars.set_one( - L!("VARIABLE_IN_COMMAND"), - EnvMode::LOCAL, - L!("a").to_owned(), - ); - vars.set_one( - L!("VARIABLE_IN_COMMAND2"), - EnvMode::LOCAL, - L!("at").to_owned(), - ); - - let _cleanup = ScopeGuard::new((), |_| { - vars.remove(L!("VARIABLE_IN_COMMAND"), EnvMode::default()); - vars.remove(L!("VARIABLE_IN_COMMAND2"), EnvMode::default()); - }); - - validate!( - ("echo", fg(HighlightRole::command)), - ("./foo", param_valid_path), - ("&", fg(HighlightRole::statement_terminator)), - ); - - validate!( - ("command", fg(HighlightRole::keyword)), - ("echo", fg(HighlightRole::command)), - ("abc", fg(HighlightRole::param)), - ("foo", param_valid_path), - ("&", fg(HighlightRole::statement_terminator)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("foo&bar", fg(HighlightRole::param)), - ("foo", fg(HighlightRole::param), ns), - ("&", fg(HighlightRole::statement_terminator)), - ("echo", fg(HighlightRole::command)), - ("&>", fg(HighlightRole::redirection)), - ); - - validate!( - ("if command", fg(HighlightRole::keyword)), - ("ls", fg(HighlightRole::command)), - ("; ", fg(HighlightRole::statement_terminator)), - ("echo", fg(HighlightRole::command)), - ("abc", fg(HighlightRole::param)), - ("; ", fg(HighlightRole::statement_terminator)), - ("/bin/definitely_not_a_command", fg(HighlightRole::error)), - ("; ", fg(HighlightRole::statement_terminator)), - ("end", fg(HighlightRole::keyword)), - ); - - validate!( - ("if", fg(HighlightRole::keyword)), - ("true", fg(HighlightRole::command)), - (";", fg(HighlightRole::statement_terminator)), - ("else", fg(HighlightRole::keyword)), - ("true", fg(HighlightRole::command)), - (";", fg(HighlightRole::statement_terminator)), - ("end", fg(HighlightRole::keyword)), - ); - - // Verify that cd shows errors for non-directories. - validate!( - ("cd", fg(HighlightRole::command)), - ("dir", param_valid_path), - ); - - validate!( - ("cd", fg(HighlightRole::command)), - ("foo", fg(HighlightRole::error)), - ); - - validate!( - ("cd", fg(HighlightRole::command)), - ("--help", fg(HighlightRole::option)), - ("-h", fg(HighlightRole::option)), - ("definitely_not_a_directory", fg(HighlightRole::error)), - ); - - validate!( - ("cd", fg(HighlightRole::command)), - ("dir-in-cdpath", param_valid_path), - ); - - // Command substitutions. - validate!( - ("echo", fg(HighlightRole::command)), - ("param1", fg(HighlightRole::param)), - ("-l", fg(HighlightRole::option)), - ("--", fg(HighlightRole::option)), - ("-l", fg(HighlightRole::param)), - ("(", fg(HighlightRole::operat)), - ("ls", fg(HighlightRole::command)), - ("-l", fg(HighlightRole::option)), - ("--", fg(HighlightRole::option)), - ("-l", fg(HighlightRole::param)), - ("param2", fg(HighlightRole::param)), - (")", fg(HighlightRole::operat)), - ("|", fg(HighlightRole::statement_terminator)), - ("cat", fg(HighlightRole::command)), - ); - validate!( - ("true", fg(HighlightRole::command)), - ("$(", fg(HighlightRole::operat)), - ("true", fg(HighlightRole::command)), - (")", fg(HighlightRole::operat)), - ); - validate!( - ("true", fg(HighlightRole::command)), - ("\"before", fg(HighlightRole::quote)), - ("$(", fg(HighlightRole::operat)), - ("true", fg(HighlightRole::command)), - ("param1", fg(HighlightRole::param)), - (")", fg(HighlightRole::operat)), - ("after\"", fg(HighlightRole::quote)), - ("param2", fg(HighlightRole::param)), - ); - validate!( - ("true", fg(HighlightRole::command)), - ("\"", fg(HighlightRole::error)), - ("unclosed quote", fg(HighlightRole::quote)), - ("$(", fg(HighlightRole::operat)), - ("true", fg(HighlightRole::command)), - (")", fg(HighlightRole::operat)), - ); - - // Redirections substitutions. - validate!( - ("echo", fg(HighlightRole::command)), - ("param1", fg(HighlightRole::param)), - // Input redirection. - ("<", fg(HighlightRole::redirection)), - ("/bin/echo", fg(HighlightRole::redirection)), - // Output redirection to a valid fd. - ("1>&2", fg(HighlightRole::redirection)), - // Output redirection to an invalid fd. - ("2>&", fg(HighlightRole::redirection)), - ("LO", fg(HighlightRole::error)), - // Just a param, not a redirection. - ("test/blah", fg(HighlightRole::param)), - // Input redirection from directory. - ("<", fg(HighlightRole::redirection)), - ("test/", fg(HighlightRole::error)), - // Output redirection to an invalid path. - ("3>", fg(HighlightRole::redirection)), - ("/not/a/valid/path/nope", fg(HighlightRole::error)), - // Output redirection to directory. - ("3>", fg(HighlightRole::redirection)), - ("test/nope/", fg(HighlightRole::error)), - // Redirections to overflow fd. - ("99999999999999999999>&2", fg(HighlightRole::error)), - ("2>&", fg(HighlightRole::redirection)), - ("99999999999999999999", fg(HighlightRole::error)), - // Output redirection containing a command substitution. - ("4>", fg(HighlightRole::redirection)), - ("(", fg(HighlightRole::operat)), - ("echo", fg(HighlightRole::command)), - ("test/somewhere", fg(HighlightRole::param)), - (")", fg(HighlightRole::operat)), - // Just another param. - ("param2", fg(HighlightRole::param)), - ); - - validate!( - ("for", fg(HighlightRole::keyword)), - ("x", fg(HighlightRole::param)), - ("in", fg(HighlightRole::keyword)), - ("set-by-for-1", fg(HighlightRole::param)), - ("set-by-for-2", fg(HighlightRole::param)), - (";", fg(HighlightRole::statement_terminator)), - ("echo", fg(HighlightRole::command)), - (">", fg(HighlightRole::redirection)), - ("$x", fg(HighlightRole::redirection)), - (";", fg(HighlightRole::statement_terminator)), - ("end", fg(HighlightRole::keyword)), - ); - - validate!( - ("set", fg(HighlightRole::command)), - ("x", fg(HighlightRole::param)), - ("set-by-set", fg(HighlightRole::param)), - (";", fg(HighlightRole::statement_terminator)), - ("echo", fg(HighlightRole::command)), - (">", fg(HighlightRole::redirection)), - ("$x", fg(HighlightRole::redirection)), - ("2>", fg(HighlightRole::redirection)), - ("$totally_not_x", fg(HighlightRole::error)), - ("<", fg(HighlightRole::redirection)), - ("$x_but_its_an_impostor", fg(HighlightRole::error)), - ); - - validate!( - ("x", fg(HighlightRole::param), ns), - ("=", fg(HighlightRole::operat), ns), - ("set-by-variable-override", fg(HighlightRole::param), ns), - ("echo", fg(HighlightRole::command)), - (">", fg(HighlightRole::redirection)), - ("$x", fg(HighlightRole::redirection)), - ); - - validate!( - ("end", fg(HighlightRole::error)), - (";", fg(HighlightRole::statement_terminator)), - ("if", fg(HighlightRole::keyword)), - ("end", fg(HighlightRole::error)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("'", fg(HighlightRole::error)), - ("single_quote", fg(HighlightRole::quote)), - ("$stuff", fg(HighlightRole::quote)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("\"", fg(HighlightRole::error)), - ("double_quote", fg(HighlightRole::quote)), - ("$stuff", fg(HighlightRole::operat)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("$foo", fg(HighlightRole::operat)), - ("\"", fg(HighlightRole::quote)), - ("$bar", fg(HighlightRole::operat)), - ("\"", fg(HighlightRole::quote)), - ("$baz[", fg(HighlightRole::operat)), - ("1 2..3", fg(HighlightRole::param)), - ("]", fg(HighlightRole::operat)), - ); - - validate!( - ("for", fg(HighlightRole::keyword)), - ("i", fg(HighlightRole::param)), - ("in", fg(HighlightRole::keyword)), - ("1 2 3", fg(HighlightRole::param)), - (";", fg(HighlightRole::statement_terminator)), - ("end", fg(HighlightRole::keyword)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("$$foo[", fg(HighlightRole::operat)), - ("1", fg(HighlightRole::param)), - ("][", fg(HighlightRole::operat)), - ("2", fg(HighlightRole::param)), - ("]", fg(HighlightRole::operat)), - ("[3]", fg(HighlightRole::param)), // two dollar signs, so last one is not an expansion - ); - - validate!( - ("cat", fg(HighlightRole::command)), - ("/dev/null", param_valid_path), - ("|", fg(HighlightRole::statement_terminator)), - // This is bogus, but we used to use "less" here and that doesn't have to be installed. - ("cat", fg(HighlightRole::command)), - ("2>", fg(HighlightRole::redirection)), - ); - - // Highlight path-prefixes only at the cursor. - validate!( - ("cat", fg(HighlightRole::command)), - ("/dev/nu", fg(HighlightRole::param)), - ("/dev/nu", param_valid_path), - ); - - validate!( - ("if", fg(HighlightRole::keyword)), - ("true", fg(HighlightRole::command)), - ("&&", fg(HighlightRole::operat)), - ("false", fg(HighlightRole::command)), - (";", fg(HighlightRole::statement_terminator)), - ("or", fg(HighlightRole::operat)), - ("false", fg(HighlightRole::command)), - ("||", fg(HighlightRole::operat)), - ("true", fg(HighlightRole::command)), - (";", fg(HighlightRole::statement_terminator)), - ("and", fg(HighlightRole::operat)), - ("not", fg(HighlightRole::operat)), - ("!", fg(HighlightRole::operat)), - ("true", fg(HighlightRole::command)), - (";", fg(HighlightRole::statement_terminator)), - ("end", fg(HighlightRole::keyword)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("%self", fg(HighlightRole::operat)), - ("not%self", fg(HighlightRole::param)), - ("self%not", fg(HighlightRole::param)), - ); - - validate!( - ("false", fg(HighlightRole::command)), - ("&|", fg(HighlightRole::statement_terminator)), - ("true", fg(HighlightRole::command)), - ); - - validate!( - ("HOME", fg(HighlightRole::param)), - ("=", fg(HighlightRole::operat), ns), - (".", fg(HighlightRole::param), ns), - ("VAR1", fg(HighlightRole::param)), - ("=", fg(HighlightRole::operat), ns), - ("VAL1", fg(HighlightRole::param), ns), - ("VAR", fg(HighlightRole::param)), - ("=", fg(HighlightRole::operat), ns), - ("false", fg(HighlightRole::command)), - ("|&", fg(HighlightRole::error)), - ("true", fg(HighlightRole::command)), - ("stuff", fg(HighlightRole::param)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), // ( - (")", fg(HighlightRole::error)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("stuff", fg(HighlightRole::param)), - ("# comment", fg(HighlightRole::comment)), - ); - - validate!( - ("echo", fg(HighlightRole::command)), - ("--", fg(HighlightRole::option)), - ("-s", fg(HighlightRole::param)), - ); - - // Overlong paths don't crash (#7837). - let overlong = get_overlong_path(); - validate!( - ("touch", fg(HighlightRole::command)), - (&overlong, fg(HighlightRole::param)), - ); - - validate!( - ("a", fg(HighlightRole::param)), - ("=", fg(HighlightRole::operat), ns), - ); - - // Highlighting works across escaped line breaks (#8444). - validate!( - ("echo", fg(HighlightRole::command)), - ("$FISH_\\\n", fg(HighlightRole::operat)), - ("VERSION", fg(HighlightRole::operat), ns), - ); - - validate!( - ("/bin/ca", fg(HighlightRole::command), ns), - ("*", fg(HighlightRole::operat), ns) - ); - - validate!( - ("/bin/c", fg(HighlightRole::command), ns), - ("*", fg(HighlightRole::operat), ns) - ); - - validate!( - ("/bin/c", fg(HighlightRole::command), ns), - ("{$VARIABLE_IN_COMMAND}", fg(HighlightRole::operat), ns), - ("*", fg(HighlightRole::operat), ns) - ); - - validate!( - ("/bin/c", fg(HighlightRole::command), ns), - ("$VARIABLE_IN_COMMAND2", fg(HighlightRole::operat), ns) - ); - - validate!(("$EMPTY_VARIABLE", fg(HighlightRole::error))); - validate!(("\"$EMPTY_VARIABLE\"", fg(HighlightRole::error))); - - validate!( - ("echo", fg(HighlightRole::command)), - ("\\UFDFD", fg(HighlightRole::escape)), - ); - validate!( - ("echo", fg(HighlightRole::command)), - ("\\U10FFFF", fg(HighlightRole::escape)), - ); - validate!( - ("echo", fg(HighlightRole::command)), - ("\\U110000", fg(HighlightRole::error)), - ); - - validate!( - (">", fg(HighlightRole::error)), - ("echo", fg(HighlightRole::error)), - ); -} - -/// Tests that trailing spaces after a command don't inherit the underline formatting of the -/// command. -#[test] -#[serial] -#[allow(clippy::needless_range_loop)] -fn test_trailing_spaces_after_command() { - let _cleanup = test_init(); - let parser = TestParser::new(); - let vars = parser.vars(); - - // First, set up fish_color_command to include underline - vars.set_one( - L!("fish_color_command"), - EnvMode::LOCAL, - L!("--underline").to_owned(), - ); - - // Prepare a command with trailing spaces for highlighting - let text = L!("echo ").to_owned(); // Command 'echo' followed by 3 spaces - let mut colors = vec![]; - highlight_shell( - &text, - &mut colors, - &OperationContext::background(vars, EXPANSION_LIMIT_BACKGROUND), - true, /* io_ok */ - Some(text.len()), - ); - - // Verify we have the right number of colors - assert_eq!(colors.len(), text.len()); - - // Create a resolver to check actual RGB colors with their attributes - let mut resolver = HighlightColorResolver::new(); - - // Check that 'echo' is underlined - for i in 0..4 { - let face = resolver.resolve_spec(&colors[i], vars); - assert_eq!( - face.style.underline_style(), - Some(UnderlineStyle::Single), - "Character at position {} of 'echo' should be underlined", - i - ); - } - - // Check that trailing spaces are NOT underlined - for i in 4..text.len() { - let face = resolver.resolve_spec(&colors[i], vars); - assert_eq!( - face.style.underline_style(), - None, - "Trailing space at position {} should NOT be underlined", - i - ); - } -} - -pub use super::file_tester::{FileTester, IsErr, IsFile}; -mod file_tester_tests { - use super::*; - use crate::common::charptr2wcstring; - use crate::redirection::RedirectionMode; - use std::fs::{self, File, Permissions, create_dir_all}; - use std::os::unix::fs::PermissionsExt; - - struct TempDir { - basepath: WString, - ctx: OperationContext<'static>, - } - - impl TempDir { - fn new() -> TempDir { - let mut t1 = *b"/tmp/fish_file_tester_dir.XXXXXX\0"; - let basepath_narrow = unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }; - assert!(!basepath_narrow.is_null(), "mkdtemp failed"); - let basepath: WString = charptr2wcstring(basepath_narrow); - TempDir { - basepath, - ctx: OperationContext::empty(), - } - } - - fn filepath(&self, name: &str) -> String { - let mut result = self.basepath.to_string(); - result.push('/'); - result.push_str(name); - result - } - - fn file_tester(&self) -> FileTester<'_> { - FileTester::new(self.basepath.clone(), &self.ctx) - } - } - - impl Drop for TempDir { - fn drop(&mut self) { - let _ = std::fs::remove_dir_all(self.basepath.to_string()); - } - } - - #[test] - fn test_ispath() { - let temp = TempDir::new(); - let tester = temp.file_tester(); - - let file_path = temp.filepath("file.txt"); - File::create(file_path).unwrap(); - - let result = tester.test_path(L!("file.txt"), false); - assert!(result); - - let result = tester.test_path(L!("file.txt"), true); - assert!(result); - - let result = tester.test_path(L!("fi"), false); - assert!(!result); - - let result = tester.test_path(L!("fi"), true); - assert!(result); - - let result = tester.test_path(L!("file.txt-more"), false); - assert!(!result); - - let result = tester.test_path(L!("file.txt-more"), true); - assert!(!result); - - let result = tester.test_path(L!("ffiledfk.txt"), false); - assert!(!result); - - let result = tester.test_path(L!("ffiledfk.txt"), true); - assert!(!result); - - // Directories are also files. - let dir_path = temp.filepath("somedir"); - create_dir_all(dir_path).unwrap(); - - let result = tester.test_path(L!("somedir"), false); - assert!(result); - - let result = tester.test_path(L!("somedir"), true); - assert!(result); - - let result = tester.test_path(L!("some"), false); - assert!(!result); - - let result = tester.test_path(L!("some"), true); - assert!(result); - } - - #[test] - fn test_iscdpath() { - let temp = TempDir::new(); - let tester = temp.file_tester(); - - // Note cd (unlike file paths) should report IsErr for invalid cd paths, - // rather than IsFile(false). - - let dir_path = temp.filepath("somedir"); - create_dir_all(dir_path).unwrap(); - - let result = tester.test_cd_path(L!("somedir"), false); - assert_eq!(result, Ok(IsFile(true))); - - let result = tester.test_cd_path(L!("somedir"), true); - assert_eq!(result, Ok(IsFile(true))); - - let result = tester.test_cd_path(L!("some"), false); - assert_eq!(result, Err(IsErr)); - - let result = tester.test_cd_path(L!("some"), true); - assert_eq!(result, Ok(IsFile(true))); - - let result = tester.test_cd_path(L!("notdir"), false); - assert_eq!(result, Err(IsErr)); - - let result = tester.test_cd_path(L!("notdir"), true); - assert_eq!(result, Err(IsErr)); - } - - #[test] - fn test_redirections() { - // Note we use is_ok and is_err since we don't care about the IsFile part. - let temp = TempDir::new(); - let tester = temp.file_tester(); - let file_path = temp.filepath("file.txt"); - File::create(&file_path).unwrap(); - - let dir_path = temp.filepath("somedir"); - create_dir_all(&dir_path).unwrap(); - - // Normal redirection. - let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::input); - assert!(result); - - // Can't redirect from a missing file - let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::input); - assert!(!result); - let result = - tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::input); - assert!(!result); - - // Can't redirect from a directory. - let result = tester.test_redirection_target(L!("somedir"), RedirectionMode::input); - assert!(!result); - - // Can't redirect from an unreadable file. - #[cfg(not(cygwin))] // Can't mark a file write-only on MSYS, this may work on true Cygwin - { - fs::set_permissions(&file_path, Permissions::from_mode(0o200)).unwrap(); - let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::input); - assert!(!result); - fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap(); - } - - // try_input syntax highlighting reports an error even though the command will succeed. - let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::try_input); - assert!(result); - let result = tester.test_redirection_target(L!("notfile.txt"), RedirectionMode::try_input); - assert!(!result); - let result = - tester.test_redirection_target(L!("bogus_path/file.txt"), RedirectionMode::try_input); - assert!(!result); - - // Test write redirections. - // Overwrite an existing file. - let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::overwrite); - assert!(result); - - // Append to an existing file. - let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::append); - assert!(result); - - // Write to a missing file. - let result = tester.test_redirection_target(L!("newfile.txt"), RedirectionMode::overwrite); - assert!(result); - - // No-clobber write to existing file should fail. - let result = tester.test_redirection_target(L!("file.txt"), RedirectionMode::noclob); - assert!(!result); - - // No-clobber write to missing file should succeed. - let result = tester.test_redirection_target(L!("unique.txt"), RedirectionMode::noclob); - assert!(result); - - let write_modes = &[ - RedirectionMode::overwrite, - RedirectionMode::append, - RedirectionMode::noclob, - ]; - - // Can't write to a directory. - for mode in write_modes { - assert!( - !tester.test_redirection_target(L!("somedir"), *mode), - "Should not be able to write to a directory with mode {:?}", - mode - ); - } - - // Can't write without write permissions. - fs::set_permissions(&file_path, Permissions::from_mode(0o400)).unwrap(); // Read-only. - for mode in write_modes { - assert!( - !tester.test_redirection_target(L!("file.txt"), *mode), - "Should not be able to write to a read-only file with mode {:?}", - mode - ); - } - fs::set_permissions(&file_path, Permissions::from_mode(0o600)).unwrap(); // Restore permissions. - - // Writing into a directory without write permissions (loop through all modes). - #[cfg(not(cygwin))] // Can't mark a file write-only on MSYS, this may work on true Cygwin - { - fs::set_permissions(&dir_path, Permissions::from_mode(0o500)).unwrap(); // Read and execute, no write. - for mode in write_modes { - assert!( - !tester.test_redirection_target(L!("somedir/newfile.txt"), *mode), - "Should not be able to create/write in a read-only directory with mode {:?}", - mode - ); - } - fs::set_permissions(&dir_path, Permissions::from_mode(0o700)).unwrap(); // Restore permissions. - } - - // Test fd redirections. - assert!(tester.test_redirection_target(L!("-"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("0"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("1"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("2"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("3"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("500"), RedirectionMode::fd)); - - // We are base 10, despite the leading 0. - assert!(tester.test_redirection_target(L!("000"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("01"), RedirectionMode::fd)); - assert!(tester.test_redirection_target(L!("07"), RedirectionMode::fd)); - - // Invalid fd redirections. - assert!(!tester.test_redirection_target(L!("0x2"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("0x3F"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("0F"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("-1"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("-0009"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("--"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("derp"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("123boo"), RedirectionMode::fd)); - assert!(!tester.test_redirection_target(L!("18446744073709551616"), RedirectionMode::fd)); - } -} diff --git a/src/history.rs b/src/history.rs index 08a25a150..65cb3122f 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1811,3 +1811,665 @@ pub fn start_private_mode(vars: &EnvStack) { pub fn in_private_mode(vars: &dyn Environment) -> bool { vars.get_unless_empty(L!("fish_private_mode")).is_some() } + +#[cfg(test)] +mod tests { + use super::{ + History, HistoryItem, HistorySearch, PathList, PersistenceMode, SearchDirection, + SearchFlags, SearchType, VACUUM_FREQUENCY, + }; + use crate::common::ESCAPE_TEST_CHAR; + use crate::common::{ScopeGuard, bytes2wcstring, wcs2bytes, wcs2osstring}; + use crate::env::{EnvMode, EnvStack}; + use crate::fs::{LockedFile, WriteMethod}; + use crate::path::path_get_data; + use crate::tests::prelude::*; + use crate::util::get_rng; + use crate::wchar::prelude::*; + use crate::wcstringutil::{string_prefixes_string, string_prefixes_string_case_insensitive}; + use fish_build_helper::workspace_root; + use rand::Rng; + use rand::rngs::SmallRng; + use std::collections::VecDeque; + use std::ffi::CString; + use std::io::BufReader; + use std::os::unix::ffi::OsStrExt; + use std::sync::Arc; + use std::time::UNIX_EPOCH; + use std::time::{Duration, SystemTime}; + + fn history_contains(history: &History, txt: &wstr) -> bool { + for i in 1.. { + let Some(item) = history.item_at_index(i) else { + break; + }; + + if item.str() == txt { + return true; + } + } + + false + } + + fn random_string(rng: &mut SmallRng) -> WString { + let mut result = WString::new(); + let max = rng.gen_range(1..=32); + for _ in 0..max { + let c = char::from_u32(u32::try_from(1 + rng.gen_range(0..ESCAPE_TEST_CHAR)).unwrap()) + .unwrap(); + result.push(c); + } + result + } + + #[test] + #[serial] + fn test_history() { + let _cleanup = test_init(); + macro_rules! test_history_matches { + ($search:expr, $expected:expr) => { + let expected: Vec<&wstr> = $expected; + let mut found = vec![]; + while $search.go_to_next_match(SearchDirection::Backward) { + found.push($search.current_string().to_owned()); + } + assert_eq!(expected, found); + }; + } + + let items = [ + L!("Gamma"), + L!("beta"), + L!("BetA"), + L!("Beta"), + L!("alpha"), + L!("AlphA"), + L!("Alpha"), + L!("alph"), + L!("ALPH"), + L!("ZZZ"), + ]; + let nocase = SearchFlags::IGNORE_CASE; + + // Populate a history. + let history = History::with_name(L!("test_history")); + history.clear(); + for s in items { + history.add_commandline(s.to_owned()); + } + + // Helper to set expected items to those matching a predicate, in reverse order. + let set_expected = |filt: fn(&wstr) -> bool| { + let mut expected = vec![]; + for s in items { + if filt(s) { + expected.push(s); + } + } + expected.reverse(); + expected + }; + + // Items matching "a", case-sensitive. + let mut searcher = HistorySearch::new(history.clone(), L!("a").to_owned()); + let expected = set_expected(|s| s.contains('a')); + test_history_matches!(searcher, expected); + + // Items matching "alpha", case-insensitive. + let mut searcher = + HistorySearch::new_with_flags(history.clone(), L!("AlPhA").to_owned(), nocase); + let expected = set_expected(|s| s.to_lowercase().find(L!("alpha")).is_some()); + test_history_matches!(searcher, expected); + + // Items matching "et", case-sensitive. + let mut searcher = HistorySearch::new(history.clone(), L!("et").to_owned()); + let expected = set_expected(|s| s.find(L!("et")).is_some()); + test_history_matches!(searcher, expected); + + // Items starting with "be", case-sensitive. + let mut searcher = + HistorySearch::new_with_type(history.clone(), L!("be").to_owned(), SearchType::Prefix); + let expected = set_expected(|s| string_prefixes_string(L!("be"), s)); + test_history_matches!(searcher, expected); + + // Items starting with "be", case-insensitive. + let mut searcher = HistorySearch::new_with( + history.clone(), + L!("be").to_owned(), + SearchType::Prefix, + nocase, + 0, + ); + let expected = set_expected(|s| string_prefixes_string_case_insensitive(L!("be"), s)); + test_history_matches!(searcher, expected); + + // Items exactly matching "alph", case-sensitive. + let mut searcher = + HistorySearch::new_with_type(history.clone(), L!("alph").to_owned(), SearchType::Exact); + let expected = set_expected(|s| s == "alph"); + test_history_matches!(searcher, expected); + + // Items exactly matching "alph", case-insensitive. + let mut searcher = HistorySearch::new_with( + history.clone(), + L!("alph").to_owned(), + SearchType::Exact, + nocase, + 0, + ); + let expected = set_expected(|s| s.to_lowercase() == "alph"); + test_history_matches!(searcher, expected); + + // Test item removal case-sensitive. + let mut searcher = HistorySearch::new(history.clone(), L!("Alpha").to_owned()); + test_history_matches!(searcher, vec![L!("Alpha")]); + history.remove(L!("Alpha")); + let mut searcher = HistorySearch::new(history.clone(), L!("Alpha").to_owned()); + test_history_matches!(searcher, vec![]); + + // Test history escaping and unescaping, yaml, etc. + let mut before: VecDeque = VecDeque::new(); + let mut after: VecDeque = VecDeque::new(); + history.clear(); + let max = 100; + let mut rng = get_rng(); + for i in 1..=max { + // Generate a value. + let mut value = WString::from_str("test item ") + &i.to_wstring()[..]; + + // Maybe add some backslashes. + if i % 3 == 0 { + value += L!("(slashies \\\\\\ slashies)"); + } + + // Generate some paths. + let paths: PathList = (0..rng.gen_range(0..6)) + .map(|_| random_string(&mut rng)) + .collect(); + + // Record this item. + let mut item = HistoryItem::new(value, SystemTime::now(), 0, PersistenceMode::Disk); + item.set_required_paths(paths); + before.push_back(item.clone()); + history.add(item, false); + } + history.save(); + + // Empty items should just be dropped (#6032). + history.add_commandline(L!("").into()); + assert!(!history.item_at_index(1).unwrap().is_empty()); + + // Read items back in reverse order and ensure they're the same. + for i in (1..=100).rev() { + after.push_back(history.item_at_index(i).unwrap()); + } + assert_eq!(before.len(), after.len()); + for i in 0..before.len() { + let bef = &before[i]; + let aft = &after[i]; + assert_eq!(bef.str(), aft.str()); + assert_eq!(bef.timestamp(), aft.timestamp()); + assert_eq!(bef.get_required_paths(), aft.get_required_paths()); + } + + // Items should be explicitly added to the history. + history.add_commandline(L!("test-command").into()); + assert!(history_contains(&history, L!("test-command"))); + + // Clean up after our tests. + history.clear(); + } + + // Wait until the next second. + fn time_barrier() { + let start = SystemTime::now(); + loop { + std::thread::sleep(std::time::Duration::from_millis(1)); + if SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + != start.duration_since(UNIX_EPOCH).unwrap().as_secs() + { + break; + } + } + } + + fn generate_history_lines(item_count: usize, idx: usize) -> Vec { + let mut result = Vec::with_capacity(item_count); + for i in 0..item_count { + result.push(sprintf!("%u %u", idx, i)); + } + result + } + + fn pound_on_history(item_count: usize, idx: usize) -> Arc { + // Called in child thread to modify history. + let hist = History::new(L!("race_test")); + let hist_lines = generate_history_lines(item_count, idx); + for line in hist_lines { + hist.add_commandline(line); + hist.save(); + } + hist + } + + #[test] + #[serial] + fn test_history_races() { + let _cleanup = test_init(); + + // Fail nearly every time on Cygwin (probably caused by flock issue, see #11933) + if cfg!(cygwin) { + return; + } + + let tmp_path = std::env::current_dir() + .unwrap() + .join("history-races-test-balloon"); + std::fs::write(&tmp_path, []).unwrap(); + let _cleanup = ScopeGuard::new((), |()| { + std::fs::remove_file(&tmp_path).unwrap(); + }); + if LockedFile::new( + crate::fs::LockingMode::Exclusive(WriteMethod::RenameIntoPlace), + &bytes2wcstring(tmp_path.as_os_str().as_bytes()), + ) + .is_err() + { + return; + } + + // Testing history race conditions + + // Test concurrent history writing. + // How many concurrent writers we have + const RACE_COUNT: usize = 4; + + // How many items each writer makes + const ITEM_COUNT: usize = 256; + + // Ensure history is clear. + History::new(L!("race_test")).clear(); + + let mut children = Vec::with_capacity(RACE_COUNT); + for i in 0..RACE_COUNT { + children.push(std::thread::spawn(move || { + pound_on_history(ITEM_COUNT, i); + })); + } + + // Wait for all children. + for child in children { + child.join().unwrap(); + } + + // Compute the expected lines. + let mut expected_lines: [Vec; RACE_COUNT] = + std::array::from_fn(|i| generate_history_lines(ITEM_COUNT, i)); + + // Ensure we consider the lines that have been outputted as part of our history. + time_barrier(); + + // Ensure that we got sane, sorted results. + let hist = History::new(L!("race_test")); + + // History is enumerated from most recent to least + // Every item should be the last item in some array + let mut hist_idx = 0; + loop { + hist_idx += 1; + let Some(item) = hist.item_at_index(hist_idx) else { + break; + }; + + let mut found = false; + for list in &mut expected_lines { + let Some(position) = list.iter().position(|elem| *elem == item.str()) else { + continue; + }; + + // Remove the item we found. + list.remove(position); + + // We expected this item to be the last. Items after this item + // in this array were therefore not found in history. + let removed = list.drain(position..); + for line in removed.into_iter().rev() { + printf!("Item dropped from history: %s\n", line); + } + + found = true; + break; + } + if !found { + printf!( + "Line '%s' found in history, but not found in some array\n", + item.str() + ); + for list in &expected_lines { + if !list.is_empty() { + printf!("\tRemaining: %s\n", list.last().unwrap()) + } + } + } + } + + // +1 to account for history's 1-based offset + let expected_idx = RACE_COUNT * ITEM_COUNT + 1; + assert_eq!(hist_idx, expected_idx); + + for list in expected_lines { + assert_eq!(list, Vec::::new(), "Lines still left in the array"); + } + hist.clear(); + } + + #[test] + #[serial] + fn test_history_external_rewrites() { + let _cleanup = test_init(); + + // Write some history to disk. + { + let hist = pound_on_history(VACUUM_FREQUENCY / 2, 0); + hist.add_commandline("needle".into()); + hist.save(); + } + std::thread::sleep(Duration::from_secs(1)); + + // Read history from disk. + let hist = History::new(L!("race_test")); + assert_eq!(hist.item_at_index(1).unwrap().str(), "needle"); + + // Add items until we rewrite the file. + // In practice this might be done by another shell. + pound_on_history(VACUUM_FREQUENCY, 0); + + for i in 1.. { + if hist.item_at_index(i).unwrap().str() == "needle" { + break; + } + } + } + + #[test] + #[serial] + fn test_history_merge() { + let _cleanup = test_init(); + // In a single fish process, only one history is allowed to exist with the given name But it's + // common to have multiple history instances with the same name active in different processes, + // e.g. when you have multiple shells open. We try to get that right and merge all their history + // together. Test that case. + const COUNT: usize = 3; + let name = L!("merge_test"); + let hists = [History::new(name), History::new(name), History::new(name)]; + let texts = [L!("History 1"), L!("History 2"), L!("History 3")]; + let alt_texts = [ + L!("History Alt 1"), + L!("History Alt 2"), + L!("History Alt 3"), + ]; + + // Make sure history is clear. + for hist in &hists { + hist.clear(); + } + + // Make sure we don't add an item in the same second as we created the history. + time_barrier(); + + // Add a different item to each. + for i in 0..COUNT { + hists[i].add_commandline(texts[i].to_owned()); + } + + // Save them. + for hist in &hists { + hist.save() + } + + // Make sure each history contains what it ought to, but they have not leaked into each other. + #[allow(clippy::needless_range_loop)] + for i in 0..COUNT { + for j in 0..COUNT { + let does_contain = history_contains(&hists[i], texts[j]); + let should_contain = i == j; + assert_eq!(should_contain, does_contain); + } + } + + // Make a new history. It should contain everything. The time_barrier() is so that the timestamp + // is newer, since we only pick up items whose timestamp is before the birth stamp. + time_barrier(); + let everything = History::new(name); + for text in texts { + assert!(history_contains(&everything, text)); + } + + // Tell all histories to merge. Now everybody should have everything. + for hist in &hists { + hist.incorporate_external_changes(); + } + + // Everyone should also have items in the same order (#2312) + let hist_vals1 = hists[0].get_history(); + for hist in &hists { + assert_eq!(hist_vals1, hist.get_history()); + } + + // Add some more per-history items. + for i in 0..COUNT { + hists[i].add_commandline(alt_texts[i].to_owned()); + } + // Everybody should have old items, but only one history should have each new item. + #[allow(clippy::needless_range_loop)] + for i in 0..COUNT { + for j in 0..COUNT { + // Old item. + assert!(history_contains(&hists[i], texts[j])); + + // New item. + let does_contain = history_contains(&hists[i], alt_texts[j]); + let should_contain = i == j; + assert_eq!(should_contain, does_contain); + } + } + + // Make sure incorporate_external_changes doesn't drop items! (#3496) + let writer = &hists[0]; + let reader = &hists[1]; + let more_texts = [ + L!("Item_#3496_1"), + L!("Item_#3496_2"), + L!("Item_#3496_3"), + L!("Item_#3496_4"), + L!("Item_#3496_5"), + L!("Item_#3496_6"), + ]; + for i in 0..more_texts.len() { + // time_barrier because merging will ignore items that may be newer + if i > 0 { + time_barrier(); + } + writer.add_commandline(more_texts[i].to_owned()); + writer.incorporate_external_changes(); + reader.incorporate_external_changes(); + for text in more_texts.iter().take(i) { + assert!(history_contains(reader, text)); + } + } + everything.clear(); + } + + #[test] + #[serial] + fn test_history_path_detection() { + let _cleanup = test_init(); + // Regression test for #7582. + let tmpdirbuff = CString::new("/tmp/fish_test_history.XXXXXX").unwrap(); + let tmpdir = unsafe { libc::mkdtemp(tmpdirbuff.into_raw()) }; + let tmpdir = unsafe { CString::from_raw(tmpdir) }; + let mut tmpdir = bytes2wcstring(tmpdir.to_bytes()); + if !tmpdir.ends_with('/') { + tmpdir.push('/'); + } + + // Place one valid file in the directory. + let filename = L!("testfile"); + std::fs::write(wcs2osstring(&(tmpdir.clone() + filename)), []).unwrap(); + + let test_vars = EnvStack::new(); + test_vars.set_one(L!("PWD"), EnvMode::GLOBAL, tmpdir.clone()); + test_vars.set_one(L!("HOME"), EnvMode::GLOBAL, tmpdir.clone()); + + let history = History::with_name(L!("path_detection")); + history.clear(); + assert_eq!(history.size(), 0); + history.clone().add_pending_with_file_detection( + L!("cmd0 not/a/valid/path"), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd1 ").to_owned() + filename), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd2 ").to_owned() + &tmpdir[..] + L!("/") + filename), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd3 $HOME/").to_owned() + filename), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd4 $HOME/notafile"), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + &(L!("cmd5 ~/").to_owned() + filename), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd6 ~/notafile"), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd7 ~/*f*"), + &test_vars, + PersistenceMode::Disk, + ); + history.clone().add_pending_with_file_detection( + L!("cmd8 ~/*zzz*"), + &test_vars, + PersistenceMode::Disk, + ); + history.resolve_pending(); + + const hist_size: usize = 9; + assert_eq!(history.size(), 9); + + // Expected sets of paths. + let expected_paths = [ + vec![], // cmd0 + vec![filename.to_owned()], // cmd1 + vec![tmpdir.clone() + L!("/") + filename], // cmd2 + vec![L!("$HOME/").to_owned() + filename], // cmd3 + vec![], // cmd4 + vec![L!("~/").to_owned() + filename], // cmd5 + vec![], // cmd6 + vec![], // cmd7 - we do not expand globs + vec![], // cmd8 + ]; + + let maxlap = 128; + for _lap in 0..maxlap { + let mut failures = 0; + for i in 1..=hist_size { + if history.item_at_index(i).unwrap().get_required_paths() + != expected_paths[hist_size - i] + { + failures += 1; + } + } + if failures == 0 { + break; + } + // The file detection takes a little time since it occurs in the background. + // Loop until the test passes. + std::thread::sleep(std::time::Duration::from_millis(2)); + } + history.clear(); + let _ = std::fs::remove_dir_all(wcs2osstring(&tmpdir)); + } + + fn install_sample_history(name: &wstr) { + let path = path_get_data().expect("Failed to get data directory"); + std::fs::copy( + workspace_root() + .join("tests") + .join(std::str::from_utf8(&wcs2bytes(name)).unwrap()), + wcs2osstring(&(path + L!("/") + name + L!("_history"))), + ) + .unwrap(); + } + + #[test] + #[serial] + fn test_history_formats() { + let _cleanup = test_init(); + // Test inferring and reading legacy and bash history formats. + let name = L!("history_sample_fish_2_0"); + install_sample_history(name); + let expected: Vec = vec![ + "echo this has\\\nbackslashes".into(), + "function foo\necho bar\nend".into(), + "echo alpha".into(), + ]; + let test_history_imported = History::with_name(name); + assert_eq!(test_history_imported.get_history(), expected); + test_history_imported.clear(); + + // Test bash import + // The results are in the reverse order that they appear in the bash history file. + // We don't expect whitespace to be elided (#4908: except for leading/trailing whitespace) + let expected: Vec = vec![ + "EOF".into(), + "sleep 123".into(), + "posix_cmd_sub $(is supported but only splits on newlines)".into(), + "posix_cmd_sub \"$(is supported)\"".into(), + "a && echo valid construct".into(), + "final line".into(), + "echo supsup".into(), + "export XVAR='exported'".into(), + "history --help".into(), + "echo foo".into(), + ]; + let test_history_imported_from_bash = History::with_name(L!("bash_import")); + let file = std::fs::File::open(workspace_root().join("tests/history_sample_bash")).unwrap(); + test_history_imported_from_bash.populate_from_bash(BufReader::new(file)); + assert_eq!(test_history_imported_from_bash.get_history(), expected); + test_history_imported_from_bash.clear(); + + let name = L!("history_sample_corrupt1"); + install_sample_history(name); + // We simply invoke get_string_representation. If we don't die, the test is a success. + let test_history_imported_from_corrupted = History::with_name(name); + let expected: Vec = vec![ + "no_newline_at_end_of_file".into(), + "corrupt_prefix".into(), + "this_command_is_ok".into(), + ]; + assert_eq!(test_history_imported_from_corrupted.get_history(), expected); + test_history_imported_from_corrupted.clear(); + } +} diff --git a/src/input.rs b/src/input.rs index 8f91e4b0d..6a58e76f3 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1030,3 +1030,72 @@ pub fn input_function_get_code(name: &wstr) -> Option { // a binary search for the matching name. get_by_sorted_name(name, INPUT_FUNCTION_METADATA).map(|md| md.code) } + +#[cfg(test)] +mod tests { + use super::{DEFAULT_BIND_MODE, EventQueuePeeker, InputMappingSet, KeyNameStyle}; + use crate::env::EnvStack; + use crate::input_common::{CharEvent, InputData, InputEventQueuer, KeyEvent}; + use crate::key::Key; + use crate::wchar::prelude::*; + + struct TestInputEventQueuer { + input_data: InputData, + } + + impl InputEventQueuer for TestInputEventQueuer { + fn get_input_data(&self) -> &InputData { + &self.input_data + } + fn get_input_data_mut(&mut self) -> &mut InputData { + &mut self.input_data + } + } + + #[test] + fn test_input() { + let vars = EnvStack::new(); + let mut input = TestInputEventQueuer { + input_data: InputData::new(i32::MAX, None), // value doesn't matter since we don't read from it + }; + // Ensure sequences are order independent. Here we add two bindings where the first is a prefix + // of the second, and then emit the second key list. The second binding should be invoked, not + // the first! + let prefix_binding: Vec = "qqqqqqqa".chars().map(Key::from_raw).collect(); + let mut desired_binding = prefix_binding.clone(); + desired_binding.push(Key::from_raw('a')); + + let default_mode = || DEFAULT_BIND_MODE.to_owned(); + + let mut input_mappings = InputMappingSet::default(); + input_mappings.add1( + prefix_binding, + KeyNameStyle::Plain, + WString::from_str("up-line"), + default_mode(), + None, + true, + ); + input_mappings.add1( + desired_binding.clone(), + KeyNameStyle::Plain, + WString::from_str("down-line"), + default_mode(), + None, + true, + ); + + // Push the desired binding to the queue. + for key in desired_binding { + input + .input_data + .queue_char(CharEvent::from_key(KeyEvent::from(key))); + } + + let mut peeker = EventQueuePeeker::new(&mut input); + let mapping = peeker.find_mapping(&vars, &input_mappings); + assert!(mapping.is_some()); + assert!(mapping.unwrap().commands == ["down-line"]); + peeker.restart(); + } +} diff --git a/src/input_common.rs b/src/input_common.rs index b44051486..72600b4c0 100644 --- a/src/input_common.rs +++ b/src/input_common.rs @@ -271,88 +271,6 @@ pub(crate) fn match_key_event_to_key(event: &KeyEvent, key: &Key) -> Option { - assert_eq!(match_key_event_to_key(&$evt, &$key), $expected); - }; - } - - let none = Modifiers::default(); - let shift = Modifiers::SHIFT; - let ctrl = Modifiers::CTRL; - let ctrl_shift = Modifiers { - ctrl: true, - shift: true, - ..Default::default() - }; - - let exact = KeyMatchQuality::Exact; - let modulo_shift = KeyMatchQuality::ModuloShift; - let base_layout = KeyMatchQuality::BaseLayout; - let base_layout_modulo_shift = KeyMatchQuality::BaseLayoutModuloShift; - - validate!(KeyEvent::new(none, 'a'), Key::new(none, 'a'), Some(exact)); - validate!(KeyEvent::new(none, 'a'), Key::new(none, 'A'), None); - validate!(KeyEvent::new(shift, 'a'), Key::new(shift, 'a'), Some(exact)); - validate!(KeyEvent::new(shift, 'a'), Key::new(none, 'A'), None); - validate!(KeyEvent::new(shift, 'ä'), Key::new(none, 'Ä'), None); - // For historical reasons we canonicalize notation for ASCII keys like "shift-a" to "A", - // but not "shift-a" events - those should send a shifted key. - validate!( - KeyEvent::new(none, 'A'), - Key::new(shift, 'a'), - Some(modulo_shift) - ); - validate!(KeyEvent::new(none, 'A'), Key::new(shift, 'A'), None); - validate!(KeyEvent::new(none, 'Ä'), Key::new(none, 'Ä'), Some(exact)); - validate!(KeyEvent::new(none, 'Ä'), Key::new(shift, 'ä'), None); - - // FYI: for codepoints that are not letters with uppercase/lowercase versions, we use - // the shifted key in the canonical notation, because the unshifted one may depend on the - // keyboard layout. - let ctrl_shift_equals = KeyEvent::new_with(ctrl_shift, '=', Some('+'), None); - validate!(ctrl_shift_equals, Key::new(ctrl_shift, '='), Some(exact)); - validate!(ctrl_shift_equals, Key::new(ctrl, '+'), Some(modulo_shift)); // canonical notation - validate!(ctrl_shift_equals, Key::new(ctrl_shift, '+'), None); - validate!(ctrl_shift_equals, Key::new(ctrl, '='), None); - - // A event like capslock-shift-ä may or may not include a shifted codepoint. - // - // Without a shifted codepoint, we cannot easily match ctrl-Ä. - let caps_ctrl_shift_ä = KeyEvent::new(ctrl_shift, 'ä'); - validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation - validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None); - validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), None); // can't match without shifted key - validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None); - // With a shifted codepoint, we can match the alternative notation too. - let caps_ctrl_shift_ä = KeyEvent::new_with(ctrl_shift, 'ä', Some('Ä'), None); - validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation - validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None); - validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), Some(modulo_shift)); // matched via shifted key - validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None); - - let ctrl_ц = KeyEvent::new_with(ctrl, 'ц', None, Some('w')); - let ctrl_shift_ц = KeyEvent::new_with(ctrl_shift, 'ц', Some('Ц'), Some('w')); - validate!(ctrl_ц, Key::new(ctrl, 'ц'), Some(exact)); - validate!(ctrl_ц, Key::new(ctrl, 'w'), Some(base_layout)); - validate!(ctrl_ц, Key::new(ctrl_shift, 'ц'), None); - validate!(ctrl_ц, Key::new(ctrl_shift, 'w'), None); - validate!( - ctrl_shift_ц, - Key::new(ctrl, 'W'), - Some(base_layout_modulo_shift) - ); - validate!(ctrl_shift_ц, Key::new(ctrl, 'w'), None); - - // Note that "bind ctrl-Ц" will win over "bind ctrl-shift-w". - // This is because we consider shift transformation to be less magic than base-key - // transformation. - validate!(ctrl_shift_ц, Key::new(ctrl, 'Ц'), Some(modulo_shift)); - validate!(ctrl_shift_ц, Key::new(ctrl_shift, 'w'), Some(base_layout)); -} - /// Represents an event on the character input stream. #[derive(Debug, Clone)] pub enum CharEventType { @@ -1777,7 +1695,156 @@ fn parse_hex(hex: &[u8]) -> Option> { Some(result) } -#[test] -fn test_parse_hex() { - assert_eq!(parse_hex(b"3d"), Some(vec![61])); +#[cfg(test)] +mod tests { + use super::{ + CharEvent, InputEventQueue, InputEventQueuer, KeyEvent, KeyMatchQuality, ReadlineCmd, + match_key_event_to_key, parse_hex, + }; + use crate::key::{Key, Modifiers}; + + #[test] + fn test_match_key_event_to_key() { + macro_rules! validate { + ($evt:expr, $key:expr, $expected:expr) => { + assert_eq!(match_key_event_to_key(&$evt, &$key), $expected); + }; + } + + let none = Modifiers::default(); + let shift = Modifiers::SHIFT; + let ctrl = Modifiers::CTRL; + let ctrl_shift = Modifiers { + ctrl: true, + shift: true, + ..Default::default() + }; + + let exact = KeyMatchQuality::Exact; + let modulo_shift = KeyMatchQuality::ModuloShift; + let base_layout = KeyMatchQuality::BaseLayout; + let base_layout_modulo_shift = KeyMatchQuality::BaseLayoutModuloShift; + + validate!(KeyEvent::new(none, 'a'), Key::new(none, 'a'), Some(exact)); + validate!(KeyEvent::new(none, 'a'), Key::new(none, 'A'), None); + validate!(KeyEvent::new(shift, 'a'), Key::new(shift, 'a'), Some(exact)); + validate!(KeyEvent::new(shift, 'a'), Key::new(none, 'A'), None); + validate!(KeyEvent::new(shift, 'ä'), Key::new(none, 'Ä'), None); + // For historical reasons we canonicalize notation for ASCII keys like "shift-a" to "A", + // but not "shift-a" events - those should send a shifted key. + validate!( + KeyEvent::new(none, 'A'), + Key::new(shift, 'a'), + Some(modulo_shift) + ); + validate!(KeyEvent::new(none, 'A'), Key::new(shift, 'A'), None); + validate!(KeyEvent::new(none, 'Ä'), Key::new(none, 'Ä'), Some(exact)); + validate!(KeyEvent::new(none, 'Ä'), Key::new(shift, 'ä'), None); + + // FYI: for codepoints that are not letters with uppercase/lowercase versions, we use + // the shifted key in the canonical notation, because the unshifted one may depend on the + // keyboard layout. + let ctrl_shift_equals = KeyEvent::new_with(ctrl_shift, '=', Some('+'), None); + validate!(ctrl_shift_equals, Key::new(ctrl_shift, '='), Some(exact)); + validate!(ctrl_shift_equals, Key::new(ctrl, '+'), Some(modulo_shift)); // canonical notation + validate!(ctrl_shift_equals, Key::new(ctrl_shift, '+'), None); + validate!(ctrl_shift_equals, Key::new(ctrl, '='), None); + + // A event like capslock-shift-ä may or may not include a shifted codepoint. + // + // Without a shifted codepoint, we cannot easily match ctrl-Ä. + let caps_ctrl_shift_ä = KeyEvent::new(ctrl_shift, 'ä'); + validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation + validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None); + validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), None); // can't match without shifted key + validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None); + // With a shifted codepoint, we can match the alternative notation too. + let caps_ctrl_shift_ä = KeyEvent::new_with(ctrl_shift, 'ä', Some('Ä'), None); + validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'ä'), Some(exact)); // canonical notation + validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'ä'), None); + validate!(caps_ctrl_shift_ä, Key::new(ctrl, 'Ä'), Some(modulo_shift)); // matched via shifted key + validate!(caps_ctrl_shift_ä, Key::new(ctrl_shift, 'Ä'), None); + + let ctrl_ц = KeyEvent::new_with(ctrl, 'ц', None, Some('w')); + let ctrl_shift_ц = KeyEvent::new_with(ctrl_shift, 'ц', Some('Ц'), Some('w')); + validate!(ctrl_ц, Key::new(ctrl, 'ц'), Some(exact)); + validate!(ctrl_ц, Key::new(ctrl, 'w'), Some(base_layout)); + validate!(ctrl_ц, Key::new(ctrl_shift, 'ц'), None); + validate!(ctrl_ц, Key::new(ctrl_shift, 'w'), None); + validate!( + ctrl_shift_ц, + Key::new(ctrl, 'W'), + Some(base_layout_modulo_shift) + ); + validate!(ctrl_shift_ц, Key::new(ctrl, 'w'), None); + + // Note that "bind ctrl-Ц" will win over "bind ctrl-shift-w". + // This is because we consider shift transformation to be less magic than base-key + // transformation. + validate!(ctrl_shift_ц, Key::new(ctrl, 'Ц'), Some(modulo_shift)); + validate!(ctrl_shift_ц, Key::new(ctrl_shift, 'w'), Some(base_layout)); + } + + #[test] + fn test_push_front_back() { + let mut queue = InputEventQueue::new(0, None); + queue.push_front(CharEvent::from_char('a')); + queue.push_front(CharEvent::from_char('b')); + queue.push_back(CharEvent::from_char('c')); + queue.push_back(CharEvent::from_char('d')); + assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'c'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'd'); + assert!(queue.try_pop().is_none()); + } + + #[test] + fn test_promote_interruptions_to_front() { + let mut queue = InputEventQueue::new(0, None); + queue.push_back(CharEvent::from_char('a')); + queue.push_back(CharEvent::from_char('b')); + queue.push_back(CharEvent::from_readline(ReadlineCmd::Undo)); + queue.push_back(CharEvent::from_readline(ReadlineCmd::Redo)); + queue.push_back(CharEvent::from_char('c')); + queue.push_back(CharEvent::from_char('d')); + queue.promote_interruptions_to_front(); + + assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Undo); + assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Redo); + assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'c'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'd'); + assert!(!queue.has_lookahead()); + + queue.push_back(CharEvent::from_char('e')); + queue.promote_interruptions_to_front(); + assert_eq!(queue.try_pop().unwrap().get_char(), 'e'); + assert!(!queue.has_lookahead()); + } + + #[test] + fn test_insert_front() { + let mut queue = InputEventQueue::new(0, None); + queue.push_back(CharEvent::from_char('a')); + queue.push_back(CharEvent::from_char('b')); + + let events = vec![ + CharEvent::from_char('A'), + CharEvent::from_char('B'), + CharEvent::from_char('C'), + ]; + queue.insert_front(events); + assert_eq!(queue.try_pop().unwrap().get_char(), 'A'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'B'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'C'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); + assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); + } + + #[test] + fn test_parse_hex() { + assert_eq!(parse_hex(b"3d"), Some(vec![61])); + } } diff --git a/src/key.rs b/src/key.rs index b2554b97b..273646046 100644 --- a/src/key.rs +++ b/src/key.rs @@ -456,3 +456,26 @@ pub fn char_to_symbol(c: char, is_first_in_token: bool) -> WString { } buff } + +#[cfg(test)] +mod tests { + use crate::key::{self, Key, ctrl, function_key, parse_keys}; + use crate::wchar::prelude::*; + + #[test] + fn test_parse_key() { + assert_eq!( + parse_keys(L!("escape")), + Ok(vec![Key::from_raw(key::Escape)]) + ); + assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::Escape)])); + assert_eq!(parse_keys(L!("ctrl-a")), Ok(vec![ctrl('a')])); + assert_eq!(parse_keys(L!("\x01")), Ok(vec![ctrl('a')])); + assert!(parse_keys(L!("f0")).is_err()); + assert_eq!( + parse_keys(L!("f1")), + Ok(vec![Key::from_raw(function_key(1))]) + ); + assert!(parse_keys(L!("F1")).is_err()); + } +} diff --git a/src/kill.rs b/src/kill.rs index 5ec988341..84853e1a8 100644 --- a/src/kill.rs +++ b/src/kill.rs @@ -83,28 +83,34 @@ pub fn kill_entries() -> Vec { KILL_RING.lock().unwrap().entries() } -#[test] -fn test_killring() { - let mut kr = KillRing::new(); +#[cfg(test)] +mod tests { + use super::KillRing; + use crate::wchar::prelude::*; - assert!(kr.is_empty()); + #[test] + fn test_killring() { + let mut kr = KillRing::new(); - kr.add(WString::from_str("a")); - kr.add(WString::from_str("b")); - kr.add(WString::from_str("c")); + assert!(kr.is_empty()); - assert!(kr.entries() == [L!("c"), L!("b"), L!("a")]); + kr.add(WString::from_str("a")); + kr.add(WString::from_str("b")); + kr.add(WString::from_str("c")); - assert!(kr.yank_rotate() == "b"); - assert!(kr.entries() == [L!("b"), L!("a"), L!("c")]); + assert!(kr.entries() == [L!("c"), L!("b"), L!("a")]); - assert!(kr.yank_rotate() == "a"); - assert!(kr.entries() == [L!("a"), L!("c"), L!("b")]); + assert!(kr.yank_rotate() == "b"); + assert!(kr.entries() == [L!("b"), L!("a"), L!("c")]); - kr.add(WString::from_str("d")); + assert!(kr.yank_rotate() == "a"); + assert!(kr.entries() == [L!("a"), L!("c"), L!("b")]); - assert!((kr.entries() == [L!("d"), L!("a"), L!("c"), L!("b")])); + kr.add(WString::from_str("d")); - assert!(kr.yank_rotate() == "a"); - assert!((kr.entries() == [L!("a"), L!("c"), L!("b"), L!("d")])); + assert!((kr.entries() == [L!("d"), L!("a"), L!("c"), L!("b")])); + + assert!(kr.yank_rotate() == "a"); + assert!((kr.entries() == [L!("a"), L!("c"), L!("b"), L!("d")])); + } } diff --git a/src/lib.rs b/src/lib.rs index e6ce692a9..07e5fbbbf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(feature = "benchmark", feature(test))] +#![allow(non_camel_case_types)] pub const BUILD_VERSION: &str = env!("FISH_BUILD_VERSION"); @@ -60,6 +61,7 @@ pub mod redirection; pub mod screen; pub mod signal; +pub mod stdx; pub mod terminal; pub mod termsize; pub mod text_face; diff --git a/src/null_terminated_array.rs b/src/null_terminated_array.rs index 932141c9c..42bf33082 100644 --- a/src/null_terminated_array.rs +++ b/src/null_terminated_array.rs @@ -84,32 +84,39 @@ pub fn iter(&self) -> impl Iterator { } } -#[test] -fn test_null_terminated_array() { - let owned_strs = &[CString::new("foo").unwrap(), CString::new("bar").unwrap()]; - let strs = owned_strs.iter().map(|s| s.as_c_str()).collect::>(); - let arr = NullTerminatedArray::new(&strs); - let ptr = arr.get(); - unsafe { - assert_eq!(CStr::from_ptr(*ptr).to_str().unwrap(), "foo"); - assert_eq!(CStr::from_ptr(*ptr.offset(1)).to_str().unwrap(), "bar"); - assert_eq!(*ptr.offset(2), ptr::null()); - } -} +#[cfg(test)] +mod tests { + use super::{NullTerminatedArray, OwningNullTerminatedArray}; + use std::ffi::{CStr, CString}; + use std::ptr; -#[test] -fn test_owning_null_terminated_array() { - let owned_strs = vec![CString::new("foo").unwrap(), CString::new("bar").unwrap()]; - let arr = OwningNullTerminatedArray::new(owned_strs); - let ptr = arr.get(); - unsafe { - assert_eq!(CStr::from_ptr(*ptr).to_str().unwrap(), "foo"); - assert_eq!(CStr::from_ptr(*ptr.offset(1)).to_str().unwrap(), "bar"); - assert_eq!(*ptr.offset(2), ptr::null()); + #[test] + fn test_null_terminated_array() { + let owned_strs = &[CString::new("foo").unwrap(), CString::new("bar").unwrap()]; + let strs = owned_strs.iter().map(|s| s.as_c_str()).collect::>(); + let arr = NullTerminatedArray::new(&strs); + let ptr = arr.get(); + unsafe { + assert_eq!(CStr::from_ptr(*ptr).to_str().unwrap(), "foo"); + assert_eq!(CStr::from_ptr(*ptr.offset(1)).to_str().unwrap(), "bar"); + assert_eq!(*ptr.offset(2), ptr::null()); + } + } + + #[test] + fn test_owning_null_terminated_array() { + let owned_strs = vec![CString::new("foo").unwrap(), CString::new("bar").unwrap()]; + let arr = OwningNullTerminatedArray::new(owned_strs); + let ptr = arr.get(); + unsafe { + assert_eq!(CStr::from_ptr(*ptr).to_str().unwrap(), "foo"); + assert_eq!(CStr::from_ptr(*ptr.offset(1)).to_str().unwrap(), "bar"); + assert_eq!(*ptr.offset(2), ptr::null()); + } + assert_eq!(arr.len(), 2); + let mut iter = arr.iter(); + assert_eq!(iter.next().map(|s| s.to_str().unwrap()), Some("foo")); + assert_eq!(iter.next().map(|s| s.to_str().unwrap()), Some("bar")); + assert_eq!(iter.next(), None); } - assert_eq!(arr.len(), 2); - let mut iter = arr.iter(); - assert_eq!(iter.next().map(|s| s.to_str().unwrap()), Some("foo")); - assert_eq!(iter.next().map(|s| s.to_str().unwrap()), Some("bar")); - assert_eq!(iter.next(), None); } diff --git a/src/pager.rs b/src/pager.rs index 9bee102cb..c43626b42 100644 --- a/src/pager.rs +++ b/src/pager.rs @@ -1260,3 +1260,215 @@ fn process_completions_into_infos(lst: &[Completion]) -> Vec { } result } + +#[cfg(test)] +mod tests { + use super::{Pager, SelectionMotion}; + use crate::common::get_ellipsis_char; + use crate::complete::{CompleteFlags, Completion}; + use crate::termsize::Termsize; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + use crate::wcstringutil::StringFuzzyMatch; + + #[test] + #[serial] + fn test_pager_navigation() { + let _cleanup = test_init(); + // Generate 19 strings of width 10. There's 2 spaces between completions, and our term size is + // 80; these can therefore fit into 6 columns (6 * 12 - 2 = 70) or 5 columns (58) but not 7 + // columns (7 * 12 - 2 = 82). + // + // You can simulate this test by creating 19 files named "file00.txt" through "file_18.txt". + let mut completions = vec![]; + for _ in 0..19 { + completions.push(Completion::new( + L!("abcdefghij").to_owned(), + "".into(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )); + } + + let mut pager = Pager::default(); + pager.set_completions(&completions, true); + pager.set_term_size(&Termsize::defaults()); + let mut render = pager.render(); + + assert_eq!(render.term_width, Some(80)); + assert_eq!(render.term_height, Some(24)); + + let rows = 4; + let cols = 5; + + // We have 19 completions. We can fit into 6 columns with 4 rows or 5 columns with 4 rows; the + // second one is better and so is what we ought to have picked. + assert_eq!(render.rows, rows); + assert_eq!(render.cols, cols); + + // Initially expect to have no completion index. + assert!(render.selected_completion_idx.is_none()); + + // Here are navigation directions and where we expect the selection to be. + macro_rules! validate { + ($pager:ident, $render:ident, $dir:expr, $sel:expr) => { + $pager.select_next_completion_in_direction($dir, &$render); + $pager.update_rendering(&mut $render); + assert_eq!( + Some($sel), + $render.selected_completion_idx, + "For command {:?}", + $dir + ); + }; + } + + // Tab completion to get into the list. + validate!(pager, render, SelectionMotion::Next, 0); + // Westward motion in upper left goes to the last filled column in the last row. + validate!(pager, render, SelectionMotion::West, 15); + // East goes back. + validate!(pager, render, SelectionMotion::East, 0); + validate!(pager, render, SelectionMotion::West, 15); + validate!(pager, render, SelectionMotion::West, 11); + validate!(pager, render, SelectionMotion::East, 15); + validate!(pager, render, SelectionMotion::East, 0); + // "Next" motion goes down the column. + validate!(pager, render, SelectionMotion::Next, 1); + validate!(pager, render, SelectionMotion::Next, 2); + validate!(pager, render, SelectionMotion::West, 17); + validate!(pager, render, SelectionMotion::East, 2); + validate!(pager, render, SelectionMotion::East, 6); + validate!(pager, render, SelectionMotion::East, 10); + validate!(pager, render, SelectionMotion::East, 14); + validate!(pager, render, SelectionMotion::East, 18); + validate!(pager, render, SelectionMotion::West, 14); + validate!(pager, render, SelectionMotion::East, 18); + // Eastward motion wraps back to the upper left, westward goes to the prior column. + validate!(pager, render, SelectionMotion::East, 3); + validate!(pager, render, SelectionMotion::East, 7); + validate!(pager, render, SelectionMotion::East, 11); + validate!(pager, render, SelectionMotion::East, 15); + // Pages. + validate!(pager, render, SelectionMotion::PageNorth, 12); + validate!(pager, render, SelectionMotion::PageSouth, 15); + validate!(pager, render, SelectionMotion::PageNorth, 12); + validate!(pager, render, SelectionMotion::East, 16); + validate!(pager, render, SelectionMotion::PageSouth, 18); + validate!(pager, render, SelectionMotion::East, 3); + validate!(pager, render, SelectionMotion::North, 2); + validate!(pager, render, SelectionMotion::PageNorth, 0); + validate!(pager, render, SelectionMotion::PageSouth, 3); + } + + #[test] + #[serial] + fn test_pager_layout() { + let _cleanup = test_init(); + // These tests are woefully incomplete + // They only test the truncation logic for a single completion + + let rendered_line = |pager: &mut Pager, width: isize| { + pager.set_term_size(&Termsize::new(width, 24)); + let rendering = pager.render(); + let sd = &rendering.screen_data; + assert_eq!(sd.line_count(), 1); + let line = sd.line(0); + WString::from(Vec::from_iter((0..line.len()).map(|i| line.char_at(i)))) + }; + let compute_expected = |expected: &wstr| { + let ellipsis_char = get_ellipsis_char(); + if ellipsis_char != '\u{2026}' { + // hack: handle the case where ellipsis is not L'\x2026' + expected.replace(L!("\u{2026}"), wstr::from_char_slice(&[ellipsis_char])) + } else { + expected.to_owned() + } + }; + + macro_rules! validate { + ($pager:expr, $width:expr, $expected:expr) => { + assert_eq!( + rendered_line($pager, $width), + compute_expected($expected), + "width {}", + $width + ); + }; + } + + let mut pager = Pager::default(); + + // These test cases have equal completions and descriptions + let c1s = vec![Completion::new( + L!("abcdefghij").to_owned(), + L!("1234567890").to_owned(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )]; + pager.set_completions(&c1s, true); + + validate!(&mut pager, 26, L!("abcdefghij (1234567890)")); + validate!(&mut pager, 25, L!("abcdefghij (1234567890)")); + validate!(&mut pager, 24, L!("abcdefghij (1234567890)")); + validate!(&mut pager, 23, L!("abcdefghij (12345678…)")); + validate!(&mut pager, 22, L!("abcdefghij (1234567…)")); + validate!(&mut pager, 21, L!("abcdefghij (123456…)")); + validate!(&mut pager, 20, L!("abcdefghij (12345…)")); + validate!(&mut pager, 19, L!("abcdefghij (1234…)")); + validate!(&mut pager, 18, L!("abcdefgh… (1234…)")); + validate!(&mut pager, 17, L!("abcdefg… (1234…)")); + validate!(&mut pager, 16, L!("abcdefg… (123…)")); + + // These test cases have heavyweight completions + let c2s = vec![Completion::new( + L!("abcdefghijklmnopqrs").to_owned(), + L!("1").to_owned(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )]; + pager.set_completions(&c2s, true); + validate!(&mut pager, 26, L!("abcdefghijklmnopqrs (1)")); + validate!(&mut pager, 25, L!("abcdefghijklmnopqrs (1)")); + validate!(&mut pager, 24, L!("abcdefghijklmnopqrs (1)")); + validate!(&mut pager, 23, L!("abcdefghijklmnopq… (1)")); + validate!(&mut pager, 22, L!("abcdefghijklmnop… (1)")); + validate!(&mut pager, 21, L!("abcdefghijklmno… (1)")); + validate!(&mut pager, 20, L!("abcdefghijklmn… (1)")); + validate!(&mut pager, 19, L!("abcdefghijklm… (1)")); + validate!(&mut pager, 18, L!("abcdefghijkl… (1)")); + validate!(&mut pager, 17, L!("abcdefghijk… (1)")); + validate!(&mut pager, 16, L!("abcdefghij… (1)")); + + // These test cases have no descriptions + let c3s = vec![Completion::new( + L!("abcdefghijklmnopqrst").to_owned(), + L!("").to_owned(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )]; + pager.set_completions(&c3s, true); + validate!(&mut pager, 26, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 25, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 24, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 23, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 22, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 21, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 20, L!("abcdefghijklmnopqrst")); + validate!(&mut pager, 19, L!("abcdefghijklmnopqr…")); + validate!(&mut pager, 18, L!("abcdefghijklmnopq…")); + validate!(&mut pager, 17, L!("abcdefghijklmnop…")); + validate!(&mut pager, 16, L!("abcdefghijklmno…")); + + // Newlines in prefix + let c4s = vec![Completion::new( + L!("Hello").to_owned(), + L!("").to_owned(), + StringFuzzyMatch::exact_match(), + CompleteFlags::default(), + )]; + pager.set_prefix(L!("{\\\n"), false); // } + pager.set_completions(&c4s, true); + validate!(&mut pager, 30, L!("{\\␊Hello")); // } + } +} diff --git a/src/parse_util.rs b/src/parse_util.rs index 59fb56ef0..015b4cdb6 100644 --- a/src/parse_util.rs +++ b/src/parse_util.rs @@ -1967,3 +1967,456 @@ pub fn parse_util_expand_variable_error( /// Maximum length of a variable name to show in error reports before truncation const var_err_len: usize = 16; + +#[cfg(test)] +mod tests { + use super::{ + BOOL_AFTER_BACKGROUND_ERROR_MSG, parse_util_cmdsubst_extent, parse_util_compute_indents, + parse_util_detect_errors, parse_util_escape_string_with_quote, parse_util_process_extent, + parse_util_slice_length, + }; + use crate::common::EscapeFlags; + use crate::parse_constants::{ + ERROR_BAD_VAR_CHAR1, ERROR_BRACKETED_VARIABLE_QUOTED1, ERROR_BRACKETED_VARIABLE1, + ERROR_NO_VAR_NAME, ERROR_NOT_ARGV_AT, ERROR_NOT_ARGV_COUNT, ERROR_NOT_ARGV_STAR, + ERROR_NOT_PID, ERROR_NOT_STATUS, + }; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + use pcre2::utf32::Regex; + + #[test] + #[serial] + fn test_error_messages() { + let _cleanup = test_init(); + // Given a format string, returns a list of non-empty strings separated by format specifiers. The + // format specifiers themselves are omitted. + fn separate_by_format_specifiers(format: &wstr) -> Vec<&wstr> { + let format_specifier_regex = Regex::new(L!(r"%[cds]").as_char_slice()).unwrap(); + let mut result = vec![]; + let mut offset = 0; + for mtch in format_specifier_regex.find_iter(format.as_char_slice()) { + let mtch = mtch.unwrap(); + let component = &format[offset..mtch.start()]; + result.push(component); + offset = mtch.end(); + } + result.push(&format[offset..]); + // Avoid mismatch from localized quotes. + for component in &mut result { + *component = component.trim_matches('\''); + } + result + } + + // Given a format string 'format', return true if the string may have been produced by that format + // string. We do this by splitting the format string around the format specifiers, and then ensuring + // that each of the remaining chunks is found (in order) in the string. + fn string_matches_format(s: &wstr, format: &wstr) -> bool { + let components = separate_by_format_specifiers(format); + assert!(!components.is_empty()); + let mut idx = 0; + for component in components { + let Some(relpos) = s[idx..].find(component) else { + return false; + }; + idx += relpos + component.len(); + assert!(idx <= s.len()); + } + true + } + + macro_rules! validate { + ($src:expr, $error_text_format:expr) => { + let mut errors = vec![]; + let res = parse_util_detect_errors(L!($src), Some(&mut errors), false); + let fmt = wgettext!($error_text_format); + assert!(res.is_err()); + assert!( + string_matches_format(&errors[0].text, fmt), + "command '{}' is expected to match error pattern '{}' but is '{}'", + $src, + $error_text_format.localize(), + &errors[0].text + ); + }; + } + + validate!("echo $^", ERROR_BAD_VAR_CHAR1); + validate!("echo foo${a}bar", ERROR_BRACKETED_VARIABLE1); + validate!("echo foo\"${a}\"bar", ERROR_BRACKETED_VARIABLE_QUOTED1); + validate!("echo foo\"${\"bar", ERROR_BAD_VAR_CHAR1); + validate!("echo $?", ERROR_NOT_STATUS); + validate!("echo $$", ERROR_NOT_PID); + validate!("echo $#", ERROR_NOT_ARGV_COUNT); + validate!("echo $@", ERROR_NOT_ARGV_AT); + validate!("echo $*", ERROR_NOT_ARGV_STAR); + validate!("echo $", ERROR_NO_VAR_NAME); + validate!("echo foo\"$\"bar", ERROR_NO_VAR_NAME); + validate!("echo \"foo\"$\"bar\"", ERROR_NO_VAR_NAME); + validate!("echo foo $ bar", ERROR_NO_VAR_NAME); + validate!("echo 1 & && echo 2", BOOL_AFTER_BACKGROUND_ERROR_MSG); + validate!( + "echo 1 && echo 2 & && echo 3", + BOOL_AFTER_BACKGROUND_ERROR_MSG + ); + } + + #[test] + fn test_parse_util_process_extent() { + macro_rules! validate { + ($commandline:literal, $cursor:expr, $expected_range:expr) => { + assert_eq!( + parse_util_process_extent(L!($commandline), $cursor, None), + $expected_range + ); + }; + } + validate!("for file in (path base\necho", 22, 13..22); + validate!("begin\n\n\nec", 10, 6..10); + validate!("begin; echo; end", 12, 12..16); + } + + #[test] + #[serial] + fn test_parse_util_cmdsubst_extent() { + let _cleanup = test_init(); + const a: &wstr = L!("echo (echo (echo hi"); + assert_eq!(parse_util_cmdsubst_extent(a, 0), 0..a.len()); + assert_eq!(parse_util_cmdsubst_extent(a, 1), 0..a.len()); + assert_eq!(parse_util_cmdsubst_extent(a, 2), 0..a.len()); + assert_eq!(parse_util_cmdsubst_extent(a, 3), 0..a.len()); + assert_eq!( + parse_util_cmdsubst_extent(a, 8), + "echo (".chars().count()..a.len() + ); + assert_eq!( + parse_util_cmdsubst_extent(a, 17), + "echo (echo (".chars().count()..a.len() + ); + } + + #[test] + #[serial] + fn test_parse_util_slice_length() { + let _cleanup = test_init(); + assert_eq!(parse_util_slice_length(L!("[2]")), Some(3)); + assert_eq!(parse_util_slice_length(L!("[12]")), Some(4)); + assert_eq!(parse_util_slice_length(L!("[\"foo\"]")), Some(7)); + assert_eq!(parse_util_slice_length(L!("[\"foo\"")), None); + } + + #[test] + #[serial] + fn test_escape_quotes() { + let _cleanup = test_init(); + macro_rules! validate { + ($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => { + assert_eq!( + parse_util_escape_string_with_quote( + L!($cmd), + $quote, + if $no_tilde { + EscapeFlags::NO_TILDE + } else { + EscapeFlags::empty() + } + ), + L!($expected) + ); + }; + } + macro_rules! validate_no_quoted { + ($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => { + assert_eq!( + parse_util_escape_string_with_quote( + L!($cmd), + $quote, + EscapeFlags::NO_QUOTED + | if $no_tilde { + EscapeFlags::NO_TILDE + } else { + EscapeFlags::empty() + } + ), + L!($expected) + ); + }; + } + + validate!("abc~def", None, false, "'abc~def'"); + validate!("abc~def", None, true, "abc~def"); + validate!("~abc", None, false, "'~abc'"); + validate!("~abc", None, true, "~abc"); + + // These are "raw string literals" + validate_no_quoted!("abc", None, false, "abc"); + validate_no_quoted!("abc~def", None, false, "abc\\~def"); + validate_no_quoted!("abc~def", None, true, "abc~def"); + validate_no_quoted!("abc\\~def", None, false, "abc\\\\\\~def"); + validate_no_quoted!("abc\\~def", None, true, "abc\\\\~def"); + validate_no_quoted!("~abc", None, false, "\\~abc"); + validate_no_quoted!("~abc", None, true, "~abc"); + validate_no_quoted!("~abc|def", None, false, "\\~abc\\|def"); + validate_no_quoted!("|abc~def", None, false, "\\|abc\\~def"); + validate_no_quoted!("|abc~def", None, true, "\\|abc~def"); + validate_no_quoted!("foo\nbar", None, false, "foo\\nbar"); + + // Note tildes are not expanded inside quotes, so no_tilde is ignored with a quote. + validate_no_quoted!("abc", Some('\''), false, "abc"); + validate_no_quoted!("abc\\def", Some('\''), false, "abc\\\\def"); + validate_no_quoted!("abc'def", Some('\''), false, "abc\\'def"); + validate_no_quoted!("~abc'def", Some('\''), false, "~abc\\'def"); + validate_no_quoted!("~abc'def", Some('\''), true, "~abc\\'def"); + validate_no_quoted!("foo\nba'r", Some('\''), false, "foo'\\n'ba\\'r"); + validate_no_quoted!("foo\\\\bar", Some('\''), false, "foo\\\\\\\\bar"); + + validate_no_quoted!("abc", Some('"'), false, "abc"); + validate_no_quoted!("abc\\def", Some('"'), false, "abc\\\\def"); + validate_no_quoted!("~abc'def", Some('"'), false, "~abc'def"); + validate_no_quoted!("~abc'def", Some('"'), true, "~abc'def"); + validate_no_quoted!("foo\nba'r", Some('"'), false, "foo\"\\n\"ba'r"); + validate_no_quoted!("foo\\\\bar", Some('"'), false, "foo\\\\\\\\bar"); + } + + #[test] + #[serial] + fn test_indents() { + let _cleanup = test_init(); + // A struct which is either text or a new indent. + struct Segment { + // The indent to set + indent: i32, + text: &'static str, + } + fn do_validate(segments: &[Segment]) { + // Compute the indents. + let mut expected_indents = vec![]; + let mut text = WString::new(); + for segment in segments { + text.push_str(segment.text); + for _ in segment.text.chars() { + expected_indents.push(segment.indent); + } + } + let indents = parse_util_compute_indents(&text); + assert_eq!(indents, expected_indents); + } + macro_rules! validate { + ( $( $(,)? $indent:literal, $text:literal )* $(,)? ) => { + let segments = vec![ + $( + Segment{ indent: $indent, text: $text }, + )* + ]; + do_validate(&segments); + }; + } + + #[rustfmt::skip] + #[allow(clippy::redundant_closure_call)] + (|| { + validate!( + 0, "if", 1, " foo", + 0, "\nend" + ); + validate!( + 0, "if", 1, " foo", + 1, "\nfoo", + 0, "\nend" + ); + + validate!( + 0, "if", 1, " foo", + 1, "\nif", 2, " bar", + 1, "\nend", + 0, "\nend" + ); + + validate!( + 0, "if", 1, " foo", + 1, "\nif", 2, " bar", + 2, "\n", + 1, "\nend\n" + ); + + validate!( + 0, "if", 1, " foo", + 1, "\nif", 2, " bar", + 2, "\n" + ); + + validate!( + 0, "begin", + 1, "\nfoo", + 1, "\n" + ); + + validate!( + 0, "begin", + 1, "\n;", + 0, "end", + 0, "\nfoo", 0, "\n" + ); + + validate!( + 0, "begin", + 1, "\n;", + 0, "end", + 0, "\nfoo", 0, "\n" + ); + + validate!( + 0, "if", 1, " foo", + 1, "\nif", 2, " bar", + 2, "\nbaz", + 1, "\nend", 1, "\n" + ); + + validate!( + 0, "switch foo", + 1, "\n" + ); + + validate!( + 0, "switch foo", + 1, "\ncase bar", + 1, "\ncase baz", + 2, "\nquux", + 2, "\nquux" + ); + + validate!( + 0, + "switch foo", + 1, + "\ncas" // parse error indentation handling + ); + + validate!( + 0, "while", + 1, " false", + 1, "\n# comment", // comment indentation handling + 1, "\ncommand", + 1, "\n# comment 2" + ); + + validate!( + 0, "begin", + 1, "\n", // "begin" is special because this newline belongs to the block header + 1, "\n" + ); + + // Continuation lines. + validate!( + 0, "echo 'continuation line' \\", + 1, "\ncont", + 0, "\n" + ); + validate!( + 0, "echo 'empty continuation line' \\", + 1, "\n" + ); + validate!( + 0, "begin # continuation line in block", + 1, "\necho \\", + 2, "\ncont" + ); + validate!( + 0, "begin # empty continuation line in block", + 1, "\necho \\", + 2, "\n", + 0, "\nend" + ); + validate!( + 0, "echo 'multiple continuation lines' \\", + 1, "\nline1 \\", + 1, "\n# comment", + 1, "\n# more comment", + 1, "\nline2 \\", + 1, "\n" + ); + validate!( + 0, "echo # inline comment ending in \\", + 0, "\nline" + ); + validate!( + 0, "# line comment ending in \\", + 0, "\nline" + ); + validate!( + 0, "echo 'multiple empty continuation lines' \\", + 1, "\n\\", + 1, "\n", + 0, "\n" + ); + validate!( + 0, "echo 'multiple statements with continuation lines' \\", + 1, "\nline 1", + 0, "\necho \\", + 1, "\n" + ); + // This is an edge case, probably okay to change the behavior here. + validate!( + 0, "begin", + 1, " \\", + 2, "\necho 'continuation line in block header' \\", + 2, "\n", + 1, "\n", + 0, "\nend" + ); + validate!( + 0, "if", 1, " true", + 1, "\n begin", + 2, "\n echo", + 1, "\n end", + 0, "\nend", + ); + + // Quotes and command substitutions. + validate!( + 0, "if", 1, " foo \"", + 0, "\nquoted", + ); + validate!( + 0, "if", 1, " foo \"", + 0, "\n", + ); + validate!( + 0, "echo (", + 1, "\n", // ) + ); + validate!( + 0, "echo \"$(", + 1, "\n" // ) + ); + validate!( + 0, "echo (", // ) + 1, "\necho \"", + 0, "\n" + ); + validate!( + 0, "echo (", // ) + 1, "\necho (", // ) + 2, "\necho" + ); + validate!( + 0, "if", 1, " true", + 1, "\n echo \"line1", + 0, "\nline2 ", 1, "$(", + 2, "\n echo line3", + 0, "\n) line4", + 0, "\nline5\"", + ); + validate!( + 0, r#"echo "$()"'"#, + 0, "\n" + ); + validate!( + 0, r#"""#, + 0, "\n", + 0, r#"$()"$() ""# + ); + })(); + } +} diff --git a/src/parser.rs b/src/parser.rs index ab950bc5f..fa11e7da0 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1386,3 +1386,995 @@ pub enum LoopStatus { /// current loop block should be skipped continues, } + +#[cfg(test)] +mod tests { + use super::{CancelBehavior, Parser}; + use crate::ast::{ + self, Ast, Castable, JobList, JobPipeline, Kind, Node, Traversal, is_same_node, + }; + use crate::env::EnvStack; + use crate::expand::ExpandFlags; + use crate::io::{IoBufferfill, IoChain}; + use crate::parse_constants::{ + ParseErrorCode, ParseTokenType, ParseTreeFlags, ParserTestErrorBits, StatementDecoration, + }; + use crate::parse_tree::{LineCounter, parse_source}; + use crate::parse_util::{parse_util_detect_errors, parse_util_detect_errors_in_argument}; + use crate::reader::{fake_scoped_reader, reader_reset_interrupted}; + use crate::signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handlers}; + use crate::tests::prelude::*; + use crate::threads::iothread_perform; + use crate::wchar::prelude::*; + use crate::wcstringutil::join_strings; + use libc::SIGINT; + use std::time::Duration; + + #[test] + #[serial] + fn test_parser() { + let _cleanup = test_init(); + macro_rules! detect_errors { + ($src:literal) => { + parse_util_detect_errors(L!($src), None, true /* accept incomplete */) + }; + } + + fn detect_argument_errors(src: &str) -> Result<(), ParserTestErrorBits> { + let src = WString::from_str(src); + let ast = ast::parse_argument_list(&src, ParseTreeFlags::default(), None); + if ast.errored() { + return Err(ParserTestErrorBits::ERROR); + } + let args = &ast.top().arguments; + let first_arg = args.first().expect("Failed to parse an argument"); + let mut errors = None; + parse_util_detect_errors_in_argument(first_arg, first_arg.source(&src), &mut errors) + } + + // Testing block nesting + assert!( + detect_errors!("if; end").is_err(), + "Incomplete if statement undetected" + ); + assert!( + detect_errors!("if test; echo").is_err(), + "Missing end undetected" + ); + assert!( + detect_errors!("if test; end; end").is_err(), + "Unbalanced end undetected" + ); + + // Testing detection of invalid use of builtin commands + assert!( + detect_errors!("case foo").is_err(), + "'case' command outside of block context undetected" + ); + assert!( + detect_errors!("switch ggg; if true; case foo;end;end").is_err(), + "'case' command outside of switch block context undetected" + ); + assert!( + detect_errors!("else").is_err(), + "'else' command outside of conditional block context undetected" + ); + assert!( + detect_errors!("else if").is_err(), + "'else if' command outside of conditional block context undetected" + ); + assert!( + detect_errors!("if false; else if; end").is_err(), + "'else if' missing command undetected" + ); + + assert!( + detect_errors!("break").is_err(), + "'break' command outside of loop block context undetected" + ); + + assert!( + detect_errors!("break --help").is_ok(), + "'break --help' incorrectly marked as error" + ); + + assert!( + detect_errors!("while false ; function foo ; break ; end ; end ").is_err(), + "'break' command inside function allowed to break from loop outside it" + ); + + assert!( + detect_errors!("exec ls|less").is_err() && detect_errors!("echo|return").is_err(), + "Invalid pipe command undetected" + ); + + assert!( + detect_errors!("for i in foo ; switch $i ; case blah ; break; end; end ").is_ok(), + "'break' command inside switch falsely reported as error" + ); + + assert!( + detect_errors!("or cat | cat").is_ok() && detect_errors!("and cat | cat").is_ok(), + "boolean command at beginning of pipeline falsely reported as error" + ); + + assert!( + detect_errors!("cat | and cat").is_err(), + "'and' command in pipeline not reported as error" + ); + + assert!( + detect_errors!("cat | or cat").is_err(), + "'or' command in pipeline not reported as error" + ); + + assert!( + detect_errors!("cat | exec").is_err() && detect_errors!("exec | cat").is_err(), + "'exec' command in pipeline not reported as error" + ); + + assert!( + detect_errors!("begin ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("switch foo ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("if true; else if false ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("if true; else ; end arg").is_err(), + "argument to 'end' not reported as error" + ); + + assert!( + detect_errors!("begin ; end 2> /dev/null").is_ok(), + "redirection after 'end' wrongly reported as error" + ); + + assert!( + detect_errors!("true | ") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated pipe not reported properly" + ); + + assert!( + detect_errors!("echo (\nfoo\n bar") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated multiline subshell not reported properly" + ); + + assert!( + detect_errors!("begin ; true ; end | ") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated pipe not reported properly" + ); + + assert!( + detect_errors!(" | true ") == Err(ParserTestErrorBits::ERROR), + "leading pipe not reported properly" + ); + + assert!( + detect_errors!("true | # comment") == Err(ParserTestErrorBits::INCOMPLETE), + "comment after pipe not reported as incomplete" + ); + + assert!( + detect_errors!("true | # comment \n false ").is_ok(), + "comment and newline after pipe wrongly reported as error" + ); + + assert!( + detect_errors!("true | ; false ") == Err(ParserTestErrorBits::ERROR), + "semicolon after pipe not detected as error" + ); + + assert!( + detect_argument_errors("foo").is_ok(), + "simple argument reported as error" + ); + + assert!( + detect_argument_errors("''").is_ok(), + "Empty string reported as error" + ); + + assert!( + detect_argument_errors("foo$$") + .unwrap_err() + .contains(ParserTestErrorBits::ERROR), + "Bad variable expansion not reported as error" + ); + + assert!( + detect_argument_errors("foo$@") + .unwrap_err() + .contains(ParserTestErrorBits::ERROR), + "Bad variable expansion not reported as error" + ); + + // Within command substitutions, we should be able to detect everything that + // parse_util_detect_errors! can detect. + assert!( + detect_argument_errors("foo(cat | or cat)") + .unwrap_err() + .contains(ParserTestErrorBits::ERROR), + "Bad command substitution not reported as error" + ); + + assert!( + detect_errors!("false & ; and cat").is_err(), + "'and' command after background not reported as error" + ); + + assert!( + detect_errors!("true & ; or cat").is_err(), + "'or' command after background not reported as error" + ); + + assert!( + detect_errors!("true & ; not cat").is_ok(), + "'not' command after background falsely reported as error" + ); + + assert!( + detect_errors!("if true & ; end").is_err(), + "backgrounded 'if' conditional not reported as error" + ); + + assert!( + detect_errors!("if false; else if true & ; end").is_err(), + "backgrounded 'else if' conditional not reported as error" + ); + + assert!( + detect_errors!("while true & ; end").is_err(), + "backgrounded 'while' conditional not reported as error" + ); + + assert!( + detect_errors!("true | || false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("|| false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("&& false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true ; && false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true ; || false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true || && false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true && || false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true && && false").is_err(), + "bogus boolean statement error not detected" + ); + + assert!( + detect_errors!("true && ") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated conjunction not reported properly" + ); + + assert!( + detect_errors!("true && \n true").is_ok(), + "newline after && reported as error" + ); + + assert!( + detect_errors!("true || \n") == Err(ParserTestErrorBits::INCOMPLETE), + "unterminated conjunction not reported properly" + ); + + assert!( + detect_errors!("begin ; echo hi; }") == Err(ParserTestErrorBits::ERROR), + "closing of unopened brace statement not reported properly" + ); + + assert_eq!( + detect_errors!("begin {"), // } + Err(ParserTestErrorBits::INCOMPLETE), + "brace after begin not reported properly" + ); + assert_eq!( + detect_errors!("a=b {"), // } + Err(ParserTestErrorBits::INCOMPLETE), + "brace after variable override not reported properly" + ); + } + + #[test] + #[serial] + fn test_new_parser_correctness() { + let _cleanup = test_init(); + macro_rules! validate { + ($src:expr, $ok:expr) => { + let ast = ast::parse(L!($src), ParseTreeFlags::default(), None); + assert_eq!(ast.errored(), !$ok); + }; + } + validate!("; ; ; ", true); + validate!("if ; end", false); + validate!("if true ; end", true); + validate!("if true; end ; end", false); + validate!("if end; end ; end", false); + validate!("if end", false); + validate!("end", false); + validate!("for i i", false); + validate!("for i in a b c ; end", true); + validate!("begin end", true); + validate!("begin; end", true); + validate!("begin if true; end; end;", true); + validate!("begin if true ; echo hi ; end; end", true); + validate!("true && false || false", true); + validate!("true || false; and true", true); + validate!("true || ||", false); + validate!("|| true", false); + validate!("true || \n\n false", true); + } + + #[test] + #[serial] + fn test_new_parser_correctness_by_fuzzing() { + let _cleanup = test_init(); + let fuzzes = [ + L!("if"), + L!("else"), + L!("for"), + L!("in"), + L!("while"), + L!("begin"), + L!("function"), + L!("switch"), + L!("case"), + L!("end"), + L!("and"), + L!("or"), + L!("not"), + L!("command"), + L!("builtin"), + L!("foo"), + L!("|"), + L!("^"), + L!("&"), + L!(";"), + ]; + + // Generate a list of strings of all keyword / token combinations. + let mut src = WString::new(); + src.reserve(128); + + // Given that we have an array of 'fuzz_count' strings, we wish to enumerate all permutations of + // 'len' values. We do this by incrementing an integer, interpreting it as "base fuzz_count". + fn string_for_permutation( + fuzzes: &[&wstr], + len: usize, + permutation: usize, + ) -> Option { + let mut remaining_permutation = permutation; + let mut out_str = WString::new(); + for _i in 0..len { + let idx = remaining_permutation % fuzzes.len(); + remaining_permutation /= fuzzes.len(); + out_str.push_utfstr(fuzzes[idx]); + out_str.push(' '); + } + // Return false if we wrapped. + (remaining_permutation == 0).then_some(out_str) + } + + let max_len = 5; + for len in 0..max_len { + // We wish to look at all permutations of 4 elements of 'fuzzes' (with replacement). + // Construct an int and keep incrementing it. + let mut permutation = 0; + while let Some(src) = string_for_permutation(&fuzzes, len, permutation) { + permutation += 1; + ast::parse(&src, ParseTreeFlags::default(), None); + } + } + } + + // Test the LL2 (two token lookahead) nature of the parser by exercising the special builtin and + // command handling. In particular, 'command foo' should be a decorated statement 'foo' but 'command + // -help' should be an undecorated statement 'command' with argument '--help', and NOT attempt to + // run a command called '--help'. + #[test] + #[serial] + fn test_new_parser_ll2() { + let _cleanup = test_init(); + // Parse a statement, returning the command, args (joined by spaces), and the decoration. Returns + // true if successful. + fn test_1_parse_ll2(src: &wstr) -> Option<(WString, WString, StatementDecoration)> { + let ast = ast::parse(src, ParseTreeFlags::default(), None); + if ast.errored() { + return None; + } + + // Get the statement. Should only have one. + let mut statement = None; + for n in Traversal::new(ast.top()) { + if let Kind::DecoratedStatement(tmp) = n.kind() { + assert!( + statement.is_none(), + "More than one decorated statement found in '{}'", + src + ); + statement = Some(tmp); + } + } + let statement = statement.expect("No decorated statement found"); + + // Return its decoration and command. + let out_deco = statement.decoration(); + let out_cmd = statement.command.source(src).to_owned(); + + // Return arguments separated by spaces. + let out_joined_args = join_strings( + &statement + .args_or_redirs + .iter() + .filter(|a| a.is_argument()) + .map(|a| a.source(src)) + .collect::>(), + ' ', + ); + + Some((out_cmd, out_joined_args, out_deco)) + } + macro_rules! validate { + ($src:expr, $cmd:expr, $args:expr, $deco:expr) => { + let (cmd, args, deco) = test_1_parse_ll2(L!($src)).unwrap(); + assert_eq!(cmd, L!($cmd)); + assert_eq!(args, L!($args)); + assert_eq!(deco, $deco); + }; + } + + validate!("echo hello", "echo", "hello", StatementDecoration::none); + validate!( + "command echo hello", + "echo", + "hello", + StatementDecoration::command + ); + validate!( + "exec echo hello", + "echo", + "hello", + StatementDecoration::exec + ); + validate!( + "command command hello", + "command", + "hello", + StatementDecoration::command + ); + validate!( + "builtin command hello", + "command", + "hello", + StatementDecoration::builtin + ); + validate!( + "command --help", + "command", + "--help", + StatementDecoration::none + ); + validate!("command -h", "command", "-h", StatementDecoration::none); + validate!("command", "command", "", StatementDecoration::none); + validate!("command -", "command", "-", StatementDecoration::none); + validate!("command --", "command", "--", StatementDecoration::none); + validate!( + "builtin --names", + "builtin", + "--names", + StatementDecoration::none + ); + validate!("function", "function", "", StatementDecoration::none); + validate!( + "function --help", + "function", + "--help", + StatementDecoration::none + ); + + // Verify that 'function -h' and 'function --help' are plain statements but 'function --foo' is + // not (issue #1240). + macro_rules! check_function_help { + ($src:expr, $kind:pat) => { + let ast = ast::parse(L!($src), ParseTreeFlags::default(), None); + assert!(!ast.errored()); + assert_eq!( + Traversal::new(ast.top()) + .filter(|n| matches!(n.kind(), $kind)) + .count(), + 1 + ); + }; + } + check_function_help!("function -h", ast::Kind::DecoratedStatement(_)); + check_function_help!("function --help", ast::Kind::DecoratedStatement(_)); + check_function_help!("function --foo; end", ast::Kind::FunctionHeader(_)); + check_function_help!("function foo; end", ast::Kind::FunctionHeader(_)); + } + + #[test] + #[serial] + fn test_new_parser_ad_hoc() { + let _cleanup = test_init(); + // Very ad-hoc tests for issues encountered. + + // Ensure that 'case' terminates a job list. + let src = L!("switch foo ; case bar; case baz; end"); + let ast = ast::parse(src, ParseTreeFlags::default(), None); + assert!(!ast.errored()); + // Expect two CaseItems. The bug was that we'd + // try to run a command 'case'. + assert_eq!( + Traversal::new(ast.top()) + .filter(|n| matches!(n.kind(), ast::Kind::CaseItem(_))) + .count(), + 2 + ); + + // Ensure that naked variable assignments don't hang. + // The bug was that "a=" would produce an error but not be consumed, + // leading to an infinite loop. + + // By itself it should produce an error. + let ast = ast::parse(L!("a="), ParseTreeFlags::default(), None); + assert!(ast.errored()); + + // If we are leaving things unterminated, this should not produce an error. + // i.e. when typing "a=" at the command line, it should be treated as valid + // because we don't want to color it as an error. + let ast = ast::parse(L!("a="), ParseTreeFlags::LEAVE_UNTERMINATED, None); + assert!(!ast.errored()); + + let mut errors = vec![]; + ast::parse( + L!("begin; echo ("), + ParseTreeFlags::LEAVE_UNTERMINATED, + Some(&mut errors), + ); + assert!(errors.len() == 1); + assert!(errors[0].code == ParseErrorCode::tokenizer_unterminated_subshell); + + errors.clear(); + ast::parse( + L!("for x in ("), + ParseTreeFlags::LEAVE_UNTERMINATED, + Some(&mut errors), + ); + assert!(errors.len() == 1); + assert!(errors[0].code == ParseErrorCode::tokenizer_unterminated_subshell); + + errors.clear(); + ast::parse( + L!("begin; echo '"), + ParseTreeFlags::LEAVE_UNTERMINATED, + Some(&mut errors), + ); + assert!(errors.len() == 1); + assert!(errors[0].code == ParseErrorCode::tokenizer_unterminated_quote); + } + + #[test] + #[serial] + fn test_new_parser_errors() { + let _cleanup = test_init(); + macro_rules! validate { + ($src:expr, $expected_code:expr) => { + let mut errors = vec![]; + let ast = ast::parse(L!($src), ParseTreeFlags::default(), Some(&mut errors)); + assert!(ast.errored()); + assert_eq!( + errors.into_iter().map(|e| e.code).collect::>(), + vec![$expected_code], + ); + }; + } + + validate!("echo 'abc", ParseErrorCode::tokenizer_unterminated_quote); + validate!("'", ParseErrorCode::tokenizer_unterminated_quote); + validate!("echo (abc", ParseErrorCode::tokenizer_unterminated_subshell); + + validate!("end", ParseErrorCode::unbalancing_end); + validate!("echo hi ; end", ParseErrorCode::unbalancing_end); + + validate!("else", ParseErrorCode::unbalancing_else); + validate!("if true ; end ; else", ParseErrorCode::unbalancing_else); + + validate!("case", ParseErrorCode::unbalancing_case); + validate!("if true ; case ; end", ParseErrorCode::unbalancing_case); + + validate!("begin ; }", ParseErrorCode::unbalancing_brace); + + validate!("true | and", ParseErrorCode::andor_in_pipeline); + + validate!("a=", ParseErrorCode::bare_variable_assignment); + } + + #[test] + #[serial] + fn test_eval_recursion_detection() { + let _cleanup = test_init(); + // Ensure that we don't crash on infinite self recursion and mutual recursion. + let parser = TestParser::new(); + parser.eval( + L!("function recursive ; recursive ; end ; recursive; "), + &IoChain::new(), + ); + + parser.eval( + L!(concat!( + "function recursive1 ; recursive2 ; end ; ", + "function recursive2 ; recursive1 ; end ; recursive1; ", + )), + &IoChain::new(), + ); + } + + #[test] + #[serial] + fn test_eval_illegal_exit_code() { + let _cleanup = test_init(); + let parser = TestParser::new(); + macro_rules! validate { + ($cmd:expr, $result:expr) => { + parser.eval($cmd, &IoChain::new()); + let exit_status = parser.get_last_status(); + assert_eq!(exit_status, parser.get_last_status()); + }; + } + + // We need to be in an empty directory so that none of the wildcards match a file that might be + // in the fish source tree. In particular we need to ensure that "?" doesn't match a file + // named by a single character. See issue #3852. + parser.pushd("test/temp"); + validate!(L!("echo -n"), STATUS_CMD_OK.unwrap()); + validate!(L!("pwd"), STATUS_CMD_OK.unwrap()); + validate!(L!("UNMATCHABLE_WILDCARD*"), STATUS_UNMATCHED_WILDCARD); + validate!(L!("UNMATCHABLE_WILDCARD**"), STATUS_UNMATCHED_WILDCARD); + validate!(L!("?"), STATUS_UNMATCHED_WILDCARD); + validate!(L!("abc?def"), STATUS_UNMATCHED_WILDCARD); + parser.popd(); + } + + #[test] + #[serial] + fn test_eval_empty_function_name() { + let _cleanup = test_init(); + let parser = TestParser::new(); + parser.eval( + L!("function '' ; echo fail; exit 42 ; end ; ''"), + &IoChain::new(), + ); + } + + #[test] + #[serial] + fn test_expand_argument_list() { + let _cleanup = test_init(); + let parser = TestParser::new(); + let comps: Vec = Parser::expand_argument_list( + L!("alpha 'beta gamma' delta"), + ExpandFlags::default(), + &parser.context(), + ) + .into_iter() + .map(|c| c.completion) + .collect(); + assert_eq!(comps, &[L!("alpha"), L!("beta gamma"), L!("delta"),]); + } + + fn test_1_cancellation(parser: &Parser, src: &wstr) { + let filler = IoBufferfill::create().unwrap(); + let delay = Duration::from_millis(100); + #[allow(clippy::unnecessary_cast)] + let thread = unsafe { libc::pthread_self() } as usize; + iothread_perform(move || { + // Wait a while and then SIGINT the main thread. + std::thread::sleep(delay); + unsafe { + libc::pthread_kill(thread as libc::pthread_t, SIGINT); + } + }); + let mut io = IoChain::new(); + io.push(filler.clone()); + let res = parser.eval(src, &io); + let buffer = IoBufferfill::finish(filler); + assert_eq!( + buffer.len(), + 0, + "Expected 0 bytes in out_buff, but instead found {} bytes, for command {}", + buffer.len(), + src + ); + assert!(res.status.signal_exited() && res.status.signal_code() == SIGINT); + } + + #[test] + #[serial] + fn test_cancellation() { + let _cleanup = test_init(); + let parser = Parser::new(EnvStack::new(), CancelBehavior::Clear); + let _pop = fake_scoped_reader(&parser); + + printf!("Testing Ctrl-C cancellation. If this hangs, that's a bug!\n"); + + // Enable fish's signal handling here. + signal_set_handlers(true); + + // This tests that we can correctly ctrl-C out of certain loop constructs, and that nothing gets + // printed if we do. + + // Here the command substitution is an infinite loop. echo never even gets its argument, so when + // we cancel we expect no output. + test_1_cancellation(&parser, L!("echo (while true ; echo blah ; end)")); + + // Nasty infinite loop that doesn't actually execute anything. + test_1_cancellation( + &parser, + L!("echo (while true ; end) (while true ; end) (while true ; end)"), + ); + test_1_cancellation(&parser, L!("while true ; end")); + test_1_cancellation(&parser, L!("while true ; echo nothing > /dev/null; end")); + test_1_cancellation(&parser, L!("for i in (while true ; end) ; end")); + + signal_reset_handlers(); + + // Ensure that we don't think we should cancel. + reader_reset_interrupted(); + signal_clear_cancel(); + } + + #[test] + fn test_line_counter() { + let src = L!("echo line1; echo still_line_1;\n\necho line3"); + let ps = parse_source(src.to_owned(), ParseTreeFlags::default(), None) + .expect("Failed to parse source"); + assert!(!ps.ast.errored()); + let mut line_counter = ps.line_counter(); + + // Test line_offset_of_character_at_offset, both forwards and backwards to exercise the cache. + let mut expected = 0; + for (idx, c) in src.chars().enumerate() { + let line_offset = line_counter.line_offset_of_character_at_offset(idx); + assert_eq!(line_offset, expected); + if c == '\n' { + expected += 1; + } + } + for (idx, c) in src.chars().enumerate().rev() { + if c == '\n' { + expected -= 1; + } + let line_offset = line_counter.line_offset_of_character_at_offset(idx); + assert_eq!(line_offset, expected); + } + + let pipelines: Vec<_> = ps.ast.walk().filter_map(ast::JobPipeline::cast).collect(); + assert_eq!(pipelines.len(), 3); + let src_offsets = [0, 0, 2]; + assert_eq!(line_counter.source_offset_of_node(), None); + assert_eq!(line_counter.line_offset_of_node(), None); + + for (idx, &node) in pipelines.iter().enumerate() { + line_counter.node = node as *const _; + assert_eq!( + line_counter.source_offset_of_node(), + Some(node.source_range().start()) + ); + assert_eq!(line_counter.line_offset_of_node(), Some(src_offsets[idx])); + } + + for (idx, &node) in pipelines.iter().enumerate().rev() { + line_counter.node = node as *const _; + assert_eq!( + line_counter.source_offset_of_node(), + Some(node.source_range().start()) + ); + assert_eq!(line_counter.line_offset_of_node(), Some(src_offsets[idx])); + } + } + + #[test] + fn test_line_counter_empty() { + let mut line_counter = LineCounter::::empty(); + assert_eq!(line_counter.line_offset_of_character_at_offset(0), 0); + assert_eq!(line_counter.line_offset_of_node(), None); + assert_eq!(line_counter.source_offset_of_node(), None); + } + + // Helper for testing a simple ast traversal. + // The ast is always for the command 'true;'. + struct TrueSemiAstTester<'a> { + // The AST we are testing. + ast: &'a Ast, + + // Expected parent-child relationships, in the order we expect to encounter them. + parent_child: Box<[(&'a dyn Node, &'a dyn Node)]>, + } + + impl<'a> TrueSemiAstTester<'a> { + const TRUE_SEMI: &'static wstr = L!("true;"); + fn new(ast: &'a Ast) -> Self { + let job_list: &JobList = ast.top(); + let job_conjunction = &job_list[0]; + let job_pipeline = &job_conjunction.job; + let variable_assignment_list = &job_pipeline.variables; + let statement = &job_pipeline.statement; + + let decorated_statement = statement + .as_decorated_statement() + .expect("Expected decorated_statement"); + let command = &decorated_statement.command; + let args_or_redirs = &decorated_statement.args_or_redirs; + let job_continuation = &job_pipeline.continuation; + let job_conjunction_continuation = &job_conjunction.continuations; + let semi_nl = job_conjunction.semi_nl.as_ref().expect("Expected semi_nl"); + + // Helpful parent-child map, such that the children are in the order that we expect to encounter them + // in the AST. + let parent_child: &[(&'a dyn Node, &'a dyn Node)] = &[ + (job_list, job_conjunction), + (job_conjunction, job_pipeline), + (job_pipeline, variable_assignment_list), + (job_pipeline, statement), + (statement, decorated_statement), + (decorated_statement, command), + (decorated_statement, args_or_redirs), + (job_pipeline, job_continuation), + (job_conjunction, job_conjunction_continuation), + (job_conjunction, semi_nl), + ]; + Self { + ast, + parent_child: Box::from(parent_child), + } + } + + // Expected nodes, in-order. + fn expected_nodes(&self) -> Vec<&'a dyn Node> { + let mut expected: Vec<&dyn Node> = vec![self.ast.top()]; + expected.extend(self.parent_child.iter().map(|&(_p, c)| c)); + expected + } + + // Helper function to construct the parent list of a given node, such at the first entry is + // the node itself, and the last entry is the root node. + fn get_parents<'s>( + &'s self, + node: &'a dyn Node, + ) -> impl Iterator + 's { + let mut next = Some(node); + std::iter::from_fn(move || { + let out = next?; + next = self + .parent_child + .iter() + .find_map(|&(p, c)| is_same_node(c, out).then_some(p)); + Some(out) + }) + } + } + + #[test] + fn test_ast() { + // Light testing of the AST and traversals. + let ast = ast::parse(TrueSemiAstTester::TRUE_SEMI, ParseTreeFlags::empty(), None); + let tester = TrueSemiAstTester::new(&ast); + + // Walk the AST and collect all nodes. + // See is_same_node comments for why we can't use assert_eq! here. + let found = ast.walk().collect::>(); + let expected = tester.expected_nodes(); + assert_eq!(found.len(), expected.len()); + for idx in 0..found.len() { + assert!(is_same_node(found[idx], expected[idx])); + } + + // Walk and check parents. + let mut traversal = ast.walk(); + while let Some(node) = traversal.next() { + let expected_parents = tester.get_parents(node).collect::>(); + let found_parents = traversal.parent_nodes().collect::>(); + assert_eq!(found_parents.len(), expected_parents.len()); + for idx in 0..found_parents.len() { + assert!(is_same_node(found_parents[idx], expected_parents[idx])); + } + } + + // Find the decorated statement. + let decorated_statement = ast + .walk() + .find(|n| matches!(n.kind(), ast::Kind::DecoratedStatement(_))) + .expect("Expected decorated statement"); + + // Test the skip feature. Don't descend into the decorated_statement. + let expected_skip: Vec<&dyn Node> = expected + .iter() + .copied() + .filter(|&n| { + // Discard nodes who have the decorated_statement as a parent, + // excepting the decorated_statement itself. + tester + .get_parents(n) + .skip(1) + .all(|p| !is_same_node(p, decorated_statement)) + }) + .collect(); + + let mut found = vec![]; + let mut traversal = ast.walk(); + while let Some(node) = traversal.next() { + if is_same_node(node, decorated_statement) { + traversal.skip_children(node); + } + found.push(node); + } + assert_eq!(found.len(), expected_skip.len()); + for idx in 0..found.len() { + assert!(is_same_node(found[idx], expected_skip[idx])); + } + } + + #[test] + #[should_panic] + fn test_traversal_skip_children_panics() { + // Test that we panic if we try to skip children of a node that is not the current node. + let ast = ast::parse(L!("true;"), ParseTreeFlags::empty(), None); + let mut traversal = ast.walk(); + while let Some(node) = traversal.next() { + if matches!(node.kind(), ast::Kind::DecoratedStatement(_)) { + // Should panic as we can only skip the current node. + traversal.skip_children(ast.top()); + } + } + } + + #[test] + #[should_panic] + fn test_traversal_parent_panics() { + // Can only get the parent of nodes still on the stack. + let ast = ast::parse(L!("true;"), ParseTreeFlags::empty(), None); + let mut traversal = ast.walk(); + let mut decorated_statement = None; + while let Some(node) = traversal.next() { + if let Kind::DecoratedStatement(_) = node.kind() { + decorated_statement = Some(node); + } else if node.as_token().map(|t| t.token_type()) == Some(ParseTokenType::end) { + // should panic as the decorated_statement is not on the stack. + let _ = traversal.parent(decorated_statement.unwrap()); + } + } + } +} diff --git a/src/path.rs b/src/path.rs index 2326103ed..9a41e9367 100644 --- a/src/path.rs +++ b/src/path.rs @@ -767,55 +767,61 @@ pub fn append_path_component(path: &mut WString, component: &wstr) { } } -#[test] -fn test_path_make_canonical() { - let mut path = L!("//foo//////bar/").to_owned(); - path_make_canonical(&mut path); - assert_eq!(path, "/foo/bar"); +#[cfg(test)] +mod tests { + use super::{path_apply_working_directory, path_make_canonical, paths_are_equivalent}; + use crate::wchar::prelude::*; - path = L!("/").to_owned(); - path_make_canonical(&mut path); - assert_eq!(path, "/"); -} - -#[test] -fn test_path() { - let mut path = L!("//foo//////bar/").to_owned(); - path_make_canonical(&mut path); - assert_eq!(&path, L!("/foo/bar")); - - path = L!("/").to_owned(); - path_make_canonical(&mut path); - assert_eq!(&path, L!("/")); - - path = L!("/home/fishuser/").to_owned(); - path_make_canonical(&mut path); - assert_eq!(&path, L!("/home/fishuser")); - - assert!(!paths_are_equivalent(L!("/foo/bar/baz"), L!("foo/bar/baz"))); - assert!(paths_are_equivalent( - L!("///foo///bar/baz"), - L!("/foo/bar////baz//") - )); - assert!(paths_are_equivalent(L!("/foo/bar/baz"), L!("/foo/bar/baz"))); - assert!(paths_are_equivalent(L!("/"), L!("/"))); - - assert_eq!( - path_apply_working_directory(L!("abc"), L!("/def/")), - L!("/def/abc") - ); - assert_eq!( - path_apply_working_directory(L!("abc/"), L!("/def/")), - L!("/def/abc/") - ); - assert_eq!( - path_apply_working_directory(L!("/abc/"), L!("/def/")), - L!("/abc/") - ); - assert_eq!( - path_apply_working_directory(L!("/abc"), L!("/def/")), - L!("/abc") - ); - assert!(path_apply_working_directory(L!(""), L!("/def/")).is_empty()); - assert_eq!(path_apply_working_directory(L!("abc"), L!("")), L!("abc")); + #[test] + fn test_path_make_canonical() { + let mut path = L!("//foo//////bar/").to_owned(); + path_make_canonical(&mut path); + assert_eq!(path, "/foo/bar"); + + path = L!("/").to_owned(); + path_make_canonical(&mut path); + assert_eq!(path, "/"); + } + + #[test] + fn test_path() { + let mut path = L!("//foo//////bar/").to_owned(); + path_make_canonical(&mut path); + assert_eq!(&path, L!("/foo/bar")); + + path = L!("/").to_owned(); + path_make_canonical(&mut path); + assert_eq!(&path, L!("/")); + + path = L!("/home/fishuser/").to_owned(); + path_make_canonical(&mut path); + assert_eq!(&path, L!("/home/fishuser")); + + assert!(!paths_are_equivalent(L!("/foo/bar/baz"), L!("foo/bar/baz"))); + assert!(paths_are_equivalent( + L!("///foo///bar/baz"), + L!("/foo/bar////baz//") + )); + assert!(paths_are_equivalent(L!("/foo/bar/baz"), L!("/foo/bar/baz"))); + assert!(paths_are_equivalent(L!("/"), L!("/"))); + + assert_eq!( + path_apply_working_directory(L!("abc"), L!("/def/")), + L!("/def/abc") + ); + assert_eq!( + path_apply_working_directory(L!("abc/"), L!("/def/")), + L!("/def/abc/") + ); + assert_eq!( + path_apply_working_directory(L!("/abc/"), L!("/def/")), + L!("/abc/") + ); + assert_eq!( + path_apply_working_directory(L!("/abc"), L!("/def/")), + L!("/abc") + ); + assert!(path_apply_working_directory(L!(""), L!("/def/")).is_empty()); + assert_eq!(path_apply_working_directory(L!("abc"), L!("")), L!("abc")); + } } diff --git a/src/re.rs b/src/re.rs index da4e56bd0..ed1e55e86 100644 --- a/src/re.rs +++ b/src/re.rs @@ -20,32 +20,38 @@ pub fn to_boxed_chars(s: &wstr) -> Box<[char]> { chars.into() } -#[test] -fn test_regex_make_anchored() { - use pcre2::utf32::{Regex, RegexBuilder}; +#[cfg(test)] +mod tests { + use super::{regex_make_anchored, to_boxed_chars}; + use crate::wchar::prelude::*; - fn test_match(re: &Regex, subject: &wstr) -> bool { - re.is_match(&to_boxed_chars(subject)).unwrap() + #[test] + fn test_regex_make_anchored() { + use pcre2::utf32::{Regex, RegexBuilder}; + + fn test_match(re: &Regex, subject: &wstr) -> bool { + re.is_match(&to_boxed_chars(subject)).unwrap() + } + + let builder = RegexBuilder::new(); + let result = builder.build(to_boxed_chars(®ex_make_anchored(L!("ab(.+?)")))); + assert!(result.is_ok()); + let re = &result.unwrap(); + + assert!(!test_match(re, L!(""))); + assert!(!test_match(re, L!("ab"))); + assert!(test_match(re, L!("abcd"))); + assert!(!test_match(re, L!("xabcd"))); + assert!(test_match(re, L!("abcdefghij"))); + + let result = builder.build(to_boxed_chars(®ex_make_anchored(L!("(a+)|(b+)")))); + assert!(result.is_ok()); + + let re = &result.unwrap(); + assert!(!test_match(re, L!(""))); + assert!(!test_match(re, L!("aabb"))); + assert!(test_match(re, L!("aaaa"))); + assert!(test_match(re, L!("bbbb"))); + assert!(!test_match(re, L!("aaaax"))); } - - let builder = RegexBuilder::new(); - let result = builder.build(to_boxed_chars(®ex_make_anchored(L!("ab(.+?)")))); - assert!(result.is_ok()); - let re = &result.unwrap(); - - assert!(!test_match(re, L!(""))); - assert!(!test_match(re, L!("ab"))); - assert!(test_match(re, L!("abcd"))); - assert!(!test_match(re, L!("xabcd"))); - assert!(test_match(re, L!("abcdefghij"))); - - let result = builder.build(to_boxed_chars(®ex_make_anchored(L!("(a+)|(b+)")))); - assert!(result.is_ok()); - - let re = &result.unwrap(); - assert!(!test_match(re, L!(""))); - assert!(!test_match(re, L!("aabb"))); - assert!(test_match(re, L!("aaaa"))); - assert!(test_match(re, L!("bbbb"))); - assert!(!test_match(re, L!("aaaax"))); } diff --git a/src/reader.rs b/src/reader.rs index a387a0e48..c5c58a9e2 100644 --- a/src/reader.rs +++ b/src/reader.rs @@ -1594,8 +1594,7 @@ pub fn reader_save_screen_state() { } /// Given a command line and an autosuggestion, return the string that gets shown to the user. -/// Exposed for testing purposes only. -pub fn combine_command_and_autosuggestion( +fn combine_command_and_autosuggestion( cmdline: &wstr, line_range: Range, autosuggestion: &wstr, @@ -6735,3 +6734,198 @@ fn completion_insert( self.set_buffer_maintaining_pager(&new_command_line, cursor); } } + +#[cfg(test)] +mod tests { + use crate::complete::CompleteFlags; + use crate::operation_context::{OperationContext, no_cancel}; + use crate::reader::{combine_command_and_autosuggestion, completion_apply_to_command_line}; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + + #[test] + fn test_autosuggestion_combining() { + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("alphabeta")), + L!("alphabeta") + ); + + // When the last token contains no capital letters, we use the case of the autosuggestion. + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHABETA")), + L!("ALPHABETA") + ); + + // When the last token contains capital letters, we use its case. + assert_eq!( + combine_command_and_autosuggestion(L!("alPha"), 0..5, L!("alphabeTa")), + L!("alPhabeTa") + ); + + // If autosuggestion is not longer than input, use the input's case. + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHAA")), + L!("ALPHAA") + ); + assert_eq!( + combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHA")), + L!("ALPHA") + ); + + assert_eq!( + combine_command_and_autosuggestion(L!("al\nbeta"), 0..2, L!("alpha")), + L!("alpha\nbeta").to_owned() + ); + assert_eq!( + combine_command_and_autosuggestion(L!("alpha\nbe"), 6..8, L!("beta")), + L!("alpha\nbeta").to_owned() + ); + assert_eq!( + combine_command_and_autosuggestion(L!("alpha\nbe\ngamma"), 6..8, L!("beta")), + L!("alpha\nbeta\ngamma").to_owned() + ); + } + + #[test] + fn test_completion_insertions() { + let parser = TestParser::new(); + + macro_rules! validate { + ( + $line:expr, $completion:expr, + $flags:expr, $append_only:expr, + $expected:expr + ) => { + // line is given with a caret, which we use to represent the cursor position. Find it. + let mut line = L!($line).to_owned(); + let completion = L!($completion); + let mut expected = L!($expected).to_owned(); + let in_cursor_pos = line.find(L!("^")).unwrap(); + line.remove(in_cursor_pos); + + let out_cursor_pos = expected.find(L!("^")).unwrap(); + expected.remove(out_cursor_pos); + + let mut cursor_pos = in_cursor_pos; + + let result = completion_apply_to_command_line( + &OperationContext::test_only_foreground( + &parser, + parser.vars(), + Box::new(no_cancel), + ), + completion, + $flags, + &line, + &mut cursor_pos, + $append_only, + /*is_unique=*/ false, + ); + assert_eq!(result, expected); + assert_eq!(cursor_pos, out_cursor_pos); + }; + } + + validate!("foo^", "bar", CompleteFlags::default(), false, "foobar ^"); + // An unambiguous completion of a token that is already trailed by a space character. + // After completing, the cursor moves on to the next token, suggesting to the user that the + // current token is finished. + validate!( + "foo^ baz", + "bar", + CompleteFlags::default(), + false, + "foobar ^baz" + ); + validate!( + "'foo^", + "bar", + CompleteFlags::default(), + false, + "'foobar' ^" + ); + validate!( + "'foo'^", + "bar", + CompleteFlags::default(), + false, + "'foobar' ^" + ); + validate!( + "'foo\\'^", + "bar", + CompleteFlags::default(), + false, + "'foo\\'bar' ^" + ); + validate!( + "foo\\'^", + "bar", + CompleteFlags::default(), + false, + "foo\\'bar ^" + ); + + // Test append only. + validate!("foo^", "bar", CompleteFlags::default(), true, "foobar ^"); + validate!( + "foo^ baz", + "bar", + CompleteFlags::default(), + true, + "foobar ^baz" + ); + validate!("'foo^", "bar", CompleteFlags::default(), true, "'foobar' ^"); + validate!( + "'foo'^", + "bar", + CompleteFlags::default(), + true, + "'foo'bar ^" + ); + validate!( + "'foo\\'^", + "bar", + CompleteFlags::default(), + true, + "'foo\\'bar' ^" + ); + validate!( + "foo\\'^", + "bar", + CompleteFlags::default(), + true, + "foo\\'bar ^" + ); + + validate!("foo^", "bar", CompleteFlags::NO_SPACE, false, "foobar^"); + validate!("'foo^", "bar", CompleteFlags::NO_SPACE, false, "'foobar^"); + validate!("'foo'^", "bar", CompleteFlags::NO_SPACE, false, "'foobar'^"); + validate!( + "'foo\\'^", + "bar", + CompleteFlags::NO_SPACE, + false, + "'foo\\'bar^" + ); + validate!( + "foo\\'^", + "bar", + CompleteFlags::NO_SPACE, + false, + "foo\\'bar^" + ); + + validate!("foo^", "bar", CompleteFlags::REPLACES_TOKEN, false, "bar ^"); + validate!( + "'foo^", + "bar", + CompleteFlags::REPLACES_TOKEN, + false, + "bar ^" + ); + + // See #6130 + validate!(": (:^ ''", "", CompleteFlags::default(), false, ": (: ^''"); + } +} diff --git a/src/redirection.rs b/src/redirection.rs index 23416ff68..f6cb9365b 100644 --- a/src/redirection.rs +++ b/src/redirection.rs @@ -152,3 +152,50 @@ pub fn add_close(&mut self, fd: RawFd) { }) } } + +#[cfg(test)] +mod tests { + use crate::io::{IoChain, IoClose, IoFd}; + use crate::redirection::dup2_list_resolve_chain; + use std::sync::Arc; + + #[test] + fn test_dup2s() { + let mut chain = IoChain::new(); + chain.push(Arc::new(IoClose::new(17))); + chain.push(Arc::new(IoFd::new(3, 19))); + let list = dup2_list_resolve_chain(&chain); + assert_eq!(list.get_actions().len(), 2); + + let act1 = list.get_actions()[0]; + assert_eq!(act1.src, 17); + assert_eq!(act1.target, -1); + + let act2 = list.get_actions()[1]; + assert_eq!(act2.src, 19); + assert_eq!(act2.target, 3); + } + + #[test] + fn test_dup2s_fd_for_target_fd() { + let mut chain = IoChain::new(); + // note io_fd_t params are backwards from dup2. + chain.push(Arc::new(IoClose::new(10))); + chain.push(Arc::new(IoFd::new(9, 10))); + chain.push(Arc::new(IoFd::new(5, 8))); + chain.push(Arc::new(IoFd::new(1, 4))); + chain.push(Arc::new(IoFd::new(3, 5))); + let list = dup2_list_resolve_chain(&chain); + + assert_eq!(list.fd_for_target_fd(3), 8); + assert_eq!(list.fd_for_target_fd(5), 8); + assert_eq!(list.fd_for_target_fd(8), 8); + assert_eq!(list.fd_for_target_fd(1), 4); + assert_eq!(list.fd_for_target_fd(4), 4); + assert_eq!(list.fd_for_target_fd(100), 100); + assert_eq!(list.fd_for_target_fd(0), 0); + assert_eq!(list.fd_for_target_fd(-1), -1); + assert_eq!(list.fd_for_target_fd(9), -1); + assert_eq!(list.fd_for_target_fd(10), -1); + } +} diff --git a/src/screen.rs b/src/screen.rs index 74995e3a5..6d61c57d7 100644 --- a/src/screen.rs +++ b/src/screen.rs @@ -1365,23 +1365,22 @@ pub fn screen_force_clear_to_end() { /// Information about the layout of a prompt. #[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct PromptLayout { +struct PromptLayout { /// Start offsets for each line in the truncated prompt. - pub line_starts: Vec, + line_starts: Vec, /// Width of the last line. - pub last_line_width: usize, + last_line_width: usize, } -// Fields exposed for testing. -pub struct PromptCacheEntry { +struct PromptCacheEntry { /// Original prompt string. - pub text: WString, + text: WString, /// Max line width when computing layout (for truncation). - pub max_line_width: usize, + max_line_width: usize, /// Resulting truncated prompt string. - pub trunc_text: WString, + trunc_text: WString, /// Resulting layout. - pub layout: PromptLayout, + layout: PromptLayout, } // Maintain a mapping of escape sequences to their widths for fast lookup. @@ -1392,8 +1391,7 @@ pub struct LayoutCache { esc_cache: Vec, // LRU-list of prompts and their layouts. // Use a list so we can promote to the front on a cache hit. - // Exposed for testing. - pub prompt_cache: LinkedList, + prompt_cache: LinkedList, } // Singleton of the cached escape sequences seen in prompts and similar strings. @@ -1425,7 +1423,7 @@ pub fn add_escape_code(&mut self, s: WString) { } /// Return the length of an escape code, accessing and perhaps populating the cache. - pub fn escape_code_length(&mut self, code: &wstr) -> usize { + fn escape_code_length(&mut self, code: &wstr) -> usize { if code.char_at(0) != '\x1B' { return 0; } @@ -1463,7 +1461,7 @@ pub fn find_escape_code(&self, entry: &wstr) -> usize { /// Computes a prompt layout for `prompt_str`, perhaps truncating it to `max_line_width`. /// Return the layout, and optionally the truncated prompt itself, by reference. - pub fn calc_prompt_layout( + fn calc_prompt_layout( &mut self, prompt_str: &wstr, out_trunc_prompt: Option<&mut WString>, @@ -1526,8 +1524,7 @@ pub fn clear(&mut self) { } /// Add a cache entry. - /// Exposed for testing. - pub fn add_prompt_layout(&mut self, entry: PromptCacheEntry) { + fn add_prompt_layout(&mut self, entry: PromptCacheEntry) { self.prompt_cache.push_front(entry); if self.prompt_cache.len() > Self::PROMPT_CACHE_MAX_SIZE { self.prompt_cache.pop_back(); @@ -1535,7 +1532,6 @@ pub fn add_prompt_layout(&mut self, entry: PromptCacheEntry) { } /// Finds the layout for a prompt, promoting it to the front. Returns whether this was found. - /// Exposed for testing. pub fn find_prompt_layout( &mut self, input: &wstr, @@ -1915,9 +1911,8 @@ pub(crate) fn only_grayscale() -> bool { ONLY_GRAYSCALE.load() } -// Exposed for testing. #[derive(Debug, Default, Eq, PartialEq)] -pub(crate) struct ScreenLayout { +struct ScreenLayout { // The left prompt that we're going to use. pub(crate) left_prompt: WString, // How many lines in the left prompt. @@ -1945,9 +1940,8 @@ fn truncation_offset_for_width(str: &wstr, max_width: usize) -> usize { i - 1 } -// Exposed for testing. #[allow(clippy::too_many_arguments)] -pub(crate) fn compute_layout( +fn compute_layout( ellipsis_char: char, screen_width: usize, left_untrunc_prompt: &wstr, @@ -2075,3 +2069,460 @@ pub fn wcwidth_rendered(c: char) -> isize { pub fn wcswidth_rendered(s: &wstr) -> isize { s.chars().map(wcwidth_rendered).sum() } + +#[cfg(test)] +mod tests { + use crate::common::get_ellipsis_char; + use crate::highlight::HighlightSpec; + use crate::parse_util::parse_util_compute_indents; + use crate::screen::{ + LayoutCache, PromptCacheEntry, PromptLayout, ScreenLayout, compute_layout, + }; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + use crate::wcstringutil::join_strings; + + #[test] + #[serial] + fn test_complete() { + let _cleanup = test_init(); + let mut lc = LayoutCache::new(); + assert_eq!(lc.escape_code_length(L!("")), 0); + assert_eq!(lc.escape_code_length(L!("abcd")), 0); + assert_eq!(lc.escape_code_length(L!("\x1B[2J")), 4); + assert_eq!( + lc.escape_code_length(L!("\x1B[38;5;123mABC")), + "\x1B[38;5;123m".len() + ); + assert_eq!(lc.escape_code_length(L!("\x1B@")), 2); + + // iTerm2 escape sequences. + assert_eq!( + lc.escape_code_length(L!("\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE")), + 25 + ); + assert_eq!( + lc.escape_code_length(L!("\x1B]50;SetMark\x07NOT_PART_OF_SEQUENCE")), + 13 + ); + assert_eq!( + lc.escape_code_length(L!("\x1B]6;1;bg;red;brightness;255\x07NOT_PART_OF_SEQUENCE")), + 28 + ); + assert_eq!( + lc.escape_code_length(L!("\x1B]Pg4040ff\x1B\\NOT_PART_OF_SEQUENCE")), + 12 + ); + assert_eq!(lc.escape_code_length(L!("\x1B]blahblahblah\x1B\\")), 16); + assert_eq!(lc.escape_code_length(L!("\x1B]blahblahblah\x07")), 15); + } + + #[test] + #[serial] + fn test_layout_cache() { + let _cleanup = test_init(); + let mut seqs = LayoutCache::new(); + + // Verify escape code cache. + assert_eq!(seqs.find_escape_code(L!("abc")), 0); + seqs.add_escape_code(L!("abc").to_owned()); + seqs.add_escape_code(L!("abc").to_owned()); + assert_eq!(seqs.esc_cache_size(), 1); + assert_eq!(seqs.find_escape_code(L!("abc")), 3); + assert_eq!(seqs.find_escape_code(L!("abcd")), 3); + assert_eq!(seqs.find_escape_code(L!("abcde")), 3); + assert_eq!(seqs.find_escape_code(L!("xabcde")), 0); + seqs.add_escape_code(L!("ac").to_owned()); + assert_eq!(seqs.find_escape_code(L!("abcd")), 3); + assert_eq!(seqs.find_escape_code(L!("acbd")), 2); + seqs.add_escape_code(L!("wxyz").to_owned()); + assert_eq!(seqs.find_escape_code(L!("abc")), 3); + assert_eq!(seqs.find_escape_code(L!("abcd")), 3); + assert_eq!(seqs.find_escape_code(L!("wxyz123")), 4); + assert_eq!(seqs.find_escape_code(L!("qwxyz123")), 0); + assert_eq!(seqs.esc_cache_size(), 3); + seqs.clear(); + assert_eq!(seqs.esc_cache_size(), 0); + assert_eq!(seqs.find_escape_code(L!("abcd")), 0); + + let huge = usize::MAX; + + // Verify prompt layout cache. + for i in 0..LayoutCache::PROMPT_CACHE_MAX_SIZE { + let input = i.to_wstring(); + assert!(!seqs.find_prompt_layout(&input, usize::MAX)); + seqs.add_prompt_layout(PromptCacheEntry { + text: input.clone(), + max_line_width: huge, + trunc_text: input.clone(), + layout: PromptLayout { + line_starts: vec![], + last_line_width: i, + }, + }); + assert!(seqs.find_prompt_layout(&input, usize::MAX)); + assert_eq!(seqs.prompt_cache.front().unwrap().layout.last_line_width, i); + } + + let expected_evictee = 3; + for i in 0..LayoutCache::PROMPT_CACHE_MAX_SIZE { + if i != expected_evictee { + assert!(seqs.find_prompt_layout(&i.to_wstring(), usize::MAX)); + assert_eq!(seqs.prompt_cache.front().unwrap().layout.last_line_width, i); + } + } + + seqs.add_prompt_layout(PromptCacheEntry { + text: "whatever".into(), + max_line_width: huge, + trunc_text: "whatever".into(), + layout: PromptLayout { + line_starts: vec![], + last_line_width: 100, + }, + }); + assert!(!seqs.find_prompt_layout(&expected_evictee.to_wstring(), usize::MAX)); + assert!(seqs.find_prompt_layout(L!("whatever"), huge)); + assert_eq!( + seqs.prompt_cache.front().unwrap().layout.last_line_width, + 100 + ); + } + + #[test] + #[serial] + fn test_prompt_truncation() { + let _cleanup = test_init(); + let mut cache = LayoutCache::new(); + let mut trunc = WString::new(); + + let ellipsis = || WString::from_chars([get_ellipsis_char()]); + + // No truncation. + let layout = cache.calc_prompt_layout(L!("abcd"), Some(&mut trunc), usize::MAX); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0], + last_line_width: 4, + } + ); + assert_eq!(trunc, L!("abcd")); + + // Line break calculation. + let layout = cache.calc_prompt_layout( + L!(concat!( + "0123456789ABCDEF\n", + "012345\n", + "0123456789abcdef\n", + "xyz" + )), + Some(&mut trunc), + 80, + ); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0, 17, 24, 41], + last_line_width: 3, + } + ); + + // Basic truncation. + let layout = cache.calc_prompt_layout(L!("0123456789ABCDEF"), Some(&mut trunc), 8); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0], + last_line_width: 8, + }, + ); + assert_eq!(trunc, ellipsis() + L!("9ABCDEF")); + + // Multiline truncation. + let layout = cache.calc_prompt_layout( + L!(concat!( + "0123456789ABCDEF\n", + "012345\n", + "0123456789abcdef\n", + "xyz" + )), + Some(&mut trunc), + 8, + ); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0, 9, 16, 25], + last_line_width: 3, + }, + ); + assert_eq!( + trunc, + join_strings( + &[ + ellipsis() + L!("9ABCDEF"), + L!("012345").to_owned(), + ellipsis() + L!("9abcdef"), + L!("xyz").to_owned(), + ], + '\n', + ), + ); + + // Escape sequences are not truncated. + let layout = cache.calc_prompt_layout( + L!("\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE"), + Some(&mut trunc), + 4, + ); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0], + last_line_width: 4, + }, + ); + assert_eq!(trunc, ellipsis() + L!("\x1B]50;CurrentDir=test/foo\x07NCE")); + + // Newlines in escape sequences are skipped. + let layout = cache.calc_prompt_layout( + L!("\x1B]50;CurrentDir=\ntest/foo\x07NOT_PART_OF_SEQUENCE"), + Some(&mut trunc), + 4, + ); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0], + last_line_width: 4, + }, + ); + assert_eq!( + trunc, + ellipsis() + L!("\x1B]50;CurrentDir=\ntest/foo\x07NCE") + ); + + // We will truncate down to one character if we have to. + let layout = cache.calc_prompt_layout(L!("Yay"), Some(&mut trunc), 1); + assert_eq!( + layout, + PromptLayout { + line_starts: vec![0], + last_line_width: 1, + }, + ); + assert_eq!(trunc, ellipsis()); + } + + #[test] + fn test_compute_layout() { + macro_rules! validate { + ( + ( + $screen_width:expr, + $left_untrunc_prompt:literal, + $right_untrunc_prompt:literal, + $commandline_before_suggestion:literal, + $autosuggestion_str:literal, + $commandline_after_suggestion:literal + ) + -> ( + $left_prompt:literal, + $left_prompt_space:expr, + $right_prompt:literal, + $autosuggestion:literal $(,)? + ) + ) => {{ + let full_commandline = L!($commandline_before_suggestion).to_owned() + + L!($autosuggestion_str) + + L!($commandline_after_suggestion); + let mut colors = vec![HighlightSpec::default(); full_commandline.len()]; + let mut indent = parse_util_compute_indents(&full_commandline); + assert_eq!( + compute_layout( + '…', + $screen_width, + L!($left_untrunc_prompt), + L!($right_untrunc_prompt), + L!($commandline_before_suggestion), + &mut colors, + &mut indent, + L!($autosuggestion_str), + ), + ScreenLayout { + left_prompt: L!($left_prompt).to_owned(), + left_prompt_space: $left_prompt_space, + left_prompt_lines: 1, + right_prompt: L!($right_prompt).to_owned(), + autosuggestion: L!($autosuggestion).to_owned(), + } + ); + indent + }}; + } + + validate!( + ( + 80, "left>", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", + " auto…", + ) + ); + validate!( + ( + 18, "left>", " ( + "left>", + 5, + "", + "s", + ) + ); + validate!( + ( + 18, "left>", " ( + "left>", + 5, + "", + "…", + ) + ); + validate!( + ( + 18, "left>", " ( + "left>", + 5, + "", + "uggestion long so…", + ) + ); + validate!( + ( + 18, "left>", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", " ( + "left>", + 5, + "", + "and …", + ) + ); + validate!( + ( + 18, "left>", " ( + "left>", + 5, + "", + "…", + ) + ); + validate!( + ( + 18, "left>", " ( + "left>", + 5, + "", + "utosuggestion sof…", + ) + ); + } +} diff --git a/src/signal.rs b/src/signal.rs index 4973a97a2..481ee844f 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -565,42 +565,46 @@ fn from(value: Signal) -> Self { } } -// Need to use add_test for wgettext support. +#[cfg(test)] +mod tests { + use super::Signal; + use crate::wchar::prelude::*; -#[test] -fn test_signal_name() { - let sig = Signal::new(libc::SIGINT); - assert_eq!(sig.name(), "SIGINT"); -} - -#[rustfmt::skip] -#[test] -fn test_signal_parse() { - assert_eq!(Signal::parse(L!("SIGHUP")), Some(Signal::new(libc::SIGHUP))); - assert_eq!(Signal::parse(L!("sigwinch")), Some(Signal::new(libc::SIGWINCH))); - assert_eq!(Signal::parse(L!("TSTP")), Some(Signal::new(libc::SIGTSTP))); - assert_eq!(Signal::parse(L!("TstP")), Some(Signal::new(libc::SIGTSTP))); - assert_eq!(Signal::parse(L!("sigCONT")), Some(Signal::new(libc::SIGCONT))); - assert_eq!(Signal::parse(L!("SIGFOO")), None); - assert_eq!(Signal::parse(L!("")), None); - assert_eq!(Signal::parse(L!("SIG")), None); - assert_eq!(Signal::parse(L!("سلام")), None); - - assert_eq!(Signal::parse(&libc::SIGINT.to_wstring()), Some(Signal::new(libc::SIGINT))); - assert_eq!(Signal::parse(L!("0")), None); - assert_eq!(Signal::parse(L!("-0")), None); - assert_eq!(Signal::parse(L!("-1")), None); -} - -#[test] -#[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] -/// Verify bsd feature is detected on the known BSDs, which gives us greater confidence it'll work -/// for the unknown ones too. We don't need to do this for Linux and macOS because we're using -/// rust's native OS targeting for those. -fn bsd_signals() { - assert_eq!(Signal::parse(L!("SIGEMT")), Some(Signal::new(libc::SIGEMT))); - assert_eq!( - Signal::parse(L!("SIGINFO")), - Some(Signal::new(libc::SIGINFO)) - ); + #[test] + fn test_signal_name() { + let sig = Signal::new(libc::SIGINT); + assert_eq!(sig.name(), "SIGINT"); + } + + #[rustfmt::skip] + #[test] + fn test_signal_parse() { + assert_eq!(Signal::parse(L!("SIGHUP")), Some(Signal::new(libc::SIGHUP))); + assert_eq!(Signal::parse(L!("sigwinch")), Some(Signal::new(libc::SIGWINCH))); + assert_eq!(Signal::parse(L!("TSTP")), Some(Signal::new(libc::SIGTSTP))); + assert_eq!(Signal::parse(L!("TstP")), Some(Signal::new(libc::SIGTSTP))); + assert_eq!(Signal::parse(L!("sigCONT")), Some(Signal::new(libc::SIGCONT))); + assert_eq!(Signal::parse(L!("SIGFOO")), None); + assert_eq!(Signal::parse(L!("")), None); + assert_eq!(Signal::parse(L!("SIG")), None); + assert_eq!(Signal::parse(L!("سلام")), None); + + assert_eq!(Signal::parse(&libc::SIGINT.to_wstring()), Some(Signal::new(libc::SIGINT))); + assert_eq!(Signal::parse(L!("0")), None); + assert_eq!(Signal::parse(L!("-0")), None); + assert_eq!(Signal::parse(L!("-1")), None); + } + + #[test] + #[cfg(any(target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] + /// Verify bsd feature is detected on the known BSDs, which gives us greater confidence it'll work + /// for the unknown ones too. We don't need to do this for Linux and macOS because we're using + /// rust's native OS targeting for those. + fn bsd_signals() { + assert_eq!(Signal::parse(L!("SIGEMT")), Some(Signal::new(libc::SIGEMT))); + assert_eq!( + Signal::parse(L!("SIGINFO")), + Some(Signal::new(libc::SIGINFO)) + ); + } } diff --git a/src/stdx.rs b/src/stdx.rs new file mode 100644 index 000000000..4954230f5 --- /dev/null +++ b/src/stdx.rs @@ -0,0 +1,21 @@ +/// This module contains tests that assert the functionality and behavior of the rust standard +/// library, to ensure we can safely use its abstractions to perform low-level operations. +#[cfg(test)] +mod tests { + use std::fs::File; + use std::os::fd::AsRawFd; + + #[test] + fn test_fd_cloexec() { + // Just open a file. Any file. + let file = File::create("test_file_for_fd_cloexec").unwrap(); + let fd = file.as_raw_fd(); + unsafe { + assert_eq!( + libc::fcntl(fd, libc::F_GETFD) & libc::FD_CLOEXEC, + libc::FD_CLOEXEC + ); + } + let _ = std::fs::remove_file("test_file_for_fd_cloexec"); + } +} diff --git a/src/termsize.rs b/src/termsize.rs index ce2afa0bf..07ed57036 100644 --- a/src/termsize.rs +++ b/src/termsize.rs @@ -83,8 +83,7 @@ pub fn defaults() -> Self { } } -/// Exposed for testing. -pub(crate) struct TermsizeData { +struct TermsizeData { // The last termsize returned by TIOCGWINSZ, or none if none. last_from_tty: Option, // The last termsize seen from the environment (COLUMNS/LINES), or none if none. @@ -127,17 +126,14 @@ fn mark_override_from_env(&mut self, ts: Termsize) { /// SIGWINCH. pub struct TermsizeContainer { // Our lock-protected data. - /// Exposed for testing. - pub(crate) data: Mutex, + data: Mutex, // An indication that we are currently in the process of setting COLUMNS and LINES, and so do // not react to any changes. - /// Exposed for testing. - pub(crate) setting_env_vars: AtomicBool, + setting_env_vars: AtomicBool, - /// A function used for accessing the termsize from the tty. This is only exposed for testing. - /// Exposed for testing. - pub(crate) tty_size_reader: fn() -> Option, + /// A function used for accessing the termsize from the tty. + tty_size_reader: fn() -> Option, } impl TermsizeContainer { @@ -207,7 +203,6 @@ fn set_columns_lines_vars(&self, val: Termsize, parser: &Parser) { } /// Note that COLUMNS and/or LINES global variables changed. - /// Exposed for testing. pub(crate) fn handle_columns_lines_var_change(&self, vars: &dyn Environment) { // Do nothing if we are the ones setting it. if self.setting_env_vars.load(Ordering::Relaxed) { @@ -270,3 +265,90 @@ pub fn termsize_update(parser: &Parser) -> Termsize { pub fn termsize_invalidate_tty() { TermsizeContainer::invalidate_tty(); } + +#[cfg(test)] +mod tests { + use crate::env::{EnvMode, Environment}; + use crate::termsize::*; + use crate::tests::prelude::*; + use std::sync::Mutex; + use std::sync::atomic::AtomicBool; + + #[test] + #[serial] + fn test_termsize() { + let _cleanup = test_init(); + let env_global = EnvMode::GLOBAL; + let parser = TestParser::new(); + let vars = parser.vars(); + + // Use a static variable so we can pretend we're the kernel exposing a terminal size. + static STUBBY_TERMSIZE: Mutex> = Mutex::new(None); + fn stubby_termsize() -> Option { + *STUBBY_TERMSIZE.lock().unwrap() + } + let ts = TermsizeContainer { + data: Mutex::new(TermsizeData::defaults()), + setting_env_vars: AtomicBool::new(false), + tty_size_reader: stubby_termsize, + }; + + // Initially default value. + assert_eq!(ts.last(), Termsize::defaults()); + + // Haha we change the value, it doesn't even know. + *STUBBY_TERMSIZE.lock().unwrap() = Some(Termsize { + width: 42, + height: 84, + }); + assert_eq!(ts.last(), Termsize::defaults()); + + // Ok let's tell it. But it still doesn't update right away. + TermsizeContainer::handle_winch(); + assert_eq!(ts.last(), Termsize::defaults()); + + // Ok now we tell it to update. + ts.updating(&parser); + assert_eq!(ts.last(), Termsize::new(42, 84)); + assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "42"); + assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "84"); + + // Wow someone set COLUMNS and LINES to a weird value. + // Now the tty's termsize doesn't matter. + vars.set_one(L!("COLUMNS"), env_global, L!("75").to_owned()); + vars.set_one(L!("LINES"), env_global, L!("150").to_owned()); + ts.handle_columns_lines_var_change(parser.vars()); + assert_eq!(ts.last(), Termsize::new(75, 150)); + assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "75"); + assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "150"); + + vars.set_one(L!("COLUMNS"), env_global, L!("33").to_owned()); + ts.handle_columns_lines_var_change(parser.vars()); + assert_eq!(ts.last(), Termsize::new(33, 150)); + + // Oh it got SIGWINCH, now the tty matters again. + TermsizeContainer::handle_winch(); + assert_eq!(ts.last(), Termsize::new(33, 150)); + assert_eq!(ts.updating(&parser), stubby_termsize().unwrap()); + assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "42"); + assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "84"); + + // Test initialize(). + vars.set_one(L!("COLUMNS"), env_global, L!("83").to_owned()); + vars.set_one(L!("LINES"), env_global, L!("38").to_owned()); + ts.initialize(vars); + assert_eq!(ts.last(), Termsize::new(83, 38)); + + // initialize() even beats the tty reader until a sigwinch. + let ts2 = TermsizeContainer { + data: Mutex::new(TermsizeData::defaults()), + setting_env_vars: AtomicBool::new(false), + tty_size_reader: stubby_termsize, + }; + ts.initialize(parser.vars()); + ts2.updating(&parser); + assert_eq!(ts.last(), Termsize::new(83, 38)); + TermsizeContainer::handle_winch(); + assert_eq!(ts2.updating(&parser), stubby_termsize().unwrap()); + } +} diff --git a/src/tests/abbrs.rs b/src/tests/abbrs.rs deleted file mode 100644 index 7e60e8096..000000000 --- a/src/tests/abbrs.rs +++ /dev/null @@ -1,137 +0,0 @@ -use crate::abbrs::{self, Abbreviation, abbrs_get_set, abbrs_match}; -use crate::editable_line::{Edit, apply_edit}; -use crate::highlight::HighlightSpec; -use crate::reader::reader_expand_abbreviation_at_cursor; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; - -#[test] -#[serial] -fn test_abbreviations() { - let _cleanup = test_init(); - let parser = TestParser::new(); - { - let mut abbrs = abbrs_get_set(); - abbrs.add(Abbreviation::new( - L!("gc").to_owned(), - L!("gc").to_owned(), - L!("git checkout").to_owned(), - abbrs::Position::Command, - false, - )); - abbrs.add(Abbreviation::new( - L!("foo").to_owned(), - L!("foo").to_owned(), - L!("bar").to_owned(), - abbrs::Position::Command, - false, - )); - abbrs.add(Abbreviation::new( - L!("gx").to_owned(), - L!("gx").to_owned(), - L!("git checkout").to_owned(), - abbrs::Position::Command, - false, - )); - abbrs.add(Abbreviation::new( - L!("yin").to_owned(), - L!("yin").to_owned(), - L!("yang").to_owned(), - abbrs::Position::Anywhere, - false, - )); - } - - // Helper to expand an abbreviation, enforcing we have no more than one result. - macro_rules! abbr_expand_1 { - ($token:expr, $position:expr) => { - let result = abbrs_match(L!($token), $position, L!("")); - assert_eq!(result, vec![]); - }; - ($token:expr, $position:expr, $expected:expr) => { - let result = abbrs_match(L!($token), $position, L!("")); - assert_eq!( - result - .into_iter() - .map(|a| a.replacement) - .collect::>(), - vec![L!($expected).to_owned()] - ); - }; - } - - let cmd = abbrs::Position::Command; - abbr_expand_1!("", cmd); - abbr_expand_1!("nothing", cmd); - - abbr_expand_1!("gc", cmd, "git checkout"); - abbr_expand_1!("foo", cmd, "bar"); - - let expand_abbreviation_in_command = - |cmdline: &wstr, cursor_pos: Option| -> Option { - let replacement = reader_expand_abbreviation_at_cursor( - cmdline, - cursor_pos.unwrap_or(cmdline.len()), - &parser, - )?; - let mut cmdline_expanded = cmdline.to_owned(); - let mut colors = vec![HighlightSpec::new(); cmdline.len()]; - apply_edit( - &mut cmdline_expanded, - &mut colors, - &Edit::new(replacement.range.into(), replacement.text), - ); - Some(cmdline_expanded) - }; - - macro_rules! validate { - ($cmdline:expr, $cursor:expr) => {{ - let actual = expand_abbreviation_in_command(L!($cmdline), $cursor); - assert_eq!(actual, None); - }}; - ($cmdline:expr, $cursor:expr, $expected:expr) => {{ - let actual = expand_abbreviation_in_command(L!($cmdline), $cursor); - assert_eq!(actual, Some(L!($expected).to_owned())); - }}; - } - - validate!("just a command", Some(3)); - validate!("gc somebranch", Some(0), "git checkout somebranch"); - - validate!( - "gc somebranch", - Some("gc".chars().count()), - "git checkout somebranch" - ); - - // Space separation. - validate!( - "gx somebranch", - Some("gc".chars().count()), - "git checkout somebranch" - ); - - validate!( - "echo hi ; gc somebranch", - Some("echo hi ; g".chars().count()), - "echo hi ; git checkout somebranch" - ); - - validate!( - "echo (echo (echo (echo (gc ", - Some("echo (echo (echo (echo (gc".chars().count()), - "echo (echo (echo (echo (git checkout " - ); - - // If commands should be expanded. - validate!("if gc", None, "if git checkout"); - - // Others should not be. - validate!("of gc", None); - - // Other decorations generally should be. - validate!("command gc", None, "command git checkout"); - - // yin/yang expands everywhere. - validate!("command yin", None, "command yang"); -} diff --git a/src/tests/ast.rs b/src/tests/ast.rs deleted file mode 100644 index 14d88e30b..000000000 --- a/src/tests/ast.rs +++ /dev/null @@ -1,46 +0,0 @@ -use crate::ast::{self, Node, is_same_node}; -use crate::wchar::prelude::*; - -const FISH_FUNC: &str = r#" -function stuff --description 'Stuff' - set -l log "/tmp/chaos_log.(random)" - set -x PATH /custom/bin $PATH - - echo "[$USER] Hooray" | tee -a $log 2>/dev/null - - time if test (count $argv) -eq 0 - echo "No targets specified" >> $log 2>&1 - return 1 - end - - for target in $argv - command bash -c "echo" >> $log 2> /dev/null - switch $status - case 0 - echo "Success" | tee -a $log - case '*' - echo "Failure" >> $log - end - end - set_color green -end -"#; - -#[test] -fn test_is_same_node() { - // is_same_node is pretty subtle! Let's check it. - let src = WString::from_str(FISH_FUNC); - let ast = ast::parse(&src, Default::default(), None); - assert!(!ast.errored()); - let all_nodes: Vec<&dyn Node> = ast.walk().collect(); - for i in 0..all_nodes.len() { - for j in 0..all_nodes.len() { - let same = is_same_node(all_nodes[i], all_nodes[j]); - if i == j { - assert!(same, "Node {} should be the same as itself", i); - } else { - assert!(!same, "Node {} should not be the same as node {}", i, j); - } - } - } -} diff --git a/src/tests/ast_bench.rs b/src/tests/ast_bench.rs deleted file mode 100644 index a81822bb6..000000000 --- a/src/tests/ast_bench.rs +++ /dev/null @@ -1,45 +0,0 @@ -// Run with cargo +nightly bench --features=benchmark -#[cfg(feature = "benchmark")] -mod bench { - extern crate test; - use crate::ast; - use crate::wchar::prelude::*; - use test::Bencher; - - // Return a long string suitable for benchmarking. - fn generate_fish_script() -> WString { - let mut buff = WString::new(); - let s = &mut buff; - - for i in 0..1000 { - // command with args and redirections - sprintf!(=> s, - "echo arg%d arg%d > out%d.txt 2> err%d.txt\n", - i, i + 1, i, i - ); - - // simple block - sprintf!(=> s, "begin\n echo inside block %d\nend\n", i ); - - // conditional - sprintf!(=> s, "if test %d\n echo even\nelse\n echo odd\nend\n", i % 2); - - // loop - sprintf!(=> s, "for x in a b c\n echo $x %d\nend\n", i); - - // pipeline - sprintf!(=> s, "echo foo%d | grep f | wc -l\n", i); - } - - buff - } - - #[bench] - fn bench_ast_construction(b: &mut Bencher) { - let src = generate_fish_script(); - b.bytes = (src.len() * 4) as u64; // 4 bytes per character - b.iter(|| { - let _ast = ast::parse(&src, Default::default(), None); - }); - } -} diff --git a/src/tests/common.rs b/src/tests/common.rs deleted file mode 100644 index 78661aeb3..000000000 --- a/src/tests/common.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::common::{ScopeGuard, ScopedCell, ScopedRefCell, truncate_at_nul}; -use crate::wchar::prelude::*; - -#[test] -fn test_scoped_cell() { - let cell = ScopedCell::new(42); - - { - let _guard = cell.scoped_mod(|x| *x += 1); - assert_eq!(cell.get(), 43); - } - - assert_eq!(cell.get(), 42); -} - -#[test] -fn test_scoped_refcell() { - #[derive(Debug, PartialEq, Clone)] - struct Data { - x: i32, - y: i32, - } - - let cell = ScopedRefCell::new(Data { x: 1, y: 2 }); - - { - let _guard = cell.scoped_set(10, |d| &mut d.x); - assert_eq!(cell.borrow().x, 10); - } - assert_eq!(cell.borrow().x, 1); - - { - let _guard = cell.scoped_replace(Data { x: 42, y: 99 }); - assert_eq!(*cell.borrow(), Data { x: 42, y: 99 }); - } - assert_eq!(*cell.borrow(), Data { x: 1, y: 2 }); -} - -#[test] -fn test_scope_guard() { - let relaxed = std::sync::atomic::Ordering::Relaxed; - let counter = std::sync::atomic::AtomicUsize::new(0); - { - let guard = ScopeGuard::new(123, |arg| { - assert_eq!(arg, 123); - counter.fetch_add(1, relaxed); - }); - assert_eq!(counter.load(relaxed), 0); - std::mem::drop(guard); - assert_eq!(counter.load(relaxed), 1); - } - // commit also invokes the callback. - { - let guard = ScopeGuard::new(123, |arg| { - assert_eq!(arg, 123); - counter.fetch_add(1, relaxed); - }); - assert_eq!(counter.load(relaxed), 1); - ScopeGuard::commit(guard); - assert_eq!(counter.load(relaxed), 2); - } -} - -#[test] -fn test_truncate_at_nul() { - assert_eq!(truncate_at_nul(L!("abc\0def")), L!("abc")); - assert_eq!(truncate_at_nul(L!("abc")), L!("abc")); - assert_eq!(truncate_at_nul(L!("\0abc")), L!("")); -} diff --git a/src/tests/complete.rs b/src/tests/complete.rs deleted file mode 100644 index 15602d68a..000000000 --- a/src/tests/complete.rs +++ /dev/null @@ -1,683 +0,0 @@ -use crate::abbrs::{self, Abbreviation, with_abbrs_mut}; -use crate::complete::{ - CompleteFlags, CompleteOptionType, CompletionMode, CompletionRequestOptions, complete, - complete_add, complete_add_wrapper, complete_get_wrap_targets, complete_remove_wrapper, - sort_and_prioritize, -}; -use crate::env::{EnvMode, Environment}; -use crate::io::IoChain; -use crate::operation_context::{ - EXPANSION_LIMIT_BACKGROUND, EXPANSION_LIMIT_DEFAULT, OperationContext, no_cancel, -}; -use crate::reader::completion_apply_to_command_line; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; -use crate::wcstringutil::join_strings; -use std::collections::HashMap; -use std::ffi::CString; - -/// Joins a std::vector via commas. -fn comma_join(lst: Vec) -> WString { - join_strings(&lst, ',') -} - -#[test] -#[serial] -fn test_complete() { - let _cleanup = test_init(); - let vars = PwdEnvironment { - parent: TestEnvironment { - vars: HashMap::from([ - (WString::from_str("Foo1"), WString::new()), - (WString::from_str("Foo2"), WString::new()), - (WString::from_str("Foo3"), WString::new()), - (WString::from_str("Bar1"), WString::new()), - (WString::from_str("Bar2"), WString::new()), - (WString::from_str("Bar3"), WString::new()), - (WString::from_str("alpha"), WString::new()), - (WString::from_str("ALPHA!"), WString::new()), - (WString::from_str("gamma1"), WString::new()), - (WString::from_str("GAMMA2"), WString::new()), - (WString::from_str("SOMEDIR"), L!("/").to_owned()), - (WString::from_str("SOMEVAR"), WString::new()), - ]), - }, - }; - - let parser = TestParser::new(); - let ctx = OperationContext::test_only_foreground(&parser, &vars, Box::new(no_cancel)); - - let do_complete = |cmd: &wstr, flags: CompletionRequestOptions| complete(cmd, flags, &ctx).0; - - let mut completions = do_complete(L!("$"), CompletionRequestOptions::default()); - sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); - assert_eq!( - completions - .into_iter() - .map(|c| c.completion.to_string()) - .collect::>(), - [ - "alpha", "ALPHA!", "Bar1", "Bar2", "Bar3", "Foo1", "Foo2", "Foo3", "gamma1", "GAMMA2", - "PWD", "SOMEDIR", "SOMEVAR", - ] - .into_iter() - .map(|s| s.to_owned()) - .collect::>() - ); - - // Smartcase test. Lowercase inputs match both lowercase and uppercase. - let mut completions = do_complete(L!("$a"), CompletionRequestOptions::default()); - sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); - - assert_eq!(completions.len(), 2); - assert_eq!(completions[0].completion, L!("$ALPHA!")); - assert_eq!(completions[1].completion, L!("lpha")); - - let mut completions = do_complete(L!("$F"), CompletionRequestOptions::default()); - sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); - assert_eq!(completions.len(), 3); - assert_eq!(completions[0].completion, L!("oo1")); - assert_eq!(completions[1].completion, L!("oo2")); - assert_eq!(completions[2].completion, L!("oo3")); - - completions = do_complete(L!("$1"), CompletionRequestOptions::default()); - sort_and_prioritize(&mut completions, CompletionRequestOptions::default()); - assert_eq!(completions, vec![]); - - let fuzzy_options = CompletionRequestOptions { - fuzzy_match: true, - ..Default::default() - }; - let mut completions = do_complete(L!("$1"), fuzzy_options); - sort_and_prioritize(&mut completions, fuzzy_options); - assert_eq!(completions.len(), 3); - assert_eq!(completions[0].completion, L!("$Bar1")); - assert_eq!(completions[1].completion, L!("$Foo1")); - assert_eq!(completions[2].completion, L!("$gamma1")); - - std::fs::create_dir_all("test/complete_test").unwrap(); - std::fs::write("test/complete_test/has space", []).unwrap(); - std::fs::write("test/complete_test/bracket[abc]", []).unwrap(); - #[cfg(not(cygwin))] - // Backslashes and colons are not legal filename characters on WIN32/CYGWIN - { - std::fs::write(r"test/complete_test/gnarlybracket\[abc]", []).unwrap(); - std::fs::write(r"test/complete_test/colon:abc", []).unwrap(); - } - std::fs::write(r"test/complete_test/equal=abc", []).unwrap(); - // On MSYS, the executable bit cannot be set manually, is set automatically - // based on the file content/type. So make it a shell script - std::fs::write("test/complete_test/testfile", "#!/bin/sh").unwrap(); - let testfile = CString::new("test/complete_test/testfile").unwrap(); - assert_eq!(unsafe { libc::chmod(testfile.as_ptr(), 0o700,) }, 0); - std::fs::create_dir_all("test/complete_test/foo1").unwrap(); - std::fs::create_dir_all("test/complete_test/foo2").unwrap(); - std::fs::create_dir_all("test/complete_test/foo3").unwrap(); - - completions = do_complete( - L!("echo (test/complete_test/testfil"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("e")); - - completions = do_complete( - L!("echo (ls test/complete_test/testfil"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("e")); - - completions = do_complete( - L!("echo (command ls test/complete_test/testfil"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("e")); - - // Completing after spaces - see #2447 - completions = do_complete( - L!("echo (ls test/complete_test/has\\ "), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("space")); - - #[cfg(not(cygwin))] - // Backslashes and colons are not legal filename characters on WIN32/CYGWIN - { - completions = do_complete( - L!(": test/complete_test/colon:"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("abc")); - } - - macro_rules! unique_completion_applies_as { - ( $cmdline:expr, $completion_result:expr, $applied:expr $(,)? ) => { - let cmdline = L!($cmdline); - let completions = do_complete(cmdline, CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!( - completions[0].completion, - L!($completion_result), - "completion mismatch" - ); - let mut cursor = cmdline.len(); - let newcmdline = completion_apply_to_command_line( - &ctx, - &completions[0].completion, - completions[0].flags, - cmdline, - &mut cursor, - /*append_only=*/ false, - /*is_unique=*/ true, - ); - assert_eq!(newcmdline, L!($applied), "apply result mismatch"); - }; - } - - unique_completion_applies_as!( - "touch test/complete_test/{testfi", - r"le", - "touch test/complete_test/{testfile", - ); - - // Brackets - see #5831 - unique_completion_applies_as!( - "touch test/complete_test/bracket[", - "test/complete_test/bracket[abc]", - "touch 'test/complete_test/bracket[abc]' ", - ); - unique_completion_applies_as!( - "echo (ls test/complete_test/bracket[", - "test/complete_test/bracket[abc]", - "echo (ls 'test/complete_test/bracket[abc]' ", - ); - #[cfg(not(cygwin))] - // Backslashes are not legal filename characters on WIN32/CYGWIN - { - unique_completion_applies_as!( - r"touch test/complete_test/gnarlybracket\\[", - r"test/complete_test/gnarlybracket\[abc]", - r"touch 'test/complete_test/gnarlybracket\\[abc]' ", - ); - unique_completion_applies_as!( - r"a=test/complete_test/bracket[", - r"test/complete_test/bracket[abc]", - r"a='test/complete_test/bracket[abc]' ", - ); - } - - #[cfg(not(cygwin))] - // Colons are not legal filename characters on WIN32/CYGWIN - { - unique_completion_applies_as!( - r"touch test/complete_test/colon", - r":abc", - r"touch test/complete_test/colon:abc ", - ); - unique_completion_applies_as!( - r"touch test/complete_test/colon:", - r"abc", - r"touch test/complete_test/colon:abc ", - ); - unique_completion_applies_as!( - r#"touch "test/complete_test/colon:"#, - r"abc", - r#"touch "test/complete_test/colon:abc" "#, - ); - } - - unique_completion_applies_as!("echo $SOMEV", r"AR", "echo $SOMEVAR "); - unique_completion_applies_as!("echo $SOMED", r"IR", "echo $SOMEDIR/"); - unique_completion_applies_as!(r#"echo "$SOMED"#, r"IR", r#"echo "$SOMEDIR/"#); - - // #8820 - let mut cursor_pos = 11; - let newcmdline = completion_apply_to_command_line( - &ctx, - L!("Debug/"), - CompleteFlags::REPLACES_TOKEN | CompleteFlags::NO_SPACE, - L!("mv debug debug"), - &mut cursor_pos, - true, - /*is_unique=*/ false, - ); - assert_eq!(newcmdline, L!("mv debug Debug/")); - - // Add a function and test completing it in various ways. - parser.eval(L!("function scuttlebutt; end"), &IoChain::new()); - - // Complete a function name. - completions = do_complete(L!("echo (scuttlebut"), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("t")); - - // But not with the command prefix. - completions = do_complete( - L!("echo (command scuttlebut"), - CompletionRequestOptions::default(), - ); - assert_eq!(&completions, &[]); - - // Not with the builtin prefix. - let completions = do_complete( - L!("echo (builtin scuttlebut"), - CompletionRequestOptions::default(), - ); - assert_eq!(&completions, &[]); - - // Not after a redirection. - let completions = do_complete( - L!("echo hi > scuttlebut"), - CompletionRequestOptions::default(), - ); - assert_eq!(&completions, &[]); - - // Trailing spaces (#1261). - let no_files = CompletionMode { - no_files: true, - ..Default::default() - }; - complete_add( - L!("foobarbaz").into(), - false, - WString::new(), - CompleteOptionType::ArgsOnly, - no_files, - vec![], - L!("qux").into(), - WString::new(), - CompleteFlags::AUTO_SPACE, - ); - let completions = do_complete(L!("foobarbaz "), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("qux")); - - // Don't complete variable names in single quotes (#1023). - let completions = do_complete(L!("echo '$Foo"), CompletionRequestOptions::default()); - assert_eq!(completions, vec![]); - let completions = do_complete(L!("echo \\$Foo"), CompletionRequestOptions::default()); - assert_eq!(completions, vec![]); - - // File completions. - let completions = do_complete( - L!("cat test/complete_test/te"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - let completions = do_complete( - L!("echo sup > test/complete_test/te"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - let completions = do_complete( - L!("echo sup > test/complete_test/te"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - - parser.pushd("test/complete_test"); - let completions = do_complete(L!("cat te"), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - assert!(!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN))); - assert!( - !(completions[0] - .flags - .contains(CompleteFlags::DUPLICATES_ARGUMENT)) - ); - let completions = do_complete(L!("cat testfile te"), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - assert!( - completions[0] - .flags - .contains(CompleteFlags::DUPLICATES_ARGUMENT) - ); - let completions = do_complete(L!("cat testfile TE"), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("testfile")); - assert!(completions[0].flags.contains(CompleteFlags::REPLACES_TOKEN)); - assert!( - completions[0] - .flags - .contains(CompleteFlags::DUPLICATES_ARGUMENT) - ); - let completions = do_complete( - L!("something --abc=te"), - CompletionRequestOptions::default(), - ); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - let completions = do_complete(L!("something -abc=te"), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - let completions = do_complete(L!("something abc=te"), CompletionRequestOptions::default()); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("stfile")); - let completions = do_complete( - L!("something abc=stfile"), - CompletionRequestOptions::default(), - ); - assert_eq!(&completions, &[]); - let completions = do_complete(L!("something abc=stfile"), fuzzy_options); - assert_eq!(completions.len(), 1); - assert_eq!(completions[0].completion, L!("abc=testfile")); - - // Zero escapes can cause problems. See issue #1631. - let completions = do_complete(L!("cat foo\\0"), CompletionRequestOptions::default()); - assert_eq!(&completions, &[]); - let completions = do_complete(L!("cat foo\\0bar"), CompletionRequestOptions::default()); - assert_eq!(&completions, &[]); - let completions = do_complete(L!("cat \\0"), CompletionRequestOptions::default()); - assert_eq!(&completions, &[]); - let mut completions = do_complete(L!("cat te\\0"), CompletionRequestOptions::default()); - assert_eq!(&completions, &[]); - - parser.popd(); - completions.clear(); - - // Test abbreviations. - parser.eval( - L!("function testabbrsonetwothreefour; end"), - &IoChain::new(), - ); - with_abbrs_mut(|abbrset| { - abbrset.add(Abbreviation::new( - L!("somename").into(), - L!("testabbrsonetwothreezero").into(), - L!("expansion").into(), - abbrs::Position::Command, - false, - )) - }); - - let completions = complete( - L!("testabbrsonetwothree"), - CompletionRequestOptions::default(), - &parser.context(), - ) - .0; - assert_eq!(completions.len(), 2); - // Abbreviations should not have a space after them. - assert_eq!(completions[0].completion, L!("zero")); - assert!(completions[0].flags.contains(CompleteFlags::NO_SPACE)); - with_abbrs_mut(|abbrset| { - abbrset.erase(L!("testabbrsonetwothreezero")); - }); - assert_eq!(completions[1].completion, L!("four")); - assert!(!completions[1].flags.contains(CompleteFlags::NO_SPACE)); - - // Test wraps. - assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty()); - complete_add_wrapper(L!("wrapper1").into(), L!("wrapper2").into()); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper1"))), - L!("wrapper2") - ); - complete_add_wrapper(L!("wrapper2").into(), L!("wrapper3").into()); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper1"))), - L!("wrapper2") - ); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper2"))), - L!("wrapper3") - ); - complete_add_wrapper(L!("wrapper3").into(), L!("wrapper1").into()); // loop! - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper1"))), - L!("wrapper2") - ); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper2"))), - L!("wrapper3") - ); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper3"))), - L!("wrapper1") - ); - complete_remove_wrapper(L!("wrapper1").into(), L!("wrapper2")); - assert!(comma_join(complete_get_wrap_targets(L!("wrapper1"))).is_empty()); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper2"))), - L!("wrapper3") - ); - assert_eq!( - comma_join(complete_get_wrap_targets(L!("wrapper3"))), - L!("wrapper1") - ); - - // Test cd wrapping chain - parser.pushd("test/complete_test"); - - complete_add_wrapper(L!("cdwrap1").into(), L!("cd").into()); - complete_add_wrapper(L!("cdwrap2").into(), L!("cdwrap1").into()); - - let mut cd_compl = do_complete(L!("cd "), CompletionRequestOptions::default()); - sort_and_prioritize(&mut cd_compl, CompletionRequestOptions::default()); - - let mut cdwrap1_compl = do_complete(L!("cdwrap1 "), CompletionRequestOptions::default()); - sort_and_prioritize(&mut cdwrap1_compl, CompletionRequestOptions::default()); - - let mut cdwrap2_compl = do_complete(L!("cdwrap2 "), CompletionRequestOptions::default()); - sort_and_prioritize(&mut cdwrap2_compl, CompletionRequestOptions::default()); - - let min_compl_size = cd_compl - .len() - .min(cdwrap1_compl.len().min(cdwrap2_compl.len())); - - assert_eq!(cd_compl.len(), min_compl_size); - assert_eq!(cdwrap1_compl.len(), min_compl_size); - assert_eq!(cdwrap2_compl.len(), min_compl_size); - for i in 0..min_compl_size { - assert_eq!(cd_compl[i].completion, cdwrap1_compl[i].completion); - assert_eq!(cdwrap1_compl[i].completion, cdwrap2_compl[i].completion); - } - - complete_remove_wrapper(L!("cdwrap1").into(), L!("cd")); - complete_remove_wrapper(L!("cdwrap2").into(), L!("cdwrap1")); - parser.popd(); -} - -// Testing test_autosuggest_suggest_special, in particular for properly handling quotes and -// backslashes. -#[test] -#[serial] -fn test_autosuggest_suggest_special() { - let _cleanup = test_init(); - let parser = TestParser::new(); - macro_rules! perform_one_autosuggestion_cd_test { - ($command:literal, $expected:literal, $vars:expr) => { - let mut comps = complete( - L!($command), - CompletionRequestOptions::autosuggest(), - &OperationContext::background($vars, EXPANSION_LIMIT_BACKGROUND), - ) - .0; - - let expects_error = $expected == ""; - - assert_eq!(expects_error, comps.is_empty()); - if !expects_error { - sort_and_prioritize(&mut comps, CompletionRequestOptions::default()); - let suggestion = &comps[0]; - assert_eq!(suggestion.completion, L!($expected)); - } - }; - } - - macro_rules! perform_one_completion_cd_test { - ($command:literal, $expected:literal) => { - let mut comps = complete( - L!($command), - CompletionRequestOptions::default(), - &OperationContext::foreground( - &parser, - Box::new(no_cancel), - EXPANSION_LIMIT_DEFAULT, - ), - ) - .0; - - let expects_error = $expected == ""; - - assert_eq!(expects_error, comps.is_empty()); - if !expects_error { - sort_and_prioritize(&mut comps, CompletionRequestOptions::default()); - let suggestion = &comps[0]; - assert_eq!(suggestion.completion, L!($expected)); - } - }; - } - - std::fs::create_dir_all("test/autosuggest_test/0foobar").unwrap(); - std::fs::create_dir_all("test/autosuggest_test/1foo bar").unwrap(); - std::fs::create_dir_all("test/autosuggest_test/2foo bar").unwrap(); - // Cygwin disallows backslashes in filenames. - #[cfg(not(cygwin))] - std::fs::create_dir_all("test/autosuggest_test/3foo\\bar").unwrap(); - // a path with a single quote - std::fs::create_dir_all("test/autosuggest_test/4foo'bar").unwrap(); - // a path with a double quote - std::fs::create_dir_all("test/autosuggest_test/5foo\"bar").unwrap(); - // This is to ensure tilde expansion is handled. See the `cd ~/test_autosuggest_suggest_specia` - // test below. - // Fake out the home directory - parser.vars().set_one( - L!("HOME"), - EnvMode::LOCAL | EnvMode::EXPORT, - L!("test/test-home").to_owned(), - ); - std::fs::create_dir_all("test/test-home/test_autosuggest_suggest_special/").unwrap(); - std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi4").unwrap(); - std::fs::create_dir_all("test/autosuggest_test/start/unique2/unique3/multi42").unwrap(); - std::fs::create_dir_all("test/autosuggest_test/start/unique2/.hiddenDir/moreStuff").unwrap(); - - // Ensure symlink don't cause us to chase endlessly. - // Symbolic link is complicated on Windows/Cygwin (see winsymlinks). The behavior - // depends on the env var CYGWIN (or MSYS). Currently, the default is to copy - // the target, which will fail with recursive symlinks - #[cfg(not(cygwin))] - { - std::fs::create_dir_all("test/autosuggest_test/has_loop/loopy").unwrap(); - let _ = std::fs::remove_file("test/autosuggest_test/has_loop/loopy/loop"); - std::os::unix::fs::symlink("../loopy", "test/autosuggest_test/has_loop/loopy/loop") - .unwrap(); - } - - let wd = "test/autosuggest_test"; - - let mut vars = PwdEnvironment::default(); - vars.parent.vars.insert( - L!("HOME").into(), - parser.vars().get(L!("HOME")).unwrap().as_string(), - ); - - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/1", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/1", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/1", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/2", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/2", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/2", "foo bar/", &vars); - #[cfg(not(cygwin))] - // Windows does not allow backslashes in filenames - { - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/3", "foo\\bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/3", "foo\\bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/3", "foo\\bar/", &vars); - } - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/4", "foo'bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/4", "foo'bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/4", "foo'bar/", &vars); - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/5", "foo\"bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"test/autosuggest_test/5", "foo\"bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 'test/autosuggest_test/5", "foo\"bar/", &vars); - - vars.parent - .vars - .insert(L!("AUTOSUGGEST_TEST_LOC").to_owned(), WString::from_str(wd)); - perform_one_autosuggestion_cd_test!("cd $AUTOSUGGEST_TEST_LOC/0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd ~/test_autosuggest_suggest_specia", "l/", &vars); - - perform_one_autosuggestion_cd_test!( - "cd test/autosuggest_test/start/", - "unique2/unique3/", - &vars - ); - - #[cfg(not(cygwin))] - // We skipped the creation of `loopy/loop` above - perform_one_autosuggestion_cd_test!("cd test/autosuggest_test/has_loop/", "loopy/loop/", &vars); - - parser.pushd(wd); - perform_one_autosuggestion_cd_test!("cd 0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd '0", "foobar/", &vars); - perform_one_autosuggestion_cd_test!("cd 1", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"1", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd '1", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 2", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"2", "foo bar/", &vars); - perform_one_autosuggestion_cd_test!("cd '2", "foo bar/", &vars); - #[cfg(not(cygwin))] - // Windows does not allow backslashes in filenames - { - perform_one_autosuggestion_cd_test!("cd 3", "foo\\bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"3", "foo\\bar/", &vars); - perform_one_autosuggestion_cd_test!("cd '3", "foo\\bar/", &vars); - } - perform_one_autosuggestion_cd_test!("cd 4", "foo'bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"4", "foo'bar/", &vars); - perform_one_autosuggestion_cd_test!("cd '4", "foo'bar/", &vars); - perform_one_autosuggestion_cd_test!("cd 5", "foo\"bar/", &vars); - perform_one_autosuggestion_cd_test!("cd \"5", "foo\"bar/", &vars); - perform_one_autosuggestion_cd_test!("cd '5", "foo\"bar/", &vars); - - // A single quote should defeat tilde expansion. - perform_one_autosuggestion_cd_test!("cd '~/test_autosuggest_suggest_specia'", "", &vars); - - // Don't crash on ~ (issue #2696). Note this is cwd dependent. - std::fs::create_dir_all("~absolutelynosuchuser/path1/path2/").unwrap(); - perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchus", "er/path1/path2/", &vars); - perform_one_autosuggestion_cd_test!("cd ~absolutelynosuchuser/", "path1/path2/", &vars); - perform_one_completion_cd_test!("cd ~absolutelynosuchus", "er/"); - perform_one_completion_cd_test!("cd ~absolutelynosuchuser/", "path1/"); - - parser - .vars() - .remove(L!("HOME"), EnvMode::LOCAL | EnvMode::EXPORT); - parser.popd(); -} - -#[test] -#[serial] -fn test_autosuggestion_ignores() { - let _cleanup = test_init(); - // Testing scenarios that should produce no autosuggestions - macro_rules! perform_one_autosuggestion_should_ignore_test { - ($command:literal) => { - let comps = complete( - L!($command), - CompletionRequestOptions::autosuggest(), - &OperationContext::empty(), - ) - .0; - assert_eq!(&comps, &[]); - }; - } - // Do not do file autosuggestions immediately after certain statement terminators - see #1631. - perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST|"); - perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST&"); - perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST#comment"); - perform_one_autosuggestion_should_ignore_test!("echo PIPE_TEST;"); -} diff --git a/src/tests/debounce.rs b/src/tests/debounce.rs deleted file mode 100644 index 3b21a7a8e..000000000 --- a/src/tests/debounce.rs +++ /dev/null @@ -1,131 +0,0 @@ -use std::sync::{ - Arc, Condvar, Mutex, - atomic::{AtomicU32, Ordering}, -}; -use std::time::Duration; - -use crate::global_safety::RelaxedAtomicBool; -use crate::reader::{Reader, fake_scoped_reader}; -use crate::tests::prelude::*; -use crate::threads::{Debounce, iothread_drain_all, iothread_service_main}; - -#[test] -#[serial] -fn test_debounce() { - let _cleanup = test_init(); - let parser = TestParser::new(); - // Run 8 functions using a condition variable. - // Only the first and last should run. - let db = Debounce::new(Duration::from_secs(0)); - const count: usize = 8; - - struct Context { - handler_ran: [RelaxedAtomicBool; count], - completion_ran: [RelaxedAtomicBool; count], - ready_to_go: Mutex, - cv: Condvar, - } - - let ctx = Arc::new(Context { - handler_ran: std::array::from_fn(|_i| RelaxedAtomicBool::new(false)), - completion_ran: std::array::from_fn(|_i| RelaxedAtomicBool::new(false)), - ready_to_go: Mutex::new(false), - cv: Condvar::new(), - }); - - // "Enqueue" all functions. Each one waits until ready_to_go. - for idx in 0..count { - assert!(!ctx.handler_ran[idx].load()); - let performer = { - let ctx = ctx.clone(); - move || { - let guard = ctx.ready_to_go.lock().unwrap(); - let _guard = ctx.cv.wait_while(guard, |ready| !*ready).unwrap(); - ctx.handler_ran[idx].store(true); - idx - } - }; - let completer = { - let ctx = ctx.clone(); - move |_ctx: &mut Reader, idx: usize| { - ctx.completion_ran[idx].store(true); - } - }; - db.perform_with_completion(performer, completer); - } - - // We're ready to go. - *ctx.ready_to_go.lock().unwrap() = true; - ctx.cv.notify_all(); - - // Wait until the last completion is done. - let mut reader = fake_scoped_reader(&parser); - while !ctx.completion_ran.last().unwrap().load() { - iothread_service_main(&mut reader); - } - iothread_drain_all(&mut reader); - - // Each perform() call may displace an existing queued operation. - // Each operation waits until all are queued. - // Therefore we expect the last perform() to have run, and at most one more. - assert!(ctx.handler_ran.last().unwrap().load()); - assert!(ctx.completion_ran.last().unwrap().load()); - - let mut total_ran = 0; - for idx in 0..count { - if ctx.handler_ran[idx].load() { - total_ran += 1; - } - assert_eq!(ctx.handler_ran[idx].load(), ctx.completion_ran[idx].load()); - } - assert!(total_ran <= 2); -} - -#[test] -#[serial] -fn test_debounce_timeout() { - let _cleanup = test_init(); - // Verify that debounce doesn't wait forever. - // Use a shared_ptr so we don't have to join our threads. - let timeout = Duration::from_millis(500); - - struct Data { - db: Debounce, - exit_ok: Mutex, - cv: Condvar, - running: AtomicU32, - } - - let data = Arc::new(Data { - db: Debounce::new(timeout), - exit_ok: Mutex::new(false), - cv: Condvar::new(), - running: AtomicU32::new(0), - }); - - // Our background handler. Note this just blocks until exit_ok is set. - let handler = { - let data = data.clone(); - move || { - data.running.fetch_add(1, Ordering::Relaxed); - let guard = data.exit_ok.lock().unwrap(); - let _guard = data.cv.wait_while(guard, |exit_ok| !*exit_ok); - } - }; - - // Spawn the handler twice. This should not modify the thread token. - let token1 = data.db.perform(handler.clone()); - let token2 = data.db.perform(handler.clone()); - assert_eq!(token1, token2); - - // Wait 75 msec, then enqueue something else; this should spawn a new thread. - std::thread::sleep(timeout + timeout / 2); - assert!(data.running.load(Ordering::Relaxed) == 1); - let token3 = data.db.perform(handler); - assert!(token3 > token2); - - // Release all the threads. - let mut exit_ok = data.exit_ok.lock().unwrap(); - *exit_ok = true; - data.cv.notify_all(); -} diff --git a/src/tests/editable_line.rs b/src/tests/editable_line.rs deleted file mode 100644 index 6f4df30bb..000000000 --- a/src/tests/editable_line.rs +++ /dev/null @@ -1,73 +0,0 @@ -use crate::{ - editable_line::{Edit, EditableLine}, - wchar::prelude::*, -}; - -#[test] -fn test_undo() { - let mut line = EditableLine::default(); - - let insert = |line: &EditableLine| line.position()..line.position(); - - assert!(!line.undo()); // nothing to undo - assert!(line.text().is_empty()); - assert_eq!(line.position(), 0); - line.push_edit(Edit::new(0..0, L!("a b c").to_owned()), true); - assert_eq!(line.text(), L!("a b c").to_owned()); - assert_eq!(line.position(), 5); - line.set_position(2); - line.push_edit(Edit::new(2..3, L!("B").to_owned()), true); // replacement right of cursor - assert_eq!(line.text(), L!("a B c").to_owned()); - line.undo(); - assert_eq!(line.text(), L!("a b c").to_owned()); - assert_eq!(line.position(), 2); - line.redo(); - assert_eq!(line.text(), L!("a B c").to_owned()); - assert_eq!(line.position(), 3); - - assert!(!line.redo()); // nothing to redo - - line.push_edit(Edit::new(0..2, L!("").to_owned()), true); // deletion left of cursor - assert_eq!(line.text(), L!("B c").to_owned()); - assert_eq!(line.position(), 1); - line.undo(); - assert_eq!(line.text(), L!("a B c").to_owned()); - assert_eq!(line.position(), 3); - line.redo(); - assert_eq!(line.text(), L!("B c").to_owned()); - assert_eq!(line.position(), 1); - - line.push_edit(Edit::new(0..line.len(), L!("a b c").to_owned()), true); // replacement left and right of cursor - assert_eq!(line.text(), L!("a b c").to_owned()); - assert_eq!(line.position(), 5); - - // Undo coalesced edits - line.push_edit(Edit::new(0..line.len(), L!("").to_owned()), false); - line.push_edit(Edit::new(insert(&line), L!("a").to_owned()), true); - line.push_edit(Edit::new(insert(&line), L!("b").to_owned()), true); - line.push_edit(Edit::new(insert(&line), L!("c").to_owned()), true); - line.push_edit(Edit::new(insert(&line), L!(" ").to_owned()), true); - line.undo(); - line.undo(); - line.redo(); - assert_eq!(line.text(), L!("abc").to_owned()); - // This removes the space insertion from the history, but does not coalesce with the first edit. - line.push_edit(Edit::new(insert(&line), L!("d").to_owned()), true); - line.push_edit(Edit::new(insert(&line), L!("e").to_owned()), true); - assert_eq!(line.text(), L!("abcde").to_owned()); - line.undo(); - assert_eq!(line.text(), L!("abc").to_owned()); -} - -#[test] -fn test_undo_group() { - let mut line = EditableLine::default(); - line.begin_edit_group(); - line.push_edit(Edit::new(0..0, L!("a").to_owned()), true); - line.end_edit_group(); - line.begin_edit_group(); - line.push_edit(Edit::new(1..1, L!("b").to_owned()), true); - line.end_edit_group(); - line.undo(); - assert_eq!(line.text(), "a"); -} diff --git a/src/tests/encoding.rs b/src/tests/encoding.rs deleted file mode 100644 index 0bfc91246..000000000 --- a/src/tests/encoding.rs +++ /dev/null @@ -1,33 +0,0 @@ -use crate::common::{bytes2wcstring, wcs2bytes}; -use crate::wchar::prelude::*; - -/// Verify correct behavior with embedded nulls. -#[test] -fn test_convert_nulls() { - let input = L!("AAA\0BBB"); - let out_str = wcs2bytes(input); - assert_eq!( - input.chars().collect::>(), - std::str::from_utf8(&out_str) - .unwrap() - .chars() - .collect::>() - ); - - let out_wstr = bytes2wcstring(&out_str); - assert_eq!(input, &out_wstr); -} - -#[cfg(feature = "benchmark")] -mod bench { - extern crate test; - use crate::tests::encoding::bytes2wcstring; - use test::Bencher; - - #[bench] - fn bench_convert_ascii(b: &mut Bencher) { - let s: [u8; 128 * 1024] = std::array::from_fn(|i| b'0' + u8::try_from(i % 10).unwrap()); - b.bytes = u64::try_from(s.len()).unwrap(); - b.iter(|| bytes2wcstring(&s)); - } -} diff --git a/src/tests/env.rs b/src/tests/env.rs deleted file mode 100644 index cd08e4e98..000000000 --- a/src/tests/env.rs +++ /dev/null @@ -1,194 +0,0 @@ -use crate::env::{EnvMode, EnvStack, EnvVar, EnvVarFlags, Environment}; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; -use crate::wutil::wgetcwd; -use std::collections::HashMap; -use std::mem::MaybeUninit; -use std::time::{SystemTime, UNIX_EPOCH}; - -/// An environment built around an std::map. -#[derive(Clone, Default)] -pub struct TestEnvironment { - pub vars: HashMap, -} -impl TestEnvironment { - #[allow(dead_code)] - pub fn new() -> Self { - Self::default() - } -} -impl Environment for TestEnvironment { - fn getf(&self, name: &wstr, _mode: EnvMode) -> Option { - self.vars - .get(name) - .map(|value| EnvVar::new(value.clone(), EnvVarFlags::default())) - } - fn get_names(&self, _flags: EnvMode) -> Vec { - self.vars.keys().cloned().collect() - } -} - -/// A test environment that knows about PWD. -#[derive(Default)] -pub struct PwdEnvironment { - pub parent: TestEnvironment, -} -impl PwdEnvironment { - #[allow(dead_code)] - pub fn new() -> Self { - Self::default() - } -} -impl Environment for PwdEnvironment { - fn getf(&self, name: &wstr, mode: EnvMode) -> Option { - if name == "PWD" { - return Some(EnvVar::new(wgetcwd(), EnvVarFlags::default())); - } - self.parent.getf(name, mode) - } - - fn get_names(&self, flags: EnvMode) -> Vec { - let mut res = self.parent.get_names(flags); - if !res.iter().any(|n| n == "PWD") { - res.push(L!("PWD").to_owned()); - } - res - } -} - -/// Helper for test_timezone_env_vars(). -fn return_timezone_hour(tstamp: SystemTime, timezone: &wstr) -> libc::c_int { - let vars = EnvStack::globals().create_child(true /* dispatches_var_changes */); - - vars.set_one(L!("TZ"), EnvMode::EXPORT, timezone.to_owned()); - - let _var = vars.get(L!("TZ")); - - #[allow(deprecated)] - let tstamp: libc::time_t = tstamp - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - .try_into() - .unwrap(); - let mut local_time = MaybeUninit::uninit(); - unsafe { libc::localtime_r(&tstamp, local_time.as_mut_ptr()) }; - let local_time = unsafe { local_time.assume_init() }; - local_time.tm_hour -} - -/// Verify that setting TZ calls tzset() in the current shell process. -fn test_timezone_env_vars() { - // Confirm changing the timezone affects fish's idea of the local time. - let tstamp = SystemTime::now(); - - let first_tstamp = return_timezone_hour(tstamp, L!("UTC-1")); - let second_tstamp = return_timezone_hour(tstamp, L!("UTC-2")); - let delta = second_tstamp - first_tstamp; - assert!(delta == 1 || delta == -23); -} - -// Verify that setting special env vars have the expected effect on the current shell process. -#[test] -#[serial] -fn test_env_vars() { - let _cleanup = test_init(); - test_timezone_env_vars(); - // TODO: Add tests for the locale vars. - - let v1 = EnvVar::new(L!("abc").to_owned(), EnvVarFlags::EXPORT); - let v2 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::EXPORT); - let v3 = EnvVar::new_vec(vec![L!("abc").to_owned()], EnvVarFlags::empty()); - let v4 = EnvVar::new_vec( - vec![L!("abc").to_owned(), L!("def").to_owned()], - EnvVarFlags::EXPORT, - ); - assert_eq!(v1, v2); - assert_ne!(v1, v3); - assert_ne!(v1, v4); -} - -#[test] -#[serial] -fn test_env_snapshot() { - let _cleanup = test_init(); - std::fs::create_dir_all("test/fish_env_snapshot_test/").unwrap(); - let parser = TestParser::new(); - let vars = parser.vars(); - parser.pushd("test/fish_env_snapshot_test/"); - vars.push(true); - let before_pwd = vars.get(L!("PWD")).unwrap().as_string(); - vars.set_one( - L!("test_env_snapshot_var"), - EnvMode::default(), - L!("before").to_owned(), - ); - let snapshot = vars.snapshot(); - vars.set_one(L!("PWD"), EnvMode::default(), L!("/newdir").to_owned()); - vars.set_one( - L!("test_env_snapshot_var"), - EnvMode::default(), - L!("after").to_owned(), - ); - vars.set_one( - L!("test_env_snapshot_var_2"), - EnvMode::default(), - L!("after").to_owned(), - ); - - // vars should be unaffected by the snapshot - assert_eq!(vars.get(L!("PWD")).unwrap().as_string(), L!("/newdir")); - assert_eq!( - vars.get(L!("test_env_snapshot_var")).unwrap().as_string(), - L!("after") - ); - assert_eq!( - vars.get(L!("test_env_snapshot_var_2")).unwrap().as_string(), - L!("after") - ); - - // snapshot should have old values of vars - assert_eq!(snapshot.get(L!("PWD")).unwrap().as_string(), before_pwd); - assert_eq!( - snapshot - .get(L!("test_env_snapshot_var")) - .unwrap() - .as_string(), - L!("before") - ); - assert_eq!(snapshot.get(L!("test_env_snapshot_var_2")), None); - - // snapshots see global var changes except for perproc like PWD - vars.set_one( - L!("test_env_snapshot_var_3"), - EnvMode::GLOBAL, - L!("reallyglobal").to_owned(), - ); - assert_eq!( - vars.get(L!("test_env_snapshot_var_3")).unwrap().as_string(), - L!("reallyglobal") - ); - assert_eq!( - snapshot - .get(L!("test_env_snapshot_var_3")) - .unwrap() - .as_string(), - L!("reallyglobal") - ); - - vars.pop(); - parser.popd(); -} - -// Can't push/pop from globals. -#[test] -#[should_panic] -fn test_no_global_push() { - EnvStack::globals().push(true); -} - -#[test] -#[should_panic] -fn test_no_global_pop() { - EnvStack::globals().pop(); -} diff --git a/src/tests/env_universal_common.rs b/src/tests/env_universal_common.rs deleted file mode 100644 index 60637ae94..000000000 --- a/src/tests/env_universal_common.rs +++ /dev/null @@ -1,340 +0,0 @@ -use crate::common::ENCODE_DIRECT_BASE; -use crate::common::char_offset; -use crate::common::wcs2osstring; -use crate::env::{EnvVar, EnvVarFlags, VarTable}; -use crate::env_universal_common::{EnvUniversal, UvarFormat}; -use crate::reader::fake_scoped_reader; -use crate::tests::prelude::*; -use crate::threads::{iothread_drain_all, iothread_perform}; -use crate::wchar::prelude::*; -use crate::wutil::{INVALID_FILE_ID, file_id_for_path}; - -const UVARS_PER_THREAD: usize = 8; -const UVARS_TEST_PATH: &wstr = L!("test/fish_uvars_test/varsfile.txt"); - -fn test_universal_helper(x: usize) { - let _cleanup = test_init(); - let mut uvars = EnvUniversal::new(); - uvars.initialize_at_path(UVARS_TEST_PATH.to_owned()); - - for j in 0..UVARS_PER_THREAD { - let key = sprintf!("key_%d_%d", x, j); - let val = sprintf!("val_%d_%d", x, j); - uvars.set(&key, EnvVar::new(val, EnvVarFlags::empty())); - let (synced, _) = uvars.sync(); - assert!( - synced, - "Failed to sync universal variables after modification" - ); - } - - // Last step is to delete the first key. - uvars.remove(&sprintf!("key_%d_%d", x, 0)); - let (synced, _) = uvars.sync(); - assert!(synced, "Failed to sync universal variables after deletion"); -} - -#[test] -#[serial] -fn test_universal() { - let _cleanup = test_init(); - let _ = std::fs::remove_dir_all("test/fish_uvars_test/"); - std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); - let parser = TestParser::new(); - - let mut reader = fake_scoped_reader(&parser); - - let threads = 1; - for i in 0..threads { - iothread_perform(move || test_universal_helper(i)); - } - iothread_drain_all(&mut reader); - - let mut uvars = EnvUniversal::new(); - uvars.initialize_at_path(UVARS_TEST_PATH.to_owned()); - - for i in 0..threads { - for j in 0..UVARS_PER_THREAD { - let key = sprintf!("key_%d_%d", i, j); - let expected_val = if j == 0 { - None - } else { - Some(EnvVar::new( - sprintf!("val_%d_%d", i, j), - EnvVarFlags::empty(), - )) - }; - let var = uvars.get(&key); - assert_eq!(var, expected_val); - } - } - - std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); -} - -#[test] -#[serial] -fn test_universal_output() { - let _cleanup = test_init(); - let flag_export = EnvVarFlags::EXPORT; - let flag_pathvar = EnvVarFlags::PATHVAR; - - let mut vars = VarTable::new(); - vars.insert( - L!("varA").to_owned(), - EnvVar::new_vec( - vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], - EnvVarFlags::empty(), - ), - ); - vars.insert( - L!("varB").to_owned(), - EnvVar::new_vec(vec![L!("ValB1").to_owned()], flag_export), - ); - vars.insert( - L!("varC").to_owned(), - EnvVar::new_vec(vec![L!("ValC1").to_owned()], EnvVarFlags::empty()), - ); - vars.insert( - L!("varD").to_owned(), - EnvVar::new_vec(vec![L!("ValD1").to_owned()], flag_export | flag_pathvar), - ); - vars.insert( - L!("varE").to_owned(), - EnvVar::new_vec( - vec![L!("ValE1").to_owned(), L!("ValE2").to_owned()], - flag_pathvar, - ), - ); - vars.insert( - L!("varF").to_owned(), - EnvVar::new_vec( - vec![WString::from_chars([char_offset(ENCODE_DIRECT_BASE, 0xfc)])], - EnvVarFlags::empty(), - ), - ); - - let text = EnvUniversal::serialize_with_vars(&vars); - let expected = concat!( - "# This file contains fish universal variable definitions.\n", - "# VERSION: 3.0\n", - "SETUVAR varA:ValA1\\x1eValA2\n", - "SETUVAR --export varB:ValB1\n", - "SETUVAR varC:ValC1\n", - "SETUVAR --export --path varD:ValD1\n", - "SETUVAR --path varE:ValE1\\x1eValE2\n", - "SETUVAR varF:\\xfc\n", - ) - .as_bytes(); - assert_eq!(text, expected); -} - -#[test] -#[serial] -fn test_universal_parsing() { - let _cleanup = test_init(); - let input = concat!( - "# This file contains fish universal variable definitions.\n", - "# VERSION: 3.0\n", - "SETUVAR varA:ValA1\\x1eValA2\n", - "SETUVAR --export varB:ValB1\n", - "SETUVAR --nonsenseflag varC:ValC1\n", - "SETUVAR --export --path varD:ValD1\n", - "SETUVAR --path --path varE:ValE1\\x1eValE2\n", - ) - .as_bytes(); - - let flag_export = EnvVarFlags::EXPORT; - let flag_pathvar = EnvVarFlags::PATHVAR; - - let mut vars = VarTable::new(); - - vars.insert( - L!("varA").to_owned(), - EnvVar::new_vec( - vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], - EnvVarFlags::empty(), - ), - ); - vars.insert( - L!("varB").to_owned(), - EnvVar::new_vec(vec![L!("ValB1").to_owned()], flag_export), - ); - vars.insert( - L!("varC").to_owned(), - EnvVar::new_vec(vec![L!("ValC1").to_owned()], EnvVarFlags::empty()), - ); - vars.insert( - L!("varD").to_owned(), - EnvVar::new_vec(vec![L!("ValD1").to_owned()], flag_export | flag_pathvar), - ); - vars.insert( - L!("varE").to_owned(), - EnvVar::new_vec( - vec![L!("ValE1").to_owned(), L!("ValE2").to_owned()], - flag_pathvar, - ), - ); - - let mut parsed_vars = VarTable::new(); - EnvUniversal::populate_variables(input, &mut parsed_vars); - assert_eq!(vars, parsed_vars); -} - -#[test] -#[serial] -fn test_universal_parsing_legacy() { - let _cleanup = test_init(); - let input = concat!( - "# This file contains fish universal variable definitions.\n", - "SET varA:ValA1\\x1eValA2\n", - "SET_EXPORT varB:ValB1\n", - ) - .as_bytes(); - - let mut vars = VarTable::new(); - vars.insert( - L!("varA").to_owned(), - EnvVar::new_vec( - vec![L!("ValA1").to_owned(), L!("ValA2").to_owned()], - EnvVarFlags::empty(), - ), - ); - vars.insert( - L!("varB").to_owned(), - EnvVar::new(L!("ValB1").to_owned(), EnvVarFlags::EXPORT), - ); - - let mut parsed_vars = VarTable::new(); - EnvUniversal::populate_variables(input, &mut parsed_vars); - assert_eq!(vars, parsed_vars); -} - -#[test] -#[serial] -fn test_universal_callbacks() { - let _cleanup = test_init(); - std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); - let mut uvars1 = EnvUniversal::new(); - let mut uvars2 = EnvUniversal::new(); - let mut callbacks = uvars1 - .initialize_at_path(UVARS_TEST_PATH.to_owned()) - .unwrap_or_default(); - callbacks.append( - &mut uvars2 - .initialize_at_path(UVARS_TEST_PATH.to_owned()) - .unwrap_or_default(), - ); - - macro_rules! sync { - ($uvars:expr) => { - let (_, cb_opt) = $uvars.sync(); - if let Some(mut cb) = cb_opt { - callbacks.append(&mut cb); - } - }; - } - - let noflags = EnvVarFlags::empty(); - - // Put some variables into both. - uvars1.set(L!("alpha"), EnvVar::new(L!("1").to_owned(), noflags)); // - uvars1.set(L!("beta"), EnvVar::new(L!("1").to_owned(), noflags)); // - uvars1.set(L!("delta"), EnvVar::new(L!("1").to_owned(), noflags)); // - uvars1.set(L!("epsilon"), EnvVar::new(L!("1").to_owned(), noflags)); // - uvars1.set(L!("lambda"), EnvVar::new(L!("1").to_owned(), noflags)); // - uvars1.set(L!("kappa"), EnvVar::new(L!("1").to_owned(), noflags)); // - uvars1.set(L!("omicron"), EnvVar::new(L!("1").to_owned(), noflags)); // - - sync!(uvars1); - sync!(uvars2); - - // Change uvars1. - uvars1.set(L!("alpha"), EnvVar::new(L!("2").to_owned(), noflags)); // changes value - uvars1.set( - L!("beta"), - EnvVar::new(L!("1").to_owned(), EnvVarFlags::EXPORT), - ); // changes export - uvars1.remove(L!("delta")); // erases value - uvars1.set(L!("epsilon"), EnvVar::new(L!("1").to_owned(), noflags)); // changes nothing - sync!(uvars1); - - // Change uvars2. It should treat its value as correct and ignore changes from uvars1. - uvars2.set(L!("lambda"), EnvVar::new(L!("1").to_owned(), noflags)); // same value - uvars2.set(L!("kappa"), EnvVar::new(L!("2").to_owned(), noflags)); // different value - - // Now see what uvars2 sees. - callbacks.clear(); - sync!(uvars2); - - // Sort them to get them in a predictable order. - callbacks.sort_by(|a, b| a.key.cmp(&b.key)); - - // Should see exactly three changes. - assert_eq!(callbacks.len(), 3); - assert_eq!(callbacks[0].key, L!("alpha")); - assert_eq!(callbacks[0].val.as_ref().unwrap().as_string(), L!("2")); - assert_eq!(callbacks[1].key, L!("beta")); - assert_eq!(callbacks[1].val.as_ref().unwrap().as_string(), L!("1")); - assert_eq!(callbacks[2].key, L!("delta")); - assert_eq!(callbacks[2].val, None); - std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); -} - -#[test] -#[serial] -fn test_universal_formats() { - let _cleanup = test_init(); - macro_rules! validate { - ( $version_line:literal, $expected_format:expr ) => { - assert_eq!( - EnvUniversal::format_for_contents($version_line), - $expected_format - ); - }; - } - validate!(b"# VERSION: 3.0", UvarFormat::fish_3_0); - validate!(b"# version: 3.0", UvarFormat::fish_2_x); - validate!(b"# blah blahVERSION: 3.0", UvarFormat::fish_2_x); - validate!(b"stuff\n# blah blahVERSION: 3.0", UvarFormat::fish_2_x); - validate!(b"# blah\n# VERSION: 3.0", UvarFormat::fish_3_0); - validate!(b"# blah\n#VERSION: 3.0", UvarFormat::fish_3_0); - validate!(b"# blah\n#VERSION:3.0", UvarFormat::fish_3_0); - validate!(b"# blah\n#VERSION:3.1", UvarFormat::future); -} - -#[test] -#[serial] -fn test_universal_ok_to_save() { - let _cleanup = test_init(); - // Ensure we don't try to save after reading from a newer fish. - std::fs::create_dir_all("test/fish_uvars_test/").unwrap(); - let contents = b"# VERSION: 99999.99\n"; - std::fs::write(wcs2osstring(UVARS_TEST_PATH), contents).unwrap(); - - let before_id = file_id_for_path(UVARS_TEST_PATH); - assert_ne!( - before_id, INVALID_FILE_ID, - "UVARS_TEST_PATH should be readable" - ); - - let mut uvars = EnvUniversal::new(); - uvars - .initialize_at_path(UVARS_TEST_PATH.to_owned()) - .unwrap_or_default(); - assert!(!uvars.is_ok_to_save(), "Should not be OK to save"); - uvars.sync(); - assert!(!uvars.is_ok_to_save(), "Should still not be OK to save"); - uvars.set( - L!("SOMEVAR"), - EnvVar::new(L!("SOMEVALUE").to_owned(), EnvVarFlags::empty()), - ); - - // Ensure file is same. - let after_id = file_id_for_path(UVARS_TEST_PATH); - assert_eq!( - before_id, after_id, - "UVARS_TEST_PATH should not have changed", - ); - std::fs::remove_dir_all("test/fish_uvars_test/").unwrap(); -} diff --git a/src/tests/expand.rs b/src/tests/expand.rs deleted file mode 100644 index 66be6171f..000000000 --- a/src/tests/expand.rs +++ /dev/null @@ -1,458 +0,0 @@ -use crate::abbrs::Abbreviation; -use crate::abbrs::{self}; -use crate::abbrs::{with_abbrs, with_abbrs_mut}; -use crate::complete::{CompletionList, CompletionReceiver}; -use crate::env::{EnvMode, EnvStackSetResult}; -use crate::expand::{ExpandResultCode, expand_to_receiver}; -use crate::operation_context::{EXPANSION_LIMIT_DEFAULT, no_cancel}; -use crate::parse_constants::ParseErrorList; -use crate::tests::prelude::*; -use crate::wildcard::ANY_STRING; -use crate::{ - expand::{ExpandFlags, expand_string}, - operation_context::OperationContext, - wchar::prelude::*, -}; -use std::collections::HashSet; -use std::collections::hash_map::RandomState; - -fn expand_test_impl( - input: &wstr, - flags: ExpandFlags, - expected: Vec, - error_message: Option<&str>, -) { - let parser = TestParser::new(); - let mut output = CompletionList::new(); - let mut errors = ParseErrorList::new(); - let pwd = PwdEnvironment::default(); - let ctx = OperationContext::test_only_foreground(&parser, &pwd, Box::new(no_cancel)); - - if expand_string( - input.to_owned(), - &mut output, - flags, - &ctx, - Some(&mut errors), - ) == ExpandResultCode::error - { - assert_ne!( - errors, - vec![], - "Bug: Parse error reported but no error text found." - ); - panic!( - "{}", - errors[0].describe(input, ctx.parser().is_interactive()) - ); - } - - let expected_set: HashSet = HashSet::from_iter(expected); - let output_set = HashSet::from_iter(output.into_iter().map(|c| c.completion)); - assert_eq!( - expected_set, - output_set, - "{}", - error_message.unwrap_or("expand mismatch") - ); -} - -// Test globbing and other parameter expansion. -#[test] -#[serial] -fn test_expand() { - let _cleanup = test_init(); - let parser = TestParser::new(); - /// Perform parameter expansion and test if the output equals the zero-terminated parameter list /// supplied. - /// - /// \param in the string to expand - /// \param flags the flags to send to expand_string - /// \param ... A zero-terminated parameter list of values to test. - /// After the zero terminator comes one more arg, a string, which is the error - /// message to print if the test fails. - macro_rules! expand_test { - ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? )) => { - expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], None) - }; - ($input:expr, $flags:expr, ( $($expected:expr),* $(,)? ), $error:literal) => { - expand_test_impl(L!($input), $flags, vec![$( $expected.into(), )*], Some($error)) - }; - ($input:expr, $flags:expr, $expected:expr) => { - expand_test_impl(L!($input), $flags, vec![ $expected.into() ], None) - }; - ($input:expr, $flags:expr, $expected:expr, $error:literal) => { - expand_test_impl(L!($input), $flags, vec![ $expected.into() ], Some($error)) - }; - } - - // Testing parameter expansion - let noflags = ExpandFlags::default(); - - expand_test!("foo", noflags, "foo", "Strings do not expand to themselves"); - - expand_test!( - "a{b,c,d}e", - noflags, - ("abe", "ace", "ade"), - "Bracket expansion is broken" - ); - expand_test!( - "a*", - ExpandFlags::SKIP_WILDCARDS, - "a*", - "Cannot skip wildcard expansion" - ); - expand_test!( - "/bin/l\\0", - ExpandFlags::FOR_COMPLETIONS, - (), - "Failed to handle null escape in expansion" - ); - expand_test!( - "foo\\$bar", - ExpandFlags::SKIP_VARIABLES, - "foo$bar", - "Failed to handle dollar sign in variable-skipping expansion" - ); - - // bb - // x - // bar - // baz - // xxx - // yyy - // bax - // xxx - // lol - // nub - // q - // zzz - // .foo - // aaa - // aaa2 - // x - std::fs::create_dir_all("test/fish_expand_test/").unwrap(); - std::fs::create_dir_all("test/fish_expand_test/bb/").unwrap(); - std::fs::create_dir_all("test/fish_expand_test/baz/").unwrap(); - std::fs::create_dir_all("test/fish_expand_test/bax/").unwrap(); - std::fs::create_dir_all("test/fish_expand_test/lol/nub/").unwrap(); - std::fs::create_dir_all("test/fish_expand_test/aaa/").unwrap(); - std::fs::create_dir_all("test/fish_expand_test/aaa2/").unwrap(); - std::fs::write("test/fish_expand_test/.foo", []).unwrap(); - std::fs::write("test/fish_expand_test/bb/x", []).unwrap(); - std::fs::write("test/fish_expand_test/bar", []).unwrap(); - std::fs::write("test/fish_expand_test/bax/xxx", []).unwrap(); - std::fs::write("test/fish_expand_test/baz/xxx", []).unwrap(); - std::fs::write("test/fish_expand_test/baz/yyy", []).unwrap(); - std::fs::write("test/fish_expand_test/lol/nub/q", []).unwrap(); - std::fs::write("test/fish_expand_test/lol/nub/zzz", []).unwrap(); - std::fs::write("test/fish_expand_test/aaa2/x", []).unwrap(); - - // This is checking that .* does NOT match . and .. - // (https://github.com/fish-shell/fish-shell/issues/270). But it does have to match literal - // components (e.g. "./*" has to match the same as "*". - expand_test!( - "test/fish_expand_test/.*", - noflags, - "test/fish_expand_test/.foo", - "Expansion not correctly handling dotfiles" - ); - - expand_test!( - "test/fish_expand_test/./.*", - noflags, - "test/fish_expand_test/./.foo", - "Expansion not correctly handling literal path components in dotfiles" - ); - - expand_test!( - "test/fish_expand_test/*/xxx", - noflags, - ( - "test/fish_expand_test/bax/xxx", - "test/fish_expand_test/baz/xxx" - ), - "Glob did the wrong thing 1" - ); - - expand_test!( - "test/fish_expand_test/*z/xxx", - noflags, - "test/fish_expand_test/baz/xxx", - "Glob did the wrong thing 2" - ); - - expand_test!( - "test/fish_expand_test/**z/xxx", - noflags, - "test/fish_expand_test/baz/xxx", - "Glob did the wrong thing 3" - ); - - expand_test!( - "test/fish_expand_test////baz/xxx", - noflags, - "test/fish_expand_test////baz/xxx", - "Glob did the wrong thing 3" - ); - - expand_test!( - "test/fish_expand_test/b**", - noflags, - ( - "test/fish_expand_test/bb", - "test/fish_expand_test/bb/x", - "test/fish_expand_test/bar", - "test/fish_expand_test/bax", - "test/fish_expand_test/bax/xxx", - "test/fish_expand_test/baz", - "test/fish_expand_test/baz/xxx", - "test/fish_expand_test/baz/yyy" - ), - "Glob did the wrong thing 4" - ); - - // A trailing slash should only produce directories. - expand_test!( - "test/fish_expand_test/b*/", - noflags, - ( - "test/fish_expand_test/bb/", - "test/fish_expand_test/baz/", - "test/fish_expand_test/bax/" - ), - "Glob did the wrong thing 5" - ); - - expand_test!( - "test/fish_expand_test/b**/", - noflags, - ( - "test/fish_expand_test/bb/", - "test/fish_expand_test/baz/", - "test/fish_expand_test/bax/" - ), - "Glob did the wrong thing 6" - ); - - expand_test!( - "test/fish_expand_test/**/q", - noflags, - "test/fish_expand_test/lol/nub/q", - "Glob did the wrong thing 7" - ); - - expand_test!( - "test/fish_expand_test/BA", - ExpandFlags::FOR_COMPLETIONS, - ( - "test/fish_expand_test/bar", - "test/fish_expand_test/bax/", - "test/fish_expand_test/baz/" - ), - "Case insensitive test did the wrong thing" - ); - - expand_test!( - "test/fish_expand_test/BA", - ExpandFlags::FOR_COMPLETIONS, - ( - "test/fish_expand_test/bar", - "test/fish_expand_test/bax/", - "test/fish_expand_test/baz/" - ), - "Case insensitive test did the wrong thing" - ); - - expand_test!( - "test/fish_expand_test/bb/yyy", - ExpandFlags::FOR_COMPLETIONS, - (), /* nothing! */ - "Wrong fuzzy matching 1" - ); - - expand_test!( - "test/fish_expand_test/bb/x", - ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH, - "", - // we just expect the empty string since this is an exact match - "Wrong fuzzy matching 2" - ); - - // Some vswprintfs refuse to append ANY_STRING in a format specifiers, so don't use - // format_string here. - let fuzzy_comp = ExpandFlags::FOR_COMPLETIONS | ExpandFlags::FUZZY_MATCH; - let any_str_str = ANY_STRING.to_string(); - expand_test!( - "test/fish_expand_test/b/xx*", - fuzzy_comp, - ( - (String::from("test/fish_expand_test/bax/xx") + &any_str_str), - (String::from("test/fish_expand_test/baz/xx") + &any_str_str) - ), - "Wrong fuzzy matching 3" - ); - - expand_test!( - "test/fish_expand_test/*/nu/zz", - fuzzy_comp, - (format!("test/fish_expand_test/{any_str_str}/nub/zzz")), - "Glob did not expand correctly with more than one path item after the *" - ); - - expand_test!( - "test/fish_expand_test/b/yyy", - fuzzy_comp, - "test/fish_expand_test/baz/yyy", - "Wrong fuzzy matching 4" - ); - - expand_test!( - "test/fish_expand_test/aa/x", - fuzzy_comp, - "test/fish_expand_test/aaa2/x", - "Wrong fuzzy matching 5" - ); - - expand_test!( - "test/fish_expand_test/aaa/x", - fuzzy_comp, - (), - "Wrong fuzzy matching 6 - shouldn't remove valid directory names (#3211)" - ); - - // Dotfiles - expand_test!( - "test/fish_expand_test/.*", - noflags, - "test/fish_expand_test/.foo", - "" - ); - - // Literal path components in dotfiles. - expand_test!( - "test/fish_expand_test/./.*", - noflags, - "test/fish_expand_test/./.foo", - "" - ); - - parser.pushd("test/fish_expand_test"); - - expand_test!( - "b/xx", - fuzzy_comp, - ("bax/xxx", "baz/xxx"), - "Wrong fuzzy matching 5" - ); - - // multiple slashes with fuzzy matching - #3185 - expand_test!("l///n", fuzzy_comp, "lol///nub/", "Wrong fuzzy matching 6"); - - parser.popd(); -} - -#[test] -#[serial] -fn test_expand_overflow() { - let _cleanup = test_init(); - // Testing overflowing expansions - // Ensure that we have sane limits on number of expansions - see #7497. - - // Make a list of 64 elements, then expand it cartesian-style 64 times. - // This is far too large to expand. - let vals: Vec = (1..=64).map(|i| i.to_wstring()).collect(); - let expansion = WString::from_str(&str::repeat("$bigvar", 64)); - - let parser = TestParser::new(); - parser.vars().push(true); - let set = parser.vars().set(L!("bigvar"), EnvMode::LOCAL, vals); - assert_eq!(set, EnvStackSetResult::Ok); - - let mut errors = ParseErrorList::new(); - let ctx = OperationContext::foreground(&parser, Box::new(no_cancel), EXPANSION_LIMIT_DEFAULT); - - // We accept only 1024 completions. - let mut output = CompletionReceiver::new(1024); - - let res = expand_to_receiver( - expansion, - &mut output, - ExpandFlags::default(), - &ctx, - Some(&mut errors), - ); - assert_ne!(errors, vec![]); - assert_eq!(res, ExpandResultCode::error); - - parser.vars().pop(); -} - -#[test] -#[serial] -fn test_abbreviations() { - let _cleanup = test_init(); - // Testing abbreviations - - with_abbrs_mut(|abbrset| { - abbrset.add(Abbreviation::new( - L!("gc").to_owned(), - L!("gc").to_owned(), - L!("git checkout").to_owned(), - abbrs::Position::Command, - false, - )); - abbrset.add(Abbreviation::new( - L!("foo").to_owned(), - L!("foo").to_owned(), - L!("bar").to_owned(), - abbrs::Position::Command, - false, - )); - abbrset.add(Abbreviation::new( - L!("gx").to_owned(), - L!("gx").to_owned(), - L!("git checkout").to_owned(), - abbrs::Position::Command, - false, - )); - abbrset.add(Abbreviation::new( - L!("yin").to_owned(), - L!("yin").to_owned(), - L!("yang").to_owned(), - abbrs::Position::Anywhere, - false, - )); - }); - - // Helper to expand an abbreviation, enforcing we have no more than one result. - let abbr_expand_1 = |token, pos| -> Option { - let result = with_abbrs(|abbrset| abbrset.r#match(token, pos, L!(""))); - if result.is_empty() { - return None; - } - assert_eq!( - &result[1..], - &[], - "abbreviation expansion for {token} returned more than 1 result" - ); - Some(result.into_iter().next().unwrap().replacement) - }; - - let cmd = abbrs::Position::Command; - assert!( - abbr_expand_1(L!(""), cmd).is_none(), - "Unexpected success with empty abbreviation" - ); - assert!( - abbr_expand_1(L!("nothing"), cmd).is_none(), - "Unexpected success with missing abbreviation" - ); - - assert_eq!( - abbr_expand_1(L!("gc"), cmd), - Some(L!("git checkout").into()) - ); - - assert_eq!(abbr_expand_1(L!("foo"), cmd), Some(L!("bar").into())); -} diff --git a/src/tests/fd_monitor.rs b/src/tests/fd_monitor.rs deleted file mode 100644 index f9b423cb5..000000000 --- a/src/tests/fd_monitor.rs +++ /dev/null @@ -1,241 +0,0 @@ -#[cfg(not(target_has_atomic = "64"))] -use portable_atomic::AtomicU64; -use std::fs::File; -use std::io::Write; -use std::os::fd::{AsRawFd, IntoRawFd, OwnedFd}; -#[cfg(target_has_atomic = "64")] -use std::sync::atomic::AtomicU64; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::{Arc, Barrier, Mutex}; -use std::thread; -use std::time::Duration; - -use errno::errno; - -use crate::fd_monitor::{FdEventSignaller, FdMonitor}; -use crate::fd_readable_set::{FdReadableSet, Timeout}; -use crate::fds::{AutoCloseFd, AutoClosePipes, make_autoclose_pipes}; -use crate::tests::prelude::*; - -/// Helper to make an item which counts how many times its callback was invoked. -/// -/// This could be structured differently to avoid the `Mutex` on `writer`, but it's not worth it -/// since this is just used for test purposes. -struct ItemMaker { - pub length_read: AtomicUsize, - pub total_calls: AtomicUsize, - item_id: AtomicU64, - pub always_close: bool, - pub writer: Mutex>, -} - -impl ItemMaker { - pub fn insert_new_into(monitor: &FdMonitor) -> Arc { - Self::insert_new_into2(monitor, |_| {}) - } - - pub fn insert_new_into2(monitor: &FdMonitor, config: F) -> Arc { - let pipes = make_autoclose_pipes().expect("fds exhausted!"); - - let mut result = ItemMaker { - length_read: 0.into(), - total_calls: 0.into(), - item_id: 0.into(), - always_close: false, - writer: Mutex::new(Some(File::from(pipes.write))), - }; - - config(&mut result); - - let result = Arc::new(result); - let callback = { - let result = Arc::clone(&result); - move |fd: &mut AutoCloseFd| result.callback(fd) - }; - let fd = AutoCloseFd::new(pipes.read.into_raw_fd()); - let item_id = monitor.add(fd, Box::new(callback)); - result.item_id.store(u64::from(item_id), Ordering::Relaxed); - - result - } - - fn callback(&self, fd: &mut AutoCloseFd) { - let mut buf = [0u8; 1024]; - let res = nix::unistd::read(&fd, &mut buf); - let amt = res.expect("read error!"); - self.length_read.fetch_add(amt, Ordering::Relaxed); - let was_closed = amt == 0; - - self.total_calls.fetch_add(1, Ordering::Relaxed); - if was_closed || self.always_close { - fd.close(); - } - } - - /// Write 42 bytes to our write end. - fn write42(&self) { - let buf = [0u8; 42]; - let mut writer = self.writer.lock().expect("Mutex poisoned!"); - writer - .as_mut() - .unwrap() - .write_all(&buf) - .expect("Error writing 42 bytes to pipe!"); - } -} - -#[test] -#[serial] -fn fd_monitor_items() { - let _cleanup = test_init(); - let monitor = FdMonitor::new(); - - // Item which will never receive data or be called. - let item_never = ItemMaker::insert_new_into(&monitor); - - // Item which should get exactly 42 bytes. - let item42 = ItemMaker::insert_new_into(&monitor); - - // Item which should get 42 bytes then get notified it is closed. - let item42_then_close = ItemMaker::insert_new_into(&monitor); - - // Item which should get a callback exactly once. - let item_oneshot = ItemMaker::insert_new_into2(&monitor, |item| { - item.always_close = true; - }); - - item42.write42(); - item42_then_close.write42(); - *item42_then_close.writer.lock().expect("Mutex poisoned!") = None; - item_oneshot.write42(); - - // May need to loop here to ensure our fd_monitor gets scheduled. See #7699. - for _ in 0..100 { - std::thread::sleep(Duration::from_millis(84)); - if item_oneshot.total_calls.load(Ordering::Relaxed) > 0 { - break; - } - } - - drop(monitor); - - assert_eq!(item_never.length_read.load(Ordering::Relaxed), 0); - - assert_eq!(item42.length_read.load(Ordering::Relaxed), 42); - - assert_eq!(item42_then_close.length_read.load(Ordering::Relaxed), 42); - assert_eq!(item42_then_close.total_calls.load(Ordering::Relaxed), 2); - - assert_eq!(item_oneshot.length_read.load(Ordering::Relaxed), 42); - assert_eq!(item_oneshot.total_calls.load(Ordering::Relaxed), 1); -} - -#[test] -fn test_fd_event_signaller() { - let sema = FdEventSignaller::new(); - assert!(!sema.try_consume()); - assert!(!sema.poll(false)); - - // Post once. - sema.post(); - assert!(sema.poll(false)); - assert!(sema.poll(false)); - assert!(sema.try_consume()); - assert!(!sema.poll(false)); - assert!(!sema.try_consume()); - - // Posts are coalesced. - sema.post(); - sema.post(); - sema.post(); - assert!(sema.poll(false)); - assert!(sema.poll(false)); - assert!(sema.try_consume()); - assert!(!sema.poll(false)); - assert!(!sema.try_consume()); -} - -// A helper function which calls poll() or selects() on a file descriptor in the background, -// and then invokes the `bad_action` function on the file descriptor while the poll/select is -// waiting. The function returns Result: either the number of readable file descriptors -// or the error code from poll/select. -#[cfg(test)] -fn do_something_bad_during_select(bad_action: F) -> Result -where - F: FnOnce(OwnedFd) -> Option, -{ - let AutoClosePipes { - read: read_fd, - write: write_fd, - } = make_autoclose_pipes().expect("Failed to create pipe"); - let raw_read_fd = read_fd.as_raw_fd(); - - // Try to ensure that the thread will be scheduled by waiting until it is. - let barrier = Arc::new(Barrier::new(2)); - let barrier_clone = Arc::clone(&barrier); - - let select_thread = thread::spawn(move || -> Result { - let mut fd_set = FdReadableSet::new(); - fd_set.add(raw_read_fd); - - barrier_clone.wait(); - - // Timeout after 500 msec. - // macOS will eagerly return EBADF if the fd is closed; Linux will hit the timeout. - let timeout = Timeout::Duration(Duration::from_millis(500)); - let ret = fd_set.check_readable(timeout); - if ret < 0 { Err(errno().0) } else { Ok(ret) } - }); - - barrier.wait(); - thread::sleep(Duration::from_millis(100)); - let read_fd = bad_action(read_fd); - - let result = select_thread.join().expect("Select thread panicked"); - // Ensure these stay alive until after thread is joined. - drop(read_fd); - drop(write_fd); - result -} - -#[test] -fn test_close_during_select_ebadf() { - use crate::common::{WSL, is_windows_subsystem_for_linux as is_wsl}; - let close_it = |read_fd: OwnedFd| { - drop(read_fd); - None - }; - let result = do_something_bad_during_select(close_it); - - // WSLv1 does not error out with EBADF if the fd is closed mid-select. - // This is OK because we do not _depend_ on this behavior; the only - // true requirement is that we don't panic in the handling code above. - assert!( - is_wsl(WSL::V1) || matches!(result, Err(libc::EBADF) | Ok(1)), - "select/poll should have failed with EBADF or marked readable" - ); -} - -#[test] -fn test_dup2_during_select_ebadf() { - // Make a random file descriptor that we can dup2 stdin to. - let AutoClosePipes { - read: pipe_read, - write: pipe_write, - } = make_autoclose_pipes().expect("Failed to create pipe"); - - let dup2_it = |read_fd: OwnedFd| { - // We are going to dup2 stdin to this fd, which should cause select/poll to fail. - assert!(read_fd.as_raw_fd() > 0, "fd should be valid and not stdin"); - unsafe { libc::dup2(pipe_read.as_raw_fd(), read_fd.as_raw_fd()) }; - Some(read_fd) - }; - let result = do_something_bad_during_select(dup2_it); - assert!( - matches!(result, Err(libc::EBADF) | Ok(0) | Ok(1)), - "select/poll should have failed with EBADF or timed out or the fd should be ready" - ); - // Ensure these stay alive until after thread is joined. - drop(pipe_read); - drop(pipe_write); -} diff --git a/src/tests/history.rs b/src/tests/history.rs deleted file mode 100644 index 83b8690cb..000000000 --- a/src/tests/history.rs +++ /dev/null @@ -1,664 +0,0 @@ -use crate::common::{ScopeGuard, bytes2wcstring, wcs2bytes, wcs2osstring}; -use crate::env::{EnvMode, EnvStack}; -use crate::fs::{LockedFile, WriteMethod}; -use crate::history::{ - self, History, HistoryItem, HistorySearch, PathList, SearchDirection, VACUUM_FREQUENCY, -}; -use crate::path::path_get_data; -use crate::tests::prelude::*; -use crate::tests::string_escape::ESCAPE_TEST_CHAR; -use crate::util::get_rng; -use crate::wchar::prelude::*; -use crate::wcstringutil::{string_prefixes_string, string_prefixes_string_case_insensitive}; -use fish_build_helper::workspace_root; -use rand::Rng; -use rand::rngs::SmallRng; -use std::collections::VecDeque; -use std::ffi::CString; -use std::io::BufReader; -use std::os::unix::ffi::OsStrExt; -use std::sync::Arc; -use std::time::UNIX_EPOCH; -use std::time::{Duration, SystemTime}; - -fn history_contains(history: &History, txt: &wstr) -> bool { - for i in 1.. { - let Some(item) = history.item_at_index(i) else { - break; - }; - - if item.str() == txt { - return true; - } - } - - false -} - -fn random_string(rng: &mut SmallRng) -> WString { - let mut result = WString::new(); - let max = rng.gen_range(1..=32); - for _ in 0..max { - let c = - char::from_u32(u32::try_from(1 + rng.gen_range(0..ESCAPE_TEST_CHAR)).unwrap()).unwrap(); - result.push(c); - } - result -} - -#[test] -#[serial] -fn test_history() { - let _cleanup = test_init(); - macro_rules! test_history_matches { - ($search:expr, $expected:expr) => { - let expected: Vec<&wstr> = $expected; - let mut found = vec![]; - while $search.go_to_next_match(SearchDirection::Backward) { - found.push($search.current_string().to_owned()); - } - assert_eq!(expected, found); - }; - } - - let items = [ - L!("Gamma"), - L!("beta"), - L!("BetA"), - L!("Beta"), - L!("alpha"), - L!("AlphA"), - L!("Alpha"), - L!("alph"), - L!("ALPH"), - L!("ZZZ"), - ]; - let nocase = history::SearchFlags::IGNORE_CASE; - - // Populate a history. - let history = History::with_name(L!("test_history")); - history.clear(); - for s in items { - history.add_commandline(s.to_owned()); - } - - // Helper to set expected items to those matching a predicate, in reverse order. - let set_expected = |filt: fn(&wstr) -> bool| { - let mut expected = vec![]; - for s in items { - if filt(s) { - expected.push(s); - } - } - expected.reverse(); - expected - }; - - // Items matching "a", case-sensitive. - let mut searcher = HistorySearch::new(history.clone(), L!("a").to_owned()); - let expected = set_expected(|s| s.contains('a')); - test_history_matches!(searcher, expected); - - // Items matching "alpha", case-insensitive. - let mut searcher = - HistorySearch::new_with_flags(history.clone(), L!("AlPhA").to_owned(), nocase); - let expected = set_expected(|s| s.to_lowercase().find(L!("alpha")).is_some()); - test_history_matches!(searcher, expected); - - // Items matching "et", case-sensitive. - let mut searcher = HistorySearch::new(history.clone(), L!("et").to_owned()); - let expected = set_expected(|s| s.find(L!("et")).is_some()); - test_history_matches!(searcher, expected); - - // Items starting with "be", case-sensitive. - let mut searcher = HistorySearch::new_with_type( - history.clone(), - L!("be").to_owned(), - history::SearchType::Prefix, - ); - let expected = set_expected(|s| string_prefixes_string(L!("be"), s)); - test_history_matches!(searcher, expected); - - // Items starting with "be", case-insensitive. - let mut searcher = HistorySearch::new_with( - history.clone(), - L!("be").to_owned(), - history::SearchType::Prefix, - nocase, - 0, - ); - let expected = set_expected(|s| string_prefixes_string_case_insensitive(L!("be"), s)); - test_history_matches!(searcher, expected); - - // Items exactly matching "alph", case-sensitive. - let mut searcher = HistorySearch::new_with_type( - history.clone(), - L!("alph").to_owned(), - history::SearchType::Exact, - ); - let expected = set_expected(|s| s == "alph"); - test_history_matches!(searcher, expected); - - // Items exactly matching "alph", case-insensitive. - let mut searcher = HistorySearch::new_with( - history.clone(), - L!("alph").to_owned(), - history::SearchType::Exact, - nocase, - 0, - ); - let expected = set_expected(|s| s.to_lowercase() == "alph"); - test_history_matches!(searcher, expected); - - // Test item removal case-sensitive. - let mut searcher = HistorySearch::new(history.clone(), L!("Alpha").to_owned()); - test_history_matches!(searcher, vec![L!("Alpha")]); - history.remove(L!("Alpha")); - let mut searcher = HistorySearch::new(history.clone(), L!("Alpha").to_owned()); - test_history_matches!(searcher, vec![]); - - // Test history escaping and unescaping, yaml, etc. - let mut before: VecDeque = VecDeque::new(); - let mut after: VecDeque = VecDeque::new(); - history.clear(); - let max = 100; - let mut rng = get_rng(); - for i in 1..=max { - // Generate a value. - let mut value = WString::from_str("test item ") + &i.to_wstring()[..]; - - // Maybe add some backslashes. - if i % 3 == 0 { - value += L!("(slashies \\\\\\ slashies)"); - } - - // Generate some paths. - let paths: PathList = (0..rng.gen_range(0..6)) - .map(|_| random_string(&mut rng)) - .collect(); - - // Record this item. - let mut item = - HistoryItem::new(value, SystemTime::now(), 0, history::PersistenceMode::Disk); - item.set_required_paths(paths); - before.push_back(item.clone()); - history.add(item, false); - } - history.save(); - - // Empty items should just be dropped (#6032). - history.add_commandline(L!("").into()); - assert!(!history.item_at_index(1).unwrap().is_empty()); - - // Read items back in reverse order and ensure they're the same. - for i in (1..=100).rev() { - after.push_back(history.item_at_index(i).unwrap()); - } - assert_eq!(before.len(), after.len()); - for i in 0..before.len() { - let bef = &before[i]; - let aft = &after[i]; - assert_eq!(bef.str(), aft.str()); - assert_eq!(bef.timestamp(), aft.timestamp()); - assert_eq!(bef.get_required_paths(), aft.get_required_paths()); - } - - // Items should be explicitly added to the history. - history.add_commandline(L!("test-command").into()); - assert!(history_contains(&history, L!("test-command"))); - - // Clean up after our tests. - history.clear(); -} - -// Wait until the next second. -fn time_barrier() { - let start = SystemTime::now(); - loop { - std::thread::sleep(std::time::Duration::from_millis(1)); - if SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() - != start.duration_since(UNIX_EPOCH).unwrap().as_secs() - { - break; - } - } -} - -fn generate_history_lines(item_count: usize, idx: usize) -> Vec { - let mut result = Vec::with_capacity(item_count); - for i in 0..item_count { - result.push(sprintf!("%u %u", idx, i)); - } - result -} - -fn pound_on_history(item_count: usize, idx: usize) -> Arc { - // Called in child thread to modify history. - let hist = History::new(L!("race_test")); - let hist_lines = generate_history_lines(item_count, idx); - for line in hist_lines { - hist.add_commandline(line); - hist.save(); - } - hist -} - -#[test] -#[serial] -fn test_history_races() { - let _cleanup = test_init(); - - // Fail nearly every time on Cygwin (probably caused by flock issue, see #11933) - if cfg!(cygwin) { - return; - } - - let tmp_path = std::env::current_dir() - .unwrap() - .join("history-races-test-balloon"); - std::fs::write(&tmp_path, []).unwrap(); - let _cleanup = ScopeGuard::new((), |()| { - std::fs::remove_file(&tmp_path).unwrap(); - }); - if LockedFile::new( - crate::fs::LockingMode::Exclusive(WriteMethod::RenameIntoPlace), - &bytes2wcstring(tmp_path.as_os_str().as_bytes()), - ) - .is_err() - { - return; - } - - // Testing history race conditions - - // Test concurrent history writing. - // How many concurrent writers we have - const RACE_COUNT: usize = 4; - - // How many items each writer makes - const ITEM_COUNT: usize = 256; - - // Ensure history is clear. - History::new(L!("race_test")).clear(); - - let mut children = Vec::with_capacity(RACE_COUNT); - for i in 0..RACE_COUNT { - children.push(std::thread::spawn(move || { - pound_on_history(ITEM_COUNT, i); - })); - } - - // Wait for all children. - for child in children { - child.join().unwrap(); - } - - // Compute the expected lines. - let mut expected_lines: [Vec; RACE_COUNT] = - std::array::from_fn(|i| generate_history_lines(ITEM_COUNT, i)); - - // Ensure we consider the lines that have been outputted as part of our history. - time_barrier(); - - // Ensure that we got sane, sorted results. - let hist = History::new(L!("race_test")); - - // History is enumerated from most recent to least - // Every item should be the last item in some array - let mut hist_idx = 0; - loop { - hist_idx += 1; - let Some(item) = hist.item_at_index(hist_idx) else { - break; - }; - - let mut found = false; - for list in &mut expected_lines { - let Some(position) = list.iter().position(|elem| *elem == item.str()) else { - continue; - }; - - // Remove the item we found. - list.remove(position); - - // We expected this item to be the last. Items after this item - // in this array were therefore not found in history. - let removed = list.drain(position..); - for line in removed.into_iter().rev() { - printf!("Item dropped from history: %s\n", line); - } - - found = true; - break; - } - if !found { - printf!( - "Line '%s' found in history, but not found in some array\n", - item.str() - ); - for list in &expected_lines { - if !list.is_empty() { - printf!("\tRemaining: %s\n", list.last().unwrap()) - } - } - } - } - - // +1 to account for history's 1-based offset - let expected_idx = RACE_COUNT * ITEM_COUNT + 1; - assert_eq!(hist_idx, expected_idx); - - for list in expected_lines { - assert_eq!(list, Vec::::new(), "Lines still left in the array"); - } - hist.clear(); -} - -#[test] -#[serial] -fn test_history_external_rewrites() { - let _cleanup = test_init(); - - // Write some history to disk. - { - let hist = pound_on_history(VACUUM_FREQUENCY / 2, 0); - hist.add_commandline("needle".into()); - hist.save(); - } - std::thread::sleep(Duration::from_secs(1)); - - // Read history from disk. - let hist = History::new(L!("race_test")); - assert_eq!(hist.item_at_index(1).unwrap().str(), "needle"); - - // Add items until we rewrite the file. - // In practice this might be done by another shell. - pound_on_history(VACUUM_FREQUENCY, 0); - - for i in 1.. { - if hist.item_at_index(i).unwrap().str() == "needle" { - break; - } - } -} - -#[test] -#[serial] -fn test_history_merge() { - let _cleanup = test_init(); - // In a single fish process, only one history is allowed to exist with the given name But it's - // common to have multiple history instances with the same name active in different processes, - // e.g. when you have multiple shells open. We try to get that right and merge all their history - // together. Test that case. - const COUNT: usize = 3; - let name = L!("merge_test"); - let hists = [History::new(name), History::new(name), History::new(name)]; - let texts = [L!("History 1"), L!("History 2"), L!("History 3")]; - let alt_texts = [ - L!("History Alt 1"), - L!("History Alt 2"), - L!("History Alt 3"), - ]; - - // Make sure history is clear. - for hist in &hists { - hist.clear(); - } - - // Make sure we don't add an item in the same second as we created the history. - time_barrier(); - - // Add a different item to each. - for i in 0..COUNT { - hists[i].add_commandline(texts[i].to_owned()); - } - - // Save them. - for hist in &hists { - hist.save() - } - - // Make sure each history contains what it ought to, but they have not leaked into each other. - #[allow(clippy::needless_range_loop)] - for i in 0..COUNT { - for j in 0..COUNT { - let does_contain = history_contains(&hists[i], texts[j]); - let should_contain = i == j; - assert_eq!(should_contain, does_contain); - } - } - - // Make a new history. It should contain everything. The time_barrier() is so that the timestamp - // is newer, since we only pick up items whose timestamp is before the birth stamp. - time_barrier(); - let everything = History::new(name); - for text in texts { - assert!(history_contains(&everything, text)); - } - - // Tell all histories to merge. Now everybody should have everything. - for hist in &hists { - hist.incorporate_external_changes(); - } - - // Everyone should also have items in the same order (#2312) - let hist_vals1 = hists[0].get_history(); - for hist in &hists { - assert_eq!(hist_vals1, hist.get_history()); - } - - // Add some more per-history items. - for i in 0..COUNT { - hists[i].add_commandline(alt_texts[i].to_owned()); - } - // Everybody should have old items, but only one history should have each new item. - #[allow(clippy::needless_range_loop)] - for i in 0..COUNT { - for j in 0..COUNT { - // Old item. - assert!(history_contains(&hists[i], texts[j])); - - // New item. - let does_contain = history_contains(&hists[i], alt_texts[j]); - let should_contain = i == j; - assert_eq!(should_contain, does_contain); - } - } - - // Make sure incorporate_external_changes doesn't drop items! (#3496) - let writer = &hists[0]; - let reader = &hists[1]; - let more_texts = [ - L!("Item_#3496_1"), - L!("Item_#3496_2"), - L!("Item_#3496_3"), - L!("Item_#3496_4"), - L!("Item_#3496_5"), - L!("Item_#3496_6"), - ]; - for i in 0..more_texts.len() { - // time_barrier because merging will ignore items that may be newer - if i > 0 { - time_barrier(); - } - writer.add_commandline(more_texts[i].to_owned()); - writer.incorporate_external_changes(); - reader.incorporate_external_changes(); - for text in more_texts.iter().take(i) { - assert!(history_contains(reader, text)); - } - } - everything.clear(); -} - -#[test] -#[serial] -fn test_history_path_detection() { - let _cleanup = test_init(); - // Regression test for #7582. - let tmpdirbuff = CString::new("/tmp/fish_test_history.XXXXXX").unwrap(); - let tmpdir = unsafe { libc::mkdtemp(tmpdirbuff.into_raw()) }; - let tmpdir = unsafe { CString::from_raw(tmpdir) }; - let mut tmpdir = bytes2wcstring(tmpdir.to_bytes()); - if !tmpdir.ends_with('/') { - tmpdir.push('/'); - } - - // Place one valid file in the directory. - let filename = L!("testfile"); - std::fs::write(wcs2osstring(&(tmpdir.clone() + filename)), []).unwrap(); - - let test_vars = EnvStack::new(); - test_vars.set_one(L!("PWD"), EnvMode::GLOBAL, tmpdir.clone()); - test_vars.set_one(L!("HOME"), EnvMode::GLOBAL, tmpdir.clone()); - - let history = History::with_name(L!("path_detection")); - history.clear(); - assert_eq!(history.size(), 0); - history.clone().add_pending_with_file_detection( - L!("cmd0 not/a/valid/path"), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - &(L!("cmd1 ").to_owned() + filename), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - &(L!("cmd2 ").to_owned() + &tmpdir[..] + L!("/") + filename), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - &(L!("cmd3 $HOME/").to_owned() + filename), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - L!("cmd4 $HOME/notafile"), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - &(L!("cmd5 ~/").to_owned() + filename), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - L!("cmd6 ~/notafile"), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - L!("cmd7 ~/*f*"), - &test_vars, - history::PersistenceMode::Disk, - ); - history.clone().add_pending_with_file_detection( - L!("cmd8 ~/*zzz*"), - &test_vars, - history::PersistenceMode::Disk, - ); - history.resolve_pending(); - - const hist_size: usize = 9; - assert_eq!(history.size(), 9); - - // Expected sets of paths. - let expected_paths = [ - vec![], // cmd0 - vec![filename.to_owned()], // cmd1 - vec![tmpdir.clone() + L!("/") + filename], // cmd2 - vec![L!("$HOME/").to_owned() + filename], // cmd3 - vec![], // cmd4 - vec![L!("~/").to_owned() + filename], // cmd5 - vec![], // cmd6 - vec![], // cmd7 - we do not expand globs - vec![], // cmd8 - ]; - - let maxlap = 128; - for _lap in 0..maxlap { - let mut failures = 0; - for i in 1..=hist_size { - if history.item_at_index(i).unwrap().get_required_paths() - != expected_paths[hist_size - i] - { - failures += 1; - } - } - if failures == 0 { - break; - } - // The file detection takes a little time since it occurs in the background. - // Loop until the test passes. - std::thread::sleep(std::time::Duration::from_millis(2)); - } - history.clear(); - let _ = std::fs::remove_dir_all(wcs2osstring(&tmpdir)); -} - -fn install_sample_history(name: &wstr) { - let path = path_get_data().expect("Failed to get data directory"); - std::fs::copy( - workspace_root() - .join("tests") - .join(std::str::from_utf8(&wcs2bytes(name)).unwrap()), - wcs2osstring(&(path + L!("/") + name + L!("_history"))), - ) - .unwrap(); -} - -#[test] -#[serial] -fn test_history_formats() { - let _cleanup = test_init(); - // Test inferring and reading legacy and bash history formats. - let name = L!("history_sample_fish_2_0"); - install_sample_history(name); - let expected: Vec = vec![ - "echo this has\\\nbackslashes".into(), - "function foo\necho bar\nend".into(), - "echo alpha".into(), - ]; - let test_history_imported = History::with_name(name); - assert_eq!(test_history_imported.get_history(), expected); - test_history_imported.clear(); - - // Test bash import - // The results are in the reverse order that they appear in the bash history file. - // We don't expect whitespace to be elided (#4908: except for leading/trailing whitespace) - let expected: Vec = vec![ - "EOF".into(), - "sleep 123".into(), - "posix_cmd_sub $(is supported but only splits on newlines)".into(), - "posix_cmd_sub \"$(is supported)\"".into(), - "a && echo valid construct".into(), - "final line".into(), - "echo supsup".into(), - "export XVAR='exported'".into(), - "history --help".into(), - "echo foo".into(), - ]; - let test_history_imported_from_bash = History::with_name(L!("bash_import")); - let file = std::fs::File::open(workspace_root().join("tests/history_sample_bash")).unwrap(); - test_history_imported_from_bash.populate_from_bash(BufReader::new(file)); - assert_eq!(test_history_imported_from_bash.get_history(), expected); - test_history_imported_from_bash.clear(); - - let name = L!("history_sample_corrupt1"); - install_sample_history(name); - // We simply invoke get_string_representation. If we don't die, the test is a success. - let test_history_imported_from_corrupted = History::with_name(name); - let expected: Vec = vec![ - "no_newline_at_end_of_file".into(), - "corrupt_prefix".into(), - "this_command_is_ok".into(), - ]; - assert_eq!(test_history_imported_from_corrupted.get_history(), expected); - test_history_imported_from_corrupted.clear(); -} diff --git a/src/tests/input.rs b/src/tests/input.rs deleted file mode 100644 index 283223844..000000000 --- a/src/tests/input.rs +++ /dev/null @@ -1,65 +0,0 @@ -use crate::env::EnvStack; -use crate::input::{DEFAULT_BIND_MODE, EventQueuePeeker, InputMappingSet, KeyNameStyle}; -use crate::input_common::{CharEvent, InputData, InputEventQueuer, KeyEvent}; -use crate::key::Key; -use crate::wchar::prelude::*; - -struct TestInputEventQueuer { - input_data: InputData, -} - -impl InputEventQueuer for TestInputEventQueuer { - fn get_input_data(&self) -> &InputData { - &self.input_data - } - fn get_input_data_mut(&mut self) -> &mut InputData { - &mut self.input_data - } -} - -#[test] -fn test_input() { - let vars = EnvStack::new(); - let mut input = TestInputEventQueuer { - input_data: InputData::new(i32::MAX, None), // value doesn't matter since we don't read from it - }; - // Ensure sequences are order independent. Here we add two bindings where the first is a prefix - // of the second, and then emit the second key list. The second binding should be invoked, not - // the first! - let prefix_binding: Vec = "qqqqqqqa".chars().map(Key::from_raw).collect(); - let mut desired_binding = prefix_binding.clone(); - desired_binding.push(Key::from_raw('a')); - - let default_mode = || DEFAULT_BIND_MODE.to_owned(); - - let mut input_mappings = InputMappingSet::default(); - input_mappings.add1( - prefix_binding, - KeyNameStyle::Plain, - WString::from_str("up-line"), - default_mode(), - None, - true, - ); - input_mappings.add1( - desired_binding.clone(), - KeyNameStyle::Plain, - WString::from_str("down-line"), - default_mode(), - None, - true, - ); - - // Push the desired binding to the queue. - for key in desired_binding { - input - .input_data - .queue_char(CharEvent::from_key(KeyEvent::from(key))); - } - - let mut peeker = EventQueuePeeker::new(&mut input); - let mapping = peeker.find_mapping(&vars, &input_mappings); - assert!(mapping.is_some()); - assert!(mapping.unwrap().commands == ["down-line"]); - peeker.restart(); -} diff --git a/src/tests/input_common.rs b/src/tests/input_common.rs deleted file mode 100644 index ed0182b64..000000000 --- a/src/tests/input_common.rs +++ /dev/null @@ -1,59 +0,0 @@ -use crate::input_common::{CharEvent, InputEventQueue, InputEventQueuer, ReadlineCmd}; - -#[test] -fn test_push_front_back() { - let mut queue = InputEventQueue::new(0, None); - queue.push_front(CharEvent::from_char('a')); - queue.push_front(CharEvent::from_char('b')); - queue.push_back(CharEvent::from_char('c')); - queue.push_back(CharEvent::from_char('d')); - assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'c'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'd'); - assert!(queue.try_pop().is_none()); -} - -#[test] -fn test_promote_interruptions_to_front() { - let mut queue = InputEventQueue::new(0, None); - queue.push_back(CharEvent::from_char('a')); - queue.push_back(CharEvent::from_char('b')); - queue.push_back(CharEvent::from_readline(ReadlineCmd::Undo)); - queue.push_back(CharEvent::from_readline(ReadlineCmd::Redo)); - queue.push_back(CharEvent::from_char('c')); - queue.push_back(CharEvent::from_char('d')); - queue.promote_interruptions_to_front(); - - assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Undo); - assert_eq!(queue.try_pop().unwrap().get_readline(), ReadlineCmd::Redo); - assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'c'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'd'); - assert!(!queue.has_lookahead()); - - queue.push_back(CharEvent::from_char('e')); - queue.promote_interruptions_to_front(); - assert_eq!(queue.try_pop().unwrap().get_char(), 'e'); - assert!(!queue.has_lookahead()); -} - -#[test] -fn test_insert_front() { - let mut queue = InputEventQueue::new(0, None); - queue.push_back(CharEvent::from_char('a')); - queue.push_back(CharEvent::from_char('b')); - - let events = vec![ - CharEvent::from_char('A'), - CharEvent::from_char('B'), - CharEvent::from_char('C'), - ]; - queue.insert_front(events); - assert_eq!(queue.try_pop().unwrap().get_char(), 'A'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'B'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'C'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'a'); - assert_eq!(queue.try_pop().unwrap().get_char(), 'b'); -} diff --git a/src/tests/key.rs b/src/tests/key.rs deleted file mode 100644 index 4c6e4c960..000000000 --- a/src/tests/key.rs +++ /dev/null @@ -1,19 +0,0 @@ -use crate::key::{self, Key, ctrl, function_key, parse_keys}; -use crate::wchar::prelude::*; - -#[test] -fn test_parse_key() { - assert_eq!( - parse_keys(L!("escape")), - Ok(vec![Key::from_raw(key::Escape)]) - ); - assert_eq!(parse_keys(L!("\x1b")), Ok(vec![Key::from_raw(key::Escape)])); - assert_eq!(parse_keys(L!("ctrl-a")), Ok(vec![ctrl('a')])); - assert_eq!(parse_keys(L!("\x01")), Ok(vec![ctrl('a')])); - assert!(parse_keys(L!("f0")).is_err()); - assert_eq!( - parse_keys(L!("f1")), - Ok(vec![Key::from_raw(function_key(1))]) - ); - assert!(parse_keys(L!("F1")).is_err()); -} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index b0de79c17..b9d720933 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,121 +1 @@ -mod abbrs; -mod ast; -mod ast_bench; -mod common; -mod complete; -mod debounce; -mod editable_line; -mod encoding; -mod env; -mod env_universal_common; -mod expand; -mod fd_monitor; -mod history; -mod input; -mod input_common; -mod key; -mod pager; -mod parse_util; -mod parser; -mod reader; -mod redirection; -mod screen; -mod std; -mod string_escape; -mod termsize; -mod threads; -mod tokenizer; -mod topic_monitor; -mod wgetopt; - -pub mod prelude { - use crate::common::{BUILD_DIR, ScopeGuard, ScopeGuarding}; - use crate::env::env_init; - use crate::parser::{CancelBehavior, Parser}; - use crate::reader::{reader_deinit, reader_init}; - use crate::signal::signal_reset_handlers; - pub use crate::tests::env::{PwdEnvironment, TestEnvironment}; - use crate::topic_monitor::topic_monitor_init; - use crate::wutil::wgetcwd; - use crate::{env::EnvStack, proc::proc_init}; - use once_cell::sync::OnceCell; - use std::cell::RefCell; - use std::env::set_current_dir; - use std::ffi::CString; - use std::path::PathBuf; - - /// A wrapper around a Parser with some test helpers. - pub struct TestParser { - parser: Parser, - pushed_dirs: RefCell>, - } - - impl TestParser { - pub fn new() -> TestParser { - TestParser { - parser: Parser::new(EnvStack::new(), CancelBehavior::default()), - pushed_dirs: RefCell::new(Vec::new()), - } - } - - /// Helper to chdir and then update $PWD. - pub fn pushd(&self, path: &str) { - let cwd = wgetcwd(); - self.pushed_dirs.borrow_mut().push(cwd.to_string()); - - // We might need to create the directory. We don't care if this fails due to the directory - // already being present. - std::fs::create_dir_all(path).unwrap(); - - std::env::set_current_dir(path).unwrap(); - self.parser.vars().set_pwd_from_getcwd(); - } - - pub fn popd(&self) { - let old_cwd = self.pushed_dirs.borrow_mut().pop().unwrap(); - std::env::set_current_dir(old_cwd).unwrap(); - self.parser.vars().set_pwd_from_getcwd(); - } - } - - impl std::ops::Deref for TestParser { - type Target = Parser; - fn deref(&self) -> &Self::Target { - &self.parser - } - } - - pub fn test_init() -> impl ScopeGuarding { - static DONE: OnceCell<()> = OnceCell::new(); - DONE.get_or_init(|| { - // If we are building with `cargo build` and have build w/ `cmake`, this might not - // yet exist. - let mut test_dir = PathBuf::from(BUILD_DIR); - test_dir.push("fish-test"); - std::fs::create_dir_all(&test_dir).unwrap(); - set_current_dir(&test_dir).unwrap(); - { - let s = CString::new("").unwrap(); - unsafe { - libc::setlocale(libc::LC_ALL, s.as_ptr()); - } - } - topic_monitor_init(); - crate::threads::init(); - proc_init(); - env_init(None, true, false); - - // Set default signal handlers, so we can ctrl-C out of this. - signal_reset_handlers(); - - // Set PWD from getcwd - fixes #5599 - EnvStack::globals().set_pwd_from_getcwd(); - }); - reader_init(false); - ScopeGuard::new((), |()| { - reader_deinit(false); - }) - } - - pub use serial_test::serial; -} +pub mod prelude; diff --git a/src/tests/pager.rs b/src/tests/pager.rs deleted file mode 100644 index 6a0ebb181..000000000 --- a/src/tests/pager.rs +++ /dev/null @@ -1,208 +0,0 @@ -use crate::common::get_ellipsis_char; -use crate::complete::{CompleteFlags, Completion}; -use crate::pager::{Pager, SelectionMotion}; -use crate::termsize::Termsize; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; -use crate::wcstringutil::StringFuzzyMatch; - -#[test] -#[serial] -fn test_pager_navigation() { - let _cleanup = test_init(); - // Generate 19 strings of width 10. There's 2 spaces between completions, and our term size is - // 80; these can therefore fit into 6 columns (6 * 12 - 2 = 70) or 5 columns (58) but not 7 - // columns (7 * 12 - 2 = 82). - // - // You can simulate this test by creating 19 files named "file00.txt" through "file_18.txt". - let mut completions = vec![]; - for _ in 0..19 { - completions.push(Completion::new( - L!("abcdefghij").to_owned(), - "".into(), - StringFuzzyMatch::exact_match(), - CompleteFlags::default(), - )); - } - - let mut pager = Pager::default(); - pager.set_completions(&completions, true); - pager.set_term_size(&Termsize::defaults()); - let mut render = pager.render(); - - assert_eq!(render.term_width, Some(80)); - assert_eq!(render.term_height, Some(24)); - - let rows = 4; - let cols = 5; - - // We have 19 completions. We can fit into 6 columns with 4 rows or 5 columns with 4 rows; the - // second one is better and so is what we ought to have picked. - assert_eq!(render.rows, rows); - assert_eq!(render.cols, cols); - - // Initially expect to have no completion index. - assert!(render.selected_completion_idx.is_none()); - - // Here are navigation directions and where we expect the selection to be. - macro_rules! validate { - ($pager:ident, $render:ident, $dir:expr, $sel:expr) => { - $pager.select_next_completion_in_direction($dir, &$render); - $pager.update_rendering(&mut $render); - assert_eq!( - Some($sel), - $render.selected_completion_idx, - "For command {:?}", - $dir - ); - }; - } - - // Tab completion to get into the list. - validate!(pager, render, SelectionMotion::Next, 0); - // Westward motion in upper left goes to the last filled column in the last row. - validate!(pager, render, SelectionMotion::West, 15); - // East goes back. - validate!(pager, render, SelectionMotion::East, 0); - validate!(pager, render, SelectionMotion::West, 15); - validate!(pager, render, SelectionMotion::West, 11); - validate!(pager, render, SelectionMotion::East, 15); - validate!(pager, render, SelectionMotion::East, 0); - // "Next" motion goes down the column. - validate!(pager, render, SelectionMotion::Next, 1); - validate!(pager, render, SelectionMotion::Next, 2); - validate!(pager, render, SelectionMotion::West, 17); - validate!(pager, render, SelectionMotion::East, 2); - validate!(pager, render, SelectionMotion::East, 6); - validate!(pager, render, SelectionMotion::East, 10); - validate!(pager, render, SelectionMotion::East, 14); - validate!(pager, render, SelectionMotion::East, 18); - validate!(pager, render, SelectionMotion::West, 14); - validate!(pager, render, SelectionMotion::East, 18); - // Eastward motion wraps back to the upper left, westward goes to the prior column. - validate!(pager, render, SelectionMotion::East, 3); - validate!(pager, render, SelectionMotion::East, 7); - validate!(pager, render, SelectionMotion::East, 11); - validate!(pager, render, SelectionMotion::East, 15); - // Pages. - validate!(pager, render, SelectionMotion::PageNorth, 12); - validate!(pager, render, SelectionMotion::PageSouth, 15); - validate!(pager, render, SelectionMotion::PageNorth, 12); - validate!(pager, render, SelectionMotion::East, 16); - validate!(pager, render, SelectionMotion::PageSouth, 18); - validate!(pager, render, SelectionMotion::East, 3); - validate!(pager, render, SelectionMotion::North, 2); - validate!(pager, render, SelectionMotion::PageNorth, 0); - validate!(pager, render, SelectionMotion::PageSouth, 3); -} - -#[test] -#[serial] -fn test_pager_layout() { - let _cleanup = test_init(); - // These tests are woefully incomplete - // They only test the truncation logic for a single completion - - let rendered_line = |pager: &mut Pager, width: isize| { - pager.set_term_size(&Termsize::new(width, 24)); - let rendering = pager.render(); - let sd = &rendering.screen_data; - assert_eq!(sd.line_count(), 1); - let line = sd.line(0); - WString::from(Vec::from_iter((0..line.len()).map(|i| line.char_at(i)))) - }; - let compute_expected = |expected: &wstr| { - let ellipsis_char = get_ellipsis_char(); - if ellipsis_char != '\u{2026}' { - // hack: handle the case where ellipsis is not L'\x2026' - expected.replace(L!("\u{2026}"), wstr::from_char_slice(&[ellipsis_char])) - } else { - expected.to_owned() - } - }; - - macro_rules! validate { - ($pager:expr, $width:expr, $expected:expr) => { - assert_eq!( - rendered_line($pager, $width), - compute_expected($expected), - "width {}", - $width - ); - }; - } - - let mut pager = Pager::default(); - - // These test cases have equal completions and descriptions - let c1s = vec![Completion::new( - L!("abcdefghij").to_owned(), - L!("1234567890").to_owned(), - StringFuzzyMatch::exact_match(), - CompleteFlags::default(), - )]; - pager.set_completions(&c1s, true); - - validate!(&mut pager, 26, L!("abcdefghij (1234567890)")); - validate!(&mut pager, 25, L!("abcdefghij (1234567890)")); - validate!(&mut pager, 24, L!("abcdefghij (1234567890)")); - validate!(&mut pager, 23, L!("abcdefghij (12345678…)")); - validate!(&mut pager, 22, L!("abcdefghij (1234567…)")); - validate!(&mut pager, 21, L!("abcdefghij (123456…)")); - validate!(&mut pager, 20, L!("abcdefghij (12345…)")); - validate!(&mut pager, 19, L!("abcdefghij (1234…)")); - validate!(&mut pager, 18, L!("abcdefgh… (1234…)")); - validate!(&mut pager, 17, L!("abcdefg… (1234…)")); - validate!(&mut pager, 16, L!("abcdefg… (123…)")); - - // These test cases have heavyweight completions - let c2s = vec![Completion::new( - L!("abcdefghijklmnopqrs").to_owned(), - L!("1").to_owned(), - StringFuzzyMatch::exact_match(), - CompleteFlags::default(), - )]; - pager.set_completions(&c2s, true); - validate!(&mut pager, 26, L!("abcdefghijklmnopqrs (1)")); - validate!(&mut pager, 25, L!("abcdefghijklmnopqrs (1)")); - validate!(&mut pager, 24, L!("abcdefghijklmnopqrs (1)")); - validate!(&mut pager, 23, L!("abcdefghijklmnopq… (1)")); - validate!(&mut pager, 22, L!("abcdefghijklmnop… (1)")); - validate!(&mut pager, 21, L!("abcdefghijklmno… (1)")); - validate!(&mut pager, 20, L!("abcdefghijklmn… (1)")); - validate!(&mut pager, 19, L!("abcdefghijklm… (1)")); - validate!(&mut pager, 18, L!("abcdefghijkl… (1)")); - validate!(&mut pager, 17, L!("abcdefghijk… (1)")); - validate!(&mut pager, 16, L!("abcdefghij… (1)")); - - // These test cases have no descriptions - let c3s = vec![Completion::new( - L!("abcdefghijklmnopqrst").to_owned(), - L!("").to_owned(), - StringFuzzyMatch::exact_match(), - CompleteFlags::default(), - )]; - pager.set_completions(&c3s, true); - validate!(&mut pager, 26, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 25, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 24, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 23, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 22, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 21, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 20, L!("abcdefghijklmnopqrst")); - validate!(&mut pager, 19, L!("abcdefghijklmnopqr…")); - validate!(&mut pager, 18, L!("abcdefghijklmnopq…")); - validate!(&mut pager, 17, L!("abcdefghijklmnop…")); - validate!(&mut pager, 16, L!("abcdefghijklmno…")); - - // Newlines in prefix - let c4s = vec![Completion::new( - L!("Hello").to_owned(), - L!("").to_owned(), - StringFuzzyMatch::exact_match(), - CompleteFlags::default(), - )]; - pager.set_prefix(L!("{\\\n"), false); // } - pager.set_completions(&c4s, true); - validate!(&mut pager, 30, L!("{\\␊Hello")); // } -} diff --git a/src/tests/parse_util.rs b/src/tests/parse_util.rs deleted file mode 100644 index 51d9a79bf..000000000 --- a/src/tests/parse_util.rs +++ /dev/null @@ -1,450 +0,0 @@ -use pcre2::utf32::Regex; - -use crate::common::EscapeFlags; -use crate::parse_constants::{ - ERROR_BAD_VAR_CHAR1, ERROR_BRACKETED_VARIABLE_QUOTED1, ERROR_BRACKETED_VARIABLE1, - ERROR_NO_VAR_NAME, ERROR_NOT_ARGV_AT, ERROR_NOT_ARGV_COUNT, ERROR_NOT_ARGV_STAR, ERROR_NOT_PID, - ERROR_NOT_STATUS, -}; -use crate::parse_util::{ - BOOL_AFTER_BACKGROUND_ERROR_MSG, parse_util_cmdsubst_extent, parse_util_compute_indents, - parse_util_detect_errors, parse_util_escape_string_with_quote, parse_util_process_extent, - parse_util_slice_length, -}; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; - -#[test] -#[serial] -fn test_error_messages() { - let _cleanup = test_init(); - // Given a format string, returns a list of non-empty strings separated by format specifiers. The - // format specifiers themselves are omitted. - fn separate_by_format_specifiers(format: &wstr) -> Vec<&wstr> { - let format_specifier_regex = Regex::new(L!(r"%[cds]").as_char_slice()).unwrap(); - let mut result = vec![]; - let mut offset = 0; - for mtch in format_specifier_regex.find_iter(format.as_char_slice()) { - let mtch = mtch.unwrap(); - let component = &format[offset..mtch.start()]; - result.push(component); - offset = mtch.end(); - } - result.push(&format[offset..]); - // Avoid mismatch from localized quotes. - for component in &mut result { - *component = component.trim_matches('\''); - } - result - } - - // Given a format string 'format', return true if the string may have been produced by that format - // string. We do this by splitting the format string around the format specifiers, and then ensuring - // that each of the remaining chunks is found (in order) in the string. - fn string_matches_format(s: &wstr, format: &wstr) -> bool { - let components = separate_by_format_specifiers(format); - assert!(!components.is_empty()); - let mut idx = 0; - for component in components { - let Some(relpos) = s[idx..].find(component) else { - return false; - }; - idx += relpos + component.len(); - assert!(idx <= s.len()); - } - true - } - - macro_rules! validate { - ($src:expr, $error_text_format:expr) => { - let mut errors = vec![]; - let res = parse_util_detect_errors(L!($src), Some(&mut errors), false); - let fmt = wgettext!($error_text_format); - assert!(res.is_err()); - assert!( - string_matches_format(&errors[0].text, fmt), - "command '{}' is expected to match error pattern '{}' but is '{}'", - $src, - $error_text_format.localize(), - &errors[0].text - ); - }; - } - - validate!("echo $^", ERROR_BAD_VAR_CHAR1); - validate!("echo foo${a}bar", ERROR_BRACKETED_VARIABLE1); - validate!("echo foo\"${a}\"bar", ERROR_BRACKETED_VARIABLE_QUOTED1); - validate!("echo foo\"${\"bar", ERROR_BAD_VAR_CHAR1); - validate!("echo $?", ERROR_NOT_STATUS); - validate!("echo $$", ERROR_NOT_PID); - validate!("echo $#", ERROR_NOT_ARGV_COUNT); - validate!("echo $@", ERROR_NOT_ARGV_AT); - validate!("echo $*", ERROR_NOT_ARGV_STAR); - validate!("echo $", ERROR_NO_VAR_NAME); - validate!("echo foo\"$\"bar", ERROR_NO_VAR_NAME); - validate!("echo \"foo\"$\"bar\"", ERROR_NO_VAR_NAME); - validate!("echo foo $ bar", ERROR_NO_VAR_NAME); - validate!("echo 1 & && echo 2", BOOL_AFTER_BACKGROUND_ERROR_MSG); - validate!( - "echo 1 && echo 2 & && echo 3", - BOOL_AFTER_BACKGROUND_ERROR_MSG - ); -} - -#[test] -fn test_parse_util_process_extent() { - macro_rules! validate { - ($commandline:literal, $cursor:expr, $expected_range:expr) => { - assert_eq!( - parse_util_process_extent(L!($commandline), $cursor, None), - $expected_range - ); - }; - } - validate!("for file in (path base\necho", 22, 13..22); - validate!("begin\n\n\nec", 10, 6..10); - validate!("begin; echo; end", 12, 12..16); -} - -#[test] -#[serial] -fn test_parse_util_cmdsubst_extent() { - let _cleanup = test_init(); - const a: &wstr = L!("echo (echo (echo hi"); - assert_eq!(parse_util_cmdsubst_extent(a, 0), 0..a.len()); - assert_eq!(parse_util_cmdsubst_extent(a, 1), 0..a.len()); - assert_eq!(parse_util_cmdsubst_extent(a, 2), 0..a.len()); - assert_eq!(parse_util_cmdsubst_extent(a, 3), 0..a.len()); - assert_eq!( - parse_util_cmdsubst_extent(a, 8), - "echo (".chars().count()..a.len() - ); - assert_eq!( - parse_util_cmdsubst_extent(a, 17), - "echo (echo (".chars().count()..a.len() - ); -} - -#[test] -#[serial] -fn test_parse_util_slice_length() { - let _cleanup = test_init(); - assert_eq!(parse_util_slice_length(L!("[2]")), Some(3)); - assert_eq!(parse_util_slice_length(L!("[12]")), Some(4)); - assert_eq!(parse_util_slice_length(L!("[\"foo\"]")), Some(7)); - assert_eq!(parse_util_slice_length(L!("[\"foo\"")), None); -} - -#[test] -#[serial] -fn test_escape_quotes() { - let _cleanup = test_init(); - macro_rules! validate { - ($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => { - assert_eq!( - parse_util_escape_string_with_quote( - L!($cmd), - $quote, - if $no_tilde { - EscapeFlags::NO_TILDE - } else { - EscapeFlags::empty() - } - ), - L!($expected) - ); - }; - } - macro_rules! validate_no_quoted { - ($cmd:expr, $quote:expr, $no_tilde:expr, $expected:expr) => { - assert_eq!( - parse_util_escape_string_with_quote( - L!($cmd), - $quote, - EscapeFlags::NO_QUOTED - | if $no_tilde { - EscapeFlags::NO_TILDE - } else { - EscapeFlags::empty() - } - ), - L!($expected) - ); - }; - } - - validate!("abc~def", None, false, "'abc~def'"); - validate!("abc~def", None, true, "abc~def"); - validate!("~abc", None, false, "'~abc'"); - validate!("~abc", None, true, "~abc"); - - // These are "raw string literals" - validate_no_quoted!("abc", None, false, "abc"); - validate_no_quoted!("abc~def", None, false, "abc\\~def"); - validate_no_quoted!("abc~def", None, true, "abc~def"); - validate_no_quoted!("abc\\~def", None, false, "abc\\\\\\~def"); - validate_no_quoted!("abc\\~def", None, true, "abc\\\\~def"); - validate_no_quoted!("~abc", None, false, "\\~abc"); - validate_no_quoted!("~abc", None, true, "~abc"); - validate_no_quoted!("~abc|def", None, false, "\\~abc\\|def"); - validate_no_quoted!("|abc~def", None, false, "\\|abc\\~def"); - validate_no_quoted!("|abc~def", None, true, "\\|abc~def"); - validate_no_quoted!("foo\nbar", None, false, "foo\\nbar"); - - // Note tildes are not expanded inside quotes, so no_tilde is ignored with a quote. - validate_no_quoted!("abc", Some('\''), false, "abc"); - validate_no_quoted!("abc\\def", Some('\''), false, "abc\\\\def"); - validate_no_quoted!("abc'def", Some('\''), false, "abc\\'def"); - validate_no_quoted!("~abc'def", Some('\''), false, "~abc\\'def"); - validate_no_quoted!("~abc'def", Some('\''), true, "~abc\\'def"); - validate_no_quoted!("foo\nba'r", Some('\''), false, "foo'\\n'ba\\'r"); - validate_no_quoted!("foo\\\\bar", Some('\''), false, "foo\\\\\\\\bar"); - - validate_no_quoted!("abc", Some('"'), false, "abc"); - validate_no_quoted!("abc\\def", Some('"'), false, "abc\\\\def"); - validate_no_quoted!("~abc'def", Some('"'), false, "~abc'def"); - validate_no_quoted!("~abc'def", Some('"'), true, "~abc'def"); - validate_no_quoted!("foo\nba'r", Some('"'), false, "foo\"\\n\"ba'r"); - validate_no_quoted!("foo\\\\bar", Some('"'), false, "foo\\\\\\\\bar"); -} - -#[test] -#[serial] -fn test_indents() { - let _cleanup = test_init(); - // A struct which is either text or a new indent. - struct Segment { - // The indent to set - indent: i32, - text: &'static str, - } - fn do_validate(segments: &[Segment]) { - // Compute the indents. - let mut expected_indents = vec![]; - let mut text = WString::new(); - for segment in segments { - text.push_str(segment.text); - for _ in segment.text.chars() { - expected_indents.push(segment.indent); - } - } - let indents = parse_util_compute_indents(&text); - assert_eq!(indents, expected_indents); - } - macro_rules! validate { - ( $( $(,)? $indent:literal, $text:literal )* $(,)? ) => { - let segments = vec![ - $( - Segment{ indent: $indent, text: $text }, - )* - ]; - do_validate(&segments); - }; - } - - #[rustfmt::skip] - #[allow(clippy::redundant_closure_call)] - (|| { - validate!( - 0, "if", 1, " foo", - 0, "\nend" - ); - validate!( - 0, "if", 1, " foo", - 1, "\nfoo", - 0, "\nend" - ); - - validate!( - 0, "if", 1, " foo", - 1, "\nif", 2, " bar", - 1, "\nend", - 0, "\nend" - ); - - validate!( - 0, "if", 1, " foo", - 1, "\nif", 2, " bar", - 2, "\n", - 1, "\nend\n" - ); - - validate!( - 0, "if", 1, " foo", - 1, "\nif", 2, " bar", - 2, "\n" - ); - - validate!( - 0, "begin", - 1, "\nfoo", - 1, "\n" - ); - - validate!( - 0, "begin", - 1, "\n;", - 0, "end", - 0, "\nfoo", 0, "\n" - ); - - validate!( - 0, "begin", - 1, "\n;", - 0, "end", - 0, "\nfoo", 0, "\n" - ); - - validate!( - 0, "if", 1, " foo", - 1, "\nif", 2, " bar", - 2, "\nbaz", - 1, "\nend", 1, "\n" - ); - - validate!( - 0, "switch foo", - 1, "\n" - ); - - validate!( - 0, "switch foo", - 1, "\ncase bar", - 1, "\ncase baz", - 2, "\nquux", - 2, "\nquux" - ); - - validate!( - 0, - "switch foo", - 1, - "\ncas" // parse error indentation handling - ); - - validate!( - 0, "while", - 1, " false", - 1, "\n# comment", // comment indentation handling - 1, "\ncommand", - 1, "\n# comment 2" - ); - - validate!( - 0, "begin", - 1, "\n", // "begin" is special because this newline belongs to the block header - 1, "\n" - ); - - // Continuation lines. - validate!( - 0, "echo 'continuation line' \\", - 1, "\ncont", - 0, "\n" - ); - validate!( - 0, "echo 'empty continuation line' \\", - 1, "\n" - ); - validate!( - 0, "begin # continuation line in block", - 1, "\necho \\", - 2, "\ncont" - ); - validate!( - 0, "begin # empty continuation line in block", - 1, "\necho \\", - 2, "\n", - 0, "\nend" - ); - validate!( - 0, "echo 'multiple continuation lines' \\", - 1, "\nline1 \\", - 1, "\n# comment", - 1, "\n# more comment", - 1, "\nline2 \\", - 1, "\n" - ); - validate!( - 0, "echo # inline comment ending in \\", - 0, "\nline" - ); - validate!( - 0, "# line comment ending in \\", - 0, "\nline" - ); - validate!( - 0, "echo 'multiple empty continuation lines' \\", - 1, "\n\\", - 1, "\n", - 0, "\n" - ); - validate!( - 0, "echo 'multiple statements with continuation lines' \\", - 1, "\nline 1", - 0, "\necho \\", - 1, "\n" - ); - // This is an edge case, probably okay to change the behavior here. - validate!( - 0, "begin", - 1, " \\", - 2, "\necho 'continuation line in block header' \\", - 2, "\n", - 1, "\n", - 0, "\nend" - ); - validate!( - 0, "if", 1, " true", - 1, "\n begin", - 2, "\n echo", - 1, "\n end", - 0, "\nend", - ); - - // Quotes and command substitutions. - validate!( - 0, "if", 1, " foo \"", - 0, "\nquoted", - ); - validate!( - 0, "if", 1, " foo \"", - 0, "\n", - ); - validate!( - 0, "echo (", - 1, "\n", // ) - ); - validate!( - 0, "echo \"$(", - 1, "\n" // ) - ); - validate!( - 0, "echo (", // ) - 1, "\necho \"", - 0, "\n" - ); - validate!( - 0, "echo (", // ) - 1, "\necho (", // ) - 2, "\necho" - ); - validate!( - 0, "if", 1, " true", - 1, "\n echo \"line1", - 0, "\nline2 ", 1, "$(", - 2, "\n echo line3", - 0, "\n) line4", - 0, "\nline5\"", - ); - validate!( - 0, r#"echo "$()"'"#, - 0, "\n" - ); - validate!( - 0, r#"""#, - 0, "\n", - 0, r#"$()"$() ""# - ); - })(); -} diff --git a/src/tests/parser.rs b/src/tests/parser.rs deleted file mode 100644 index 6b010c2cd..000000000 --- a/src/tests/parser.rs +++ /dev/null @@ -1,979 +0,0 @@ -use crate::ast::{self, Ast, Castable, JobList, JobPipeline, Kind, Node, Traversal, is_same_node}; -use crate::env::EnvStack; -use crate::expand::ExpandFlags; -use crate::io::{IoBufferfill, IoChain}; -use crate::parse_constants::{ - ParseErrorCode, ParseTokenType, ParseTreeFlags, ParserTestErrorBits, StatementDecoration, -}; -use crate::parse_tree::{LineCounter, parse_source}; -use crate::parse_util::{parse_util_detect_errors, parse_util_detect_errors_in_argument}; -use crate::parser::{CancelBehavior, Parser}; -use crate::reader::{fake_scoped_reader, reader_reset_interrupted}; -use crate::signal::{signal_clear_cancel, signal_reset_handlers, signal_set_handlers}; -use crate::tests::prelude::*; -use crate::threads::iothread_perform; -use crate::wchar::prelude::*; -use crate::wcstringutil::join_strings; -use libc::SIGINT; -use std::time::Duration; - -#[test] -#[serial] -fn test_parser() { - let _cleanup = test_init(); - macro_rules! detect_errors { - ($src:literal) => { - parse_util_detect_errors(L!($src), None, true /* accept incomplete */) - }; - } - - fn detect_argument_errors(src: &str) -> Result<(), ParserTestErrorBits> { - let src = WString::from_str(src); - let ast = ast::parse_argument_list(&src, ParseTreeFlags::default(), None); - if ast.errored() { - return Err(ParserTestErrorBits::ERROR); - } - let args = &ast.top().arguments; - let first_arg = args.first().expect("Failed to parse an argument"); - let mut errors = None; - parse_util_detect_errors_in_argument(first_arg, first_arg.source(&src), &mut errors) - } - - // Testing block nesting - assert!( - detect_errors!("if; end").is_err(), - "Incomplete if statement undetected" - ); - assert!( - detect_errors!("if test; echo").is_err(), - "Missing end undetected" - ); - assert!( - detect_errors!("if test; end; end").is_err(), - "Unbalanced end undetected" - ); - - // Testing detection of invalid use of builtin commands - assert!( - detect_errors!("case foo").is_err(), - "'case' command outside of block context undetected" - ); - assert!( - detect_errors!("switch ggg; if true; case foo;end;end").is_err(), - "'case' command outside of switch block context undetected" - ); - assert!( - detect_errors!("else").is_err(), - "'else' command outside of conditional block context undetected" - ); - assert!( - detect_errors!("else if").is_err(), - "'else if' command outside of conditional block context undetected" - ); - assert!( - detect_errors!("if false; else if; end").is_err(), - "'else if' missing command undetected" - ); - - assert!( - detect_errors!("break").is_err(), - "'break' command outside of loop block context undetected" - ); - - assert!( - detect_errors!("break --help").is_ok(), - "'break --help' incorrectly marked as error" - ); - - assert!( - detect_errors!("while false ; function foo ; break ; end ; end ").is_err(), - "'break' command inside function allowed to break from loop outside it" - ); - - assert!( - detect_errors!("exec ls|less").is_err() && detect_errors!("echo|return").is_err(), - "Invalid pipe command undetected" - ); - - assert!( - detect_errors!("for i in foo ; switch $i ; case blah ; break; end; end ").is_ok(), - "'break' command inside switch falsely reported as error" - ); - - assert!( - detect_errors!("or cat | cat").is_ok() && detect_errors!("and cat | cat").is_ok(), - "boolean command at beginning of pipeline falsely reported as error" - ); - - assert!( - detect_errors!("cat | and cat").is_err(), - "'and' command in pipeline not reported as error" - ); - - assert!( - detect_errors!("cat | or cat").is_err(), - "'or' command in pipeline not reported as error" - ); - - assert!( - detect_errors!("cat | exec").is_err() && detect_errors!("exec | cat").is_err(), - "'exec' command in pipeline not reported as error" - ); - - assert!( - detect_errors!("begin ; end arg").is_err(), - "argument to 'end' not reported as error" - ); - - assert!( - detect_errors!("switch foo ; end arg").is_err(), - "argument to 'end' not reported as error" - ); - - assert!( - detect_errors!("if true; else if false ; end arg").is_err(), - "argument to 'end' not reported as error" - ); - - assert!( - detect_errors!("if true; else ; end arg").is_err(), - "argument to 'end' not reported as error" - ); - - assert!( - detect_errors!("begin ; end 2> /dev/null").is_ok(), - "redirection after 'end' wrongly reported as error" - ); - - assert!( - detect_errors!("true | ") == Err(ParserTestErrorBits::INCOMPLETE), - "unterminated pipe not reported properly" - ); - - assert!( - detect_errors!("echo (\nfoo\n bar") == Err(ParserTestErrorBits::INCOMPLETE), - "unterminated multiline subshell not reported properly" - ); - - assert!( - detect_errors!("begin ; true ; end | ") == Err(ParserTestErrorBits::INCOMPLETE), - "unterminated pipe not reported properly" - ); - - assert!( - detect_errors!(" | true ") == Err(ParserTestErrorBits::ERROR), - "leading pipe not reported properly" - ); - - assert!( - detect_errors!("true | # comment") == Err(ParserTestErrorBits::INCOMPLETE), - "comment after pipe not reported as incomplete" - ); - - assert!( - detect_errors!("true | # comment \n false ").is_ok(), - "comment and newline after pipe wrongly reported as error" - ); - - assert!( - detect_errors!("true | ; false ") == Err(ParserTestErrorBits::ERROR), - "semicolon after pipe not detected as error" - ); - - assert!( - detect_argument_errors("foo").is_ok(), - "simple argument reported as error" - ); - - assert!( - detect_argument_errors("''").is_ok(), - "Empty string reported as error" - ); - - assert!( - detect_argument_errors("foo$$") - .unwrap_err() - .contains(ParserTestErrorBits::ERROR), - "Bad variable expansion not reported as error" - ); - - assert!( - detect_argument_errors("foo$@") - .unwrap_err() - .contains(ParserTestErrorBits::ERROR), - "Bad variable expansion not reported as error" - ); - - // Within command substitutions, we should be able to detect everything that - // parse_util_detect_errors! can detect. - assert!( - detect_argument_errors("foo(cat | or cat)") - .unwrap_err() - .contains(ParserTestErrorBits::ERROR), - "Bad command substitution not reported as error" - ); - - assert!( - detect_errors!("false & ; and cat").is_err(), - "'and' command after background not reported as error" - ); - - assert!( - detect_errors!("true & ; or cat").is_err(), - "'or' command after background not reported as error" - ); - - assert!( - detect_errors!("true & ; not cat").is_ok(), - "'not' command after background falsely reported as error" - ); - - assert!( - detect_errors!("if true & ; end").is_err(), - "backgrounded 'if' conditional not reported as error" - ); - - assert!( - detect_errors!("if false; else if true & ; end").is_err(), - "backgrounded 'else if' conditional not reported as error" - ); - - assert!( - detect_errors!("while true & ; end").is_err(), - "backgrounded 'while' conditional not reported as error" - ); - - assert!( - detect_errors!("true | || false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("|| false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("&& false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("true ; && false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("true ; || false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("true || && false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("true && || false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("true && && false").is_err(), - "bogus boolean statement error not detected" - ); - - assert!( - detect_errors!("true && ") == Err(ParserTestErrorBits::INCOMPLETE), - "unterminated conjunction not reported properly" - ); - - assert!( - detect_errors!("true && \n true").is_ok(), - "newline after && reported as error" - ); - - assert!( - detect_errors!("true || \n") == Err(ParserTestErrorBits::INCOMPLETE), - "unterminated conjunction not reported properly" - ); - - assert!( - detect_errors!("begin ; echo hi; }") == Err(ParserTestErrorBits::ERROR), - "closing of unopened brace statement not reported properly" - ); - - assert_eq!( - detect_errors!("begin {"), // } - Err(ParserTestErrorBits::INCOMPLETE), - "brace after begin not reported properly" - ); - assert_eq!( - detect_errors!("a=b {"), // } - Err(ParserTestErrorBits::INCOMPLETE), - "brace after variable override not reported properly" - ); -} - -#[test] -#[serial] -fn test_new_parser_correctness() { - let _cleanup = test_init(); - macro_rules! validate { - ($src:expr, $ok:expr) => { - let ast = ast::parse(L!($src), ParseTreeFlags::default(), None); - assert_eq!(ast.errored(), !$ok); - }; - } - validate!("; ; ; ", true); - validate!("if ; end", false); - validate!("if true ; end", true); - validate!("if true; end ; end", false); - validate!("if end; end ; end", false); - validate!("if end", false); - validate!("end", false); - validate!("for i i", false); - validate!("for i in a b c ; end", true); - validate!("begin end", true); - validate!("begin; end", true); - validate!("begin if true; end; end;", true); - validate!("begin if true ; echo hi ; end; end", true); - validate!("true && false || false", true); - validate!("true || false; and true", true); - validate!("true || ||", false); - validate!("|| true", false); - validate!("true || \n\n false", true); -} - -#[test] -#[serial] -fn test_new_parser_correctness_by_fuzzing() { - let _cleanup = test_init(); - let fuzzes = [ - L!("if"), - L!("else"), - L!("for"), - L!("in"), - L!("while"), - L!("begin"), - L!("function"), - L!("switch"), - L!("case"), - L!("end"), - L!("and"), - L!("or"), - L!("not"), - L!("command"), - L!("builtin"), - L!("foo"), - L!("|"), - L!("^"), - L!("&"), - L!(";"), - ]; - - // Generate a list of strings of all keyword / token combinations. - let mut src = WString::new(); - src.reserve(128); - - // Given that we have an array of 'fuzz_count' strings, we wish to enumerate all permutations of - // 'len' values. We do this by incrementing an integer, interpreting it as "base fuzz_count". - fn string_for_permutation(fuzzes: &[&wstr], len: usize, permutation: usize) -> Option { - let mut remaining_permutation = permutation; - let mut out_str = WString::new(); - for _i in 0..len { - let idx = remaining_permutation % fuzzes.len(); - remaining_permutation /= fuzzes.len(); - out_str.push_utfstr(fuzzes[idx]); - out_str.push(' '); - } - // Return false if we wrapped. - (remaining_permutation == 0).then_some(out_str) - } - - let max_len = 5; - for len in 0..max_len { - // We wish to look at all permutations of 4 elements of 'fuzzes' (with replacement). - // Construct an int and keep incrementing it. - let mut permutation = 0; - while let Some(src) = string_for_permutation(&fuzzes, len, permutation) { - permutation += 1; - ast::parse(&src, ParseTreeFlags::default(), None); - } - } -} - -// Test the LL2 (two token lookahead) nature of the parser by exercising the special builtin and -// command handling. In particular, 'command foo' should be a decorated statement 'foo' but 'command -// -help' should be an undecorated statement 'command' with argument '--help', and NOT attempt to -// run a command called '--help'. -#[test] -#[serial] -fn test_new_parser_ll2() { - let _cleanup = test_init(); - // Parse a statement, returning the command, args (joined by spaces), and the decoration. Returns - // true if successful. - fn test_1_parse_ll2(src: &wstr) -> Option<(WString, WString, StatementDecoration)> { - let ast = ast::parse(src, ParseTreeFlags::default(), None); - if ast.errored() { - return None; - } - - // Get the statement. Should only have one. - let mut statement = None; - for n in Traversal::new(ast.top()) { - if let Kind::DecoratedStatement(tmp) = n.kind() { - assert!( - statement.is_none(), - "More than one decorated statement found in '{}'", - src - ); - statement = Some(tmp); - } - } - let statement = statement.expect("No decorated statement found"); - - // Return its decoration and command. - let out_deco = statement.decoration(); - let out_cmd = statement.command.source(src).to_owned(); - - // Return arguments separated by spaces. - let out_joined_args = join_strings( - &statement - .args_or_redirs - .iter() - .filter(|a| a.is_argument()) - .map(|a| a.source(src)) - .collect::>(), - ' ', - ); - - Some((out_cmd, out_joined_args, out_deco)) - } - macro_rules! validate { - ($src:expr, $cmd:expr, $args:expr, $deco:expr) => { - let (cmd, args, deco) = test_1_parse_ll2(L!($src)).unwrap(); - assert_eq!(cmd, L!($cmd)); - assert_eq!(args, L!($args)); - assert_eq!(deco, $deco); - }; - } - - validate!("echo hello", "echo", "hello", StatementDecoration::none); - validate!( - "command echo hello", - "echo", - "hello", - StatementDecoration::command - ); - validate!( - "exec echo hello", - "echo", - "hello", - StatementDecoration::exec - ); - validate!( - "command command hello", - "command", - "hello", - StatementDecoration::command - ); - validate!( - "builtin command hello", - "command", - "hello", - StatementDecoration::builtin - ); - validate!( - "command --help", - "command", - "--help", - StatementDecoration::none - ); - validate!("command -h", "command", "-h", StatementDecoration::none); - validate!("command", "command", "", StatementDecoration::none); - validate!("command -", "command", "-", StatementDecoration::none); - validate!("command --", "command", "--", StatementDecoration::none); - validate!( - "builtin --names", - "builtin", - "--names", - StatementDecoration::none - ); - validate!("function", "function", "", StatementDecoration::none); - validate!( - "function --help", - "function", - "--help", - StatementDecoration::none - ); - - // Verify that 'function -h' and 'function --help' are plain statements but 'function --foo' is - // not (issue #1240). - macro_rules! check_function_help { - ($src:expr, $kind:pat) => { - let ast = ast::parse(L!($src), ParseTreeFlags::default(), None); - assert!(!ast.errored()); - assert_eq!( - Traversal::new(ast.top()) - .filter(|n| matches!(n.kind(), $kind)) - .count(), - 1 - ); - }; - } - check_function_help!("function -h", ast::Kind::DecoratedStatement(_)); - check_function_help!("function --help", ast::Kind::DecoratedStatement(_)); - check_function_help!("function --foo; end", ast::Kind::FunctionHeader(_)); - check_function_help!("function foo; end", ast::Kind::FunctionHeader(_)); -} - -#[test] -#[serial] -fn test_new_parser_ad_hoc() { - let _cleanup = test_init(); - // Very ad-hoc tests for issues encountered. - - // Ensure that 'case' terminates a job list. - let src = L!("switch foo ; case bar; case baz; end"); - let ast = ast::parse(src, ParseTreeFlags::default(), None); - assert!(!ast.errored()); - // Expect two CaseItems. The bug was that we'd - // try to run a command 'case'. - assert_eq!( - Traversal::new(ast.top()) - .filter(|n| matches!(n.kind(), ast::Kind::CaseItem(_))) - .count(), - 2 - ); - - // Ensure that naked variable assignments don't hang. - // The bug was that "a=" would produce an error but not be consumed, - // leading to an infinite loop. - - // By itself it should produce an error. - let ast = ast::parse(L!("a="), ParseTreeFlags::default(), None); - assert!(ast.errored()); - - // If we are leaving things unterminated, this should not produce an error. - // i.e. when typing "a=" at the command line, it should be treated as valid - // because we don't want to color it as an error. - let ast = ast::parse(L!("a="), ParseTreeFlags::LEAVE_UNTERMINATED, None); - assert!(!ast.errored()); - - let mut errors = vec![]; - ast::parse( - L!("begin; echo ("), - ParseTreeFlags::LEAVE_UNTERMINATED, - Some(&mut errors), - ); - assert!(errors.len() == 1); - assert!(errors[0].code == ParseErrorCode::tokenizer_unterminated_subshell); - - errors.clear(); - ast::parse( - L!("for x in ("), - ParseTreeFlags::LEAVE_UNTERMINATED, - Some(&mut errors), - ); - assert!(errors.len() == 1); - assert!(errors[0].code == ParseErrorCode::tokenizer_unterminated_subshell); - - errors.clear(); - ast::parse( - L!("begin; echo '"), - ParseTreeFlags::LEAVE_UNTERMINATED, - Some(&mut errors), - ); - assert!(errors.len() == 1); - assert!(errors[0].code == ParseErrorCode::tokenizer_unterminated_quote); -} - -#[test] -#[serial] -fn test_new_parser_errors() { - let _cleanup = test_init(); - macro_rules! validate { - ($src:expr, $expected_code:expr) => { - let mut errors = vec![]; - let ast = ast::parse(L!($src), ParseTreeFlags::default(), Some(&mut errors)); - assert!(ast.errored()); - assert_eq!( - errors.into_iter().map(|e| e.code).collect::>(), - vec![$expected_code], - ); - }; - } - - validate!("echo 'abc", ParseErrorCode::tokenizer_unterminated_quote); - validate!("'", ParseErrorCode::tokenizer_unterminated_quote); - validate!("echo (abc", ParseErrorCode::tokenizer_unterminated_subshell); - - validate!("end", ParseErrorCode::unbalancing_end); - validate!("echo hi ; end", ParseErrorCode::unbalancing_end); - - validate!("else", ParseErrorCode::unbalancing_else); - validate!("if true ; end ; else", ParseErrorCode::unbalancing_else); - - validate!("case", ParseErrorCode::unbalancing_case); - validate!("if true ; case ; end", ParseErrorCode::unbalancing_case); - - validate!("begin ; }", ParseErrorCode::unbalancing_brace); - - validate!("true | and", ParseErrorCode::andor_in_pipeline); - - validate!("a=", ParseErrorCode::bare_variable_assignment); -} - -#[test] -#[serial] -fn test_eval_recursion_detection() { - let _cleanup = test_init(); - // Ensure that we don't crash on infinite self recursion and mutual recursion. - let parser = TestParser::new(); - parser.eval( - L!("function recursive ; recursive ; end ; recursive; "), - &IoChain::new(), - ); - - parser.eval( - L!(concat!( - "function recursive1 ; recursive2 ; end ; ", - "function recursive2 ; recursive1 ; end ; recursive1; ", - )), - &IoChain::new(), - ); -} - -#[test] -#[serial] -fn test_eval_illegal_exit_code() { - let _cleanup = test_init(); - let parser = TestParser::new(); - macro_rules! validate { - ($cmd:expr, $result:expr) => { - parser.eval($cmd, &IoChain::new()); - let exit_status = parser.get_last_status(); - assert_eq!(exit_status, parser.get_last_status()); - }; - } - - // We need to be in an empty directory so that none of the wildcards match a file that might be - // in the fish source tree. In particular we need to ensure that "?" doesn't match a file - // named by a single character. See issue #3852. - parser.pushd("test/temp"); - validate!(L!("echo -n"), STATUS_CMD_OK.unwrap()); - validate!(L!("pwd"), STATUS_CMD_OK.unwrap()); - validate!(L!("UNMATCHABLE_WILDCARD*"), STATUS_UNMATCHED_WILDCARD); - validate!(L!("UNMATCHABLE_WILDCARD**"), STATUS_UNMATCHED_WILDCARD); - validate!(L!("?"), STATUS_UNMATCHED_WILDCARD); - validate!(L!("abc?def"), STATUS_UNMATCHED_WILDCARD); - parser.popd(); -} - -#[test] -#[serial] -fn test_eval_empty_function_name() { - let _cleanup = test_init(); - let parser = TestParser::new(); - parser.eval( - L!("function '' ; echo fail; exit 42 ; end ; ''"), - &IoChain::new(), - ); -} - -#[test] -#[serial] -fn test_expand_argument_list() { - let _cleanup = test_init(); - let parser = TestParser::new(); - let comps: Vec = Parser::expand_argument_list( - L!("alpha 'beta gamma' delta"), - ExpandFlags::default(), - &parser.context(), - ) - .into_iter() - .map(|c| c.completion) - .collect(); - assert_eq!(comps, &[L!("alpha"), L!("beta gamma"), L!("delta"),]); -} - -fn test_1_cancellation(parser: &Parser, src: &wstr) { - let filler = IoBufferfill::create().unwrap(); - let delay = Duration::from_millis(100); - #[allow(clippy::unnecessary_cast)] - let thread = unsafe { libc::pthread_self() } as usize; - iothread_perform(move || { - // Wait a while and then SIGINT the main thread. - std::thread::sleep(delay); - unsafe { - libc::pthread_kill(thread as libc::pthread_t, SIGINT); - } - }); - let mut io = IoChain::new(); - io.push(filler.clone()); - let res = parser.eval(src, &io); - let buffer = IoBufferfill::finish(filler); - assert_eq!( - buffer.len(), - 0, - "Expected 0 bytes in out_buff, but instead found {} bytes, for command {}", - buffer.len(), - src - ); - assert!(res.status.signal_exited() && res.status.signal_code() == SIGINT); -} - -#[test] -#[serial] -fn test_cancellation() { - let _cleanup = test_init(); - let parser = Parser::new(EnvStack::new(), CancelBehavior::Clear); - let _pop = fake_scoped_reader(&parser); - - printf!("Testing Ctrl-C cancellation. If this hangs, that's a bug!\n"); - - // Enable fish's signal handling here. - signal_set_handlers(true); - - // This tests that we can correctly ctrl-C out of certain loop constructs, and that nothing gets - // printed if we do. - - // Here the command substitution is an infinite loop. echo never even gets its argument, so when - // we cancel we expect no output. - test_1_cancellation(&parser, L!("echo (while true ; echo blah ; end)")); - - // Nasty infinite loop that doesn't actually execute anything. - test_1_cancellation( - &parser, - L!("echo (while true ; end) (while true ; end) (while true ; end)"), - ); - test_1_cancellation(&parser, L!("while true ; end")); - test_1_cancellation(&parser, L!("while true ; echo nothing > /dev/null; end")); - test_1_cancellation(&parser, L!("for i in (while true ; end) ; end")); - - signal_reset_handlers(); - - // Ensure that we don't think we should cancel. - reader_reset_interrupted(); - signal_clear_cancel(); -} - -#[test] -fn test_line_counter() { - let src = L!("echo line1; echo still_line_1;\n\necho line3"); - let ps = parse_source(src.to_owned(), ParseTreeFlags::default(), None) - .expect("Failed to parse source"); - assert!(!ps.ast.errored()); - let mut line_counter = ps.line_counter(); - - // Test line_offset_of_character_at_offset, both forwards and backwards to exercise the cache. - let mut expected = 0; - for (idx, c) in src.chars().enumerate() { - let line_offset = line_counter.line_offset_of_character_at_offset(idx); - assert_eq!(line_offset, expected); - if c == '\n' { - expected += 1; - } - } - for (idx, c) in src.chars().enumerate().rev() { - if c == '\n' { - expected -= 1; - } - let line_offset = line_counter.line_offset_of_character_at_offset(idx); - assert_eq!(line_offset, expected); - } - - let pipelines: Vec<_> = ps.ast.walk().filter_map(ast::JobPipeline::cast).collect(); - assert_eq!(pipelines.len(), 3); - let src_offsets = [0, 0, 2]; - assert_eq!(line_counter.source_offset_of_node(), None); - assert_eq!(line_counter.line_offset_of_node(), None); - - for (idx, &node) in pipelines.iter().enumerate() { - line_counter.node = node as *const _; - assert_eq!( - line_counter.source_offset_of_node(), - Some(node.source_range().start()) - ); - assert_eq!(line_counter.line_offset_of_node(), Some(src_offsets[idx])); - } - - for (idx, &node) in pipelines.iter().enumerate().rev() { - line_counter.node = node as *const _; - assert_eq!( - line_counter.source_offset_of_node(), - Some(node.source_range().start()) - ); - assert_eq!(line_counter.line_offset_of_node(), Some(src_offsets[idx])); - } -} - -#[test] -fn test_line_counter_empty() { - let mut line_counter = LineCounter::::empty(); - assert_eq!(line_counter.line_offset_of_character_at_offset(0), 0); - assert_eq!(line_counter.line_offset_of_node(), None); - assert_eq!(line_counter.source_offset_of_node(), None); -} - -// Helper for testing a simple ast traversal. -// The ast is always for the command 'true;'. -struct TrueSemiAstTester<'a> { - // The AST we are testing. - ast: &'a Ast, - - // Expected parent-child relationships, in the order we expect to encounter them. - parent_child: Box<[(&'a dyn Node, &'a dyn Node)]>, -} - -impl<'a> TrueSemiAstTester<'a> { - const TRUE_SEMI: &'static wstr = L!("true;"); - fn new(ast: &'a Ast) -> Self { - let job_list: &JobList = ast.top(); - let job_conjunction = &job_list[0]; - let job_pipeline = &job_conjunction.job; - let variable_assignment_list = &job_pipeline.variables; - let statement = &job_pipeline.statement; - - let decorated_statement = statement - .as_decorated_statement() - .expect("Expected decorated_statement"); - let command = &decorated_statement.command; - let args_or_redirs = &decorated_statement.args_or_redirs; - let job_continuation = &job_pipeline.continuation; - let job_conjunction_continuation = &job_conjunction.continuations; - let semi_nl = job_conjunction.semi_nl.as_ref().expect("Expected semi_nl"); - - // Helpful parent-child map, such that the children are in the order that we expect to encounter them - // in the AST. - let parent_child: &[(&'a dyn Node, &'a dyn Node)] = &[ - (job_list, job_conjunction), - (job_conjunction, job_pipeline), - (job_pipeline, variable_assignment_list), - (job_pipeline, statement), - (statement, decorated_statement), - (decorated_statement, command), - (decorated_statement, args_or_redirs), - (job_pipeline, job_continuation), - (job_conjunction, job_conjunction_continuation), - (job_conjunction, semi_nl), - ]; - Self { - ast, - parent_child: Box::from(parent_child), - } - } - - // Expected nodes, in-order. - fn expected_nodes(&self) -> Vec<&'a dyn Node> { - let mut expected: Vec<&dyn Node> = vec![self.ast.top()]; - expected.extend(self.parent_child.iter().map(|&(_p, c)| c)); - expected - } - - // Helper function to construct the parent list of a given node, such at the first entry is - // the node itself, and the last entry is the root node. - fn get_parents<'s>(&'s self, node: &'a dyn Node) -> impl Iterator + 's { - let mut next = Some(node); - std::iter::from_fn(move || { - let out = next?; - next = self - .parent_child - .iter() - .find_map(|&(p, c)| is_same_node(c, out).then_some(p)); - Some(out) - }) - } -} - -#[test] -fn test_ast() { - // Light testing of the AST and traversals. - let ast = ast::parse(TrueSemiAstTester::TRUE_SEMI, ParseTreeFlags::empty(), None); - let tester = TrueSemiAstTester::new(&ast); - - // Walk the AST and collect all nodes. - // See is_same_node comments for why we can't use assert_eq! here. - let found = ast.walk().collect::>(); - let expected = tester.expected_nodes(); - assert_eq!(found.len(), expected.len()); - for idx in 0..found.len() { - assert!(is_same_node(found[idx], expected[idx])); - } - - // Walk and check parents. - let mut traversal = ast.walk(); - while let Some(node) = traversal.next() { - let expected_parents = tester.get_parents(node).collect::>(); - let found_parents = traversal.parent_nodes().collect::>(); - assert_eq!(found_parents.len(), expected_parents.len()); - for idx in 0..found_parents.len() { - assert!(is_same_node(found_parents[idx], expected_parents[idx])); - } - } - - // Find the decorated statement. - let decorated_statement = ast - .walk() - .find(|n| matches!(n.kind(), ast::Kind::DecoratedStatement(_))) - .expect("Expected decorated statement"); - - // Test the skip feature. Don't descend into the decorated_statement. - let expected_skip: Vec<&dyn Node> = expected - .iter() - .copied() - .filter(|&n| { - // Discard nodes who have the decorated_statement as a parent, - // excepting the decorated_statement itself. - tester - .get_parents(n) - .skip(1) - .all(|p| !is_same_node(p, decorated_statement)) - }) - .collect(); - - let mut found = vec![]; - let mut traversal = ast.walk(); - while let Some(node) = traversal.next() { - if is_same_node(node, decorated_statement) { - traversal.skip_children(node); - } - found.push(node); - } - assert_eq!(found.len(), expected_skip.len()); - for idx in 0..found.len() { - assert!(is_same_node(found[idx], expected_skip[idx])); - } -} - -#[test] -#[should_panic] -fn test_traversal_skip_children_panics() { - // Test that we panic if we try to skip children of a node that is not the current node. - let ast = ast::parse(L!("true;"), ParseTreeFlags::empty(), None); - let mut traversal = ast.walk(); - while let Some(node) = traversal.next() { - if matches!(node.kind(), ast::Kind::DecoratedStatement(_)) { - // Should panic as we can only skip the current node. - traversal.skip_children(ast.top()); - } - } -} - -#[test] -#[should_panic] -fn test_traversal_parent_panics() { - // Can only get the parent of nodes still on the stack. - let ast = ast::parse(L!("true;"), ParseTreeFlags::empty(), None); - let mut traversal = ast.walk(); - let mut decorated_statement = None; - while let Some(node) = traversal.next() { - if let Kind::DecoratedStatement(_) = node.kind() { - decorated_statement = Some(node); - } else if node.as_token().map(|t| t.token_type()) == Some(ParseTokenType::end) { - // should panic as the decorated_statement is not on the stack. - let _ = traversal.parent(decorated_statement.unwrap()); - } - } -} diff --git a/src/tests/prelude.rs b/src/tests/prelude.rs new file mode 100644 index 000000000..4510b4bb0 --- /dev/null +++ b/src/tests/prelude.rs @@ -0,0 +1,141 @@ +use crate::common::{BUILD_DIR, ScopeGuard, ScopeGuarding}; +use crate::env::env_init; +use crate::env::{EnvMode, EnvVar, EnvVarFlags, Environment}; +use crate::parser::{CancelBehavior, Parser}; +use crate::reader::{reader_deinit, reader_init}; +use crate::signal::signal_reset_handlers; +use crate::topic_monitor::topic_monitor_init; +use crate::wchar::prelude::*; +use crate::wutil::wgetcwd; +use crate::{env::EnvStack, proc::proc_init}; +use once_cell::sync::OnceCell; +use std::cell::RefCell; +use std::collections::HashMap; +use std::env::set_current_dir; +use std::ffi::CString; +use std::path::PathBuf; + +pub use serial_test::serial; + +pub fn test_init() -> impl ScopeGuarding { + static DONE: OnceCell<()> = OnceCell::new(); + DONE.get_or_init(|| { + // If we are building with `cargo build` and have build w/ `cmake`, this might not + // yet exist. + let mut test_dir = PathBuf::from(BUILD_DIR); + test_dir.push("fish-test"); + std::fs::create_dir_all(&test_dir).unwrap(); + set_current_dir(&test_dir).unwrap(); + { + let s = CString::new("").unwrap(); + unsafe { + libc::setlocale(libc::LC_ALL, s.as_ptr()); + } + } + topic_monitor_init(); + crate::threads::init(); + proc_init(); + env_init(None, true, false); + + // Set default signal handlers, so we can ctrl-C out of this. + signal_reset_handlers(); + + // Set PWD from getcwd - fixes #5599 + EnvStack::globals().set_pwd_from_getcwd(); + }); + reader_init(false); + ScopeGuard::new((), |()| { + reader_deinit(false); + }) +} + +/// An environment built around an std::map. +#[derive(Clone, Default)] +pub struct TestEnvironment { + pub vars: HashMap, +} +impl TestEnvironment { + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } +} +impl Environment for TestEnvironment { + fn getf(&self, name: &wstr, _mode: EnvMode) -> Option { + self.vars + .get(name) + .map(|value| EnvVar::new(value.clone(), EnvVarFlags::default())) + } + fn get_names(&self, _flags: EnvMode) -> Vec { + self.vars.keys().cloned().collect() + } +} + +/// A test environment that knows about PWD. +#[derive(Default)] +pub struct PwdEnvironment { + pub parent: TestEnvironment, +} +impl PwdEnvironment { + #[allow(dead_code)] + pub fn new() -> Self { + Self::default() + } +} +impl Environment for PwdEnvironment { + fn getf(&self, name: &wstr, mode: EnvMode) -> Option { + if name == "PWD" { + return Some(EnvVar::new(wgetcwd(), EnvVarFlags::default())); + } + self.parent.getf(name, mode) + } + + fn get_names(&self, flags: EnvMode) -> Vec { + let mut res = self.parent.get_names(flags); + if !res.iter().any(|n| n == "PWD") { + res.push(L!("PWD").to_owned()); + } + res + } +} + +/// A wrapper around a Parser with some test helpers. +pub struct TestParser { + parser: Parser, + pushed_dirs: RefCell>, +} + +impl TestParser { + pub fn new() -> TestParser { + TestParser { + parser: Parser::new(EnvStack::new(), CancelBehavior::default()), + pushed_dirs: RefCell::new(Vec::new()), + } + } + + /// Helper to chdir and then update $PWD. + pub fn pushd(&self, path: &str) { + let cwd = wgetcwd(); + self.pushed_dirs.borrow_mut().push(cwd.to_string()); + + // We might need to create the directory. We don't care if this fails due to the directory + // already being present. + std::fs::create_dir_all(path).unwrap(); + + std::env::set_current_dir(path).unwrap(); + self.parser.vars().set_pwd_from_getcwd(); + } + + pub fn popd(&self) { + let old_cwd = self.pushed_dirs.borrow_mut().pop().unwrap(); + std::env::set_current_dir(old_cwd).unwrap(); + self.parser.vars().set_pwd_from_getcwd(); + } +} + +impl std::ops::Deref for TestParser { + type Target = Parser; + fn deref(&self) -> &Self::Target { + &self.parser + } +} diff --git a/src/tests/reader.rs b/src/tests/reader.rs deleted file mode 100644 index 30e619394..000000000 --- a/src/tests/reader.rs +++ /dev/null @@ -1,191 +0,0 @@ -use crate::complete::CompleteFlags; -use crate::operation_context::{OperationContext, no_cancel}; -use crate::reader::{combine_command_and_autosuggestion, completion_apply_to_command_line}; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; - -#[test] -fn test_autosuggestion_combining() { - assert_eq!( - combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("alphabeta")), - L!("alphabeta") - ); - - // When the last token contains no capital letters, we use the case of the autosuggestion. - assert_eq!( - combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHABETA")), - L!("ALPHABETA") - ); - - // When the last token contains capital letters, we use its case. - assert_eq!( - combine_command_and_autosuggestion(L!("alPha"), 0..5, L!("alphabeTa")), - L!("alPhabeTa") - ); - - // If autosuggestion is not longer than input, use the input's case. - assert_eq!( - combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHAA")), - L!("ALPHAA") - ); - assert_eq!( - combine_command_and_autosuggestion(L!("alpha"), 0..5, L!("ALPHA")), - L!("ALPHA") - ); - - assert_eq!( - combine_command_and_autosuggestion(L!("al\nbeta"), 0..2, L!("alpha")), - L!("alpha\nbeta").to_owned() - ); - assert_eq!( - combine_command_and_autosuggestion(L!("alpha\nbe"), 6..8, L!("beta")), - L!("alpha\nbeta").to_owned() - ); - assert_eq!( - combine_command_and_autosuggestion(L!("alpha\nbe\ngamma"), 6..8, L!("beta")), - L!("alpha\nbeta\ngamma").to_owned() - ); -} - -#[test] -fn test_completion_insertions() { - let parser = TestParser::new(); - - macro_rules! validate { - ( - $line:expr, $completion:expr, - $flags:expr, $append_only:expr, - $expected:expr - ) => { - // line is given with a caret, which we use to represent the cursor position. Find it. - let mut line = L!($line).to_owned(); - let completion = L!($completion); - let mut expected = L!($expected).to_owned(); - let in_cursor_pos = line.find(L!("^")).unwrap(); - line.remove(in_cursor_pos); - - let out_cursor_pos = expected.find(L!("^")).unwrap(); - expected.remove(out_cursor_pos); - - let mut cursor_pos = in_cursor_pos; - - let result = completion_apply_to_command_line( - &OperationContext::test_only_foreground( - &parser, - parser.vars(), - Box::new(no_cancel), - ), - completion, - $flags, - &line, - &mut cursor_pos, - $append_only, - /*is_unique=*/ false, - ); - assert_eq!(result, expected); - assert_eq!(cursor_pos, out_cursor_pos); - }; - } - - validate!("foo^", "bar", CompleteFlags::default(), false, "foobar ^"); - // An unambiguous completion of a token that is already trailed by a space character. - // After completing, the cursor moves on to the next token, suggesting to the user that the - // current token is finished. - validate!( - "foo^ baz", - "bar", - CompleteFlags::default(), - false, - "foobar ^baz" - ); - validate!( - "'foo^", - "bar", - CompleteFlags::default(), - false, - "'foobar' ^" - ); - validate!( - "'foo'^", - "bar", - CompleteFlags::default(), - false, - "'foobar' ^" - ); - validate!( - "'foo\\'^", - "bar", - CompleteFlags::default(), - false, - "'foo\\'bar' ^" - ); - validate!( - "foo\\'^", - "bar", - CompleteFlags::default(), - false, - "foo\\'bar ^" - ); - - // Test append only. - validate!("foo^", "bar", CompleteFlags::default(), true, "foobar ^"); - validate!( - "foo^ baz", - "bar", - CompleteFlags::default(), - true, - "foobar ^baz" - ); - validate!("'foo^", "bar", CompleteFlags::default(), true, "'foobar' ^"); - validate!( - "'foo'^", - "bar", - CompleteFlags::default(), - true, - "'foo'bar ^" - ); - validate!( - "'foo\\'^", - "bar", - CompleteFlags::default(), - true, - "'foo\\'bar' ^" - ); - validate!( - "foo\\'^", - "bar", - CompleteFlags::default(), - true, - "foo\\'bar ^" - ); - - validate!("foo^", "bar", CompleteFlags::NO_SPACE, false, "foobar^"); - validate!("'foo^", "bar", CompleteFlags::NO_SPACE, false, "'foobar^"); - validate!("'foo'^", "bar", CompleteFlags::NO_SPACE, false, "'foobar'^"); - validate!( - "'foo\\'^", - "bar", - CompleteFlags::NO_SPACE, - false, - "'foo\\'bar^" - ); - validate!( - "foo\\'^", - "bar", - CompleteFlags::NO_SPACE, - false, - "foo\\'bar^" - ); - - validate!("foo^", "bar", CompleteFlags::REPLACES_TOKEN, false, "bar ^"); - validate!( - "'foo^", - "bar", - CompleteFlags::REPLACES_TOKEN, - false, - "bar ^" - ); - - // See #6130 - validate!(": (:^ ''", "", CompleteFlags::default(), false, ": (: ^''"); -} diff --git a/src/tests/redirection.rs b/src/tests/redirection.rs deleted file mode 100644 index 744af966d..000000000 --- a/src/tests/redirection.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::io::{IoChain, IoClose, IoFd}; -use crate::redirection::dup2_list_resolve_chain; -use std::sync::Arc; - -#[test] -fn test_dup2s() { - let mut chain = IoChain::new(); - chain.push(Arc::new(IoClose::new(17))); - chain.push(Arc::new(IoFd::new(3, 19))); - let list = dup2_list_resolve_chain(&chain); - assert_eq!(list.get_actions().len(), 2); - - let act1 = list.get_actions()[0]; - assert_eq!(act1.src, 17); - assert_eq!(act1.target, -1); - - let act2 = list.get_actions()[1]; - assert_eq!(act2.src, 19); - assert_eq!(act2.target, 3); -} - -#[test] -fn test_dup2s_fd_for_target_fd() { - let mut chain = IoChain::new(); - // note io_fd_t params are backwards from dup2. - chain.push(Arc::new(IoClose::new(10))); - chain.push(Arc::new(IoFd::new(9, 10))); - chain.push(Arc::new(IoFd::new(5, 8))); - chain.push(Arc::new(IoFd::new(1, 4))); - chain.push(Arc::new(IoFd::new(3, 5))); - let list = dup2_list_resolve_chain(&chain); - - assert_eq!(list.fd_for_target_fd(3), 8); - assert_eq!(list.fd_for_target_fd(5), 8); - assert_eq!(list.fd_for_target_fd(8), 8); - assert_eq!(list.fd_for_target_fd(1), 4); - assert_eq!(list.fd_for_target_fd(4), 4); - assert_eq!(list.fd_for_target_fd(100), 100); - assert_eq!(list.fd_for_target_fd(0), 0); - assert_eq!(list.fd_for_target_fd(-1), -1); - assert_eq!(list.fd_for_target_fd(9), -1); - assert_eq!(list.fd_for_target_fd(10), -1); -} diff --git a/src/tests/screen.rs b/src/tests/screen.rs deleted file mode 100644 index 79a734d05..000000000 --- a/src/tests/screen.rs +++ /dev/null @@ -1,451 +0,0 @@ -use crate::common::get_ellipsis_char; -use crate::highlight::HighlightSpec; -use crate::parse_util::parse_util_compute_indents; -use crate::screen::{LayoutCache, PromptCacheEntry, PromptLayout, ScreenLayout, compute_layout}; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; -use crate::wcstringutil::join_strings; - -#[test] -#[serial] -fn test_complete() { - let _cleanup = test_init(); - let mut lc = LayoutCache::new(); - assert_eq!(lc.escape_code_length(L!("")), 0); - assert_eq!(lc.escape_code_length(L!("abcd")), 0); - assert_eq!(lc.escape_code_length(L!("\x1B[2J")), 4); - assert_eq!( - lc.escape_code_length(L!("\x1B[38;5;123mABC")), - "\x1B[38;5;123m".len() - ); - assert_eq!(lc.escape_code_length(L!("\x1B@")), 2); - - // iTerm2 escape sequences. - assert_eq!( - lc.escape_code_length(L!("\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE")), - 25 - ); - assert_eq!( - lc.escape_code_length(L!("\x1B]50;SetMark\x07NOT_PART_OF_SEQUENCE")), - 13 - ); - assert_eq!( - lc.escape_code_length(L!("\x1B]6;1;bg;red;brightness;255\x07NOT_PART_OF_SEQUENCE")), - 28 - ); - assert_eq!( - lc.escape_code_length(L!("\x1B]Pg4040ff\x1B\\NOT_PART_OF_SEQUENCE")), - 12 - ); - assert_eq!(lc.escape_code_length(L!("\x1B]blahblahblah\x1B\\")), 16); - assert_eq!(lc.escape_code_length(L!("\x1B]blahblahblah\x07")), 15); -} - -#[test] -#[serial] -fn test_layout_cache() { - let _cleanup = test_init(); - let mut seqs = LayoutCache::new(); - - // Verify escape code cache. - assert_eq!(seqs.find_escape_code(L!("abc")), 0); - seqs.add_escape_code(L!("abc").to_owned()); - seqs.add_escape_code(L!("abc").to_owned()); - assert_eq!(seqs.esc_cache_size(), 1); - assert_eq!(seqs.find_escape_code(L!("abc")), 3); - assert_eq!(seqs.find_escape_code(L!("abcd")), 3); - assert_eq!(seqs.find_escape_code(L!("abcde")), 3); - assert_eq!(seqs.find_escape_code(L!("xabcde")), 0); - seqs.add_escape_code(L!("ac").to_owned()); - assert_eq!(seqs.find_escape_code(L!("abcd")), 3); - assert_eq!(seqs.find_escape_code(L!("acbd")), 2); - seqs.add_escape_code(L!("wxyz").to_owned()); - assert_eq!(seqs.find_escape_code(L!("abc")), 3); - assert_eq!(seqs.find_escape_code(L!("abcd")), 3); - assert_eq!(seqs.find_escape_code(L!("wxyz123")), 4); - assert_eq!(seqs.find_escape_code(L!("qwxyz123")), 0); - assert_eq!(seqs.esc_cache_size(), 3); - seqs.clear(); - assert_eq!(seqs.esc_cache_size(), 0); - assert_eq!(seqs.find_escape_code(L!("abcd")), 0); - - let huge = usize::MAX; - - // Verify prompt layout cache. - for i in 0..LayoutCache::PROMPT_CACHE_MAX_SIZE { - let input = i.to_wstring(); - assert!(!seqs.find_prompt_layout(&input, usize::MAX)); - seqs.add_prompt_layout(PromptCacheEntry { - text: input.clone(), - max_line_width: huge, - trunc_text: input.clone(), - layout: PromptLayout { - line_starts: vec![], - last_line_width: i, - }, - }); - assert!(seqs.find_prompt_layout(&input, usize::MAX)); - assert_eq!(seqs.prompt_cache.front().unwrap().layout.last_line_width, i); - } - - let expected_evictee = 3; - for i in 0..LayoutCache::PROMPT_CACHE_MAX_SIZE { - if i != expected_evictee { - assert!(seqs.find_prompt_layout(&i.to_wstring(), usize::MAX)); - assert_eq!(seqs.prompt_cache.front().unwrap().layout.last_line_width, i); - } - } - - seqs.add_prompt_layout(PromptCacheEntry { - text: "whatever".into(), - max_line_width: huge, - trunc_text: "whatever".into(), - layout: PromptLayout { - line_starts: vec![], - last_line_width: 100, - }, - }); - assert!(!seqs.find_prompt_layout(&expected_evictee.to_wstring(), usize::MAX)); - assert!(seqs.find_prompt_layout(L!("whatever"), huge)); - assert_eq!( - seqs.prompt_cache.front().unwrap().layout.last_line_width, - 100 - ); -} - -#[test] -#[serial] -fn test_prompt_truncation() { - let _cleanup = test_init(); - let mut cache = LayoutCache::new(); - let mut trunc = WString::new(); - - let ellipsis = || WString::from_chars([get_ellipsis_char()]); - - // No truncation. - let layout = cache.calc_prompt_layout(L!("abcd"), Some(&mut trunc), usize::MAX); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0], - last_line_width: 4, - } - ); - assert_eq!(trunc, L!("abcd")); - - // Line break calculation. - let layout = cache.calc_prompt_layout( - L!(concat!( - "0123456789ABCDEF\n", - "012345\n", - "0123456789abcdef\n", - "xyz" - )), - Some(&mut trunc), - 80, - ); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0, 17, 24, 41], - last_line_width: 3, - } - ); - - // Basic truncation. - let layout = cache.calc_prompt_layout(L!("0123456789ABCDEF"), Some(&mut trunc), 8); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0], - last_line_width: 8, - }, - ); - assert_eq!(trunc, ellipsis() + L!("9ABCDEF")); - - // Multiline truncation. - let layout = cache.calc_prompt_layout( - L!(concat!( - "0123456789ABCDEF\n", - "012345\n", - "0123456789abcdef\n", - "xyz" - )), - Some(&mut trunc), - 8, - ); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0, 9, 16, 25], - last_line_width: 3, - }, - ); - assert_eq!( - trunc, - join_strings( - &[ - ellipsis() + L!("9ABCDEF"), - L!("012345").to_owned(), - ellipsis() + L!("9abcdef"), - L!("xyz").to_owned(), - ], - '\n', - ), - ); - - // Escape sequences are not truncated. - let layout = cache.calc_prompt_layout( - L!("\x1B]50;CurrentDir=test/foo\x07NOT_PART_OF_SEQUENCE"), - Some(&mut trunc), - 4, - ); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0], - last_line_width: 4, - }, - ); - assert_eq!(trunc, ellipsis() + L!("\x1B]50;CurrentDir=test/foo\x07NCE")); - - // Newlines in escape sequences are skipped. - let layout = cache.calc_prompt_layout( - L!("\x1B]50;CurrentDir=\ntest/foo\x07NOT_PART_OF_SEQUENCE"), - Some(&mut trunc), - 4, - ); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0], - last_line_width: 4, - }, - ); - assert_eq!( - trunc, - ellipsis() + L!("\x1B]50;CurrentDir=\ntest/foo\x07NCE") - ); - - // We will truncate down to one character if we have to. - let layout = cache.calc_prompt_layout(L!("Yay"), Some(&mut trunc), 1); - assert_eq!( - layout, - PromptLayout { - line_starts: vec![0], - last_line_width: 1, - }, - ); - assert_eq!(trunc, ellipsis()); -} - -#[test] -fn test_compute_layout() { - macro_rules! validate { - ( - ( - $screen_width:expr, - $left_untrunc_prompt:literal, - $right_untrunc_prompt:literal, - $commandline_before_suggestion:literal, - $autosuggestion_str:literal, - $commandline_after_suggestion:literal - ) - -> ( - $left_prompt:literal, - $left_prompt_space:expr, - $right_prompt:literal, - $autosuggestion:literal $(,)? - ) - ) => {{ - let full_commandline = L!($commandline_before_suggestion).to_owned() - + L!($autosuggestion_str) - + L!($commandline_after_suggestion); - let mut colors = vec![HighlightSpec::default(); full_commandline.len()]; - let mut indent = parse_util_compute_indents(&full_commandline); - assert_eq!( - compute_layout( - '…', - $screen_width, - L!($left_untrunc_prompt), - L!($right_untrunc_prompt), - L!($commandline_before_suggestion), - &mut colors, - &mut indent, - L!($autosuggestion_str), - ), - ScreenLayout { - left_prompt: L!($left_prompt).to_owned(), - left_prompt_space: $left_prompt_space, - left_prompt_lines: 1, - right_prompt: L!($right_prompt).to_owned(), - autosuggestion: L!($autosuggestion).to_owned(), - } - ); - indent - }}; - } - - validate!( - ( - 80, "left>", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", - " auto…", - ) - ); - validate!( - ( - 18, "left>", " ( - "left>", - 5, - "", - "s", - ) - ); - validate!( - ( - 18, "left>", " ( - "left>", - 5, - "", - "…", - ) - ); - validate!( - ( - 18, "left>", " ( - "left>", - 5, - "", - "uggestion long so…", - ) - ); - validate!( - ( - 18, "left>", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", " ( - "left>", - 5, - "", - "and …", - ) - ); - validate!( - ( - 18, "left>", " ( - "left>", - 5, - "", - "…", - ) - ); - validate!( - ( - 18, "left>", " ( - "left>", - 5, - "", - "utosuggestion sof…", - ) - ); -} diff --git a/src/tests/std.rs b/src/tests/std.rs deleted file mode 100644 index 510ac019d..000000000 --- a/src/tests/std.rs +++ /dev/null @@ -1,19 +0,0 @@ -//! This module contains tests that assert the functionality and behavior of the rust standard -//! library, to ensure we can safely use its abstractions to perform low-level operations. - -use std::fs::File; -use std::os::fd::AsRawFd; - -#[test] -fn test_fd_cloexec() { - // Just open a file. Any file. - let file = File::create("test_file_for_fd_cloexec").unwrap(); - let fd = file.as_raw_fd(); - unsafe { - assert_eq!( - libc::fcntl(fd, libc::F_GETFD) & libc::FD_CLOEXEC, - libc::FD_CLOEXEC - ); - } - let _ = std::fs::remove_file("test_file_for_fd_cloexec"); -} diff --git a/src/tests/string_escape.rs b/src/tests/string_escape.rs deleted file mode 100644 index 939761849..000000000 --- a/src/tests/string_escape.rs +++ /dev/null @@ -1,234 +0,0 @@ -use crate::common::{ - ENCODE_DIRECT_BASE, ENCODE_DIRECT_END, EscapeFlags, EscapeStringStyle, UnescapeStringStyle, - bytes2wcstring, escape_string, unescape_string, wcs2bytes, -}; -use crate::util::{get_rng_seed, get_seeded_rng}; -use crate::wchar::{L, WString, wstr}; -use rand::{Rng, RngCore}; - -#[test] -fn test_escape_string() { - let regex = |input| escape_string(input, EscapeStringStyle::Regex); - - // plain text should not be needlessly escaped - assert_eq!(regex(L!("hello world!")), L!("hello world!")); - - // all the following are intended to be ultimately matched literally - even if they - // don't look like that's the intent - so we escape them. - assert_eq!(regex(L!(".ext")), L!("\\.ext")); - assert_eq!(regex(L!("{word}")), L!("\\{word\\}")); - assert_eq!(regex(L!("hola-mundo")), L!("hola\\-mundo")); - assert_eq!( - regex(L!("$17.42 is your total?")), - L!("\\$17\\.42 is your total\\?") - ); - assert_eq!( - regex(L!("not really escaped\\?")), - L!("not really escaped\\\\\\?") - ); -} - -#[test] -pub fn test_unescape_sane() { - const TEST_CASES: &[(&wstr, &wstr)] = &[ - (L!("abcd"), L!("abcd")), - (L!("'abcd'"), L!("abcd")), - (L!("'abcd\\n'"), L!("abcd\\n")), - (L!("\"abcd\\n\""), L!("abcd\\n")), - (L!("\"abcd\\n\""), L!("abcd\\n")), - (L!("\\143"), L!("c")), - (L!("'\\143'"), L!("\\143")), - (L!("\\n"), L!("\n")), // \n normally becomes newline - ]; - - for (input, expected) in TEST_CASES { - let Some(output) = unescape_string(input, UnescapeStringStyle::default()) else { - panic!("Failed to unescape string {input:?}"); - }; - - assert_eq!( - output, *expected, - "In unescaping {input:?}, expected {expected:?} but got {output:?}\n" - ); - } -} - -#[test] -fn test_escape_var() { - const TEST_CASES: &[(&wstr, &wstr)] = &[ - (L!(" a"), L!("_20_a")), - (L!("a B "), L!("a_20_42_20_")), - (L!("a b "), L!("a_20_b_20_")), - (L!(" B"), L!("_20_42_")), - (L!(" f"), L!("_20_f")), - (L!(" 1"), L!("_20_31_")), - (L!("a\nghi_"), L!("a_0A_ghi__")), - ]; - - for (input, expected) in TEST_CASES { - let output = escape_string(input, EscapeStringStyle::Var); - - assert_eq!( - output, *expected, - "In escaping {input:?} with style var, expected {expected:?} but got {output:?}\n" - ); - } -} - -fn escape_test(escape_style: EscapeStringStyle, unescape_style: UnescapeStringStyle) { - let seed: u128 = 92348567983274852905629743984572; - let mut rng = get_seeded_rng(seed); - - let mut random_string = WString::new(); - let mut escaped_string; - for _ in 0..(ESCAPE_TEST_COUNT as u32) { - random_string.clear(); - let length = rng.gen_range(0..=(2 * ESCAPE_TEST_LENGTH)); - for _ in 0..length { - random_string - .push(char::from_u32((rng.next_u32() % ESCAPE_TEST_CHAR as u32) + 1).unwrap()); - } - - escaped_string = escape_string(&random_string, escape_style); - let Some(unescaped_string) = unescape_string(&escaped_string, unescape_style) else { - let slice = escaped_string.as_char_slice(); - panic!("Failed to unescape string {slice:?}"); - }; - assert_eq!( - random_string, unescaped_string, - "Escaped and then unescaped string {random_string:?}, but got back a different string {unescaped_string:?}. The intermediate escape looked like {escaped_string:?}." - ); - } -} - -#[test] -fn test_escape_random_script() { - escape_test(EscapeStringStyle::default(), UnescapeStringStyle::default()); -} - -#[test] -fn test_escape_random_var() { - escape_test(EscapeStringStyle::Var, UnescapeStringStyle::Var); -} - -#[test] -fn test_escape_random_url() { - escape_test(EscapeStringStyle::Url, UnescapeStringStyle::Url); -} - -#[test] -fn test_escape_no_printables() { - // Verify that ESCAPE_NO_PRINTABLES also escapes backslashes so we don't regress on issue #3892. - let random_string = L!("line 1\\n\nline 2").to_owned(); - let escaped_string = escape_string( - &random_string, - EscapeStringStyle::Script(EscapeFlags::NO_PRINTABLES | EscapeFlags::NO_QUOTED), - ); - let Some(unescaped_string) = unescape_string(&escaped_string, UnescapeStringStyle::default()) - else { - panic!("Failed to unescape string <{escaped_string}>"); - }; - - assert_eq!( - random_string, unescaped_string, - "Escaped and then unescaped string '{random_string}', but got back a different string '{unescaped_string}'" - ); -} - -/// The number of tests to run. -const ESCAPE_TEST_COUNT: usize = 20_000; -/// The average length of strings to unescape. -const ESCAPE_TEST_LENGTH: usize = 100; -/// The highest character number of character to try and escape. -pub const ESCAPE_TEST_CHAR: usize = 4000; - -/// Helper to convert a narrow string to a sequence of hex digits. -fn bytes2hex(input: &[u8]) -> String { - let mut output = "".to_string(); - for byte in input { - output += &format!("0x{:2X} ", *byte); - } - output -} - -/// Test wide/narrow conversion by creating random strings and verifying that the original -/// string comes back through double conversion. -#[test] -fn test_convert() { - let seed = get_rng_seed(); - let mut rng = get_seeded_rng(seed); - let mut origin = Vec::new(); - - for _ in 0..ESCAPE_TEST_COUNT { - let length: usize = rng.gen_range(0..=(2 * ESCAPE_TEST_LENGTH)); - origin.resize(length, 0); - rng.fill_bytes(&mut origin); - - let w = bytes2wcstring(&origin[..]); - let n = wcs2bytes(&w); - assert_eq!( - origin, - n, - "Conversion cycle of string:\n{:4} chars: {}\n\ - produced different string:\n\ - {:4} chars: {}\n - Use this seed to reproduce: {}", - origin.len(), - &bytes2hex(&origin), - n.len(), - &bytes2hex(&n), - seed, - ); - } -} - -/// Verify that ASCII narrow->wide conversions are correct. -#[test] -fn test_convert_ascii() { - let mut s = vec![b'\0'; 4096]; - for (i, c) in s.iter_mut().enumerate() { - *c = u8::try_from(i % 10).unwrap() + b'0'; - } - - // Test a variety of alignments. - for left in 0..16 { - for right in 0..16 { - let len = s.len() - left - right; - let input = &s[left..left + len]; - let wide = bytes2wcstring(input); - let narrow = wcs2bytes(&wide); - assert_eq!(narrow, input); - } - } - - // Put some non-ASCII bytes in and ensure it all still works. - for i in 0..s.len() { - let saved = s[i]; - s[i] = 0xF7; - assert_eq!(wcs2bytes(&bytes2wcstring(&s)), s); - s[i] = saved; - } -} - -/// fish uses the private-use range to encode bytes that are not valid UTF-8. -/// If the input decodes to these private-use codepoints, -/// then fish should also use the direct encoding for those bytes. -/// Verify that characters in the private use area are correctly round-tripped. See #7723. -#[test] -fn test_convert_private_use() { - for c in ENCODE_DIRECT_BASE..ENCODE_DIRECT_END { - // A `char` represents an Unicode scalar value, which takes up at most 4 bytes when encoded in UTF-8. - // TODO MSRV(1.92?) replace 4 by `char::MAX_LEN_UTF8` once that's available in our MSRV. - // https://doc.rust-lang.org/std/primitive.char.html#associatedconstant.MAX_LEN_UTF8 - let mut converted = [0_u8; 4]; - let s = c.encode_utf8(&mut converted).as_bytes(); - // Ask fish to decode this via bytes2wcstring. - // bytes2wcstring should notice that the decoded form collides with its private use - // and encode it directly. - let ws = bytes2wcstring(s); - - // Each byte should be encoded directly, and round tripping should work. - assert_eq!(ws.len(), s.len()); - assert_eq!(wcs2bytes(&ws), s); - } -} diff --git a/src/tests/termsize.rs b/src/tests/termsize.rs deleted file mode 100644 index 284dab8d9..000000000 --- a/src/tests/termsize.rs +++ /dev/null @@ -1,84 +0,0 @@ -use crate::env::{EnvMode, Environment}; -use crate::termsize::*; -use crate::tests::prelude::*; -use crate::wchar::prelude::*; -use std::sync::Mutex; -use std::sync::atomic::AtomicBool; - -#[test] -#[serial] -fn test_termsize() { - let _cleanup = test_init(); - let env_global = EnvMode::GLOBAL; - let parser = TestParser::new(); - let vars = parser.vars(); - - // Use a static variable so we can pretend we're the kernel exposing a terminal size. - static STUBBY_TERMSIZE: Mutex> = Mutex::new(None); - fn stubby_termsize() -> Option { - *STUBBY_TERMSIZE.lock().unwrap() - } - let ts = TermsizeContainer { - data: Mutex::new(TermsizeData::defaults()), - setting_env_vars: AtomicBool::new(false), - tty_size_reader: stubby_termsize, - }; - - // Initially default value. - assert_eq!(ts.last(), Termsize::defaults()); - - // Haha we change the value, it doesn't even know. - *STUBBY_TERMSIZE.lock().unwrap() = Some(Termsize { - width: 42, - height: 84, - }); - assert_eq!(ts.last(), Termsize::defaults()); - - // Ok let's tell it. But it still doesn't update right away. - TermsizeContainer::handle_winch(); - assert_eq!(ts.last(), Termsize::defaults()); - - // Ok now we tell it to update. - ts.updating(&parser); - assert_eq!(ts.last(), Termsize::new(42, 84)); - assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "42"); - assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "84"); - - // Wow someone set COLUMNS and LINES to a weird value. - // Now the tty's termsize doesn't matter. - vars.set_one(L!("COLUMNS"), env_global, L!("75").to_owned()); - vars.set_one(L!("LINES"), env_global, L!("150").to_owned()); - ts.handle_columns_lines_var_change(parser.vars()); - assert_eq!(ts.last(), Termsize::new(75, 150)); - assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "75"); - assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "150"); - - vars.set_one(L!("COLUMNS"), env_global, L!("33").to_owned()); - ts.handle_columns_lines_var_change(parser.vars()); - assert_eq!(ts.last(), Termsize::new(33, 150)); - - // Oh it got SIGWINCH, now the tty matters again. - TermsizeContainer::handle_winch(); - assert_eq!(ts.last(), Termsize::new(33, 150)); - assert_eq!(ts.updating(&parser), stubby_termsize().unwrap()); - assert_eq!(vars.get(L!("COLUMNS")).unwrap().as_string(), "42"); - assert_eq!(vars.get(L!("LINES")).unwrap().as_string(), "84"); - - // Test initialize(). - vars.set_one(L!("COLUMNS"), env_global, L!("83").to_owned()); - vars.set_one(L!("LINES"), env_global, L!("38").to_owned()); - ts.initialize(vars); - assert_eq!(ts.last(), Termsize::new(83, 38)); - - // initialize() even beats the tty reader until a sigwinch. - let ts2 = TermsizeContainer { - data: Mutex::new(TermsizeData::defaults()), - setting_env_vars: AtomicBool::new(false), - tty_size_reader: stubby_termsize, - }; - ts.initialize(parser.vars()); - ts2.updating(&parser); - assert_eq!(ts.last(), Termsize::new(83, 38)); - TermsizeContainer::handle_winch(); - assert_eq!(ts2.updating(&parser), stubby_termsize().unwrap()); -} diff --git a/src/tests/threads.rs b/src/tests/threads.rs deleted file mode 100644 index 1102c5588..000000000 --- a/src/tests/threads.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::threads::spawn; -use std::sync::atomic::{AtomicI32, Ordering}; -use std::sync::{Arc, Condvar, Mutex}; -use std::time::Duration; - -#[test] -fn test_pthread() { - struct Context { - val: AtomicI32, - condvar: Condvar, - } - let ctx = Arc::new(Context { - val: AtomicI32::new(3), - condvar: Condvar::new(), - }); - let mutex = Mutex::new(()); - let ctx2 = ctx.clone(); - let made = spawn(move || { - ctx2.val.fetch_add(2, Ordering::Release); - ctx2.condvar.notify_one(); - printf!("condvar signalled\n"); - }); - assert!(made); - - let lock = mutex.lock().unwrap(); - let (_lock, timeout) = ctx - .condvar - .wait_timeout_while(lock, Duration::from_secs(5), |()| { - printf!("looping with lock held\n"); - if ctx.val.load(Ordering::Acquire) != 5 { - printf!("test_pthread: value did not yet reach goal\n"); - return true; - } - false - }) - .unwrap(); - if timeout.timed_out() { - panic!(concat!( - "Timeout waiting for condition variable to be notified! ", - "Does the platform support signalling a condvar without the mutex held?" - )); - } -} diff --git a/src/tests/tokenizer.rs b/src/tests/tokenizer.rs deleted file mode 100644 index 0ab0df4de..000000000 --- a/src/tests/tokenizer.rs +++ /dev/null @@ -1,345 +0,0 @@ -use crate::redirection::RedirectionMode; -use crate::tokenizer::{ - MoveWordStateMachine, MoveWordStyle, PipeOrRedir, TokFlags, TokenType, Tokenizer, - TokenizerError, -}; -use crate::wchar::prelude::*; -use libc::{STDERR_FILENO, STDOUT_FILENO}; -use std::collections::HashSet; - -#[test] -fn test_tokenizer() { - { - let s = L!("alpha beta"); - let mut t = Tokenizer::new(s, TokFlags(0)); - - let token = t.next(); // alpha - assert!(token.is_some()); - let token = token.unwrap(); - assert_eq!(token.type_, TokenType::string); - assert_eq!(token.length, 5); - assert_eq!(t.text_of(&token), "alpha"); - - let token = t.next(); // beta - assert!(token.is_some()); - let token = token.unwrap(); - assert_eq!(token.type_, TokenType::string); - assert_eq!(token.offset, 6); - assert_eq!(token.length, 4); - assert_eq!(t.text_of(&token), "beta"); - - assert!(t.next().is_none()); - } - - { - let s = L!("{ echo"); - let mut t = Tokenizer::new(s, TokFlags(0)); - - let token = t.next(); // { - assert!(token.is_some()); - let token = token.unwrap(); - assert_eq!(token.type_, TokenType::left_brace); - assert_eq!(token.length, 1); - assert_eq!(t.text_of(&token), "{"); - - let token = t.next(); // echo - assert!(token.is_some()); - let token = token.unwrap(); - assert_eq!(token.type_, TokenType::string); - assert_eq!(token.offset, 2); - assert_eq!(token.length, 4); - assert_eq!(t.text_of(&token), "echo"); - - assert!(t.next().is_none()); - } - - { - let s = L!("{echo, foo}"); - let mut t = Tokenizer::new(s, TokFlags(0)); - let token = t.next().unwrap(); - assert_eq!(token.type_, TokenType::left_brace); - assert_eq!(token.length, 1); - } - { - let s = L!("{ echo; foo}"); - let mut t = Tokenizer::new(s, TokFlags(0)); - let token = t.next().unwrap(); - assert_eq!(token.type_, TokenType::left_brace); - } - - { - let s = L!("{ | { name } '"); - let mut t = Tokenizer::new(s, TokFlags(0)); - let mut next_type = || t.next().unwrap().type_; - assert_eq!(next_type(), TokenType::left_brace); - assert_eq!(next_type(), TokenType::pipe); - assert_eq!(next_type(), TokenType::left_brace); - assert_eq!(next_type(), TokenType::string); - assert_eq!(next_type(), TokenType::right_brace); - assert_eq!(next_type(), TokenType::error); - assert!(t.next().is_none()); - } - - let s = L!(concat!( - "string &1 'nested \"quoted\" '(string containing subshells ", - "){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ ^ ^^is_a_redirect ", - "&| &> ", - "&&& ||| ", - "&& || & |", - "Compress_Newlines\n \n\t\n \nInto_Just_One", - )); - type tt = TokenType; - #[rustfmt::skip] - let types = [ - tt::string, tt::redirect, tt::string, tt::redirect, tt::string, tt::string, tt::string, - tt::string, tt::string, tt::pipe, tt::redirect, tt::andand, tt::background, tt::oror, - tt::pipe, tt::andand, tt::oror, tt::background, tt::pipe, tt::string, tt::end, tt::string, - ]; - - { - let t = Tokenizer::new(s, TokFlags(0)); - let mut actual_types = vec![]; - for token in t { - actual_types.push(token.type_); - } - assert_eq!(&actual_types[..], types); - } - - // Test some errors. - - { - let mut t = Tokenizer::new(L!("abc\\"), TokFlags(0)); - let token = t.next().unwrap(); - assert_eq!(token.type_, TokenType::error); - assert_eq!(token.error, TokenizerError::unterminated_escape); - assert_eq!(token.error_offset_within_token, 3); - } - - { - let mut t = Tokenizer::new(L!("abc )defg(hij"), TokFlags(0)); - let _token = t.next().unwrap(); - let token = t.next().unwrap(); - assert_eq!(token.type_, TokenType::error); - assert_eq!(token.error, TokenizerError::closing_unopened_subshell); - assert_eq!(token.offset, 4); - assert_eq!(token.error_offset_within_token, 0); - } - - { - let mut t = Tokenizer::new(L!("abc defg(hij (klm)"), TokFlags(0)); - let _token = t.next().unwrap(); - let token = t.next().unwrap(); - assert_eq!(token.type_, TokenType::error); - assert_eq!(token.error, TokenizerError::unterminated_subshell); - assert_eq!(token.error_offset_within_token, 4); - } - - { - let mut t = Tokenizer::new(L!("abc defg[hij (klm)"), TokFlags(0)); - let _token = t.next().unwrap(); - let token = t.next().unwrap(); - assert_eq!(token.type_, TokenType::error); - assert_eq!(token.error, TokenizerError::unterminated_slice); - assert_eq!(token.error_offset_within_token, 4); - } - - // Test some redirection parsing. - macro_rules! pipe_or_redir { - ($s:literal) => { - PipeOrRedir::try_from(L!($s)).unwrap() - }; - } - - assert!(pipe_or_redir!("|").is_pipe); - assert!(pipe_or_redir!("0>|").is_pipe); - assert_eq!(pipe_or_redir!("0>|").fd, 0); - assert!(pipe_or_redir!("2>|").is_pipe); - assert_eq!(pipe_or_redir!("2>|").fd, 2); - assert!(pipe_or_redir!(">|").is_pipe); - assert_eq!(pipe_or_redir!(">|").fd, STDOUT_FILENO); - assert!(!pipe_or_redir!(">").is_pipe); - assert_eq!(pipe_or_redir!(">").fd, STDOUT_FILENO); - assert_eq!(pipe_or_redir!("2>").fd, STDERR_FILENO); - assert_eq!(pipe_or_redir!("9999999999999>").fd, -1); - assert_eq!(pipe_or_redir!("9999999999999>&2").fd, -1); - assert!(!pipe_or_redir!("9999999999999>&2").is_valid()); - assert!(!pipe_or_redir!("9999999999999>&2").is_valid()); - - assert!(pipe_or_redir!("&|").is_pipe); - assert!(pipe_or_redir!("&|").stderr_merge); - assert!(!pipe_or_redir!("&>").is_pipe); - assert!(pipe_or_redir!("&>").stderr_merge); - assert!(pipe_or_redir!("&>>").stderr_merge); - assert!(pipe_or_redir!("&>?").stderr_merge); - - macro_rules! get_redir_mode { - ($s:literal) => { - pipe_or_redir!($s).mode - }; - } - - assert_eq!(get_redir_mode!("<"), RedirectionMode::input); - assert_eq!(get_redir_mode!(">"), RedirectionMode::overwrite); - assert_eq!(get_redir_mode!("2>"), RedirectionMode::overwrite); - assert_eq!(get_redir_mode!(">>"), RedirectionMode::append); - assert_eq!(get_redir_mode!("2>>"), RedirectionMode::append); - assert_eq!(get_redir_mode!("2>?"), RedirectionMode::noclob); - assert_eq!( - get_redir_mode!("9999999999999999>?"), - RedirectionMode::noclob - ); - assert_eq!(get_redir_mode!("2>&3"), RedirectionMode::fd); - assert_eq!(get_redir_mode!("3<&0"), RedirectionMode::fd); - assert_eq!(get_redir_mode!("3 { - let mut command = WString::new(); - let mut stops = HashSet::new(); - - // Carets represent stops and should be cut out of the command. - for c in $line.chars() { - if c == '^' { - stops.insert(command.len()); - } else { - command.push(c); - } - } - - let (mut idx, end) = if $direction == Direction::Left { - (stops.iter().max().unwrap().clone(), 0) - } else { - (stops.iter().min().unwrap().clone(), command.len()) - }; - stops.remove(&idx); - - let mut sm = MoveWordStateMachine::new($style); - while idx != end { - let char_idx = if $direction == Direction::Left { - idx - 1 - } else { - idx - }; - let c = command.as_char_slice()[char_idx]; - let will_stop = !sm.consume_char(c); - let expected_stop = stops.contains(&idx); - assert_eq!(will_stop, expected_stop); - // We don't expect to stop here next time. - if expected_stop { - stops.remove(&idx); - sm.reset(); - } else { - if $direction == Direction::Left { - idx -= 1; - } else { - idx += 1; - } - } - } - }; - } - - validate!( - Direction::Left, - MoveWordStyle::Punctuation, - "^echo ^hello_^world.^txt^" - ); - validate!( - Direction::Right, - MoveWordStyle::Punctuation, - "^echo^ hello^_world^.txt^" - ); - - validate!( - Direction::Left, - MoveWordStyle::Punctuation, - "echo ^foo_^foo_^foo/^/^/^/^/^ ^" - ); - validate!( - Direction::Right, - MoveWordStyle::Punctuation, - "^echo^ foo^_foo^_foo^/^/^/^/^/ ^" - ); - - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^/^foo/^bar/^baz/^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^echo ^--foo ^--bar^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^echo ^hi ^> ^/^dev/^null^" - ); - - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^echo ^/^foo/^bar{^aaa,^bbb,^ccc}^bak/^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^echo ^bak ^///^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^aaa ^@ ^@^aaa^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^aaa ^a ^@^aaa^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^aaa ^@@@ ^@@^aa^" - ); - validate!( - Direction::Left, - MoveWordStyle::PathComponents, - "^aa^@@ ^aa@@^a^" - ); - - validate!(Direction::Right, MoveWordStyle::Punctuation, "^a^ bcd^"); - validate!(Direction::Right, MoveWordStyle::Punctuation, "a^b^ cde^"); - validate!(Direction::Right, MoveWordStyle::Punctuation, "^ab^ cde^"); - validate!( - Direction::Right, - MoveWordStyle::Punctuation, - "^ab^&cd^ ^& ^e^ f^&" - ); - - validate!( - Direction::Right, - MoveWordStyle::Whitespace, - "^^a-b-c^ d-e-f" - ); - validate!( - Direction::Right, - MoveWordStyle::Whitespace, - "^a-b-c^\n d-e-f^ " - ); - validate!( - Direction::Right, - MoveWordStyle::Whitespace, - "^a-b-c^\n\nd-e-f^ " - ); -} diff --git a/src/tests/topic_monitor.rs b/src/tests/topic_monitor.rs deleted file mode 100644 index 505efa7ec..000000000 --- a/src/tests/topic_monitor.rs +++ /dev/null @@ -1,81 +0,0 @@ -use crate::tests::prelude::*; -use crate::topic_monitor::{GenerationsList, Topic, TopicMonitor}; -#[cfg(not(target_has_atomic = "64"))] -use portable_atomic::AtomicU64; -#[cfg(target_has_atomic = "64")] -use std::sync::atomic::AtomicU64; -use std::sync::{ - Arc, - atomic::{AtomicU32, Ordering}, -}; - -#[test] -#[serial] -fn test_topic_monitor() { - let _cleanup = test_init(); - let monitor = TopicMonitor::default(); - let gens = GenerationsList::new(); - let t = Topic::sigchld; - gens.sigchld.set(0); - assert_eq!(monitor.generation_for_topic(t), 0); - let changed = monitor.check(&gens, false /* wait */); - assert!(!changed); - assert_eq!(gens.sigchld.get(), 0); - - monitor.post(t); - let changed = monitor.check(&gens, true /* wait */); - assert!(changed); - assert_eq!(gens.get(t), 1); - assert_eq!(monitor.generation_for_topic(t), 1); - - monitor.post(t); - assert_eq!(monitor.generation_for_topic(t), 2); - let changed = monitor.check(&gens, true /* wait */); - assert!(changed); - assert_eq!(gens.sigchld.get(), 2); -} - -#[test] -#[serial] -fn test_topic_monitor_torture() { - let _cleanup = test_init(); - let monitor = Arc::new(TopicMonitor::default()); - const THREAD_COUNT: usize = 64; - let t1 = Topic::sigchld; - let t2 = Topic::sighupint; - let mut gens_list = vec![GenerationsList::invalid(); THREAD_COUNT]; - let post_count = Arc::new(AtomicU64::new(0)); - for r#gen in &mut gens_list { - *r#gen = monitor.current_generations(); - post_count.fetch_add(1, Ordering::Relaxed); - monitor.post(t1); - } - - let completed = Arc::new(AtomicU32::new(0)); - let mut threads = vec![]; - - for gens in gens_list { - let monitor = Arc::downgrade(&monitor); - let post_count = Arc::downgrade(&post_count); - let completed = Arc::downgrade(&completed); - threads.push(std::thread::spawn(move || { - for _ in 0..1 << 11 { - let before = gens.clone(); - let _changed = monitor.upgrade().unwrap().check(&gens, true /* wait */); - assert!(before.get(t1) < gens.get(t1)); - assert!(gens.get(t1) <= post_count.upgrade().unwrap().load(Ordering::Relaxed)); - assert_eq!(gens.get(t2), 0); - } - let _amt = completed.upgrade().unwrap().fetch_add(1, Ordering::Relaxed); - })); - } - - while completed.load(Ordering::Relaxed) < THREAD_COUNT.try_into().unwrap() { - post_count.fetch_add(1, Ordering::Relaxed); - monitor.post(t1); - std::thread::yield_now(); - } - for t in threads { - t.join().unwrap(); - } -} diff --git a/src/tests/wgetopt.rs b/src/tests/wgetopt.rs deleted file mode 100644 index e97ffef8b..000000000 --- a/src/tests/wgetopt.rs +++ /dev/null @@ -1,43 +0,0 @@ -use crate::wchar::prelude::*; -use crate::wcstringutil::join_strings; -use crate::wgetopt::{ArgType, WGetopter, WOption, wopt}; - -#[test] -fn test_wgetopt() { - // Regression test for a crash. - const short_options: &wstr = L!("-a"); - const long_options: &[WOption] = &[wopt(L!("add"), ArgType::NoArgument, 'a')]; - let mut argv = [ - L!("abbr"), - L!("--add"), - L!("emacsnw"), - L!("emacs"), - L!("-nw"), - ]; - let mut w = WGetopter::new(short_options, long_options, &mut argv); - let mut a_count = 0; - let mut arguments = vec![]; - while let Some(opt) = w.next_opt() { - match opt { - 'a' => { - a_count += 1; - } - '\x01' => { - // non-option argument - arguments.push(w.woptarg.as_ref().unwrap().to_owned()); - } - '?' => { - // unrecognized option - if let Some(arg) = w.argv.get(w.wopt_index - 1) { - arguments.push(arg.to_owned()); - } - } - _ => { - panic!("unexpected option: {:?}", opt); - } - } - } - assert_eq!(a_count, 1); - assert_eq!(arguments.len(), 3); - assert_eq!(join_strings(&arguments, ' '), "emacsnw emacs -nw"); -} diff --git a/src/threads.rs b/src/threads.rs index 56b5178af..c6bdd2c96 100644 --- a/src/threads.rs +++ b/src/threads.rs @@ -109,14 +109,6 @@ fn thread_id() -> usize { id } -#[test] -fn test_thread_ids() { - let start_thread_id = thread_id(); - assert_eq!(start_thread_id, thread_id()); - let spawned_thread_id = std::thread::spawn(thread_id).join(); - assert_ne!(start_thread_id, spawned_thread_id.unwrap()); -} - #[inline(always)] pub fn is_main_thread() -> bool { thread_id() == main_thread_id() @@ -677,67 +669,250 @@ fn perform_inner(&self, work_item: WorkItem) -> NonZeroU64 { } } -#[test] -/// Verify that spawning a thread normally via [`std::thread::spawn()`] causes the calling thread's -/// sigmask to be inherited by the newly spawned thread. -fn std_thread_inherits_sigmask() { - // First change our own thread mask - let (saved_set, t1_set) = unsafe { - let mut new_set = MaybeUninit::uninit(); - let new_set = new_set.as_mut_ptr(); - libc::sigemptyset(new_set); - libc::sigaddset(new_set, libc::SIGILL); // mask bad jump - - let mut saved_set: libc::sigset_t = std::mem::zeroed(); - let result = libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set as *mut _); - assert_eq!(result, 0, "Failed to set thread mask!"); - - // Now get the current set that includes the masked SIGILL - let mut t1_set: libc::sigset_t = std::mem::zeroed(); - let mut empty_set = MaybeUninit::uninit(); - let empty_set = empty_set.as_mut_ptr(); - libc::sigemptyset(empty_set); - let result = libc::pthread_sigmask(libc::SIG_UNBLOCK, empty_set, &mut t1_set as *mut _); - assert_eq!(result, 0, "Failed to get own altered thread mask!"); - - (saved_set, t1_set) +#[cfg(test)] +mod tests { + use super::{Debounce, iothread_drain_all, iothread_service_main, spawn, thread_id}; + use crate::global_safety::RelaxedAtomicBool; + use crate::reader::{Reader, fake_scoped_reader}; + use crate::tests::prelude::*; + use std::mem::MaybeUninit; + use std::sync::{ + Arc, Condvar, Mutex, + atomic::{AtomicI32, AtomicU32, Ordering}, }; + use std::time::Duration; - // Launch a new thread that can access existing variables - let t2_set = std::thread::scope(|_| { - unsafe { - // Set a new thread sigmask and verify that the old one is what we expect it to be + #[test] + fn test_thread_ids() { + let start_thread_id = thread_id(); + assert_eq!(start_thread_id, thread_id()); + let spawned_thread_id = std::thread::spawn(thread_id).join(); + assert_ne!(start_thread_id, spawned_thread_id.unwrap()); + } + + #[test] + /// Verify that spawning a thread normally via [`std::thread::spawn()`] causes the calling thread's + /// sigmask to be inherited by the newly spawned thread. + fn std_thread_inherits_sigmask() { + // First change our own thread mask + let (saved_set, t1_set) = unsafe { let mut new_set = MaybeUninit::uninit(); let new_set = new_set.as_mut_ptr(); libc::sigemptyset(new_set); - let mut saved_set2: libc::sigset_t = std::mem::zeroed(); - let result = libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set2 as *mut _); - assert_eq!(result, 0, "Failed to get existing sigmask for new thread"); - saved_set2 + libc::sigaddset(new_set, libc::SIGILL); // mask bad jump + + let mut saved_set: libc::sigset_t = std::mem::zeroed(); + let result = libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set as *mut _); + assert_eq!(result, 0, "Failed to set thread mask!"); + + // Now get the current set that includes the masked SIGILL + let mut t1_set: libc::sigset_t = std::mem::zeroed(); + let mut empty_set = MaybeUninit::uninit(); + let empty_set = empty_set.as_mut_ptr(); + libc::sigemptyset(empty_set); + let result = libc::pthread_sigmask(libc::SIG_UNBLOCK, empty_set, &mut t1_set as *mut _); + assert_eq!(result, 0, "Failed to get own altered thread mask!"); + + (saved_set, t1_set) + }; + + // Launch a new thread that can access existing variables + let t2_set = std::thread::scope(|_| { + unsafe { + // Set a new thread sigmask and verify that the old one is what we expect it to be + let mut new_set = MaybeUninit::uninit(); + let new_set = new_set.as_mut_ptr(); + libc::sigemptyset(new_set); + let mut saved_set2: libc::sigset_t = std::mem::zeroed(); + let result = + libc::pthread_sigmask(libc::SIG_BLOCK, new_set, &mut saved_set2 as *mut _); + assert_eq!(result, 0, "Failed to get existing sigmask for new thread"); + saved_set2 + } + }); + + // Compare the sigset_t values + unsafe { + let t1_sigset_slice = std::slice::from_raw_parts( + &t1_set as *const _ as *const u8, + core::mem::size_of::(), + ); + let t2_sigset_slice = std::slice::from_raw_parts( + &t2_set as *const _ as *const u8, + core::mem::size_of::(), + ); + + assert_eq!(t1_sigset_slice, t2_sigset_slice); + }; + + // Restore the thread sigset so we don't affect `cargo test`'s multithreaded test harnesses + unsafe { + let result = libc::pthread_sigmask( + libc::SIG_SETMASK, + &saved_set as *const _, + core::ptr::null_mut(), + ); + assert_eq!(result, 0, "Failed to restore sigmask!"); } - }); + } - // Compare the sigset_t values - unsafe { - let t1_sigset_slice = std::slice::from_raw_parts( - &t1_set as *const _ as *const u8, - core::mem::size_of::(), - ); - let t2_sigset_slice = std::slice::from_raw_parts( - &t2_set as *const _ as *const u8, - core::mem::size_of::(), - ); + #[test] + fn test_pthread() { + struct Context { + val: AtomicI32, + condvar: Condvar, + } + let ctx = Arc::new(Context { + val: AtomicI32::new(3), + condvar: Condvar::new(), + }); + let mutex = Mutex::new(()); + let ctx2 = ctx.clone(); + let made = spawn(move || { + ctx2.val.fetch_add(2, Ordering::Release); + ctx2.condvar.notify_one(); + printf!("condvar signalled\n"); + }); + assert!(made); - assert_eq!(t1_sigset_slice, t2_sigset_slice); - }; + let lock = mutex.lock().unwrap(); + let (_lock, timeout) = ctx + .condvar + .wait_timeout_while(lock, Duration::from_secs(5), |()| { + printf!("looping with lock held\n"); + if ctx.val.load(Ordering::Acquire) != 5 { + printf!("test_pthread: value did not yet reach goal\n"); + return true; + } + false + }) + .unwrap(); + if timeout.timed_out() { + panic!(concat!( + "Timeout waiting for condition variable to be notified! ", + "Does the platform support signalling a condvar without the mutex held?" + )); + } + } - // Restore the thread sigset so we don't affect `cargo test`'s multithreaded test harnesses - unsafe { - let result = libc::pthread_sigmask( - libc::SIG_SETMASK, - &saved_set as *const _, - core::ptr::null_mut(), - ); - assert_eq!(result, 0, "Failed to restore sigmask!"); + #[test] + #[serial] + fn test_debounce() { + let _cleanup = test_init(); + let parser = TestParser::new(); + // Run 8 functions using a condition variable. + // Only the first and last should run. + let db = Debounce::new(Duration::from_secs(0)); + const count: usize = 8; + + struct Context { + handler_ran: [RelaxedAtomicBool; count], + completion_ran: [RelaxedAtomicBool; count], + ready_to_go: Mutex, + cv: Condvar, + } + + let ctx = Arc::new(Context { + handler_ran: std::array::from_fn(|_i| RelaxedAtomicBool::new(false)), + completion_ran: std::array::from_fn(|_i| RelaxedAtomicBool::new(false)), + ready_to_go: Mutex::new(false), + cv: Condvar::new(), + }); + + // "Enqueue" all functions. Each one waits until ready_to_go. + for idx in 0..count { + assert!(!ctx.handler_ran[idx].load()); + let performer = { + let ctx = ctx.clone(); + move || { + let guard = ctx.ready_to_go.lock().unwrap(); + let _guard = ctx.cv.wait_while(guard, |ready| !*ready).unwrap(); + ctx.handler_ran[idx].store(true); + idx + } + }; + let completer = { + let ctx = ctx.clone(); + move |_ctx: &mut Reader, idx: usize| { + ctx.completion_ran[idx].store(true); + } + }; + db.perform_with_completion(performer, completer); + } + + // We're ready to go. + *ctx.ready_to_go.lock().unwrap() = true; + ctx.cv.notify_all(); + + // Wait until the last completion is done. + let mut reader = fake_scoped_reader(&parser); + while !ctx.completion_ran.last().unwrap().load() { + iothread_service_main(&mut reader); + } + iothread_drain_all(&mut reader); + + // Each perform() call may displace an existing queued operation. + // Each operation waits until all are queued. + // Therefore we expect the last perform() to have run, and at most one more. + assert!(ctx.handler_ran.last().unwrap().load()); + assert!(ctx.completion_ran.last().unwrap().load()); + + let mut total_ran = 0; + for idx in 0..count { + if ctx.handler_ran[idx].load() { + total_ran += 1; + } + assert_eq!(ctx.handler_ran[idx].load(), ctx.completion_ran[idx].load()); + } + assert!(total_ran <= 2); + } + + #[test] + #[serial] + fn test_debounce_timeout() { + let _cleanup = test_init(); + // Verify that debounce doesn't wait forever. + // Use a shared_ptr so we don't have to join our threads. + let timeout = Duration::from_millis(500); + + struct Data { + db: Debounce, + exit_ok: Mutex, + cv: Condvar, + running: AtomicU32, + } + + let data = Arc::new(Data { + db: Debounce::new(timeout), + exit_ok: Mutex::new(false), + cv: Condvar::new(), + running: AtomicU32::new(0), + }); + + // Our background handler. Note this just blocks until exit_ok is set. + let handler = { + let data = data.clone(); + move || { + data.running.fetch_add(1, Ordering::Relaxed); + let guard = data.exit_ok.lock().unwrap(); + let _guard = data.cv.wait_while(guard, |exit_ok| !*exit_ok); + } + }; + + // Spawn the handler twice. This should not modify the thread token. + let token1 = data.db.perform(handler.clone()); + let token2 = data.db.perform(handler.clone()); + assert_eq!(token1, token2); + + // Wait 75 msec, then enqueue something else; this should spawn a new thread. + std::thread::sleep(timeout + timeout / 2); + assert!(data.running.load(Ordering::Relaxed) == 1); + let token3 = data.db.perform(handler); + assert!(token3 > token2); + + // Release all the threads. + let mut exit_ok = data.exit_ok.lock().unwrap(); + *exit_ok = true; + data.cv.notify_all(); } } diff --git a/src/timer.rs b/src/timer.rs index 53ab1f817..31f09cf88 100644 --- a/src/timer.rs +++ b/src/timer.rs @@ -184,31 +184,37 @@ fn convert_micros(&self, micros: i64) -> f64 { } } -#[test] -fn timer_format_and_alignment() { - let mut t1 = TimerSnapshot::take(); - t1.cpu_fish.ru_utime.tv_usec = 0; - t1.cpu_fish.ru_stime.tv_usec = 0; - t1.cpu_children.ru_utime.tv_usec = 0; - t1.cpu_children.ru_stime.tv_usec = 0; +#[cfg(test)] +mod tests { + use super::TimerSnapshot; + use std::time::Duration; - let mut t2 = TimerSnapshot::take(); - t2.cpu_fish.ru_utime.tv_usec = 999995; - t2.cpu_fish.ru_stime.tv_usec = 999994; - t2.cpu_children.ru_utime.tv_usec = 1000; - t2.cpu_children.ru_stime.tv_usec = 500; - t2.wall_time = t1.wall_time + Duration::from_micros(500); + #[test] + fn timer_format_and_alignment() { + let mut t1 = TimerSnapshot::take(); + t1.cpu_fish.ru_utime.tv_usec = 0; + t1.cpu_fish.ru_stime.tv_usec = 0; + t1.cpu_children.ru_utime.tv_usec = 0; + t1.cpu_children.ru_stime.tv_usec = 0; - let expected = r#" + let mut t2 = TimerSnapshot::take(); + t2.cpu_fish.ru_utime.tv_usec = 999995; + t2.cpu_fish.ru_stime.tv_usec = 999994; + t2.cpu_children.ru_utime.tv_usec = 1000; + t2.cpu_children.ru_stime.tv_usec = 500; + t2.wall_time = t1.wall_time + Duration::from_micros(500); + + let expected = r#" ________________________________________________________ Executed in 500.00 micros fish external usr time 1.00 secs 1.00 secs 1.00 millis sys time 1.00 secs 1.00 secs 0.50 millis "#; - // (a) (b) (c) - // (a) remaining columns should align even if there are different units - // (b) carry to the next unit when it would overflow %6.2F - // (c) carry to the next unit when the larger one exceeds 1000 - let actual = TimerSnapshot::get_delta(&t1, &t2, true); - assert_eq!(actual, expected); + // (a) (b) (c) + // (a) remaining columns should align even if there are different units + // (b) carry to the next unit when it would overflow %6.2F + // (c) carry to the next unit when the larger one exceeds 1000 + let actual = TimerSnapshot::get_delta(&t1, &t2, true); + assert_eq!(actual, expected); + } } diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 0005e417b..e7fa528d5 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -1397,3 +1397,353 @@ pub fn variable_assignment_equals_pos(txt: &wstr) -> Option { } None } + +#[cfg(test)] +mod tests { + use super::{ + MoveWordStateMachine, MoveWordStyle, PipeOrRedir, TokFlags, TokenType, Tokenizer, + TokenizerError, + }; + use crate::redirection::RedirectionMode; + use crate::wchar::prelude::*; + use libc::{STDERR_FILENO, STDOUT_FILENO}; + use std::collections::HashSet; + + #[test] + fn test_tokenizer() { + { + let s = L!("alpha beta"); + let mut t = Tokenizer::new(s, TokFlags(0)); + + let token = t.next(); // alpha + assert!(token.is_some()); + let token = token.unwrap(); + assert_eq!(token.type_, TokenType::string); + assert_eq!(token.length, 5); + assert_eq!(t.text_of(&token), "alpha"); + + let token = t.next(); // beta + assert!(token.is_some()); + let token = token.unwrap(); + assert_eq!(token.type_, TokenType::string); + assert_eq!(token.offset, 6); + assert_eq!(token.length, 4); + assert_eq!(t.text_of(&token), "beta"); + + assert!(t.next().is_none()); + } + + { + let s = L!("{ echo"); + let mut t = Tokenizer::new(s, TokFlags(0)); + + let token = t.next(); // { + assert!(token.is_some()); + let token = token.unwrap(); + assert_eq!(token.type_, TokenType::left_brace); + assert_eq!(token.length, 1); + assert_eq!(t.text_of(&token), "{"); + + let token = t.next(); // echo + assert!(token.is_some()); + let token = token.unwrap(); + assert_eq!(token.type_, TokenType::string); + assert_eq!(token.offset, 2); + assert_eq!(token.length, 4); + assert_eq!(t.text_of(&token), "echo"); + + assert!(t.next().is_none()); + } + + { + let s = L!("{echo, foo}"); + let mut t = Tokenizer::new(s, TokFlags(0)); + let token = t.next().unwrap(); + assert_eq!(token.type_, TokenType::left_brace); + assert_eq!(token.length, 1); + } + { + let s = L!("{ echo; foo}"); + let mut t = Tokenizer::new(s, TokFlags(0)); + let token = t.next().unwrap(); + assert_eq!(token.type_, TokenType::left_brace); + } + + { + let s = L!("{ | { name } '"); + let mut t = Tokenizer::new(s, TokFlags(0)); + let mut next_type = || t.next().unwrap().type_; + assert_eq!(next_type(), TokenType::left_brace); + assert_eq!(next_type(), TokenType::pipe); + assert_eq!(next_type(), TokenType::left_brace); + assert_eq!(next_type(), TokenType::string); + assert_eq!(next_type(), TokenType::right_brace); + assert_eq!(next_type(), TokenType::error); + assert!(t.next().is_none()); + } + + let s = L!(concat!( + "string &1 'nested \"quoted\" '(string containing subshells ", + "){and,brackets}$as[$well (as variable arrays)] not_a_redirect^ ^ ^^is_a_redirect ", + "&| &> ", + "&&& ||| ", + "&& || & |", + "Compress_Newlines\n \n\t\n \nInto_Just_One", + )); + type tt = TokenType; + #[rustfmt::skip] + let types = [ + tt::string, tt::redirect, tt::string, tt::redirect, tt::string, tt::string, tt::string, + tt::string, tt::string, tt::pipe, tt::redirect, tt::andand, tt::background, tt::oror, + tt::pipe, tt::andand, tt::oror, tt::background, tt::pipe, tt::string, tt::end, + tt::string, + ]; + + { + let t = Tokenizer::new(s, TokFlags(0)); + let mut actual_types = vec![]; + for token in t { + actual_types.push(token.type_); + } + assert_eq!(&actual_types[..], types); + } + + // Test some errors. + + { + let mut t = Tokenizer::new(L!("abc\\"), TokFlags(0)); + let token = t.next().unwrap(); + assert_eq!(token.type_, TokenType::error); + assert_eq!(token.error, TokenizerError::unterminated_escape); + assert_eq!(token.error_offset_within_token, 3); + } + + { + let mut t = Tokenizer::new(L!("abc )defg(hij"), TokFlags(0)); + let _token = t.next().unwrap(); + let token = t.next().unwrap(); + assert_eq!(token.type_, TokenType::error); + assert_eq!(token.error, TokenizerError::closing_unopened_subshell); + assert_eq!(token.offset, 4); + assert_eq!(token.error_offset_within_token, 0); + } + + { + let mut t = Tokenizer::new(L!("abc defg(hij (klm)"), TokFlags(0)); + let _token = t.next().unwrap(); + let token = t.next().unwrap(); + assert_eq!(token.type_, TokenType::error); + assert_eq!(token.error, TokenizerError::unterminated_subshell); + assert_eq!(token.error_offset_within_token, 4); + } + + { + let mut t = Tokenizer::new(L!("abc defg[hij (klm)"), TokFlags(0)); + let _token = t.next().unwrap(); + let token = t.next().unwrap(); + assert_eq!(token.type_, TokenType::error); + assert_eq!(token.error, TokenizerError::unterminated_slice); + assert_eq!(token.error_offset_within_token, 4); + } + + // Test some redirection parsing. + macro_rules! pipe_or_redir { + ($s:literal) => { + PipeOrRedir::try_from(L!($s)).unwrap() + }; + } + + assert!(pipe_or_redir!("|").is_pipe); + assert!(pipe_or_redir!("0>|").is_pipe); + assert_eq!(pipe_or_redir!("0>|").fd, 0); + assert!(pipe_or_redir!("2>|").is_pipe); + assert_eq!(pipe_or_redir!("2>|").fd, 2); + assert!(pipe_or_redir!(">|").is_pipe); + assert_eq!(pipe_or_redir!(">|").fd, STDOUT_FILENO); + assert!(!pipe_or_redir!(">").is_pipe); + assert_eq!(pipe_or_redir!(">").fd, STDOUT_FILENO); + assert_eq!(pipe_or_redir!("2>").fd, STDERR_FILENO); + assert_eq!(pipe_or_redir!("9999999999999>").fd, -1); + assert_eq!(pipe_or_redir!("9999999999999>&2").fd, -1); + assert!(!pipe_or_redir!("9999999999999>&2").is_valid()); + assert!(!pipe_or_redir!("9999999999999>&2").is_valid()); + + assert!(pipe_or_redir!("&|").is_pipe); + assert!(pipe_or_redir!("&|").stderr_merge); + assert!(!pipe_or_redir!("&>").is_pipe); + assert!(pipe_or_redir!("&>").stderr_merge); + assert!(pipe_or_redir!("&>>").stderr_merge); + assert!(pipe_or_redir!("&>?").stderr_merge); + + macro_rules! get_redir_mode { + ($s:literal) => { + pipe_or_redir!($s).mode + }; + } + + assert_eq!(get_redir_mode!("<"), RedirectionMode::input); + assert_eq!(get_redir_mode!(">"), RedirectionMode::overwrite); + assert_eq!(get_redir_mode!("2>"), RedirectionMode::overwrite); + assert_eq!(get_redir_mode!(">>"), RedirectionMode::append); + assert_eq!(get_redir_mode!("2>>"), RedirectionMode::append); + assert_eq!(get_redir_mode!("2>?"), RedirectionMode::noclob); + assert_eq!( + get_redir_mode!("9999999999999999>?"), + RedirectionMode::noclob + ); + assert_eq!(get_redir_mode!("2>&3"), RedirectionMode::fd); + assert_eq!(get_redir_mode!("3<&0"), RedirectionMode::fd); + assert_eq!(get_redir_mode!("3 { + let mut command = WString::new(); + let mut stops = HashSet::new(); + + // Carets represent stops and should be cut out of the command. + for c in $line.chars() { + if c == '^' { + stops.insert(command.len()); + } else { + command.push(c); + } + } + + let (mut idx, end) = if $direction == Direction::Left { + (stops.iter().max().unwrap().clone(), 0) + } else { + (stops.iter().min().unwrap().clone(), command.len()) + }; + stops.remove(&idx); + + let mut sm = MoveWordStateMachine::new($style); + while idx != end { + let char_idx = if $direction == Direction::Left { + idx - 1 + } else { + idx + }; + let c = command.as_char_slice()[char_idx]; + let will_stop = !sm.consume_char(c); + let expected_stop = stops.contains(&idx); + assert_eq!(will_stop, expected_stop); + // We don't expect to stop here next time. + if expected_stop { + stops.remove(&idx); + sm.reset(); + } else { + if $direction == Direction::Left { + idx -= 1; + } else { + idx += 1; + } + } + } + }; + } + + validate!( + Direction::Left, + MoveWordStyle::Punctuation, + "^echo ^hello_^world.^txt^" + ); + validate!( + Direction::Right, + MoveWordStyle::Punctuation, + "^echo^ hello^_world^.txt^" + ); + + validate!( + Direction::Left, + MoveWordStyle::Punctuation, + "echo ^foo_^foo_^foo/^/^/^/^/^ ^" + ); + validate!( + Direction::Right, + MoveWordStyle::Punctuation, + "^echo^ foo^_foo^_foo^/^/^/^/^/ ^" + ); + + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^/^foo/^bar/^baz/^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^echo ^--foo ^--bar^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^echo ^hi ^> ^/^dev/^null^" + ); + + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^echo ^/^foo/^bar{^aaa,^bbb,^ccc}^bak/^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^echo ^bak ^///^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^aaa ^@ ^@^aaa^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^aaa ^a ^@^aaa^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^aaa ^@@@ ^@@^aa^" + ); + validate!( + Direction::Left, + MoveWordStyle::PathComponents, + "^aa^@@ ^aa@@^a^" + ); + + validate!(Direction::Right, MoveWordStyle::Punctuation, "^a^ bcd^"); + validate!(Direction::Right, MoveWordStyle::Punctuation, "a^b^ cde^"); + validate!(Direction::Right, MoveWordStyle::Punctuation, "^ab^ cde^"); + validate!( + Direction::Right, + MoveWordStyle::Punctuation, + "^ab^&cd^ ^& ^e^ f^&" + ); + + validate!( + Direction::Right, + MoveWordStyle::Whitespace, + "^^a-b-c^ d-e-f" + ); + validate!( + Direction::Right, + MoveWordStyle::Whitespace, + "^a-b-c^\n d-e-f^ " + ); + validate!( + Direction::Right, + MoveWordStyle::Whitespace, + "^a-b-c^\n\nd-e-f^ " + ); + } +} diff --git a/src/topic_monitor.rs b/src/topic_monitor.rs index 7c08cbc20..d71be1fcd 100644 --- a/src/topic_monitor.rs +++ b/src/topic_monitor.rs @@ -601,3 +601,88 @@ pub fn topic_monitor_principal() -> &'static TopicMonitor { &*s_principal } } + +#[cfg(test)] +mod tests { + use super::{GenerationsList, Topic, TopicMonitor}; + use crate::tests::prelude::*; + #[cfg(not(target_has_atomic = "64"))] + use portable_atomic::AtomicU64; + #[cfg(target_has_atomic = "64")] + use std::sync::atomic::AtomicU64; + use std::sync::{ + Arc, + atomic::{AtomicU32, Ordering}, + }; + + #[test] + #[serial] + fn test_topic_monitor() { + let _cleanup = test_init(); + let monitor = TopicMonitor::default(); + let gens = GenerationsList::new(); + let t = Topic::sigchld; + gens.sigchld.set(0); + assert_eq!(monitor.generation_for_topic(t), 0); + let changed = monitor.check(&gens, false /* wait */); + assert!(!changed); + assert_eq!(gens.sigchld.get(), 0); + + monitor.post(t); + let changed = monitor.check(&gens, true /* wait */); + assert!(changed); + assert_eq!(gens.get(t), 1); + assert_eq!(monitor.generation_for_topic(t), 1); + + monitor.post(t); + assert_eq!(monitor.generation_for_topic(t), 2); + let changed = monitor.check(&gens, true /* wait */); + assert!(changed); + assert_eq!(gens.sigchld.get(), 2); + } + + #[test] + #[serial] + fn test_topic_monitor_torture() { + let _cleanup = test_init(); + let monitor = Arc::new(TopicMonitor::default()); + const THREAD_COUNT: usize = 64; + let t1 = Topic::sigchld; + let t2 = Topic::sighupint; + let mut gens_list = vec![GenerationsList::invalid(); THREAD_COUNT]; + let post_count = Arc::new(AtomicU64::new(0)); + for r#gen in &mut gens_list { + *r#gen = monitor.current_generations(); + post_count.fetch_add(1, Ordering::Relaxed); + monitor.post(t1); + } + + let completed = Arc::new(AtomicU32::new(0)); + let mut threads = vec![]; + + for gens in gens_list { + let monitor = Arc::downgrade(&monitor); + let post_count = Arc::downgrade(&post_count); + let completed = Arc::downgrade(&completed); + threads.push(std::thread::spawn(move || { + for _ in 0..1 << 11 { + let before = gens.clone(); + let _changed = monitor.upgrade().unwrap().check(&gens, true /* wait */); + assert!(before.get(t1) < gens.get(t1)); + assert!(gens.get(t1) <= post_count.upgrade().unwrap().load(Ordering::Relaxed)); + assert_eq!(gens.get(t2), 0); + } + let _amt = completed.upgrade().unwrap().fetch_add(1, Ordering::Relaxed); + })); + } + + while completed.load(Ordering::Relaxed) < THREAD_COUNT.try_into().unwrap() { + post_count.fetch_add(1, Ordering::Relaxed); + monitor.post(t1); + std::thread::yield_now(); + } + for t in threads { + t.join().unwrap(); + } + } +} diff --git a/src/universal_notifier/inotify.rs b/src/universal_notifier/inotify.rs index 95c45d348..114872faa 100644 --- a/src/universal_notifier/inotify.rs +++ b/src/universal_notifier/inotify.rs @@ -65,32 +65,39 @@ fn notification_fd_became_readable(&self, fd: RawFd) -> bool { } } -#[test] -fn test_inotify_notifiers() { - use crate::common::{cstr2wcstring, wcs2osstring}; - use std::ffi::CString; - use std::fs::remove_dir_all; - use std::path::PathBuf; +#[cfg(test)] +mod tests { + use super::InotifyNotifier; + use crate::universal_notifier::{UniversalNotifier, test_helpers::test_notifiers}; - let template = CString::new("/tmp/fish_inotify_XXXXXX").unwrap(); - let temp_dir_ptr = unsafe { libc::mkdtemp(template.into_raw() as *mut libc::c_char) }; - if temp_dir_ptr.is_null() { - panic!("failed to create temp dir"); + #[test] + fn test_inotify_notifiers() { + use crate::common::{cstr2wcstring, wcs2osstring}; + use std::ffi::CString; + use std::fs::remove_dir_all; + use std::path::PathBuf; + + let template = CString::new("/tmp/fish_inotify_XXXXXX").unwrap(); + let temp_dir_ptr = unsafe { libc::mkdtemp(template.into_raw() as *mut libc::c_char) }; + if temp_dir_ptr.is_null() { + panic!("failed to create temp dir"); + } + let tmp_dir = unsafe { CString::from_raw(temp_dir_ptr) }; + let fake_uvars_dir = cstr2wcstring(tmp_dir.as_bytes_with_nul()); + let fake_uvars_path = fake_uvars_dir.clone() + "/fish_variables"; + + let mut notifiers = Vec::new(); + for _ in 0..16 { + notifiers.push( + InotifyNotifier::new_at(&fake_uvars_path).expect("failed to create notifier"), + ); + } + let notifiers = notifiers + .iter() + .map(|n| n as &dyn UniversalNotifier) + .collect::>(); + test_notifiers(¬ifiers, Some(&fake_uvars_path)); + + let _ = remove_dir_all(PathBuf::from(wcs2osstring(&fake_uvars_dir))); } - let tmp_dir = unsafe { CString::from_raw(temp_dir_ptr) }; - let fake_uvars_dir = cstr2wcstring(tmp_dir.as_bytes_with_nul()); - let fake_uvars_path = fake_uvars_dir.clone() + "/fish_variables"; - - let mut notifiers = Vec::new(); - for _ in 0..16 { - notifiers - .push(InotifyNotifier::new_at(&fake_uvars_path).expect("failed to create notifier")); - } - let notifiers = notifiers - .iter() - .map(|n| n as &dyn UniversalNotifier) - .collect::>(); - super::test_helpers::test_notifiers(¬ifiers, Some(&fake_uvars_path)); - - let _ = remove_dir_all(PathBuf::from(wcs2osstring(&fake_uvars_dir))); } diff --git a/src/universal_notifier/kqueue.rs b/src/universal_notifier/kqueue.rs index d33e314b6..6bbda35bc 100644 --- a/src/universal_notifier/kqueue.rs +++ b/src/universal_notifier/kqueue.rs @@ -178,33 +178,40 @@ fn notification_fd_became_readable(&self, fd: RawFd) -> bool { } } -#[test] -fn test_kqueue_notifiers() { - use crate::common::cstr2wcstring; - use std::ffi::CStr; - use std::fs::remove_dir_all; - use std::path::PathBuf; +#[cfg(test)] +mod tests { + use super::KqueueNotifier; + use crate::common::wcs2osstring; + use crate::universal_notifier::{UniversalNotifier, test_helpers::test_notifiers}; - let mut template: Box<[u8]> = Box::from(&b"/tmp/fish_kqueue_XXXXXX\0"[..]); + #[test] + fn test_kqueue_notifiers() { + use crate::common::cstr2wcstring; + use std::ffi::CStr; + use std::fs::remove_dir_all; + use std::path::PathBuf; - let temp_dir_ptr = unsafe { libc::mkdtemp(template.as_mut_ptr().cast()) }; - if temp_dir_ptr.is_null() { - panic!("failed to create temp dir"); + let mut template: Box<[u8]> = Box::from(&b"/tmp/fish_kqueue_XXXXXX\0"[..]); + + let temp_dir_ptr = unsafe { libc::mkdtemp(template.as_mut_ptr().cast()) }; + if temp_dir_ptr.is_null() { + panic!("failed to create temp dir"); + } + let tmp_dir = unsafe { CStr::from_ptr(temp_dir_ptr) }; + let fake_uvars_dir = cstr2wcstring(tmp_dir.to_bytes_with_nul()); + let fake_uvars_path = fake_uvars_dir.clone() + "/fish_variables"; + + let mut notifiers = Vec::new(); + for _ in 0..16 { + notifiers + .push(KqueueNotifier::new_at(&fake_uvars_path).expect("failed to create notifier")); + } + let notifiers = notifiers + .iter() + .map(|n| n as &dyn UniversalNotifier) + .collect::>(); + test_notifiers(¬ifiers, Some(&fake_uvars_path)); + + let _ = remove_dir_all(PathBuf::from(wcs2osstring(&fake_uvars_dir))); } - let tmp_dir = unsafe { CStr::from_ptr(temp_dir_ptr) }; - let fake_uvars_dir = cstr2wcstring(tmp_dir.to_bytes_with_nul()); - let fake_uvars_path = fake_uvars_dir.clone() + "/fish_variables"; - - let mut notifiers = Vec::new(); - for _ in 0..16 { - notifiers - .push(KqueueNotifier::new_at(&fake_uvars_path).expect("failed to create notifier")); - } - let notifiers = notifiers - .iter() - .map(|n| n as &dyn UniversalNotifier) - .collect::>(); - super::test_helpers::test_notifiers(¬ifiers, Some(&fake_uvars_path)); - - let _ = remove_dir_all(PathBuf::from(wcs2osstring(&fake_uvars_dir))); } diff --git a/src/universal_notifier/notifyd.rs b/src/universal_notifier/notifyd.rs index 2b10d57f6..cd010bed8 100644 --- a/src/universal_notifier/notifyd.rs +++ b/src/universal_notifier/notifyd.rs @@ -136,15 +136,21 @@ fn notification_fd_became_readable(&self, fd: RawFd) -> bool { } } -#[test] -fn test_notifyd_notifiers() { - let mut notifiers = Vec::new(); - for _ in 0..16 { - notifiers.push(NotifydNotifier::new().expect("failed to create notifier")); +#[cfg(test)] +mod tests { + use super::NotifydNotifier; + use crate::universal_notifier::{UniversalNotifier, test_helpers::test_notifiers}; + + #[test] + fn test_notifyd_notifiers() { + let mut notifiers = Vec::new(); + for _ in 0..16 { + notifiers.push(NotifydNotifier::new().expect("failed to create notifier")); + } + let notifiers = notifiers + .iter() + .map(|n| n as &dyn UniversalNotifier) + .collect::>(); + test_notifiers(¬ifiers, None); } - let notifiers = notifiers - .iter() - .map(|n| n as &dyn UniversalNotifier) - .collect::>(); - super::test_helpers::test_notifiers(¬ifiers, None); } diff --git a/src/util.rs b/src/util.rs index f31119e6a..ffd35b79a 100644 --- a/src/util.rs +++ b/src/util.rs @@ -271,52 +271,59 @@ pub fn find_subslice( haystack.windows(needle.len()).position(|w| w == needle) } -/// Verify the behavior of the `wcsfilecmp()` function. -#[test] -fn test_wcsfilecmp() { - macro_rules! validate { - ($str1:expr, $str2:expr, $expected_rc:expr) => { - assert_eq!(wcsfilecmp(L!($str1), L!($str2)), $expected_rc) - }; - } +#[cfg(test)] +mod tests { + use super::wcsfilecmp; + use crate::wchar::prelude::*; + use std::cmp::Ordering; - // Not using L as suffix because the macro munges error locations. - validate!("", "", Ordering::Equal); - validate!("", "def", Ordering::Less); - validate!("abc", "", Ordering::Greater); - validate!("abc", "def", Ordering::Less); - validate!("abc", "DEF", Ordering::Less); - validate!("DEF", "abc", Ordering::Greater); - validate!("abc", "abc", Ordering::Equal); - validate!("ABC", "ABC", Ordering::Equal); - validate!("AbC", "abc", Ordering::Less); - validate!("AbC", "ABC", Ordering::Greater); - validate!("def", "abc", Ordering::Greater); - validate!("1ghi", "1gHi", Ordering::Greater); - validate!("1ghi", "2ghi", Ordering::Less); - validate!("1ghi", "01ghi", Ordering::Greater); - validate!("1ghi", "02ghi", Ordering::Less); - validate!("01ghi", "1ghi", Ordering::Less); - validate!("1ghi", "002ghi", Ordering::Less); - validate!("002ghi", "1ghi", Ordering::Greater); - validate!("abc01def", "abc1def", Ordering::Less); - validate!("abc1def", "abc01def", Ordering::Greater); - validate!("abc12", "abc5", Ordering::Greater); - validate!("51abc", "050abc", Ordering::Greater); - validate!("abc5", "abc12", Ordering::Less); - validate!("5abc", "12ABC", Ordering::Less); - validate!("abc0789", "abc789", Ordering::Less); - validate!("abc0xA789", "abc0xA0789", Ordering::Greater); - validate!("abc002", "abc2", Ordering::Less); - validate!("abc002g", "abc002", Ordering::Greater); - validate!("abc002g", "abc02g", Ordering::Less); - validate!("abc002.txt", "abc02.txt", Ordering::Less); - validate!("abc005", "abc012", Ordering::Less); - validate!("abc02", "abc002", Ordering::Greater); - validate!("abc002.txt", "abc02.txt", Ordering::Less); - validate!("GHI1abc2.txt", "ghi1abc2.txt", Ordering::Less); - validate!("a0", "a00", Ordering::Less); - validate!("a00b", "a0b", Ordering::Less); - validate!("a0b", "a00b", Ordering::Greater); - validate!("a-b", "azb", Ordering::Greater); + /// Verify the behavior of the `wcsfilecmp()` function. + #[test] + fn test_wcsfilecmp() { + macro_rules! validate { + ($str1:expr, $str2:expr, $expected_rc:expr) => { + assert_eq!(wcsfilecmp(L!($str1), L!($str2)), $expected_rc) + }; + } + + // Not using L as suffix because the macro munges error locations. + validate!("", "", Ordering::Equal); + validate!("", "def", Ordering::Less); + validate!("abc", "", Ordering::Greater); + validate!("abc", "def", Ordering::Less); + validate!("abc", "DEF", Ordering::Less); + validate!("DEF", "abc", Ordering::Greater); + validate!("abc", "abc", Ordering::Equal); + validate!("ABC", "ABC", Ordering::Equal); + validate!("AbC", "abc", Ordering::Less); + validate!("AbC", "ABC", Ordering::Greater); + validate!("def", "abc", Ordering::Greater); + validate!("1ghi", "1gHi", Ordering::Greater); + validate!("1ghi", "2ghi", Ordering::Less); + validate!("1ghi", "01ghi", Ordering::Greater); + validate!("1ghi", "02ghi", Ordering::Less); + validate!("01ghi", "1ghi", Ordering::Less); + validate!("1ghi", "002ghi", Ordering::Less); + validate!("002ghi", "1ghi", Ordering::Greater); + validate!("abc01def", "abc1def", Ordering::Less); + validate!("abc1def", "abc01def", Ordering::Greater); + validate!("abc12", "abc5", Ordering::Greater); + validate!("51abc", "050abc", Ordering::Greater); + validate!("abc5", "abc12", Ordering::Less); + validate!("5abc", "12ABC", Ordering::Less); + validate!("abc0789", "abc789", Ordering::Less); + validate!("abc0xA789", "abc0xA0789", Ordering::Greater); + validate!("abc002", "abc2", Ordering::Less); + validate!("abc002g", "abc002", Ordering::Greater); + validate!("abc002g", "abc02g", Ordering::Less); + validate!("abc002.txt", "abc02.txt", Ordering::Less); + validate!("abc005", "abc012", Ordering::Less); + validate!("abc02", "abc002", Ordering::Greater); + validate!("abc002.txt", "abc02.txt", Ordering::Less); + validate!("GHI1abc2.txt", "ghi1abc2.txt", Ordering::Less); + validate!("a0", "a00", Ordering::Less); + validate!("a00b", "a0b", Ordering::Less); + validate!("a0b", "a00b", Ordering::Greater); + validate!("a-b", "azb", Ordering::Greater); + } } diff --git a/src/wait_handle.rs b/src/wait_handle.rs index 12cf0ef62..f564f01ac 100644 --- a/src/wait_handle.rs +++ b/src/wait_handle.rs @@ -121,42 +121,49 @@ pub fn size(&self) -> usize { } } -#[test] -fn test_wait_handles() { - let limit: usize = 4; - let mut whs = WaitHandleStore::new_with_capacity(limit); - assert_eq!(whs.size(), 0); +#[cfg(test)] +mod tests { + use super::{WaitHandle, WaitHandleStore}; + use crate::proc::Pid; + use crate::wchar::prelude::*; - fn p(pid: i32) -> Pid { - Pid::new(pid) + #[test] + fn test_wait_handles() { + let limit: usize = 4; + let mut whs = WaitHandleStore::new_with_capacity(limit); + assert_eq!(whs.size(), 0); + + fn p(pid: i32) -> Pid { + Pid::new(pid) + } + + assert!(whs.get_by_pid(p(5)).is_none()); + + // Duplicate pids drop oldest. + whs.add(WaitHandle::new(p(5), 0, L!("first").to_owned())); + whs.add(WaitHandle::new(p(5), 0, L!("second").to_owned())); + assert_eq!(whs.size(), 1); + assert_eq!(whs.get_by_pid(p(5)).unwrap().base_name, "second"); + + whs.remove_by_pid(p(123)); + assert_eq!(whs.size(), 1); + whs.remove_by_pid(p(5)); + assert_eq!(whs.size(), 0); + + // Test evicting oldest. + whs.add(WaitHandle::new(p(1), 0, L!("1").to_owned())); + whs.add(WaitHandle::new(p(2), 0, L!("2").to_owned())); + whs.add(WaitHandle::new(p(3), 0, L!("3").to_owned())); + whs.add(WaitHandle::new(p(4), 0, L!("4").to_owned())); + whs.add(WaitHandle::new(p(5), 0, L!("5").to_owned())); + assert_eq!(whs.size(), 4); + + let entries = whs.get_list(); + let mut iter = entries.iter(); + assert_eq!(iter.next().unwrap().base_name, "5"); + assert_eq!(iter.next().unwrap().base_name, "4"); + assert_eq!(iter.next().unwrap().base_name, "3"); + assert_eq!(iter.next().unwrap().base_name, "2"); + assert!(iter.next().is_none()); } - - assert!(whs.get_by_pid(p(5)).is_none()); - - // Duplicate pids drop oldest. - whs.add(WaitHandle::new(p(5), 0, L!("first").to_owned())); - whs.add(WaitHandle::new(p(5), 0, L!("second").to_owned())); - assert_eq!(whs.size(), 1); - assert_eq!(whs.get_by_pid(p(5)).unwrap().base_name, "second"); - - whs.remove_by_pid(p(123)); - assert_eq!(whs.size(), 1); - whs.remove_by_pid(p(5)); - assert_eq!(whs.size(), 0); - - // Test evicting oldest. - whs.add(WaitHandle::new(p(1), 0, L!("1").to_owned())); - whs.add(WaitHandle::new(p(2), 0, L!("2").to_owned())); - whs.add(WaitHandle::new(p(3), 0, L!("3").to_owned())); - whs.add(WaitHandle::new(p(4), 0, L!("4").to_owned())); - whs.add(WaitHandle::new(p(5), 0, L!("5").to_owned())); - assert_eq!(whs.size(), 4); - - let entries = whs.get_list(); - let mut iter = entries.iter(); - assert_eq!(iter.next().unwrap().base_name, "5"); - assert_eq!(iter.next().unwrap().base_name, "4"); - assert_eq!(iter.next().unwrap().base_name, "3"); - assert_eq!(iter.next().unwrap().base_name, "2"); - assert!(iter.next().is_none()); } diff --git a/src/wchar_ext.rs b/src/wchar_ext.rs index 3f8ac3a1c..e11ddc174 100644 --- a/src/wchar_ext.rs +++ b/src/wchar_ext.rs @@ -67,27 +67,6 @@ fn to_wstring(&self) -> WString { impl_to_wstring_unsigned!(u8, u16, u32, u64, u128, usize); -#[test] -fn test_to_wstring() { - assert_eq!(0_u64.to_wstring(), "0"); - assert_eq!(1_u64.to_wstring(), "1"); - assert_eq!(0_i64.to_wstring(), "0"); - assert_eq!(1_i64.to_wstring(), "1"); - assert_eq!((-1_i64).to_wstring(), "-1"); - assert_eq!((-5_i64).to_wstring(), "-5"); - let mut val: i64 = 1; - loop { - assert_eq!(val.to_wstring(), val.to_string()); - let Some(next) = val.checked_mul(-3) else { - break; - }; - val = next; - } - assert_eq!(u64::MAX.to_wstring(), "18446744073709551615"); - assert_eq!(i64::MIN.to_wstring(), "-9223372036854775808"); - assert_eq!(i64::MAX.to_wstring(), "9223372036854775807"); -} - /// A trait for a thing that can produce a double-ended, cloneable /// iterator of chars. /// Common implementations include char, &str, &wstr, &WString. @@ -325,7 +304,28 @@ fn as_char_slice(&self) -> &[char] { mod tests { use super::*; use crate::wchar::L; - /// Write some tests. + + #[test] + fn test_to_wstring() { + assert_eq!(0_u64.to_wstring(), "0"); + assert_eq!(1_u64.to_wstring(), "1"); + assert_eq!(0_i64.to_wstring(), "0"); + assert_eq!(1_i64.to_wstring(), "1"); + assert_eq!((-1_i64).to_wstring(), "-1"); + assert_eq!((-5_i64).to_wstring(), "-5"); + let mut val: i64 = 1; + loop { + assert_eq!(val.to_wstring(), val.to_string()); + let Some(next) = val.checked_mul(-3) else { + break; + }; + val = next; + } + assert_eq!(u64::MAX.to_wstring(), "18446744073709551615"); + assert_eq!(i64::MIN.to_wstring(), "-9223372036854775808"); + assert_eq!(i64::MAX.to_wstring(), "9223372036854775807"); + } + #[test] fn test_find_char() { assert_eq!(Some(0), L!("abc").find_char('a')); diff --git a/src/wcstringutil.rs b/src/wcstringutil.rs index 587deda93..8ec05fefb 100644 --- a/src/wcstringutil.rs +++ b/src/wcstringutil.rs @@ -534,173 +534,182 @@ pub fn fish_wcwidth_visible(c: char) -> isize { fish_wcwidth(c).max(0) } -#[test] -fn test_ifind() { - macro_rules! validate { - ($haystack:expr, $needle:expr, $expected:expr) => { - assert_eq!(ifind(L!($haystack), L!($needle), false), $expected); - }; +#[cfg(test)] +mod tests { + use super::{ + CaseSensitivity, ContainType, LineIterator, count_newlines, ifind, join_strings, + split_string_tok, string_fuzzy_match_string, + }; + use crate::wchar::prelude::*; + + #[test] + fn test_ifind() { + macro_rules! validate { + ($haystack:expr, $needle:expr, $expected:expr) => { + assert_eq!(ifind(L!($haystack), L!($needle), false), $expected); + }; + } + validate!("alpha", "alpha", Some(0)); + validate!("alphab", "alpha", Some(0)); + validate!("alpha", "balpha", None); + validate!("balpha", "alpha", Some(1)); + validate!("alphab", "balpha", None); + validate!("balpha", "lPh", Some(2)); + validate!("balpha", "Plh", None); + validate!("echo Ö", "ö", Some(5)); } - validate!("alpha", "alpha", Some(0)); - validate!("alphab", "alpha", Some(0)); - validate!("alpha", "balpha", None); - validate!("balpha", "alpha", Some(1)); - validate!("alphab", "balpha", None); - validate!("balpha", "lPh", Some(2)); - validate!("balpha", "Plh", None); - validate!("echo Ö", "ö", Some(5)); -} -#[test] -fn test_ifind_fuzzy() { - macro_rules! validate { - ($haystack:expr, $needle:expr, $expected:expr) => { - assert_eq!(ifind(L!($haystack), L!($needle), true), $expected); - }; + #[test] + fn test_ifind_fuzzy() { + macro_rules! validate { + ($haystack:expr, $needle:expr, $expected:expr) => { + assert_eq!(ifind(L!($haystack), L!($needle), true), $expected); + }; + } + validate!("alpha", "alpha", Some(0)); + validate!("alphab", "alpha", Some(0)); + validate!("alpha-b", "alpha_b", Some(0)); + validate!("alpha-_", "alpha_-", Some(0)); + validate!("alpha-b", "alpha b", None); } - validate!("alpha", "alpha", Some(0)); - validate!("alphab", "alpha", Some(0)); - validate!("alpha-b", "alpha_b", Some(0)); - validate!("alpha-_", "alpha_-", Some(0)); - validate!("alpha-b", "alpha b", None); -} -#[test] -fn test_fuzzy_match() { - // Check that a string fuzzy match has the expected type and case folding. - macro_rules! validate { - ($needle:expr, $haystack:expr, $contain_type:expr, $case_fold:expr) => { - let m = string_fuzzy_match_string(L!($needle), L!($haystack), false).unwrap(); - assert_eq!(m.typ, $contain_type); - assert_eq!(m.case_fold, $case_fold); - }; - ($needle:expr, $haystack:expr, None) => { - assert_eq!( - string_fuzzy_match_string(L!($needle), L!($haystack), false), - None, - ); - }; + #[test] + fn test_fuzzy_match() { + // Check that a string fuzzy match has the expected type and case folding. + macro_rules! validate { + ($needle:expr, $haystack:expr, $contain_type:expr, $case_fold:expr) => { + let m = string_fuzzy_match_string(L!($needle), L!($haystack), false).unwrap(); + assert_eq!(m.typ, $contain_type); + assert_eq!(m.case_fold, $case_fold); + }; + ($needle:expr, $haystack:expr, None) => { + assert_eq!( + string_fuzzy_match_string(L!($needle), L!($haystack), false), + None, + ); + }; + } + validate!("", "", ContainType::Exact, CaseSensitivity::Sensitive); + validate!( + "alpha", + "alpha", + ContainType::Exact, + CaseSensitivity::Sensitive + ); + validate!( + "alp", + "alpha", + ContainType::Prefix, + CaseSensitivity::Sensitive + ); + validate!("alpha", "AlPhA", ContainType::Exact, CaseSensitivity::Smart); + validate!( + "alpha", + "AlPhA!", + ContainType::Prefix, + CaseSensitivity::Smart + ); + validate!( + "ALPHA", + "alpha!", + ContainType::Prefix, + CaseSensitivity::Insensitive + ); + validate!( + "ALPHA!", + "alPhA!", + ContainType::Exact, + CaseSensitivity::Insensitive + ); + validate!( + "alPh", + "ALPHA!", + ContainType::Prefix, + CaseSensitivity::Insensitive + ); + validate!( + "LPH", + "ALPHA!", + ContainType::Substr, + CaseSensitivity::Sensitive + ); + validate!("lph", "AlPhA!", ContainType::Substr, CaseSensitivity::Smart); + validate!( + "lPh", + "ALPHA!", + ContainType::Substr, + CaseSensitivity::Insensitive + ); + validate!( + "AA", + "ALPHA!", + ContainType::Subseq, + CaseSensitivity::Sensitive + ); + // no subseq icase + validate!("lh", "ALPHA!", None); + validate!("BB", "ALPHA!", None); } - validate!("", "", ContainType::Exact, CaseSensitivity::Sensitive); - validate!( - "alpha", - "alpha", - ContainType::Exact, - CaseSensitivity::Sensitive - ); - validate!( - "alp", - "alpha", - ContainType::Prefix, - CaseSensitivity::Sensitive - ); - validate!("alpha", "AlPhA", ContainType::Exact, CaseSensitivity::Smart); - validate!( - "alpha", - "AlPhA!", - ContainType::Prefix, - CaseSensitivity::Smart - ); - validate!( - "ALPHA", - "alpha!", - ContainType::Prefix, - CaseSensitivity::Insensitive - ); - validate!( - "ALPHA!", - "alPhA!", - ContainType::Exact, - CaseSensitivity::Insensitive - ); - validate!( - "alPh", - "ALPHA!", - ContainType::Prefix, - CaseSensitivity::Insensitive - ); - validate!( - "LPH", - "ALPHA!", - ContainType::Substr, - CaseSensitivity::Sensitive - ); - validate!("lph", "AlPhA!", ContainType::Substr, CaseSensitivity::Smart); - validate!( - "lPh", - "ALPHA!", - ContainType::Substr, - CaseSensitivity::Insensitive - ); - validate!( - "AA", - "ALPHA!", - ContainType::Subseq, - CaseSensitivity::Sensitive - ); - // no subseq icase - validate!("lh", "ALPHA!", None); - validate!("BB", "ALPHA!", None); -} -#[test] -fn test_split_string_tok() { - macro_rules! validate { - ($val:expr, $seps:expr, $max_len:expr, $expected:expr) => { - assert_eq!(split_string_tok(L!($val), L!($seps), $max_len), $expected,); - }; + #[test] + fn test_split_string_tok() { + macro_rules! validate { + ($val:expr, $seps:expr, $max_len:expr, $expected:expr) => { + assert_eq!(split_string_tok(L!($val), L!($seps), $max_len), $expected,); + }; + } + validate!(" hello \t world", " \t\n", None, vec!["hello", "world"]); + validate!(" stuff ", " ", Some(0), vec![] as Vec<&wstr>); + validate!(" stuff ", " ", Some(1), vec![" stuff "]); + validate!( + " hello \t world andstuff ", + " \t\n", + Some(3), + vec!["hello", "world", " andstuff "] + ); + // NUL chars are OK. + validate!("hello \x00 world", " \0", None, vec!["hello", "world"]); } - validate!(" hello \t world", " \t\n", None, vec!["hello", "world"]); - validate!(" stuff ", " ", Some(0), vec![] as Vec<&wstr>); - validate!(" stuff ", " ", Some(1), vec![" stuff "]); - validate!( - " hello \t world andstuff ", - " \t\n", - Some(3), - vec!["hello", "world", " andstuff "] - ); - // NUL chars are OK. - validate!("hello \x00 world", " \0", None, vec!["hello", "world"]); -} -#[test] -fn test_join_strings() { - let empty: &[&wstr] = &[]; - assert_eq!(join_strings(empty, '/'), ""); - assert_eq!(join_strings(&[] as &[&wstr], '/'), ""); - assert_eq!(join_strings(&[L!("foo")], '/'), "foo"); - assert_eq!( - join_strings(&[L!("foo"), L!("bar"), L!("baz")], '/'), - "foo/bar/baz" - ); -} - -#[test] -fn test_line_iterator() { - let text = b"Alpha\nBeta\nGamma\n\nDelta\n"; - let mut lines = vec![]; - let iter = LineIterator::new(text); - for line in iter { - lines.push(line); + #[test] + fn test_join_strings() { + let empty: &[&wstr] = &[]; + assert_eq!(join_strings(empty, '/'), ""); + assert_eq!(join_strings(&[] as &[&wstr], '/'), ""); + assert_eq!(join_strings(&[L!("foo")], '/'), "foo"); + assert_eq!( + join_strings(&[L!("foo"), L!("bar"), L!("baz")], '/'), + "foo/bar/baz" + ); } - assert_eq!( - lines, - vec![ - &b"Alpha"[..], - &b"Beta"[..], - &b"Gamma"[..], - &b""[..], - &b"Delta"[..] - ] - ); -} -#[test] -fn test_count_newlines() { - assert_eq!(count_newlines(L!("")), 0); - assert_eq!(count_newlines(L!("foo")), 0); - assert_eq!(count_newlines(L!("foo\nbar")), 1); - assert_eq!(count_newlines(L!("foo\nbar\nbaz")), 2); - assert_eq!(count_newlines(L!("\n")), 1); - assert_eq!(count_newlines(L!("\n\n")), 2); + #[test] + fn test_line_iterator() { + let text = b"Alpha\nBeta\nGamma\n\nDelta\n"; + let mut lines = vec![]; + let iter = LineIterator::new(text); + for line in iter { + lines.push(line); + } + assert_eq!( + lines, + vec![ + &b"Alpha"[..], + &b"Beta"[..], + &b"Gamma"[..], + &b""[..], + &b"Delta"[..] + ] + ); + } + + #[test] + fn test_count_newlines() { + assert_eq!(count_newlines(L!("")), 0); + assert_eq!(count_newlines(L!("foo")), 0); + assert_eq!(count_newlines(L!("foo\nbar")), 1); + assert_eq!(count_newlines(L!("foo\nbar\nbaz")), 2); + assert_eq!(count_newlines(L!("\n")), 1); + assert_eq!(count_newlines(L!("\n\n")), 2); + } } diff --git a/src/wgetopt.rs b/src/wgetopt.rs index 419859014..b1b49c648 100644 --- a/src/wgetopt.rs +++ b/src/wgetopt.rs @@ -564,40 +564,87 @@ fn wgetopt_inner(&mut self, longopt_index: &mut usize) -> Option { } } -#[test] -fn test_exchange() { - let base_argv = [ - L!("0"), - L!("1"), - L!("2"), - L!("3"), - L!("4"), - L!("5"), - L!("6"), - ]; - let argc = base_argv.len(); - for start in 0..=argc { - for mid in start..=argc { - for end in mid..=argc { - let mut argv: Vec<&wstr> = base_argv.to_vec(); - // After exchange, we expect the start..mid and mid..end ranges to be swapped. - let mut expected = argv[mid..end].to_vec(); - expected.extend(argv[start..mid].iter()); +#[cfg(test)] +mod tests { + use super::{ArgType, WGetopter, WOption, wopt}; + use crate::wchar::prelude::*; + use crate::wcstringutil::join_strings; - let mut w = WGetopter::new(L!(""), &[], &mut argv); + #[test] + fn test_exchange() { + let base_argv = [ + L!("0"), + L!("1"), + L!("2"), + L!("3"), + L!("4"), + L!("5"), + L!("6"), + ]; + let argc = base_argv.len(); + for start in 0..=argc { + for mid in start..=argc { + for end in mid..=argc { + let mut argv: Vec<&wstr> = base_argv.to_vec(); + // After exchange, we expect the start..mid and mid..end ranges to be swapped. + let mut expected = argv[mid..end].to_vec(); + expected.extend(argv[start..mid].iter()); - w.first_nonopt = start; - w.last_nonopt = mid; - w.wopt_index = end; - w.exchange(); + let mut w = WGetopter::new(L!(""), &[], &mut argv); - // Non-options were permuted to the end. - let options_scanned = end - mid; - assert_eq!(w.first_nonopt, start + options_scanned); - assert_eq!(w.last_nonopt, mid + options_scanned); - assert_eq!(w.wopt_index, end); - assert_eq!(&w.argv[start..end], expected); + w.first_nonopt = start; + w.last_nonopt = mid; + w.wopt_index = end; + w.exchange(); + + // Non-options were permuted to the end. + let options_scanned = end - mid; + assert_eq!(w.first_nonopt, start + options_scanned); + assert_eq!(w.last_nonopt, mid + options_scanned); + assert_eq!(w.wopt_index, end); + assert_eq!(&w.argv[start..end], expected); + } } } } + + #[test] + fn test_wgetopt() { + // Regression test for a crash. + const short_options: &wstr = L!("-a"); + const long_options: &[WOption] = &[wopt(L!("add"), ArgType::NoArgument, 'a')]; + let mut argv = [ + L!("abbr"), + L!("--add"), + L!("emacsnw"), + L!("emacs"), + L!("-nw"), + ]; + let mut w = WGetopter::new(short_options, long_options, &mut argv); + let mut a_count = 0; + let mut arguments = vec![]; + while let Some(opt) = w.next_opt() { + match opt { + 'a' => { + a_count += 1; + } + '\x01' => { + // non-option argument + arguments.push(w.woptarg.as_ref().unwrap().to_owned()); + } + '?' => { + // unrecognized option + if let Some(arg) = w.argv.get(w.wopt_index - 1) { + arguments.push(arg.to_owned()); + } + } + _ => { + panic!("unexpected option: {:?}", opt); + } + } + } + assert_eq!(a_count, 1); + assert_eq!(arguments.len(), 3); + assert_eq!(join_strings(&arguments, ' '), "emacsnw emacs -nw"); + } } diff --git a/src/wutil/dir_iter.rs b/src/wutil/dir_iter.rs index f5adc942e..1c1cc64d3 100644 --- a/src/wutil/dir_iter.rs +++ b/src/wutil/dir_iter.rs @@ -326,174 +326,183 @@ fn next(&mut self) -> Option { } } -#[test] -fn test_dir_iter_bad_path() { - // Regression test: DirIter does not crash given a bad path. - use crate::wchar::L; - let dir = DirIter::new(L!("/a/bogus/path/which/does/notexist")); - assert!(dir.is_err()); -} +#[cfg(test)] +mod tests { + use super::{DirEntryType, DirIter}; + use crate::common::wcs2zstring; + use crate::wchar::prelude::*; -#[test] -fn test_no_dots() { - use crate::wchar::L; - // DirIter does not return . or .. by default. - let dir = DirIter::new(L!(".")).expect("Should be able to open CWD"); - for entry in dir { - let entry = entry.unwrap(); - assert_ne!(entry.name, "."); - assert_ne!(entry.name, ".."); + #[test] + fn test_dir_iter_bad_path() { + // Regression test: DirIter does not crash given a bad path. + use crate::wchar::L; + let dir = DirIter::new(L!("/a/bogus/path/which/does/notexist")); + assert!(dir.is_err()); } -} -#[test] -fn test_dots() { - use crate::wchar::L; - // DirIter returns . or .. if you ask nicely. - let dir = DirIter::new_with_dots(L!(".")).expect("Should be able to open CWD"); - let mut seen_dot = false; - let mut seen_dotdot = false; - for entry in dir { - let entry = entry.unwrap(); - if entry.name == "." { - seen_dot = true; - } else if entry.name == ".." { - seen_dotdot = true; + #[test] + fn test_no_dots() { + use crate::wchar::L; + // DirIter does not return . or .. by default. + let dir = DirIter::new(L!(".")).expect("Should be able to open CWD"); + for entry in dir { + let entry = entry.unwrap(); + assert_ne!(entry.name, "."); + assert_ne!(entry.name, ".."); } } - assert!(seen_dot); - assert!(seen_dotdot); -} -// Test ported from C++. -#[test] -#[allow(clippy::if_same_then_else)] -fn test_dir_iter() { - use crate::common::charptr2wcstring; - use crate::common::wcs2osstring; - use crate::wchar::L; - #[cfg(not(cygwin))] - use libc::symlink; - use libc::{O_CREAT, O_WRONLY, close, mkfifo, open}; - use std::ffi::CString; + #[test] + fn test_dots() { + use crate::wchar::L; + // DirIter returns . or .. if you ask nicely. + let dir = DirIter::new_with_dots(L!(".")).expect("Should be able to open CWD"); + let mut seen_dot = false; + let mut seen_dotdot = false; + for entry in dir { + let entry = entry.unwrap(); + if entry.name == "." { + seen_dot = true; + } else if entry.name == ".." { + seen_dotdot = true; + } + } + assert!(seen_dot); + assert!(seen_dotdot); + } - let baditer = DirIter::new(L!("/definitely/not/a/valid/directory/for/sure")); - assert!(baditer.is_err()); - let Err(err) = baditer else { - panic!("Expected error"); - }; - let err = err.raw_os_error().expect("Should have an errno value"); - assert!(err == ENOENT || err == EACCES); - - let mut t1: [u8; 31] = *b"/tmp/fish_test_dir_iter.XXXXXX\0"; - let basepath_narrow = unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }; - assert!(!basepath_narrow.is_null(), "mkdtemp failed"); - let basepath: WString = charptr2wcstring(basepath_narrow); - - let makepath = |s: &str| -> CString { - let mut tmp = basepath.clone(); - tmp.push('/'); - tmp.push_str(s); - wcs2zstring(&tmp) - }; - - let dirname = "dir"; - let regname = "reg"; - let reglinkname = "reglink"; // link to regular file - let dirlinkname = "dirlink"; // link to directory - let badlinkname = "badlink"; // link to nowhere - let selflinkname = "selflink"; // link to self - let fifoname = "fifo"; - #[rustfmt::skip] - let names = if cfg!(not(cygwin)) { - vec![ - dirname, regname, reglinkname, dirlinkname, - badlinkname, selflinkname, fifoname, - ] - } else { - // Symbolic links on Windows are complicated. Their behavior depends - // on the CYGWIN or MSYS env variable. So we skip that part of the test - vec![dirname, regname, fifoname] - }; - - #[cfg(not(cygwin))] - let is_link_name = |name: &wstr| -> bool { - name == reglinkname || name == dirlinkname || name == badlinkname || name == selflinkname - }; - - // Make our different file types - unsafe { - let mut ret = libc::mkdir(makepath(dirname).as_ptr(), 0o700); - assert!(ret == 0); - ret = open(makepath(regname).as_ptr(), O_CREAT | O_WRONLY, 0o600); - assert!(ret >= 0); - close(ret); + #[test] + #[allow(clippy::if_same_then_else)] + fn test_dir_iter() { + use crate::common::charptr2wcstring; + use crate::common::wcs2osstring; + use crate::wchar::L; #[cfg(not(cygwin))] - { - ret = symlink(makepath(regname).as_ptr(), makepath(reglinkname).as_ptr()); - assert!(ret == 0); - ret = symlink(makepath(dirname).as_ptr(), makepath(dirlinkname).as_ptr()); - assert!(ret == 0); - ret = symlink( - c"/this/is/an/invalid/path".as_ptr().cast(), - makepath(badlinkname).as_ptr(), - ); - assert!(ret == 0); - ret = symlink( - makepath(selflinkname).as_ptr(), - makepath(selflinkname).as_ptr(), - ); - assert!(ret == 0); - } + use libc::symlink; + use libc::{EACCES, ENOENT, O_CREAT, O_WRONLY, close, mkfifo, open}; + use std::ffi::CString; - ret = mkfifo(makepath(fifoname).as_ptr(), 0o600); - assert!(ret == 0); - } + let baditer = DirIter::new(L!("/definitely/not/a/valid/directory/for/sure")); + assert!(baditer.is_err()); + let Err(err) = baditer else { + panic!("Expected error"); + }; + let err = err.raw_os_error().expect("Should have an errno value"); + assert!(err == ENOENT || err == EACCES); - let mut iter1 = DirIter::new(&basepath).expect("Should be able to open directory"); - let mut seen = 0; - while let Some(entry) = iter1.next() { - let entry = entry.expect("Should not have gotten error"); - seen += 1; - assert!(entry.name != "." && entry.name != ".."); - assert!(names.iter().any(|&n| entry.name == n)); + let mut t1: [u8; 31] = *b"/tmp/fish_test_dir_iter.XXXXXX\0"; + let basepath_narrow = unsafe { libc::mkdtemp(t1.as_mut_ptr().cast()) }; + assert!(!basepath_narrow.is_null(), "mkdtemp failed"); + let basepath: WString = charptr2wcstring(basepath_narrow); - let expected = if entry.name == dirname { - Some(DirEntryType::dir) - } else if entry.name == regname { - Some(DirEntryType::reg) - } else if entry.name == reglinkname { - Some(DirEntryType::reg) - } else if entry.name == dirlinkname { - Some(DirEntryType::dir) - } else if entry.name == badlinkname { - None - } else if entry.name == selflinkname { - Some(DirEntryType::lnk) - } else if entry.name == fifoname { - Some(DirEntryType::fifo) - } else { - panic!("Unexpected file type"); + let makepath = |s: &str| -> CString { + let mut tmp = basepath.clone(); + tmp.push('/'); + tmp.push_str(s); + wcs2zstring(&tmp) }; - // Links should never have a fast type if we are resolving them, since we cannot resolve a - // symlink from readdir. - #[cfg(not(cygwin))] - if is_link_name(&entry.name) { - assert!(entry.fast_type().is_none()); - } - // If we have a fast type, it should be correct. - assert!(entry.fast_type().is_none() || entry.fast_type() == expected); - assert!( - entry.check_type() == expected, - "Wrong type for {}. Expected {:?}, got {:?}", - entry.name, - expected, - entry.check_type() - ); - } - assert_eq!(seen, names.len()); + let dirname = "dir"; + let regname = "reg"; + let reglinkname = "reglink"; // link to regular file + let dirlinkname = "dirlink"; // link to directory + let badlinkname = "badlink"; // link to nowhere + let selflinkname = "selflink"; // link to self + let fifoname = "fifo"; + #[rustfmt::skip] + let names = if cfg!(not(cygwin)) { + vec![ + dirname, regname, reglinkname, dirlinkname, + badlinkname, selflinkname, fifoname, + ] + } else { + // Symbolic links on Windows are complicated. Their behavior depends + // on the CYGWIN or MSYS env variable. So we skip that part of the test + vec![dirname, regname, fifoname] + }; - // Clean up. - let _ = std::fs::remove_dir_all(wcs2osstring(&basepath)); + #[cfg(not(cygwin))] + let is_link_name = |name: &wstr| -> bool { + name == reglinkname + || name == dirlinkname + || name == badlinkname + || name == selflinkname + }; + + // Make our different file types + unsafe { + let mut ret = libc::mkdir(makepath(dirname).as_ptr(), 0o700); + assert!(ret == 0); + ret = open(makepath(regname).as_ptr(), O_CREAT | O_WRONLY, 0o600); + assert!(ret >= 0); + close(ret); + #[cfg(not(cygwin))] + { + ret = symlink(makepath(regname).as_ptr(), makepath(reglinkname).as_ptr()); + assert!(ret == 0); + ret = symlink(makepath(dirname).as_ptr(), makepath(dirlinkname).as_ptr()); + assert!(ret == 0); + ret = symlink( + c"/this/is/an/invalid/path".as_ptr().cast(), + makepath(badlinkname).as_ptr(), + ); + assert!(ret == 0); + ret = symlink( + makepath(selflinkname).as_ptr(), + makepath(selflinkname).as_ptr(), + ); + assert!(ret == 0); + } + + ret = mkfifo(makepath(fifoname).as_ptr(), 0o600); + assert!(ret == 0); + } + + let mut iter1 = DirIter::new(&basepath).expect("Should be able to open directory"); + let mut seen = 0; + while let Some(entry) = iter1.next() { + let entry = entry.expect("Should not have gotten error"); + seen += 1; + assert!(entry.name != "." && entry.name != ".."); + assert!(names.iter().any(|&n| entry.name == n)); + + let expected = if entry.name == dirname { + Some(DirEntryType::dir) + } else if entry.name == regname { + Some(DirEntryType::reg) + } else if entry.name == reglinkname { + Some(DirEntryType::reg) + } else if entry.name == dirlinkname { + Some(DirEntryType::dir) + } else if entry.name == badlinkname { + None + } else if entry.name == selflinkname { + Some(DirEntryType::lnk) + } else if entry.name == fifoname { + Some(DirEntryType::fifo) + } else { + panic!("Unexpected file type"); + }; + + // Links should never have a fast type if we are resolving them, since we cannot resolve a + // symlink from readdir. + #[cfg(not(cygwin))] + if is_link_name(&entry.name) { + assert!(entry.fast_type().is_none()); + } + // If we have a fast type, it should be correct. + assert!(entry.fast_type().is_none() || entry.fast_type() == expected); + assert!( + entry.check_type() == expected, + "Wrong type for {}. Expected {:?}, got {:?}", + entry.name, + expected, + entry.check_type() + ); + } + assert_eq!(seen, names.len()); + + // Clean up. + let _ = std::fs::remove_dir_all(wcs2osstring(&basepath)); + } } diff --git a/src/wutil/gettext.rs b/src/wutil/gettext.rs index 35a6e2c97..e4d7ea381 100644 --- a/src/wutil/gettext.rs +++ b/src/wutil/gettext.rs @@ -2,8 +2,6 @@ #[cfg(feature = "localize-messages")] use crate::env::EnvStack; -#[cfg(test)] -use crate::tests::prelude::*; use crate::wchar::prelude::*; use once_cell::sync::Lazy; @@ -377,14 +375,21 @@ macro_rules! wgettext_fmt { } pub use wgettext_fmt; -#[test] -#[serial] -fn test_unlocalized() { - let _cleanup = test_init(); - let abc_str = LocalizableString::from_external_source(WString::from("abc")); - let s: &'static wstr = wgettext!(abc_str); - assert_eq!(s, "abc"); - let static_str = LocalizableString::from_external_source(WString::from("static")); - let s2: &'static wstr = wgettext!(static_str); - assert_eq!(s2, "static"); +#[cfg(test)] +mod tests { + use super::LocalizableString; + use crate::tests::prelude::*; + use crate::wchar::prelude::*; + + #[test] + #[serial] + fn test_unlocalized() { + let _cleanup = test_init(); + let abc_str = LocalizableString::from_external_source(WString::from("abc")); + let s: &'static wstr = wgettext!(abc_str); + assert_eq!(s, "abc"); + let static_str = LocalizableString::from_external_source(WString::from("static")); + let s2: &'static wstr = wgettext!(static_str); + assert_eq!(s2, "static"); + } } diff --git a/src/wutil/mod.rs b/src/wutil/mod.rs index bce8b5f8a..12108bdac 100644 --- a/src/wutil/mod.rs +++ b/src/wutil/mod.rs @@ -5,8 +5,6 @@ mod hex_float; #[macro_use] pub mod printf; -#[cfg(test)] -mod tests; pub mod wcstod; pub mod wcstoi; @@ -244,36 +242,6 @@ pub fn normalize_path(path: &wstr, allow_leading_double_slashes: bool) -> WStrin result } -#[test] -fn test_normalize_path() { - fn norm_path(path: &wstr) -> WString { - normalize_path(path, true) - } - assert_eq!(norm_path(L!("")), "."); - assert_eq!(norm_path(L!("..")), ".."); - assert_eq!(norm_path(L!("./")), "."); - assert_eq!(norm_path(L!("./.")), "."); - assert_eq!(norm_path(L!("/")), "/"); - assert_eq!(norm_path(L!("//")), "//"); - assert_eq!(norm_path(L!("///")), "/"); - assert_eq!(norm_path(L!("////")), "/"); - assert_eq!(norm_path(L!("/.///")), "/"); - assert_eq!(norm_path(L!(".//")), "."); - assert_eq!(norm_path(L!("/.//../")), "/"); - assert_eq!(norm_path(L!("////abc")), "/abc"); - assert_eq!(norm_path(L!("/abc")), "/abc"); - assert_eq!(norm_path(L!("/abc/")), "/abc"); - assert_eq!(norm_path(L!("/abc/..def/")), "/abc/..def"); - assert_eq!(norm_path(L!("//abc/../def/")), "//def"); - assert_eq!(norm_path(L!("abc/../abc/../abc/../abc")), "abc"); - assert_eq!(norm_path(L!("../../")), "../.."); - assert_eq!(norm_path(L!("foo/./bar")), "foo/bar"); - assert_eq!(norm_path(L!("foo/../")), "."); - assert_eq!(norm_path(L!("foo/../foo")), "foo"); - assert_eq!(norm_path(L!("foo/../foo/")), "foo"); - assert_eq!(norm_path(L!("foo/././bar/.././baz")), "foo/baz"); -} - /// Given an input path `path` and a working directory `wd`, do a "normalizing join" in a way /// appropriate for cd. That is, return effectively wd + path while resolving leading ../s from /// path. The intent here is to allow 'cd' out of a directory which may no longer exist, without @@ -330,98 +298,6 @@ pub fn path_normalize_for_cd(wd: &wstr, path: &wstr) -> WString { result } -#[cfg(test)] -mod path_cd_tests { - use super::path_normalize_for_cd; - use crate::wchar::L; - - #[test] - fn relative_path() { - let wd = L!("/home/user/"); - let path = L!("projects"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user/projects")); - } - - #[test] - fn absolute_path() { - let wd = L!("/home/user/"); - let path = L!("/etc"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/etc")); - } - - #[test] - fn parent_directory() { - let wd = L!("/home/user/projects/"); - let path = L!("../docs"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user/docs")); - } - - #[test] - fn current_directory() { - let wd = L!("/home/user/"); - let path = L!("./"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user")); - } - - #[test] - fn nested_parent_directory() { - let wd = L!("/home/user/projects/"); - let path = L!("../../"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/home")); - } - - #[test] - fn complex_path() { - let wd = L!("/home/user/projects/"); - let path = L!("./../other/projects/./.././../docs"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!( - path_normalize_for_cd(wd, path), - L!("/home/user/other/projects/./.././../docs") - ); - } - - #[test] - fn root_directory() { - let wd = L!("/"); - let path = L!(".."); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/..")); - } - - #[test] - fn up_to_root_directory() { - let wd = L!("/foo/"); - let path = L!(".."); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/")); - } - - #[test] - fn empty_path() { - let wd = L!("/home/user/"); - let path = L!(""); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user/")); - } - - #[test] - fn trailing_slash() { - let wd = L!("/home/user/projects/"); - let path = L!("docs/"); - eprintf!("(%s, %s)\n", wd, path); - assert_eq!( - path_normalize_for_cd(wd, path), - L!("/home/user/projects/docs/") - ); - } -} - /// Wide character version of dirname(). pub fn wdirname(mut path: &wstr) -> &wstr { // Do not use system-provided dirname (#7837). @@ -603,12 +479,238 @@ pub fn wstr_offset_in(cursor: &wstr, base: &wstr) -> usize { offset as usize } -#[test] -fn test_wstr_offset_in() { - use crate::wchar::L; - let base = L!("hello world"); - assert_eq!(wstr_offset_in(&base[6..], base), 6); - assert_eq!(wstr_offset_in(&base[0..], base), 0); - assert_eq!(wstr_offset_in(&base[6..], &base[6..]), 0); - assert_eq!(wstr_offset_in(&base[base.len()..], base), base.len()); +#[cfg(test)] +mod tests { + use super::{normalize_path, wbasename, wdirname, wstr_offset_in, wwrite_to_fd}; + use crate::common::wcs2bytes; + use crate::tests::prelude::*; + use crate::util::get_rng; + use crate::wchar::prelude::*; + use crate::{fds::AutoCloseFd, fs::create_temporary_file}; + use libc::{O_CREAT, O_RDWR, O_TRUNC, SEEK_SET, c_void}; + use rand::Rng; + use std::ffi::CString; + use std::ptr; + + mod test_path_normalize_for_cd { + use super::super::path_normalize_for_cd; + use crate::wchar::L; + + #[test] + fn relative_path() { + let wd = L!("/home/user/"); + let path = L!("projects"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user/projects")); + } + + #[test] + fn absolute_path() { + let wd = L!("/home/user/"); + let path = L!("/etc"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/etc")); + } + + #[test] + fn parent_directory() { + let wd = L!("/home/user/projects/"); + let path = L!("../docs"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user/docs")); + } + + #[test] + fn current_directory() { + let wd = L!("/home/user/"); + let path = L!("./"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user")); + } + + #[test] + fn nested_parent_directory() { + let wd = L!("/home/user/projects/"); + let path = L!("../../"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/home")); + } + + #[test] + fn complex_path() { + let wd = L!("/home/user/projects/"); + let path = L!("./../other/projects/./.././../docs"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!( + path_normalize_for_cd(wd, path), + L!("/home/user/other/projects/./.././../docs") + ); + } + + #[test] + fn root_directory() { + let wd = L!("/"); + let path = L!(".."); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/..")); + } + + #[test] + fn up_to_root_directory() { + let wd = L!("/foo/"); + let path = L!(".."); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/")); + } + + #[test] + fn empty_path() { + let wd = L!("/home/user/"); + let path = L!(""); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!(path_normalize_for_cd(wd, path), L!("/home/user/")); + } + + #[test] + fn trailing_slash() { + let wd = L!("/home/user/projects/"); + let path = L!("docs/"); + eprintf!("(%s, %s)\n", wd, path); + assert_eq!( + path_normalize_for_cd(wd, path), + L!("/home/user/projects/docs/") + ); + } + } + + #[test] + fn test_normalize_path() { + fn norm_path(path: &wstr) -> WString { + normalize_path(path, true) + } + assert_eq!(norm_path(L!("")), "."); + assert_eq!(norm_path(L!("..")), ".."); + assert_eq!(norm_path(L!("./")), "."); + assert_eq!(norm_path(L!("./.")), "."); + assert_eq!(norm_path(L!("/")), "/"); + assert_eq!(norm_path(L!("//")), "//"); + assert_eq!(norm_path(L!("///")), "/"); + assert_eq!(norm_path(L!("////")), "/"); + assert_eq!(norm_path(L!("/.///")), "/"); + assert_eq!(norm_path(L!(".//")), "."); + assert_eq!(norm_path(L!("/.//../")), "/"); + assert_eq!(norm_path(L!("////abc")), "/abc"); + assert_eq!(norm_path(L!("/abc")), "/abc"); + assert_eq!(norm_path(L!("/abc/")), "/abc"); + assert_eq!(norm_path(L!("/abc/..def/")), "/abc/..def"); + assert_eq!(norm_path(L!("//abc/../def/")), "//def"); + assert_eq!(norm_path(L!("abc/../abc/../abc/../abc")), "abc"); + assert_eq!(norm_path(L!("../../")), "../.."); + assert_eq!(norm_path(L!("foo/./bar")), "foo/bar"); + assert_eq!(norm_path(L!("foo/../")), "."); + assert_eq!(norm_path(L!("foo/../foo")), "foo"); + assert_eq!(norm_path(L!("foo/../foo/")), "foo"); + assert_eq!(norm_path(L!("foo/././bar/.././baz")), "foo/baz"); + } + + #[test] + fn test_wdirname_wbasename() { + // path, dir, base + struct Test(&'static wstr, &'static wstr, &'static wstr); + const testcases: &[Test] = &[ + Test(L!(""), L!("."), L!(".")), + Test(L!("foo//"), L!("."), L!("foo")), + Test(L!("foo//////"), L!("."), L!("foo")), + Test(L!("/////foo"), L!("/"), L!("foo")), + Test(L!("//foo/////bar"), L!("//foo"), L!("bar")), + Test(L!("foo/////bar"), L!("foo"), L!("bar")), + // Examples given in XPG4.2. + Test(L!("/usr/lib"), L!("/usr"), L!("lib")), + Test(L!("usr"), L!("."), L!("usr")), + Test(L!("/"), L!("/"), L!("/")), + Test(L!("."), L!("."), L!(".")), + Test(L!(".."), L!("."), L!("..")), + ]; + + for tc in testcases { + let Test(path, tc_dir, tc_base) = *tc; + let dir = wdirname(path); + assert_eq!( + dir, tc_dir, + "\npath: {:?}, dir: {:?}, tc.dir: {:?}", + path, dir, tc_dir + ); + + let base = wbasename(path); + assert_eq!( + base, tc_base, + "\npath: {:?}, base: {:?}, tc.base: {:?}", + path, base, tc_base + ); + } + + // Ensure strings which greatly exceed PATH_MAX still work (#7837). + const PATH_MAX: usize = libc::PATH_MAX as usize; + let mut longpath = WString::new(); + longpath.reserve(PATH_MAX * 2 + 10); + while longpath.char_count() <= PATH_MAX * 2 { + longpath.push_str("/overlong"); + } + let last_slash = longpath.chars().rposition(|c| c == '/').unwrap(); + let longpath_dir = &longpath[..last_slash]; + assert_eq!(wdirname(&longpath), longpath_dir); + assert_eq!(wbasename(&longpath), L!("overlong")); + } + + #[test] + #[serial] + fn test_wwrite_to_fd() { + let _cleanup = test_init(); + let (_file, filename) = create_temporary_file(L!("/tmp/fish_test_wwrite.XXXXXX")).unwrap(); + let filename = CString::new(filename.to_string()).unwrap(); + let mut rng = get_rng(); + let sizes = [1, 2, 3, 5, 13, 23, 64, 128, 255, 4096, 4096 * 2]; + for &size in &sizes { + let fd = AutoCloseFd::new(unsafe { + libc::open(filename.as_ptr(), O_RDWR | O_TRUNC | O_CREAT, 0o666) + }); + assert!(fd.is_valid()); + let mut input = WString::new(); + for _i in 0..size { + input.push(rng.r#gen()); + } + + let amt = wwrite_to_fd(&input, fd.fd()).unwrap(); + let narrow = wcs2bytes(&input); + assert_eq!(amt, narrow.len()); + + assert!(unsafe { libc::lseek(fd.fd(), 0, SEEK_SET) } >= 0); + + let mut contents = vec![0u8; narrow.len()]; + let read_amt = unsafe { + libc::read( + fd.fd(), + if size == 0 { + ptr::null_mut() + } else { + (&mut contents[0]) as *mut u8 as *mut c_void + }, + narrow.len(), + ) + }; + assert!(usize::try_from(read_amt).unwrap() == narrow.len()); + assert_eq!(&contents, &narrow); + } + unsafe { libc::remove(filename.as_ptr()) }; + } + + #[test] + fn test_wstr_offset_in() { + use crate::wchar::L; + let base = L!("hello world"); + assert_eq!(wstr_offset_in(&base[6..], base), 6); + assert_eq!(wstr_offset_in(&base[0..], base), 0); + assert_eq!(wstr_offset_in(&base[6..], &base[6..]), 0); + assert_eq!(wstr_offset_in(&base[base.len()..], base), base.len()); + } } diff --git a/src/wutil/tests.rs b/src/wutil/tests.rs deleted file mode 100644 index 1e362f34b..000000000 --- a/src/wutil/tests.rs +++ /dev/null @@ -1,100 +0,0 @@ -use crate::tests::prelude::*; -use crate::util::get_rng; -use crate::{fds::AutoCloseFd, fs::create_temporary_file}; -use libc::{O_CREAT, O_RDWR, O_TRUNC, SEEK_SET, c_void}; -use rand::Rng; -use std::ffi::CString; -use std::ptr; - -use super::*; - -#[test] -fn test_wdirname_wbasename() { - // path, dir, base - struct Test(&'static wstr, &'static wstr, &'static wstr); - const testcases: &[Test] = &[ - Test(L!(""), L!("."), L!(".")), - Test(L!("foo//"), L!("."), L!("foo")), - Test(L!("foo//////"), L!("."), L!("foo")), - Test(L!("/////foo"), L!("/"), L!("foo")), - Test(L!("//foo/////bar"), L!("//foo"), L!("bar")), - Test(L!("foo/////bar"), L!("foo"), L!("bar")), - // Examples given in XPG4.2. - Test(L!("/usr/lib"), L!("/usr"), L!("lib")), - Test(L!("usr"), L!("."), L!("usr")), - Test(L!("/"), L!("/"), L!("/")), - Test(L!("."), L!("."), L!(".")), - Test(L!(".."), L!("."), L!("..")), - ]; - - for tc in testcases { - let Test(path, tc_dir, tc_base) = *tc; - let dir = wdirname(path); - assert_eq!( - dir, tc_dir, - "\npath: {:?}, dir: {:?}, tc.dir: {:?}", - path, dir, tc_dir - ); - - let base = wbasename(path); - assert_eq!( - base, tc_base, - "\npath: {:?}, base: {:?}, tc.base: {:?}", - path, base, tc_base - ); - } - - // Ensure strings which greatly exceed PATH_MAX still work (#7837). - const PATH_MAX: usize = libc::PATH_MAX as usize; - let mut longpath = WString::new(); - longpath.reserve(PATH_MAX * 2 + 10); - while longpath.char_count() <= PATH_MAX * 2 { - longpath.push_str("/overlong"); - } - let last_slash = longpath.chars().rposition(|c| c == '/').unwrap(); - let longpath_dir = &longpath[..last_slash]; - assert_eq!(wdirname(&longpath), longpath_dir); - assert_eq!(wbasename(&longpath), L!("overlong")); -} - -#[test] -#[serial] -fn test_wwrite_to_fd() { - let _cleanup = test_init(); - let (_file, filename) = create_temporary_file(L!("/tmp/fish_test_wwrite.XXXXXX")).unwrap(); - let filename = CString::new(filename.to_string()).unwrap(); - let mut rng = get_rng(); - let sizes = [1, 2, 3, 5, 13, 23, 64, 128, 255, 4096, 4096 * 2]; - for &size in &sizes { - let fd = AutoCloseFd::new(unsafe { - libc::open(filename.as_ptr(), O_RDWR | O_TRUNC | O_CREAT, 0o666) - }); - assert!(fd.is_valid()); - let mut input = WString::new(); - for _i in 0..size { - input.push(rng.r#gen()); - } - - let amt = wwrite_to_fd(&input, fd.fd()).unwrap(); - let narrow = wcs2bytes(&input); - assert_eq!(amt, narrow.len()); - - assert!(unsafe { libc::lseek(fd.fd(), 0, SEEK_SET) } >= 0); - - let mut contents = vec![0u8; narrow.len()]; - let read_amt = unsafe { - libc::read( - fd.fd(), - if size == 0 { - ptr::null_mut() - } else { - (&mut contents[0]) as *mut u8 as *mut c_void - }, - narrow.len(), - ) - }; - assert!(usize::try_from(read_amt).unwrap() == narrow.len()); - assert_eq!(&contents, &narrow); - } - unsafe { libc::remove(filename.as_ptr()) }; -} diff --git a/src/wutil/wcstod.rs b/src/wutil/wcstod.rs index 0fcb38a2a..7c52a8a2f 100644 --- a/src/wutil/wcstod.rs +++ b/src/wutil/wcstod.rs @@ -279,7 +279,7 @@ pub fn wcstod_underscores(s: Chars, consumed: &mut usize) -> Result