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

Commit 8916f72

Browse files
committed
Add extensive tests
1 parent acde065 commit 8916f72

File tree

5 files changed

+328
-0
lines changed

5 files changed

+328
-0
lines changed

crates/libm-test/Cargo.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,14 @@ short-benchmarks = []
2323
[dependencies]
2424
anyhow = "1.0.90"
2525
az = { version = "1.2.1", optional = true }
26+
indicatif = { version = "0.17.9", default-features = false }
2627
libm = { path = "../..", features = ["unstable-test-support"] }
2728
libm-macros = { path = "../libm-macros" }
2829
musl-math-sys = { path = "../musl-math-sys", optional = true }
2930
paste = "1.0.15"
3031
rand = "0.8.5"
3132
rand_chacha = "0.3.1"
33+
rayon = "1.10.0"
3234
rug = { version = "1.26.1", optional = true, default-features = false, features = ["float", "std"] }
3335

3436
[target.'cfg(target_family = "wasm")'.dependencies]
@@ -40,7 +42,15 @@ rand = { version = "0.8.5", optional = true }
4042

4143
[dev-dependencies]
4244
criterion = { version = "0.5.1", default-features = false, features = ["cargo_bench_support"] }
45+
# FIXME: use the crates.io version once it supports runtime skipping of tests
46+
libtest-mimic = { git = "https://github.com/tgross35/libtest-mimic.git", rev = "4c8413b493e1b499bb941d2ced1f4c3d8462f53c" }
4347

4448
[[bench]]
4549
name = "random"
4650
harness = false
51+
52+
[[test]]
53+
# No harness so that we can skip tests at runtime based on env. Prefixed with
54+
# `z` so these tests get run last.
55+
name = "z_extensive"
56+
harness = false

crates/libm-test/src/gen.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use crate::GenerateInput;
44
pub mod domain_logspace;
55
pub mod edge_cases;
6+
pub mod extensive;
67
pub mod random;
78

