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

Commit db76a54

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 ...
1 parent 200a876 commit db76a54

File tree

9 files changed

+376
-7
lines changed

9 files changed

+376
-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
@@ -23,7 +23,7 @@ pub use libm::support::{Float, Int, IntTy, MinInt};
2323
pub use num::{FloatExt, logspace};
2424
pub use op::{BaseName, FloatTy, Identifier, MathOp, OpCFn, OpFTy, OpRustFn, OpRustRet, Ty};
2525
pub use precision::{MaybeOverride, SpecialCase, default_ulp};
26-
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind};
26+
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind, skip_extensive_test};
2727
pub use test_traits::{CheckOutput, Hex, TupleCall};
2828

2929
/// 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 ctx.base_name != BaseName::Jn {
@@ -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)