diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b41f2bc..2179706 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,56 +1,70 @@ name: ci on: pull_request: + branches: + - master push: branches: - master schedule: - cron: '00 01 * * *' + +# The section is needed to drop write-all permissions that are granted on +# `schedule` event. By specifying any permission explicitly all others are set +# to none. By using the principle of least privilege the damage a compromised +# workflow can do (because of an injection or compromised third party tool or +# action) is restricted. Currently the worklow doesn't need any additional +# permission except for pulling the code. Adding labels to issues, commenting +# on pull-requests, etc. may need additional permissions: +# +# Syntax for this section: +# https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions +# +# Reference for how to assign permissions on a job-by-job basis: +# https://docs.github.com/en/actions/using-jobs/assigning-permissions-to-jobs +# +# Reference for available permissions that we can enable if needed: +# https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token +permissions: + # to fetch code (actions/checkout) + contents: read + jobs: test: name: test runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - build: - - pinned - - stable - - beta - - nightly - - macos - - win-msvc - - win-gnu include: - build: pinned - os: ubuntu-18.04 - rust: 1.46.0 + os: ubuntu-latest + rust: 1.71.0 - build: stable - os: ubuntu-18.04 + os: ubuntu-latest rust: stable - build: beta - os: ubuntu-18.04 + os: ubuntu-latest rust: beta - build: nightly - os: ubuntu-18.04 + os: ubuntu-latest rust: nightly - build: macos os: macos-latest rust: stable - build: win-msvc - os: windows-2019 + os: windows-latest rust: stable - build: win-gnu - os: windows-2019 + os: windows-latest rust: stable-x86_64-gnu steps: - name: Checkout repository - uses: actions/checkout@v1 - with: - fetch-depth: 1 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: dtolnay/rust-toolchain@master with: - rust-version: ${{ matrix.rust }} + toolchain: ${{ matrix.rust }} - run: cargo build --verbose - run: cargo doc --verbose - run: cargo test --verbose @@ -58,19 +72,15 @@ jobs: - run: cargo test --verbose --manifest-path quickcheck_macros/Cargo.toml rustfmt: - name: rustfmt - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v1 - with: - fetch-depth: 1 + uses: actions/checkout@v4 - name: Install Rust - uses: hecrj/setup-rust-action@v1 + uses: dtolnay/rust-toolchain@master with: - rust-version: stable - - name: Install rustfmt - run: rustup component add rustfmt + toolchain: stable + components: rustfmt - name: Check formatting run: | cargo fmt --all -- --check diff --git a/Cargo.toml b/Cargo.toml index 17cc17a..1733aa6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,9 @@ repository = "https://github.com/BurntSushi/quickcheck" readme = "README.md" keywords = ["testing", "quickcheck", "property", "shrinking", "fuzz"] categories = ["development-tools::testing"] -license = "Unlicense/MIT" -exclude = ["/.travis.yml", "/Makefile", "/ctags.rust", "/session.vim"] -edition = "2018" +license = "Unlicense OR MIT" +exclude = ["/Makefile", "/ctags.rust", "/session.vim"] +edition = "2021" [workspace] members = ["quickcheck_macros"] @@ -25,6 +25,6 @@ regex = ["env_logger/regex"] name = "quickcheck" [dependencies] -env_logger = { version = "0.8.2", default-features = false, optional = true } +env_logger = { version = "0.11", default-features = false, optional = true } log = { version = "0.4", optional = true } -rand = { version = "0.8", default-features = false, features = ["getrandom", "small_rng"] } +rand = { version = "0.9", default-features = false, features = ["os_rng", "small_rng"] } diff --git a/README.md b/README.md index 2546a63..c4bf1bc 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ -quickcheck -========== +# quickcheck + QuickCheck is a way to do property based testing using randomly generated input. This crate comes with the ability to randomly generate and shrink integers, floats, tuples, booleans, lists, strings, options and results. @@ -15,28 +15,22 @@ the input space quickly. (It should be the same strategy used in Haskell](https://hackage.haskell.org/package/QuickCheck).) [![Build status](https://github.com/BurntSushi/quickcheck/workflows/ci/badge.svg)](https://github.com/BurntSushi/quickcheck/actions) -[![](https://meritbadge.herokuapp.com/quickcheck)](https://crates.io/crates/quickcheck) - -Dual-licensed under MIT or the [UNLICENSE](https://unlicense.org). +[![crates.io](https://img.shields.io/crates/v/quickcheck.svg)](https://crates.io/crates/quickcheck) +Dual-licensed under MIT or the [UNLICENSE](https://unlicense.org/). -### Documentation +## Documentation The API is fully documented: [https://docs.rs/quickcheck](https://docs.rs/quickcheck). - -### Simple example +## Simple example Here's an example that tests a function that reverses a vector: ```rust -#[cfg(test)] -#[macro_use] -extern crate quickcheck; - fn reverse(xs: &[T]) -> Vec { - let mut rev = vec!(); + let mut rev = vec![]; for x in xs.iter() { rev.insert(0, x.clone()) } @@ -45,10 +39,13 @@ fn reverse(xs: &[T]) -> Vec { #[cfg(test)] mod tests { - quickcheck! { - fn prop(xs: Vec) -> bool { - xs == reverse(&reverse(&xs)) - } + use quickcheck::quickcheck; + use super::reverse; + + quickcheck! { + fn prop(xs: Vec) -> bool { + xs == reverse(&reverse(&xs)) + } } } ``` @@ -56,7 +53,7 @@ mod tests { This example uses the `quickcheck!` macro, which is backwards compatible with old versions of Rust. -### The `#[quickcheck]` attribute +## The `#[quickcheck]` attribute To make it easier to write QuickCheck tests, the `#[quickcheck]` attribute will convert a property function into a `#[test]` function. @@ -65,21 +62,18 @@ To use the `#[quickcheck]` attribute, you must import the `quickcheck` macro from the `quickcheck_macros` crate: ```rust -#[cfg(test)] -extern crate quickcheck; -#[cfg(test)] -#[macro_use(quickcheck)] -extern crate quickcheck_macros; +fn reverse(xs: &[T]) -> Vec { + let mut rev = vec![]; + for x in xs { + rev.insert(0, x.clone()) + } + rev +} #[cfg(test)] mod tests { - fn reverse(xs: &[T]) -> Vec { - let mut rev = vec!(); - for x in xs { - rev.insert(0, x.clone()) - } - rev - } + use quickcheck_macros::quickcheck; + use super::reverse; #[quickcheck] fn double_reversal_is_identity(xs: Vec) -> bool { @@ -88,8 +82,7 @@ mod tests { } ``` - -### Installation +## Installation `quickcheck` is on `crates.io`, so you can include it in your project like so: @@ -126,10 +119,9 @@ Crate features: - `"regex"`: (Enabled by default.) Enables the use of regexes with `env_logger`. +## Minimum Rust version policy -### Minimum Rust version policy - -This crate's minimum supported `rustc` version is `1.46.0`. +This crate's minimum supported `rustc` version is `1.71.0`. The current policy is that the minimum Rust version required to use this crate can be increased in minor version updates. For example, if `crate 1.0` requires @@ -145,8 +137,7 @@ With all of that said, currently, `rand` is a public dependency of aggressive than `rand`'s MSRV policy. Otherwise, `quickcheck` will defer to `rand`'s MSRV policy. - -### Compatibility +## Compatibility In general, this crate considers the `Arbitrary` implementations provided as implementation details. Strategies may or may not change over time, which may @@ -154,29 +145,26 @@ cause new test failures, presumably due to the discovery of new bugs due to a new kind of witness being generated. These sorts of changes may happen in semver compatible releases. - -### Alternative Rust crates for property testing +## Alternative Rust crates for property testing The [`proptest`](https://docs.rs/proptest) crate is inspired by the -[Hypothesis](https://hypothesis.works) framework for Python. +[Hypothesis](https://hypothesis.works/) framework for Python. You can read a comparison between `proptest` and `quickcheck` -[here](https://github.com/AltSysrq/proptest/blob/master/proptest/README.md#differences-between-quickcheck-and-proptest) +[here](https://github.com/proptest-rs/proptest/blob/main/proptest/README.md#differences-between-quickcheck-and-proptest) and -[here](https://github.com/AltSysrq/proptest/issues/15#issuecomment-348382287). +[here](https://github.com/proptest-rs/proptest/issues/15#issuecomment-348382287). In particular, `proptest` improves on the concept of shrinking. So if you've ever had problems/frustration with shrinking in `quickcheck`, then `proptest` might be worth a try! - -### Alternatives for fuzzing +## Alternatives for fuzzing Please see the [Rust Fuzz Book](https://rust-fuzz.github.io/book/introduction.html) and the [`arbitrary`](https://crates.io/crates/arbitrary) crate. - -### Discarding test results (or, properties are polymorphic!) +## Discarding test results (or, properties are polymorphic!) Sometimes you want to test a property that only holds for a *subset* of the possible inputs, so that when your property is given an input that is outside @@ -285,8 +273,7 @@ There is also `QUICKCHECK_MIN_TESTS_PASSED` which sets the minimum number of valid tests that need pass (defaults to `0`) in order for it to be considered a success. - -### Shrinking +## Shrinking Shrinking is a crucial part of QuickCheck that simplifies counter-examples for your properties automatically. For example, if you erroneously defined a @@ -313,7 +300,7 @@ quickcheck(prop as fn(Vec) -> bool); Then without shrinking, you might get a counter-example like: -``` +```text [quickcheck] TEST FAILED. Arguments: ([-17, 13, -12, 17, -8, -10, 15, -19, -19, -9, 11, -5, 1, 19, -16, 6]) ``` @@ -321,13 +308,13 @@ Then without shrinking, you might get a counter-example like: Which is pretty mysterious. But with shrinking enabled, you're nearly guaranteed to get this counter-example every time: -``` +```text [quickcheck] TEST FAILED. Arguments: ([0]) ``` Which is going to be much easier to debug. -### More Thorough Checking +## More Thorough Checking Quickcheck uses random input to test, so it won't always find bugs that could be uncovered with a particular @@ -341,7 +328,7 @@ lot, you might also be interested in trying out [`cargo fuzz`](https://github.com/rust-fuzz/cargo-fuzz), which runs in a loop by default. -##### Running in a Loop +### Running in a Loop One approach is to run your quickcheck properties in a loop that just keeps going until you tell it to stop or it finds a bug. @@ -369,7 +356,7 @@ would take time away from quickcheck! Checking the return code and exiting is also important. Without that test, you won't ever notice when a failure happens. -##### Cranking the Number of Tests +### Cranking the Number of Tests Another approach is to just ask quickcheck to run properties more times. You can do this either via the @@ -380,7 +367,7 @@ the loop approach this will take a bounded amount of time, which makes it more suitable for something like a release cycle that wants to really hammer your software. -##### Making Arbitrary Smarter +### Making Arbitrary Smarter This approach entails spending more time generating interesting inputs in your implementations of Arbitrary. The idea is to @@ -390,7 +377,7 @@ and the whole idea of property checking is to take that burden off the programmer. Despite the theoretical discomfort, this approach can turn out to be practical. -### Generating Structs +## Generating Structs It is very simple to generate structs in QuickCheck. Consider the following example, where the struct `Point` is defined: @@ -418,8 +405,7 @@ impl Arbitrary for Point { } ``` - -### Case study: The Sieve of Eratosthenes +## Case study: The Sieve of Eratosthenes The [Sieve of Eratosthenes](https://en.wikipedia.org/wiki/Sieve_of_Eratosthenes) is a simple and elegant way to find all primes less than or equal to `N`. @@ -458,7 +444,7 @@ fn sieve(n: usize) -> Vec { Let's try it on a few inputs by hand: -``` +```text sieve(3) => [2, 3] sieve(5) => [2, 3, 5] sieve(8) => [2, 3, 5, 7, 8] # !!! @@ -503,7 +489,7 @@ A fully working source file with this code is in The output of running this program has this message: -``` +```text [quickcheck] TEST FAILED. Arguments: (4) ``` @@ -558,7 +544,7 @@ fn main() { we see that it fails immediately for value n = 2. -``` +```text [quickcheck] TEST FAILED. Arguments: (2) ``` @@ -566,18 +552,18 @@ If we inspect `sieve()` once again, we see that we mistakenly mark `2` as non-prime. Removing the line `marked[2] = true;` results in both properties passing. -### What's not in this port of QuickCheck? +## What's not in this port of QuickCheck? I think I've captured the key features, but there are still things missing: -* Only functions with 8 or fewer parameters can be quickchecked. This +- Only functions with 8 or fewer parameters can be quickchecked. This limitation can be lifted to some `N`, but requires an implementation for each `n` of the `Testable` trait. -* Functions that fail because of a stack overflow are not caught by QuickCheck. +- Functions that fail because of a stack overflow are not caught by QuickCheck. Therefore, such failures will not have a witness attached to them. (I'd like to fix this, but I don't know how.) -* `Coarbitrary` does not exist in any form in this package. It's unlikely that +- `Coarbitrary` does not exist in any form in this package. It's unlikely that it ever will. -* `Arbitrary` is not implemented for closures. See +- `Arbitrary` is not implemented for closures. See [issue #56](https://github.com/BurntSushi/quickcheck/issues/56) for more details on why. diff --git a/examples/btree_set_range.rs b/examples/btree_set_range.rs index 0e7418b..bf52e24 100644 --- a/examples/btree_set_range.rs +++ b/examples/btree_set_range.rs @@ -31,19 +31,19 @@ impl RangeBounds for RangeAny { fn panics(range: RangeAny) -> bool { match (&range.0, &range.1) { (Excluded(start), Excluded(end)) => start >= end, - (Included(start), Excluded(end)) - | (Excluded(start), Included(end)) - | (Included(start), Included(end)) => start > end, + (Included(start), Excluded(end) | Included(end)) + | (Excluded(start), Included(end)) => start > end, (Unbounded, _) | (_, Unbounded) => false, } } -/// Checks that `BTreeSet::range` returns all items contained in the given `range`. +/// Checks that `BTreeSet::range` returns all items contained in the given +/// `range`. fn check_range(set: BTreeSet, range: RangeAny) -> TestResult { if panics(range) { TestResult::discard() } else { - let xs: BTreeSet<_> = set.range(range).cloned().collect(); + let xs: BTreeSet<_> = set.range(range).copied().collect(); TestResult::from_bool( set.iter().all(|x| range.contains(x) == xs.contains(x)), ) diff --git a/examples/reverse.rs b/examples/reverse.rs index fff6e71..66d395b 100644 --- a/examples/reverse.rs +++ b/examples/reverse.rs @@ -3,7 +3,7 @@ use quickcheck::quickcheck; fn reverse(xs: &[T]) -> Vec { let mut rev = vec![]; for x in xs { - rev.insert(0, x.clone()) + rev.insert(0, x.clone()); } rev } diff --git a/examples/reverse_single.rs b/examples/reverse_single.rs index 6112509..cbfd679 100644 --- a/examples/reverse_single.rs +++ b/examples/reverse_single.rs @@ -3,7 +3,7 @@ use quickcheck::{quickcheck, TestResult}; fn reverse(xs: &[T]) -> Vec { let mut rev = vec![]; for x in xs { - rev.insert(0, x.clone()) + rev.insert(0, x.clone()); } rev } @@ -13,7 +13,7 @@ fn main() { if xs.len() != 1 { return TestResult::discard(); } - TestResult::from_bool(xs == reverse(&*xs)) + TestResult::from_bool(xs == reverse(&xs)) } quickcheck(prop as fn(Vec) -> TestResult); } diff --git a/examples/sieve.rs b/examples/sieve.rs index f05b8e3..bb921ea 100644 --- a/examples/sieve.rs +++ b/examples/sieve.rs @@ -31,7 +31,7 @@ fn main() { } fn prop_prime_iff_in_the_sieve(n: usize) -> bool { - sieve(n) == (0..(n + 1)).filter(|&i| is_prime(i)).collect::>() + sieve(n) == (0..=n).filter(|&i| is_prime(i)).collect::>() } quickcheck(prop_all_prime as fn(usize) -> bool); diff --git a/examples/sort.rs b/examples/sort.rs index 0f495a0..335f5ce 100644 --- a/examples/sort.rs +++ b/examples/sort.rs @@ -4,18 +4,18 @@ use quickcheck::quickcheck; fn smaller_than(xs: &[T], pivot: &T) -> Vec { - xs.iter().filter(|&x| *x < *pivot).map(|x| x.clone()).collect() + xs.iter().filter(|&x| *x < *pivot).cloned().collect() } fn larger_than(xs: &[T], pivot: &T) -> Vec { - xs.iter().filter(|&x| *x > *pivot).map(|x| x.clone()).collect() + xs.iter().filter(|&x| *x > *pivot).cloned().collect() } fn sortk(x: &T, xs: &[T]) -> Vec { - let mut result: Vec = sort(&*smaller_than(xs, x)); - let last_part = sort(&*larger_than(xs, x)); + let mut result: Vec = sort(&smaller_than(xs, x)); + let last_part = sort(&larger_than(xs, x)); result.push(x.clone()); - result.extend(last_part.iter().map(|x| x.clone())); + result.extend(last_part.iter().cloned()); result } @@ -38,9 +38,9 @@ fn main() { } fn keeps_length(xs: Vec) -> bool { - xs.len() == sort(&*xs).len() + xs.len() == sort(&xs).len() } quickcheck(keeps_length as fn(Vec) -> bool); - quickcheck(is_sorted as fn(Vec) -> bool) + quickcheck(is_sorted as fn(Vec) -> bool); } diff --git a/quickcheck_macros/Cargo.toml b/quickcheck_macros/Cargo.toml index 1d9041d..f8495e7 100644 --- a/quickcheck_macros/Cargo.toml +++ b/quickcheck_macros/Cargo.toml @@ -8,7 +8,8 @@ homepage = "https://github.com/BurntSushi/quickcheck" repository = "https://github.com/BurntSushi/quickcheck" readme = "../README.md" keywords = ["testing", "quickcheck", "property", "shrinking", "fuzz"] -license = "Unlicense/MIT" +license = "Unlicense OR MIT" +edition = "2021" autotests = false [lib] @@ -19,7 +20,7 @@ proc-macro = true [dependencies] proc-macro2 = "1.0" quote = "1.0" -syn = { version = "1.0", features = ["full"] } +syn = { version = "2.0", features = ["full"] } [dev-dependencies] quickcheck = { path = "..", version = "1.0.0" } diff --git a/quickcheck_macros/src/lib.rs b/quickcheck_macros/src/lib.rs index 132e8e0..eda6ff4 100644 --- a/quickcheck_macros/src/lib.rs +++ b/quickcheck_macros/src/lib.rs @@ -1,8 +1,3 @@ -extern crate proc_macro; -extern crate proc_macro2; -extern crate quote; -extern crate syn; - use std::mem; use proc_macro::TokenStream; @@ -31,16 +26,23 @@ pub fn quickcheck(_args: TokenStream, input: TokenStream) -> TokenStream { }); if errors.is_empty() { - let attrs = mem::replace(&mut item_fn.attrs, Vec::new()); + let attrs = mem::take(&mut item_fn.attrs); let name = &item_fn.sig.ident; + if let Some(variadic) = &item_fn.sig.variadic { + // variadics are just for `extern fn` + errors.push(syn::parse::Error::new( + variadic.span(), + "unsupported variadic", + )); + } let fn_type = syn::TypeBareFn { lifetimes: None, - unsafety: item_fn.sig.unsafety.clone(), + unsafety: item_fn.sig.unsafety, abi: item_fn.sig.abi.clone(), fn_token: ::default(), paren_token: syn::token::Paren::default(), inputs, - variadic: item_fn.sig.variadic.clone(), + variadic: None, output: item_fn.sig.output.clone(), }; @@ -60,7 +62,7 @@ pub fn quickcheck(_args: TokenStream, input: TokenStream) -> TokenStream { } } Ok(syn::Item::Static(mut item_static)) => { - let attrs = mem::replace(&mut item_static.attrs, Vec::new()); + let attrs = mem::take(&mut item_static.attrs); let name = &item_static.ident; quote! { diff --git a/src/arbitrary.rs b/src/arbitrary.rs index 92f893b..897641a 100644 --- a/src/arbitrary.rs +++ b/src/arbitrary.rs @@ -21,13 +21,13 @@ use std::path::PathBuf; use std::sync::Arc; use std::time::{Duration, SystemTime, UNIX_EPOCH}; -use rand::seq::SliceRandom; -use rand::{self, Rng, SeedableRng}; +use rand::prelude::*; +use rand::{Rng, SeedableRng}; -/// Gen represents a PRNG. +/// `Gen` represents a PRNG. /// -/// It is the source of randomness from which QuickCheck will generate -/// values. An instance of `Gen` is passed to every invocation of +/// It is the source of randomness from which QuickCheck will generate values. +/// An instance of `Gen` is passed to every invocation of /// `Arbitrary::arbitrary`, which permits callers to use lower level RNG /// routines to generate values. /// @@ -47,7 +47,7 @@ impl Gen { /// randomly generated number. (Unless that number is used to control the /// size of a data structure.) pub fn new(size: usize) -> Gen { - Gen { rng: rand::rngs::SmallRng::from_entropy(), size: size } + Gen { rng: rand::rngs::SmallRng::from_os_rng(), size } } /// Returns the size configured with this generator. @@ -62,19 +62,19 @@ impl Gen { slice.choose(&mut self.rng) } - fn gen(&mut self) -> T + fn random(&mut self) -> T where - rand::distributions::Standard: rand::distributions::Distribution, + rand::distr::StandardUniform: rand::distr::Distribution, { - self.rng.gen() + self.rng.random() } - fn gen_range(&mut self, range: R) -> T + fn random_range(&mut self, range: R) -> T where - T: rand::distributions::uniform::SampleUniform, - R: rand::distributions::uniform::SampleRange, + T: rand::distr::uniform::SampleUniform, + R: rand::distr::uniform::SampleRange, { - self.rng.gen_range(range) + self.rng.random_range(range) } } @@ -91,14 +91,14 @@ pub fn single_shrinker(value: A) -> Box> { /// `Arbitrary` describes types whose values can be randomly generated and /// shrunk. /// -/// Aside from shrinking, `Arbitrary` is different from typical RNGs in that -/// it respects `Gen::size()` for controlling how much memory a particular -/// value uses, for practical purposes. For example, `Vec::arbitrary()` -/// respects `Gen::size()` to decide the maximum `len()` of the vector. -/// This behavior is necessary due to practical speed and size limitations. -/// Conversely, `i32::arbitrary()` ignores `size()` since all `i32` values -/// require `O(1)` memory and operations between `i32`s require `O(1)` time -/// (with the exception of exponentiation). +/// Aside from shrinking, `Arbitrary` is different from typical RNGs in that it +/// respects `Gen::size()` for controlling how much memory a particular value +/// uses, for practical purposes. For example, `Vec::arbitrary()` respects +/// `Gen::size()` to decide the maximum `len()` of the vector. This behavior is +/// necessary due to practical speed and size limitations. Conversely, +/// `i32::arbitrary()` ignores `size()` since all `i32` values require `O(1)` +/// memory and operations between `i32`s require `O(1)` time (with the +/// exception of exponentiation). /// /// Additionally, all types that implement `Arbitrary` must also implement /// `Clone`. @@ -131,14 +131,12 @@ pub trait Arbitrary: Clone + 'static { } impl Arbitrary for () { - fn arbitrary(_: &mut Gen) -> () { - () - } + fn arbitrary(_: &mut Gen) {} } impl Arbitrary for bool { fn arbitrary(g: &mut Gen) -> bool { - g.gen() + g.random() } fn shrink(&self) -> Box> { @@ -152,7 +150,7 @@ impl Arbitrary for bool { impl Arbitrary for Option { fn arbitrary(g: &mut Gen) -> Option { - if g.gen() { + if g.random() { None } else { Some(Arbitrary::arbitrary(g)) @@ -172,7 +170,7 @@ impl Arbitrary for Option { impl Arbitrary for Result { fn arbitrary(g: &mut Gen) -> Result { - if g.gen() { + if g.random() { Ok(Arbitrary::arbitrary(g)) } else { Err(Arbitrary::arbitrary(g)) @@ -248,11 +246,31 @@ impl_arb_for_tuples! { (H, 7), } +impl Arbitrary for [A; N] { + fn arbitrary(g: &mut Gen) -> Self { + std::array::from_fn(|_ix| A::arbitrary(g)) + } + + fn shrink(&self) -> Box> { + let cloned = self.clone(); + let iter = (0..N).flat_map(move |n| { + let cloned = cloned.clone(); + cloned[n].shrink().map(move |shr_value| { + let mut result = cloned.clone(); + result[n] = shr_value; + result + }) + }); + + Box::new(iter) + } +} + impl Arbitrary for Vec { fn arbitrary(g: &mut Gen) -> Vec { let size = { let s = g.size(); - g.gen_range(0..s) + g.random_range(0..s) }; (0..size).map(|_| A::arbitrary(g)).collect() } @@ -275,15 +293,16 @@ struct VecShrinker { } impl VecShrinker { + #[allow(clippy::new_ret_no_self)] fn new(seed: Vec) -> Box>> { - let es = match seed.get(0) { + let es = match seed.first() { Some(e) => e.shrink(), None => return empty_shrinker(), }; let size = seed.len(); Box::new(VecShrinker { - seed: seed, - size: size, + seed, + size, offset: size, element_shrinker: es, }) @@ -344,7 +363,7 @@ where // offset (self.offset == 0 only on first entry to this part of the // iterator) if self.offset == 0 { - self.offset = 1 + self.offset = 1; } match self.next_element() { @@ -352,7 +371,7 @@ where self.seed[..self.offset - 1] .iter() .cloned() - .chain(Some(e).into_iter()) + .chain(Some(e)) .chain(self.seed[self.offset..].iter().cloned()) .collect(), ), @@ -461,7 +480,7 @@ impl Arbitrary for VecDeque { impl Arbitrary for IpAddr { fn arbitrary(g: &mut Gen) -> IpAddr { - let ipv4: bool = g.gen(); + let ipv4: bool = g.random(); if ipv4 { IpAddr::V4(Arbitrary::arbitrary(g)) } else { @@ -472,40 +491,45 @@ impl Arbitrary for IpAddr { impl Arbitrary for Ipv4Addr { fn arbitrary(g: &mut Gen) -> Ipv4Addr { - Ipv4Addr::new(g.gen(), g.gen(), g.gen(), g.gen()) + Ipv4Addr::new(g.random(), g.random(), g.random(), g.random()) } } impl Arbitrary for Ipv6Addr { fn arbitrary(g: &mut Gen) -> Ipv6Addr { Ipv6Addr::new( - g.gen(), - g.gen(), - g.gen(), - g.gen(), - g.gen(), - g.gen(), - g.gen(), - g.gen(), + g.random(), + g.random(), + g.random(), + g.random(), + g.random(), + g.random(), + g.random(), + g.random(), ) } } impl Arbitrary for SocketAddr { fn arbitrary(g: &mut Gen) -> SocketAddr { - SocketAddr::new(Arbitrary::arbitrary(g), g.gen()) + SocketAddr::new(Arbitrary::arbitrary(g), g.random()) } } impl Arbitrary for SocketAddrV4 { fn arbitrary(g: &mut Gen) -> SocketAddrV4 { - SocketAddrV4::new(Arbitrary::arbitrary(g), g.gen()) + SocketAddrV4::new(Arbitrary::arbitrary(g), g.random()) } } impl Arbitrary for SocketAddrV6 { fn arbitrary(g: &mut Gen) -> SocketAddrV6 { - SocketAddrV6::new(Arbitrary::arbitrary(g), g.gen(), g.gen(), g.gen()) + SocketAddrV6::new( + Arbitrary::arbitrary(g), + g.random(), + g.random(), + g.random(), + ) } } @@ -513,11 +537,12 @@ impl Arbitrary for PathBuf { fn arbitrary(g: &mut Gen) -> PathBuf { // use some real directories as guesses, so we may end up with // actual working directories in case that is relevant. - let here = - env::current_dir().unwrap_or(PathBuf::from("/test/directory")); + let here = env::current_dir() + .unwrap_or_else(|_| PathBuf::from("/test/directory")); let temp = env::temp_dir(); #[allow(deprecated)] - let home = env::home_dir().unwrap_or(PathBuf::from("/home/user")); + let home = + env::home_dir().unwrap_or_else(|| PathBuf::from("/home/user")); let mut p = g .choose(&[ here, @@ -567,7 +592,7 @@ impl Arbitrary for OsString { fn shrink(&self) -> Box> { let mystring: String = self.clone().into_string().unwrap(); - Box::new(mystring.shrink().map(|s| OsString::from(s))) + Box::new(mystring.shrink().map(OsString::from)) } } @@ -575,7 +600,7 @@ impl Arbitrary for String { fn arbitrary(g: &mut Gen) -> String { let size = { let s = g.size(); - g.gen_range(0..s) + g.random_range(0..s) }; (0..size).map(|_| char::arbitrary(g)).collect() } @@ -591,10 +616,10 @@ impl Arbitrary for CString { fn arbitrary(g: &mut Gen) -> Self { let size = { let s = g.size(); - g.gen_range(0..s) + g.random_range(0..s) }; // Use either random bytes or random UTF-8 encoded codepoints. - let utf8: bool = g.gen(); + let utf8: bool = g.random(); if utf8 { CString::new( (0..) @@ -629,16 +654,17 @@ impl Arbitrary for CString { impl Arbitrary for char { fn arbitrary(g: &mut Gen) -> char { - let mode = g.gen_range(0..100); + let mode = g.random_range(0..100); match mode { 0..=49 => { // ASCII + some control characters - g.gen_range(0..0xB0) as u8 as char + g.random_range(0u8..0xB0) as char } 50..=59 => { // Unicode BMP characters loop { - if let Some(x) = char::from_u32(g.gen_range(0..0x10000)) { + if let Some(x) = char::from_u32(g.random_range(0..0x10000)) + { return x; } // ignore surrogate pairs @@ -714,11 +740,11 @@ impl Arbitrary for char { } 90..=94 => { // Tricky unicode, part 2 - char::from_u32(g.gen_range(0x2000..0x2070)).unwrap() + char::from_u32(g.random_range(0x2000..0x2070)).unwrap() } 95..=99 => { // Completely arbitrary characters - g.gen() + g.random() } _ => unreachable!(), } @@ -738,6 +764,7 @@ macro_rules! unsigned_shrinker { } impl UnsignedShrinker { + #[allow(clippy::new_ret_no_self)] pub fn new(x: $ty) -> Box> { if x == 0 { super::empty_shrinker() @@ -745,7 +772,7 @@ macro_rules! unsigned_shrinker { Box::new( vec![0] .into_iter() - .chain(UnsignedShrinker { x: x, i: x / 2 }), + .chain(UnsignedShrinker { x, i: x / 2 }), ) } } @@ -756,7 +783,7 @@ macro_rules! unsigned_shrinker { fn next(&mut self) -> Option<$ty> { if self.x - self.i < self.x { let result = Some(self.x - self.i); - self.i = self.i / 2; + self.i /= 2; result } else { None @@ -769,7 +796,7 @@ macro_rules! unsigned_shrinker { macro_rules! unsigned_problem_values { ($t:ty) => { - &[<$t>::min_value(), 1, <$t>::max_value()] + &[<$t>::MIN, 1, <$t>::MAX] }; } @@ -778,11 +805,9 @@ macro_rules! unsigned_arbitrary { $( impl Arbitrary for $ty { fn arbitrary(g: &mut Gen) -> $ty { - match g.gen_range(0..10) { - 0 => { - *g.choose(unsigned_problem_values!($ty)).unwrap() - }, - _ => g.gen() + match g.random_range(0..10) { + 0 => *g.choose(unsigned_problem_values!($ty)).unwrap(), + _ => g.random() } } fn shrink(&self) -> Box> { @@ -795,7 +820,33 @@ macro_rules! unsigned_arbitrary { } unsigned_arbitrary! { - usize, u8, u16, u32, u64, u128 + u8, u16, u32, u64, u128 +} + +impl Arbitrary for usize { + fn arbitrary(g: &mut Gen) -> usize { + match g.random_range(0..10) { + 0 => *g.choose(unsigned_problem_values!(usize)).unwrap(), + _ => { + #[cfg(target_pointer_width = "16")] + { + g.random::() as usize + } + #[cfg(target_pointer_width = "32")] + { + g.random::() as usize + } + #[cfg(target_pointer_width = "64")] + { + g.random::() as usize + } + } + } + } + fn shrink(&self) -> Box> { + unsigned_shrinker!(usize); + shrinker::UnsignedShrinker::new(*self) + } } macro_rules! signed_shrinker { @@ -807,11 +858,12 @@ macro_rules! signed_shrinker { } impl SignedShrinker { + #[allow(clippy::new_ret_no_self)] pub fn new(x: $ty) -> Box> { if x == 0 { super::empty_shrinker() } else { - let shrinker = SignedShrinker { x: x, i: x / 2 }; + let shrinker = SignedShrinker { x, i: x / 2 }; let mut items = vec![0]; if shrinker.i < 0 && shrinker.x != <$ty>::MIN { items.push(shrinker.x.abs()); @@ -828,7 +880,7 @@ macro_rules! signed_shrinker { || (self.x - self.i).abs() < self.x.abs() { let result = Some(self.x - self.i); - self.i = self.i / 2; + self.i /= 2; result } else { None @@ -841,7 +893,7 @@ macro_rules! signed_shrinker { macro_rules! signed_problem_values { ($t:ty) => { - &[<$t>::min_value(), 0, <$t>::max_value()] + &[<$t>::MIN, 0, <$t>::MAX] }; } @@ -850,11 +902,9 @@ macro_rules! signed_arbitrary { $( impl Arbitrary for $ty { fn arbitrary(g: &mut Gen) -> $ty { - match g.gen_range(0..10) { - 0 => { - *g.choose(signed_problem_values!($ty)).unwrap() - }, - _ => g.gen() + match g.random_range(0..10) { + 0 => *g.choose(signed_problem_values!($ty)).unwrap(), + _ => g.random() } } fn shrink(&self) -> Box> { @@ -867,27 +917,58 @@ macro_rules! signed_arbitrary { } signed_arbitrary! { - isize, i8, i16, i32, i64, i128 + i8, i16, i32, i64, i128 +} + +impl Arbitrary for isize { + fn arbitrary(g: &mut Gen) -> isize { + match g.random_range(0..10) { + 0 => *g.choose(signed_problem_values!(isize)).unwrap(), + _ => { + #[cfg(target_pointer_width = "16")] + { + g.random::() as isize + } + #[cfg(target_pointer_width = "32")] + { + g.random::() as isize + } + #[cfg(target_pointer_width = "64")] + { + g.random::() as isize + } + } + } + } + fn shrink(&self) -> Box> { + signed_shrinker!(isize); + shrinker::SignedShrinker::new(*self) + } } macro_rules! float_problem_values { - ($path:path) => {{ - // hack. see: https://github.com/rust-lang/rust/issues/48067 - use $path as p; - &[p::NAN, p::NEG_INFINITY, p::MIN, -0., 0., p::MAX, p::INFINITY] + ($t:ty) => {{ + &[ + <$t>::NAN, + <$t>::NEG_INFINITY, + <$t>::MIN, + -0., + 0., + <$t>::MAX, + <$t>::INFINITY, + ] }}; } macro_rules! float_arbitrary { - ($($t:ty, $path:path, $shrinkable:ty),+) => {$( + ($($t:ty, $shrinkable:ty),+) => {$( impl Arbitrary for $t { fn arbitrary(g: &mut Gen) -> $t { - match g.gen_range(0..10) { - 0 => *g.choose(float_problem_values!($path)).unwrap(), + match g.random_range(0..10) { + 0 => *g.choose(float_problem_values!($t)).unwrap(), _ => { - use $path as p; - let exp = g.gen_range((0.)..p::MAX_EXP as i16 as $t); - let mantissa = g.gen_range((1.)..2.); + let exp = g.random_range((0.)..<$t>::MAX_EXP as i16 as $t); + let mantissa = g.random_range((1.)..2.); let sign = *g.choose(&[-1., 1.]).unwrap(); sign * mantissa * exp.exp2() } @@ -902,7 +983,7 @@ macro_rules! float_arbitrary { )*}; } -float_arbitrary!(f32, std::f32, i32, f64, std::f64, i64); +float_arbitrary!(f32, i32, f64, i64); macro_rules! unsigned_non_zero_shrinker { ($ty:tt) => { @@ -913,6 +994,7 @@ macro_rules! unsigned_non_zero_shrinker { } impl UnsignedNonZeroShrinker { + #[allow(clippy::new_ret_no_self)] pub fn new(x: $ty) -> Box> { debug_assert!(x > 0); @@ -921,7 +1003,7 @@ macro_rules! unsigned_non_zero_shrinker { } else { Box::new( std::iter::once(1).chain( - UnsignedNonZeroShrinker { x: x, i: x / 2 }, + UnsignedNonZeroShrinker { x, i: x / 2 }, ), ) } @@ -934,7 +1016,7 @@ macro_rules! unsigned_non_zero_shrinker { fn next(&mut self) -> Option<$ty> { if self.x - self.i < self.x { let result = Some(self.x - self.i); - self.i = self.i / 2; + self.i /= 2; result } else { None @@ -950,11 +1032,11 @@ macro_rules! unsigned_non_zero_arbitrary { $( impl Arbitrary for $ty { fn arbitrary(g: &mut Gen) -> $ty { - let mut v: $inner = g.gen(); + let mut v = $inner::arbitrary(g); if v == 0 { v += 1; } - $ty::new(v).expect("non-zero value contsturction failed") + $ty::new(v).expect("non-zero value construction failed") } fn shrink(&self) -> Box> { @@ -982,13 +1064,13 @@ impl Arbitrary for Wrapping { Wrapping(T::arbitrary(g)) } fn shrink(&self) -> Box>> { - Box::new(self.0.shrink().map(|inner| Wrapping(inner))) + Box::new(self.0.shrink().map(Wrapping)) } } impl Arbitrary for Bound { fn arbitrary(g: &mut Gen) -> Bound { - match g.gen_range(0..3) { + match g.random_range(0..3) { 0 => Bound::Included(T::arbitrary(g)), 1 => Bound::Excluded(T::arbitrary(g)), _ => Bound::Unbounded, @@ -1065,9 +1147,9 @@ impl Arbitrary for RangeFull { } impl Arbitrary for Duration { - fn arbitrary(gen: &mut Gen) -> Self { - let seconds = gen.gen_range(0..gen.size() as u64); - let nanoseconds = gen.gen_range(0..1_000_000); + fn arbitrary(rng: &mut Gen) -> Self { + let seconds = rng.random_range(0..rng.size() as u64); + let nanoseconds = rng.random_range(0..1_000_000); Duration::new(seconds, nanoseconds) } @@ -1101,9 +1183,9 @@ impl Arbitrary for Arc { } impl Arbitrary for SystemTime { - fn arbitrary(gen: &mut Gen) -> Self { - let after_epoch = bool::arbitrary(gen); - let duration = Duration::arbitrary(gen); + fn arbitrary(rng: &mut Gen) -> Self { + let after_epoch = bool::arbitrary(rng); + let duration = Duration::arbitrary(rng); if after_epoch { UNIX_EPOCH + duration } else { @@ -1138,7 +1220,7 @@ mod test { #[test] fn arby_unit() { - assert_eq!(arby::<()>(), ()); + let _: () = arby::<()>(); } macro_rules! arby_int { @@ -1151,8 +1233,8 @@ mod test { }; assert!(problems.all(|p| arbys.any(|arby| arby == *p)), "Arbitrary does not generate all problematic values"); - let max = <$t>::max_value(); - let mid = (max + <$t>::min_value()) / 2; + let max = <$t>::MAX; + let mid = (max + <$t>::MIN) / 2; // split full range of $t into chunks // Arbitrary must return some value in each chunk let double_chunks: $t = 9; @@ -1183,14 +1265,13 @@ mod test { } macro_rules! arby_float { - ($($t:ty, $path:path),+) => {$({ - use $path as p; + ($($t:ty),+) => {$({ let mut arbys = (0..1_000_000).map(|_| arby::<$t>()); //NaN != NaN assert!(arbys.any(|f| f.is_nan()), "Arbitrary does not generate the problematic value NaN" ); - for p in float_problem_values!($path).iter().filter(|f| !f.is_nan()) { + for p in float_problem_values!($t).iter().filter(|f| !f.is_nan()) { assert!(arbys.any(|arby| arby == *p), "Arbitrary does not generate the problematic value {}", p @@ -1202,7 +1283,7 @@ mod test { let chunks = double_chunks * 2; // chunks must be even let lim = (-double_chunks..=double_chunks) .map(|idx| <$t>::from(idx)) - .map(|idx| p::MAX/(<$t>::from(chunks/2)) * idx); + .map(|idx| <$t>::MAX/(<$t>::from(chunks/2)) * idx); let mut lim = lim.peekable(); while let (Some(low), Some(&high)) = (lim.next(), lim.peek()) { assert!( @@ -1217,7 +1298,7 @@ mod test { #[test] fn arby_float() { - arby_float!(f32, std::f32, f64, std::f64); + arby_float!(f32, f64); } fn arby() -> A { @@ -1365,11 +1446,11 @@ mod test { for n in v { let found = shrunk.iter().any(|&i| i == n); if !found { - panic!(format!( + panic!( "Element {:?} was not found \ in shrink results {:?}", n, shrunk - )); + ); } } } @@ -1560,6 +1641,7 @@ mod test { eq(Unbounded::, vec![]); } + #[allow(clippy::reversed_empty_ranges)] #[test] fn ranges() { ordered_eq(0..0, vec![]); diff --git a/src/lib.rs b/src/lib.rs index 58dc99d..d3a5ba4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,11 @@ This crate is a port of [Haskell's QuickCheck](https://hackage.haskell.org/package/QuickCheck). +QuickCheck is a library for random testing of program properties. The +programmer provides a specification of the program, in the form of properties +which functions should satisfy, and QuickCheck then tests that the properties +hold in a large number of randomly generated cases. + For detailed examples, please see the [README](https://github.com/BurntSushi/quickcheck). diff --git a/src/tester.rs b/src/tester.rs index e2eaa20..5f70f86 100644 --- a/src/tester.rs +++ b/src/tester.rs @@ -8,12 +8,13 @@ use crate::{ Arbitrary, Gen, }; -/// The main QuickCheck type for setting configuration and running QuickCheck. +/// The main `QuickCheck` type for setting configuration and running +/// `QuickCheck`. pub struct QuickCheck { tests: u64, max_tests: u64, min_tests_passed: u64, - gen: Gen, + rng: Gen, } fn qc_tests() -> u64 { @@ -48,28 +49,39 @@ fn qc_min_tests_passed() -> u64 { } } +impl Default for QuickCheck { + fn default() -> Self { + Self::new() + } +} + impl QuickCheck { - /// Creates a new QuickCheck value. + /// Creates a new `QuickCheck` value. /// - /// This can be used to run QuickCheck on things that implement `Testable`. - /// You may also adjust the configuration, such as the number of tests to - /// run. + /// This can be used to run `QuickCheck` on things that implement + /// `Testable`. You may also adjust the configuration, such as the + /// number of tests to run. /// /// By default, the maximum number of passed tests is set to `100`, the max /// number of overall tests is set to `10000` and the generator is created /// with a size of `100`. pub fn new() -> QuickCheck { - let gen = Gen::new(qc_gen_size()); + let rng = Gen::new(qc_gen_size()); let tests = qc_tests(); let max_tests = cmp::max(tests, qc_max_tests()); let min_tests_passed = qc_min_tests_passed(); - QuickCheck { tests, max_tests, min_tests_passed, gen } + QuickCheck { tests, max_tests, min_tests_passed, rng } + } + + /// Set the random number generator to be used by `QuickCheck`. + pub fn set_rng(self, rng: Gen) -> QuickCheck { + QuickCheck { rng, ..self } } - /// Set the random number generator to be used by QuickCheck. - pub fn gen(self, gen: Gen) -> QuickCheck { - QuickCheck { gen, ..self } + #[deprecated(since = "1.1.0", note = "use `set_rng` instead")] + pub fn r#gen(self, rng: Gen) -> QuickCheck { + self.set_rng(rng) } /// Set the number of tests to run. @@ -86,7 +98,7 @@ impl QuickCheck { /// Set the maximum number of tests to run. /// /// The number of invocations of a property will never exceed this number. - /// This is necessary to cap the number of tests because QuickCheck + /// This is necessary to cap the number of tests because `QuickCheck` /// properties can discard tests. pub fn max_tests(mut self, max_tests: u64) -> QuickCheck { self.max_tests = max_tests; @@ -118,7 +130,7 @@ impl QuickCheck { if n_tests_passed >= self.tests { break; } - match f.result(&mut self.gen) { + match f.result(&mut self.rng) { TestResult { status: Pass, .. } => n_tests_passed += 1, TestResult { status: Discard, .. } => continue, r @ TestResult { status: Fail, .. } => return Err(r), @@ -137,7 +149,7 @@ impl QuickCheck { /// /// Note that if the environment variable `RUST_LOG` is set to enable /// `info` level log messages for the `quickcheck` crate, then this will - /// include output on how many QuickCheck tests were passed. + /// include output on how many `QuickCheck` tests were passed. /// /// # Example /// @@ -162,21 +174,21 @@ impl QuickCheck { let n_tests_passed = match self.quicktest(f) { Ok(n_tests_passed) => n_tests_passed, - Err(result) => panic!(result.failed_msg()), + Err(result) => panic!("{}", result.failed_msg()), }; if n_tests_passed >= self.min_tests_passed { - info!("(Passed {} QuickCheck tests.)", n_tests_passed) + info!("(Passed {} QuickCheck tests.)", n_tests_passed); } else { panic!( "(Unable to generate enough tests, {} not discarded.)", n_tests_passed - ) + ); } } } -/// Convenience function for running QuickCheck. +/// Convenience function for running `QuickCheck`. /// /// This is an alias for `QuickCheck::new().quickcheck(f)`. pub fn quickcheck(f: A) { @@ -186,15 +198,15 @@ pub fn quickcheck(f: A) { /// Describes the status of a single instance of a test. /// /// All testable things must be capable of producing a `TestResult`. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] pub struct TestResult { status: Status, - arguments: Vec, + arguments: Option>, err: Option, } /// Whether a test has passed, failed or been discarded. -#[derive(Clone, Debug)] +#[derive(Clone, Debug, PartialEq)] enum Status { Pass, Fail, @@ -224,7 +236,7 @@ impl TestResult { /// When a test is discarded, `quickcheck` will replace it with a /// fresh one (up to a certain limit). pub fn discard() -> TestResult { - TestResult { status: Discard, arguments: vec![], err: None } + TestResult { status: Discard, arguments: None, err: None } } /// Converts a `bool` to a `TestResult`. A `true` value indicates that @@ -233,7 +245,7 @@ impl TestResult { pub fn from_bool(b: bool) -> TestResult { TestResult { status: if b { Pass } else { Fail }, - arguments: vec![], + arguments: None, err: None, } } @@ -266,21 +278,34 @@ impl TestResult { } fn failed_msg(&self) -> String { + let arguments_msg = match self.arguments { + None => "No Arguments Provided".to_owned(), + Some(ref args) => format!("Arguments: ({})", args.join(", ")), + }; match self.err { - None => format!( - "[quickcheck] TEST FAILED. Arguments: ({})", - self.arguments.join(", ") - ), + None => format!("[quickcheck] TEST FAILED. {arguments_msg}"), Some(ref err) => format!( - "[quickcheck] TEST FAILED (runtime error). \ - Arguments: ({})\nError: {}", - self.arguments.join(", "), - err + "[quickcheck] TEST FAILED (runtime error). {arguments_msg}\nError: {err}" ), } } } +impl From for TestResult { + /// A shorter way of producing a `TestResult` from a `bool`. + /// + /// # Example + /// + /// ```rust + /// # use quickcheck::TestResult; + /// let result: TestResult = (2 > 1).into(); + /// assert_eq!(result, TestResult::passed()); + /// ``` + fn from(b: bool) -> TestResult { + TestResult::from_bool(b) + } +} + /// `Testable` describes types (e.g., a function) whose values can be /// tested. /// @@ -322,14 +347,14 @@ where fn result(&self, g: &mut Gen) -> TestResult { match *self { Ok(ref r) => r.result(g), - Err(ref err) => TestResult::error(format!("{:?}", err)), + Err(ref err) => TestResult::error(format!("{err:?}")), } } } /// Return a vector of the debug formatting of each item in `args` fn debug_reprs(args: &[&dyn Debug]) -> Vec { - args.iter().map(|x| format!("{:?}", x)).collect() + args.iter().map(|x| format!("{x:?}")).collect() } macro_rules! testable_fn { @@ -339,47 +364,34 @@ impl Testable for fn($($name),*) -> T { #[allow(non_snake_case)] fn result(&self, g: &mut Gen) -> TestResult { - fn shrink_failure( - g: &mut Gen, - self_: fn($($name),*) -> T, - a: ($($name,)*), - ) -> Option { - for t in a.shrink() { + let self_ = *self; + let a: ($($name,)*) = Arbitrary::arbitrary(g); + let ( $($name,)* ) = a.clone(); + let mut r = safe(move || {self_($($name),*)}).result(g); + + if r.is_failure() { + let mut a = a.shrink(); + while let Some(t) = a.next() { let ($($name,)*) = t.clone(); let mut r_new = safe(move || {self_($($name),*)}).result(g); if r_new.is_failure() { { let ($(ref $name,)*) : ($($name,)*) = t; - r_new.arguments = debug_reprs(&[$($name),*]); + r_new.arguments = Some(debug_reprs(&[$($name),*])); } - // The shrunk value *does* witness a failure, so keep - // trying to shrink it. - let shrunk = shrink_failure(g, self_, t); + // The shrunk value *does* witness a failure, so remember + // it for now + r = r_new; - // If we couldn't witness a failure on any shrunk value, - // then return the failure we already have. - return Some(shrunk.unwrap_or(r_new)) + // ... and switch over to that value, i.e. try to shrink + // it further. + a = t.shrink() } } - None } - let self_ = *self; - let a: ($($name,)*) = Arbitrary::arbitrary(g); - let ( $($name,)* ) = a.clone(); - let mut r = safe(move || {self_($($name),*)}).result(g); - - { - let ( $(ref $name,)* ) = a; - r.arguments = debug_reprs(&[$($name),*]); - } - match r.status { - Pass|Discard => r, - Fail => { - shrink_failure(g, self_, a).unwrap_or(r) - } - } + r } }}} @@ -412,10 +424,6 @@ where }) } -/// Convenient aliases. -trait AShow: Arbitrary + Debug {} -impl AShow for A {} - #[cfg(test)] mod test { use crate::{Gen, QuickCheck}; @@ -429,7 +437,7 @@ mod test { .quicktest(thetest as fn(vals: Vec) -> bool) .unwrap_err(); let expected_argument = format!("{:?}", [true, true]); - assert_eq!(failing_case.arguments, vec![expected_argument]); + assert_eq!(failing_case.arguments, Some(vec![expected_argument])); } #[test] @@ -437,7 +445,9 @@ mod test { fn t(_: i8) -> bool { true } - QuickCheck::new().gen(Gen::new(129)).quickcheck(t as fn(i8) -> bool); + QuickCheck::new() + .set_rng(Gen::new(129)) + .quickcheck(t as fn(i8) -> bool); } #[test] diff --git a/src/tests.rs b/src/tests.rs index 465ef15..e1b2f6e 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1,4 +1,3 @@ -use std::cmp::Ord; use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; use std::ffi::CString; @@ -13,13 +12,12 @@ fn prop_oob() { let zero: Vec = vec![]; zero[0] } - match QuickCheck::new().quicktest(prop as fn() -> bool) { - Ok(n) => panic!( + if let Ok(n) = QuickCheck::new().quicktest(prop as fn() -> bool) { + panic!( "prop_oob should fail with a runtime error \ - but instead it passed {} tests.", + but instead it passed {} tests.", n - ), - _ => return, + ); } } @@ -65,12 +63,12 @@ fn reverse_single() { fn reverse_app() { fn prop(xs: Vec, ys: Vec) -> bool { let mut app = xs.clone(); - app.extend(ys.iter().cloned()); + app.extend(ys.iter().copied()); let app_rev: Vec = app.into_iter().rev().collect(); let rxs: Vec = xs.into_iter().rev().collect(); let mut rev_app = ys.into_iter().rev().collect::>(); - rev_app.extend(rxs.into_iter()); + rev_app.extend(rxs); app_rev == rev_app } @@ -92,7 +90,7 @@ fn max() { #[test] fn sort() { fn prop(mut xs: Vec) -> bool { - xs.sort_by(|x, y| x.cmp(y)); + xs.sort_unstable(); for i in xs.windows(2) { if i[0] > i[1] { return false; @@ -142,7 +140,7 @@ fn sieve_not_prime() { fn sieve_not_all_primes() { fn prop_prime_iff_in_the_sieve(n: u8) -> bool { let n = n as usize; - sieve(n) == (0..(n + 1)).filter(|&i| is_prime(i)).collect::>() + sieve(n) == (0..=n).filter(|&i| is_prime(i)).collect::>() } quickcheck(prop_prime_iff_in_the_sieve as fn(u8) -> bool); } @@ -170,7 +168,7 @@ fn testable_unit() { #[test] fn testable_unit_panic() { fn panic() { - panic!() + panic!(); } assert!(QuickCheck::new().quicktest(panic as fn()).is_err()); } @@ -180,7 +178,9 @@ fn regression_issue_83() { fn prop(_: u8) -> bool { true } - QuickCheck::new().gen(Gen::new(1024)).quickcheck(prop as fn(u8) -> bool) + QuickCheck::new() + .set_rng(Gen::new(1024)) + .quickcheck(prop as fn(u8) -> bool); } #[test] @@ -188,7 +188,9 @@ fn regression_issue_83_signed() { fn prop(_: i8) -> bool { true } - QuickCheck::new().gen(Gen::new(1024)).quickcheck(prop as fn(i8) -> bool) + QuickCheck::new() + .set_rng(Gen::new(1024)) + .quickcheck(prop as fn(i8) -> bool); } // Test that we can show the message after panic @@ -242,7 +244,7 @@ fn all_tests_discarded_min_tests_passed_set() { QuickCheck::new() .tests(16) .min_tests_passed(8) - .quickcheck(prop_discarded as fn(u8) -> TestResult) + .quickcheck(prop_discarded as fn(u8) -> TestResult); } #[test] @@ -251,7 +253,7 @@ fn all_tests_discarded_min_tests_passed_missing() { TestResult::discard() } - QuickCheck::new().quickcheck(prop_discarded as fn(u8) -> TestResult) + QuickCheck::new().quickcheck(prop_discarded as fn(u8) -> TestResult); } quickcheck! {