Skip to content

Add RunPhase, update(), with_context(), shrink_enabled, and on_failure() to TestRunContext - #314

Open
camshaft with Copilot wants to merge 7 commits into
masterfrom
copilot/suppress-tracing-logs
Open

camshaft with Copilot wants to merge 7 commits into
masterfrom
copilot/suppress-tracing-logs

Conversation

Copilot AI commented May 20, 2026

Copy link
Copy Markdown
Contributor

TestRunContext only tracked iteration and input, giving logging/tracing filters no way to distinguish normal test runs from shrinking or confirmed failures, and no way to mutate the live context without paying the cost of a full guard reconstruction each iteration.

Changes

bolero-engine: RunPhase enum + update() + with_context()

  • Added RunPhase { Normal, Shrink, Failure } (derives Default = Normal) and a run_phase field on TestRunContext
  • Added update(FnOnce(&mut TestRunContext)) — acquires a single TLS borrow and lets the caller mutate any fields in one shot, replacing the need for multiple individual setter calls
  • Added with_context<F, R>(FnOnce(&TestRunContext) -> R) -> Option<R> — a zero-clone callback API for read-only inspection that avoids the PathBuf clone that current_context() requires
  • Added shrink_enabled: bool field on TestRunContext — lets applications know whether the harness will produce RunPhase::Shrink iterations before RunPhase::Failure; defaults to true
  • Added on_failure(f: impl FnOnce() + 'static) — registers a callback invoked just before the harness panics or aborts to report a confirmed failure, allowing applications to flush buffered diagnostic output
  • Updated lib.rs re-exports to include RunPhase, update, with_context, and on_failure

bolero test engine: one guard, update() per iteration

  • run_tests and run_exhaustive now enter() once before the loop and call update(|ctx| { ... }) at the top of each iteration to set input, iteration, and run_phase in a single TLS borrow
  • shrink_enabled is set in the initial context based on options.shrink_time_or_default() in run_tests, and false in run_exhaustive (exhaustive mode never shrinks)
  • clear_on_failure() is called at the start of each iteration to prevent stale callbacks from firing on a later failure
  • invoke_on_failure() is called just before panic!("test failed") in both run_tests and run_exhaustive

Shrink phase propagation (managed by Shrinker)

  • RunPhase::Shrink and RunPhase::Failure are set entirely inside Shrinker::shrink() in bolero-engine/src/shrink.rs — individual engines no longer manage these transitions
  • RunPhase::Shrink is set at the start of the shrink loop (after the zero-shrink-time early return)
  • RunPhase::Failure is set immediately before the final confirmed-failure execute() call, so the application actually re-runs the minimal failing input with Failure phase and can capture diagnostic output
  • For the "no-shrink" path (test.shrink() returns None — shrink time is zero or no improvement found), each call site sets RunPhase::Failure and re-runs the original input before formatting the error message, covering the File, Rng, exhaustive, and libfuzzer cases

Fuzzer engines (libfuzzer, afl, honggfuzz, kani)

  • All TestRunContext::new() call sites updated to pass RunPhase::Normal
  • Added no-op stubs to kani_impl for update, on_failure, invoke_on_failure, and clear_on_failure
  • invoke_on_failure() called before std::process::abort() in libfuzzer

Usage

fn my_function(input: &[u8]) {
    // Zero-clone callback — no PathBuf allocation
    bolero::with_context(|ctx| {
        if !ctx.shrink_enabled {
            // Shrinking disabled: no Shrink phase will occur, go straight to Failure
        }
        match ctx.run_phase {
            bolero::RunPhase::Normal => { /* suppress logs */ }
            bolero::RunPhase::Shrink => { /* capture shrink attempts */ }
            bolero::RunPhase::Failure => { /* always emit — this is the confirmed (minimal) failure */ }
        }
    });

    // Register a callback to flush buffered logs right before the harness panics
    bolero::on_failure(move || {
        eprintln!("=== captured logs ===");
        // flush_buffered_logs();
    });

    // Owned copy when needed
    if let Some(ctx) = bolero::current_context() {
        eprintln!("engine: {:?}, iteration: {}", ctx.engine, ctx.iteration);
    }
}

When a test fails, the application will observe:

  1. Many iterations with RunPhase::Normal
  2. Shrink attempts with RunPhase::Shrink (if shrink_enabled)
  3. A re-execution of the minimal (or original) failing input with RunPhase::Failure — this is the run where diagnostic output should be emitted
  4. The on_failure callback fires immediately after step 3, before the harness panics or aborts

Copilot AI and others added 3 commits May 20, 2026 14:14

@camshaft camshaft left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

I think we'll need to make the input path a Rc instead if we require that the application clones out the state every time. That or we'll need a way for them to pass a callback and inspect the context and return something from that. That's probably cheaper than the clone

Comment thread lib/bolero-engine/src/test_context.rs Outdated
/// iteration counter without reconstructing the context guard. Has no effect
/// when called outside a bolero test harness.
#[doc(hidden)]
pub fn set_iteration(iteration: u64) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Maybe instead of individual setters we just had an update function where the caller could mutate it themselves. That way you don't have to deref the thread local over and over again.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 02ec219. Replaced the three individual setters with a single update(FnOnce(&mut TestRunContext)) that acquires one TLS borrow and lets the caller mutate any fields they need in one shot.

Copilot AI commented May 20, 2026

Copy link
Copy Markdown
Contributor Author

I think we'll need to make the input path a Rc instead if we require that the application clones out the state every time. That or we'll need a way for them to pass a callback and inspect the co...

Added with_context<F, R>(f: F) -> Option<R> in 02ec219. Callers can now inspect the context via a callback without cloning — no PathBuf clone or heap allocation for read-only access. current_context() is kept for cases where an owned copy is needed.

Copilot AI changed the title Add RunPhase and in-place context setters to TestRunContext Add RunPhase, update(), and with_context() to TestRunContext May 20, 2026
Copilot AI requested a review from camshaft May 20, 2026 15:11
Comment thread lib/bolero-libfuzzer/src/lib.rs Outdated
Comment on lines +69 to +71
bolero_engine::test_context::update(|ctx| {
ctx.run_phase = bolero_engine::RunPhase::Shrink;
});

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

instead of making every engine set the shrink phase would it be better to have the shrinking logic set the phase? and i think that would actually be better cause it would need to run the input one last time with the Failure run phase set so the application could actually capture the log. otherwise the failure is set but we didn't actually rerun anything so the log output will be empty if the application is filtering based on run_phase == Failure

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 0807b78.

RunPhase::Shrink and RunPhase::Failure are now set entirely inside Shrinker::shrink() in bolero-engine/src/shrink.rs — the individual engines no longer touch the phase during shrinking:

  • RunPhase::Shrink is set at the start of the shrink loop (after the zero-shrink-time early return)
  • RunPhase::Failure is set immediately before the final confirmed-failure execute() call (line ~149), so the application actually runs the minimal failing input with Failure phase and can capture diagnostic output

For the "no shrink" path where test.shrink() returns None (shrink time=0 or no improvement), each call site now sets RunPhase::Failure and re-runs the original input before formatting the error message — covering the File, Rng, exhaustive, and libfuzzer cases.

Copilot AI requested a review from camshaft May 20, 2026 16:13
Comment on lines +147 to +151
// Set the failure phase before the final confirmed-failure execution so
// the application can capture diagnostic output for the minimal failing input.
crate::test_context::update(|ctx| {
ctx.run_phase = crate::RunPhase::Failure;
});

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

yeah this is great! exactly what i was thinking. that being said, we need to figure out what to do when shrinking is disabled. I think we probably add another field to the context to indicate that it is and that the application will need to buffer differently. and it would be more about the last input that was ran instead of getting an explicit signal that there was a failure. the problem is by that point, bolero panics and there's no way for the application to get a signal that we're about to panic and it needs to dump its buffered log. so i think we'll probably also need to add a way for the application to register a Box<dyn Fn()> that triggers right before we panic and fail the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in b001508.

Added two things:

shrink_enabled: bool on TestRunContext — defaults to true. The test engine sets it correctly in run_tests (based on rng_options.shrink_time_or_default().is_zero()) and in run_exhaustive (always false). Fuzzer engines leave it at the default true since their shrinking is handled externally.

bolero::on_failure(f: impl FnOnce() + 'static) — stored in a separate ON_FAILURE TLS cell (kept separate from TestRunContext since FnOnce isn't Clone). The callback is:

  • Cleared at the start of each iteration via clear_on_failure() (prevents a stale callback from a passing iteration firing on a later failure)
  • Invoked just before panic!("test failed") in run_tests/run_exhaustive and just before abort() in libfuzzer

The intended flow: during the RunPhase::Failure re-run, the application registers bolero::on_failure(|| flush_logs()). When the re-run ends and the harness is about to panic, invoke_on_failure() fires the callback and the buffered logs are flushed before the process exits.

Copilot AI changed the title Add RunPhase, update(), and with_context() to TestRunContext Add RunPhase, update(), with_context(), shrink_enabled, and on_failure() to TestRunContext May 20, 2026
Copilot AI requested a review from camshaft May 20, 2026 16:30
@camshaft
camshaft marked this pull request as ready for review May 25, 2026 07:21
Copilot AI review requested due to automatic review settings May 25, 2026 07:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

This PR expands TestRunContext so applications can distinguish normal runs vs shrinking vs confirmed failures, read context without cloning, and run a per-failure callback before the harness panics/aborts. It also updates the built-in test engine and several fuzzing engines to populate the new context fields and propagate phase transitions through shrinking.

Changes:

  • Added RunPhase, iteration, shrink_enabled, with_context(), update(), and an on_failure() callback mechanism to bolero-engine’s test context.
  • Updated the bolero test engine to enter the TLS context once and update() it per-iteration; invokes on_failure just before panicking.
  • Propagated shrink/failure phases from Shrinker and updated multiple fuzzing engines for the new TestRunContext::new(...) signature.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
lib/bolero/src/test/mod.rs Enters context once, updates per iteration, clears/invokes failure callbacks, and replays failing inputs with RunPhase::Failure.
lib/bolero/src/lib.rs Re-exports new public context APIs (RunPhase, with_context, on_failure).
lib/bolero-libfuzzer/src/lib.rs Updates context construction; adds failure-phase replay on no-shrink path and invokes failure callback before abort (in one path).
lib/bolero-honggfuzz/src/lib.rs Updates context construction for new TestRunContext::new(...) signature.
lib/bolero-afl/src/lib.rs Updates context construction for new TestRunContext::new(...) signature.
lib/bolero-engine/src/test_context.rs Introduces RunPhase, extends TestRunContext, adds update()/with_context()/on_failure() + TLS callback plumbing.
lib/bolero-engine/src/shrink.rs Sets RunPhase::Shrink during shrinking and RunPhase::Failure for the final confirmed-failure execution.
lib/bolero-engine/src/lib.rs Re-exports new public context types/functions from test_context.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

#[cfg(not(kani))]
mod std_impl {
use super::TestRunContext;
use super::{RunPhase, TestInput, TestRunContext};
#[doc(hidden)]
pub fn new(engine: EngineKind, input: TestInput) -> Self {
Self { engine, input }
pub fn new(engine: EngineKind, input: TestInput, iteration: u64, run_phase: RunPhase) -> Self {
Comment on lines 33 to +37
pub use test::*;
pub use test_context::{current_context, is_active, EngineKind, TestInput, TestRunContext};
pub use test_context::{
current_context, is_active, on_failure, with_context, EngineKind, RunPhase, TestInput,
TestRunContext,
};
Comment on lines 69 to +83
let shrunken = test.shrink(slice.to_vec(), None, options);

if let Some(shrunken) = shrunken {
// shrink.rs already ran the final confirmed-failure execution
// with RunPhase::Failure set
eprintln!("{shrunken:#}");
} else {
// Shrinking was skipped or made no progress.
// Set failure phase and re-run the original input so the
// application can capture diagnostic output.
bolero_engine::test_context::update(|ctx| {
ctx.run_phase = bolero_engine::RunPhase::Failure;
});
let mut replay = input::cache::Bytes::new(slice, options, &mut cache);
let _ = test.test(&mut replay);
Comment on lines 111 to 120
panic::set_hook();
panic::forward_panic(false);

let _ctx_guard =
bolero_engine::test_context::enter(bolero_engine::TestRunContext::new(
bolero_engine::EngineKind::LibFuzzer,
bolero_engine::TestInput::default(),
0,
bolero_engine::RunPhase::Normal,
));
Comment thread lib/bolero-afl/src/lib.rs
Comment on lines 41 to 47
let _ctx_guard =
bolero_engine::test_context::enter(bolero_engine::TestRunContext::new(
bolero_engine::EngineKind::Afl,
bolero_engine::TestInput::default(),
0,
bolero_engine::RunPhase::Normal,
));
Comment on lines 32 to 38
let _ctx_guard =
bolero_engine::test_context::enter(bolero_engine::TestRunContext::new(
bolero_engine::EngineKind::Honggfuzz,
bolero_engine::TestInput::default(),
0,
bolero_engine::RunPhase::Normal,
));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants