Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 98b43f1

Browse files
committed
Add extensive and exhaustive tests
Add a generator that will test all inputs for input spaces `u32::MAX` or smaller (e.g. single-argument `f32` routines). For anything larger, still run approximately `u32::MAX` tests, but distribute inputs evenly across the function domain. Since we often only want to run one of these tests at a time, this implementation parallelizes within each test using `rayon`. A custom test runner is used so a progress bar is possible. Specific tests must be enabled by setting the `LIBM_EXTENSIVE_TESTS` environment variable, e.g. LIBM_EXTENSIVE_TESTS=all_f16,cos,cosf cargo run ... Testing on a recent machine, most tests take about two minutes or less. The Bessel functions are quite slow and take closer to 10 minutes, and FMA is increased to run for about the same.
1 parent b02ca43 commit 98b43f1

File tree

9 files changed

+423
-7
lines changed

9 files changed

+423
-7
lines changed

crates/libm-test/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,14 @@ short-benchmarks = []
2626
[dependencies]
2727
anyhow = "1.0.90"
2828
az = { version = "1.2.1", optional = true }
29+
indicatif = { version = "0.17.9", default-features = false }
2930
libm = { path = "../..", features = ["unstable-test-support"] }
3031
libm-macros = { path = "../libm-macros" }
3132
musl-math-sys = { path = "../musl-math-sys", optional = true }
3233
paste = "1.0.15"
3334
rand = "0.8.5"
3435
rand_chacha = "0.3.1"
36+
rayon = "1.10.0"
3537
rug = { version = "1.26.1", optional = true, default-features = false, features = ["float", "std"] }
3638

3739
[target.'cfg(target_family = "wasm")'.dependencies]
@@ -43,11 +45,18 @@ rand = { version = "0.8.5", optional = true }
4345

4446
[dev-dependencies]
4547
criterion = { version = "0.5.1", default-features = false, features = ["cargo_bench_support"] }
48+
libtest-mimic = "0.8.1"
4649

4750
[[bench]]
4851
name = "random"
4952
harness = false
5053

