diff --git a/crates/fuzz/src/trident/flow_executor.rs b/crates/fuzz/src/trident/flow_executor.rs index c52699594..28d77e241 100644 --- a/crates/fuzz/src/trident/flow_executor.rs +++ b/crates/fuzz/src/trident/flow_executor.rs @@ -1,4 +1,3 @@ -use std::io::IsTerminal; use std::panic::catch_unwind; use std::panic::AssertUnwindSafe; use std::sync::atomic::AtomicBool; @@ -10,6 +9,7 @@ use std::time::Instant; use trident_config::TridentConfig; use trident_fuzz_metrics::TridentFuzzingData; +use crate::trident::progress; use crate::trident::Trident; // Thread-local storage for panic location information. @@ -61,13 +61,6 @@ impl ExitCodeMode { } } -/// Events sent from worker threads to the UI/controller thread in parallel fuzzing. -enum WorkerEvent { - ProgressDelta(u64), - InvariantFailure(String), - ProgramPanicsDelta(u64), -} - /// Final process exit outcomes for a fuzzing run. #[derive(Debug, Clone, Copy, PartialEq, Eq)] enum FuzzRunExit { @@ -114,86 +107,6 @@ fn determine_exit_outcome(input: ExitDecisionInput) -> FuzzRunExit { } } -fn colors_enabled() -> bool { - std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none() -} - -fn paint_red(text: &str) -> String { - if colors_enabled() { - format!("\x1b[31m{}\x1b[0m", text) - } else { - text.to_string() - } -} - -fn paint_bold_yellow(text: &str) -> String { - if colors_enabled() { - format!("\x1b[1;33m{}\x1b[0m", text) - } else { - text.to_string() - } -} - -fn paint_cyan(text: &str) -> String { - if colors_enabled() { - format!("\x1b[36m{}\x1b[0m", text) - } else { - text.to_string() - } -} - -fn paint_magenta(text: &str) -> String { - if colors_enabled() { - format!("\x1b[35m{}\x1b[0m", text) - } else { - text.to_string() - } -} - -fn format_invariant_line(text: &str) -> String { - const PREFIX: &str = "Assertion failed at "; - const SEED_PREFIX: &str = " (seed: "; - - if !colors_enabled() { - return text.to_string(); - } - - // Expected format from handle_panic: - // "Assertion failed at : (seed: )" - if let Some(location_start) = text.strip_prefix(PREFIX) { - if let Some(seed_idx) = location_start.rfind(SEED_PREFIX) { - let before_seed = &location_start[..seed_idx]; - let seed_with_suffix = &location_start[seed_idx + SEED_PREFIX.len()..]; - if let Some(seed) = seed_with_suffix.strip_suffix(')') { - if let Some(separator_idx) = before_seed.find(": ") { - let location = &before_seed[..separator_idx]; - let message = &before_seed[separator_idx + 2..]; - return format!( - "{} {}{}: {}{}{}{}", - paint_red("!"), - PREFIX, - paint_cyan(location), - message, - SEED_PREFIX, - paint_magenta(seed), - ")" - ); - } - } - } - } - - // Fallback to accent-only if line doesn't match expected format. - format!("{} {}", paint_red("!"), text) -} - -/// Final aggregated runtime summary produced by the UI/controller thread. -struct ParallelRunSummary { - invariant_failures: u64, - program_panics: u64, - panic_messages: Vec, -} - /// Trait for executing fuzzing flows in the Trident framework /// /// This trait defines the interface for fuzzing executors that can run @@ -406,6 +319,9 @@ pub trait FlowExecutor: Send + 'static + Sized { let mut invariant_failed = false; // Tracks fuzz test assertion/invariant failures let mut invariant_failure_count: u64 = 0; let mut panic_messages: Vec = Vec::new(); + let total_flow_calls = iterations * flow_calls_per_iteration; + let mut completed_flow_calls = 0u64; + let start_time = Instant::now(); // Configure debug seed if in debug mode if is_debug_mode { @@ -419,20 +335,11 @@ pub trait FlowExecutor: Send + 'static + Sized { let pb = if is_debug_mode { None } else { - let total_flow_calls = iterations * flow_calls_per_iteration; - let pb = indicatif::ProgressBar::new(total_flow_calls); - pb.set_style( - indicatif::ProgressStyle::with_template( - "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%) [{eta_precise}] {msg}" - ) - .unwrap() - .progress_chars("#>-"), - ); - pb.set_message(format!( - "Fuzzing {} iterations with {} flow calls each...", - iterations, flow_calls_per_iteration - )); - Some(pb) + Some(progress::create_single_progress_bar( + total_flow_calls, + iterations, + flow_calls_per_iteration, + )) }; // Main fuzzing loop: execute flows, catch panics, and track progress @@ -455,7 +362,7 @@ pub trait FlowExecutor: Send + 'static + Sized { // In debug mode print immediately, otherwise collect for end if is_debug_mode { - eprintln!("{}", format_invariant_line(&panic_msg)); + eprintln!("{}", progress::format_invariant_line(&panic_msg)); } else { panic_messages.push(panic_msg); } @@ -475,16 +382,19 @@ pub trait FlowExecutor: Send + 'static + Sized { // Update progress bar with live stats if let Some(ref pb) = pb { pb.inc(flow_calls_per_iteration); + completed_flow_calls += flow_calls_per_iteration; let program_panics = fuzzer .trident_mut() .get_fuzzing_data() .get_program_panic_count(); - pb.set_message(format!( - "Iteration {}/{} | Invariant failures: {} | Program panics: {}", + pb.set_message(progress::format_live_status_single( i + 1, iterations, + completed_flow_calls, + total_flow_calls, invariant_failure_count, - program_panics + program_panics, + start_time.elapsed(), )); } } @@ -498,13 +408,13 @@ pub trait FlowExecutor: Send + 'static + Sized { if !panic_messages.is_empty() { eprintln!( "\n{}", - paint_bold_yellow(&format!( + progress::paint_bold_yellow(&format!( "--- Invariant Failures ({}) ---", panic_messages.len() )) ); for msg in &panic_messages { - eprintln!("{}", format_invariant_line(msg)); + eprintln!("{}", progress::format_invariant_line(msg)); } } @@ -536,56 +446,12 @@ pub trait FlowExecutor: Send + 'static + Sized { let remainder_iterations = iterations % num_threads as u64; let total_flow_calls = iterations * flow_calls_per_iteration; let exit_code_mode = ExitCodeMode::from_env(); - let (event_tx, event_rx) = mpsc::channel::(); - - // Setup shared progress bar - let main_pb = indicatif::ProgressBar::new(total_flow_calls); - main_pb.set_style( - indicatif::ProgressStyle::with_template( - "Overall: {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%) [{eta_precise}] {msg}" - ) - .unwrap() - .progress_chars("#>-"), - ); - main_pb.set_message(format!( - "Fuzzing with {} threads | Invariant failures: 0 | Program panics: 0", - num_threads - )); + let (event_tx, event_rx) = mpsc::channel::(); // Single UI/controller owner: consumes worker events, updates progress bar, // and builds global live counters + invariant messages. - let ui_handle = thread::spawn(move || -> ParallelRunSummary { - let mut invariant_failures = 0u64; - let mut program_panics = 0u64; - let mut panic_messages = Vec::new(); - - while let Ok(event) = event_rx.recv() { - match event { - WorkerEvent::ProgressDelta(delta) => { - main_pb.inc(delta); - } - WorkerEvent::InvariantFailure(message) => { - invariant_failures += 1; - panic_messages.push(message); - } - WorkerEvent::ProgramPanicsDelta(delta) => { - program_panics += delta; - } - } - - main_pb.set_message(format!( - "Invariant failures: {} | Program panics: {}", - invariant_failures, program_panics - )); - } - - main_pb.finish_with_message("Parallel fuzzing completed!"); - ParallelRunSummary { - invariant_failures, - program_panics, - panic_messages, - } - }); + let ui_handle = + progress::spawn_parallel_ui_controller(event_rx, num_threads, total_flow_calls); // Spawn worker threads let mut handles = Vec::new(); @@ -658,23 +524,25 @@ pub trait FlowExecutor: Send + 'static + Sized { } // Get final aggregated runtime summary from the controller thread. - let run_summary = ui_handle.join().unwrap_or_else(|_| ParallelRunSummary { - invariant_failures: 0, - program_panics: 0, - panic_messages: Vec::new(), - }); + let run_summary = ui_handle + .join() + .unwrap_or_else(|_| progress::ParallelRunSummary { + invariant_failures: 0, + program_panics: 0, + panic_messages: Vec::new(), + }); // Print collected invariant failure messages if !run_summary.panic_messages.is_empty() { eprintln!( "\n{}", - paint_bold_yellow(&format!( + progress::paint_bold_yellow(&format!( "--- Invariant Failures ({}) ---", run_summary.panic_messages.len() )) ); for msg in &run_summary.panic_messages { - eprintln!("{}", format_invariant_line(msg)); + eprintln!("{}", progress::format_invariant_line(msg)); } } @@ -750,7 +618,10 @@ pub trait FlowExecutor: Send + 'static + Sized { } } -fn send_worker_event(event_tx: &mpsc::Sender, event: WorkerEvent) -> bool { +fn send_worker_event( + event_tx: &mpsc::Sender, + event: progress::WorkerEvent, +) -> bool { event_tx.send(event).is_ok() } @@ -762,7 +633,7 @@ fn run_thread_workload_impl( thread_id: usize, thread_iterations: u64, flow_calls_per_iteration: u64, - event_tx: mpsc::Sender, + event_tx: mpsc::Sender, ) -> TridentFuzzingData { let mut fuzzer = E::new(); fuzzer @@ -789,7 +660,10 @@ fn run_thread_workload_impl( { // Intentional invariant failure - count it, continue fuzzing let panic_msg = E::handle_panic(&panic_err, &mut fuzzer, None); - if !send_worker_event(&event_tx, WorkerEvent::InvariantFailure(panic_msg)) { + if !send_worker_event( + &event_tx, + progress::WorkerEvent::InvariantFailure(panic_msg), + ) { return fuzzer.trident_mut().get_fuzzing_data(); } } else { @@ -814,7 +688,10 @@ fn run_thread_workload_impl( || i == thread_iterations - 1; // Always update on last iteration if should_update { - if !send_worker_event(&event_tx, WorkerEvent::ProgressDelta(local_counter)) { + if !send_worker_event( + &event_tx, + progress::WorkerEvent::ProgressDelta(local_counter), + ) { return fuzzer.trident_mut().get_fuzzing_data(); } let thread_prog_panics = fuzzer @@ -823,7 +700,10 @@ fn run_thread_workload_impl( .get_program_panic_count(); let new_panics = thread_prog_panics.saturating_sub(local_observed_program_panics); if new_panics > 0 { - if !send_worker_event(&event_tx, WorkerEvent::ProgramPanicsDelta(new_panics)) { + if !send_worker_event( + &event_tx, + progress::WorkerEvent::ProgramPanicsDelta(new_panics), + ) { return fuzzer.trident_mut().get_fuzzing_data(); } local_observed_program_panics = thread_prog_panics; @@ -834,7 +714,11 @@ fn run_thread_workload_impl( } // Ensure any remaining progress is reported - if local_counter > 0 && !send_worker_event(&event_tx, WorkerEvent::ProgressDelta(local_counter)) + if local_counter > 0 + && !send_worker_event( + &event_tx, + progress::WorkerEvent::ProgressDelta(local_counter), + ) { return fuzzer.trident_mut().get_fuzzing_data(); } @@ -846,7 +730,10 @@ fn run_thread_workload_impl( .get_program_panic_count(); let final_new_panics = final_thread_prog_panics.saturating_sub(local_observed_program_panics); if final_new_panics > 0 - && !send_worker_event(&event_tx, WorkerEvent::ProgramPanicsDelta(final_new_panics)) + && !send_worker_event( + &event_tx, + progress::WorkerEvent::ProgramPanicsDelta(final_new_panics), + ) { return fuzzer.trident_mut().get_fuzzing_data(); } diff --git a/crates/fuzz/src/trident/mod.rs b/crates/fuzz/src/trident/mod.rs index c9b6967c5..03cdf5dbf 100644 --- a/crates/fuzz/src/trident/mod.rs +++ b/crates/fuzz/src/trident/mod.rs @@ -15,6 +15,7 @@ mod system; pub mod transaction_result; mod metrics; +mod progress; mod random; mod seed; #[cfg(feature = "stake")] diff --git a/crates/fuzz/src/trident/progress.rs b/crates/fuzz/src/trident/progress.rs new file mode 100644 index 000000000..917de7aa4 --- /dev/null +++ b/crates/fuzz/src/trident/progress.rs @@ -0,0 +1,286 @@ +use std::io::IsTerminal; +use std::sync::mpsc; +use std::thread; +use std::time::Duration; +use std::time::Instant; + +/// Events sent from worker threads to the UI/controller thread in parallel fuzzing. +pub(crate) enum WorkerEvent { + ProgressDelta(u64), + InvariantFailure(String), + ProgramPanicsDelta(u64), +} + +/// Final aggregated runtime summary produced by the UI/controller thread. +pub(crate) struct ParallelRunSummary { + pub(crate) invariant_failures: u64, + pub(crate) program_panics: u64, + pub(crate) panic_messages: Vec, +} + +fn colors_enabled() -> bool { + std::io::stderr().is_terminal() && std::env::var_os("NO_COLOR").is_none() +} + +fn paint_red(text: &str) -> String { + if colors_enabled() { + format!("\x1b[31m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +pub(crate) fn paint_bold_yellow(text: &str) -> String { + if colors_enabled() { + format!("\x1b[1;33m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn paint_cyan(text: &str) -> String { + if colors_enabled() { + format!("\x1b[36m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn paint_magenta(text: &str) -> String { + if colors_enabled() { + format!("\x1b[35m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn paint_green(text: &str) -> String { + if colors_enabled() { + format!("\x1b[32m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn paint_dim(text: &str) -> String { + if colors_enabled() { + format!("\x1b[2m{}\x1b[0m", text) + } else { + text.to_string() + } +} + +fn paint_counter(value: u64, is_problem: bool) -> String { + let raw = value.to_string(); + if !colors_enabled() { + return raw; + } + if value == 0 { + paint_green(&raw) + } else if is_problem { + paint_red(&raw) + } else { + paint_bold_yellow(&raw) + } +} + +pub(crate) fn format_invariant_line(text: &str) -> String { + const PREFIX: &str = "Assertion failed at "; + const SEED_PREFIX: &str = " (seed: "; + + if !colors_enabled() { + return text.to_string(); + } + + // Expected format from handle_panic: + // "Assertion failed at : (seed: )" + if let Some(location_start) = text.strip_prefix(PREFIX) { + if let Some(seed_idx) = location_start.rfind(SEED_PREFIX) { + let before_seed = &location_start[..seed_idx]; + let seed_with_suffix = &location_start[seed_idx + SEED_PREFIX.len()..]; + if let Some(seed) = seed_with_suffix.strip_suffix(')') { + if let Some(separator_idx) = before_seed.find(": ") { + let location = &before_seed[..separator_idx]; + let message = &before_seed[separator_idx + 2..]; + return format!( + "{} {}{}: {}{}{}{}", + paint_red("!"), + PREFIX, + paint_cyan(location), + message, + SEED_PREFIX, + paint_magenta(seed), + ")" + ); + } + } + } + } + + // Fallback to accent-only if line doesn't match expected format. + format!("{} {}", paint_red("!"), text) +} + +fn format_exec_rate(completed: u64, elapsed: Duration) -> f64 { + let secs = elapsed.as_secs_f64(); + if secs <= f64::EPSILON { + 0.0 + } else { + completed as f64 / secs + } +} + +pub(crate) fn format_live_status_single( + iteration: u64, + iterations: u64, + completed_flow_calls: u64, + total_flow_calls: u64, + invariant_failures: u64, + program_panics: u64, + elapsed: Duration, +) -> String { + let exec_rate = format_exec_rate(completed_flow_calls, elapsed); + let iter_label = paint_cyan("iter"); + let flow_label = paint_cyan("flow calls"); + let inv_label = paint_magenta("inv"); + let panics_label = paint_magenta("panics"); + let exec_label = paint_cyan("exec/s"); + let inv_val = paint_counter(invariant_failures, true); + let panic_val = paint_counter(program_panics, true); + let iter_sep = paint_dim("|"); + let metric_sep = paint_dim("|"); + format!( + "{iter_label}: {iteration}/{iterations} {iter_sep} {flow_label}: {completed_flow_calls}/{total_flow_calls}\n{inv_label}: {inv_val} {metric_sep} {panics_label}: {panic_val} {metric_sep} {exec_label}: {exec_rate:.0}", + iter_label = iter_label, + iteration = iteration, + iterations = iterations, + iter_sep = iter_sep, + flow_label = flow_label, + completed_flow_calls = completed_flow_calls, + total_flow_calls = total_flow_calls, + inv_label = inv_label, + inv_val = inv_val, + metric_sep = metric_sep, + panics_label = panics_label, + panic_val = panic_val, + exec_label = exec_label, + exec_rate = exec_rate + ) +} + +fn format_live_status_parallel( + num_threads: usize, + completed_flow_calls: u64, + total_flow_calls: u64, + invariant_failures: u64, + program_panics: u64, + elapsed: Duration, +) -> String { + let exec_rate = format_exec_rate(completed_flow_calls, elapsed); + let threads_label = paint_cyan("threads"); + let flow_label = paint_cyan("flow calls"); + let inv_label = paint_magenta("inv"); + let panics_label = paint_magenta("panics"); + let exec_label = paint_cyan("exec/s"); + let inv_val = paint_counter(invariant_failures, true); + let panic_val = paint_counter(program_panics, true); + let sep = paint_dim("|"); + format!( + "{threads_label}: {num_threads} {sep} {flow_label}: {completed_flow_calls}/{total_flow_calls}\n{inv_label}: {inv_val} {sep} {panics_label}: {panic_val} {sep} {exec_label}: {exec_rate:.0}", + threads_label = threads_label, + num_threads = num_threads, + sep = sep, + flow_label = flow_label, + completed_flow_calls = completed_flow_calls, + total_flow_calls = total_flow_calls, + inv_label = inv_label, + inv_val = inv_val, + panics_label = panics_label, + panic_val = panic_val, + exec_label = exec_label, + exec_rate = exec_rate + ) +} + +pub(crate) fn create_single_progress_bar( + total_flow_calls: u64, + iterations: u64, + flow_calls_per_iteration: u64, +) -> indicatif::ProgressBar { + let pb = indicatif::ProgressBar::new(total_flow_calls); + pb.set_style( + indicatif::ProgressStyle::with_template( + "{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%) [{eta_precise}] {msg}", + ) + .unwrap() + .progress_chars("#>-"), + ); + pb.set_message(format!( + "Fuzzing {} iterations with {} flow calls each...", + iterations, flow_calls_per_iteration + )); + pb +} + +pub(crate) fn spawn_parallel_ui_controller( + event_rx: mpsc::Receiver, + num_threads: usize, + total_flow_calls: u64, +) -> thread::JoinHandle { + let main_pb = indicatif::ProgressBar::new(total_flow_calls); + main_pb.set_style( + indicatif::ProgressStyle::with_template( + "Overall: {spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {pos}/{len} ({percent}%) [{eta_precise}] {msg}", + ) + .unwrap() + .progress_chars("#>-"), + ); + let ui_start_time = Instant::now(); + main_pb.set_message(format_live_status_parallel( + num_threads, + 0, + total_flow_calls, + 0, + 0, + ui_start_time.elapsed(), + )); + + thread::spawn(move || -> ParallelRunSummary { + let mut invariant_failures = 0u64; + let mut program_panics = 0u64; + let mut panic_messages = Vec::new(); + let mut completed_flow_calls = 0u64; + + while let Ok(event) = event_rx.recv() { + match event { + WorkerEvent::ProgressDelta(delta) => { + completed_flow_calls += delta; + main_pb.inc(delta); + } + WorkerEvent::InvariantFailure(message) => { + invariant_failures += 1; + panic_messages.push(message); + } + WorkerEvent::ProgramPanicsDelta(delta) => { + program_panics += delta; + } + } + + main_pb.set_message(format_live_status_parallel( + num_threads, + completed_flow_calls, + total_flow_calls, + invariant_failures, + program_panics, + ui_start_time.elapsed(), + )); + } + + main_pb.finish_with_message("Parallel fuzzing completed!"); + ParallelRunSummary { + invariant_failures, + program_panics, + panic_messages, + } + }) +}