alloc-chaos 0.1.0

Deterministic allocation-failure testing for Rust.
Documentation
//! Demonstrates the main `alloc-chaos` scenarios in one example.
//!
//! Run it with:
//!
//! ```sh
//! cargo run --example scenarios
//! ```
//!
//! The example intentionally includes both successful checks and diagnostic
//! reports that are expected to be partial or failing.

use std::panic;
use std::sync::atomic::{AtomicUsize, Ordering};

#[global_allocator]
static GLOBAL: alloc_chaos::ChaosAllocator = alloc_chaos::ChaosAllocator::system();

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum BuildError {
    OutOfMemory,
}

#[derive(Debug, PartialEq, Eq)]
struct Packet {
    header: Vec<u8>,
    body: Vec<u8>,
}

fn reserve_zeroed(len: usize) -> Result<Vec<u8>, BuildError> {
    let mut bytes = Vec::new();
    bytes
        .try_reserve_exact(len)
        .map_err(|_| BuildError::OutOfMemory)?;

    // This should not allocate after the successful `try_reserve_exact` call.
    bytes.resize(len, 0);

    Ok(bytes)
}

fn build_packet() -> Result<Packet, BuildError> {
    Ok(Packet {
        header: reserve_zeroed(16)?,
        body: reserve_zeroed(256)?,
    })
}

fn checked_packet_builder() {
    match build_packet() {
        Ok(packet) => {
            assert_eq!(packet.header.len(), 16);
            assert_eq!(packet.body.len(), 256);
        }
        Err(BuildError::OutOfMemory) => {}
    }
}

fn main() {
    strict_success();
    bounded_diagnostic_run();
    range_diagnostic_run();
    reproduce_one_failure_and_show_metadata();
    unstable_baseline_diagnostic();
    mishandled_oom_diagnostic();
}

fn strict_success() {
    let report = alloc_chaos::check(checked_packet_builder);

    print_report("strict exhaustive check", &report);
    report.assert_success();
}

fn bounded_diagnostic_run() {
    let report = alloc_chaos::Check::new()
        .max_failures(1)
        .stop_on_failure(true)
        .run(checked_packet_builder);

    print_report("bounded diagnostic run", &report);
    assert!(report.is_truncated());
    assert!(!report.is_success());
}

fn range_diagnostic_run() {
    let report = alloc_chaos::Check::new()
        .failure_range(1..2)
        .run(checked_packet_builder);

    print_report("selected failure range", &report);
    assert!(report.is_truncated());
    assert_eq!(report.tested_failures(), 1);
    assert!(!report.is_success());
}

fn reproduce_one_failure_and_show_metadata() {
    let full_report = alloc_chaos::check(checked_packet_builder);
    full_report.assert_success();

    let target = full_report
        .attempts()
        .first()
        .map(alloc_chaos::Attempt::target_allocation)
        .expect("packet builder should allocate during the baseline run");

    let report = alloc_chaos::Check::new()
        .only_failure(target)
        .run(checked_packet_builder);

    print_report("single-target reproduction", &report);
    print_allocation_metadata(&report);

    assert!(report.is_truncated());
    assert_eq!(report.tested_failures(), 1);
    assert!(report.attempts()[0].is_success());
    assert!(!report.is_success());
}

fn unstable_baseline_diagnostic() {
    let runs = AtomicUsize::new(0);

    let report = alloc_chaos::Check::new().stability_runs(2).run(|| {
        if runs.fetch_add(1, Ordering::SeqCst) == 0 {
            checked_packet_builder();
        }
    });

    print_report("unstable baseline", &report);
    assert!(!report.baseline_is_stable());
    assert!(report.attempts().is_empty());
    assert!(!report.is_success());
}

fn mishandled_oom_diagnostic() {
    with_quiet_expected_panics(|| {
        let report = alloc_chaos::Check::new()
            .max_failures(1)
            .run(|| match build_packet() {
                Ok(packet) => assert_eq!(packet.body.len(), 256),
                Err(BuildError::OutOfMemory) => panic!("allocation failure was not handled"),
            });

        print_report("mishandled OOM path", &report);
        assert!(report.first_failure().is_some());
        assert!(!report.is_success());
    });
}

fn print_report(title: &str, report: &alloc_chaos::Report) {
    println!("\n=== {title} ===");
    println!("{report}");
}

fn print_allocation_metadata(report: &alloc_chaos::Report) {
    for attempt in report.attempts() {
        if let Some(allocation) = attempt.injected_allocation() {
            println!(
                "metadata: target #{} -> {} size={} align={} new_size={:?}",
                allocation.index(),
                allocation.operation(),
                allocation.size(),
                allocation.align(),
                allocation.new_size(),
            );
        }
    }
}

fn with_quiet_expected_panics<R>(f: impl FnOnce() -> R) -> R {
    let previous_hook = panic::take_hook();
    panic::set_hook(Box::new(|_| {}));

    let result = panic::catch_unwind(panic::AssertUnwindSafe(f));

    panic::set_hook(previous_hook);

    match result {
        Ok(value) => value,
        Err(payload) => panic::resume_unwind(payload),
    }
}