Replace HasDomain to enable multi-argument edge case and domain tests

This also allows reusing the same generator logic between logspace tests
and extensive tests, so comes with a nice bit of cleanup.

Changes:

* Make the generator part of `CheckCtx` since a `Generator` and
  `CheckCtx` are almost always passed together.
* Rename `domain_logspace` to `spaced` since this no longer only
  operates within a domain and we may want to handle integer spacing.
* Domain is now calculated at runtime rather than using traits, which is
  much easier to work with.
* With the above, domains for multidimensional functions are added.
* The extensive test generator code tests has been combined with the
  domain_logspace generator code. With this, the domain tests have just
  become a subset of extensive tests. These were renamed to "quickspace"
  since, technically, the extensive tests are also "domain" or "domain
  logspace" tests.
* Edge case generators now handle functions with multiple inputs.
* The test runners can be significantly cleaned up and deduplicated.
This commit is contained in:
Trevor Gross 2025-01-07 06:28:04 +00:00
parent 20cd1e7257
commit 2d857e1c21
12 changed files with 539 additions and 520 deletions

View file

@ -4,7 +4,7 @@ use std::time::Duration;
use criterion::{Criterion, criterion_main};
use libm_test::gen::random;
use libm_test::gen::random::RandomInput;
use libm_test::{CheckBasis, CheckCtx, MathOp, TupleCall};
use libm_test::{CheckBasis, CheckCtx, GeneratorKind, MathOp, TupleCall};
/// Benchmark with this many items to get a variety
const BENCH_ITER_ITEMS: usize = if cfg!(feature = "short-benchmarks") { 50 } else { 500 };
@ -52,7 +52,7 @@ where
{
let name = Op::NAME;
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Musl);
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Musl, GeneratorKind::Random);
let benchvec: Vec<_> =
random::get_test_cases::<Op::RustArgs>(&ctx).take(BENCH_ITER_ITEMS).collect();

View file

@ -12,9 +12,9 @@ use std::path::Path;
use std::process::Command;
use std::{env, fs};
use libm_test::domain::HasDomain;
use libm_test::gen::{domain_logspace, edge_cases};
use libm_test::{CheckBasis, CheckCtx, MathOp, op};
use libm_test::gen::spaced::SpacedInput;
use libm_test::gen::{edge_cases, spaced};
use libm_test::{CheckBasis, CheckCtx, GeneratorKind, MathOp, op};
const JL_PLOT: &str = "examples/plot_file.jl";
@ -52,23 +52,13 @@ fn main() {
/// Run multiple generators for a single operator.
fn plot_one_operator<Op>(out_dir: &Path, config: &mut String)
where
Op: MathOp<FTy = f32> + HasDomain<f32>,
Op: MathOp<FTy = f32, RustArgs = (f32,)>,
Op::RustArgs: SpacedInput<Op>,
{
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
plot_one_generator(
out_dir,
&ctx,
"logspace",
config,
domain_logspace::get_test_cases::<Op>(&ctx),
);
plot_one_generator(
out_dir,
&ctx,
"edge_cases",
config,
edge_cases::get_test_cases::<Op, _>(&ctx),
);
let mut ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr, GeneratorKind::QuickSpaced);
plot_one_generator(out_dir, &ctx, "logspace", config, spaced::get_test_cases::<Op>(&ctx).0);
ctx.gen_kind = GeneratorKind::EdgeCases;
plot_one_generator(out_dir, &ctx, "edge_cases", config, edge_cases::get_test_cases::<Op>(&ctx));
}
/// Plot the output of a single generator.

View file

