|  | //! Configuration for how tests get run. | 
|  |  | 
|  | use std::ops::RangeInclusive; | 
|  | use std::sync::LazyLock; | 
|  | use std::{env, str}; | 
|  |  | 
|  | use crate::generate::random::{SEED, SEED_ENV}; | 
|  | use crate::{BaseName, FloatTy, Identifier, test_log}; | 
|  |  | 
|  | /// The environment variable indicating which extensive tests should be run. | 
|  | pub const EXTENSIVE_ENV: &str = "LIBM_EXTENSIVE_TESTS"; | 
|  |  | 
|  | /// Specify the number of iterations via this environment variable, rather than using the default. | 
|  | pub const EXTENSIVE_ITER_ENV: &str = "LIBM_EXTENSIVE_ITERATIONS"; | 
|  |  | 
|  | /// The override value, if set by the above environment. | 
|  | static EXTENSIVE_ITER_OVERRIDE: LazyLock<Option<u64>> = LazyLock::new(|| { | 
|  | env::var(EXTENSIVE_ITER_ENV) | 
|  | .map(|v| v.parse().expect("failed to parse iteration count")) | 
|  | .ok() | 
|  | }); | 
|  |  | 
|  | /// Specific tests that need to have a reduced amount of iterations to complete in a reasonable | 
|  | /// amount of time. | 
|  | const EXTREMELY_SLOW_TESTS: &[SlowTest] = &[ | 
|  | SlowTest { | 
|  | ident: Identifier::Fmodf128, | 
|  | gen_kind: GeneratorKind::Spaced, | 
|  | extensive: false, | 
|  | reduce_factor: 50, | 
|  | }, | 
|  | SlowTest { | 
|  | ident: Identifier::Fmodf128, | 
|  | gen_kind: GeneratorKind::Spaced, | 
|  | extensive: true, | 
|  | reduce_factor: 50, | 
|  | }, | 
|  | ]; | 
|  |  | 
|  | /// A pattern to match a `CheckCtx`, plus a factor to reduce by. | 
|  | struct SlowTest { | 
|  | ident: Identifier, | 
|  | gen_kind: GeneratorKind, | 
|  | extensive: bool, | 
|  | reduce_factor: u64, | 
|  | } | 
|  |  | 
|  | impl SlowTest { | 
|  | /// True if the test in `CheckCtx` should be reduced by `reduce_factor`. | 
|  | fn matches_ctx(&self, ctx: &CheckCtx) -> bool { | 
|  | self.ident == ctx.fn_ident | 
|  | && self.gen_kind == ctx.gen_kind | 
|  | && self.extensive == ctx.extensive | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Maximum number of iterations to run for a single routine. | 
|  | /// | 
|  | /// The default value of one greater than `u32::MAX` allows testing single-argument `f32` routines | 
|  | /// and single- or double-argument `f16` routines exhaustively. `f64` and `f128` can't feasibly | 
|  | /// be tested exhaustively; however, [`EXTENSIVE_ITER_ENV`] can be set to run tests for multiple | 
|  | /// hours. | 
|  | pub fn extensive_max_iterations() -> u64 { | 
|  | let default = 1 << 32; // default value | 
|  | EXTENSIVE_ITER_OVERRIDE.unwrap_or(default) | 
|  | } | 
|  |  | 
|  | /// Context passed to [`CheckOutput`]. | 
|  | #[derive(Clone, Debug, PartialEq, Eq)] | 
|  | pub struct CheckCtx { | 
|  | /// Allowed ULP deviation | 
|  | pub ulp: u32, | 
|  | pub fn_ident: Identifier, | 
|  | pub base_name: BaseName, | 
|  | /// Function name. | 
|  | pub fn_name: &'static str, | 
|  | /// Return the unsuffixed version of the function name. | 
|  | pub base_name_str: &'static str, | 
|  | /// Source of truth for tests. | 
|  | pub basis: CheckBasis, | 
|  | pub gen_kind: GeneratorKind, | 
|  | pub extensive: bool, | 
|  | /// If specified, this value will override the value returned by [`iteration_count`]. | 
|  | pub override_iterations: Option<u64>, | 
|  | } | 
|  |  | 
|  | impl CheckCtx { | 
|  | /// Create a new check context, using the default ULP for the function. | 
|  | pub fn new(fn_ident: Identifier, basis: CheckBasis, gen_kind: GeneratorKind) -> Self { | 
|  | let mut ret = Self { | 
|  | ulp: 0, | 
|  | fn_ident, | 
|  | fn_name: fn_ident.as_str(), | 
|  | base_name: fn_ident.base_name(), | 
|  | base_name_str: fn_ident.base_name().as_str(), | 
|  | basis, | 
|  | gen_kind, | 
|  | extensive: false, | 
|  | override_iterations: None, | 
|  | }; | 
|  | ret.ulp = crate::default_ulp(&ret); | 
|  | ret | 
|  | } | 
|  |  | 
|  | /// Configure that this is an extensive test. | 
|  | pub fn extensive(mut self, extensive: bool) -> Self { | 
|  | self.extensive = extensive; | 
|  | self | 
|  | } | 
|  |  | 
|  | /// The number of input arguments for this function. | 
|  | pub fn input_count(&self) -> usize { | 
|  | self.fn_ident.math_op().rust_sig.args.len() | 
|  | } | 
|  |  | 
|  | pub fn override_iterations(&mut self, count: u64) { | 
|  | self.override_iterations = Some(count) | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Possible items to test against | 
|  | #[derive(Clone, Debug, PartialEq, Eq)] | 
|  | pub enum CheckBasis { | 
|  | /// Check against Musl's math sources. | 
|  | Musl, | 
|  | /// Check against infinite precision (MPFR). | 
|  | Mpfr, | 
|  | /// Benchmarks or other times when this is not relevant. | 
|  | None, | 
|  | } | 
|  |  | 
|  | /// The different kinds of generators that provide test input, which account for input pattern | 
|  | /// and quantity. | 
|  | #[derive(Clone, Copy, Debug, PartialEq, Eq)] | 
|  | pub enum GeneratorKind { | 
|  | /// Extremes, zeros, nonstandard numbers, etc. | 
|  | EdgeCases, | 
|  | /// Spaced by logarithm (floats) or linear (integers). | 
|  | Spaced, | 
|  | /// Test inputs from an RNG. | 
|  | Random, | 
|  | /// A provided test case list. | 
|  | List, | 
|  | } | 
|  |  | 
|  | /// A list of all functions that should get extensive tests, as configured by environment variable. | 
|  | /// | 
|  | /// This also supports the special test name `all` to run all tests, as well as `all_f16`, | 
|  | /// `all_f32`, `all_f64`, and `all_f128` to run all tests for a specific float type. | 
|  | static EXTENSIVE: LazyLock<Vec<Identifier>> = LazyLock::new(|| { | 
|  | let var = env::var(EXTENSIVE_ENV).unwrap_or_default(); | 
|  | let list = var.split(",").filter(|s| !s.is_empty()).collect::<Vec<_>>(); | 
|  | let mut ret = Vec::new(); | 
|  |  | 
|  | let append_ty_ops = |ret: &mut Vec<_>, fty: FloatTy| { | 
|  | let iter = Identifier::ALL | 
|  | .iter() | 
|  | .filter(move |id| id.math_op().float_ty == fty) | 
|  | .copied(); | 
|  | ret.extend(iter); | 
|  | }; | 
|  |  | 
|  | for item in list { | 
|  | match item { | 
|  | "all" => ret = Identifier::ALL.to_owned(), | 
|  | "all_f16" => append_ty_ops(&mut ret, FloatTy::F16), | 
|  | "all_f32" => append_ty_ops(&mut ret, FloatTy::F32), | 
|  | "all_f64" => append_ty_ops(&mut ret, FloatTy::F64), | 
|  | "all_f128" => append_ty_ops(&mut ret, FloatTy::F128), | 
|  | s => { | 
|  | let id = Identifier::from_str(s) | 
|  | .unwrap_or_else(|| panic!("unrecognized test name `{s}`")); | 
|  | ret.push(id); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | ret | 
|  | }); | 
|  |  | 
|  | /// Information about the function to be tested. | 
|  | #[derive(Debug)] | 
|  | struct TestEnv { | 
|  | /// Tests should be reduced because the platform is slow. E.g. 32-bit or emulated. | 
|  | slow_platform: bool, | 
|  | /// The float cannot be tested exhaustively, `f64` or `f128`. | 
|  | large_float_ty: bool, | 
|  | /// Env indicates that an extensive test should be run. | 
|  | should_run_extensive: bool, | 
|  | /// Multiprecision tests will be run. | 
|  | mp_tests_enabled: bool, | 
|  | /// The number of inputs to the function. | 
|  | input_count: usize, | 
|  | } | 
|  |  | 
|  | impl TestEnv { | 
|  | fn from_env(ctx: &CheckCtx) -> Self { | 
|  | let id = ctx.fn_ident; | 
|  | let op = id.math_op(); | 
|  |  | 
|  | let will_run_mp = cfg!(feature = "build-mpfr"); | 
|  | let large_float_ty = match op.float_ty { | 
|  | FloatTy::F16 | FloatTy::F32 => false, | 
|  | FloatTy::F64 | FloatTy::F128 => true, | 
|  | }; | 
|  |  | 
|  | let will_run_extensive = EXTENSIVE.contains(&id); | 
|  |  | 
|  | let input_count = op.rust_sig.args.len(); | 
|  |  | 
|  | Self { | 
|  | slow_platform: slow_platform(), | 
|  | large_float_ty, | 
|  | should_run_extensive: will_run_extensive, | 
|  | mp_tests_enabled: will_run_mp, | 
|  | input_count, | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Tests are pretty slow on non-64-bit targets, x86 MacOS, and targets that run in QEMU. Start | 
|  | /// with a reduced number on these platforms. | 
|  | fn slow_platform() -> bool { | 
|  | let slow_on_ci = crate::emulated() | 
|  | || usize::BITS < 64 | 
|  | || cfg!(all(target_arch = "x86_64", target_vendor = "apple")); | 
|  |  | 
|  | // If not running in CI, there is no need to reduce iteration count. | 
|  | slow_on_ci && crate::ci() | 
|  | } | 
|  |  | 
|  | /// The number of iterations to run for a given test. | 
|  | pub fn iteration_count(ctx: &CheckCtx, argnum: usize) -> u64 { | 
|  | let t_env = TestEnv::from_env(ctx); | 
|  |  | 
|  | // Ideally run 5M tests | 
|  | let mut domain_iter_count: u64 = 4_000_000; | 
|  |  | 
|  | // Start with a reduced number of tests on slow platforms. | 
|  | if t_env.slow_platform { | 
|  | domain_iter_count = 100_000; | 
|  | } | 
|  |  | 
|  | // If we will be running tests against MPFR, we don't need to test as much against musl. | 
|  | // However, there are some platforms where we have to test against musl since MPFR can't be | 
|  | // built. | 
|  | if t_env.mp_tests_enabled && ctx.basis == CheckBasis::Musl { | 
|  | domain_iter_count /= 100; | 
|  | } | 
|  |  | 
|  | // Run fewer random tests than domain tests. | 
|  | let random_iter_count = domain_iter_count / 100; | 
|  |  | 
|  | let mut total_iterations = match ctx.gen_kind { | 
|  | GeneratorKind::Spaced if ctx.extensive => extensive_max_iterations(), | 
|  | GeneratorKind::Spaced => domain_iter_count, | 
|  | GeneratorKind::Random => random_iter_count, | 
|  | GeneratorKind::EdgeCases | GeneratorKind::List => { | 
|  | unimplemented!("shoudn't need `iteration_count` for {:?}", ctx.gen_kind) | 
|  | } | 
|  | }; | 
|  |  | 
|  | // Larger float types get more iterations. | 
|  | if t_env.large_float_ty { | 
|  | if ctx.extensive { | 
|  | // Extensive already has a pretty high test count. | 
|  | total_iterations *= 2; | 
|  | } else { | 
|  | total_iterations *= 4; | 
|  | } | 
|  | } | 
|  |  | 
|  | // Functions with more arguments get more iterations. | 
|  | let arg_multiplier = 1 << (t_env.input_count - 1); | 
|  | total_iterations *= arg_multiplier; | 
|  |  | 
|  | // FMA has a huge domain but is reasonably fast to run, so increase another 1.5x. | 
|  | if ctx.base_name == BaseName::Fma { | 
|  | total_iterations = 3 * total_iterations / 2; | 
|  | } | 
|  |  | 
|  | // Some tests are significantly slower than others and need to be further reduced. | 
|  | if let Some(slow) = EXTREMELY_SLOW_TESTS | 
|  | .iter() | 
|  | .find(|slow| slow.matches_ctx(ctx)) | 
|  | { | 
|  | // However, do not override if the extensive iteration count has been manually set. | 
|  | if !(ctx.extensive && EXTENSIVE_ITER_OVERRIDE.is_some()) { | 
|  | total_iterations /= slow.reduce_factor; | 
|  | } | 
|  | } | 
|  |  | 
|  | if cfg!(optimizations_enabled) { | 
|  | // Always run at least 10,000 tests. | 
|  | total_iterations = total_iterations.max(10_000); | 
|  | } else { | 
|  | // Without optimizations, just run a quick check regardless of other parameters. | 
|  | total_iterations = 800; | 
|  | } | 
|  |  | 
|  | let mut overridden = false; | 
|  | if let Some(count) = ctx.override_iterations { | 
|  | total_iterations = count; | 
|  | overridden = true; | 
|  | } | 
|  |  | 
|  | // Adjust for the number of inputs | 
|  | let ntests = match t_env.input_count { | 
|  | 1 => total_iterations, | 
|  | 2 => (total_iterations as f64).sqrt().ceil() as u64, | 
|  | 3 => (total_iterations as f64).cbrt().ceil() as u64, | 
|  | _ => panic!("test has more than three arguments"), | 
|  | }; | 
|  |  | 
|  | let total = ntests.pow(t_env.input_count.try_into().unwrap()); | 
|  |  | 
|  | let seed_msg = match ctx.gen_kind { | 
|  | GeneratorKind::Spaced => String::new(), | 
|  | GeneratorKind::Random => { | 
|  | format!( | 
|  | " using `{SEED_ENV}={}`", | 
|  | str::from_utf8(SEED.as_slice()).unwrap() | 
|  | ) | 
|  | } | 
|  | GeneratorKind::EdgeCases | GeneratorKind::List => unimplemented!(), | 
|  | }; | 
|  |  | 
|  | test_log(&format!( | 
|  | "{gen_kind:?} {basis:?} {fn_ident} arg {arg}/{args}: {ntests} iterations \ | 
|  | ({total} total){seed_msg}{omsg}", | 
|  | gen_kind = ctx.gen_kind, | 
|  | basis = ctx.basis, | 
|  | fn_ident = ctx.fn_ident, | 
|  | arg = argnum + 1, | 
|  | args = t_env.input_count, | 
|  | omsg = if overridden { " (overridden)" } else { "" } | 
|  | )); | 
|  |  | 
|  | ntests | 
|  | } | 
|  |  | 
|  | /// Some tests require that an integer be kept within reasonable limits; generate that here. | 
|  | pub fn int_range(ctx: &CheckCtx, argnum: usize) -> RangeInclusive<i32> { | 
|  | let t_env = TestEnv::from_env(ctx); | 
|  |  | 
|  | if !matches!(ctx.base_name, BaseName::Jn | BaseName::Yn) { | 
|  | return i32::MIN..=i32::MAX; | 
|  | } | 
|  |  | 
|  | assert_eq!( | 
|  | argnum, 0, | 
|  | "For `jn`/`yn`, only the first argument takes an integer" | 
|  | ); | 
|  |  | 
|  | // The integer argument to `jn` is an iteration count. Limit this to ensure tests can be | 
|  | // completed in a reasonable amount of time. | 
|  | let non_extensive_range = if t_env.slow_platform || !cfg!(optimizations_enabled) { | 
|  | (-0xf)..=0xff | 
|  | } else { | 
|  | (-0xff)..=0xffff | 
|  | }; | 
|  |  | 
|  | let extensive_range = (-0xfff)..=0xfffff; | 
|  |  | 
|  | match ctx.gen_kind { | 
|  | _ if ctx.extensive => extensive_range, | 
|  | GeneratorKind::Spaced | GeneratorKind::Random => non_extensive_range, | 
|  | GeneratorKind::EdgeCases => extensive_range, | 
|  | GeneratorKind::List => unimplemented!("shoudn't need range for {:?}", ctx.gen_kind), | 
|  | } | 
|  | } | 
|  |  | 
|  | /// For domain tests, limit how many asymptotes or specified check points we test. | 
|  | pub fn check_point_count(ctx: &CheckCtx) -> usize { | 
|  | assert_eq!( | 
|  | ctx.gen_kind, | 
|  | GeneratorKind::EdgeCases, | 
|  | "check_point_count is intended for edge case tests" | 
|  | ); | 
|  | let t_env = TestEnv::from_env(ctx); | 
|  | if t_env.slow_platform || !cfg!(optimizations_enabled) { | 
|  | 4 | 
|  | } else { | 
|  | 10 | 
|  | } | 
|  | } | 
|  |  | 
|  | /// When validating points of interest (e.g. asymptotes, inflection points, extremes), also check | 
|  | /// this many surrounding values. | 
|  | pub fn check_near_count(ctx: &CheckCtx) -> u64 { | 
|  | assert_eq!( | 
|  | ctx.gen_kind, | 
|  | GeneratorKind::EdgeCases, | 
|  | "check_near_count is intended for edge case tests" | 
|  | ); | 
|  | if cfg!(optimizations_enabled) { | 
|  | // Taper based on the number of inputs. | 
|  | match ctx.input_count() { | 
|  | 1 | 2 => 100, | 
|  | 3 => 50, | 
|  | x => panic!("unexpected argument count {x}"), | 
|  | } | 
|  | } else { | 
|  | 8 | 
|  | } | 
|  | } | 
|  |  | 
|  | /// Check whether extensive actions should be run or skipped. | 
|  | pub fn skip_extensive_test(ctx: &CheckCtx) -> bool { | 
|  | let t_env = TestEnv::from_env(ctx); | 
|  | !t_env.should_run_extensive | 
|  | } | 
|  |  | 
|  | /// The number of iterations to run for `u256` fuzz tests. | 
|  | pub fn bigint_fuzz_iteration_count() -> u64 { | 
|  | if !cfg!(optimizations_enabled) { | 
|  | return 1000; | 
|  | } | 
|  |  | 
|  | if slow_platform() { 100_000 } else { 5_000_000 } | 
|  | } |