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

Commit 58c608e

Browse files
committed
Add traits for testing
These traits give us a more generic way to interface with tuples used for (1) test input, (2) function arguments, and (3) test input.
1 parent 6f6b0e1 commit 58c608e

File tree

4 files changed

+249
-0
lines changed

4 files changed

+249
-0
lines changed

crates/libm-test/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ default = []
1212
test-musl-serialized = ["rand"]
1313

1414
[dependencies]
15+
anyhow = "1.0.90"
1516
libm = { path = "../.." }
1617
libm-macros = { path = "../libm-macros" }
1718

crates/libm-test/src/lib.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
mod num_traits;
2+
mod test_traits;
23

34
pub use num_traits::{Float, Hex, Int};
5+
pub use test_traits::{CheckBasis, CheckCtx, CheckOutput, GenerateInput, TupleCall};
6+
7+
/// Result type for tests is usually from `anyhow`. Most times there is no success value to
8+
/// propagate.
9+
pub type TestResult<T = (), E = anyhow::Error> = Result<T, E>;
410

511
// List of all files present in libm's source
612
include!(concat!(env!("OUT_DIR"), "/all_files.rs"));

crates/libm-test/src/num_traits.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use std::fmt;
22

3+
use crate::TestResult;
4+
35
/// Common types and methods for floating point numbers.
46
pub trait Float: Copy + fmt::Display + fmt::Debug + PartialEq<Self> {
57
type Int: Int<OtherSign = Self::SignedInt, Unsigned = Self::Int>;
@@ -134,6 +136,29 @@ macro_rules! impl_int {
134136
format!("{self:#0width$x}", width = ((Self::BITS / 4) + 2) as usize)
135137
}
136138
}
139+
140+
impl<Input: Hex + fmt::Debug> $crate::CheckOutput<Input> for $ty {
141+
fn validate<'a>(
142+
self,
143+
expected: Self,
144+
input: Input,
145+
_ctx: &$crate::CheckCtx,
146+
) -> TestResult {
147+
anyhow::ensure!(
148+
self == expected,
149+
"\
150+
\n input: {input:?} {ibits}\
151+
\n expected: {expected:<22?} {expbits}\
152+
\n actual: {self:<22?} {actbits}\
153+
",
154+
actbits = self.hex(),
155+
expbits = expected.hex(),
156+
ibits = input.hex(),
157+
);
158+
159+
Ok(())
160+
}
161+
}
137162
}
138163
}
139164

