name = "slotmap"
description = """
ETNA workload for the Rust `slotmap` crate. Each variant re-introduces
one historical bug-fix into a fresh patched branch and pairs it with a
framework-neutral property, four PBT adapters, and a deterministic
witness test.
"""
language = "rust"
strategies = ["proptest", "quickcheck", "crabcheck", "hegel"]
crate = "slotmap"
base_commit = "97b73db42eb9d8e4aa8784f73a312713aaf557d0"

[[tasks]]
mutations = ["kd_debug_null_format_226c22a_1"]

[tasks.source]
repo = "https://github.com/orlp/slotmap"
commits = ["226c22ab07423fd04818ddbcd3a1f594729206f8"]
commit_subjects = ["Update Debug impl for KeyData to nicely format nulls. (#141)"]
summary = "`KeyData::fmt` printed the raw `{idx}v{version}` pair, so the null sentinel (`idx = u32::MAX`, `version = 1`) rendered as `\"4294967295v1\"` in debug output instead of the expected `\"null\"`. The fix branches on `is_null()` and emits the `\"null\"` string for the sentinel key."

[tasks.injection]
kind = "marauders"
files = ["src/lib.rs"]
locations = [{ file = "src/lib.rs", line = 301 }]

[tasks.bug]
short_name = "kd_debug_null_format"
invariant = """
the `Debug` implementation of `KeyData` must render the null key (`KeyData::null()`, i.e. `idx == u32::MAX`) as the string `"null"`; non-null keys must render as `"{idx}v{version}"`.
"""
how_triggered = """
the fix replaced the raw `write!(f, "{}v{}", self.idx, self.version.get())` with an `is_null()` branch that emits `"null"` for the sentinel key. The variant restores the single `write!` call, so `format!("{:?}", KeyData::null())` now yields `"4294967295v1"` (the null sentinel `(idx=u32::MAX, version=1)` formatted raw) instead of `"null"`. The property calls `format!("{:?}", kd)` and compares against the expected string based on `kd.is_null()`.
"""

[[tasks.tasks]]
property = "KeyDataDebugFormat"
witnesses = [
  { test_fn = "witness_key_data_debug_format_case_null_default" },
  { test_fn = "witness_key_data_debug_format_case_null_any_version_seed" },
  { test_fn = "witness_key_data_debug_format_case_non_null" },
]

[[tasks]]
mutations = ["sparse_secondary_null_insert_guard_2d49bbd_1"]

[tasks.source]
repo = "https://github.com/orlp/slotmap"
commits = ["2d49bbd4fdb6c329c49ba2ac13a929198760364f"]
commit_subjects = ["Reject insertion of null keys into secondary maps."]
summary = "`SparseSecondaryMap::insert` accepted the null key, storing it in the backing `HashMap<u32, Slot<V>>`; subsequent queries for the null key then returned the inserted value, violating the documented \"null keys never map to anything\" invariant. The fix adds an early `if key.is_null() { return None; }` guard."

[tasks.injection]
kind = "marauders"
files = ["src/sparse_secondary.rs"]
locations = [{ file = "src/sparse_secondary.rs", line = 273 }]

[tasks.bug]
short_name = "sparse_secondary_null_insert_guard"
invariant = "`SparseSecondaryMap::insert(null_key, v)` must be a no-op — returning `None`, leaving `len()` at 0, and leaving `contains_key(null_key)` and `get(null_key)` both false/None."
how_triggered = "the fix added an early `if key.is_null() { return None; }` guard at the top of `SparseSecondaryMap::insert`. The variant deletes that guard, so the null key's `KeyData { idx: u32::MAX, version: NonZeroU32::new(1) }` is stored in the backing `HashMap<u32, Slot<V>>`. The property inserts the default null key, then observes that the map grew to `len() == 1`, `contains_key` returns true, and `get` returns `Some(&value)` — each of which is a `Fail`."

[[tasks.tasks]]
property = "SparseSecondaryNullInsertIgnored"
witnesses = [
  { test_fn = "witness_sparse_secondary_null_insert_ignored_case_small_value" },
  { test_fn = "witness_sparse_secondary_null_insert_ignored_case_zero_value" },
]

