//! Utilities for using resource tests. The two main methods are `setup_resource_test` and //! `assert_resource_test`. The setup methods copies the test resource into a temp directory. //! After the test has been run, the temp directory can be compared against a reference folder //! using `assert_resource_test`. //! The folder structure for the resource tests is `test_resource_folder`/source and //! `test_resource_folder`/reference. //! //! This utility can also generate the resource folder instead of running a comparison. //! To do this set the environment variable `TEST_REF_REGEN=1`. use crate::test_util::build_reference_test_runner::{ format_string, get_abi_cli_version, get_abi_version_from_path, get_cli_version_from_path, get_partisia_cli_version, GlobalReplacement, }; use crate::test_util::constants::{ABI_CLI, PARTISIA_CLI}; use crate::test_util::jar_util::get_jar_url; use predicates::prelude::predicate; use predicates::Predicate; use std::fs; use std::fs::{copy, read, File}; use std::path::{Path, PathBuf}; use toml_edit::DocumentMut; /// Initialize the tempdir with the files in `test_resource_folder`/source. /// /// ### Parameters: /// /// * `tempdir_path`: &[`Path`], the directory to copy the reference files to. /// /// * `test_resource_folder`: &[`Path`], the path to the resource folder to copy reference files from. /// /// ### Returns: /// The full path to the test directory. pub fn setup_resource_test( tempdir_path: &Path, test_resource_folder: &Path, globals: &Vec, ) -> PathBuf { let source_folder = test_resource_folder.join("source"); copy_dir_all(&source_folder, tempdir_path, globals); PathBuf::from(tempdir_path) } fn copy_dir_all(source: &Path, destination: &Path, map_of_globals: &Vec) { fs::create_dir_all(destination).unwrap(); if let Ok(dir) = fs::read_dir(source) { for entry in dir { let entry = entry.unwrap(); if entry.file_type().unwrap().is_dir() { copy_dir_all( &entry.path(), &destination.join(entry.file_name()), map_of_globals, ); } else { let file_name = entry.file_name().to_str().unwrap().to_string(); if file_name.ends_with(".txt") || file_name.ends_with(".toml") { copy_and_insert_globals( entry.path(), destination.join(entry.file_name()), map_of_globals, ); } copy(entry.path(), destination.join(entry.file_name())).unwrap(); } } } } fn copy_and_insert_globals, Q: AsRef>( from: P, to: Q, map_of_globals: &Vec, ) { let content = fs::read_to_string(from).unwrap(); let content_with_globals = insert_globals(content, map_of_globals); fs::write(to, content_with_globals.as_bytes()).unwrap(); } /// Replace global values in `original`. /// /// ### Parameters: /// /// * `original`: [`String`], the original string to replace the globals in. /// /// ### Returns: /// The given string with globals replaced. pub fn insert_globals(original: String, map_of_globals: &Vec) -> String { // Handle paths let mut string = original; for global in map_of_globals { string = string.replace(global.identifier, global.in_file_value.as_str()); } string } /// Remove global values in `original`. /// /// ### Parameters: /// /// * `original`: [`String`], the original string to remove the globals in. /// /// ### Returns: /// The given string with globals removed. fn copy_and_remove_globals, Q: AsRef>( from: P, to: Q, globals: &&Vec, ) { if let Ok(content) = fs::read_to_string(&from) { let content_with_globals = remove_globals(content, globals); fs::write(to, content_with_globals.as_bytes()).unwrap(); } else { fs::copy(from, to).unwrap(); } } /// Remove global values in `original`. /// /// ### Parameters: /// /// * `original`: [`String`], the original string to remove the globals in. /// /// ### Returns: /// The given string with globals removed. pub fn remove_globals(original: String, globals: &Vec) -> String { // Handle paths let mut string = original; for global in globals { string = string.replace(global.in_file_value.as_str(), global.identifier); } string } /// Asserts all files in the tempdir_path against the reference files in `test_resource_folder`/reference. /// If the environement variable `TEST_REF_REGEN` is set, then instead of asserting /// the files this method generates the reference folder based on the actual result /// in the `tempdir_path`. /// /// ### Parameters: /// /// * `tempdir_path`: &[`Path`], the directory to with files to assert against the reference files. /// /// * `ignored_files`: [`Vec`], the files to not make assertions on. /// /// * `test_resource_folder`: &[`Path`], the path to the resource folder to get reference files from. pub fn assert_resource_test( tempdir_path: &Path, test_resource_folder: &Path, globals: &Vec, ) { let reference_folder = test_resource_folder.join("reference"); walk_dir_and_apply_to_files( &reference_folder, tempdir_path, globals, assert_file, &Vec::new(), ); } /// Asserts all files in the tempdir_path against the reference files in `test_resource_folder`/reference. /// If the environement variable `TEST_REF_REGEN` is set, then instead of asserting /// the files this method generates the reference folder based on the actual result /// in the `tempdir_path`. /// /// ### Parameters: /// /// * `tempdir_path`: &[`Path`], the directory to with files to assert against the reference files. /// /// * `ignored_files`: [`Vec`], the files to not make assertions on. /// /// * `test_resource_folder`: &[`Path`], the path to the resource folder to get reference files from. pub fn generate_new_reference_file( actual: &Path, reference_location: &Path, globals: &&Vec, ) { let fail_string = format!("Failed to create reference file at {reference_location:?}"); let create_empty_file = |file_name: String| { fs::create_dir_all(reference_location.parent().unwrap()).expect(&fail_string); let mut new_path = reference_location.to_owned(); if !file_name.is_empty() { new_path.set_file_name(file_name); } File::create(new_path).expect(&fail_string); }; let copy_file = || { fs::create_dir_all(reference_location.parent().unwrap()).expect(&fail_string); copy_and_remove_globals(actual, reference_location, globals); }; if let Some(extension) = actual.extension() { match extension.to_str().unwrap() { "jar" => { let file_name = reference_location .file_name() .expect(&fail_string) .to_str() .unwrap(); let mut new_name = file_name.parse().unwrap(); if file_name.contains("partisia-cli") && get_partisia_cli_version(get_jar_url(PARTISIA_CLI)) == get_cli_version_from_path(file_name) { new_name = "partisia-cli-PARTISIA-CLI-VERSION-jar-with-dependencies.jar".to_string(); } else if file_name.contains("abi-cli") && get_abi_cli_version(get_jar_url(ABI_CLI)) == get_abi_version_from_path(file_name) { new_name = "abi-cli-ABI-CLI-VERSION-jar-with-dependencies.jar".to_string(); } create_empty_file(new_name.parse().unwrap()); } "pbc" | "zkwa" | "wasm" | "exe" => { let parent = actual.parent().unwrap(); if parent.ends_with("wasm32-unknown-unknown/debug") | parent.ends_with("wasm32-unknown-unknown/release") { create_empty_file("".parse().unwrap()); } } "abi" => { copy_file(); } "txt" | "err" | "out" => { copy_file(); } "toml" => { if actual.ends_with("config.toml") || (actual.ends_with("Cargo.toml") && !actual.ends_with("native-contract-runner/Cargo.toml")) { copy_file(); } } "rs" => { let parent_path = actual.parent().unwrap(); let path_string = parent_path.to_string_lossy().to_string(); if !path_string.contains("target") || actual.ends_with("native-contract-runner/src/main.rs") { copy_file(); } } _ => {} } } } fn assert_file(reference: &Path, actual: &Path, globals: &&Vec) { if !reference.exists() { return; } match reference.extension().unwrap().to_str().unwrap() { "toml" => { assert_toml_files(actual, reference, globals); } "wasm" => { assert_wellformed_wasm_file(actual); } "zkwa" => { assert_wellformed_zkwa_file(actual); } "pbc" => { assert!(actual.exists()); } "abi" => { assert_abi_files(actual, reference); } "rs" => { assert_rs_files(actual, reference); } "jar" => { assert_jar_files(actual, reference, globals); } "exe" => { assert_executable_files(actual); } _s => { // panic!("Assert on unsupported file type: {s}"); } } } /// Apply the function `func` to all files in the `reference_root` and `tempdir_root` recursively. /// /// ### Parameters: /// /// * `reference_root`: &[`Path`], the reference directory. /// /// * `tempdir_root`: &[`Path`], the tempdir directory. /// /// * `func`: [`fn(reference: &Path, actual: &Path)`], function applied to each file in the /// `reference_root` together with the corresponding file in `tempdir_root`. /// /// * `ignored_files`: &[`Vec`], files that where `func` will not be applied. pub fn walk_dir_and_apply_to_files( reference_root: &Path, tempdir_root: &Path, globals: &Vec, func: fn(&Path, &Path, &&Vec), ignored_files: &Vec, ) { if Path::exists(reference_root) { for entry in fs::read_dir(reference_root).unwrap() { let entry = entry.unwrap(); let tempdir_path = tempdir_root.join(entry.file_name()); if entry.file_type().unwrap().is_dir() { walk_dir_and_apply_to_files( &entry.path(), &tempdir_path, globals, func, ignored_files, ); } else if !(ignored_files.contains(&tempdir_path) || ignored_files.contains(&entry.path())) { func(&entry.path(), &tempdir_path, &globals); } } } } /// Compare abi-file with a reference file. Panics if the files aren't equal. /// /// ### Parameters: /// /// * `actual`: [`&Path`], the path to the abi file to compare. /// /// * `expected`: [`&Path`], the path to the reference file to compare against. pub fn assert_abi_files(actual: &Path, expected: &Path) { test_equality(actual, expected); } /// Checks that a jar-file exists. /// /// ### Parameters: /// /// * `actual`: [`&Path`], the path to the jar file to check. /// * `globals`: [`&Vec`], a list of strings to replace in the file name. pub fn assert_jar_files(actual: &Path, expected: &Path, globals: &Vec) { let mut actual_file_name = actual .file_name() .expect("Expect to find file name to compare.") .to_str() .unwrap() .to_string(); actual_file_name = insert_globals(actual_file_name, globals); let mut expected_file_name = expected .file_name() .expect("Expect to find file name of reference file to compare with.") .to_str() .unwrap() .to_string(); expected_file_name = insert_globals(expected_file_name, globals); assert!(expected.exists()); assert_eq!(actual_file_name, expected_file_name); } /// Checks that an executable exists. /// /// ### Parameters: /// /// * `actual`: [`&Path`], the path to the executable to check. fn assert_executable_files(actual: &Path) { assert!( actual.exists() || actual.with_extension("").exists(), "{actual:?} does not exist" ); } /// Check that a wasm-file is well formed. Panics if the file does not exist. /// /// ### Parameters: /// /// * `wasm_file`: [`&Path`], the path to the wasm file under test. pub fn assert_wellformed_wasm_file(wasm_file: &Path) { assert!(wasm_file.exists()); let bytes = read(wasm_file).unwrap(); assert_wasm_bytes(bytes); } fn assert_wasm_bytes(wasm_bytes: Vec) { assert!(wasm_bytes.starts_with(b"\0asm")); } /// Check that a zkwa-file is well formed. Panics if the file does not exist. /// /// ### Parameters: /// /// * `zkwa_file`: [`&Path`], the path to the zkwa file under test. pub fn assert_wellformed_zkwa_file(zkwa_file: &Path) { assert!(zkwa_file.exists()); let mut bytes = read(zkwa_file).unwrap().into_iter(); // See https://gitlab.com/partisiablockchain/language/zk-compiler/-/blob/main/src/main/java/com/partisiablockchain/language/zkcompiler/cliparser/CliParser.java#L116 // wasm_magic_byte assert_eq!(bytes.next(), Some(2)); let wasm_length: u32 = pop_n::<4, u8>(&mut bytes).map(u32::from_be_bytes).unwrap(); let wasm_bytes: Vec = pop_n_vec(&mut bytes, wasm_length as usize); assert_wasm_bytes(wasm_bytes); // zk_magic_byte assert_eq!(bytes.next(), Some(3)); let zkmc_length: u32 = pop_n(&mut bytes).map(u32::from_be_bytes).unwrap(); assert_eq!(bytes.len(), zkmc_length as usize); assert_zkmc_bytes(bytes.collect()); } fn pop_n(iter: &mut impl Iterator) -> Option<[T; N]> { iter.take(N).collect::>().try_into().ok() } fn pop_n_vec(iter: &mut impl Iterator, n: usize) -> Vec { iter.take(n).collect::>() } fn assert_zkmc_bytes(zkmc_bytes: Vec) { assert!(zkmc_bytes.starts_with(b"ZKMC")); } /// Compare toml-file with a reference file. /// Compares all tables and comments beside package.metadata.partisiablockchain. /// /// ### Parameters: /// /// * `actual`: [`&Path`], the path to the toml file to compare. /// /// * `expected`: [`&Path`], the path to the reference file to compare against. pub fn assert_toml_files(actual: &Path, expected: &Path, globals: &&Vec) { let actual_content = fs::read_to_string(actual).unwrap(); let mut actual_doc = actual_content.parse::().unwrap(); let expected_content = fs::read_to_string(expected).unwrap(); let mut expected_doc = expected_content.parse::().unwrap(); if let Some(actual_metadata) = actual_doc["package"]["metadata"].as_table_mut() { actual_metadata.remove("partisiablockchain"); } if let Some(expected_metadata) = expected_doc["package"]["metadata"].as_table_mut() { expected_metadata.remove("partisiablockchain"); } let mut actual_string = insert_globals(actual_doc.to_string(), globals); let mut expected_string = insert_globals(expected_doc.to_string(), globals); actual_string = format_string(actual_string); expected_string = format_string(expected_string); assert_eq!(actual_string, expected_string); } /// Compare rust file with a reference file. Panics if the files aren't equal. /// /// ### Parameters: /// /// * `actual`: [`&Path`], the path to the rust file to compare. /// /// * `expected`: [`&Path`], the path to the reference file to compare against. pub fn assert_rs_files(actual: &Path, expected: &Path) { test_equality(actual, expected); } /// Test that two files are completely equal. /// /// ### Parameters: /// /// * `actual`: [`&Path`], the path to the file to compare. /// /// * `expected`: [`&Path`], the path to the reference file to compare against. fn test_equality(actual: &Path, expected: &Path) { assert!(actual.exists(), "{actual:?} does not exits"); assert!(expected.exists(), "{expected:?} does not exits"); let predicate = predicate::path::eq_file(expected); assert!( predicate.eval(actual), "{actual:?} does not equal {expected:?}" ); }