54+
[[test]]
55+
# No harness so that we can skip tests at runtime based on env. Prefixed with
56+
# `z` so these tests get run last.
57+
name = "z_extensive"
58+
harness = false
59+
5160
[lints.rust]
5261
# Values from the chared config.rs used by `libm` but not the test crate
5362
unexpected_cfgs = { level = "warn", check-cfg = [

crates/libm-test/src/gen.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
33
pub mod domain_logspace;
44
pub mod edge_cases;
5+
pub mod extensive;
56
pub mod random;
67

78
/// A wrapper to turn any iterator into an `ExactSizeIterator`. Asserts the final result to ensure

crates/libm-test/src/gen/extensive.rs

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
use std::fmt;
2+
use std::ops::RangeInclusive;
3+
4+
use libm::support::MinInt;
5+
6+
use crate::domain::HasDomain;
7+
use crate::gen::KnownSize;
8+
use crate::op::OpITy;
9+
use crate::run_cfg::{int_range, iteration_count};
10+
use crate::{CheckCtx, GeneratorKind, MathOp, logspace};
11+
12+
/// Generate a sequence of inputs that either cover the domain in completeness (for smaller float
13+
/// types and single argument functions) or provide evenly spaced inputs across the domain with
14+
/// approximately `u32::MAX` total iterations.
15+
pub trait ExtensiveInput<Op> {
16+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> + Send;
17+
}
18+
19+
/// Construct an iterator from `logspace` and also calculate the total number of steps expected
20+
/// for that iterator.
21+
fn logspace_steps<Op>(
22+
start: Op::FTy,
23+
end: Op::FTy,
24+
ctx: &CheckCtx,
25+
argnum: usize,
26+
) -> (impl Iterator<Item = Op::FTy> + Clone, u64)
27+
where
28+
Op: MathOp,
29+
OpITy<Op>: TryFrom<u64, Error: fmt::Debug>,
30+
RangeInclusive<OpITy<Op>>: Iterator,
31+
{
32+
let max_steps = iteration_count(ctx, GeneratorKind::Extensive, argnum);
33+
let max_steps = OpITy::<Op>::try_from(max_steps).unwrap_or(OpITy::<Op>::MAX);
34+
let iter = logspace(start, end, max_steps);
35+
36+
// `logspace` can't implement `ExactSizeIterator` because of the range, but its size hint
37+
// should be accurate (assuming <= usize::MAX iterations).
38+
let size_hint = iter.size_hint();
39+
assert_eq!(size_hint.0, size_hint.1.unwrap());
40+
41+
(iter, size_hint.0.try_into().unwrap())
42+
}
43+
44+
macro_rules! impl_extensive_input {
45+
($fty:ty) => {
46+
impl<Op> ExtensiveInput<Op> for ($fty,)
47+
where
48+
Op: MathOp<RustArgs = Self, FTy = $fty>,
49+
Op: HasDomain<Op::FTy>,
50+
{
51+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
52+
let start = Op::DOMAIN.range_start();
53+
let end = Op::DOMAIN.range_end();
54+
let (iter0, steps0) = logspace_steps::<Op>(start, end, ctx, 0);
55+
let iter0 = iter0.map(|v| (v,));
56+
KnownSize::new(iter0, steps0)
57+
}
58+
}
59+
60+
impl<Op> ExtensiveInput<Op> for ($fty, $fty)
61+
where
62+
Op: MathOp<RustArgs = Self, FTy = $fty>,
63+
{
64+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
65+
let start = <$fty>::NEG_INFINITY;
66+
let end = <$fty>::INFINITY;
67+
let (iter0, steps0) = logspace_steps::<Op>(start, end, ctx, 0);
68+
let (iter1, steps1) = logspace_steps::<Op>(start, end, ctx, 1);
69+
let iter =
70+
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
71+
let count = steps0.checked_mul(steps1).unwrap();
72+
KnownSize::new(iter, count)
73+
}
74+
}
75+
76+
impl<Op> ExtensiveInput<Op> for ($fty, $fty, $fty)
77+
where
78+
Op: MathOp<RustArgs = Self, FTy = $fty>,
79+
{
80+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
81+
let start = <$fty>::NEG_INFINITY;
82+
let end = <$fty>::INFINITY;
83+
84+
let (iter0, steps0) = logspace_steps::<Op>(start, end, ctx, 0);
85+
let (iter1, steps1) = logspace_steps::<Op>(start, end, ctx, 1);
86+
let (iter2, steps2) = logspace_steps::<Op>(start, end, ctx, 2);
87+
88+
let iter = iter0
89+
.flat_map(move |first| iter1.clone().map(move |second| (first, second)))
90+
.flat_map(move |(first, second)| {
91+
iter2.clone().map(move |third| (first, second, third))
92+
});
93+
let count = steps0.checked_mul(steps1).unwrap().checked_mul(steps2).unwrap();
94+
95+
KnownSize::new(iter, count)
96+
}
97+
}
98+
99+
impl<Op> ExtensiveInput<Op> for (i32, $fty)
100+
where
101+
Op: MathOp<RustArgs = Self, FTy = $fty>,
102+
{
103+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
104+
let start = <$fty>::NEG_INFINITY;
105+
let end = <$fty>::INFINITY;
106+
107+
let iter0 = int_range(ctx, GeneratorKind::Extensive, 0);
108+
let steps0 = iteration_count(ctx, GeneratorKind::Extensive, 0);
109+
let (iter1, steps1) = logspace_steps::<Op>(start, end, ctx, 1);
110+
111+
let iter =
112+
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
113+
let count = steps0.checked_mul(steps1).unwrap();
114+
115+
KnownSize::new(iter, count)
116+
}
117+
}
118+
};
119+
}
120+
121+
impl_extensive_input!(f32);
122+
impl_extensive_input!(f64);
123+
124+
/// Create a test case iterator for extensive inputs.
125+
pub fn get_test_cases<Op>(
126+
ctx: &CheckCtx,
127+
) -> impl ExactSizeIterator<Item = Op::RustArgs> + Send + use<'_, Op>
128+
where
129+
Op: MathOp,
130+
Op::RustArgs: ExtensiveInput<Op>,
131+
{
132+
Op::RustArgs::get_cases(ctx)
133+
}

crates/libm-test/src/gen/random.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ macro_rules! impl_random_input {
7373
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
7474
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
7575
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
76-
let range0 = int_range(ctx, 0);
76+
let range0 = int_range(ctx, GeneratorKind::Random, 0);
7777
let iter = random_ints(count0, range0)
7878
.flat_map(move |f1: i32| random_floats(count1).map(move |f2: $fty| (f1, f2)));
7979
KnownSize::new(iter, count0 * count1)
@@ -84,7 +84,7 @@ macro_rules! impl_random_input {
8484
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
8585
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
8686
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
87-
let range1 = int_range(ctx, 1);
87+
let range1 = int_range(ctx, GeneratorKind::Random, 1);
8888
let iter = random_floats(count0).flat_map(move |f1: $fty| {
8989
random_ints(count1, range1.clone()).map(move |f2: i32| (f1, f2))
9090
});

crates/libm-test/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub use libm::support::{Float, Int, IntTy, MinInt};
2525
pub use num::{FloatExt, logspace};
2626
pub use op::{BaseName, FloatTy, Identifier, MathOp, OpCFn, OpFTy, OpRustFn, OpRustRet, Ty};
2727
pub use precision::{MaybeOverride, SpecialCase, default_ulp};
28-
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind};
28+
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind, skip_extensive_test};
2929
pub use test_traits::{CheckOutput, Hex, TupleCall};
3030

