blob: 90f81195c856055504254e9aa37014a1930404ab [file] [log] [blame]
//! 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 }
}