Skip to content

Commit a4f8436

Browse files
committed
Remove allow_nan because it isn't useful; adjust logic for number of fuzz tests
1 parent caa16fd commit a4f8436

File tree

9 files changed

+101
-72
lines changed

9 files changed

+101
-72
lines changed

src/etc/test-float-parse/src/gen/fuzz.rs

+34-8
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,22 @@
1+
use std::any::{type_name, TypeId};
2+
use std::collections::BTreeMap;
13
use std::fmt::Write;
24
use std::marker::PhantomData;
35
use std::ops::Range;
4-
use std::sync::atomic::{AtomicU64, Ordering};
6+
use std::sync::Mutex;
57

68
use rand::distributions::{Distribution, Standard};
79
use rand::Rng;
810
use rand_chacha::rand_core::SeedableRng;
911
use rand_chacha::ChaCha8Rng;
1012

11-
use crate::{Float, Generator, SEED};
13+
use crate::{Float, Generator, Int, SEED};
1214

13-
/// How many iterations to fuzz for; can be updated before launching. We use a static since
14-
/// `Generator::new` doesn't take any input.
15-
pub static FUZZ_COUNT: AtomicU64 = AtomicU64::new(crate::DEFAULT_FUZZ_COUNT);
15+
/// Mapping of float types to the number of iterations that should be run.
16+
///
17+
/// We could probably make `Generator::new` take an argument instead of the global state,
18+
/// but we only load this once so it works.
19+
static FUZZ_COUNTS: Mutex<BTreeMap<TypeId, u64>> = Mutex::new(BTreeMap::new());
1620

