Introduce XFAILs that assert failure

Currently our XFAILs are open ended; we do not check that it actually
fails, so we have no easy way of knowing that a previously-failing test
starts passing. Introduce a new enum that we return from overrides to
give us more flexibility here, including the ability to assert that
expected failures happen.

With the new enum, it is also possible to specify ULP via return value
rather than passing a `&mut u32` parameter.

This includes refactoring of `precision.rs` to be more accurate about
where errors come from, if possible.

Fixes: https://github.com/rust-lang/libm/issues/455
This commit is contained in:
Trevor Gross 2025-01-23 05:08:10 +00:00 committed by Trevor Gross
parent 8dc4ef6f0f
commit ca8dccc5b6
2 changed files with 279 additions and 264 deletions

View file

@ -118,13 +118,13 @@ pub fn default_ulp(ctx: &CheckCtx) -> u32 {
// FIXME(#401): these need to be correctly rounded but are not.
Id::Fmaf => ulp = 1,
Id::Fdim => ulp = 1,
Id::Round => ulp = 1,
Id::Asinh => ulp = 3,
Id::Asinhf => ulp = 3,
Id::Exp10 | Id::Exp10f => ulp = 1_000_000,
Id::Exp2 | Id::Exp2f => ulp = 10_000_000,
Id::Log1p | Id::Log1pf => ulp = 2,
Id::Round => ulp = 1,
Id::Tan => ulp = 2,
_ => (),
}
@ -133,12 +133,42 @@ pub fn default_ulp(ctx: &CheckCtx) -> u32 {
ulp
}
/// Don't run further validation on this test case.
const SKIP: Option<TestResult> = Some(Ok(()));
/// Result of checking for possible overrides.
#[derive(Debug, Default)]
pub enum CheckAction {
/// The check should pass. Default case.
#[default]
AssertSuccess,
/// Return this to skip checks on a test that currently fails but shouldn't. Looks
/// the same as skip, but we keep them separate to better indicate purpose.
const XFAIL: Option<TestResult> = Some(Ok(()));
/// Override the ULP for this check.
AssertWithUlp(u32),
/// Failure is expected, ensure this is the case (xfail). Takes a contxt string to help trace
/// back exactly why we expect this to fail.
AssertFailure(&'static str),
/// The override somehow validated the result, here it is.
Custom(TestResult),
/// Disregard the output.
Skip,
}
/// Don't run further validation on this test case.
const SKIP: CheckAction = CheckAction::Skip;
/// Return this to skip checks on a test that currently fails but shouldn't. Takes a description
/// of context.
const XFAIL: fn(&'static str) -> CheckAction = CheckAction::AssertFailure;
/// Indicates that we expect a test to fail but we aren't asserting that it does (e.g. some results
/// within a range do actually pass).
///
/// Same as `SKIP`, just indicates we have something to eventually fix.
const XFAIL_NOCHECK: CheckAction = CheckAction::Skip;
/// By default, all tests should pass.
const DEFAULT: CheckAction = CheckAction::AssertSuccess;
/// Allow overriding the outputs of specific test cases.
///
@ -158,19 +188,13 @@ pub trait MaybeOverride<Input> {
_input: Input,
_actual: F,
_expected: F,
_ulp: &mut u32,
_ctx: &CheckCtx,
) -> Option<TestResult> {
None
) -> CheckAction {
DEFAULT
}
fn check_int<I: Int>(
_input: Input,
_actual: I,
_expected: I,
_ctx: &CheckCtx,
) -> Option<TestResult> {
None
fn check_int<I: Int>(_input: Input, _actual: I, _expected: I, _ctx: &CheckCtx) -> CheckAction {
DEFAULT
}
}
@ -178,47 +202,18 @@ pub trait MaybeOverride<Input> {
impl MaybeOverride<(f16,)> for SpecialCase {}
impl MaybeOverride<(f32,)> for SpecialCase {
fn check_float<F: Float>(
input: (f32,),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
if ctx.base_name == BaseName::Expm1 && input.0 > 80.0 && actual.is_infinite() {
fn check_float<F: Float>(input: (f32,), actual: F, expected: F, ctx: &CheckCtx) -> CheckAction {
if ctx.base_name == BaseName::Expm1
&& !input.0.is_infinite()
&& input.0 > 80.0
&& actual.is_infinite()
&& !expected.is_infinite()
{
// we return infinity but the number is representable
return XFAIL;
}
if ctx.base_name == BaseName::Sinh && input.0.abs() > 80.0 && actual.is_nan() {
// we return some NaN that should be real values or infinite
return XFAIL;
}
if ctx.base_name == BaseName::Acosh && input.0 < -1.0 {
// acoshf is undefined for x <= 1.0, but we return a random result at lower
// values.
return XFAIL;
}
if ctx.base_name == BaseName::Lgamma || ctx.base_name == BaseName::LgammaR && input.0 < 0.0
{
// loggamma should not be defined for x < 0, yet we both return results
return XFAIL;
}
if (ctx.base_name == BaseName::Lgamma || ctx.base_name == BaseName::LgammaR)
&& input.0 > 4e36
&& expected.is_infinite()
&& !actual.is_infinite()
{
// This result should saturate but we return a finite value.
return XFAIL;
}
if ctx.base_name == BaseName::J0 && input.0 < -1e34 {
// Errors get huge close to -inf
return XFAIL;
if ctx.basis == CheckBasis::Musl {
return XFAIL_NOCHECK;
}
return XFAIL("expm1 representable numbers");
}
if cfg!(x86_no_sse)
@ -227,18 +222,35 @@ impl MaybeOverride<(f32,)> for SpecialCase {
&& actual.is_infinite()
{
// We return infinity when there is a representable value. Test input: 127.97238
return XFAIL;
return XFAIL("586 exp2 representable numbers");
}
maybe_check_nan_bits(actual, expected, ctx)
if ctx.base_name == BaseName::Sinh && input.0.abs() > 80.0 && actual.is_nan() {
// we return some NaN that should be real values or infinite
if ctx.basis == CheckBasis::Musl {
return XFAIL_NOCHECK;
}
return XFAIL("sinh unexpected NaN");
}
if (ctx.base_name == BaseName::Lgamma || ctx.base_name == BaseName::LgammaR)
&& input.0 > 4e36
&& expected.is_infinite()
&& !actual.is_infinite()
{
// This result should saturate but we return a finite value.
return XFAIL_NOCHECK;
}
if ctx.base_name == BaseName::J0 && input.0 < -1e34 {
// Errors get huge close to -inf
return XFAIL_NOCHECK;
}
unop_common(input, actual, expected, ctx)
}
fn check_int<I: Int>(
input: (f32,),
actual: I,
expected: I,
ctx: &CheckCtx,
) -> Option<anyhow::Result<()>> {
fn check_int<I: Int>(input: (f32,), actual: I, expected: I, ctx: &CheckCtx) -> CheckAction {
// On MPFR for lgammaf_r, we set -1 as the integer result for negative infinity but MPFR
// sets +1
if ctx.basis == CheckBasis::Mpfr
@ -246,37 +258,25 @@ impl MaybeOverride<(f32,)> for SpecialCase {
&& input.0 == f32::NEG_INFINITY
&& actual.abs() == expected.abs()
{
XFAIL
} else {
None
return XFAIL("lgammar integer result");
}
DEFAULT
}
}
impl MaybeOverride<(f64,)> for SpecialCase {
fn check_float<F: Float>(
input: (f64,),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
if ctx.basis == CheckBasis::Musl {
if cfg!(target_arch = "x86") && ctx.base_name == BaseName::Acosh && input.0 < 1.0 {
// The function is undefined, both implementations return random results
return SKIP;
}
if cfg!(x86_no_sse)
&& ctx.base_name == BaseName::Ceil
&& input.0 < 0.0
&& input.0 > -1.0
&& expected == F::ZERO
&& actual == F::ZERO
{
// musl returns -0.0, we return +0.0
return XFAIL;
}
fn check_float<F: Float>(input: (f64,), actual: F, expected: F, ctx: &CheckCtx) -> CheckAction {
if cfg!(x86_no_sse)
&& ctx.base_name == BaseName::Ceil
&& ctx.basis == CheckBasis::Musl
&& input.0 < 0.0
&& input.0 > -1.0
&& expected == F::ZERO
&& actual == F::ZERO
{
// musl returns -0.0, we return +0.0
return XFAIL("i586 ceil signed zero");
}
if cfg!(x86_no_sse)
@ -285,53 +285,37 @@ impl MaybeOverride<(f64,)> for SpecialCase {
&& (expected - actual).abs() > F::ZERO
{
// Our rounding mode is incorrect.
return XFAIL;
return XFAIL("i586 rint rounding mode");
}
if ctx.base_name == BaseName::Acosh && input.0 < 1.0 {
// The function is undefined for the inputs, musl and our libm both return
// random results.
return XFAIL;
}
if ctx.base_name == BaseName::Lgamma || ctx.base_name == BaseName::LgammaR && input.0 < 0.0
{
// loggamma should not be defined for x < 0, yet we both return results
return XFAIL;
}
if ctx.base_name == BaseName::J0 && input.0 < -1e300 {
// Errors get huge close to -inf
return XFAIL;
}
if (ctx.fn_ident == Identifier::Ceil || ctx.fn_ident == Identifier::Floor)
&& cfg!(x86_no_sse)
if cfg!(x86_no_sse)
&& (ctx.fn_ident == Identifier::Ceil || ctx.fn_ident == Identifier::Floor)
&& expected.eq_repr(F::NEG_ZERO)
&& actual.eq_repr(F::ZERO)
{
// FIXME: the x87 implementations do not keep the distinction between -0.0 and 0.0.
// See https://github.com/rust-lang/libm/pull/404#issuecomment-2572399955
return XFAIL;
return XFAIL("i586 ceil/floor signed zero");
}
if (ctx.fn_ident == Identifier::Exp10 || ctx.fn_ident == Identifier::Exp2)
&& cfg!(x86_no_sse)
if cfg!(x86_no_sse)
&& (ctx.fn_ident == Identifier::Exp10 || ctx.fn_ident == Identifier::Exp2)
{
// FIXME: i586 has very imprecise results with ULP > u32::MAX for these
// operations so we can't reasonably provide a limit.
return XFAIL;
return XFAIL_NOCHECK;
}
maybe_check_nan_bits(actual, expected, ctx)
if ctx.base_name == BaseName::J0 && input.0 < -1e300 {
// Errors get huge close to -inf
return XFAIL_NOCHECK;
}
// maybe_check_nan_bits(actual, expected, ctx)
unop_common(input, actual, expected, ctx)
}
fn check_int<I: Int>(
input: (f64,),
actual: I,
expected: I,
ctx: &CheckCtx,
) -> Option<anyhow::Result<()>> {
fn check_int<I: Int>(input: (f64,), actual: I, expected: I, ctx: &CheckCtx) -> CheckAction {
// On MPFR for lgamma_r, we set -1 as the integer result for negative infinity but MPFR
// sets +1
if ctx.basis == CheckBasis::Mpfr
@ -339,41 +323,68 @@ impl MaybeOverride<(f64,)> for SpecialCase {
&& input.0 == f64::NEG_INFINITY
&& actual.abs() == expected.abs()
{
XFAIL
} else {
None
return XFAIL("lgammar integer result");
}
DEFAULT
}
}
#[cfg(f128_enabled)]
impl MaybeOverride<(f128,)> for SpecialCase {}
/// Check NaN bits if the function requires it
fn maybe_check_nan_bits<F: Float>(actual: F, expected: F, ctx: &CheckCtx) -> Option<TestResult> {
if !(ctx.base_name == BaseName::Fabs || ctx.base_name == BaseName::Copysign) {
return None;
// F1 and F2 are always the same type, this is just to please generics
fn unop_common<F1: Float, F2: Float>(
input: (F1,),
actual: F2,
expected: F2,
ctx: &CheckCtx,
) -> CheckAction {
if ctx.base_name == BaseName::Acosh
&& input.0 < F1::NEG_ONE
&& !(expected.is_nan() && actual.is_nan())
{
// acoshf is undefined for x <= 1.0, but we return a random result at lower values.
if ctx.basis == CheckBasis::Musl {
return XFAIL_NOCHECK;
}
return XFAIL("acoshf undefined");
}
// LLVM currently uses x87 instructions which quieten signalling NaNs to handle the i686
// `extern "C"` `f32`/`f64` return ABI.
// LLVM issue <https://github.com/llvm/llvm-project/issues/66803>
// Rust issue <https://github.com/rust-lang/rust/issues/115567>
if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl {
return SKIP;
if (ctx.base_name == BaseName::Lgamma || ctx.base_name == BaseName::LgammaR)
&& input.0 < F1::ZERO
&& !input.0.is_infinite()
{
// loggamma should not be defined for x < 0, yet we both return results
return XFAIL_NOCHECK;
}
// MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate.
if ctx.basis == CheckBasis::Mpfr {
return SKIP;
// fabs and copysign must leave NaNs untouched.
if ctx.base_name == BaseName::Fabs && input.0.is_nan() {
// LLVM currently uses x87 instructions which quieten signalling NaNs to handle the i686
// `extern "C"` `f32`/`f64` return ABI.
// LLVM issue <https://github.com/llvm/llvm-project/issues/66803>
// Rust issue <https://github.com/rust-lang/rust/issues/115567>
if cfg!(target_arch = "x86") && ctx.basis == CheckBasis::Musl && actual.is_nan() {
return XFAIL_NOCHECK;
}
// MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate.
if ctx.basis == CheckBasis::Mpfr {
return DEFAULT;
}
// abs and copysign require signaling NaNs to be propagated, so verify bit equality.
if actual.to_bits() == expected.to_bits() {
return CheckAction::Custom(Ok(()));
} else {
return CheckAction::Custom(Err(anyhow::anyhow!("NaNs have different bitpatterns")));
}
}
// abs and copysign require signaling NaNs to be propagated, so verify bit equality.
if actual.to_bits() == expected.to_bits() {
SKIP
} else {
Some(Err(anyhow::anyhow!("NaNs have different bitpatterns")))
}
DEFAULT
}
#[cfg(f16_enabled)]
@ -382,9 +393,8 @@ impl MaybeOverride<(f16, f16)> for SpecialCase {
input: (f16, f16),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
binop_common(input, actual, expected, ctx)
}
}
@ -394,18 +404,8 @@ impl MaybeOverride<(f32, f32)> for SpecialCase {
input: (f32, f32),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
if ctx.base_name == BaseName::Fmin
&& input.0.biteq(f32::NEG_ZERO)
&& input.1.biteq(f32::ZERO)
&& expected.biteq(F::NEG_ZERO)
&& actual.biteq(F::ZERO)
{
return XFAIL;
}
) -> CheckAction {
binop_common(input, actual, expected, ctx)
}
@ -414,7 +414,7 @@ impl MaybeOverride<(f32, f32)> for SpecialCase {
actual: I,
expected: I,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
remquo_common(actual, expected, ctx)
}
}
@ -424,18 +424,8 @@ impl MaybeOverride<(f64, f64)> for SpecialCase {
input: (f64, f64),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
if ctx.base_name == BaseName::Fmin
&& input.0.biteq(f64::NEG_ZERO)
&& input.1.biteq(f64::ZERO)
&& expected.biteq(F::ZERO)
&& actual.biteq(F::NEG_ZERO)
{
return XFAIL;
}
) -> CheckAction {
binop_common(input, actual, expected, ctx)
}
@ -444,33 +434,19 @@ impl MaybeOverride<(f64, f64)> for SpecialCase {
actual: I,
expected: I,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
remquo_common(actual, expected, ctx)
}
}
fn remquo_common<I: Int>(actual: I, expected: I, ctx: &CheckCtx) -> Option<TestResult> {
// FIXME: Our MPFR implementation disagrees with musl and may need to be updated.
if ctx.basis == CheckBasis::Mpfr
&& ctx.base_name == BaseName::Remquo
&& expected == I::MIN
&& actual == I::ZERO
{
return XFAIL;
}
None
}
#[cfg(f128_enabled)]
impl MaybeOverride<(f128, f128)> for SpecialCase {
fn check_float<F: Float>(
input: (f128, f128),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
binop_common(input, actual, expected, ctx)
}
}
@ -481,8 +457,17 @@ fn binop_common<F1: Float, F2: Float>(
actual: F2,
expected: F2,
ctx: &CheckCtx,
) -> Option<TestResult> {
/* FIXME(#439): we do not compare signed zeros */
) -> CheckAction {
// MPFR only has one NaN bitpattern; allow the default `.is_nan()` checks to validate. Skip if
// the first input (magnitude source) is NaN and the output is also a NaN, or if the second
// input (sign source) is NaN.
if ctx.basis == CheckBasis::Mpfr
&& ((input.0.is_nan() && actual.is_nan() && expected.is_nan()) || input.1.is_nan())
{
return SKIP;
}
/* FIXME(#439): our fmin and fmax do not compare signed zeros */
if ctx.base_name == BaseName::Fmin
&& input.0.biteq(F1::NEG_ZERO)
@ -490,7 +475,7 @@ fn binop_common<F1: Float, F2: Float>(
&& expected.biteq(F2::NEG_ZERO)
&& actual.biteq(F2::ZERO)
{
return XFAIL;
return XFAIL("fmin signed zeroes");
}
if ctx.base_name == BaseName::Fmax
@ -499,21 +484,32 @@ fn binop_common<F1: Float, F2: Float>(
&& expected.biteq(F2::ZERO)
&& actual.biteq(F2::NEG_ZERO)
{
return XFAIL;
return XFAIL("fmax signed zeroes");
}
// Musl propagates NaNs if one is provided as the input, but we return the other input.
match (&ctx.basis, ctx.base_name) {
(Musl, BaseName::Fmin | BaseName::Fmax)
if (input.0.is_nan() || input.1.is_nan()) && expected.is_nan() =>
{
XFAIL
}
(Mpfr, BaseName::Copysign) if input.1.is_nan() => SKIP,
_ => None,
if (ctx.base_name == BaseName::Fmax || ctx.base_name == BaseName::Fmin)
&& ctx.basis == Musl
&& (input.0.is_nan() ^ input.1.is_nan())
&& expected.is_nan()
{
return XFAIL("fmax/fmin musl NaN");
}
DEFAULT
}
fn remquo_common<I: Int>(actual: I, expected: I, ctx: &CheckCtx) -> CheckAction {
// FIXME: Our MPFR implementation disagrees with musl and may need to be updated.
if ctx.basis == CheckBasis::Mpfr
&& ctx.base_name == BaseName::Remquo
&& expected == I::MIN
&& actual == I::ZERO
{
return XFAIL("remquo integer mismatch");
}
DEFAULT
}
impl MaybeOverride<(i32, f32)> for SpecialCase {
@ -521,28 +517,19 @@ impl MaybeOverride<(i32, f32)> for SpecialCase {
input: (i32, f32),
actual: F,
expected: F,
ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
match (&ctx.basis, ctx.base_name) {
(Musl, _) => bessel_prec_dropoff(input, actual, expected, ulp, ctx),
// We return +0.0, MPFR returns -0.0
(Mpfr, BaseName::Jn | BaseName::Yn)
if input.1 == f32::NEG_INFINITY && actual == F::ZERO && expected == F::ZERO =>
{
XFAIL
}
// `ynf(213, 109.15641) = -inf` with our library, should be finite.
(_, BaseName::Yn)
if input.0 > 200 && !expected.is_infinite() && actual.is_infinite() =>
{
XFAIL
}
_ => None,
) -> CheckAction {
// `ynf(213, 109.15641) = -inf` with our library, should be finite.
if ctx.basis == Mpfr
&& ctx.base_name == BaseName::Yn
&& input.0 > 200
&& !expected.is_infinite()
&& actual.is_infinite()
{
return XFAIL("ynf infinity mismatch");
}
int_float_common(input, actual, expected, ctx)
}
}
@ -551,55 +538,51 @@ impl MaybeOverride<(i32, f64)> for SpecialCase {
input: (i32, f64),
actual: F,
expected: F,
ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
match (&ctx.basis, ctx.base_name) {
(Musl, _) => bessel_prec_dropoff(input, actual, expected, ulp, ctx),
// We return +0.0, MPFR returns -0.0
(Mpfr, BaseName::Jn | BaseName::Yn)
if input.1 == f64::NEG_INFINITY && actual == F::ZERO && expected == F::ZERO =>
{
XFAIL
}
_ => None,
}
) -> CheckAction {
int_float_common(input, actual, expected, ctx)
}
}
/// Our bessel functions blow up with large N values
fn bessel_prec_dropoff<F1: Float, F2: Float>(
fn int_float_common<F1: Float, F2: Float>(
input: (i32, F1),
actual: F2,
expected: F2,
ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
if ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn {
) -> CheckAction {
if ctx.basis == Mpfr
&& (ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn)
&& input.1 == F1::NEG_INFINITY
&& actual == F2::ZERO
&& expected == F2::ZERO
{
return XFAIL("mpfr b");
}
// Our bessel functions blow up with large N values
if ctx.basis == Musl && (ctx.base_name == BaseName::Jn || ctx.base_name == BaseName::Yn) {
if input.0 > 4000 {
return XFAIL;
return XFAIL_NOCHECK;
} else if input.0 > 2000 {
// *ulp = 20_000;
*ulp = 20000;
return CheckAction::AssertWithUlp(20_000);
} else if input.0 > 1000 {
*ulp = 4000;
return CheckAction::AssertWithUlp(4_000);
}
}
// Values near infinity sometimes get cut off for us. `ynf(681, 509.90924) = -inf` but should
// be -3.2161271e38.
if ctx.fn_ident == Identifier::Ynf
if ctx.basis == Musl
&& ctx.fn_ident == Identifier::Ynf
&& !expected.is_infinite()
&& actual.is_infinite()
&& (expected.abs().to_bits().abs_diff(actual.abs().to_bits())
< F2::Int::cast_from(1_000_000u32))
{
return XFAIL;
return XFAIL_NOCHECK;
}
None
DEFAULT
}
impl MaybeOverride<(f32, i32)> for SpecialCase {}
@ -610,9 +593,8 @@ impl MaybeOverride<(f32, f32, f32)> for SpecialCase {
input: (f32, f32, f32),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
ternop_common(input, actual, expected, ctx)
}
}
@ -621,9 +603,8 @@ impl MaybeOverride<(f64, f64, f64)> for SpecialCase {
input: (f64, f64, f64),
actual: F,
expected: F,
_ulp: &mut u32,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
ternop_common(input, actual, expected, ctx)
}
}
@ -634,7 +615,7 @@ fn ternop_common<F1: Float, F2: Float>(
actual: F2,
expected: F2,
ctx: &CheckCtx,
) -> Option<TestResult> {
) -> CheckAction {
// FIXME(fma): 754-2020 says "When the exact result of (a × b) + c is non-zero yet the result
// of fusedMultiplyAdd is zero because of rounding, the zero result takes the sign of the
// exact result". Our implementation returns the wrong sign:
@ -647,8 +628,8 @@ fn ternop_common<F1: Float, F2: Float>(
&& expected.biteq(F2::NEG_ZERO)
&& actual.biteq(F2::ZERO)
{
return XFAIL;
return XFAIL("fma sign");
}
None
DEFAULT
}

View file

@ -8,8 +8,9 @@
use std::fmt;
use anyhow::{Context, bail, ensure};
use anyhow::{Context, anyhow, bail, ensure};
use crate::precision::CheckAction;
use crate::{CheckCtx, Float, Int, MaybeOverride, SpecialCase, TestResult};
/// Trait for calling a function with a tuple as arguments.
@ -185,20 +186,34 @@ where
Input: Hex + fmt::Debug,
SpecialCase: MaybeOverride<Input>,
{
if let Some(res) = SpecialCase::check_int(input, actual, expected, ctx) {
return res;
}
let (result, xfail_msg) = match SpecialCase::check_int(input, actual, expected, ctx) {
CheckAction::AssertSuccess => (actual == expected, None),
CheckAction::AssertFailure(msg) => (actual != expected, Some(msg)),
CheckAction::Custom(res) => return res,
CheckAction::Skip => return Ok(()),
CheckAction::AssertWithUlp(_) => panic!("ulp has no meaning for integer checks"),
};
let make_xfail_msg = || match xfail_msg {
Some(m) => format!(
"expected failure but test passed. Does an XFAIL need to be updated?\n\
failed at: {m}",
),
None => String::new(),
};
anyhow::ensure!(
actual == expected,
result,
"\
\n input: {input:?} {ibits}\
\n expected: {expected:<22?} {expbits}\
\n actual: {actual:<22?} {actbits}\
\n {msg}\
",
actbits = actual.hex(),
expbits = expected.hex(),
ibits = input.hex(),
msg = make_xfail_msg()
);
Ok(())
@ -246,15 +261,19 @@ where
u32: TryFrom<F::SignedInt, Error: fmt::Debug>,
SpecialCase: MaybeOverride<Input>,
{
let mut assert_failure_msg = None;
// Create a wrapper function so we only need to `.with_context` once.
let inner = || -> TestResult {
let mut inner = || -> TestResult {
let mut allowed_ulp = ctx.ulp;
// If the tested function requires a nonstandard test, run it here.
if let Some(res) = SpecialCase::check_float(input, actual, expected, &mut allowed_ulp, ctx)
{
return res;
}
match SpecialCase::check_float(input, actual, expected, ctx) {
CheckAction::AssertSuccess => (),
CheckAction::AssertFailure(msg) => assert_failure_msg = Some(msg),
CheckAction::Custom(res) => return res,
CheckAction::Skip => return Ok(()),
CheckAction::AssertWithUlp(ulp_override) => allowed_ulp = ulp_override,
};
// Check when both are NaNs
if actual.is_nan() && expected.is_nan() {
@ -280,14 +299,29 @@ where
let ulp_diff = act_bits.checked_sub(exp_bits).unwrap().abs();
let ulp_u32 = u32::try_from(ulp_diff)
.map_err(|e| anyhow::anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?;
.map_err(|e| anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?;
ensure!(ulp_u32 <= allowed_ulp, "ulp {ulp_diff} > {allowed_ulp}",);
Ok(())
};
inner().with_context(|| {
let mut res = inner();
if let Some(msg) = assert_failure_msg {
// Invert `Ok` and `Err` if the test is an xfail.
if res.is_ok() {
let e = anyhow!(
"expected failure but test passed. Does an XFAIL need to be updated?\n\
failed at: {msg}",
);
res = Err(e)
} else {
res = Ok(())
}
}
res.with_context(|| {
format!(
"\
\n input: {input:?} {ibits}\