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

Commit aaa48b3

Browse files
committed
Replace NTESTS with better configuration
1 parent 095bc3f commit aaa48b3

File tree

5 files changed

+234
-57
lines changed

5 files changed

+234
-57
lines changed

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

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -4,40 +4,25 @@ use libm::support::{IntTy, MinInt};
44

55
use crate::domain::HasDomain;
66
use crate::op::OpITy;
7+
use crate::run_cfg::{GeneratorKind, iteration_count};
78
use crate::{CheckCtx, MathOp, logspace};
89

9-
/// Number of tests to run.
10-
// FIXME(ntests): replace this with a more logical algorithm
11-
const NTESTS: usize = {
12-
if cfg!(optimizations_enabled) {
13-
if crate::emulated()
14-
|| !cfg!(target_pointer_width = "64")
15-
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
16-
{
17-
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run
18-
// in QEMU.
19-
100_000
20-
} else {
21-
5_000_000
22-
}
23-
} else {
24-
// Without optimizations just run a quick check
25-
800
26-
}
27-
};
28-
2910
/// Create a range of logarithmically spaced inputs within a function's domain.
3011
///
3112
/// This allows us to get reasonably thorough coverage without wasting time on values that are
3213
/// NaN or out of range. Random tests will still cover values that are excluded here.
33-
pub fn get_test_cases<Op>(_ctx: &CheckCtx) -> impl Iterator<Item = (Op::FTy,)>
14+
pub fn get_test_cases<Op>(ctx: &CheckCtx) -> impl Iterator<Item = (Op::FTy,)>
3415
where
3516
Op: MathOp + HasDomain<Op::FTy>,
36-
IntTy<Op::FTy>: TryFrom<usize>,
17+
IntTy<Op::FTy>: TryFrom<u64>,
3718
{
3819
let domain = Op::DOMAIN;
20+
let ntests = iteration_count(ctx, GeneratorKind::Domain, 0);
21+
22+
// We generate logspaced inputs within a specific range, excluding values that are out of
23+
// range in order to make iterations useful (random tests still cover the full range).
3924
let start = domain.range_start();
4025
let end = domain.range_end();
41-
let steps = OpITy::<Op>::try_from(NTESTS).unwrap_or(OpITy::<Op>::MAX);
26+
let steps = OpITy::<Op>::try_from(ntests).unwrap_or(OpITy::<Op>::MAX);
4227
logspace(start, end, steps).map(|v| (v,))
4328
}

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

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,11 @@
33
use libm::support::Float;
44

55
use crate::domain::HasDomain;
6+
use crate::run_cfg::{check_near_count, check_point_count};
67
use crate::{CheckCtx, FloatExt, MathOp};
78

8-
/// Number of values near an interesting point to check.
9-
// FIXME(ntests): replace this with a more logical algorithm
10-
const AROUND: usize = 100;
11-
12-
/// Functions have infinite asymptotes, limit how many we check.
13-
// FIXME(ntests): replace this with a more logical algorithm
14-
const MAX_CHECK_POINTS: usize = 10;
15-
169
/// Create a list of values around interesting points (infinities, zeroes, NaNs).
17-
pub fn get_test_cases<Op, F>(_ctx: &CheckCtx) -> impl Iterator<Item = (F,)>
10+
pub fn get_test_cases<Op, F>(ctx: &CheckCtx) -> impl Iterator<Item = (F,)>
1811
where
1912
Op: MathOp<FTy = F> + HasDomain<F>,
2013
F: Float,
@@ -25,23 +18,26 @@ where
2518
let domain_start = domain.range_start();
2619
let domain_end = domain.range_end();
2720

