fish-shell/src/tests/expand.rs

454 lines
13 KiB
Rust
Raw Normal View History

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::{expand_to_receiver, ExpandResultCode};
use crate::operation_context::{no_cancel, EXPANSION_LIMIT_DEFAULT};
use crate::parse_constants::ParseErrorList;
use crate::parser::Parser;
use crate::tests::prelude::*;
use crate::wildcard::ANY_STRING;
use crate::{
expand::{expand_string, ExpandFlags},
operation_context::OperationContext,
wchar::prelude::*,
};
use std::collections::hash_map::RandomState;
use std::collections::HashSet;
fn expand_test_impl(
input: &wstr,
flags: ExpandFlags,
expected: Vec<WString>,
error_message: Option<&str>,
) {
let mut output = CompletionList::new();
let mut errors = ParseErrorList::new();
let pwd = PwdEnvironment::default();
let ctx = OperationContext::test_only_foreground(
Parser::principal_parser().shared(),
&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<WString, RandomState> = 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();
/// 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
// .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/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/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",
""
);
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");
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<WString> = (1..=64).map(|i| i.to_wstring()).collect();
let expansion = WString::from_str(&str::repeat("$bigvar", 64));
let parser = Parser::principal_parser().shared();
parser.vars().push(true);
let set = parser.vars().set(L!("bigvar"), EnvMode::LOCAL, vals);
assert_eq!(set, EnvStackSetResult::ENV_OK);
let mut errors = ParseErrorList::new();
let ctx =
OperationContext::foreground(parser.clone(), 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<WString> {
let result = with_abbrs(|abbrset| abbrset.r#match(token, pos));
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()));
}