Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/cargo_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ jobs:
- name: Run Cargo workspace tests
env:
FUZZTEST_LIB_PATH: ${{ github.workspace }}/bazel-bin/centipede
CENTIPEDE_BINARY_PATH: ${{ github.workspace }}/bazel-bin/centipede/centipede
FUZZTEST_CENTIPEDE_BINARY_PATH: ${{ github.workspace }}/bazel-bin/centipede/centipede
run: |
cargo test --locked --workspace --no-fail-fast -- --test-threads=1

Expand Down
121 changes: 104 additions & 17 deletions rust/cargo_fuzztest/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

use anyhow::{Context, Result};
use clap::Parser;
pub use fuzztest_options::{ExecutionMode, FuzzTestOptions};
pub use fuzztest_options::{ExecutionMode, FuzzFor, FuzzOptions, FuzzTestOptions};
use std::env;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
Expand All @@ -35,17 +35,61 @@ pub struct CargoFuzzTestOptions {
/// List all fuzz tests in the crate without running them.
#[arg(long)]
pub list: bool,

/// Optional target test path to run (for example, `__fuzztest_mod__my_test::my_test`).
///
/// If omitted, all generated fuzz tests in the binary are run.
#[arg()]
pub test_path: Option<String>,

/// Optional path to the Centipede binary executable.
///
/// When specified via CLI `--centipede-binary-path <path>`, this is forwarded to
/// the compiled test executable via the `FUZZTEST_CENTIPEDE_BINARY_PATH` environment variable.
#[arg(env = "FUZZTEST_CENTIPEDE_BINARY_PATH", long)]
pub centipede_binary_path: Option<String>,
}

