use std::alloc::{GlobalAlloc, Layout, System};
use std::cell::Cell;
use structio::beve;
thread_local! {
static LIVE: Cell<isize> = const { Cell::new(0) };
static PEAK: Cell<isize> = const { Cell::new(0) };
}
struct Tracking;
fn note(delta: isize) {
let _ = LIVE.try_with(|live| {
let now = live.get() + delta;
live.set(now);
let _ = PEAK.try_with(|peak| peak.set(peak.get().max(now)));
});
}
fn measuring() -> isize {
let base = LIVE.get();
PEAK.set(base);
base
}
fn peak_over(base: isize) -> usize {
PEAK.get().saturating_sub(base).max(0) as usize
}
unsafe impl GlobalAlloc for Tracking {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let p = unsafe { System.alloc(layout) };
if !p.is_null() {
note(layout.size() as isize);
}
p
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
unsafe { System.dealloc(ptr, layout) };
note(-(layout.size() as isize));
}
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let p = unsafe { System.realloc(ptr, layout, new_size) };
if !p.is_null() {
note(new_size as isize - layout.size() as isize);
}
p
}
}
#[global_allocator]
static ALLOC: Tracking = Tracking;
#[derive(Default)]
struct Blob {
text: String,
tail: String,
}
structio::object!(Blob { text, tail });
struct Discard(usize);
impl std::io::Write for Discard {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.0 += buf.len();
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_sink_writer_does_not_grow_to_hold_the_largest_value() {
const CAP: usize = 512;
const BIG: usize = 4 * 1024 * 1024;
let value = Blob {
text: "x".repeat(BIG),
tail: "y".repeat(40),
};
let mut sink = Discard(0);
let base = measuring();
beve::to_writer_buffered(&value, &mut sink, CAP).unwrap();
let peak = peak_over(base);
assert_eq!(
sink.0,
structio::to_beve(&value).len(),
"wrote the wrong bytes"
);
assert!(
peak <= CAP,
"writing a {BIG}-byte value through a {CAP}-byte buffer peaked at {peak} bytes"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn streaming_an_array_does_not_hold_the_file() {
const ELEMENTS: usize = 512 * 1024;
let file = structio::to_beve(&(0..ELEMENTS).map(|i| i as f64).collect::<Vec<f64>>());
assert!(file.len() > 4 * 1024 * 1024);
let mut sum = 0.0;
let base = measuring();
let mut docs = beve::Documents::array(&file[..]);
let mut value = 0.0f64;
while let Some(result) = docs.next_value_into(&mut value) {
result.unwrap();
sum += value;
}
let peak = peak_over(base);
assert_eq!(sum, (0..ELEMENTS).map(|i| i as f64).sum::<f64>());
assert!(
peak < 256 * 1024,
"streaming a {}-byte file peaked at {peak} bytes",
file.len()
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn measuring_a_value_allocates_nothing() {
const BIG: usize = 4 * 1024 * 1024;
let value = Blob {
text: "x".repeat(BIG),
tail: "y".repeat(40),
};
let samples = vec![1.5f64; BIG / 8];
let base = measuring();
let size = beve::size(&value);
let aligned = beve::size_aligned(&samples);
let framed = beve::size_aligned_after(&samples, 12);
let peak = peak_over(base);
assert_eq!(size, structio::to_beve(&value).len());
assert_eq!(aligned, structio::to_beve_aligned(&samples).len());
let mut behind_a_header = vec![0u8; 12];
beve::append_aligned(&samples, &mut behind_a_header);
assert_eq!(framed, behind_a_header.len() - 12);
assert_ne!(framed, aligned);
assert_eq!(peak, 0, "measuring asked for {peak} bytes");
}
#[test]
#[cfg_attr(miri, ignore)]
fn appending_a_listing_allocates_nothing() {
const ENTRIES: usize = 64;
const ENTRY: usize = 4096;
let value = Blob {
text: "x".repeat(ENTRY),
tail: "y".repeat(ENTRY),
};
let mut listing = Vec::with_capacity(48 + 1 + ENTRIES * (2 * ENTRY + 64));
listing.extend_from_slice(&[0u8; 48]); listing.push(b'[');
let ptr = listing.as_ptr();
let base = measuring();
for _ in 0..ENTRIES {
structio::append(&value, &mut listing);
listing.push(b',');
}
let peak = peak_over(base);
*listing.last_mut().unwrap() = b']';
assert_eq!(peak, 0, "appending asked for {peak} bytes");
assert!(std::ptr::eq(listing.as_ptr(), ptr));
assert_eq!(&listing[..48], &[0u8; 48]);
assert_eq!(
structio::from_slice::<Vec<Blob>>(&listing[48..])
.unwrap()
.len(),
ENTRIES
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn reading_an_array_from_a_reader_does_not_hold_the_encoding() {
const ELEMENTS: usize = 512 * 1024;
const PAYLOAD: usize = ELEMENTS * size_of::<f64>();
let file = structio::to_beve(&(0..ELEMENTS).map(|i| i as f64).collect::<Vec<f64>>());
assert!(file.len() > PAYLOAD);
let base = measuring();
let slurped: Vec<f64> = beve::from_reader(&file[..]).unwrap();
let slurped_peak = peak_over(base);
drop(slurped);
let base = measuring();
let mut streamed: Vec<f64> = Vec::new();
beve::read_array_into(&mut streamed, &file[..]).unwrap();
let streamed_peak = peak_over(base);
assert_eq!(streamed.len(), ELEMENTS);
assert_eq!(streamed[ELEMENTS - 1], (ELEMENTS - 1) as f64);
assert_eq!(streamed.capacity(), ELEMENTS);
assert!(
streamed_peak < PAYLOAD + 256 * 1024,
"streaming a {PAYLOAD}-byte array peaked at {streamed_peak} bytes"
);
assert!(
slurped_peak > streamed_peak + PAYLOAD / 2,
"slurping peaked at {slurped_peak} bytes against {streamed_peak}"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn reading_a_complex_array_from_a_reader_does_not_hold_the_encoding() {
const ELEMENTS: usize = 256 * 1024;
const PAYLOAD: usize = ELEMENTS * size_of::<structio::Complex<f64>>();
let file = structio::to_beve(
&(0..ELEMENTS)
.map(|i| structio::Complex {
re: i as f64,
im: -(i as f64),
})
.collect::<Vec<_>>(),
);
assert!(file.len() > PAYLOAD);
let base = measuring();
let slurped: Vec<structio::Complex<f64>> = beve::from_reader(&file[..]).unwrap();
let slurped_peak = peak_over(base);
drop(slurped);
let base = measuring();
let mut streamed: Vec<structio::Complex<f64>> = Vec::new();
beve::read_array_into(&mut streamed, &file[..]).unwrap();
let streamed_peak = peak_over(base);
assert_eq!(streamed.len(), ELEMENTS);
assert_eq!(
streamed[ELEMENTS - 1],
structio::Complex {
re: (ELEMENTS - 1) as f64,
im: -((ELEMENTS - 1) as f64),
}
);
assert_eq!(streamed.capacity(), ELEMENTS);
assert!(
streamed_peak < PAYLOAD + 256 * 1024,
"streaming a {PAYLOAD}-byte complex array peaked at {streamed_peak} bytes"
);
assert!(
slurped_peak > streamed_peak + PAYLOAD / 2,
"slurping peaked at {slurped_peak} bytes against {streamed_peak}"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn a_lying_count_allocates_what_arrives_and_not_what_it_claims() {
use structio::beve::header;
let mut doc = vec![header::array_of(header::CAT_FLOAT, 3)];
let mut size = [0u8; 8];
let used = header::encode_size(4_000_000_000, &mut size);
doc.extend_from_slice(&size[..used]);
doc.extend_from_slice(&[0u8; 1024]);
let base = measuring();
let err = beve::from_reader_array::<f64, _>(&doc[..]).unwrap_err();
let peak = peak_over(base);
assert_eq!(
err.as_parse().unwrap().code,
structio::ErrorCode::UnexpectedEnd
);
assert!(
peak < 4 * 1024 * 1024,
"a count of four billion elements asked for {peak} bytes"
);
}
#[test]
#[cfg_attr(miri, ignore)]
fn the_read_size_bounds_the_window() {
let file = structio::to_beve(&vec![1.5f64, 2.5]);
let base = measuring();
let mut docs = beve::Documents::array(&file[..]).read_size(256);
let mut value = 0.0f64;
let mut got = Vec::new();
while let Some(result) = docs.next_value_into(&mut value) {
result.unwrap();
got.push(value);
}
let peak = peak_over(base);
assert_eq!(got, [1.5, 2.5]);
assert!(peak < 1024, "a 256-byte read size peaked at {peak} bytes");
}