PageRenderTime 49ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 0ms

/tests/util.rs

https://github.com/BurntSushi/ripgrep
Rust | 482 lines | 350 code | 44 blank | 88 comment | 16 complexity | f6119bc60a0af708bd80ff5435e59467 MD5 | raw file
Possible License(s): MIT, Unlicense
  1. use std::env;
  2. use std::error;
  3. use std::ffi::OsStr;
  4. use std::fs::{self, File};
  5. use std::io::{self, Write};
  6. use std::path::{Path, PathBuf};
  7. use std::process::{self, Command};
  8. use std::sync::atomic::{AtomicUsize, Ordering};
  9. use std::thread;
  10. use std::time::Duration;
  11. static TEST_DIR: &'static str = "ripgrep-tests";
  12. static NEXT_ID: AtomicUsize = AtomicUsize::new(0);
  13. /// Setup an empty work directory and return a command pointing to the ripgrep
  14. /// executable whose CWD is set to the work directory.
  15. ///
  16. /// The name given will be used to create the directory. Generally, it should
  17. /// correspond to the test name.
  18. pub fn setup(test_name: &str) -> (Dir, TestCommand) {
  19. let dir = Dir::new(test_name);
  20. let cmd = dir.command();
  21. (dir, cmd)
  22. }
  23. /// Like `setup`, but uses PCRE2 as the underlying regex engine.
  24. pub fn setup_pcre2(test_name: &str) -> (Dir, TestCommand) {
  25. let mut dir = Dir::new(test_name);
  26. dir.pcre2(true);
  27. let cmd = dir.command();
  28. (dir, cmd)
  29. }
  30. /// Break the given string into lines, sort them and then join them back
  31. /// together. This is useful for testing output from ripgrep that may not
  32. /// always be in the same order.
  33. pub fn sort_lines(lines: &str) -> String {
  34. let mut lines: Vec<&str> = lines.trim().lines().collect();
  35. lines.sort();
  36. format!("{}\n", lines.join("\n"))
  37. }
  38. /// Returns true if and only if the given program can be successfully executed
  39. /// with a `--help` flag.
  40. pub fn cmd_exists(program: &str) -> bool {
  41. Command::new(program).arg("--help").output().is_ok()
  42. }
  43. /// Dir represents a directory in which tests should be run.
  44. ///
  45. /// Directories are created from a global atomic counter to avoid duplicates.
  46. #[derive(Clone, Debug)]
  47. pub struct Dir {
  48. /// The directory in which this test executable is running.
  49. root: PathBuf,
  50. /// The directory in which the test should run. If a test needs to create
  51. /// files, they should go in here. This directory is also used as the CWD
  52. /// for any processes created by the test.
  53. dir: PathBuf,
  54. /// Set to true when the test should use PCRE2 as the regex engine.
  55. pcre2: bool,
  56. }
  57. impl Dir {
  58. /// Create a new test working directory with the given name. The name
  59. /// does not need to be distinct for each invocation, but should correspond
  60. /// to a logical grouping of tests.
  61. pub fn new(name: &str) -> Dir {
  62. let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
  63. let root = env::current_exe()
  64. .unwrap()
  65. .parent()
  66. .expect("executable's directory")
  67. .to_path_buf();
  68. let dir =
  69. env::temp_dir().join(TEST_DIR).join(name).join(&format!("{}", id));
  70. if dir.exists() {
  71. nice_err(&dir, fs::remove_dir_all(&dir));
  72. }
  73. nice_err(&dir, repeat(|| fs::create_dir_all(&dir)));
  74. Dir { root: root, dir: dir, pcre2: false }
  75. }
  76. /// Use PCRE2 for this test.
  77. pub fn pcre2(&mut self, yes: bool) {
  78. self.pcre2 = yes;
  79. }
  80. /// Returns true if and only if this test is configured to use PCRE2 as
  81. /// the regex engine.
  82. pub fn is_pcre2(&self) -> bool {
  83. self.pcre2
  84. }
  85. /// Create a new file with the given name and contents in this directory,
  86. /// or panic on error.
  87. pub fn create<P: AsRef<Path>>(&self, name: P, contents: &str) {
  88. self.create_bytes(name, contents.as_bytes());
  89. }
  90. /// Try to create a new file with the given name and contents in this
  91. /// directory.
  92. #[allow(dead_code)] // unused on Windows
  93. pub fn try_create<P: AsRef<Path>>(
  94. &self,
  95. name: P,
  96. contents: &str,
  97. ) -> io::Result<()> {
  98. let path = self.dir.join(name);
  99. self.try_create_bytes(path, contents.as_bytes())
  100. }
  101. /// Create a new file with the given name and size.
  102. pub fn create_size<P: AsRef<Path>>(&self, name: P, filesize: u64) {
  103. let path = self.dir.join(name);
  104. let file = nice_err(&path, File::create(&path));
  105. nice_err(&path, file.set_len(filesize));
  106. }
  107. /// Create a new file with the given name and contents in this directory,
  108. /// or panic on error.
  109. pub fn create_bytes<P: AsRef<Path>>(&self, name: P, contents: &[u8]) {
  110. let path = self.dir.join(&name);
  111. nice_err(&path, self.try_create_bytes(name, contents));
  112. }
  113. /// Try to create a new file with the given name and contents in this
  114. /// directory.
  115. pub fn try_create_bytes<P: AsRef<Path>>(
  116. &self,
  117. name: P,
  118. contents: &[u8],
  119. ) -> io::Result<()> {
  120. let path = self.dir.join(name);
  121. let mut file = File::create(path)?;
  122. file.write_all(contents)?;
  123. file.flush()
  124. }
  125. /// Remove a file with the given name from this directory.
  126. pub fn remove<P: AsRef<Path>>(&self, name: P) {
  127. let path = self.dir.join(name);
  128. nice_err(&path, fs::remove_file(&path));
  129. }
  130. /// Create a new directory with the given path (and any directories above
  131. /// it) inside this directory.
  132. pub fn create_dir<P: AsRef<Path>>(&self, path: P) {
  133. let path = self.dir.join(path);
  134. nice_err(&path, repeat(|| fs::create_dir_all(&path)));
  135. }
  136. /// Creates a new command that is set to use the ripgrep executable in
  137. /// this working directory.
  138. ///
  139. /// This also:
  140. ///
  141. /// * Unsets the `RIPGREP_CONFIG_PATH` environment variable.
  142. /// * Sets the `--path-separator` to `/` so that paths have the same output
  143. /// on all systems. Tests that need to check `--path-separator` itself
  144. /// can simply pass it again to override it.
  145. pub fn command(&self) -> TestCommand {
  146. let mut cmd = self.bin();
  147. cmd.env_remove("RIPGREP_CONFIG_PATH");
  148. cmd.current_dir(&self.dir);
  149. cmd.arg("--path-separator").arg("/");
  150. if self.is_pcre2() {
  151. cmd.arg("--pcre2");
  152. }
  153. TestCommand { dir: self.clone(), cmd: cmd }
  154. }
  155. /// Returns the path to the ripgrep executable.
  156. pub fn bin(&self) -> process::Command {
  157. let rg = self.root.join(format!("../rg{}", env::consts::EXE_SUFFIX));
  158. match cross_runner() {
  159. None => process::Command::new(rg),
  160. Some(runner) => {
  161. let mut cmd = process::Command::new(runner);
  162. cmd.arg(rg);
  163. cmd
  164. }
  165. }
  166. }
  167. /// Returns the path to this directory.
  168. pub fn path(&self) -> &Path {
  169. &self.dir
  170. }
  171. /// Creates a directory symlink to the src with the given target name
  172. /// in this directory.
  173. #[cfg(not(windows))]
  174. pub fn link_dir<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
  175. use std::os::unix::fs::symlink;
  176. let src = self.dir.join(src);
  177. let target = self.dir.join(target);
  178. let _ = fs::remove_file(&target);
  179. nice_err(&target, symlink(&src, &target));
  180. }
  181. /// Creates a directory symlink to the src with the given target name
  182. /// in this directory.
  183. #[cfg(windows)]
  184. pub fn link_dir<S: AsRef<Path>, T: AsRef<Path>>(&self, src: S, target: T) {
  185. use std::os::windows::fs::symlink_dir;
  186. let src = self.dir.join(src);
  187. let target = self.dir.join(target);
  188. let _ = fs::remove_dir(&target);
  189. nice_err(&target, symlink_dir(&src, &target));
  190. }
  191. /// Creates a file symlink to the src with the given target name
  192. /// in this directory.
  193. #[cfg(not(windows))]
  194. pub fn link_file<S: AsRef<Path>, T: AsRef<Path>>(
  195. &self,
  196. src: S,
  197. target: T,
  198. ) {
  199. self.link_dir(src, target);
  200. }
  201. /// Creates a file symlink to the src with the given target name
  202. /// in this directory.
  203. #[cfg(windows)]
  204. #[allow(dead_code)] // unused on Windows
  205. pub fn link_file<S: AsRef<Path>, T: AsRef<Path>>(
  206. &self,
  207. src: S,
  208. target: T,
  209. ) {
  210. use std::os::windows::fs::symlink_file;
  211. let src = self.dir.join(src);
  212. let target = self.dir.join(target);
  213. let _ = fs::remove_file(&target);
  214. nice_err(&target, symlink_file(&src, &target));
  215. }
  216. }
  217. /// A simple wrapper around a process::Command with some conveniences.
  218. #[derive(Debug)]
  219. pub struct TestCommand {
  220. /// The dir used to launched this command.
  221. dir: Dir,
  222. /// The actual command we use to control the process.
  223. cmd: Command,
  224. }
  225. impl TestCommand {
  226. /// Returns a mutable reference to the underlying command.
  227. pub fn cmd(&mut self) -> &mut Command {
  228. &mut self.cmd
  229. }
  230. /// Add an argument to pass to the command.
  231. pub fn arg<A: AsRef<OsStr>>(&mut self, arg: A) -> &mut TestCommand {
  232. self.cmd.arg(arg);
  233. self
  234. }
  235. /// Add any number of arguments to the command.
  236. pub fn args<I, A>(&mut self, args: I) -> &mut TestCommand
  237. where
  238. I: IntoIterator<Item = A>,
  239. A: AsRef<OsStr>,
  240. {
  241. self.cmd.args(args);
  242. self
  243. }
  244. /// Set the working directory for this command.
  245. ///
  246. /// Note that this does not need to be called normally, since the creation
  247. /// of this TestCommand causes its working directory to be set to the
  248. /// test's directory automatically.
  249. pub fn current_dir<P: AsRef<Path>>(&mut self, dir: P) -> &mut TestCommand {
  250. self.cmd.current_dir(dir);
  251. self
  252. }
  253. /// Runs and captures the stdout of the given command.
  254. pub fn stdout(&mut self) -> String {
  255. let o = self.output();
  256. let stdout = String::from_utf8_lossy(&o.stdout);
  257. match stdout.parse() {
  258. Ok(t) => t,
  259. Err(err) => {
  260. panic!(
  261. "could not convert from string: {:?}\n\n{}",
  262. err, stdout
  263. );
  264. }
  265. }
  266. }
  267. /// Pipe `input` to a command, and collect the output.
  268. pub fn pipe(&mut self, input: &[u8]) -> String {
  269. self.cmd.stdin(process::Stdio::piped());
  270. self.cmd.stdout(process::Stdio::piped());
  271. self.cmd.stderr(process::Stdio::piped());
  272. let mut child = self.cmd.spawn().unwrap();
  273. // Pipe input to child process using a separate thread to avoid
  274. // risk of deadlock between parent and child process.
  275. let mut stdin = child.stdin.take().expect("expected standard input");
  276. let input = input.to_owned();
  277. let worker = thread::spawn(move || stdin.write_all(&input));
  278. let output = self.expect_success(child.wait_with_output().unwrap());
  279. worker.join().unwrap().unwrap();
  280. let stdout = String::from_utf8_lossy(&output.stdout);
  281. match stdout.parse() {
  282. Ok(t) => t,
  283. Err(err) => {
  284. panic!(
  285. "could not convert from string: {:?}\n\n{}",
  286. err, stdout
  287. );
  288. }
  289. }
  290. }
  291. /// Gets the output of a command. If the command failed, then this panics.
  292. pub fn output(&mut self) -> process::Output {
  293. let output = self.cmd.output().unwrap();
  294. self.expect_success(output)
  295. }
  296. /// Runs the command and asserts that it resulted in an error exit code.
  297. pub fn assert_err(&mut self) {
  298. let o = self.cmd.output().unwrap();
  299. if o.status.success() {
  300. panic!(
  301. "\n\n===== {:?} =====\n\
  302. command succeeded but expected failure!\
  303. \n\ncwd: {}\
  304. \n\ndir list: {:?}\
  305. \n\nstatus: {}\
  306. \n\nstdout: {}\n\nstderr: {}\
  307. \n\n=====\n",
  308. self.cmd,
  309. self.dir.dir.display(),
  310. dir_list(&self.dir.dir),
  311. o.status,
  312. String::from_utf8_lossy(&o.stdout),
  313. String::from_utf8_lossy(&o.stderr)
  314. );
  315. }
  316. }
  317. /// Runs the command and asserts that its exit code matches expected exit
  318. /// code.
  319. pub fn assert_exit_code(&mut self, expected_code: i32) {
  320. let code = self.cmd.output().unwrap().status.code().unwrap();
  321. assert_eq!(
  322. expected_code,
  323. code,
  324. "\n\n===== {:?} =====\n\
  325. expected exit code did not match\
  326. \n\ncwd: {}\
  327. \n\ndir list: {:?}\
  328. \n\nexpected: {}\
  329. \n\nfound: {}\
  330. \n\n=====\n",
  331. self.cmd,
  332. self.dir.dir.display(),
  333. dir_list(&self.dir.dir),
  334. expected_code,
  335. code
  336. );
  337. }
  338. /// Runs the command and asserts that something was printed to stderr.
  339. pub fn assert_non_empty_stderr(&mut self) {
  340. let o = self.cmd.output().unwrap();
  341. if o.status.success() || o.stderr.is_empty() {
  342. panic!(
  343. "\n\n===== {:?} =====\n\
  344. command succeeded but expected failure!\
  345. \n\ncwd: {}\
  346. \n\ndir list: {:?}\
  347. \n\nstatus: {}\
  348. \n\nstdout: {}\n\nstderr: {}\
  349. \n\n=====\n",
  350. self.cmd,
  351. self.dir.dir.display(),
  352. dir_list(&self.dir.dir),
  353. o.status,
  354. String::from_utf8_lossy(&o.stdout),
  355. String::from_utf8_lossy(&o.stderr)
  356. );
  357. }
  358. }
  359. fn expect_success(&self, o: process::Output) -> process::Output {
  360. if !o.status.success() {
  361. let suggest = if o.stderr.is_empty() {
  362. "\n\nDid your search end up with no results?".to_string()
  363. } else {
  364. "".to_string()
  365. };
  366. panic!(
  367. "\n\n==========\n\
  368. command failed but expected success!\
  369. {}\
  370. \n\ncommand: {:?}\
  371. \n\ncwd: {}\
  372. \n\ndir list: {:?}\
  373. \n\nstatus: {}\
  374. \n\nstdout: {}\
  375. \n\nstderr: {}\
  376. \n\n==========\n",
  377. suggest,
  378. self.cmd,
  379. self.dir.dir.display(),
  380. dir_list(&self.dir.dir),
  381. o.status,
  382. String::from_utf8_lossy(&o.stdout),
  383. String::from_utf8_lossy(&o.stderr)
  384. );
  385. }
  386. o
  387. }
  388. }
  389. fn nice_err<T, E: error::Error>(path: &Path, res: Result<T, E>) -> T {
  390. match res {
  391. Ok(t) => t,
  392. Err(err) => panic!("{}: {:?}", path.display(), err),
  393. }
  394. }
  395. fn repeat<F: FnMut() -> io::Result<()>>(mut f: F) -> io::Result<()> {
  396. let mut last_err = None;
  397. for _ in 0..10 {
  398. if let Err(err) = f() {
  399. last_err = Some(err);
  400. thread::sleep(Duration::from_millis(500));
  401. } else {
  402. return Ok(());
  403. }
  404. }
  405. Err(last_err.unwrap())
  406. }
  407. /// Return a recursive listing of all files and directories in the given
  408. /// directory. This is useful for debugging transient and odd failures in
  409. /// integration tests.
  410. fn dir_list<P: AsRef<Path>>(dir: P) -> Vec<String> {
  411. walkdir::WalkDir::new(dir)
  412. .follow_links(true)
  413. .into_iter()
  414. .map(|result| result.unwrap().path().to_string_lossy().into_owned())
  415. .collect()
  416. }
  417. /// When running tests with cross, we need to be a bit smarter about how we
  418. /// run our `rg` binary. We can't just run it directly since it might be
  419. /// compiled for a totally different target. Instead, it's likely that `cross`
  420. /// will have setup qemu to run it. While this is integrated into the Rust
  421. /// testing by default, we need to handle it ourselves for integration tests.
  422. ///
  423. /// Thankfully, cross sets an environment variable that points to the proper
  424. /// qemu binary that we want to run. So we just search for that env var and
  425. /// return its value if we could find it.
  426. fn cross_runner() -> Option<String> {
  427. for (k, v) in std::env::vars_os() {
  428. let (k, v) = (k.to_string_lossy(), v.to_string_lossy());
  429. if !k.starts_with("CARGO_TARGET_") && !k.ends_with("_RUNNER") {
  430. continue;
  431. }
  432. if !v.starts_with("qemu-") {
  433. continue;
  434. }
  435. return Some(v.into_owned());
  436. }
  437. None
  438. }