reifydb_testing/testscript/
runner.rs1use std::{env::temp_dir, error::Error, fs, io, io::Write as _, panic, path, process, time};
13
14use fs::read_to_string;
15use io::ErrorKind;
16use panic::AssertUnwindSafe;
17use path::Path;
18use time::SystemTime;
19
20use crate::{
21 goldenfile::Mint,
22 testscript::{command::Command, parser::parse},
23};
24
25pub trait Runner {
27 fn run(&mut self, command: &Command) -> Result<String, Box<dyn Error>>;
38
39 fn start_script(&mut self) -> Result<(), Box<dyn Error>> {
43 Ok(())
44 }
45
46 fn end_script(&mut self) -> Result<(), Box<dyn Error>> {
50 Ok(())
51 }
52
53 fn start_block(&mut self) -> Result<String, Box<dyn Error>> {
56 Ok(String::new())
57 }
58
59 fn end_block(&mut self) -> Result<String, Box<dyn Error>> {
62 Ok(String::new())
63 }
64
65 #[allow(unused_variables)]
69 fn start_command(&mut self, command: &Command) -> Result<String, Box<dyn Error>> {
70 Ok(String::new())
71 }
72
73 #[allow(unused_variables)]
77 fn end_command(&mut self, command: &Command) -> Result<String, Box<dyn Error>> {
78 Ok(String::new())
79 }
80}
81
82pub fn run_path<R: Runner, P: AsRef<Path>>(runner: &mut R, path: P) -> io::Result<()> {
89 let path = path.as_ref();
90 let Some(dir) = path.parent() else {
91 return Err(io::Error::new(ErrorKind::InvalidInput, format!("invalid path '{path:?}'")));
92 };
93 let Some(filename) = path.file_name() else {
94 return Err(io::Error::new(ErrorKind::InvalidInput, format!("invalid path '{path:?}'")));
95 };
96
97 if filename.to_str().unwrap().ends_with(".skip") {
98 return Ok(());
99 }
100
101 let input = read_to_string(dir.join(filename))?;
102 let output = generate(runner, &input)?;
103
104 Mint::new(dir).new_goldenfile(filename)?.write_all(output.as_bytes())
105}
106
107pub fn run<R: Runner, S: Into<String>>(runner: R, test: S) {
108 try_run(runner, test).unwrap();
109}
110
111pub fn try_run<R: Runner, S: Into<String>>(mut runner: R, test: S) -> io::Result<()> {
112 let input = test.into();
113
114 let dir = temp_dir();
115 let file_name = format!(
116 "test-{}-{}.txt",
117 process::id(),
118 SystemTime::now().duration_since(time::UNIX_EPOCH).unwrap().as_nanos()
119 );
120 let file_path = dir.join(&file_name);
121
122 let mut file = fs::File::create(&file_path)?;
123 file.write_all(input.as_bytes())?;
124
125 let output = generate(&mut runner, &input)?;
126 Mint::new(dir).new_goldenfile(&file_name)?.write_all(output.as_bytes())
127}
128
129pub fn generate<R: Runner>(runner: &mut R, input: &str) -> io::Result<String> {
131 let mut output = String::with_capacity(input.len()); let eol = match input.find("\r\n") {
135 Some(_) => "\r\n",
136 None => "\n",
137 };
138
139 let blocks = parse(input).map_err(|e| {
141 io::Error::new(
142 ErrorKind::InvalidInput,
143 format!(
144 "parse error at line {} column {} for {:?}:\n{}\n{}^",
145 e.input.location_line(),
146 e.input.get_column(),
147 e.code,
148 String::from_utf8_lossy(e.input.get_line_beginning()),
149 ' '.to_string().repeat(e.input.get_utf8_column() - 1)
150 ),
151 )
152 })?;
153
154 runner.start_script().map_err(|e| io::Error::new(ErrorKind::Other, format!("start_script failed: {e}")))?;
156
157 for (i, block) in blocks.iter().enumerate() {
158 if block.commands.is_empty() {
162 output.push_str(&block.literal);
163 continue;
164 }
165
166 let mut block_output = String::new();
168
169 block_output.push_str(&ensure_eol(
171 runner.start_block().map_err(|e| {
172 io::Error::new(
173 ErrorKind::Other,
174 format!("start_block failed at line {}: {e}", block.line_number),
175 )
176 })?,
177 eol,
178 ));
179
180 for command in &block.commands {
181 let mut command_output = String::new();
182
183 command_output.push_str(&ensure_eol(
185 runner.start_command(command).map_err(|e| {
186 io::Error::new(
187 ErrorKind::Other,
188 format!("start_command failed at line {}: {e}", command.line_number),
189 )
190 })?,
191 eol,
192 ));
193
194 let run = AssertUnwindSafe(|| runner.run(command));
199 command_output.push_str(&match panic::catch_unwind(run) {
200 Ok(Ok(output)) if command.fail => {
202 return Err(io::Error::new(
203 ErrorKind::Other,
204 format!(
205 "expected command '{}' to fail at line {}, succeeded with: {output}",
206 command.name, command.line_number
207 ),
208 ));
209 }
210
211 Ok(Ok(output)) => output,
213
214 Ok(Err(e)) if command.fail => {
216 format!("{e}")
217 }
218
219 Ok(Err(e)) => {
221 return Err(io::Error::new(
222 ErrorKind::Other,
223 format!(
224 "command '{}' failed at line {}: {e}",
225 command.name, command.line_number
226 ),
227 ));
228 }
229
230 Err(panic) if command.fail => {
232 let message = panic
233 .downcast_ref::<&str>()
234 .map(|s| s.to_string())
235 .or_else(|| panic.downcast_ref::<String>().cloned())
236 .unwrap_or_else(|| panic::resume_unwind(panic));
237 format!("Panic: {message}")
238 }
239
240 Err(panic) => panic::resume_unwind(panic),
242 });
243
244 command_output = ensure_eol(command_output, eol);
247
248 command_output.push_str(&ensure_eol(
250 runner.end_command(command).map_err(|e| {
251 io::Error::new(
252 ErrorKind::Other,
253 format!("end_command failed at line {}: {e}", command.line_number),
254 )
255 })?,
256 eol,
257 ));
258
259 if command.silent {
261 command_output = "".to_string();
262 }
263
264 if let Some(prefix) = &command.prefix {
266 if !command_output.is_empty() {
267 command_output = format!(
268 "{prefix}: {}{eol}",
269 command_output
270 .strip_suffix(eol)
271 .unwrap_or(command_output.as_str())
272 .replace('\n', &format!("\n{prefix}: "))
273 );
274 }
275 }
276
277 block_output.push_str(&command_output);
278 }
279
280 block_output.push_str(&ensure_eol(
282 runner.end_block().map_err(|e| {
283 io::Error::new(
284 ErrorKind::Other,
285 format!("end_block failed at line {}: {e}", block.line_number),
286 )
287 })?,
288 eol,
289 ));
290
291 if block_output.is_empty() {
293 block_output.push_str("ok\n")
294 }
295
296 if block_output.starts_with('\n')
302 || block_output.starts_with("\r\n")
303 || block_output.contains("\n\n")
304 || block_output.contains("\n\r\n")
305 {
306 block_output = format!("> {}", block_output.replace('\n', "\n> "));
307 block_output = block_output.replace("> \n", ">\n");
309 block_output.pop();
313 block_output.pop();
314 }
315
316 output.push_str(&format!("{}---{eol}{}", block.literal, block_output));
319 if i < blocks.len() - 1 {
320 output.push_str(eol);
321 }
322 }
323
324 runner.end_script().map_err(|e| io::Error::new(ErrorKind::Other, format!("end_script failed: {e}")))?;
326
327 Ok(output)
328}
329
330fn ensure_eol(mut s: String, eol: &str) -> String {
332 if let Some(c) = s.chars().next_back() {
333 if c != '\n' {
334 s.push_str(eol)
335 }
336 }
337 s
338}
339
340#[cfg(test)]
342pub mod tests {
343 use super::*;
344
345 #[derive(Default)]
348 struct HookRunner {
349 start_script_count: usize,
350 end_script_count: usize,
351 start_block_count: usize,
352 end_block_count: usize,
353 start_command_count: usize,
354 end_command_count: usize,
355 }
356
357 impl Runner for HookRunner {
358 fn run(&mut self, _: &Command) -> Result<String, Box<dyn Error>> {
359 Ok(String::new())
360 }
361
362 fn start_script(&mut self) -> Result<(), Box<dyn Error>> {
363 self.start_script_count += 1;
364 Ok(())
365 }
366
367 fn end_script(&mut self) -> Result<(), Box<dyn Error>> {
368 self.end_script_count += 1;
369 Ok(())
370 }
371
372 fn start_block(&mut self) -> Result<String, Box<dyn Error>> {
373 self.start_block_count += 1;
374 Ok(String::new())
375 }
376
377 fn end_block(&mut self) -> Result<String, Box<dyn Error>> {
378 self.end_block_count += 1;
379 Ok(String::new())
380 }
381
382 fn start_command(&mut self, _: &Command) -> Result<String, Box<dyn Error>> {
383 self.start_command_count += 1;
384 Ok(String::new())
385 }
386
387 fn end_command(&mut self, _: &Command) -> Result<String, Box<dyn Error>> {
388 self.end_command_count += 1;
389 Ok(String::new())
390 }
391 }
392
393 #[test]
395 fn hooks() {
396 let mut runner = HookRunner::default();
397 generate(
398 &mut runner,
399 r#"
400command
401---
402
403command
404command
405---
406"#,
407 )
408 .unwrap();
409
410 assert_eq!(runner.start_script_count, 1);
411 assert_eq!(runner.end_script_count, 1);
412 assert_eq!(runner.start_block_count, 2);
413 assert_eq!(runner.end_block_count, 2);
414 assert_eq!(runner.start_command_count, 3);
415 assert_eq!(runner.end_command_count, 3);
416 }
417}