diff --git a/CHANGELOG.md b/CHANGELOG.md index 02cc7ea7..bcaf57aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ incremented upon a breaking change and the patch version will be incremented for ## [Unreleased] ### Added +- feat/support of automatically obtaining fully qualified paths of Data Accounts Custom types for `accounts_snapshots.rs` ([#141](https://github.com/Ackee-Blockchain/trdelnik/pull/141)) - feat/allow direct accounts manipulation and storage ([#142](https://github.com/Ackee-Blockchain/trdelnik/pull/142)) - feat/support of non-corresponding instruction and context names ([#130](https://github.com/Ackee-Blockchain/trdelnik/pull/130)) - feat/refactored and improved program flow during init and build, added activity indicator ([#129](https://github.com/Ackee-Blockchain/trdelnik/pull/129)) diff --git a/Cargo.lock b/Cargo.lock index 92136c40..236c768b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6013,6 +6013,7 @@ dependencies = [ "quinn-proto", "quote", "rand 0.8.5", + "regex", "rstest", "serde", "serde_json", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 7b7d70cc..439151be 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -70,3 +70,4 @@ shellexpand = { workspace = true } pathdiff = "0.2.1" solana-banks-client = "<1.18" indicatif = "0.17.8" +regex = "1.10.3" diff --git a/crates/client/src/cleaner.rs b/crates/client/src/cleaner.rs index 00b8dcea..e012d94a 100644 --- a/crates/client/src/cleaner.rs +++ b/crates/client/src/cleaner.rs @@ -54,7 +54,7 @@ impl Cleaner { fs::remove_dir_all(hfuzz_target_path).await?; } else { println!( - "skipping {}/{}/{}/{} directory: not found", + "{SKIP} [{}/{}/{}/{}] directory not found", TESTS_WORKSPACE_DIRECTORY, FUZZ_TEST_DIRECTORY, FUZZING, HFUZZ_TARGET ) } diff --git a/crates/client/src/commander.rs b/crates/client/src/commander.rs index 8b6f358c..0464d5a6 100644 --- a/crates/client/src/commander.rs +++ b/crates/client/src/commander.rs @@ -1,6 +1,7 @@ use crate::config::Config; +use crate::test_generator::ProgramData; use crate::{ - idl::{self, Idl}, + idl::{self}, Client, }; use fehler::{throw, throws}; @@ -17,6 +18,8 @@ use tokio::{ signal, }; +use crate::constants::*; + #[derive(Error, Debug)] pub enum Error { #[error("{0:?}")] @@ -173,7 +176,7 @@ impl Commander { if let Ok(crash_files) = get_crash_files(&crash_dir, &ext) { if !crash_files.is_empty() { - println!("Error: The crash directory {} already contains crash files from previous runs. \n\nTo run Trdelnik fuzzer with exit code, you must either (backup and) remove the old crash files or alternatively change the crash folder using for example the --crashdir option and the HFUZZ_RUN_ARGS env variable such as:\nHFUZZ_RUN_ARGS=\"--crashdir ./new_crash_dir\"", crash_dir.to_string_lossy()); + println!("{ERROR} The crash directory {} already contains crash files from previous runs. \n\nTo run Trdelnik fuzzer with exit code, you must either (backup and) remove the old crash files or alternatively change the crash folder using for example the --crashdir option and the HFUZZ_RUN_ARGS env variable such as:\nHFUZZ_RUN_ARGS=\"--crashdir ./new_crash_dir\"", crash_dir.to_string_lossy()); process::exit(1); } } @@ -256,7 +259,7 @@ impl Commander { let crash_file = std::path::Path::new(&self.root as &str).join(crash_file_path); if !crash_file.try_exists()? { - println!("The crash file {:?} not found!", crash_file); + println!("{ERROR} The crash file [{:?}] not found", crash_file); throw!(Error::CrashFileNotFound); } @@ -321,7 +324,7 @@ impl Commander { .unwrap(), ); - let msg = format!("\x1b[92mExpanding\x1b[0m: {package_name}... this may take a while"); + let msg = format!("{EXPANDING_PROGRESS_BAR} [{package_name}] ... this may take a while"); progress_bar.set_message(msg); while mutex.load(std::sync::atomic::Ordering::SeqCst) { progress_bar.inc(1); @@ -352,11 +355,8 @@ impl Commander { /// - The expansion of a package fails due to issues in processing its code or IDL (`Error::ReadProgramCodeFailed`). /// - No programs are found after processing all packages (`Error::NoProgramsFound`). #[throws] - pub async fn expand_program_packages( - packages: &[cargo_metadata::Package], - ) -> (Idl, Vec<(String, cargo_metadata::camino::Utf8PathBuf)>) { - let shared_mutex = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); - let shared_mutex_fuzzer = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); + pub async fn expand_program_packages(packages: &[cargo_metadata::Package]) -> Vec { + let shared_mutex_data = std::sync::Arc::new(std::sync::Mutex::new(Vec::new())); for package in packages.iter() { let mutex = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); @@ -365,7 +365,7 @@ impl Commander { let name = package.name.clone(); let mut libs = package.targets.iter().filter(|&t| t.is_lib()); - let lib_path = libs + let path = libs .next() .ok_or(Error::ReadProgramCodeFailed( "Cannot find program library path.".into(), @@ -373,8 +373,7 @@ impl Commander { .src_path .clone(); - let c_shared_mutex = std::sync::Arc::clone(&shared_mutex); - let c_shared_mutex_fuzzer = std::sync::Arc::clone(&shared_mutex_fuzzer); + let c_shared_mutex_data = std::sync::Arc::clone(&shared_mutex_data); let cargo_thread = std::thread::spawn(move || -> Result<(), Error> { let output = Self::expand_package(&name); @@ -387,16 +386,18 @@ impl Commander { if output.status.success() { let code = String::from_utf8(output.stdout).expect("Reading stdout failed"); - let idl_program = idl::parse_to_idl_program(name, &code)?; - let mut vec = c_shared_mutex - .lock() - .expect("Acquire IdlProgram lock failed"); - let mut vec_fuzzer = c_shared_mutex_fuzzer + let program_idl = idl::parse_to_idl_program(name, &code)?; + let mut programs_data = c_shared_mutex_data .lock() - .expect("Acquire Fuzzer data lock failed"); + .expect("Acquire Programs Data lock failed"); + + let program_data = ProgramData { + code, + path, + program_idl, + }; - vec.push(idl_program); - vec_fuzzer.push((code, lib_path)); + programs_data.push(program_data); Ok(()) } else { @@ -409,19 +410,8 @@ impl Commander { Self::expand_progress_bar(&package.name, &mutex); cargo_thread.join().unwrap()?; } - let idl_programs = shared_mutex.lock().unwrap().to_vec(); - let codes_libs_pairs = shared_mutex_fuzzer.lock().unwrap().to_vec(); - - if idl_programs.is_empty() { - throw!(Error::NoProgramsFound); - } else { - ( - Idl { - programs: idl_programs, - }, - codes_libs_pairs, - ) - } + let programs_data = shared_mutex_data.lock().unwrap().to_vec(); + programs_data } /// Executes a cargo command to expand the Rust source code of a specified package. /// diff --git a/crates/client/src/fuzzer/fuzzer_generator.rs b/crates/client/src/fuzzer/fuzzer_generator.rs index 815e02c5..7ed7b515 100644 --- a/crates/client/src/fuzzer/fuzzer_generator.rs +++ b/crates/client/src/fuzzer/fuzzer_generator.rs @@ -1,21 +1,21 @@ use std::collections::HashMap; -use crate::idl::Idl; +use crate::test_generator::ProgramData; use proc_macro2::Ident; use quote::{format_ident, ToTokens}; use syn::{parse_quote, parse_str}; /// Generates `fuzz_instructions.rs` from [Idl] created from Anchor programs. -pub fn generate_source_code(idl: &Idl) -> String { - let code = idl - .programs +pub fn generate_source_code(programs_data: &[ProgramData]) -> String { + let code = programs_data .iter() - .map(|idl_program| { - let program_name = &idl_program.name.snake_case; + .map(|program_data| { + let program_name = &program_data.program_idl.name.snake_case; let fuzz_instructions_module_name = format_ident!("{}_fuzz_instructions", program_name); let module_name: syn::Ident = parse_str(program_name).unwrap(); - let instructions = idl_program + let instructions = program_data + .program_idl .instruction_account_pairs .iter() .fold( @@ -34,7 +34,8 @@ pub fn generate_source_code(idl: &Idl) -> String { ) .into_iter(); - let instructions_data = idl_program + let instructions_data = program_data + .program_idl .instruction_account_pairs .iter() .fold( @@ -110,7 +111,8 @@ pub fn generate_source_code(idl: &Idl) -> String { ) .into_iter(); - let instructions_ixops_impls = idl_program + let instructions_ixops_impls = program_data + .program_idl .instruction_account_pairs .iter() .fold( @@ -184,21 +186,25 @@ pub fn generate_source_code(idl: &Idl) -> String { ) .into_iter(); - let fuzz_accounts = idl_program.instruction_account_pairs.iter().fold( - HashMap::new(), - |mut fuzz_accounts: HashMap, - (_idl_instruction, idl_account_group)| { - idl_account_group.accounts.iter().fold( - &mut fuzz_accounts, - |fuzz_accounts, (name, _ty)| { - let name = format_ident!("{name}"); - fuzz_accounts.entry(name).or_default(); - fuzz_accounts - }, - ); - fuzz_accounts - }, - ); + let fuzz_accounts = program_data + .program_idl + .instruction_account_pairs + .iter() + .fold( + HashMap::new(), + |mut fuzz_accounts: HashMap, + (_idl_instruction, idl_account_group)| { + idl_account_group.accounts.iter().fold( + &mut fuzz_accounts, + |fuzz_accounts, (name, _ty)| { + let name = format_ident!("{name}"); + fuzz_accounts.entry(name).or_default(); + fuzz_accounts + }, + ); + fuzz_accounts + }, + ); // this ensures that the order of accounts is deterministic // so we can use expected generated template within tests diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 75b490a8..9664e93e 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -6,7 +6,6 @@ use std::collections::HashMap; use std::{error::Error, fs::File, io::Read}; use anchor_lang::anchor_syn::{AccountField, Ty}; -use cargo_metadata::camino::Utf8PathBuf; use heck::ToUpperCamelCase; use proc_macro2::{Ident, Span, TokenStream}; use quote::{format_ident, quote, ToTokens}; @@ -16,10 +15,24 @@ use syn::{parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, Pat use anchor_lang::anchor_syn::parser::accounts::parse_account_field; -pub fn generate_snapshots_code(code_path: &[(String, Utf8PathBuf)]) -> Result { - let code = code_path.iter().map(|(code, path)| { +use regex::Regex; + +use crate::idl::find_item_path; + +use crate::constants::*; +use crate::test_generator::ProgramData; + +const ACCOUNT_STRUCT: &str = r"Account<'info,\s*(.*?)\s*>"; +const ACCOUNT_FN: &str = r"anchor_lang::accounts::account::Account<\s*(.*?)\s*>"; + +const ACCOUNT_LOADER_STRUCT: &str = r"AccountLoader<'info,\s*(.*?)\s*>"; +const ACCOUNT_LOADER_FN: &str = + r"anchor_lang::accounts::account_loader::AccountLoader<\s*(.*?)\s*>"; + +pub fn generate_snapshots_code(programs_data: &[ProgramData]) -> Result { + let code = programs_data.iter().map(|program_data| { let mut mod_program = None::; - let mut file = File::open(path).map_err(|e| e.to_string())?; + let mut file = File::open(&program_data.path).map_err(|e| e.to_string())?; let mut content = String::new(); file.read_to_string(&mut content) .map_err(|e| e.to_string())?; @@ -44,11 +57,18 @@ pub fn generate_snapshots_code(code_path: &[(String, Utf8PathBuf)]) -> Result Result Result<(String, String, String), String> { let mut structs = String::new(); let mut impls = String::new(); @@ -104,12 +125,21 @@ fn get_snapshot_structs_and_impls( .map_err(|e| e.to_string())?; let ix_snapshot_name = format_ident!("{}Snapshot", ix_name); - let wrapped_struct = - create_snapshot_struct(&ix_snapshot_name, ctx_struct_item, &fields_parsed) - .unwrap(); - let deser_code = - deserialize_ctx_struct_anchor(&ix_snapshot_name, &fields_parsed) - .map_err(|e| e.to_string())?; + let wrapped_struct = create_snapshot_struct( + &ix_snapshot_name, + ctx_struct_item, + &fields_parsed, + &parse_result, + program_name, + ) + .unwrap(); + let deser_code = deserialize_ctx_struct_anchor( + &ix_snapshot_name, + &fields_parsed, + &parse_result, + program_name, + ) + .map_err(|e| e.to_string())?; structs = format!("{}{}", structs, wrapped_struct.into_token_stream()); impls = format!("{}{}", impls, deser_code.into_token_stream()); unique_ctxs.insert(ctx.clone(), ix_snapshot_name); @@ -210,6 +240,8 @@ fn create_snapshot_struct( snapshot_name: &Ident, orig_struct: &ItemStruct, parsed_fields: &[AccountField], + parsed_file: &syn::File, + program_name: &String, ) -> Result> { let wrapped_fields = match orig_struct.fields.clone() { Fields::Named(named) => { @@ -243,7 +275,7 @@ fn create_snapshot_struct( .starts_with("AccountInfo<"); } else { - println!("\x1b[1;93mWarning\x1b[0m: The context `{}` has a field named `{}` of composite type `{}`. \ + println!("{WARNING} The context `{}` has a field named `{}` of composite type `{}`. \ The automatic deserialization of composite types is currently not supported. You will have \ to implement it manually in the generated `accounts_snapshots.rs` file. The field deserialization \ was replaced by a `todo!()` macro. Also, you might want to adapt the corresponding FuzzInstruction \ @@ -256,9 +288,15 @@ fn create_snapshot_struct( (true, true) => { Ok(quote! {pub #field_name: Option<&'info #field_type>,}) } - (true, _) => Ok(quote! {pub #field_name: Option<#field_type>,}), + (true, _) => { + let field_type = construct_full_path(&field_type.to_token_stream(), parsed_file, program_name).unwrap_or_else(|| field_type.clone()); + Ok(quote! {pub #field_name: Option<#field_type>,}) + }, (_, true) => Ok(quote! {pub #field_name: &'info #field_type,}), - _ => Ok(quote! {pub #field_name: #field_type,}), + _ => { + let field_type = construct_full_path(&field_type.to_token_stream(), parsed_file, program_name).unwrap_or_else(|| field_type.clone()); + Ok(quote! {pub #field_name: #field_type,}) + }, } }); @@ -302,6 +340,8 @@ fn extract_inner_type(field_type: &Type) -> Option<&Type> { fn deserialize_ctx_struct_anchor( snapshot_name: &Ident, parsed_fields: &[AccountField], + parse_result: &syn::File, + program_name: &String, ) -> Result> { let names_deser_pairs: Vec<(TokenStream, TokenStream)> = parsed_fields .iter() @@ -315,6 +355,8 @@ fn deserialize_ctx_struct_anchor( is_optional, return_type, deser_method, + parse_result, + program_name, ), None if matches!(&f.ty, Ty::UncheckedAccount) => { acc_unchecked_tokens(&field_name, is_optional) @@ -443,7 +485,16 @@ fn deserialize_account_tokens( is_optional: bool, return_type: TokenStream, deser_method: TokenStream, + parse_result: &syn::File, + program_name: &String, ) -> TokenStream { + let return_type = if let Some(with_full_path) = + construct_full_path(&return_type, parse_result, program_name) + { + with_full_path.to_token_stream() + } else { + return_type + }; if is_optional { let name_str = name.to_string(); // TODO make this more idiomatic @@ -538,3 +589,177 @@ fn has_program_attribute(attrs: &Vec) -> bool { } false } + +/// Constructs a full path for a given field type within the parsed syntax tree of a Rust file. +/// +/// This function is designed to work with the `Account` and `AccountLoader` structs from the +/// `anchor_lang` crate, resolving their types to fully qualified paths based on the syntax tree +/// provided. It utilizes regular expressions to match against the struct and function syntax for +/// these specific types. +/// +/// # Arguments +/// +/// * `field_type` - A reference to the token stream representing the type of a field. +/// * `parsed_file` - A reference to the parsed file (`syn::File`) containing the Rust source code. +/// * `program_name` - A reference to a string representing the name of the program. +/// +/// # Returns +/// +/// An `Option` which is: +/// - `Some(Type)` where `Type` is the modified type with its path fully qualified, if the type matches +/// the `Account` or `AccountLoader` struct syntax and a corresponding item is found. +/// - `None` if no matching type is found or the type cannot be parsed. +/// +/// # Example +/// +/// Suppose you have a field type `Account<'info, UserData>`, and `UserData` is defined within +/// the file being analyzed. This function will replace `UserData` with its fully qualified path +/// based on the analysis of `parsed_file`, helping with tasks like code generation or analysis +/// where fully qualified paths are required. +fn construct_full_path( + field_type: &TokenStream, + parsed_file: &syn::File, + program_name: &String, +) -> Option { + // Combine regex patterns to match both struct and function syntax for Account and AccountLoader + // this can be obviously extended if needed for further types. + let regex_patterns = [ + (ACCOUNT_STRUCT, ACCOUNT_FN), + (ACCOUNT_LOADER_STRUCT, ACCOUNT_LOADER_FN), + ]; + + // remove spaces in the field_type expression. + let type_as_string = field_type.to_token_stream().to_string().replace(' ', ""); + + regex_patterns + .iter() + .find_map(|(struct_pattern, fn_pattern)| { + // construct regular expressions + let struct_re = Regex::new(struct_pattern).unwrap(); + let fn_re = Regex::new(fn_pattern).unwrap(); + + // check if either of expression matches + struct_re + .captures(&type_as_string) + .or_else(|| fn_re.captures(&type_as_string)) + .and_then(|caps| { + let data_account = caps[1].to_string(); + // there may be inner data account specified as crate::abcd::XYZ + // so due to this we extract the last part, or use whole as default. + let data_account = data_account.split("::").last().unwrap_or(&data_account); + // try to obtain full path + find_item_path(data_account, parsed_file).map(|full_path| { + let full_final_path = format!("{program_name}{full_path}"); + let type_with_full_path = + type_as_string.replace(data_account, &full_final_path); + syn::parse_str::(&type_with_full_path).ok() + }) + }) + }) + .flatten() +} + +#[cfg(test)] +mod tests { + use regex::Regex; + fn extract_type(pattern: &str, text: &str) -> String { + let re = Regex::new(pattern).unwrap(); + match re.captures(text) { + Some(caps) => caps[1].to_string(), + None => String::default(), + } + } + #[test] + fn test_regexp_match1() { + let pattern = super::ACCOUNT_STRUCT; + assert_eq!(extract_type(pattern, "Account<'info, Escrow>,"), "Escrow"); + assert_eq!( + extract_type(pattern, "Option>,"), + "Escrow" + ); + assert_eq!( + extract_type(pattern, "account::Account<'info, abcd::efgh::xyz::Escrow>,"), + "abcd::efgh::xyz::Escrow" + ); + assert_eq!( + extract_type( + pattern, + "Account<'info, abcd::efgh::xyz::Escrow > ," + ), + "abcd::efgh::xyz::Escrow" + ); + } + #[test] + fn test_regexp_match2() { + let pattern = super::ACCOUNT_LOADER_STRUCT; + assert_eq!( + extract_type(pattern, "AccountLoader<'info, Escrow>,"), + "Escrow" + ); + assert_eq!( + extract_type(pattern, "account::AccountLoader<'info, Escrow>,"), + "Escrow" + ); + assert_eq!( + extract_type( + pattern, + "AccountLoader<'info, fuzz_example3::state::Escrow>," + ), + "fuzz_example3::state::Escrow" + ); + assert_eq!( + extract_type( + pattern, + "AccountLoader<'info, abcd::efgh::xyz::Escrow > ," + ), + "abcd::efgh::xyz::Escrow" + ); + } + #[test] + fn test_regexp_match3() { + let pattern = super::ACCOUNT_FN; + assert_eq!( + extract_type(pattern, "anchor_lang::accounts::account::Account,"), + "Escrow" + ); + assert_eq!( + extract_type( + pattern, + "anchor_lang::accounts::account::Account," + ), + "fuzz_example3::state::Escrow" + ); + assert_eq!( + extract_type( + pattern, + "some random text before:anchor_lang::accounts::account::Account< fuzz_example3::state::Escrow >,some random text after:" + ), + "fuzz_example3::state::Escrow" + ); + } + #[test] + fn test_regexp_match4() { + let pattern = super::ACCOUNT_LOADER_FN; + assert_eq!( + extract_type( + pattern, + "anchor_lang::accounts::account_loader::AccountLoader," + ), + "Escrow" + ); + assert_eq!( + extract_type( + pattern, + "anchor_lang::accounts::account_loader::AccountLoader," + ), + "fuzz_example3::state::Escrow" + ); + assert_eq!( + extract_type( + pattern, + "some random text before:anchor_lang::accounts::account_loader::AccountLoader< fuzz_example3::state::Escrow >,some random text after:" + ), + "fuzz_example3::state::Escrow" + ); + } +} diff --git a/crates/client/src/idl.rs b/crates/client/src/idl.rs index 6cf34887..01b9419d 100644 --- a/crates/client/src/idl.rs +++ b/crates/client/src/idl.rs @@ -94,6 +94,7 @@ //! } //! ``` +use crate::constants::*; use heck::{ToSnakeCase, ToUpperCamelCase}; use quote::ToTokens; use syn::{visit::Visit, File}; @@ -126,7 +127,7 @@ struct FullPathFinder { module_pub: Vec, } -fn find_item_path(target_item_name: &str, syn_file: &File) -> Option { +pub fn find_item_path(target_item_name: &str, syn_file: &File) -> Option { let mut finder = FullPathFinder { target_item_name: target_item_name.to_string(), current_module: "".to_string(), @@ -152,7 +153,10 @@ impl<'ast> syn::visit::Visit<'ast> for FullPathFinder { self.found_path = Some(format!("{}::{}", self.current_module, ident)); for x in &self.module_pub { if !x.is_pub { - println!("\nMod: \x1b[91m{}\x1b[0m is private!! \n- modify visibility to \x1b[93mpub\x1b[0m in order to use the custom type inside program_client.", x.mod_name) + println!( + "{WARNING} {} is private. Prefix with pub to access via fully qualified path of {}", + x.mod_name,ident + ); } } return; @@ -548,7 +552,15 @@ pub fn parse_to_idl_program(name: String, code: &str) -> Result String { +pub fn generate_source_code(programs_data: &[ProgramData], use_modules: &[syn::ItemUse]) -> String { let mut output = "// DO NOT EDIT - automatically generated file (except `use` statements inside the `*_instruction` module\n".to_owned(); - let code = idl - .programs + // let code = code_path.into_iter().map(|(_, _, idl_program)| {}); + let code = programs_data .iter() - .map(|idl_program| { - let program_name = &idl_program.name.snake_case; + .map(|program_data| { + let program_name = &program_data.program_idl.name.snake_case; let instruction_module_name = format_ident!("{}_instruction", program_name); let module_name: syn::Ident = parse_str(program_name).unwrap(); - let pubkey_bytes: syn::ExprArray = parse_str(&idl_program.id).unwrap(); + let pubkey_bytes: syn::ExprArray = parse_str(&program_data.program_idl.id).unwrap(); - let instructions = idl_program + let instructions = program_data + .program_idl .instruction_account_pairs .iter() .fold( diff --git a/crates/client/src/test_generator.rs b/crates/client/src/test_generator.rs index 82ba75be..029b3ad2 100644 --- a/crates/client/src/test_generator.rs +++ b/crates/client/src/test_generator.rs @@ -1,7 +1,7 @@ use crate::{ commander::{Commander, Error as CommanderError}, fuzzer, - idl::Idl, + idl::IdlProgram, program_client_generator, snapshot_generator::generate_snapshots_code, }; @@ -91,6 +91,13 @@ macro_rules! load_template { }; } +#[derive(Clone)] +pub struct ProgramData { + pub code: String, + pub path: Utf8PathBuf, + pub program_idl: IdlProgram, +} + /// Represents a generator for creating tests. /// /// This struct is designed to hold all necessary information for generating @@ -100,9 +107,7 @@ macro_rules! load_template { /// # Fields /// - `root`: A `PathBuf` indicating the root directory of the project for which tests are being generated. /// This path is used as a base for any relative paths within the project. -/// - `idl`: An `Idl` struct. This field is used to understand the interfaces -/// and data structures that tests may need to interact with. -/// - `codes_libs_pairs`: A vector of tuples, each containing a `String` and a `Utf8PathBuf`. +/// - `programs_data`: A vector of tuples, each containing a `String`, `Utf8PathBuf` and IDL Program data. /// Each tuple represents a pair of code and the package path associated with it. /// - `packages`: A vector of `Package` structs, representing the different packages /// that make up the project. @@ -110,8 +115,7 @@ macro_rules! load_template { /// should be included in the generated code for .program_client. pub struct TestGenerator { pub root: PathBuf, - pub idl: Idl, - pub codes_libs_pairs: Vec<(String, Utf8PathBuf)>, + pub programs_data: Vec, pub packages: Vec, pub use_tokens: Vec, } @@ -130,8 +134,7 @@ impl TestGenerator { pub fn new() -> Self { Self { root: Path::new("../../").to_path_buf(), - idl: Idl::default(), - codes_libs_pairs: Vec::default(), + programs_data: Vec::default(), packages: Vec::default(), use_tokens: Vec::default(), } @@ -148,8 +151,7 @@ impl TestGenerator { pub fn new_with_root(root: String) -> Self { Self { root: Path::new(&root).to_path_buf(), - idl: Idl::default(), - codes_libs_pairs: Vec::default(), + programs_data: Vec::default(), packages: Vec::default(), use_tokens: Vec::default(), } @@ -260,8 +262,7 @@ impl TestGenerator { #[throws] async fn expand_programs(&mut self) { self.packages = Commander::collect_program_packages().await?; - (self.idl, self.codes_libs_pairs) = - Commander::expand_program_packages(&self.packages).await?; + self.programs_data = Commander::expand_program_packages(&self.packages).await?; } /// Get user specified use statements from .program_client lib. #[throws] @@ -269,7 +270,9 @@ impl TestGenerator { let lib_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY, LIB); if lib_path.exists() { let code = fs::read_to_string(lib_path).await.unwrap_or_else(|_e| { - println!("\x1b[1;93mWarning\x1b[0m: Unable to read .program_client, use statements set to default."); + println!( + "{WARNING} Unable to read [.program_client], use statements set to default" + ); String::default() }); Commander::get_use_statements(&code, &mut self.use_tokens)?; @@ -290,7 +293,7 @@ impl TestGenerator { let lib_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY, LIB); if cargo_path.exists() && src_path.exists() && crate_path.exists() && lib_path.exists() { - println!("\x1b[93mSkipping\x1b[0m: looks like .program_client is already initialized."); + println!("{SKIP} looks like [.program_client] is already initialized"); } else { self.add_program_client().await?; } @@ -322,7 +325,7 @@ impl TestGenerator { directories.retain(|x| x != CARGO_TOML); // if folder structure exists and fuzz_tests directory is not empty we skip if fuzz_tests_manifest_path.exists() && !directories.is_empty() { - println!("\x1b[93mSkipping\x1b[0m: looks like Fuzz Tests are already initialized."); + println!("{SKIP} looks like [Fuzz] Tests are already initialized"); } else { self.add_new_fuzz_test().await? } @@ -349,7 +352,7 @@ impl TestGenerator { && cargo_path.exists() && poc_test_path.exists() { - println!("\x1b[93mSkipping\x1b[0m: looks like PoC Tests are already initialized."); + println!("{SKIP} looks like [PoC] Tests are already initialized"); } else { self.add_new_poc_test().await?; } @@ -364,12 +367,17 @@ impl TestGenerator { /// If not present add poc_tests into the workspace virtual manifest as member #[throws] async fn add_new_poc_test(&self) { - let program_name = if !&self.idl.programs.is_empty() { - &self.idl.programs.first().unwrap().name.snake_case + let program_name = if !&self.programs_data.is_empty() { + &self + .programs_data + .first() + .unwrap() + .program_idl + .name + .snake_case } else { throw!(Error::NoProgramsFound) }; - let poc_dir_path = construct_path!(self.root, TESTS_WORKSPACE_DIRECTORY, POC_TEST_DIRECTORY); let new_poc_test_dir = construct_path!(poc_dir_path, TESTS_DIRECTORY); @@ -409,8 +417,14 @@ impl TestGenerator { /// If not present add fuzz_tests into the workspace virtual manifest as member #[throws] pub async fn add_new_fuzz_test(&self) { - let program_name = if !&self.idl.programs.is_empty() { - &self.idl.programs.first().unwrap().name.snake_case + let program_name = if !&self.programs_data.is_empty() { + &self + .programs_data + .first() + .unwrap() + .program_idl + .name + .snake_case } else { throw!(Error::NoProgramsFound) }; @@ -475,7 +489,7 @@ impl TestGenerator { // create fuzz instructions file let fuzz_instructions_path = new_fuzz_test_dir.join(FUZZ_INSTRUCTIONS_FILE_NAME); - let program_fuzzer = fuzzer::fuzzer_generator::generate_source_code(&self.idl); + let program_fuzzer = fuzzer::fuzzer_generator::generate_source_code(&self.programs_data); let program_fuzzer = Commander::format_program_code(&program_fuzzer).await?; self.create_file(&fuzz_instructions_path, &program_fuzzer) @@ -483,8 +497,8 @@ impl TestGenerator { // // create accounts_snapshots file let accounts_snapshots_path = new_fuzz_test_dir.join(ACCOUNTS_SNAPSHOTS_FILE_NAME); - let fuzzer_snapshots = generate_snapshots_code(&self.codes_libs_pairs) - .map_err(Error::ReadProgramCodeFailed)?; + let fuzzer_snapshots = + generate_snapshots_code(&self.programs_data).map_err(Error::ReadProgramCodeFailed)?; let fuzzer_snapshots = Commander::format_program_code(&fuzzer_snapshots).await?; self.create_file(&accounts_snapshots_path, &fuzzer_snapshots) @@ -534,7 +548,7 @@ impl TestGenerator { .await?; let program_client = - program_client_generator::generate_source_code(&self.idl, &self.use_tokens); + program_client_generator::generate_source_code(&self.programs_data, &self.use_tokens); let program_client = Commander::format_program_code(&program_client).await?; if lib_path.exists() { @@ -592,11 +606,11 @@ impl TestGenerator { match members.iter().find(|&x| x.eq(&new_member)) { Some(_) => { - println!("\x1b[93mSkipping\x1b[0m: {CARGO_TOML}, already contains {member}.") + println!("{SKIP} [{CARGO_TOML}], already contains [{member}]") } None => { members.push(new_member); - println!("\x1b[92mSuccessfully\x1b[0m updated: {CARGO_TOML} with {member} member."); + println!("{FINISH} [{CARGO_TOML}] with [{member}]"); fs::write(cargo, content.to_string()).await?; } }; @@ -631,11 +645,11 @@ impl TestGenerator { match path.exists() { true => { - println!("\x1b[93mSkipping\x1b[0m: {file}, already exists.") + println!("{SKIP} [{file}] already exists") } false => { fs::write(path, content).await?; - println!("\x1b[92mSuccessfully\x1b[0m created: {file}."); + println!("{FINISH} [{file}] created"); } }; } @@ -647,11 +661,11 @@ impl TestGenerator { match path.exists() { true => { fs::write(path, content).await?; - println!("\x1b[92mSuccessfully\x1b[0m updated: {file}."); + println!("{FINISH} [{file}] updated"); } false => { fs::write(path, content).await?; - println!("\x1b[92mSuccessfully\x1b[0m created: {file}."); + println!("{FINISH} [{file}] created"); } }; } @@ -680,9 +694,7 @@ impl TestGenerator { for line in io::BufReader::new(file).lines().flatten() { if line == ignored_path { // INFO do not add the ignored path again if it is already in the .gitignore file - println!( - "\x1b[93mSkipping\x1b[0m: {GIT_IGNORE}, already contains {ignored_path}." - ); + println!("{SKIP} [{GIT_IGNORE}], already contains [{ignored_path}]"); return; } @@ -694,10 +706,10 @@ impl TestGenerator { if let Ok(mut file) = file { writeln!(file, "{}", ignored_path)?; - println!("\x1b[92mSuccessfully\x1b[0m updated: {GIT_IGNORE} with {ignored_path}."); + println!("{FINISH} [{GIT_IGNORE}] update with [{ignored_path}]"); } } else { - println!("\x1b[93mSkipping\x1b[0m: {GIT_IGNORE}, not found.") + println!("{SKIP} [{GIT_IGNORE}], not found") } } /// Adds a new binary target to a Cargo.toml file. diff --git a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs index e43b533c..a58794fa 100644 --- a/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs +++ b/crates/client/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs @@ -1,9 +1,10 @@ +use fuzz_example3::ID as PROGRAM_ID; use trdelnik_client::anchor_lang::{self, prelude::*}; use trdelnik_client::fuzzing::FuzzingError; pub struct InitVestingSnapshot<'info> { pub sender: Signer<'info>, pub sender_token_account: Account<'info, TokenAccount>, - pub escrow: Option>, + pub escrow: Option>, pub escrow_token_account: Account<'info, TokenAccount>, pub mint: Account<'info, Mint>, pub token_program: Program<'info, Token>, @@ -12,7 +13,7 @@ pub struct InitVestingSnapshot<'info> { pub struct WithdrawUnlockedSnapshot<'info> { pub recipient: Signer<'info>, pub recipient_token_account: Account<'info, TokenAccount>, - pub escrow: Option>, + pub escrow: Option>, pub escrow_token_account: Account<'info, TokenAccount>, pub escrow_pda_authority: &'info AccountInfo<'info>, pub mint: Account<'info, Mint>, @@ -45,22 +46,24 @@ impl<'info> InitVestingSnapshot<'info> { .map_err(|_| { FuzzingError::CannotDeserializeAccount("sender_token_account".to_string()) })?; - let escrow: Option> = accounts_iter - .next() - .ok_or(FuzzingError::NotEnoughAccounts("escrow".to_string()))? - .as_ref() - .map(|acc| { - if acc.key() != PROGRAM_ID { - anchor_lang::accounts::account::Account::try_from(acc) - .map_err(|_| FuzzingError::CannotDeserializeAccount("escrow".to_string())) - } else { - Err(FuzzingError::OptionalAccountNotProvided( - "escrow".to_string(), - )) - } - }) - .transpose() - .unwrap_or(None); + let escrow: Option> = + accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts("escrow".to_string()))? + .as_ref() + .map(|acc| { + if acc.key() != PROGRAM_ID { + anchor_lang::accounts::account::Account::try_from(acc).map_err(|_| { + FuzzingError::CannotDeserializeAccount("escrow".to_string()) + }) + } else { + Err(FuzzingError::OptionalAccountNotProvided( + "escrow".to_string(), + )) + } + }) + .transpose() + .unwrap_or(None); let escrow_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() @@ -135,22 +138,24 @@ impl<'info> WithdrawUnlockedSnapshot<'info> { .map_err(|_| { FuzzingError::CannotDeserializeAccount("recipient_token_account".to_string()) })?; - let escrow: Option> = accounts_iter - .next() - .ok_or(FuzzingError::NotEnoughAccounts("escrow".to_string()))? - .as_ref() - .map(|acc| { - if acc.key() != PROGRAM_ID { - anchor_lang::accounts::account::Account::try_from(acc) - .map_err(|_| FuzzingError::CannotDeserializeAccount("escrow".to_string())) - } else { - Err(FuzzingError::OptionalAccountNotProvided( - "escrow".to_string(), - )) - } - }) - .transpose() - .unwrap_or(None); + let escrow: Option> = + accounts_iter + .next() + .ok_or(FuzzingError::NotEnoughAccounts("escrow".to_string()))? + .as_ref() + .map(|acc| { + if acc.key() != PROGRAM_ID { + anchor_lang::accounts::account::Account::try_from(acc).map_err(|_| { + FuzzingError::CannotDeserializeAccount("escrow".to_string()) + }) + } else { + Err(FuzzingError::OptionalAccountNotProvided( + "escrow".to_string(), + )) + } + }) + .transpose() + .unwrap_or(None); let escrow_token_account: anchor_lang::accounts::account::Account = accounts_iter .next() diff --git a/crates/client/tests/test_fuzz.rs b/crates/client/tests/test_fuzz.rs index e7118049..ed0863d4 100644 --- a/crates/client/tests/test_fuzz.rs +++ b/crates/client/tests/test_fuzz.rs @@ -2,41 +2,13 @@ use anyhow::Error; use cargo_metadata::camino::Utf8PathBuf; use fehler::throws; use pretty_assertions::assert_str_eq; +use trdelnik_client::test_generator::ProgramData; const PROGRAM_NAME: &str = "fuzz_example3"; #[throws] #[tokio::test] -async fn test_fuzz_instructions() { - let expanded_fuzz_example3 = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/test_data/expanded_source_codes/expanded_fuzz_example3.rs" - )); - - let expected_fuzz_instructions_code = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/tests/test_data/expected_source_codes/expected_fuzz_instructions.rs" - )); - - let program_idl = trdelnik_client::idl::parse_to_idl_program( - PROGRAM_NAME.to_owned(), - expanded_fuzz_example3, - )?; - - let idl = trdelnik_client::idl::Idl { - programs: vec![program_idl], - }; - - let fuzz_instructions_code = trdelnik_client::fuzzer_generator::generate_source_code(&idl); - let fuzz_instructions_code = - trdelnik_client::Commander::format_program_code(&fuzz_instructions_code).await?; - - assert_str_eq!(fuzz_instructions_code, expected_fuzz_instructions_code); -} - -#[throws] -#[tokio::test] -async fn test_account_snapshots() { +async fn test_snapshots_and_instructions() { let expanded_fuzz_example3 = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/test_data/expanded_source_codes/expanded_fuzz_example3.rs" @@ -46,6 +18,10 @@ async fn test_account_snapshots() { env!("CARGO_MANIFEST_DIR"), "/tests/test_data/expected_source_codes/expected_accounts_snapshots.rs" )); + let expected_fuzz_instructions_code = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/test_data/expected_source_codes/expected_fuzz_instructions.rs" + )); let mut program_path = std::env::current_dir() .unwrap() @@ -57,14 +33,33 @@ async fn test_account_snapshots() { let path = Utf8PathBuf::from(program_path); - let codes_libs_pairs = vec![(expanded_fuzz_example3.to_string(), path)]; + let program_idl = trdelnik_client::idl::parse_to_idl_program( + PROGRAM_NAME.to_owned(), + expanded_fuzz_example3, + )?; + + let code = expanded_fuzz_example3.to_string(); + + let program_data = ProgramData { + code, + path, + program_idl, + }; + + let program_data = vec![program_data]; let fuzzer_snapshots = - trdelnik_client::snapshot_generator::generate_snapshots_code(&codes_libs_pairs).unwrap(); + trdelnik_client::snapshot_generator::generate_snapshots_code(&program_data).unwrap(); let fuzzer_snapshots = trdelnik_client::Commander::format_program_code(&fuzzer_snapshots).await?; + let fuzz_instructions_code = + trdelnik_client::fuzzer_generator::generate_source_code(&program_data); + let fuzz_instructions_code = + trdelnik_client::Commander::format_program_code(&fuzz_instructions_code).await?; + assert_str_eq!(fuzzer_snapshots, expected_accounts_snapshots); + assert_str_eq!(fuzz_instructions_code, expected_fuzz_instructions_code); } #[throws] diff --git a/crates/client/tests/test_program_client.rs b/crates/client/tests/test_program_client.rs index d29dc48b..7ec74905 100644 --- a/crates/client/tests/test_program_client.rs +++ b/crates/client/tests/test_program_client.rs @@ -1,6 +1,7 @@ use anyhow::Error; use fehler::throws; use pretty_assertions::assert_str_eq; +use trdelnik_client::test_generator::ProgramData; #[throws] #[tokio::test] @@ -8,11 +9,14 @@ pub async fn generate_program_client() { // Generate with this command: // `trdelnik/examples/escrow/programs/escrow$ cargo expand > escrow_expanded.rs` // and the content copy to `test_data/expanded_escrow.rs` - let expanded_escrow = include_str!(concat!( + let code = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/tests/test_data/expanded_source_codes/expanded_escrow.rs" )); + // for this test we do not need path + let path = String::default(); + // You can copy the content from the `program_client` crate from an example // after you've called `makers trdelnik test`. let expected_client_code = include_str!(concat!( @@ -20,16 +24,20 @@ pub async fn generate_program_client() { "/tests/test_data/expected_source_codes/expected_program_client_code.rs" )); - let program_idl = - trdelnik_client::idl::parse_to_idl_program("escrow".to_owned(), expanded_escrow)?; + let program_idl = trdelnik_client::idl::parse_to_idl_program("escrow".to_owned(), code)?; - let idl = trdelnik_client::idl::Idl { - programs: vec![program_idl], + let program_data = ProgramData { + code: code.to_string(), + path: path.into(), + program_idl, }; + let program_data = vec![program_data]; let use_modules: Vec = vec![syn::parse_quote! { use trdelnik_client::*; }]; - let client_code = - trdelnik_client::program_client_generator::generate_source_code(&idl, &use_modules); + let client_code = trdelnik_client::program_client_generator::generate_source_code( + &program_data, + &use_modules, + ); let client_code = trdelnik_client::Commander::format_program_code(&client_code).await?; assert_str_eq!(client_code, expected_client_code);