From 96204dbf5b8e48f1797e32e0c3954b23d5c4ab8c Mon Sep 17 00:00:00 2001 From: Filippo Neysofu Costa Date: Sun, 27 Aug 2023 17:06:47 +0200 Subject: [PATCH] `Hash`, `std::iter` support and new `Error` type (#4) Signed-off-by: Filippo Costa --- .github/workflows/ci.yml | 25 +- Cargo.toml | 12 +- README.md | 65 ++- examples/f32_partial_eq_is_not_reflexive.rs | 8 + examples/proptest.rs | 32 ++ examples/quickcheck.rs | 27 ++ rust-toolchain.toml | 2 +- src/error.rs | 148 ++++++ src/invariants.rs | 340 ++++++++++++++ src/lib.rs | 476 +++++++------------- tests/arweave_ord.rs | 63 +++ tests/f23_cant_not_eq.rs | 4 + tests/hash.rs | 49 ++ tests/iterator.rs | 20 + 14 files changed, 938 insertions(+), 333 deletions(-) create mode 100644 examples/f32_partial_eq_is_not_reflexive.rs create mode 100644 examples/proptest.rs create mode 100644 examples/quickcheck.rs create mode 100644 src/error.rs create mode 100644 src/invariants.rs create mode 100644 tests/arweave_ord.rs create mode 100644 tests/f23_cant_not_eq.rs create mode 100644 tests/hash.rs create mode 100644 tests/iterator.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 078c646..e8c3d21 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] env: CARGO_TERM_COLOR: always @@ -13,10 +13,17 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - name: Build - run: cargo build --verbose - - name: Run cargo clippy - run: cargo clippy - - name: Run tests - run: cargo test --verbose + - uses: actions/checkout@v2 + - run: cargo check --verbose + - run: cargo clippy + - run: cargo test --verbose + - run: cargo test --examples + - run: cargo test --doc + msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: install cargo-binstall + run: curl -L --proto '=https' --tlsv1.2 -sSf https://github.com/raw/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash + - run: cargo binstall --version 0.15.1 --no-confirm cargo-msrv + - run: cargo msrv verify diff --git a/Cargo.toml b/Cargo.toml index 58ce59f..6d1f0c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,15 +1,19 @@ [package] name = "reltester" version = "1.0.1" -edition = "2018" +edition = "2021" repository = "https://github.com/neysofu/reltester" license = "MIT" +rust-version = "1.56" description = "Automatically verify the correctness of [Partial]Eq/Ord implementations" authors = ["Filippo Neysofu Costa "] [dependencies] -thiserror = "1.0.26" +rand = "0.8" +thiserror = "1" [dev-dependencies] -quickcheck = "1.0" -quickcheck_macros = "1.0" +quickcheck = "1" +quickcheck_macros = "1" +proptest = "1" +proptest-derive = "0.3" diff --git a/README.md b/README.md index 71094f7..925e19e 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # Reltester -[![Crates.io](https://img.shields.io/crates/l/reltester)](https://github.com/neysofu/reltester/blob/main/LICENSE.txt) [![docs.rs](https://img.shields.io/docsrs/reltester)](https://docs.rs/reltester/latest/reltester/) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/neysofu/reltester/ci.yml)](https://github.com/neysofu/reltester/actions) [![Crates.io](https://img.shields.io/crates/v/reltester)](https://crates.io/crates/reltester) [![min-rustc](https://img.shields.io/badge/min--rustc-1.53-blue)](https://github.com/neysofu/reltester/blob/main/rust-toolchain.toml) +[![Crates.io](https://img.shields.io/crates/l/reltester)](https://github.com/neysofu/reltester/blob/main/LICENSE.txt) [![docs.rs](https://img.shields.io/docsrs/reltester)](https://docs.rs/reltester/latest/reltester/) [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/neysofu/reltester/ci.yml)](https://github.com/neysofu/reltester/actions) [![Crates.io](https://img.shields.io/crates/v/reltester)](https://crates.io/crates/reltester) [![min-rustc](https://img.shields.io/badge/min--rustc-1.56-blue)](https://github.com/neysofu/reltester/blob/main/rust-toolchain.toml) -**Rel**ation **tester** is a small testing utility for automatically checking the correctness of `PartialEq`, `PartialOrd`, `Eq`, and `Ord` implementations. It's most useful when used in conjuction with [`quickcheck`](https://github.com/BurntSushi/quickcheck) or some other property-based testing framework. +**Rel**ation **tester** is a small testing utility for automatically checking the correctness of `[Partial]Eq`, `[Partial]Ord`, `Hash`, and `[DoubleEnded|Fused]Iterator` trait implementations. It's most useful when used in conjuction with [`quickcheck`](https://github.com/BurntSushi/quickcheck) or some other property-based testing framework. *Go to the [docs](https://docs.rs/reltester/latest/reltester/)!* @@ -11,23 +11,32 @@ Imagine a scenario where you have a type `Foo` with a custom implementation of either `PartialEq`, `Eq`, `PartialOrd`, or `Ord`. By "custom" we mean hand-written as opposed to derived. The Rust compiler alone cannot verify the correctness of these implementations and thus it is up to you, the programmer, to uphold certain invariants about the specific [binary relation](https://en.wikipedia.org/wiki/Binary_relation) that you're implementing. For example, if you implement `PartialEq` for `Foo`, you must guarantee that `foo1 == foo2` implies `foo2 == foo1` (*symmetry*). -This is what `reltester` is for. Rather than learning all subtle details of `PartialEq`, `Eq`, `PartialOrd`, and `Ord`, you can write some tests that will automatically check these invariants for you. +Other traits such as `Hash` and `Iterator` mandate several invariants as well – some of which are very intuitive, and [others](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) which are not. It's especially common for less-than-perfect implementations of the `std::iter` family of traits to introduce off-by-one bugs[^1][^2][^3][^4] among others. + +The idea is, instead of keeping these invariants in your head whenever you go about manually implementing one of these traits in your codebase, you can add a Reltester check to your test suite and have a higher degree of confidence that your implementation is correct. + ## How to use -1. Write some tests that generate random values of the type you wish to test. You can do this by hand or using crates such as [`quickcheck`](https://github.com/BurntSushi/quickcheck) and [`proptest`](https://github.com/proptest-rs/proptest). -2. Based on the traits that your type implements, call the appropriate checker: +1. Write some tests that generate random values of the type you wish to test. You can do this by hand or using crates such as [`quickcheck`](https://github.com/BurntSushi/quickcheck) and [`proptest`](https://github.com/proptest-rs/proptest). Calling the checkers on static, non-randomized values is possible but is less effective in catching bugs. +2. Based on the traits that your type implements, call the appropriate checker(s): - `reltester::eq` for `Eq`; - `reltester::ord` for `Ord`; - `reltester::partial_eq` for `PartialEq`; - - `reltester::partial_ord` for `PartialOrd`. + - `reltester::partial_ord` for `PartialOrd`; + - `reltester::hash` for `Hash`; + - `reltester::iterator` for `Iterator`; + - `reltester::fused_iterator` for `FusedIterator`; + - `reltester::double_ended_iterator` for `DoubleEndedIterator`; + + Some of these functions take multiple (two or three) values of the same type. This is because it takes up to three values to test some invariants. - All of these functions take three arguments of the same type: `a`, `b`, and `c`. This is because it takes up to three values to test some invariants. +Please refer to the documentation for more information. The `reltester::invariants` module is available for more granular checks if you can't satisfy the type bounds of the main functions. -Please refer to the documentation for more advanced use cases. +## Examples -# A small example +### `f32` (`PartialEq`, `PartialOrd`) ```rust use reltester; @@ -36,12 +45,48 @@ use quickcheck_macros::quickcheck; #[quickcheck] fn test_f32(a: f32, b: f32, c: f32) -> bool { // Let's check if `f32` implements `PartialEq` and `PartialOrd` correctly - // (spoiler: it does) + // (spoiler: it does). reltester::partial_eq(&a, &b, &c).is_ok() && reltester::partial_ord(&a, &b, &c).is_ok() } ``` +### `u32` (`Hash`) + +```rust +use reltester; +use quickcheck_macros::quickcheck; + +#[quickcheck] +fn test_u32(a: u32, b: u32) -> bool { + // Unlike `f32`, `u32` implements both `Eq` and `Hash`, which allows us to + // test `Hash` invariants. + reltester::hash(&a, &b).is_ok() +} +``` + +### `Vec` (`DoubleEndedIterator`, `FusedIterator`, `Iterator`) + +```rust +use reltester; +use quickcheck_macros::quickcheck; + +#[quickcheck] +fn test_vec_u32(nums: Vec) -> bool { + // `Iterator` is implied and checked by both `DoubleEndedIterator` and + // `FusedIterator`. + reltester::double_ended_iterator(nums.iter()).is_ok() + && reltester::fused_iterator(nums.iter()).is_ok() +} +``` + ## Legal Reltester is available under the terms of the MIT license. + +## External references and footnotes + +[^1]: https://github.com/rust-lang/rust/issues/41964 +[^2]: https://github.com/bevyengine/bevy/pull/7469 +[^3]: https://github.com/bluejekyll/trust-dns/issues/1638 +[^4]: https://github.com/sparsemat/sprs/issues/261 diff --git a/examples/f32_partial_eq_is_not_reflexive.rs b/examples/f32_partial_eq_is_not_reflexive.rs new file mode 100644 index 0000000..129ead4 --- /dev/null +++ b/examples/f32_partial_eq_is_not_reflexive.rs @@ -0,0 +1,8 @@ +//! Why can't `f32` be `Eq`? Here's a counterexample to show why: + +fn main() {} + +#[test] +fn f64_partial_eq_is_not_reflexive() { + assert!(reltester::invariants::eq_reflexivity(&f64::NAN).is_err()); +} diff --git a/examples/proptest.rs b/examples/proptest.rs new file mode 100644 index 0000000..cada957 --- /dev/null +++ b/examples/proptest.rs @@ -0,0 +1,32 @@ +fn main() {} + +#[cfg(test)] +mod tests { + use proptest::prelude::*; + use std::net::IpAddr; + + proptest! { + #[test] + fn correctness_u32(a: u32, b: u32, c: u32) { + reltester::eq(&a, &b, &c).unwrap(); + reltester::ord(&a, &b, &c).unwrap(); + } + + #[test] + fn correctness_f32(a: f32, b: f32, c: f32) { + reltester::partial_eq(&a, &b, &c).unwrap(); + reltester::partial_ord(&a, &b, &c).unwrap(); + } + + #[test] + fn correctness_ip_address(a: IpAddr, b: IpAddr, c: IpAddr) { + reltester::eq(&a, &b, &c).unwrap(); + reltester::ord(&a, &b, &c).unwrap(); + } + + #[test] + fn vec_u32_is_truly_double_ended(x: Vec) { + reltester::double_ended_iterator(x.iter()).unwrap(); + } + } +} diff --git a/examples/quickcheck.rs b/examples/quickcheck.rs new file mode 100644 index 0000000..a4c2c51 --- /dev/null +++ b/examples/quickcheck.rs @@ -0,0 +1,27 @@ +fn main() {} + +#[cfg(test)] +mod tests { + use quickcheck_macros::quickcheck; + use std::net::IpAddr; + + #[quickcheck] + fn correctness_u32(a: u32, b: u32, c: u32) -> bool { + reltester::eq(&a, &b, &c).is_ok() && reltester::ord(&a, &b, &c).is_ok() + } + + #[quickcheck] + fn correctness_f32(a: f32, b: f32, c: f32) -> bool { + reltester::partial_eq(&a, &b, &c).is_ok() && reltester::partial_ord(&a, &b, &c).is_ok() + } + + #[quickcheck] + fn correctness_ip_address(a: IpAddr, b: IpAddr, c: IpAddr) -> bool { + reltester::eq(&a, &b, &c).is_ok() && reltester::ord(&a, &b, &c).is_ok() + } + + #[quickcheck] + fn vec_u32_is_truly_double_ended(x: Vec) -> bool { + reltester::double_ended_iterator(x.iter()).is_ok() + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 8878a9b..7dd3f58 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.53" +channel = "1.70" # MSRV is not 1.70 but our dev-dependencies require a more recent rustc. profile = "default" diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..80f1383 --- /dev/null +++ b/src/error.rs @@ -0,0 +1,148 @@ +//! Crate error types. + +use thiserror::Error; + +/// Represents a broken invariant of [`PartialEq`]. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum PartialEqError { + /// [`PartialEq::ne`] *MUST* always return the negation of [`PartialEq::eq`]. + #[error("PartialEq::ne MUST always return the negation of PartialEq::eq")] + BadNe, + /// If `A: PartialEq` and `B: PartialEq`, then `a == b` *MUST* imply `b == a`. + #[error("a == b MUST imply b == a")] + BrokeSymmetry, + /// If `A: PartialEq` and `B: PartialEq` and `A: PartialEq`, then + /// `a == b && b == c` *MUST* imply `a == c`. + #[error("a == b && b == c MUST imply a == c")] + BrokeTransitivity, +} + +/// Represents a broken invariant of [`Eq`]. +/// +/// Note that [`Eq`] also mandates all invariants of [`PartialEq`]. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum EqError { + /// All values must be equal to themselves. + #[error("a == a MUST be true")] + BrokeReflexivity, +} + +/// Represents a broken invariant of [`PartialOrd`]. +/// +/// Note that [`PartialOrd`] also mandates all invariants of [`PartialEq`]. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum PartialOrdError { + /// [`PartialOrd::partial_cmp`] *MUST* return `Some(Ordering::Equal)` if + /// and only if [`PartialEq::eq`] returns [`true`]. + #[error("PartialOrd::partial_cmp MUST return Some(Ordering::Equal) if and only if PartialEq::eq returns true")] + BadPartialCmp, + /// [`PartialOrd::lt`] *MUST* return [`true`] + /// if and only if [`PartialOrd::partial_cmp`] returns `Some(Ordering::Less)`. + #[error("PartialOrd::lt MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Less)")] + BadLt, + /// [`PartialOrd::le`] *MUST* return [`true`] if and only if + /// [`PartialOrd::partial_cmp`] returns `Some(Ordering::Less)` or + /// [`Some(Ordering::Equal)`]. + #[error("PartialOrd::le MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Less) or Some(Ordering::Equal)")] + BadLe, + /// [`PartialOrd::gt`] *MUST* return [`true`] if and only if + /// [`PartialOrd::partial_cmp`] returns `Some(Ordering::Greater)`. + #[error("PartialOrd::gt MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Greater)")] + BadGt, + /// [`PartialOrd::ge`] *MUST* return [`true`] if and only if + /// [`PartialOrd::partial_cmp`] returns `Some(Ordering::Greater)` or + /// `Some(Ordering::Equal)`. + #[error("PartialOrd::ge MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Greater) or Some(Ordering::Equal)")] + BadGe, + /// If `a > b`, then `b < a` *MUST* be true. + #[error("If a > b, then b < a MUST be true")] + BrokeDuality, + /// If `a > b` and `b > c`, then `a > c` *MUST* be true. The same must hold true for `<`. + #[error("If a > b and b > c, then a > c MUST be true. The same must hold true for <")] + BrokeTransitivity, +} + +/// Represents a broken invariant of [`Ord`]. +/// +/// Note that [`Ord`] also mandates all invariants of [`PartialOrd`] and [`Eq`]. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum OrdError { + /// [`Ord::cmp`] *MUST* always return `Some(PartialOrd::partial_cmp())`. + #[error("`cmp` and `partial_cmp` are not consistent")] + BadCmp, + /// [`Ord::cmp`] and [`Ord::max`] are not consistent. + #[error("`cmp` and `max` are not consistent")] + BadMax, + /// [`Ord::cmp`] and [`Ord::min`] are not consistent. + #[error("`cmp` and `min` are not consistent")] + BadMin, + /// [`Ord::cmp`] and [`Ord::clamp`] are not consistent. + #[error("`cmp` and `clamp` are not consistent")] + BadClamp, +} + +/// Represents a broken invariant of [`Hash`]. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum HashError { + /// Equal values *MUST* have equal hash values. + #[error("Equal values MUST have equal hash values")] + EqualButDifferentHashes, + /// When two values are different (as defined by [`PartialEq::ne`]), neither + /// of the two hash outputs can be a prefix of the other. See + /// + /// for more information. + #[error("When two values are different, one of the two hash outputs CAN NOT be a prefix of the other")] + PrefixCollision, +} + +/// Represents a broken invariant of [`Iterator`]. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum IteratorError { + /// [`Iterator::size_hint`] *MUST* always provide correct lower and upper + /// bounds. + #[error("Iterator::size_hint MUST always provide correct lower and upper bounds")] + BadSizeHint, + /// [`Iterator::count`] *MUST* be consistent with the actual number of + /// elements returned by [`Iterator::next`]. + #[error( + "Iterator::count MUST be consistent with the actual number of elements returned by .next()" + )] + BadCount, + /// [`Iterator::last`] *MUST* be equal to the last element of the + /// [`Vec`] resulting from [`Iterator::collect`]. + #[error(".last() MUST be equal to the last element of the Vec<_> resulting from .collect()")] + BadLast, + /// [`DoubleEndedIterator::next_back`] *MUST* return the same values as + /// [`Iterator::next`], just in reverse order, and it MUST NOT return + /// different values. + #[error("DoubleEndedIterator::next_back() MUST return the same values as .next(), but in reverse order")] + BadNextBack, + /// [`FusedIterator`](core::iter::FusedIterator) *MUST* return [`None`] + /// indefinitely after exhaustion. + #[error("FusedIterator MUST return None indefinitely after exhaustion")] + FusedIteratorReturnedSomeAfterExhaustion, +} + +/// The crate error type. +#[derive(Error, Debug, Clone)] +#[non_exhaustive] +pub enum Error { + #[error(transparent)] + PartialEq(#[from] PartialEqError), + #[error(transparent)] + Eq(#[from] EqError), + #[error(transparent)] + PartiaOrd(#[from] PartialOrdError), + #[error(transparent)] + Ord(#[from] OrdError), + #[error(transparent)] + Hash(#[from] HashError), + #[error(transparent)] + Iterator(#[from] IteratorError), +} diff --git a/src/invariants.rs b/src/invariants.rs new file mode 100644 index 0000000..6d5107b --- /dev/null +++ b/src/invariants.rs @@ -0,0 +1,340 @@ +//! Granular checkers for specific trait invariants. Only use these if you +//! implement [`PartialEq`] and [`PartialOrd`] with a non-`Self` type parameter +//! and you can't satisfy the type bounds of the main helper functions. + +use std::{ + cmp::{max_by, min_by, Ordering}, + hash::{Hash, Hasher}, + iter::FusedIterator, +}; + +use crate::error::*; + +/// Checks that [`PartialEq::eq`] and [`PartialEq::ne`] are strict inverses. +/// +/// This is guaranteed by default method implementations but may be broken +/// by non-default method implementations. +pub fn partial_eq_methods_consistency(a: &A, b: &B) -> Result<(), PartialEqError> +where + A: PartialEq, +{ + if (a == b) != !(a != b) { + return Err(PartialEqError::BadNe); + } + + Ok(()) +} + +/// Checks that [`PartialEq`] is a +/// [symmetric relation](https://en.wikipedia.org/wiki/Symmetric_relation). +pub fn partial_eq_symmetry(a: &A, b: &B) -> Result<(), PartialEqError> +where + A: PartialEq, + B: PartialEq, +{ + if (a == b) != (b == a) { + return Err(PartialEqError::BrokeSymmetry); + } + + Ok(()) +} + +/// Checks that [`PartialEq`] is a [transitive +/// relation](https://en.wikipedia.org/wiki/Transitive_relation). +pub fn partial_eq_transitivity(a: &A, b: &B, c: &C) -> Result<(), PartialEqError> +where + A: PartialEq + PartialEq, + B: PartialEq, +{ + if a == b && b == c && a != c { + return Err(PartialEqError::BrokeTransitivity); + } + + Ok(()) +} + +/// Checks that [`PartialEq`] is a [reflexive +/// relation](https://en.wikipedia.org/wiki/Reflexive_relation). +/// +/// Note that [`PartialEq`] alone does **not** require reflexivity, [`Eq`] +/// does. +pub fn eq_reflexivity(a: &A) -> Result<(), PartialEqError> +where + A: PartialEq, +{ + if a != a { + return Err(PartialEqError::BrokeTransitivity); + } + + Ok(()) +} + +/// Checks that [`PartialOrd`] methods are implemented consistently with +/// each other. +/// +/// This is guaranteed by default method implementations but may be broken +/// by non-default method implementations. +pub fn partial_ord_methods_consistency(a: &A, b: &B) -> Result<(), PartialOrdError> +where + A: PartialOrd, +{ + if (a == b) != (a.partial_cmp(b) == Some(Ordering::Equal)) { + return Err(PartialOrdError::BadPartialCmp); + } + if (a < b) != (a.partial_cmp(b) == Some(Ordering::Less)) { + return Err(PartialOrdError::BadLt); + } + if (a > b) != (a.partial_cmp(b) == Some(Ordering::Greater)) { + return Err(PartialOrdError::BadGt); + } + if (a <= b) != ((a < b) || (a == b)) { + return Err(PartialOrdError::BadLe); + } + if (a >= b) != ((a > b) || (a == b)) { + return Err(PartialOrdError::BadGe); + } + + Ok(()) +} + +/// Checks that [`PartialOrd`] respects +/// [duality](https://en.wikipedia.org/wiki/Duality_(order_theory)) (i.e. `a +/// > b` iff `b < a`). +pub fn partial_ord_duality(a: &A, b: &B) -> Result<(), PartialOrdError> +where + A: PartialOrd, + B: PartialOrd, +{ + if ((a < b) != (b > a)) && ((a > b) != (b < a)) { + return Err(PartialOrdError::BrokeDuality); + } + + Ok(()) +} + +/// Checks that [`PartialOrd`] is a [transitive +/// relation](https://en.wikipedia.org/wiki/Transitive_relation). +pub fn partial_ord_transitivity(a: &A, b: &B, c: &C) -> Result<(), PartialOrdError> +where + A: PartialOrd + PartialOrd, + B: PartialOrd, +{ + if a < b && b < c && !(a < c) { + return Err(PartialOrdError::BrokeTransitivity); + } + if a > b && b > c && !(a > c) { + return Err(PartialOrdError::BrokeTransitivity); + } + + Ok(()) +} + +/// Checks that [`Ord`] methods are implemented consistently with each other. +/// +/// This is guaranteed by default method implementations but may be broken +/// by non-default method implementations. +pub fn ord_methods_consistency(a: &T, b: &T, c: &T) -> Result<(), OrdError> +where + T: Ord, +{ + if a.partial_cmp(b) != Some(a.cmp(b)) { + return Err(OrdError::BadCmp); + } + if a.max(b) != max_by(a, b, |x, y| x.cmp(y)) { + return Err(OrdError::BadMax); + } + if a.min(b) != min_by(a, b, |x, y| x.cmp(y)) { + return Err(OrdError::BadMin); + } + + // clamp + let min = b.min(c); + let max = b.max(c); + let clamped = a.clamp(min, max); + if clamped < min || clamped > max { + return Err(OrdError::BadClamp); + } + + Ok(()) +} + +/// Checks that the output of [`Hash`] is the same for equal values, and +/// different for different values. +/// +/// See what the `std` +/// [docs](https://doc.rust-lang.org/std/hash/trait.Hash.html#hash-and-eq) have +/// to say about this invariant. +pub fn hash_consistency_with_eq(a: &K, b: &K) -> Result<(), HashError> +where + K: Hash + Eq + ?Sized, +{ + let hasher_output_equality = hasher_output(a) == hasher_output(b); + let equality = a == b; + + if hasher_output_equality != equality { + return Err(HashError::EqualButDifferentHashes); + } + + Ok(()) +} + +/// Checks that neither of the outputs of [`Hash`] of two different values is a +/// prefix of the other. +/// +/// See what the `std` +/// [docs](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) have +/// to say about this invariant. +pub fn hash_prefix_collision(a: &K, b: &K) -> Result<(), HashError> +where + K: Hash + Eq + ?Sized, +{ + if a != b { + let hasher_output_a = hasher_output(a); + let hasher_output_b = hasher_output(b); + + if hasher_output_a.starts_with(&hasher_output_b) + || hasher_output_b.starts_with(&hasher_output_a) + { + return Err(HashError::PrefixCollision); + } + } + + Ok(()) +} + +/// Checks that [`Iterator::size_hint`] provides correct lower and upper bounds +/// which are consistent with the true value of [`Iterator::count`]. +pub fn iterator_size_hint(iter: I) -> Result<(), IteratorError> +where + I: Iterator, +{ + let size_hint = iter.size_hint(); + let count = iter.count(); + + if size_hint.0 > count { + return Err(IteratorError::BadSizeHint); + } else if let Some(upper_bound) = size_hint.1 { + if upper_bound < count { + return Err(IteratorError::BadSizeHint); + } + } + + Ok(()) +} + +/// Checks that [`Iterator::count`] returns the same value as the length of the +/// [`Vec`] obtained from [`Iterator::collect`]. +pub fn iterator_count(iter: I) -> Result<(), IteratorError> +where + I: Iterator + Clone, +{ + let count = iter.clone().count(); + let collected = iter.collect::>(); + + if count != collected.len() { + return Err(IteratorError::BadCount); + } + + Ok(()) +} + +/// Checks that [`Iterator::last`] returns the same value as the last element of +/// the [`Vec`] obtained from [`Iterator::collect`]. +pub fn iterator_last(iter: I) -> Result<(), IteratorError> +where + I: Iterator + Clone, + I::Item: PartialEq, +{ + let last = iter.clone().last(); + let collected = iter.collect::>(); + + if last.as_ref() != collected.last() { + return Err(IteratorError::BadLast); + } + + Ok(()) +} + +/// Checks that alternating random calls to [`Iterator::next`] and +/// [`DoubleEndedIterator::next_back`] results in the same sequence as the +/// [`Vec`] obtained from [`Iterator::collect`]. +pub fn double_ended_iterator_next_back(mut iter: I) -> Result<(), IteratorError> +where + I: DoubleEndedIterator + Clone, + I::Item: PartialEq, +{ + let collected = iter.clone().collect::>(); + + let mut from_start = vec![]; + let mut from_end = vec![]; + loop { + if rand::random() { + if let Some(item) = iter.next() { + from_start.push(item); + } else { + break; + } + } else { + if let Some(item) = iter.next_back() { + from_end.push(item); + } else { + break; + } + } + } + + let assembled = from_start + .into_iter() + .chain(from_end.into_iter().rev()) + .collect::>(); + + if assembled != collected { + return Err(IteratorError::BadNextBack); + } + + Ok(()) +} + +/// Checks that [`FusedIterator`] returns [`None`] for a large number of times after +/// returning [`None`] for the first time. +pub fn fused_iterator_none_forever(mut iter: I) -> Result<(), IteratorError> +where + I: FusedIterator + Clone, +{ + let mut count = 0; + while iter.next().is_some() { + count += 1; + } + + // How many times does it make sense to keep going to have decent confidence + // it will return `None` forever? Hard to say. I'm going with .count() + 1 + // in case the iterator "goes back" or something. + for _ in 0..count + 1 { + if iter.next().is_some() { + return Err(IteratorError::FusedIteratorReturnedSomeAfterExhaustion); + } + } + + Ok(()) +} + +fn hasher_output(item: &K) -> Vec +where + K: Hash + ?Sized, +{ + struct NoHasher(Vec); + + impl Hasher for NoHasher { + fn finish(&self) -> u64 { + 0 + } + + fn write(&mut self, bytes: &[u8]) { + self.0.extend_from_slice(bytes); + } + } + + let mut hasher = NoHasher(vec![]); + item.hash(&mut hasher); + hasher.0 +} diff --git a/src/lib.rs b/src/lib.rs index b4ea347..cdd2509 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,49 +1,70 @@ -//! **Rel**ation **tester** is a small testing utility for automatically checking the correctness of -//! [`PartialEq`], [`Eq`], -//! [`PartialOrd`], and [`Ord`] implementations. It's most useful when used in -//! conjuction with [`quickcheck`](https://github.com/BurntSushi/quickcheck) or -//! some other property-based testing framework. +//! **Rel**ation **tester** is a small testing utility for automatically +//! checking the correctness of `[Partial]Eq`, `[Partial]Ord`, `Hash`, and +//! `[DoubleEnded|Fused]Iterator` trait implementations. It's most useful when +//! used in conjuction with +//! [`quickcheck`](https://github.com/BurntSushi/quickcheck) or some other +//! property-based testing framework. //! //! # Rationale //! -//! Imagine a scenario where you have a type `Foo` with a custom implementation of either [`PartialEq`], [`Eq`], -//! [`PartialOrd`], or [`Ord`]. By "custom" we mean hand-written as opposed to -//! derived. The Rust compiler alone cannot verify the correctness of these -//! implementations and thus it is up to you, the programmer, to uphold certain -//! invariants about the specific [binary +//! Imagine a scenario where you have a type `Foo` with a custom implementation +//! of either [`PartialEq`], [`Eq`], [`PartialOrd`], or [`Ord`]. By "custom" we mean +//! hand-written as opposed to derived. The Rust compiler alone cannot verify +//! the correctness of these implementations and thus it is up to you, the +//! programmer, to uphold certain invariants about the specific [binary //! relation](https://en.wikipedia.org/wiki/Binary_relation) that you're -//! implementing. For example, if you implement [`PartialEq`] for `Foo`, you -//! must guarantee that `foo1 == foo2` implies `foo2 == foo1` (*symmetry*). +//! implementing. For example, if you implement [`PartialEq`] for `Foo`, you must +//! guarantee that `foo1 == foo2` implies `foo2 == foo1` (*symmetry*). //! -//! This is what `reltester` is for. Rather than learning all subtle details of [`PartialEq`], [`Eq`], -//! [`PartialOrd`], and [`Ord`], you can write some tests that will -//! automatically check these invariants for you. +//! Other traits such as [`Hash`] and [`Iterator`] mandate their own invariants as +//! well – some of which are very intuitive, and +//! [others](https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions) +//! which are not. It's especially common for less-than-perfect implementations +//! of the [`std::iter`] family of traits to introduce off-by-one +//! bugs[^1] [^2] [^3] [^4] among others. +//! +//! The idea is, instead of keeping these invariants in your head whenever you +//! go about manually implementing one of these traits in your codebase, you can +//! add a Reltester check to your test suite and have a higher degree of +//! confidence that your implementation is correct. //! //! # How to use //! //! 1. Write some tests that generate random values of the type you wish to -//! test. You can do this by hand or using crates such as -//! [`quickcheck`](https://github.com/BurntSushi/quickcheck) and -//! [`proptest`](https://github.com/proptest-rs/proptest). -//! 2. Based on the traits that your type implements, call the appropriate checker: +//! test. You can do this by hand or using crates such as +//! [`quickcheck`](https://github.com/BurntSushi/quickcheck) and +//! [`proptest`](https://github.com/proptest-rs/proptest). Calling the checkers +//! on static, non-randomized values is possible but is less effective in +//! catching bugs. +//! 2. Based on the traits that your type implements, call the appropriate checker(s): //! //! - [`reltester::eq`](eq) for [`Eq`]; //! - [`reltester::ord`](ord) for [`Ord`]; //! - [`reltester::partial_eq`](partial_eq) for [`PartialEq`]; -//! - [`reltester::partial_ord`](partial_ord) for [`PartialOrd`]. +//! - [`reltester::partial_ord`](partial_ord) for [`PartialOrd`]; +//! - [`reltester::hash`](hash) for [`Hash`]; +//! - [`reltester::iterator`](iterator) for [`Iterator`]; +//! - [`reltester::fused_iterator`](fused_iterator) for [`FusedIterator`]; +//! - [`reltester::double_ended_iterator`](double_ended_iterator) for [`DoubleEndedIterator`]; +//! +//! Some of these functions take multiple (two or three) values of the same +//! type. This is because it takes up to three values to test some +//! invariants. //! -//! All of these functions take three arguments of the same type: `a`, `b`, and -//! `c`. This is because it takes up to three values to test some invariants. +//! The [`reltester::invariants`](invariants) module is available for more +//! granular checks if you can't satisfy the type bounds of the main functions. //! //! ## Multi-type relations: `Foo: PartialEq` and `Foo: PartialOrd` //! //! In some cases your [`PartialEq`] and [`PartialOrd`] implementations //! may use a non-`Self` type parameter. (Note: [`Eq`] and [`Ord`] don't accept -//! type parameters and this use case doesn't apply to them.) `reltester` +//! type parameters and this use case doesn't apply to them.) Reltester //! supports this use case and exposes granular invariant checking functions in //! the [`invariants`] module with more lax type constraints. //! -//! # Examples +//! ## Examples +//! +//! ### `f32` (`PartialEq`, `PartialOrd`) //! //! ```rust //! use reltester; @@ -52,19 +73,53 @@ //! #[quickcheck] //! fn test_f32(a: f32, b: f32, c: f32) -> bool { //! // Let's check if `f32` implements `PartialEq` and `PartialOrd` correctly -//! // (spoiler: it does) +//! // (spoiler: it does). //! reltester::partial_eq(&a, &b, &c).is_ok() //! && reltester::partial_ord(&a, &b, &c).is_ok() //! } //! ``` //! -//! # TL;DR invariants +//! ### `u32` (`Hash`) +//! +//! ```rust +//! use reltester; +//! use quickcheck_macros::quickcheck; +//! +//! #[quickcheck] +//! fn test_u32(a: u32, b: u32) -> bool { +//! // Unlike `f32`, `u32` implements both `Eq` and `Hash`, which allows us to +//! // test `Hash` invariants. +//! reltester::hash(&a, &b).is_ok() +//! } +//! ``` +//! +//! ### `Vec` (`DoubleEndedIterator`, `FusedIterator`, `Iterator`) +//! +//! ```rust +//! use reltester; +//! use quickcheck_macros::quickcheck; +//! +//! #[quickcheck] +//! fn test_vec_u32(nums: Vec) -> bool { +//! // `Iterator` is implied and checked by both `DoubleEndedIterator` and +//! // `FusedIterator`. +//! reltester::double_ended_iterator(nums.iter()).is_ok() +//! && reltester::fused_iterator(nums.iter()).is_ok() +//! } +//! ``` +//! +//! # TL;DR invariants of the comparison traits //! //! Chances are you don't need to concern yourself with the mathematical definitions of -//! comparison traits, as long as your implementations are sensible. They are -//! listed here only for the sake of completeness. +//! comparison traits; as long as your implementations are sensible and your +//! `reltester` tests pass, you can move on and assume your implementations are +//! correct. The required invariants are listed here only for the sake of +//! completeness. //! -//! - [`PartialEq`] requires **symmetry** and **transitivity** of `==` ([partial equivalence relation](https://en.wikipedia.org/wiki/Partial_equivalence_relation)). +//! - [`PartialEq`] requires **symmetry** and **transitivity** of `==` whenever applicable ([partial +//! equivalence +//! relation](https://en.wikipedia.org/wiki/Partial_equivalence_relation) in the +//! case of `Rhs == Self`). //! - [`Eq`] requires **symmetry**, **transitivity**, and **reflexivity** of `==` ([equivalence relation](https://en.wikipedia.org/wiki/Equivalence_relation)). //! - [`PartialOrd`] requires **symmetry** of `==`, **transitivity** of `>`, //! `==`, and `<`; and **duality** of `>` and `<`. Note that duality is not @@ -75,70 +130,37 @@ //! though it's generally understood to mean [strict partial //! order](https://en.wikipedia.org/wiki/Partially_ordered_set#Strict_partial_orders). //! - [`Ord`] requires **symmetry** and **reflexivity** of `==`; **transitivity** of `>`, `==`, and `<`; and **duality** of `>` and `<`. -//! `==`; **transitivity** and **duality** of `>` and `<`; and must be **trichotomous**[^1]. Just like +//! `==`; **transitivity** and **duality** of `>` and `<`; and must be **trichotomous**[^5]. Just like //! [`PartialOrd`], the mathematical definition of [`Ord`] is a bit open to //! interpretation, though it's generally understood to mean [total //! order](https://en.wikipedia.org/wiki/Total_order#Strict_and_non-strict_total_orders). //! -//! [^1]: Trichotomy is a corollary that follows from the definitions of `>`, -//! `==`, and `<` based on [`Ordering`]. +//! In addition to the above, trait method default implementation overrides (for e.g. +//! [`PartialOrd::lt`] or [`Ord::max`]) must have the same behavior as the +//! default implementations. `reltester` always checks these for you. +//! +//! +//! [^1]: +//! +//! [^2]: +//! +//! [^3]: +//! +//! [^4]: +//! +//! [^5]: Trichotomy is a corollary that follows from the definitions of `>`, +//! `==`, and `<` based on [`Ordering`](std::cmp::Ordering). #![allow(clippy::eq_op, clippy::double_comparisons)] -use std::cmp::{max_by, min_by, Ordering}; -use thiserror::Error; +pub mod error; +pub mod invariants; -/// Represents a broken invariant of a tested trait implementation. -#[derive(Error, Debug)] -#[non_exhaustive] -pub enum Error { - /// [`PartialEq::eq`] and [`PartialEq::ne`] are not consistent. - #[error("`eq` and `ne` are not consistent")] - NotConsistentEqNe, - /// [`PartialOrd::partial_cmp`] and [`PartialEq::eq`] are not consistent. - #[error("`partial_cmp` and `eq` are not consistent")] - NotConsistentPartialCmpEq, - /// [`PartialOrd::partial_cmp`] and [`PartialOrd::lt`] are not consistent. - #[error("`partial_cmp` and `lt` are not consistent")] - NotConsistentPartialCmpLt, - /// [`PartialOrd::partial_cmp`] and [`PartialOrd::le`] are not consistent. - #[error("`partial_cmp` and `le` are not consistent")] - NotConsistentPartialCmpLe, - /// [`PartialOrd::partial_cmp`] and [`PartialOrd::gt`] are not consistent. - #[error("`partial_cmp` and `gt` are not consistent")] - NotConsistentPartialCmpGt, - /// [`PartialOrd::partial_cmp`] and [`PartialOrd::ge`] are not consistent. - #[error("`partial_cmp` and `ge` are not consistent")] - NotConsistentPartialCmpGe, - /// [`Ord::cmp`] and [`PartialOrd::partial_cmp`] are not consistent. - #[error("`cmp` and `partial_cmp` are not consistent")] - NotConsistentCmpPartialCmp, - /// [`Ord::cmp`] and [`Ord::max`] are not consistent. - #[error("`cmp` and `max` are not consistent")] - NotConsistentCmpMax, - /// [`Ord::cmp`] and [`Ord::min`] are not consistent. - #[error("`cmp` and `min` are not consistent")] - NotConsistentCmpMin, - /// [`Ord::cmp`] and [`Ord::clamp`] are not consistent. - #[error("`cmp` and `clamp` are not consistent")] - NotConsistentCmpClamp, - /// Reflexivity is broken in this [`Eq`] implementation. - #[error("`Eq` is not reflexive")] - BrokeReflexivity, - /// Symmetry is broken in this [`PartialEq`] implementation. - #[error("`PartialEq` is not symmetric")] - BrokeSymmetry, - /// Transitivity is broken in this [`PartialEq`] or [`PartialOrd`] - /// implementation. - #[error("`PartialEq` or `PartialOrd` is not transitive")] - BrokeTransitivity, - /// Duality is broken in this [`PartialOrd`] implementation. - #[error("`PartialOrd` is not dual")] - BrokeDuality, -} +use error::*; +use std::{hash::Hash, iter::FusedIterator}; -/// Checks the correctness of the [`Ord`] trait (and [`PartialOrd`] by extension) -/// for some values. +/// Checks the correctness of the [`Ord`] trait (and [`Eq`] and [`PartialOrd`] +/// by extension) for some values. pub fn ord(a: &T, b: &T, c: &T) -> Result<(), Error> where T: Ord, @@ -151,7 +173,8 @@ where Ok(()) } -/// Checks the correctness of the [`PartialOrd`] trait for some values. +/// Checks the correctness of the [`PartialOrd`] trait (and [`PartialEq`] by +/// extension) for some values. pub fn partial_ord(a: &T, b: &T, c: &T) -> Result<(), Error> where T: PartialOrd, @@ -167,13 +190,18 @@ where /// Checks the correctness of the [`Eq`] trait (and [`PartialEq`] by extension) /// for some values. +/// +/// The type bound is intentionally [`PartialEq`] instead of [`Eq`] to allow +/// for negative testing, i.e. ensuring that your [`PartialEq`] implementor +/// *doesn't* implement [`Eq`] when it shouldn't. pub fn eq(a: &T, b: &T, c: &T) -> Result<(), Error> where - T: Eq, + T: PartialEq, { partial_eq(a, b, c)?; - // `Eq` is just like `PartialEq`, except it also requires reflexivity. + // Checking `Eq` is the same as checking `PartialEq`, except it also + // requires reflexivity. invariants::eq_reflexivity(a)?; Ok(()) @@ -181,7 +209,7 @@ where /// Checks the correctness of the [`PartialEq`] trait /// for some values. -pub fn partial_eq(a: &T, b: &T, c: &T) -> Result<(), Error> +pub fn partial_eq(a: &T, b: &T, c: &T) -> Result<(), PartialEqError> where T: PartialEq, { @@ -192,235 +220,65 @@ where Ok(()) } -/// Granular checkers for specific trait invariants. Only use these if you -/// implement [`PartialEq`] and [`PartialOrd`] with a non-`Self` type parameter. -pub mod invariants { - use super::*; - - /// Checks that [`PartialEq::eq`] and [`PartialEq::ne`] are strict inverses. - /// - /// This is guaranteed by default method implementations but may be broken - /// by non-default method implementations. - pub fn partial_eq_methods_consistency(a: &A, b: &B) -> Result<(), Error> - where - A: PartialEq, - { - if (a == b) != !(a != b) { - return Err(Error::NotConsistentEqNe); - } - - Ok(()) - } - - /// Checks that [`PartialEq`] is a - /// [symmetric relation](https://en.wikipedia.org/wiki/Symmetric_relation). - pub fn partial_eq_symmetry(a: &A, b: &B) -> Result<(), Error> - where - A: PartialEq, - B: PartialEq, - { - if (a == b) != (b == a) { - return Err(Error::BrokeSymmetry); - } - - Ok(()) - } - - /// Checks that [`PartialEq`] is a [transitive - /// relation](https://en.wikipedia.org/wiki/Transitive_relation). - pub fn partial_eq_transitivity(a: &A, b: &B, c: &C) -> Result<(), Error> - where - A: PartialEq + PartialEq, - B: PartialEq, - { - if a == b && b == c && a != c { - return Err(Error::BrokeTransitivity); - } - - Ok(()) - } - - /// Checks that [`PartialEq`] is a [reflexive - /// relation](https://en.wikipedia.org/wiki/Reflexive_relation). - /// - /// Note that [`PartialEq`] alone does **not** require reflexivity, [`Eq`] does. - pub fn eq_reflexivity(a: &A) -> Result<(), Error> - where - A: PartialEq, - { - if a != a { - return Err(Error::BrokeReflexivity); - } - - Ok(()) - } - - /// Checks that [`PartialOrd`] methods are implemented consistently with each other. - /// - /// This is guaranteed by default method implementations but may be broken - /// by non-default method implementations. - pub fn partial_ord_methods_consistency(a: &A, b: &B) -> Result<(), Error> - where - A: PartialOrd, - { - partial_eq_methods_consistency(a, b)?; - - if (a == b) != (a.partial_cmp(b) == Some(Ordering::Equal)) { - return Err(Error::NotConsistentPartialCmpEq); - } - if (a < b) != (a.partial_cmp(b) == Some(Ordering::Less)) { - return Err(Error::NotConsistentPartialCmpLt); - } - if (a > b) != (a.partial_cmp(b) == Some(Ordering::Greater)) { - return Err(Error::NotConsistentPartialCmpGt); - } - if (a <= b) != ((a < b) || (a == b)) { - return Err(Error::NotConsistentPartialCmpLe); - } - if (a >= b) != ((a > b) || (a == b)) { - return Err(Error::NotConsistentPartialCmpGe); - } - - Ok(()) - } - - /// TODO - pub fn partial_ord_duality(a: &A, b: &B) -> Result<(), Error> - where - A: PartialOrd, - B: PartialOrd, - { - if ((a < b) != (b > a)) && ((a > b) != (b < a)) { - return Err(Error::BrokeDuality); - } - - Ok(()) - } - - /// Checks that [`PartialOrd`] is a [transitive - /// relation](https://en.wikipedia.org/wiki/Transitive_relation). - pub fn partial_ord_transitivity(a: &A, b: &B, c: &C) -> Result<(), Error> - where - A: PartialOrd + PartialOrd, - B: PartialOrd, - { - partial_eq_transitivity(a, b, c)?; - - if a < b && b < c && !(a < c) { - return Err(Error::BrokeTransitivity); - } - if a > b && b > c && !(a > c) { - return Err(Error::BrokeTransitivity); - } - - Ok(()) - } - - /// Checks that [`Ord`] methods are implemented consistently with each other. - /// - /// This is guaranteed by default method implementations but may be broken - /// by non-default method implementations. - pub fn ord_methods_consistency(a: &T, b: &T, _c: &T) -> Result<(), Error> - where - T: Ord, - { - if a.partial_cmp(b) != Some(a.cmp(b)) { - return Err(Error::NotConsistentCmpPartialCmp); - } - if a.max(b) != max_by(a, b, |x, y| x.cmp(y)) { - return Err(Error::NotConsistentCmpMax); - } - if a.min(b) != min_by(a, b, |x, y| x.cmp(y)) { - return Err(Error::NotConsistentCmpMin); - } - // TODO: clamp +/// Checks the correctness of the [`Hash`] trait in relation to [`Eq`] for some +/// values. +pub fn hash(a: &K, b: &K) -> Result<(), HashError> +where + K: Hash + Eq + ?Sized, +{ + invariants::hash_consistency_with_eq(a, b)?; + invariants::hash_prefix_collision(a, b)?; - Ok(()) - } + Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - use quickcheck::{Arbitrary, Gen}; - use quickcheck_macros::quickcheck; - use std::net::IpAddr; - - #[quickcheck] - fn correctness_u32(a: u32, b: u32, c: u32) -> bool { - eq(&a, &b, &c).is_ok() && ord(&a, &b, &c).is_ok() - } - - #[quickcheck] - fn correctness_f32(a: f32, b: f32, c: f32) -> bool { - partial_eq(&a, &b, &c).is_ok() && partial_ord(&a, &b, &c).is_ok() - } - - #[test] - fn f64_not_eq() { - assert!(invariants::eq_reflexivity(&f64::NAN).is_err()); - } - - #[quickcheck] - fn correctness_ip_address(a: IpAddr, b: IpAddr, c: IpAddr) -> bool { - eq(&a, &b, &c).is_ok() && ord(&a, &b, &c).is_ok() - } - - #[derive(Clone, Debug)] - pub enum ArweaveTrigger { - Block(u32), - Transaction(u32), - } - - /// Dumb counter-example that I took from https://github.com/graphprotocol/graph-node. - #[quickcheck] - #[should_panic] - fn arweave_is_incorrect(a: ArweaveTrigger, b: ArweaveTrigger, c: ArweaveTrigger) -> bool { - impl Arbitrary for ArweaveTrigger { - fn arbitrary(g: &mut Gen) -> Self { - if bool::arbitrary(g) { - ArweaveTrigger::Block(u32::arbitrary(g)) - } else { - ArweaveTrigger::Transaction(u32::arbitrary(g)) - } - } - } +/// Checks the correctness of the [`Iterator`] trait for some value `iter`. +/// +/// Note that `iter` must be a finite iterator. +pub fn iterator(iter: I) -> Result<(), IteratorError> +where + I: Iterator + Clone, + I::Item: PartialEq, +{ + invariants::iterator_size_hint(iter.clone())?; + invariants::iterator_count(iter.clone())?; + invariants::iterator_last(iter)?; - impl PartialEq for ArweaveTrigger { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::Block(a_ptr), Self::Block(b_ptr)) => a_ptr == b_ptr, - (Self::Transaction(a_tx), Self::Transaction(b_tx)) => a_tx == b_tx, - _ => false, - } - } - } + Ok(()) +} - impl Eq for ArweaveTrigger {} +/// Checks the correctness of the [`DoubleEndedIterator`] trait (and +/// [`Iterator`] by extension) for some value `iter`. +/// +/// Note that `iter` must be a finite iterator. +pub fn double_ended_iterator(iter: I) -> Result<(), IteratorError> +where + I: DoubleEndedIterator + Clone, + I::Item: PartialEq, +{ + iterator(iter.clone())?; - impl PartialOrd for ArweaveTrigger { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } - } + invariants::double_ended_iterator_next_back(iter)?; - impl Ord for ArweaveTrigger { - fn cmp(&self, other: &Self) -> Ordering { - match (self, other) { - // Keep the order when comparing two block triggers - (Self::Block(..), Self::Block(..)) => Ordering::Equal, + Ok(()) +} - // Block triggers always come last - (Self::Block(..), _) => Ordering::Greater, - (_, Self::Block(..)) => Ordering::Less, +/// Checks the correctness of the [`FusedIterator`] trait (and +/// [`Iterator`] by extension) for some value `iter`. +/// +/// Note that `iter` must be a finite iterator. +pub fn fused_iterator(iter: I) -> Result<(), IteratorError> +where + I: FusedIterator + Clone, + I::Item: PartialEq, +{ + iterator(iter.clone())?; - // Execution outcomes have no intrinsic ordering information so we keep the order in - // which they are included in the `txs` field of `Block`. - (Self::Transaction(..), Self::Transaction(..)) => Ordering::Equal, - } - } - } + invariants::fused_iterator_none_forever(iter)?; - ord(&a, &b, &c).is_ok() - } + Ok(()) } + +#[allow(dead_code)] +#[doc = include_str!("../README.md")] +struct ReadmeDoctest; diff --git a/tests/arweave_ord.rs b/tests/arweave_ord.rs new file mode 100644 index 0000000..9ded0ce --- /dev/null +++ b/tests/arweave_ord.rs @@ -0,0 +1,63 @@ +//! Dumb counter-example that I took from https://github.com/graphprotocol/graph-node. + +use std::cmp::Ordering; + +use quickcheck::{Arbitrary, Gen}; +use quickcheck_macros::quickcheck; + +#[derive(Clone, Debug)] +pub enum ArweaveTrigger { + Block(u32), + Transaction(u32), +} + +#[quickcheck] +#[should_panic] +fn arweave_is_incorrect(a: ArweaveTrigger, b: ArweaveTrigger, c: ArweaveTrigger) -> bool { + impl Arbitrary for ArweaveTrigger { + fn arbitrary(g: &mut Gen) -> Self { + if bool::arbitrary(g) { + ArweaveTrigger::Block(u32::arbitrary(g)) + } else { + ArweaveTrigger::Transaction(u32::arbitrary(g)) + } + } + } + + impl PartialEq for ArweaveTrigger { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Block(a_ptr), Self::Block(b_ptr)) => a_ptr == b_ptr, + (Self::Transaction(a_tx), Self::Transaction(b_tx)) => a_tx == b_tx, + _ => false, + } + } + } + + impl Eq for ArweaveTrigger {} + + impl PartialOrd for ArweaveTrigger { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } + } + + impl Ord for ArweaveTrigger { + fn cmp(&self, other: &Self) -> Ordering { + match (self, other) { + // Keep the order when comparing two block triggers + (Self::Block(..), Self::Block(..)) => Ordering::Equal, + + // Block triggers always come last + (Self::Block(..), _) => Ordering::Greater, + (_, Self::Block(..)) => Ordering::Less, + + // Execution outcomes have no intrinsic ordering information so we keep the order in + // which they are included in the `txs` field of `Block`. + (Self::Transaction(..), Self::Transaction(..)) => Ordering::Equal, + } + } + } + + reltester::ord(&a, &b, &c).is_ok() +} diff --git a/tests/f23_cant_not_eq.rs b/tests/f23_cant_not_eq.rs new file mode 100644 index 0000000..798706b --- /dev/null +++ b/tests/f23_cant_not_eq.rs @@ -0,0 +1,4 @@ +#[test] +fn f64_not_eq() { + assert!(reltester::invariants::eq_reflexivity(&f64::NAN).is_err()); +} diff --git a/tests/hash.rs b/tests/hash.rs new file mode 100644 index 0000000..c27ff99 --- /dev/null +++ b/tests/hash.rs @@ -0,0 +1,49 @@ +use std::{collections::BTreeSet, marker::PhantomData, path::PathBuf, rc::Rc}; + +use quickcheck_macros::quickcheck; + +#[quickcheck] +fn hash_string(x1: String, x2: String) -> bool { + reltester::hash(&x1, &x2).is_ok() +} + +#[quickcheck] +fn hash_u32(x1: u32, x2: u32) -> bool { + reltester::hash(&x1, &x2).is_ok() +} + +#[quickcheck] +fn hash_string_tuples(x1: (String, String), x2: (String, String)) -> bool { + reltester::hash(&x1, &x2).is_ok() +} + +#[quickcheck] +fn hash_btreeset_of_units(x1: BTreeSet<()>, x2: BTreeSet<()>) -> bool { + reltester::hash(&x1, &x2).is_ok() +} + +#[quickcheck] +fn hash_path(x1: PathBuf, x2: PathBuf) -> bool { + reltester::hash(x1.as_path(), x2.as_path()).is_ok() +} + +#[test] +fn hash_array_tuples() { + let x1 = ([1, 2, 3, 4], [5, 6, 7, 8]); + let x2 = ([0, 0, 0, 0], [5, 6, 7, 8]); + assert!(reltester::hash(&x1, &x2).is_ok()); +} + +#[test] +fn hash_phantomdata() { + let phantom = PhantomData::::default(); + assert!(reltester::hash(&phantom, &phantom).is_ok()); +} + +#[test] +fn hash_rc() { + let rc1 = Rc::new(1337); + let rc2 = Rc::new(1337); + let _rc2_cloned = rc2.clone(); + assert!(reltester::hash(&rc1, &rc2).is_ok()); +} diff --git a/tests/iterator.rs b/tests/iterator.rs new file mode 100644 index 0000000..74be985 --- /dev/null +++ b/tests/iterator.rs @@ -0,0 +1,20 @@ +use std::collections::BTreeSet; + +use quickcheck_macros::quickcheck; + +#[quickcheck] +fn iterator_chars(x: String) -> bool { + reltester::iterator(x.char_indices()).is_ok() +} + +#[quickcheck] +fn iterator_vec_of_strings(x: Vec) -> bool { + reltester::double_ended_iterator(x.iter()).is_ok() + && reltester::fused_iterator(x.iter()).is_ok() +} + +#[quickcheck] +fn iterator_btreeset_of_u32(x: BTreeSet) -> bool { + reltester::double_ended_iterator(x.iter()).is_ok() + && reltester::fused_iterator(x.iter()).is_ok() +}