Skip to content

Commit 2ef2083

Browse files
committed
cargo-rbmt: add fuzz subcommand
Initially only supporting the dynamic listing of targets.
1 parent 32c0c8d commit 2ef2083

File tree

2 files changed

+139
-2
lines changed

2 files changed

+139
-2
lines changed

cargo-rbmt/src/fuzz.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//! Fuzz test tasks for workspaces with honggfuzz fuzz targets.
2+
3+
use crate::{
4+
environment::{quiet_println, CONFIG_FILE_PATH},
5+
quiet_cmd,
6+
};
7+
use serde::Deserialize;
8+
use std::path::Path;
9+
use xshell::Shell;
10+
11+
/// Default package name for fuzz targets.
12+
const FUZZ_PACKAGE: &str = "bitcoin-fuzz";
13+
14+
/// Fuzz configuration loaded from rbmt.toml.
15+
#[derive(Debug, Deserialize, Default)]
16+
#[serde(default)]
17+
struct Config {
18+
fuzz: FuzzConfig,
19+
}
20+
21+
/// Fuzz-specific configuration.
22+
#[derive(Debug, Deserialize, Default)]
23+
#[serde(default)]
24+
struct FuzzConfig {
25+
/// Package name containing fuzz targets (defaults to [`FUZZ_PACKAGE`]).
26+
package: Option<String>,
27+
}
28+
29+
impl FuzzConfig {
30+
/// Load fuzz configuration from workspace root.
31+
fn load(workspace_root: &Path) -> Result<Self, Box<dyn std::error::Error>> {
32+
let config_path = workspace_root.join(CONFIG_FILE_PATH);
33+
34+
if !config_path.exists() {
35+
return Ok(FuzzConfig::default());
36+
}
37+
38+
let contents = std::fs::read_to_string(&config_path)?;
39+
let config: Config = toml::from_str(&contents)?;
40+
Ok(config.fuzz)
41+
}
42+
43+
/// Get the package name (defaults to [`FUZZ_PACKAGE`]).
44+
fn package_name(&self) -> &str {
45+
self.package.as_deref().unwrap_or(FUZZ_PACKAGE)
46+
}
47+
}
48+
49+
/// Discover all fuzz targets using cargo metadata.
50+
///
51+
/// Targets are discovered by querying cargo metadata for all binary targets
52+
/// in the specified fuzz package.
53+
fn discover_fuzz_targets(
54+
sh: &Shell,
55+
package_name: &str,
56+
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
57+
let metadata = quiet_cmd!(sh, "cargo metadata --format-version 1 --no-deps").read()?;
58+
let json: serde_json::Value = serde_json::from_str(&metadata)?;
59+
60+
let mut targets = Vec::new();
61+
62+
// Find binary targets in the specified fuzz package.
63+
if let Some(packages) = json["packages"].as_array() {
64+
for package in packages {
65+
if package["name"].as_str() == Some(package_name) {
66+
if let Some(package_targets) = package["targets"].as_array() {
67+
for target in package_targets {
68+
// Filter for binary targets only.
69+
let Some(kinds) = target["kind"].as_array() else {
70+
continue;
71+
};
72+
let Some(name) = target["name"].as_str() else {
73+
continue;
74+
};
75+
76+
if kinds.iter().any(|k| k.as_str() == Some("bin")) {
77+
targets.push(name.to_string());
78+
}
79+
}
80+
}
81+
break; // Found the package, no need to continue.
82+
}
83+
}
84+
}
85+
86+
// Sort for consistent output.
87+
targets.sort();
88+
89+
Ok(targets)
90+
}
91+
92+
/// List discovered fuzz targets.
93+
pub fn list(sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
94+
let workspace_root = sh.current_dir();
95+
let config = FuzzConfig::load(&workspace_root)?;
96+
let package_name = config.package_name();
97+
98+
let targets = discover_fuzz_targets(sh, package_name)?;
99+
100+
if targets.is_empty() {
101+
quiet_println("No fuzz targets found");
102+
} else {
103+
for target in targets {
104+
println!("{}", target);
105+
}
106+
}
107+
108+
Ok(())
109+
}
110+
111+
/// Run fuzz tests for the workspace.
112+
pub fn run(_sh: &Shell) -> Result<(), Box<dyn std::error::Error>> {
113+
quiet_println("Fuzz execution not yet implemented");
114+
115+
Ok(())
116+
}

cargo-rbmt/src/main.rs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ mod api;
22
mod bench;
33
mod docs;
44
mod environment;
5+
mod fuzz;
56
mod integration;
67
mod lint;
78
mod lock;
@@ -60,6 +61,12 @@ enum Commands {
6061
},
6162
/// Run bitcoin core integration tests.
6263
Integration,
64+
/// Run fuzz tests.
65+
Fuzz {
66+
/// List available fuzz targets instead of running them.
67+
#[arg(long)]
68+
list: bool,
69+
},
6370
/// Update Cargo-minimal.lock and Cargo-recent.lock files.
6471
Lock,
6572
/// Run pre-release readiness checks.
@@ -80,8 +87,11 @@ fn main() {
8087
configure_log_level(&sh);
8188
change_to_repo_root(&sh);
8289

83-
// Restore the specified lock file before running any command (except Lock and Integration).
84-
if !matches!(cli.command, Commands::Lock | Commands::Integration) {
90+
// Restore the specified lock file before running any command (except Lock, Integration, and Fuzz).
91+
if !matches!(
92+
cli.command,
93+
Commands::Lock | Commands::Integration | Commands::Fuzz { .. }
94+
) {
8595
if let Err(e) = cli.lock_file.restore(&sh) {
8696
eprintln!("Error restoring lock file: {}", e);
8797
process::exit(1);
@@ -134,6 +144,17 @@ fn main() {
134144
process::exit(1);
135145
}
136146
}
147+
Commands::Fuzz { list } => {
148+
if list {
149+
if let Err(e) = fuzz::list(&sh) {
150+
eprintln!("Error listing fuzz targets: {}", e);
151+
process::exit(1);
152+
}
153+
} else if let Err(e) = fuzz::run(&sh) {
154+
eprintln!("Error running fuzz tests: {}", e);
155+
process::exit(1);
156+
}
157+
}
137158
Commands::Lock => {
138159
if let Err(e) = lock::run(&sh) {
139160
eprintln!("Error updating lock files: {}", e);

0 commit comments

Comments
 (0)