#[test]
fn run_returns_ok_when_property_holds() -> noprop::TestResult {
noprop::Runner::new(0xDEAD_BEEF).run(16, |ctx| {
let x = noprop::sample_u32(ctx);
assert_eq!(x, x);
Ok(())
})?;
Ok(())
}
#[test]
fn runner_display_includes_seed_and_every_stats_field() {
let mut runner = noprop::Runner::new(0xDEAD_BEEF);
runner
.run(3, |ctx| {
let _ = noprop::sample_u32(ctx);
Ok(())
})
.expect("run must succeed");
let s = format!("{runner}");
assert!(
s.contains("0x00000000deadbeef"),
"seed must appear in {{:#018x}} form: {s}"
);
for label in ["accepted: 3", "rejected: 0", "total_samples: 3"] {
assert!(
s.contains(label),
"missing '{label}' in Display output: {s}"
);
}
}
#[test]
fn test_result_chains_config_and_runner_via_question_mark() {
let result: noprop::TestResult = (|| {
let seed = noprop::seed_from_env_or_time("NOPROP_E2E_ABSOLUTELY_UNSET_SEED_7C4A_1B2D")?;
noprop::Runner::new(seed).run(4, |_ctx| Ok(()))?;
Ok(())
})();
assert!(
result.is_ok(),
"fallback config + passing property must succeed"
);
}
#[test]
fn run_returns_err_on_failed_assertion() {
let result = noprop::Runner::new(0x1234).run(64, |ctx| {
let x = noprop::sample_u32(ctx);
assert_eq!(x, 0, "expected zero, got {x}");
Ok(())
});
let err = result.expect_err("expected Err, got Ok");
assert_eq!(err.seed(), 0x1234);
assert!(err.case_index() < 64);
assert_eq!(
err.kind(),
noprop::RunErrorKind::PropertyFailure,
"a property failure must be classified as PropertyFailure"
);
}
#[test]
fn run_budget_is_per_call() {
let mut runner = noprop::Runner::new(0xABCD);
let failing = |ctx: &mut noprop::TestCaseContext| {
let _ = noprop::sample_u32(ctx);
if true {
panic!("deterministic failure");
}
Ok(())
};
let err = runner.run(16, failing).expect_err("first run must fail");
assert!(
format!("{err}").contains("Runner::new(0x000000000000abcd).run(16, |ctx| ...)"),
"hint must carry the first run's budget: {err}"
);
let err = runner.run(32, failing).expect_err("second run must fail");
assert!(
format!("{err}").contains("Runner::new(0x000000000000abcd).run(32, |ctx| ...)"),
"hint must carry the second run's budget, not the first: {err}"
);
}
#[test]
fn too_many_rejections_is_classified_by_kind() {
let err = noprop::Runner::new(1)
.run(8, |ctx| {
ctx.reject_case();
})
.expect_err("always-rejecting property must hit the rejection cap");
assert_eq!(
err.kind(),
noprop::RunErrorKind::TooManyRejections,
"rejection-cap exhaustion must be classified as TooManyRejections"
);
}
#[test]
fn same_seed_reproduces_same_failure() {
let seed = 0xABCD_1234_5678_9ABC;
let run = || {
noprop::Runner::new(seed).run(32, |ctx| {
let x = noprop::sample_u32(ctx);
assert!(x < 0x8000_0000, "high bit set: {x:#010x}");
Ok(())
})
};
let a = run();
let b = run();
match (a, b) {
(Err(a), Err(b)) => {
assert_eq!(a.seed(), b.seed());
assert_eq!(a.case_index(), b.case_index());
}
other => panic!("expected two matching Err results, got {other:?}"),
}
}
#[test]
fn zero_cases_returns_ok_without_invoking_property() -> noprop::TestResult {
let invoked = std::cell::Cell::new(false);
noprop::Runner::new(0).run(0, |_ctx| {
invoked.set(true);
Ok(())
})?;
assert!(
!invoked.get(),
"property should not be invoked when cases is 0"
);
Ok(())
}
#[test]
fn error_debug_output_contains_seed_and_case() {
let seed = 0xFEED_FACE_C0DE_BABE;
let result = noprop::Runner::new(seed).run(1, |_ctx| {
panic!("boom");
});
let err = result.expect_err("expected panic to become Err");
let debug = format!("{err:?}");
assert!(debug.contains("0xfeedfacec0debabe"), "debug: {debug}");
assert!(debug.contains("case_index: 0"), "debug: {debug}");
assert!(debug.contains("boom"), "debug: {debug}");
}
#[test]
fn subsequent_cases_are_skipped_after_failure() {
let count = std::cell::Cell::new(0usize);
let _ = noprop::Runner::new(0).run(100, |_ctx| {
let n = count.get() + 1;
count.set(n);
if n == 3 {
panic!("stop here");
}
Ok(())
});
assert_eq!(count.get(), 3);
}
#[test]
fn generated_values_are_recorded_in_error() {
let result = noprop::Runner::new(42).run(1, |ctx| {
let x = noprop::sample_u32(ctx);
let b = noprop::sample_bool(ctx);
let c = noprop::sample_ascii_char(ctx);
panic!("forced failure with x={x}, b={b}, c={c:?}");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 3, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "u32");
assert_eq!(generated[1].type_name(), "bool");
assert_eq!(generated[2].type_name(), "char");
assert!(!generated[0].is_elided());
assert!(
generated[0]
.value_repr()
.expect("a u32 sample must have a value repr")
.parse::<u32>()
.is_ok()
);
assert!(generated[0].location().file().ends_with("e2e.rs"));
}
#[test]
fn generated_trace_dedups_same_location_run() {
let result = noprop::Runner::new(1).run(1, |ctx| {
for _ in 0..100 {
let _ = noprop::sample_u8(ctx);
}
panic!("fail after loop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 17, "generated: {generated:?}");
for (i, entry) in generated.iter().take(8).enumerate() {
assert!(!entry.is_elided(), "entry {i} should be a value");
assert_eq!(entry.type_name(), "u8");
}
assert!(generated[8].is_elided());
assert_eq!(generated[8].elided_count(), Some(84));
for (i, entry) in generated.iter().skip(9).enumerate() {
assert!(!entry.is_elided(), "entry {} should be a value", i + 9);
assert_eq!(entry.type_name(), "u8");
}
}
#[test]
fn generated_trace_does_not_dedup_below_head_plus_tail() {
let result = noprop::Runner::new(1).run(1, |ctx| {
for _ in 0..16 {
let _ = noprop::sample_u8(ctx);
}
panic!("fail after loop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 16, "generated: {generated:?}");
assert!(generated.iter().all(|e| !e.is_elided()));
}
#[test]
fn generated_trace_treats_different_locations_independently() {
let result = noprop::Runner::new(1).run(1, |ctx| {
for _ in 0..3 {
let _ = noprop::sample_u8(ctx);
}
for _ in 0..100 {
let _ = noprop::sample_u16(ctx);
}
panic!("fail after loops");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 20, "generated: {generated:?}");
for entry in generated.iter().take(3) {
assert_eq!(entry.type_name(), "u8");
assert!(!entry.is_elided());
}
for entry in generated.iter().skip(3).take(8) {
assert_eq!(entry.type_name(), "u16");
assert!(!entry.is_elided());
}
assert!(generated[11].is_elided());
assert_eq!(generated[11].elided_count(), Some(84));
for entry in generated.iter().skip(12) {
assert_eq!(entry.type_name(), "u16");
assert!(!entry.is_elided());
}
}
#[test]
fn generated_trace_is_isolated_per_case() {
let case = std::cell::Cell::new(0usize);
let result = noprop::Runner::new(7).run(5, |ctx| {
let c = case.get();
if c == 0 {
let _ = noprop::sample_u64(ctx);
} else {
let _ = noprop::sample_u16(ctx);
panic!("fail on case {c}");
}
case.set(c + 1);
Ok(())
});
let err = result.expect_err("expected Err");
assert_eq!(err.case_index(), 1);
let generated = err.generated();
assert_eq!(generated.len(), 1, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "u16");
}
#[test]
fn error_debug_output_includes_generated_values() {
let result = noprop::Runner::new(42).run(1, |ctx| {
let _ = noprop::sample_u32(ctx);
panic!("boom");
});
let err = result.expect_err("expected Err");
let debug = format!("{err:?}");
assert!(debug.contains("generated: ["), "debug: {debug}");
assert!(debug.contains("- u32 ="), "debug: {debug}");
assert!(debug.contains("(at "), "debug: {debug}");
assert!(debug.contains("e2e.rs:"), "debug: {debug}");
}
#[test]
fn sample_bytes_records_the_array_as_one_trace_entry() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let _key: [u8; 16] = noprop::sample_bytes(ctx);
panic!("stop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 1);
assert_eq!(generated[0].type_name(), "[u8; 16]");
}
#[test]
fn sample_bytes_vec_records_the_vec_as_one_trace_entry() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let _buf = noprop::sample_bytes_vec(ctx, 42);
panic!("stop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 1);
assert_eq!(generated[0].type_name(), "alloc::vec::Vec<u8>");
}
#[test]
fn error_display_output_includes_generated_values() {
let result = noprop::Runner::new(42).run(1, |ctx| {
let _ = noprop::sample_u8(ctx);
panic!("boom");
});
let err = result.expect_err("expected Err");
let display = format!("{err}");
assert!(display.contains("Generated values:"), "display: {display}");
assert!(display.contains("- u8 ="), "display: {display}");
}
#[test]
fn sample_usize_in_records_only_the_chosen_value() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let _v = noprop::sample_usize_in(ctx, 0..7);
panic!("stop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 1, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "usize");
let repr = generated[0]
.value_repr()
.expect("a usize sample must have a value repr");
let v: usize = repr.parse().expect("value repr must be a usize");
assert!(v < 7);
}
#[test]
fn sample_ratio_records_only_the_chosen_bool() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let _b = noprop::sample_ratio(ctx, noprop::Ratio::one_nth(3));
panic!("stop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 1, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "bool");
}
#[test]
fn sample_with_boundaries_records_bool_and_value() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let _v = noprop::sample_with_boundaries(
ctx,
&[0, 1500, u32::MAX],
noprop::Ratio::one_nth(10),
noprop::sample_u32,
);
panic!("stop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 2, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "bool");
assert_eq!(generated[1].type_name(), "u32");
}
#[test]
fn sample_with_boundaries_is_reproducible_across_runs() {
let seed = 0xBAD_CAFE_1234_5678u64;
let run = || {
noprop::Runner::new(seed).run(64, |ctx| {
let v = noprop::sample_with_boundaries(
ctx,
&[u32::MAX],
noprop::Ratio::one_nth(2),
noprop::sample_u32,
);
assert!(v != u32::MAX, "hit boundary");
Ok(())
})
};
let a = run().expect_err("expected Err");
let b = run().expect_err("expected Err");
assert_eq!(a.case_index(), b.case_index());
}
#[test]
fn sample_weighted_index_records_only_the_chosen_index() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let _idx = noprop::sample_weighted_index(ctx, &[1, 2, 3, 4]);
panic!("stop");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 1, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "usize");
let repr = generated[0]
.value_repr()
.expect("a usize sample must have a value repr");
let idx: usize = repr.parse().expect("value repr must be a usize");
assert!(idx < 4);
}
#[test]
fn selection_primitives_are_reproducible_across_runs() {
let seed = 0xC0FF_EE99_1234_5678u64;
let run = || {
noprop::Runner::new(seed).run(64, |ctx| {
let idx = noprop::sample_weighted_index(ctx, &[1, 1, 1, 1]);
let _n = noprop::sample_usize_in(ctx, 0..=100);
let flip = noprop::sample_ratio(ctx, noprop::Ratio::one_nth(4));
assert!(!(flip && idx == 0), "hit forbidden pattern");
Ok(())
})
};
let a = run().expect_err("expected Err");
let b = run().expect_err("expected Err");
assert_eq!(a.case_index(), b.case_index());
}
#[test]
fn sample_with_rejection_returns_first_accepted_value() -> noprop::TestResult {
noprop::Runner::new(1).run(8, |ctx| {
let v = noprop::sample_with_rejection(ctx, 4, |ctx| {
let x = noprop::sample_u32(ctx);
x.is_multiple_of(2).then_some(x)
});
assert!(v.is_multiple_of(2));
Ok(())
})?;
Ok(())
}
#[test]
fn reject_case_retries_iteration_without_counting_it() -> noprop::TestResult {
let attempts = std::cell::Cell::new(0usize);
let accepted = std::cell::Cell::new(0usize);
let target_accepts = 4;
noprop::Runner::new(42).run(target_accepts, |ctx| {
let n = attempts.get();
attempts.set(n + 1);
if n < 3 {
ctx.reject_case();
}
accepted.set(accepted.get() + 1);
Ok(())
})?;
assert_eq!(accepted.get(), target_accepts);
assert_eq!(attempts.get(), 3 + target_accepts);
Ok(())
}
#[test]
fn always_reject_hits_too_many_rejections_and_reports_case_index_zero() {
let result = noprop::Runner::new(7).run(8, |ctx| {
ctx.reject_case();
});
let err = result.expect_err("expected TooManyRejections");
assert_eq!(err.seed(), 7);
assert_eq!(err.case_index(), 0);
let debug = format!("{err:?}");
assert!(
debug.contains("too_many_rejections"),
"debug output should mention too_many_rejections: {debug}"
);
let display = format!("{err}");
assert!(
display.contains("too many rejections"),
"display output should mention too many rejections: {display}"
);
}
#[test]
fn always_reject_is_reproducible_from_seed() {
let seed = 0xBEEF_1234u64;
let run = || {
noprop::Runner::new(seed).run(4, |ctx| {
ctx.reject_case();
})
};
let a = run().expect_err("expected Err");
let b = run().expect_err("expected Err");
assert_eq!(a.seed(), b.seed());
assert_eq!(a.case_index(), b.case_index());
}
#[test]
fn too_many_rejections_generated_carries_last_rejected_iteration_trace() {
let err = noprop::Runner::new(0xB5_B5_B5_B5)
.run(1, |ctx| {
let _marker = noprop::sample_u32(ctx);
ctx.reject_case();
})
.expect_err("always-rejecting property must hit the rejection cap");
assert_eq!(err.kind(), noprop::RunErrorKind::TooManyRejections);
assert_eq!(
err.generated().len(),
1,
"trace must carry the one sample_u32 entry from the last rejected iteration"
);
}
#[test]
fn rejection_state_overrides_user_catch_returning_ok() -> noprop::TestResult {
let attempts = std::cell::Cell::new(0usize);
noprop::Runner::new(1).run(2, |ctx| {
let n = attempts.get();
attempts.set(n + 1);
if n == 0 {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ctx.reject_case();
}));
}
Ok(())
})?;
assert_eq!(attempts.get(), 3);
Ok(())
}
#[test]
fn rejection_state_overrides_user_catch_and_reraise() -> noprop::TestResult {
let attempts = std::cell::Cell::new(0usize);
noprop::Runner::new(1).run(1, |ctx| {
let n = attempts.get();
attempts.set(n + 1);
if n == 0 {
let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
ctx.reject_case();
}));
std::panic::panic_any("re-raised, should be dropped");
}
Ok(())
})?;
assert_eq!(attempts.get(), 2);
Ok(())
}
#[test]
fn sample_with_rejection_all_rejected_triggers_iteration_rejection() -> noprop::TestResult {
let outer_attempts = std::cell::Cell::new(0usize);
noprop::Runner::new(1).run(2, |ctx| {
let n = outer_attempts.get();
outer_attempts.set(n + 1);
if n < 2 {
let _: u32 = noprop::sample_with_rejection(ctx, 4, |_ctx| None);
unreachable!("sample_with_rejection exhaustion should unwind");
}
Ok(())
})?;
assert_eq!(outer_attempts.get(), 4);
Ok(())
}
#[test]
fn sample_string_records_one_entry_per_call() {
let result = noprop::Runner::new(11).run(1, |ctx| {
let a = noprop::sample_string(ctx, 4);
let b = noprop::sample_ascii_string(ctx, 4);
let c = noprop::sample_ascii_printable_string(ctx, 4);
panic!("stop with a={a:?} b={b:?} c={c:?}");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 3, "generated: {generated:?}");
for entry in generated {
assert_eq!(entry.type_name(), "alloc::string::String");
assert!(entry.location().file().ends_with("e2e.rs"));
let repr = entry.value_repr().expect("value entry");
assert!(
repr.starts_with('"') && repr.ends_with('"'),
"expected quoted Debug form, got {repr:?}"
);
}
}
#[test]
fn sample_floats_record_type_and_value() {
let result = noprop::Runner::new(5).run(1, |ctx| {
let a = noprop::sample_f32(ctx);
let b = noprop::sample_f64(ctx);
panic!("stop with a={a} b={b}");
});
let err = result.expect_err("expected Err");
let generated = err.generated();
assert_eq!(generated.len(), 2, "generated: {generated:?}");
assert_eq!(generated[0].type_name(), "f32");
assert_eq!(generated[1].type_name(), "f64");
let a_repr: f32 = generated[0]
.value_repr()
.expect("an f32 sample must have a value repr")
.parse()
.expect("value repr must be an f32");
let b_repr: f64 = generated[1]
.value_repr()
.expect("an f64 sample must have a value repr")
.parse()
.expect("value repr must be an f64");
assert!(a_repr.is_finite());
assert!(b_repr.is_finite());
assert!(generated[0].location().file().ends_with("e2e.rs"));
}
#[test]
fn failure_display_contains_reproduce_line_that_reproduces_the_same_failure() {
let seed = 0x5EED_1EAD_BEEF_C0DEu64;
let target = std::cell::Cell::new(0usize);
let run = || {
target.set(0);
noprop::Runner::new(seed).run(128, |_ctx| {
let n = target.get();
target.set(n + 1);
if n >= 3 {
panic!("boom at iteration {n}");
}
Ok(())
})
};
let err = run().expect_err("expected panic to become Err");
assert_eq!(err.case_index(), 3);
let display = format!("{err}");
let expected_cases = 128;
let hint = format!(
"reproduce with: noprop::Runner::new({:#018x}).run({expected_cases}, |ctx| ...)",
err.seed(),
);
assert!(
display.contains(&hint),
"Display should contain reproduce hint {hint:?}, got:\n{display}"
);
let debug = format!("{err:?}");
let debug_hint = format!(
"reproduce: noprop::Runner::new({:#018x}).run({expected_cases}, |ctx| ...)",
err.seed(),
);
assert!(
debug.contains(&debug_hint),
"Debug should contain reproduce hint {debug_hint:?}, got:\n{debug}"
);
let target = std::cell::Cell::new(0usize);
let replay = noprop::Runner::new(err.seed()).run(expected_cases, |_ctx| {
let n = target.get();
target.set(n + 1);
if n >= 3 {
panic!("boom at iteration {n}");
}
Ok(())
});
let replayed = replay.expect_err("hint cases must reproduce the failure");
assert_eq!(replayed.seed(), err.seed());
assert_eq!(replayed.case_index(), err.case_index());
}
#[test]
fn stats_success_reports_accepted_cases_and_zero_rejections() -> noprop::TestResult {
let mut runner = noprop::Runner::new(0xDEAD_BEEF);
runner.run(10, |ctx| {
let _a = noprop::sample_u32(ctx);
let _b = noprop::sample_u32(ctx);
Ok(())
})?;
let stats = runner.stats();
assert_eq!(stats.accepted_cases, 10);
assert_eq!(stats.rejected_cases, 0);
assert_eq!(stats.total_samples, 20);
Ok(())
}
#[test]
fn stats_counts_reject_case_unwinds() -> noprop::TestResult {
use std::cell::Cell;
let counter = Cell::new(0usize);
let mut runner = noprop::Runner::new(1);
runner.run(3, |ctx| {
let n = counter.get();
counter.set(n + 1);
if n < 2 {
ctx.reject_case();
}
Ok(())
})?;
let stats = runner.stats();
assert_eq!(stats.accepted_cases, 3);
assert_eq!(stats.rejected_cases, 2);
Ok(())
}
#[test]
fn stats_counts_sample_with_rejection_exhaustion_as_rejected_iteration() {
let mut runner = noprop::Runner::new(1);
let result = runner.run(1, |ctx| {
let _x = noprop::sample_with_rejection(ctx, 4, |ctx| {
let u = noprop::sample_u32(ctx);
(u == 0).then_some(u)
});
Ok(())
});
let err = result.expect_err("all-reject property must fail");
assert_eq!(err.stats(), runner.stats());
let s = runner.stats();
assert_eq!(s.accepted_cases, 0);
assert!(
s.rejected_cases > 0,
"expected some rejected cases, got {}",
s.rejected_cases
);
}
#[test]
fn stats_is_deterministic_per_seed() -> noprop::TestResult {
let mut a = noprop::Runner::new(42);
a.run(5, |ctx| {
let _ = noprop::sample_u32(ctx);
let _ = noprop::sample_bool(ctx);
Ok(())
})?;
let mut b = noprop::Runner::new(42);
b.run(5, |ctx| {
let _ = noprop::sample_u32(ctx);
let _ = noprop::sample_bool(ctx);
Ok(())
})?;
assert_eq!(a.stats(), b.stats());
Ok(())
}
#[test]
fn stats_on_failure_reports_progress_up_to_failing_case() {
use std::cell::Cell;
let counter = Cell::new(0usize);
let err = noprop::Runner::new(7)
.run(10, |_ctx| {
let n = counter.get();
counter.set(n + 1);
if n == 4 {
panic!("boom at iteration {n}");
}
Ok(())
})
.expect_err("closure must fail at case 4");
let stats = err.stats();
assert_eq!(stats.accepted_cases, 4);
assert_eq!(stats.rejected_cases, 0);
assert_eq!(err.case_index(), stats.accepted_cases);
}