Skip to content

Commit 33456f2

Browse files
committed
Add a deterministic random generator
Create a test generator that creates a known number of random inputs and caches them, such that the same inputs are used for all functions.
1 parent 83eb4c0 commit 33456f2

File tree

3 files changed

+132
-0
lines changed

3 files changed

+132
-0
lines changed

libm/crates/libm-test/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@ test-musl-serialized = ["rand"]
1515
anyhow = "1.0.90"
1616
libm = { path = "../.." }
1717
libm-macros = { path = "../libm-macros" }
18+
rand = "0.8.5"
19+
rand_chacha = "0.3.1"
20+
21+
[target.'cfg(target_family = "wasm")'.dependencies]
22+
# Enable randomness on WASM
23+
getrandom = { version = "0.2", features = ["js"] }
1824

1925
[build-dependencies]
2026
rand = { version = "0.8.5", optional = true }

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
//! Different generators that can create random or systematic bit patterns.
22
33
use crate::GenerateInput;
4+
pub mod random;
45

56
/// Helper type to turn any reusable input into a generator.
67
#[derive(Clone, Debug, Default)]
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//! A simple generator that produces deterministic random input, caching to use the same
2+
//! inputs for all functions.
3+
4+
use std::sync::LazyLock;
5+
6+
use rand::{Rng, SeedableRng};
7+
use rand_chacha::ChaCha8Rng;
8+
9+
use super::CachedInput;
10+
use crate::GenerateInput;
11+
12+
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
13+
14+
/// Number of tests to run.
15+
const NTESTS: usize = {
16+
let ntests = if cfg!(optimizations_enabled) {
17+
if cfg!(target_arch = "x86_64") || cfg!(target_arch = "aarch64") {
18+
5_000_000
19+
} else if !cfg!(target_pointer_width = "64")
20+
|| cfg!(all(target_arch = "x86_64", target_vendor = "apple"))
21+
|| option_env!("EMULATED").is_some()
22+
&& cfg!(any(target_arch = "aarch64", target_arch = "powerpc64"))
23+
{
24+
// Tests are pretty slow on:
25+
// - Non-64-bit targets
26+
// - Emulated ppc
27+
// - Emulated aarch64
28+
// - x86 MacOS
29+
// So reduce the number of iterations
30+
100_000
31+
} else {
32+
// Most everything else gets tested in docker and works okay, but we still
33+
// don't need 20 minutes of tests.
34+
1_000_000
35+
}
36+
} else {
37+
800
38+
};
39+
40+
ntests
41+
};
42+
43+
/// Tested inputs.
44+
static TEST_CASES: LazyLock<CachedInput> = LazyLock::new(|| make_test_cases(NTESTS));
45+
46+
/// The first argument to `jn` and `jnf` is the number of iterations. Make this a reasonable
47+
/// value so tests don't run forever.
48+
static TEST_CASES_JN: LazyLock<CachedInput> = LazyLock::new(|| {
49+
// Start with regular test cases
50+
let mut cases = (&*TEST_CASES).clone();
51+
52+
// These functions are extremely slow, limit them
53+
cases.inputs_i32.truncate((NTESTS / 1000).max(80));
54+
cases.inputs_f32.truncate((NTESTS / 1000).max(80));
55+
cases.inputs_f64.truncate((NTESTS / 1000).max(80));
56+
57+
// It is easy to overflow the stack with these in debug mode
58+
let max_iterations = if cfg!(optimizations_enabled) && cfg!(target_pointer_width = "64") {
59+
0xffff
60+
} else if cfg!(windows) {
61+
0x00ff
62+
} else {
63+
0x0fff
64+
};
65+
66+
let mut rng = ChaCha8Rng::from_seed(SEED);
67+
68+
for case in cases.inputs_i32.iter_mut() {
69+
case.0 = rng.gen_range(3..=max_iterations);
70+
}
71+
72+
cases
73+
});
74+
75+
fn make_test_cases(ntests: usize) -> CachedInput {
76+
let mut rng = ChaCha8Rng::from_seed(SEED);
77+
78+
// make sure we include some basic cases
79+
let mut inputs_i32 = vec![(0, 0, 0), (1, 1, 1), (-1, -1, -1)];
80+
let mut inputs_f32 = vec![
81+
(0.0, 0.0, 0.0),
82+
(f32::EPSILON, f32::EPSILON, f32::EPSILON),
83+
(f32::INFINITY, f32::INFINITY, f32::INFINITY),
84+
(f32::NEG_INFINITY, f32::NEG_INFINITY, f32::NEG_INFINITY),
85+
(f32::MAX, f32::MAX, f32::MAX),
86+
(f32::MIN, f32::MIN, f32::MIN),
87+
(f32::MIN_POSITIVE, f32::MIN_POSITIVE, f32::MIN_POSITIVE),
88+
(f32::NAN, f32::NAN, f32::NAN),
89+
];
90+
let mut inputs_f64 = vec![
91+
(0.0, 0.0, 0.0),
92+
(f64::EPSILON, f64::EPSILON, f64::EPSILON),
93+
(f64::INFINITY, f64::INFINITY, f64::INFINITY),
94+
(f64::NEG_INFINITY, f64::NEG_INFINITY, f64::NEG_INFINITY),
95+
(f64::MAX, f64::MAX, f64::MAX),
96+
(f64::MIN, f64::MIN, f64::MIN),
97+
(f64::MIN_POSITIVE, f64::MIN_POSITIVE, f64::MIN_POSITIVE),
98+
(f64::NAN, f64::NAN, f64::NAN),
99+
];
100+
101+
inputs_i32.extend((0..(ntests - inputs_i32.len())).map(|_| rng.gen::<(i32, i32, i32)>()));
102+
103+
// Generate integers to get a full range of bitpatterns, then convert back to
104+
// floats.
105+
inputs_f32.extend((0..(ntests - inputs_f32.len())).map(|_| {
106+
let ints = rng.gen::<(u32, u32, u32)>();
107+
(f32::from_bits(ints.0), f32::from_bits(ints.1), f32::from_bits(ints.2))
108+
}));
109+
inputs_f64.extend((0..(ntests - inputs_f64.len())).map(|_| {
110+
let ints = rng.gen::<(u64, u64, u64)>();
111+
(f64::from_bits(ints.0), f64::from_bits(ints.1), f64::from_bits(ints.2))
112+
}));
113+
114+
CachedInput { inputs_f32, inputs_f64, inputs_i32 }
115+
}
116+
117+
/// Create a test case iterator.
118+
pub fn get_test_cases<RustArgs>(fname: &str) -> impl Iterator<Item = RustArgs>
119+
where
120+
CachedInput: GenerateInput<RustArgs>,
121+
{
122+
let inputs = if fname == "jn" || fname == "jnf" { &TEST_CASES_JN } else { &TEST_CASES };
123+
124+
CachedInput::get_cases(inputs)
125+
}

0 commit comments

Comments
 (0)