From 5bbdd817a779e2f530f0bec5583a25ea9b308477 Mon Sep 17 00:00:00 2001 From: lukacan Date: Thu, 15 Feb 2024 22:57:05 +0100 Subject: [PATCH 1/8] =?UTF-8?q?=E2=9C=A8=20Refactored=20program=20flow=20d?= =?UTF-8?q?uring=20init=20and=20build?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Init template updates From b1fd35701c6f42f6a9de42313ad7f047a896ccf4 Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 21 Feb 2024 23:39:41 +0100 Subject: [PATCH 2/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20testgenerator=20and?= =?UTF-8?q?=20commander=20revision=20+=20add=20simple=20progress=20bar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 update fuzz command fn name --- crates/cli/src/command/fuzz.rs | 2 +- crates/client/src/commander.rs | 287 ++++++----- crates/client/src/test_generator.rs | 753 +++++++++++----------------- 3 files changed, 470 insertions(+), 572 deletions(-) diff --git a/crates/cli/src/command/fuzz.rs b/crates/cli/src/command/fuzz.rs index ea357b8f..9e1e9519 100644 --- a/crates/cli/src/command/fuzz.rs +++ b/crates/cli/src/command/fuzz.rs @@ -68,7 +68,7 @@ pub async fn fuzz(root: Option, subcmd: FuzzCommand) { // generate generator with root so that we do not need to again // look for root within the generator let mut generator = TestGenerator::new_with_root(root); - generator.add_new_fuzz_test().await?; + generator.add_fuzz_test().await?; } }; } diff --git a/crates/client/src/commander.rs b/crates/client/src/commander.rs index 274e74f2..e10319e8 100644 --- a/crates/client/src/commander.rs +++ b/crates/client/src/commander.rs @@ -3,10 +3,7 @@ use crate::{ idl::{self, Idl}, Client, }; -use cargo_metadata::camino::Utf8PathBuf; -use cargo_metadata::{MetadataCommand, Package}; use fehler::{throw, throws}; -use futures::future::try_join_all; use log::debug; use solana_sdk::signer::keypair::Keypair; use std::path::PathBuf; @@ -19,7 +16,6 @@ use tokio::{ process::{Child, Command}, signal, }; -use toml::Value; #[derive(Error, Debug)] pub enum Error { @@ -49,6 +45,8 @@ pub enum Error { NotInitialized, #[error("the crash file does not exist")] CrashFileNotFound, + #[error("The Solana project does not contain any programs")] + NoProgramsFound, } /// Localnet (the validator process) handle. @@ -282,16 +280,17 @@ impl Commander { eprintln!("cannot execute \"cargo hfuzz run-debug\" command"); } - - /// Returns an [Iterator] of program [Package]s read from `Cargo.toml` files. - pub fn program_packages(&self) -> impl Iterator { - let cargo_toml_data = MetadataCommand::new() + pub fn program_packages() -> impl Iterator { + let cargo_toml_data = cargo_metadata::MetadataCommand::new() .no_deps() .exec() .expect("Cargo.toml reading failed"); cargo_toml_data.packages.into_iter().filter(|package| { - // @TODO less error-prone test if the package is a _program_? + // TODO less error-prone test if the package is a _program_? + // This will only consider Packages where path: + // /home/xyz/xyz/trdelnik/trdelnik/examples/example_project/programs/package1 + // NOTE we can obtain more important information here, only to remember if let Some("programs") = package.manifest_path.iter().nth_back(2) { return true; } @@ -299,87 +298,175 @@ impl Commander { }) } - /// Updates the `program_client` dependencies. - /// - /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. #[throws] - pub async fn get_programs_deps(&self) -> Vec { - // let trdelnik_dep = r#"trdelnik-client = "0.5.0""#.parse().unwrap(); - // @TODO replace the line above with the specific version or commit hash - // when Trdelnik is released or when its repo is published. - // Or use both variants - path for Trdelnik repo/dev and version/commit for users. - // Some related snippets: - // - // println!("Trdelnik Version: {}", std::env!("VERGEN_BUILD_SEMVER")); - // println!("Trdelnik Commit: {}", std::env!("VERGEN_GIT_SHA")); - // https://docs.rs/vergen/latest/vergen/#environment-variables - // - // `trdelnik = "0.1.0"` - // `trdelnik = { git = "https://github.com/Ackee-Blockchain/trdelnik.git", rev = "cf867aea87e67d7be029982baa39767f426e404d" }` - - let absolute_root = fs::canonicalize(self.root.as_ref()).await?; - - let program_deps = self - .program_packages() - .map(|package| { - let name = package.name; - let path = package - .manifest_path - .parent() - .unwrap() - .strip_prefix(&absolute_root) - .unwrap(); - format!(r#"{name} = {{ path = "../{path}", features = ["no-entrypoint"] }}"#) - .parse() - .unwrap() - }) - .collect(); - program_deps + pub async fn collect_program_packages() -> Vec { + let packages: Vec = Commander::program_packages().collect(); + if packages.is_empty() { + throw!(Error::NoProgramsFound) + } else { + packages + } + } + fn expand_progress_bar( + package_name: &str, + mutex: &std::sync::Arc, + ) { + let progress_bar = indicatif::ProgressBar::new_spinner(); + progress_bar.set_style( + indicatif::ProgressStyle::default_spinner() + .template("{spinner} {wide_msg}") + .unwrap(), + ); - // @TODO remove renamed or deleted programs from deps? + let msg = format!("Expanding: {package_name}... this may take a while"); + progress_bar.set_message(msg); + while mutex.load(std::sync::atomic::Ordering::SeqCst) { + progress_bar.inc(1); + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + progress_bar.finish_and_clear(); } + #[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())); - /// Updates the `program_client` `lib.rs`. - /// - /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. + for package in packages.iter() { + let mutex = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let c_mutex = std::sync::Arc::clone(&mutex); + + let name = package.name.clone(); + + let mut libs = package.targets.iter().filter(|&t| t.is_lib()); + let lib_path = libs + .next() + .ok_or(Error::ReadProgramCodeFailed( + "Cannot find program library path.".into(), + ))? + .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 cargo_thread = std::thread::spawn(move || -> Result<(), Error> { + let output = Self::expand_package(&name); + + if output.status.success() { + let code = String::from_utf8(output.stdout).unwrap(); + + let idl_program = idl::parse_to_idl_program(name, &code)?; + let mut vec = c_shared_mutex.lock().unwrap(); + let mut vec_fuzzer = c_shared_mutex_fuzzer.lock().unwrap(); + + vec.push(idl_program); + vec_fuzzer.push((code, lib_path)); + + c_mutex.store(false, std::sync::atomic::Ordering::SeqCst); + Ok(()) + } else { + let error_text = String::from_utf8(output.stderr).unwrap(); + c_mutex.store(false, std::sync::atomic::Ordering::SeqCst); + Err(Error::ReadProgramCodeFailed(error_text)) + } + }); + + 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, + ) + } + } + fn expand_package(package_name: &str) -> std::process::Output { + std::process::Command::new("cargo") + .arg("+nightly") + .arg("rustc") + .args(["--package", package_name]) + .arg("--profile=check") + .arg("--") + .arg("-Zunpretty=expanded") + .output() + .unwrap() + } + + /// Returns `use` modules / statements + /// The goal of this method is to find all `use` statements defined by the user in the `.program_client` + /// crate. It solves the problem with regenerating the program client and removing imports defined by + /// the user. #[throws] - pub async fn get_programs_source_codes(&self) -> (Idl, Vec<(String, Utf8PathBuf)>) { - let program_idls_codes = self.program_packages().map(|package| async move { - let name = package.name; - let output = Command::new("cargo") - .arg("+nightly-2023-12-28") - .arg("rustc") - .args(["--package", &name]) - .arg("--profile=check") - .arg("--") - .arg("-Zunpretty=expanded") - .output() - .await?; + pub async fn expand_program_client() -> Vec { + let shared_mutex = std::sync::Arc::new(std::sync::Mutex::new(String::new())); + + let mutex = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); + let c_mutex = std::sync::Arc::clone(&mutex); + + let c_shared_mutex = std::sync::Arc::clone(&shared_mutex); + + let cargo_thread = std::thread::spawn(move || -> Result<(), Error> { + let output = Self::expand_package("program_client"); + if output.status.success() { - let code = String::from_utf8(output.stdout)?; - let mut libs = package.targets.iter().filter(|&t| t.is_lib()); - let lib_path = libs - .next() - .ok_or(Error::ReadProgramCodeFailed( - "Cannot find program library path.".into(), - ))? - .src_path - .clone(); - Ok(( - idl::parse_to_idl_program(name, &code).await?, - (code, lib_path), - )) + let mut code = c_shared_mutex.lock().unwrap(); + code.push_str(&String::from_utf8(output.stdout)?); + + c_mutex.store(false, std::sync::atomic::Ordering::SeqCst); + Ok(()) } else { - let error_text = String::from_utf8(output.stderr)?; - Err(Error::ReadProgramCodeFailed(error_text)) + // command failed leave unmodified + c_mutex.store(false, std::sync::atomic::Ordering::SeqCst); + Ok(()) } }); - let (program_idls, codes_libs_pairs): (Vec<_>, Vec<_>) = - try_join_all(program_idls_codes).await?.into_iter().unzip(); - let idl = Idl { - programs: program_idls, - }; - (idl, codes_libs_pairs) + + Self::expand_progress_bar("program_client", &mutex); + + cargo_thread.join().unwrap()?; + + let code = shared_mutex.lock().unwrap(); + let code = code.as_str(); + let mut use_modules: Vec = vec![]; + if code.is_empty() { + use_modules.push(syn::parse_quote! { use trdelnik_client::*; }) + } else { + Self::get_use_statements(code, &mut use_modules)?; + if use_modules.is_empty() { + use_modules.push(syn::parse_quote! { use trdelnik_client::*; }) + } + } + use_modules + } + + #[throws] + pub fn get_use_statements(code: &str, use_modules: &mut Vec) { + for item in syn::parse_file(code).unwrap().items.into_iter() { + if let syn::Item::Mod(module) = item { + let modules = module + .content + .ok_or("account mod: empty content") + .unwrap() + .1 + .into_iter(); + for module in modules { + if let syn::Item::Use(u) = module { + use_modules.push(u); + } + } + } + } } /// Formats program code. @@ -419,44 +506,6 @@ impl Commander { solana_test_validator_process: process, } } - - /// Returns `use` modules / statements - /// The goal of this method is to find all `use` statements defined by the user in the `.program_client` - /// crate. It solves the problem with regenerating the program client and removing imports defined by - /// the user. - #[throws] - pub async fn parse_program_client_imports(&self) -> Vec { - let output = Command::new("cargo") - .arg("+nightly-2023-12-28") - .arg("rustc") - .args(["--package", "program_client"]) - .arg("--profile=check") - .arg("--") - .arg("-Zunpretty=expanded") - .output() - .await?; - let code = String::from_utf8(output.stdout)?; - let mut use_modules: Vec = vec![]; - for item in syn::parse_file(code.as_str()).unwrap().items.into_iter() { - if let syn::Item::Mod(module) = item { - let modules = module - .content - .ok_or("account mod: empty content") - .unwrap() - .1 - .into_iter(); - for module in modules { - if let syn::Item::Use(u) = module { - use_modules.push(u); - } - } - } - } - if use_modules.is_empty() { - use_modules.push(syn::parse_quote! { use trdelnik_client::*; }) - } - use_modules - } } impl Default for Commander { diff --git a/crates/client/src/test_generator.rs b/crates/client/src/test_generator.rs index 9770f7f3..5e124468 100644 --- a/crates/client/src/test_generator.rs +++ b/crates/client/src/test_generator.rs @@ -1,37 +1,25 @@ use crate::{ commander::{Commander, Error as CommanderError}, - config::{CARGO_TOML, TRDELNIK_TOML}, fuzzer, idl::Idl, program_client_generator, snapshot_generator::generate_snapshots_code, }; -use cargo_metadata::camino::Utf8PathBuf; +use cargo_metadata::{camino::Utf8PathBuf, Package}; use fehler::{throw, throws}; -use log::debug; use std::{ env, fs::OpenOptions, - io, iter, + io, path::{Path, PathBuf}, }; use std::{fs::File, io::prelude::*}; +use syn::ItemUse; use thiserror::Error; use tokio::fs; use toml::{value::Table, Value}; -pub(crate) const TESTS_WORKSPACE: &str = "trdelnik-tests"; -const TESTS_FILE_NAME: &str = "test.rs"; -pub(crate) const FUZZ_INSTRUCTIONS_FILE_NAME: &str = "fuzz_instructions.rs"; -pub(crate) const ACCOUNTS_SNAPSHOTS_FILE_NAME: &str = "accounts_snapshots.rs"; -pub(crate) const HFUZZ_TARGET: &str = "hfuzz_target"; - -pub(crate) const FUZZ_TEST_DIRECTORY: &str = "fuzz_tests"; -pub(crate) const FUZZ_TEST: &str = "test_fuzz.rs"; -pub(crate) const POC_TEST_DIRECTORY: &str = "poc_tests"; -pub(crate) const TESTS: &str = "tests"; -pub(crate) const FUZZING: &str = "fuzzing"; -pub(crate) const PROGRAM_CLIENT_DIRECTORY: &str = ".program_client"; +use crate::constants::*; #[derive(Error, Debug)] pub enum Error { @@ -57,318 +45,249 @@ pub struct TestGenerator { pub root: PathBuf, pub idl: Idl, pub codes_libs_pairs: Vec<(String, Utf8PathBuf)>, - pub program_deps: Vec, + pub packages: Vec, + pub use_tokens: Vec, } impl Default for TestGenerator { fn default() -> Self { Self::new() } } + +macro_rules! construct_path { + ($root:expr, $($component:expr),*) => { + { + let mut path = $root.to_owned(); + $(path = path.join($component);)* + path + } + }; +} + impl TestGenerator { + /// Creates a new instance of `TestGenerator` with default values. + /// + /// # Returns + /// + /// A new `TestGenerator` instance. pub fn new() -> Self { Self { root: Path::new("../../").to_path_buf(), idl: Idl::default(), codes_libs_pairs: Vec::default(), - program_deps: Vec::default(), + packages: Vec::default(), + use_tokens: Vec::default(), } } + /// Creates a new instance of `TestGenerator` with a specified root directory. + /// + /// # Arguments + /// + /// * `root` - A string slice that holds the path to the root directory. + /// + /// # Returns + /// + /// A new `TestGenerator` instance with the specified root directory. pub fn new_with_root(root: String) -> Self { Self { root: Path::new(&root).to_path_buf(), idl: Idl::default(), codes_libs_pairs: Vec::default(), - program_deps: Vec::default(), + packages: Vec::default(), + use_tokens: Vec::default(), } } - /// Builds all the programs and creates `.program_client` directory. Initializes the - /// `trdelnik-tests/tests` directory with all the necessary files. Adds the - /// `test.rs` file and generates `Cargo.toml` with `dependencies`. Updates root's `Cargo.toml` - /// workspace members. - /// - /// The crate is generated from `trdelnik-tests` template located in `client/src/templates`. - /// - /// Before you start writing trdelnik tests do not forget to add your program as a dependency - /// to the `trdelnik-tests/Cargo.toml`. For example: - /// - /// ```toml - /// # /trdelnik-tests/Cargo.toml - /// # ... - /// [dependencies] - /// my-program = { path = "../programs/my-program" } - /// # ... - /// ``` - /// - /// Then you can easily use it in tests: - /// - /// ```rust,ignore - /// use my_program; - /// - /// // ... - /// - /// #[trdelnik_test] - /// async fn test() { - /// // ... - /// my_program::do_something(/*...*/); - /// // ... - /// } - /// ``` - /// - /// # Errors - /// - /// It fails when: - /// - there is not a root directory (no `Anchor.toml` file) + /// Generates both proof of concept (POC) and fuzz tests along with necessary scaffolding. #[throws] pub async fn generate_both(&mut self) { - let root_path = self.root.to_str().unwrap().to_string(); - let commander = Commander::with_root(root_path); - - // build the project first, this is technically not necessary. - // However it can be useful to check if the project can be built - // for the bpf or sbf target - commander.build_programs().await?; - - // next we obtain important data from the source codes - // these are further used within the generation process - (self.idl, self.codes_libs_pairs) = commander.get_programs_source_codes().await?; - - // next generate program dependencies - self.program_deps = commander.get_programs_deps().await?; - - // generate program client - self.generate_program_client(&commander).await?; - // generate poc test files - self.generate_test_files().await?; - // update workspace manifest - self.update_workspace("trdelnik-tests/poc_tests").await?; - // generate fuzz test files - // manifest is updated inside - self.generate_fuzz_test_files().await?; - - self.generate_trdelnik_toml().await?; - - // update gitignore to exclude hfuzz target - self.update_gitignore(&format!( - "{TESTS_WORKSPACE}/{FUZZ_TEST_DIRECTORY}/{FUZZING}/{HFUZZ_TARGET}" - ))?; + // expands programs within programs folder + self.expand_programs_data().await?; + // expands program_client and obtains + // use statements + // if program_client is not yet initialized + // use statements are set to default + self.expand_program_client().await?; + self.create_program_client_crate().await?; + self.create_trdelnik_tests_crate().await?; + self.add_new_poc_test().await?; + self.add_new_fuzz_test().await?; + self.create_trdelnik_manifest().await?; + self.update_gitignore("hfuzz_target")?; } + /// Generates fuzz tests along with the necessary setup. #[throws] pub async fn generate_fuzz(&mut self) { - let root_path = self.root.to_str().unwrap().to_string(); - let commander = Commander::with_root(root_path); - // build the project first, this is technically not necessary. - // However it can be useful to check if the project can be built - // for the bpf or sbf target - commander.build_programs().await?; - - // next we obtain important data from the source codes - // these are further used within the generation process - (self.idl, self.codes_libs_pairs) = commander.get_programs_source_codes().await?; - - // generate fuzz test files - // manifest is updated inside - self.generate_fuzz_test_files().await?; - - self.generate_trdelnik_toml().await?; - - // update gitignore to exclude hfuzz target - self.update_gitignore(&format!( - "{TESTS_WORKSPACE}/{FUZZ_TEST_DIRECTORY}/{FUZZING}/{HFUZZ_TARGET}" - ))?; + self.expand_programs_data().await?; + self.expand_program_client().await?; + self.create_trdelnik_tests_crate().await?; + self.add_new_fuzz_test().await?; + self.create_trdelnik_manifest().await?; + self.update_gitignore("trdelnik-tests/fuzz_tests/fuzzing/hfuzz_target")?; } + /// Generates proof of concept (POC) tests along with the necessary setup. #[throws] pub async fn generate_poc(&mut self) { - let root_path = self.root.to_str().unwrap().to_string(); - let commander = Commander::with_root(root_path); - // build the project first, this is technically not necessary. - // However it can be useful to check if the project can be built - // for the bpf or sbf target - commander.build_programs().await?; - - // next we obtain important data from the source codes - // these are further used within the generation process - (self.idl, self.codes_libs_pairs) = commander.get_programs_source_codes().await?; - - // next generate program dependencies - self.program_deps = commander.get_programs_deps().await?; - - // generate program client - self.generate_program_client(&commander).await?; - // generate poc test files - self.generate_test_files().await?; - // update workspace manifest - self.update_workspace("trdelnik-tests/poc_tests").await?; - - self.generate_trdelnik_toml().await?; + self.expand_programs_data().await?; + self.expand_program_client().await?; + self.create_program_client_crate().await?; + self.create_trdelnik_tests_crate().await?; + self.add_new_poc_test().await?; + self.create_trdelnik_manifest().await?; } #[throws] pub async fn build(&mut self) { - let root_path = self.root.to_str().unwrap().to_string(); - - let commander = Commander::with_root(root_path); - // build the project first, this is technically not necessary. - // However it can be useful to check if the project can be built - // for the bpf or sbf target - commander.build_programs().await?; - // next we obtain important data from the source codes - // these are further used within the generation process - (self.idl, self.codes_libs_pairs) = commander.get_programs_source_codes().await?; - // next generate program dependencies - self.program_deps = commander.get_programs_deps().await?; - - // generate program client - self.generate_program_client(&commander).await?; + self.expand_programs_data().await?; + self.create_program_client_crate().await?; } + /// ## Adds new Fuzz test template to the trdelnik-tests folder #[throws] - pub async fn add_new_fuzz_test(&mut self) { - let root_path = self.root.to_str().unwrap().to_string(); - let commander = Commander::with_root(root_path); - - commander.build_programs().await?; - - // next we obtain important data from the source codes - // these are further used within the generation process - (self.idl, self.codes_libs_pairs) = commander.get_programs_source_codes().await?; - - // next generate program dependencies - // self.program_deps = commander.generate_program_client_deps().await?; - - self.generate_fuzz_test_files().await?; - - self.update_gitignore(&format!( - "{TESTS_WORKSPACE}/{FUZZ_TEST_DIRECTORY}/{FUZZING}/{HFUZZ_TARGET}" - ))?; + pub async fn add_fuzz_test(&mut self) { + self.packages = Commander::collect_program_packages().await?; + (self.idl, self.codes_libs_pairs) = + Commander::expand_program_packages(&self.packages).await?; + self.add_new_fuzz_test().await?; } + /// Gathers and expands program data necessary for generating tests. #[throws] - pub async fn generate_trdelnik_toml(&self) { - let trdelnik_toml_path = self.root.join(TRDELNIK_TOML); - let trdelnik_toml_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/Trdelnik.toml.tmpl" - )); - // in case trdelnik toml is already initialized this will not overwrite the configuration - self.create_file(&trdelnik_toml_path, TRDELNIK_TOML, trdelnik_toml_content) - .await?; + async fn expand_programs_data(&mut self) { + self.packages = Commander::collect_program_packages().await?; + (self.idl, self.codes_libs_pairs) = + Commander::expand_program_packages(&self.packages).await?; + self.use_tokens = Commander::expand_program_client().await?; } - - /// Creates the `program_client` crate. - /// - /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. + /// Gathers and expands program data necessary for generating tests. #[throws] - pub async fn generate_program_client(&self, commander: &Commander) { - let crate_path = self.root.join(PROGRAM_CLIENT_DIRECTORY); - // @TODO Would it be better to: - // zip the template folder -> embed the archive to the binary -> unzip to a given location? - - self.create_directory(&crate_path, PROGRAM_CLIENT_DIRECTORY) - .await?; - - let cargo_toml_path = crate_path.join(CARGO_TOML); - - let cargo_toml_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/program_client/Cargo.toml.tmpl" - )); - // this will create Cargo.toml if it does not already exist. - // In case Cargo.toml is already initialized, it will be only updated - // within the next steps - self.create_file(&cargo_toml_path, CARGO_TOML, cargo_toml_content) - .await?; + async fn expand_program_client(&mut self) { + self.use_tokens = Commander::expand_program_client().await?; + } - let mut cargo_toml_content: toml::Value = - fs::read_to_string(&cargo_toml_path).await?.parse()?; + /// Adds a new proof of concept (POC) test to the test workspace. + #[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 + } else { + throw!(Error::NoProgramsFound) + }; - let trdelnik_dep = r#"trdelnik-client = "0.5.0""#.parse().unwrap(); + 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); + let cargo_path = construct_path!(poc_dir_path, CARGO_TOML); + let poc_test_path = construct_path!(new_poc_test_dir, POC_TEST); - let cargo_toml_deps = cargo_toml_content - .get_mut("dependencies") - .and_then(toml::Value::as_table_mut) - .ok_or(Error::ParsingCargoTomlDependenciesFailed)?; + self.create_directory(&poc_dir_path).await?; + self.create_directory(&new_poc_test_dir).await?; + let cargo_toml_content = + load_template("/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl")?; + self.create_file(&cargo_path, &cargo_toml_content).await?; - for dep in iter::once(&trdelnik_dep).chain(&self.program_deps) { - if let toml::Value::Table(table) = dep { - let (name, value) = table.into_iter().next().unwrap(); - cargo_toml_deps.entry(name).or_insert(value.clone()); - } - } - fs::write(cargo_toml_path, cargo_toml_content.to_string()).await?; + let poc_test_content = load_template("/src/templates/trdelnik-tests/test.rs")?; + let test_content = poc_test_content.replace("###PROGRAM_NAME###", program_name); + let use_instructions = format!("use program_client::{}_instruction::*;\n", program_name); + let template = format!("{use_instructions}{test_content}"); - let src_path = crate_path.join("src"); - self.create_directory(&src_path, "src").await?; + self.create_file(&poc_test_path, &template).await?; - let use_tokens = commander.parse_program_client_imports().await?; - let program_client = program_client_generator::generate_source_code(&self.idl, &use_tokens); - let program_client = Commander::format_program_code(&program_client).await?; - fs::write(src_path.join("lib.rs"), &program_client).await?; + // add poc test to the workspace virtual manifest + self.add_workspace_member(&format!("{TESTS_WORKSPACE_DIRECTORY}/{POC_TEST_DIRECTORY}",)) + .await?; - debug!("program_client crate created") + // add program dev-dependencies into the poc tests Cargo + // dev-deps are ok as they are used with the cargo test + self.add_program_dependencies(&poc_dir_path, "dev-dependencies", None) + .await?; } - /// Creates the `trdelnik-tests` workspace with `tests` directory and empty `test.rs` file - /// finally it generates the `Cargo.toml` file. Crate is generated from `trdelnik-tests` - /// template located in `client/src/templates` + /// Creates the `Trdelnik.toml` file #[throws] - async fn generate_test_files(&self) { - let workspace_path = self - .root - .join(TESTS_WORKSPACE) - .join(POC_TEST_DIRECTORY) - .join(TESTS); - - self.create_directory_all(&workspace_path, TESTS).await?; - - let test_path = workspace_path.join(TESTS_FILE_NAME); + async fn create_trdelnik_manifest(&self) { + let trdelnik_toml_path = construct_path!(self.root, TRDELNIK_TOML); + let trdelnik_toml_content = load_template("/src/templates/Trdelnik.toml.tmpl")?; + self.create_file(&trdelnik_toml_path, &trdelnik_toml_content) + .await?; + } - let test_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/trdelnik-tests/test.rs" - )); + /// Creates the `trdelnik-tests` folder + #[throws] + async fn create_trdelnik_tests_crate(&self) { + let workspace_path = construct_path!(self.root, TESTS_WORKSPACE_DIRECTORY); + self.create_directory(&workspace_path).await?; + } + #[throws] + async fn add_workspace_member(&self, member: &str) { + let cargo = construct_path!(self.root, CARGO_TOML); + let mut content: Value = fs::read_to_string(&cargo).await?.parse()?; + let new_member = Value::String(String::from(member)); - let program_libs = self.get_program_lib_names().await?; + let members = content + .as_table_mut() + .ok_or(Error::CannotParseCargoToml)? + .entry("workspace") + .or_insert(Value::Table(Table::default())) + .as_table_mut() + .ok_or(Error::CannotParseCargoToml)? + .entry("members") + .or_insert(Value::Array(vec![new_member.clone()])) + .as_array_mut() + .ok_or(Error::CannotParseCargoToml)?; - let program_name = if let Some(name) = program_libs.first() { - name - } else { - throw!(Error::NoProgramsFound) + match members.iter().find(|&x| x.eq(&new_member)) { + Some(_) => { + println!("\x1b[93mSkipping\x1b[0m: {CARGO_TOML}, already contains {member}.") + } + None => { + members.push(new_member); + println!("\x1b[92mSuccesfully\x1b[0m updated: {CARGO_TOML} with {member} member."); + } }; + fs::write(cargo, content.to_string()).await?; + } - let test_content = test_content.replace("###PROGRAM_NAME###", program_name); - let use_instructions = format!("use program_client::{}_instruction::*;\n", program_name); - let template = format!("{use_instructions}{test_content}"); + /// ## Creates program client folder and generates source code + #[throws] + async fn create_program_client_crate(&self) { + let cargo_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, CARGO_TOML); + let src_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY); + let crate_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY); + let lib_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY, LIB); - self.create_file(&test_path, TESTS_FILE_NAME, &template) - .await?; + self.create_directory_all(&src_path).await?; - let cargo_toml_path = self - .root - .join(TESTS_WORKSPACE) - .join(POC_TEST_DIRECTORY) - .join(CARGO_TOML); + // load template + let cargo_toml_content = load_template("/src/templates/program_client/Cargo.toml.tmpl")?; - let cargo_toml_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl" - )); + // if path exists the file will not be overwritten + self.create_file(&cargo_path, &cargo_toml_content).await?; - self.create_file(&cargo_toml_path, CARGO_TOML, cargo_toml_content) + self.add_program_dependencies(&crate_path, "dependencies", Some(vec!["no-entrypoint"])) .await?; - let cargo_toml_dir = self.root.join(TESTS_WORKSPACE).join(POC_TEST_DIRECTORY); - self.add_program_deps(&cargo_toml_dir).await?; - } + let program_client = + program_client_generator::generate_source_code(&self.idl, &self.use_tokens); + let program_client = Commander::format_program_code(&program_client).await?; + if lib_path.exists() { + self.update_file(&lib_path, &program_client).await?; + } else { + self.create_file(&lib_path, &program_client).await?; + } + } /// Creates the `trdelnik-tests` workspace with `src/bin` directory and empty `fuzz_target.rs` file #[throws] - async fn generate_fuzz_test_files(&self) { - let fuzz_dir_path = self.root.join(TESTS_WORKSPACE).join(FUZZ_TEST_DIRECTORY); - let fuzz_tests_manifest_path = fuzz_dir_path.join(CARGO_TOML); - - self.create_directory_all(&fuzz_dir_path, FUZZ_TEST_DIRECTORY) - .await?; + 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 + } else { + throw!(Error::NoProgramsFound) + }; + let fuzz_dir_path = + construct_path!(self.root, TESTS_WORKSPACE_DIRECTORY, FUZZ_TEST_DIRECTORY); + let fuzz_tests_manifest_path = construct_path!(fuzz_dir_path, CARGO_TOML); - let libs = self.get_program_lib_names().await?; + self.create_directory_all(&fuzz_dir_path).await?; let fuzz_id = if fuzz_dir_path.read_dir()?.next().is_none() { 0 @@ -404,8 +323,7 @@ impl TestGenerator { let new_fuzz_test_dir = fuzz_dir_path.join(&new_fuzz_test); let new_bin_target = format!("{new_fuzz_test}/test_fuzz.rs"); - self.create_directory(&new_fuzz_test_dir, &new_fuzz_test) - .await?; + self.create_directory(&new_fuzz_test_dir).await?; let fuzz_test_path = new_fuzz_test_dir.join(FUZZ_TEST); @@ -414,23 +332,17 @@ impl TestGenerator { "/src/templates/trdelnik-tests/test_fuzz.rs" )) .to_string(); - - // create fuzz target file - let fuzz_test_content = if let Some(lib) = libs.first() { - let use_entry = format!("use {}::entry;\n", lib); - let use_instructions = format!("use {}::ID as PROGRAM_ID;\n", lib); - let use_fuzz_instructions = format!( - "use fuzz_instructions::{}_fuzz_instructions::FuzzInstruction;\n", - lib - ); - let template = - format!("{use_entry}{use_instructions}{use_fuzz_instructions}{fuzz_test_content}"); - template.replace("###PROGRAM_NAME###", lib) - } else { - throw!(Error::NoProgramsFound) - }; - - self.create_file(&fuzz_test_path, FUZZ_TEST, &fuzz_test_content) + let use_entry = format!("use {}::entry;\n", program_name); + let use_instructions = format!("use {}::ID as PROGRAM_ID;\n", program_name); + let use_fuzz_instructions = format!( + "use fuzz_instructions::{}_fuzz_instructions::FuzzInstruction;\n", + program_name + ); + let template = + format!("{use_entry}{use_instructions}{use_fuzz_instructions}{fuzz_test_content}"); + let fuzz_test_content = template.replace("###PROGRAM_NAME###", program_name); + + self.create_file(&fuzz_test_path, &fuzz_test_content) .await?; // create fuzz instructions file @@ -438,12 +350,8 @@ impl TestGenerator { let program_fuzzer = fuzzer::fuzzer_generator::generate_source_code(&self.idl); let program_fuzzer = Commander::format_program_code(&program_fuzzer).await?; - self.create_file( - &fuzz_instructions_path, - FUZZ_INSTRUCTIONS_FILE_NAME, - &program_fuzzer, - ) - .await?; + self.create_file(&fuzz_instructions_path, &program_fuzzer) + .await?; // // create accounts_snapshots file let accounts_snapshots_path = new_fuzz_test_dir.join(ACCOUNTS_SNAPSHOTS_FILE_NAME); @@ -451,130 +359,110 @@ impl TestGenerator { .map_err(Error::ReadProgramCodeFailed)?; let fuzzer_snapshots = Commander::format_program_code(&fuzzer_snapshots).await?; - self.create_file( - &accounts_snapshots_path, - ACCOUNTS_SNAPSHOTS_FILE_NAME, - &fuzzer_snapshots, - ) - .await?; + self.create_file(&accounts_snapshots_path, &fuzzer_snapshots) + .await?; let cargo_toml_content = include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl" )); - self.create_file(&fuzz_tests_manifest_path, CARGO_TOML, cargo_toml_content) + self.create_file(&fuzz_tests_manifest_path, cargo_toml_content) .await?; self.add_bin_target(&fuzz_tests_manifest_path, &new_fuzz_test, &new_bin_target) .await?; - self.add_program_deps(&fuzz_dir_path).await?; + self.add_program_dependencies(&fuzz_dir_path, "dependencies", None) + .await?; - self.update_workspace("trdelnik-tests/fuzz_tests").await?; + self.add_workspace_member(&format!( + "{TESTS_WORKSPACE_DIRECTORY}/{FUZZ_TEST_DIRECTORY}", + )) + .await?; } - /// Creates a new file with a given content on the specified `path` and `name` - // todo: the function should be located in the different module, File module for example - async fn create_file<'a>( - &self, - path: &'a PathBuf, - name: &str, - content: &str, - ) -> Result<&'a PathBuf, Error> { + /// ## Creates a new directory and all missing parent directories on the specified path + #[throws] + async fn create_directory_all<'a>(&self, path: &'a PathBuf) { match path.exists() { - true => println!("Skipping creating the {name} file"), + true => {} false => { - println!("Creating the {name} file ..."); - fs::write(path, content).await?; + fs::create_dir_all(path).await?; } }; - Ok(path) } - - /// Creates a new directory on the specified `path` and with the specified `name` - // todo: the function should be located in the different module, File module for example - async fn create_directory<'a>( - &self, - path: &'a PathBuf, - name: &str, - ) -> Result<&'a PathBuf, Error> { + /// ## Creates directory with specified path + #[throws] + async fn create_directory<'a>(&self, path: &'a Path) { match path.exists() { - true => println!("Skipping creating the {name} directory"), + true => {} false => { - println!("Creating the {name} directory ..."); fs::create_dir(path).await?; } }; - Ok(path) } + /// ## Creates a new file with a given content on the specified path + /// - Skip if file already exists + #[throws] + async fn create_file<'a>(&self, path: &'a Path, content: &str) { + let file = path.strip_prefix(&self.root).unwrap().to_str().unwrap(); - /// Creates a new directory and all missing parent directories on the specified `path` and with the specified `name` - // todo: the function should be located in the different module, File module for example - async fn create_directory_all<'a>( - &self, - path: &'a PathBuf, - name: &str, - ) -> Result<&'a PathBuf, Error> { match path.exists() { - true => println!("Skipping creating the {name} directory"), + true => { + println!("\x1b[93mSkipping\x1b[0m: {file}, already exists.") + } false => { - println!("Creating the {name} directory ..."); - fs::create_dir_all(path).await?; + fs::write(path, content).await?; + println!("\x1b[92mSuccesfully\x1b[0m created: {file}."); } }; - Ok(path) } - - /// Adds `trdelnik-tests` workspace to the `root`'s `Cargo.toml` workspace members if needed. + /// ## Updates a file with a given content on the specified path + /// - Skip if file does not exists #[throws] - async fn update_workspace(&self, new_member: &str) { - let cargo = Path::new(&self.root).join(CARGO_TOML); - let mut content: Value = fs::read_to_string(&cargo).await?.parse()?; - let test_workspace_value = Value::String(String::from(new_member)); - let members = content - .as_table_mut() - .ok_or(Error::CannotParseCargoToml)? - .entry("workspace") - .or_insert(Value::Table(Table::default())) - .as_table_mut() - .ok_or(Error::CannotParseCargoToml)? - .entry("members") - .or_insert(Value::Array(vec![test_workspace_value.clone()])) - .as_array_mut() - .ok_or(Error::CannotParseCargoToml)?; - match members.iter().find(|&x| x.eq(&test_workspace_value)) { - Some(_) => println!("Skipping updating project workspace"), - None => { - members.push(test_workspace_value); - println!("Project workspace successfully updated"); + async fn update_file<'a>(&self, path: &'a Path, content: &str) { + let file = path.strip_prefix(&self.root).unwrap().to_str().unwrap(); + match path.exists() { + true => { + fs::write(path, content).await?; + println!("\x1b[92mSuccesfully\x1b[0m updated: {file}."); + } + false => { + fs::write(path, content).await?; + println!("\x1b[92mSuccesfully\x1b[0m created: {file}."); } }; - fs::write(cargo, content.to_string()).await?; } - /// Updates .gitignore file in the `root` directory and appends `ignored_path` to the end of the file + /// ## Updates .gitignore file in the `root` directory and appends `ignored_path` to the end of the file #[throws] fn update_gitignore(&self, ignored_path: &str) { - let file_path = self.root.join(".gitignore"); - if file_path.exists() { - let file = File::open(&file_path)?; + let gitignore_path = construct_path!(self.root, GIT_IGNORE); + if gitignore_path.exists() { + let file = File::open(&gitignore_path)?; for line in io::BufReader::new(file).lines().flatten() { if line == ignored_path { - // do not add the ignored path again if it is already in the .gitignore file + // 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}." + ); + return; } } - let file = OpenOptions::new().write(true).append(true).open(file_path); + let file = OpenOptions::new() + .write(true) + .append(true) + .open(gitignore_path); if let Ok(mut file) = file { writeln!(file, "{}", ignored_path)?; - println!(".gitignore file sucessfully updated"); + println!("\x1b[92mSuccesfully\x1b[0m updated: {GIT_IGNORE} with {ignored_path}."); } } else { - println!("Skipping updating .gitignore file"); + println!("\x1b[93mSkipping\x1b[0m: {GIT_IGNORE}, not found.") } } - #[throws] async fn add_bin_target(&self, cargo_path: &PathBuf, name: &str, path: &str) { // Read the existing Cargo.toml file @@ -603,102 +491,63 @@ impl TestGenerator { // Write the updated Cargo.toml file fs::write(cargo_path, cargo_toml.to_string()).await?; } - - /// Adds programs to Cargo.toml as a dependencies to be able to be used in tests and fuzz targets + /// ## Adds program dependency to specified Cargo.toml + /// - for example, we need to use program entry within the fuzzer #[throws] - async fn add_program_deps(&self, cargo_toml_dir: &Path) { - let cargo_toml_path = cargo_toml_dir.join("Cargo.toml"); - let programs = self.get_programs(&cargo_toml_dir.to_path_buf()).await?; - if !programs.is_empty() { - println!("Adding programs to Cargo.toml ..."); - let mut content: Value = fs::read_to_string(&cargo_toml_path).await?.parse()?; - let dev_deps = content - .get_mut("dependencies") - .and_then(Value::as_table_mut) - .ok_or(Error::CannotParseCargoToml)?; - for dep in programs { - if let Value::Table(table) = dep { + async fn add_program_dependencies( + &self, + cargo_dir: &PathBuf, + deps: &str, + features: Option>, + ) { + let cargo_path = construct_path!(cargo_dir, "Cargo.toml"); + + let mut cargo_toml_content: toml::Value = fs::read_to_string(&cargo_path).await?.parse()?; + + let client_toml_deps = cargo_toml_content + .get_mut(deps) + .and_then(toml::Value::as_table_mut) + .ok_or(Error::ParsingCargoTomlDependenciesFailed)?; + + if !&self.packages.is_empty() { + for package in self.packages.iter() { + let manifest_path = package.manifest_path.parent().unwrap().as_std_path(); + // INFO this will obtain relative path + let relative_path = pathdiff::diff_paths(manifest_path, cargo_dir).unwrap(); + let dep: Value = if features.is_some() { + format!( + r#"{} = {{ path = "{}", features = {:?} }}"#, + package.name, + relative_path.to_str().unwrap(), + features.as_ref().unwrap() + ) + .parse() + .unwrap() + } else { + format!( + r#"{} = {{ path = "{}" }}"#, + package.name, + relative_path.to_str().unwrap() + ) + .parse() + .unwrap() + }; + if let toml::Value::Table(table) = dep { let (name, value) = table.into_iter().next().unwrap(); - dev_deps.entry(name).or_insert(value); + client_toml_deps.entry(name).or_insert(value.clone()); } } - fs::write(&cargo_toml_path, content.to_string()).await?; + fs::write(cargo_path, cargo_toml_content.to_string()).await?; } else { - println!("Skipping adding programs to Cargo.toml ..."); - } - } - - /// Scans `programs` directory and returns a list of `toml::Value` programs and their paths. - async fn get_programs(&self, cargo_dir: &PathBuf) -> Result, Error> { - let programs = self.root.join("programs"); - if !programs.exists() { - println!("Programs folder does not exist."); - return Ok(Vec::new()); - } - println!("Searching for programs ..."); - let mut program_names: Vec = vec![]; - let programs = std::fs::read_dir(programs)?; - for program in programs { - let file = program?; - if file.path().is_dir() { - let path = file.path().join(CARGO_TOML); - if path.exists() { - let dependency = self.get_program_dep(&path, cargo_dir).await?; - program_names.push(dependency); - } - } + throw!(Error::NoProgramsFound) } - Ok(program_names) } +} - /// Scans `programs` directory and returns a list of names of libraries - async fn get_program_lib_names(&self) -> Result, Error> { - let programs = self.root.join("programs"); - if !programs.exists() { - println!("Programs folder does not exist."); - return Ok(Vec::new()); - } - println!("Searching for programs ..."); - let mut program_names: Vec = vec![]; - let programs = std::fs::read_dir(programs)?; - for program in programs { - let file = program?; - if file.path().is_dir() { - let path = file.path().join(CARGO_TOML); - if path.exists() { - let content: Value = fs::read_to_string(&path).await?.parse()?; - let name = content - .get("lib") - .and_then(Value::as_table) - .and_then(|table| table.get("name")) - .and_then(Value::as_str) - .ok_or(Error::CannotParseCargoToml)?; - program_names.push(name.to_string()); - } - } - } - Ok(program_names) - } +pub fn load_template(file_path: &str) -> Result { + let mut _path = String::from(MANIFEST_PATH); + _path.push_str(file_path); + let full_path = Path::new(&_path); - /// Gets the program name from `/Cargo.toml` and returns a `toml::Value` program dependency. - #[throws] - async fn get_program_dep(&self, dir: &Path, cargo_dir: &PathBuf) -> Value { - let manifest_path = dir.parent().unwrap(); - let relative_path = pathdiff::diff_paths(manifest_path, cargo_dir).unwrap(); - - let content: Value = fs::read_to_string(&dir).await?.parse()?; - let name = content - .get("package") - .and_then(Value::as_table) - .and_then(|table| table.get("name")) - .and_then(Value::as_str) - .ok_or(Error::CannotParseCargoToml)?; - format!( - r#"{} = {{ path = "{}" }}"#, - name, - relative_path.to_str().unwrap() - ) - .parse() - .unwrap() - } + std::fs::read_to_string(full_path) } From 23d9f5cfcbe1d8ed2fe61dae58701098122f9c2c Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 21 Feb 2024 23:40:43 +0100 Subject: [PATCH 3/8] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20=20group=20constans?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🐛 snake case + clone ✅ update tests --- crates/client/src/cleaner.rs | 15 ++++--- crates/client/src/client.rs | 3 +- crates/client/src/config.rs | 9 +---- crates/client/src/fuzzer/fuzzer_generator.rs | 4 +- crates/client/src/idl.rs | 12 +++--- crates/client/src/lib.rs | 40 +++++++++++++++++++ crates/client/src/program_client_generator.rs | 4 +- crates/client/tests/test_fuzz.rs | 7 ++-- crates/client/tests/test_program_client.rs | 2 +- 9 files changed, 64 insertions(+), 32 deletions(-) diff --git a/crates/client/src/cleaner.rs b/crates/client/src/cleaner.rs index 56217191..00b8dcea 100644 --- a/crates/client/src/cleaner.rs +++ b/crates/client/src/cleaner.rs @@ -7,6 +7,8 @@ use std::{ use thiserror::Error; use tokio::{fs, process::Command}; +use crate::constants::*; + #[derive(Error, Debug)] pub enum Error { #[error("{0:?}")] @@ -44,19 +46,16 @@ impl Cleaner { #[throws] async fn clean_hfuzz_target(&self, root: &PathBuf) { let hfuzz_target_path = Path::new(root) - .join(crate::test_generator::TESTS_WORKSPACE) - .join(crate::test_generator::FUZZ_TEST_DIRECTORY) - .join(crate::test_generator::FUZZING) - .join(crate::test_generator::HFUZZ_TARGET); + .join(TESTS_WORKSPACE_DIRECTORY) + .join(FUZZ_TEST_DIRECTORY) + .join(FUZZING) + .join(HFUZZ_TARGET); if hfuzz_target_path.exists() { fs::remove_dir_all(hfuzz_target_path).await?; } else { println!( "skipping {}/{}/{}/{} directory: not found", - crate::test_generator::TESTS_WORKSPACE, - crate::test_generator::FUZZ_TEST_DIRECTORY, - crate::test_generator::FUZZING, - crate::test_generator::HFUZZ_TARGET + TESTS_WORKSPACE_DIRECTORY, FUZZ_TEST_DIRECTORY, FUZZING, HFUZZ_TARGET ) } } diff --git a/crates/client/src/client.rs b/crates/client/src/client.rs index 4fcb8148..b52c09ca 100644 --- a/crates/client/src/client.rs +++ b/crates/client/src/client.rs @@ -36,8 +36,7 @@ use std::{thread::sleep, time::Duration}; // @TODO: Make compatible with the latest Anchor deps. // https://github.com/project-serum/anchor/pull/1307#issuecomment-1022592683 -const RETRY_LOCALNET_EVERY_MILLIS: u64 = 500; -const DEFAULT_KEYPAIR_PATH: &str = "~/.config/solana/id.json"; +use crate::constants::*; type Payer = Rc; diff --git a/crates/client/src/config.rs b/crates/client/src/config.rs index 191e4ca4..c965af6b 100644 --- a/crates/client/src/config.rs +++ b/crates/client/src/config.rs @@ -4,14 +4,7 @@ use serde::Deserialize; use std::{collections::HashMap, env, fs, io, path::PathBuf}; use thiserror::Error; -pub const CARGO_TOML: &str = "Cargo.toml"; -pub const TRDELNIK_TOML: &str = "Trdelnik.toml"; -pub const ANCHOR_TOML: &str = "Anchor.toml"; -pub const CARGO_TARGET_DIR_DEFAULT: &str = "trdelnik-tests/fuzz_tests/fuzzing/hfuzz_target"; -pub const HFUZZ_WORKSPACE_DEFAULT: &str = "trdelnik-tests/fuzz_tests/fuzzing/hfuzz_workspace"; - -pub const CARGO_TARGET_DIR_ENV: &str = "CARGO_TARGET_DIR"; -pub const HFUZZ_WORKSPACE_ENV: &str = "HFUZZ_WORKSPACE"; +use crate::constants::*; #[derive(Error, Debug)] pub enum Error { diff --git a/crates/client/src/fuzzer/fuzzer_generator.rs b/crates/client/src/fuzzer/fuzzer_generator.rs index eca50859..287fedf6 100644 --- a/crates/client/src/fuzzer/fuzzer_generator.rs +++ b/crates/client/src/fuzzer/fuzzer_generator.rs @@ -11,9 +11,9 @@ pub fn generate_source_code(idl: &Idl) -> String { .programs .iter() .map(|idl_program| { - let program_name = idl_program.name.snake_case.replace('-', "_"); + let program_name = &idl_program.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 module_name: syn::Ident = parse_str(program_name).unwrap(); let instructions = idl_program .instruction_account_pairs diff --git a/crates/client/src/idl.rs b/crates/client/src/idl.rs index 224942e4..6cf34887 100644 --- a/crates/client/src/idl.rs +++ b/crates/client/src/idl.rs @@ -188,32 +188,32 @@ pub struct Idl { pub programs: Vec, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IdlName { pub snake_case: String, pub upper_camel_case: String, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IdlProgram { pub name: IdlName, pub id: String, pub instruction_account_pairs: Vec<(IdlInstruction, IdlAccountGroup)>, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IdlInstruction { pub name: IdlName, pub parameters: Vec<(String, String)>, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct IdlAccountGroup { pub name: IdlName, pub accounts: Vec<(String, String)>, } -pub async fn parse_to_idl_program(name: String, code: &str) -> Result { +pub fn parse_to_idl_program(name: String, code: &str) -> Result { let mut static_program_id = None::; let mut mod_private = None::; let mut mod_instruction = None::; @@ -638,7 +638,7 @@ pub async fn parse_to_idl_program(name: String, code: &str) -> Result String { .programs .iter() .map(|idl_program| { - let program_name = idl_program.name.snake_case.replace('-', "_"); + let program_name = &idl_program.name.snake_case; let instruction_module_name = format_ident!("{}_instruction", program_name); - let module_name: syn::Ident = parse_str(&program_name).unwrap(); + let module_name: syn::Ident = parse_str(program_name).unwrap(); let pubkey_bytes: syn::ExprArray = parse_str(&idl_program.id).unwrap(); let instructions = idl_program diff --git a/crates/client/tests/test_fuzz.rs b/crates/client/tests/test_fuzz.rs index ae929bee..e7118049 100644 --- a/crates/client/tests/test_fuzz.rs +++ b/crates/client/tests/test_fuzz.rs @@ -18,9 +18,10 @@ async fn test_fuzz_instructions() { "/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) - .await?; + 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], diff --git a/crates/client/tests/test_program_client.rs b/crates/client/tests/test_program_client.rs index 74ae4d7e..d29dc48b 100644 --- a/crates/client/tests/test_program_client.rs +++ b/crates/client/tests/test_program_client.rs @@ -21,7 +21,7 @@ pub async fn generate_program_client() { )); let program_idl = - trdelnik_client::idl::parse_to_idl_program("escrow".to_owned(), expanded_escrow).await?; + trdelnik_client::idl::parse_to_idl_program("escrow".to_owned(), expanded_escrow)?; let idl = trdelnik_client::idl::Idl { programs: vec![program_idl], From 7e0f83a918e4fca111d9f64b6c9f7f0ded15df31 Mon Sep 17 00:00:00 2001 From: lukacan Date: Wed, 21 Feb 2024 23:41:59 +0100 Subject: [PATCH 4/8] =?UTF-8?q?=F0=9F=93=9D=20update=20templates?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ➕ dependency for progress bar ⬆️ update locks and tomls for poc examples --- Cargo.lock | 1 + crates/client/Cargo.toml | 1 + .../templates/program_client/Cargo.toml.tmpl | 3 ++- .../trdelnik-tests/Cargo_poc.toml.tmpl | 10 ++++----- examples/escrow/Cargo.lock | 22 ++++--------------- .../trdelnik-tests/poc_tests/Cargo.toml | 13 +++++------ examples/turnstile/Cargo.lock | 22 ++++--------------- .../trdelnik-tests/poc_tests/Cargo.toml | 11 +++++----- 8 files changed, 26 insertions(+), 57 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e8e7a7be..92136c40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6003,6 +6003,7 @@ dependencies = [ "futures", "heck 0.4.1", "honggfuzz", + "indicatif", "lazy_static", "log", "macrotest", diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index fb17c55a..98d3bd73 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -69,3 +69,4 @@ trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" } trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" } pathdiff = "0.2.1" solana-banks-client = "<1.18" +indicatif = "0.17.8" diff --git a/crates/client/src/templates/program_client/Cargo.toml.tmpl b/crates/client/src/templates/program_client/Cargo.toml.tmpl index 13dc1b86..7ac256a4 100644 --- a/crates/client/src/templates/program_client/Cargo.toml.tmpl +++ b/crates/client/src/templates/program_client/Cargo.toml.tmpl @@ -3,4 +3,5 @@ name = "program_client" version = "0.1.0" edition = "2018" -[dependencies] +[dependencies.trdelnik-client] +trdelnik-client = "0.5.0" diff --git a/crates/client/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl b/crates/client/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl index d3a97b23..12a6b688 100644 --- a/crates/client/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl +++ b/crates/client/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl @@ -4,6 +4,10 @@ version = "0.1.0" description = "Created with Trdelnik" edition = "2021" +# Dependencies specific to PoC test +# Dev-dependencies are used for compiling tests, +[dev-dependencies] +fehler = "1.0.0" [dev-dependencies.trdelnik-client] version = "0.5.0" @@ -11,9 +15,3 @@ version = "0.5.0" [dev-dependencies.program_client] path = "../../.program_client" - - -[dependencies] -assert_matches = "1.4.0" -fehler = "1.0.0" -rstest = "0.12.0" diff --git a/examples/escrow/Cargo.lock b/examples/escrow/Cargo.lock index 42f17d01..dba95f5f 100644 --- a/examples/escrow/Cargo.lock +++ b/examples/escrow/Cargo.lock @@ -2757,11 +2757,9 @@ name = "poc_tests" version = "0.1.0" dependencies = [ "anchor-spl", - "assert_matches", "escrow", "fehler", "program_client", - "rstest 0.12.0", "trdelnik-client", ] @@ -3176,19 +3174,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "rstest" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - [[package]] name = "rstest" version = "0.18.2" @@ -4910,9 +4895,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -5200,13 +5185,14 @@ dependencies = [ "fehler", "futures", "heck 0.4.1", + "indicatif", "lazy_static", "log", "pathdiff", "proc-macro2", "quote", "rand 0.8.5", - "rstest 0.18.2", + "rstest", "serde", "serde_json", "serial_test", diff --git a/examples/escrow/trdelnik-tests/poc_tests/Cargo.toml b/examples/escrow/trdelnik-tests/poc_tests/Cargo.toml index 740da081..192fe3c2 100644 --- a/examples/escrow/trdelnik-tests/poc_tests/Cargo.toml +++ b/examples/escrow/trdelnik-tests/poc_tests/Cargo.toml @@ -4,18 +4,15 @@ version = "0.1.0" description = "Created with Trdelnik" edition = "2021" +[dev-dependencies] +fehler = "1.0.0" +anchor-spl = "0.29.0" + [dev-dependencies.trdelnik-client] path = "../../../../crates/client" [dev-dependencies.program_client] path = "../../.program_client" -[dependencies] -assert_matches = "1.4.0" -fehler = "1.0.0" -rstest = "0.12.0" -anchor-spl = "0.29.0" - - -[dependencies.escrow] +[dev-dependencies.escrow] path = "../../programs/escrow" diff --git a/examples/turnstile/Cargo.lock b/examples/turnstile/Cargo.lock index e632bd39..8906bdc6 100644 --- a/examples/turnstile/Cargo.lock +++ b/examples/turnstile/Cargo.lock @@ -2754,10 +2754,8 @@ checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" name = "poc_tests" version = "0.1.0" dependencies = [ - "assert_matches", "fehler", "program_client", - "rstest 0.12.0", "trdelnik-client", "turnstile", ] @@ -3182,19 +3180,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "rstest" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d912f35156a3f99a66ee3e11ac2e0b3f34ac85a07e05263d05a7e2c8810d616f" -dependencies = [ - "cfg-if", - "proc-macro2", - "quote", - "rustc_version", - "syn 1.0.109", -] - [[package]] name = "rstest" version = "0.18.2" @@ -4916,9 +4901,9 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.1.7" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ "cfg-if", "once_cell", @@ -5217,13 +5202,14 @@ dependencies = [ "fehler", "futures", "heck 0.4.1", + "indicatif", "lazy_static", "log", "pathdiff", "proc-macro2", "quote", "rand 0.8.5", - "rstest 0.18.2", + "rstest", "serde", "serde_json", "serial_test", diff --git a/examples/turnstile/trdelnik-tests/poc_tests/Cargo.toml b/examples/turnstile/trdelnik-tests/poc_tests/Cargo.toml index 4ec98821..bdb6ba02 100644 --- a/examples/turnstile/trdelnik-tests/poc_tests/Cargo.toml +++ b/examples/turnstile/trdelnik-tests/poc_tests/Cargo.toml @@ -3,16 +3,15 @@ name = "poc_tests" version = "0.1.0" description = "Created with Trdelnik" edition = "2021" + +[dev-dependencies] +fehler = "1.0.0" + [dev-dependencies.trdelnik-client] path = "../../../../crates/client" [dev-dependencies.program_client] path = "../../.program_client" -[dependencies] -assert_matches = "1.4.0" -fehler = "1.0.0" -rstest = "0.12.0" - -[dependencies.turnstile] +[dev-dependencies.turnstile] path = "../../programs/turnstile" From 85dc9090748ea013b29db7312b7f8f28a046b750 Mon Sep 17 00:00:00 2001 From: lukacan Date: Thu, 22 Feb 2024 00:30:04 +0100 Subject: [PATCH 5/8] =?UTF-8?q?=F0=9F=94=A5=20remove=20unnecessary=20code?= =?UTF-8?q?=20+=20comments=20+=20function=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🔥 remove unnecessary code ♻️ group dependencies --- crates/cli/src/command/fuzz.rs | 2 +- crates/client/Cargo.toml | 75 +++++++------ crates/client/src/test_generator.rs | 168 +++++++++++++--------------- 3 files changed, 118 insertions(+), 127 deletions(-) diff --git a/crates/cli/src/command/fuzz.rs b/crates/cli/src/command/fuzz.rs index 9e1e9519..7453e7f4 100644 --- a/crates/cli/src/command/fuzz.rs +++ b/crates/cli/src/command/fuzz.rs @@ -68,7 +68,7 @@ pub async fn fuzz(root: Option, subcmd: FuzzCommand) { // generate generator with root so that we do not need to again // look for root within the generator let mut generator = TestGenerator::new_with_root(root); - generator.add_fuzz_test().await?; + generator.generate_fuzz().await?; } }; } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 98d3bd73..30054f3b 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -24,49 +24,50 @@ pretty_assertions = "1.1.0" macrotest = "1.0.9" [dependencies] -trdelnik-test = { workspace = true } -anchor-lang = { version = "0.29.0", features = ["idl-build"] } +trdelnik-derive-displayix = { path = "./derive/display_ix" } +trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" } +trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" } +trdelnik-test = { workspace = true } # INFO: This is a hack! Anchor-spl is here as dependency only to activate the idl-build feature, so that # users do not have to do it manually in their program's Cargo.toml -anchor-spl = { version = "0.29.0", features = ["idl-build"] } -solana-sdk = { workspace = true } -solana-cli-output = { workspace = true } -solana-transaction-status = { workspace = true } -solana-account-decoder = { workspace = true } -anchor-client = { workspace = true } -spl-token = { workspace = true } -spl-associated-token-account = { workspace = true } -tokio = { workspace = true } -rand = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } -bincode = { workspace = true } -borsh = { workspace = true } -futures = { workspace = true } -fehler = { workspace = true } -thiserror = { workspace = true } -ed25519-dalek = { workspace = true } -serial_test = { workspace = true } -anyhow = { workspace = true } -cargo_metadata = { workspace = true } -syn = { workspace = true } -quote = { workspace = true } -heck = { workspace = true } -toml = { workspace = true } -log = { workspace = true } -rstest = { workspace = true } -lazy_static = { workspace = true } -proc-macro2 = { workspace = true } -honggfuzz = { version = "0.5.55", optional = true } -arbitrary = { version = "1.3.0", features = ["derive"] } +anchor-spl = { version = "0.29.0", features = ["idl-build"] } +anchor-lang = { version = "0.29.0", features = ["idl-build"] } +solana-sdk = { workspace = true } +solana-cli-output = { workspace = true } +solana-transaction-status = { workspace = true } +solana-account-decoder = { workspace = true } +anchor-client = { workspace = true } +spl-token = { workspace = true } +spl-associated-token-account = { workspace = true } +tokio = { workspace = true } +rand = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +bincode = { workspace = true } +borsh = { workspace = true } +futures = { workspace = true } +fehler = { workspace = true } +thiserror = { workspace = true } +ed25519-dalek = { workspace = true } +serial_test = { workspace = true } +anyhow = { workspace = true } +cargo_metadata = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } +heck = { workspace = true } +toml = { workspace = true } +log = { workspace = true } +rstest = { workspace = true } +lazy_static = { workspace = true } +proc-macro2 = { workspace = true } +arbitrary = { version = "1.3.0", features = ["derive"] } +honggfuzz = { version = "0.5.55", optional = true } # solana-program-test = { version = "=1.17.16", optional = true } -solana-program-test-anchor-fix = { version = "1.17.9", optional = true } quinn-proto = { version = "0.10.6", optional = true } solana-program-runtime = { version = "<1.18", optional = true } shellexpand = { workspace = true } -trdelnik-derive-displayix = { path = "./derive/display_ix" } -trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" } -trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" } + pathdiff = "0.2.1" solana-banks-client = "<1.18" indicatif = "0.17.8" +solana-program-test-anchor-fix = {version = "1.17.9", optional = true} diff --git a/crates/client/src/test_generator.rs b/crates/client/src/test_generator.rs index 5e124468..cd9622a7 100644 --- a/crates/client/src/test_generator.rs +++ b/crates/client/src/test_generator.rs @@ -7,13 +7,12 @@ use crate::{ }; use cargo_metadata::{camino::Utf8PathBuf, Package}; use fehler::{throw, throws}; +use std::{fs::File, io::prelude::*}; use std::{ - env, fs::OpenOptions, io, path::{Path, PathBuf}, }; -use std::{fs::File, io::prelude::*}; use syn::ItemUse; use thiserror::Error; use tokio::fs; @@ -102,62 +101,66 @@ impl TestGenerator { #[throws] pub async fn generate_both(&mut self) { // expands programs within programs folder - self.expand_programs_data().await?; + self.expand_programs().await?; // expands program_client and obtains // use statements // if program_client is not yet initialized // use statements are set to default self.expand_program_client().await?; - self.create_program_client_crate().await?; - self.create_trdelnik_tests_crate().await?; + // add/update program_client + self.add_program_client().await?; + // add poc test self.add_new_poc_test().await?; + // add fuzz test self.add_new_fuzz_test().await?; + // add trdelnik.toml self.create_trdelnik_manifest().await?; - self.update_gitignore("hfuzz_target")?; + // update gitignore + self.update_gitignore(CARGO_TARGET_DIR_DEFAULT)?; } /// Generates fuzz tests along with the necessary setup. #[throws] pub async fn generate_fuzz(&mut self) { - self.expand_programs_data().await?; - self.expand_program_client().await?; - self.create_trdelnik_tests_crate().await?; + // expand programs + self.expand_programs().await?; + // generate fuzz test self.add_new_fuzz_test().await?; + // add trdelnik.toml self.create_trdelnik_manifest().await?; - self.update_gitignore("trdelnik-tests/fuzz_tests/fuzzing/hfuzz_target")?; + // update gitignore + self.update_gitignore(CARGO_TARGET_DIR_DEFAULT)?; } /// Generates proof of concept (POC) tests along with the necessary setup. #[throws] pub async fn generate_poc(&mut self) { - self.expand_programs_data().await?; + // expand programs + self.expand_programs().await?; + // expand program_client self.expand_program_client().await?; - self.create_program_client_crate().await?; - self.create_trdelnik_tests_crate().await?; + // add/update program_client + self.add_program_client().await?; + // add poc test self.add_new_poc_test().await?; + // add trdelnik.toml self.create_trdelnik_manifest().await?; } #[throws] pub async fn build(&mut self) { - self.expand_programs_data().await?; - self.create_program_client_crate().await?; - } - /// ## Adds new Fuzz test template to the trdelnik-tests folder - #[throws] - pub async fn add_fuzz_test(&mut self) { - self.packages = Commander::collect_program_packages().await?; - (self.idl, self.codes_libs_pairs) = - Commander::expand_program_packages(&self.packages).await?; - self.add_new_fuzz_test().await?; + // expand programs + self.expand_programs().await?; + // expand program_client + self.expand_program_client().await?; + // add/update program_client + self.add_program_client().await?; } /// Gathers and expands program data necessary for generating tests. #[throws] - async fn expand_programs_data(&mut self) { + 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.use_tokens = Commander::expand_program_client().await?; } - /// Gathers and expands program data necessary for generating tests. #[throws] async fn expand_program_client(&mut self) { self.use_tokens = Commander::expand_program_client().await?; @@ -178,8 +181,8 @@ impl TestGenerator { let cargo_path = construct_path!(poc_dir_path, CARGO_TOML); let poc_test_path = construct_path!(new_poc_test_dir, POC_TEST); - self.create_directory(&poc_dir_path).await?; - self.create_directory(&new_poc_test_dir).await?; + // self.create_directory(&poc_dir_path).await?; + self.create_directory_all(&new_poc_test_dir).await?; let cargo_toml_content = load_template("/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl")?; self.create_file(&cargo_path, &cargo_toml_content).await?; @@ -200,55 +203,9 @@ impl TestGenerator { self.add_program_dependencies(&poc_dir_path, "dev-dependencies", None) .await?; } - - /// Creates the `Trdelnik.toml` file - #[throws] - async fn create_trdelnik_manifest(&self) { - let trdelnik_toml_path = construct_path!(self.root, TRDELNIK_TOML); - let trdelnik_toml_content = load_template("/src/templates/Trdelnik.toml.tmpl")?; - self.create_file(&trdelnik_toml_path, &trdelnik_toml_content) - .await?; - } - - /// Creates the `trdelnik-tests` folder - #[throws] - async fn create_trdelnik_tests_crate(&self) { - let workspace_path = construct_path!(self.root, TESTS_WORKSPACE_DIRECTORY); - self.create_directory(&workspace_path).await?; - } - #[throws] - async fn add_workspace_member(&self, member: &str) { - let cargo = construct_path!(self.root, CARGO_TOML); - let mut content: Value = fs::read_to_string(&cargo).await?.parse()?; - let new_member = Value::String(String::from(member)); - - let members = content - .as_table_mut() - .ok_or(Error::CannotParseCargoToml)? - .entry("workspace") - .or_insert(Value::Table(Table::default())) - .as_table_mut() - .ok_or(Error::CannotParseCargoToml)? - .entry("members") - .or_insert(Value::Array(vec![new_member.clone()])) - .as_array_mut() - .ok_or(Error::CannotParseCargoToml)?; - - match members.iter().find(|&x| x.eq(&new_member)) { - Some(_) => { - println!("\x1b[93mSkipping\x1b[0m: {CARGO_TOML}, already contains {member}.") - } - None => { - members.push(new_member); - println!("\x1b[92mSuccesfully\x1b[0m updated: {CARGO_TOML} with {member} member."); - } - }; - fs::write(cargo, content.to_string()).await?; - } - /// ## Creates program client folder and generates source code #[throws] - async fn create_program_client_crate(&self) { + async fn add_program_client(&self) { let cargo_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, CARGO_TOML); let src_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY); let crate_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY); @@ -327,11 +284,8 @@ impl TestGenerator { let fuzz_test_path = new_fuzz_test_dir.join(FUZZ_TEST); - let fuzz_test_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/trdelnik-tests/test_fuzz.rs" - )) - .to_string(); + let fuzz_test_content = load_template("/src/templates/trdelnik-tests/test_fuzz.rs")?; + let use_entry = format!("use {}::entry;\n", program_name); let use_instructions = format!("use {}::ID as PROGRAM_ID;\n", program_name); let use_fuzz_instructions = format!( @@ -362,12 +316,10 @@ impl TestGenerator { self.create_file(&accounts_snapshots_path, &fuzzer_snapshots) .await?; - let cargo_toml_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl" - )); + let cargo_toml_content = + load_template("/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl")?; - self.create_file(&fuzz_tests_manifest_path, cargo_toml_content) + self.create_file(&fuzz_tests_manifest_path, &cargo_toml_content) .await?; self.add_bin_target(&fuzz_tests_manifest_path, &new_fuzz_test, &new_bin_target) @@ -381,9 +333,47 @@ impl TestGenerator { .await?; } + /// Creates the `Trdelnik.toml` file + #[throws] + async fn create_trdelnik_manifest(&self) { + let trdelnik_toml_path = construct_path!(self.root, TRDELNIK_TOML); + let trdelnik_toml_content = load_template("/src/templates/Trdelnik.toml.tmpl")?; + self.create_file(&trdelnik_toml_path, &trdelnik_toml_content) + .await?; + } + #[throws] + async fn add_workspace_member(&self, member: &str) { + let cargo = construct_path!(self.root, CARGO_TOML); + let mut content: Value = fs::read_to_string(&cargo).await?.parse()?; + let new_member = Value::String(String::from(member)); + + let members = content + .as_table_mut() + .ok_or(Error::CannotParseCargoToml)? + .entry("workspace") + .or_insert(Value::Table(Table::default())) + .as_table_mut() + .ok_or(Error::CannotParseCargoToml)? + .entry("members") + .or_insert(Value::Array(vec![new_member.clone()])) + .as_array_mut() + .ok_or(Error::CannotParseCargoToml)?; + + match members.iter().find(|&x| x.eq(&new_member)) { + Some(_) => { + println!("\x1b[93mSkipping\x1b[0m: {CARGO_TOML}, already contains {member}.") + } + None => { + members.push(new_member); + println!("\x1b[92mSuccesfully\x1b[0m updated: {CARGO_TOML} with {member} member."); + fs::write(cargo, content.to_string()).await?; + } + }; + } + /// ## Creates a new directory and all missing parent directories on the specified path #[throws] - async fn create_directory_all<'a>(&self, path: &'a PathBuf) { + async fn create_directory_all(&self, path: &PathBuf) { match path.exists() { true => {} false => { @@ -393,7 +383,7 @@ impl TestGenerator { } /// ## Creates directory with specified path #[throws] - async fn create_directory<'a>(&self, path: &'a Path) { + async fn create_directory(&self, path: &PathBuf) { match path.exists() { true => {} false => { @@ -404,7 +394,7 @@ impl TestGenerator { /// ## Creates a new file with a given content on the specified path /// - Skip if file already exists #[throws] - async fn create_file<'a>(&self, path: &'a Path, content: &str) { + async fn create_file(&self, path: &PathBuf, content: &str) { let file = path.strip_prefix(&self.root).unwrap().to_str().unwrap(); match path.exists() { @@ -420,7 +410,7 @@ impl TestGenerator { /// ## Updates a file with a given content on the specified path /// - Skip if file does not exists #[throws] - async fn update_file<'a>(&self, path: &'a Path, content: &str) { + async fn update_file(&self, path: &PathBuf, content: &str) { let file = path.strip_prefix(&self.root).unwrap().to_str().unwrap(); match path.exists() { true => { From 4ef374745857f37895250df400a7fbdeba623c08 Mon Sep 17 00:00:00 2001 From: lukacan Date: Thu, 22 Feb 2024 19:32:11 +0100 Subject: [PATCH 6/8] =?UTF-8?q?=F0=9F=94=A5=20remove=20old=20cargo=20build?= =?UTF-8?q?=20and=20use=20simple=20anchor=20build=20as=20we=20support=20on?= =?UTF-8?q?ly=20anchor=20programs=20for=20both=20test=20types=20+=20add=20?= =?UTF-8?q?build=20before=20poc=20test?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/cli/src/command/test.rs | 1 + crates/client/src/commander.rs | 12 +++--------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/crates/cli/src/command/test.rs b/crates/cli/src/command/test.rs index 4a16b1cf..e2fed666 100644 --- a/crates/cli/src/command/test.rs +++ b/crates/cli/src/command/test.rs @@ -21,5 +21,6 @@ pub async fn test(root: Option) { } }; let commander = Commander::with_root(root); + commander.build_anchor_project().await?; commander.run_tests().await?; } diff --git a/crates/client/src/commander.rs b/crates/client/src/commander.rs index e10319e8..1ae06c17 100644 --- a/crates/client/src/commander.rs +++ b/crates/client/src/commander.rs @@ -110,15 +110,10 @@ impl Commander { Self { root: root.into() } } - /// Builds programs (smart contracts). #[throws] - pub async fn build_programs(&self) { - let success = Command::new("cargo") - .arg("build-bpf") - .arg("--") - // prevent prevent dependency loop: - // program tests -> program_client -> program - .args(["-Z", "avoid-dev-deps"]) + pub async fn build_anchor_project(&self) { + let success = Command::new("anchor") + .arg("build") .spawn()? .wait() .await? @@ -127,7 +122,6 @@ impl Commander { throw!(Error::BuildProgramsFailed); } } - /// Runs standard Rust tests. /// /// _Note_: The [--nocapture](https://doc.rust-lang.org/cargo/commands/cargo-test.html#display-options) argument is used From f6539e296ad91fcb512ed61ffad6d34dcfe2302a Mon Sep 17 00:00:00 2001 From: lukacan Date: Fri, 23 Feb 2024 14:46:14 +0100 Subject: [PATCH 7/8] =?UTF-8?q?=F0=9F=90=9B=20init=20will=20not=20initiali?= =?UTF-8?q?ze=20fuzz=20test=20if=20already=20initialized?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ⏪️ root option with Commander ♻️ remove nested block ♻️ spelling ♻️ remove unnecessary condition ♻️ change fn name 📝 add basic documentation for test_generator functions 📝 Commander simple documentation --- crates/cli/src/command/fuzz.rs | 2 +- crates/client/Cargo.toml | 75 ++- crates/client/src/commander.rs | 84 +++- .../templates/program_client/Cargo.toml.tmpl | 2 +- crates/client/src/test_generator.rs | 474 ++++++++++++++---- 5 files changed, 475 insertions(+), 162 deletions(-) diff --git a/crates/cli/src/command/fuzz.rs b/crates/cli/src/command/fuzz.rs index 7453e7f4..9e1e9519 100644 --- a/crates/cli/src/command/fuzz.rs +++ b/crates/cli/src/command/fuzz.rs @@ -68,7 +68,7 @@ pub async fn fuzz(root: Option, subcmd: FuzzCommand) { // generate generator with root so that we do not need to again // look for root within the generator let mut generator = TestGenerator::new_with_root(root); - generator.generate_fuzz().await?; + generator.add_fuzz_test().await?; } }; } diff --git a/crates/client/Cargo.toml b/crates/client/Cargo.toml index 30054f3b..5309f99e 100644 --- a/crates/client/Cargo.toml +++ b/crates/client/Cargo.toml @@ -24,50 +24,49 @@ pretty_assertions = "1.1.0" macrotest = "1.0.9" [dependencies] -trdelnik-derive-displayix = { path = "./derive/display_ix" } -trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" } -trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" } -trdelnik-test = { workspace = true } +trdelnik-derive-displayix = { path = "./derive/display_ix" } +trdelnik-derive-fuzz-deserialize = { path = "./derive/fuzz_deserialize" } +trdelnik-derive-fuzz-test-executor = { path = "./derive/fuzz_test_executor" } +trdelnik-test = { workspace = true } # INFO: This is a hack! Anchor-spl is here as dependency only to activate the idl-build feature, so that # users do not have to do it manually in their program's Cargo.toml -anchor-spl = { version = "0.29.0", features = ["idl-build"] } -anchor-lang = { version = "0.29.0", features = ["idl-build"] } -solana-sdk = { workspace = true } -solana-cli-output = { workspace = true } -solana-transaction-status = { workspace = true } -solana-account-decoder = { workspace = true } -anchor-client = { workspace = true } -spl-token = { workspace = true } -spl-associated-token-account = { workspace = true } -tokio = { workspace = true } -rand = { workspace = true } -serde_json = { workspace = true } -serde = { workspace = true } -bincode = { workspace = true } -borsh = { workspace = true } -futures = { workspace = true } -fehler = { workspace = true } -thiserror = { workspace = true } -ed25519-dalek = { workspace = true } -serial_test = { workspace = true } -anyhow = { workspace = true } -cargo_metadata = { workspace = true } -syn = { workspace = true } -quote = { workspace = true } -heck = { workspace = true } -toml = { workspace = true } -log = { workspace = true } -rstest = { workspace = true } -lazy_static = { workspace = true } -proc-macro2 = { workspace = true } -arbitrary = { version = "1.3.0", features = ["derive"] } -honggfuzz = { version = "0.5.55", optional = true } +anchor-spl = { version = "0.29.0", features = ["idl-build"] } +anchor-lang = { version = "0.29.0", features = ["idl-build"] } +solana-sdk = { workspace = true } +solana-cli-output = { workspace = true } +solana-transaction-status = { workspace = true } +solana-account-decoder = { workspace = true } +anchor-client = { workspace = true } +spl-token = { workspace = true } +spl-associated-token-account = { workspace = true } +tokio = { workspace = true } +rand = { workspace = true } +serde_json = { workspace = true } +serde = { workspace = true } +bincode = { workspace = true } +borsh = { workspace = true } +futures = { workspace = true } +fehler = { workspace = true } +thiserror = { workspace = true } +ed25519-dalek = { workspace = true } +serial_test = { workspace = true } +anyhow = { workspace = true } +cargo_metadata = { workspace = true } +syn = { workspace = true } +quote = { workspace = true } +heck = { workspace = true } +toml = { workspace = true } +log = { workspace = true } +rstest = { workspace = true } +lazy_static = { workspace = true } +proc-macro2 = { workspace = true } +honggfuzz = { version = "0.5.55", optional = true } +arbitrary = { version = "1.3.0", features = ["derive"] } +solana-program-test-anchor-fix = { version = "1.17.9", optional = true } # solana-program-test = { version = "=1.17.16", optional = true } quinn-proto = { version = "0.10.6", optional = true } solana-program-runtime = { version = "<1.18", optional = true } shellexpand = { workspace = true } - pathdiff = "0.2.1" solana-banks-client = "<1.18" indicatif = "0.17.8" -solana-program-test-anchor-fix = {version = "1.17.9", optional = true} diff --git a/crates/client/src/commander.rs b/crates/client/src/commander.rs index 1ae06c17..a0891488 100644 --- a/crates/client/src/commander.rs +++ b/crates/client/src/commander.rs @@ -282,16 +282,13 @@ impl Commander { cargo_toml_data.packages.into_iter().filter(|package| { // TODO less error-prone test if the package is a _program_? - // This will only consider Packages where path: - // /home/xyz/xyz/trdelnik/trdelnik/examples/example_project/programs/package1 - // NOTE we can obtain more important information here, only to remember if let Some("programs") = package.manifest_path.iter().nth_back(2) { return true; } false }) } - + /// Collects and returns a list of program packages using cargo metadata. #[throws] pub async fn collect_program_packages() -> Vec { let packages: Vec = Commander::program_packages().collect(); @@ -301,6 +298,18 @@ impl Commander { packages } } + /// Displays a progress spinner for a package expansion process. + /// + /// This function creates and manages a spinner-style progress bar using the `indicatif` + /// crate to visually represent the progress of expanding a specified package. + /// The progress bar continues to spin as long as a shared atomic boolean, wrapped in an `Arc`, + /// is set to `true`. Once the boolean is flipped to `false`, indicating the expansion process is complete, + /// the spinner is cleared from the terminal. + /// + /// # Parameters + /// - `package_name`: A string slice (`&str`) representing the name of the package being expanded. + /// This name is displayed within the progress bar's message to indicate which package is currently being processed. + /// - `mutex`: A reference to an `Arc` that acts as a flag for the progress bar's activity. fn expand_progress_bar( package_name: &str, mutex: &std::sync::Arc, @@ -312,7 +321,7 @@ impl Commander { .unwrap(), ); - let msg = format!("Expanding: {package_name}... this may take a while"); + let msg = format!("\x1b[92mExpanding\x1b[0m: {package_name}... this may take a while"); progress_bar.set_message(msg); while mutex.load(std::sync::atomic::Ordering::SeqCst) { progress_bar.inc(1); @@ -321,6 +330,27 @@ impl Commander { progress_bar.finish_and_clear(); } + /// Expands program packages, extracting IDL and codes_libs pairs. + /// + /// This function iterates over a slice of `cargo_metadata::Package` objects, + /// initiating an expansion process for each. + /// The expansion involves parsing the package to extract IDL and determining the path to the code. + /// It utilizes multithreading to handle package expansions and concurrently progress bar. + /// + /// # Parameters + /// - `packages`: A slice of `cargo_metadata::Package`, representing the packages to be processed. + /// + /// # Returns + /// A tuple containing: + /// - An `Idl` object aggregating all IDL programs extracted from the packages. + /// - A vector of tuples, each consisting of a `String` (representing code) and a `cargo_metadata::camino::Utf8PathBuf` + /// (representing the path to the library code) for each package. + /// + /// # Errors + /// Returns an error if: + /// - The library path for a package cannot be found (`Error::ReadProgramCodeFailed`). + /// - 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], @@ -385,9 +415,22 @@ impl Commander { ) } } + /// Executes a cargo command to expand the Rust source code of a specified package. + /// + /// This function leverages the `cargo +nightly` command to compile a given package with the `rustc` compiler, + /// specifically using the `-Zunpretty=expanded` option to output the expanded form of the Rust source code. + /// This operation is performed under the `check` profile, which checks the code for errors without producing + /// executable artifacts. The function is designed to provide insights into the macro-expanded source code + /// + /// # Parameters + /// - `package_name`: A string slice (`&str`) representing the name of the package to be expanded. + /// + /// # Returns + /// - Returns a `std::process::Output` object containing the outcome of the cargo command execution. This includes + /// the expanded source code in the command's stdout, along with any stderr output and the exit status. fn expand_package(package_name: &str) -> std::process::Output { std::process::Command::new("cargo") - .arg("+nightly") + .arg("+nightly-2023-12-28") .arg("rustc") .args(["--package", package_name]) .arg("--profile=check") @@ -397,12 +440,23 @@ impl Commander { .unwrap() } - /// Returns `use` modules / statements - /// The goal of this method is to find all `use` statements defined by the user in the `.program_client` - /// crate. It solves the problem with regenerating the program client and removing imports defined by - /// the user. + /// Retrieves Rust `use` statements from the expanded source code of the "program_client" package. + /// + /// This function initiates an expansion of the "program_client" package to obtain + /// the macro-expanded source code. It then parses this source to extract all `use` statement declarations, + /// returning them as a vector of `syn::ItemUse`. If no `use` statements are found, a default import for + /// `trdelnik_client::*` is included in the returned vector to ensure the caller has at least one valid import. + /// + /// # Returns + /// A `Vec` containing the parsed `use` statements from the "program_client" package's source code. + /// If no `use` statements are found, the vector contains a default `use trdelnik_client::*;` statement. + /// + /// # Errors + /// - Returns an `Error` if the package expansion fails, either due to command execution failure or issues + /// parsing the expanded code into a UTF-8 string. + /// - Propagates errors encountered while acquiring locks on shared state or parsing `use` statements from the code. #[throws] - pub async fn expand_program_client() -> Vec { + pub async fn get_program_client_imports() -> Vec { let shared_mutex = std::sync::Arc::new(std::sync::Mutex::new(String::new())); let mutex = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true)); @@ -433,13 +487,9 @@ impl Commander { let code = shared_mutex.lock().unwrap(); let code = code.as_str(); let mut use_modules: Vec = vec![]; - if code.is_empty() { + Self::get_use_statements(code, &mut use_modules)?; + if use_modules.is_empty() { use_modules.push(syn::parse_quote! { use trdelnik_client::*; }) - } else { - Self::get_use_statements(code, &mut use_modules)?; - if use_modules.is_empty() { - use_modules.push(syn::parse_quote! { use trdelnik_client::*; }) - } } use_modules } diff --git a/crates/client/src/templates/program_client/Cargo.toml.tmpl b/crates/client/src/templates/program_client/Cargo.toml.tmpl index 7ac256a4..eefda30c 100644 --- a/crates/client/src/templates/program_client/Cargo.toml.tmpl +++ b/crates/client/src/templates/program_client/Cargo.toml.tmpl @@ -4,4 +4,4 @@ version = "0.1.0" edition = "2018" [dependencies.trdelnik-client] -trdelnik-client = "0.5.0" +version = "0.5.0" diff --git a/crates/client/src/test_generator.rs b/crates/client/src/test_generator.rs index cd9622a7..9a6f2eb8 100644 --- a/crates/client/src/test_generator.rs +++ b/crates/client/src/test_generator.rs @@ -40,6 +40,67 @@ pub enum Error { ParsingCargoTomlDependenciesFailed, } +/// Constructs a `PathBuf` from a given root and a series of path components. +/// +/// This macro simplifies the creation of a `PathBuf` from multiple strings or string slices, +/// starting with a root path and appending each subsequent component in order. It's useful for +/// dynamically constructing file or directory paths in a more readable manner. +/// +/// # Syntax +/// `construct_path!(root, component1, component2, ..., componentN)` +/// +/// - `root`: The base path from which to start. Can be a `PathBuf` or any type that implements +/// `Into`, such as a string or string slice. +/// - `component1` to `componentN`: These are the components to be joined to the root path. Each +/// can be any type that implements `Into`, allowing for flexible path construction. +/// +/// # Returns +/// - Returns a `PathBuf` representing the combined path. +/// +/// # Examples +/// Basic usage: +/// +/// ``` +/// # #[macro_use] extern crate trdelnik_client; +/// # fn main() { +/// use std::path::PathBuf; +/// +/// // Constructs a PathBuf from a series of string slices +/// let path = construct_path!(PathBuf::from("/tmp"), "my_project", "src", "main.rs"); +/// assert_eq!(path, PathBuf::from("/tmp/my_project/src/main.rs")); +/// # } +/// ``` +/// +/// Note: Replace `your_crate_name` with the name of your crate where this macro is defined. + +#[macro_export] +macro_rules! construct_path { + ($root:expr, $($component:expr),*) => { + { + let mut path = $root.to_owned(); + $(path = path.join($component);)* + path + } + }; +} + +/// Represents a generator for creating tests. +/// +/// This struct is designed to hold all necessary information for generating +/// test cases for a project. It includes paths to project components, +/// interface definitions, and additional configuration for code generation. +/// +/// # 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`. +/// 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. +/// - `use_tokens`: A vector of `ItemUse` tokens from the Rust syntax, representing `use` statements that +/// should be included in the generated code for .program_client. pub struct TestGenerator { pub root: PathBuf, pub idl: Idl, @@ -53,16 +114,6 @@ impl Default for TestGenerator { } } -macro_rules! construct_path { - ($root:expr, $($component:expr),*) => { - { - let mut path = $root.to_owned(); - $(path = path.join($component);)* - path - } - }; -} - impl TestGenerator { /// Creates a new instance of `TestGenerator` with default values. /// @@ -96,23 +147,26 @@ impl TestGenerator { use_tokens: Vec::default(), } } - - /// Generates both proof of concept (POC) and fuzz tests along with necessary scaffolding. + /// Generates both proof of concept (POC) and fuzz tests along with the necessary setup. #[throws] pub async fn generate_both(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); + let commander = Commander::with_root(root_path); + // build first + commander.build_anchor_project().await?; // expands programs within programs folder self.expand_programs().await?; // expands program_client and obtains // use statements // if program_client is not yet initialized // use statements are set to default - self.expand_program_client().await?; - // add/update program_client - self.add_program_client().await?; - // add poc test - self.add_new_poc_test().await?; - // add fuzz test - self.add_new_fuzz_test().await?; + self.get_program_client_imports().await?; + // initialize program client if it is not yet initialized + self.init_program_client().await?; + // initialize poc tests if thay are not yet initialized + self.init_poc_tests().await?; + // initialize fuzz tests if thay are not yet initialized + self.init_fuzz_tests().await?; // add trdelnik.toml self.create_trdelnik_manifest().await?; // update gitignore @@ -122,10 +176,14 @@ impl TestGenerator { /// Generates fuzz tests along with the necessary setup. #[throws] pub async fn generate_fuzz(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); + let commander = Commander::with_root(root_path); + // build first + commander.build_anchor_project().await?; // expand programs self.expand_programs().await?; - // generate fuzz test - self.add_new_fuzz_test().await?; + // initialize fuzz tests if thay are not yet initialized + self.init_fuzz_tests().await?; // add trdelnik.toml self.create_trdelnik_manifest().await?; // update gitignore @@ -134,39 +192,155 @@ impl TestGenerator { /// Generates proof of concept (POC) tests along with the necessary setup. #[throws] pub async fn generate_poc(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); + let commander = Commander::with_root(root_path); + // build first + commander.build_anchor_project().await?; // expand programs self.expand_programs().await?; // expand program_client - self.expand_program_client().await?; - // add/update program_client - self.add_program_client().await?; - // add poc test - self.add_new_poc_test().await?; + self.get_program_client_imports().await?; + // initialize program client if it is not yet initialized + self.init_program_client().await?; + // initialize poc tests if thay are not yet initialized + self.init_poc_tests().await?; + // add trdelnik.toml + self.create_trdelnik_manifest().await?; + } + + /// Adds new fuzz test. This means create new directory within the + /// trdelnik-tests/fuzz_tests directory, generate necessary files + /// for fuzzing (instructions and snapshots) and modify + /// trdelnik-tests/fuzz_tests/Cargo.toml with the new generated + /// fuzz test binary. + #[throws] + pub async fn add_fuzz_test(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); + let commander = Commander::with_root(root_path); + // build first + commander.build_anchor_project().await?; + // expand programs + self.expand_programs().await?; + // initialize fuzz tests if thay are not yet initialized + self.add_new_fuzz_test().await?; // add trdelnik.toml self.create_trdelnik_manifest().await?; + // update gitignore + self.update_gitignore(CARGO_TARGET_DIR_DEFAULT)?; } + /// Performs anchor build command and modify .program_client + /// folder based on the updated program contents. If the .program_client + /// is not yet generated, this will also generate the crate. #[throws] pub async fn build(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); + let commander = Commander::with_root(root_path); + // build first + commander.build_anchor_project().await?; // expand programs self.expand_programs().await?; // expand program_client - self.expand_program_client().await?; + self.get_program_client_imports().await?; // add/update program_client self.add_program_client().await?; } - /// Gathers and expands program data necessary for generating tests. + /// Collect program packages within the programs folder. + /// Call rustc +nightly command in order to expand program macros, then parse + /// the expanded code and obtain necessary data for generating test files #[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?; } + /// Expand .program_client source code and obtain its use statements. + #[throws] + async fn get_program_client_imports(&mut self) { + self.use_tokens = Commander::get_program_client_imports().await?; + } + + /// Checks if the whole folder structure for .program_client is already + /// present, if not create/update .program_client crate with the necessary files. #[throws] - async fn expand_program_client(&mut self) { - self.use_tokens = Commander::expand_program_client().await?; + async fn init_program_client(&mut self) { + let cargo_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, CARGO_TOML); + let src_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY); + let crate_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY); + 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."); + } else { + self.add_program_client().await?; + } + } + + // Checks if whole Fuzz Test folder structer is already initialized, + // and if fuzz_tests directory contains anything except Cargo.toml and fuzzing folder + // if so the function does not proceed with Fuzz inicialization + #[throws] + async fn init_fuzz_tests(&mut self) { + // create reuqired paths + let fuzz_dir_path = + construct_path!(self.root, TESTS_WORKSPACE_DIRECTORY, FUZZ_TEST_DIRECTORY); + let fuzz_tests_manifest_path = construct_path!(fuzz_dir_path, CARGO_TOML); + + if fuzz_dir_path.exists() { + // obtain directory contents + let mut directories: std::collections::HashSet<_> = fuzz_dir_path + .read_dir() + .expect("Reading directory failed") + .map(|r| { + r.expect("Reading directory; DirEntry error") + .file_name() + .to_string_lossy() + .to_string() + }) + .collect(); + directories.retain(|x| x != "fuzzing"); + 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."); + } else { + self.add_new_fuzz_test().await? + } + } else { + self.add_new_fuzz_test().await? + } + } + + // Checks if whole PoC Test folder structer is already initialized, if so + // the function does not proceed with PoC inicialization + #[throws] + async fn init_poc_tests(&mut self) { + // create reuqired paths + + 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); + let cargo_path = construct_path!(poc_dir_path, CARGO_TOML); + let poc_test_path = construct_path!(new_poc_test_dir, POC_TEST); + + // if folder structure exists we skip + if poc_dir_path.exists() + && new_poc_test_dir.exists() + && cargo_path.exists() + && poc_test_path.exists() + { + println!("\x1b[93mSkipping\x1b[0m: looks like PoC Tests are already initialized."); + } else { + self.add_new_poc_test().await?; + } } - /// Adds a new proof of concept (POC) test to the test workspace. + /// Adds new PoC Test (This will Generate only one PoC Test file). + /// If not present create trdelnik-tests directory. + /// If not present create poc_tests directory. + /// If not present create tests directory. + /// If not present generate PoC test file. + /// If not present add program dependencies into the Cargo.toml file inside poc_tests folder + /// 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() { @@ -203,36 +377,15 @@ impl TestGenerator { self.add_program_dependencies(&poc_dir_path, "dev-dependencies", None) .await?; } - /// ## Creates program client folder and generates source code - #[throws] - async fn add_program_client(&self) { - let cargo_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, CARGO_TOML); - let src_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY); - let crate_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY); - let lib_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY, LIB); - - self.create_directory_all(&src_path).await?; - - // load template - let cargo_toml_content = load_template("/src/templates/program_client/Cargo.toml.tmpl")?; - - // if path exists the file will not be overwritten - self.create_file(&cargo_path, &cargo_toml_content).await?; - - self.add_program_dependencies(&crate_path, "dependencies", Some(vec!["no-entrypoint"])) - .await?; - - let program_client = - program_client_generator::generate_source_code(&self.idl, &self.use_tokens); - let program_client = Commander::format_program_code(&program_client).await?; - if lib_path.exists() { - self.update_file(&lib_path, &program_client).await?; - } else { - self.create_file(&lib_path, &program_client).await?; - } - } - /// Creates the `trdelnik-tests` workspace with `src/bin` directory and empty `fuzz_target.rs` file + /// Adds new Fuzz Test. + /// If not present create trdelnik-tests directory. + /// If not present create fuzz_tests directory. + /// Obtain name for the new fuzz test and generate new fuzz test + /// directory inside fuzz_tests folder. + /// Generate fuzz test files and update Cargo.toml with the new Fuzz Test binary path. + /// If not present add program dependencies into the Cargo.toml file inside fuzz_tests folder + /// 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() { @@ -333,7 +486,44 @@ impl TestGenerator { .await?; } - /// Creates the `Trdelnik.toml` file + /// Add/Update .program_client + /// If not present create .program_client directory. + /// If not present create src directory. + /// If not present create Cargo.toml file + /// If not present add program dependencies into the Cargo.toml file inside .program_client folder + /// Generate .program_client code. + /// If not present add .program_client code + /// If present update .program_client code + #[throws] + async fn add_program_client(&self) { + let cargo_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, CARGO_TOML); + let src_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY); + let crate_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY); + let lib_path = construct_path!(self.root, PROGRAM_CLIENT_DIRECTORY, SRC_DIRECTORY, LIB); + + self.create_directory_all(&src_path).await?; + + // load template + let cargo_toml_content = load_template("/src/templates/program_client/Cargo.toml.tmpl")?; + + // if path exists the file will not be overwritten + self.create_file(&cargo_path, &cargo_toml_content).await?; + + self.add_program_dependencies(&crate_path, "dependencies", Some(vec!["no-entrypoint"])) + .await?; + + let program_client = + program_client_generator::generate_source_code(&self.idl, &self.use_tokens); + let program_client = Commander::format_program_code(&program_client).await?; + + if lib_path.exists() { + self.update_file(&lib_path, &program_client).await?; + } else { + self.create_file(&lib_path, &program_client).await?; + } + } + + /// If not present create Trdelnik manifest with the templte. #[throws] async fn create_trdelnik_manifest(&self) { let trdelnik_toml_path = construct_path!(self.root, TRDELNIK_TOML); @@ -341,6 +531,26 @@ impl TestGenerator { self.create_file(&trdelnik_toml_path, &trdelnik_toml_content) .await?; } + /// Adds a new member to the Cargo workspace manifest (`Cargo.toml`). + /// + /// This function updates the `Cargo.toml` file located at the root of the Cargo workspace + /// by adding a new member to the `members` array within the `[workspace]` table. If the specified member + /// already exists in the `members` array, the function will skip the addition and print a message indicating + /// that the member is already present. Otherwise, it will add the new member and print a success message. + /// + /// # Parameters + /// - `&self`: A reference to the current instance of the TestGenerator struct that holds the workspace root path. + /// - `member`: A string slice (`&str`) representing the path to the new member to be added. This path should be + /// relative to the workspace root. + /// + /// # Errors + /// Returns an error if: + /// - The `Cargo.toml` file cannot be found, read, or is not properly formatted. + /// - The `Cargo.toml` file does not contain a `[workspace]` table or a `members` array within that table, + /// and it cannot be created. + /// + /// The function uses `Error::CannotParseCargoToml` to indicate failures related to parsing or updating the + /// `Cargo.toml` file. #[throws] async fn add_workspace_member(&self, member: &str) { let cargo = construct_path!(self.root, CARGO_TOML); @@ -365,13 +575,14 @@ impl TestGenerator { } None => { members.push(new_member); - println!("\x1b[92mSuccesfully\x1b[0m updated: {CARGO_TOML} with {member} member."); + println!("\x1b[92mSuccessfully\x1b[0m updated: {CARGO_TOML} with {member} member."); fs::write(cargo, content.to_string()).await?; } }; } - /// ## Creates a new directory and all missing parent directories on the specified path + /// If not present creates a new directory and all missing + /// parent directories on the specified path #[throws] async fn create_directory_all(&self, path: &PathBuf) { match path.exists() { @@ -381,7 +592,7 @@ impl TestGenerator { } }; } - /// ## Creates directory with specified path + /// If not present creates directory with specified path #[throws] async fn create_directory(&self, path: &PathBuf) { match path.exists() { @@ -391,8 +602,8 @@ impl TestGenerator { } }; } - /// ## Creates a new file with a given content on the specified path - /// - Skip if file already exists + /// If not present creates a new file with a given content on the specified path + /// If file is present, skip #[throws] async fn create_file(&self, path: &PathBuf, content: &str) { let file = path.strip_prefix(&self.root).unwrap().to_str().unwrap(); @@ -403,28 +614,43 @@ impl TestGenerator { } false => { fs::write(path, content).await?; - println!("\x1b[92mSuccesfully\x1b[0m created: {file}."); + println!("\x1b[92mSuccessfully\x1b[0m created: {file}."); } }; } - /// ## Updates a file with a given content on the specified path - /// - Skip if file does not exists + /// If present update a new file with a given content on the specified path + /// If file is not present, skip #[throws] async fn update_file(&self, path: &PathBuf, content: &str) { let file = path.strip_prefix(&self.root).unwrap().to_str().unwrap(); match path.exists() { true => { fs::write(path, content).await?; - println!("\x1b[92mSuccesfully\x1b[0m updated: {file}."); + println!("\x1b[92mSuccessfully\x1b[0m updated: {file}."); } false => { fs::write(path, content).await?; - println!("\x1b[92mSuccesfully\x1b[0m created: {file}."); + println!("\x1b[92mSuccessfully\x1b[0m created: {file}."); } }; } - /// ## Updates .gitignore file in the `root` directory and appends `ignored_path` to the end of the file + /// Updates the `.gitignore` file by appending a specified path to ignore. + /// + /// This function checks if the given `ignored_path` is already listed in the `.gitignore` file at the root + /// of the repository. If the path is not found, it appends the `ignored_path` to the file, ensuring that it + /// is ignored by Git. If the `.gitignore` file does not exist or the path is already included, the function + /// will skip the addition and print a message. + /// + /// # Parameters + /// - `&self`: A reference to the current instance of the TestGenerator that holds the repository root path. + /// - `ignored_path`: A string slice (`&str`) representing the path to be ignored by Git. This path should be + /// relative to the repository root. + /// + /// # Errors + /// Returns an error if: + /// - The `.gitignore` file exists but cannot be opened or read. + /// - There is an error writing the new `ignored_path` to the `.gitignore` file. #[throws] fn update_gitignore(&self, ignored_path: &str) { let gitignore_path = construct_path!(self.root, GIT_IGNORE); @@ -447,12 +673,31 @@ impl TestGenerator { if let Ok(mut file) = file { writeln!(file, "{}", ignored_path)?; - println!("\x1b[92mSuccesfully\x1b[0m updated: {GIT_IGNORE} with {ignored_path}."); + println!("\x1b[92mSuccessfully\x1b[0m updated: {GIT_IGNORE} with {ignored_path}."); } } else { println!("\x1b[93mSkipping\x1b[0m: {GIT_IGNORE}, not found.") } } + /// Adds a new binary target to a Cargo.toml file. + /// + /// This function reads the existing `Cargo.toml` file from the specified path, adds a new binary target + /// configuration to it, and writes the updated content back to the file. It handles the creation of a new + /// `[[bin]]` section if one does not already exist or appends the new binary target to the existing `[[bin]]` + /// array. The new binary target is specified by its name and the path to its source file. + /// + /// # Parameters + /// - `&self`: A reference to the current instance of the TestGenerator, not used directly in this function but + /// necessary for method calls on the instance. + /// - `cargo_path`: A reference to a `PathBuf` that specifies the path to the `Cargo.toml` file to be updated. + /// - `name`: A string slice (`&str`) representing the name of the binary target to be added. + /// - `path`: A string slice (`&str`) representing the path to the source file of the binary target, relative + /// to the Cargo package's root. + /// + /// # Errors + /// This function returns an error if: + /// - The `Cargo.toml` file cannot be read or written to. + /// - The content of `Cargo.toml` cannot be parsed into a `toml::Value` or manipulated as expected. #[throws] async fn add_bin_target(&self, cargo_path: &PathBuf, name: &str, path: &str) { // Read the existing Cargo.toml file @@ -481,8 +726,28 @@ impl TestGenerator { // Write the updated Cargo.toml file fs::write(cargo_path, cargo_toml.to_string()).await?; } - /// ## Adds program dependency to specified Cargo.toml - /// - for example, we need to use program entry within the fuzzer + /// Adds program dependencies to a specified Cargo.toml file. + /// + /// This function updates the Cargo.toml file located in the given directory by adding new dependencies + /// specified by the `deps` parameter. It supports adding dependencies with or without features. The + /// dependencies are added based on the packages found in the `self.packages` collection of the TestGenerator, + /// where each package's path is adjusted to be relative to the specified `cargo_dir`. If no packages are + /// found in `self.packages`, the function will return an error. + /// + /// # Parameters + /// - `&self`: A reference to the current instance of the TestGenerator, which contains a collection of packages + /// to be added as dependencies. + /// - `cargo_dir`: A reference to a `PathBuf` indicating the directory where the `Cargo.toml` file to be updated is located. + /// - `deps`: A string slice (`&str`) specifying the section under which the dependencies should be added + /// (e.g., `dependencies`, `dev-dependencies`, etc.). + /// - `features`: An optional vector of string slices (`Vec<&str>`) specifying the features that should be + /// enabled for the dependencies being added. If `None`, no features are specified. + /// + /// # Errors + /// This function can return errors in several cases, including: + /// - If the specified `Cargo.toml` file cannot be read or written to. + /// - If parsing of the `Cargo.toml` file or the dependencies fails. + /// - If no packages are found in `self.packages`. #[throws] async fn add_program_dependencies( &self, @@ -499,38 +764,37 @@ impl TestGenerator { .and_then(toml::Value::as_table_mut) .ok_or(Error::ParsingCargoTomlDependenciesFailed)?; - if !&self.packages.is_empty() { - for package in self.packages.iter() { - let manifest_path = package.manifest_path.parent().unwrap().as_std_path(); - // INFO this will obtain relative path - let relative_path = pathdiff::diff_paths(manifest_path, cargo_dir).unwrap(); - let dep: Value = if features.is_some() { - format!( - r#"{} = {{ path = "{}", features = {:?} }}"#, - package.name, - relative_path.to_str().unwrap(), - features.as_ref().unwrap() - ) - .parse() - .unwrap() - } else { - format!( - r#"{} = {{ path = "{}" }}"#, - package.name, - relative_path.to_str().unwrap() - ) - .parse() - .unwrap() - }; - if let toml::Value::Table(table) = dep { - let (name, value) = table.into_iter().next().unwrap(); - client_toml_deps.entry(name).or_insert(value.clone()); - } - } - fs::write(cargo_path, cargo_toml_content.to_string()).await?; - } else { + if self.packages.is_empty() { throw!(Error::NoProgramsFound) } + for package in self.packages.iter() { + let manifest_path = package.manifest_path.parent().unwrap().as_std_path(); + // INFO this will obtain relative path + let relative_path = pathdiff::diff_paths(manifest_path, cargo_dir).unwrap(); + let dep: Value = if features.is_some() { + format!( + r#"{} = {{ path = "{}", features = {:?} }}"#, + package.name, + relative_path.to_str().unwrap(), + features.as_ref().unwrap() + ) + .parse() + .unwrap() + } else { + format!( + r#"{} = {{ path = "{}" }}"#, + package.name, + relative_path.to_str().unwrap() + ) + .parse() + .unwrap() + }; + if let toml::Value::Table(table) = dep { + let (name, value) = table.into_iter().next().unwrap(); + client_toml_deps.entry(name).or_insert(value.clone()); + } + } + fs::write(cargo_path, cargo_toml_content.to_string()).await?; } } From 22851eb503ada309881ed7ffc7dccf12aa000d18 Mon Sep 17 00:00:00 2001 From: lukacan Date: Tue, 27 Feb 2024 22:56:30 +0100 Subject: [PATCH 8/8] =?UTF-8?q?=F0=9F=90=9B=20change=20load=5Ftemplate=20f?= =?UTF-8?q?rom=20function=20to=20macro?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/client/src/lib.rs | 1 - crates/client/src/test_generator.rs | 35 ++++++++++++++--------------- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/crates/client/src/lib.rs b/crates/client/src/lib.rs index 0af02b07..ba185eb8 100644 --- a/crates/client/src/lib.rs +++ b/crates/client/src/lib.rs @@ -132,7 +132,6 @@ mod constants { // workspace pub const GIT_IGNORE: &str = ".gitignore"; - pub const MANIFEST_PATH: &str = env!("CARGO_MANIFEST_DIR"); // client pub const RETRY_LOCALNET_EVERY_MILLIS: u64 = 500; diff --git a/crates/client/src/test_generator.rs b/crates/client/src/test_generator.rs index 9a6f2eb8..1a836bb5 100644 --- a/crates/client/src/test_generator.rs +++ b/crates/client/src/test_generator.rs @@ -84,6 +84,13 @@ macro_rules! construct_path { }; } +/// Includes a file as a string at compile time. +macro_rules! load_template { + ($file:expr) => { + include_str!(concat!(env!("CARGO_MANIFEST_DIR"), $file)) + }; +} + /// Represents a generator for creating tests. /// /// This struct is designed to hold all necessary information for generating @@ -358,10 +365,10 @@ impl TestGenerator { // self.create_directory(&poc_dir_path).await?; self.create_directory_all(&new_poc_test_dir).await?; let cargo_toml_content = - load_template("/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl")?; - self.create_file(&cargo_path, &cargo_toml_content).await?; + load_template!("/src/templates/trdelnik-tests/Cargo_poc.toml.tmpl"); + self.create_file(&cargo_path, cargo_toml_content).await?; - let poc_test_content = load_template("/src/templates/trdelnik-tests/test.rs")?; + let poc_test_content = load_template!("/src/templates/trdelnik-tests/test.rs"); let test_content = poc_test_content.replace("###PROGRAM_NAME###", program_name); let use_instructions = format!("use program_client::{}_instruction::*;\n", program_name); let template = format!("{use_instructions}{test_content}"); @@ -437,7 +444,7 @@ impl TestGenerator { let fuzz_test_path = new_fuzz_test_dir.join(FUZZ_TEST); - let fuzz_test_content = load_template("/src/templates/trdelnik-tests/test_fuzz.rs")?; + let fuzz_test_content = load_template!("/src/templates/trdelnik-tests/test_fuzz.rs"); let use_entry = format!("use {}::entry;\n", program_name); let use_instructions = format!("use {}::ID as PROGRAM_ID;\n", program_name); @@ -470,9 +477,9 @@ impl TestGenerator { .await?; let cargo_toml_content = - load_template("/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl")?; + load_template!("/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl"); - self.create_file(&fuzz_tests_manifest_path, &cargo_toml_content) + self.create_file(&fuzz_tests_manifest_path, cargo_toml_content) .await?; self.add_bin_target(&fuzz_tests_manifest_path, &new_fuzz_test, &new_bin_target) @@ -504,10 +511,10 @@ impl TestGenerator { self.create_directory_all(&src_path).await?; // load template - let cargo_toml_content = load_template("/src/templates/program_client/Cargo.toml.tmpl")?; + let cargo_toml_content = load_template!("/src/templates/program_client/Cargo.toml.tmpl"); // if path exists the file will not be overwritten - self.create_file(&cargo_path, &cargo_toml_content).await?; + self.create_file(&cargo_path, cargo_toml_content).await?; self.add_program_dependencies(&crate_path, "dependencies", Some(vec!["no-entrypoint"])) .await?; @@ -527,8 +534,8 @@ impl TestGenerator { #[throws] async fn create_trdelnik_manifest(&self) { let trdelnik_toml_path = construct_path!(self.root, TRDELNIK_TOML); - let trdelnik_toml_content = load_template("/src/templates/Trdelnik.toml.tmpl")?; - self.create_file(&trdelnik_toml_path, &trdelnik_toml_content) + let trdelnik_toml_content = load_template!("/src/templates/Trdelnik.toml.tmpl"); + self.create_file(&trdelnik_toml_path, trdelnik_toml_content) .await?; } /// Adds a new member to the Cargo workspace manifest (`Cargo.toml`). @@ -797,11 +804,3 @@ impl TestGenerator { fs::write(cargo_path, cargo_toml_content.to_string()).await?; } } - -pub fn load_template(file_path: &str) -> Result { - let mut _path = String::from(MANIFEST_PATH); - _path.push_str(file_path); - let full_path = Path::new(&_path); - - std::fs::read_to_string(full_path) -}