/ src / arbitrary.rs
arbitrary.rs
  1  //! Utilities for fuzz and/or property testing using `arbitrary`.
  2  
  3  use arbitrary::Unstructured;
  4  use kurbo::{CubicBez, Point};
  5  
  6  use crate::{curve::Quadratic, geom};
  7  
  8  /// Generate an arbitrary float in some range.
  9  pub fn float_in_range(
 10      start: f64,
 11      end: f64,
 12      u: &mut Unstructured<'_>,
 13  ) -> Result<f64, arbitrary::Error> {
 14      let num: u32 = u.arbitrary()?;
 15      let t = num as f64 / u32::MAX as f64;
 16      Ok((start + t * (end - start)).clamp(start, end))
 17  }
 18  
 19  fn float(u: &mut Unstructured<'_>) -> Result<f64, arbitrary::Error> {
 20      float_in_range(-1e6, 1e6, u)
 21  }
 22  
 23  fn float_at_least(min: f64, u: &mut Unstructured<'_>) -> Result<f64, arbitrary::Error> {
 24      float_in_range(min, 1e6, u)
 25  }
 26  
 27  fn float_at_most(max: f64, u: &mut Unstructured<'_>) -> Result<f64, arbitrary::Error> {
 28      float_in_range(-1e6, max, u)
 29  }
 30  
 31  /// Generate a float in some range, but give it a chance to be close to another float.
 32  fn another_float_in_range(
 33      orig: f64,
 34      start: f64,
 35      end: f64,
 36      u: &mut Unstructured<'_>,
 37  ) -> Result<f64, arbitrary::Error> {
 38      let close: bool = u.arbitrary()?;
 39      if close {
 40          let ulps: i32 = u.int_in_range(-32..=32)?;
 41          let scale = 1.0f64 + ulps as f64 * f64::EPSILON;
 42          Ok((orig * scale).clamp(start, end))
 43      } else {
 44          float_in_range(start, end, u)
 45      }
 46  }
 47  
 48  fn point(u: &mut Unstructured<'_>) -> Result<Point, arbitrary::Error> {
 49      Ok(Point::new(float(u)?, float(u)?))
 50  }
 51  
 52  fn point_at_least(min: f64, u: &mut Unstructured<'_>) -> Result<Point, arbitrary::Error> {
 53      Ok(Point::new(float(u)?, float_at_least(min, u)?))
 54  }
 55  
 56  fn point_at_most(max: f64, u: &mut Unstructured<'_>) -> Result<Point, arbitrary::Error> {
 57      Ok(Point::new(float(u)?, float_at_most(max, u)?))
 58  }
 59  
 60  /// Generate an arbitrary cubic Bezier, guaranteed to be monotonically increasing in y.
 61  pub fn monotonic_bezier(u: &mut Unstructured<'_>) -> Result<CubicBez, arbitrary::Error> {
 62      let p0 = point(u)?;
 63      let p3 = point(u)?;
 64      let (p0, mut p3) = if p0.y < p3.y { (p0, p3) } else { (p3, p0) };
 65      if p3.y == p0.y {
 66          p3.y += 1.0;
 67      }
 68  
 69      let p1 = point_at_least(p0.y, u)?;
 70      let p2 = point_at_most(p3.y, u)?;
 71  
 72      let ret = CubicBez::new(p0, p1, p2, p3);
 73      Ok(geom::monotonic_pieces(ret)
 74          .into_iter()
 75          .find(|c| c.p0.y < c.p3.y)
 76          // unwrap: we started with p0 having smaller y, so there must be
 77          // a monotonic component that's increasing in y.
 78          .unwrap())
 79  }
 80  
 81  /// Generate an arbitrary cubic Bezier, guaranteed to be monotonically increasing in y.
 82  ///
 83  /// This generated Bezier has a chance to be "close" to `first`, for example by starting
 84  /// at the same point or with the same tangent.
 85  pub fn another_monotonic_bezier(
 86      u: &mut Unstructured<'_>,
 87      first: &CubicBez,
 88  ) -> Result<CubicBez, arbitrary::Error> {
 89      let same_start: bool = u.arbitrary()?;
 90      let same_start_tangent: bool = same_start && u.arbitrary()?;
 91  
 92      let p0 = if same_start { first.p0 } else { point(u)? };
 93  
 94      let p1 = if same_start_tangent {
 95          first.p1
 96      } else {
 97          point_at_least(p0.y, u)?
 98      };
 99  
100      let p3 = point_at_least(p0.y.max(first.p0.y), u)?;
101      let p2 = point_at_most(p3.y, u)?;
102  
103      let ret = CubicBez::new(p0, p1, p2, p3);
104      Ok(geom::monotonic_pieces(ret)
105          .into_iter()
106          .next()
107          .unwrap_or(ret))
108  }
109  
110  /// Generate an arbitrary quadratic function, with coefficients of roughly the scale `size`.
111  pub fn quadratic(size: f64, u: &mut Unstructured<'_>) -> Result<Quadratic, arbitrary::Error> {
112      let use_coeffs: bool = u.arbitrary()?;
113      if use_coeffs {
114          let c2 = float_in_range(-size, size, u)?;
115          let c1 = another_float_in_range(c2, -size, size, u)?;
116          let c0 = another_float_in_range(c1, -size, size, u)?;
117  
118          Ok(Quadratic { c2, c1, c0 })
119      } else {
120          // Generate the roots, with a bias towards an almost-repeated root.
121          let size = size.sqrt();
122  
123          let r1 = float_in_range(-size, size, u)?;
124          let r2 = another_float_in_range(r1, -size, size, u)?;
125          let scale = float_in_range(-size, size, u)?;
126  
127          Ok(Quadratic {
128              c2: scale,
129              c1: -scale * (r1 + r2),
130              c0: scale * r1 * r2,
131          })
132      }
133  }