assert_fs/
assert.rs

1//! Filesystem assertions.
2//!
3//! See [`PathAssert`].
4//!
5//! # Examples
6//!
7//! ```rust
8//! use assert_fs::prelude::*;
9//! use predicates::prelude::*;
10//!
11//! let temp = assert_fs::TempDir::new().unwrap();
12//! let input_file = temp.child("foo.txt");
13//! input_file.touch().unwrap();
14//!
15//! // ... do something with input_file ...
16//!
17//! input_file.assert("");
18//! temp.child("bar.txt").assert(predicate::path::missing());
19//!
20//! temp.close().unwrap();
21//! ```
22//!
23
24use std::fmt;
25use std::path;
26
27#[cfg(feature = "color")]
28use anstream::panic;
29use predicates::path::PredicateFileContentExt;
30use predicates::str::PredicateStrExt;
31use predicates_tree::CaseTreeExt;
32
33use crate::fixture;
34
35/// Assert the state of files within [`TempDir`].
36///
37/// This uses [`IntoPathPredicate`] to provide short-hands for common cases, accepting:
38/// - `Predicate<Path>` for validating a path.
39/// - `Predicate<str>` for validating the content of the file.
40/// - `&[u8]` or `&str` representing the content of the file.
41///
42/// See [`predicates`] for more predicates.
43///
44/// # Examples
45///
46/// ```rust
47/// use assert_fs::prelude::*;
48/// use predicates::prelude::*;
49///
50/// let temp = assert_fs::TempDir::new().unwrap();
51/// let input_file = temp.child("foo.txt");
52/// input_file.touch().unwrap();
53///
54/// // ... do something with input_file ...
55///
56/// input_file.assert("");
57/// temp.child("bar.txt").assert(predicate::path::missing());
58///
59/// temp.close().unwrap();
60/// ```
61///
62/// [`TempDir`]: super::TempDir
63pub trait PathAssert {
64    /// Assert the state of files within [`TempDir`].
65    ///
66    /// This uses [`IntoPathPredicate`] to provide short-hands for common cases, accepting:
67    /// - `Predicate<Path>` for validating a path.
68    /// - `Predicate<str>` for validating the content of the file.
69    /// - `&[u8]` or `&str` representing the content of the file.
70    ///
71    /// See [`predicates`] for more predicates.
72    ///
73    /// # Panic
74    ///
75    /// Will panic if the condition is not satisfied
76    ///
77    /// # Examples
78    ///
79    /// ```rust
80    /// use assert_fs::prelude::*;
81    /// use predicates::prelude::*;
82    ///
83    /// let temp = assert_fs::TempDir::new().unwrap();
84    /// let input_file = temp.child("foo.txt");
85    /// input_file.touch().unwrap();
86    ///
87    /// // ... do something with input_file ...
88    ///
89    /// input_file.assert("");
90    /// // or
91    /// input_file.assert(predicate::str::is_empty());
92    ///
93    /// temp.child("bar.txt").assert(predicate::path::missing());
94    ///
95    /// temp.close().unwrap();
96    /// ```
97    ///
98    /// [`TempDir`]: super::TempDir
99    #[track_caller]
100    fn assert<I, P>(&self, pred: I) -> &Self
101    where
102        I: IntoPathPredicate<P>,
103        P: predicates_core::Predicate<path::Path>;
104}
105
106impl PathAssert for fixture::TempDir {
107    #[track_caller]
108    fn assert<I, P>(&self, pred: I) -> &Self
109    where
110        I: IntoPathPredicate<P>,
111        P: predicates_core::Predicate<path::Path>,
112    {
113        assert(self.path(), pred);
114        self
115    }
116}
117
118impl PathAssert for fixture::NamedTempFile {
119    #[track_caller]
120    fn assert<I, P>(&self, pred: I) -> &Self
121    where
122        I: IntoPathPredicate<P>,
123        P: predicates_core::Predicate<path::Path>,
124    {
125        assert(self.path(), pred);
126        self
127    }
128}
129
130impl PathAssert for fixture::ChildPath {
131    #[track_caller]
132    fn assert<I, P>(&self, pred: I) -> &Self
133    where
134        I: IntoPathPredicate<P>,
135        P: predicates_core::Predicate<path::Path>,
136    {
137        assert(self.path(), pred);
138        self
139    }
140}
141
142#[track_caller]
143fn assert<I, P>(path: &path::Path, pred: I)
144where
145    I: IntoPathPredicate<P>,
146    P: predicates_core::Predicate<path::Path>,
147{
148    let pred = pred.into_path();
149    if let Some(case) = pred.find_case(false, path) {
150        let palette = crate::Palette::color();
151        panic!(
152            "Unexpected file, failed {:#}\n{:#}={:#}",
153            case.tree(),
154            palette.key("path"),
155            palette.value(path.display())
156        );
157    }
158}
159
160/// Used by [`PathAssert`] to convert Self into the needed [`predicates_core::Predicate<Path>`].
161///
162/// # Examples
163///
164/// ```rust
165/// use std::path;
166///
167/// use assert_fs::prelude::*;
168/// use predicates::prelude::*;
169///
170/// let temp = assert_fs::TempDir::new().unwrap();
171///
172/// // ... do something with input_file ...
173///
174/// temp.child("bar.txt").assert(predicate::path::missing()); // Uses IntoPathPredicate
175///
176/// temp.close().unwrap();
177/// ```
178pub trait IntoPathPredicate<P>
179where
180    P: predicates_core::Predicate<path::Path>,
181{
182    /// The type of the predicate being returned.
183    type Predicate;
184
185    /// Convert to a predicate for testing a path.
186    fn into_path(self) -> P;
187}
188
189impl<P> IntoPathPredicate<P> for P
190where
191    P: predicates_core::Predicate<path::Path>,
192{
193    type Predicate = P;
194
195    fn into_path(self) -> Self::Predicate {
196        self
197    }
198}
199
200/// Keep `predicates` concrete Predicates out of our public API.
201/// [`predicates_core::Predicate`] used by [`IntoPathPredicate`] for bytes.
202///
203/// # Example
204///
205/// ```rust
206/// use assert_fs::prelude::*;
207///
208/// let temp = assert_fs::TempDir::new().unwrap();
209/// let input_file = temp.child("foo.txt");
210/// input_file.touch().unwrap();
211///
212/// // ... do something with input_file ...
213///
214/// input_file.assert(b"" as &[u8]); // uses BytesContentPathPredicate
215///
216/// temp.close().unwrap();
217/// ```
218#[derive(Debug)]
219pub struct BytesContentPathPredicate(
220    predicates::path::FileContentPredicate<predicates::ord::EqPredicate<&'static [u8]>>,
221);
222
223impl BytesContentPathPredicate {
224    pub(crate) fn new(value: &'static [u8]) -> Self {
225        let pred = predicates::ord::eq(value).from_file_path();
226        BytesContentPathPredicate(pred)
227    }
228}
229
230impl predicates_core::reflection::PredicateReflection for BytesContentPathPredicate {
231    fn parameters<'a>(
232        &'a self,
233    ) -> Box<dyn Iterator<Item = predicates_core::reflection::Parameter<'a>> + 'a> {
234        self.0.parameters()
235    }
236
237    /// Nested `Predicate`s of the current `Predicate`.
238    fn children<'a>(
239        &'a self,
240    ) -> Box<dyn Iterator<Item = predicates_core::reflection::Child<'a>> + 'a> {
241        self.0.children()
242    }
243}
244
245impl predicates_core::Predicate<path::Path> for BytesContentPathPredicate {
246    fn eval(&self, item: &path::Path) -> bool {
247        self.0.eval(item)
248    }
249
250    fn find_case<'a>(
251        &'a self,
252        expected: bool,
253        variable: &path::Path,
254    ) -> Option<predicates_core::reflection::Case<'a>> {
255        self.0.find_case(expected, variable)
256    }
257}
258
259impl fmt::Display for BytesContentPathPredicate {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        self.0.fmt(f)
262    }
263}
264
265impl IntoPathPredicate<BytesContentPathPredicate> for &'static [u8] {
266    type Predicate = BytesContentPathPredicate;
267
268    fn into_path(self) -> Self::Predicate {
269        Self::Predicate::new(self)
270    }
271}
272
273/// Keep `predicates` concrete Predicates out of our public API.
274/// [`predicates_core::Predicate`] used by `IntoPathPredicate` for `str`.
275///
276/// # Example
277///
278/// ```rust
279/// use assert_fs::prelude::*;
280///
281/// let temp = assert_fs::TempDir::new().unwrap();
282/// let input_file = temp.child("foo.txt");
283/// input_file.touch().unwrap();
284///
285/// // ... do something with input_file ...
286///
287/// input_file.assert(""); // Uses StrContentPathPredicate
288///
289/// temp.close().unwrap();
290/// ```
291#[derive(Debug, Clone)]
292pub struct StrContentPathPredicate(
293    predicates::path::FileContentPredicate<
294        predicates::str::Utf8Predicate<predicates::str::DifferencePredicate>,
295    >,
296);
297
298impl StrContentPathPredicate {
299    pub(crate) fn new(value: String) -> Self {
300        let pred = predicates::str::diff(value).from_utf8().from_file_path();
301        StrContentPathPredicate(pred)
302    }
303}
304
305impl predicates_core::reflection::PredicateReflection for StrContentPathPredicate {
306    fn parameters<'a>(
307        &'a self,
308    ) -> Box<dyn Iterator<Item = predicates_core::reflection::Parameter<'a>> + 'a> {
309        self.0.parameters()
310    }
311
312    /// Nested `Predicate`s of the current `Predicate`.
313    fn children<'a>(
314        &'a self,
315    ) -> Box<dyn Iterator<Item = predicates_core::reflection::Child<'a>> + 'a> {
316        self.0.children()
317    }
318}
319
320impl predicates_core::Predicate<path::Path> for StrContentPathPredicate {
321    fn eval(&self, item: &path::Path) -> bool {
322        self.0.eval(item)
323    }
324
325    fn find_case<'a>(
326        &'a self,
327        expected: bool,
328        variable: &path::Path,
329    ) -> Option<predicates_core::reflection::Case<'a>> {
330        self.0.find_case(expected, variable)
331    }
332}
333
334impl fmt::Display for StrContentPathPredicate {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        self.0.fmt(f)
337    }
338}
339
340impl IntoPathPredicate<StrContentPathPredicate> for String {
341    type Predicate = StrContentPathPredicate;
342
343    fn into_path(self) -> Self::Predicate {
344        Self::Predicate::new(self)
345    }
346}
347
348impl IntoPathPredicate<StrContentPathPredicate> for &str {
349    type Predicate = StrContentPathPredicate;
350
351    fn into_path(self) -> Self::Predicate {
352        Self::Predicate::new(self.to_owned())
353    }
354}
355
356impl IntoPathPredicate<StrContentPathPredicate> for &String {
357    type Predicate = StrContentPathPredicate;
358
359    fn into_path(self) -> Self::Predicate {
360        Self::Predicate::new(self.to_owned())
361    }
362}
363
364/// Keep `predicates` concrete Predicates out of our public API.
365/// [`predicates_core::Predicate`] used by `IntoPathPredicate` for `str` predicates.
366///
367/// # Example
368///
369/// ```rust
370/// use assert_fs::prelude::*;
371/// use predicates::prelude::*;
372///
373/// let temp = assert_fs::TempDir::new().unwrap();
374/// let input_file = temp.child("foo.txt");
375/// input_file.touch().unwrap();
376///
377/// // ... do something with input_file ...
378///
379/// input_file.assert(predicate::str::is_empty()); // Uses StrPathPredicate
380///
381/// temp.close().unwrap();
382/// ```
383#[derive(Debug, Clone)]
384pub struct StrPathPredicate<P: predicates_core::Predicate<str>>(
385    predicates::path::FileContentPredicate<predicates::str::Utf8Predicate<P>>,
386);
387
388impl<P> StrPathPredicate<P>
389where
390    P: predicates_core::Predicate<str>,
391{
392    pub(crate) fn new(value: P) -> Self {
393        let pred = value.from_utf8().from_file_path();
394        StrPathPredicate(pred)
395    }
396}
397
398impl<P> predicates_core::reflection::PredicateReflection for StrPathPredicate<P>
399where
400    P: predicates_core::Predicate<str>,
401{
402    fn parameters<'a>(
403        &'a self,
404    ) -> Box<dyn Iterator<Item = predicates_core::reflection::Parameter<'a>> + 'a> {
405        self.0.parameters()
406    }
407
408    /// Nested `Predicate`s of the current `Predicate`.
409    fn children<'a>(
410        &'a self,
411    ) -> Box<dyn Iterator<Item = predicates_core::reflection::Child<'a>> + 'a> {
412        self.0.children()
413    }
414}
415
416impl<P> predicates_core::Predicate<path::Path> for StrPathPredicate<P>
417where
418    P: predicates_core::Predicate<str>,
419{
420    fn eval(&self, item: &path::Path) -> bool {
421        self.0.eval(item)
422    }
423
424    fn find_case<'a>(
425        &'a self,
426        expected: bool,
427        variable: &path::Path,
428    ) -> Option<predicates_core::reflection::Case<'a>> {
429        self.0.find_case(expected, variable)
430    }
431}
432
433impl<P> fmt::Display for StrPathPredicate<P>
434where
435    P: predicates_core::Predicate<str>,
436{
437    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
438        self.0.fmt(f)
439    }
440}
441
442impl<P> IntoPathPredicate<StrPathPredicate<P>> for P
443where
444    P: predicates_core::Predicate<str>,
445{
446    type Predicate = StrPathPredicate<P>;
447
448    fn into_path(self) -> Self::Predicate {
449        Self::Predicate::new(self)
450    }
451}
452
453#[cfg(test)]
454mod test {
455    use super::*;
456
457    use predicates::prelude::*;
458
459    // Since IntoPathPredicate exists solely for conversion, test it under that scenario to ensure
460    // it works as expected.
461    fn convert_path<I, P>(pred: I) -> P
462    where
463        I: IntoPathPredicate<P>,
464        P: Predicate<path::Path>,
465    {
466        pred.into_path()
467    }
468
469    #[test]
470    fn into_path_from_pred() {
471        let pred = convert_path(predicate::eq(path::Path::new("hello.md")));
472        let case = pred.find_case(false, path::Path::new("hello.md"));
473        println!("Failing case: {case:?}");
474        assert!(case.is_none());
475    }
476
477    #[test]
478    fn into_path_from_bytes() {
479        let pred = convert_path(b"hello\n" as &[u8]);
480        let case = pred.find_case(false, path::Path::new("tests/fixture/hello.txt"));
481        println!("Failing case: {case:?}");
482        assert!(case.is_none());
483    }
484
485    #[test]
486    fn into_path_from_str() {
487        let pred = convert_path("hello\n");
488        let case = pred.find_case(false, path::Path::new("tests/fixture/hello.txt"));
489        println!("Failing case: {case:?}");
490        assert!(case.is_none());
491    }
492}