@ -1,11 +1,13 @@
//! Traits and operations related to bounds of a function.
use std::fmt;
use std::ops::{self, Bound};
use std::ops::Bound;
use crate::{Float, FloatExt};
use libm::support::Int;
/// Representation of a function's domain.
use crate::{BaseName, Float, FloatExt, Identifier};
/// Representation of a single dimension of a function's domain.
#[derive(Clone, Debug)]
pub struct Domain<T> {
/// Start of the region for which a function is defined (ignoring poles).
@ -39,56 +41,131 @@ impl<F: FloatExt> Domain<F> {
}
}
/// A value that may be any float type or any integer type.
#[derive(Clone, Debug)]
pub enum EitherPrim<F, I> {
Float(F),
Int(I),
}
impl<F: fmt::Debug, I: fmt::Debug> EitherPrim<F, I> {
pub fn unwrap_float(self) -> F {
match self {
EitherPrim::Float(f) => f,
EitherPrim::Int(_) => panic!("expected float; got {self:?}"),
}
}
pub fn unwrap_int(self) -> I {
match self {
EitherPrim::Float(_) => panic!("expected int; got {self:?}"),
EitherPrim::Int(i) => i,
}
}
}
/// Convenience 1-dimensional float domains.
impl<F: Float> Domain<F> {
/// x ∈
pub const UNBOUNDED: Self =
const UNBOUNDED: Self =
Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None };
/// x ∈ >= 0
pub const POSITIVE: Self =
const POSITIVE: Self =
Self { start: Bound::Included(F::ZERO), end: Bound::Unbounded, check_points: None };
/// x ∈ > 0
pub const STRICTLY_POSITIVE: Self =
const STRICTLY_POSITIVE: Self =
Self { start: Bound::Excluded(F::ZERO), end: Bound::Unbounded, check_points: None };
/// Wrap in the float variant of [`EitherPrim`].
const fn into_prim_float<I>(self) -> EitherPrim<Self, Domain<I>> {
EitherPrim::Float(self)
}
}
/// Convenience 1-dimensional integer domains.
impl<I: Int> Domain<I> {
/// x ∈
const UNBOUNDED_INT: Self =
Self { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None };
/// Wrap in the int variant of [`EitherPrim`].
const fn into_prim_int<F>(self) -> EitherPrim<Domain<F>, Self> {
EitherPrim::Int(self)
}
}
/// Multidimensional domains, represented as an array of 1-D domains.
impl<F: Float, I: Int> EitherPrim<Domain<F>, Domain<I>> {
/// x ∈
const UNBOUNDED1: [Self; 1] =
[Domain { start: Bound::Unbounded, end: Bound::Unbounded, check_points: None }
.into_prim_float()];
/// {x1, x2} ∈
const UNBOUNDED2: [Self; 2] =
[Domain::UNBOUNDED.into_prim_float(), Domain::UNBOUNDED.into_prim_float()];
/// {x1, x2, x3} ∈
const UNBOUNDED3: [Self; 3] = [
Domain::UNBOUNDED.into_prim_float(),
Domain::UNBOUNDED.into_prim_float(),
Domain::UNBOUNDED.into_prim_float(),
];
/// {x1, x2} ∈ , one float and one int
const UNBOUNDED_F_I: [Self; 2] =
[Domain::UNBOUNDED.into_prim_float(), Domain::UNBOUNDED_INT.into_prim_int()];
/// x ∈ >= 0
const POSITIVE: [Self; 1] = [Domain::POSITIVE.into_prim_float()];
/// x ∈ > 0
const STRICTLY_POSITIVE: [Self; 1] = [Domain::STRICTLY_POSITIVE.into_prim_float()];
/// Used for versions of `asin` and `acos`.
pub const INVERSE_TRIG_PERIODIC: Self = Self {
const INVERSE_TRIG_PERIODIC: [Self; 1] = [Domain {
start: Bound::Included(F::NEG_ONE),
end: Bound::Included(F::ONE),
check_points: None,
};
}
.into_prim_float()];
/// Domain for `acosh`
pub const ACOSH: Self =
Self { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None };
const ACOSH: [Self; 1] =
[Domain { start: Bound::Included(F::ONE), end: Bound::Unbounded, check_points: None }
.into_prim_float()];
/// Domain for `atanh`
pub const ATANH: Self = Self {
const ATANH: [Self; 1] = [Domain {
start: Bound::Excluded(F::NEG_ONE),
end: Bound::Excluded(F::ONE),
check_points: None,
};
}
.into_prim_float()];
/// Domain for `sin`, `cos`, and `tan`
pub const TRIG: Self = Self {
// TODO
const TRIG: [Self; 1] = [Domain {
// Trig functions have special behavior at fractions of π.
check_points: Some(|| Box::new([-F::PI, -F::FRAC_PI_2, F::FRAC_PI_2, F::PI].into_iter())),
..Self::UNBOUNDED
};
..Domain::UNBOUNDED
}
.into_prim_float()];
/// Domain for `log` in various bases
pub const LOG: Self = Self::STRICTLY_POSITIVE;
const LOG: [Self; 1] = Self::STRICTLY_POSITIVE;
/// Domain for `log1p` i.e. `log(1 + x)`
pub const LOG1P: Self =
Self { start: Bound::Excluded(F::NEG_ONE), end: Bound::Unbounded, check_points: None };
const LOG1P: [Self; 1] =
[Domain { start: Bound::Excluded(F::NEG_ONE), end: Bound::Unbounded, check_points: None }
.into_prim_float()];
/// Domain for `sqrt`
pub const SQRT: Self = Self::POSITIVE;
const SQRT: [Self; 1] = Self::POSITIVE;
/// Domain for `gamma`
pub const GAMMA: Self = Self {
const GAMMA: [Self; 1] = [Domain {
check_points: Some(|| {
// Negative integers are asymptotes
Box::new((0..u8::MAX).map(|scale| {
@ -100,122 +177,84 @@ impl<F: Float> Domain<F> {
}))
}),
// Whether or not gamma is defined for negative numbers is implementation dependent
..Self::UNBOUNDED
};
..Domain::UNBOUNDED
}
.into_prim_float()];
/// Domain for `loggamma`
pub const LGAMMA: Self = Self::STRICTLY_POSITIVE;
const LGAMMA: [Self; 1] = Self::STRICTLY_POSITIVE;
/// Domain for `jn` and `yn`.
// FIXME: the domain should provide some sort of "reasonable range" so we don't actually test
// the entire system unbounded.
const BESSEL_N: [Self; 2] =
[Domain::UNBOUNDED_INT.into_prim_int(), Domain::UNBOUNDED.into_prim_float()];
}
/// Implement on `op::*` types to indicate how they are bounded.
pub trait HasDomain<T>
where
T: Copy + fmt::Debug + ops::Add<Output = T> + ops::Sub<Output = T> + PartialOrd + 'static,
{
const DOMAIN: Domain<T>;
}
/// Implement [`HasDomain`] for both the `f32` and `f64` variants of a function.
macro_rules! impl_has_domain {
($($fn_name:ident => $domain:expr;)*) => {
paste::paste! {
$(
// Implement for f64 functions
impl HasDomain<f64> for $crate::op::$fn_name::Routine {
const DOMAIN: Domain<f64> = Domain::<f64>::$domain;
}
// Implement for f32 functions
impl HasDomain<f32> for $crate::op::[< $fn_name f >]::Routine {
const DOMAIN: Domain<f32> = Domain::<f32>::$domain;
}
)*
}
/// Get the domain for a given function.
pub fn get_domain<F: Float, I: Int>(
id: Identifier,
argnum: usize,
) -> EitherPrim<Domain<F>, Domain<I>> {
let x = match id.base_name() {
BaseName::Acos => &EitherPrim::INVERSE_TRIG_PERIODIC[..],
BaseName::Acosh => &EitherPrim::ACOSH[..],
BaseName::Asin => &EitherPrim::INVERSE_TRIG_PERIODIC[..],
BaseName::Asinh => &EitherPrim::UNBOUNDED1[..],
BaseName::Atan => &EitherPrim::UNBOUNDED1[..],
BaseName::Atan2 => &EitherPrim::UNBOUNDED2[..],
BaseName::Cbrt => &EitherPrim::UNBOUNDED1[..],
BaseName::Atanh => &EitherPrim::ATANH[..],
BaseName::Ceil => &EitherPrim::UNBOUNDED1[..],
BaseName::Cosh => &EitherPrim::UNBOUNDED1[..],
BaseName::Copysign => &EitherPrim::UNBOUNDED2[..],
BaseName::Cos => &EitherPrim::TRIG[..],
BaseName::Exp => &EitherPrim::UNBOUNDED1[..],
BaseName::Erf => &EitherPrim::UNBOUNDED1[..],
BaseName::Erfc => &EitherPrim::UNBOUNDED1[..],
BaseName::Expm1 => &EitherPrim::UNBOUNDED1[..],
BaseName::Exp10 => &EitherPrim::UNBOUNDED1[..],
BaseName::Exp2 => &EitherPrim::UNBOUNDED1[..],
BaseName::Frexp => &EitherPrim::UNBOUNDED1[..],
BaseName::Fabs => &EitherPrim::UNBOUNDED1[..],
BaseName::Fdim => &EitherPrim::UNBOUNDED2[..],
BaseName::Floor => &EitherPrim::UNBOUNDED1[..],
BaseName::Fma => &EitherPrim::UNBOUNDED3[..],
BaseName::Fmax => &EitherPrim::UNBOUNDED2[..],
BaseName::Fmin => &EitherPrim::UNBOUNDED2[..],
BaseName::Fmod => &EitherPrim::UNBOUNDED2[..],
BaseName::Hypot => &EitherPrim::UNBOUNDED2[..],
BaseName::Ilogb => &EitherPrim::UNBOUNDED1[..],
BaseName::J0 => &EitherPrim::UNBOUNDED1[..],
BaseName::J1 => &EitherPrim::UNBOUNDED1[..],
BaseName::Jn => &EitherPrim::BESSEL_N[..],
BaseName::Ldexp => &EitherPrim::UNBOUNDED_F_I[..],
BaseName::Lgamma => &EitherPrim::LGAMMA[..],
BaseName::LgammaR => &EitherPrim::LGAMMA[..],
BaseName::Log => &EitherPrim::LOG[..],
BaseName::Log10 => &EitherPrim::LOG[..],
BaseName::Log1p => &EitherPrim::LOG1P[..],
BaseName::Log2 => &EitherPrim::LOG[..],
BaseName::Modf => &EitherPrim::UNBOUNDED1[..],
BaseName::Nextafter => &EitherPrim::UNBOUNDED2[..],
BaseName::Pow => &EitherPrim::UNBOUNDED2[..],
BaseName::Remainder => &EitherPrim::UNBOUNDED2[..],
BaseName::Remquo => &EitherPrim::UNBOUNDED2[..],
BaseName::Rint => &EitherPrim::UNBOUNDED1[..],
BaseName::Round => &EitherPrim::UNBOUNDED1[..],
BaseName::Scalbn => &EitherPrim::UNBOUNDED_F_I[..],
BaseName::Sin => &EitherPrim::TRIG[..],
BaseName::Sincos => &EitherPrim::TRIG[..],
BaseName::Sinh => &EitherPrim::UNBOUNDED1[..],
BaseName::Sqrt => &EitherPrim::SQRT[..],
BaseName::Tan => &EitherPrim::TRIG[..],
BaseName::Tanh => &EitherPrim::UNBOUNDED1[..],
BaseName::Tgamma => &EitherPrim::GAMMA[..],
BaseName::Trunc => &EitherPrim::UNBOUNDED1[..],
BaseName::Y0 => &EitherPrim::UNBOUNDED1[..],
BaseName::Y1 => &EitherPrim::UNBOUNDED1[..],
BaseName::Yn => &EitherPrim::BESSEL_N[..],
};
}
// Tie functions together with their domains.
impl_has_domain! {
acos => INVERSE_TRIG_PERIODIC;
acosh => ACOSH;
asin => INVERSE_TRIG_PERIODIC;
asinh => UNBOUNDED;
atan => UNBOUNDED;
atanh => ATANH;
cbrt => UNBOUNDED;
ceil => UNBOUNDED;
cos => TRIG;
cosh => UNBOUNDED;
erf => UNBOUNDED;
erfc => UNBOUNDED;
exp => UNBOUNDED;
exp10 => UNBOUNDED;
exp2 => UNBOUNDED;
expm1 => UNBOUNDED;
fabs => UNBOUNDED;
floor => UNBOUNDED;
frexp => UNBOUNDED;
ilogb => UNBOUNDED;
j0 => UNBOUNDED;
j1 => UNBOUNDED;
lgamma => LGAMMA;
log => LOG;
log10 => LOG;
log1p => LOG1P;
log2 => LOG;
modf => UNBOUNDED;
rint => UNBOUNDED;
round => UNBOUNDED;
sin => TRIG;
sincos => TRIG;
sinh => UNBOUNDED;
sqrt => SQRT;
tan => TRIG;
tanh => UNBOUNDED;
tgamma => GAMMA;
trunc => UNBOUNDED;
y0 => UNBOUNDED;
y1 => UNBOUNDED;
}
/* Manual implementations, these functions don't follow `foo`->`foof` naming */
impl HasDomain<f32> for crate::op::lgammaf_r::Routine {
const DOMAIN: Domain<f32> = Domain::<f32>::LGAMMA;
}
impl HasDomain<f64> for crate::op::lgamma_r::Routine {
const DOMAIN: Domain<f64> = Domain::<f64>::LGAMMA;
}
/* Not all `f16` and `f128` functions exist yet so we can't easily use the macros. */
#[cfg(f16_enabled)]
impl HasDomain<f16> for crate::op::fabsf16::Routine {
const DOMAIN: Domain<f16> = Domain::<f16>::UNBOUNDED;
}
#[cfg(f128_enabled)]
impl HasDomain<f128> for crate::op::fabsf128::Routine {
const DOMAIN: Domain<f128> = Domain::<f128>::UNBOUNDED;
}
#[cfg(f16_enabled)]
impl HasDomain<f16> for crate::op::fdimf16::Routine {
const DOMAIN: Domain<f16> = Domain::<f16>::UNBOUNDED;
}
#[cfg(f128_enabled)]
impl HasDomain<f128> for crate::op::fdimf128::Routine {
const DOMAIN: Domain<f128> = Domain::<f128>::UNBOUNDED;
}
#[cfg(f16_enabled)]
impl HasDomain<f16> for crate::op::truncf16::Routine {
const DOMAIN: Domain<f16> = Domain::<f16>::UNBOUNDED;
}
#[cfg(f128_enabled)]
impl HasDomain<f128> for crate::op::truncf128::Routine {
const DOMAIN: Domain<f128> = Domain::<f128>::UNBOUNDED;
x[argnum].clone()
}

View file

@ -1,9 +1,8 @@
//! Different generators that can create random or systematic bit patterns.
pub mod domain_logspace;
pub mod edge_cases;
pub mod extensive;
pub mod random;
pub mod spaced;
/// A wrapper to turn any iterator into an `ExactSizeIterator`. Asserts the final result to ensure
/// the provided size was correct.

View file

@ -1,31 +0,0 @@
//! A generator that produces logarithmically spaced values within domain bounds.
use std::ops::RangeInclusive;
use libm::support::{IntTy, MinInt};
use crate::domain::HasDomain;
use crate::op::OpITy;
use crate::run_cfg::{GeneratorKind, iteration_count};
use crate::{CheckCtx, MathOp, logspace};
/// Create a range of logarithmically spaced inputs within a function's domain.
///
/// This allows us to get reasonably thorough coverage without wasting time on values that are
/// NaN or out of range. Random tests will still cover values that are excluded here.
pub fn get_test_cases<Op>(ctx: &CheckCtx) -> impl Iterator<Item = (Op::FTy,)>
where
Op: MathOp + HasDomain<Op::FTy>,
IntTy<Op::FTy>: TryFrom<u64>,
RangeInclusive<IntTy<Op::FTy>>: Iterator,
{
let domain = Op::DOMAIN;
let ntests = iteration_count(ctx, GeneratorKind::Domain, 0);
// We generate logspaced inputs within a specific range, excluding values that are out of
// range in order to make iterations useful (random tests still cover the full range).
let start = domain.range_start();
let end = domain.range_end();
let steps = OpITy::<Op>::try_from(ntests).unwrap_or(OpITy::<Op>::MAX);
logspace(start, end, steps).0.map(|v| (v,))
}

View file

@ -1,20 +1,28 @@
//! A generator that checks a handful of cases near infinities, zeros, asymptotes, and NaNs.
use libm::support::Float;
use libm::support::{Float, Int};
use crate::domain::HasDomain;
use crate::domain::get_domain;
use crate::gen::KnownSize;
use crate::run_cfg::{check_near_count, check_point_count};
use crate::{CheckCtx, FloatExt, MathOp};
use crate::{CheckCtx, FloatExt, MathOp, test_log};
/// Generate a sequence of edge cases, e.g. numbers near zeroes and infiniteis.
pub trait EdgeCaseInput<Op> {
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> + Send;
}
/// Create a list of values around interesting points (infinities, zeroes, NaNs).
pub fn get_test_cases<Op, F>(ctx: &CheckCtx) -> impl Iterator<Item = (F,)>
fn float_edge_cases<Op>(
ctx: &CheckCtx,
argnum: usize,
) -> (impl Iterator<Item = Op::FTy> + Clone, u64)
where
Op: MathOp<FTy = F> + HasDomain<F>,
F: Float,
Op: MathOp,
{
let mut ret = Vec::new();
let values = &mut ret;
let domain = Op::DOMAIN;
let domain = get_domain::<_, i8>(ctx.fn_ident, argnum).unwrap_float();
let domain_start = domain.range_start();
let domain_end = domain.range_end();
@ -22,17 +30,17 @@ where
let near_points = check_near_count(ctx);
// Check near some notable constants
count_up(F::ONE, near_points, values);
count_up(F::ZERO, near_points, values);
count_up(F::NEG_ONE, near_points, values);
count_down(F::ONE, near_points, values);
count_down(F::ZERO, near_points, values);
count_down(F::NEG_ONE, near_points, values);
values.push(F::NEG_ZERO);
count_up(Op::FTy::ONE, near_points, values);
count_up(Op::FTy::ZERO, near_points, values);
count_up(Op::FTy::NEG_ONE, near_points, values);
count_down(Op::FTy::ONE, near_points, values);
count_down(Op::FTy::ZERO, near_points, values);
count_down(Op::FTy::NEG_ONE, near_points, values);
values.push(Op::FTy::NEG_ZERO);
// Check values near the extremes
count_up(F::NEG_INFINITY, near_points, values);
count_down(F::INFINITY, near_points, values);
count_up(Op::FTy::NEG_INFINITY, near_points, values);
count_down(Op::FTy::INFINITY, near_points, values);
count_down(domain_end, near_points, values);
count_up(domain_start, near_points, values);
count_down(domain_start, near_points, values);
@ -40,8 +48,8 @@ where
count_down(domain_end, near_points, values);
// Check some special values that aren't included in the above ranges
values.push(F::NAN);
values.extend(F::consts().iter());
values.push(Op::FTy::NAN);
values.extend(Op::FTy::consts().iter());
// Check around asymptotes
if let Some(f) = domain.check_points {
@ -56,7 +64,18 @@ where
values.sort_by_key(|x| x.to_bits());
values.dedup_by_key(|x| x.to_bits());
ret.into_iter().map(|v| (v,))
let count = ret.len().try_into().unwrap();
test_log(&format!(
"{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {count} edge cases",
gen_kind = ctx.gen_kind,
basis = ctx.basis,
fn_ident = ctx.fn_ident,
arg = argnum + 1,
args = ctx.input_count(),
));
(ret.into_iter(), count)
}
/// Add `AROUND` values starting at and including `x` and counting up. Uses the smallest possible
@ -84,3 +103,131 @@ fn count_down<F: Float>(mut x: F, points: u64, values: &mut Vec<F>) {
count += 1;
}
}
/// Create a list of values around interesting integer points (min, zero, max).
pub fn int_edge_cases<I: Int>(
ctx: &CheckCtx,
_argnum: usize,
) -> (impl Iterator<Item = I> + Clone, u64) {
let mut values = Vec::new();
let near_points = check_near_count(ctx);
for up_from in [I::MIN, I::ZERO] {
let mut x = up_from;
for _ in 0..near_points {
values.push(x);
x += I::ONE;
}
}
for down_from in [I::ZERO, I::MAX] {
let mut x = down_from;
for _ in 0..near_points {
values.push(x);
x -= I::ONE;
}
}
values.sort();
values.dedup();
let len = values.len().try_into().unwrap();
(values.into_iter(), len)
}
macro_rules! impl_edge_case_input {
($fty:ty) => {
impl<Op> EdgeCaseInput<Op> for ($fty,)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let iter0 = iter0.map(|v| (v,));
KnownSize::new(iter0, steps0)
}
}
impl<Op> EdgeCaseInput<Op> for ($fty, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let (iter1, steps1) = float_edge_cases::<Op>(ctx, 1);
let iter =
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
let count = steps0.checked_mul(steps1).unwrap();
KnownSize::new(iter, count)
}
}
impl<Op> EdgeCaseInput<Op> for ($fty, $fty, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let (iter1, steps1) = float_edge_cases::<Op>(ctx, 1);
let (iter2, steps2) = float_edge_cases::<Op>(ctx, 2);
let iter = iter0
.flat_map(move |first| iter1.clone().map(move |second| (first, second)))
.flat_map(move |(first, second)| {
iter2.clone().map(move |third| (first, second, third))
});
let count = steps0.checked_mul(steps1).unwrap().checked_mul(steps2).unwrap();
KnownSize::new(iter, count)
}
}
impl<Op> EdgeCaseInput<Op> for (i32, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let (iter0, steps0) = int_edge_cases(ctx, 0);
let (iter1, steps1) = float_edge_cases::<Op>(ctx, 1);
let iter =
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
let count = steps0.checked_mul(steps1).unwrap();
KnownSize::new(iter, count)
}
}
impl<Op> EdgeCaseInput<Op> for ($fty, i32)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let (iter0, steps0) = float_edge_cases::<Op>(ctx, 0);
let (iter1, steps1) = int_edge_cases(ctx, 1);
let iter =
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
let count = steps0.checked_mul(steps1).unwrap();
KnownSize::new(iter, count)
}
}
};
}
#[cfg(f16_enabled)]
impl_edge_case_input!(f16);
impl_edge_case_input!(f32);
impl_edge_case_input!(f64);
#[cfg(f128_enabled)]
impl_edge_case_input!(f128);
pub fn get_test_cases<Op>(
ctx: &CheckCtx,
) -> impl ExactSizeIterator<Item = Op::RustArgs> + use<'_, Op>
where
Op: MathOp,
Op::RustArgs: EdgeCaseInput<Op>,
{
Op::RustArgs::get_cases(ctx)
}

