diff --git a/tests/hay.rs b/tests/hay.rs index 74d2f6cc..a004eae3 100644 --- a/tests/hay.rs +++ b/tests/hay.rs @@ -6,19 +6,3 @@ can extract a clew from a wisp of straw or a flake of cigar ash; but Doctor Watson has to have it taken out for him and dusted, and exhibited clearly, with a label attached. "; - -pub const CODE: &'static str = "\ -extern crate snap; - -use std::io; - -fn main() { - let stdin = io::stdin(); - let stdout = io::stdout(); - - // Wrap the stdin reader in a Snappy reader. - let mut rdr = snap::Reader::new(stdin.lock()); - let mut wtr = stdout.lock(); - io::copy(&mut rdr, &mut wtr).expect(\"I/O operation failed\"); -} -"; diff --git a/tests/macros.rs b/tests/macros.rs new file mode 100644 index 00000000..edb9f3bb --- /dev/null +++ b/tests/macros.rs @@ -0,0 +1,22 @@ +#[macro_export] +macro_rules! assert_eq_nice { + ($expected:expr, $got:expr) => { + let expected = &*$expected; + let got = &*$got; + if expected != got { + panic!(" +printed outputs differ! + +expected: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +got: +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +{} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +", expected, got); + } + } +} diff --git a/tests/regression.rs b/tests/regression.rs new file mode 100644 index 00000000..0454403d --- /dev/null +++ b/tests/regression.rs @@ -0,0 +1,36 @@ +use hay::SHERLOCK; +use workdir::WorkDir; + +// See: https://github.com/BurntSushi/ripgrep/issues/16 +#[test] +fn r16() { + let (wd, mut cmd) = WorkDir::new_with("r16"); + wd.create_dir(".git"); + wd.create(".gitignore", "ghi/"); + wd.create_dir("ghi"); + wd.create_dir("def/ghi"); + wd.create("ghi/toplevel.txt", "xyz"); + wd.create("def/ghi/subdir.txt", "xyz"); + + cmd.arg("xyz"); + wd.assert_err(&mut cmd); +} + +// See: https://github.com/BurntSushi/ripgrep/issues/25 +#[test] +fn r25() { + let (wd, mut cmd) = WorkDir::new_with("r25"); + wd.create_dir(".git"); + wd.create(".gitignore", "/llvm/"); + wd.create_dir("src/llvm"); + wd.create("src/llvm/foo", "test"); + + cmd.arg("test"); + + let lines: String = wd.stdout(&mut cmd); + assert_eq_nice!("src/llvm/foo:test\n", lines); + + cmd.current_dir(wd.path().join("src")); + let lines: String = wd.stdout(&mut cmd); + assert_eq_nice!("llvm/foo:test\n", lines); +} diff --git a/tests/tests.rs b/tests/tests.rs index 1c40f22e..480b7b1e 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -1,18 +1,15 @@ -/*! -This module contains *integration* tests. Their purpose is to test the CLI -interface. Namely, that passing a flag does what it says on the tin. - -Tests for more fine grained behavior (like the search or the globber) should be -unit tests in their respective modules. -*/ - #![allow(dead_code, unused_imports)] use std::process::Command; use workdir::WorkDir; +#[macro_use] +mod macros; + mod hay; +mod regression; +mod util; mod workdir; macro_rules! sherlock { @@ -47,11 +44,14 @@ macro_rules! clean { } fn path(unix: &str) -> String { + unix.to_string() + /* if cfg!(windows) { unix.replace("/", "\\") } else { unix.to_string() } + */ } fn paths(unix: &[&str]) -> Vec { diff --git a/tests/util.rs b/tests/util.rs new file mode 100644 index 00000000..39329a89 --- /dev/null +++ b/tests/util.rs @@ -0,0 +1,361 @@ +use std::env; +use std::error; +use std::fmt; +use std::fs::{self, File}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; +use std::process::{self, Command}; +use std::str::FromStr; +use std::sync::atomic::{ATOMIC_USIZE_INIT, AtomicUsize, Ordering}; +use std::thread; +use std::time::Duration; + +static TEST_DIR: &'static str = "ripgrep-tests"; +static NEXT_ID: AtomicUsize = ATOMIC_USIZE_INIT; + +/// Dir represents a directory in which tests should be run. +/// +/// Directories are created from a global atomic counter to avoid duplicates. +#[derive(Debug)] +pub struct Dir { + /// The directory in which this test executable is running. + root: PathBuf, + /// The directory in which the test should run. If a test needs to create + /// files, they should go in here. This directory is also used as the CWD + /// for any processes created by the test. + dir: PathBuf, +} + +impl Dir { + /// Create a new test working directory with the given name. The name + /// does not need to be distinct for each invocation, but should correspond + /// to a logical grouping of tests. + pub fn new(name: &str) -> Dir { + let id = NEXT_ID.fetch_add(1, Ordering::SeqCst); + let root = env::current_exe() + .unwrap() + .parent() + .expect("executable's directory") + .to_path_buf(); + let dir = env::temp_dir() + .join(TEST_DIR) + .join(name) + .join(&format!("{}", id)); + nice_err(&dir, repeat(|| fs::create_dir_all(&dir))); + Dir { + root: root, + dir: dir, + } + } + + /// Create a new file with the given name and contents in this directory, + /// or panic on error. + pub fn create>(&self, name: P, contents: &str) { + self.create_bytes(name, contents.as_bytes()); + } + + /// Try to create a new file with the given name and contents in this + /// directory. + pub fn try_create>( + &self, + name: P, + contents: &str, + ) -> io::Result<()> { + let path = self.dir.join(name); + self.try_create_bytes(path, contents.as_bytes()) + } + + /// Create a new file with the given name and size. + pub fn create_size>(&self, name: P, filesize: u64) { + let path = self.dir.join(name); + let file = nice_err(&path, File::create(&path)); + nice_err(&path, file.set_len(filesize)); + } + + /// Create a new file with the given name and contents in this directory, + /// or panic on error. + pub fn create_bytes>(&self, name: P, contents: &[u8]) { + let path = self.dir.join(name); + nice_err(&path, self.try_create_bytes(&path, contents)); + } + + /// Try to create a new file with the given name and contents in this + /// directory. + fn try_create_bytes>( + &self, + path: P, + contents: &[u8], + ) -> io::Result<()> { + let mut file = File::create(&path)?; + file.write_all(contents)?; + file.flush() + } + + /// Remove a file with the given name from this directory. + pub fn remove>(&self, name: P) { + let path = self.dir.join(name); + nice_err(&path, fs::remove_file(&path)); + } + + /// Create a new directory with the given path (and any directories above + /// it) inside this directory. + pub fn create_dir>(&self, path: P) { + let path = self.dir.join(path); + nice_err(&path, repeat(|| fs::create_dir_all(&path))); + } + + /// Creates a new command that is set to use the ripgrep executable in + /// this working directory. + /// + /// This also: + /// + /// * Unsets the `RIPGREP_CONFIG_PATH` environment variable. + /// * Sets the `--path-separator` to `/` so that paths have the same output + /// on all systems. Tests that need to check `--path-separator` itself + /// can simply pass it again to override it. + pub fn command(&self) -> process::Command { + let mut cmd = process::Command::new(&self.bin()); + cmd.env_remove("RIPGREP_CONFIG_PATH"); + cmd.current_dir(&self.dir); + cmd.arg("--path-separator").arg("/"); + cmd + } + + /// Returns the path to the ripgrep executable. + pub fn bin(&self) -> PathBuf { + if cfg!(windows) { + self.root.join("../rg.exe") + } else { + self.root.join("../rg") + } + } + + /// Returns the path to this directory. + pub fn path(&self) -> &Path { + &self.dir + } + + /// Creates a directory symlink to the src with the given target name + /// in this directory. + #[cfg(not(windows))] + pub fn link_dir, T: AsRef>(&self, src: S, target: T) { + use std::os::unix::fs::symlink; + let src = self.dir.join(src); + let target = self.dir.join(target); + let _ = fs::remove_file(&target); + nice_err(&target, symlink(&src, &target)); + } + + /// Creates a directory symlink to the src with the given target name + /// in this directory. + #[cfg(windows)] + pub fn link_dir, T: AsRef>(&self, src: S, target: T) { + use std::os::windows::fs::symlink_dir; + let src = self.dir.join(src); + let target = self.dir.join(target); + let _ = fs::remove_dir(&target); + nice_err(&target, symlink_dir(&src, &target)); + } + + /// Creates a file symlink to the src with the given target name + /// in this directory. + #[cfg(not(windows))] + pub fn link_file, T: AsRef>( + &self, + src: S, + target: T, + ) { + self.link_dir(src, target); + } + + /// Creates a file symlink to the src with the given target name + /// in this directory. + #[cfg(windows)] + pub fn link_file, T: AsRef>( + &self, + src: S, + target: T, + ) { + use std::os::windows::fs::symlink_file; + let src = self.dir.join(src); + let target = self.dir.join(target); + let _ = fs::remove_file(&target); + nice_err(&target, symlink_file(&src, &target)); + } + + /// Runs and captures the stdout of the given command. + /// + /// If the return type could not be created from a string, then this + /// panics. + pub fn stdout>( + &self, + cmd: &mut process::Command, + ) -> T { + let o = self.output(cmd); + let stdout = String::from_utf8_lossy(&o.stdout); + match stdout.parse() { + Ok(t) => t, + Err(err) => { + panic!( + "could not convert from string: {:?}\n\n{}", + err, + stdout + ); + } + } + } + + /// Gets the output of a command. If the command failed, then this panics. + pub fn output(&self, cmd: &mut process::Command) -> process::Output { + let output = cmd.output().unwrap(); + self.expect_success(cmd, output) + } + + /// Pipe `input` to a command, and collect the output. + pub fn pipe( + &self, + cmd: &mut process::Command, + input: &str + ) -> process::Output { + cmd.stdin(process::Stdio::piped()); + cmd.stdout(process::Stdio::piped()); + cmd.stderr(process::Stdio::piped()); + + let mut child = cmd.spawn().unwrap(); + + // Pipe input to child process using a separate thread to avoid + // risk of deadlock between parent and child process. + let mut stdin = child.stdin.take().expect("expected standard input"); + let input = input.to_owned(); + let worker = thread::spawn(move || { + write!(stdin, "{}", input) + }); + + let output = self.expect_success( + cmd, + child.wait_with_output().unwrap(), + ); + worker.join().unwrap().unwrap(); + output + } + + /// If `o` is not the output of a successful process run + fn expect_success( + &self, + cmd: &process::Command, + o: process::Output + ) -> process::Output { + if !o.status.success() { + let suggest = + if o.stderr.is_empty() { + "\n\nDid your search end up with no results?".to_string() + } else { + "".to_string() + }; + + panic!("\n\n==========\n\ + command failed but expected success!\ + {}\ + \n\ncommand: {:?}\ + \ncwd: {}\ + \n\nstatus: {}\ + \n\nstdout: {}\ + \n\nstderr: {}\ + \n\n==========\n", + suggest, cmd, self.dir.display(), o.status, + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr)); + } + o + } + + /// Runs the given command and asserts that it resulted in an error exit + /// code. + pub fn assert_err(&self, cmd: &mut process::Command) { + let o = cmd.output().unwrap(); + if o.status.success() { + panic!( + "\n\n===== {:?} =====\n\ + command succeeded but expected failure!\ + \n\ncwd: {}\ + \n\nstatus: {}\ + \n\nstdout: {}\n\nstderr: {}\ + \n\n=====\n", + cmd, + self.dir.display(), + o.status, + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr) + ); + } + } + + /// Runs the given command and asserts that its exit code matches expected + /// exit code. + pub fn assert_exit_code( + &self, + expected_code: i32, + cmd: &mut process::Command, + ) { + let code = cmd.status().unwrap().code().unwrap(); + + assert_eq!( + expected_code, code, + "\n\n===== {:?} =====\n\ + expected exit code did not match\ + \n\nexpected: {}\ + \n\nfound: {}\ + \n\n=====\n", + cmd, expected_code, code + ); + } + + /// Runs the given command and asserts that something was printed to + /// stderr. + pub fn assert_non_empty_stderr(&self, cmd: &mut process::Command) { + let o = cmd.output().unwrap(); + if o.status.success() || o.stderr.is_empty() { + panic!("\n\n===== {:?} =====\n\ + command succeeded but expected failure!\ + \n\ncwd: {}\ + \n\nstatus: {}\ + \n\nstdout: {}\n\nstderr: {}\ + \n\n=====\n", + cmd, self.dir.display(), o.status, + String::from_utf8_lossy(&o.stdout), + String::from_utf8_lossy(&o.stderr)); + } + } +} + +/// A simple wrapper around a process::Command with some conveniences. +#[derive(Debug)] +pub struct TestCommand { + /// The dir used to launched this command. + dir: Dir, + /// The actual command we use to control the process. + cmd: Command, +} + +fn nice_err( + path: &Path, + res: Result, +) -> T { + match res { + Ok(t) => t, + Err(err) => panic!("{}: {:?}", path.display(), err), + } +} + +fn repeat io::Result<()>>(mut f: F) -> io::Result<()> { + let mut last_err = None; + for _ in 0..10 { + if let Err(err) = f() { + last_err = Some(err); + thread::sleep(Duration::from_millis(500)); + } else { + return Ok(()); + } + } + Err(last_err.unwrap()) +} diff --git a/tests/workdir.rs b/tests/workdir.rs index 7bf0172d..309eb08f 100644 --- a/tests/workdir.rs +++ b/tests/workdir.rs @@ -48,6 +48,15 @@ impl WorkDir { } } + /// Like `new`, but also returns a command that whose program is configured + /// to ripgrep's executable and has its current working directory set to + /// this work dir. + pub fn new_with(name: &str) -> (WorkDir, process::Command) { + let wd = WorkDir::new(name); + let command = wd.command(); + (wd, command) + } + /// Create a new file with the given name and contents in this directory, /// or panic on error. pub fn create>(&self, name: P, contents: &str) { @@ -106,10 +115,18 @@ impl WorkDir { /// Creates a new command that is set to use the ripgrep executable in /// this working directory. + /// + /// This also: + /// + /// * Unsets the `RIPGREP_CONFIG_PATH` environment variable. + /// * Sets the `--path-separator` to `/` so that paths have the same output + /// on all systems. Tests that need to check `--path-separator` itself + /// can simply pass it again to override it. pub fn command(&self) -> process::Command { let mut cmd = process::Command::new(&self.bin()); cmd.env_remove("RIPGREP_CONFIG_PATH"); cmd.current_dir(&self.dir); + cmd.arg("--path-separator").arg("/"); cmd }