Skip to content
This repository was archived by the owner on Apr 28, 2025. It is now read-only.

Commit 487fe07

Browse files
committed
Introduce a generic way to control checks for specific cases
Sometimes we want to be able to xfail specific inputs without changing the checked ULP for all cases or skipping the tests. There are also some cases where we need to perform extra checks for only specific functions. Add a trait that provides a hook for providing extra checks or skipping existing checks on a per-function or per-input basis.
1 parent bc0e731 commit 487fe07

File tree

4 files changed

+163
-8
lines changed

4 files changed

+163
-8
lines changed

crates/libm-test/src/lib.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
pub mod gen;
22
mod num_traits;
3+
mod special_case;
34
mod test_traits;
45

56
pub use num_traits::{Float, Hex, Int};
7+
pub use special_case::{MaybeOverride, SpecialCase};
68
pub use test_traits::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, TupleCall};
79

810
/// Result type for tests is usually from `anyhow`. Most times there is no success value to
@@ -11,3 +13,24 @@ pub type TestResult<T = (), E = anyhow::Error> = Result<T, E>;
1113

1214
// List of all files present in libm's source
1315
include!(concat!(env!("OUT_DIR"), "/all_files.rs"));
16+
17+
/// Return the unsuffixed version of a function name; e.g. `abs` and `absf` both return `abs`,
18+
/// `lgamma_r` and `lgammaf_r` both return `lgamma_r`.
19+
pub fn canonical_name(name: &str) -> &str {
20+
let known_mappings = &[
21+
("erff", "erf"),
22+
("erf", "erf"),
23+
("lgammaf_r", "lgamma_r"),
24+
("modff", "modf"),
25+
("modf", "modf"),
26+
];
27+
28+
match known_mappings.iter().find(|known| known.0 == name) {
29+
Some(found) => found.1,
30+
None => name
31+
.strip_suffix("f")
32+
.or_else(|| name.strip_suffix("f16"))
33+
.or_else(|| name.strip_suffix("f128"))
34+
.unwrap_or(name),
35+
}
36+
}

crates/libm-test/src/num_traits.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::fmt;
22

3-
use crate::TestResult;
3+
use crate::{MaybeOverride, SpecialCase, TestResult};
44

55
/// Common types and methods for floating point numbers.
66
pub trait Float: Copy + fmt::Display + fmt::Debug + PartialEq<Self> {
@@ -137,13 +137,21 @@ macro_rules! impl_int {
137137
}
138138
}
139139

