diff --git a/cargo-rbmt/src/environment.rs b/cargo-rbmt/src/environment.rs index 2a6de68..aaf57ee 100644 --- a/cargo-rbmt/src/environment.rs +++ b/cargo-rbmt/src/environment.rs @@ -105,10 +105,8 @@ pub fn get_packages( } if !invalid_packages.is_empty() { - let mut error_msg = format!( - "Package not found in workspace: {}", - invalid_packages.join(", ") - ); + let mut error_msg = + format!("Package not found in workspace: {}", invalid_packages.join(", ")); error_msg.push_str("\n\nAvailable packages:"); for name in &available_names { @@ -119,10 +117,8 @@ pub fn get_packages( } // Filter to only requested packages. - let package_info: Vec<(String, PathBuf)> = all_packages - .into_iter() - .filter(|(name, _)| packages.iter().any(|p| p == name)) - .collect(); + let package_info: Vec<(String, PathBuf)> = + all_packages.into_iter().filter(|(name, _)| packages.iter().any(|p| p == name)).collect(); Ok(package_info) } diff --git a/cargo-rbmt/src/fuzz.rs b/cargo-rbmt/src/fuzz.rs new file mode 100644 index 0000000..d4d61ef --- /dev/null +++ b/cargo-rbmt/src/fuzz.rs @@ -0,0 +1,110 @@ +//! Fuzz test tasks for workspaces with honggfuzz fuzz targets. + +use std::path::Path; + +use serde::Deserialize; +use xshell::Shell; + +use crate::environment::{quiet_println, CONFIG_FILE_PATH}; +use crate::quiet_cmd; + +/// Default package name for fuzz targets. +const FUZZ_PACKAGE: &str = "fuzz"; + +/// Fuzz configuration loaded from rbmt.toml. +#[derive(Debug, Deserialize, Default)] +#[serde(default)] +struct Config { + fuzz: FuzzConfig, +} + +/// Fuzz-specific configuration. +#[derive(Debug, Deserialize, Default)] +#[serde(default)] +struct FuzzConfig { + /// Package name containing fuzz targets (defaults to [`FUZZ_PACKAGE`]). + package: Option, +} + +impl FuzzConfig { + /// Load fuzz configuration from workspace root. + fn load(workspace_root: &Path) -> Result> { + let config_path = workspace_root.join(CONFIG_FILE_PATH); + + if !config_path.exists() { + return Ok(Self::default()); + } + + let contents = std::fs::read_to_string(&config_path)?; + let config: Config = toml::from_str(&contents)?; + Ok(config.fuzz) + } + + /// Get the package name (defaults to [`FUZZ_PACKAGE`]). + fn package_name(&self) -> &str { self.package.as_deref().unwrap_or(FUZZ_PACKAGE) } +} + +/// Discover all fuzz targets using cargo metadata. +/// +/// Targets are discovered by querying cargo metadata for all binary targets +/// in the specified fuzz package. +fn discover_fuzz_targets( + sh: &Shell, + package_name: &str, +) -> Result, Box> { + let metadata = quiet_cmd!(sh, "cargo metadata --format-version 1 --no-deps").read()?; + let json: serde_json::Value = serde_json::from_str(&metadata)?; + + let mut targets = Vec::new(); + + // Find binary targets in the specified fuzz package. + if let Some(packages) = json["packages"].as_array() { + for package in packages { + if package["name"].as_str() == Some(package_name) { + if let Some(package_targets) = package["targets"].as_array() { + for target in package_targets { + // Filter for binary targets only. + let Some(kinds) = target["kind"].as_array() else { + continue; + }; + let Some(name) = target["name"].as_str() else { + continue; + }; + + if kinds.iter().any(|k| k.as_str() == Some("bin")) { + targets.push(name.to_string()); + } + } + } + break; // Found the package, no need to continue. + } + } + } + + // Sort for consistent output. + targets.sort(); + + Ok(targets) +} + +/// List discovered fuzz targets. +pub fn list(sh: &Shell) -> Result<(), Box> { + let workspace_root = sh.current_dir(); + let config = FuzzConfig::load(&workspace_root)?; + let package_name = config.package_name(); + + let targets = discover_fuzz_targets(sh, package_name)?; + + if targets.is_empty() { + quiet_println("No fuzz targets found"); + } else { + for target in targets { + println!("{}", target); + } + } + + Ok(()) +} + +/// Run fuzz tests for the workspace. +pub fn run(_sh: &Shell) { quiet_println("Fuzz execution not yet implemented"); } diff --git a/cargo-rbmt/src/main.rs b/cargo-rbmt/src/main.rs index 0a6b218..ad379e5 100644 --- a/cargo-rbmt/src/main.rs +++ b/cargo-rbmt/src/main.rs @@ -2,6 +2,7 @@ mod api; mod bench; mod docs; mod environment; +mod fuzz; mod integration; mod lint; mod lock; @@ -60,6 +61,12 @@ enum Commands { }, /// Run bitcoin core integration tests. Integration, + /// Run fuzz tests. + Fuzz { + /// List available fuzz targets instead of running them. + #[arg(long)] + list: bool, + }, /// Update Cargo-minimal.lock and Cargo-recent.lock files. Lock, /// Run pre-release readiness checks. @@ -80,8 +87,8 @@ fn main() { configure_log_level(&sh); change_to_repo_root(&sh); - // Restore the specified lock file before running any command (except Lock and Integration). - if !matches!(cli.command, Commands::Lock | Commands::Integration) { + // Restore the specified lock file before running any command (except Lock, Integration, and Fuzz). + if !matches!(cli.command, Commands::Lock | Commands::Integration | Commands::Fuzz { .. }) { if let Err(e) = cli.lock_file.restore(&sh) { eprintln!("Error restoring lock file: {}", e); process::exit(1); @@ -115,17 +122,25 @@ fn main() { eprintln!("Error running bench tests: {}", e); process::exit(1); }, - Commands::Test { toolchain, no_debug_assertions } => { + Commands::Test { toolchain, no_debug_assertions } => if let Err(e) = test::run(&sh, toolchain, no_debug_assertions, &cli.packages) { eprintln!("Error running tests: {}", e); process::exit(1); - } - } + }, Commands::Integration => if let Err(e) = integration::run(&sh, &cli.packages) { eprintln!("Error running integration tests: {}", e); process::exit(1); }, + Commands::Fuzz { list } => + if list { + if let Err(e) = fuzz::list(&sh) { + eprintln!("Error listing fuzz targets: {}", e); + process::exit(1); + } + } else { + fuzz::run(&sh); + }, Commands::Lock => if let Err(e) = lock::run(&sh) { eprintln!("Error updating lock files: {}", e); diff --git a/cargo-rbmt/src/test.rs b/cargo-rbmt/src/test.rs index 09b464c..b39a9e4 100644 --- a/cargo-rbmt/src/test.rs +++ b/cargo-rbmt/src/test.rs @@ -275,7 +275,8 @@ fn loop_features>( .chain(additional.iter().map(std::convert::AsRef::as_ref)) .collect::>() .join(" "), - None => additional.iter().map(std::convert::AsRef::as_ref).collect::>().join(" "), + None => + additional.iter().map(std::convert::AsRef::as_ref).collect::>().join(" "), } }