use hashbrown::HashMap;
use std::collections::BTreeSet;
use std::{
hash::{BuildHasher, Hash, Hasher},
panic::{AssertUnwindSafe, catch_unwind},
sync::Mutex,
sync::atomic::{AtomicUsize, Ordering},
};
static PANIC_COUNTER: AtomicUsize = AtomicUsize::new(0);
static TEST_LOCK: Mutex<()> = Mutex::new(());
#[derive(Default)]
struct ZeroHasher;
impl Hasher for ZeroHasher {
fn finish(&self) -> u64 {
0
}
fn write(&mut self, _bytes: &[u8]) {}
}
#[derive(Clone, Default)]
struct PanicBuildHasher;
impl BuildHasher for PanicBuildHasher {
type Hasher = ZeroHasher;
fn build_hasher(&self) -> Self::Hasher {
if PANIC_COUNTER.fetch_sub(1, Ordering::SeqCst) == 0 {
panic!("panic in BuildHasher::build_hasher");
}
ZeroHasher
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
struct Key(u64);
type Map = HashMap<Key, u64, PanicBuildHasher>;
fn make_full_map() -> Map {
PANIC_COUNTER.store(!0, Ordering::SeqCst);
let mut map = HashMap::with_capacity_and_hasher(128, PanicBuildHasher);
for i in 0.. {
map.insert(Key(i), i);
if map.len() == map.capacity() {
return map;
}
}
unreachable!()
}
fn panics_silently(f: impl FnOnce()) -> bool {
let previous_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let panicked = catch_unwind(AssertUnwindSafe(f)).is_err();
std::panic::set_hook(previous_hook);
panicked
}
fn hashmap_reserve_survives_panicking_build_hasher_inner(count: usize) {
let mut map = make_full_map();
let original_len = map.len();
assert_eq!(
(map.len(), map.capacity()),
(224, 224),
"this minimized workload is tuned for the validated std/hashbrown layout"
);
for i in 1..114 {
assert_eq!(map.remove(&Key(i)), Some(i));
}
assert_eq!(
map.len(),
111,
"setup should leave the expected tombstone pattern"
);
PANIC_COUNTER.store(count, Ordering::SeqCst);
let reserve_panicked = panics_silently(|| {
map.reserve(1);
});
assert!(
reserve_panicked,
"the minimized workload should panic during the in-place rehash"
);
let mut expected_visible_keys: Vec<_> = map.keys().map(|&Key(i)| i).collect();
let visible_keys: Vec<_> = (0..original_len as u64)
.filter(|&i| map.get(&Key(i)).copied() == Some(i))
.collect();
expected_visible_keys.sort();
let iter_sample: Vec<_> = map.iter().take(8).map(|(k, v)| (k.0, *v)).collect();
let distinct_entries = iter_sample.iter().copied().collect::<BTreeSet<_>>();
assert_eq!(
map.len(),
expected_visible_keys.len(),
"the table length should stay coherent"
);
assert_eq!(
visible_keys, expected_visible_keys,
"the surviving keys should stay reachable after the caught panic"
);
assert_eq!(
distinct_entries.len(),
iter_sample.len(),
"the iterator sample should not contain duplicate entries after the caught panic"
);
}
#[test]
fn hashmap_reserve_survives_panicking_build_hasher() {
let _guard = TEST_LOCK.lock().unwrap_or_else(|e| e.into_inner());
if cfg!(miri) {
for i in [0, 50, 110] {
hashmap_reserve_survives_panicking_build_hasher_inner(i);
}
} else {
for i in 0..111 {
hashmap_reserve_survives_panicking_build_hasher_inner(i);
}
}
}