crates/libm-test/src/test_traits.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
//! Traits related to testing.
2+
//!
3+
//! There are three main traits in this module:
4+
//!
5+
//! - `GenerateInput`: implemented on any types that create test cases.
6+
//! - `TupleCall`: implemented on tuples to allow calling them as function arguments.
7+
//! - `CheckOutput`: implemented on anything that is an output type for validation against an
8+
//! expected value.
9+
10+
use std::fmt;
11+
12+
use anyhow::{Context, bail, ensure};
13+
14+
use crate::{Float, Hex, Int, TestResult};
15+
16+
/// Implement this on types that can generate a sequence of tuples for test input.
17+
pub trait GenerateInput<TupleArgs> {
18+
fn get_cases(&self) -> impl Iterator<Item = TupleArgs>;
19+
}
20+
21+
/// Trait for calling a function with a tuple as arguments.
22+
///
23+
/// Implemented on the tuple with the function signature as the generic (so we can use the same
24+
/// tuple for multiple signatures).
25+
pub trait TupleCall<Func>: fmt::Debug {
26+
type Output;
27+
fn call(self, f: Func) -> Self::Output;
28+
}
29+
30+
/// Context passed to [`CheckOutput`].
31+
#[derive(Clone, Debug, PartialEq, Eq)]
32+
pub struct CheckCtx {
33+
/// Allowed ULP deviation
34+
pub ulp: u32,
35+
/// Function name.
36+
pub fname: &'static str,
37+
/// Source of truth for tests.
38+
pub basis: CheckBasis,
39+
}
40+
41+
/// Possible items to test against
42+
#[derive(Clone, Debug, PartialEq, Eq)]
43+
pub enum CheckBasis {}
44+
45+
/// A trait to implement on any output type so we can verify it in a generic way.
46+
pub trait CheckOutput<Input>: Sized {
47+
/// Validate `self` (actual) and `expected` are the same.
48+
///
49+
/// `input` is only used here for error messages.
50+
fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult;
51+
}
52+
53+
impl<T1, R> TupleCall<fn(T1) -> R> for (T1,)
54+
where
55+
T1: fmt::Debug,
56+
{
57+
type Output = R;
58+
59+
fn call(self, f: fn(T1) -> R) -> Self::Output {
60+
f(self.0)
61+
}
62+
}
63+
64+
impl<T1, T2, R> TupleCall<fn(T1, T2) -> R> for (T1, T2)
65+
where
66+
T1: fmt::Debug,
67+
T2: fmt::Debug,
68+
{
69+
type Output = R;
70+
71+
fn call(self, f: fn(T1, T2) -> R) -> Self::Output {
72+
f(self.0, self.1)
73+
}
74+
}
75+
76+
impl<T1, T2, R> TupleCall<fn(T1, &mut T2) -> R> for (T1,)
77+
where
78+
T1: fmt::Debug,
79+
T2: fmt::Debug + Default,
80+
{
81+
type Output = (R, T2);
82+
83+
fn call(self, f: fn(T1, &mut T2) -> R) -> Self::Output {
84+
let mut t2 = T2::default();
85+
(f(self.0, &mut t2), t2)
86+
}
87+
}
88+
89+
impl<T1, T2, T3, R> TupleCall<fn(T1, T2, T3) -> R> for (T1, T2, T3)
90+
where
91+
T1: fmt::Debug,
92+
T2: fmt::Debug,
93+
T3: fmt::Debug,
94+
{
95+
type Output = R;
96+
97+
fn call(self, f: fn(T1, T2, T3) -> R) -> Self::Output {
98+
f(self.0, self.1, self.2)
99+
}
100+
}
101+
102+
impl<T1, T2, T3, R> TupleCall<fn(T1, T2, &mut T3) -> R> for (T1, T2)
103+
where
104+
T1: fmt::Debug,
105+
T2: fmt::Debug,
106+
T3: fmt::Debug + Default,
107+
{
108+
type Output = (R, T3);
109+
110+
fn call(self, f: fn(T1, T2, &mut T3) -> R) -> Self::Output {
111+
let mut t3 = T3::default();
112+
(f(self.0, self.1, &mut t3), t3)
113+
}
114+
}
115+
116+
impl<T1, T2, T3> TupleCall<fn(T1, &mut T2, &mut T3)> for (T1,)
117+
where
118+
T1: fmt::Debug,
119+
T2: fmt::Debug + Default,
120+
T3: fmt::Debug + Default,
121+
{
122+
type Output = (T2, T3);
123+
124+
fn call(self, f: fn(T1, &mut T2, &mut T3)) -> Self::Output {
125+
let mut t2 = T2::default();
126+
let mut t3 = T3::default();
127+
f(self.0, &mut t2, &mut t3);
128+
(t2, t3)
129+
}
130+
}
131+
132+
// Implement for floats
133+
impl<F, Input> CheckOutput<Input> for F
134+
where
135+
F: Float + Hex,
136+
Input: Hex + fmt::Debug,
137+
u32: TryFrom<F::SignedInt, Error: fmt::Debug>,
138+
{
139+
fn validate<'a>(self, expected: Self, input: Input, ctx: &CheckCtx) -> TestResult {
140+
// Create a wrapper function so we only need to `.with_context` once.
141+
let inner = || -> TestResult {
142+
// Check when both are NaNs
143+
if self.is_nan() && expected.is_nan() {
144+
ensure!(self.to_bits() == expected.to_bits(), "NaNs have different bitpatterns");
145+
// Nothing else to check
146+
return Ok(());
147+
} else if self.is_nan() || expected.is_nan() {
148+
// Check when only one is a NaN
149+
bail!("real value != NaN")
150+
}
151+
152+
// Make sure that the signs are the same before checing ULP to avoid wraparound
153+
let act_sig = self.signum();
154+
let exp_sig = expected.signum();
155+
ensure!(act_sig == exp_sig, "mismatched signs {act_sig} {exp_sig}");
156+
157+
if self.is_infinite() ^ expected.is_infinite() {
158+
bail!("mismatched infinities");
159+
}
160+
161+
let act_bits = self.to_bits().signed();
162+
let exp_bits = expected.to_bits().signed();
163+
164+
let ulp_diff = act_bits.checked_sub(exp_bits).unwrap().abs();
165+
166+
let ulp_u32 = u32::try_from(ulp_diff)
167+
.map_err(|e| anyhow::anyhow!("{e:?}: ulp of {ulp_diff} exceeds u32::MAX"))?;
168+
169+
let allowed_ulp = ctx.ulp;
170+
ensure!(ulp_u32 <= allowed_ulp, "ulp {ulp_diff} > {allowed_ulp}",);
171+
172+
Ok(())
173+
};
174+
175+
inner().with_context(|| {
176+
format!(
177+
"\
178+
\n input: {input:?} {ibits}\
179+
\n expected: {expected:<22?} {expbits}\
180+
\n actual: {self:<22?} {actbits}\
181+
",
182+
actbits = self.hex(),
183+
expbits = expected.hex(),
184+
ibits = input.hex(),
185+
)
186+
})
187+
}
188+
}
189+
190+
/// Implement `CheckOutput` for combinations of types.
191+
macro_rules! impl_tuples {
192+
($(($a:ty, $b:ty);)*) => {
193+
$(
194+
impl<Input: Hex + fmt::Debug> CheckOutput<Input> for ($a, $b) {
195+
fn validate<'a>(
196+
self,
197+
expected: Self,
198+
input: Input,
199+
ctx: &CheckCtx,
200+
) -> TestResult {
201+
self.0.validate(expected.0, input, ctx,)
202+
.and_then(|()| self.1.validate(expected.1, input, ctx))
203+
.with_context(|| format!(
204+
"full input {input:?} full actual {self:?} expected {expected:?}"
205+
))
206+
}
207+
}
208+
)*
209+
};
210+
}
211+
212+
impl_tuples!(
213+
(f32, i32);
214+
(f64, i32);
215+
(f32, f32);
216+
(f64, f64);
217+
);

0 commit comments

Comments
 (0)