Skip to content

Commit a59d285

Browse files
authored
Merge pull request #373 from epage/dir
feat(dir): Allow in-source dir fixtures
2 parents 0099305 + 99dc7df commit a59d285

File tree

4 files changed

+166
-10
lines changed

4 files changed

+166
-10
lines changed

crates/snapbox/src/dir/fixture.rs

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
/// Collection of files
2+
pub trait DirFixture: std::fmt::Debug {
3+
/// Initialize a test fixture directory `root`
4+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error>;
5+
}
6+
7+
#[cfg(feature = "dir")] // for documentation purposes only
8+
impl DirFixture for std::path::Path {
9+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
10+
super::copy_template(self, root)
11+
}
12+
}
13+
14+
#[cfg(feature = "dir")] // for documentation purposes only
15+
impl DirFixture for &'_ std::path::Path {
16+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
17+
std::path::Path::new(self).write_to_path(root)
18+
}
19+
}
20+
21+
#[cfg(feature = "dir")] // for documentation purposes only
22+
impl DirFixture for &'_ std::path::PathBuf {
23+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
24+
std::path::Path::new(self).write_to_path(root)
25+
}
26+
}
27+
28+
#[cfg(feature = "dir")] // for documentation purposes only
29+
impl DirFixture for std::path::PathBuf {
30+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
31+
std::path::Path::new(self).write_to_path(root)
32+
}
33+
}
34+
35+
#[cfg(feature = "dir")] // for documentation purposes only
36+
impl DirFixture for str {
37+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
38+
std::path::Path::new(self).write_to_path(root)
39+
}
40+
}
41+
42+
#[cfg(feature = "dir")] // for documentation purposes only
43+
impl DirFixture for &'_ str {
44+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
45+
std::path::Path::new(self).write_to_path(root)
46+
}
47+
}
48+
49+
#[cfg(feature = "dir")] // for documentation purposes only
50+
impl DirFixture for &'_ String {
51+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
52+
std::path::Path::new(self).write_to_path(root)
53+
}
54+
}
55+
56+
#[cfg(feature = "dir")] // for documentation purposes only
57+
impl DirFixture for String {
58+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
59+
std::path::Path::new(self).write_to_path(root)
60+
}
61+
}
62+
63+
impl<P, S> DirFixture for &[(P, S)]
64+
where
65+
P: AsRef<std::path::Path>,
66+
P: std::fmt::Debug,
67+
S: AsRef<[u8]>,
68+
S: std::fmt::Debug,
69+
{
70+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
71+
let root = super::ops::canonicalize(root)
72+
.map_err(|e| format!("Failed to canonicalize {}: {}", root.display(), e))?;
73+
74+
for (path, content) in self.iter() {
75+
let rel_path = path.as_ref();
76+
let path = root.join(rel_path);
77+
let path = super::ops::normalize_path(&path);
78+
if !path.starts_with(&root) {
79+
return Err(crate::assert::Error::new(format!(
80+
"Fixture {} is for outside of the target root",
81+
rel_path.display(),
82+
)));
83+
}
84+
85+
let content = content.as_ref();
86+
87+
if let Some(dir) = path.parent() {
88+
std::fs::create_dir_all(dir).map_err(|e| {
89+
format!(
90+
"Failed to create fixture directory {}: {}",
91+
dir.display(),
92+
e
93+
)
94+
})?;
95+
}
96+
std::fs::write(&path, content)
97+
.map_err(|e| format!("Failed to write fixture {}: {}", path.display(), e))?;
98+
}
99+
Ok(())
100+
}
101+
}
102+
103+
impl<const N: usize, P, S> DirFixture for [(P, S); N]
104+
where
105+
P: AsRef<std::path::Path>,
106+
P: std::fmt::Debug,
107+
S: AsRef<[u8]>,
108+
S: std::fmt::Debug,
109+
{
110+
fn write_to_path(&self, root: &std::path::Path) -> Result<(), crate::assert::Error> {
111+
let s: &[(P, S)] = self;
112+
s.write_to_path(root)
113+
}
114+
}

crates/snapbox/src/dir/mod.rs

+2
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
//! Initialize working directories and assert on how they've changed
22
33
mod diff;
4+
mod fixture;
45
mod ops;
56
mod root;
67
#[cfg(test)]
78
mod tests;
89

910
pub use diff::FileType;
1011
pub use diff::PathDiff;
12+
pub use fixture::DirFixture;
1113
#[cfg(feature = "dir")]
1214
pub use ops::copy_template;
1315
pub use ops::resolve_dir;

crates/snapbox/src/dir/ops.rs

+44
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,50 @@ pub fn strip_trailing_slash(path: &std::path::Path) -> &std::path::Path {
172172
path.components().as_path()
173173
}
174174

175+
/// Normalize a path, removing things like `.` and `..`.
176+
///
177+
/// CAUTION: This does not resolve symlinks (unlike
178+
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
179+
/// behavior at times. This should be used carefully. Unfortunately,
180+
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
181+
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
182+
/// needs to improve on.
183+
pub(crate) fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
184+
use std::path::Component;
185+
186+
let mut components = path.components().peekable();
187+
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
188+
components.next();
189+
std::path::PathBuf::from(c.as_os_str())
190+
} else {
191+
std::path::PathBuf::new()
192+
};
193+
194+
for component in components {
195+
match component {
196+
Component::Prefix(..) => unreachable!(),
197+
Component::RootDir => {
198+
ret.push(Component::RootDir);
199+
}
200+
Component::CurDir => {}
201+
Component::ParentDir => {
202+
if ret.ends_with(Component::ParentDir) {
203+
ret.push(Component::ParentDir);
204+
} else {
205+
let popped = ret.pop();
206+
if !popped && !ret.has_root() {
207+
ret.push(Component::ParentDir);
208+
}
209+
}
210+
}
211+
Component::Normal(c) => {
212+
ret.push(c);
213+
}
214+
}
215+
}
216+
ret
217+
}
218+
175219
pub(crate) fn display_relpath(path: impl AsRef<std::path::Path>) -> String {
176220
let path = path.as_ref();
177221
let relpath = if let Ok(cwd) = std::env::current_dir() {

crates/snapbox/src/dir/root.rs

+6-10
Original file line numberDiff line numberDiff line change
@@ -43,21 +43,17 @@ impl DirRoot {
4343
}
4444

4545
#[cfg(feature = "dir")]
46-
pub fn with_template(
47-
self,
48-
template_root: &std::path::Path,
49-
) -> Result<Self, crate::assert::Error> {
46+
pub fn with_template<F>(self, template: &F) -> Result<Self, crate::assert::Error>
47+
where
48+
F: crate::dir::DirFixture + ?Sized,
49+
{
5050
match &self.0 {
5151
DirRootInner::None | DirRootInner::Immutable(_) => {
5252
return Err("Sandboxing is disabled".into());
5353
}
5454
DirRootInner::MutablePath(path) | DirRootInner::MutableTemp { path, .. } => {
55-
crate::debug!(
56-
"Initializing {} from {}",
57-
path.display(),
58-
template_root.display()
59-
);
60-
super::copy_template(template_root, path)?;
55+
crate::debug!("Initializing {} from {:?}", path.display(), template);
56+
template.write_to_path(path)?;
6157
}
6258
}
6359

0 commit comments

Comments
 (0)