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 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
111static 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 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}