diff --git a/all-is-cubes/src/math/grid_aab.rs b/all-is-cubes/src/math/grid_aab.rs index 9efe510c9..53ac6e539 100644 --- a/all-is-cubes/src/math/grid_aab.rs +++ b/all-is-cubes/src/math/grid_aab.rs @@ -187,49 +187,6 @@ impl GridAab { } } - /// Generate a [`GridAab`] whose volume is as specified or smaller. - #[cfg(feature = "arbitrary")] - pub(crate) fn arbitrary_with_max_volume( - u: &mut arbitrary::Unstructured<'_>, - volume: usize, - ) -> arbitrary::Result { - // Pick sizes within the volume constraint. - let mut limit: GridCoordinate = volume.try_into().unwrap_or(GridCoordinate::MAX); - let size_1 = u.int_in_range(0..=limit)?; - limit /= size_1.max(1); - let size_2 = u.int_in_range(0..=limit)?; - limit /= size_2.max(1); - let size_3 = u.int_in_range(0..=limit)?; - - // Shuffle the sizes to remove any bias. - let sizes = *u.choose(&[ - GridVector::new(size_1, size_2, size_3), - GridVector::new(size_1, size_3, size_2), - GridVector::new(size_2, size_1, size_3), - GridVector::new(size_2, size_3, size_1), - GridVector::new(size_3, size_1, size_2), - GridVector::new(size_3, size_2, size_1), - ])?; - - // Compute lower bounds that are valid for the sizes. - let lower_bounds = GridPoint::new( - u.int_in_range(GridCoordinate::MIN..=GridCoordinate::MAX - sizes.x)?, - u.int_in_range(GridCoordinate::MIN..=GridCoordinate::MAX - sizes.y)?, - u.int_in_range(GridCoordinate::MIN..=GridCoordinate::MAX - sizes.z)?, - ); - - Ok(Self::from_lower_size(lower_bounds, sizes)) - } - - #[cfg(feature = "arbitrary")] - const ARBITRARY_SIZE_HINT: (usize, Option) = { - // 6 bounding coordinates plus one permutation selection. - // Depending on the volume we could *maybe* end up consuming only 1 byte each - // for the sizes. - let gc = core::mem::size_of::(); - ((gc + 1) * 3 + 1, Some(gc * 6 + 1)) - }; - /// Compute volume with checked arithmetic. In a function solely for the convenience /// of the `?` operator without which this is even worse. fn checked_volume_helper(sizes: GridVector) -> Result { @@ -744,11 +701,11 @@ impl From for Aab { #[mutants::skip] impl<'a> arbitrary::Arbitrary<'a> for GridAab { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - Self::arbitrary_with_max_volume(u, usize::MAX) + Ok(Vol::<()>::arbitrary_with_max_volume(u, usize::MAX)?.bounds()) } fn size_hint(_depth: usize) -> (usize, Option) { - GridAab::ARBITRARY_SIZE_HINT + crate::math::vol::vol_arb::ARBITRARY_BOUNDS_SIZE_HINT } } @@ -1113,50 +1070,4 @@ mod tests { assert_eq!(item, Some(expected_sequence[current])); // sanity check, not what we're testing } } - - #[cfg(feature = "arbitrary")] - #[test] - fn arbitrary_grid_aab_size_hint() { - use arbitrary::{Arbitrary, Unstructured}; - let hint = GridAab::ARBITRARY_SIZE_HINT; - let most_bytes_used = (0..=255) - .map(|byte| { - // TODO: sketchy coverage; would be better to generate some random/hashed data - let data = [byte; 1000]; - let mut u = Unstructured::new(&data); - GridAab::arbitrary(&mut u).unwrap(); - let bytes_used = 1000 - u.len(); - assert!( - bytes_used >= hint.0, - "used {}, less than {}", - bytes_used, - hint.0 - ); - bytes_used - }) - .max(); - assert_eq!(most_bytes_used, hint.1); - - // TODO: Also look at the resulting Grids and see if they're good coverage. - } - - #[cfg(feature = "arbitrary")] - #[test] - fn arbitrary_grid_aab_volume() { - use arbitrary::Unstructured; - use itertools::Itertools as _; - let max_volume = 100; - let minmax = (0..=255) - .map(|byte| { - // TODO: sketchy coverage; would be better to generate some random/hashed data - let data = [byte; 25]; - let mut u = Unstructured::new(&data); - GridAab::arbitrary_with_max_volume(&mut u, max_volume) - .unwrap() - .volume() - }) - .minmax() - .into_option(); - assert_eq!(minmax, Some((0, max_volume))); - } } diff --git a/all-is-cubes/src/math/vol.rs b/all-is-cubes/src/math/vol.rs index 99c3d726c..9dfbc3e8e 100644 --- a/all-is-cubes/src/math/vol.rs +++ b/all-is-cubes/src/math/vol.rs @@ -544,7 +544,7 @@ mod aab_compat { } #[cfg(feature = "arbitrary")] -mod vol_arb { +pub(crate) mod vol_arb { use super::*; use arbitrary::Arbitrary; @@ -552,25 +552,76 @@ mod vol_arb { /// This does reduce coverage... const MAX_VOLUME: usize = 2_usize.pow(16); + /// Size hint for [`Vol::arbitrary_with_max_volume()`]. + pub(crate) const ARBITRARY_BOUNDS_SIZE_HINT: (usize, Option) = { + // 6 bounding coordinates plus one permutation selection. + // Depending on the volume we could *maybe* end up consuming only 1 byte each + // for the sizes. + let gc = core::mem::size_of::(); + ((gc + 1) * 3 + 1, Some(gc * 6 + 1)) + }; + + impl Vol<(), O> { + #[cfg(feature = "arbitrary")] + pub(crate) fn arbitrary_with_max_volume( + u: &mut arbitrary::Unstructured<'_>, + volume: usize, + ) -> arbitrary::Result { + // Pick sizes within the volume constraint. + let mut limit: GridCoordinate = volume.try_into().unwrap_or(GridCoordinate::MAX); + let size_1 = u.int_in_range(0..=limit)?; + limit /= size_1.max(1); + let size_2 = u.int_in_range(0..=limit)?; + limit /= size_2.max(1); + let size_3 = u.int_in_range(0..=limit)?; + + // Shuffle the sizes to remove any bias. + let sizes = *u.choose(&[ + GridVector::new(size_1, size_2, size_3), + GridVector::new(size_1, size_3, size_2), + GridVector::new(size_2, size_1, size_3), + GridVector::new(size_2, size_3, size_1), + GridVector::new(size_3, size_1, size_2), + GridVector::new(size_3, size_2, size_1), + ])?; + + // Compute lower bounds that are valid for the sizes. + let lower_bounds = GridPoint::new( + u.int_in_range(GridCoordinate::MIN..=GridCoordinate::MAX - sizes.x)?, + u.int_in_range(GridCoordinate::MIN..=GridCoordinate::MAX - sizes.y)?, + u.int_in_range(GridCoordinate::MIN..=GridCoordinate::MAX - sizes.z)?, + ); + + Ok(GridAab::from_lower_size(lower_bounds, sizes) + .to_vol() + .unwrap()) + } + } + impl<'a, V: Arbitrary<'a>, C> Arbitrary<'a> for Vol where C: FromIterator + Deref, { fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - let bounds = GridAab::arbitrary_with_max_volume(u, MAX_VOLUME)?; + let bounds = Vol::<()>::arbitrary_with_max_volume(u, MAX_VOLUME)?; let contents: C = u .arbitrary_iter()? .take(bounds.volume()) .collect::>()?; - Vol::from_elements(bounds, contents).map_err(|_| arbitrary::Error::NotEnoughData) + bounds + .with_elements(contents) + .map_err(|_| arbitrary::Error::NotEnoughData) } fn size_hint(depth: usize) -> (usize, Option) { arbitrary::size_hint::recursion_guard(depth, |depth| { let (lower, upper) = V::size_hint(depth); - ( - lower.saturating_mul(MAX_VOLUME), - upper.map(|u| u.saturating_mul(MAX_VOLUME)), + arbitrary::size_hint::and( + ARBITRARY_BOUNDS_SIZE_HINT, + ( + lower.saturating_mul(MAX_VOLUME), + upper.map(|u| u.saturating_mul(MAX_VOLUME)), + ), ) }) } @@ -780,4 +831,50 @@ mod tests { Some(((1500 * 2000) + 1500) * 2000 + 1500) ); } + + #[cfg(feature = "arbitrary")] + #[test] + fn arbitrary_bounds_size_hint() { + use arbitrary::{Arbitrary, Unstructured}; + let hint = vol_arb::ARBITRARY_BOUNDS_SIZE_HINT; + let most_bytes_used = (0..=255) + .map(|byte| { + // TODO: sketchy coverage; would be better to generate some random/hashed data + let data = [byte; 1000]; + let mut u = Unstructured::new(&data); + GridAab::arbitrary(&mut u).unwrap(); + let bytes_used = 1000 - u.len(); + assert!( + bytes_used >= hint.0, + "used {}, less than {}", + bytes_used, + hint.0 + ); + bytes_used + }) + .max(); + assert_eq!(most_bytes_used, hint.1); + + // TODO: Also look at the resulting Grids and see if they're good coverage. + } + + #[cfg(feature = "arbitrary")] + #[test] + fn arbitrary_bounds_volume() { + use arbitrary::Unstructured; + use itertools::Itertools as _; + let max_volume = 100; + let minmax = (0..=255) + .map(|byte| { + // TODO: sketchy coverage; would be better to generate some random/hashed data + let data = [byte; 25]; + let mut u = Unstructured::new(&data); + Vol::<()>::arbitrary_with_max_volume(&mut u, max_volume) + .unwrap() + .volume() + }) + .minmax() + .into_option(); + assert_eq!(minmax, Some((0, max_volume))); + } } diff --git a/all-is-cubes/src/space/builder.rs b/all-is-cubes/src/space/builder.rs index d11301512..a3f15281d 100644 --- a/all-is-cubes/src/space/builder.rs +++ b/all-is-cubes/src/space/builder.rs @@ -284,8 +284,8 @@ impl<'a> arbitrary::Arbitrary<'a> for Space { // TODO: Should be reusing Vol as Arbitrary for this. - let bounds = GridAab::arbitrary_with_max_volume(u, 2048)?; - let mut space = Space::builder(bounds) + let bounds = Vol::<()>::arbitrary_with_max_volume(u, 2048)?; + let mut space = Space::builder(bounds.bounds()) // TODO: builder should accept Vol .physics(u.arbitrary()?) .spawn(u.arbitrary()?) .build();