89
/// Helper type to turn any reusable input into a generator.

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

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
#![allow(unused)]
2+
3+
use std::iter;
4+
5+
use libm::support::Float;
6+
7+
use crate::domain::HasDomain;
8+
use crate::num::ulp_between;
9+
use crate::{CheckCtx, FloatExt, MathOp, logspace};
10+
11+
const MAX_ITERATIONS: u64 = u32::MAX as u64;
12+
13+
pub fn get_test_cases<Op>(ctx: &CheckCtx) -> impl Iterator<Item = Op::RustArgs> + Send
14+
where
15+
Op: MathOp,
16+
Op::RustArgs: ExtensiveInput<Op>,
17+
{
18+
Op::RustArgs::gen()
19+
}
20+
21+
pub trait ExtensiveInput<Op> {
22+
fn gen() -> impl Iterator<Item = Self> + Send;
23+
fn count() -> u64 {
24+
MAX_ITERATIONS
25+
}
26+
}
27+
28+
impl<Op> ExtensiveInput<Op> for (f32,)
29+
where
30+
Op: MathOp<RustArgs = Self, FTy = f32>,
31+
Op: HasDomain<Op::FTy>,
32+
{
33+
fn gen() -> impl Iterator<Item = Self> {
34+
let mut start = Op::DOMAIN.range_start();
35+
let end = Op::DOMAIN.range_end();
36+
iter::from_fn(move || {
37+
if start > end || start >= Op::FTy::INFINITY {
38+
return None;
39+
}
40+
let ret = start;
41+
start = FloatExt::next_up(start);
42+
Some((ret,))
43+
})
44+
}
45+
46+
fn count() -> u64 {
47+
u64::from(ulp_between(Op::DOMAIN.range_start(), Op::DOMAIN.range_end()).unwrap()) + 1
48+
}
49+
}
50+
51+
impl<Op> ExtensiveInput<Op> for (f64,)
52+
where
53+
Op: MathOp<RustArgs = Self, FTy = f64>,
54+
Op: HasDomain<Op::FTy>,
55+
{
56+
fn gen() -> impl Iterator<Item = Self> {
57+
let start = Op::DOMAIN.range_start();
58+
let end = Op::DOMAIN.range_end();
59+
let steps = <Op::FTy as Float>::Int::try_from(MAX_ITERATIONS)
60+
.unwrap_or(<Op::FTy as Float>::Int::MAX);
61+
logspace(start, end, steps).map(|v| (v,))
62+
}
63+
}
64+
65+
impl<Op> ExtensiveInput<Op> for (f32, f32)
66+
where
67+
Op: MathOp<RustArgs = Self, FTy = f32>,
68+
{
69+
fn gen() -> impl Iterator<Item = Self> {
70+
let start = f32::NEG_INFINITY;
71+
let end = f32::INFINITY;
72+
let per_arg = MAX_ITERATIONS.isqrt().try_into().unwrap();
73+
logspace(start, end, per_arg)
74+
.flat_map(move |first| logspace(start, end, per_arg).map(move |second| (first, second)))
75+
}
76+
}
77+
78+
impl<Op> ExtensiveInput<Op> for (f64, f64)
79+
where
80+
Op: MathOp<RustArgs = Self, FTy = f64>,
81+
{
82+
fn gen() -> impl Iterator<Item = Self> {
83+
let start = f64::NEG_INFINITY;
84+
let end = f64::INFINITY;
85+
let per_arg = MAX_ITERATIONS.isqrt();
86+
logspace(start, end, per_arg)
87+
.flat_map(move |first| logspace(start, end, per_arg).map(move |second| (first, second)))
88+
}
89+
}
90+
91+
impl<Op> ExtensiveInput<Op> for (f32, f32, f32)
92+
where
93+
Op: MathOp<RustArgs = Self, FTy = f32>,
94+
{
95+
fn gen() -> impl Iterator<Item = Self> {
96+
let start = f32::NEG_INFINITY;
97+
let end = f32::INFINITY;
98+
let per_arg = (MAX_ITERATIONS as f32).cbrt() as u32;
99+
logspace(start, end, per_arg)
100+
.flat_map(move |first| logspace(start, end, per_arg).map(move |second| (first, second)))
101+
.flat_map(move |(first, second)| {
102+
logspace(start, end, per_arg).map(move |third| (first, second, third))
103+
})
104+
}
105+
}
106+
107+
impl<Op> ExtensiveInput<Op> for (f64, f64, f64)
108+
where
109+
Op: MathOp<RustArgs = Self, FTy = f64>,
110+
{
111+
fn gen() -> impl Iterator<Item = Self> {
112+
let start = f64::NEG_INFINITY;
113+
let end = f64::INFINITY;
114+
let per_arg = (MAX_ITERATIONS as f32).cbrt() as u64;
115+
logspace(start, end, per_arg)
116+
.flat_map(move |first| logspace(start, end, per_arg).map(move |second| (first, second)))
117+
.flat_map(move |(first, second)| {
118+
logspace(start, end, per_arg).map(move |third| (first, second, third))
119+
})
120+
}
121+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
#[cfg(not(feature = "test-multiprecision"))]
2+
fn main() {}
3+
4+
#[cfg(feature = "test-multiprecision")]
5+
mod run;
6+
7+
#[cfg(feature = "test-multiprecision")]
8+
fn main() {
9+
run::run();
10+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
//! Exhaustive tests for `f16` and `f32`, high-iteration for `f64` and `f128`.
2+
3+
use std::sync::atomic::{AtomicU64, Ordering};
4+
use std::time::Duration;
5+
6+
use indicatif::{ProgressBar, ProgressStyle};
7+
use libm_test::gen::extensive::{self, ExtensiveInput};
8+
use libm_test::mpfloat::MpOp;
9+
use libm_test::{
10+
CheckBasis, CheckCtx, CheckOutput, EXTENSIVE_ENV, GeneratorKind, MathOp, TestAction,
11+
TestResult, TupleCall, get_iterations,
12+
};
13+
use libtest_mimic::{Arguments, Completion, Trial};
14+
use rayon::prelude::*;
15+
16+
/// Template for `indicatif` progress bars.
17+
const PB_TEMPLATE: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
18+
{human_pos:>13}/{human_len:13} {per_sec:18} eta {eta:8} {msg}";
19+
const PB_TEMPLATE_FINAL: &str = "[{elapsed:3} {percent:3}%] {bar:20.cyan/blue} NAME \
20+
{human_pos:>13}/{human_len:13} {per_sec:18} done in {elapsed_precise}";
21+
22+
pub fn run() {
23+
let mut args = Arguments::from_args();
24+
// Prevent multiple tests from running in parallel, each test gets parallized internally.
25+
args.test_threads = Some(1);
26+
let tests = register_tests();
27+
28+
// With default parallelism, the CPU doesn't saturate. We don't need to be nice to
29+
// other processes, so do 1.5x to make sure we use all available resources.
30+
let threads = std::thread::available_parallelism().map(Into::into).unwrap_or(0) * 3 / 2;
31+
rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap();
32+
33+
libtest_mimic::run(&args, tests).exit();
34+
}
35+
36+
macro_rules! mp_extensive_tests {
37+
(
38+
fn_name: $fn_name:ident,
39+
extra: [$push_to:ident],
40+
) => {
41+
register_one::<libm_test::op::$fn_name::Routine>(&mut $push_to, stringify!($fn_name));
42+
};
43+
}
44+
45+
fn register_tests() -> Vec<Trial> {
46+
let mut all_tests = Vec::new();
47+
libm_macros::for_each_function! {
48+
callback: mp_extensive_tests,
49+
extra: [all_tests],
50+
skip: [
51+
// TODO
52+
jn,
53+
jnf,
54+
55+
// FIXME: MPFR tests needed
56+
frexp,
57+
frexpf,
58+
ilogb,
59+
ilogbf,
60+
ldexp,
61+
ldexpf,
62+
modf,
63+
modff,
64+
remquo,
65+
remquof,
66+
scalbn,
67+
scalbnf,
68+
69+
// FIXME: test needed, see
70+
// https://github.com/rust-lang/libm/pull/311#discussion_r1818273392
71+
nextafter,
72+
nextafterf,
73+
],
74+
}
75+
all_tests
76+
}
77+
78+
/// Add a single test to the list.
79+
fn register_one<Op>(all: &mut Vec<Trial>, name: &'static str)
80+
where
81+
Op: MathOp + MpOp,
82+
Op::RustArgs: ExtensiveInput<Op> + Send,
83+
{
84+
let test_name = format!("mp_extensive_{name}");
85+
all.push(Trial::skippable_test(test_name, move || {
86+
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
87+
let action = get_iterations(&ctx, GeneratorKind::Extensive, 0);
88+
match action {
89+
TestAction::Run => (),
90+
TestAction::Iterations(_) => panic!("extensive tests disregard iteration counts"),
91+
TestAction::Skip => {
92+
return Ok(Completion::Ignored {
93+
reason: format!("extensive tests are only run if specified in {EXTENSIVE_ENV}"),
94+
});
95+
}
96+
};
97+
98+
if !cfg!(optimizations_enabled) {
99+
panic!("extensivetests should be run with --release");
100+
}
101+
102+
test_one::<Op>(name).map(|()| Completion::Completed).map_err(Into::into)
103+
}));
104+
}
105+
106+
/// Test runner for a signle routine.
107+
fn test_one<Op>(name: &str) -> TestResult
108+
where
109+
Op: MathOp + MpOp,
110+
Op::RustArgs: ExtensiveInput<Op> + Send,
111+
{
112+
// How may iterations have been completed.
113+
static COMPLETED: AtomicU64 = AtomicU64::new(0);
114+
let ctx = CheckCtx::new(Op::IDENTIFIER, CheckBasis::Mpfr);
115+
116+
let expected_checks = Op::RustArgs::count();
117+
let name_padded = format!("{name:9}");
118+
let pb_style = ProgressStyle::with_template(&PB_TEMPLATE.replace("NAME", &name_padded))
119+
.unwrap()
120+
.progress_chars("##-");
121+
122+
// Just delay a bit before printing anything so other output (ignored tests list) has a
123+
// chance to flush.
124+
std::thread::sleep(Duration::from_millis(500));
125+
126+
eprintln!("starting extensive tests for `{name}`");
127+
let pb = ProgressBar::new(expected_checks);
128+
COMPLETED.store(0, Ordering::Relaxed);
129+
pb.set_style(pb_style);
130+
131+
let run_single_input = |mp_vals: &mut Op::MpTy, input: Op::RustArgs| -> TestResult {
132+
let mp_res = Op::run(mp_vals, input);
133+
let crate_res = input.call(Op::ROUTINE);
134+
135+
crate_res.validate(mp_res, input, &ctx)?;
136+
let completed = COMPLETED.fetch_add(1, Ordering::Relaxed) + 1;
137+
138+
// Infrequently update the progress bar.
139+
if completed % 20_000 == 0 {
140+
pb.set_position(completed);
141+
}
142+
if completed % 500_000 == 0 {
143+
pb.set_message(format!("input: {input:?}"));
144+
}
145+
Ok(())
146+
};
147+
148+
let cases = &mut extensive::get_test_cases::<Op>(&ctx);
149+
150+
// Chunk the cases so Rayon doesn't switch threads between each iterator item. 50k seems to be
151+
// a performance sweet spot.
152+
//
153+
// This allocates then discards a vector for each chunk. These should be reusable
154+
let chunk_size = 50_000;
155+
let chunks = std::iter::from_fn(move || {
156+
let v = Vec::from_iter(cases.take(chunk_size));
157+
(!v.is_empty()).then_some(v)
158+
});
159+
160+
let res = chunks.par_bridge().try_for_each_init(
161+
|| Op::new_mp(),
162+
|mp_vals, input_vec| -> TestResult {
163+
for x in input_vec {
164+
run_single_input(mp_vals, x)?;
165+
}
166+
Ok(())
167+
},
168+
);
169+
170+
let total_run = COMPLETED.load(Ordering::Relaxed);
171+
172+
let pb_style = ProgressStyle::with_template(&PB_TEMPLATE_FINAL.replace("NAME", &name_padded))
173+
.unwrap()
174+
.progress_chars("##-");
175+
pb.set_style(pb_style);
176+
pb.set_position(total_run);
177+
pb.abandon();
178+
179+
if total_run != expected_checks {
180+
// Provide a warning if our estimate needs to be updated.
181+
eprintln!("WARNING: total run {total_run} does not match expected {expected_checks}");
182+
}
183+
184+
eprintln!();
185+
res
186+
}

0 commit comments

Comments
 (0)