camino_tempfile_ext/
assert.rs

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