View file

@ -9,8 +9,8 @@ use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
use super::KnownSize;
use crate::CheckCtx;
use crate::run_cfg::{int_range, iteration_count};
use crate::{CheckCtx, GeneratorKind};
pub(crate) const SEED_ENV: &str = "LIBM_SEED";
@ -52,7 +52,7 @@ macro_rules! impl_random_input {
($fty:ty) => {
impl RandomInput for ($fty,) {
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let count = iteration_count(ctx, GeneratorKind::Random, 0);
let count = iteration_count(ctx, 0);
let iter = random_floats(count).map(|f: $fty| (f,));
KnownSize::new(iter, count)
}
@ -60,8 +60,8 @@ macro_rules! impl_random_input {
impl RandomInput for ($fty, $fty) {
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
let count0 = iteration_count(ctx, 0);
let count1 = iteration_count(ctx, 1);
let iter = random_floats(count0)
.flat_map(move |f1: $fty| random_floats(count1).map(move |f2: $fty| (f1, f2)));
KnownSize::new(iter, count0 * count1)
@ -70,9 +70,9 @@ macro_rules! impl_random_input {
impl RandomInput for ($fty, $fty, $fty) {
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
let count2 = iteration_count(ctx, GeneratorKind::Random, 2);
let count0 = iteration_count(ctx, 0);
let count1 = iteration_count(ctx, 1);
let count2 = iteration_count(ctx, 2);
let iter = random_floats(count0).flat_map(move |f1: $fty| {
random_floats(count1).flat_map(move |f2: $fty| {
random_floats(count2).map(move |f3: $fty| (f1, f2, f3))
@ -84,9 +84,9 @@ macro_rules! impl_random_input {
impl RandomInput for (i32, $fty) {
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
let range0 = int_range(ctx, GeneratorKind::Random, 0);
let count0 = iteration_count(ctx, 0);
let count1 = iteration_count(ctx, 1);
let range0 = int_range(ctx, 0);
let iter = random_ints(count0, range0)
.flat_map(move |f1: i32| random_floats(count1).map(move |f2: $fty| (f1, f2)));
KnownSize::new(iter, count0 * count1)
@ -95,9 +95,9 @@ macro_rules! impl_random_input {
impl RandomInput for ($fty, i32) {
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
let range1 = int_range(ctx, GeneratorKind::Random, 1);
let count0 = iteration_count(ctx, 0);
let count1 = iteration_count(ctx, 1);
let range1 = int_range(ctx, 1);
let iter = random_floats(count0).flat_map(move |f1: $fty| {
random_ints(count1, range1.clone()).map(move |f2: i32| (f1, f2))
});

View file

@ -3,23 +3,23 @@ use std::ops::RangeInclusive;
use libm::support::{Float, MinInt};
use crate::domain::HasDomain;
use crate::domain::get_domain;
use crate::op::OpITy;
use crate::run_cfg::{int_range, iteration_count};
use crate::{CheckCtx, GeneratorKind, MathOp, linear_ints, logspace};
use crate::{CheckCtx, MathOp, linear_ints, logspace};
/// Generate a sequence of inputs that either cover the domain in completeness (for smaller float
/// Generate a sequence of inputs that eiher cover the domain in completeness (for smaller float
/// types and single argument functions) or provide evenly spaced inputs across the domain with
/// approximately `u32::MAX` total iterations.
pub trait ExtensiveInput<Op> {
pub trait SpacedInput<Op> {
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self> + Send, u64);
}
/// Construct an iterator from `logspace` and also calculate the total number of steps expected
/// for that iterator.
fn logspace_steps<Op>(
start: Op::FTy,
end: Op::FTy,
ctx: &CheckCtx,
argnum: usize,
max_steps: u64,
) -> (impl Iterator<Item = Op::FTy> + Clone, u64)
where
@ -28,6 +28,11 @@ where
u64: TryFrom<OpITy<Op>, Error: fmt::Debug>,
RangeInclusive<OpITy<Op>>: Iterator,
{
// i8 is a dummy type here, it can be any integer.
let domain = get_domain::<Op::FTy, i8>(ctx.fn_ident, argnum).unwrap_float();
let start = domain.range_start();
let end = domain.range_end();
let max_steps = OpITy::<Op>::try_from(max_steps).unwrap_or(OpITy::<Op>::MAX);
let (iter, steps) = logspace(start, end, max_steps);
@ -76,15 +81,14 @@ where
(F::Int::MIN..=F::Int::MAX).map(|bits| F::from_bits(bits))
}
macro_rules! impl_extensive_input {
macro_rules! impl_spaced_input {
($fty:ty) => {
impl<Op> ExtensiveInput<Op> for ($fty,)
impl<Op> SpacedInput<Op> for ($fty,)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
Op: HasDomain<Op::FTy>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0);
let max_steps0 = iteration_count(ctx, 0);
// `f16` and `f32` can have exhaustive tests.
match value_count::<Op::FTy>() {
Some(steps0) if steps0 <= max_steps0 => {
@ -93,9 +97,7 @@ macro_rules! impl_extensive_input {
(EitherIter::A(iter0), steps0)
}
_ => {
let start = Op::DOMAIN.range_start();
let end = Op::DOMAIN.range_end();
let (iter0, steps0) = logspace_steps::<Op>(start, end, max_steps0);
let (iter0, steps0) = logspace_steps::<Op>(ctx, 0, max_steps0);
let iter0 = iter0.map(|v| (v,));
(EitherIter::B(iter0), steps0)
}
@ -103,13 +105,13 @@ macro_rules! impl_extensive_input {
}
}
impl<Op> ExtensiveInput<Op> for ($fty, $fty)
impl<Op> SpacedInput<Op> for ($fty, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0);
let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1);
let max_steps0 = iteration_count(ctx, 0);
let max_steps1 = iteration_count(ctx, 1);
// `f16` can have exhaustive tests.
match value_count::<Op::FTy>() {
Some(count) if count <= max_steps0 && count <= max_steps1 => {
@ -118,10 +120,8 @@ macro_rules! impl_extensive_input {
(EitherIter::A(iter), count.checked_mul(count).unwrap())
}
_ => {
let start = <$fty>::NEG_INFINITY;
let end = <$fty>::INFINITY;
let (iter0, steps0) = logspace_steps::<Op>(start, end, max_steps0);
let (iter1, steps1) = logspace_steps::<Op>(start, end, max_steps1);
let (iter0, steps0) = logspace_steps::<Op>(ctx, 0, max_steps0);
let (iter1, steps1) = logspace_steps::<Op>(ctx, 1, max_steps1);
let iter = iter0.flat_map(move |first| {
iter1.clone().map(move |second| (first, second))
});
@ -132,14 +132,14 @@ macro_rules! impl_extensive_input {
}
}
impl<Op> ExtensiveInput<Op> for ($fty, $fty, $fty)
impl<Op> SpacedInput<Op> for ($fty, $fty, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0);
let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1);
let max_steps2 = iteration_count(ctx, GeneratorKind::Extensive, 2);
let max_steps0 = iteration_count(ctx, 0);
let max_steps1 = iteration_count(ctx, 1);
let max_steps2 = iteration_count(ctx, 2);
// `f16` can be exhaustive tested if `LIBM_EXTENSIVE_TESTS` is incresed.
match value_count::<Op::FTy>() {
Some(count)
@ -153,12 +153,9 @@ macro_rules! impl_extensive_input {
(EitherIter::A(iter), count.checked_pow(3).unwrap())
}
_ => {
let start = <$fty>::NEG_INFINITY;
let end = <$fty>::INFINITY;
let (iter0, steps0) = logspace_steps::<Op>(start, end, max_steps0);
let (iter1, steps1) = logspace_steps::<Op>(start, end, max_steps1);
let (iter2, steps2) = logspace_steps::<Op>(start, end, max_steps2);
let (iter0, steps0) = logspace_steps::<Op>(ctx, 0, max_steps0);
let (iter1, steps1) = logspace_steps::<Op>(ctx, 1, max_steps1);
let (iter2, steps2) = logspace_steps::<Op>(ctx, 2, max_steps2);
let iter = iter0
.flat_map(move |first| iter1.clone().map(move |second| (first, second)))
@ -174,14 +171,14 @@ macro_rules! impl_extensive_input {
}
}
impl<Op> ExtensiveInput<Op> for (i32, $fty)
impl<Op> SpacedInput<Op> for (i32, $fty)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let range0 = int_range(ctx, GeneratorKind::Extensive, 0);
let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0);
let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1);
let range0 = int_range(ctx, 0);
let max_steps0 = iteration_count(ctx, 0);
let max_steps1 = iteration_count(ctx, 1);
match value_count::<Op::FTy>() {
Some(count1) if count1 <= max_steps1 => {
let (iter0, steps0) = linear_ints(range0, max_steps0);
@ -190,11 +187,8 @@ macro_rules! impl_extensive_input {
(EitherIter::A(iter), steps0.checked_mul(count1).unwrap())
}
_ => {
let start = <$fty>::NEG_INFINITY;
let end = <$fty>::INFINITY;
let (iter0, steps0) = linear_ints(range0, max_steps0);
let (iter1, steps1) = logspace_steps::<Op>(start, end, max_steps1);
let (iter1, steps1) = logspace_steps::<Op>(ctx, 1, max_steps1);
let iter = iter0.flat_map(move |first| {
iter1.clone().map(move |second| (first, second))
@ -207,14 +201,14 @@ macro_rules! impl_extensive_input {
}
}
impl<Op> ExtensiveInput<Op> for ($fty, i32)
impl<Op> SpacedInput<Op> for ($fty, i32)
where
Op: MathOp<RustArgs = Self, FTy = $fty>,
{
fn get_cases(ctx: &CheckCtx) -> (impl Iterator<Item = Self>, u64) {
let max_steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0);
let range1 = int_range(ctx, GeneratorKind::Extensive, 1);
let max_steps1 = iteration_count(ctx, GeneratorKind::Extensive, 1);
let max_steps0 = iteration_count(ctx, 0);
let range1 = int_range(ctx, 1);
let max_steps1 = iteration_count(ctx, 1);
match value_count::<Op::FTy>() {
Some(count0) if count0 <= max_steps0 => {
let (iter1, steps1) = linear_ints(range1, max_steps1);
@ -224,10 +218,7 @@ macro_rules! impl_extensive_input {
(EitherIter::A(iter), count0.checked_mul(steps1).unwrap())
}
_ => {
let start = <$fty>::NEG_INFINITY;
let end = <$fty>::INFINITY;
let (iter0, steps0) = logspace_steps::<Op>(start, end, max_steps0);
let (iter0, steps0) = logspace_steps::<Op>(ctx, 0, max_steps0);
let (iter1, steps1) = linear_ints(range1, max_steps1);
let iter = iter0.flat_map(move |first| {
@ -244,11 +235,11 @@ macro_rules! impl_extensive_input {
}
#[cfg(f16_enabled)]
impl_extensive_input!(f16);
impl_extensive_input!(f32);
impl_extensive_input!(f64);
impl_spaced_input!(f16);
impl_spaced_input!(f32);
impl_spaced_input!(f64);
#[cfg(f128_enabled)]
impl_extensive_input!(f128);
impl_spaced_input!(f128);
/// Create a test case iterator for extensive inputs. Also returns the total test case count.
pub fn get_test_cases<Op>(
@ -256,7 +247,7 @@ pub fn get_test_cases<Op>(
) -> (impl Iterator<Item = Op::RustArgs> + Send + use<'_, Op>, u64)
where
Op: MathOp,
Op::RustArgs: ExtensiveInput<Op>,
Op::RustArgs: SpacedInput<Op>,
{
Op::RustArgs::get_cases(ctx)
}

View file

@ -39,11 +39,12 @@ pub struct CheckCtx {
pub base_name_str: &'static str,
/// Source of truth for tests.
pub basis: CheckBasis,
pub gen_kind: GeneratorKind,
}
impl CheckCtx {
/// Create a new check context, using the default ULP for the function.
pub fn new(fn_ident: Identifier, basis: CheckBasis) -> Self {
pub fn new(fn_ident: Identifier, basis: CheckBasis, gen_kind: GeneratorKind) -> Self {
let mut ret = Self {
ulp: 0,
fn_ident,
@ -51,10 +52,16 @@ impl CheckCtx {
base_name: fn_ident.base_name(),
base_name_str: fn_ident.base_name().as_str(),
basis,
gen_kind,
};
ret.ulp = crate::default_ulp(&ret);
ret
}
/// The number of input arguments for this function.
pub fn input_count(&self) -> usize {
self.fn_ident.math_op().rust_sig.args.len()
}
}
/// Possible items to test against
@ -66,11 +73,13 @@ pub enum CheckBasis {
Mpfr,
}
/// The different kinds of generators that provide test input.
/// The different kinds of generators that provide test input, which account for input pattern
/// and quantity.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum GeneratorKind {
Domain,
EdgeCases,
Extensive,
QuickSpaced,
Random,
}
@ -155,7 +164,7 @@ impl TestEnv {
}
/// The number of iterations to run for a given test.
pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> u64 {
pub fn iteration_count(ctx: &CheckCtx, argnum: usize) -> u64 {
let t_env = TestEnv::from_env(ctx);
// Ideally run 5M tests
@ -185,10 +194,13 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
// Run fewer random tests than domain tests.
let random_iter_count = domain_iter_count / 100;
let mut total_iterations = match gen_kind {
GeneratorKind::Domain => domain_iter_count,
let mut total_iterations = match ctx.gen_kind {
GeneratorKind::QuickSpaced => domain_iter_count,
GeneratorKind::Random => random_iter_count,
GeneratorKind::Extensive => *EXTENSIVE_MAX_ITERATIONS,
GeneratorKind::EdgeCases => {
unimplemented!("edge case tests shoudn't need `iteration_count`")
}
};
// FMA has a huge domain but is reasonably fast to run, so increase iterations.
@ -213,16 +225,18 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
};
let total = ntests.pow(t_env.input_count.try_into().unwrap());
let seed_msg = match gen_kind {
GeneratorKind::Domain | GeneratorKind::Extensive => String::new(),
let seed_msg = match ctx.gen_kind {
GeneratorKind::QuickSpaced | GeneratorKind::Extensive => String::new(),
GeneratorKind::Random => {
format!(" using `{SEED_ENV}={}`", str::from_utf8(SEED.as_slice()).unwrap())
}
GeneratorKind::EdgeCases => unreachable!(),
};
test_log(&format!(
"{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \
({total} total){seed_msg}",
gen_kind = ctx.gen_kind,
basis = ctx.basis,
fn_ident = ctx.fn_ident,
arg = argnum + 1,
@ -233,7 +247,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
}
/// Some tests require that an integer be kept within reasonable limits; generate that here.
pub fn int_range(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> RangeInclusive<i32> {
pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> {
let t_env = TestEnv::from_env(ctx);
if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) {
@ -252,22 +266,42 @@ pub fn int_range(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> Rang
let extensive_range = (-0xfff)..=0xfffff;
match gen_kind {
match ctx.gen_kind {
GeneratorKind::Extensive => extensive_range,
GeneratorKind::Domain | GeneratorKind::Random => non_extensive_range,
GeneratorKind::QuickSpaced | GeneratorKind::Random => non_extensive_range,
GeneratorKind::EdgeCases => extensive_range,
}
}
/// For domain tests, limit how many asymptotes or specified check points we test.
pub fn check_point_count(ctx: &CheckCtx) -> usize {
assert_eq!(
ctx.gen_kind,
GeneratorKind::EdgeCases,
"check_point_count is intended for edge case tests"
);
let t_env = TestEnv::from_env(ctx);
if t_env.slow_platform || !cfg!(optimizations_enabled) { 4 } else { 10 }
}
/// When validating points of interest (e.g. asymptotes, inflection points, extremes), also check
/// this many surrounding values.
pub fn check_near_count(_ctx: &CheckCtx) -> u64 {
if cfg!(optimizations_enabled) { 100 } else { 10 }
pub fn check_near_count(ctx: &CheckCtx) -> u64 {
assert_eq!(
ctx.gen_kind,
GeneratorKind::EdgeCases,
"check_near_count is intended for edge case tests"
);
if cfg!(optimizations_enabled) {
// Taper based on the number of inputs.
match ctx.input_count() {
1 | 2 => 100,
3 => 50,
x => panic!("unexpected argument count {x}"),
}
} else {
10
}
}
/// Check whether extensive actions should be run or skipped.

View file

@ -9,12 +9,26 @@
// There are some targets we can't build musl for
#![cfg(feature = "build-musl")]
use libm_test::domain::HasDomain;
use libm_test::gen::random::RandomInput;
use libm_test::gen::{domain_logspace, edge_cases, random};
use libm_test::{CheckBasis, CheckCtx, CheckOutput, MathOp, TupleCall};
use libm_test::gen::{edge_cases, random, spaced};
use libm_test::{CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TupleCall};
macro_rules! musl_rand_tests {
const BASIS: CheckBasis = CheckBasis::Musl;
fn musl_runner<Op: MathOp>(
ctx: &CheckCtx,
cases: impl Iterator<Item = Op::RustArgs>,
musl_fn: Op::CFn,
) {
for input in cases {
let musl_res = input.call(musl_fn);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(musl_res, input, ctx).unwrap();
}
}
/// Test against musl with generators from a domain.
macro_rules! musl_tests {
(
fn_name: $fn_name:ident,
attrs: [$($attr:meta),*],
@ -23,136 +37,50 @@ macro_rules! musl_rand_tests {
#[test]
$(#[$attr])*
fn [< musl_random_ $fn_name >]() {
test_one_random::<libm_test::op::$fn_name::Routine>(musl_math_sys::$fn_name);
type Op = libm_test::op::$fn_name::Routine;
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::Random);
let cases = random::get_test_cases::<<Op as MathOp>::RustArgs>(&ctx);
musl_runner::<Op>(&ctx, cases, musl_math_sys::$fn_name);
}
}
};
}
fn test_one_random<Op>(musl_fn: Op::CFn)
where
Op: MathOp,
Op::RustArgs: RandomInput,
{
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Musl);
let cases = random::get_test_cases::<Op::RustArgs>(&ctx);
for input in cases {
let musl_res = input.call(musl_fn);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(musl_res, input, &ctx).unwrap();
}
}
libm_macros::for_each_function! {
callback: musl_rand_tests,
// Musl does not support `f16` and `f128` on all platforms.
skip: [
copysignf128,
copysignf16,
fabsf128,
fabsf16,
fdimf128,
fdimf16,
truncf128,
truncf16,
],
attributes: [
#[cfg_attr(x86_no_sse, ignore)] // FIXME(correctness): wrong result on i586
[exp10, exp10f, exp2, exp2f, rint]
],
}
/// Test against musl with generators from a domain.
macro_rules! musl_domain_tests {
(
fn_name: $fn_name:ident,
attrs: [$($attr:meta),*],
) => {
paste::paste! {
#[test]
$(#[$attr])*
fn [< musl_edge_case_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op, _>(
edge_cases::get_test_cases::<Op, _>,
musl_math_sys::$fn_name,
);
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::EdgeCases);
let cases = edge_cases::get_test_cases::<Op>(&ctx);
musl_runner::<Op>(&ctx, cases, musl_math_sys::$fn_name);
}
#[test]
$(#[$attr])*
fn [< musl_logspace_ $fn_name >]() {
fn [< musl_quickspace_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op, _>(
domain_logspace::get_test_cases::<Op>,
musl_math_sys::$fn_name,
);
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::QuickSpaced);
let cases = spaced::get_test_cases::<Op>(&ctx).0;
musl_runner::<Op>(&ctx, cases, musl_math_sys::$fn_name);
}
}
};
}
/// Test a single routine against domaine-aware inputs.
fn domain_test_runner<Op, I>(gen: impl FnOnce(&CheckCtx) -> I, musl_fn: Op::CFn)
where
Op: MathOp,
Op: HasDomain<Op::FTy>,
I: Iterator<Item = Op::RustArgs>,
{
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Musl);
let cases = gen(&ctx);
for input in cases {
let musl_res = input.call(musl_fn);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(musl_res, input, &ctx).unwrap();
}
}
libm_macros::for_each_function! {
callback: musl_domain_tests,
callback: musl_tests,
attributes: [],
skip: [
// Functions with multiple inputs
atan2,
atan2f,
copysign,
copysignf,
copysignf16,
copysignf128,
fdim,
fdimf,
fma,
fmaf,
fmax,
fmaxf,
fmin,
fminf,
fmod,
fmodf,
hypot,
hypotf,
// TODO integer inputs
jn,
jnf,
ldexp,
ldexpf,
nextafter,
nextafterf,
pow,
powf,
remainder,
remainderf,
remquo,
remquof,
scalbn,
scalbnf,
yn,
ynf,
// Not provided by musl
copysignf128,
copysignf16,
fabsf128,
fabsf16,
fdimf128,

View file

@ -2,14 +2,23 @@
#![cfg(feature = "build-mpfr")]
use libm_test::domain::HasDomain;
use libm_test::gen::random::RandomInput;
use libm_test::gen::{domain_logspace, edge_cases, random};
use libm_test::gen::{edge_cases, random, spaced};
use libm_test::mpfloat::MpOp;
use libm_test::{CheckBasis, CheckCtx, CheckOutput, MathOp, OpFTy, OpRustFn, OpRustRet, TupleCall};
use libm_test::{CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TupleCall};
/// Test against MPFR with random inputs.
macro_rules! mp_rand_tests {
const BASIS: CheckBasis = CheckBasis::Mpfr;
fn mp_runner<Op: MathOp + MpOp>(ctx: &CheckCtx, cases: impl Iterator<Item = Op::RustArgs>) {
let mut mp_vals = Op::new_mp();
for input in cases {
let mp_res = Op::run(&mut mp_vals, input);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(mp_res, input, ctx).unwrap();
}
}
macro_rules! mp_tests {
(
fn_name: $fn_name:ident,
attrs: [$($attr:meta),*],
@ -18,32 +27,35 @@ macro_rules! mp_rand_tests {
#[test]
$(#[$attr])*
fn [< mp_random_ $fn_name >]() {
test_one_random::<libm_test::op::$fn_name::Routine>();
type Op = libm_test::op::$fn_name::Routine;
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::Random);
let cases = random::get_test_cases::<<Op as MathOp>::RustArgs>(&ctx);
mp_runner::<Op>(&ctx, cases);
}
#[test]
$(#[$attr])*
fn [< mp_edge_case_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::EdgeCases);
let cases = edge_cases::get_test_cases::<Op>(&ctx);
mp_runner::<Op>(&ctx, cases);
}
#[test]
$(#[$attr])*
fn [< mp_quickspace_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GeneratorKind::QuickSpaced);
let cases = spaced::get_test_cases::<Op>(&ctx).0;
mp_runner::<Op>(&ctx, cases);
}
}
};
}
/// Test a single routine with random inputs
fn test_one_random<Op>()
where
Op: MathOp + MpOp,
Op::RustArgs: RandomInput,
{
let mut mp_vals = Op::new_mp();
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
let cases = random::get_test_cases::<Op::RustArgs>(&ctx);
for input in cases {
let mp_res = Op::run(&mut mp_vals, input);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(mp_res, input, &ctx).unwrap();
}
}
libm_macros::for_each_function! {
callback: mp_rand_tests,
callback: mp_tests,
attributes: [
// Also an assertion failure on i686: at `MPFR_ASSERTN (! mpfr_erangeflag_p ())`
#[ignore = "large values are infeasible in MPFR"]
@ -56,97 +68,3 @@ libm_macros::for_each_function! {
nextafterf,
],
}
/// Test against MPFR with generators from a domain.
macro_rules! mp_domain_tests {
(
fn_name: $fn_name:ident,
attrs: [$($attr:meta),*],
) => {
paste::paste! {
#[test]
$(#[$attr])*
fn [< mp_edge_case_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op, _>(edge_cases::get_test_cases::<Op, _>);
}
#[test]
$(#[$attr])*
fn [< mp_logspace_ $fn_name >]() {
type Op = libm_test::op::$fn_name::Routine;
domain_test_runner::<Op, _>(domain_logspace::get_test_cases::<Op>);
}
}
};
}
/// Test a single routine against domaine-aware inputs.
fn domain_test_runner<Op, I>(gen: impl FnOnce(&CheckCtx) -> I)
where
// Complicated generics...
// The operation must take a single float argument (unary only)
Op: MathOp<RustArgs = (<Op as MathOp>::FTy,)>,
// It must also support multiprecision operations
Op: MpOp,
// And it must have a domain specified
Op: HasDomain<Op::FTy>,
// The single float argument tuple must be able to call the `RustFn` and return `RustRet`
(OpFTy<Op>,): TupleCall<OpRustFn<Op>, Output = OpRustRet<Op>>,
I: Iterator<Item = (Op::FTy,)>,
{
let mut mp_vals = Op::new_mp();
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
let cases = gen(&ctx);
for input in cases {
let mp_res = Op::run(&mut mp_vals, input);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(mp_res, input, &ctx).unwrap();
}
}
libm_macros::for_each_function! {
callback: mp_domain_tests,
attributes: [],
skip: [
// Functions with multiple inputs
atan2,
atan2f,
copysign,
copysignf,
copysignf16,
copysignf128,
fdim,
fdimf,
fdimf16,
fdimf128,
fma,
fmaf,
fmax,
fmaxf,
fmin,
fminf,
fmod,
fmodf,
hypot,
hypotf,
jn,
jnf,
ldexp,
ldexpf,
nextafter,
nextafterf,
pow,
powf,
remainder,
remainderf,
remquo,
remquof,
scalbn,
scalbnf,
yn,
ynf,
],
}

View file

@ -6,13 +6,18 @@ use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use indicatif::{ProgressBar, ProgressStyle};
use libm_test::gen::extensive::{self, ExtensiveInput};
use libm_test::gen::spaced;
use libm_test::mpfloat::MpOp;
use libm_test::{
CheckBasis, CheckCtx, CheckOutput, MathOp, TestResult, TupleCall, skip_extensive_test,
CheckBasis, CheckCtx, CheckOutput, GeneratorKind, MathOp, TestResult, TupleCall,
skip_extensive_test,
};
use libtest_mimic::{Arguments, Trial};
use rayon::prelude::*;
use spaced::SpacedInput;
const BASIS: CheckBasis = CheckBasis::Mpfr;
const GEN_KIND: GeneratorKind = GeneratorKind::Extensive;
/// Run the extensive test suite.
pub fn run() {
@ -62,10 +67,10 @@ fn register_all_tests() -> Vec<Trial> {
fn register_single_test<Op>(all: &mut Vec<Trial>)
where
Op: MathOp + MpOp,
Op::RustArgs: ExtensiveInput<Op> + Send,
Op::RustArgs: SpacedInput<Op> + Send,
{
let test_name = format!("mp_extensive_{}", Op::NAME);
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
let ctx = CheckCtx::new(Op::IDENTIFIER, BASIS, GEN_KIND);
let skip = skip_extensive_test(&ctx);
let runner = move || {
@ -73,7 +78,7 @@ where
panic!("extensive tests should be run with --release");
}
let res = run_single_test::<Op>();
let res = run_single_test::<Op>(&ctx);
let e = match res {
Ok(()) => return Ok(()),
Err(e) => e,
@ -91,18 +96,17 @@ where
}
/// Test runner for a signle routine.
fn run_single_test<Op>() -> TestResult
fn run_single_test<Op>(ctx: &CheckCtx) -> TestResult
where
Op: MathOp + MpOp,
Op::RustArgs: ExtensiveInput<Op> + Send,
Op::RustArgs: SpacedInput<Op> + Send,
{
// Small delay before printing anything so other output from the runner has a chance to flush.
std::thread::sleep(Duration::from_millis(500));
eprintln!();
let completed = AtomicU64::new(0);
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
let (ref mut cases, total) = extensive::get_test_cases::<Op>(&ctx);
let (ref mut cases, total) = spaced::get_test_cases::<Op>(ctx);
let pb = Progress::new(Op::NAME, total);
let test_single_chunk = |mp_vals: &mut Op::MpTy, input_vec: Vec<Op::RustArgs>| -> TestResult {
@ -110,7 +114,7 @@ where
// Test the input.
let mp_res = Op::run(mp_vals, input);
let crate_res = input.call(Op::ROUTINE);
crate_res.validate(mp_res, input, &ctx)?;
crate_res.validate(mp_res, input, ctx)?;
let completed = completed.fetch_add(1, Ordering::Relaxed) + 1;
pb.update(completed, input);