[[tasks]]
mutations = ["vacant_entry_insert_no_double_count_bd32b7d_1"]

[tasks.source]
repo = "https://github.com/orlp/slotmap"
commits = ["bd32b7d5ee96beef3cc3299b79ea42c340f5ad28"]
commit_subjects = ["Fixed possible memory leak in secondary::VacantEntry::insert."]
summary = "`VacantEntry::insert` unconditionally incremented `num_elems` and `mem::forget`-ed the replaced slot, so inserting over a stale-occupied slot both leaked the previous value and double-counted it in `len()`. The fix matches on the previous slot state and only increments the counter when the slot was truly vacant."

[tasks.injection]
kind = "marauders"
files = ["src/secondary.rs"]
locations = [{ file = "src/secondary.rs", line = 1271 }]

[tasks.bug]
short_name = "vacant_entry_insert_no_double_count"
invariant = "after `sec.entry(k_new).or_insert(v)` on a `SecondaryMap` where the underlying slot is stale-occupied from an older key version, `sec.len()` must reflect exactly one live element (the newly inserted one), not two. The stale value's drop is owed."
how_triggered = "the fix `match`es the replaced slot and only bumps `num_elems` if the prior slot was `Vacant`, correctly dropping any `Occupied` slot it replaces. The variant unconditionally increments `num_elems` and `core::mem::forget`s the old slot, so `len()` becomes 2 after inserting via a fresh `VacantEntry` over a stale-occupied slot. The property inserts a key, removes it without dropping the backing secondary slot (another key with the same `idx` but new `version` makes the old secondary slot stale-occupied), then inserts via `entry(...).or_insert(...)` and checks `sec.len() == 1`."

[[tasks.tasks]]
property = "SecondaryEntryDoesNotDoubleCount"
witnesses = [
  { test_fn = "witness_secondary_entry_does_not_double_count_case_basic" },
  { test_fn = "witness_secondary_entry_does_not_double_count_case_same_value" },
]

[[tasks]]
mutations = ["slot_clone_from_drops_occupied_5c3fb22_1"]

[tasks.source]
repo = "https://github.com/orlp/slotmap"
commits = ["5c3fb220071f935f77c4ef33dd4d0e745f28d2d6"]
commit_subjects = ["Fix memory leak and panic issue in clone_from. (#137)"]
summary = "`SlotMap::clone_from` overwrote an Occupied destination slot with a Vacant source slot by writing a new `SlotUnion { next_free }` directly into the destination union, leaving the old `ManuallyDrop<T>` never dropped. The fix adds an explicit `(OccupiedMut, Vacant)` arm that calls `ManuallyDrop::drop` before reusing the slot."

[tasks.injection]
kind = "marauders"
files = ["src/basic.rs"]
locations = [{ file = "src/basic.rs", line = 94 }]

[tasks.bug]
short_name = "slot_clone_from_drops_occupied"
invariant = "when `SlotMap::clone_from(&source)` overwrites a destination slot that was previously `Occupied`, the old `T` in that slot must be dropped exactly once. No destructors may be leaked, even when the source slot at the same position is `Vacant`."
how_triggered = "the fix added the `(OccupiedMut, Vacant)` arm that explicitly calls `ManuallyDrop::drop(&mut self.u.value)` before writing the `next_free` union variant. The variant removes that arm, falling back to a pattern-exhaustive match that overwrites `self.u` with a fresh `SlotUnion { next_free }` while the old `ManuallyDrop<T>` still owns a live value — leaking its `Drop`. The property builds a `SlotMap<CountDrop>` with `n` elements, removes one (leaving a mix of occupied and vacant slots), clones-into a freshly-allocated `SlotMap`, drops the destination, and asserts that `drops_during_clone_from + drops_after_drop_dest == n` rather than `< n`."

[[tasks.tasks]]
property = "SlotmapCloneFromDropsOverwrittenSlot"
witnesses = [
  { test_fn = "witness_slotmap_clone_from_drops_overwritten_slot_case_small" },
  { test_fn = "witness_slotmap_clone_from_drops_overwritten_slot_case_remove_first" },
  { test_fn = "witness_slotmap_clone_from_drops_overwritten_slot_case_remove_last" },
]
