From f3741eed90a9576c778fb4c5f5a7facfcce0cc9d Mon Sep 17 00:00:00 2001 From: andrejlukacovic <37964423+lukacan@users.noreply.github.com> Date: Fri, 23 Feb 2024 09:36:10 +0100 Subject: [PATCH] feat/Added CLI option to initialize fuzz tests or poc tests only (#124) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ✨ Init Template * Init template updates * 🐛 Build command to execute only if trdelnik is initialized * 🐛 fixes --- crates/cli/src/command.rs | 2 +- crates/cli/src/command/build.rs | 29 +- crates/cli/src/command/fuzz.rs | 39 +- crates/cli/src/command/init.rs | 38 +- crates/cli/src/command/test.rs | 20 +- crates/cli/src/lib.rs | 47 ++- crates/client/src/commander.rs | 127 ++----- .../client/src/fuzzer/snapshot_generator.rs | 2 +- crates/client/src/idl.rs | 2 +- .../trdelnik-tests/Cargo_fuzz.toml.tmpl | 3 - crates/client/src/test_generator.rs | 336 +++++++++++++----- crates/client/tests/test_fuzz.rs | 2 +- 12 files changed, 394 insertions(+), 253 deletions(-) diff --git a/crates/cli/src/command.rs b/crates/cli/src/command.rs index 5a6f6729..f21c1392 100644 --- a/crates/cli/src/command.rs +++ b/crates/cli/src/command.rs @@ -17,7 +17,7 @@ mod explorer; pub use explorer::{explorer, ExplorerCommand}; mod init; -pub use init::init; +pub use init::{init, TestsType}; mod clean; pub use clean::clean; diff --git a/crates/cli/src/command/build.rs b/crates/cli/src/command/build.rs index be90f134..1ef46878 100644 --- a/crates/cli/src/command/build.rs +++ b/crates/cli/src/command/build.rs @@ -1,12 +1,27 @@ -use anyhow::Error; +use anyhow::{bail, Error}; use fehler::throws; use trdelnik_client::*; +use crate::_discover; + +use super::fuzz::TRDELNIK_TOML; + #[throws] -pub async fn build(root: String) { - let commander = Commander::with_root(root); - commander.create_program_client_crate().await?; - commander.build_programs().await?; - commander.generate_program_client_deps().await?; - commander.generate_program_client_lib_rs(None).await?; +pub async fn build(root: Option) { + // if the root is present from the command line we will use it + // if the root is not present we will look for the Cargo.toml file + // Trdelnik does not have to be already defined to actually create/build + // program client + let root = match root { + Some(r) => r, + _ => { + if let Some(r) = _discover(TRDELNIK_TOML)? { + r + } else { + bail!("It does not seem that Trdelnik is initialized because the Trdelnik.toml file was not found in any parent directory!"); + } + } + }; + let mut generator: TestGenerator = TestGenerator::new_with_root(root); + generator.build().await?; } diff --git a/crates/cli/src/command/fuzz.rs b/crates/cli/src/command/fuzz.rs index 794ffbe4..ea357b8f 100644 --- a/crates/cli/src/command/fuzz.rs +++ b/crates/cli/src/command/fuzz.rs @@ -1,9 +1,13 @@ -use anyhow::{bail, Context, Error, Result}; +use anyhow::{bail, Error}; use clap::Subcommand; use fehler::throws; use trdelnik_client::{Commander, TestGenerator}; +use crate::_discover; + +pub const TRDELNIK_TOML: &str = "Trdelnik.toml"; + #[derive(Subcommand)] #[allow(non_camel_case_types)] pub enum FuzzCommand { @@ -31,7 +35,7 @@ pub async fn fuzz(root: Option, subcmd: FuzzCommand) { let root = match root { Some(r) => r, _ => { - let root = _discover()?; + let root = _discover(TRDELNIK_TOML)?; if let Some(r) = root { r } else { @@ -61,35 +65,10 @@ pub async fn fuzz(root: Option, subcmd: FuzzCommand) { } FuzzCommand::Add => { - let generator = TestGenerator::new(); + // 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?; } }; } - -// Climbs each parent directory until we find Trdelnik.toml. -fn _discover() -> Result> { - let _cwd = std::env::current_dir()?; - let mut cwd_opt = Some(_cwd.as_path()); - - while let Some(cwd) = cwd_opt { - for f in std::fs::read_dir(cwd) - .with_context(|| format!("Error reading the directory with path: {}", cwd.display()))? - { - let p = f - .with_context(|| { - format!("Error reading the directory with path: {}", cwd.display()) - })? - .path(); - if let Some(filename) = p.file_name() { - if filename.to_str() == Some("Trdelnik.toml") { - return Ok(Some(cwd.to_string_lossy().to_string())); - } - } - } - - cwd_opt = cwd.parent(); - } - - Ok(None) -} diff --git a/crates/cli/src/command/init.rs b/crates/cli/src/command/init.rs index 2f11da76..5da8e60e 100644 --- a/crates/cli/src/command/init.rs +++ b/crates/cli/src/command/init.rs @@ -1,9 +1,39 @@ -use anyhow::Error; +use anyhow::{bail, Error}; +use clap::ValueEnum; use fehler::throws; use trdelnik_client::TestGenerator; +use crate::_discover; + +pub const ANCHOR_TOML: &str = "Anchor.toml"; + +#[derive(ValueEnum, Clone)] +pub enum TestsType { + Both, + Fuzz, + Poc, +} + #[throws] -pub async fn init(skip_fuzzer: bool) { - let generator = TestGenerator::new(); - generator.generate(skip_fuzzer).await?; +pub async fn init(tests_type: TestsType) { + // look for Anchor.toml + let root = if let Some(r) = _discover(ANCHOR_TOML)? { + r + } else { + bail!("It does not seem that Anchor is initialized because the Anchor.toml file was not found in any parent directory!"); + }; + + let mut generator: TestGenerator = TestGenerator::new_with_root(root); + + match tests_type { + TestsType::Poc => { + generator.generate_poc().await?; + } + TestsType::Both => { + generator.generate_both().await?; + } + TestsType::Fuzz => { + generator.generate_fuzz().await?; + } + }; } diff --git a/crates/cli/src/command/test.rs b/crates/cli/src/command/test.rs index b4eb3030..4a16b1cf 100644 --- a/crates/cli/src/command/test.rs +++ b/crates/cli/src/command/test.rs @@ -1,9 +1,25 @@ -use anyhow::Error; +use anyhow::{bail, Error}; use fehler::throws; use trdelnik_client::*; +use crate::_discover; + +use super::fuzz::TRDELNIK_TOML; + #[throws] -pub async fn test(root: String) { +pub async fn test(root: Option) { + // if the root is present from the command line we will use it + // if the root is not present we will look for the Trdelnik.toml file + let root = match root { + Some(r) => r, + _ => { + if let Some(r) = _discover(TRDELNIK_TOML)? { + r + } else { + bail!("It does not seem that Trdelnik is initialized because the Cargo.toml file was not found in any parent directory!"); + } + } + }; let commander = Commander::with_root(root); commander.run_tests().await?; } diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index d07fee67..5aef3fac 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -1,5 +1,7 @@ use anyhow::Error; +use anyhow::{Context, Result}; use clap::{Parser, Subcommand}; +use command::TestsType; use fehler::throws; // subcommand functions to call and nested subcommands @@ -19,11 +21,11 @@ struct Cli { #[derive(Subcommand)] enum Command { - /// Create a `program_client` crate + /// Create or update a `program_client` crate Build { /// Anchor project root - #[clap(short, long, default_value = "./")] - root: String, + #[clap(short, long)] + root: Option, }, /// Get information about a keypair KeyPair { @@ -33,8 +35,8 @@ enum Command { /// Run program Integration tests Test { /// Anchor project root - #[clap(short, long, default_value = "./")] - root: String, + #[clap(short, long)] + root: Option, }, /// Run and debug Fuzz tests Fuzz { @@ -53,9 +55,9 @@ enum Command { }, /// Initialize test environment Init { - /// Flag to skip generating template for fuzzing and activating the fuzzing feature. - #[arg(short, long)] - skip_fuzzer: bool, + /// Specifies the types of tests for which the frameworks should be initialized. + #[clap(default_value = "both")] + tests_type: TestsType, }, /// Removes target contents except for KeyPair and removes hfuzz_target folder Clean, @@ -72,7 +74,34 @@ pub async fn start() { Command::Fuzz { root, subcmd } => command::fuzz(root, subcmd).await?, Command::Localnet => command::localnet().await?, Command::Explorer { subcmd } => command::explorer(subcmd).await?, - Command::Init { skip_fuzzer } => command::init(skip_fuzzer).await?, + Command::Init { tests_type } => command::init(tests_type).await?, Command::Clean => command::clean().await?, } } + +// Climbs each parent directory until we find target. +fn _discover(target: &str) -> Result> { + let _cwd = std::env::current_dir()?; + let mut cwd_opt = Some(_cwd.as_path()); + + while let Some(cwd) = cwd_opt { + for f in std::fs::read_dir(cwd) + .with_context(|| format!("Error reading the directory with path: {}", cwd.display()))? + { + let p = f + .with_context(|| { + format!("Error reading the directory with path: {}", cwd.display()) + })? + .path(); + if let Some(filename) = p.file_name() { + if filename.to_str() == Some(target) { + return Ok(Some(cwd.to_string_lossy().to_string())); + } + } + } + + cwd_opt = cwd.parent(); + } + + Ok(None) +} diff --git a/crates/client/src/commander.rs b/crates/client/src/commander.rs index e363c2c8..a74fdc3e 100644 --- a/crates/client/src/commander.rs +++ b/crates/client/src/commander.rs @@ -1,13 +1,9 @@ use crate::config::Config; -use crate::fuzzer; -use crate::snapshot_generator::generate_snapshots_code; -use crate::test_generator::ACCOUNTS_SNAPSHOTS_FILE_NAME; use crate::{ idl::{self, Idl}, - program_client_generator, - test_generator::FUZZ_INSTRUCTIONS_FILE_NAME, Client, }; +use cargo_metadata::camino::Utf8PathBuf; use cargo_metadata::{MetadataCommand, Package}; use fehler::{throw, throws}; use futures::future::try_join_all; @@ -15,10 +11,7 @@ use log::debug; use solana_sdk::signer::keypair::Keypair; use std::path::PathBuf; use std::process; -use std::{ - borrow::Cow, io, iter, os::unix::process::CommandExt, path::Path, process::Stdio, - string::FromUtf8Error, -}; +use std::{borrow::Cow, io, os::unix::process::CommandExt, process::Stdio, string::FromUtf8Error}; use thiserror::Error; use tokio::{ fs, @@ -26,8 +19,7 @@ use tokio::{ process::{Child, Command}, signal, }; - -pub const PROGRAM_CLIENT_DIRECTORY: &str = ".program_client"; +use toml::Value; #[derive(Error, Debug)] pub enum Error { @@ -291,39 +283,6 @@ impl Commander { eprintln!("cannot execute \"cargo hfuzz run-debug\" command"); } - /// Creates the `program_client` crate. - /// - /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. - #[throws] - pub async fn create_program_client_crate(&self) { - let crate_path = Path::new(self.root.as_ref()).join(PROGRAM_CLIENT_DIRECTORY); - if fs::metadata(&crate_path).await.is_ok() { - return; - } - - // @TODO Would it be better to: - // zip the template folder -> embed the archive to the binary -> unzip to a given location? - - fs::create_dir(&crate_path).await?; - - let cargo_toml_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/program_client/Cargo.toml.tmpl" - )); - fs::write(crate_path.join("Cargo.toml"), &cargo_toml_content).await?; - - let src_path = crate_path.join("src"); - fs::create_dir(&src_path).await?; - - let lib_rs_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/program_client/lib.rs" - )); - fs::write(src_path.join("lib.rs"), &lib_rs_content).await?; - - debug!("program_client crate created") - } - /// 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() @@ -344,8 +303,8 @@ impl Commander { /// /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. #[throws] - pub async fn generate_program_client_deps(&self) { - let trdelnik_dep = r#"trdelnik-client = "0.5.0""#.parse().unwrap(); + 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. @@ -360,48 +319,31 @@ impl Commander { 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() - }); - - let cargo_toml_path = Path::new(self.root.as_ref()) - .join(PROGRAM_CLIENT_DIRECTORY) - .join("Cargo.toml"); - - let mut cargo_toml_content: toml::Value = - fs::read_to_string(&cargo_toml_path).await?.parse()?; - - let cargo_toml_deps = cargo_toml_content - .get_mut("dependencies") - .and_then(toml::Value::as_table_mut) - .ok_or(Error::ParsingCargoTomlDependenciesFailed)?; - - for dep in iter::once(trdelnik_dep).chain(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); - } - } + 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 // @TODO remove renamed or deleted programs from deps? - - fs::write(cargo_toml_path, cargo_toml_content.to_string()).await?; } /// Updates the `program_client` `lib.rs`. /// /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. #[throws] - pub async fn generate_program_client_lib_rs(&self, new_fuzz_test_dir: Option) { + 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") @@ -437,30 +379,7 @@ impl Commander { let idl = Idl { programs: program_idls, }; - let use_tokens = self.parse_program_client_imports().await?; - let program_client = program_client_generator::generate_source_code(&idl, &use_tokens); - let program_client = Self::format_program_code(&program_client).await?; - - // TODO do not overwrite files if they already exist to keep user changes - let rust_file_path = Path::new(self.root.as_ref()) - .join(PROGRAM_CLIENT_DIRECTORY) - .join("src/lib.rs"); - fs::write(rust_file_path, &program_client).await?; - - if let Some(fuzz_test_dir) = new_fuzz_test_dir { - let program_fuzzer = fuzzer::fuzzer_generator::generate_source_code(&idl); - let program_fuzzer = Self::format_program_code(&program_fuzzer).await?; - - let fuzzer_snapshots = - generate_snapshots_code(codes_libs_pairs).map_err(Error::ReadProgramCodeFailed)?; - let fuzzer_snapshots = Self::format_program_code(&fuzzer_snapshots).await?; - - let rust_file_path = fuzz_test_dir.join(FUZZ_INSTRUCTIONS_FILE_NAME); - fs::write(rust_file_path, &program_fuzzer).await?; - - let rust_file_path = fuzz_test_dir.join(ACCOUNTS_SNAPSHOTS_FILE_NAME); - fs::write(rust_file_path, &fuzzer_snapshots).await?; - } + (idl, codes_libs_pairs) } /// Formats program code. diff --git a/crates/client/src/fuzzer/snapshot_generator.rs b/crates/client/src/fuzzer/snapshot_generator.rs index 4a0493fa..868a7045 100644 --- a/crates/client/src/fuzzer/snapshot_generator.rs +++ b/crates/client/src/fuzzer/snapshot_generator.rs @@ -14,7 +14,7 @@ use syn::{parse_quote, Attribute, Fields, GenericArgument, Item, ItemStruct, Pat use anchor_lang::anchor_syn::parser::accounts::parse_account_field; -pub fn generate_snapshots_code(code_path: Vec<(String, Utf8PathBuf)>) -> Result { +pub fn generate_snapshots_code(code_path: &[(String, Utf8PathBuf)]) -> Result { let code = code_path.iter().map(|(code, path)| { let mut mod_program = None::; let mut file = File::open(path).map_err(|e| e.to_string())?; diff --git a/crates/client/src/idl.rs b/crates/client/src/idl.rs index f2831dcf..224942e4 100644 --- a/crates/client/src/idl.rs +++ b/crates/client/src/idl.rs @@ -183,7 +183,7 @@ impl<'ast> syn::visit::Visit<'ast> for FullPathFinder { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct Idl { pub programs: Vec, } diff --git a/crates/client/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl b/crates/client/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl index cbe01b23..5a7fe236 100644 --- a/crates/client/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl +++ b/crates/client/src/templates/trdelnik-tests/Cargo_fuzz.toml.tmpl @@ -15,6 +15,3 @@ assert_matches = "1.4.0" [dependencies.trdelnik-client] version = "0.5.0" features=["fuzzing"] - -[dependencies.program_client] -path = "../../.program_client" diff --git a/crates/client/src/test_generator.rs b/crates/client/src/test_generator.rs index c3513612..9770f7f3 100644 --- a/crates/client/src/test_generator.rs +++ b/crates/client/src/test_generator.rs @@ -1,12 +1,18 @@ use crate::{ commander::{Commander, Error as CommanderError}, - config::{Config, CARGO_TOML, TRDELNIK_TOML}, + config::{CARGO_TOML, TRDELNIK_TOML}, + fuzzer, + idl::Idl, + program_client_generator, + snapshot_generator::generate_snapshots_code, }; +use cargo_metadata::camino::Utf8PathBuf; use fehler::{throw, throws}; +use log::debug; use std::{ env, fs::OpenOptions, - io, + io, iter, path::{Path, PathBuf}, }; use std::{fs::File, io::prelude::*}; @@ -20,11 +26,12 @@ 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 const FUZZ_TEST_DIRECTORY: &str = "fuzz_tests"; -pub const FUZZ_TEST: &str = "test_fuzz.rs"; -pub const POC_TEST_DIRECTORY: &str = "poc_tests"; -pub const TESTS: &str = "tests"; -pub const FUZZING: &str = "fuzzing"; +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"; #[derive(Error, Debug)] pub enum Error { @@ -40,9 +47,18 @@ pub enum Error { BadWorkspace, #[error("The Anchor project does not contain any programs")] NoProgramsFound, + #[error("read program code failed: '{0}'")] + ReadProgramCodeFailed(String), + #[error("parsing Cargo.toml dependencies failed")] + ParsingCargoTomlDependenciesFailed, } -pub struct TestGenerator; +pub struct TestGenerator { + pub root: PathBuf, + pub idl: Idl, + pub codes_libs_pairs: Vec<(String, Utf8PathBuf)>, + pub program_deps: Vec, +} impl Default for TestGenerator { fn default() -> Self { Self::new() @@ -50,7 +66,20 @@ impl Default for TestGenerator { } impl TestGenerator { pub fn new() -> Self { - Self + Self { + root: Path::new("../../").to_path_buf(), + idl: Idl::default(), + codes_libs_pairs: Vec::default(), + program_deps: Vec::default(), + } + } + 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(), + } } /// Builds all the programs and creates `.program_client` directory. Initializes the @@ -73,7 +102,7 @@ impl TestGenerator { /// /// Then you can easily use it in tests: /// - /// ```ignore + /// ```rust,ignore /// use my_program; /// /// // ... @@ -91,60 +120,199 @@ impl TestGenerator { /// It fails when: /// - there is not a root directory (no `Anchor.toml` file) #[throws] - pub async fn generate(&self, _skip_fuzzer: bool) { - let root = match Config::discover_root() { - Ok(root) => root, - Err(_) => throw!(Error::BadWorkspace), - }; - let root_path = root.to_str().unwrap().to_string(); + pub async fn generate_both(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); let commander = Commander::with_root(root_path); - commander.create_program_client_crate().await?; - self.generate_test_files(&root).await?; - self.update_workspace(&root, "trdelnik-tests/poc_tests") - .await?; - let new_fuzz_test_dir = self.generate_fuzz_test_files(&root).await?; - self.build_program_client(&commander, new_fuzz_test_dir) - .await?; - self.update_gitignore( - &root, - &format!("{TESTS_WORKSPACE}/{FUZZ_TEST_DIRECTORY}/{FUZZING}/{HFUZZ_TARGET}"), - )?; + + // 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}" + ))?; } #[throws] - pub async fn add_new_fuzz_test(&self) { - let root = match Config::discover_root() { - Ok(root) => root, - Err(_) => throw!(Error::BadWorkspace), - }; - let new_fuzz_test_dir = self.generate_fuzz_test_files(&root).await?; + 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?; - let root_path = root.to_str().unwrap().to_string(); + self.generate_trdelnik_toml().await?; + + // update gitignore to exclude hfuzz target + self.update_gitignore(&format!( + "{TESTS_WORKSPACE}/{FUZZ_TEST_DIRECTORY}/{FUZZING}/{HFUZZ_TARGET}" + ))?; + } + #[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); - self.build_program_client(&commander, new_fuzz_test_dir) - .await?; - self.update_gitignore( - &root, - &format!("{TESTS_WORKSPACE}/{FUZZ_TEST_DIRECTORY}/{FUZZING}/{HFUZZ_TARGET}"), - )?; + // 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?; } + #[throws] + pub async fn build(&mut self) { + let root_path = self.root.to_str().unwrap().to_string(); - /// Builds and generates programs for `program_client` module + 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?; + } #[throws] - async fn build_program_client(&self, commander: &Commander, new_fuzz_test_dir: PathBuf) { + 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?; - commander.generate_program_client_deps().await?; - commander - .generate_program_client_lib_rs(Some(new_fuzz_test_dir)) + + // 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}" + ))?; + } + #[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?; } + /// Creates the `program_client` crate. + /// + /// It's used internally by the [`#[trdelnik_test]`](trdelnik_test::trdelnik_test) macro. + #[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?; + + let mut cargo_toml_content: toml::Value = + fs::read_to_string(&cargo_toml_path).await?.parse()?; + + let trdelnik_dep = r#"trdelnik-client = "0.5.0""#.parse().unwrap(); + + let cargo_toml_deps = cargo_toml_content + .get_mut("dependencies") + .and_then(toml::Value::as_table_mut) + .ok_or(Error::ParsingCargoTomlDependenciesFailed)?; + + 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 src_path = crate_path.join("src"); + self.create_directory(&src_path, "src").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?; + + debug!("program_client crate created") + } + /// 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` #[throws] - async fn generate_test_files(&self, root: &Path) { - let workspace_path = root + async fn generate_test_files(&self) { + let workspace_path = self + .root .join(TESTS_WORKSPACE) .join(POC_TEST_DIRECTORY) .join(TESTS); @@ -158,7 +326,7 @@ impl TestGenerator { "/src/templates/trdelnik-tests/test.rs" )); - let program_libs = self.get_program_lib_names(root).await?; + let program_libs = self.get_program_lib_names().await?; let program_name = if let Some(name) = program_libs.first() { name @@ -173,7 +341,8 @@ impl TestGenerator { self.create_file(&test_path, TESTS_FILE_NAME, &template) .await?; - let cargo_toml_path = root + let cargo_toml_path = self + .root .join(TESTS_WORKSPACE) .join(POC_TEST_DIRECTORY) .join(CARGO_TOML); @@ -186,28 +355,20 @@ impl TestGenerator { self.create_file(&cargo_toml_path, CARGO_TOML, cargo_toml_content) .await?; - let cargo_toml_dir = root.join(TESTS_WORKSPACE).join(POC_TEST_DIRECTORY); - self.add_program_deps(root, &cargo_toml_dir).await?; - - let trdelnik_toml_path = root.join(TRDELNIK_TOML); - let trdelnik_toml_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/Trdelnik.toml.tmpl" - )); - self.create_file(&trdelnik_toml_path, TRDELNIK_TOML, trdelnik_toml_content) - .await?; + let cargo_toml_dir = self.root.join(TESTS_WORKSPACE).join(POC_TEST_DIRECTORY); + self.add_program_deps(&cargo_toml_dir).await?; } /// Creates the `trdelnik-tests` workspace with `src/bin` directory and empty `fuzz_target.rs` file #[throws] - async fn generate_fuzz_test_files(&self, root: &Path) -> PathBuf { - let fuzz_dir_path = root.join(TESTS_WORKSPACE).join(FUZZ_TEST_DIRECTORY); + 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?; - let libs = self.get_program_lib_names(root).await?; + let libs = self.get_program_lib_names().await?; let fuzz_id = if fuzz_dir_path.read_dir()?.next().is_none() { 0 @@ -257,7 +418,7 @@ impl TestGenerator { // 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 program_client::{}_instruction::*;\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 @@ -274,27 +435,26 @@ impl TestGenerator { // create fuzz instructions file let fuzz_instructions_path = new_fuzz_test_dir.join(FUZZ_INSTRUCTIONS_FILE_NAME); - let fuzz_instructions_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/trdelnik-tests/fuzz_instructions.rs" - )); + 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, - fuzz_instructions_content, + &program_fuzzer, ) .await?; // // create accounts_snapshots file let accounts_snapshots_path = new_fuzz_test_dir.join(ACCOUNTS_SNAPSHOTS_FILE_NAME); - let accounts_snapshots_content = include_str!(concat!( - env!("CARGO_MANIFEST_DIR"), - "/src/templates/trdelnik-tests/accounts_snapshots.rs" - )); + let fuzzer_snapshots = generate_snapshots_code(&self.codes_libs_pairs) + .map_err(Error::ReadProgramCodeFailed)?; + let fuzzer_snapshots = Commander::format_program_code(&fuzzer_snapshots).await?; + self.create_file( &accounts_snapshots_path, ACCOUNTS_SNAPSHOTS_FILE_NAME, - accounts_snapshots_content, + &fuzzer_snapshots, ) .await?; @@ -308,11 +468,9 @@ impl TestGenerator { self.add_bin_target(&fuzz_tests_manifest_path, &new_fuzz_test, &new_bin_target) .await?; - self.add_program_deps(root, &fuzz_dir_path).await?; + self.add_program_deps(&fuzz_dir_path).await?; - self.update_workspace(&root.to_path_buf(), "trdelnik-tests/fuzz_tests") - .await?; - new_fuzz_test_dir + self.update_workspace("trdelnik-tests/fuzz_tests").await?; } /// Creates a new file with a given content on the specified `path` and `name` @@ -369,8 +527,8 @@ impl TestGenerator { /// Adds `trdelnik-tests` workspace to the `root`'s `Cargo.toml` workspace members if needed. #[throws] - async fn update_workspace(&self, root: &PathBuf, new_member: &str) { - let cargo = Path::new(&root).join(CARGO_TOML); + 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 @@ -396,8 +554,8 @@ impl TestGenerator { /// Updates .gitignore file in the `root` directory and appends `ignored_path` to the end of the file #[throws] - fn update_gitignore(&self, root: &Path, ignored_path: &str) { - let file_path = root.join(".gitignore"); + fn update_gitignore(&self, ignored_path: &str) { + let file_path = self.root.join(".gitignore"); if file_path.exists() { let file = File::open(&file_path)?; for line in io::BufReader::new(file).lines().flatten() { @@ -448,11 +606,9 @@ impl TestGenerator { /// Adds programs to Cargo.toml as a dependencies to be able to be used in tests and fuzz targets #[throws] - async fn add_program_deps(&self, root: &Path, cargo_toml_dir: &Path) { + 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(root, &cargo_toml_dir.to_path_buf()) - .await?; + 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()?; @@ -473,8 +629,8 @@ impl TestGenerator { } /// Scans `programs` directory and returns a list of `toml::Value` programs and their paths. - async fn get_programs(&self, root: &Path, cargo_dir: &PathBuf) -> Result, Error> { - let programs = root.join("programs"); + 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()); @@ -496,8 +652,8 @@ impl TestGenerator { } /// Scans `programs` directory and returns a list of names of libraries - async fn get_program_lib_names(&self, root: &Path) -> Result, Error> { - let programs = root.join("programs"); + 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()); @@ -526,7 +682,7 @@ impl TestGenerator { /// Gets the program name from `/Cargo.toml` and returns a `toml::Value` program dependency. #[throws] - async fn get_program_dep<'a>(&self, dir: &Path, cargo_dir: &PathBuf) -> Value { + 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(); diff --git a/crates/client/tests/test_fuzz.rs b/crates/client/tests/test_fuzz.rs index eaad3d69..ae929bee 100644 --- a/crates/client/tests/test_fuzz.rs +++ b/crates/client/tests/test_fuzz.rs @@ -59,7 +59,7 @@ async fn test_account_snapshots() { let codes_libs_pairs = vec![(expanded_fuzz_example3.to_string(), path)]; let fuzzer_snapshots = - trdelnik_client::snapshot_generator::generate_snapshots_code(codes_libs_pairs).unwrap(); + trdelnik_client::snapshot_generator::generate_snapshots_code(&codes_libs_pairs).unwrap(); let fuzzer_snapshots = trdelnik_client::Commander::format_program_code(&fuzzer_snapshots).await?;