140-
impl<Input: Hex + fmt::Debug> $crate::CheckOutput<Input> for $ty {
140+
impl<Input> $crate::CheckOutput<Input> for $ty
141+
where
142+
Input: Hex + fmt::Debug,
143+
SpecialCase: MaybeOverride<Input>,
144+
{
141145
fn validate<'a>(
142146
self,
143147
expected: Self,
144148
input: Input,
145-
_ctx: &$crate::CheckCtx,
149+
ctx: &$crate::CheckCtx,
146150
) -> TestResult {
151+
if let Some(res) = SpecialCase::check_int(input, self, expected, ctx) {
152+
return res;
153+
}
154+
147155
anyhow::ensure!(
148156
self == expected,
149157
"\

crates/libm-test/src/special_case.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Configuration for skipping or changing the result for individual test cases (inputs) rather
2+
//! than ignoring entire tests.
3+
4+
use crate::{CheckCtx, Float, Int, TestResult};
5+
6+
/// Type implementing [`IgnoreCase`].
7+
pub struct SpecialCase;
8+
9+
/// Don't run further validation on this test case.
10+
const SKIP: Option<TestResult> = Some(Ok(()));
11+
12+
/// Return this to skip checks on a test that currently fails but shouldn't. Looks
13+
/// the same as skip, but we keep them separate to better indicate purpose.
14+
const XFAIL: Option<TestResult> = Some(Ok(()));
15+
16+
/// Allow overriding the outputs of specific test cases.
17+
///
18+
/// There are some cases where we want to xfail specific cases or handle certain inputs
19+
/// differently than the rest of calls to `validate`. This provides a hook to do that.
20+
///
21+
/// If `None` is returned, checks will proceed as usual. If `Some(result)` is returned, checks
22+
/// are skipped and the provided result is returned instead.
23+
///
24+
/// This gets implemented once per input type, then the functions provide further filtering
25+
/// based on function name and values.
26+
///
27+
/// `ulp` can also be set to adjust the ULP for that specific test, even if `None` is still
28+
/// returned.
29+
pub trait MaybeOverride<Input> {
30+
fn check_float<F: Float>(
31+
_input: Input,
32+
_actual: F,
33+
_expected: F,
34+
_ulp: &mut u32,
35+
_ctx: &CheckCtx,
36+
) -> Option<TestResult> {
37+
None
38+
}
39+
40+
fn check_int<I: Int>(
41+
_input: Input,
42+
_actual: I,
43+
_expected: I,
44+
_ctx: &CheckCtx,
45+
) -> Option<TestResult> {
46+
None
47+
}
48+
}
49+
50+
impl MaybeOverride<(f32,)> for SpecialCase {
51+
fn check_float<F: Float>(
52+
_input: (f32,),
53+
actual: F,
54+
expected: F,
55+
_ulp: &mut u32,
56+
ctx: &CheckCtx,
57+
) -> Option<TestResult> {
58+
maybe_check_nan_bits(actual, expected, ctx)
59+
}
60+
}
61+
62+
impl MaybeOverride<(f64,)> for SpecialCase {
63+
fn check_float<F: Float>(
64+
_input: (f64,),
65+
actual: F,
66+
expected: F,
67+
_ulp: &mut u32,
68+
ctx: &CheckCtx,
69+
) -> Option<TestResult> {
70+
maybe_check_nan_bits(actual, expected, ctx)
71+
}
72+
}
73+
74+
impl MaybeOverride<(f32, f32)> for SpecialCase {}
75+
impl MaybeOverride<(f64, f64)> for SpecialCase {}
76+
impl MaybeOverride<(f32, f32, f32)> for SpecialCase {}
77+
impl MaybeOverride<(f64, f64, f64)> for SpecialCase {}
78+
impl MaybeOverride<(i32, f32)> for SpecialCase {}
79+
impl MaybeOverride<(i32, f64)> for SpecialCase {}
80+
impl MaybeOverride<(f32, i32)> for SpecialCase {}
81+
impl MaybeOverride<(f64, i32)> for SpecialCase {}
82+
83+
/// Check NaN bits if the function requires it
84+
fn maybe_check_nan_bits<F: Float>(actual: F, expected: F, ctx: &CheckCtx) -> Option<TestResult> {
85+
if !(ctx.canonical_name == "abs" || ctx.canonical_name == "copysigh") {
86+
return None;
87+
}
88+
89+
// abs and copysign require signaling NaNs to be propagated, so verify bit equality.
90+
if actual.to_bits() == expected.to_bits() {
91+
return SKIP;
92+
} else {
93+
Some(Err(anyhow::anyhow!("NaNs have different bitpatterns")))
94+
}
95+
}

crates/libm-test/src/test_traits.rs

Lines changed: 34 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use std::fmt;
1111

1212
use anyhow::{Context, bail, ensure};
1313

14-
use crate::{Float, Hex, Int, TestResult};
14+
use crate::{Float, Hex, Int, MaybeOverride, SpecialCase, TestResult};
1515

1616
/// Implement this on types that can generate a sequence of tuples for test input.
1717
pub trait GenerateInput<TupleArgs> {
@@ -34,10 +34,19 @@ pub struct CheckCtx {
3434
pub ulp: u32,
3535
/// Function name.
3636
pub fname: &'static str,
37+
/// Return the unsuffixed version of the function name.
38+
pub canonical_name: &'static str,
3739
/// Source of truth for tests.
3840
pub basis: CheckBasis,
3941
}
4042

43+
impl CheckCtx {
44+
pub fn new(ulp: u32, fname: &'static str, basis: CheckBasis) -> Self {
45+
let canonical_fname = crate::canonical_name(fname);
46+
Self { ulp, fname, canonical_name: canonical_fname, basis }
47+
}
48+
}
49+
4150
/// Possible items to test against
4251
#[derive(Clone, Debug, PartialEq, Eq)]
4352
pub enum CheckBasis {}
@@ -135,10 +144,20 @@ where
135144
F: Float + Hex,
136145
Input: Hex + fmt::Debug,
137146
u32: TryFrom<F::SignedInt, Error: fmt::Debug>,
147+
SpecialCase: MaybeOverride<Input>,
138148
{
139149
fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult {
140150
// Create a wrapper function so we only need to `.with_context` once.
141151
let inner = || -> TestResult {
152+
let mut allowed_ulp = ctx.ulp;
153+
154+
// If the tested function requires a nonstandard test, run it here.
155+
if let Some(res) =
156+
SpecialCase::check_float(input, self, expected, &mut allowed_ulp, ctx)
157+
{
158+
return res;
159+
}
160+
142161
// Check when both are NaNs
143162
if self.is_nan() && expected.is_nan() {
144163
ensure!(self.to_bits() == expected.to_bits(), "NaNs have different bitpatterns");
@@ -165,7 +184,6 @@ where
165184
let ulp_u32 = u32::try_from(ulp_diff)
166185
.map_err(|e| anyhow::anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?;
167186

168-
let allowed_ulp = ctx.ulp;
169187
ensure!(ulp_u32 <= allowed_ulp, "ulp {ulp_diff} > {allowed_ulp}",);
170188

171189
Ok(())
@@ -190,17 +208,28 @@ where
190208
macro_rules! impl_tuples {
191209
($(($a:ty, $b:ty);)*) => {
192210
$(
193-
impl<Input: Hex + fmt::Debug> CheckOutput<Input> for ($a, $b) {
211+
impl<Input> CheckOutput<Input> for ($a, $b)
212+
where
213+
Input: Hex + fmt::Debug,
214+
SpecialCase: MaybeOverride<Input>,
215+
{
194216
fn validate<'a>(
195217
self,
196218
expected: Self,
197219
input: Input,
198220
ctx: &CheckCtx,
199221
) -> TestResult {
200-
self.0.validate(expected.0, input, ctx,)
222+
self.0.validate(expected.0, input, ctx)
201223
.and_then(|()| self.1.validate(expected.1, input, ctx))
202224
.with_context(|| format!(
203-
"full input {input:?} full actual {self:?} expected {expected:?}"
225+
"full context:\
226+
\n input: {input:?} {ibits}\
227+
\n expected: {expected:?} {expbits}\
228+
\n actual: {self:?} {actbits}\
229+
",
230+
actbits = self.hex(),
231+
expbits = expected.hex(),
232+
ibits = input.hex(),
204233
))
205234
}
206235
}

0 commit comments

Comments
 (0)