use std::env;
use std::error;
use std::ffi::OsStr;
use std::fs::{self, File};
use std::io::{self, Write};
use std::path::{Path, PathBuf};
use std::process::{self, Command};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::thread;
use std::time::Duration;
use bstr::ByteSlice;
static TEST_DIR: &'static str = "ripgrep-tests";
static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
pub fn setup(test_name: &str) -> (Dir, TestCommand) {
let dir = Dir::new(test_name);
let cmd = dir.command();
(dir, cmd)
}
pub fn setup_pcre2(test_name: &str) -> (Dir, TestCommand) {
let mut dir = Dir::new(test_name);
dir.pcre2(true);
let cmd = dir.command();
(dir, cmd)
}
pub fn sort_lines(lines: &str) -> String {
let mut lines: Vec<&str> = lines.trim().lines().collect();
lines.sort();
format!("{}\n", lines.join("\n"))
}
pub fn cmd_exists(program: &str) -> bool {
Command::new(program).arg("--help").output().is_ok()
}
#[derive(Clone, Debug)]
pub struct Dir {
root: PathBuf,
dir: PathBuf,
pcre2: bool,
}
impl Dir {
pub fn new(name: &str) -> Dir {
let id = NEXT_ID.fetch_add(1, Ordering::Relaxed);
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}"));
if dir.exists() {
nice_err(&dir, fs::remove_dir_all(&dir));
}
nice_err(&dir, repeat(|| fs::create_dir_all(&dir)));
Dir { root, dir, pcre2: false }
}
pub fn pcre2(&mut self, yes: bool) {
self.pcre2 = yes;
}
pub fn is_pcre2(&self) -> bool {
self.pcre2
}
pub fn create<P: AsRef<Path>>(&self, name: P, contents: &str) {
self.create_bytes(name, contents.as_bytes());
}
#[allow(dead_code)] pub fn try_create<P: AsRef<Path>>(
&self,
name: P,
contents: &str,
) -> io::Result<()> {
let path = self.dir.join(name);
self.try_create_bytes(path, contents.as_bytes())
}
pub fn create_size<P: AsRef<Path>>(&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));
}
pub fn create_bytes<P: AsRef<Path>>(&self, name: P, contents: &[u8]) {
let path = self.dir.join(&name);
nice_err(&path, self.try_create_bytes(name, contents));
}
pub fn try_create_bytes<P: AsRef<Path>>(
&self,
name: P,
contents: &[u8],
) -> io::Result<()> {
let path = self.dir.join(name);
let mut file = File::create(path)?;
file.write_all(contents)?;
file.flush()
}
pub fn remove<P: AsRef<Path>>(&self, name: P) {
let path = self.dir.join(name);
nice_err(&path, fs::remove_file(&path));
}
pub fn create_dir<P: AsRef<Path>>(&self, path: P) {
let path = self.dir.join(path);
nice_err(&path, repeat(|| fs::create_dir_all(&path)));
}
pub fn command(&self) -> TestCommand {
let mut cmd = self.bin();
cmd.env_remove("RIPGREP_CONFIG_PATH");
cmd.current_dir(&self.dir);
cmd.arg("--path-separator").arg("/");
if self.is_pcre2() {
cmd.arg("--pcre2");
}
TestCommand { dir: self.clone(), cmd }
}
pub fn bin(&self) -> process::Command {
let rg = self.root.join(format!("../rg{}", env::consts::EXE_SUFFIX));
match cross_runner() {
None => process::Command::new(rg),
Some(runner) => {
let mut cmd = process::Command::new(runner);
cmd.arg(rg);
cmd
}
}
}
pub fn path(&self) -> &Path {
&self.dir
}
#[cfg(not(windows))]
pub fn link_dir<S: AsRef<Path>, T: AsRef<Path>>(&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_dir<S: AsRef<Path>, T: AsRef<Path>>(&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));
}
#[cfg(not(windows))]
pub fn link_file<S: AsRef<Path>, T: AsRef<Path>>(
&self,
src: S,
target: T,
) {
self.link_dir(src, target);
}
#[cfg(windows)]
#[allow(dead_code)] pub fn link_file<S: AsRef<Path>, T: AsRef<Path>>(
&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));
}
}
#[derive(Debug)]
pub struct TestCommand {
dir: Dir,
cmd: Command,
}
impl TestCommand {
pub fn cmd(&mut self) -> &mut Command {
&mut self.cmd
}
pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut TestCommand {
self.cmd.arg(arg);
self
}
pub fn args<I, A>(&mut self, args: I) -> &mut TestCommand
where
I: IntoIterator<Item = A>,
A: AsRef<OsStr>,
{
self.cmd.args(args);
self
}
pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut TestCommand {
self.cmd.current_dir(self.dir.path().join(dir));
self
}
pub fn stdout(&mut self) -> String {
let o = self.output();
String::from_utf8_lossy(&o.stdout).into_owned()
}
pub fn pipe(&mut self, input: &[u8]) -> String {
self.cmd.stdin(process::Stdio::piped());
self.cmd.stdout(process::Stdio::piped());
self.cmd.stderr(process::Stdio::piped());
let mut child = self.cmd.spawn().unwrap();
let mut stdin = child.stdin.take().expect("expected standard input");
let input = input.to_owned();
let worker = thread::spawn(move || stdin.write_all(&input));
let output = self.expect_success(child.wait_with_output().unwrap());
worker.join().unwrap().unwrap();
String::from_utf8_lossy(&output.stdout).into_owned()
}
pub fn output(&mut self) -> process::Output {
let output = self.raw_output();
self.expect_success(output)
}
pub fn raw_output(&mut self) -> process::Output {
let mut output = self.cmd.output().unwrap();
output.stderr = strip_jemalloc_nonsense(&output.stderr);
output
}
pub fn assert_err(&mut self) {
let o = self.raw_output();
if o.status.success() {
panic!(
"\n\n===== {:?} =====\n\
command succeeded but expected failure!\
\n\ncwd: {}\
\n\ndir list: {:?}\
\n\nstatus: {}\
\n\nstdout: {}\n\nstderr: {}\
\n\n=====\n",
self.cmd,
self.dir.dir.display(),
dir_list(&self.dir.dir),
o.status,
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
);
}
}
pub fn assert_exit_code(&mut self, expected_code: i32) {
let code = self.cmd.output().unwrap().status.code().unwrap();
assert_eq!(
expected_code,
code,
"\n\n===== {:?} =====\n\
expected exit code did not match\
\n\ncwd: {}\
\n\ndir list: {:?}\
\n\nexpected: {}\
\n\nfound: {}\
\n\n=====\n",
self.cmd,
self.dir.dir.display(),
dir_list(&self.dir.dir),
expected_code,
code
);
}
pub fn assert_non_empty_stderr(&mut self) {
let o = self.cmd.output().unwrap();
if o.status.success() || o.stderr.is_empty() {
panic!(
"\n\n===== {:?} =====\n\
command succeeded but expected failure!\
\n\ncwd: {}\
\n\ndir list: {:?}\
\n\nstatus: {}\
\n\nstdout: {}\n\nstderr: {}\
\n\n=====\n",
self.cmd,
self.dir.dir.display(),
dir_list(&self.dir.dir),
o.status,
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
);
}
}
fn expect_success(&self, 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: {:?}\
\n\ncwd: {}\
\n\ndir list: {:?}\
\n\nstatus: {}\
\n\nstdout: {}\
\n\nstderr: {}\
\n\n==========\n",
suggest,
self.cmd,
self.dir.dir.display(),
dir_list(&self.dir.dir),
o.status,
String::from_utf8_lossy(&o.stdout),
String::from_utf8_lossy(&o.stderr)
);
}
o
}
}
fn nice_err<T, E: error::Error>(path: &Path, res: Result<T, E>) -> T {
match res {
Ok(t) => t,
Err(err) => panic!("{}: {:?}", path.display(), err),
}
}
fn repeat<F: FnMut() -> 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())
}
fn dir_list<P: AsRef<Path>>(dir: P) -> Vec<String> {
walkdir::WalkDir::new(dir)
.follow_links(true)
.into_iter()
.map(|result| result.unwrap().path().to_string_lossy().into_owned())
.collect()
}
fn cross_runner() -> Option<String> {
let runner = std::env::var("CROSS_RUNNER").ok()?;
if runner.is_empty() || runner == "empty" {
return None;
}
if cfg!(target_arch = "powerpc64") {
Some("qemu-ppc64".to_string())
} else if cfg!(target_arch = "x86") {
Some("i386".to_string())
} else {
Some(format!("qemu-{}", std::env::consts::ARCH))
}
}
pub fn is_cross() -> bool {
std::env::var("CROSS_RUNNER").ok().map_or(false, |v| !v.is_empty())
}
fn strip_jemalloc_nonsense(data: &[u8]) -> Vec<u8> {
let lines = data
.lines_with_terminator()
.filter(|line| !line.starts_with_str("<jemalloc>:"));
bstr::concat(lines)
}