1#![feature(array_windows, decl_macro)]
2
3use std::{
4 env,
5 path::{Path, PathBuf},
6};
7
8mod diff;
9
10pub fn function_name<T>(_: T) -> String {
11 std::any::type_name::<T>().replace("::", "-")
12}
13
14struct Config {
15 cahce_dir: PathBuf,
16 write_changes: bool,
17}
18
19impl Config {
20 fn new() -> Self {
21 Self {
22 cahce_dir: env::var("PRINT_TEST_CACHE_DIR")
23 .map(PathBuf::from)
24 .unwrap_or_else(|_| PathBuf::from("print-test-cache")),
25 write_changes: env::var("PRINT_TEST_WRITE_CHANGES")
26 .map(|s| s == "1")
27 .unwrap_or(false),
28 }
29 }
30}
31
32fn get_config() -> &'static Config {
33 static CONFIG: std::sync::OnceLock<Config> = std::sync::OnceLock::new();
34 CONFIG.get_or_init(|| Config::new())
35}
36
37pub fn case(name: &str, body: impl FnOnce(&mut String)) {
38 struct Case<'a> {
39 name: &'a str,
40 buffer: String,
41 }
42
43 fn save_result(name: &str, source: &str) {
44 use std::fmt::Write;
45
46 struct Dump {
47 buffer: String,
48 }
49
50 impl Drop for Dump {
51 fn drop(&mut self) {
52 if self.buffer.is_empty() {
53 return;
54 }
55 eprintln!("{}", self.buffer);
56 }
57 }
58
59 let mut dump = Dump {
60 buffer: String::new(),
61 };
62
63 macro log($dump:ident, $($arg:tt)*) {
64 writeln!($dump.buffer, "print-test: {}", format_args!($($arg)*)).unwrap();
65 }
66
67 fn usage_hint(dump: &mut Dump) {
68 log!(dump, "to save, run with 'PRINT_TEST_WRITE_CHANGES=1'");
69 }
70
71 let config = get_config();
72
73 let path = config.cahce_dir.join(name);
74
75 fn write_file(dump: &mut Dump, path: &Path, source: &str, should_run: bool) -> bool {
76 if !should_run {
77 usage_hint(dump);
78 return true;
79 }
80
81 if let Err(e) = std::fs::create_dir_all(path.parent().unwrap_or(Path::new("."))) {
82 log!(dump, "failed to create directory: {}", e);
83 return false;
84 }
85 if let Err(e) = std::fs::write(&path, source) {
86 log!(dump, "failed to write to file: {}", e);
87 return false;
88 }
89
90 true
91 }
92
93 if !path.exists() {
94 if !write_file(&mut dump, &path, source, config.write_changes) {
95 return;
96 }
97
98 log!(dump, "no previous result found, current form:");
99
100 for line in source.lines() {
101 log!(dump, " {}", line);
102 }
103
104 if !std::thread::panicking() && !config.write_changes {
105 panic!("new test case detected");
106 }
107
108 return;
109 }
110
111 let prev = match std::fs::read_to_string(&path) {
112 Ok(prev) => prev,
113 Err(e) => {
114 log!(dump, "failed to read from file: {}", e);
115 return;
116 }
117 };
118
119 let diff = diff::lines(&prev, &source);
120
121 if diff.iter().all(|d| matches!(d, diff::Result::Both(..))) {
122 return;
123 }
124
125 log!(dump, "changes detected for test '{}':", name);
126 for line in diff.iter() {
127 let ansi_term = "\u{001b}[0m";
128 match line {
129 diff::Result::Both(line, ..) => {
130 log!(dump, " {}", line);
131 }
132 diff::Result::Left(line) => {
133 let ansi_red = "\u{001b}[31m";
134 log!(dump, "{ansi_red}- {line}{ansi_term}");
135 }
136 diff::Result::Right(line) => {
137 let ansi_green = "\u{001b}[32m";
138 log!(dump, "{ansi_green}+ {line}{ansi_term}");
139 }
140 }
141 }
142
143 write_file(&mut dump, &path, source, config.write_changes);
144
145 if !std::thread::panicking() && !config.write_changes {
146 panic!("test '{}' failed", name);
147 }
148 }
149
150 let mut case = Case {
151 name,
152 buffer: String::new(),
153 };
154
155 body(&mut case.buffer);
156
157 impl Drop for Case<'_> {
158 fn drop(&mut self) {
159 if std::thread::panicking() {
160 self.buffer.push_str("\n\n");
161 self.buffer
162 .push_str("Panic occurred during test execution.");
163 }
164
165 save_result(self.name, &self.buffer);
166 }
167 }
168}
169
170#[macro_export]
171macro_rules! cases {
172 ($(fn $name:ident($ctx:ident) $body:block)*) => {$(
173 #[test]
174 fn $name() {
175 let fn_name = $crate::function_name($name);
176 let test_fn = |$ctx: &mut String| $body;
177 $crate::case(&fn_name, test_fn);
178 }
179 )*};
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185
186 cases! {
187 fn test1(ctx) {
188 ctx.push_str("test1");
189 }
190
191 fn test2(ctx) {
192 ctx.push_str("test2");
193 }
194 }
195}