use leto::{ArrayViewMut, Layout, LetoError};
fn assert_overflow<T: std::fmt::Debug>(result: Result<T, LetoError>, case: &str) {
match result {
Err(LetoError::Overflow { reason }) => {
assert!(
!reason.is_empty(),
"{case}: Overflow must name the failing arithmetic"
);
}
other => panic!("{case}: expected LetoError::Overflow, got {other:?}"),
}
}
fn assert_storage_error<T: std::fmt::Debug>(result: Result<T, LetoError>, case: &str) {
match result {
Err(LetoError::StorageError { reason }) => {
assert!(
!reason.is_empty(),
"{case}: StorageError must name the violated bound"
);
}
other => panic!("{case}: expected LetoError::StorageError, got {other:?}"),
}
}
#[test]
fn shape_product_overflowing_usize_is_rejected() {
let half = 1usize << 32;
assert_overflow(
Layout::<2>::try_new([half, half], [1, 1], 0),
"shape product 2^64",
);
}
#[test]
fn maximal_shape_product_overflows_on_every_axis_ordering() {
assert_overflow(
Layout::<3>::try_new([usize::MAX, 2, 2], [1, 1, 1], 0),
"leading maximal extent",
);
assert_overflow(
Layout::<3>::try_new([2, 2, usize::MAX], [1, 1, 1], 0),
"trailing maximal extent",
);
}
#[test]
fn stride_extent_product_overflowing_isize_is_rejected() {
assert_overflow(
Layout::<1>::try_new([4], [isize::MAX / 2], 0),
"stride-extent product",
);
}
#[test]
fn accumulated_offset_overflowing_isize_is_rejected() {
let big = isize::MAX / 2;
assert_overflow(
Layout::<3>::try_new([2, 2, 2], [big, big, big], 0),
"accumulated maximum offset",
);
}
#[test]
fn base_offset_beyond_isize_is_rejected() {
let past_isize = (isize::MAX as usize) + 1;
assert_overflow(
Layout::<1>::try_new([1], [1], past_isize),
"base offset conversion",
);
}
#[test]
fn negative_stride_underrunning_the_base_offset_is_rejected() {
assert_storage_error(
Layout::<1>::try_new([4], [-1], 2),
"negative stride underruns base",
);
}
#[test]
fn negative_stride_within_the_base_offset_is_accepted() {
let layout = Layout::<1>::try_new([4], [-1], 3).expect("reverse view over [0, 3] is valid");
assert_eq!(layout.min_max_offsets(), (0, 3));
}
#[test]
fn mixed_sign_strides_are_evaluated_on_the_minimum_not_the_sum() {
assert_storage_error(
Layout::<2>::try_new([2, 4], [10, -4], 5),
"mixed-sign minimum offset",
);
}
#[test]
fn zero_extent_axis_collapses_the_addressed_span() {
let layout = Layout::<2>::try_new([0, 4], [isize::MAX, 1], 0)
.expect("an empty layout addresses no element");
assert_eq!(layout.size(), 0);
assert_eq!(layout.min_max_offsets(), (0, 0));
}
#[test]
fn zero_extent_axis_with_negative_stride_is_accepted() {
let layout =
Layout::<2>::try_new([3, 0], [-100, -100], 0).expect("no addressable element to underrun");
assert_eq!(layout.size(), 0);
}
#[test]
fn rank_zero_layout_is_valid() {
let layout = Layout::<0>::try_new([], [], 7).expect("rank-0 scalar layout");
assert_eq!(layout.size(), 1);
assert_eq!(layout.offset(), 7);
}
#[test]
fn unit_extent_axes_ignore_their_strides() {
let layout = Layout::<2>::try_new([1, 3], [isize::MAX, 1], 0)
.expect("unit extent never applies its stride");
assert_eq!(layout.min_max_offsets(), (0, 2));
}
#[test]
fn try_from_tuple_matches_try_new() {
let parts = ([2usize, 3], [3isize, 1], 4usize);
let from_try_new = Layout::<2>::try_new(parts.0, parts.1, parts.2).expect("valid layout");
let from_try_from = Layout::<2>::try_from(parts).expect("valid layout");
assert_eq!(from_try_new, from_try_from);
let bad = ([4usize], [-1isize], 2usize);
assert_storage_error(Layout::<1>::try_from(bad), "TryFrom rejects underrun");
}
#[test]
fn accessors_round_trip_the_constructed_parts() {
let layout = Layout::<3>::try_new([2, 3, 4], [12, 4, 1], 5).expect("valid layout");
assert_eq!(layout.shape(), [2, 3, 4]);
assert_eq!(layout.strides(), [12, 4, 1]);
assert_eq!(layout.offset(), 5);
}
#[test]
fn deserialization_routes_through_validation() {
let hostile = r#"{"shape":[4],"strides":[-1],"offset":2}"#;
let decoded: Result<Layout<1>, _> = serde_json::from_str(hostile);
let message = decoded
.expect_err("negative-underrun payload must be rejected")
.to_string();
assert!(
message.contains("negative physical offset"),
"deserialization error must name the violated bound, got: {message}"
);
let sound = Layout::<1>::try_new([4], [-1], 3).expect("valid reverse layout");
let encoded = serde_json::to_string(&sound).expect("serialize");
let round_tripped: Layout<1> = serde_json::from_str(&encoded).expect("deserialize");
assert_eq!(round_tripped, sound);
}
#[test]
#[should_panic(expected = "exceeds backing length")]
fn index_past_the_backing_buffer_panics_rather_than_reading_out_of_bounds() {
let mut data = [0u32; 4];
let layout = Layout::c_contiguous([1000]).expect("c-contiguous layout is self-consistent");
let view = ArrayViewMut::<u32, 1>::new(layout, &mut data);
let _ = view[999usize];
}
#[test]
#[should_panic(expected = "exceeds backing length")]
fn index_mut_past_the_backing_buffer_panics_rather_than_writing_out_of_bounds() {
let mut data = [0u32; 4];
let layout = Layout::c_contiguous([1000]).expect("c-contiguous layout is self-consistent");
let mut view = ArrayViewMut::<u32, 1>::new(layout, &mut data);
view[999usize] = 0xDEAD_BEEF;
}
#[test]
#[should_panic(expected = "exceeds backing length")]
fn rank_two_index_past_the_backing_buffer_panics() {
let mut data = [0u32; 4];
let layout = Layout::c_contiguous([100, 100]).expect("c-contiguous layout is self-consistent");
let mut view = ArrayViewMut::<u32, 2>::new(layout, &mut data);
view[[99, 99]] = 1;
}
#[test]
fn get_reports_an_over_long_layout_as_a_typed_error() {
let mut data = [0u32; 4];
let layout = Layout::c_contiguous([1000]).expect("c-contiguous layout is self-consistent");
let mut view = ArrayViewMut::<u32, 1>::new(layout, &mut data);
assert_storage_error(view.get([999]).copied(), "get past buffer");
assert_storage_error(view.get_mut([999]).copied(), "get_mut past buffer");
}
#[test]
fn try_new_rejects_a_view_whose_layout_overruns_its_buffer() {
let data = [0u32; 4];
let layout = Layout::c_contiguous([1000]).expect("c-contiguous layout is self-consistent");
assert_storage_error(
leto::ArrayView::<u32, 1>::try_new(layout, &data).map(|_| ()),
"ArrayView::try_new bound check",
);
}
#[test]
fn in_bounds_strided_indexing_is_unaffected() {
let mut data = [10u32, 11, 12, 13, 14, 15];
let layout = Layout::<2>::try_new([2, 3], [3, 1], 0).expect("valid layout");
let mut view = ArrayViewMut::<u32, 2>::new(layout, &mut data);
assert_eq!(view[[0, 0]], 10);
assert_eq!(view[[1, 2]], 15);
view[[1, 0]] = 99;
assert_eq!(view[[1, 0]], 99);
let reverse = Layout::<1>::try_new([3], [-1], 5).expect("valid reverse layout");
let reverse_view = ArrayViewMut::<u32, 1>::new(reverse, &mut data);
assert_eq!(reverse_view[0usize], 15);
assert_eq!(reverse_view[1usize], 14);
assert_eq!(reverse_view[2usize], 99, "aliases the [1, 0] write above");
}