//! //! Testing framework responsible for ensuring the expected behaviour of running some command-line. //! The test sets up a temporary directory with the associated `source` files. Then it runs the //! partisia-contract cargo command with the commandline arguments given in `command_args.txt`. //! The `std.out` and `std.err` is compared with the associated files and the exit code is checked //! against the `exitcode.txt` file. The resulting generated files are then asserted against the //! associated `reference` folder. //! //! If the `TEST_REF_REGEN` environment variable is set the framework generates //! the `reference` folder, `exitcode.txt`, `std.err` and `std.out` files instead of asserting. use std::ffi::OsString; use std::fs::File; use std::io::Write; use std::path::{Path, PathBuf}; use std::{env, fs}; use assert_cmd::assert::{Assert, OutputAssertExt}; use assert_fs::TempDir; use java_locator::errors::JavaLocatorError; use java_locator::locate_java_home; use regex::Regex; use crate::test_util::cargo_partisia_command; use crate::test_util::constants::{ ABI_CLI, CONTRACTS, DEFAULT_PBC_FOLDER_NAME, ENV_VAR_PBC_FOLDER, PARTISIA_CLI, ZK_COMPILER_3_63_0, ZK_COMPILER_3_82_0, ZK_COMPILER_4_35_0, }; use crate::test_util::jar_util::get_jar_url; use crate::test_util::resource_test_util::{ assert_resource_test, generate_new_reference_file, insert_globals, remove_globals, setup_resource_test, walk_dir_and_apply_to_files, }; const STD_OUT: &str = "std-out.txt"; const STD_ERR: &str = "std-err.txt"; const EXIT_CODE: &str = "exitcode.txt"; const COMMAND_ARGS: &str = "command_args.txt"; /// The runner of the reference tests. pub struct BuildReferenceTestRunner<'a> { /// The path to the reference under test, should include both a `source` and a `reference` folder, /// as well as `command_arg.txt`, `std.out`, `std.err`, and `exitcode.txt` files. test_path: &'a Path, /// The tempdir used to set up the reference test. This is where the source is copied into. pub temp_dir: TempDir, } pub struct GlobalReplacement { pub(crate) identifier: &'static str, pub(crate) in_file_value: String, } pub struct RegexToStringReplacement { pub(crate) regex: String, pub(crate) replacement: String, } impl<'a> BuildReferenceTestRunner<'a> { /// Run the reference tests by setting up the test in the temp dir, running the command /// and asserting the generated files against the reference files. /// If the `TEST_REF_REGEN` environment variable is set, the runner generates /// the reference files instead of asserting against them. pub fn run_reference_test(&self) { // self.generate_empty_files(); let java = locate_java_home(); // create map of globals let globals = self.create_globals(&java); // Copy source folder into temporary directory and pass map of globals as argument setup_resource_test(&self.temp_dir, self.test_path, &globals); // Run the command specified in the command_args.txt file let result = self.run_command(&globals); // Generate std.out, std.err and exit code in temporary directory self.generate_output_files(&result, &globals); // Regenerates reference folder if the environment variable is set let generate_resource_tests = env::var("TEST_REF_REGEN").is_ok(); // Does not format to Linux standard if the "NO_FORMATTING" env variable is set let no_formatting = env::var("NO_FORMATTING").is_ok(); if generate_resource_tests { self.create_reference_folder(&self.temp_dir, self.test_path, no_formatting, &globals); } else { self.assert_test(result, &globals); } } fn create_globals<'b>( &'b self, java: &'b Result, ) -> Vec { let test_dir_path_absolute = self .test_path .canonicalize() .unwrap() .to_string_lossy() .to_string() .replace("\\\\?\\", ""); let mut globals = vec![ // Constant urls GlobalReplacement { identifier: "{{ abi_client_url }}", in_file_value: get_jar_url(ABI_CLI), }, GlobalReplacement { identifier: "{{ partisia_cli_url }}", in_file_value: get_jar_url(PARTISIA_CLI), }, GlobalReplacement { identifier: "{{ zk_compiler_4_35_0 }}", in_file_value: ZK_COMPILER_4_35_0.to_string(), }, GlobalReplacement { identifier: "{{ zk_compiler_3_82_0 }}", in_file_value: ZK_COMPILER_3_82_0.to_string(), }, GlobalReplacement { identifier: "{{ zk_compiler_3_63_0 }}", in_file_value: ZK_COMPILER_3_63_0.to_string(), }, GlobalReplacement { identifier: "{{ example_contracts }}", in_file_value: CONTRACTS.to_string(), }, // Path to temporary directory GlobalReplacement { identifier: "{{ temp_dir_for_jar }}", in_file_value: format_string(self.temp_dir.to_str().unwrap().to_string()), }, // Path to temporary directory GlobalReplacement { identifier: "{{ temp_dir }}", in_file_value: self.temp_dir.to_str().unwrap().to_string(), }, // Path to test resource folder GlobalReplacement { identifier: "{{ test_resource_folder }}", in_file_value: test_dir_path_absolute, }, GlobalReplacement { identifier: "ABI-CLI-VERSION", in_file_value: get_abi_cli_version(get_jar_url(ABI_CLI)), }, GlobalReplacement { identifier: "PARTISIA-CLI-VERSION", in_file_value: get_partisia_cli_version(get_jar_url(PARTISIA_CLI)), }, ]; // Path to java home if java.is_ok() { let java_string = java.as_ref().unwrap(); globals.push(GlobalReplacement { identifier: "{{ java_home }}", in_file_value: java_string.to_string(), }); } globals } fn create_regex_string_replacements() -> Vec { vec![ RegexToStringReplacement { regex: "Path,.*\n".to_string(), replacement: "Path,\n".to_string(), }, RegexToStringReplacement { regex: r"\s*(Compiling|Finished).*\n".to_string(), replacement: "".to_string(), }, RegexToStringReplacement { regex: r"\s*Blocking waiting for file lock on package cache".to_string(), replacement: "".to_string(), }, RegexToStringReplacement { regex: r"\s*LLVM Profile Error: Failed to write file.*\n".to_string(), replacement: "".to_string(), }, ] } fn assert_test(&self, result: Assert, globals: &Vec) { let reference_folder = self.test_path.join("reference"); // Assert exit code, std.out and std.err let result = self.assert_exit_code(result, &reference_folder, globals); let std_out = &result.get_output().stdout; let std_out_string = std::str::from_utf8(std_out).unwrap(); let std_err = &result.get_output().stderr; let std_err_string = std::str::from_utf8(std_err).unwrap(); self.assert_std(std_out_string, STD_OUT, &reference_folder, globals); self.assert_std(std_err_string, STD_ERR, &reference_folder, globals); // Assert all other files assert_resource_test(&self.temp_dir, self.test_path, globals); } fn generate_empty_files(&self) { self.create_file_in_reference_folder(STD_OUT, "".to_string()); self.create_file_in_reference_folder(STD_ERR, "".to_string()); self.create_file_in_reference_folder(EXIT_CODE, "".to_string()); } fn generate_output_files(&self, result: &Assert, globals: &Vec) { // Generate std.out and add the file to the reference folder let stdout = &result.get_output().stdout; let mut stdout_string = std::str::from_utf8(stdout).unwrap().to_string(); stdout_string = insert_globals(stdout_string, globals); self.create_file_in_temp_dir(STD_OUT, stdout_string); // Generate std.err add the file to the reference folder let stderr = &result.get_output().stderr; let mut stderr_string = std::str::from_utf8(stderr).unwrap().to_string(); stderr_string = insert_globals(stderr_string, globals); self.create_file_in_temp_dir(STD_ERR, stderr_string); // Generate exit code and add the file to the reference folder let exit_code_string = result.get_output().status.code().unwrap().to_string(); self.create_file_in_temp_dir(EXIT_CODE, exit_code_string); } fn create_reference_folder( &self, tempdir_path: &Path, test_resource_folder: &Path, no_formatting: bool, globals: &Vec, ) { let reference_folder = test_resource_folder.join("reference"); // Delete the old reference folder if it exists if reference_folder.exists() { fs::remove_dir_all(&reference_folder).unwrap_or_else(|_| { panic!("Failed to remove old reference file at {reference_folder:?}") }); } // Format content of tempdir unless NO_FORMATTING flag is set if !no_formatting && Path::exists(tempdir_path) { for entry in fs::read_dir(tempdir_path).unwrap() { let entry = entry.unwrap(); if entry.file_type().unwrap().is_file() { let file = entry.path(); let binding = file.extension().unwrap(); let extension = binding.to_str().unwrap(); match extension { "jar" | "zkwa" | "wasm" | "exe" | "abi" | "zkbc" | "pbc" => {} _ => { let file_string = fs::read_to_string(entry.path()); if let Ok(file_string) = file_string { let content_remove_globals = remove_globals(file_string, globals); let formatted_content = self.format_file(content_remove_globals); fs::write(entry.path(), formatted_content) .expect("Was not able to format file."); } else { println!("{:?}", entry.path()) } } } } } } // Generate new reference file by copying the contents of tempdir into it walk_dir_and_apply_to_files( tempdir_path, &reference_folder, globals, generate_new_reference_file, &Vec::new(), ); } fn create_file_in_reference_folder(&self, path: &str, content: String) { let binding = self.test_path.join("reference"); let reference_folder = binding.as_path(); let reference_buf = PathBuf::from(reference_folder); // Delete the old reference folder if it exists if !reference_buf.exists() { fs::create_dir(reference_folder).expect("fail"); } let path_to_file = reference_buf.join(path); fs::create_dir_all(path_to_file.parent().unwrap()).unwrap(); let mut file = File::create(path_to_file).unwrap(); file.write_all(content.as_bytes()).unwrap(); } fn create_file_in_temp_dir(&self, path: &str, content: String) { let temp_path = TempDir::path(&self.temp_dir); let temp_buf = PathBuf::from(temp_path); // Delete the old reference folder if it exists if !temp_buf.exists() { fs::create_dir(temp_path).expect("fail"); } let path_to_file = temp_buf.join(path); fs::create_dir_all(path_to_file.parent().unwrap()).unwrap(); let mut file = File::create(path_to_file).unwrap(); file.write_all(content.as_bytes()).unwrap(); } fn run_command(&self, globals: &Vec) -> Assert { let command_args = self.read_comparison_file(&self.test_path.join(COMMAND_ARGS), globals); let args = shlex::split(&command_args).unwrap(); let mut build_cmd = cargo_partisia_command(); build_cmd.current_dir(&self.temp_dir); build_cmd.args(args); build_cmd.env( ENV_VAR_PBC_FOLDER, self.temp_dir.join(DEFAULT_PBC_FOLDER_NAME), ); build_cmd.env("NO_COLOR", OsString::from("1")); build_cmd.assert() } fn assert_exit_code( &self, result: Assert, reference_folder: &Path, globals: &Vec, ) -> Assert { let expected_exit_code = self.read_comparison_file(&reference_folder.join(EXIT_CODE), globals); let exit_code_command = if expected_exit_code == "0" { Assert::success } else { Assert::failure }; exit_code_command(result) } fn assert_std( &self, result_string: &str, path: &str, reference_folder: &Path, globals: &Vec, ) { let path_to_expected_out = reference_folder.join(path); let expected_out = self.read_comparison_file(&path_to_expected_out, globals); let actual = self.format_file(result_string.to_string()); assert_eq!(expected_out, actual, "expected (left) != actual (right)"); } fn read_comparison_file(&self, file: &Path, globals: &Vec) -> String { let original_string = fs::read_to_string(file).unwrap(); let string = insert_globals(original_string, globals); format_string(string) } // Formats content of a file (String) by removing and replacing strings fn format_file(&self, file_content: String) -> String { let info_string = "\u{1b}[38;5;12mINFO\u{1b}[0m".to_string(); let error_string = "\u{1b}[38;5;9mERROR\u{1b}[0m".to_string(); let mut file_not_found = vec![ "The system cannot find the file specified. (os error 2)".to_string(), "program not found".to_string(), "The system cannot find the path specified. (os error 3)".to_string(), "No such file or directory (os error 2)".to_string(), ]; let mut remove_list = vec![info_string, error_string]; remove_list.append(&mut file_not_found); let mut formatted_content = file_content; let regex_list = Self::create_regex_string_replacements(); formatted_content = apply_regex(formatted_content, regex_list); for to_remove in &remove_list { formatted_content = formatted_content.replace(to_remove, ""); } formatted_content = format_string(formatted_content); formatted_content } } pub fn get_abi_cli_version(abi_url: String) -> String { let regex = Regex::new(r".*\d+.\d+.\d+/abi-cli-(?.+)-jar-with-dependencies.jar").unwrap(); let Some(caps) = regex.captures(abi_url.as_str()) else { return "NO_VERSION".to_string(); }; caps["version"].to_string() } pub fn get_abi_version_from_path(abi: &str) -> String { let regex = Regex::new(r".*abi-cli-(?\d+.\d+.\d+)-jar-with-dependencies.jar").unwrap(); let Some(caps) = regex.captures(abi) else { return "NO_VERSION".to_string(); }; caps["version"].to_string() } pub fn get_partisia_cli_version(cli_url: String) -> String { let regex = Regex::new(r".*\d+.\d+.\d+/partisia-cli-(?\d+.\d+.\d+)-jar-with-dependencies.jar") .unwrap(); let Some(caps) = regex.captures(cli_url.as_str()) else { return "NO_VERSION".to_string(); }; caps["version"].to_string() } pub fn get_cli_version_from_path(cli: &str) -> String { let regex = Regex::new(r".*partisia-cli-(?\d+.\d+.\d+)-jar-with-dependencies.jar").unwrap(); let Some(caps) = regex.captures(cli) else { return "NO_VERSION".to_string(); }; caps["version"].to_string() } impl<'a> From<&'a str> for BuildReferenceTestRunner<'a> { /// Get a `BuildReferenceTestRunner` from the resource folder path to the given test. fn from(resource: &'a str) -> Self { BuildReferenceTestRunner { test_path: Path::new(resource), temp_dir: TempDir::new().unwrap(), } } } fn apply_regex(string_input: String, regex_list: Vec) -> String { let mut string_output = string_input; for regex_and_replacement in regex_list { let regex = Regex::new(regex_and_replacement.regex.as_str()).unwrap(); string_output = regex .replace_all(&string_output, regex_and_replacement.replacement) .to_string(); } string_output } pub fn format_string(string_to_format: String) -> String { let mut variable = string_to_format.trim().to_string(); variable = variable.replace("\r\n", "\n"); variable = variable.replace('\\', "/"); variable.replace("file:///tmp", "file:////tmp") } fn netrc_path() -> Option { let path = match env::var_os("NETRC") { Some(path) => Some(PathBuf::from(path)), None => { let home_dir = dirs::home_dir()?; let possible_files = [".netrc", "_netrc"]; let mut possible_paths = possible_files .iter() .map(|possible_file| home_dir.join(possible_file)); possible_paths.find(|path| path.exists()) } }?; Some(path) }