blob: 1321a3c3354997b778c48b0868c424d3542f40be [file] [log] [blame]
#![feature(f16)]
#![feature(cfg_target_has_reliable_f16_f128)]
#![expect(internal_features)] // reliable_f16_f128
mod traits;
mod ui;
mod validate;
use std::any::type_name;
use std::cmp::min;
use std::ops::RangeInclusive;
use std::process::ExitCode;
use std::sync::OnceLock;
use std::sync::atomic::{AtomicU64, Ordering};
use std::{fmt, time};
use rand::distr::{Distribution, StandardUniform};
use rayon::prelude::*;
use time::{Duration, Instant};
use traits::{Float, Generator, Int};
use validate::CheckError;
/// Test generators.
mod gen_ {
pub mod exhaustive;
pub mod exponents;
pub mod fuzz;
pub mod integers;
pub mod long_fractions;
pub mod many_digits;
pub mod sparse;
pub mod spot_checks;
pub mod subnorm;
}
/// How many failures to exit after if unspecified.
const DEFAULT_MAX_FAILURES: u64 = 20;
/// Register exhaustive tests only for <= 32 bits. No more because it would take years.
const MAX_BITS_FOR_EXHAUUSTIVE: u32 = 32;
/// If there are more tests than this threshold, the test will be deferred until after all
/// others run (so as to avoid thread pool starvation). They also can be excluded with
/// `--skip-huge`.
const HUGE_TEST_CUTOFF: u64 = 5_000_000;
/// Seed for tests that use a deterministic RNG.
const SEED: [u8; 32] = *b"3.141592653589793238462643383279";
/// Global configuration.
#[derive(Debug)]
pub struct Config {
pub timeout: Duration,
/// Failures per test
pub max_failures: u64,
pub disable_max_failures: bool,
/// If `None`, the default will be used
pub fuzz_count: Option<u64>,
pub skip_huge: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
timeout: Duration::from_secs(60 * 60 * 3),
max_failures: DEFAULT_MAX_FAILURES,
disable_max_failures: false,
fuzz_count: None,
skip_huge: false,
}
}
}
/// Collect, filter, and launch all tests.
pub fn run(cfg: Config, include: &[String], exclude: &[String]) -> ExitCode {
// With default parallelism, the CPU doesn't saturate. We don't need to be nice to
// other processes, so do 1.5x to make sure we use all available resources.
let threads = std::thread::available_parallelism().map(Into::into).unwrap_or(0) * 3 / 2;
rayon::ThreadPoolBuilder::new().num_threads(threads).build_global().unwrap();
let mut tests = register_tests(&cfg);
println!("registered");
let initial_tests: Vec<_> = tests.iter().map(|t| t.name.clone()).collect();
let unmatched: Vec<_> = include
.iter()
.chain(exclude.iter())
.filter(|filt| !tests.iter().any(|t| t.matches(filt)))
.collect();
assert!(
unmatched.is_empty(),
"filters were provided that have no matching tests: {unmatched:#?}"
);
tests.retain(|test| !exclude.iter().any(|exc| test.matches(exc)));
if cfg.skip_huge {
tests.retain(|test| !test.is_huge_test());
}
if !include.is_empty() {
tests.retain(|test| include.iter().any(|inc| test.matches(inc)));
}
for exc in initial_tests.iter().filter(|orig_name| !tests.iter().any(|t| t.name == **orig_name))
{
println!("Skipping test '{exc}'");
}
println!("Launching all");
let elapsed = launch_tests(&mut tests, &cfg);
ui::finish_all(&tests, elapsed, &cfg)
}
/// Enumerate tests to run but don't actually run them.
pub fn register_tests(cfg: &Config) -> Vec<TestInfo> {
let mut tests = Vec::new();
// Register normal generators for all floats.
#[cfg(target_has_reliable_f16)]
register_float::<f16>(&mut tests, cfg);
register_float::<f32>(&mut tests, cfg);
register_float::<f64>(&mut tests, cfg);
tests.sort_unstable_by_key(|t| (t.float_name, t.gen_name));
for i in 0..(tests.len() - 1) {
if tests[i].gen_name == tests[i + 1].gen_name {
panic!("duplicate test name {}", tests[i].gen_name);
}
}
tests
}
/// Register all generators for a single float.
fn register_float<F: Float>(tests: &mut Vec<TestInfo>, cfg: &Config)
where
RangeInclusive<F::Int>: Iterator<Item = F::Int>,
<F::Int as TryFrom<u128>>::Error: std::fmt::Debug,
StandardUniform: Distribution<<F as traits::Float>::Int>,
{
if F::BITS <= MAX_BITS_FOR_EXHAUUSTIVE {
// Only run exhaustive tests if there is a chance of completion.
TestInfo::register::<F, gen_::exhaustive::Exhaustive<F>>(tests);
}
gen_::fuzz::Fuzz::<F>::set_iterations(cfg.fuzz_count);
TestInfo::register::<F, gen_::exponents::LargeExponents<F>>(tests);
TestInfo::register::<F, gen_::exponents::SmallExponents<F>>(tests);
TestInfo::register::<F, gen_::fuzz::Fuzz<F>>(tests);
TestInfo::register::<F, gen_::integers::LargeInt<F>>(tests);
TestInfo::register::<F, gen_::integers::SmallInt>(tests);
TestInfo::register::<F, gen_::long_fractions::RepeatingDecimal>(tests);
TestInfo::register::<F, gen_::many_digits::RandDigits<F>>(tests);
TestInfo::register::<F, gen_::sparse::FewOnesFloat<F>>(tests);
TestInfo::register::<F, gen_::sparse::FewOnesInt<F>>(tests);
TestInfo::register::<F, gen_::spot_checks::RegressionCheck>(tests);
TestInfo::register::<F, gen_::spot_checks::Special>(tests);
TestInfo::register::<F, gen_::subnorm::SubnormComplete<F>>(tests);
TestInfo::register::<F, gen_::subnorm::SubnormEdgeCases<F>>(tests);
}
/// Configuration for a single test.
#[derive(Debug)]
pub struct TestInfo {
pub name: String,
float_name: &'static str,
float_bits: u32,
gen_name: &'static str,
/// Name for display in the progress bar.
short_name: String,
/// Pad the short name to a common width for progress bar use.
short_name_padded: String,
total_tests: u64,
/// Function to launch this test.
launch: fn(&TestInfo, &Config),
/// Progress bar to be updated.
progress: Option<ui::Progress>,
/// Once completed, this will be set.
completed: OnceLock<Completed>,
}
impl TestInfo {
/// Check if either the name or short name is a match, for filtering.
fn matches(&self, pat: &str) -> bool {
self.short_name.contains(pat) || self.name.contains(pat)
}
/// Create a `TestInfo` for a given float and generator, then add it to a list.
fn register<F: Float, G: Generator<F>>(v: &mut Vec<Self>) {
let f_name = type_name::<F>();
let gen_name = G::NAME;
let gen_short_name = G::SHORT_NAME;
let name = format!("{f_name} {gen_name}");
let short_name = format!("{f_name} {gen_short_name}");
let short_name_padded = format!("{short_name:18}");
let info = TestInfo {
float_name: f_name,
float_bits: F::BITS,
gen_name,
progress: None,
name,
short_name_padded,
short_name,
launch: test_runner::<F, G>,
total_tests: G::total_tests(),
completed: OnceLock::new(),
};
v.push(info);
}
/// True if this should be run after all others.
fn is_huge_test(&self) -> bool {
self.total_tests >= HUGE_TEST_CUTOFF
}
/// When the test is finished, update progress bar messages and finalize.
fn complete(&self, c: Completed) {
self.progress.as_ref().unwrap().complete(&c, 0);
self.completed.set(c).unwrap();
}
}
/// Result of an input did not parsing successfully.
#[derive(Clone, Debug)]
enum CheckFailure {
/// Above the zero cutoff but got rounded to zero.
UnexpectedZero,
/// Below the infinity cutoff but got rounded to infinity.
UnexpectedInf,
/// Above the negative infinity cutoff but got rounded to negative infinity.
UnexpectedNegInf,
/// Got a `NaN` when none was expected.
UnexpectedNan,
/// Expected `NaN`, got none.
ExpectedNan,
/// Expected infinity, got finite.
ExpectedInf,
/// Expected negative infinity, got finite.
ExpectedNegInf,
/// The value exceeded its error tolerance.
InvalidReal {
/// Error from the expected value, as a float.
error_float: Option<f64>,
/// Error as a rational string (since it can't always be represented as a float).
error_str: Box<str>,
/// True if the error was caused by not rounding to even at the midpoint between
/// two representable values.
incorrect_midpoint_rounding: bool,
},
/// String did not parse successfully.
ParsingFailed(Box<str>),
/// A panic was caught.
Panic(Box<str>),
}
impl fmt::Display for CheckFailure {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CheckFailure::UnexpectedZero => {
write!(f, "incorrectly rounded to 0 (expected nonzero)")
}
CheckFailure::UnexpectedInf => {
write!(f, "incorrectly rounded to +inf (expected finite)")
}
CheckFailure::UnexpectedNegInf => {
write!(f, "incorrectly rounded to -inf (expected finite)")
}
CheckFailure::UnexpectedNan => write!(f, "got a NaN where none was expected"),
CheckFailure::ExpectedNan => write!(f, "expected a NaN but did not get it"),
CheckFailure::ExpectedInf => write!(f, "expected +inf but did not get it"),
CheckFailure::ExpectedNegInf => write!(f, "expected -inf but did not get it"),
CheckFailure::InvalidReal { error_float, error_str, incorrect_midpoint_rounding } => {
if *incorrect_midpoint_rounding {
write!(
f,
"midpoint between two representable values did not correctly \
round to even; error: {error_str}"
)?;
} else {
write!(f, "real number did not parse correctly; error: {error_str}")?;
}
if let Some(float) = error_float {
write!(f, " ({float})")?;
}
Ok(())
}
CheckFailure::ParsingFailed(e) => write!(f, "parsing failed: {e}"),
CheckFailure::Panic(e) => write!(f, "function panicked: {e}"),
}
}
}
/// Information about a completed test generator.
#[derive(Clone, Debug)]
struct Completed {
/// Finished tests (both successful and failed).
executed: u64,
/// Failed tests.
failures: u64,
/// Extra exit information if unsuccessful.
result: Result<FinishedAll, EarlyExit>,
/// If there is something to warn about (e.g bad estimate), leave it here.
warning: Option<Box<str>>,
/// Total time to run the test.
elapsed: Duration,
}
/// Marker for completing all tests (used in `Result` types).
#[derive(Clone, Debug)]
struct FinishedAll;
/// Reasons for exiting early.
#[derive(Clone, Debug)]
enum EarlyExit {
Timeout,
MaxFailures,
}
/// Run all tests in `tests`.
///
/// This launches a main thread that receives messages and handlees UI updates, and uses the
/// rest of the thread pool to execute the tests.
fn launch_tests(tests: &mut [TestInfo], cfg: &Config) -> Duration {
// Run shorter tests and smaller float types first.
tests.sort_unstable_by_key(|test| (test.total_tests, test.float_bits));
for test in tests.iter() {
println!("Launching test '{}'", test.name);
}
let mut all_progress_bars = Vec::new();
let start = Instant::now();
for test in tests.iter_mut() {
test.progress = Some(ui::Progress::new(test, &mut all_progress_bars));
ui::set_panic_hook(&all_progress_bars);
(test.launch)(test, cfg);
}
start.elapsed()
}
/// Test runer for a single generator.
///
/// This calls the generator's iterator multiple times (in parallel) and validates each output.
fn test_runner<F: Float, G: Generator<F>>(test: &TestInfo, cfg: &Config) {
let gen_ = G::new();
let executed = AtomicU64::new(0);
let failures = AtomicU64::new(0);
let checks_per_update = min(test.total_tests, 1000);
let started = Instant::now();
// Function to execute for a single test iteration.
let check_one = |buf: &mut String, ctx: G::WriteCtx| {
let executed = executed.fetch_add(1, Ordering::Relaxed);
buf.clear();
G::write_string(buf, ctx);
match validate::validate::<F>(buf) {
Ok(()) => (),
Err(e) => {
let CheckError { fail, input, float_res } = e;
test.progress.as_ref().unwrap().println(&format!(
"Failure in '{}': {fail}. parsing '{input}'. Parsed as: {float_res}",
test.name
));
let f = failures.fetch_add(1, Ordering::Relaxed);
// End early if the limit is exceeded.
if f >= cfg.max_failures {
return Err(EarlyExit::MaxFailures);
}
}
};
// Send periodic updates
if executed % checks_per_update == 0 {
let failures = failures.load(Ordering::Relaxed);
test.progress.as_ref().unwrap().update(executed, failures);
if started.elapsed() > cfg.timeout {
return Err(EarlyExit::Timeout);
}
}
Ok(())
};
// Run the test iterations in parallel. Each thread gets a string buffer to write
// its check values to.
let res = gen_.par_bridge().try_for_each_init(String::new, check_one);
let elapsed = started.elapsed();
let executed = executed.into_inner();
let failures = failures.into_inner();
// Warn about bad estimates if relevant.
let warning = if executed != test.total_tests && res.is_ok() {
let msg = format!(
"executed tests != estimated ({executed} != {}) for {}",
test.total_tests,
G::NAME
);
Some(msg.into())
} else {
None
};
let result = res.map(|()| FinishedAll);
test.complete(Completed { executed, failures, result, warning, elapsed });
}