From f83cd63b11f3fa6e85cd5bb0f803a69fa05efa84 Mon Sep 17 00:00:00 2001 From: Andrew Gallant Date: Fri, 9 Sep 2016 22:58:30 -0400 Subject: [PATCH] Add integration tests. --- Cargo.toml | 4 + src/args.rs | 19 +- src/search.rs | 53 ++-- src/search_buffer.rs | 19 +- tests/hay.rs | 24 ++ tests/tests.rs | 563 +++++++++++++++++++++++++++++++++++++++++++ tests/workdir.rs | 189 +++++++++++++++ 7 files changed, 819 insertions(+), 52 deletions(-) create mode 100644 tests/hay.rs create mode 100644 tests/tests.rs create mode 100644 tests/workdir.rs diff --git a/Cargo.toml b/Cargo.toml index 3bf5477..29586bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,10 @@ bench = false path = "src/main.rs" name = "rg" +[[test]] +name = "integration" +path = "tests/tests.rs" + [dependencies] crossbeam = "0.2" docopt = "0.6" diff --git a/src/args.rs b/src/args.rs index d6a9657..ea7299f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -109,10 +109,6 @@ Less common options: -L, --follow Follow symlinks. - --line-terminator ARG - The byte to use for a line terminator. Escape sequences may be used. - [default: \\n] - --mmap Search using memory maps when possible. This is enabled by default when ripgrep thinks it will be faster. (Note that mmap searching @@ -174,7 +170,6 @@ pub struct RawArgs { flag_ignore_case: bool, flag_invert_match: bool, flag_line_number: bool, - flag_line_terminator: String, flag_literal: bool, flag_mmap: bool, flag_no_heading: bool, @@ -248,7 +243,9 @@ impl RawArgs { }; let paths = if self.arg_path.is_empty() { - if sys::stdin_is_atty() { + if sys::stdin_is_atty() + || self.flag_files + || self.flag_type_list { vec![Path::new("./").to_path_buf()] } else { vec![Path::new("-").to_path_buf()] @@ -277,15 +274,6 @@ impl RawArgs { if mmap { debug!("will try to use memory maps"); } - let eol = { - let eol = unescape(&self.flag_line_terminator); - if eol.is_empty() { - errored!("Empty line terminator is not allowed."); - } else if eol.len() > 1 { - errored!("Line terminators are limited to exactly 1 byte."); - } - eol[0] - }; let glob_overrides = if self.flag_glob.is_empty() { None @@ -309,6 +297,7 @@ impl RawArgs { } else { self.flag_color == "always" }; + let eol = b'\n'; let mut with_filename = self.flag_with_filename; if !with_filename { with_filename = paths.len() > 1 || paths[0].is_dir(); diff --git a/src/search.rs b/src/search.rs index 523d1e4..027bd0d 100644 --- a/src/search.rs +++ b/src/search.rs @@ -695,8 +695,7 @@ mod tests { use super::{InputBuffer, Searcher, start_of_previous_lines}; - lazy_static! { - static ref SHERLOCK: &'static str = "\ + const SHERLOCK: &'static str = "\ For the Doctor Watsons of this world, as opposed to the Sherlock Holmeses, success in the province of detective work must always be, to a very large extent, the result of luck. Sherlock Holmes @@ -704,7 +703,8 @@ 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.\ "; - static ref CODE: &'static str = "\ + + const CODE: &'static str = "\ extern crate snap; use std::io; @@ -719,7 +719,6 @@ fn main() { io::copy(&mut rdr, &mut wtr).expect(\"I/O operation failed\"); } "; - } fn hay(s: &str) -> io::Cursor> { io::Cursor::new(s.to_string().into_bytes()) @@ -874,7 +873,7 @@ fn main() { #[test] fn basic_search1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s|s); + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s|s); assert_eq!(2, count); assert_eq!(out, "\ /baz.rs:For the Doctor Watsons of this world, as opposed to the Sherlock @@ -901,7 +900,7 @@ fn main() { #[test] fn line_numbers() { let (count, out) = search_smallcap( - "Sherlock", &*SHERLOCK, |s| s.line_number(true)); + "Sherlock", SHERLOCK, |s| s.line_number(true)); assert_eq!(2, count); assert_eq!(out, "\ /baz.rs:1:For the Doctor Watsons of this world, as opposed to the Sherlock @@ -912,7 +911,7 @@ fn main() { #[test] fn count() { let (count, out) = search_smallcap( - "Sherlock", &*SHERLOCK, |s| s.count(true)); + "Sherlock", SHERLOCK, |s| s.count(true)); assert_eq!(2, count); assert_eq!(out, "/baz.rs:2\n"); } @@ -920,7 +919,7 @@ fn main() { #[test] fn invert_match() { let (count, out) = search_smallcap( - "Sherlock", &*SHERLOCK, |s| s.invert_match(true)); + "Sherlock", SHERLOCK, |s| s.invert_match(true)); assert_eq!(4, count); assert_eq!(out, "\ /baz.rs:Holmeses, success in the province of detective work must always @@ -932,7 +931,7 @@ fn main() { #[test] fn invert_match_line_numbers() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.invert_match(true).line_number(true) }); assert_eq!(4, count); @@ -946,7 +945,7 @@ fn main() { #[test] fn invert_match_count() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.invert_match(true).count(true) }); assert_eq!(4, count); @@ -955,7 +954,7 @@ fn main() { #[test] fn before_context_one1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).before_context(1) }); assert_eq!(2, count); @@ -968,7 +967,7 @@ fn main() { #[test] fn before_context_invert_one1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).before_context(1).invert_match(true) }); assert_eq!(4, count); @@ -984,7 +983,7 @@ fn main() { #[test] fn before_context_invert_one2() { - let (count, out) = search_smallcap(" a ", &*SHERLOCK, |s| { + let (count, out) = search_smallcap(" a ", SHERLOCK, |s| { s.line_number(true).before_context(1).invert_match(true) }); assert_eq!(3, count); @@ -999,7 +998,7 @@ fn main() { #[test] fn before_context_two1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).before_context(2) }); assert_eq!(2, count); @@ -1012,7 +1011,7 @@ fn main() { #[test] fn before_context_two2() { - let (count, out) = search_smallcap("dusted", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("dusted", SHERLOCK, |s| { s.line_number(true).before_context(2) }); assert_eq!(1, count); @@ -1026,7 +1025,7 @@ fn main() { #[test] fn before_context_two3() { let (count, out) = search_smallcap( - "success|attached", &*SHERLOCK, |s| { + "success|attached", SHERLOCK, |s| { s.line_number(true).before_context(2) }); assert_eq!(2, count); @@ -1042,7 +1041,7 @@ fn main() { #[test] fn before_context_two4() { - let (count, out) = search("stdin", &*CODE, |s| { + let (count, out) = search("stdin", CODE, |s| { s.line_number(true).before_context(2) }); assert_eq!(3, count); @@ -1059,7 +1058,7 @@ fn main() { #[test] fn before_context_two5() { - let (count, out) = search("stdout", &*CODE, |s| { + let (count, out) = search("stdout", CODE, |s| { s.line_number(true).before_context(2) }); assert_eq!(2, count); @@ -1076,7 +1075,7 @@ fn main() { #[test] fn before_context_three1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).before_context(3) }); assert_eq!(2, count); @@ -1089,7 +1088,7 @@ fn main() { #[test] fn after_context_one1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).after_context(1) }); assert_eq!(2, count); @@ -1103,7 +1102,7 @@ fn main() { #[test] fn after_context_invert_one1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).after_context(1).invert_match(true) }); assert_eq!(4, count); @@ -1118,7 +1117,7 @@ fn main() { #[test] fn after_context_invert_one2() { - let (count, out) = search_smallcap(" a ", &*SHERLOCK, |s| { + let (count, out) = search_smallcap(" a ", SHERLOCK, |s| { s.line_number(true).after_context(1).invert_match(true) }); assert_eq!(3, count); @@ -1134,7 +1133,7 @@ fn main() { #[test] fn after_context_two1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).after_context(2) }); assert_eq!(2, count); @@ -1149,7 +1148,7 @@ fn main() { #[test] fn after_context_two2() { - let (count, out) = search_smallcap("dusted", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("dusted", SHERLOCK, |s| { s.line_number(true).after_context(2) }); assert_eq!(1, count); @@ -1162,7 +1161,7 @@ fn main() { #[test] fn after_context_two3() { let (count, out) = search_smallcap( - "success|attached", &*SHERLOCK, |s| { + "success|attached", SHERLOCK, |s| { s.line_number(true).after_context(2) }); assert_eq!(2, count); @@ -1177,7 +1176,7 @@ fn main() { #[test] fn after_context_three1() { - let (count, out) = search_smallcap("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search_smallcap("Sherlock", SHERLOCK, |s| { s.line_number(true).after_context(3) }); assert_eq!(2, count); @@ -1194,7 +1193,7 @@ fn main() { #[test] fn before_after_context_two1() { let (count, out) = search( - r"fn main|let mut rdr", &*CODE, |s| { + r"fn main|let mut rdr", CODE, |s| { s.line_number(true).after_context(2).before_context(2) }); assert_eq!(2, count); diff --git a/src/search_buffer.rs b/src/search_buffer.rs index 23578de..fc8cd3a 100644 --- a/src/search_buffer.rs +++ b/src/search_buffer.rs @@ -151,8 +151,7 @@ mod tests { use super::BufferSearcher; - lazy_static! { - static ref SHERLOCK: &'static str = "\ + const SHERLOCK: &'static str = "\ For the Doctor Watsons of this world, as opposed to the Sherlock Holmeses, success in the province of detective work must always be, to a very large extent, the result of luck. Sherlock Holmes @@ -160,7 +159,8 @@ 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.\ "; - static ref CODE: &'static str = "\ + + const CODE: &'static str = "\ extern crate snap; use std::io; @@ -175,7 +175,6 @@ fn main() { io::copy(&mut rdr, &mut wtr).expect(\"I/O operation failed\"); } "; - } fn matcher(pat: &str) -> Grep { GrepBuilder::new(pat).build().unwrap() @@ -205,7 +204,7 @@ fn main() { #[test] fn basic_search() { - let (count, out) = search("Sherlock", &*SHERLOCK, |s|s); + let (count, out) = search("Sherlock", SHERLOCK, |s|s); assert_eq!(2, count); assert_eq!(out, "\ /baz.rs:For the Doctor Watsons of this world, as opposed to the Sherlock @@ -233,7 +232,7 @@ fn main() { #[test] fn line_numbers() { let (count, out) = search( - "Sherlock", &*SHERLOCK, |s| s.line_number(true)); + "Sherlock", SHERLOCK, |s| s.line_number(true)); assert_eq!(2, count); assert_eq!(out, "\ /baz.rs:1:For the Doctor Watsons of this world, as opposed to the Sherlock @@ -244,7 +243,7 @@ fn main() { #[test] fn count() { let (count, out) = search( - "Sherlock", &*SHERLOCK, |s| s.count(true)); + "Sherlock", SHERLOCK, |s| s.count(true)); assert_eq!(2, count); assert_eq!(out, "/baz.rs:2\n"); } @@ -252,7 +251,7 @@ fn main() { #[test] fn invert_match() { let (count, out) = search( - "Sherlock", &*SHERLOCK, |s| s.invert_match(true)); + "Sherlock", SHERLOCK, |s| s.invert_match(true)); assert_eq!(4, count); assert_eq!(out, "\ /baz.rs:Holmeses, success in the province of detective work must always @@ -264,7 +263,7 @@ fn main() { #[test] fn invert_match_line_numbers() { - let (count, out) = search("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search("Sherlock", SHERLOCK, |s| { s.invert_match(true).line_number(true) }); assert_eq!(4, count); @@ -278,7 +277,7 @@ fn main() { #[test] fn invert_match_count() { - let (count, out) = search("Sherlock", &*SHERLOCK, |s| { + let (count, out) = search("Sherlock", SHERLOCK, |s| { s.invert_match(true).count(true) }); assert_eq!(4, count); diff --git a/tests/hay.rs b/tests/hay.rs new file mode 100644 index 0000000..74d2f6c --- /dev/null +++ b/tests/hay.rs @@ -0,0 +1,24 @@ +pub const SHERLOCK: &'static str = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +Holmeses, success in the province of detective work must always +be, to a very large extent, the result of luck. Sherlock Holmes +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/tests.rs b/tests/tests.rs new file mode 100644 index 0000000..608f357 --- /dev/null +++ b/tests/tests.rs @@ -0,0 +1,563 @@ +/*! +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; + +mod hay; +mod workdir; + +macro_rules! sherlock { + ($name:ident, $fun:expr) => { + sherlock!($name, "Sherlock", $fun); + }; + ($name:ident, $query:expr, $fun:expr) => { + sherlock!($name, $query, "sherlock", $fun); + }; + ($name:ident, $query:expr, $path:expr, $fun:expr) => { + #[test] + fn $name() { + let wd = WorkDir::new(stringify!($name)); + wd.create("sherlock", hay::SHERLOCK); + let mut cmd = wd.command(); + cmd.arg($query).arg($path); + $fun(wd, cmd); + } + }; +} + +sherlock!(single_file, |wd: WorkDir, mut cmd| { + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(dir, "Sherlock", ".", |wd: WorkDir, mut cmd| { + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(line_numbers, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-n"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +3:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(columns, |wd: WorkDir, mut cmd: Command| { + cmd.arg("--column"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +57:For the Doctor Watsons of this world, as opposed to the Sherlock +49:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(with_filename, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-H"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(with_heading, |wd: WorkDir, mut cmd: Command| { + // This forces the issue since --with-filename is disabled by default + // when searching one fil.e + cmd.arg("--with-filename").arg("--heading"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock +For the Doctor Watsons of this world, as opposed to the Sherlock +be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(with_heading_default, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + // Search two or more and get --with-filename enabled by default. + // Use -j1 to get deterministic results. + wd.create("foo", "Sherlock Holmes lives on Baker Street."); + cmd.arg("-j1").arg("--heading"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +foo +Sherlock Holmes lives on Baker Street. + +sherlock +For the Doctor Watsons of this world, as opposed to the Sherlock +be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(inverted, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-v"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +Holmeses, success in the province of detective work must always +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. +"; + assert_eq!(lines, expected); +}); + +sherlock!(inverted_line_numbers, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-n").arg("-v"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +2:Holmeses, success in the province of detective work must always +4:can extract a clew from a wisp of straw or a flake of cigar ash; +5:but Doctor Watson has to have it taken out for him and dusted, +6:and exhibited clearly, with a label attached. +"; + assert_eq!(lines, expected); +}); + +sherlock!(case_insensitive, "sherlock", |wd: WorkDir, mut cmd: Command| { + cmd.arg("-i"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(word, "as", |wd: WorkDir, mut cmd: Command| { + cmd.arg("-w"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +"; + assert_eq!(lines, expected); +}); + +sherlock!(literal, "()", "file", |wd: WorkDir, mut cmd: Command| { + wd.create("file", "blib\n()\nblab\n"); + cmd.arg("-Q"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "()\n"); +}); + +sherlock!(quiet, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-q"); + let lines: String = wd.stdout(&mut cmd); + assert!(lines.is_empty()); +}); + +sherlock!(replace, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-r").arg("FooBar"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the FooBar +be, to a very large extent, the result of luck. FooBar Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(replace_groups, "([A-Z][a-z]+) ([A-Z][a-z]+)", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("-r").arg("$2, $1"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Watsons, Doctor of this world, as opposed to the Sherlock +be, to a very large extent, the result of luck. Holmes, Sherlock +but Watson, Doctor has to have it taken out for him and dusted, +"; + assert_eq!(lines, expected); +}); + +sherlock!(replace_named_groups, "(?P[A-Z][a-z]+) (?P[A-Z][a-z]+)", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("-r").arg("$last, $first"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Watsons, Doctor of this world, as opposed to the Sherlock +be, to a very large extent, the result of luck. Holmes, Sherlock +but Watson, Doctor has to have it taken out for him and dusted, +"; + assert_eq!(lines, expected); +}); + +sherlock!(file_types, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("file.py", "Sherlock"); + wd.create("file.rs", "Sherlock"); + cmd.arg("-t").arg("rust"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "file.rs:Sherlock\n"); +}); + +sherlock!(file_types_negate, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create("file.py", "Sherlock"); + wd.create("file.rs", "Sherlock"); + cmd.arg("-T").arg("rust"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "file.py:Sherlock\n"); +}); + +sherlock!(file_type_clear, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("file.py", "Sherlock"); + wd.create("file.rs", "Sherlock"); + cmd.arg("--type-clear").arg("rust").arg("-t").arg("rust"); + wd.assert_err(&mut cmd); +}); + +sherlock!(file_type_add, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("file.py", "Sherlock"); + wd.create("file.rs", "Sherlock"); + wd.create("file.wat", "Sherlock"); + cmd.arg("--type-add").arg("wat:*.wat").arg("-t").arg("wat"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "file.wat:Sherlock\n"); +}); + +sherlock!(glob, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create("file.py", "Sherlock"); + wd.create("file.rs", "Sherlock"); + cmd.arg("-g").arg("*.rs"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "file.rs:Sherlock\n"); +}); + +sherlock!(glob_negate, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create("file.py", "Sherlock"); + wd.create("file.rs", "Sherlock"); + cmd.arg("-g").arg("!*.rs"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "file.py:Sherlock\n"); +}); + +sherlock!(after_context, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-A").arg("1"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +Holmeses, success in the province of detective work must always +be, to a very large extent, the result of luck. Sherlock Holmes +can extract a clew from a wisp of straw or a flake of cigar ash; +"; + assert_eq!(lines, expected); +}); + +sherlock!(after_context_line_numbers, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-A").arg("1").arg("-n"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2-Holmeses, success in the province of detective work must always +3:be, to a very large extent, the result of luck. Sherlock Holmes +4-can extract a clew from a wisp of straw or a flake of cigar ash; +"; + assert_eq!(lines, expected); +}); + +sherlock!(before_context, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-B").arg("1"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +Holmeses, success in the province of detective work must always +be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(before_context_line_numbers, |wd: WorkDir, mut cmd: Command| { + cmd.arg("-B").arg("1").arg("-n"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2-Holmeses, success in the province of detective work must always +3:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(context, "world|attached", |wd: WorkDir, mut cmd: Command| { + cmd.arg("-C").arg("1"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +For the Doctor Watsons of this world, as opposed to the Sherlock +Holmeses, success in the province of detective work must always +-- +but Doctor Watson has to have it taken out for him and dusted, +and exhibited clearly, with a label attached. +"; + assert_eq!(lines, expected); +}); + +sherlock!(context_line_numbers, "world|attached", +|wd: WorkDir, mut cmd: Command| { + cmd.arg("-C").arg("1").arg("-n"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +1:For the Doctor Watsons of this world, as opposed to the Sherlock +2-Holmeses, success in the province of detective work must always +-- +5-but Doctor Watson has to have it taken out for him and dusted, +6:and exhibited clearly, with a label attached. +"; + assert_eq!(lines, expected); +}); + +sherlock!(ignore_hidden, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create(".sherlock", hay::SHERLOCK); + wd.assert_err(&mut cmd); +}); + +sherlock!(no_ignore_hidden, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create(".sherlock", hay::SHERLOCK); + + cmd.arg("--hidden"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +.sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +.sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(ignore_git, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create(".gitignore", "sherlock\n"); + wd.assert_err(&mut cmd); +}); + +sherlock!(ignore_ripgrep, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create(".rgignore", "sherlock\n"); + wd.assert_err(&mut cmd); +}); + +sherlock!(no_ignore, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.create(".gitignore", "sherlock\n"); + cmd.arg("--no-ignore"); + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(ignore_git_parent, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create(".gitignore", "sherlock\n"); + wd.create_dir(".git"); + wd.create_dir("foo"); + wd.create("foo/sherlock", hay::SHERLOCK); + // Even though we search in foo/, which has no .gitignore, ripgrep will + // search parent directories and respect the gitignore files found. + cmd.current_dir(wd.path().join("foo")); + wd.assert_err(&mut cmd); +}); + +sherlock!(ignore_git_parent_stop, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + // This tests that searching parent directories for .gitignore files stops + // after it sees a .git directory. To test this, we create this directory + // hierarchy: + // + // .gitignore (contains `sherlock`) + // foo/ + // .git + // bar/ + // sherlock + // + // And we perform the search inside `foo/bar/`. ripgrep will stop looking + // for .gitignore files after it sees `foo/.git/`, and therefore not + // respect the top-level `.gitignore` containing `sherlock`. + wd.remove("sherlock"); + wd.create(".gitignore", "sherlock\n"); + wd.create_dir("foo"); + wd.create_dir("foo/.git"); + wd.create_dir("foo/bar"); + wd.create("foo/bar/sherlock", hay::SHERLOCK); + cmd.current_dir(wd.path().join("foo").join("bar")); + + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(ignore_ripgrep_parent_no_stop, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + // This is like the `ignore_git_parent_stop` test, except it checks that + // ripgrep *doesn't* stop checking for .rgignore files. + wd.remove("sherlock"); + wd.create(".rgignore", "sherlock\n"); + wd.create_dir("foo"); + wd.create_dir("foo/.git"); + wd.create_dir("foo/bar"); + wd.create("foo/bar/sherlock", hay::SHERLOCK); + cmd.current_dir(wd.path().join("foo").join("bar")); + // The top-level .rgignore applies. + wd.assert_err(&mut cmd); +}); + +sherlock!(no_parent_ignore_git, "Sherlock", ".", +|wd: WorkDir, mut cmd: Command| { + // Set up a directory hierarchy like this: + // + // .gitignore + // foo/ + // .gitignore + // sherlock + // watson + // + // Where `.gitignore` contains `sherlock` and `foo/.gitignore` contains + // `watson`. + // + // Now *do the search* from the foo directory. By default, ripgrep will + // search parent directories for .gitignore files. The --no-ignore-parent + // flag should prevent that. At the same time, the `foo/.gitignore` file + // will still be respected (since the search is happening in `foo/`). + // + // In other words, we should only see results from `sherlock`, not from + // `watson`. + wd.remove("sherlock"); + wd.create(".gitignore", "sherlock\n"); + wd.create_dir("foo"); + wd.create("foo/.gitignore", "watson\n"); + wd.create("foo/sherlock", hay::SHERLOCK); + wd.create("foo/watson", hay::SHERLOCK); + cmd.current_dir(wd.path().join("foo")); + cmd.arg("--no-ignore-parent"); + + let lines: String = wd.stdout(&mut cmd); + let expected = "\ +sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); +}); + +sherlock!(symlink_nofollow, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create_dir("foo"); + wd.create_dir("foo/bar"); + wd.link("foo/baz", "foo/bar/baz"); + wd.create_dir("foo/baz"); + wd.create("foo/baz/sherlock", hay::SHERLOCK); + cmd.current_dir(wd.path().join("foo/bar")); + wd.assert_err(&mut cmd); +}); + +sherlock!(symlink_follow, "Sherlock", ".", |wd: WorkDir, mut cmd: Command| { + wd.remove("sherlock"); + wd.create_dir("foo"); + wd.create_dir("foo/bar"); + wd.create_dir("foo/baz"); + wd.create("foo/baz/sherlock", hay::SHERLOCK); + wd.link("foo/baz", "foo/bar/baz"); + cmd.arg("-L"); + cmd.current_dir(wd.path().join("foo/bar")); + + let lines: String = wd.stdout(&mut cmd); + if cfg!(windows) { + let expected = "\ +baz\\sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +baz\\sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); + } else { + let expected = "\ +baz/sherlock:For the Doctor Watsons of this world, as opposed to the Sherlock +baz/sherlock:be, to a very large extent, the result of luck. Sherlock Holmes +"; + assert_eq!(lines, expected); + } +}); + +#[test] +fn binary_nosearch() { + let wd = WorkDir::new("binary_nosearch"); + wd.create("file", "foo\x00bar\nfoo\x00baz\n"); + let mut cmd = wd.command(); + cmd.arg("foo").arg("file"); + wd.assert_err(&mut cmd); +} + +// The following two tests show a discrepancy in search results between +// searching with memory mapped files and stream searching. Stream searching +// uses a heuristic (that GNU grep also uses) where NUL bytes are replaced with +// the EOL terminator, which tends to avoid allocating large amounts of memory +// for really long "lines." The memory map searcher has no need to worry about +// such things, and more than that, it would be pretty hard for it to match +// the semantics of streaming search in this case. +// +// Binary files with lots of NULs aren't really part of the use case of ripgrep +// (or any other grep-like tool for that matter), so we shouldn't feel too bad +// about it. +#[test] +fn binary_search_mmap() { + let wd = WorkDir::new("binary_search_mmap"); + wd.create("file", "foo\x00bar\nfoo\x00baz\n"); + let mut cmd = wd.command(); + cmd.arg("-a").arg("--mmap").arg("foo").arg("file"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "foo\x00bar\nfoo\x00baz\n"); +} + +#[test] +fn binary_search_no_mmap() { + let wd = WorkDir::new("binary_search_no_mmap"); + wd.create("file", "foo\x00bar\nfoo\x00baz\n"); + let mut cmd = wd.command(); + cmd.arg("-a").arg("--no-mmap").arg("foo").arg("file"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "foo\nfoo\n"); +} + +#[test] +fn files() { + let wd = WorkDir::new("files"); + wd.create("file", ""); + wd.create_dir("dir"); + wd.create("dir/file", ""); + + let mut cmd = wd.command(); + cmd.arg("--files"); + let lines: String = wd.stdout(&mut cmd); + assert_eq!(lines, "./file\n./dir/file\n"); +} + +#[test] +fn type_list() { + let wd = WorkDir::new("type_list"); + + let mut cmd = wd.command(); + cmd.arg("--type-list"); + let lines: String = wd.stdout(&mut cmd); + // This can change over time, so just make sure we print something. + assert!(!lines.is_empty()); +} diff --git a/tests/workdir.rs b/tests/workdir.rs new file mode 100644 index 0000000..89b2da1 --- /dev/null +++ b/tests/workdir.rs @@ -0,0 +1,189 @@ +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; +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; + +/// WorkDir represents a directory in which tests are run. +/// +/// Directories are created from a global atomic counter to avoid duplicates. +#[derive(Debug)] +pub struct WorkDir { + /// 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. + dir: PathBuf, +} + +impl WorkDir { + /// 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) -> WorkDir { + 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 = root.join(TEST_DIR).join(name).join(&format!("{}", id)); + nice_err(&dir, repeat(|| fs::create_dir_all(&dir))); + WorkDir { + root: root, + dir: dir, + } + } + + /// Create a new file with the given name and contents in this directory. + pub fn create>(&self, name: P, contents: &str) { + let path = self.dir.join(name); + let mut file = nice_err(&path, File::create(&path)); + nice_err(&path, file.write_all(contents.as_bytes())); + nice_err(&path, 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. + pub fn command(&self) -> process::Command { + let mut cmd = process::Command::new(&self.bin()); + cmd.current_dir(&self.dir); + cmd + } + + /// Returns the path to the ripgrep executable. + pub fn bin(&self) -> PathBuf { + 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, 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)); + } + + #[cfg(windows)] + pub fn link, 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)); + } + + /// 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 o = cmd.output().unwrap(); + 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)); + } + } +} + +fn nice_err, T, E: error::Error>( + path: P, + res: Result, +) -> T { + match res { + Ok(t) => t, + Err(err) => { + panic!("{}: {:?}", path.as_ref().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()) +} -- GitLab