exec_diff/
exec.rs

1use super::Diff;
2use command_extra::CommandExtra;
3use derive_more::{Display, Error};
4use pipe_trait::Pipe;
5use std::{
6    fs, io,
7    process::{Command, Output},
8};
9use tempfile::tempdir;
10
11/// Error returned by [`Diff::exec`].
12#[derive(Debug, Display, Error)]
13pub enum DiffExecError {
14    #[display(fmt = "Failed to create temporary directory: {_0}")]
15    Workspace(io::Error),
16    #[display(fmt = "Failed to create temporary file the left value: {_0}")]
17    Left(io::Error),
18    #[display(fmt = "Failed to create temporary file the right value: {_0}")]
19    Right(io::Error),
20    #[display(fmt = "Failed to execute the 'diff' command: {_0}")]
21    Exec(io::Error),
22    #[display(fmt = "The 'diff' process exits with code {code:?}")]
23    Status { code: Option<i32>, stderr: Vec<u8> },
24}
25
26impl<Left, Right> Diff<Left, Right>
27where
28    Left: AsRef<str>,
29    Right: AsRef<str>,
30{
31    /// Use the GNU `diff` command to compare two strings.
32    ///
33    /// **Return:**
34    /// * `Err(error)` means that the `diff` command was failed to executed or status > 1
35    /// * `Ok(None)` means that no diff was found (status = 0).
36    /// * `Ok(Some(diff))` means that there was diff (status = 1).
37    pub fn exec(&self) -> Result<Option<String>, DiffExecError> {
38        let Diff {
39            left,
40            right,
41            color,
42            unified,
43        } = self;
44
45        let workspace = tempdir().map_err(DiffExecError::Workspace)?;
46
47        let write_file = |name: &str, text: &str| fs::write(workspace.path().join(name), text);
48        write_file("left", left.as_ref()).map_err(DiffExecError::Left)?;
49        write_file("right", right.as_ref()).map_err(DiffExecError::Right)?;
50
51        let output = Command::new("diff")
52            .with_current_dir(&workspace)
53            .with_args(color.as_flag())
54            .with_args(unified.then_some("--unified"))
55            .with_arg("--label=left")
56            .with_arg("--label=right")
57            .with_arg("left")
58            .with_arg("right")
59            .output()
60            .map_err(DiffExecError::Exec)?;
61        let Output {
62            status,
63            stdout,
64            stderr,
65        } = output;
66
67        if status.success() {
68            return Ok(None);
69        }
70
71        if status.code() == Some(1) {
72            return stdout
73                .pipe(String::from_utf8)
74                .expect("The stdout of the diff command should be valid UTF-8")
75                .pipe(Some)
76                .pipe(Ok);
77        }
78
79        Err(DiffExecError::Status {
80            code: status.code(),
81            stderr,
82        })
83    }
84
85    /// Assert that two strings are equal.
86    ///
87    /// It will fallback to [`pretty_assertions`] if `diff` is failed to execute.
88    pub fn assert_eq(&self) {
89        match self.exec() {
90            Ok(None) => { /* assertion passed, do nothing */ }
91            Ok(Some(diff)) => panic!("assertion failed: `(left == right)`\n{diff}"),
92            Err(_) => pretty_assertions::assert_str_eq!(self.left.as_ref(), self.right.as_ref()),
93        }
94    }
95}