21+
let check_points = check_point_count(ctx);
22+
let near_points = check_near_count(ctx);
23+
2824
// Check near some notable constants
29-
count_up(F::ONE, values);
30-
count_up(F::ZERO, values);
31-
count_up(F::NEG_ONE, values);
32-
count_down(F::ONE, values);
33-
count_down(F::ZERO, values);
34-
count_down(F::NEG_ONE, values);
25+
count_up(F::ONE, near_points, values);
26+
count_up(F::ZERO, near_points, values);
27+
count_up(F::NEG_ONE, near_points, values);
28+
count_down(F::ONE, near_points, values);
29+
count_down(F::ZERO, near_points, values);
30+
count_down(F::NEG_ONE, near_points, values);
3531
values.push(F::NEG_ZERO);
3632

3733
// Check values near the extremes
38-
count_up(F::NEG_INFINITY, values);
39-
count_down(F::INFINITY, values);
40-
count_down(domain_end, values);
41-
count_up(domain_start, values);
42-
count_down(domain_start, values);
43-
count_up(domain_end, values);
44-
count_down(domain_end, values);
34+
count_up(F::NEG_INFINITY, near_points, values);
35+
count_down(F::INFINITY, near_points, values);
36+
count_down(domain_end, near_points, values);
37+
count_up(domain_start, near_points, values);
38+
count_down(domain_start, near_points, values);
39+
count_up(domain_end, near_points, values);
40+
count_down(domain_end, near_points, values);
4541

