Skip to content

Commit

Permalink
Merge pull request #373 from epage/dir
Browse files Browse the repository at this point in the history
feat(dir): Allow in-source dir fixtures
  • Loading branch information
epage authored Dec 16, 2024
2 parents 0099305 + 99dc7df commit a59d285
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 10 deletions.
114 changes: 114 additions & 0 deletions crates/snapbox/src/dir/fixture.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/// Collection of files
pub trait DirFixture: std::fmt::Debug {
/// Initialize a test fixture directory `root`
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error>;
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for std::path::Path {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
super::copy_template(self, root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for &'_ std::path::Path {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for &'_ std::path::PathBuf {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for std::path::PathBuf {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for str {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for &'_ str {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for &'_ String {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

#[cfg(feature = "dir")] // for documentation purposes only
impl DirFixture for String {
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
std::path::Path::new(self).write_to_path(root)
}
}

impl<P, S> DirFixture for &[(P, S)]
where
P: AsRef<std::path::Path>,
P: std::fmt::Debug,
S: AsRef<[u8]>,
S: std::fmt::Debug,
{
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
let root = super::ops::canonicalize(root)
.map_err(|e| format!("Failed to canonicalize {}: {}", root.display(), e))?;

for (path, content) in self.iter() {
let rel_path = path.as_ref();
let path = root.join(rel_path);
let path = super::ops::normalize_path(&path);
if !path.starts_with(&root) {
return Err(crate::assert::Error::new(format!(
"Fixture {} is for outside of the target root",
rel_path.display(),
)));
}

let content = content.as_ref();

if let Some(dir) = path.parent() {
std::fs::create_dir_all(dir).map_err(|e| {
format!(
"Failed to create fixture directory {}: {}",
dir.display(),
e
)
})?;
}
std::fs::write(&path, content)
.map_err(|e| format!("Failed to write fixture {}: {}", path.display(), e))?;
}
Ok(())
}
}

impl<const N: usize, P, S> DirFixture for [(P, S); N]
where
P: AsRef<std::path::Path>,
P: std::fmt::Debug,
S: AsRef<[u8]>,
S: std::fmt::Debug,
{
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
let s: &[(P, S)] = self;
s.write_to_path(root)
}
}
2 changes: 2 additions & 0 deletions crates/snapbox/src/dir/mod.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
//! Initialize working directories and assert on how they've changed
mod diff;
mod fixture;
mod ops;
mod root;
#[cfg(test)]
mod tests;

pub use diff::FileType;
pub use diff::PathDiff;
pub use fixture::DirFixture;
#[cfg(feature = "dir")]
pub use ops::copy_template;
pub use ops::resolve_dir;
Expand Down
44 changes: 44 additions & 0 deletions crates/snapbox/src/dir/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,50 @@ pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path {
path.components().as_path()
}

/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
use std::path::Component;

let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
std::path::PathBuf::from(c.as_os_str())
} else {
std::path::PathBuf::new()
};

for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(Component::RootDir);
}
Component::CurDir => {}
Component::ParentDir => {
if ret.ends_with(Component::ParentDir) {
ret.push(Component::ParentDir);
} else {
let popped = ret.pop();
if !popped && !ret.has_root() {
ret.push(Component::ParentDir);
}
}
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}

pub(crate) fn display_relpath(path: impl AsRef<std::path::Path>) -> String {
let path = path.as_ref();
let relpath = if let Ok(cwd) = std::env::current_dir() {
Expand Down
16 changes: 6 additions & 10 deletions crates/snapbox/src/dir/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -43,21 +43,17 @@ impl DirRoot {
}

#[cfg(feature = "dir")]
pub fn with_template(
self,
template_root: &std::path::Path,
) -> Result<Self, crate::assert::Error> {
pub fn with_template<F>(self, template: &F) -> Result<Self, crate::assert::Error>
where
F: crate::dir::DirFixture + ?Sized,
{
match &self.0 {
DirRootInner::None | DirRootInner::Immutable(_) => {
return Err("Sandboxing is disabled".into());
}
DirRootInner::MutablePath(path) | DirRootInner::MutableTemp { path, .. } => {
crate::debug!(
"Initializing {} from {}",
path.display(),
template_root.display()
);
super::copy_template(template_root, path)?;
crate::debug!("Initializing {} from {:?}", path.display(), template);
template.write_to_path(path)?;
}
}

Expand Down

0 comments on commit a59d285

Please sign in to comment.