#![warn(clippy::all)]
use std::error::Error;
use std::fmt::Write as _;
use std::io::Write as _;
use test_each_file::test_each_path;
test_each_path! { in "tests/scripts" as scripts => test_goldenscript }
fn test_goldenscript(path: &std::path::Path) {
goldenscript::run(&mut DebugRunner::new(), path).expect("runner failed")
}
test_each_path! { for ["in", "out"] in "tests/generate" as generate => test_generate }
fn test_generate([in_path, out_path]: [&std::path::Path; 2]) {
let input = std::fs::read_to_string(in_path).expect("failed to read file");
let output = goldenscript::generate(&mut DebugRunner::new(), &input).expect("runner failed");
let dir = out_path.parent().expect("invalid path");
let filename = out_path.file_name().expect("invalid path");
let mut mint = goldenfile::Mint::new(dir);
let mut f = mint.new_goldenfile(filename).expect("failed to create goldenfile");
f.write_all(output.as_bytes()).expect("failed to write output");
}
test_each_path! { for ["in", "error"] in "tests/errors" as errors => test_error }
fn test_error([in_path, out_path]: [&std::path::Path; 2]) {
let input = std::fs::read_to_string(in_path).expect("failed to read file");
let run =
std::panic::AssertUnwindSafe(|| goldenscript::generate(&mut DebugRunner::new(), &input));
let message = match std::panic::catch_unwind(run) {
Ok(Ok(_)) => panic!("script succeeded"),
Ok(Err(e)) => e.to_string(),
Err(panic) => panic
.downcast_ref::<&str>()
.map(|s| s.to_string())
.or_else(|| panic.downcast_ref::<String>().cloned())
.unwrap_or_else(|| std::panic::resume_unwind(panic)),
};
let dir = out_path.parent().expect("invalid path");
let filename = out_path.file_name().expect("invalid path");
let mut mint = goldenfile::Mint::new(dir);
let mut f = mint.new_goldenfile(filename).expect("failed to create goldenfile");
f.write_all(message.as_bytes()).expect("failed to write goldenfile");
}
#[derive(Default)]
struct DebugRunner {
prefix: String,
suffix: String,
start_block: String,
end_block: String,
start_command: String,
end_command: String,
}
impl DebugRunner {
fn new() -> Self {
Self::default()
}
}
impl goldenscript::Runner for DebugRunner {
fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {
let output = match command.name.as_str() {
"_echo" => {
for arg in &command.args {
if arg.key.is_some() {
return Err("echo args can't have keys".into());
}
}
command.args.iter().map(|a| a.value.clone()).collect::<Vec<String>>().join(" ")
}
"_error" => {
let message = command.args.first().map(|a| a.value.as_str()).unwrap_or("error");
return Err(message.to_string().into());
}
"_panic" => {
let message = command.args.first().map(|a| a.value.as_str()).unwrap_or("panic");
panic!("{message}");
}
"_set" => {
for arg in &command.args {
match arg.key.as_deref() {
Some("prefix") => self.prefix = arg.value.clone(),
Some("suffix") => self.suffix = arg.value.clone(),
Some("start_block") => self.start_block = arg.value.clone(),
Some("end_block") => self.end_block = arg.value.clone(),
Some("start_command") => self.start_command = arg.value.clone(),
Some("end_command") => self.end_command = arg.value.clone(),
Some(key) => return Err(format!("unknown argument key {key}").into()),
None => return Err("argument must have a key".into()),
}
}
return Ok(String::new());
}
_ if command.fail => return Err(format!("{command:?}").into()),
_ => format!("{command:?}"),
};
Ok(format!("{}{output}{}", self.prefix, self.suffix))
}
fn start_block(&mut self) -> Result<String, Box<dyn Error>> {
Ok(self.start_block.clone())
}
fn end_block(&mut self) -> Result<String, Box<dyn Error>> {
Ok(self.end_block.clone())
}
fn start_command(&mut self, _: &goldenscript::Command) -> Result<String, Box<dyn Error>> {
Ok(self.start_command.clone())
}
fn end_command(&mut self, _: &goldenscript::Command) -> Result<String, Box<dyn Error>> {
Ok(self.end_command.clone())
}
}
#[derive(Default)]
struct BTreeMapRunner {
map: std::collections::BTreeMap<String, String>,
}
impl goldenscript::Runner for BTreeMapRunner {
fn run(&mut self, command: &goldenscript::Command) -> Result<String, Box<dyn Error>> {
let mut output = String::new();
match command.name.as_str() {
"get" => {
let mut args = command.consume_args();
let key = &args.next_pos().ok_or("key not given")?.value;
args.reject_rest()?;
let value = self.map.get(key);
writeln!(output, "get → {value:?}")?;
}
"insert" => {
let mut args = command.consume_args();
for arg in args.rest_key() {
let old = self.map.insert(arg.key.clone().unwrap(), arg.value.clone());
writeln!(output, "insert → {old:?}")?;
}
args.reject_rest()?;
}
"range" => {
use std::ops::Bound::*;
let mut args = command.consume_args();
let from = args.next_pos().map(|a| Included(a.value.clone())).unwrap_or(Unbounded);
let to = args.next_pos().map(|a| Excluded(a.value.clone())).unwrap_or(Unbounded);
args.reject_rest()?;
for (key, value) in self.map.range((from, to)) {
writeln!(output, "{key}={value}")?;
}
}
name => return Err(format!("invalid command {name}").into()),
};
Ok(output)
}
}
#[test]
fn btreemap() {
goldenscript::run(&mut BTreeMapRunner::default(), "tests/btreemap")
.expect("goldenscript failed")
}