1use serde::{Deserialize, Serialize};
4use std::fmt::{Debug, Display};
5use std::fs::OpenOptions;
6use std::io::{BufWriter, Write};
7use std::path::{Path, PathBuf};
8
9#[derive(Serialize, Deserialize, Debug, PartialEq)]
10#[serde(rename_all = "lowercase")]
11enum RegType {
12 Display,
13 Debug,
14}
15
16#[derive(Serialize, Deserialize, Debug)]
17struct RegEntry {
18 #[serde(rename = "type")]
19 reg_type: RegType,
20 message: String,
21}
22
23enum Mode {
25 Write,
28 Read,
31}
32
33pub struct RegTest {
55 file_path: PathBuf,
57 mode: Mode,
60 buffer: Vec<RegEntry>,
67 read_index: usize,
69}
70
71impl RegTest {
72 pub fn new<P: AsRef<Path>>(path: P) -> std::io::Result<Self> {
73 let file_path = path.as_ref().to_path_buf();
74
75 if file_path.exists() {
76 let file = OpenOptions::new().read(true).open(&file_path)?;
78
79 let mut reader = std::io::BufReader::new(file);
80
81 let buffer = match serde_json::from_reader(&mut reader) {
82 Ok(entries) => entries,
83 Err(e) => {
84 eprintln!(
85 "Failed to read regression test file {}: {}",
86 file_path.display(),
87 e
88 );
89 return Err(e.into());
90 }
91 };
92
93 Ok(RegTest {
94 file_path,
95 mode: Mode::Read,
96 buffer,
97 read_index: 0,
98 })
99 } else {
100 Ok(RegTest {
101 file_path,
102 mode: Mode::Write,
103 buffer: Vec::new(),
104 read_index: 0,
105 })
106 }
107 }
108
109 fn regtest_internal(&mut self, message: String, reg_type: RegType) {
110 match self.mode {
111 Mode::Write => {
112 self.buffer.push(RegEntry { reg_type, message });
113 }
114 Mode::Read => {
115 if self.read_index >= self.buffer.len() {
116 panic!("No more regression entries in file, but test expected more.");
117 }
118
119 let expected = &self.buffer[self.read_index];
120 self.read_index += 1;
121
122 if expected.reg_type != reg_type {
123 panic!(
124 "Regression data generated in different ways: expected {:?}, got {:?}",
125 expected.reg_type, reg_type
126 );
127 }
128
129 if expected.message != message {
130 panic!(
131 "Regression message mismatch:\nExpected: {}\nActual: {}\n\nDiff:\n{}",
132 expected.message,
133 message,
134 diff_lines(&expected.message, &message)
135 );
136 }
137 }
138 }
139 }
140
141 pub fn regtest<T: Display>(&mut self, value: T) {
142 self.regtest_internal(format!("{}", value), RegType::Display);
143 }
144
145 pub fn regtest_dbg<T: Debug>(&mut self, value: T) {
146 self.regtest_internal(format!("{:?}", value), RegType::Debug);
147 }
148}
149
150impl Drop for RegTest {
151 fn drop(&mut self) {
152 if let Mode::Write = self.mode {
153 if let Ok(file) = OpenOptions::new()
155 .write(true)
156 .create(true)
157 .truncate(true)
158 .open(&self.file_path)
159 {
160 let mut writer = BufWriter::new(file);
161 if serde_json::to_writer_pretty(&mut writer, &self.buffer).is_ok() {
162 let _ = writer.flush();
163 }
164 }
165 }
166 }
167}
168
169fn diff_lines(expected: &str, actual: &str) -> String {
170 let exp_lines: Vec<_> = expected.lines().collect();
171 let act_lines: Vec<_> = actual.lines().collect();
172 let max = exp_lines.len().max(act_lines.len());
173
174 let mut diff = String::new();
175 let mut minus_block = Vec::new();
176 let mut plus_block = Vec::new();
177
178 for i in 0..max {
179 let exp = exp_lines.get(i).unwrap_or(&"");
180 let act = act_lines.get(i).unwrap_or(&"");
181
182 if exp != act {
183 if !exp.is_empty() {
184 minus_block.push(exp);
185 }
186 if !act.is_empty() {
187 plus_block.push(act);
188 }
189 } else {
190 if !minus_block.is_empty() || !plus_block.is_empty() {
191 if !minus_block.is_empty() {
192 for line in &minus_block {
193 diff.push_str(&format!("- {}\n", line));
194 }
195 minus_block.clear();
196 }
197 if !plus_block.is_empty() {
198 for line in &plus_block {
199 diff.push_str(&format!("+ {}\n", line));
200 }
201 plus_block.clear();
202 }
203 } else {
204 diff.push_str(&format!(" {}\n", exp));
205 }
206 }
207 }
208
209 if !minus_block.is_empty() {
211 for line in &minus_block {
212 diff.push_str(&format!("- {}\n", line));
213 }
214 }
215 if !plus_block.is_empty() {
216 for line in &plus_block {
217 diff.push_str(&format!("+ {}\n", line));
218 }
219 }
220
221 diff
222}