375 lines
15 KiB
Rust
375 lines
15 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
use std::iter::FromIterator;
|
|
use std::sync::Arc;
|
|
|
|
use galaxy_completer::completer::PathCompletionContext;
|
|
use galaxy_completer::completer::{CompletionContext, EngineDirEntry};
|
|
use galaxy_completer::signatures::CommandRegistry;
|
|
use galaxyui::App;
|
|
use itertools::Itertools;
|
|
use typed_path::TypedPathBuf;
|
|
#[cfg(windows)]
|
|
use typed_path::{UnixComponent, WindowsComponent, WindowsPrefix};
|
|
|
|
use crate::completer::SessionContext;
|
|
use crate::terminal::model::session::Session;
|
|
use crate::terminal::model::session::{
|
|
command_executor::testing::TestCommandExecutor, SessionInfo,
|
|
};
|
|
use crate::test_util::{Stub, VirtualFS};
|
|
|
|
fn test_session_context(session: Session, cwd: TypedPathBuf, app: &App) -> SessionContext {
|
|
app.read(|ctx| SessionContext::new(session, CommandRegistry::default().into(), cwd, ctx))
|
|
}
|
|
|
|
fn working_directory() -> TypedPathBuf {
|
|
#[cfg(unix)]
|
|
let cwd = TypedPathBuf::from("/test/home/");
|
|
#[cfg(windows)]
|
|
let cwd = TypedPathBuf::from_windows(shellexpand::tilde("~").into_owned());
|
|
|
|
cwd
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_top_level_commands_includes_function_names() {
|
|
App::test((), |app| async move {
|
|
let function_names = vec![
|
|
"my_func".into(),
|
|
"foo".into(),
|
|
"bar".into(),
|
|
"foobar".into(),
|
|
];
|
|
let session = Session::new(
|
|
SessionInfo::new_for_test()
|
|
.with_function_names(function_names.clone().into_iter().collect()),
|
|
Arc::new(TestCommandExecutor::default()),
|
|
);
|
|
let ctx = test_session_context(session, working_directory(), &app);
|
|
|
|
let top_level_commands = ctx.top_level_commands().collect_vec();
|
|
for function_name in function_names.iter() {
|
|
assert!(top_level_commands.contains(&function_name.as_str()));
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_top_level_commands_includes_aliases() {
|
|
App::test((), |app| async move {
|
|
let aliases = HashMap::from_iter([
|
|
("first".into(), "test one".into()),
|
|
("second".into(), "first".into()),
|
|
("third".into(), "cd".into()),
|
|
("ls".into(), "ls -l".into()),
|
|
]);
|
|
let session = Session::new(
|
|
SessionInfo::new_for_test().with_aliases(aliases.clone()),
|
|
Arc::new(TestCommandExecutor::default()),
|
|
);
|
|
let ctx = test_session_context(session, working_directory(), &app);
|
|
|
|
let top_level_commands = ctx.top_level_commands().collect_vec();
|
|
for alias in aliases.keys() {
|
|
assert!(top_level_commands.contains(&alias.as_str()));
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_top_level_commands_includes_abbreviations() {
|
|
App::test((), |app| async move {
|
|
let abbreviations = HashMap::from_iter([
|
|
("gl".into(), "git log".into()),
|
|
("gs".into(), "git status".into()),
|
|
]);
|
|
let session = Session::new(
|
|
SessionInfo::new_for_test().with_abbreviations(abbreviations.clone()),
|
|
Arc::new(TestCommandExecutor::default()),
|
|
);
|
|
let ctx = test_session_context(session, working_directory(), &app);
|
|
|
|
let top_level_commands = ctx.top_level_commands().collect_vec();
|
|
for abbreviation in abbreviations.keys() {
|
|
assert!(top_level_commands.contains(&abbreviation.as_str()));
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_top_level_commands_includes_keywords() {
|
|
App::test((), |app| async move {
|
|
let keywords = vec!["while".into(), "foreach".into(), "repeat".into()];
|
|
let session = Session::new(
|
|
SessionInfo::new_for_test().with_keywords(keywords.clone()),
|
|
Arc::new(TestCommandExecutor::default()),
|
|
);
|
|
let ctx = test_session_context(session, working_directory(), &app);
|
|
|
|
let top_level_commands = ctx.top_level_commands().collect_vec();
|
|
for keyword in keywords.iter() {
|
|
assert!(top_level_commands.contains(&keyword.as_str()));
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_top_level_commands_includes_external_commands() {
|
|
App::test((), |app| async move {
|
|
let session = Session::new(
|
|
SessionInfo::new_for_test(),
|
|
Arc::new(TestCommandExecutor::default()),
|
|
);
|
|
galaxyui::r#async::block_on(session.load_external_commands());
|
|
|
|
let ctx = test_session_context(session, working_directory(), &app);
|
|
|
|
// We expect git to be installed and on the PATH on all machines on
|
|
// which we're running our unit tests.
|
|
assert!(ctx.top_level_commands().contains(&"git"));
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_top_level_commands_includes_builtins() {
|
|
App::test((), |app| async move {
|
|
let builtins = vec!["export".into(), "print".into(), "break".into()];
|
|
let session = Session::new(
|
|
SessionInfo::new_for_test().with_builtins(builtins.clone().into_iter().collect()),
|
|
Arc::new(TestCommandExecutor::default()),
|
|
);
|
|
let ctx = test_session_context(session, working_directory(), &app);
|
|
|
|
let top_level_commands = ctx.top_level_commands().collect_vec();
|
|
for builtin in builtins.iter() {
|
|
assert!(top_level_commands.contains(&builtin.as_str()));
|
|
}
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_lists_directory_entries_locally() {
|
|
App::test((), |app| async move {
|
|
VirtualFS::test(
|
|
"test_session_context_lists_directory_entries_locally",
|
|
|dirs, mut sandbox| {
|
|
sandbox.mkdir("src/app");
|
|
sandbox.mkdir("target/debug");
|
|
sandbox.mkdir(".hidden/foo");
|
|
|
|
sandbox.touch(vec![
|
|
Stub::EmptyFile("Cargo.toml"),
|
|
Stub::EmptyFile("src/app/mod.rs"),
|
|
Stub::EmptyFile("target/debug/galaxyui"),
|
|
]);
|
|
|
|
let tests_dir = TypedPathBuf::from(dirs.tests().to_string_lossy().as_bytes());
|
|
|
|
let ctx = test_session_context(Session::test(), tests_dir.clone(), &app);
|
|
let ctx = ctx
|
|
.path_completion_context()
|
|
.expect("Path completion context should exist with active session");
|
|
|
|
assert_eq!(
|
|
HashSet::<EngineDirEntry>::from_iter(Arc::unwrap_or_clone(
|
|
galaxyui::r#async::block_on(ctx.list_directory_entries(tests_dir))
|
|
)),
|
|
HashSet::from_iter([
|
|
EngineDirEntry::test_dir(".hidden"),
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_dir("target"),
|
|
EngineDirEntry::test_dir("src"),
|
|
])
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Given a Windows-encoded path, such as `C:\User\my_username`,
|
|
/// convert it to a UNIX shell path that will work with the `bash`
|
|
/// executable, such as `/mnt/c/Users/my_username`.
|
|
///
|
|
/// This is NOT the same as MSYS2 encoding, which does not
|
|
/// use the `/mnt` prefix.
|
|
#[cfg(windows)]
|
|
fn windows_to_unix_shell_encoding(
|
|
windows_path: &typed_path::Path<typed_path::WindowsEncoding>,
|
|
) -> TypedPathBuf {
|
|
let mut unix_path = TypedPathBuf::unix();
|
|
for component in windows_path.components() {
|
|
match component {
|
|
WindowsComponent::Prefix(p) => {
|
|
match p.kind() {
|
|
WindowsPrefix::Disk(disk_letter) | WindowsPrefix::VerbatimDisk(disk_letter) => {
|
|
let disk_byte = &[disk_letter];
|
|
let drive_name = String::from_utf8_lossy(disk_byte);
|
|
unix_path.push(UnixComponent::RootDir);
|
|
unix_path.push("mnt");
|
|
unix_path.push(drive_name.to_string().to_ascii_lowercase());
|
|
}
|
|
_ => {} // We don't care about other prefix types (see https://doc.rust-lang.org/nightly/std/path/enum.Prefix.html).
|
|
}
|
|
}
|
|
// Avoid adding the root directory twice if there's already a drive
|
|
WindowsComponent::RootDir => {}
|
|
_ => {
|
|
unix_path.push(component);
|
|
}
|
|
}
|
|
}
|
|
unix_path
|
|
}
|
|
|
|
#[cfg_attr(windows, ignore = "TODO(CORE-3626)")]
|
|
#[test]
|
|
pub fn test_session_context_lists_directory_entries_remotely() {
|
|
App::test((), |app| async move {
|
|
VirtualFS::test(
|
|
"test_session_context_lists_directory_entries_remotely",
|
|
|dirs, mut sandbox| {
|
|
sandbox.mkdir("src/app");
|
|
sandbox.mkdir("target/debug");
|
|
|
|
sandbox.touch(vec![
|
|
Stub::EmptyFile("control_path.socket"),
|
|
Stub::EmptyFile("Cargo.toml"),
|
|
Stub::EmptyFile("src/app/mod.rs"),
|
|
Stub::EmptyFile("target/debug/galaxyui"),
|
|
]);
|
|
|
|
let cwd = TypedPathBuf::from(dirs.tests().to_string_lossy().as_bytes());
|
|
|
|
// We assume all remotes are UNIX-based.
|
|
// The test directory we're using here is a local temp directory, which means
|
|
// it uses native path encoding.
|
|
// On Windows, we must convert the test directory to UNIX encoding
|
|
// before being able to run bash commands within it.
|
|
#[cfg(windows)]
|
|
let cwd = match cwd {
|
|
TypedPathBuf::Unix(_) => cwd,
|
|
TypedPathBuf::Windows(windows_path) => {
|
|
windows_to_unix_shell_encoding(windows_path.as_path())
|
|
}
|
|
};
|
|
|
|
let ctx = test_session_context(Session::test_remote(), cwd.clone(), &app);
|
|
|
|
let mut entries = HashSet::<EngineDirEntry>::from_iter(Arc::unwrap_or_clone(
|
|
galaxyui::r#async::block_on(ctx.list_directory_entries(cwd)),
|
|
));
|
|
// TODO(CORE-2000): The ls script we use to list entries in remote
|
|
// sessions adds a spurious "." directory when run in the VirtualFS.
|
|
// As a temporary workaround, we remove this file in the test.
|
|
entries.remove(&EngineDirEntry::test_dir("."));
|
|
|
|
assert_eq!(
|
|
entries,
|
|
HashSet::from_iter([
|
|
EngineDirEntry::test_file("Cargo.toml"),
|
|
EngineDirEntry::test_file("control_path.socket"),
|
|
EngineDirEntry::test_dir("src"),
|
|
EngineDirEntry::test_dir("target"),
|
|
])
|
|
);
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
fn perform_special_characters_in_path_test(session: Session, file_names: Vec<&str>) {
|
|
let file_names = file_names
|
|
.iter()
|
|
.map(|&filename| String::from(filename))
|
|
.collect_vec();
|
|
App::test((), |app| async move {
|
|
VirtualFS::test(
|
|
"test_session_context_lists_directory_entries_with_special_characters",
|
|
|dirs, mut sandbox| {
|
|
sandbox.mkdir("te st/");
|
|
sandbox.mkdir("te st/foo");
|
|
|
|
let files_to_create = file_names
|
|
.iter()
|
|
.map(|file_name| String::from("te st/") + file_name.as_str())
|
|
.collect_vec();
|
|
let file_stubs = files_to_create
|
|
.iter()
|
|
.map(|file_path| Stub::EmptyFile(file_path.as_str()))
|
|
.collect_vec();
|
|
sandbox.touch(file_stubs);
|
|
|
|
let test_dir_base = TypedPathBuf::from(dirs.tests().to_string_lossy().as_bytes());
|
|
let test_dir = test_dir_base.join("te st/");
|
|
|
|
#[cfg(windows)]
|
|
let test_dir = if session.is_local() {
|
|
test_dir
|
|
} else {
|
|
// We assume all remotes are UNIX-based.
|
|
// The test directory we're using here is a local temp directory, which means
|
|
// it uses native path encoding.
|
|
// On Windows, we must convert the test directory to UNIX encoding
|
|
// before being able to run bash commands within it.
|
|
match test_dir {
|
|
TypedPathBuf::Unix(_) => test_dir,
|
|
TypedPathBuf::Windows(windows_path) => {
|
|
windows_to_unix_shell_encoding(windows_path.as_path())
|
|
}
|
|
}
|
|
};
|
|
|
|
let ctx = test_session_context(session, test_dir.clone(), &app);
|
|
|
|
let mut entries = HashSet::<EngineDirEntry>::from_iter(Arc::unwrap_or_clone(
|
|
galaxyui::r#async::block_on(ctx.list_directory_entries(test_dir)),
|
|
));
|
|
// TODO(CORE-2000): The ls script we use to list entries in remote
|
|
// sessions adds a spurious "." directory when run in the VirtualFS.
|
|
// As a temporary workaround, we remove this file in the test.
|
|
entries.remove(&EngineDirEntry::test_dir("."));
|
|
|
|
let mut expected_dir_entries = file_names
|
|
.into_iter()
|
|
.map(|file_name| EngineDirEntry::test_file(&file_name))
|
|
.collect_vec();
|
|
expected_dir_entries.push(EngineDirEntry::test_dir("foo"));
|
|
|
|
assert_eq!(entries, HashSet::from_iter(expected_dir_entries));
|
|
},
|
|
);
|
|
});
|
|
}
|
|
|
|
#[test]
|
|
pub fn test_session_context_lists_directory_entries_locally_with_special_characters_in_path() {
|
|
#[cfg(unix)]
|
|
let file_names = vec!["a.txt", "b file.txt", "c's.txt", "\"d\".txt", "e\nfile.txt"];
|
|
|
|
// Windows filenames are more restrictive than UNIX. Notably,
|
|
// Windows doesn't allow characters in the 1-31 range, which includes carriage returns (13, '\r')
|
|
// and newlines (10, '\n') and reserves certain characters.
|
|
// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions.
|
|
#[cfg(windows)]
|
|
let file_names = vec![r"a.txt", r"b file.txt", r"c's.txt", r"#d&.txt"];
|
|
|
|
perform_special_characters_in_path_test(Session::test(), file_names);
|
|
}
|
|
|
|
/// Regression test for CORE-1927.
|
|
#[cfg_attr(windows, ignore = "TODO(CORE-3626)")]
|
|
#[test]
|
|
pub fn test_session_context_lists_directory_entries_remotely_with_special_characters_in_path() {
|
|
#[cfg(unix)]
|
|
let file_names = vec!["a.txt", "b file.txt", "c's.txt", "\"d\".txt", "e\nfile.txt"];
|
|
|
|
// Windows filenames are more restrictive than UNIX. Notably,
|
|
// Windows doesn't allow characters in the 1-31 range, which includes carriage returns (13, '\r')
|
|
// and newlines (10, '\n') and reserves certain characters.
|
|
// See https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions.
|
|
#[cfg(windows)]
|
|
let file_names = vec![r"a.txt", r"b file.txt", r"c's.txt", r"#d&.txt"];
|
|
|
|
perform_special_characters_in_path_test(Session::test_remote(), file_names);
|
|
}
|