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

feat(invariants): real-time runs counter #7302

Closed
Closed
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
1 change: 1 addition & 0 deletions crates/config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ include_storage = true
include_push_bytes = true
shrink_sequence = true
preserve_state = false
show_progress = false

[fmt]
line_length = 100
Expand Down
3 changes: 3 additions & 0 deletions crates/config/src/invariant.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct InvariantConfig {
/// Applies only when `fail_on_revert` set to true. Use it with caution, introduces performance
/// penalty.
pub preserve_state: bool,
/// Whether to show invariant test progress (as completed percentage and completed/total runs).
pub show_progress: bool,
}

impl Default for InvariantConfig {
Expand All @@ -46,6 +48,7 @@ impl Default for InvariantConfig {
shrink_sequence: true,
shrink_run_limit: 2usize.pow(18_u32),
preserve_state: false,
show_progress: false,
}
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/evm/evm/src/executors/invariant/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ use foundry_evm_fuzz::strategies::CalldataFuzzDictionary;
mod funcs;
pub use funcs::{assert_invariants, replay_run};

pub mod report;
pub use report::init;
use report::{add_runs, complete_run};

/// Alias for (Dictionary for fuzzing, initial contracts to fuzz and an InvariantStrategy).
type InvariantPreparation = (
EvmFuzzState,
Expand Down Expand Up @@ -109,6 +113,8 @@ impl<'a> InvariantExecutor<'a> {
return Err(eyre!("Invariant test function should have no inputs"))
}

add_runs(self.config.runs);

let (fuzz_state, targeted_contracts, strat, calldata_fuzz_dictionary) =
self.prepare_fuzzing(&invariant_contract)?;

Expand Down Expand Up @@ -248,6 +254,8 @@ impl<'a> InvariantExecutor<'a> {

fuzz_cases.borrow_mut().push(FuzzedCases::new(fuzz_runs));

complete_run();

Ok(())
});

Expand Down
121 changes: 121 additions & 0 deletions crates/evm/evm/src/executors/invariant/report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
use foundry_common::term::Spinner;
use parking_lot::Mutex;
use std::{
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
thread,
time::Duration,
};

static GLOBAL_INVARIANT_REPORTER_STATE: AtomicUsize = AtomicUsize::new(UN_SET);

const UN_SET: usize = 0;
const SETTING: usize = 1;
const SET: usize = 2;

static mut GLOBAL_INVARIANT_REPORTER: Option<InvariantRunsReporter> = None;

/// Stores information about overall invariant tests progress (completed runs / total runs).
#[derive(Default)]
struct InvariantProgress {
/// Total number of runs (for all invariant tests).
total_runs: u32,
/// Number of completed runs (for all invariant tests).
completed_runs: u32,
}

/// Reporter of the invariant test progress, set as a global reporter.
/// The number of invariant runs are incremented prior of each test execution.
/// Completed runs are incremented on each test execution.
/// Status is displayed in terminal as a spinner message on a thread that polls progress every
/// 100ms.
#[derive(Clone)]
pub struct InvariantRunsReporter {
inner: Arc<Mutex<InvariantProgress>>,
}

impl InvariantRunsReporter {
pub fn add_runs(&self, runs: u32) {
self.inner.lock().total_runs += runs;
}

pub fn complete_run(&self) {
self.inner.lock().completed_runs += 1;
}
}

impl Default for InvariantRunsReporter {
fn default() -> Self {
let inner_reporter = Arc::new(Mutex::new(InvariantProgress::default()));

let inner_reporter_clone = inner_reporter.clone();
// Spawn thread to periodically poll invariant progress and to display status in console.
thread::spawn(move || {
let mut spinner = Spinner::new("");
loop {
thread::sleep(Duration::from_millis(100));
spinner.tick();
let progress = &inner_reporter_clone.lock();
if progress.total_runs != 0 && progress.completed_runs != 0 {
spinner.message(format!(
"Invariant runs {:.0}% ({}/{})",
(100.0 * (progress.completed_runs as f32 / progress.total_runs as f32))
.floor(),
progress.completed_runs,
progress.total_runs
));
}
}
});

Self { inner: inner_reporter }
}
}

/// Create invariant reporter and set it as a global reporter.
pub fn init(show_progress: bool) {
if show_progress {
set_global_reporter(InvariantRunsReporter::default());
}
}

/// Add test runs to the total runs counter.
pub fn add_runs(runs: u32) {
if let Some(reporter) = get_global_reporter() {
reporter.add_runs(runs);
}
}

/// Increment invariant completed runs counter.
pub fn complete_run() {
if let Some(reporter) = get_global_reporter() {
reporter.complete_run();
}
}

fn set_global_reporter(reporter: InvariantRunsReporter) {
if GLOBAL_INVARIANT_REPORTER_STATE
.compare_exchange(UN_SET, SETTING, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
unsafe {
GLOBAL_INVARIANT_REPORTER = Some(reporter);
}
GLOBAL_INVARIANT_REPORTER_STATE.store(SET, Ordering::SeqCst);
}
}

fn get_global_reporter() -> Option<&'static InvariantRunsReporter> {
if GLOBAL_INVARIANT_REPORTER_STATE.load(Ordering::SeqCst) != SET {
return None;
}
unsafe {
// This is safe given the invariant that setting the global reporter
// also sets `GLOBAL_INVARIANT_REPORTER_STATE` to `SET`.
Some(GLOBAL_INVARIANT_REPORTER.as_ref().expect(
"Reporter invariant violated: GLOBAL_INVARIANT_REPORTER must be initialized before GLOBAL_INVARIANT_REPORTER_STATE is set",
))
}
}
4 changes: 3 additions & 1 deletion crates/forge/src/multi_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ use foundry_compilers::{contracts::ArtifactContracts, Artifact, ArtifactId, Proj
use foundry_evm::{
backend::Backend,
decode::RevertDecoder,
executors::{Executor, ExecutorBuilder},
executors::{invariant::report as invariant_report, Executor, ExecutorBuilder},
fork::CreateFork,
inspectors::CheatsConfig,
opts::EvmOpts,
Expand Down Expand Up @@ -172,6 +172,8 @@ impl MultiContractRunner {
find_time,
);

invariant_report::init(self.test_options.invariant.show_progress);

contracts.par_iter().for_each_with(tx, |tx, &(id, (abi, deploy_code, libs))| {
let identifier = id.identifier();
let executor = executor.clone();
Expand Down
1 change: 1 addition & 0 deletions crates/forge/tests/it/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@ pub static TEST_OPTS: Lazy<TestOptions> = Lazy::new(|| {
shrink_sequence: true,
shrink_run_limit: 2usize.pow(18u32),
preserve_state: false,
show_progress: false,
})
.build(&COMPILED, &PROJECT.paths.root)
.expect("Config loaded")
Expand Down
1 change: 1 addition & 0 deletions testdata/foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ invariant_fail_on_revert = false
invariant_call_override = false
invariant_shrink_sequence = true
invariant_preserve_state = false
invariant_show_progress = false
gas_limit = 9223372036854775807
gas_price = 0
gas_reports = ["*"]
Expand Down
Loading