1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
//! Used to create goldenfiles.

use std::env;
use std::fs;
use std::fs::File;
use std::io::{Error, ErrorKind, Result};
use std::path::{Path, PathBuf};
use std::thread;

use tempfile::TempDir;
use yansi::Paint;

use crate::differs::*;

/// A Mint creates goldenfiles.
///
/// When a Mint goes out of scope, it will do one of two things depending on the
/// value of the `UPDATE_GOLDENFILES` environment variable:
///
///   1. If `UPDATE_GOLDENFILES!=1`, it will check the new goldenfile
///      contents against their old contents, and panic if they differ.
///   2. If `UPDATE_GOLDENFILES=1`, it will replace the old goldenfile
///      contents with the newly written contents.
pub struct Mint {
    path: PathBuf,
    tempdir: TempDir,
    files: Vec<(PathBuf, Differ)>,
    create_empty: bool,
}

impl Mint {
    /// Create a new goldenfile Mint.
    fn new_internal<P: AsRef<Path>>(path: P, create_empty: bool) -> Self {
        let tempdir = TempDir::new().unwrap();
        let mint = Mint {
            path: path.as_ref().to_path_buf(),
            files: vec![],
            tempdir,
            create_empty,
        };
        fs::create_dir_all(&mint.path).unwrap_or_else(|err| {
            panic!(
                "Failed to create goldenfile directory {:?}: {:?}",
                mint.path, err
            )
        });
        mint
    }

    /// Create a new goldenfile Mint.
    pub fn new<P: AsRef<Path>>(path: P) -> Self {
        Self::new_internal(path, true)
    }

    /// Create a new goldenfile Mint. Goldenfiles will only be created when non-empty.
    pub fn new_nonempty<P: AsRef<Path>>(path: P) -> Self {
        Self::new_internal(path, false)
    }

    /// Create a new goldenfile using a differ inferred from the file extension.
    ///
    /// The returned File is a temporary file, not the goldenfile itself.
    pub fn new_goldenfile<P: AsRef<Path>>(&mut self, path: P) -> Result<File> {
        self.new_goldenfile_with_differ(&path, get_differ_for_path(&path))
    }

    /// Create a new goldenfile with the specified diff function.
    ///
    /// The returned File is a temporary file, not the goldenfile itself.
    pub fn new_goldenfile_with_differ<P: AsRef<Path>>(
        &mut self,
        path: P,
        differ: Differ,
    ) -> Result<File> {
        if path.as_ref().is_absolute() {
            return Err(Error::new(
                ErrorKind::InvalidInput,
                "Path must be relative.",
            ));
        }

        let abs_path = self.tempdir.path().to_path_buf().join(path.as_ref());
        if let Some(abs_parent) = abs_path.parent() {
            if abs_parent != self.tempdir.path() {
                fs::create_dir_all(abs_parent).unwrap_or_else(|err| {
                    panic!(
                        "Failed to create temporary subdirectory {:?}: {:?}",
                        abs_parent, err
                    )
                });
            }
        }
        let maybe_file = File::create(abs_path);
        if maybe_file.is_ok() {
            self.files.push((path.as_ref().to_path_buf(), differ));
        }
        maybe_file
    }

    /// Check new goldenfile contents against old, and panic if they differ.
    ///
    /// Called automatically when a Mint goes out of scope and
    /// `UPDATE_GOLDENFILES!=1`.
    pub fn check_goldenfiles(&self) {
        for (file, differ) in &self.files {
            let old = self.path.join(file);
            let new = self.tempdir.path().join(file);
            defer_on_unwind! {
                eprintln!("note: run with `UPDATE_GOLDENFILES=1` to update goldenfiles");
                eprintln!(
                    "{}: goldenfile changed: {}",
                    "error".bold().red(),
                    file.to_str().unwrap()
                );
            }
            differ(&old, &new);
        }
    }

    /// Overwrite old goldenfile contents with their new contents.
    ///
    /// Called automatically when a Mint goes out of scope and
    /// `UPDATE_GOLDENFILES=1`.
    pub fn update_goldenfiles(&self) {
        for (file, _) in &self.files {
            let old = self.path.join(file);
            let new = self.tempdir.path().join(file);

            let empty = File::open(&new).unwrap().metadata().unwrap().len() == 0;
            if self.create_empty || !empty {
                println!("Updating {:?}.", file.to_str().unwrap());
                fs::copy(&new, &old).unwrap_or_else(|err| {
                    panic!("Error copying {:?} to {:?}: {:?}", &new, &old, err)
                });
            } else if old.exists() {
                std::fs::remove_file(&old).unwrap();
            }
        }
    }
}

/// Get the diff function to use for a given file path.
pub fn get_differ_for_path<P: AsRef<Path>>(_path: P) -> Differ {
    match _path.as_ref().extension() {
        Some(os_str) => match os_str.to_str() {
            Some("bin") => Box::new(binary_diff),
            Some("exe") => Box::new(binary_diff),
            Some("gz") => Box::new(binary_diff),
            Some("tar") => Box::new(binary_diff),
            Some("zip") => Box::new(binary_diff),
            _ => Box::new(text_diff),
        },
        _ => Box::new(text_diff),
    }
}

impl Drop for Mint {
    /// Called when the mint goes out of scope to check or update goldenfiles.
    fn drop(&mut self) {
        if thread::panicking() {
            return;
        }
        // For backwards compatibility with 1.4 and below.
        let legacy_var = env::var("REGENERATE_GOLDENFILES");
        let update_var = env::var("UPDATE_GOLDENFILES");
        if (legacy_var.is_ok() && legacy_var.unwrap() == "1")
            || (update_var.is_ok() && update_var.unwrap() == "1")
        {
            self.update_goldenfiles();
        } else {
            self.check_goldenfiles();
        }
    }
}