ptth/crates/always_equal/src/lib.rs

239 lines
5.0 KiB
Rust

//! # Always-Equal
//!
//! Always-Equal lets you wrap `PartialEq` implementations around
//! things that cannot be compared, like a `File`.
//!
//! To use Always-Equal, put these two conditional `use`
//! statements in your module:
//!
//! ```
//! #[cfg (test)]
//! use always_equal::test::AlwaysEqual;
//!
//! #[cfg (not (test))]
//! use always_equal::prod::AlwaysEqual;
//! ```
//!
//! Then wrap `AlwaysEqual` around anything that needs
//! PartialEq but can't possibly implement it:
//!
//! ```
//! use std::fs::File;
//! use always_equal::test::AlwaysEqual;
//!
//! #[derive (Debug, PartialEq)]
//! pub struct MyStruct {
//! pub b: bool,
//! pub i: i64,
//! pub file: AlwaysEqual <File>,
//! }
//!
//! // In test code, you can create an empty wrapper using
//! // `testing_blank`, which is equal to any other wrapper:
//!
//! let expected = MyStruct {
//! b: true,
//! i: 0,
//! file: AlwaysEqual::testing_blank (),
//! };
//!
//! # let my_file = File::open ("Cargo.toml").unwrap ();
//! let actual = MyStruct {
//! b: true,
//! i: 0,
//! file: my_file.into (),
//! };
//!
//! assert_eq! (expected, actual);
//! ```
//!
//! This is implemented with `Option` in test mode.
//! In production mode, wrappers are never equal to any other
//! wrapper, and the `Option` is removed so that
//! `sizeof::<AlwaysEqual <T>> () == sizeof::<T>`
//! _should_ be true.
/// Should be used for non-test builds
pub mod prod {
use std::fmt;
/// In prod mode, `AlwaysEqual <T>` has the same size as `T`.
/// `Debug` and `Display` are passed through if `T` implements
/// them. `PartialEq` always returns `false` in prod mode.
///
/// Wrapping a string, checking its equality, and unwrapping it:
///
/// ```
/// use always_equal::prod::AlwaysEqual;
///
/// let s = "some string";
/// let a = AlwaysEqual::from (s);
/// let b = AlwaysEqual::from (s);
/// assert_ne! (a, b);
/// let s2 = a.into_inner ();
/// ```
pub struct AlwaysEqual <T> {
inner: T,
}
impl <T: fmt::Debug> fmt::Debug for AlwaysEqual <T> {
fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt (f)
}
}
impl <T: fmt::Display> fmt::Display for AlwaysEqual <T> {
fn fmt (&self, f: &mut fmt::Formatter) -> fmt::Result {
self.inner.fmt (f)
}
}
impl <T> AlwaysEqual <T> {
pub fn into_inner (self) -> T {
self.inner
}
}
impl <T> From <T> for AlwaysEqual <T> {
fn from (inner: T) -> Self {
Self {
inner,
}
}
}
impl <T> PartialEq for AlwaysEqual <T> {
fn eq (&self, _other: &Self) -> bool {
false
}
}
}
/// Should be used for test builds
pub mod test {
/// In test mode, `AlwaysEqual <T>` has the same size as
/// `Option <T>`. `Debug` is derived. `PartialEq` returns
/// `true` if either operand is a testing blank.
///
/// Wrapping a string, comparing it with a testing blank,
/// and unwrapping it:
///
/// ```
/// use always_equal::test::AlwaysEqual;
///
/// let s = "some string";
/// let a = AlwaysEqual::from (s);
/// let b = AlwaysEqual::testing_blank ();
/// assert_eq! (a, b);
/// let s2 = a.into_inner ();
/// ```
#[derive (Debug)]
pub struct AlwaysEqual <T> {
inner: Option <T>,
}
impl <T> AlwaysEqual <T> {
pub fn into_inner (self) -> T {
match self.inner {
Some (x) => x,
None => unreachable! (),
}
}
pub fn testing_blank () -> Self {
Self {
inner: None,
}
}
}
impl <T: Default> Default for AlwaysEqual <T> {
fn default () -> Self {
Self::from (T::default ())
}
}
impl <T> From <T> for AlwaysEqual <T> {
fn from (inner: T) -> Self {
Self {
inner: Some (inner),
}
}
}
impl <T> PartialEq for AlwaysEqual <T> {
fn eq (&self, other: &Self) -> bool {
self.inner.is_none () || other.inner.is_none ()
}
}
}
#[cfg (test)]
mod tests {
use std::fs::File;
use super::test::*;
// Can't impl Clone or PartialEq because of the File
type CantCompare = Option <File>;
#[derive (Debug, Default, PartialEq)]
struct MyStruct
{
file: AlwaysEqual <CantCompare>,
name: &'static str,
}
#[test]
fn test_1 () {
let concrete_1 = MyStruct {
file: None.into (),
name: "my_struct",
};
let concrete_2 = MyStruct {
file: None.into (),
name: "my_struct",
};
let concrete_bad = MyStruct {
file: None.into (),
name: "not_my_struct",
};
assert_ne! (concrete_1, concrete_2);
assert_ne! (concrete_2, concrete_bad);
assert_ne! (concrete_bad, concrete_1);
let dummy_1 = MyStruct {
file: AlwaysEqual::testing_blank (),
name: "my_struct",
};
let dummy_2 = MyStruct {
file: AlwaysEqual::testing_blank (),
name: "my_struct",
};
let dummy_bad = MyStruct {
file: AlwaysEqual::testing_blank (),
name: "not_my_struct",
};
assert_eq! (dummy_1, dummy_2);
assert_ne! (dummy_2, dummy_bad);
assert_ne! (dummy_bad, dummy_1);
assert_eq! (concrete_1, dummy_1);
assert_eq! (concrete_bad, dummy_bad);
}
#[test]
fn test_2 () {
let v1 = Vec::<AlwaysEqual <File>>::new ();
let v2 = Vec::<AlwaysEqual <File>>::new ();
assert_eq! (v1, v2);
}
}