4642
// Check some special values that aren't included in the above ranges
4743
values.push(F::NAN);
@@ -50,9 +46,9 @@ where
5046
// Check around asymptotes
5147
if let Some(f) = domain.check_points {
5248
let iter = f();
53-
for x in iter.take(MAX_CHECK_POINTS) {
54-
count_up(x, values);
55-
count_down(x, values);
49+
for x in iter.take(check_points) {
50+
count_up(x, near_points, values);
51+
count_down(x, near_points, values);
5652
}
5753
}
5854

@@ -65,11 +61,11 @@ where
6561

6662
/// Add `AROUND` values starting at and including `x` and counting up. Uses the smallest possible
6763
/// increments (1 ULP).
68-
fn count_up<F: Float>(mut x: F, values: &mut Vec<F>) {
64+
fn count_up<F: Float>(mut x: F, points: u64, values: &mut Vec<F>) {
6965
assert!(!x.is_nan());
7066

7167
let mut count = 0;
72-
while x < F::INFINITY && count < AROUND {
68+
while x < F::INFINITY && count < points {
7369
values.push(x);
7470
x = x.next_up();
7571
count += 1;
@@ -78,11 +74,11 @@ fn count_up<F: Float>(mut x: F, values: &mut Vec<F>) {
7874

7975
/// Add `AROUND` values starting at and including `x` and counting down. Uses the smallest possible
8076
/// increments (1 ULP).
81-
fn count_down<F: Float>(mut x: F, values: &mut Vec<F>) {
77+
fn count_down<F: Float>(mut x: F, points: u64, values: &mut Vec<F>) {
8278
assert!(!x.is_nan());
8379

8480
let mut count = 0;
85-
while x > F::NEG_INFINITY && count < AROUND {
81+
while x > F::NEG_INFINITY && count < points {
8682
values.push(x);
8783
x = x.next_down();
8884
count += 1;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ use crate::{BaseName, CheckCtx, GenerateInput};
1212
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
1313

1414
/// Number of tests to run.
15+
// FIXME(ntests): clean this up when possible
1516
const NTESTS: usize = {
1617
if cfg!(optimizations_enabled) {
1718
if crate::emulated()

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};
26+
pub use run_cfg::{CheckBasis, CheckCtx, EXTENSIVE_ENV, GeneratorKind, TestAction};
2727
pub use test_traits::{CheckOutput, GenerateInput, Hex, TupleCall};
2828

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

crates/libm-test/src/run_cfg.rs

Lines changed: 199 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
//! Configuration for how tests get run.
22
3-
#![allow(unused)]
4-
5-
use std::collections::BTreeMap;
63
use std::env;
4+
use std::ops::RangeInclusive;
75
use std::sync::LazyLock;
86

9-
use crate::{BaseName, FloatTy, Identifier, op};
7+
use crate::{BaseName, FloatTy, Identifier, test_log};
108

9+
/// The environment variable indicating which extensive tests should be run.
1110
pub const EXTENSIVE_ENV: &str = "LIBM_EXTENSIVE_TESTS";
1211

1312
/// Context passed to [`CheckOutput`].
@@ -49,3 +48,199 @@ pub enum CheckBasis {
4948
/// Check against infinite precision (MPFR).
5049
Mpfr,
5150
}
51+
52+
/// The different kinds of generators that provide test input.
53+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
54+
pub enum GeneratorKind {
55+
Domain,
56+
Random,
57+
}
58+
59+
/// Configuration for how a test should be run.
60+
#[derive(Clone, Debug, PartialEq, Eq)]
61+
pub enum TestAction {
62+
/// Run the test with no specific configuration.
63+
Run,
64+
/// Run the test with a certain number of iterations.
65+
Iterations(u64),
66+
/// Skip the test.
67+
Skip,
68+
}
69+
70+
/// A list of all functions that should get extensive tests.
71+
static EXTENSIVE: LazyLock<Vec<Identifier>> = LazyLock::new(|| {
72+
let var = env::var(EXTENSIVE_ENV).unwrap_or_default();
73+
let list = var.split(",").filter(|s| !s.is_empty()).collect::<Vec<_>>();
74+
let mut ret = Vec::new();
75+
76+
for item in list {
77+
match item {
78+
"all" => ret = Identifier::ALL.to_owned(),
79+
"all_f32" => ret.extend(
80+
Identifier::ALL
81+
.iter()
82+
.filter(|id| matches!(id.math_op().float_ty, FloatTy::F32))
83+
.copied(),
84+
),
85+
"all_f64" => ret.extend(
86+
Identifier::ALL
87+
.iter()
88+
.filter(|id| matches!(id.math_op().float_ty, FloatTy::F64))
89+
.copied(),
90+
),
91+
s => ret.push(
92+
Identifier::from_str(s).unwrap_or_else(|| panic!("unrecognized test name `{s}`")),
93+
),
94+
}
95+
}
96+
97+
ret
98+
});
99+
100+
/// Information about the function to be tested.
101+
#[derive(Debug)]
102+
struct TestEnv {
103+
/// Tests should be reduced because the platform is slow. E.g. 32-bit or emulated.
104+
slow_platform: bool,
105+
/// The float cannot be tested exhaustively, `f64` or `f128`.
106+
large_float_ty: bool,
107+
/// Env indicates that an extensive test should be run.
108+
should_run_extensive: bool,
109+
/// Multiprecision tests will be run.
110+
mp_tests_enabled: bool,
111+
/// The number of inputs to the function.
112+
input_count: usize,
113+
}
114+
115+
impl TestEnv {
116+
fn from_env(ctx: &CheckCtx) -> Self {
117+
let id = ctx.fn_ident;
118+
let op = id.math_op();
119+
120+
let will_run_mp = cfg!(feature = "test-multiprecision");
121+
122+
// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run in QEMU. Start
123+
// with a reduced number on these platforms.
124+
let slow_on_ci = crate::emulated()
125+
|| usize::BITS < 64
126+
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"));
127+
let slow_platform = slow_on_ci && crate::ci();
128+
129+
let large_float_ty = match op.float_ty {
130+
FloatTy::F16 | FloatTy::F32 => false,
131+
FloatTy::F64 | FloatTy::F128 => true,
132+
};
133+
134+
let will_run_extensive = EXTENSIVE.contains(&id);
135+
136+
let input_count = op.rust_sig.args.len();
137+
138+
Self {
139+
slow_platform,
140+
large_float_ty,
141+
should_run_extensive: will_run_extensive,
142+
mp_tests_enabled: will_run_mp,
143+
input_count,
144+
}
145+
}
146+
}
147+
148+
/// The number of iterations to run for a given test.
149+
pub fn iteration_count(ctx: &CheckCtx, gen_kind: GeneratorKind, argnum: usize) -> u64 {
150+
let t_env = TestEnv::from_env(ctx);
151+
152+
// Ideally run 5M tests
153+
let mut domain_iter_count: u64 = 4_000_000;
154+
155+
// Start with a reduced number of tests on slow platforms.
156+
if t_env.slow_platform {
157+
domain_iter_count = 100_000;
158+
}
159+
160+
// Larger float types get more iterations.
161+
if t_env.large_float_ty {
162+
domain_iter_count *= 4;
163+
}
164+
165+
// Functions with more arguments get more iterations.
166+
let arg_multiplier = 1 << (t_env.input_count - 1);
167+
domain_iter_count *= arg_multiplier;
168+
169+
// If we will be running tests against MPFR, we don't need to test as much against musl.
170+
// However, there are some platforms where we have to test against musl since MPFR can't be
171+
// built.
172+
if t_env.mp_tests_enabled && ctx.basis == CheckBasis::Musl {
173+
domain_iter_count /= 100;
174+
}
175+
176+
// Run fewer random tests than domain tests.
177+
let random_iter_count = domain_iter_count / 100;
178+
179+
let mut total_iterations = match gen_kind {
180+
GeneratorKind::Domain => domain_iter_count,
181+
GeneratorKind::Random => random_iter_count,
182+
};
183+
184+
if cfg!(optimizations_enabled) {
185+
// Always run at least 10,000 tests
186+
total_iterations = total_iterations.max(10_000);
187+
} else {
188+
// Without optimizations just run a quick check
189+
total_iterations = 800;
190+
}
191+
192+
// Adjust for the number of inputs
193+
let ntests = match t_env.input_count {
194+
1 => total_iterations,
195+
2 => (total_iterations as f64).sqrt().ceil() as u64,
196+
3 => (total_iterations as f64).cbrt().ceil() as u64,
197+
_ => panic!("test has more than three arguments"),
198+
};
199+
let total = ntests.pow(t_env.input_count.try_into().unwrap());
200+
201+
test_log(&format!(
202+
"{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \
203+
({total} total)",
204+
basis = ctx.basis,
205+
fn_ident = ctx.fn_ident,
206+
arg = argnum + 1,
207+
args = t_env.input_count,
208+
));
209+
210+
ntests
211+
}
212+
213+
pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> {
214+
let t_env = TestEnv::from_env(ctx);
215+
216+
if ctx.base_name != BaseName::Jn {
217+
return i32::MIN..=i32::MAX;
218+
}
219+
220+
assert_eq!(argnum, 0, "For `jn`, only the first argument takes an integer");
221+
222+
if t_env.slow_platform || !cfg!(optimizations_enabled) {
223+
(-0xf)..=0xff
224+
} else {
225+
(-0xff)..=0xffff
226+
}
227+
}
228+
229+
/// For domain tests, limit how many asymptotes or specified check points we test.
230+
pub fn check_point_count(ctx: &CheckCtx) -> usize {
231+
let t_env = TestEnv::from_env(ctx);
232+
if t_env.slow_platform || !cfg!(optimizations_enabled) { 4 } else { 10 }
233+
}
234+
235+
/// When validating points of interest (e.g. asymptotes, inflection points, extremes), also check
236+
/// this many surrounding values.
237+
pub fn check_near_count(_ctx: &CheckCtx) -> u64 {
238+
if cfg!(optimizations_enabled) { 100 } else { 10 }
239+
}
240+
241+
/// Check whether extensive actions should be run or skipped.
242+
#[expect(dead_code, reason = "extensive tests have not yet been added")]
243+
pub fn skip_extensive_test(ctx: &CheckCtx) -> bool {
244+
let t_env = TestEnv::from_env(ctx);
245+
!t_env.should_run_extensive
246+
}

0 commit comments

Comments
 (0)