Rewrite the random test generator
Currently, all inputs are generated and then cached. This works reasonably well but it isn't very configurable or extensible (adding `f16` and `f128` is awkward). Replace this with a trait for generating random sequences of tuples. This also removes possible storage limitations of caching all inputs.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
//! Different generators that can create random or systematic bit patterns.
|
||||
|
||||
use crate::GenerateInput;
|
||||
pub mod domain_logspace;
|
||||
pub mod edge_cases;
|
||||
pub mod random;
|
||||
@@ -41,71 +40,3 @@ impl<I: Iterator> Iterator for KnownSize<I> {
|
||||
}
|
||||
|
||||
impl<I: Iterator> ExactSizeIterator for KnownSize<I> {}
|
||||
|
||||
/// Helper type to turn any reusable input into a generator.
|
||||
#[derive(Clone, Debug, Default)]
|
||||
pub struct CachedInput {
|
||||
pub inputs_f32: Vec<(f32, f32, f32)>,
|
||||
pub inputs_f64: Vec<(f64, f64, f64)>,
|
||||
pub inputs_i32: Vec<(i32, i32, i32)>,
|
||||
}
|
||||
|
||||
impl GenerateInput<(f32,)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f32,)> {
|
||||
self.inputs_f32.iter().map(|f| (f.0,))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f32, f32)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f32, f32)> {
|
||||
self.inputs_f32.iter().map(|f| (f.0, f.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(i32, f32)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (i32, f32)> {
|
||||
self.inputs_i32.iter().zip(self.inputs_f32.iter()).map(|(i, f)| (i.0, f.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f32, i32)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f32, i32)> {
|
||||
GenerateInput::<(i32, f32)>::get_cases(self).map(|(i, f)| (f, i))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f32, f32, f32)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f32, f32, f32)> {
|
||||
self.inputs_f32.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f64,)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f64,)> {
|
||||
self.inputs_f64.iter().map(|f| (f.0,))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f64, f64)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f64, f64)> {
|
||||
self.inputs_f64.iter().map(|f| (f.0, f.1))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(i32, f64)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (i32, f64)> {
|
||||
self.inputs_i32.iter().zip(self.inputs_f64.iter()).map(|(i, f)| (i.0, f.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f64, i32)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f64, i32)> {
|
||||
GenerateInput::<(i32, f64)>::get_cases(self).map(|(i, f)| (f, i))
|
||||
}
|
||||
}
|
||||
|
||||
impl GenerateInput<(f64, f64, f64)> for CachedInput {
|
||||
fn get_cases(&self) -> impl Iterator<Item = (f64, f64, f64)> {
|
||||
self.inputs_f64.iter().copied()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,120 +1,118 @@
|
||||
//! A simple generator that produces deterministic random input, caching to use the same
|
||||
//! inputs for all functions.
|
||||
|
||||
use std::env;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use libm::support::Float;
|
||||
use rand::distributions::{Alphanumeric, Standard};
|
||||
use rand::prelude::Distribution;
|
||||
use rand::{Rng, SeedableRng};
|
||||
use rand_chacha::ChaCha8Rng;
|
||||
|
||||
use super::CachedInput;
|
||||
use crate::{BaseName, CheckCtx, GenerateInput};
|
||||
use super::KnownSize;
|
||||
use crate::run_cfg::{int_range, iteration_count};
|
||||
use crate::{CheckCtx, GeneratorKind};
|
||||
|
||||
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
|
||||
pub(crate) const SEED_ENV: &str = "LIBM_SEED";
|
||||
|
||||
/// Number of tests to run.
|
||||
// FIXME(ntests): clean this up when possible
|
||||
const NTESTS: usize = {
|
||||
if cfg!(optimizations_enabled) {
|
||||
if crate::emulated()
|
||||
|| !cfg!(target_pointer_width = "64")
|
||||
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
|
||||
{
|
||||
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
|
||||
// in QEMU.
|
||||
100_000
|
||||
} else {
|
||||
5_000_000
|
||||
}
|
||||
} else {
|
||||
// Without optimizations just run a quick check
|
||||
800
|
||||
}
|
||||
};
|
||||
pub(crate) static SEED: LazyLock<[u8; 32]> = LazyLock::new(|| {
|
||||
let s = env::var(SEED_ENV).unwrap_or_else(|_| {
|
||||
let mut rng = rand::thread_rng();
|
||||
(0..32).map(|_| rng.sample(Alphanumeric) as char).collect()
|
||||
});
|
||||
|
||||
/// Tested inputs.
|
||||
static TEST_CASES: LazyLock<CachedInput> = LazyLock::new(|| make_test_cases(NTESTS));
|
||||
|
||||
/// The first argument to `jn` and `jnf` is the number of iterations. Make this a reasonable
|
||||
/// value so tests don't run forever.
|
||||
static TEST_CASES_JN: LazyLock<CachedInput> = LazyLock::new(|| {
|
||||
// Start with regular test cases
|
||||
let mut cases = (*TEST_CASES).clone();
|
||||
|
||||
// These functions are extremely slow, limit them
|
||||
let ntests_jn = (NTESTS / 1000).max(80);
|
||||
cases.inputs_i32.truncate(ntests_jn);
|
||||
cases.inputs_f32.truncate(ntests_jn);
|
||||
cases.inputs_f64.truncate(ntests_jn);
|
||||
|
||||
// It is easy to overflow the stack with these in debug mode
|
||||
let max_iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") {
|
||||
0xffff
|
||||
} else if cfg!(windows) {
|
||||
0x00ff
|
||||
} else {
|
||||
0x0fff
|
||||
};
|
||||
|
||||
let mut rng = ChaCha8Rng::from_seed(SEED);
|
||||
|
||||
for case in cases.inputs_i32.iter_mut() {
|
||||
case.0 = rng.gen_range(3..=max_iterations);
|
||||
}
|
||||
|
||||
cases
|
||||
s.as_bytes().try_into().unwrap_or_else(|_| {
|
||||
panic!("Seed must be 32 characters, got `{s}`");
|
||||
})
|
||||
});
|
||||
|
||||
fn make_test_cases(ntests: usize) -> CachedInput {
|
||||
let mut rng = ChaCha8Rng::from_seed(SEED);
|
||||
|
||||
// make sure we include some basic cases
|
||||
let mut inputs_i32 = vec![(0, 0, 0), (1, 1, 1), (-1, -1, -1)];
|
||||
let mut inputs_f32 = vec![
|
||||
(0.0, 0.0, 0.0),
|
||||
(f32::EPSILON, f32::EPSILON, f32::EPSILON),
|
||||
(f32::INFINITY, f32::INFINITY, f32::INFINITY),
|
||||
(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
|
||||
(f32::MAX, f32::MAX, f32::MAX),
|
||||
(f32::MIN, f32::MIN, f32::MIN),
|
||||
(f32::MIN_POSITIVE, f32::MIN_POSITIVE, f32::MIN_POSITIVE),
|
||||
(f32::NAN, f32::NAN, f32::NAN),
|
||||
];
|
||||
let mut inputs_f64 = vec![
|
||||
(0.0, 0.0, 0.0),
|
||||
(f64::EPSILON, f64::EPSILON, f64::EPSILON),
|
||||
(f64::INFINITY, f64::INFINITY, f64::INFINITY),
|
||||
(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
|
||||
(f64::MAX, f64::MAX, f64::MAX),
|
||||
(f64::MIN, f64::MIN, f64::MIN),
|
||||
(f64::MIN_POSITIVE, f64::MIN_POSITIVE, f64::MIN_POSITIVE),
|
||||
(f64::NAN, f64::NAN, f64::NAN),
|
||||
];
|
||||
|
||||
inputs_i32.extend((0..(ntests - inputs_i32.len())).map(|_| rng.gen::<(i32, i32, i32)>()));
|
||||
|
||||
// Generate integers to get a full range of bitpatterns, then convert back to
|
||||
// floats.
|
||||
inputs_f32.extend((0..(ntests - inputs_f32.len())).map(|_| {
|
||||
let ints = rng.gen::<(u32, u32, u32)>();
|
||||
(f32::from_bits(ints.0), f32::from_bits(ints.1), f32::from_bits(ints.2))
|
||||
}));
|
||||
inputs_f64.extend((0..(ntests - inputs_f64.len())).map(|_| {
|
||||
let ints = rng.gen::<(u64, u64, u64)>();
|
||||
(f64::from_bits(ints.0), f64::from_bits(ints.1), f64::from_bits(ints.2))
|
||||
}));
|
||||
|
||||
CachedInput { inputs_f32, inputs_f64, inputs_i32 }
|
||||
/// Generate a sequence of random values of this type.
|
||||
pub trait RandomInput {
|
||||
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self>;
|
||||
}
|
||||
|
||||
/// Generate a sequence of deterministically random floats.
|
||||
fn random_floats<F: Float>(count: u64) -> impl Iterator<Item = F>
|
||||
where
|
||||
Standard: Distribution<F::Int>,
|
||||
{
|
||||
let mut rng = ChaCha8Rng::from_seed(*SEED);
|
||||
|
||||
// Generate integers to get a full range of bitpatterns (including NaNs), then convert back
|
||||
// to the float type.
|
||||
(0..count).map(move |_| F::from_bits(rng.gen::<F::Int>()))
|
||||
}
|
||||
|
||||
/// Generate a sequence of deterministically random `i32`s within a specified range.
|
||||
fn random_ints(count: u64, range: RangeInclusive<i32>) -> impl Iterator<Item = i32> {
|
||||
let mut rng = ChaCha8Rng::from_seed(*SEED);
|
||||
(0..count).map(move |_| rng.gen_range::<i32, _>(range.clone()))
|
||||
}
|
||||
|
||||
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 iter = random_floats(count).map(|f: $fty| (f,));
|
||||
KnownSize::new(iter, count)
|
||||
}
|
||||
}
|
||||
|
||||
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 iter = random_floats(count0)
|
||||
.flat_map(move |f1: $fty| random_floats(count1).map(move |f2: $fty| (f1, f2)));
|
||||
KnownSize::new(iter, count0 * count1)
|
||||
}
|
||||
}
|
||||
|
||||
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 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))
|
||||
})
|
||||
});
|
||||
KnownSize::new(iter, count0 * count1 * count2)
|
||||
}
|
||||
}
|
||||
|
||||
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, 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)
|
||||
}
|
||||
}
|
||||
|
||||
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, 1);
|
||||
let iter = random_floats(count0).flat_map(move |f1: $fty| {
|
||||
random_ints(count1, range1.clone()).map(move |f2: i32| (f1, f2))
|
||||
});
|
||||
KnownSize::new(iter, count0 * count1)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_random_input!(f32);
|
||||
impl_random_input!(f64);
|
||||
|
||||
/// Create a test case iterator.
|
||||
pub fn get_test_cases<RustArgs>(ctx: &CheckCtx) -> impl Iterator<Item = RustArgs>
|
||||
where
|
||||
CachedInput: GenerateInput<RustArgs>,
|
||||
{
|
||||
let inputs = if ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn {
|
||||
&TEST_CASES_JN
|
||||
} else {
|
||||
&TEST_CASES
|
||||
};
|
||||
inputs.get_cases()
|
||||
pub fn get_test_cases<RustArgs: RandomInput>(
|
||||
ctx: &CheckCtx,
|
||||
) -> impl Iterator<Item = RustArgs> + use<'_, RustArgs> {
|
||||
RustArgs::get_cases(ctx)
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ pub use num::{FloatExt, logspace};
|
||||
pub use op::{BaseName, FloatTy, Identifier, MathOp, OpCFn, OpFTy, OpRustFn, OpRustRet, Ty};
|
||||
pub use precision::{MaybeOverride, SpecialCase, default_ulp};
|
||||
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind};
|
||||
pub use test_traits::{CheckOutput, GenerateInput, Hex, TupleCall};
|
||||
pub use test_traits::{CheckOutput, Hex, TupleCall};
|
||||
|
||||
/// Result type for tests is usually from `anyhow`. Most times there is no success value to
|
||||
/// propagate.
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Configuration for how tests get run.
|
||||
|
||||
use std::env;
|
||||
use std::ops::RangeInclusive;
|
||||
use std::sync::LazyLock;
|
||||
use std::{env, str};
|
||||
|
||||
use crate::gen::random::{SEED, SEED_ENV};
|
||||
use crate::{BaseName, FloatTy, Identifier, test_log};
|
||||
|
||||
/// The environment variable indicating which extensive tests should be run.
|
||||
@@ -188,9 +190,16 @@ 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 => String::new(),
|
||||
GeneratorKind::Random => {
|
||||
format!(" using `{SEED_ENV}={}`", str::from_utf8(SEED.as_slice()).unwrap())
|
||||
}
|
||||
};
|
||||
|
||||
test_log(&format!(
|
||||
"{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \
|
||||
({total} total)",
|
||||
({total} total){seed_msg}",
|
||||
basis = ctx.basis,
|
||||
fn_ident = ctx.fn_ident,
|
||||
arg = argnum + 1,
|
||||
@@ -200,6 +209,25 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
|
||||
ntests
|
||||
}
|
||||
|
||||
/// Some tests require that an integer be kept within reasonable limits; generate that here.
|
||||
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) {
|
||||
return i32::MIN..=i32::MAX;
|
||||
}
|
||||
|
||||
assert_eq!(argnum, 0, "For `jn`/`yn`, only the first argument takes an integer");
|
||||
|
||||
// The integer argument to `jn` is an iteration count. Limit this to ensure tests can be
|
||||
// completed in a reasonable amount of time.
|
||||
if t_env.slow_platform || !cfg!(optimizations_enabled) {
|
||||
(-0xf)..=0xff
|
||||
} else {
|
||||
(-0xff)..=0xffff
|
||||
}
|
||||
}
|
||||
|
||||
/// For domain tests, limit how many asymptotes or specified check points we test.
|
||||
pub fn check_point_count(ctx: &CheckCtx) -> usize {
|
||||
let t_env = TestEnv::from_env(ctx);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
//! Traits related to testing.
|
||||
//!
|
||||
//! There are three main traits in this module:
|
||||
//! There are two main traits in this module:
|
||||
//!
|
||||
//! - `GenerateInput`: implemented on any types that create test cases.
|
||||
//! - `TupleCall`: implemented on tuples to allow calling them as function arguments.
|
||||
//! - `CheckOutput`: implemented on anything that is an output type for validation against an
|
||||
//! expected value.
|
||||
@@ -13,11 +12,6 @@ use anyhow::{Context, bail, ensure};
|
||||
|
||||
use crate::{CheckCtx, Float, Int, MaybeOverride, SpecialCase, TestResult};
|
||||
|
||||
/// Implement this on types that can generate a sequence of tuples for test input.
|
||||
pub trait GenerateInput<TupleArgs> {
|
||||
fn get_cases(&self) -> impl Iterator<Item = TupleArgs>;
|
||||
}
|
||||
|
||||
/// Trait for calling a function with a tuple as arguments.
|
||||
///
|
||||
/// Implemented on the tuple with the function signature as the generic (so we can use the same
|
||||
|
||||
Reference in New Issue
Block a user