/ 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.piece.p0.y < c.piece.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          .piece)
 80  }
 81  
 82  /// Generate an arbitrary cubic Bezier, guaranteed to be monotonically increasing in y.
 83  ///
 84  /// This generated Bezier has a chance to be "close" to `first`, for example by starting
 85  /// at the same point or with the same tangent.
 86  pub fn another_monotonic_bezier(
 87      u: &mut Unstructured<'_>,
 88      first: &CubicBez,
 89  ) -> Result<CubicBez, arbitrary::Error> {
 90      let same_start: bool = u.arbitrary()?;
 91      let same_start_tangent: bool = same_start && u.arbitrary()?;
 92  
 93      let p0 = if same_start { first.p0 } else { point(u)? };
 94  
 95      let p1 = if same_start_tangent {
 96          first.p1
 97      } else {
 98          point_at_least(p0.y, u)?
 99      };
100  
101      let p3 = point_at_least(p0.y.max(first.p0.y), u)?;
102      let p2 = point_at_most(p3.y, u)?;
103  
104      let ret = CubicBez::new(p0, p1, p2, p3);
105      Ok(geom::monotonic_pieces(ret)
106          .into_iter()
107          .next()
108          .map(|c| c.piece)
109          .unwrap_or(ret))
110  }
111  
112  /// Generate an arbitrary quadratic function, with coefficients of roughly the scale `size`.
113  pub fn quadratic(size: f64, u: &mut Unstructured<'_>) -> Result<Quadratic, arbitrary::Error> {
114      let use_coeffs: bool = u.arbitrary()?;
115      if use_coeffs {
116          let c2 = float_in_range(-size, size, u)?;
117          let c1 = another_float_in_range(c2, -size, size, u)?;
118          let c0 = another_float_in_range(c1, -size, size, u)?;
119  
120          Ok(Quadratic { c2, c1, c0 })
121      } else {
122          // Generate the roots, with a bias towards an almost-repeated root.
123          let size = size.sqrt();
124  
125          let r1 = float_in_range(-size, size, u)?;
126          let r2 = another_float_in_range(r1, -size, size, u)?;
127          let scale = float_in_range(-size, size, u)?;
128  
129          Ok(Quadratic {
130              c2: scale,
131              c1: -scale * (r1 + r2),
132              c0: scale * r1 * r2,
133          })
134      }
135  }