fd/tests/testenv/mod.rs

352 lines
11 KiB
Rust
Raw Normal View History

use std::env;
use std::fs;
2020-04-03 21:24:11 +02:00
use std::io::{self, Write};
#[cfg(unix)]
use std::os::unix;
#[cfg(windows)]
use std::os::windows;
2020-04-03 21:24:11 +02:00
use std::path::{Path, PathBuf};
use std::process;
use tempfile::TempDir;
/// Environment for the integration tests.
pub struct TestEnv {
/// Temporary working directory.
temp_dir: TempDir,
/// Path to the *fd* executable.
fd_exe: PathBuf,
/// Normalize each line by sorting the whitespace-separated words
normalize_line: bool,
2022-12-18 08:42:54 +01:00
/// Temporary directory for storing test config (global ignore file)
config_dir: Option<TempDir>,
}
/// Create the working directory and the test files.
Add multiple path support (#182) * Adding support for multiple paths. (panic) - Started adding multiple file support - fd panics with multiple files right now * Moved the ctrlc handler to main. - Moved the ctrlc handler to main so we can search multiple files * Tests now allow custom directory setup - TestEnv::new() now takes two arguments, the directories to create and the files to create inside those directories. * rust-fmt changes * rust-fmt changes * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Removed commented code in testenv * Refactored walk::scan to accept the path buffer vector. Using the ParallelWalker allows for multithreaded searching of multiple directories * Moved ctrlc handler back into walker, it is only called once from main now. * Moved the colored output check back to it's original place * Removing shell-escape, not sure how it got added... * Added test for `fd 'a.foo' test1` to show that a.foo is only found in the test1 and not the test2 direcotry * Removing side effect from walk::scan, `dir_vec` is no longer a mutable reference and an iterator is being used instead. * Running rustfmt to format code correctly
2017-12-06 23:52:23 +01:00
fn create_working_directory(
directories: &[&'static str],
files: &[&'static str],
) -> Result<TempDir, io::Error> {
let temp_dir = tempfile::Builder::new().prefix("fd-tests").tempdir()?;
{
let root = temp_dir.path();
2018-07-30 17:19:37 +02:00
// Pretend that this is a Git repository in order for `.gitignore` files to be respected
fs::create_dir_all(root.join(".git"))?;
Add multiple path support (#182) * Adding support for multiple paths. (panic) - Started adding multiple file support - fd panics with multiple files right now * Moved the ctrlc handler to main. - Moved the ctrlc handler to main so we can search multiple files * Tests now allow custom directory setup - TestEnv::new() now takes two arguments, the directories to create and the files to create inside those directories. * rust-fmt changes * rust-fmt changes * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Removed commented code in testenv * Refactored walk::scan to accept the path buffer vector. Using the ParallelWalker allows for multithreaded searching of multiple directories * Moved ctrlc handler back into walker, it is only called once from main now. * Moved the colored output check back to it's original place * Removing shell-escape, not sure how it got added... * Added test for `fd 'a.foo' test1` to show that a.foo is only found in the test1 and not the test2 direcotry * Removing side effect from walk::scan, `dir_vec` is no longer a mutable reference and an iterator is being used instead. * Running rustfmt to format code correctly
2017-12-06 23:52:23 +01:00
for directory in directories {
fs::create_dir_all(root.join(directory))?;
}
Add multiple path support (#182) * Adding support for multiple paths. (panic) - Started adding multiple file support - fd panics with multiple files right now * Moved the ctrlc handler to main. - Moved the ctrlc handler to main so we can search multiple files * Tests now allow custom directory setup - TestEnv::new() now takes two arguments, the directories to create and the files to create inside those directories. * rust-fmt changes * rust-fmt changes * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Removed commented code in testenv * Refactored walk::scan to accept the path buffer vector. Using the ParallelWalker allows for multithreaded searching of multiple directories * Moved ctrlc handler back into walker, it is only called once from main now. * Moved the colored output check back to it's original place * Removing shell-escape, not sure how it got added... * Added test for `fd 'a.foo' test1` to show that a.foo is only found in the test1 and not the test2 direcotry * Removing side effect from walk::scan, `dir_vec` is no longer a mutable reference and an iterator is being used instead. * Running rustfmt to format code correctly
2017-12-06 23:52:23 +01:00
for file in files {
fs::File::create(root.join(file))?;
}
2018-01-01 12:16:43 +01:00
#[cfg(unix)]
unix::fs::symlink(root.join("one/two"), root.join("symlink"))?;
// Note: creating symlinks on Windows requires the `SeCreateSymbolicLinkPrivilege` which
// is by default only granted for administrators.
2018-01-01 12:16:43 +01:00
#[cfg(windows)]
windows::fs::symlink_dir(root.join("one/two"), root.join("symlink"))?;
2018-02-21 21:41:52 +01:00
fs::File::create(root.join(".fdignore"))?.write_all(b"fdignored.foo")?;
2018-01-01 12:16:43 +01:00
fs::File::create(root.join(".gitignore"))?.write_all(b"gitignored.foo")?;
}
Ok(temp_dir)
}
2022-12-18 08:42:54 +01:00
fn create_config_directory_with_global_ignore(ignore_file_content: &str) -> io::Result<TempDir> {
let config_dir = tempfile::Builder::new().prefix("fd-config").tempdir()?;
let fd_dir = config_dir.path().join("fd");
fs::create_dir(&fd_dir)?;
let mut ignore_file = fs::File::create(fd_dir.join("ignore"))?;
ignore_file.write_all(ignore_file_content.as_bytes())?;
Ok(config_dir)
}
/// Find the *fd* executable.
fn find_fd_exe() -> PathBuf {
// Tests exe is in target/debug/deps, the *fd* exe is in target/debug
2017-10-12 08:01:51 +02:00
let root = env::current_exe()
.expect("tests executable")
.parent()
.expect("tests executable directory")
.parent()
.expect("fd executable directory")
.to_path_buf();
let exe_name = if cfg!(windows) { "fd.exe" } else { "fd" };
root.join(exe_name)
}
/// Format an error message for when *fd* did not exit successfully.
fn format_exit_error(args: &[&str], output: &process::Output) -> String {
format!(
"`fd {}` did not exit successfully.\nstdout:\n---\n{}---\nstderr:\n---\n{}---",
args.join(" "),
String::from_utf8_lossy(&output.stdout),
2017-10-12 08:01:51 +02:00
String::from_utf8_lossy(&output.stderr)
)
}
/// Format an error message for when the output of *fd* did not match the expected output.
fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String {
// Generate diff text.
2017-10-12 08:01:51 +02:00
let diff_text = diff::lines(expected, actual)
.into_iter()
.map(|diff| match diff {
diff::Result::Left(l) => format!("-{}", l),
diff::Result::Both(l, _) => format!(" {}", l),
diff::Result::Right(r) => format!("+{}", r),
2018-09-27 23:01:38 +02:00
})
.collect::<Vec<_>>()
2017-10-12 08:01:51 +02:00
.join("\n");
format!(
concat!(
"`fd {}` did not produce the expected output.\n",
2017-10-12 08:01:51 +02:00
"Showing diff between expected and actual:\n{}\n"
),
args.join(" "),
2017-10-12 08:01:51 +02:00
diff_text
)
}
/// Normalize the output for comparison.
fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
// Split into lines and normalize separators.
2018-05-14 18:39:47 +02:00
let mut lines = s
.replace('\0', "NULL\n")
.lines()
.map(|line| {
let line = if trim_start { line.trim_start() } else { line };
let line = line.replace('/', &std::path::MAIN_SEPARATOR.to_string());
if normalize_line {
let mut words: Vec<_> = line.split_whitespace().collect();
2021-07-27 08:38:09 +02:00
words.sort_unstable();
return words.join(" ");
}
line
2018-09-27 23:01:38 +02:00
})
.collect::<Vec<_>>();
lines.sort();
lines.join("\n")
}
/// Trim whitespace from the beginning of each line.
fn trim_lines(s: &str) -> String {
s.lines()
.map(|line| line.trim_start())
.fold(String::new(), |mut str, line| {
str.push_str(line);
str.push('\n');
str
})
}
impl TestEnv {
Add multiple path support (#182) * Adding support for multiple paths. (panic) - Started adding multiple file support - fd panics with multiple files right now * Moved the ctrlc handler to main. - Moved the ctrlc handler to main so we can search multiple files * Tests now allow custom directory setup - TestEnv::new() now takes two arguments, the directories to create and the files to create inside those directories. * rust-fmt changes * rust-fmt changes * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Moving code around, no need to do everything in one big loop - PathDisplay was never actually used for anything, removed it during refactor of main - Removed redundant logic for absolute paths - Moved code placed needlessly inside a loop in the last commit outside of that loop. * Removed commented code in testenv * Refactored walk::scan to accept the path buffer vector. Using the ParallelWalker allows for multithreaded searching of multiple directories * Moved ctrlc handler back into walker, it is only called once from main now. * Moved the colored output check back to it's original place * Removing shell-escape, not sure how it got added... * Added test for `fd 'a.foo' test1` to show that a.foo is only found in the test1 and not the test2 direcotry * Removing side effect from walk::scan, `dir_vec` is no longer a mutable reference and an iterator is being used instead. * Running rustfmt to format code correctly
2017-12-06 23:52:23 +01:00
pub fn new(directories: &[&'static str], files: &[&'static str]) -> TestEnv {
let temp_dir = create_working_directory(directories, files).expect("working directory");
let fd_exe = find_fd_exe();
TestEnv {
temp_dir,
fd_exe,
normalize_line: false,
2022-12-18 08:42:54 +01:00
config_dir: None,
}
}
pub fn normalize_line(self, normalize: bool) -> TestEnv {
TestEnv {
temp_dir: self.temp_dir,
fd_exe: self.fd_exe,
normalize_line: normalize,
2022-12-18 08:42:54 +01:00
config_dir: self.config_dir,
}
}
pub fn global_ignore_file(self, content: &str) -> TestEnv {
let config_dir =
create_config_directory_with_global_ignore(content).expect("config directory");
TestEnv {
config_dir: Some(config_dir),
..self
}
}
/// Create a broken symlink at the given path in the temp_dir.
pub fn create_broken_symlink<P: AsRef<Path>>(
&mut self,
link_path: P,
) -> Result<PathBuf, io::Error> {
let root = self.test_root();
let broken_symlink_link = root.join(link_path);
{
let temp_target_dir = tempfile::Builder::new()
.prefix("fd-tests-broken-symlink")
.tempdir()?;
let broken_symlink_target = temp_target_dir.path().join("broken_symlink_target");
fs::File::create(&broken_symlink_target)?;
#[cfg(unix)]
unix::fs::symlink(&broken_symlink_target, &broken_symlink_link)?;
#[cfg(windows)]
windows::fs::symlink_file(&broken_symlink_target, &broken_symlink_link)?;
}
Ok(broken_symlink_link)
}
/// Get the root directory for the tests.
pub fn test_root(&self) -> PathBuf {
self.temp_dir.path().to_path_buf()
}
/// Get the path of the fd executable.
#[cfg_attr(windows, allow(unused))]
pub fn test_exe(&self) -> &PathBuf {
&self.fd_exe
}
/// Get the root directory of the file system.
pub fn system_root(&self) -> PathBuf {
let mut components = self.temp_dir.path().components();
PathBuf::from(components.next().expect("root directory").as_os_str())
}
/// Assert that calling *fd* in the specified path under the root working directory,
/// and with the specified arguments produces the expected output.
pub fn assert_success_and_get_output<P: AsRef<Path>>(
2017-10-12 08:01:51 +02:00
&self,
path: P,
args: &[&str],
) -> process::Output {
// Run *fd*.
2022-12-18 08:42:54 +01:00
let output = self.run_command(path.as_ref(), args);
// Check for exit status.
if !output.status.success() {
2021-07-27 08:38:09 +02:00
panic!("{}", format_exit_error(args, &output));
}
output
}
pub fn assert_success_and_get_normalized_output<P: AsRef<Path>>(
&self,
path: P,
args: &[&str],
) -> String {
let output = self.assert_success_and_get_output(path, args);
normalize_output(
&String::from_utf8_lossy(&output.stdout),
false,
self.normalize_line,
)
}
/// Assert that calling *fd* with the specified arguments produces the expected output.
pub fn assert_output(&self, args: &[&str], expected: &str) {
self.assert_output_subdirectory(".", args, expected)
}
/// Similar to assert_output, but able to handle non-utf8 output
#[cfg(all(unix, not(target_os = "macos")))]
pub fn assert_output_raw(&self, args: &[&str], expected: &[u8]) {
let output = self.assert_success_and_get_output(".", args);
assert_eq!(expected, &output.stdout[..]);
}
/// Assert that calling *fd* in the specified path under the root working directory,
/// and with the specified arguments produces the expected output.
pub fn assert_output_subdirectory<P: AsRef<Path>>(
&self,
path: P,
args: &[&str],
expected: &str,
) {
// Normalize both expected and actual output.
let expected = normalize_output(expected, true, self.normalize_line);
let actual = self.assert_success_and_get_normalized_output(path, args);
// Compare actual output to expected output.
if expected != actual {
2021-07-27 08:38:09 +02:00
panic!("{}", format_output_error(args, &expected, &actual));
}
}
/// Assert that calling *fd* with the specified arguments produces the expected error,
/// and does not succeed.
pub fn assert_failure_with_error(&self, args: &[&str], expected: &str) {
let status = self.assert_error_subdirectory(".", args, Some(expected));
if status.success() {
panic!("error '{}' did not occur.", expected);
}
}
/// Assert that calling *fd* with the specified arguments does not succeed.
pub fn assert_failure(&self, args: &[&str]) {
let status = self.assert_error_subdirectory(".", args, None);
if status.success() {
panic!("Failure did not occur as expected.");
}
}
/// Assert that calling *fd* with the specified arguments produces the expected error.
pub fn assert_error(&self, args: &[&str], expected: &str) -> process::ExitStatus {
self.assert_error_subdirectory(".", args, Some(expected))
}
2022-12-18 08:42:54 +01:00
fn run_command(&self, path: &Path, args: &[&str]) -> process::Output {
// Setup *fd* command.
let mut cmd = process::Command::new(&self.fd_exe);
cmd.current_dir(self.temp_dir.path().join(path));
if let Some(config_dir) = &self.config_dir {
cmd.env("XDG_CONFIG_HOME", config_dir.path());
} else {
cmd.arg("--no-global-ignore-file");
}
cmd.args(args);
// Run *fd*.
cmd.output().expect("fd output")
}
/// Assert that calling *fd* in the specified path under the root working directory,
/// and with the specified arguments produces an error with the expected message.
2020-05-19 15:37:38 +02:00
fn assert_error_subdirectory<P: AsRef<Path>>(
&self,
path: P,
args: &[&str],
expected: Option<&str>,
2020-05-19 15:37:38 +02:00
) -> process::ExitStatus {
2022-12-18 08:42:54 +01:00
let output = self.run_command(path.as_ref(), args);
if let Some(expected) = expected {
// Normalize both expected and actual output.
let expected_error = trim_lines(expected);
let actual_err = trim_lines(&String::from_utf8_lossy(&output.stderr));
// Compare actual output to expected output.
if !actual_err.trim_start().starts_with(&expected_error) {
2021-07-27 08:38:09 +02:00
panic!(
"{}",
format_output_error(args, &expected_error, &actual_err)
);
}
}
2021-07-27 08:38:09 +02:00
output.status
}
}