gilder/
lib.rs

1use std::{
2    io::{BufRead, Seek, Write},
3    sync::Mutex,
4};
5
6#[macro_export]
7macro_rules! assert_golden {
8    ($actual:expr) => {
9        let path = $crate::__get_absolute_path(env!("CARGO_MANIFEST_DIR"), file!());
10        $crate::assert_impl(path, $actual)
11    };
12}
13
14pub fn assert_impl<T: ToString>(path: std::path::PathBuf, actual: T) {
15    let mode = get_mode();
16    let actual = actual.to_string();
17    let file_path = path.with_extension("rs.gld");
18
19    if mode == Mode::Create {
20        create_golden_file(&file_path, actual);
21    } else {
22        let Ok(file) = std::fs::File::open(&file_path) else {
23            if mode == Mode::Verify {
24                panic!("golden file not found");
25            } else {
26                // Turn on create mode
27                std::env::set_var(ENV_MODE, "create");
28                create_golden_file(&file_path, actual);
29                return;
30            }
31        };
32        if mode == Mode::Auto {
33            std::env::set_var(ENV_MODE, "verify");
34        }
35        let reader = std::io::BufReader::new(file);
36        let index = next_index(path);
37        let expect = reader
38            .lines()
39            .skip(2 + index)
40            .next()
41            .expect("End of golden file. If you want to add a new test case, please regenerate the golden file.")
42            .unwrap();
43        if expect != format!("{:?}", actual) {
44            panic!(
45                r#"assertion failed: The given value differs from the previous value (from the golden file).
46  actual: `{:?}`,
47  expect: `{}`"#,
48                actual, expect
49            )
50        }
51    }
52}
53
54#[derive(Debug, PartialEq, Eq)]
55enum Mode {
56    Create,
57    Verify,
58    Auto,
59}
60
61fn get_mode() -> Mode {
62    let mode = std::env::var(ENV_MODE).unwrap_or("auto".to_string());
63    match mode.as_str() {
64        "create" => Mode::Create,
65        "verify" => Mode::Verify,
66        "auto" => Mode::Auto,
67        _ => panic!("invalid mode"),
68    }
69}
70
71const ENV_MODE: &str = "GILDER_MODE";
72
73fn create_golden_file(path: &std::path::PathBuf, actual: String) {
74    let mut file = open_golden_file_for_create(&path).unwrap();
75    writeln!(file, "{:?}", actual).unwrap();
76}
77
78fn open_golden_file_for_create(path: &std::path::PathBuf) -> Result<std::fs::File, String> {
79    let id = get_id();
80    let file = std::fs::OpenOptions::new()
81        .read(true)
82        .write(true)
83        .create(true)
84        .open(path)
85        .map_err(|e| e.to_string())?;
86    let mut reader = std::io::BufReader::new(file);
87    let mut buf = String::new();
88    reader.read_line(&mut buf).map_err(|e| e.to_string())?;
89    if !buf.is_empty() {
90        assert!(buf.starts_with(GOLDEN_FILE_HEADER), "invalid golden file");
91        buf.clear();
92        reader.read_line(&mut buf).map_err(|e| e.to_string())?;
93        if buf.trim_end() == format!("{}", id) {
94            return Ok(reader.into_inner());
95        }
96    }
97
98    let mut file = reader.into_inner();
99    file.set_len(0).map_err(|e| e.to_string())?;
100    file.flush().map_err(|e| e.to_string())?;
101    file.seek(std::io::SeekFrom::Start(0))
102        .map_err(|e| e.to_string())?;
103
104    writeln!(file, "{}", GOLDEN_FILE_HEADER).map_err(|e| e.to_string())?;
105    writeln!(file, "{}", id).map_err(|e| e.to_string())?;
106    Ok(file)
107}
108
109const GOLDEN_FILE_HEADER: &str = "# This file is generated by gilder. Do not edit it manually.";
110
111/// Identifier for the current run.
112static ID: Mutex<Option<u128>> = Mutex::new(None);
113
114fn get_id() -> u128 {
115    let mut id = ID.lock().unwrap();
116    if id.is_none() {
117        id.replace(
118            std::time::SystemTime::now()
119                .duration_since(std::time::UNIX_EPOCH)
120                .unwrap()
121                .as_nanos(),
122        );
123    }
124    id.unwrap()
125}
126
127static COUNTERS: Mutex<Vec<(std::path::PathBuf, usize)>> = Mutex::new(Vec::new());
128
129fn next_index(name: std::path::PathBuf) -> usize {
130    let mut counters = COUNTERS.lock().unwrap();
131    if let Some((_, count)) = counters.iter_mut().find(|(n, _)| n == &name) {
132        *count += 1;
133        *count
134    } else {
135        counters.push((name, 0));
136        0
137    }
138}
139
140pub fn __get_absolute_path(manifest_dir: &str, file: &str) -> std::path::PathBuf {
141    let manifest_dir = std::path::Path::new(manifest_dir);
142    let file = std::path::Path::new(file);
143
144    let abs = manifest_dir.join(file);
145    if abs.exists() {
146        return abs;
147    }
148
149    // Search for the file in the parent directories
150    let mut dir = manifest_dir;
151    loop {
152        dir = match dir.parent() {
153            Some(dir) => dir,
154            None => panic!("not found: {}", file.display()),
155        };
156        let abs = dir.join(file);
157        if abs.starts_with(manifest_dir) && abs.exists() {
158            return abs;
159        }
160    }
161}