3131
/// Result type for tests is usually from `anyhow`. Most times there is no success value to

crates/libm-test/src/num.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ fn as_ulp_steps<F: Float>(x: F) -> Option<F::SignedInt> {
215215
/// to logarithmic spacing of their values.
216216
///
217217
/// Note that this tends to skip negative zero, so that needs to be checked explicitly.
218-
pub fn logspace<F: FloatExt>(start: F, end: F, steps: F::Int) -> impl Iterator<Item = F>
218+
pub fn logspace<F: FloatExt>(start: F, end: F, steps: F::Int) -> impl Iterator<Item = F> + Clone
219219
where
220220
RangeInclusive<F::Int>: Iterator,
221221
{

crates/libm-test/src/run_cfg.rs

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ use crate::{BaseName, FloatTy, Identifier, test_log};
99
/// The environment variable indicating which extensive tests should be run.
1010
pub const EXTENSIVE_ENV: &str = "LIBM_EXTENSIVE_TESTS";
1111

12+
const EXTENSIVE_MAX_ITERATIONS: u64 = u32::MAX as u64;
13+
1214
/// Context passed to [`CheckOutput`].
1315
#[derive(Clone, Debug, PartialEq, Eq)]
1416
pub struct CheckCtx {
@@ -53,6 +55,7 @@ pub enum CheckBasis {
5355
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5456
pub enum GeneratorKind {
5557
Domain,
58+
Extensive,
5659
Random,
5760
}
5861

@@ -170,8 +173,14 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
170173
let mut total_iterations = match gen_kind {
171174
GeneratorKind::Domain => domain_iter_count,
172175
GeneratorKind::Random => random_iter_count,
176+
GeneratorKind::Extensive => EXTENSIVE_MAX_ITERATIONS,
173177
};
174178

179+
// FMA has a huge domain but is reasonably fast to run, so increase iterations.
180+
if ctx.base_name == BaseName::Fma {
181+
total_iterations *= 4;
182+
}
183+
175184
if cfg!(optimizations_enabled) {
176185
// Always run at least 10,000 tests.
177186
total_iterations = total_iterations.max(10_000);
@@ -202,7 +211,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
202211
}
203212

204213
/// Some tests require that an integer be kept within reasonable limits; generate that here.
205-
pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> {
214+
pub fn int_range(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> RangeInclusive<i32> {
206215
let t_env = TestEnv::from_env(ctx);
207216

208217
if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) {
@@ -213,10 +222,17 @@ pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> {
213222

214223
// The integer argument to `jn` is an iteration count. Limit this to ensure tests can be
215224
// completed in a reasonable amount of time.
216-
if t_env.slow_platform || !cfg!(optimizations_enabled) {
225+
let non_extensive_range = if t_env.slow_platform || !cfg!(optimizations_enabled) {
217226
(-0xf)..=0xff
218227
} else {
219228
(-0xff)..=0xffff
229+
};
230+
231+
let extensive_range = (-0xfff)..=0xfffff;
232+
233+
match gen_kind {
234+
GeneratorKind::Extensive => extensive_range,
235+
GeneratorKind::Domain | GeneratorKind::Random => non_extensive_range,
220236
}
221237
}
222238

@@ -233,7 +249,6 @@ pub fn check_near_count(_ctx: &CheckCtx) -> u64 {
233249
}
234250

235251
/// Check whether extensive actions should be run or skipped.
236-
#[expect(dead_code, reason = "extensive tests have not yet been added")]
237252
pub fn skip_extensive_test(ctx: &CheckCtx) -> bool {
238253
let t_env = TestEnv::from_env(ctx);
239254
!t_env.should_run_extensive
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
//! `main` is just a wrapper to handle configuration.
2+
3+
#[cfg(not(feature = "test-multiprecision"))]
4+
fn main() {
5+
eprintln!("multiprecision not enabled; skipping extensive tests");
6+
}
7+
8+
#[cfg(feature = "test-multiprecision")]
9+
mod run;
10+
11+
#[cfg(feature = "test-multiprecision")]
12+
fn main() {
13+
run::run();
14+
}

0 commit comments

Comments
 (0)