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

Commit 11369bc

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 9080785 commit 11369bc

File tree

9 files changed

+450
-8
lines changed

9 files changed

+450
-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: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ 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+
use run_cfg::EXTENSIVE_MAX_ITERATIONS;
29+
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind, skip_extensive_test};
2930
pub use test_traits::{CheckOutput, Hex, TupleCall};
3031

3132
/// Result type for tests is usually from `anyhow`. Most times there is no success value to
@@ -85,6 +86,7 @@ pub fn test_log(s: &str) {
8586
writeln!(f, "cargo features: {}", env!("CFG_CARGO_FEATURES")).unwrap();
8687
writeln!(f, "opt level: {}", env!("CFG_OPT_LEVEL")).unwrap();
8788
writeln!(f, "target features: {}", env!("CFG_TARGET_FEATURES")).unwrap();
89+
writeln!(f, "extensive iterations {}", *EXTENSIVE_MAX_ITERATIONS).unwrap();
8890

8991
Some(f)
9092
});

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: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ 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+
/// Specify the number of iterations via this environment variable, rather than using the default.
14+
pub const EXTENSIVE_ITER_ENV: &str = "LIBM_EXTENSIVE_ITERATIONS";
15+
16+
/// Maximum number of iterations to run for a single routine.
17+
///
18+
/// The default value of one greater than `u32::MAX` allows testing single-argument `f32` routines
19+
/// and single- or double-argument `f16` routines exhaustively. `f64` and `f128` can't feasibly
20+
/// be tested exhaustively; however, [`EXTENSIVE_ITER_ENV`] can be set to run tests for multiple
21+
/// hours.
22+
pub const EXTENSIVE_MAX_ITERATIONS: LazyLock<u64> = LazyLock::new(|| {
23+
let default = 1 << 32;
24+
env::var(EXTENSIVE_ITER_ENV)
25+
.map(|v| v.parse().expect("failed to parse iteration count"))
26+
.unwrap_or(default)
27+
});
28+
1329
/// Context passed to [`CheckOutput`].
1430
#[derive(Clone, Debug, PartialEq, Eq)]
1531
pub struct CheckCtx {
@@ -54,6 +70,7 @@ pub enum CheckBasis {
5470
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5571
pub enum GeneratorKind {
5672
Domain,
73+
Extensive,
5774
Random,
5875
}
5976

@@ -171,8 +188,14 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
171188
let mut total_iterations = match gen_kind {
172189
GeneratorKind::Domain => domain_iter_count,
173190
GeneratorKind::Random => random_iter_count,
191+
GeneratorKind::Extensive => *EXTENSIVE_MAX_ITERATIONS,
174192
};
175193

194+
// FMA has a huge domain but is reasonably fast to run, so increase iterations.
195+
if ctx.base_name == BaseName::Fma {
196+
total_iterations *= 4;
197+
}
198+
176199
if cfg!(optimizations_enabled) {
177200
// Always run at least 10,000 tests.
178201
total_iterations = total_iterations.max(10_000);
@@ -191,7 +214,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
191214
let total = ntests.pow(t_env.input_count.try_into().unwrap());
192215

193216
let seed_msg = match gen_kind {
194-
GeneratorKind::Domain => String::new(),
217+
GeneratorKind::Domain | GeneratorKind::Extensive => String::new(),
195218
GeneratorKind::Random => {
196219
format!(" using `{SEED_ENV}={}`", str::from_utf8(SEED.as_slice()).unwrap())
197220
}
@@ -210,7 +233,7 @@ pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -
210233
}
211234

212235
/// 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> {
236+
pub fn int_range(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> RangeInclusive<i32> {
214237
let t_env = TestEnv::from_env(ctx);
215238

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

222245
// The integer argument to `jn` is an iteration count. Limit this to ensure tests can be
223246
// completed in a reasonable amount of time.
224-
if t_env.slow_platform || !cfg!(optimizations_enabled) {
247+
let non_extensive_range = if t_env.slow_platform || !cfg!(optimizations_enabled) {
225248
(-0xf)..=0xff
226249
} else {
227250
(-0xff)..=0xffff
251+
};
252+
253+
let extensive_range = (-0xfff)..=0xfffff;
254+
255+
match gen_kind {
256+
GeneratorKind::Extensive => extensive_range,
257+
GeneratorKind::Domain | GeneratorKind::Random => non_extensive_range,
228258
}
229259
}
230260

@@ -241,7 +271,6 @@ pub fn check_near_count(_ctx: &CheckCtx) -> u64 {
241271
}
242272

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