1721
/// Generic fuzzer; just tests deterministic random bit patterns N times.
1822
pub struct Fuzz<F> {
@@ -22,24 +26,46 @@ pub struct Fuzz<F> {
2226
marker: PhantomData<F>,
2327
}
2428

29+
impl<F: Float> Fuzz<F> {
30+
/// Register how many iterations the fuzzer should run for a type. Uses some logic by
31+
/// default, but if `from_cfg` is `Some`, that will be used instead.
32+
pub fn set_iterations(from_cfg: Option<u64>) {
33+
let count = if let Some(cfg_count) = from_cfg {
34+
cfg_count
35+
} else if F::BITS <= crate::MAX_BITS_FOR_EXHAUUSTIVE {
36+
// If we run exhaustively, still fuzz but only do half as many bits. The only goal here is
37+
// to catch failures from e.g. high bit patterns before exhaustive tests would get to them.
38+
(F::Int::MAX >> (F::BITS / 2)).try_into().unwrap()
39+
} else {
40+
// Eveything bigger gets a fuzz test with as many iterations as `f32` exhaustive.
41+
u32::MAX.into()
42+
};
43+
44+
let _ = FUZZ_COUNTS.lock().unwrap().insert(TypeId::of::<F>(), count);
45+
}
46+
}
47+
2548
impl<F: Float> Generator<F> for Fuzz<F>
2649
where
2750
Standard: Distribution<<F as Float>::Int>,
2851
{
2952
const NAME: &'static str = "fuzz";
3053
const SHORT_NAME: &'static str = "fuzz";
31-
const PATTERNS_CONTAIN_NAN: bool = true;
3254

3355
type WriteCtx = F;
3456

3557
fn total_tests() -> u64 {
36-
FUZZ_COUNT.load(Ordering::Relaxed)
58+
*FUZZ_COUNTS
59+
.lock()
60+
.unwrap()
61+
.get(&TypeId::of::<F>())
62+
.unwrap_or_else(|| panic!("missing fuzz count for {}", type_name::<F>()))
3763
}
3864

3965
fn new() -> Self {
4066
let rng = ChaCha8Rng::from_seed(SEED);
4167

42-
Self { iter: 0..FUZZ_COUNT.load(Ordering::Relaxed), rng, marker: PhantomData }
68+
Self { iter: 0..Self::total_tests(), rng, marker: PhantomData }
4369
}
4470

4571
fn write_string(s: &mut String, ctx: Self::WriteCtx) {

src/etc/test-float-parse/src/gen/sparse.rs

-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,6 @@ where
7575
{
7676
const NAME: &'static str = "few ones float";
7777
const SHORT_NAME: &'static str = "few ones float";
78-
const PATTERNS_CONTAIN_NAN: bool = true;
7978

8079
type WriteCtx = F;
8180

src/etc/test-float-parse/src/gen/spot_checks.rs

-2
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,6 @@ impl<F: Float> Generator<F> for Special {
1919

2020
const SHORT_NAME: &'static str = "special";
2121

22-
const PATTERNS_CONTAIN_NAN: bool = true;
23-
2422
type WriteCtx = &'static str;
2523

2624
fn total_tests() -> u64 {

src/etc/test-float-parse/src/lib.rs

+36-16
Original file line numberDiff line numberDiff line change
@@ -29,9 +29,11 @@ mod gen {
2929
pub mod subnorm;
3030
}
3131

32-
/// Fuzz iterations to run if not specified by CLI arg. By default, test as many conditions
33-
/// as the `f32` exhaustive test.
34-
pub const DEFAULT_FUZZ_COUNT: u64 = u32::MAX as u64;
32+
/// How many failures to exit after if unspecified.
33+
const DEFAULT_MAX_FAILURES: u64 = 20;
34+
35+
/// Register exhaustive tests only for <= 32 bits. No more because it would take years.
36+
const MAX_BITS_FOR_EXHAUUSTIVE: u32 = 32;
3537

3638
/// If there are more tests than this threashold, the test will be defered until after all
3739
/// others run (so as to avoid thread pool starvation). They also can be excluded with
@@ -46,21 +48,34 @@ const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
4648
pub struct Config {
4749
pub timeout: Duration,
4850
/// Failures per test
49-
pub max_failures: Option<u64>,
51+
pub max_failures: u64,
52+
pub disable_max_failures: bool,
53+
/// If `None`, the default will be used
5054
pub fuzz_count: Option<u64>,
5155
pub skip_huge: bool,
5256
}
5357

58+
impl Default for Config {
59+
fn default() -> Self {
60+
Self {
61+
timeout: Duration::from_secs(60 * 60 * 3),
62+
max_failures: DEFAULT_MAX_FAILURES,
63+
disable_max_failures: false,
64+
fuzz_count: None,
65+
skip_huge: false,
66+
}
67+
}
68+
}
69+
5470
/// Collect, filter, and launch all tests.
5571
pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode {
56-
gen::fuzz::FUZZ_COUNT.store(cfg.fuzz_count.unwrap_or(u64::MAX), Ordering::Relaxed);
57-
5872
// With default parallelism, the CPU doesn't saturate. We don't need to be nice to
5973
// other processes, so do 1.5x to make sure we use all available resources.
6074
let threads = std::thread::available_parallelism().map(Into::into).unwrap_or(0) * 3 / 2;
6175
rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap();
6276

63-
let mut tests = register_tests();
77+
let mut tests = register_tests(&cfg);
78+
println!("registered");
6479
let initial_tests: Vec<_> = tests.iter().map(|t| t.name.clone()).collect();
6580

6681
let unmatched: Vec<_> = include
@@ -89,20 +104,18 @@ pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode {
89104
println!("Skipping test '{exc}'");
90105
}
91106

107+
println!("launching");
92108
let elapsed = launch_tests(&mut tests, &cfg);
93109
ui::finish(&tests, elapsed, &cfg)
94110
}
95111

96112
/// Enumerate tests to run but don't actaully run them.
97-
pub fn register_tests() -> Vec<TestInfo> {
113+
pub fn register_tests(cfg: &Config) -> Vec<TestInfo> {
98114
let mut tests = Vec::new();
99115

100116
// Register normal generators for all floats.
101-
register_float::<f32>(&mut tests);
102-
register_float::<f64>(&mut tests);
103-
104-
// Register exhaustive tests for <= 32 bits. No more because it would take years.
105-
TestInfo::register::<f32, gen::exhaustive::Exhaustive<f32>>(&mut tests);
117+
register_float::<f32>(&mut tests, cfg);
118+
register_float::<f64>(&mut tests, cfg);
106119

107120
tests.sort_unstable_by_key(|t| (t.float_name, t.gen_name));
108121
for i in 0..(tests.len() - 1) {
@@ -115,12 +128,19 @@ pub fn register_tests() -> Vec<TestInfo> {
115128
}
116129

117130
/// Register all generators for a single float.
118-
fn register_float<F: Float>(tests: &mut Vec<TestInfo>)
131+
fn register_float<F: Float>(tests: &mut Vec<TestInfo>, cfg: &Config)
119132
where
120133
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
121134
<F::Int as TryFrom<u128>>::Error: std::fmt::Debug,
122135
Standard: Distribution<<F as traits::Float>::Int>,
123136
{
137+
if F::BITS <= MAX_BITS_FOR_EXHAUUSTIVE {
138+
// Only run exhaustive tests if there is a chance of completion.
139+
TestInfo::register::<F, gen::exhaustive::Exhaustive<F>>(tests);
140+
}
141+
142+
gen::fuzz::Fuzz::<F>::set_iterations(cfg.fuzz_count);
143+
124144
TestInfo::register::<F, gen::exponents::LargeExponents<F>>(tests);
125145
TestInfo::register::<F, gen::exponents::SmallExponents<F>>(tests);
126146
TestInfo::register::<F, gen::fuzz::Fuzz<F>>(tests);
@@ -451,13 +471,13 @@ fn test_runner<F: Float, G: Generator<F>>(tx: &mpsc::Sender<Msg>, _info: &TestIn
451471
buf.clear();
452472
G::write_string(buf, ctx);
453473

454-
match validate::validate::<F>(buf, G::PATTERNS_CONTAIN_NAN) {
474+
match validate::validate::<F>(buf) {
455475
Ok(()) => (),
456476
Err(e) => {
457477
tx.send(Msg::new::<F, G>(e)).unwrap();
458478
let f = failures.fetch_add(1, Ordering::Relaxed);
459479
// End early if the limit is exceeded.
460-
if cfg.max_failures.map_or(false, |mf| f >= mf) {
480+
if f >= cfg.max_failures {
461481
return Err(EarlyExit::MaxFailures);
462482
}
463483
}

src/etc/test-float-parse/src/main.rs

+5-10
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ fn main() -> ExitCode {
5353
}
5454

5555
if args.iter().any(|arg| arg == "--list") {
56-
let tests = tfp::register_tests();
56+
let tests = tfp::register_tests(&tfp::Config::default());
5757
println!("Available tests:");
5858
for t in tests {
5959
println!("{}", t.name);
@@ -69,12 +69,7 @@ fn main() -> ExitCode {
6969

7070
/// Simple command argument parser
7171
fn parse_args(args: Vec<String>) -> (tfp::Config, Vec<String>, Vec<String>) {
72-
let mut cfg = tfp::Config {
73-
timeout: Duration::from_secs(60 * 60 * 3),
74-
max_failures: Some(20),
75-
fuzz_count: Some(tfp::DEFAULT_FUZZ_COUNT),
76-
skip_huge: false,
77-
};
72+
let mut cfg = tfp::Config::default();
7873

7974
let mut mode = ArgMode::Any;
8075
let mut include = Vec::new();
@@ -109,15 +104,15 @@ fn parse_args(args: Vec<String>) -> (tfp::Config, Vec<String>, Vec<String>) {
109104
}
110105
ArgMode::MaxFailures => {
111106
if arg == "none" {
112-
cfg.max_failures = None;
107+
cfg.disable_max_failures = true;
113108
} else {
114-
cfg.max_failures = Some(arg.parse().unwrap());
109+
cfg.max_failures = arg.parse().unwrap();
115110
}
116111
ArgMode::Any
117112
}
118113
ArgMode::FuzzCount => {
119114
if arg == "none" {
120-
cfg.fuzz_count = None;
115+
cfg.fuzz_count = Some(u64::MAX);
121116
} else {
122117
cfg.fuzz_count = Some(arg.parse().unwrap());
123118
}

src/etc/test-float-parse/src/traits.rs

-4
Original file line numberDiff line numberDiff line change
@@ -182,10 +182,6 @@ pub trait Generator<F: Float>: Iterator<Item = Self::WriteCtx> + Send + 'static
182182
/// Name for display with the progress bar
183183
const SHORT_NAME: &'static str;
184184

185-
/// If false (default), validation will assert on NaN. Set `true` for random patterns like
186-
/// fuzzers or exhaustive.
187-
const PATTERNS_CONTAIN_NAN: bool = false;
188-
189185
/// The context needed to create a test string.
190186
type WriteCtx: Send;
191187

src/etc/test-float-parse/src/ui.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ pub fn finalize_pb(pb: &ProgressBar, short_name_padded: &str, c: &Completed) {
5252
};
5353

5454
let pb_style = ProgressStyle::with_template(
55-
&PB_TEMPLATE_FINAL.replace("NAME", &short_name_padded).replace("COLOR", color),
55+
&PB_TEMPLATE_FINAL.replace("NAME", short_name_padded).replace("COLOR", color),
5656
)
5757
.unwrap();
5858

src/etc/test-float-parse/src/validate.rs

+4-6
Original file line numberDiff line numberDiff line change
@@ -89,13 +89,13 @@ impl Constants {
8989
}
9090

9191
/// Validate that a string parses correctly
92-
pub fn validate<F: Float>(input: &str, allow_nan: bool) -> Result<(), Update> {
92+
pub fn validate<F: Float>(input: &str) -> Result<(), Update> {
9393
let parsed: F = input
9494
.parse()
9595
.unwrap_or_else(|e| panic!("parsing failed for {}: {e}. Input: {input}", type_name::<F>()));
9696

9797
// Parsed float, decoded into significand and exponent
98-
let decoded = decode(parsed, allow_nan);
98+
let decoded = decode(parsed);
9999

100100
// Float parsed separately into a rational
101101
let rational = Rational::parse(input);
@@ -249,7 +249,7 @@ impl<F: Float> FloatRes<F> {
249249
/// Decompose a float into its integral components. This includes the implicit bit.
250250
///
251251
/// If `allow_nan` is `false`, panic if `NaN` values are reached.
252-
fn decode<F: Float>(f: F, allow_nan: bool) -> FloatRes<F> {
252+
fn decode<F: Float>(f: F) -> FloatRes<F> {
253253
let ione = F::SInt::ONE;
254254
let izero = F::SInt::ZERO;
255255

@@ -263,9 +263,7 @@ fn decode<F: Float>(f: F, allow_nan: bool) -> FloatRes<F> {
263263

264264
exponent_biased += 1;
265265
} else if exponent_biased == F::EXP_SAT {
266-
if !allow_nan {
267-
assert_eq!(mantissa, izero, "Unexpected NaN for {}", f.to_bits().hex());
268-
} else if mantissa != izero {
266+
if mantissa != izero {
269267
return FloatRes::Nan;
270268
}
271269

src/etc/test-float-parse/src/validate/tests.rs

+21-24
Original file line numberDiff line numberDiff line change
@@ -35,34 +35,31 @@ fn test_parse_rational() {
3535

3636
#[test]
3737
fn test_decode() {
38-
assert_eq!(decode(0f32, false), FloatRes::Zero);
39-
assert_eq!(decode(f32::INFINITY, false), FloatRes::Inf);
40-
assert_eq!(decode(f32::NEG_INFINITY, false), FloatRes::NegInf);
41-
assert_eq!(decode(1.0f32, false).normalize(), FloatRes::Real { sig: 1, exp: 0 });
42-
assert_eq!(decode(-1.0f32, false).normalize(), FloatRes::Real { sig: -1, exp: 0 });
43-
assert_eq!(decode(100.0f32, false).normalize(), FloatRes::Real { sig: 100, exp: 0 });
44-
assert_eq!(decode(100.5f32, false).normalize(), FloatRes::Real { sig: 201, exp: -1 });
45-
assert_eq!(decode(-4.004f32, false).normalize(), FloatRes::Real { sig: -8396997, exp: -21 });
46-
assert_eq!(decode(0.0004f32, false).normalize(), FloatRes::Real { sig: 13743895, exp: -35 });
47-
assert_eq!(
48-
decode(f32::from_bits(0x1), false).normalize(),
49-
FloatRes::Real { sig: 1, exp: -149 }
50-
);
38+
assert_eq!(decode(0f32), FloatRes::Zero);
39+
assert_eq!(decode(f32::INFINITY), FloatRes::Inf);
40+
assert_eq!(decode(f32::NEG_INFINITY), FloatRes::NegInf);
41+
assert_eq!(decode(1.0f32).normalize(), FloatRes::Real { sig: 1, exp: 0 });
42+
assert_eq!(decode(-1.0f32).normalize(), FloatRes::Real { sig: -1, exp: 0 });
43+
assert_eq!(decode(100.0f32).normalize(), FloatRes::Real { sig: 100, exp: 0 });
44+
assert_eq!(decode(100.5f32).normalize(), FloatRes::Real { sig: 201, exp: -1 });
45+
assert_eq!(decode(-4.004f32).normalize(), FloatRes::Real { sig: -8396997, exp: -21 });
46+
assert_eq!(decode(0.0004f32).normalize(), FloatRes::Real { sig: 13743895, exp: -35 });
47+
assert_eq!(decode(f32::from_bits(0x1)).normalize(), FloatRes::Real { sig: 1, exp: -149 });
5148
}
5249

5350
#[test]
5451
fn test_validate() {
55-
validate::<f32>("0", false).unwrap();
56-
validate::<f32>("-0", false).unwrap();
57-
validate::<f32>("1", false).unwrap();
58-
validate::<f32>("-1", false).unwrap();
59-
validate::<f32>("1.1", false).unwrap();
60-
validate::<f32>("-1.1", false).unwrap();
61-
validate::<f32>("1e10", false).unwrap();
62-
validate::<f32>("1e1000", false).unwrap();
63-
validate::<f32>("-1e1000", false).unwrap();
64-
validate::<f32>("1e-1000", false).unwrap();
65-
validate::<f32>("-1e-1000", false).unwrap();
52+
validate::<f32>("0").unwrap();
53+
validate::<f32>("-0").unwrap();
54+
validate::<f32>("1").unwrap();
55+
validate::<f32>("-1").unwrap();
56+
validate::<f32>("1.1").unwrap();
57+
validate::<f32>("-1.1").unwrap();
58+
validate::<f32>("1e10").unwrap();
59+
validate::<f32>("1e1000").unwrap();
60+
validate::<f32>("-1e1000").unwrap();
61+
validate::<f32>("1e-1000").unwrap();
62+
validate::<f32>("-1e-1000").unwrap();
6663
}
6764

6865
#[test]

0 commit comments

Comments
 (0)