reifydb_testing/testscript/
runner.rs1use std::{env::temp_dir, error::Error, io::Write as _};
13
14use crate::testscript::{command::Command, parser::parse};
15
16pub trait Runner {
18 fn run(&mut self, command: &Command) -> Result<String, Box<dyn Error>>;
29
30 fn start_script(&mut self) -> Result<(), Box<dyn Error>> {
34 Ok(())
35 }
36
37 fn end_script(&mut self) -> Result<(), Box<dyn Error>> {
41 Ok(())
42 }
43
44 fn start_block(&mut self) -> Result<String, Box<dyn Error>> {
47 Ok(String::new())
48 }
49
50 fn end_block(&mut self) -> Result<String, Box<dyn Error>> {
53 Ok(String::new())
54 }
55
56 #[allow(unused_variables)]
60 fn start_command(&mut self, command: &Command) -> Result<String, Box<dyn Error>> {
61 Ok(String::new())
62 }
63
64 #[allow(unused_variables)]
68 fn end_command(&mut self, command: &Command) -> Result<String, Box<dyn Error>> {
69 Ok(String::new())
70 }
71}
72
73pub fn run_path<R: Runner, P: AsRef<std::path::Path>>(runner: &mut R, path: P) -> std::io::Result<()> {
80 let path = path.as_ref();
81 let Some(dir) = path.parent() else {
82 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid path '{path:?}'")));
83 };
84 let Some(filename) = path.file_name() else {
85 return Err(std::io::Error::new(std::io::ErrorKind::InvalidInput, format!("invalid path '{path:?}'")));
86 };
87
88 if filename.to_str().unwrap().ends_with(".skip") {
89 return Ok(());
90 }
91
92 let input = std::fs::read_to_string(dir.join(filename))?;
93 let output = generate(runner, &input)?;
94
95 crate::goldenfile::Mint::new(dir).new_goldenfile(filename)?.write_all(output.as_bytes())
96}
97
98pub fn run<R: Runner, S: Into<String>>(runner: R, test: S) {
99 try_run(runner, test).unwrap();
100}
101
102pub fn try_run<R: Runner, S: Into<String>>(mut runner: R, test: S) -> std::io::Result<()> {
103 let input = test.into();
104
105 let dir = temp_dir();
106 let file_name = format!(
107 "test-{}-{}.txt",
108 std::process::id(),
109 std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_nanos()
110 );
111 let file_path = dir.join(&file_name);
112
113 let mut file = std::fs::File::create(&file_path)?;
114 file.write_all(input.as_bytes())?;
115
116 let output = generate(&mut runner, &input)?;
117 crate::goldenfile::Mint::new(dir).new_goldenfile(&file_name)?.write_all(output.as_bytes())
118}
119
120pub fn generate<R: Runner>(runner: &mut R, input: &str) -> std::io::Result<String> {
122 let mut output = String::with_capacity(input.len()); let eol = match input.find("\r\n") {
126 Some(_) => "\r\n",
127 None => "\n",
128 };
129
130 let blocks = parse(input).map_err(|e| {
132 std::io::Error::new(
133 std::io::ErrorKind::InvalidInput,
134 format!(
135 "parse error at line {} column {} for {:?}:\n{}\n{}^",
136 e.input.location_line(),
137 e.input.get_column(),
138 e.code,
139 String::from_utf8_lossy(e.input.get_line_beginning()),
140 ' '.to_string().repeat(e.input.get_utf8_column() - 1)
141 ),
142 )
143 })?;
144
145 runner.start_script()
147 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("start_script failed: {e}")))?;
148
149 for (i, block) in blocks.iter().enumerate() {
150 if block.commands.is_empty() {
154 output.push_str(&block.literal);
155 continue;
156 }
157
158 let mut block_output = String::new();
160
161 block_output.push_str(&ensure_eol(
163 runner.start_block().map_err(|e| {
164 std::io::Error::new(
165 std::io::ErrorKind::Other,
166 format!("start_block failed at line {}: {e}", block.line_number),
167 )
168 })?,
169 eol,
170 ));
171
172 for command in &block.commands {
173 let mut command_output = String::new();
174
175 command_output.push_str(&ensure_eol(
177 runner.start_command(command).map_err(|e| {
178 std::io::Error::new(
179 std::io::ErrorKind::Other,
180 format!("start_command failed at line {}: {e}", command.line_number),
181 )
182 })?,
183 eol,
184 ));
185
186 let run = std::panic::AssertUnwindSafe(|| runner.run(command));
191 command_output.push_str(&match std::panic::catch_unwind(run) {
192 Ok(Ok(output)) if command.fail => {
194 return Err(std::io::Error::new(
195 std::io::ErrorKind::Other,
196 format!(
197 "expected command '{}' to fail at line {}, succeeded with: {output}",
198 command.name, command.line_number
199 ),
200 ));
201 }
202
203 Ok(Ok(output)) => output,
205
206 Ok(Err(e)) if command.fail => {
208 format!("{e}")
209 }
210
211 Ok(Err(e)) => {
213 return Err(std::io::Error::new(
214 std::io::ErrorKind::Other,
215 format!(
216 "command '{}' failed at line {}: {e}",
217 command.name, command.line_number
218 ),
219 ));
220 }
221
222 Err(panic) if command.fail => {
224 let message = panic
225 .downcast_ref::<&str>()
226 .map(|s| s.to_string())
227 .or_else(|| panic.downcast_ref::<String>().cloned())
228 .unwrap_or_else(|| std::panic::resume_unwind(panic));
229 format!("Panic: {message}")
230 }
231
232 Err(panic) => std::panic::resume_unwind(panic),
234 });
235
236 command_output = ensure_eol(command_output, eol);
239
240 command_output.push_str(&ensure_eol(
242 runner.end_command(command).map_err(|e| {
243 std::io::Error::new(
244 std::io::ErrorKind::Other,
245 format!("end_command failed at line {}: {e}", command.line_number),
246 )
247 })?,
248 eol,
249 ));
250
251 if command.silent {
253 command_output = "".to_string();
254 }
255
256 if let Some(prefix) = &command.prefix {
258 if !command_output.is_empty() {
259 command_output = format!(
260 "{prefix}: {}{eol}",
261 command_output
262 .strip_suffix(eol)
263 .unwrap_or(command_output.as_str())
264 .replace('\n', &format!("\n{prefix}: "))
265 );
266 }
267 }
268
269 block_output.push_str(&command_output);
270 }
271
272 block_output.push_str(&ensure_eol(
274 runner.end_block().map_err(|e| {
275 std::io::Error::new(
276 std::io::ErrorKind::Other,
277 format!("end_block failed at line {}: {e}", block.line_number),
278 )
279 })?,
280 eol,
281 ));
282
283 if block_output.is_empty() {
285 block_output.push_str("ok\n")
286 }
287
288 if block_output.starts_with('\n')
294 || block_output.starts_with("\r\n")
295 || block_output.contains("\n\n")
296 || block_output.contains("\n\r\n")
297 {
298 block_output = format!("> {}", block_output.replace('\n', "\n> "));
299 block_output = block_output.replace("> \n", ">\n");
301 block_output.pop();
305 block_output.pop();
306 }
307
308 output.push_str(&format!("{}---{eol}{}", block.literal, block_output));
311 if i < blocks.len() - 1 {
312 output.push_str(eol);
313 }
314 }
315
316 runner.end_script()
318 .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("end_script failed: {e}")))?;
319
320 Ok(output)
321}
322
323fn ensure_eol(mut s: String, eol: &str) -> String {
325 if let Some(c) = s.chars().next_back() {
326 if c != '\n' {
327 s.push_str(eol)
328 }
329 }
330 s
331}
332
333#[cfg(test)]
335pub mod tests {
336 use super::*;
337
338 #[derive(Default)]
341 struct HookRunner {
342 start_script_count: usize,
343 end_script_count: usize,
344 start_block_count: usize,
345 end_block_count: usize,
346 start_command_count: usize,
347 end_command_count: usize,
348 }
349
350 impl Runner for HookRunner {
351 fn run(&mut self, _: &Command) -> Result<String, Box<dyn Error>> {
352 Ok(String::new())
353 }
354
355 fn start_script(&mut self) -> Result<(), Box<dyn Error>> {
356 self.start_script_count += 1;
357 Ok(())
358 }
359
360 fn end_script(&mut self) -> Result<(), Box<dyn Error>> {
361 self.end_script_count += 1;
362 Ok(())
363 }
364
365 fn start_block(&mut self) -> Result<String, Box<dyn Error>> {
366 self.start_block_count += 1;
367 Ok(String::new())
368 }
369
370 fn end_block(&mut self) -> Result<String, Box<dyn Error>> {
371 self.end_block_count += 1;
372 Ok(String::new())
373 }
374
375 fn start_command(&mut self, _: &Command) -> Result<String, Box<dyn Error>> {
376 self.start_command_count += 1;
377 Ok(String::new())
378 }
379
380 fn end_command(&mut self, _: &Command) -> Result<String, Box<dyn Error>> {
381 self.end_command_count += 1;
382 Ok(String::new())
383 }
384 }
385
386 #[test]
388 fn hooks() {
389 let mut runner = HookRunner::default();
390 generate(
391 &mut runner,
392 r#"
393command
394---
395
396command
397command
398---
399"#,
400 )
401 .unwrap();
402
403 assert_eq!(runner.start_script_count, 1);
404 assert_eq!(runner.end_script_count, 1);
405 assert_eq!(runner.start_block_count, 2);
406 assert_eq!(runner.end_block_count, 2);
407 assert_eq!(runner.start_command_count, 3);
408 assert_eq!(runner.end_command_count, 3);
409 }
410}