Skip to content

Commit 96204db

Browse files
authored
Hash, std::iter support and new Error type (#4)
Signed-off-by: Filippo Costa <[email protected]>
1 parent 7b6629f commit 96204db

14 files changed

+938
-333
lines changed

.github/workflows/ci.yml

+16-9
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ name: CI
22

33
on:
44
push:
5-
branches: [ main ]
5+
branches: [main]
66
pull_request:
7-
branches: [ main ]
7+
branches: [main]
88

99
env:
1010
CARGO_TERM_COLOR: always
@@ -13,10 +13,17 @@ jobs:
1313
build:
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v2
17-
- name: Build
18-
run: cargo build --verbose
19-
- name: Run cargo clippy
20-
run: cargo clippy
21-
- name: Run tests
22-
run: cargo test --verbose
16+
- uses: actions/checkout@v2
17+
- run: cargo check --verbose
18+
- run: cargo clippy
19+
- run: cargo test --verbose
20+
- run: cargo test --examples
21+
- run: cargo test --doc
22+
msrv:
23+
runs-on: ubuntu-latest
24+
steps:
25+
- uses: actions/checkout@v2
26+
- name: install cargo-binstall
27+
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
28+
- run: cargo binstall --version 0.15.1 --no-confirm cargo-msrv
29+
- run: cargo msrv verify

Cargo.toml

+8-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,19 @@
11
[package]
22
name = "reltester"
33
version = "1.0.1"
4-
edition = "2018"
4+
edition = "2021"
55
repository = "https://github.com/neysofu/reltester"
66
license = "MIT"
7+
rust-version = "1.56"
78
description = "Automatically verify the correctness of [Partial]Eq/Ord implementations"
89
authors = ["Filippo Neysofu Costa <[email protected]>"]
910

1011
[dependencies]
11-
thiserror = "1.0.26"
12+
rand = "0.8"
13+
thiserror = "1"
1214

1315
[dev-dependencies]
14-
quickcheck = "1.0"
15-
quickcheck_macros = "1.0"
16+
quickcheck = "1"
17+
quickcheck_macros = "1"
18+
proptest = "1"
19+
proptest-derive = "0.3"

README.md

+55-10
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# Reltester
22

3-
[![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)
3+
[![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)
44

5-
**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.
5+
**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.
66

77

88
*Go to the [docs](https://docs.rs/reltester/latest/reltester/)!*
@@ -11,23 +11,32 @@
1111

1212
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*).
1313

14-
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.
14+
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.
15+
16+
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.
17+
1518

1619
## How to use
1720

18-
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).
19-
2. Based on the traits that your type implements, call the appropriate checker:
21+
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.
22+
2. Based on the traits that your type implements, call the appropriate checker(s):
2023

2124
- `reltester::eq` for `Eq`;
2225
- `reltester::ord` for `Ord`;
2326
- `reltester::partial_eq` for `PartialEq`;
24-
- `reltester::partial_ord` for `PartialOrd`.
27+
- `reltester::partial_ord` for `PartialOrd`;
28+
- `reltester::hash` for `Hash`;
29+
- `reltester::iterator` for `Iterator`;
30+
- `reltester::fused_iterator` for `FusedIterator`;
31+
- `reltester::double_ended_iterator` for `DoubleEndedIterator`;
32+
33+
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.
2534

26-
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.
35+
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.
2736

28-
Please refer to the documentation for more advanced use cases.
37+
## Examples
2938

30-
# A small example
39+
### `f32` (`PartialEq`, `PartialOrd`)
3140

3241
```rust
3342
use reltester;
@@ -36,12 +45,48 @@ use quickcheck_macros::quickcheck;
3645
#[quickcheck]
3746
fn test_f32(a: f32, b: f32, c: f32) -> bool {
3847
// Let's check if `f32` implements `PartialEq` and `PartialOrd` correctly
39-
// (spoiler: it does)
48+
// (spoiler: it does).
4049
reltester::partial_eq(&a, &b, &c).is_ok()
4150
&& reltester::partial_ord(&a, &b, &c).is_ok()
4251
}
4352
```
4453

54+
### `u32` (`Hash`)
55+
56+
```rust
57+
use reltester;
58+
use quickcheck_macros::quickcheck;
59+
60+
#[quickcheck]
61+
fn test_u32(a: u32, b: u32) -> bool {
62+
// Unlike `f32`, `u32` implements both `Eq` and `Hash`, which allows us to
63+
// test `Hash` invariants.
64+
reltester::hash(&a, &b).is_ok()
65+
}
66+
```
67+
68+
### `Vec<u32>` (`DoubleEndedIterator`, `FusedIterator`, `Iterator`)
69+
70+
```rust
71+
use reltester;
72+
use quickcheck_macros::quickcheck;
73+
74+
#[quickcheck]
75+
fn test_vec_u32(nums: Vec<u32>) -> bool {
76+
// `Iterator` is implied and checked by both `DoubleEndedIterator` and
77+
// `FusedIterator`.
78+
reltester::double_ended_iterator(nums.iter()).is_ok()
79+
&& reltester::fused_iterator(nums.iter()).is_ok()
80+
}
81+
```
82+
4583
## Legal
4684

4785
Reltester is available under the terms of the MIT license.
86+
87+
## External references and footnotes
88+
89+
[^1]: https://github.com/rust-lang/rust/issues/41964
90+
[^2]: https://github.com/bevyengine/bevy/pull/7469
91+
[^3]: https://github.com/bluejekyll/trust-dns/issues/1638
92+
[^4]: https://github.com/sparsemat/sprs/issues/261
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//! Why can't `f32` be `Eq`? Here's a counterexample to show why:
2+
3+
fn main() {}
4+
5+
#[test]
6+
fn f64_partial_eq_is_not_reflexive() {
7+
assert!(reltester::invariants::eq_reflexivity(&f64::NAN).is_err());
8+
}

examples/proptest.rs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
fn main() {}
2+
3+
#[cfg(test)]
4+
mod tests {
5+
use proptest::prelude::*;
6+
use std::net::IpAddr;
7+
8+
proptest! {
9+
#[test]
10+
fn correctness_u32(a: u32, b: u32, c: u32) {
11+
reltester::eq(&a, &b, &c).unwrap();
12+
reltester::ord(&a, &b, &c).unwrap();
13+
}
14+
15+
#[test]
16+
fn correctness_f32(a: f32, b: f32, c: f32) {
17+
reltester::partial_eq(&a, &b, &c).unwrap();
18+
reltester::partial_ord(&a, &b, &c).unwrap();
19+
}
20+
21+
#[test]
22+
fn correctness_ip_address(a: IpAddr, b: IpAddr, c: IpAddr) {
23+
reltester::eq(&a, &b, &c).unwrap();
24+
reltester::ord(&a, &b, &c).unwrap();
25+
}
26+
27+
#[test]
28+
fn vec_u32_is_truly_double_ended(x: Vec<u32>) {
29+
reltester::double_ended_iterator(x.iter()).unwrap();
30+
}
31+
}
32+
}

examples/quickcheck.rs

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
fn main() {}
2+
3+
#[cfg(test)]
4+
mod tests {
5+
use quickcheck_macros::quickcheck;
6+
use std::net::IpAddr;
7+
8+
#[quickcheck]
9+
fn correctness_u32(a: u32, b: u32, c: u32) -> bool {
10+
reltester::eq(&a, &b, &c).is_ok() && reltester::ord(&a, &b, &c).is_ok()
11+
}
12+
13+
#[quickcheck]
14+
fn correctness_f32(a: f32, b: f32, c: f32) -> bool {
15+
reltester::partial_eq(&a, &b, &c).is_ok() && reltester::partial_ord(&a, &b, &c).is_ok()
16+
}
17+
18+
#[quickcheck]
19+
fn correctness_ip_address(a: IpAddr, b: IpAddr, c: IpAddr) -> bool {
20+
reltester::eq(&a, &b, &c).is_ok() && reltester::ord(&a, &b, &c).is_ok()
21+
}
22+
23+
#[quickcheck]
24+
fn vec_u32_is_truly_double_ended(x: Vec<u32>) -> bool {
25+
reltester::double_ended_iterator(x.iter()).is_ok()
26+
}
27+
}

rust-toolchain.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
[toolchain]
2-
channel = "1.53"
2+
channel = "1.70" # MSRV is not 1.70 but our dev-dependencies require a more recent rustc.
33
profile = "default"

src/error.rs

+148
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
//! Crate error types.
2+
3+
use thiserror::Error;
4+
5+
/// Represents a broken invariant of [`PartialEq`].
6+
#[derive(Error, Debug, Clone)]
7+
#[non_exhaustive]
8+
pub enum PartialEqError {
9+
/// [`PartialEq::ne`] *MUST* always return the negation of [`PartialEq::eq`].
10+
#[error("PartialEq::ne MUST always return the negation of PartialEq::eq")]
11+
BadNe,
12+
/// If `A: PartialEq<B>` and `B: PartialEq<A>`, then `a == b` *MUST* imply `b == a`.
13+
#[error("a == b MUST imply b == a")]
14+
BrokeSymmetry,
15+
/// If `A: PartialEq<B>` and `B: PartialEq<C>` and `A: PartialEq<C>`, then
16+
/// `a == b && b == c` *MUST* imply `a == c`.
17+
#[error("a == b && b == c MUST imply a == c")]
18+
BrokeTransitivity,
19+
}
20+
21+
/// Represents a broken invariant of [`Eq`].
22+
///
23+
/// Note that [`Eq`] also mandates all invariants of [`PartialEq`].
24+
#[derive(Error, Debug, Clone)]
25+
#[non_exhaustive]
26+
pub enum EqError {
27+
/// All values must be equal to themselves.
28+
#[error("a == a MUST be true")]
29+
BrokeReflexivity,
30+
}
31+
32+
/// Represents a broken invariant of [`PartialOrd`].
33+
///
34+
/// Note that [`PartialOrd`] also mandates all invariants of [`PartialEq`].
35+
#[derive(Error, Debug, Clone)]
36+
#[non_exhaustive]
37+
pub enum PartialOrdError {
38+
/// [`PartialOrd::partial_cmp`] *MUST* return `Some(Ordering::Equal)` if
39+
/// and only if [`PartialEq::eq`] returns [`true`].
40+
#[error("PartialOrd::partial_cmp MUST return Some(Ordering::Equal) if and only if PartialEq::eq returns true")]
41+
BadPartialCmp,
42+
/// [`PartialOrd::lt`] *MUST* return [`true`]
43+
/// if and only if [`PartialOrd::partial_cmp`] returns `Some(Ordering::Less)`.
44+
#[error("PartialOrd::lt MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Less)")]
45+
BadLt,
46+
/// [`PartialOrd::le`] *MUST* return [`true`] if and only if
47+
/// [`PartialOrd::partial_cmp`] returns `Some(Ordering::Less)` or
48+
/// [`Some(Ordering::Equal)`].
49+
#[error("PartialOrd::le MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Less) or Some(Ordering::Equal)")]
50+
BadLe,
51+
/// [`PartialOrd::gt`] *MUST* return [`true`] if and only if
52+
/// [`PartialOrd::partial_cmp`] returns `Some(Ordering::Greater)`.
53+
#[error("PartialOrd::gt MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Greater)")]
54+
BadGt,
55+
/// [`PartialOrd::ge`] *MUST* return [`true`] if and only if
56+
/// [`PartialOrd::partial_cmp`] returns `Some(Ordering::Greater)` or
57+
/// `Some(Ordering::Equal)`.
58+
#[error("PartialOrd::ge MUST return true if and only if PartialOrd::partial_cmp returns Some(Ordering::Greater) or Some(Ordering::Equal)")]
59+
BadGe,
60+
/// If `a > b`, then `b < a` *MUST* be true.
61+
#[error("If a > b, then b < a MUST be true")]
62+
BrokeDuality,
63+
/// If `a > b` and `b > c`, then `a > c` *MUST* be true. The same must hold true for `<`.
64+
#[error("If a > b and b > c, then a > c MUST be true. The same must hold true for <")]
65+
BrokeTransitivity,
66+
}
67+
68+
/// Represents a broken invariant of [`Ord`].
69+
///
70+
/// Note that [`Ord`] also mandates all invariants of [`PartialOrd`] and [`Eq`].
71+
#[derive(Error, Debug, Clone)]
72+
#[non_exhaustive]
73+
pub enum OrdError {
74+
/// [`Ord::cmp`] *MUST* always return `Some(PartialOrd::partial_cmp())`.
75+
#[error("`cmp` and `partial_cmp` are not consistent")]
76+
BadCmp,
77+
/// [`Ord::cmp`] and [`Ord::max`] are not consistent.
78+
#[error("`cmp` and `max` are not consistent")]
79+
BadMax,
80+
/// [`Ord::cmp`] and [`Ord::min`] are not consistent.
81+
#[error("`cmp` and `min` are not consistent")]
82+
BadMin,
83+
/// [`Ord::cmp`] and [`Ord::clamp`] are not consistent.
84+
#[error("`cmp` and `clamp` are not consistent")]
85+
BadClamp,
86+
}
87+
88+
/// Represents a broken invariant of [`Hash`].
89+
#[derive(Error, Debug, Clone)]
90+
#[non_exhaustive]
91+
pub enum HashError {
92+
/// Equal values *MUST* have equal hash values.
93+
#[error("Equal values MUST have equal hash values")]
94+
EqualButDifferentHashes,
95+
/// When two values are different (as defined by [`PartialEq::ne`]), neither
96+
/// of the two hash outputs can be a prefix of the other. See
97+
/// <https://doc.rust-lang.org/std/hash/trait.Hash.html#prefix-collisions>
98+
/// for more information.
99+
#[error("When two values are different, one of the two hash outputs CAN NOT be a prefix of the other")]
100+
PrefixCollision,
101+
}
102+
103+
/// Represents a broken invariant of [`Iterator`].
104+
#[derive(Error, Debug, Clone)]
105+
#[non_exhaustive]
106+
pub enum IteratorError {
107+
/// [`Iterator::size_hint`] *MUST* always provide correct lower and upper
108+
/// bounds.
109+
#[error("Iterator::size_hint MUST always provide correct lower and upper bounds")]
110+
BadSizeHint,
111+
/// [`Iterator::count`] *MUST* be consistent with the actual number of
112+
/// elements returned by [`Iterator::next`].
113+
#[error(
114+
"Iterator::count MUST be consistent with the actual number of elements returned by .next()"
115+
)]
116+
BadCount,
117+
/// [`Iterator::last`] *MUST* be equal to the last element of the
118+
/// [`Vec`] resulting from [`Iterator::collect`].
119+
#[error(".last() MUST be equal to the last element of the Vec<_> resulting from .collect()")]
120+
BadLast,
121+
/// [`DoubleEndedIterator::next_back`] *MUST* return the same values as
122+
/// [`Iterator::next`], just in reverse order, and it MUST NOT return
123+
/// different values.
124+
#[error("DoubleEndedIterator::next_back() MUST return the same values as .next(), but in reverse order")]
125+
BadNextBack,
126+
/// [`FusedIterator`](core::iter::FusedIterator) *MUST* return [`None`]
127+
/// indefinitely after exhaustion.
128+
#[error("FusedIterator MUST return None indefinitely after exhaustion")]
129+
FusedIteratorReturnedSomeAfterExhaustion,
130+
}
131+
132+
/// The crate error type.
133+
#[derive(Error, Debug, Clone)]
134+
#[non_exhaustive]
135+
pub enum Error {
136+
#[error(transparent)]
137+
PartialEq(#[from] PartialEqError),
138+
#[error(transparent)]
139+
Eq(#[from] EqError),
140+
#[error(transparent)]
141+
PartiaOrd(#[from] PartialOrdError),
142+
#[error(transparent)]
143+
Ord(#[from] OrdError),
144+
#[error(transparent)]
145+
Hash(#[from] HashError),
146+
#[error(transparent)]
147+
Iterator(#[from] IteratorError),
148+
}

0 commit comments

Comments
 (0)