impl CargoFuzzTestOptions {
fn check_centipede_binary_path_is_set(&self) -> Result<()> {
anyhow::ensure!(
self.centipede_binary_path.is_some(),
"fuzzing mode requires `--centipede-binary-path` to be specified"
);
Ok(())
}

/// Returns the `ExecutionMode` derived from CLI options.
pub fn execution_mode(&self) -> ExecutionMode {
pub fn execution_mode(&self) -> Result<ExecutionMode> {
if self.list {
return ExecutionMode::ListFuzzTests;
return Ok(ExecutionMode::ListFuzzTests);
}

// TODO(the-shank): add support for other modes.
ExecutionMode::SmokeTest
let mode = ExecutionMode::from_fuzztest_options(&self.fuzztest_options);

let mode = match &mode {
ExecutionMode::Fuzz(_) => {
self.check_centipede_binary_path_is_set()?;
mode
}
ExecutionMode::SmokeTest => {
if self.test_path.is_some() {
self.check_centipede_binary_path_is_set()?;
return Ok(ExecutionMode::Fuzz(FuzzOptions {
fuzz_for: FuzzFor::Indefinitely,
// TODO(the-shank): support parallel jobs
jobs: None,
}));
} else {
mode
}
}
_ => {
anyhow::bail!("mode not yet supported");
}
};

Ok(mode)
}
}

Expand Down Expand Up @@ -166,15 +210,35 @@ impl FuzztestRunner {
}

/// Construct the direct binary invocation command.
pub fn build_run_command(&self, test_binary: &Path) -> Command {
pub fn build_run_command(&self, test_binary: &Path) -> Result<Command> {
let mut cmd = Command::new(test_binary);
cmd.arg("__fuzztest_mod__");

let mode = self.options.execution_mode();
if let Some(test_path) = &self.options.test_path {
cmd.arg(test_path);
cmd.arg("--exact");
} else {
cmd.arg("__fuzztest_mod__");
}

let mode = self.options.execution_mode()?;
match mode {
ExecutionMode::ListFuzzTests => {
cmd.arg("--list");
}

ExecutionMode::Fuzz(fuzz_options) => {
let FuzzOptions { fuzz_for, jobs: _ } = fuzz_options;
match fuzz_for {
FuzzFor::Indefinitely => {
cmd.env("FUZZTEST_FUZZ_FOR", "inf");
}
FuzzFor::Duration(duration) => {
cmd.env("FUZZTEST_FUZZ_FOR", duration.to_string());
}
}
// TODO(the-shank): support parallel jobs
}

ExecutionMode::SmokeTest => {
// nothing to be done
}
Expand All @@ -183,7 +247,14 @@ impl FuzztestRunner {
}
}

cmd
// If `--centipede-binary-path` was passed to `cargo-fuzztest`, forward it to the
// child test executable via `FUZZTEST_CENTIPEDE_BINARY_PATH` environment variable so the
// fuzzer runtime can locate and run Centipede during continuous fuzzing.
if let Some(centipede_binary_path) = &self.options.centipede_binary_path {
cmd.env("FUZZTEST_CENTIPEDE_BINARY_PATH", centipede_binary_path);
}

Ok(cmd)
}

/// Runs the tool in two steps:
Expand All @@ -209,8 +280,10 @@ impl FuzztestRunner {
.context("while attempting to parse compilation JSON output as UTF-8")?;
let test_binary = Self::parse_compiler_messages(&json_stdout)?;

// 2. execute command according to the selected execution mode
let mut run_cmd = self.build_run_command(&test_binary);
// 2. run the test binary
let mut run_cmd = self
.build_run_command(&test_binary)
.context("while attempting to construct test binary run command")?;
let status =
run_cmd.status().context("while attempting to execute compiled test binary")?;

Expand Down Expand Up @@ -249,7 +322,8 @@ mod tests {
fn test_build_run_command_default() {
let options = CargoFuzzTestOptions::default();
let runner = FuzztestRunner::new("sample-host-triple".to_string(), options);
let cmd = runner.build_run_command(Path::new("/tmp/test_bin"));
let cmd =
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");
let args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
assert_eq!(args, &["__fuzztest_mod__"]);
}
Expand All @@ -258,36 +332,49 @@ mod tests {
fn test_build_run_command_list() {
let options = CargoFuzzTestOptions { list: true, ..Default::default() };
let runner = FuzztestRunner::new("sample-host-triple".to_string(), options);
let cmd = runner.build_run_command(Path::new("/tmp/test_bin"));
let cmd =
runner.build_run_command(Path::new("/tmp/test_bin")).expect("should build run command");
let args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
assert_eq!(args, &["__fuzztest_mod__", "--list"]);
}

#[gtest]
fn test_execution_mode_smoke_test() {
let options = CargoFuzzTestOptions { list: false, ..Default::default() };
assert_eq!(options.execution_mode(), ExecutionMode::SmokeTest);
assert_eq!(
options.execution_mode().expect("valid execution mode"),
ExecutionMode::SmokeTest
);
}

#[gtest]
fn test_execution_mode_list_fuzz_tests() {
let options = CargoFuzzTestOptions { list: true, ..Default::default() };
assert_eq!(options.execution_mode(), ExecutionMode::ListFuzzTests);
assert_eq!(
options.execution_mode().expect("valid execution mode"),
ExecutionMode::ListFuzzTests
);
}

#[gtest]
fn test_cli_option_parsing_list_flag() {
let parsed = CargoFuzzTestOptions::try_parse_from(["cargo-fuzztest", "--list"])
.expect("--list argument should be valid CLI option");
assert!(parsed.list);
assert_eq!(parsed.execution_mode(), ExecutionMode::ListFuzzTests);
assert_eq!(
parsed.execution_mode().expect("valid execution mode"),
ExecutionMode::ListFuzzTests
);
}

#[gtest]
fn test_cli_option_parsing_default() {
let parsed = CargoFuzzTestOptions::try_parse_from(["cargo-fuzztest"])
.expect("empty CLI arguments should be valid");
assert!(!parsed.list);
assert_eq!(parsed.execution_mode(), ExecutionMode::SmokeTest);
assert_eq!(
parsed.execution_mode().expect("valid execution mode"),
ExecutionMode::SmokeTest
);
}
}
30 changes: 29 additions & 1 deletion rust/cargo_fuzztest/tests/e2e_cli_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ fn test_cargo_fuzztest_e2e_list() {
// Resolve the absolute path of the sample crate
let sample_crate_path = get_sample_crate_path("sample_fuzz_crate");

// Create a temporary directory for Cargo build outputs to avoid polluting workspace
let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory");

// Invoke: `cargo-fuzztest --list` inside the sample crate directory
let output = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path())
.arg("--list")
.output()
Expand All @@ -73,4 +73,32 @@ fn test_cargo_fuzztest_e2e_list() {
expect_true!(stdout_str.contains(
"__fuzztest_mod__another_sample_fuzztest_target::another_sample_fuzztest_target"
));
let stdout_str = String::from_utf8_lossy(&output.stdout);
expect_true!(stdout_str.contains("sample_fuzztest_target"));
expect_true!(stdout_str.contains("another_sample_fuzztest_target"));
}

#[gtest]
fn test_cargo_fuzztest_e2e_specific_target() {
// Resolve the absolute path of the sample crate
let sample_crate_path = get_sample_crate_path("sample_fuzz_crate");

let temp_target_dir = TempDir::new().expect("Failed to create temporary target directory");

let mut cmd = setup_cargo_fuzztest_command(&sample_crate_path, temp_target_dir.path());

let centipede_bin = env::var("FUZZTEST_CENTIPEDE_BINARY_PATH")
.expect("FUZZTEST_CENTIPEDE_BINARY_PATH needs to be set for the test");

cmd.arg("__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target")
.arg("--fuzz-for=2s")
.env_remove("FUZZTEST_CENTIPEDE_BINARY_PATH")
.arg("--centipede-binary-path")
.arg(centipede_bin);

let output = cmd.output().expect("Failed to run cargo-fuzztest command");

let stdout_str = String::from_utf8_lossy(&output.stdout);
expect_true!(stdout_str.contains("sample_fuzztest_target"));
expect_false!(stdout_str.contains("another_sample_fuzztest_target"));
}
97 changes: 95 additions & 2 deletions rust/cargo_fuzztest/tests/runner_test.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use cargo_fuzztest::{CargoFuzzTestOptions, FuzztestRunner};
use fuzztest_options::{FuzzFor, FuzzTestOptions};
use googletest::prelude::*;
use std::env;
use std::path::PathBuf;
Expand All @@ -12,13 +13,105 @@ fn test_runner_execution() {
let options = CargoFuzzTestOptions::default();
let runner = FuzztestRunner::new("sample-host-triple".to_string(), options);

let mut cmd = runner.build_run_command(&binary_path);
let mut cmd = runner
.build_run_command(&binary_path)
.expect("building run command in smoke test mode should succeed");

let status = cmd.status().expect("Failed to execute test binary command");

expect_true!(status.success());
}

#[gtest]
fn test_runner_build_run_command_with_target() {
let binary_path = get_sample_fuzz_test_bin_path();
let options = CargoFuzzTestOptions {
test_path: Some(
"__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target".to_string(),
),
centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd = runner.build_run_command(&binary_path).expect("valid run command");

let args: Vec<String> = cmd.get_args().map(|s| s.to_string_lossy().to_string()).collect();
expect_true!(args
.contains(&"__fuzztest_mod__sample_fuzztest_target::sample_fuzztest_target".to_string()));
expect_true!(args.contains(&"--exact".to_string()));
}

#[gtest]
fn test_runner_build_run_command_with_duration() {
let binary_path = get_sample_fuzz_test_bin_path();
let fuzztest_options = FuzzTestOptions {
fuzz_for: Some(FuzzFor::Duration("5s".parse().unwrap())),
..Default::default()
};
let options = CargoFuzzTestOptions {
fuzztest_options,
centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd = runner.build_run_command(&binary_path).expect("valid run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())))
.collect();
expect_true!(envs.contains(&("FUZZTEST_FUZZ_FOR".to_string(), Some("5s".to_string()))));
}

#[gtest]
fn test_runner_build_run_command_with_indefinitely() {
let binary_path = get_sample_fuzz_test_bin_path();
let fuzztest_options =
FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() };
let options = CargoFuzzTestOptions {
fuzztest_options,
centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd = runner.build_run_command(&binary_path).expect("valid run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())))
.collect();
expect_true!(envs.contains(&("FUZZTEST_FUZZ_FOR".to_string(), Some("inf".to_string()))));
}

#[gtest]
fn test_runner_build_run_command_with_centipede_binary_path() {
let binary_path = get_sample_fuzz_test_bin_path();
let options = CargoFuzzTestOptions {
centipede_binary_path: Some("/custom/path/to/centipede".to_string()),
..Default::default()
};
let runner = FuzztestRunner::new("x86_64-unknown-linux-gnu".to_string(), options);
let cmd = runner.build_run_command(&binary_path).expect("valid run command");

let envs: Vec<(String, Option<String>)> = cmd
.get_envs()
.map(|(k, v)| (k.to_string_lossy().to_string(), v.map(|s| s.to_string_lossy().to_string())))
.collect();
expect_true!(envs.contains(&(
"FUZZTEST_CENTIPEDE_BINARY_PATH".to_string(),
Some("/custom/path/to/centipede".to_string())
)));
}

#[gtest]
fn test_execution_mode_without_centipede_binary_path_errors() {
let fuzztest_options =
FuzzTestOptions { fuzz_for: Some(FuzzFor::Indefinitely), ..Default::default() };
let options = CargoFuzzTestOptions { fuzztest_options, ..Default::default() };
let result = options.execution_mode();
expect_true!(result.is_err());
}

#[gtest]
fn test_runner_list_command() {
let binary_path = get_sample_fuzz_test_bin_path();
Expand All @@ -29,7 +122,7 @@ fn test_runner_list_command() {

let runner = FuzztestRunner::new("sample-host-triple".to_string(), options);

let cmd = runner.build_run_command(&binary_path);
let cmd = runner.build_run_command(&binary_path).expect("should build run command");

let args: Vec<String> = cmd.get_args().map(|a| a.to_string_lossy().to_string()).collect();
expect_eq!(args, &["__fuzztest_mod__", "--list"]);
Expand Down
Loading