Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dir): Allow in-source dir fixtures #373

Merged
merged 2 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading