From 30502ceb17c033be408d9766a32ebc211518033f Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 5 Nov 2024 08:36:46 +0100 Subject: [PATCH] Add `Ranges::from_iter` See https://github.com/pubgrub-rs/pubgrub/pull/273, it's split out from that PR, for https://github.com/astral-sh/uv/pull/8797/files. Closes #33 --- version-ranges/src/lib.rs | 115 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/version-ranges/src/lib.rs b/version-ranges/src/lib.rs index 8eefa452..6d7119d7 100644 --- a/version-ranges/src/lib.rs +++ b/version-ranges/src/lib.rs @@ -283,6 +283,36 @@ impl Ranges { } } + /// We want to use `iterator_try_collect`, but since it's unstable at the time of writing, + /// we expose a public `FromIterator<(Bound, Bound)>` method and use this for internal + /// testing. + fn try_from( + into_iter: impl IntoIterator, Bound)>, + ) -> Result { + let mut iter = into_iter.into_iter(); + let Some(mut previous) = iter.next() else { + return Ok(Self { + segments: SmallVec::new(), + }); + }; + let mut segments = SmallVec::with_capacity(iter.size_hint().0); + for current in iter { + if !valid_segment(&previous.start_bound(), &previous.end_bound()) { + return Err(FromIterError::InvalidSegment); + } + if !end_before_start_with_gap(&previous.end_bound(), ¤t.start_bound()) { + return Err(FromIterError::OverlappingSegments); + } + segments.push(previous); + previous = current; + } + if !valid_segment(&previous.start_bound(), &previous.end_bound()) { + return Err(FromIterError::InvalidSegment); + } + segments.push(previous); + Ok(Self { segments }) + } + fn check_invariants(self) -> Self { if cfg!(debug_assertions) { for p in self.segments.as_slice().windows(2) { @@ -826,6 +856,40 @@ impl Ranges { } } +/// User provided segment iterator breaks [`Ranges`] invariants. +/// +/// Not user accessible since `FromIterator<(Bound, Bound)>` panics and `iterator_try_collect` +/// is unstable. +#[derive(Debug, PartialEq, Eq)] +enum FromIterError { + /// The start of a segment must be before its end, and a segment must contain at least one + /// version. + InvalidSegment, + /// The end of a segment is not before the start of the next segment, leaving at least one + /// version space. + OverlappingSegments, +} + +impl Display for FromIterError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + FromIterError::InvalidSegment => f.write_str("segment must be valid"), + FromIterError::OverlappingSegments => { + f.write_str("end of a segment and start of the next segment must not overlap") + } + } + } +} + +impl FromIterator<(Bound, Bound)> for Ranges { + /// Construct a [`Ranges`] from an ordered list of bounds. + /// + /// Panics if the bounds aren't sorted, are empty or have no space to the next bound. + fn from_iter, Bound)>>(iter: T) -> Self { + Self::try_from(iter).unwrap() + } +} + // REPORT ###################################################################### impl Display for Ranges { @@ -1130,6 +1194,24 @@ pub mod tests { } assert!(simp.segments.len() <= range.segments.len()) } + + #[test] + fn from_iter_valid(segments in proptest::collection::vec(any::<(Bound, Bound)>(), ..30)) { + match Ranges::try_from(segments.clone()) { + Ok(ranges) => { + ranges.check_invariants(); + } + Err(_) => { + assert!( + segments + .as_slice() + .windows(2) + .any(|p| !end_before_start_with_gap(&p[0].1, &p[1].0)) + || segments.iter().any(|(start, end)| !valid_segment(start, end)) + ); + } + } + } } #[test] @@ -1194,4 +1276,37 @@ pub mod tests { version_reverse_sorted.sort(); assert_eq!(version_reverse_sorted, versions); } + + /// Test all error conditions in [`Ranges::try_from`]. + #[test] + fn from_iter_errors() { + // Unbounded in not at an end + let result = Ranges::try_from([ + (Bound::Included(1), Bound::Unbounded), + (Bound::Included(2), Bound::Unbounded), + ]); + assert_eq!(result, Err(FromIterError::OverlappingSegments)); + // Not a version in between + let result = Ranges::try_from([ + (Bound::Included(1), Bound::Excluded(2)), + (Bound::Included(2), Bound::Unbounded), + ]); + assert_eq!(result, Err(FromIterError::OverlappingSegments)); + // First segment + let result = Ranges::try_from([(Bound::Excluded(2), Bound::Included(2))]); + assert_eq!(result, Err(FromIterError::InvalidSegment)); + // Middle segment + let result = Ranges::try_from([ + (Bound::Included(1), Bound::Included(2)), + (Bound::Included(3), Bound::Included(2)), + (Bound::Included(4), Bound::Included(5)), + ]); + assert_eq!(result, Err(FromIterError::InvalidSegment)); + // Last segment + let result = Ranges::try_from([ + (Bound::Included(1), Bound::Included(2)), + (Bound::Included(3), Bound::Included(2)), + ]); + assert_eq!(result, Err(FromIterError::InvalidSegment)); + } }