use std::{ ffi::OsStr, fs, path::{Path, PathBuf}, }; use assert_cmd::Command; use insta::assert_snapshot; use tempfile::{tempdir_in, TempDir}; struct Context { command: Command, working_directory: TempDir, snapshot_replace: Vec<(String, String)>, } impl Default for Context { fn default() -> Self { let working_directory = tempdir_in(env!("CARGO_TARGET_TMPDIR")).expect("unable to create temporary directory"); let mut command = Command::cargo_bin(env!("CARGO_PKG_NAME")).unwrap(); command.current_dir(working_directory.path()); Self::new(command, working_directory) } } impl Context { pub fn new(command: Command, working_directory: TempDir) -> Self { Self { command, working_directory, snapshot_replace: Vec::new(), } } pub fn arg>(mut self, arg: S) -> Self { self.command.arg(arg); self } pub fn expect_file>(&self, file_path: P) -> &Self { let file_path = file_path.as_ref(); if !file_path.exists() || !file_path.is_file() { Self::debug_working_directory(self.working_directory.path()); } assert!( file_path.exists(), "file `{}` does not exist", file_path.display() ); assert!( file_path.is_file(), "path `{}` is not a file", file_path.display() ); self } pub fn snapshot_file>( &self, snapshot_name: &'static str, file_path: P, ) -> &Self { let file_path = self.path_from_working_directory(file_path.as_ref()); self.expect_file(&file_path); let content = fs::read_to_string(file_path).expect("unable to read file"); self.snapshot_string(snapshot_name, content); self } pub fn snapshot_command(mut self, snapshot_name: &'static str) -> Self { let output = self.full_output(); self.snapshot_string(snapshot_name, output); self } fn snapshot_string(&self, snapshot_name: &'static str, content: String) { let working_directory_display = format!( "{}{}", self.working_directory.path().display(), std::path::MAIN_SEPARATOR ); let content = content.replace(&working_directory_display, "{{CWD}}"); let mut settings = insta::Settings::clone_current(); settings.set_prepend_module_to_snapshot(false); settings.add_filter("darklua\\.exe", "darklua"); for (matcher, replacement) in self.snapshot_replace.iter() { settings.add_filter(matcher, replacement); } settings.bind(|| { assert_snapshot!(snapshot_name, content); }); } pub fn write_file>(self, relative_path: P, content: &str) -> Self { let file_path = self.path_from_working_directory(relative_path); if let Some(parent) = file_path .parent() .filter(|parent| *parent != self.working_directory.as_ref()) { fs::create_dir_all(parent).expect("unable to create directory"); } fs::write(file_path, content).expect("unable to write file"); self } fn path_from_working_directory>(&self, path: P) -> PathBuf { self.working_directory.path().join(path) } fn full_output(&mut self) -> String { let output = self.command.output().expect("unable to run command"); let mut string = std::str::from_utf8(&output.stdout) .expect("unable to read output") .to_owned(); let err_output = std::str::from_utf8(&output.stderr).expect("unable to read output"); if !string.is_empty() && !err_output.is_empty() { string.push('\n'); } string.push_str(err_output); string } fn debug_working_directory(root: &Path) { eprintln!("{}", root.display()); for entry in root.read_dir().unwrap() { let entry = entry.unwrap().path(); if entry.is_dir() { Self::debug_working_directory(&entry); } else { eprintln!("{}", entry.display()); } } } pub fn expect_success(mut self) -> Self { self.command.assert().code(0); self } pub fn replace_snapshot_content( mut self, matcher: impl Into, replace_with: impl Into, ) -> Self { self.snapshot_replace .push((matcher.into(), replace_with.into())); self } pub fn replace_duration_labels(self) -> Self { self.replace_snapshot_content("\\d+\\.\\d+[µm]s", "{{DURATION}}") .replace_snapshot_content("\\d+µs", "{{DURATION}}") } pub fn replace_backslashes(self) -> Self { self.replace_snapshot_content("\\\\", "/") } } #[test] fn snapshot_help_command() { Context::default() .arg("help") .snapshot_command("help_command"); } #[test] fn snapshot_short_help_command() { Context::default() .arg("-h") .snapshot_command("short_help_command"); } #[test] fn snapshot_process_help_command() { Context::default() .arg("process") .arg("--help") .snapshot_command("process_help_command"); } #[test] fn snapshot_minify_help_command() { Context::default() .arg("minify") .arg("--help") .snapshot_command("minify_help_command"); } #[test] fn snapshot_convert_help_command() { Context::default() .arg("convert") .arg("--help") .snapshot_command("convert_help_command"); } #[test] fn run_minify_command() { Context::default() .write_file("src/init.lua", "return 1 + 1\n") .arg("minify") .arg("src") .arg("out") .replace_duration_labels() .snapshot_command("run_minify_command") .snapshot_file("run_minify_command_init_out", "out/init.lua"); } #[test] fn run_minify_with_column_span_command() { Context::default() .write_file("src/init.lua", "return 1 + 1\n") .arg("minify") .arg("--column-span") .arg("2") .arg("src") .arg("out") .replace_duration_labels() .expect_success() .snapshot_file( "run_minify_with_column_span_command_init_out", "out/init.lua", ); } #[test] fn run_minify_with_column_span_in_config_command() { Context::default() .write_file("src/init.lua", "return 1 + 1\n") // minify does not read the configuration file anymore, so we should expect // the snapshot to have the default generator used .write_file(".darklua.json", "{ column_span: 2 }") .arg("minify") .arg("src") .arg("out") .replace_duration_labels() .expect_success() .snapshot_file( "run_minify_with_column_span_in_config_command_init_out", "out/init.lua", ); } #[test] fn run_minify_verbose_command() { Context::default() .write_file("src/init.lua", "return 1 + 1\n") .arg("minify") .arg("-v") .arg("src") .arg("out") .replace_duration_labels() .replace_backslashes() .snapshot_command("run_minify_verbose_command"); } #[test] fn run_process_command() { Context::default() .write_file("src/init.lua", "return 1 + 1\n") .arg("process") .arg("src") .arg("out") .replace_duration_labels() .snapshot_command("run_process_command") .snapshot_file("run_process_command_init_out", "out/init.lua"); } #[test] fn run_process_verbose_command() { Context::default() .write_file("src/init.lua", "return 1 + 1\n") .arg("process") .arg("-v") .arg("src") .arg("out") .replace_duration_labels() .replace_backslashes() .snapshot_command("run_process_verbose_command"); } #[test] fn run_process_single_file_custom_config_command() { Context::default() .write_file("test.lua", "return _G.CONSTANT\n") .write_file( "custom.json5", "{ rules: [{ rule: 'inject_global_value', identifier: 'CONSTANT', value: true }] }", ) .arg("process") .arg("--config") .arg("custom.json5") .arg("test.lua") .arg("out.lua") .replace_duration_labels() .snapshot_command("run_process_single_file_custom_config") .snapshot_file("run_process_custom_config_command_out", "out.lua"); } #[test] fn run_process_single_file_custom_config_command_deprecated_config_path() { Context::default() .write_file("test.lua", "return _G.CONSTANT\n") .write_file( "custom.json5", "{ rules: [{ rule: 'inject_global_value', identifier: 'CONSTANT', value: true }] }", ) .arg("process") .arg("--config-path") .arg("custom.json5") .arg("test.lua") .arg("out.lua") .replace_duration_labels() .snapshot_command("run_process_single_file_custom_config") .snapshot_file("run_process_custom_config_command_out", "out.lua"); } #[test] fn run_convert_command_on_json_file_with_output() { Context::default() .write_file("data.json", "{ \"property\": true }") .arg("convert") .arg("data.json") .arg("out.lua") .replace_duration_labels() .snapshot_command("run_convert_command_on_json_file_with_output") .snapshot_file("run_convert_command_on_json_file_out", "out.lua"); } #[test] fn run_convert_command_on_json_without_extension_file_with_output() { Context::default() .write_file("data", "{ \"property\": true }") .arg("convert") .arg("data") .arg("out.lua") .arg("--format") .arg("json") .replace_duration_labels() .snapshot_command("run_convert_command_on_json_without_extension_file_with_output") .snapshot_file("run_convert_command_on_json_file_out", "out.lua"); } #[test] fn run_convert_command_on_json_file() { Context::default() .write_file("data.json", "{ \"property\": true }") .arg("convert") .arg("data.json") .replace_duration_labels() .snapshot_command("run_convert_command_on_json_file_stdout"); } #[test] fn run_convert_command_errors_when_no_extension_and_no_format() { Context::default() .write_file("data", "{ \"property\": true }") .arg("convert") .arg("data") .arg("out.lua") .replace_duration_labels() .snapshot_command("run_convert_command_errors_when_no_extension_and_no_format"); } #[test] fn run_convert_command_errors_when_unrecognized_extension() { Context::default() .write_file("data.yoyo", "{ \"property\": true }") .arg("convert") .arg("data.yoyo") .arg("out.lua") .replace_duration_labels() .snapshot_command("run_convert_command_errors_when_unrecognized_extension"); }