Merge nucleic/sleek-thistle-egret-fyej into dev
This commit is contained in:
@@ -0,0 +1,584 @@
|
||||
//! Completion integration tests for brush shell.
|
||||
|
||||
// For now, only compile this for Linux.
|
||||
#![cfg(target_os = "linux")]
|
||||
#![cfg(test)]
|
||||
#![allow(clippy::panic_in_result_fn)]
|
||||
|
||||
use anyhow::Result;
|
||||
use assert_fs::prelude::*;
|
||||
use brush_builtins::ShellBuilderExt;
|
||||
use std::path::PathBuf;
|
||||
|
||||
struct TestShellWithBashCompletion {
|
||||
shell: brush_core::Shell,
|
||||
temp_dir: assert_fs::TempDir,
|
||||
}
|
||||
|
||||
const DEFAULT_BASH_COMPLETION_SCRIPT: &str = "/usr/share/bash-completion/bash_completion";
|
||||
|
||||
impl TestShellWithBashCompletion {
|
||||
async fn new() -> Result<Self> {
|
||||
let mut shell = brush_core::Shell::builder()
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let temp_dir = assert_fs::TempDir::new()?;
|
||||
let bash_completion_script_path = Self::find_bash_completion_script()?;
|
||||
|
||||
let exec_params = shell.default_exec_params();
|
||||
let source_result = shell
|
||||
.source_script(
|
||||
bash_completion_script_path.as_path(),
|
||||
std::iter::empty::<String>(),
|
||||
&exec_params,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if !source_result.is_success() {
|
||||
return Err(anyhow::anyhow!("failed to source bash completion script"));
|
||||
}
|
||||
|
||||
shell.set_working_dir(temp_dir.path())?;
|
||||
|
||||
Ok(Self { shell, temp_dir })
|
||||
}
|
||||
|
||||
fn find_bash_completion_script() -> Result<PathBuf> {
|
||||
// See if an environmental override was provided.
|
||||
let script_path = std::env::var("BASH_COMPLETION_PATH").map_or_else(
|
||||
|_| PathBuf::from(DEFAULT_BASH_COMPLETION_SCRIPT),
|
||||
PathBuf::from,
|
||||
);
|
||||
|
||||
if script_path.exists() {
|
||||
Ok(script_path)
|
||||
} else {
|
||||
Err(anyhow::anyhow!(
|
||||
"bash completion script not found: {}",
|
||||
script_path.display()
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn complete_end_of_line(&mut self, line: &str) -> Result<Vec<String>> {
|
||||
self.complete(line, line.len()).await
|
||||
}
|
||||
|
||||
pub async fn complete(&mut self, line: &str, pos: usize) -> Result<Vec<String>> {
|
||||
let completions = self.shell.complete(line, pos).await?;
|
||||
Ok(completions
|
||||
.candidates
|
||||
.into_iter()
|
||||
.collect::<indexmap::IndexSet<_>>()
|
||||
.into_iter()
|
||||
.collect())
|
||||
}
|
||||
|
||||
pub fn set_var(&mut self, name: &str, value: &str) -> Result<()> {
|
||||
self.shell
|
||||
.env_mut()
|
||||
.set_global(name, brush_core::ShellVariable::new(value))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_relative_file_path() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("item1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
test_shell.temp_dir.child(".dot_item1").touch()?;
|
||||
test_shell.temp_dir.child("..dot_item2").touch()?;
|
||||
|
||||
// Complete; expect to see the two files.
|
||||
let mut results = test_shell.complete_end_of_line("ls item").await?;
|
||||
|
||||
assert_eq!(results, ["item1", "item2"]);
|
||||
|
||||
results = test_shell.complete_end_of_line("ls .").await?;
|
||||
// Some versions of bash-completion filter out "." and ".." via
|
||||
// `-X '?(*/)@(.|..)'`; others don't. Accept either outcome.
|
||||
assert!(
|
||||
results == ["..dot_item2", ".dot_item1"]
|
||||
|| results == [".", "..", "..dot_item2", ".dot_item1"],
|
||||
"unexpected completions for 'ls .': {results:?}"
|
||||
);
|
||||
|
||||
results = test_shell.complete_end_of_line("ls ..").await?;
|
||||
assert_eq!(results, ["..", "..dot_item2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_relative_file_path_ignoring_case() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
test_shell
|
||||
.shell
|
||||
.options_mut()
|
||||
.case_insensitive_pathname_expansion = true;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("ITEM1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
|
||||
// Complete; expect to see the two files.
|
||||
let results = test_shell.complete_end_of_line("ls item").await?;
|
||||
|
||||
assert_eq!(results, ["ITEM1", "item2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_relative_dir_path() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("item1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
|
||||
// Complete; expect to see just the dir.
|
||||
let results = test_shell.complete_end_of_line("cd item").await?;
|
||||
|
||||
assert_eq!(results, ["item2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_under_empty_dir() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("empty").create_dir_all()?;
|
||||
|
||||
// Complete; expect to see nothing.
|
||||
let results = test_shell.complete_end_of_line("ls empty/").await?;
|
||||
|
||||
assert_eq!(results, Vec::<String>::new());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_nonexistent_relative_path() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Complete; expect to see nothing.
|
||||
let results = test_shell.complete_end_of_line("ls item").await?;
|
||||
|
||||
assert_eq!(results, Vec::<String>::new());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_absolute_paths() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("item1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
|
||||
// Complete; expect to see just the dir.
|
||||
let input = std::format!("ls {}", test_shell.temp_dir.path().join("item").display());
|
||||
let results = test_shell.complete_end_of_line(input.as_str()).await?;
|
||||
|
||||
assert_eq!(
|
||||
results,
|
||||
[
|
||||
test_shell
|
||||
.temp_dir
|
||||
.child("item1")
|
||||
.path()
|
||||
.display()
|
||||
.to_string(),
|
||||
test_shell
|
||||
.temp_dir
|
||||
.child("item2")
|
||||
.path()
|
||||
.display()
|
||||
.to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_path_with_var() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("item1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
|
||||
// Complete; expect to see the two files.
|
||||
let results = test_shell.complete_end_of_line("ls $PWD/item").await?;
|
||||
|
||||
assert_eq!(
|
||||
results,
|
||||
[
|
||||
test_shell
|
||||
.temp_dir
|
||||
.child("item1")
|
||||
.path()
|
||||
.display()
|
||||
.to_string(),
|
||||
test_shell
|
||||
.temp_dir
|
||||
.child("item2")
|
||||
.path()
|
||||
.display()
|
||||
.to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_path_with_tilde() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Set HOME to the temp dir so we can use ~ to reference it.
|
||||
test_shell.set_var(
|
||||
"HOME",
|
||||
test_shell
|
||||
.temp_dir
|
||||
.path()
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.as_str(),
|
||||
)?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("item1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
|
||||
// Complete; expect to see the two files.
|
||||
let results = test_shell.complete_end_of_line("ls ~/item").await?;
|
||||
|
||||
assert_eq!(
|
||||
results,
|
||||
[
|
||||
test_shell
|
||||
.temp_dir
|
||||
.child("item1")
|
||||
.path()
|
||||
.display()
|
||||
.to_string(),
|
||||
test_shell
|
||||
.temp_dir
|
||||
.child("item2")
|
||||
.path()
|
||||
.display()
|
||||
.to_string(),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_variable_names() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Set a few vars.
|
||||
test_shell.set_var("TESTVAR1", "")?;
|
||||
test_shell.set_var("TESTVAR2", "")?;
|
||||
|
||||
// Complete.
|
||||
let results = test_shell.complete_end_of_line("echo $TESTVAR").await?;
|
||||
assert_eq!(results, ["$TESTVAR1", "$TESTVAR2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_variable_names_with_braces() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Set a few vars.
|
||||
test_shell.set_var("TESTVAR1", "")?;
|
||||
test_shell.set_var("TESTVAR2", "")?;
|
||||
|
||||
// Complete.
|
||||
let results = test_shell.complete_end_of_line("echo ${TESTVAR").await?;
|
||||
assert_eq!(results, ["${TESTVAR1}", "${TESTVAR2}"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_help_topic() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Complete.
|
||||
let results = test_shell.complete_end_of_line("help expor").await?;
|
||||
assert_eq!(results, ["export"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_command_option() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Complete.
|
||||
let results = test_shell.complete_end_of_line("ls --hel").await?;
|
||||
assert_eq!(results, ["--help"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests completion with some well-known programs that have been good manual test cases
|
||||
/// for us in the past.
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_path_args_to_well_known_programs() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Create file and dir.
|
||||
test_shell.temp_dir.child("item1").touch()?;
|
||||
test_shell.temp_dir.child("item2").create_dir_all()?;
|
||||
|
||||
// Complete.
|
||||
let results = test_shell.complete_end_of_line("tar tvf ./item").await?;
|
||||
|
||||
assert_eq!(results, ["./item2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests some 'find' completion.
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_find_command() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
// Complete.
|
||||
let results = test_shell.complete_end_of_line("find . -na").await?;
|
||||
|
||||
assert_eq!(results, ["-name"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_with::file(/usr/share/bash-completion/bash_completion)]
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn complete_quoted_filenames() -> Result<()> {
|
||||
let mut test_shell = TestShellWithBashCompletion::new().await?;
|
||||
|
||||
test_shell.temp_dir.child("item1 item2").touch()?;
|
||||
test_shell.temp_dir.child("item1'item2").touch()?;
|
||||
|
||||
let mut results = test_shell.complete_end_of_line("ls item1\\ ").await?;
|
||||
assert_eq!(results, ["item1 item2"]);
|
||||
|
||||
results = test_shell.complete_end_of_line("ls item1'").await?;
|
||||
assert_eq!(results, ["item1 item2", "item1'item2"]);
|
||||
|
||||
results = test_shell.complete_end_of_line("ls item1").await?;
|
||||
assert_eq!(results, ["item1 item2", "item1'item2"]);
|
||||
|
||||
results = test_shell.complete_end_of_line("ls 'item1 ").await?;
|
||||
assert_eq!(results, ["item1 item2"]);
|
||||
|
||||
results = test_shell.complete_end_of_line("ls \"item1 ").await?;
|
||||
assert_eq!(results, ["item1 item2"]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that interactive completion sets `COMP_KEY` and `COMP_TYPE` to 9 (TAB).
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn interactive_completion_sets_comp_key_and_comp_type() -> Result<()> {
|
||||
let mut shell = brush_core::Shell::builder()
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
// Register a completion function that captures COMP_KEY and COMP_TYPE.
|
||||
let exec_params = shell.default_exec_params();
|
||||
let source_info = brush_core::SourceInfo::default();
|
||||
shell
|
||||
.run_string(
|
||||
r"
|
||||
_test_comp() {
|
||||
CAPTURED_COMP_KEY=$COMP_KEY
|
||||
CAPTURED_COMP_TYPE=$COMP_TYPE
|
||||
COMPREPLY=(done)
|
||||
}
|
||||
complete -F _test_comp mycmd
|
||||
"
|
||||
.to_string(),
|
||||
&source_info,
|
||||
&exec_params,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Trigger interactive completion.
|
||||
let _completions = shell.complete("mycmd ", 6).await?;
|
||||
|
||||
// Check the captured values.
|
||||
let comp_key = shell
|
||||
.env()
|
||||
.get("CAPTURED_COMP_KEY")
|
||||
.map(|(_, v)| v.value().to_cow_str(&shell).to_string());
|
||||
let comp_type = shell
|
||||
.env()
|
||||
.get("CAPTURED_COMP_TYPE")
|
||||
.map(|(_, v)| v.value().to_cow_str(&shell).to_string());
|
||||
|
||||
assert_eq!(comp_key.as_deref(), Some("9"), "COMP_KEY should be 9 (TAB)");
|
||||
assert_eq!(
|
||||
comp_type.as_deref(),
|
||||
Some("9"),
|
||||
"COMP_TYPE should be 9 (TAB)"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Tests for native completion fallback (without bash-completion installed)
|
||||
|
||||
struct TestShellNative {
|
||||
shell: brush_core::Shell,
|
||||
temp_dir: assert_fs::TempDir,
|
||||
}
|
||||
|
||||
impl TestShellNative {
|
||||
async fn new() -> Result<Self> {
|
||||
let mut shell = brush_core::Shell::builder()
|
||||
.profile(brush_core::ProfileLoadBehavior::Skip)
|
||||
.rc(brush_core::RcLoadBehavior::Skip)
|
||||
.default_builtins(brush_builtins::BuiltinSet::BashMode)
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let temp_dir = assert_fs::TempDir::new()?;
|
||||
shell.set_working_dir(temp_dir.path())?;
|
||||
|
||||
Ok(Self { shell, temp_dir })
|
||||
}
|
||||
|
||||
pub async fn complete_end_of_line_full(
|
||||
&mut self,
|
||||
line: &str,
|
||||
) -> Result<brush_core::completion::Completions> {
|
||||
Ok(self.shell.complete(line, line.len()).await?)
|
||||
}
|
||||
|
||||
pub fn set_var(&mut self, name: &str, value: &str) -> Result<()> {
|
||||
self.shell
|
||||
.env_mut()
|
||||
.set_global(name, brush_core::ShellVariable::new(value))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Tests native variable completion without braces (e.g., $VAR)
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn native_complete_variable_names() -> Result<()> {
|
||||
let mut test_shell = TestShellNative::new().await?;
|
||||
|
||||
// Set test variables.
|
||||
test_shell.set_var("TESTVAR1", "value1")?;
|
||||
test_shell.set_var("TESTVAR2", "value2")?;
|
||||
|
||||
// Complete.
|
||||
let completions = test_shell
|
||||
.complete_end_of_line_full("echo $TESTVAR")
|
||||
.await?;
|
||||
let results: Vec<String> = completions.candidates.into_iter().collect();
|
||||
assert_eq!(results, ["$TESTVAR1", "$TESTVAR2"]);
|
||||
|
||||
// Variable completions should not be treated as filenames (to avoid escaping $)
|
||||
assert!(
|
||||
!completions.options.treat_as_filenames,
|
||||
"variable completions should not be treated as filenames"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests native variable completion with braces (e.g., ${VAR})
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn native_complete_variable_names_with_braces() -> Result<()> {
|
||||
let mut test_shell = TestShellNative::new().await?;
|
||||
|
||||
// Set test variables.
|
||||
test_shell.set_var("TESTVAR1", "value1")?;
|
||||
test_shell.set_var("TESTVAR2", "value2")?;
|
||||
|
||||
// Complete.
|
||||
let completions = test_shell
|
||||
.complete_end_of_line_full("echo ${TESTVAR")
|
||||
.await?;
|
||||
let results: Vec<String> = completions.candidates.into_iter().collect();
|
||||
assert_eq!(results, ["${TESTVAR1}", "${TESTVAR2}"]);
|
||||
|
||||
// Variable completions should not be treated as filenames (to avoid escaping $)
|
||||
assert!(
|
||||
!completions.options.treat_as_filenames,
|
||||
"variable completions should not be treated as filenames"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that file completion works after a variable (e.g., $VAR/path)
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn native_complete_path_after_variable() -> Result<()> {
|
||||
let mut test_shell = TestShellNative::new().await?;
|
||||
|
||||
// Create files in temp dir.
|
||||
test_shell.temp_dir.child("file1.txt").touch()?;
|
||||
test_shell.temp_dir.child("file2.txt").touch()?;
|
||||
|
||||
// Set a variable pointing to the temp dir.
|
||||
let temp_path = test_shell.temp_dir.path().to_str().unwrap().to_owned();
|
||||
test_shell.set_var("MYDIR", &temp_path)?;
|
||||
|
||||
// Complete files after variable expansion.
|
||||
// The completion system expands variables before completing, so results use expanded paths.
|
||||
let completions = test_shell
|
||||
.complete_end_of_line_full("ls $MYDIR/file")
|
||||
.await?;
|
||||
let results: Vec<String> = completions.candidates.into_iter().collect();
|
||||
let expected = [
|
||||
std::format!("{temp_path}/file1.txt"),
|
||||
std::format!("{temp_path}/file2.txt"),
|
||||
];
|
||||
assert_eq!(results, expected);
|
||||
|
||||
// Path completions should be treated as filenames
|
||||
assert!(
|
||||
completions.options.treat_as_filenames,
|
||||
"path completions should be treated as filenames"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user