Skip to content
This repository has been archived by the owner on Dec 29, 2021. It is now read-only.

Commit

Permalink
feat(tmpdir): Augment tempdir::TempDir
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
epage committed Mar 29, 2018
1 parent f7d6716 commit 9767301
Show file tree
Hide file tree
Showing 3 changed files with 280 additions and 0 deletions.
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,19 @@ build = "build.rs"
[[bin]]
name = "assert_fixture"

[features]
default = ["tempdir"]
tempdir = ["tempfile", "globwalk"]

[dependencies]
colored = "1.5"
difference = "2.0"
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"
Expand Down
5 changes: 5 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
269 changes: 269 additions & 0 deletions src/temp.rs
Original file line number Diff line number Diff line change
@@ -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<P>(&self, path: P) -> ChildPath
where
P: AsRef<path::Path>;
}

impl TempDirChildExt for tempfile::TempDir {
fn child<P>(&self, path: P) -> ChildPath
where
P: AsRef<path::Path>,
{
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<P>(path: P) -> Self
where
P: Into<path::PathBuf>,
{
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<S>(&self, program: S) -> process::Command
where
S: AsRef<ffi::OsStr>;
}

impl TempDirCommandExt for tempfile::TempDir {
fn command<S>(&self, program: S) -> process::Command
where
S: AsRef<ffi::OsStr>,
{
let mut cmd = process::Command::new(program);
cmd.current_dir(self.path());
cmd
}
}

impl TempDirCommandExt for ChildPath {
fn command<S>(&self, program: S) -> process::Command
where
S: AsRef<ffi::OsStr>,
{
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<P, S>(&self, source: P, patterns: &[S]) -> Result<(), failure::Error>
where
P: AsRef<path::Path>,
S: AsRef<str>;
}

impl TempDirCopyExt for tempfile::TempDir {
fn copy_from<P, S>(&self, source: P, patterns: &[S]) -> Result<(), failure::Error>
where
P: AsRef<path::Path>,
S: AsRef<str>,
{
copy_from(self.path(), source.as_ref(), patterns)
}
}

impl TempDirCopyExt for ChildPath {
fn copy_from<P, S>(&self, source: P, patterns: &[S]) -> Result<(), failure::Error>
where
P: AsRef<path::Path>,
S: AsRef<str>,
{
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<S>(
target: &path::Path,
source: &path::Path,
patterns: &[S],
) -> Result<(), failure::Error>
where
S: AsRef<str>,
{
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(())
}

0 comments on commit 9767301

Please sign in to comment.