From 97673016cedbcd47b24a022f7925d0717ec88eed Mon Sep 17 00:00:00 2001 From: Ed Page Date: Tue, 27 Mar 2018 17:00:31 -0600 Subject: [PATCH] feat(tmpdir): Augment tempdir::TempDir This is an experiment in what kind of tempdir operations a holistic CLI testing framework might provide, following on the previous experiments with extension traits. The exact structure in this crate or across crates is TBD. This crate extends `TempDir` with the following - In TempDir or a child path, run a command. - On child path, touch a file. - On child path, write a binary blob or str to file. - Copy to a TempDir or a child path some files. Some other potential operations include - `write_yml(serde)` - `write_json(serde)` - `write_toml(serde)` In contrast, `cli_test_dir` can: - Run a single pre-defined program within the tempdir - Write binary files to tempdir - Offer a absolute path to a child file within the crate source (so its safe to pass to the program running in the tempdir). --- Cargo.toml | 6 ++ src/lib.rs | 5 + src/temp.rs | 269 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 src/temp.rs diff --git a/Cargo.toml b/Cargo.toml index 46aa92a..7428cc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,10 @@ build = "build.rs" [[bin]] name = "assert_fixture" +[features] +default = ["tempdir"] +tempdir = ["tempfile", "globwalk"] + [dependencies] colored = "1.5" difference = "2.0" @@ -22,6 +26,8 @@ failure = "0.1" failure_derive = "0.1" serde_json = "1.0" environment = "0.1" +tempfile = { version="3.0", optional=true } +globwalk = { version="0.1", optional=true } [build-dependencies] skeptic = "0.13" diff --git a/src/lib.rs b/src/lib.rs index 808e978..8d1b58d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,9 @@ extern crate environment; extern crate failure; #[macro_use] extern crate failure_derive; +extern crate globwalk; extern crate serde_json; +extern crate tempfile; mod errors; pub use errors::AssertionError; @@ -139,6 +141,9 @@ mod output; /// `std::process::Command` extensions. pub mod cmd; +/// `tempfile::TempDir` extensions. +#[cfg(feature = "tempdir")] +pub mod temp; pub use assert::Assert; pub use assert::OutputAssertionBuilder; diff --git a/src/temp.rs b/src/temp.rs new file mode 100644 index 0000000..a831c9d --- /dev/null +++ b/src/temp.rs @@ -0,0 +1,269 @@ +use std::ffi; +use std::fs; +use std::io; +use std::io::Write; +use std::path; +use std::process; + +use globwalk; +use tempfile; +use failure; + +// Quick and dirty for doc tests; not meant for long term use. +pub use tempfile::TempDir; + +/// Extend `TempDir` to perform operations on relative paths within the temp directory via +/// `ChildPath`. +pub trait TempDirChildExt { + /// Create a path within the temp directory. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// println!("{:?}", temp.path()); + /// println!("{:?}", temp.child("foo/bar.txt").path()); + /// temp.close().unwrap(); + /// ``` + fn child

(&self, path: P) -> ChildPath + where + P: AsRef; +} + +impl TempDirChildExt for tempfile::TempDir { + fn child

(&self, path: P) -> ChildPath + where + P: AsRef, + { + ChildPath::new(self.path().join(path.as_ref())) + } +} + +/// A path within a TempDir +pub struct ChildPath { + path: path::PathBuf, +} + +impl ChildPath { + /// Wrap a path for use with special built extension traits. + /// + /// See trait implementations or `TempDirChildExt` for more details. + pub fn new

