Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add JS fuzz/invariant integration tests #658

Merged
merged 6 commits into from
Sep 13, 2024
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
6 changes: 3 additions & 3 deletions crates/edr_napi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,8 @@ export interface SolidityTestRunnerConfigArgs {
}
/** Fuzz testing configuration */
export interface FuzzConfigArgs {
/** Path where fuzz failures are recorded and replayed. */
failurePersistDir: string
/** Path where fuzz failures are recorded and replayed if set. */
failurePersistDir?: string
/** Name of the file to record fuzz failures, defaults to `failures`. */
failurePersistFile?: string
/**
Expand Down Expand Up @@ -560,7 +560,7 @@ export interface FuzzConfigArgs {
}
/** Invariant testing configuration. */
export interface InvariantConfigArgs {
/** Path where invariant failures are recorded and replayed. */
/** Path where invariant failures are recorded and replayed if set. */
failurePersistDir?: string
/**
* The number of runs that must execute for each invariant test group.
Expand Down
19 changes: 12 additions & 7 deletions crates/edr_napi/src/solidity_tests/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,8 @@ impl TryFrom<SolidityTestRunnerConfigArgs> for SolidityTestRunnerConfig {

let invariant: InvariantConfig = fuzz
.as_ref()
.map(|f| invariant.unwrap_or_default().defaults_from_fuzz(f))
.map(|f| invariant.clone().unwrap_or_default().defaults_from_fuzz(f))
.or(invariant)
.map(TryFrom::try_from)
.transpose()?
.unwrap_or_default();
Expand Down Expand Up @@ -301,8 +302,8 @@ impl TryFrom<SolidityTestRunnerConfigArgs> for SolidityTestRunnerConfig {
#[napi(object)]
#[derive(Clone, Default, Debug)]
pub struct FuzzConfigArgs {
/// Path where fuzz failures are recorded and replayed.
pub failure_persist_dir: String,
/// Path where fuzz failures are recorded and replayed if set.
pub failure_persist_dir: Option<String>,
/// Name of the file to record fuzz failures, defaults to `failures`.
pub failure_persist_file: Option<String>,
/// The amount of fuzz runs to perform for each fuzz test case. Higher
Expand Down Expand Up @@ -348,8 +349,8 @@ impl TryFrom<FuzzConfigArgs> for FuzzConfig {
include_push_bytes,
} = value;

let failure_persist_dir = Some(failure_persist_dir.into());
let failure_persist_file = Some(failure_persist_file.unwrap_or("failures".to_string()));
let failure_persist_dir = failure_persist_dir.map(PathBuf::from);
let failure_persist_file = failure_persist_file.unwrap_or("failures".to_string());
let seed = seed
.map(|s| {
s.parse().map_err(|_err| {
Expand All @@ -362,6 +363,8 @@ impl TryFrom<FuzzConfigArgs> for FuzzConfig {
seed,
failure_persist_dir,
failure_persist_file,
// TODO https://github.com/NomicFoundation/edr/issues/657
gas_report_samples: 0,
..FuzzConfig::default()
};

Expand Down Expand Up @@ -393,7 +396,7 @@ impl TryFrom<FuzzConfigArgs> for FuzzConfig {
#[napi(object)]
#[derive(Clone, Default, Debug)]
pub struct InvariantConfigArgs {
/// Path where invariant failures are recorded and replayed.
/// Path where invariant failures are recorded and replayed if set.
pub failure_persist_dir: Option<String>,
/// The number of runs that must execute for each invariant test group.
/// Defaults to 256.
Expand Down Expand Up @@ -442,7 +445,7 @@ impl InvariantConfigArgs {
} = fuzz;

if self.failure_persist_dir.is_none() {
self.failure_persist_dir = Some(failure_persist_dir.clone());
self.failure_persist_dir.clone_from(failure_persist_dir);
}

if self.runs.is_none() {
Expand Down Expand Up @@ -483,6 +486,8 @@ impl From<InvariantConfigArgs> for InvariantConfig {

let mut invariant = InvariantConfig {
failure_persist_dir,
// TODO https://github.com/NomicFoundation/edr/issues/657
gas_report_samples: 0,
..InvariantConfig::default()
};

Expand Down
11 changes: 3 additions & 8 deletions crates/foundry/config/src/fuzz.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct FuzzConfig {
/// Path where fuzz failures are recorded and replayed.
pub failure_persist_dir: Option<PathBuf>,
/// Name of the file to record fuzz failures, defaults to `failures`.
pub failure_persist_file: Option<String>,
pub failure_persist_file: String,
}

impl Default for FuzzConfig {
Expand All @@ -36,7 +36,7 @@ impl Default for FuzzConfig {
dictionary: FuzzDictionaryConfig::default(),
gas_report_samples: 0,
failure_persist_dir: None,
failure_persist_file: None,
failure_persist_file: "failures".into(),
}
}
}
Expand All @@ -46,13 +46,8 @@ impl FuzzConfig {
/// `{PROJECT_ROOT}/cache/fuzz` dir.
pub fn new(cache_dir: PathBuf) -> Self {
FuzzConfig {
runs: 256,
max_test_rejects: 65536,
seed: None,
dictionary: FuzzDictionaryConfig::default(),
gas_report_samples: 0,
failure_persist_dir: Some(cache_dir),
failure_persist_file: Some("failures".to_string()),
..FuzzConfig::default()
}
}
}
Expand Down
14 changes: 10 additions & 4 deletions crates/foundry/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,10 +69,16 @@ impl InvariantConfig {
}

/// Returns path to failure dir of given invariant test contract.
pub fn failure_dir(self, contract_name: &str) -> PathBuf {
pub fn failure_dir(&self, contract_name: &str) -> Option<PathBuf> {
self.failure_persist_dir
.unwrap()
.join("failures")
.join(contract_name.split(':').last().unwrap())
.as_ref()
.map(|failure_persist_dir| {
failure_persist_dir.join("failures").join(
contract_name
.split(':')
.last()
.expect("contract name should have solc version suffix"),
)
})
}
}
61 changes: 48 additions & 13 deletions crates/foundry/forge/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
#[macro_use]
extern crate tracing;

use std::{
collections::HashSet,
fmt::Debug,
sync::{OnceLock, RwLock},
};

use foundry_config::{FuzzConfig, InvariantConfig};
use proptest::test_runner::{
FailurePersistence, FileFailurePersistence, RngAlgorithm, TestRng, TestRunner,
Expand All @@ -25,6 +31,8 @@ pub mod result;
pub use foundry_common::traits::TestFilter;
pub use foundry_evm::*;

static FAILURE_PATHS: OnceLock<RwLock<HashSet<&'static str>>> = OnceLock::new();

/// Metadata on how to run fuzz/invariant tests
#[derive(Clone, Debug, Default)]
pub struct TestOptions {
Expand All @@ -38,19 +46,46 @@ impl TestOptions {
/// Returns a "fuzz" test runner instance.
pub fn fuzz_runner(&self) -> TestRunner {
let fuzz_config = self.fuzz_config().clone();
let failure_persist_path = fuzz_config
.failure_persist_dir
.unwrap()
.join(fuzz_config.failure_persist_file.unwrap())
.into_os_string()
.into_string()
.unwrap();
self.fuzzer_with_cases(
fuzz_config.runs,
Some(Box::new(FileFailurePersistence::Direct(
failure_persist_path.leak(),
))),
)

if let Some(failure_persist_dir) = fuzz_config.failure_persist_dir {
let failure_persist_path = failure_persist_dir
.join(fuzz_config.failure_persist_file)
.into_os_string()
.into_string()
.expect("path should be valid UTF-8");

// HACK: We need to leak the path as
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For correctness, I believe we should still drop the leaked strings upon static deinitialization. That can be achieved by adding a local struct SomeName { inner: HashSet<&'static str> } which has a impl Drop. This destructor would be called when the static FAILURE_PATHS is dropped, guaranteeing that our application doesn't report any memory leaks in tooling.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, I tried this out with LLVM leak sanitizer and it reports errors in the base branch, but not in this one, so I think it's ok as is.

Test command (tried on ARM linux in Docker): RUSTFLAGS="-Z sanitizer=leak" cargo +nightly test -p forge test_fuzz

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(I couldn't get Miri working on the forge integration tests)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for validating with leak sanitizer.

This is the reason why Leak Sanitizer would not be able to detect a "memory leak" of this kind. It depends on your definition of memory leak, though. Leak sanitizer doesn't consider it a leak because a static variable is still referring to the address of the originally allocated strings.

// `proptest::test_runner::FileFailurePersistence` requires a
// `&'static str`. We mitigate this by making sure that one particular path
// is only leaked once.
let failure_paths = FAILURE_PATHS.get_or_init(RwLock::default);
// Need to be in a block to ensure that the read lock is dropped before we try
// to insert.
{
let failure_paths_guard = failure_paths.read().expect("lock is not poisoned");
if let Some(static_path) = failure_paths_guard.get(&*failure_persist_path) {
return self.fuzzer_with_cases(
fuzz_config.runs,
Some(Box::new(FileFailurePersistence::Direct(static_path))),
);
}
}
// Write block
{
let mut failure_paths_guard = failure_paths.write().expect("lock is not poisoned");
failure_paths_guard.insert(failure_persist_path.clone().leak());
let static_path = failure_paths_guard
.get(&*failure_persist_path)
.expect("must exist since we just inserted it");

self.fuzzer_with_cases(
fuzz_config.runs,
Some(Box::new(FileFailurePersistence::Direct(static_path))),
)
}
} else {
self.fuzzer_with_cases(fuzz_config.runs, None)
}
}

/// Returns an "invariant" test runner instance.
Expand Down
Loading
Loading