#[cfg(test)]
mod tests {
use crate::{BStack, HEADER_SIZE, MAGIC};
use std::fs::OpenOptions;
use std::io::ErrorKind;
fn mk_stack() -> (BStack, std::path::PathBuf) {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_test_{pid}_{id}.bin"));
let stack = BStack::open(&path).unwrap();
(stack, path)
}
struct Guard(std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn push_returns_correct_offsets() {
let (s, p) = mk_stack();
let _g = Guard(p);
let off0 = s.push(b"hello").unwrap();
let off1 = s.push(b"world").unwrap();
let off2 = s.push(b"!").unwrap();
assert_eq!(off0, 0);
assert_eq!(off1, 5);
assert_eq!(off2, 10);
assert_eq!(s.len().unwrap(), 11);
}
#[test]
fn pop_returns_correct_bytes_and_shrinks() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.push(b"fghij").unwrap();
assert_eq!(s.len().unwrap(), 10);
let bytes = s.pop(5).unwrap();
assert_eq!(bytes, b"fghij");
assert_eq!(s.len().unwrap(), 5);
let bytes = s.pop(5).unwrap();
assert_eq!(bytes, b"abcde");
assert_eq!(s.len().unwrap(), 0);
}
#[test]
fn pop_across_push_boundary() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"12345").unwrap();
s.push(b"67890").unwrap();
let bytes = s.pop(7).unwrap();
assert_eq!(bytes, b"4567890");
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn pop_on_empty_file_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
let err = s.pop(1).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn pop_n_exceeds_file_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let err = s.pop(10).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn peek_reads_from_offset_to_end() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
assert_eq!(s.peek(0).unwrap(), b"helloworld");
assert_eq!(s.peek(5).unwrap(), b"world");
assert_eq!(s.peek(7).unwrap(), b"rld");
assert_eq!(s.peek(10).unwrap(), b"");
}
#[test]
fn peek_offset_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let err = s.peek(10).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn get_reads_half_open_range() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
assert_eq!(s.get(0, 5).unwrap(), b"hello");
assert_eq!(s.get(5, 10).unwrap(), b"world");
assert_eq!(s.get(3, 8).unwrap(), b"lowor");
assert_eq!(s.get(4, 4).unwrap(), b"");
}
#[test]
fn get_end_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let err = s.get(0, 10).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn get_end_less_than_start_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
let err = s.get(4, 2).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn get_does_not_modify_file() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
let _ = s.get(2, 8).unwrap();
assert_eq!(s.len().unwrap(), 10);
let off = s.push(b"!").unwrap();
assert_eq!(off, 10);
}
#[test]
fn interleaved_push_pop_correct_state() {
let (s, p) = mk_stack();
let _g = Guard(p);
let o0 = s.push(b"AAA").unwrap();
assert_eq!(o0, 0);
let o1 = s.push(b"BB").unwrap();
assert_eq!(o1, 3);
let popped = s.pop(2).unwrap();
assert_eq!(popped, b"BB");
let o2 = s.push(b"CCCC").unwrap();
assert_eq!(o2, 3);
assert_eq!(s.len().unwrap(), 7);
let all = s.pop(7).unwrap();
assert_eq!(all, b"AAACCCC");
assert_eq!(s.len().unwrap(), 0);
}
#[test]
fn reopen_reads_back_correct_data() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 10);
assert_eq!(s2.peek(0).unwrap(), b"helloworld");
}
#[test]
fn reopen_and_continue_pushing() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
let off0 = s.push(b"first").unwrap();
assert_eq!(off0, 0);
drop(s);
let s2 = BStack::open(&p).unwrap();
let off1 = s2.push(b"second").unwrap();
assert_eq!(off1, 5);
assert_eq!(s2.len().unwrap(), 11);
assert_eq!(s2.peek(0).unwrap(), b"firstsecond");
}
#[test]
fn reopen_after_pop_sees_truncated_file() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
s.pop(5).unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 5);
assert_eq!(s2.peek(0).unwrap(), b"hello");
}
#[test]
fn push_empty_slice() {
let (s, p) = mk_stack();
let _g = Guard(p);
let off0 = s.push(b"abc").unwrap();
let off1 = s.push(&[]).unwrap();
let off2 = s.push(b"def").unwrap();
assert_eq!(off0, 0);
assert_eq!(off1, 3);
assert_eq!(off2, 3);
assert_eq!(s.len().unwrap(), 6);
assert_eq!(s.peek(0).unwrap(), b"abcdef");
}
#[test]
fn pop_zero_bytes() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let bytes = s.pop(0).unwrap();
assert_eq!(bytes, b"");
assert_eq!(s.len().unwrap(), 3);
let off = s.push(b"d").unwrap();
assert_eq!(off, 3);
}
#[test]
fn peek_zero_offset_on_empty_file() {
let (s, p) = mk_stack();
let _g = Guard(p);
assert_eq!(s.peek(0).unwrap(), b"");
}
#[test]
fn get_zero_range_on_empty_file() {
let (s, p) = mk_stack();
let _g = Guard(p);
assert_eq!(s.get(0, 0).unwrap(), b"");
}
#[test]
fn drain_to_zero_then_push_starts_at_offset_zero() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.pop(5).unwrap();
assert_eq!(s.len().unwrap(), 0);
let off = s.push(b"world").unwrap();
assert_eq!(off, 0);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"world");
}
#[test]
fn peek_does_not_modify_file() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
let _ = s.peek(3).unwrap();
assert_eq!(s.len().unwrap(), 10);
let off = s.push(b"!").unwrap();
assert_eq!(off, 10);
}
#[test]
fn binary_roundtrip_all_byte_values() {
let (s, p) = mk_stack();
let _g = Guard(p);
let data: Vec<u8> = (0u16..512).map(|i| (i % 256) as u8).collect();
s.push(&data).unwrap();
let got = s.pop(data.len() as u64).unwrap();
assert_eq!(got, data);
assert_eq!(s.len().unwrap(), 0);
}
#[test]
fn large_payload_roundtrip() {
let (s, p) = mk_stack();
let _g = Guard(p);
let payload: Vec<u8> = (0..1024 * 1024)
.map(|i: usize| (i.wrapping_mul(7).wrapping_add(13)) as u8)
.collect();
s.push(&payload).unwrap();
let got = s.get(0, payload.len() as u64).unwrap();
assert_eq!(got, payload);
assert_eq!(s.len().unwrap(), payload.len() as u64);
}
#[test]
fn new_file_has_valid_header() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
drop(s);
let raw = std::fs::read(&p).unwrap();
assert_eq!(
raw.len(),
HEADER_SIZE as usize,
"new file should be exactly 16 bytes"
);
assert_eq!(&raw[0..8], &MAGIC, "magic mismatch");
let clen = u64::from_le_bytes(raw[8..16].try_into().unwrap());
assert_eq!(clen, 0, "committed length should be 0 for empty stack");
}
#[test]
fn header_committed_len_matches_after_pushes() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap(); s.push(b"world").unwrap(); drop(s);
let raw = std::fs::read(&p).unwrap();
let clen = u64::from_le_bytes(raw[8..16].try_into().unwrap());
assert_eq!(clen, 10);
assert_eq!(raw.len() as u64, HEADER_SIZE + 10);
}
#[test]
fn header_committed_len_matches_after_pop() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
s.pop(5).unwrap();
drop(s);
let raw = std::fs::read(&p).unwrap();
let clen = u64::from_le_bytes(raw[8..16].try_into().unwrap());
assert_eq!(clen, 5);
assert_eq!(raw.len() as u64, HEADER_SIZE + 5);
}
#[test]
fn open_rejects_bad_magic() {
let path = {
use std::sync::atomic::{AtomicU64, Ordering};
static C: AtomicU64 = AtomicU64::new(0);
let id = C.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("bstack_badmagic_{}.bin", id))
};
let _g = Guard(path.clone());
let mut bad: Vec<u8> = b"WRONGHDR".to_vec();
bad.extend_from_slice(&0u64.to_le_bytes());
std::fs::write(&path, &bad).unwrap();
let err = BStack::open(&path).err().unwrap();
assert_eq!(err.kind(), ErrorKind::InvalidData);
assert!(err.to_string().contains("magic"));
}
#[test]
fn open_rejects_truncated_header() {
let path = {
use std::sync::atomic::{AtomicU64, Ordering};
static C: AtomicU64 = AtomicU64::new(0);
let id = C.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("bstack_smallfile_{}.bin", id))
};
let _g = Guard(path.clone());
std::fs::write(&path, b"tooshort").unwrap();
let err = BStack::open(&path).err().unwrap();
assert_eq!(err.kind(), ErrorKind::InvalidData);
}
#[test]
fn recovery_truncates_partial_push() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"committed").unwrap(); drop(s);
{
use std::io::Write;
let mut f = OpenOptions::new().append(true).open(&p).unwrap();
f.write_all(b"ghost").unwrap();
}
let raw = std::fs::read(&p).unwrap();
assert_eq!(raw.len(), (HEADER_SIZE + 9 + 5) as usize);
let clen_before = u64::from_le_bytes(raw[8..16].try_into().unwrap());
assert_eq!(clen_before, 9);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 9);
assert_eq!(s2.peek(0).unwrap(), b"committed");
drop(s2);
let raw2 = std::fs::read(&p).unwrap();
assert_eq!(raw2.len(), (HEADER_SIZE + 9) as usize);
}
#[test]
fn recovery_repairs_header_after_partial_pop() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap(); s.push(b"world").unwrap(); drop(s);
{
let f = OpenOptions::new().write(true).open(&p).unwrap();
f.set_len(HEADER_SIZE + 5).unwrap();
}
let raw = std::fs::read(&p).unwrap();
assert_eq!(raw.len(), (HEADER_SIZE + 5) as usize);
let clen_before = u64::from_le_bytes(raw[8..16].try_into().unwrap());
assert_eq!(
clen_before, 10,
"header should still claim 10 before recovery"
);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 5);
assert_eq!(s2.peek(0).unwrap(), b"hello");
drop(s2);
let raw2 = std::fs::read(&p).unwrap();
let clen_after = u64::from_le_bytes(raw2[8..16].try_into().unwrap());
assert_eq!(clen_after, 5, "clen should be repaired to 5 after recovery");
}
#[test]
fn peek_into_fills_buffer() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
let mut buf = [0u8; 5];
s.peek_into(5, &mut buf).unwrap();
assert_eq!(&buf, b"world");
let mut buf2 = [0u8; 10];
s.peek_into(0, &mut buf2).unwrap();
assert_eq!(&buf2, b"helloworld");
}
#[test]
fn peek_into_empty_buf_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.peek_into(0, &mut []).unwrap();
}
#[test]
fn peek_into_range_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let mut buf = [0u8; 5];
let err = s.peek_into(0, &mut buf).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn peek_into_matches_peek() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let expected = s.peek(3).unwrap();
let mut buf = vec![0u8; expected.len()];
s.peek_into(3, &mut buf).unwrap();
assert_eq!(buf, expected);
}
#[test]
fn get_into_fills_buffer() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
let mut buf = [0u8; 5];
s.get_into(3, &mut buf).unwrap();
assert_eq!(&buf, b"lowor");
}
#[test]
fn get_into_empty_buf_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
s.get_into(1, &mut []).unwrap();
}
#[test]
fn get_into_end_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let mut buf = [0u8; 5];
let err = s.get_into(0, &mut buf).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn get_into_matches_get() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let expected = s.get(2, 8).unwrap();
let mut buf = vec![0u8; 6];
s.get_into(2, &mut buf).unwrap();
assert_eq!(buf, expected);
}
#[test]
fn pop_into_fills_buffer_and_shrinks() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.push(b"fghij").unwrap();
let mut buf = [0u8; 5];
s.pop_into(&mut buf).unwrap();
assert_eq!(&buf, b"fghij");
assert_eq!(s.len().unwrap(), 5);
s.pop_into(&mut buf).unwrap();
assert_eq!(&buf, b"abcde");
assert_eq!(s.len().unwrap(), 0);
}
#[test]
fn pop_into_empty_buf_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
s.pop_into(&mut []).unwrap();
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn pop_into_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let mut buf = [0u8; 10];
let err = s.pop_into(&mut buf).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn pop_into_matches_pop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let expected = s.pop(5).unwrap();
let (s2, p2) = mk_stack();
let _g2 = Guard(p2);
s2.push(b"helloworld").unwrap();
let mut buf = vec![0u8; 5];
s2.pop_into(&mut buf).unwrap();
assert_eq!(buf, expected);
assert_eq!(s2.len().unwrap(), 5);
}
#[test]
fn discard_removes_bytes_from_tail() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.push(b"fghij").unwrap();
assert_eq!(s.len().unwrap(), 10);
s.discard(5).unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"abcde");
s.discard(5).unwrap();
assert_eq!(s.len().unwrap(), 0);
}
#[test]
fn discard_zero_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
s.discard(0).unwrap();
assert_eq!(s.len().unwrap(), 3);
assert_eq!(s.peek(0).unwrap(), b"abc");
}
#[test]
fn discard_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let err = s.discard(10).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn discard_on_empty_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
let err = s.discard(1).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[test]
fn discard_leaves_correct_tail() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.discard(5).unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn discard_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
s.discard(5).unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 5);
assert_eq!(s2.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn set_overwrites_middle_bytes() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.set(5, b"WORLD").unwrap();
assert_eq!(s.peek(0).unwrap(), b"helloWORLD");
assert_eq!(s.len().unwrap(), 10);
}
#[cfg(feature = "set")]
#[test]
fn set_at_start() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.set(0, b"HELLO").unwrap();
assert_eq!(s.peek(0).unwrap(), b"HELLOworld");
}
#[cfg(feature = "set")]
#[test]
fn set_at_exact_end_boundary() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.set(3, b"LO").unwrap();
assert_eq!(s.peek(0).unwrap(), b"helLO");
}
#[cfg(feature = "set")]
#[test]
fn set_empty_slice_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.set(2, b"").unwrap();
assert_eq!(s.peek(0).unwrap(), b"hello");
assert_eq!(s.len().unwrap(), 5);
}
#[cfg(feature = "set")]
#[test]
fn set_does_not_change_file_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.set(1, b"XYZ").unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"aXYZe");
}
#[cfg(feature = "set")]
#[test]
fn set_rejects_write_past_end() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.set(3, b"TOOLONG").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn set_rejects_offset_past_end() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.set(10, b"x").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[cfg(feature = "set")]
#[test]
fn set_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.set(5, b"WORLD").unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"helloWORLD");
}
#[test]
fn extend_appends_zeros() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let off = s.extend(3).unwrap();
assert_eq!(off, 3);
assert_eq!(s.len().unwrap(), 6);
assert_eq!(s.peek(0).unwrap(), b"abc\x00\x00\x00");
}
#[test]
fn extend_zero_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let off = s.extend(0).unwrap();
assert_eq!(off, 5);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn extend_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hi").unwrap();
s.extend(2).unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"hi\x00\x00");
}
#[cfg(feature = "set")]
#[test]
fn zero_overwrites_with_zeros() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.zero(5, 5).unwrap();
assert_eq!(s.peek(0).unwrap(), b"hello\x00\x00\x00\x00\x00");
assert_eq!(s.len().unwrap(), 10);
}
#[cfg(feature = "set")]
#[test]
fn zero_at_start() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.zero(0, 5).unwrap();
assert_eq!(s.peek(0).unwrap(), b"\x00\x00\x00\x00\x00world");
}
#[cfg(feature = "set")]
#[test]
fn zero_at_exact_end_boundary() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.zero(3, 2).unwrap();
assert_eq!(s.peek(0).unwrap(), b"hel\x00\x00");
}
#[cfg(feature = "set")]
#[test]
fn zero_zero_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.zero(2, 0).unwrap();
assert_eq!(s.peek(0).unwrap(), b"hello");
assert_eq!(s.len().unwrap(), 5);
}
#[cfg(feature = "set")]
#[test]
fn zero_does_not_change_file_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.zero(1, 3).unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"a\x00\x00\x00e");
}
#[cfg(feature = "set")]
#[test]
fn zero_rejects_write_past_end() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.zero(3, 3).unwrap_err(); assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn zero_rejects_offset_past_end() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.zero(10, 1).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
}
#[cfg(feature = "set")]
#[test]
fn zero_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.zero(5, 5).unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"hello\x00\x00\x00\x00\x00");
}
#[test]
fn write_appends_and_survives_reopen() {
use std::io::Write;
let (mut s, p) = mk_stack();
let _g = Guard(p.clone());
s.write_all(b"hello").unwrap();
s.write_all(b"world").unwrap();
assert_eq!(s.len().unwrap(), 10);
assert_eq!(s.peek(0).unwrap(), b"helloworld");
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"helloworld");
}
#[test]
fn write_returns_exact_byte_count() {
use std::io::Write;
let (mut s, p) = mk_stack();
let _g = Guard(p);
assert_eq!(s.write(b"abcde").unwrap(), 5);
assert_eq!(s.write(b"").unwrap(), 0);
assert_eq!(s.write(b"x").unwrap(), 1);
assert_eq!(s.len().unwrap(), 6);
}
#[test]
fn write_empty_slice_is_noop() {
use std::io::Write;
let (mut s, p) = mk_stack();
let _g = Guard(p);
s.write_all(b"abc").unwrap();
s.write_all(b"").unwrap();
assert_eq!(s.len().unwrap(), 3);
assert_eq!(s.peek(0).unwrap(), b"abc");
}
#[test]
fn write_flush_is_noop() {
use std::io::Write;
let (mut s, p) = mk_stack();
let _g = Guard(p);
s.push(b"data").unwrap();
s.flush().unwrap();
assert_eq!(s.len().unwrap(), 4);
}
#[test]
fn write_shared_ref() {
use std::io::Write;
let (s, p) = mk_stack();
let _g = Guard(p);
let mut r: &BStack = &s;
r.write_all(b"abc").unwrap();
r.write_all(b"def").unwrap();
assert_eq!(s.peek(0).unwrap(), b"abcdef");
}
#[test]
fn write_shared_ref_returns_exact_byte_count() {
use std::io::Write;
let (s, p) = mk_stack();
let _g = Guard(p);
let mut r: &BStack = &s;
assert_eq!(r.write(b"hello").unwrap(), 5);
assert_eq!(r.write(b"").unwrap(), 0);
}
#[test]
fn write_via_io_copy() {
use std::io::{Cursor, copy};
let (mut s, p) = mk_stack();
let _g = Guard(p);
let mut src = Cursor::new(b"copied data");
copy(&mut src, &mut s).unwrap();
assert_eq!(s.peek(0).unwrap(), b"copied data");
}
#[test]
fn write_via_bufwriter() {
use std::io::{BufWriter, Write};
let (s, p) = mk_stack();
let _g = Guard(p);
let mut bw = BufWriter::new(&s);
bw.write_all(b"buf").unwrap();
bw.write_all(b"fered").unwrap();
bw.flush().unwrap();
drop(bw);
assert_eq!(s.peek(0).unwrap(), b"buffered");
}
#[test]
fn reader_reads_bytes_sequentially() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
let mut reader = s.reader();
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"hello");
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"world");
}
#[test]
fn reader_returns_zero_at_eof() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hi").unwrap();
let mut reader = s.reader();
let mut buf = [0u8; 10];
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 2);
assert_eq!(&buf[..2], b"hi");
assert_eq!(reader.read(&mut buf).unwrap(), 0);
assert_eq!(reader.read(&mut buf).unwrap(), 0); }
#[test]
fn reader_empty_buf_returns_zero_without_advancing() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let mut reader = s.reader();
assert_eq!(reader.read(&mut []).unwrap(), 0);
assert_eq!(reader.position(), 0); }
#[test]
fn reader_read_from_empty_stack() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
let mut reader = s.reader();
let mut buf = [0u8; 4];
assert_eq!(reader.read(&mut buf).unwrap(), 0);
}
#[test]
fn reader_read_exact_fails_at_eof() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hi").unwrap();
let mut reader = s.reader();
let mut buf = [0u8; 10];
let err = reader.read_exact(&mut buf).unwrap_err();
assert_eq!(err.kind(), ErrorKind::UnexpectedEof);
}
#[test]
fn reader_partial_reads_advance_cursor() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcdefghij").unwrap();
let mut reader = s.reader();
let mut buf = [0u8; 3];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"abc");
assert_eq!(reader.position(), 3);
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"def");
assert_eq!(reader.position(), 6);
}
#[test]
fn reader_read_to_end() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.push(b"world").unwrap();
let mut reader = s.reader_at(3);
let mut out = Vec::new();
reader.read_to_end(&mut out).unwrap();
assert_eq!(out, b"loworld");
assert_eq!(reader.position(), 10);
}
#[test]
fn reader_at_starts_at_given_offset() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut reader = s.reader_at(5);
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"world");
}
#[test]
fn reader_from_trait() {
use std::io::Read;
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"test").unwrap();
let mut reader = crate::BStackReader::from(&s);
let mut buf = [0u8; 4];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"test");
}
#[test]
fn reader_via_bufreader() {
use std::io::{BufRead, BufReader};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"line one\nline two\n").unwrap();
let reader = BufReader::new(s.reader());
let lines: Vec<String> = reader.lines().map(|l| l.unwrap()).collect();
assert_eq!(lines, ["line one", "line two"]);
}
#[test]
fn reader_seek_from_start() {
use std::io::{Read, Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut reader = s.reader();
assert_eq!(reader.seek(SeekFrom::Start(5)).unwrap(), 5);
assert_eq!(reader.position(), 5);
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"world");
}
#[test]
fn reader_seek_from_end() {
use std::io::{Read, Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut reader = s.reader();
assert_eq!(reader.seek(SeekFrom::End(-5)).unwrap(), 5);
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"world");
}
#[test]
fn reader_seek_from_end_zero_returns_len() {
use std::io::{Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut reader = s.reader();
let pos = reader.seek(SeekFrom::End(0)).unwrap();
assert_eq!(pos, s.len().unwrap());
}
#[test]
fn reader_seek_from_current() {
use std::io::{Read, Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut reader = s.reader();
reader.seek(SeekFrom::Current(3)).unwrap();
assert_eq!(reader.seek(SeekFrom::Current(2)).unwrap(), 5);
assert_eq!(reader.position(), 5);
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"world");
}
#[test]
fn reader_seek_rewind_and_reread() {
use std::io::{Read, Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
let mut reader = s.reader();
let mut buf = [0u8; 5];
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"abcde");
reader.seek(SeekFrom::Start(0)).unwrap();
assert_eq!(reader.position(), 0);
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"abcde");
}
#[test]
fn reader_seek_read_seek_read() {
use std::io::{Read, Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"ABCDEFGHIJ").unwrap();
let mut reader = s.reader();
let mut buf = [0u8; 3];
reader.seek(SeekFrom::Start(7)).unwrap();
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"HIJ");
reader.seek(SeekFrom::Start(2)).unwrap();
reader.read_exact(&mut buf).unwrap();
assert_eq!(&buf, b"CDE");
}
#[test]
fn reader_seek_before_start_returns_error() {
use std::io::{Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let mut reader = s.reader();
assert_eq!(
reader.seek(SeekFrom::End(-10)).unwrap_err().kind(),
ErrorKind::InvalidInput
);
assert_eq!(
reader.seek(SeekFrom::Current(-1)).unwrap_err().kind(),
ErrorKind::InvalidInput
);
}
#[test]
fn reader_seek_past_end_then_read_returns_zero() {
use std::io::{Read, Seek, SeekFrom};
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hi").unwrap();
let mut reader = s.reader();
reader.seek(SeekFrom::Start(100)).unwrap();
let mut buf = [0u8; 4];
assert_eq!(reader.read(&mut buf).unwrap(), 0);
}
#[cfg(any(unix, windows))]
#[test]
fn concurrent_readers_do_not_block_each_other() {
use std::io::Read;
use std::sync::Arc;
use std::thread;
let (s, p) = mk_stack();
let _g = Guard(p);
let payload: Vec<u8> = (0u8..=255).cycle().take(1024).collect();
s.push(&payload).unwrap();
let s = Arc::new(s);
let handles: Vec<_> = (0..16)
.map(|i| {
let s = Arc::clone(&s);
let expected = payload.clone();
thread::spawn(move || {
let mut reader = s.reader_at(i * 4);
let mut out = Vec::new();
reader.read_to_end(&mut out).unwrap();
assert_eq!(out, &expected[i as usize * 4..]);
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[cfg(any(unix, windows))]
#[test]
fn concurrent_reads_do_not_serialise() {
use std::sync::Arc;
use std::thread;
let (s, p) = mk_stack();
let _g = Guard(p);
const RECORDS: usize = 8;
const RSIZE: u64 = 16;
for i in 0..RECORDS {
let mut rec = [0u8; RSIZE as usize];
rec[0] = i as u8;
s.push(&rec).unwrap();
}
let s = Arc::new(s);
let handles: Vec<_> = (0..32)
.map(|_| {
let s = Arc::clone(&s);
thread::spawn(move || {
for i in 0..RECORDS {
let off = i as u64 * RSIZE;
let via_get = s.get(off, off + RSIZE).unwrap();
assert_eq!(via_get[0], i as u8);
let via_peek = s.peek(off).unwrap();
assert_eq!(via_peek[0], i as u8);
}
})
})
.collect();
for h in handles {
h.join().unwrap();
}
}
#[test]
fn concurrent_pushes_non_overlapping() {
use std::collections::HashSet;
use std::sync::Arc;
use std::thread;
let (s, p) = mk_stack();
let _g = Guard(p);
let s = Arc::new(s);
const THREADS: usize = 8;
const PER_THREAD: usize = 100;
const ITEM: usize = 16;
let handles: Vec<_> = (0..THREADS)
.map(|t| {
let s = Arc::clone(&s);
thread::spawn(move || {
(0..PER_THREAD)
.map(|i| {
let mut data = [0u8; ITEM];
data[0] = t as u8;
data[1..9].copy_from_slice(&(i as u64).to_le_bytes());
let off = s.push(&data).unwrap();
(off, t, i)
})
.collect::<Vec<_>>()
})
})
.collect();
let results: Vec<_> = handles
.into_iter()
.flat_map(|h| h.join().unwrap())
.collect();
for &(off, _, _) in &results {
assert_eq!(off % ITEM as u64, 0, "offset {off} is not aligned to ITEM");
}
let mut seen: HashSet<u64> = HashSet::new();
for &(off, _, _) in &results {
assert!(seen.insert(off), "duplicate offset {off}");
}
assert_eq!(s.len().unwrap(), (THREADS * PER_THREAD * ITEM) as u64);
for &(off, t, i) in &results {
let slot = s.get(off, off + ITEM as u64).unwrap();
assert_eq!(slot[0], t as u8, "thread id mismatch at offset {off}");
let idx = u64::from_le_bytes(slot[1..9].try_into().unwrap());
assert_eq!(idx, i as u64, "item index mismatch at offset {off}");
}
}
#[test]
fn concurrent_len_is_multiple_of_item_size() {
use std::sync::Arc;
use std::thread;
let (s, p) = mk_stack();
let _g = Guard(p);
let s = Arc::new(s);
const ITEM: u64 = 8;
const PUSH_THREADS: usize = 4;
const PUSHES_PER_THREAD: usize = 200;
let push_handles: Vec<_> = (0..PUSH_THREADS)
.map(|_| {
let s = Arc::clone(&s);
thread::spawn(move || {
for _ in 0..PUSHES_PER_THREAD {
s.push(&[0xBEu8; ITEM as usize]).unwrap();
}
})
})
.collect();
let len_handle = {
let s = Arc::clone(&s);
thread::spawn(move || {
for _ in 0..2000 {
let size = s.len().unwrap();
assert_eq!(
size % ITEM,
0,
"torn write: size {size} is not a multiple of {ITEM}"
);
}
})
};
for h in push_handles {
h.join().unwrap();
}
len_handle.join().unwrap();
assert_eq!(
s.len().unwrap(),
(PUSH_THREADS * PUSHES_PER_THREAD) as u64 * ITEM
);
}
}
#[cfg(all(test, feature = "alloc"))]
mod alloc_tests {
use crate::BStack;
use crate::alloc::{BStackAllocator, BStackBulkAllocator, BStackSlice, LinearBStackAllocator};
use std::io::{Read, Seek, SeekFrom};
use std::sync::atomic::{AtomicU64, Ordering};
fn mk_alloc() -> (LinearBStackAllocator, std::path::PathBuf) {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_alloc_test_{pid}_{id}.bin"));
let stack = BStack::open(&path).unwrap();
(LinearBStackAllocator::new(stack), path)
}
struct Guard(std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn alloc_offset_and_len() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(32).unwrap();
assert_eq!(s.start(), 0);
assert_eq!(s.len(), 32);
assert!(!s.is_empty());
assert_eq!(s.end(), 32);
}
#[test]
fn alloc_zero_len() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(0).unwrap();
assert_eq!(s.len(), 0);
assert!(s.is_empty());
assert_eq!(alloc.len().unwrap(), 0);
}
#[test]
fn alloc_sequential_offsets() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let a = alloc.alloc(8).unwrap();
let b = alloc.alloc(16).unwrap();
assert_eq!(a.start(), 0);
assert_eq!(a.len(), 8);
assert_eq!(b.start(), 8);
assert_eq!(b.len(), 16);
assert_eq!(alloc.len().unwrap(), 24);
}
#[test]
fn alloc_read_zeros() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
let data = s.read().unwrap();
assert_eq!(data, vec![0u8; 4]);
}
#[test]
fn read_into_exact_size() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
let mut buf = [0u8; 4];
s.read_into(&mut buf).unwrap();
assert_eq!(buf, [0u8; 4]);
}
#[test]
fn read_into_shorter_buffer() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
let mut buf = [0xffu8; 3];
s.read_into(&mut buf).unwrap(); assert_eq!(buf, [0u8; 3]);
}
#[test]
fn read_into_longer_buffer() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
let mut buf = [0xffu8; 6];
s.read_into(&mut buf).unwrap();
assert_eq!(&buf[..4], [0u8; 4]);
assert_eq!(&buf[4..], [0xffu8; 2]); }
#[test]
fn read_range_into_correct() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let _ = alloc.alloc(8).unwrap(); let s = BStackSlice::new(&alloc, 0, 8);
let mut buf = [0u8; 3];
s.read_range_into(2, &mut buf).unwrap(); assert_eq!(buf, [0u8; 3]);
}
#[test]
fn read_range_into_out_of_bounds() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
let mut buf = [0u8; 3];
let err = s.read_range_into(2, &mut buf).unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn realloc_tail_grow() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let s2 = alloc.realloc(s, 16).unwrap();
assert_eq!(s2.start(), 0);
assert_eq!(s2.len(), 16);
assert_eq!(alloc.len().unwrap(), 16);
}
#[test]
fn realloc_tail_shrink() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
let s2 = alloc.realloc(s, 8).unwrap();
assert_eq!(s2.start(), 0);
assert_eq!(s2.len(), 8);
assert_eq!(alloc.len().unwrap(), 8);
}
#[test]
fn realloc_same_len() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let s2 = alloc.realloc(s, 8).unwrap();
assert_eq!(s2.start(), 0);
assert_eq!(s2.len(), 8);
assert_eq!(alloc.len().unwrap(), 8);
}
#[test]
fn realloc_non_tail_unsupported() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let _ = alloc.alloc(4).unwrap(); let err = alloc.realloc(s, 16).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::Unsupported);
}
#[test]
fn dealloc_tail_reclaims() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
assert_eq!(alloc.len().unwrap(), 16);
alloc.dealloc(s).unwrap();
assert_eq!(alloc.len().unwrap(), 0);
}
#[test]
fn dealloc_non_tail_noop() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let _ = alloc.alloc(4).unwrap(); alloc.dealloc(s).unwrap(); assert_eq!(alloc.len().unwrap(), 12); }
#[test]
fn slice_reader_sequential() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let mut reader = s.reader();
assert_eq!(reader.position(), 0);
let mut buf = [0u8; 4];
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 4);
assert_eq!(reader.position(), 4);
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 4);
assert_eq!(reader.position(), 8);
let n = reader.read(&mut buf).unwrap();
assert_eq!(n, 0);
}
#[test]
fn slice_reader_seek() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let mut reader = s.reader();
let pos = reader.seek(SeekFrom::End(0)).unwrap();
assert_eq!(pos, 8);
let pos = reader.seek(SeekFrom::Current(-4)).unwrap();
assert_eq!(pos, 4);
let pos = reader.seek(SeekFrom::Start(2)).unwrap();
assert_eq!(pos, 2);
}
#[test]
fn slice_reader_seek_before_start() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let mut reader = s.reader();
let err = reader.seek(SeekFrom::Current(-1)).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn slice_reader_at() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
let reader = s.reader_at(5);
assert_eq!(reader.position(), 5);
}
#[test]
fn into_stack_recovers() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let _ = alloc.alloc(4).unwrap();
let stack = alloc.into_stack();
assert_eq!(stack.len().unwrap(), 4);
}
#[cfg(feature = "set")]
#[test]
fn write_read_roundtrip() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(5).unwrap();
s.write(b"hello").unwrap();
assert_eq!(s.read().unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn write_shorter_data() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(5).unwrap();
s.write(b"hi").unwrap(); let data = s.read().unwrap();
assert_eq!(data, b"hi\x00\x00\x00");
}
#[cfg(feature = "set")]
#[test]
fn write_longer_data() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(3).unwrap();
s.write(b"hello").unwrap(); assert_eq!(s.read().unwrap(), b"hel");
}
#[cfg(feature = "set")]
#[test]
fn write_range_partial() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(5).unwrap();
s.write_range(1, b"abc").unwrap();
let data = s.read().unwrap();
assert_eq!(data, b"\x00abc\x00");
}
#[cfg(feature = "set")]
#[test]
fn write_range_out_of_bounds() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(5).unwrap();
let err = s.write_range(3, b"abc").unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[cfg(feature = "set")]
#[test]
fn zero_clears_slice() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
s.write(b"abcd").unwrap();
s.zero().unwrap();
assert_eq!(s.read().unwrap(), vec![0u8; 4]);
}
#[cfg(feature = "set")]
#[test]
fn zero_range_partial() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
s.write(b"abcd").unwrap();
s.zero_range(1, 2).unwrap();
assert_eq!(s.read().unwrap(), b"a\x00\x00d");
}
#[cfg(feature = "set")]
#[test]
fn zero_range_out_of_bounds() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(4).unwrap();
let err = s.zero_range(3, 2).unwrap_err(); assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
}
#[test]
fn subslice_correct() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let sub = s.subslice(2, 8);
assert_eq!(sub.start(), 2);
assert_eq!(sub.len(), 6);
assert_eq!(sub.start(), 2);
assert_eq!(sub.range(), 2..8);
}
#[test]
fn subslice_empty() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let sub = s.subslice(5, 5);
assert_eq!(sub.start(), 5);
assert_eq!(sub.len(), 0);
assert!(sub.is_empty());
}
#[test]
#[should_panic(expected = "range start must be <= end")]
fn subslice_invalid_range_start_greater() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let _ = s.subslice(8, 5); }
#[test]
#[should_panic(expected = "range end must be <= slice length")]
fn subslice_out_of_bounds() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let _ = s.subslice(5, 15); }
#[test]
fn start_returns_offset() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
assert_eq!(s.start(), 0);
let sub = s.subslice(3, 7);
assert_eq!(sub.start(), 3);
}
#[test]
fn range_returns_correct() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
assert_eq!(s.range(), 0..10);
let sub = s.subslice(2, 8);
assert_eq!(sub.range(), 2..8);
}
#[test]
fn bstack_debug_contains_version_and_len() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let dbg = format!("{:?}", alloc.stack());
assert!(dbg.contains("BStack"), "{dbg}");
assert!(dbg.contains("version"), "{dbg}");
assert!(dbg.contains("len"), "{dbg}");
assert!(dbg.contains("0.1"), "{dbg}");
}
#[test]
fn slice_reader_debug_uses_public_fields() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let reader = s.reader_at(3);
let dbg = format!("{:?}", reader);
assert!(dbg.contains("BStackSliceReader"), "{dbg}");
assert!(dbg.contains("start"), "{dbg}");
assert!(dbg.contains("end"), "{dbg}");
assert!(dbg.contains("len"), "{dbg}");
assert!(dbg.contains("cursor"), "{dbg}");
assert!(!dbg.contains("\"offset\""), "raw field in debug: {dbg}");
}
#[cfg(feature = "set")]
#[test]
fn slice_writer_debug_uses_public_fields() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let writer = s.writer_at(3);
let dbg = format!("{:?}", writer);
assert!(dbg.contains("BStackSliceWriter"), "{dbg}");
assert!(dbg.contains("start"), "{dbg}");
assert!(dbg.contains("end"), "{dbg}");
assert!(dbg.contains("len"), "{dbg}");
assert!(dbg.contains("cursor"), "{dbg}");
assert!(!dbg.contains("\"offset\""), "raw field in debug: {dbg}");
}
#[test]
fn reader_ord_by_absolute_position() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let r0 = s.reader_at(0);
let r5 = s.reader_at(5);
let r10 = s.reader_at(10);
assert!(r0 < r5);
assert!(r5 < r10);
assert!(r0 < r10);
assert_eq!(r5.cmp(&s.reader_at(5)), std::cmp::Ordering::Equal);
}
#[test]
fn reader_ord_earlier_slice_before_later() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let a = alloc.alloc(8).unwrap(); let b = alloc.alloc(8).unwrap(); assert!(a.reader() < b.reader());
}
#[test]
fn reader_ord_same_abs_position_shorter_len_less() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let short = s.subslice(0, 3).reader();
let long_ = s.subslice(0, 8).reader();
assert!(short < long_);
}
#[cfg(feature = "set")]
#[test]
fn writer_ord_by_absolute_position() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let w0 = s.writer_at(0);
let w5 = s.writer_at(5);
assert!(w0 < w5);
assert_eq!(w5.cmp(&s.writer_at(5)), std::cmp::Ordering::Equal);
}
#[cfg(feature = "set")]
#[test]
fn writer_ord_earlier_slice_before_later() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let a = alloc.alloc(8).unwrap(); let b = alloc.alloc(8).unwrap(); assert!(a.writer() < b.writer());
}
#[cfg(feature = "set")]
#[test]
fn reader_writer_cross_partial_ord() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(10).unwrap();
let r3 = s.reader_at(3);
let w5 = s.writer_at(5);
let w3 = s.writer_at(3);
let r5 = s.reader_at(5);
assert!(r3 < w5);
assert!(w3 < r5);
assert_eq!(r3.partial_cmp(&w3), Some(std::cmp::Ordering::Equal));
assert_eq!(w5.partial_cmp(&r5), Some(std::cmp::Ordering::Equal));
}
#[cfg(feature = "set")]
#[test]
fn reader_writer_cross_ord_transitivity() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let s = alloc.alloc(20).unwrap();
let r2 = s.reader_at(2);
let w8 = s.writer_at(8);
let r15 = s.reader_at(15);
assert!(r2 < w8);
assert!(w8 < r15);
assert!(r2 < r15);
}
#[test]
fn bulk_alloc_empty() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([]).unwrap();
assert!(slices.is_empty());
assert_eq!(alloc.len().unwrap(), 0);
}
#[test]
fn bulk_alloc_offsets_and_lens() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([8_u64, 16, 32]).unwrap();
assert_eq!(slices.len(), 3);
assert_eq!(slices[0].start(), 0);
assert_eq!(slices[0].len(), 8);
assert_eq!(slices[1].start(), 8);
assert_eq!(slices[1].len(), 16);
assert_eq!(slices[2].start(), 24);
assert_eq!(slices[2].len(), 32);
}
#[test]
fn bulk_alloc_stack_len() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
alloc.alloc_bulk([10_u64, 20, 30]).unwrap();
assert_eq!(alloc.len().unwrap(), 60);
}
#[test]
fn bulk_alloc_single_element() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([64_u64]).unwrap();
assert_eq!(slices.len(), 1);
assert_eq!(slices[0].start(), 0);
assert_eq!(slices[0].len(), 64);
assert_eq!(alloc.len().unwrap(), 64);
}
#[test]
fn bulk_alloc_zero_len_entries() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([0_u64, 8, 0]).unwrap();
assert_eq!(slices.len(), 3);
assert!(slices[0].is_empty());
assert_eq!(slices[1].start(), 0);
assert_eq!(slices[1].len(), 8);
assert!(slices[2].is_empty());
assert_eq!(alloc.len().unwrap(), 8);
}
#[test]
fn bulk_alloc_reads_zeros() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([4_u64, 8, 4]).unwrap();
for s in &slices {
assert_eq!(s.read().unwrap(), vec![0u8; s.len() as usize]);
}
}
#[test]
fn bulk_alloc_non_overlapping_contiguous() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([5_u64, 10, 15, 20]).unwrap();
for i in 0..slices.len() - 1 {
assert_eq!(slices[i].end(), slices[i + 1].start());
}
}
#[test]
fn bulk_alloc_then_individual() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
alloc.alloc_bulk([8_u64, 16]).unwrap();
let s = alloc.alloc(4).unwrap();
assert_eq!(s.start(), 24);
}
#[test]
fn bulk_alloc_overflow_is_error() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let err = alloc.alloc_bulk([u64::MAX, 1]).unwrap_err();
assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput);
assert_eq!(alloc.len().unwrap(), 0);
}
#[test]
fn bulk_dealloc_empty() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
alloc.alloc(32).unwrap();
alloc.dealloc_bulk([]).unwrap();
assert_eq!(alloc.len().unwrap(), 32);
}
#[test]
fn bulk_dealloc_all_tail_reclaimed() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([8_u64, 16, 32]).unwrap();
alloc.dealloc_bulk(slices).unwrap();
assert_eq!(alloc.len().unwrap(), 0);
}
#[test]
fn bulk_dealloc_non_tail_slice_ignored() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let a = alloc.alloc(8).unwrap(); let b = alloc.alloc(16).unwrap(); alloc.dealloc_bulk([a]).unwrap();
assert_eq!(alloc.len().unwrap(), 24);
alloc.dealloc(b).unwrap();
}
#[test]
fn bulk_dealloc_reverse_order_same_result() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([8_u64, 16, 32]).unwrap();
let mut rev = slices;
rev.reverse();
alloc.dealloc_bulk(rev).unwrap();
assert_eq!(alloc.len().unwrap(), 0);
}
#[test]
fn bulk_dealloc_gap_limits_reclamation() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let a = alloc.alloc(8).unwrap(); let _b = alloc.alloc(8).unwrap(); let c = alloc.alloc(8).unwrap(); alloc.dealloc_bulk([a, c]).unwrap();
assert_eq!(alloc.len().unwrap(), 16);
}
#[test]
fn bulk_dealloc_single_tail() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
alloc.alloc(8).unwrap();
let tail = alloc.alloc(16).unwrap();
alloc.dealloc_bulk([tail]).unwrap();
assert_eq!(alloc.len().unwrap(), 8);
}
#[test]
fn bulk_dealloc_single_nontail_noop() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let head = alloc.alloc(8).unwrap();
alloc.alloc(16).unwrap(); alloc.dealloc_bulk([head]).unwrap();
assert_eq!(alloc.len().unwrap(), 24);
}
#[test]
fn bulk_roundtrip_empty() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([4_u64, 8, 12, 16]).unwrap();
alloc.dealloc_bulk(slices).unwrap();
assert!(alloc.is_empty().unwrap());
}
#[test]
fn bulk_dealloc_partial_then_realloc() {
let (alloc, path) = mk_alloc();
let _g = Guard(path);
let slices = alloc.alloc_bulk([8_u64, 16, 32]).unwrap();
let (head, tail) = slices.split_at(1);
alloc.dealloc_bulk(tail.to_vec()).unwrap();
assert_eq!(alloc.len().unwrap(), 8);
let new = alloc.alloc_bulk([4_u64, 4]).unwrap();
assert_eq!(new[0].start(), 8);
assert_eq!(new[1].start(), 12);
let _ = head; }
}
#[cfg(all(test, feature = "alloc", feature = "set"))]
mod first_fit_tests {
use crate::BStack;
use crate::alloc::{BStackAllocator, FirstFitBStackAllocator};
use std::sync::atomic::{AtomicU64, Ordering};
const ALFF_HDR_OFFSET: u64 = 48; const BLOCK_OVERHEAD: u64 = 24; const MIN_PAYLOAD: u64 = 16;
const FREE_HEAD_OFFSET: u64 = 32;
fn mk_ff(id_prefix: &str) -> (FirstFitBStackAllocator, std::path::PathBuf) {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_ff_test_{id_prefix}_{pid}_{id}.bin"));
let stack = BStack::open(&path).unwrap();
(FirstFitBStackAllocator::new(stack).unwrap(), path)
}
struct Guard(std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn new_empty_stack_initialises_header() {
let (alloc, path) = mk_ff("init");
let _g = Guard(path);
assert_eq!(alloc.len().unwrap(), ALFF_HDR_OFFSET);
}
#[test]
fn new_rejects_bad_magic() {
static C: AtomicU64 = AtomicU64::new(0);
let id = C.fetch_add(1, Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"bstack_ff_badmagic_{}_{}.bin",
std::process::id(),
id
));
let _g = Guard(path.clone());
{
let stack = BStack::open(&path).unwrap();
let mut hdr = [0u8; 48];
hdr[16..24].copy_from_slice(b"WRONGHDR");
stack.push(&hdr).unwrap();
}
let stack = BStack::open(&path).unwrap();
assert!(FirstFitBStackAllocator::new(stack).is_err());
}
#[test]
fn new_reopens_existing_file() {
let (alloc, path) = mk_ff("reopen");
let _g = Guard(path.clone());
let s = alloc.alloc(32).unwrap();
s.write(b"hello world reopen test!!!!!!!!! ").unwrap();
let s_start = s.start();
let _ = s;
drop(alloc.into_stack());
let stack2 = BStack::open(&path).unwrap();
let alloc2 = FirstFitBStackAllocator::new(stack2).unwrap();
let mut buf = [0u8; 11];
alloc2.stack().get_into(s_start, &mut buf).unwrap();
assert_eq!(&buf, b"hello world");
}
#[test]
fn alloc_first_block_payload_starts_after_header() {
let (alloc, path) = mk_ff("first_off");
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
assert_eq!(s.start(), ALFF_HDR_OFFSET + 16); }
#[test]
fn alloc_returns_len_as_requested() {
let (alloc, path) = mk_ff("req_len");
let _g = Guard(path);
let s = alloc.alloc(17).unwrap(); assert_eq!(s.len(), 17);
}
#[test]
fn alloc_zero_initialises_payload() {
let (alloc, path) = mk_ff("zero_init");
let _g = Guard(path);
let s = alloc.alloc(64).unwrap();
assert_eq!(s.read().unwrap(), vec![0u8; 64]);
}
#[test]
fn alloc_rounds_up_to_min_16_bytes() {
let (alloc, path) = mk_ff("min16");
let _g = Guard(path);
let s1 = alloc.alloc(4).unwrap();
let s2 = alloc.alloc(4).unwrap();
assert_eq!(s2.start() - s1.start(), MIN_PAYLOAD + BLOCK_OVERHEAD);
}
#[test]
fn alloc_rounds_up_to_multiple_of_8() {
let (alloc, path) = mk_ff("align8");
let _g = Guard(path);
let s1 = alloc.alloc(17).unwrap(); let s2 = alloc.alloc(16).unwrap();
assert_eq!(s2.start() - s1.start(), 24 + BLOCK_OVERHEAD);
}
#[test]
fn alloc_sequential_non_overlapping() {
let (alloc, path) = mk_ff("seq");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(32).unwrap();
let c = alloc.alloc(64).unwrap();
assert!(a.end() <= b.start());
assert!(b.end() <= c.start());
assert_eq!(b.start(), a.start() + 16 + BLOCK_OVERHEAD);
assert_eq!(c.start(), b.start() + 32 + BLOCK_OVERHEAD);
}
#[test]
fn dealloc_tail_shrinks_stack() {
let (alloc, path) = mk_ff("dealloc_tail");
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
let before = alloc.len().unwrap();
alloc.dealloc(s).unwrap();
assert_eq!(alloc.len().unwrap(), before - 16 - BLOCK_OVERHEAD);
assert_eq!(alloc.len().unwrap(), ALFF_HDR_OFFSET);
}
#[test]
fn dealloc_non_tail_preserves_stack_len() {
let (alloc, path) = mk_ff("dealloc_nontail");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let _b = alloc.alloc(16).unwrap();
let before = alloc.len().unwrap();
alloc.dealloc(a).unwrap(); assert_eq!(alloc.len().unwrap(), before); }
#[test]
fn dealloc_cascade_removes_free_tail() {
let (alloc, path) = mk_ff("cascade");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
alloc.dealloc(a).unwrap(); alloc.dealloc(b).unwrap(); assert_eq!(alloc.len().unwrap(), ALFF_HDR_OFFSET);
}
#[test]
fn dealloc_cascade_multi_level() {
let (alloc, path) = mk_ff("cascade_multi");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let c = alloc.alloc(16).unwrap();
alloc.dealloc(a).unwrap();
alloc.dealloc(b).unwrap();
alloc.dealloc(c).unwrap(); assert_eq!(alloc.len().unwrap(), ALFF_HDR_OFFSET);
}
#[test]
fn alloc_reuses_freed_block() {
let (alloc, path) = mk_ff("reuse");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let _b = alloc.alloc(16).unwrap(); let a_start = a.start();
alloc.dealloc(a).unwrap();
let c = alloc.alloc(16).unwrap(); assert_eq!(c.start(), a_start);
}
#[test]
fn reused_block_is_zero_initialised() {
let (alloc, path) = mk_ff("reuse_zero");
let _g = Guard(path);
let a = alloc.alloc(32).unwrap();
let _b = alloc.alloc(16).unwrap();
a.write(b"dirty data from previous use!!!!").unwrap();
alloc.dealloc(a).unwrap();
let c = alloc.alloc(32).unwrap();
assert_eq!(c.read().unwrap(), vec![0u8; 32]);
}
#[test]
fn free_list_respects_first_fit_order() {
let (alloc, path) = mk_ff("first_fit");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let _sep1 = alloc.alloc(16).unwrap(); let b = alloc.alloc(16).unwrap();
let _sep2 = alloc.alloc(16).unwrap(); let a_start = a.start();
let b_start = b.start();
alloc.dealloc(a).unwrap();
alloc.dealloc(b).unwrap();
let x = alloc.alloc(16).unwrap();
assert_eq!(x.start(), b_start);
let y = alloc.alloc(16).unwrap();
assert_eq!(y.start(), a_start);
}
#[test]
fn alloc_splits_large_free_block() {
let (alloc, path) = mk_ff("split");
let _g = Guard(path);
let big = alloc.alloc(64).unwrap();
let _sentinel = alloc.alloc(16).unwrap();
let big_start = big.start();
alloc.dealloc(big).unwrap();
let small = alloc.alloc(16).unwrap();
assert_eq!(small.start(), big_start + 48);
assert_eq!(small.len(), 16);
let remainder = alloc.alloc(24).unwrap();
assert_eq!(remainder.start(), big_start);
}
#[test]
fn alloc_takes_whole_block_when_split_would_be_too_small() {
let (alloc, path) = mk_ff("nosplit");
let _g = Guard(path);
let block = alloc.alloc(32).unwrap();
let _sentinel = alloc.alloc(16).unwrap();
let block_start = block.start();
alloc.dealloc(block).unwrap();
let reused = alloc.alloc(24).unwrap(); assert_eq!(reused.start(), block_start);
assert_eq!(reused.len(), 24); }
#[test]
fn coalesce_right_merges_with_next_free_block() {
let (alloc, path) = mk_ff("coal_right");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let _sentinel = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(b).unwrap(); alloc.dealloc(a).unwrap();
let merged = alloc.alloc(48).unwrap();
assert_eq!(merged.start(), a_start);
}
#[test]
fn coalesce_left_merges_into_prev_free_block() {
let (alloc, path) = mk_ff("coal_left");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let _sentinel = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(a).unwrap(); alloc.dealloc(b).unwrap();
let merged = alloc.alloc(48).unwrap();
assert_eq!(merged.start(), a_start);
}
#[test]
fn coalesce_both_sides() {
let (alloc, path) = mk_ff("coal_both");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let c = alloc.alloc(16).unwrap();
let _sentinel = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(a).unwrap();
alloc.dealloc(c).unwrap();
alloc.dealloc(b).unwrap();
let merged = alloc.alloc(88).unwrap(); assert_eq!(merged.start(), a_start);
}
#[test]
fn coalesce_data_is_zeroed_in_reused_merged_block() {
let (alloc, path) = mk_ff("coal_zero");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let _sentinel = alloc.alloc(16).unwrap();
a.write(b"AAAAAAAAAAAAAAAA").unwrap();
b.write(b"BBBBBBBBBBBBBBBB").unwrap();
alloc.dealloc(b).unwrap();
alloc.dealloc(a).unwrap(); let merged = alloc.alloc(48).unwrap();
assert_eq!(merged.read().unwrap(), vec![0u8; 48]);
}
#[test]
fn realloc_tail_grow() {
let (alloc, path) = mk_ff("realloc_tail_grow");
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
let s2 = alloc.realloc(s, 32).unwrap();
assert_eq!(s2.start(), s.start());
assert_eq!(s2.len(), 32);
assert_eq!(alloc.len().unwrap(), ALFF_HDR_OFFSET + 32 + BLOCK_OVERHEAD);
}
#[test]
fn realloc_tail_shrink() {
let (alloc, path) = mk_ff("realloc_tail_shrink");
let _g = Guard(path);
let s = alloc.alloc(32).unwrap();
let s2 = alloc.realloc(s, 16).unwrap();
assert_eq!(s2.start(), s.start());
assert_eq!(s2.len(), 16);
assert_eq!(alloc.len().unwrap(), ALFF_HDR_OFFSET + 16 + BLOCK_OVERHEAD);
}
#[test]
fn realloc_tail_preserves_data() {
let (alloc, path) = mk_ff("realloc_tail_data");
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
s.write(b"hello world!!!!").unwrap();
let s2 = alloc.realloc(s, 32).unwrap();
let data = s2.read().unwrap();
assert_eq!(&data[..15], b"hello world!!!!");
assert_eq!(&data[16..], vec![0u8; 16]);
}
#[test]
fn realloc_same_aligned_len_is_noop() {
let (alloc, path) = mk_ff("realloc_same");
let _g = Guard(path);
let s = alloc.alloc(16).unwrap();
let before_len = alloc.len().unwrap();
let s2 = alloc.realloc(s, 16).unwrap();
assert_eq!(s2.start(), s.start());
assert_eq!(alloc.len().unwrap(), before_len);
}
#[test]
fn realloc_nontail_moves_to_new_block() {
let (alloc, path) = mk_ff("realloc_move");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let _b = alloc.alloc(16).unwrap(); let a_start = a.start();
let a2 = alloc.realloc(a, 64).unwrap(); assert!(a2.start() != a_start); assert_eq!(a2.len(), 64);
}
#[test]
fn realloc_nontail_preserves_data() {
let (alloc, path) = mk_ff("realloc_move_data");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let _b = alloc.alloc(16).unwrap();
a.write(b"preserved!!!!!!!").unwrap();
let a2 = alloc.realloc(a, 32).unwrap();
let data = a2.read().unwrap();
assert_eq!(&data[..16], b"preserved!!!!!!!");
assert_eq!(&data[16..], vec![0u8; 16]);
}
#[test]
fn realloc_nontail_frees_old_block_for_reuse() {
let (alloc, path) = mk_ff("realloc_old_free");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let _b = alloc.alloc(16).unwrap();
let a_start = a.start();
let _a2 = alloc.realloc(a, 64).unwrap();
let reused = alloc.alloc(16).unwrap();
assert_eq!(reused.start(), a_start);
}
#[test]
fn realloc_nontail_same_block_when_fits() {
let (alloc, path) = mk_ff("realloc_inplace");
let _g = Guard(path);
let a = alloc.alloc(64).unwrap();
let _b = alloc.alloc(16).unwrap();
let a_start = a.start();
let a2 = alloc.realloc(a, 32).unwrap();
assert_eq!(a2.start(), a_start); assert_eq!(a2.len(), 32);
}
#[test]
fn realloc_inplace_merge_no_split() {
let (alloc, path) = mk_ff("merge_nosplit");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let _c = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(b).unwrap();
let a2 = alloc.realloc(a, 56).unwrap();
assert_eq!(a2.start(), a_start);
assert_eq!(a2.len(), 56);
}
#[test]
fn realloc_inplace_merge_with_split() {
let (alloc, path) = mk_ff("merge_split");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(80).unwrap();
let _c = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(b).unwrap();
let a2 = alloc.realloc(a, 32).unwrap();
assert_eq!(a2.start(), a_start);
assert_eq!(a2.len(), 32);
let rem = alloc.alloc(64).unwrap();
assert_eq!(rem.start(), a_start + 32 + BLOCK_OVERHEAD);
}
#[test]
fn realloc_inplace_merge_preserves_data_and_zeroes_new_area() {
let (alloc, path) = mk_ff("merge_data");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(64).unwrap();
let _c = alloc.alloc(16).unwrap();
a.write(b"0123456789ABCDEF").unwrap();
alloc.dealloc(b).unwrap();
let a2 = alloc.realloc(a, 40).unwrap();
let data = a2.read().unwrap();
assert_eq!(&data[..16], b"0123456789ABCDEF");
assert_eq!(&data[16..], vec![0u8; 24]);
}
#[test]
fn realloc_inplace_merge_split_remainder_is_zero_initialised() {
let (alloc, path) = mk_ff("merge_rem_zero");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(80).unwrap();
let _c = alloc.alloc(16).unwrap();
b.write(&vec![0xFFu8; 80]).unwrap();
alloc.dealloc(b).unwrap();
let _a2 = alloc.realloc(a, 32).unwrap(); let rem = alloc.alloc(64).unwrap();
assert_eq!(rem.read().unwrap(), vec![0u8; 64]);
}
#[test]
fn realloc_inplace_merge_threshold_boundary() {
let (alloc, path) = mk_ff("merge_boundary");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(56).unwrap();
let _c = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(b).unwrap();
let a2 = alloc.realloc(a, 56).unwrap();
assert_eq!(a2.start(), a_start);
assert_eq!(a2.len(), 56);
let rem = alloc.alloc(16).unwrap();
assert_eq!(rem.start(), a_start + 56 + BLOCK_OVERHEAD);
}
#[test]
fn realloc_inplace_merge_below_threshold_no_split() {
let (alloc, path) = mk_ff("merge_nosplit_thresh");
let _g = Guard(path);
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(48).unwrap();
let _c = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(b).unwrap();
let a2 = alloc.realloc(a, 56).unwrap();
assert_eq!(a2.start(), a_start);
assert_eq!(a2.len(), 56);
let before = alloc.len().unwrap();
let _x = alloc.alloc(16).unwrap();
assert!(alloc.len().unwrap() > before);
}
#[test]
fn recovery_partial_split_repairs_header() {
static C: AtomicU64 = AtomicU64::new(0);
let id = C.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
let path = std::env::temp_dir().join(format!(
"bstack_ff_partial_split_{}_{}.bin",
std::process::id(),
id
));
let _g = Guard(path.clone());
{
let stack = BStack::open(&path).unwrap();
let mut alff = [0u8; 48];
alff[16..24].copy_from_slice(b"ALFF\x00\x01\x01\x00");
alff[24..28].copy_from_slice(&1u32.to_le_bytes()); stack.push(&alff).unwrap();
let mut a_hdr = [0u8; 16];
a_hdr[..8].copy_from_slice(&80u64.to_le_bytes());
stack.push(&a_hdr).unwrap();
let mut a_pay = [0u8; 80];
a_pay[32..40].copy_from_slice(&32u64.to_le_bytes());
a_pay[40..48].copy_from_slice(&24u64.to_le_bytes());
a_pay[48..52].copy_from_slice(&1u32.to_le_bytes());
stack.push(&a_pay).unwrap();
stack.push(&24u64.to_le_bytes()).unwrap();
let mut sent = [0u8; 40];
sent[..8].copy_from_slice(&16u64.to_le_bytes());
sent[32..40].copy_from_slice(&16u64.to_le_bytes());
stack.push(&sent).unwrap();
}
let alloc = FirstFitBStackAllocator::new(BStack::open(&path).unwrap()).unwrap();
let s = alloc.alloc(24).unwrap();
assert_eq!(s.start(), 120); }
#[test]
fn alloc_persists_across_reopen() {
let (alloc, path) = mk_ff("persist");
let _g = Guard(path.clone());
let s = alloc.alloc(8).unwrap();
s.write(b"durably!").unwrap();
let start = s.start();
drop(alloc.into_stack());
let stack2 = BStack::open(&path).unwrap();
let alloc2 = FirstFitBStackAllocator::new(stack2).unwrap();
let mut buf = [0u8; 8];
alloc2.stack().get_into(start, &mut buf).unwrap();
assert_eq!(&buf, b"durably!");
}
#[test]
fn free_list_persists_across_reopen() {
let (alloc, path) = mk_ff("persist_free");
let _g = Guard(path.clone());
let a = alloc.alloc(16).unwrap();
let _b = alloc.alloc(16).unwrap();
let a_start = a.start();
alloc.dealloc(a).unwrap();
drop(alloc.into_stack());
let stack2 = BStack::open(&path).unwrap();
let alloc2 = FirstFitBStackAllocator::new(stack2).unwrap();
let reused = alloc2.alloc(16).unwrap();
assert_eq!(reused.start(), a_start);
}
#[test]
fn recovery_rebuilds_free_list_after_corruption() {
let (alloc, path) = mk_ff("recovery");
let _g = Guard(path.clone());
let a = alloc.alloc(16).unwrap();
let b = alloc.alloc(16).unwrap();
let _c = alloc.alloc(16).unwrap();
let a_start = a.start();
let _b_start = b.start();
alloc.dealloc(a).unwrap();
alloc.dealloc(b).unwrap();
let stack = alloc.into_stack();
stack.set(24, &1u32.to_le_bytes()).unwrap(); stack
.set(FREE_HEAD_OFFSET, &0xDEADBEEFu64.to_le_bytes())
.unwrap();
drop(stack);
let stack2 = BStack::open(&path).unwrap();
let alloc2 = FirstFitBStackAllocator::new(stack2).unwrap();
let r1 = alloc2.alloc(16).unwrap();
let r2 = alloc2.alloc(16).unwrap();
let mut starts = [r1.start(), r2.start()];
starts.sort();
let mut expected = [a_start, a_start + 40];
expected.sort();
assert_eq!(starts, expected);
}
#[test]
fn recovery_truncates_partial_tail_block() {
use std::io::Write;
let (alloc, path) = mk_ff("recovery_trunc");
let _g = Guard(path.clone());
let _a = alloc.alloc(16).unwrap();
let stack = alloc.into_stack();
let before_len = stack.len().unwrap();
drop(stack);
{
use std::fs::OpenOptions;
let mut f = OpenOptions::new().append(true).open(&path).unwrap();
f.write_all(&[0u8; 12]).unwrap(); }
{
use std::fs::OpenOptions;
use std::io::{Seek, SeekFrom};
let mut f = OpenOptions::new().write(true).open(&path).unwrap();
f.seek(SeekFrom::Start(16 + 24)).unwrap(); f.write_all(&1u32.to_le_bytes()).unwrap();
}
let stack2 = BStack::open(&path).unwrap();
let alloc2 = FirstFitBStackAllocator::new(stack2).unwrap();
assert_eq!(alloc2.len().unwrap(), before_len);
}
#[test]
fn into_stack_returns_underlying_bstack() {
let (alloc, path) = mk_ff("into_stack");
let _g = Guard(path);
let _ = alloc.alloc(16).unwrap();
let stack = alloc.into_stack();
assert!(stack.len().unwrap() > ALFF_HDR_OFFSET);
}
#[test]
fn stack_accessor_exposes_raw_reads() {
let (alloc, path) = mk_ff("stack_acc");
let _g = Guard(path);
let s = alloc.alloc(8).unwrap();
s.write(b"testdata").unwrap();
let raw = alloc.stack().get(s.start(), s.start() + 8).unwrap();
assert_eq!(raw, b"testdata");
}
}
#[cfg(all(test, feature = "atomic"))]
mod atomic_tests {
use crate::BStack;
use std::io::ErrorKind;
use std::sync::atomic::{AtomicU64, Ordering};
fn mk_stack() -> (BStack, std::path::PathBuf) {
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let pid = std::process::id();
let path = std::env::temp_dir().join(format!("bstack_atomic_test_{pid}_{id}.bin"));
let stack = BStack::open(&path).unwrap();
(stack, path)
}
struct Guard(std::path::PathBuf);
impl Drop for Guard {
fn drop(&mut self) {
let _ = std::fs::remove_file(&self.0);
}
}
#[test]
fn atrunc_net_truncation() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.atrunc(7, b"XY").unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"helXY");
}
#[test]
fn atrunc_net_extension() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.atrunc(2, b"WORLD").unwrap();
assert_eq!(s.len().unwrap(), 8);
assert_eq!(s.peek(0).unwrap(), b"helWORLD");
}
#[test]
fn atrunc_same_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.atrunc(5, b"WORLD").unwrap();
assert_eq!(s.len().unwrap(), 10);
assert_eq!(s.peek(0).unwrap(), b"helloWORLD");
}
#[test]
fn atrunc_n_zero_pure_append() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.atrunc(0, b"!!").unwrap();
assert_eq!(s.len().unwrap(), 7);
assert_eq!(s.peek(0).unwrap(), b"hello!!");
}
#[test]
fn atrunc_buf_empty_pure_discard() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.atrunc(4, b"").unwrap();
assert_eq!(s.len().unwrap(), 6);
assert_eq!(s.peek(0).unwrap(), b"hellow");
}
#[test]
fn atrunc_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.atrunc(0, b"").unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn atrunc_to_empty_then_fill() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.atrunc(5, b"new").unwrap();
assert_eq!(s.len().unwrap(), 3);
assert_eq!(s.peek(0).unwrap(), b"new");
}
#[test]
fn atrunc_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.atrunc(10, b"x").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn atrunc_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.atrunc(5, b"AB").unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 7);
assert_eq!(s2.peek(0).unwrap(), b"helloAB");
}
#[test]
fn splice_returns_popped_bytes() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let removed = s.splice(5, b"XYZ").unwrap();
assert_eq!(removed, b"world");
}
#[test]
fn splice_net_extension_updates_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let removed = s.splice(2, b"LONG!!").unwrap();
assert_eq!(removed, b"lo");
assert_eq!(s.len().unwrap(), 9);
assert_eq!(s.peek(0).unwrap(), b"helLONG!!");
}
#[test]
fn splice_net_truncation_correct_bytes() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcdefghij").unwrap(); let removed = s.splice(6, b"XX").unwrap(); assert_eq!(removed, b"efghij"); assert_eq!(s.len().unwrap(), 6); assert_eq!(s.peek(0).unwrap(), b"abcdXX");
}
#[test]
fn splice_same_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let removed = s.splice(5, b"WORLD").unwrap();
assert_eq!(removed, b"world");
assert_eq!(s.len().unwrap(), 10);
assert_eq!(s.peek(0).unwrap(), b"helloWORLD");
}
#[test]
fn splice_n_zero_returns_empty_appends_buf() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let removed = s.splice(0, b"!!").unwrap();
assert_eq!(removed, b"");
assert_eq!(s.len().unwrap(), 7);
assert_eq!(s.peek(0).unwrap(), b"hello!!");
}
#[test]
fn splice_buf_empty_acts_like_pop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let removed = s.splice(5, b"").unwrap();
assert_eq!(removed, b"world");
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn splice_noop_returns_empty() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let removed = s.splice(0, b"").unwrap();
assert_eq!(removed, b"");
assert_eq!(s.len().unwrap(), 5);
}
#[test]
fn splice_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let err = s.splice(10, b"x").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn splice_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
let removed = s.splice(5, b"XYZ").unwrap();
assert_eq!(removed, b"world");
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 8);
assert_eq!(s2.peek(0).unwrap(), b"helloXYZ");
}
#[test]
fn splice_into_fills_old_appends_new() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut old = [0u8; 5];
s.splice_into(&mut old, b"XYZ").unwrap();
assert_eq!(&old, b"world");
assert_eq!(s.len().unwrap(), 8);
assert_eq!(s.peek(0).unwrap(), b"helloXYZ");
}
#[test]
fn splice_into_net_extension() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let mut old = [0u8; 2];
s.splice_into(&mut old, b"EXTENDED").unwrap();
assert_eq!(&old, b"lo");
assert_eq!(s.len().unwrap(), 11);
assert_eq!(s.peek(0).unwrap(), b"helEXTENDED");
}
#[test]
fn splice_into_net_truncation() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcdefghij").unwrap();
let mut old = [0u8; 7];
s.splice_into(&mut old, b"XY").unwrap();
assert_eq!(&old, b"defghij");
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"abcXY");
}
#[test]
fn splice_into_matches_splice() {
let (s1, p1) = mk_stack();
let _g1 = Guard(p1);
let (s2, p2) = mk_stack();
let _g2 = Guard(p2);
s1.push(b"helloworld").unwrap();
s2.push(b"helloworld").unwrap();
let vec_removed = s1.splice(4, b"ABCD").unwrap();
let mut buf_removed = [0u8; 4];
s2.splice_into(&mut buf_removed, b"ABCD").unwrap();
assert_eq!(vec_removed.as_slice(), &buf_removed);
assert_eq!(s1.len().unwrap(), s2.len().unwrap());
assert_eq!(s1.peek(0).unwrap(), s2.peek(0).unwrap());
}
#[test]
fn splice_into_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abc").unwrap();
let mut old = [0u8; 10];
let err = s.splice_into(&mut old, b"x").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 3);
}
#[test]
fn try_extend_matching_size_appends_returns_true() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.try_extend(5, b"world").unwrap();
assert!(ok);
assert_eq!(s.len().unwrap(), 10);
assert_eq!(s.peek(0).unwrap(), b"helloworld");
}
#[test]
fn try_extend_mismatching_size_returns_false() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.try_extend(3, b"world").unwrap();
assert!(!ok);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn try_extend_empty_buf_matching_returns_true() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.try_extend(5, b"").unwrap();
assert!(ok);
assert_eq!(s.len().unwrap(), 5);
}
#[test]
fn try_extend_empty_buf_mismatching_returns_false() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.try_extend(0, b"").unwrap();
assert!(!ok);
assert_eq!(s.len().unwrap(), 5);
}
#[test]
fn try_extend_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"hello").unwrap();
s.try_extend(5, b"world").unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"helloworld");
}
#[test]
fn try_discard_matching_size_discards_returns_true() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let ok = s.try_discard(10, 5).unwrap();
assert!(ok);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn try_discard_mismatching_size_returns_false() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let ok = s.try_discard(7, 5).unwrap();
assert!(!ok);
assert_eq!(s.len().unwrap(), 10);
}
#[test]
fn try_discard_n_zero_matching_returns_true() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.try_discard(5, 0).unwrap();
assert!(ok);
assert_eq!(s.len().unwrap(), 5);
}
#[test]
fn try_discard_n_zero_mismatching_returns_false() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.try_discard(3, 0).unwrap();
assert!(!ok);
assert_eq!(s.len().unwrap(), 5);
}
#[test]
fn try_discard_n_exceeds_size_when_matching_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.try_discard(5, 10).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 5);
}
#[test]
fn try_discard_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.try_discard(10, 5).unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 5);
assert_eq!(s2.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn swap_returns_old_stores_new() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let old = s.swap(5, b"WORLD").unwrap();
assert_eq!(old, b"world");
assert_eq!(s.peek(0).unwrap(), b"helloWORLD");
}
#[cfg(feature = "set")]
#[test]
fn swap_empty_buf_returns_empty_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let old = s.swap(0, b"").unwrap();
assert_eq!(old, b"");
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn swap_at_start_offset() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let old = s.swap(0, b"HELLO").unwrap();
assert_eq!(old, b"hello");
assert_eq!(s.peek(0).unwrap(), b"HELLOworld");
}
#[cfg(feature = "set")]
#[test]
fn swap_does_not_change_file_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.swap(1, b"XYZ").unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"aXYZe");
}
#[cfg(feature = "set")]
#[test]
fn swap_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.swap(3, b"TOOLONG").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn swap_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.swap(5, b"WORLD").unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"helloWORLD");
}
#[cfg(feature = "set")]
#[test]
fn swap_into_fills_buf_with_old_stores_new() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut buf = *b"WORLD";
s.swap_into(5, &mut buf).unwrap();
assert_eq!(&buf, b"world");
assert_eq!(s.peek(0).unwrap(), b"helloWORLD");
}
#[cfg(feature = "set")]
#[test]
fn swap_into_empty_buf_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.swap_into(0, &mut []).unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn swap_into_matches_swap() {
let (s1, p1) = mk_stack();
let _g1 = Guard(p1);
let (s2, p2) = mk_stack();
let _g2 = Guard(p2);
s1.push(b"helloworld").unwrap();
s2.push(b"helloworld").unwrap();
let vec_old = s1.swap(3, b"XYZ").unwrap();
let mut buf = *b"XYZ";
s2.swap_into(3, &mut buf).unwrap();
assert_eq!(vec_old.as_slice(), &buf);
assert_eq!(s1.peek(0).unwrap(), s2.peek(0).unwrap());
}
#[cfg(feature = "set")]
#[test]
fn swap_into_does_not_change_file_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
let mut buf = *b"XYZ";
s.swap_into(1, &mut buf).unwrap();
assert_eq!(s.len().unwrap(), 5);
}
#[cfg(feature = "set")]
#[test]
fn swap_into_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let mut buf = [0u8; 10];
let err = s.swap_into(0, &mut buf).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn cas_matching_performs_exchange() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let ok = s.cas(5, b"world", b"WORLD").unwrap();
assert!(ok);
assert_eq!(s.peek(0).unwrap(), b"helloWORLD");
}
#[cfg(feature = "set")]
#[test]
fn cas_mismatch_returns_false_no_change() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let ok = s.cas(5, b"xxxxx", b"WORLD").unwrap();
assert!(!ok);
assert_eq!(s.peek(0).unwrap(), b"helloworld");
}
#[cfg(feature = "set")]
#[test]
fn cas_length_mismatch_returns_false() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.cas(0, b"hel", b"HELLO").unwrap();
assert!(!ok);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn cas_empty_slices_returns_true_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let ok = s.cas(0, b"", b"").unwrap();
assert!(ok);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn cas_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.cas(3, b"TOOLONG", b"TOOLONG").unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn cas_does_not_change_file_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.cas(1, b"bcd", b"XYZ").unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"aXYZe");
}
#[cfg(feature = "set")]
#[test]
fn cas_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.cas(5, b"world", b"WORLD").unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"helloWORLD");
}
#[test]
fn replace_same_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello world").unwrap();
s.replace(5, |tail| {
tail.iter().map(|b| b.to_ascii_uppercase()).collect()
})
.unwrap();
assert_eq!(s.len().unwrap(), 11);
assert_eq!(s.peek(0).unwrap(), b"hello WORLD");
}
#[test]
fn replace_net_extension() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.replace(2, |_| b"WORLD".to_vec()).unwrap();
assert_eq!(s.len().unwrap(), 8);
assert_eq!(s.peek(0).unwrap(), b"helWORLD");
}
#[test]
fn replace_net_truncation() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.replace(7, |_| b"XY".to_vec()).unwrap();
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"helXY");
}
#[test]
fn replace_n_zero_acts_as_append() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
s.replace(0, |_| b"!!".to_vec()).unwrap();
assert_eq!(s.len().unwrap(), 7);
assert_eq!(s.peek(0).unwrap(), b"hello!!");
}
#[test]
fn replace_empty_result_acts_as_discard() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
s.replace(4, |_| vec![]).unwrap();
assert_eq!(s.len().unwrap(), 6);
assert_eq!(s.peek(0).unwrap(), b"hellow");
}
#[test]
fn replace_callback_receives_correct_bytes() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut captured = Vec::new();
s.replace(5, |tail| {
captured = tail.to_vec();
tail.to_vec()
})
.unwrap();
assert_eq!(captured, b"world");
assert_eq!(s.peek(0).unwrap(), b"helloworld");
}
#[test]
fn replace_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.replace(10, |_| vec![]).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[test]
fn replace_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.replace(5, |tail| {
tail.iter().map(|b| b.to_ascii_uppercase()).collect()
})
.unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.len().unwrap(), 10);
assert_eq!(s2.peek(0).unwrap(), b"helloWORLD");
}
#[cfg(feature = "set")]
#[test]
fn process_mutates_range() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello world").unwrap();
s.process(6, 11, |buf| buf.make_ascii_uppercase()).unwrap();
assert_eq!(s.len().unwrap(), 11);
assert_eq!(s.peek(0).unwrap(), b"hello WORLD");
}
#[cfg(feature = "set")]
#[test]
fn process_middle_range() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcdefgh").unwrap();
s.process(2, 5, |buf| buf.iter_mut().for_each(|b| *b = b'X'))
.unwrap();
assert_eq!(s.peek(0).unwrap(), b"abXXXfgh");
}
#[cfg(feature = "set")]
#[test]
fn process_callback_receives_correct_bytes() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"helloworld").unwrap();
let mut captured = Vec::new();
s.process(5, 10, |buf| captured = buf.to_vec()).unwrap();
assert_eq!(captured, b"world");
assert_eq!(s.peek(0).unwrap(), b"helloworld");
}
#[cfg(feature = "set")]
#[test]
fn process_start_end_equal_is_noop() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let mut called = false;
s.process(3, 3, |_| called = true).unwrap();
assert!(called);
assert_eq!(s.len().unwrap(), 5);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn process_does_not_change_file_size() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"abcde").unwrap();
s.process(1, 4, |buf| buf.iter_mut().for_each(|b| *b = 0))
.unwrap();
assert_eq!(s.len().unwrap(), 5);
}
#[cfg(feature = "set")]
#[test]
fn process_end_less_than_start_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.process(3, 2, |_| {}).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn process_end_exceeds_size_returns_error() {
let (s, p) = mk_stack();
let _g = Guard(p);
s.push(b"hello").unwrap();
let err = s.process(2, 10, |_| {}).unwrap_err();
assert_eq!(err.kind(), ErrorKind::InvalidInput);
assert_eq!(s.peek(0).unwrap(), b"hello");
}
#[cfg(feature = "set")]
#[test]
fn process_persists_across_reopen() {
let (s, p) = mk_stack();
let _g = Guard(p.clone());
s.push(b"helloworld").unwrap();
s.process(5, 10, |buf| buf.make_ascii_uppercase()).unwrap();
drop(s);
let s2 = BStack::open(&p).unwrap();
assert_eq!(s2.peek(0).unwrap(), b"helloWORLD");
}
}