#![allow(
clippy::cast_possible_truncation,
reason = "the file pattern is a byte by construction"
)]
use std::fs::File;
use std::io::Write as _;
use iris_source::{Window, WindowError};
#[cfg(feature = "probe")]
fn handles() -> Option<u32> {
iris_source::probe::handles()
}
#[cfg(not(feature = "probe"))]
fn handles() -> Option<u32> {
None
}
fn pattern(offset: u64) -> u8 {
let mut mixed = offset.wrapping_add(0x9e37_79b9_7f4a_7c15);
mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94d0_49bb_1331_11eb);
((mixed ^ (mixed >> 31)) & 0xff) as u8
}
fn sample(len: u64) -> (tempfile::TempDir, std::path::PathBuf) {
let dir = tempfile::tempdir().expect("a temporary directory");
let path = dir.path().join("sample.bin");
let mut file = File::create(&path).expect("creating the sample");
let mut written = 0u64;
let mut block = vec![0u8; 64 * 1024];
while written < len {
let this = block.len().min((len - written) as usize);
for (i, slot) in block[..this].iter_mut().enumerate() {
*slot = pattern(written + i as u64);
}
file.write_all(&block[..this]).expect("writing the sample");
written += this as u64;
}
file.sync_all().expect("flushing the sample");
(dir, path)
}
fn assert_matches(bytes: &[u8], at: u64) {
for (i, got) in bytes.iter().enumerate() {
let offset = at + i as u64;
assert_eq!(
*got,
pattern(offset),
"byte at offset {offset} came back as {got} and the file holds {}",
pattern(offset)
);
}
}
#[test]
fn a_window_reads_the_bytes_that_are_actually_there() {
let (_dir, path) = sample(1 << 20);
let file = File::open(&path).expect("opening the sample");
let mut window = Window::with_span(file, 128 * 1024).expect("opening a window");
assert_eq!(window.len(), 1 << 20);
assert!(!window.is_empty());
for at in [
0u64,
1,
4095,
4096,
65_535,
65_536,
100_000,
(1 << 20) - 1024,
] {
let len = usize::try_from((1u64 << 20) - at).unwrap().min(1024);
let bytes = window.range(at, len).expect("a range inside the file");
assert_eq!(bytes.len(), len);
assert_matches(bytes, at);
}
}
#[test]
fn a_range_inside_the_current_view_does_not_slide() {
let (_dir, path) = sample(1 << 20);
let file = File::open(&path).expect("opening the sample");
let mut window = Window::with_span(file, 512 * 1024).expect("opening a window");
window.range(0, 16).expect("the first range");
let after_first = window.slides();
assert_eq!(after_first, 1, "the first range has to map something");
for at in (0..4096).step_by(64) {
window.range(at, 64).expect("a range inside the first view");
}
assert_eq!(
window.slides(),
after_first,
"a range inside the view must not remap"
);
}
#[test]
fn the_last_view_of_a_file_covers_the_tail() {
let len = 300_001u64;
let (_dir, path) = sample(len);
let file = File::open(&path).expect("opening the sample");
let mut window = Window::with_span(file, 64 * 1024).expect("opening a window");
let bytes = window.range(len - 1, 1).expect("the last byte");
assert_eq!(bytes[0], pattern(len - 1));
let bytes = window
.range(len - 17, 17)
.expect("the last seventeen bytes");
assert_matches(bytes, len - 17);
let refused = window.range(len, 1).unwrap_err();
assert!(
matches!(refused, WindowError::OutOfBounds { .. }),
"got {refused:?}"
);
let refused = window.range(len - 1, 2).unwrap_err();
assert!(
matches!(refused, WindowError::OutOfBounds { .. }),
"got {refused:?}"
);
}
#[test]
fn an_empty_file_opens_and_serves_nothing() {
let (_dir, path) = sample(0);
let file = File::open(&path).expect("opening the sample");
let mut window = Window::with_span(file, 64 * 1024).expect("opening a window");
assert!(window.is_empty());
assert_eq!(window.range(0, 0).expect("a zero length read"), b"");
assert!(matches!(
window.range(0, 1).unwrap_err(),
WindowError::OutOfBounds { .. }
));
assert_eq!(window.slides(), 0, "there is nothing to map");
}
#[test]
fn a_zero_length_read_at_the_end_of_the_file_maps_nothing() {
let len = 300_001u64;
let (_dir, path) = sample(len);
let file = File::open(&path).expect("opening the sample");
let mut window = Window::with_span(file, 64 * 1024).expect("opening a window");
assert_eq!(
window.range(len, 0).expect("a zero length read at the end"),
b""
);
let bytes = window.range(len - 8, 8).expect("the last eight bytes");
assert_matches(bytes, len - 8);
assert_eq!(
window
.range(len, 0)
.expect("a zero length read after a view exists"),
b""
);
let bytes = window.range(0, 64).expect("a read after the empty read");
assert_matches(bytes, 0);
}
#[test]
fn a_request_larger_than_the_span_is_refused_rather_than_half_served() {
let (_dir, path) = sample(1 << 20);
let file = File::open(&path).expect("opening the sample");
let span = 128 * 1024;
let mut window = Window::with_span(file, span).expect("opening a window");
let refused = window.range(0, span + 1).unwrap_err();
assert!(
matches!(refused, WindowError::TooLarge { .. }),
"got {refused:?}"
);
let refused = window.range(1, span).unwrap_err();
assert!(
matches!(refused, WindowError::TooLarge { .. }),
"got {refused:?}"
);
let bytes = window.range(0, span).expect("a span sized aligned range");
assert_matches(&bytes[..512], 0);
}
#[test]
fn thousands_of_slides_leak_nothing_and_never_go_stale() {
let len = 8 << 20;
let (_dir, path) = sample(len);
let file = File::open(&path).expect("opening the sample");
let span = 128 * 1024;
let mut window = Window::with_span(file, span).expect("opening a window");
let base = window.address();
let reserved = window.span();
let handles_before = handles();
let stride = 163_841u64;
let cycles = 4000u64;
let read = 512usize;
let mut at = 0u64;
for cycle in 0..cycles {
let bytes = window.range(at, read).unwrap_or_else(|e| {
panic!("cycle {cycle} at offset {at} failed: {e}");
});
assert_matches(bytes, at);
if let Some((view_at, view_len)) = window.mapped() {
let last = (view_at + view_len as u64 - 1).min(len - 1);
let bytes = window
.range(last, 1)
.expect("the last byte of the current view");
assert_eq!(
bytes[0],
pattern(last),
"the far end of the view at cycle {cycle}"
);
}
at = (at + stride) % (len - read as u64);
}
assert!(
window.slides() > cycles / 2,
"the stride has to actually move the view"
);
assert_eq!(
window.address(),
base,
"the reservation moved, so it was released and retaken"
);
assert_eq!(window.span(), reserved, "the reservation changed size");
if let (Some(before), Some(after)) = (handles_before, handles()) {
assert!(
after <= before + 8,
"the process held {before} handles before {cycles} slides and {after} after, so a slide \
is keeping one"
);
}
let bytes = window
.range(0, 64)
.expect("a read after the whole stress loop");
assert_matches(bytes, 0);
}
#[test]
#[cfg(feature = "probe")]
fn a_pointer_held_across_a_slide_stops_being_readable() {
use iris_source::probe::readable;
let len = 4 << 20;
let (_dir, path) = sample(len);
let file = File::open(&path).expect("opening the sample");
let span = 256 * 1024;
let mut window = Window::with_span(file, span).expect("opening a window");
let bytes = window.range(0, 64).expect("the first range");
assert_matches(bytes, 0);
let base = window.address();
let (_, mapped) = window.mapped().expect("something is mapped");
assert!(
readable(base, mapped),
"the view has to be readable while it is the view"
);
if mapped < span {
let past = unsafe { base.add(mapped) };
assert!(
!readable(past, 1),
"the unmapped tail of the reservation is readable"
);
}
let far = len - 1024;
let bytes = window
.range(far, 64)
.expect("a range at the end of the file");
assert_matches(bytes, far);
assert!(window.slides() >= 2, "that had to be a slide");
let (view_at, view_len) = window.mapped().expect("something is mapped");
assert!(
view_at > 0,
"the view has to have moved off the front of the file"
);
if view_len < span {
let vacated = unsafe { base.add(view_len) };
assert!(
!readable(vacated, 1),
"an address the view no longer covers is still readable, so a decoder holding it \
would read bytes from the range that used to be there"
);
}
let bytes = window
.range(view_at, 64)
.expect("the start of the current view");
assert_matches(bytes, view_at);
assert_ne!(
bytes[..8],
[
pattern(0),
pattern(1),
pattern(2),
pattern(3),
pattern(4),
pattern(5),
pattern(6),
pattern(7)
],
"the front of the reservation still holds the bytes from before the slide"
);
}
#[test]
#[cfg(target_pointer_width = "64")]
fn opening_and_dropping_many_windows_does_not_exhaust_address_space() {
let (_dir, path) = sample(2 << 20);
let span = 512 << 30;
let rounds = 400;
for round in 0..rounds {
let file = File::open(&path).expect("opening the sample");
let mut window = Window::with_span(file, span).unwrap_or_else(|e| {
panic!(
"round {round} of {rounds} could not reserve half a tebibyte, and every round \
before it was dropped, so the address space they held did not come back: {e}"
);
});
let bytes = window.range(1024, 64).expect("a range");
assert_matches(bytes, 1024);
}
}