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

Commit 31a8c13

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 ba4bc97 commit 31a8c13

File tree

9 files changed

+434
-8
lines changed

9 files changed

+434
-8
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-public-internals"] }
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: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
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+
impl<Op> ExtensiveInput<Op> for ($fty, i32)
120+
where
121+
Op: MathOp<RustArgs = Self, FTy = $fty>,
122+
{
123+
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
124+
let start = <$fty>::NEG_INFINITY;
125+
let end = <$fty>::INFINITY;
126+
127+
let (iter0, steps0) = logspace_steps::<Op>(start, end, ctx, 0);
128+
let iter1 = int_range(ctx, GeneratorKind::Extensive, 0);
129+
let steps1 = iteration_count(ctx, GeneratorKind::Extensive, 0);
130+
131+
let iter =
132+
iter0.flat_map(move |first| iter1.clone().map(move |second| (first, second)));
133+
let count = steps0.checked_mul(steps1).unwrap();
134+
135+
KnownSize::new(iter, count)
136+
}
137+
}
138+
};
139+
}
140+
141+
impl_extensive_input!(f32);
142+
impl_extensive_input!(f64);
143+
144+
/// Create a test case iterator for extensive inputs.
145+
pub fn get_test_cases<Op>(
146+
ctx: &CheckCtx,
147+
) -> impl ExactSizeIterator<Item = Op::RustArgs> + Send + use<'_, Op>
148+
where
149+
Op: MathOp,
150+
Op::RustArgs: ExtensiveInput<Op>,
151+
{
152+
Op::RustArgs::get_cases(ctx)
153+
}

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ macro_rules! impl_random_input {
8686
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
8787
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
8888
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
89-
let range0 = int_range(ctx, 0);
89+
let range0 = int_range(ctx, GeneratorKind::Random, 0);
9090
let iter = random_ints(count0, range0)
9191
.flat_map(move |f1: i32| random_floats(count1).map(move |f2: $fty| (f1, f2)));
9292
KnownSize::new(iter, count0 * count1)
@@ -97,7 +97,7 @@ macro_rules! impl_random_input {
9797
fn get_cases(ctx: &CheckCtx) -> impl ExactSizeIterator<Item = Self> {
9898
let count0 = iteration_count(ctx, GeneratorKind::Random, 0);
9999
let count1 = iteration_count(ctx, GeneratorKind::Random, 1);
100-
let range1 = int_range(ctx, 1);
100+
let range1 = int_range(ctx, GeneratorKind::Random, 1);
101101
let iter = random_floats(count0).flat_map(move |f1: $fty| {
102102
random_ints(count1, range1.clone()).map(move |f2: i32| (f1, f2))
103103
});

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: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ use crate::{BaseName, FloatTy, Identifier, test_log};
1010
/// The environment variable indicating which extensive tests should be run.
1111
pub const EXTENSIVE_ENV: &str = "LIBM_EXTENSIVE_TESTS";
1212

13+
const EXTENSIVE_MAX_ITERATIONS: u64 = u32::MAX as u64;
14+
1315
/// Context passed to [`CheckOutput`].
1416
#[derive(Clone, Debug, PartialEq, Eq)]
1517
pub struct CheckCtx {
@@ -54,6 +56,7 @@ pub enum CheckBasis {
5456
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5557
pub enum GeneratorKind {
5658
Domain,
59+
Extensive,
5760
Random,
5861
}
5962

@@ -171,8 +174,14 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
171174
let mut total_iterations = match gen_kind {
172175
GeneratorKind::Domain => domain_iter_count,
173176
GeneratorKind::Random => random_iter_count,
177+
GeneratorKind::Extensive => EXTENSIVE_MAX_ITERATIONS,
174178
};
175179

180+
// FMA has a huge domain but is reasonably fast to run, so increase iterations.
181+
if ctx.base_name == BaseName::Fma {
182+
total_iterations *= 4;
183+
}
184+
176185
if cfg!(optimizations_enabled) {
177186
// Always run at least 10,000 tests.
178187
total_iterations = total_iterations.max(10_000);
@@ -191,7 +200,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
191200
let total = ntests.pow(t_env.input_count.try_into().unwrap());
192201

193202
let seed_msg = match gen_kind {
194-
GeneratorKind::Domain => String::new(),
203+
GeneratorKind::Domain | GeneratorKind::Extensive => String::new(),
195204
GeneratorKind::Random => {
196205
format!(" using `{SEED_ENV}={}`", str::from_utf8(SEED.as_slice()).unwrap())
197206
}
@@ -210,7 +219,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
210219
}
211220

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

216225
if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) {
@@ -221,10 +230,17 @@ pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> {
221230

222231
// The integer argument to `jn` is an iteration count. Limit this to ensure tests can be
223232
// completed in a reasonable amount of time.
224-
if t_env.slow_platform || !cfg!(optimizations_enabled) {
233+
let non_extensive_range = if t_env.slow_platform || !cfg!(optimizations_enabled) {
225234
(-0xf)..=0xff
226235
} else {
227236
(-0xff)..=0xffff
237+
};
238+
239+
let extensive_range = (-0xfff)..=0xfffff;
240+
241+
match gen_kind {
242+
GeneratorKind::Extensive => extensive_range,
243+
GeneratorKind::Domain | GeneratorKind::Random => non_extensive_range,
228244
}
229245
}
230246

@@ -241,7 +257,6 @@ pub fn check_near_count(_ctx: &CheckCtx) -> u64 {
241257
}
242258

243259
/// Check whether extensive actions should be run or skipped.
244-
#[expect(dead_code, reason = "extensive tests have not yet been added")]
245260
pub fn skip_extensive_test(ctx: &CheckCtx) -> bool {
246261
let t_env = TestEnv::from_env(ctx);
247262
!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)