(path: P) -> Self + where + P: Into, + { + Self { path: path.into() } + } + + /// Access the path. + pub fn path(&self) -> &path::Path { + &self.path + } +} + +/// Extend `TempDir` to run commands in it. +pub trait TempDirCommandExt { + /// Constructs a new Command for launching the program at path program, with the following + /// default configuration: + /// + /// - The current working directory is the temp dir + /// - No arguments to the program + /// - Inherit the current process's environment + /// - Inherit the current process's working directory + /// - Inherit stdin/stdout/stderr for spawn or status, but create pipes for output + /// - Builder methods are provided to change these defaults and otherwise configure the process. + /// + /// If program is not an absolute path, the PATH will be searched in an OS-defined way. + /// + /// The search path to be used may be controlled by setting the PATH environment variable on + /// the Command, but this has some implementation limitations on Windows (see + /// https://github.com/rust-lang/rust/issues/37519). + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.command("pwd").output().unwrap(); + /// temp.close().unwrap(); + /// ``` + fn command(&self, program: S) -> process::Command + where + S: AsRef; +} + +impl TempDirCommandExt for tempfile::TempDir { + fn command(&self, program: S) -> process::Command + where + S: AsRef, + { + let mut cmd = process::Command::new(program); + cmd.current_dir(self.path()); + cmd + } +} + +impl TempDirCommandExt for ChildPath { + fn command(&self, program: S) -> process::Command + where + S: AsRef, + { + let mut cmd = process::Command::new(program); + cmd.current_dir(self.path()); + cmd + } +} + +/// Extend `ChildPath` to create empty files. +pub trait ChildPathTouchExt { + /// Create an empty file at `ChildPath`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.child("foo.txt").touch().unwrap(); + /// temp.close().unwrap(); + /// ``` + fn touch(&self) -> io::Result<()>; +} + +impl ChildPathTouchExt for ChildPath { + fn touch(&self) -> io::Result<()> { + touch(self.path()) + } +} + +/// Extend `ChildPath` to write binary files. +pub trait ChildPathWriteBinExt { + /// Write a binary file at `ChildPath`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.child("foo.txt").write_binary(b"To be or not to be...").unwrap(); + /// temp.close().unwrap(); + /// ``` + fn write_binary(&self, data: &[u8]) -> io::Result<()>; +} + +impl ChildPathWriteBinExt for ChildPath { + fn write_binary(&self, data: &[u8]) -> io::Result<()> { + write_binary(self.path(), data) + } +} + +/// Extend `ChildPath` to write text files. +pub trait ChildPathWriteStrExt { + /// Write a text file at `ChildPath`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.child("foo.txt").write_str("To be or not to be...").unwrap(); + /// temp.close().unwrap(); + /// ``` + fn write_str(&self, data: &str) -> io::Result<()>; +} + +impl ChildPathWriteStrExt for ChildPath { + fn write_str(&self, data: &str) -> io::Result<()> { + write_str(self.path(), data) + } +} + +/// Extend `TempDir` to copy files into it. +pub trait TempDirCopyExt { + /// Copy files and directories into the current path from the `source` according to the glob + /// `patterns`. + /// + /// # Examples + /// + /// ```rust,ignore + /// extern crate assert_cli; + /// use assert_cli::temp::*; + /// + /// let temp = TempDir::new("TempDirChildExt_demo").unwrap(); + /// temp.copy_from(".", &["*.rs"]).unwrap(); + /// temp.close().unwrap(); + /// ``` + fn copy_from(&self, source: P, patterns: &[S]) -> Result<(), failure::Error> + where + P: AsRef, + S: AsRef; +} + +impl TempDirCopyExt for tempfile::TempDir { + fn copy_from(&self, source: P, patterns: &[S]) -> Result<(), failure::Error> + where + P: AsRef, + S: AsRef, + { + copy_from(self.path(), source.as_ref(), patterns) + } +} + +impl TempDirCopyExt for ChildPath { + fn copy_from(&self, source: P, patterns: &[S]) -> Result<(), failure::Error> + where + P: AsRef, + S: AsRef, + { + copy_from(self.path(), source.as_ref(), patterns) + } +} + +fn touch(path: &path::Path) -> io::Result<()> { + fs::File::create(path)?; + Ok(()) +} + +fn write_binary(path: &path::Path, data: &[u8]) -> io::Result<()> { + let mut file = fs::File::create(path)?; + file.write_all(data)?; + Ok(()) +} + +fn write_str(path: &path::Path, data: &str) -> io::Result<()> { + write_binary(path, data.as_bytes()) +} + +fn copy_from( + target: &path::Path, + source: &path::Path, + patterns: &[S], +) -> Result<(), failure::Error> +where + S: AsRef, +{ + for entry in globwalk::GlobWalker::from_patterns(patterns, source)?.follow_links(true) { + let entry = entry?; + let rel = entry + .path() + .strip_prefix(source) + .expect("entries to be under `source`"); + let target_path = target.join(rel); + if entry.file_type().is_dir() { + fs::create_dir_all(target_path)?; + } else if entry.file_type().is_file() { + fs::copy(entry.path(), target)?; + } + } + Ok(()) +}