mod common;
use common::project::TempRustProject;
fn read_values(dir: &std::path::Path, label: &str) -> Vec<i64> {
let path = dir.join(label);
std::fs::read_to_string(&path)
.unwrap()
.lines()
.map(|l| l.parse().unwrap())
.collect()
}
#[test]
fn test_database_key_replays_failure() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().join("database");
std::fs::create_dir_all(&db_path).unwrap();
let db_str = db_path.to_str().unwrap().replace('\\', "/");
let test_code = format!(
r#"
use hegel::generators as gs;
use std::io::Write;
fn record_test_case(label: &str, n: i64) {{
let path = format!("{{}}/{{}}", std::env::var("VALUES_DIR").unwrap(), label);
let mut f = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.unwrap();
writeln!(f, "{{}}", n).unwrap();
}}
#[hegel::test(database = Some("{db_str}".to_string()))]
fn test_1(tc: hegel::TestCase) {{
let n: i64 = tc.draw(gs::integers());
record_test_case("test_1", n);
assert!(n < 1_000_000);
}}
#[hegel::test(database = Some("{db_str}".to_string()))]
fn test_2(tc: hegel::TestCase) {{
let n: i64 = tc.draw(gs::integers());
record_test_case("test_2", n);
assert!(n < 1_000_000);
}}
"#
);
let values_path = temp_dir.path().join("values");
std::fs::create_dir_all(&values_path).unwrap();
let project = TempRustProject::new()
.test_file("integration.rs", &test_code)
.env("VALUES_DIR", values_path.to_str().unwrap())
.expect_failure("FAILED");
project.cargo_test(&["test_1"]);
let shrunk_value = *read_values(&values_path, "test_1").last().unwrap();
assert_eq!(shrunk_value, 1_000_000);
std::fs::remove_file(values_path.join("test_1")).unwrap();
project.cargo_test(&["test_1"]);
let values = read_values(&values_path, "test_1");
assert_eq!(
values[0], shrunk_value,
"Expected to replay shrunk test case {shrunk_value} first, got {}",
values[0]
);
project.cargo_test(&["test_2"]);
let values = read_values(&values_path, "test_2");
assert_ne!(values[0], shrunk_value);
}
mod replay_logic {
use std::sync::{Arc, Mutex};
use hegel::generators as gs;
use hegel::{Hegel, Settings, TestCase};
fn run_expecting_failure<F>(f: F)
where
F: FnOnce() + std::panic::UnwindSafe,
{
let result = std::panic::catch_unwind(f);
assert!(result.is_err(), "expected the test to fail");
}
#[test]
fn test_does_not_shrink_on_replay() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();
let call_count: Arc<Mutex<u64>> = Arc::new(Mutex::new(0));
let is_first: Arc<Mutex<bool>> = Arc::new(Mutex::new(true));
let last: Arc<Mutex<Option<Vec<i64>>>> = Arc::new(Mutex::new(None));
let run = || {
let call_count = Arc::clone(&call_count);
let is_first = Arc::clone(&is_first);
let last = Arc::clone(&last);
let db_path = db_path.clone();
run_expecting_failure(std::panic::AssertUnwindSafe(move || {
Hegel::new(move |tc: TestCase| {
let ls: Vec<i64> =
tc.draw(gs::vecs(gs::integers::<i64>()).min_size(3).unique(true));
{
let mut first = is_first.lock().unwrap();
let mut last = last.lock().unwrap();
if *first && last.is_some() {
assert_eq!(&ls, last.as_ref().unwrap());
}
*first = false;
*last = Some(ls);
}
*call_count.lock().unwrap() += 1;
panic!("boom");
})
.settings(
Settings::new()
.database(Some(db_path))
.derandomize(false)
.test_cases(1000),
)
.__database_key("test_does_not_shrink_on_replay".to_string())
.run();
}));
};
run();
assert!(last.lock().unwrap().is_some());
*call_count.lock().unwrap() = 0;
*is_first.lock().unwrap() = true;
run();
assert_eq!(*call_count.lock().unwrap(), 2);
}
#[test]
fn test_will_always_shrink_if_previous_example_does_not_replay() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();
let good: Arc<Mutex<std::collections::HashSet<i64>>> =
Arc::new(Mutex::new(std::collections::HashSet::new()));
let last: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
for i in 0..20 {
let good_cl = Arc::clone(&good);
let last_cl = Arc::clone(&last);
let db_path_cl = db_path.clone();
run_expecting_failure(std::panic::AssertUnwindSafe(move || {
Hegel::new(move |tc: TestCase| {
let n: i64 = tc.draw(gs::integers::<i64>().min_value(0));
if !good_cl.lock().unwrap().contains(&n) {
*last_cl.lock().unwrap() = Some(n);
panic!("boom");
}
})
.settings(
Settings::new()
.database(Some(db_path_cl))
.derandomize(false)
.test_cases(1000),
)
.__database_key("test_will_always_shrink".to_string())
.run();
}));
assert_eq!(*last.lock().unwrap(), Some(i));
good.lock().unwrap().insert(last.lock().unwrap().unwrap());
}
}
#[test]
fn test_will_shrink_if_the_previous_example_does_not_look_right() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();
let last: Arc<Mutex<Option<i64>>> = Arc::new(Mutex::new(None));
let first_test: Arc<Mutex<bool>> = Arc::new(Mutex::new(true));
let run = || {
let last = Arc::clone(&last);
let first_test = Arc::clone(&first_test);
let db_path = db_path.clone();
run_expecting_failure(std::panic::AssertUnwindSafe(move || {
Hegel::new(move |tc: TestCase| {
let m: i64 = tc.draw(gs::integers::<i64>());
*last.lock().unwrap() = Some(m);
if *first_test.lock().unwrap() {
tc.draw(gs::integers::<i64>());
assert!(m < 10000);
} else {
panic!("boom");
}
})
.settings(Settings::new().database(Some(db_path)).derandomize(false))
.__database_key("test_will_shrink_misaligned".to_string())
.run();
}));
};
run();
let val = *last.lock().unwrap();
assert!(val.is_some());
assert!(val.unwrap() > 0);
*first_test.lock().unwrap() = false;
run();
assert_eq!(*last.lock().unwrap(), Some(0));
}
fn db_file_count(dir: &std::path::Path) -> usize {
let entries = match std::fs::read_dir(dir) {
Ok(d) => d,
Err(_) => return 0,
};
let mut n = 0;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
n += db_file_count(&path);
} else {
n += 1;
}
}
n
}
#[test]
fn test_failure_persisted_immediately_when_discovered() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();
let db_path_for_check = db_path.clone();
let observed_at: Arc<Mutex<Option<u64>>> = Arc::new(Mutex::new(None));
let observed_cl = Arc::clone(&observed_at);
let calls: Arc<Mutex<u64>> = Arc::new(Mutex::new(0));
let calls_cl = Arc::clone(&calls);
let db_root = std::path::PathBuf::from(&db_path_for_check);
run_expecting_failure(std::panic::AssertUnwindSafe(move || {
Hegel::new(move |tc: TestCase| {
{
let mut c = calls_cl.lock().unwrap();
*c += 1;
if observed_cl.lock().unwrap().is_none() && db_file_count(&db_root) > 0 {
*observed_cl.lock().unwrap() = Some(*c);
}
}
let n: i64 = tc.draw(gs::integers::<i64>().min_value(0));
assert!(n < 1_000_000);
})
.settings(
Settings::new()
.database(Some(db_path))
.derandomize(false)
.test_cases(1000),
)
.__database_key("test_failure_persisted_immediately_when_discovered".to_string())
.run();
}));
let total_calls = *calls.lock().unwrap();
assert!(
total_calls > 1,
"expected the run to make more than one test-case call, got {total_calls}"
);
let observed = *observed_at.lock().unwrap();
assert!(
observed.is_some_and(|o| o < total_calls),
"Database was not populated during the run \
(observed_at={observed:?}, total_calls={total_calls}). \
Killing the runner mid-shrink would lose the failure."
);
}
#[test]
fn test_intermediate_shrinks_update_db_during_run() {
let temp_dir = tempfile::TempDir::new().unwrap();
let db_path = temp_dir.path().to_str().unwrap().to_string();
let db_root = std::path::PathBuf::from(&db_path);
let db_root_cl = db_root.clone();
let snapshots: Arc<Mutex<std::collections::HashSet<(usize, usize)>>> =
Arc::new(Mutex::new(std::collections::HashSet::new()));
let snapshots_cl = Arc::clone(&snapshots);
let calls: Arc<Mutex<u64>> = Arc::new(Mutex::new(0));
let calls_cl = Arc::clone(&calls);
const BOUNDARY: i64 = 1_234_567;
run_expecting_failure(std::panic::AssertUnwindSafe(move || {
Hegel::new(move |tc: TestCase| {
{
*calls_cl.lock().unwrap() += 1;
let (count, total_bytes) = db_summary(&db_root_cl);
snapshots_cl.lock().unwrap().insert((count, total_bytes));
}
let n: i64 = tc.draw(gs::integers::<i64>().min_value(0));
let v: Vec<i64> = tc.draw(gs::vecs(gs::integers::<i64>()).min_size(0));
let _ = v;
assert!(n < BOUNDARY);
})
.settings(
Settings::new()
.database(Some(db_path))
.derandomize(false)
.test_cases(1000),
)
.__database_key("test_intermediate_shrinks_update_db_during_run".to_string())
.run();
}));
let snaps = snapshots.lock().unwrap().clone();
let total = *calls.lock().unwrap();
assert!(total > 1, "expected multiple test-body calls, got {total}");
let distinct_non_empty = snaps.iter().filter(|&&(_, b)| b > 0).count();
assert!(
distinct_non_empty >= 2,
"Only saw {distinct_non_empty} distinct non-empty DB \
state(s) across {total} test-body calls (snapshots: \
{snaps:?}). The shrinker is not persisting intermediate \
improvements — killing it mid-shrink would lose progress."
);
}
fn db_summary(root: &std::path::Path) -> (usize, usize) {
let entries = match std::fs::read_dir(root) {
Ok(d) => d,
Err(_) => return (0, 0),
};
let mut count = 0;
let mut total = 0;
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let (c, t) = db_summary(&path);
count += c;
total += t;
} else if let Ok(meta) = std::fs::metadata(&path) {
count += 1;
total += meta.len() as usize;
}
}
(count, total)
}
}