partial_io/
quickcheck_types.rs

1// Copyright (c) The partial-io Contributors
2// SPDX-License-Identifier: MIT
3
4//! `QuickCheck` support for partial IO operations.
5//!
6//! This module allows sequences of [`PartialOp`]s to be randomly generated. These
7//! sequences can then be fed into a [`PartialRead`], [`PartialWrite`],
8//! [`PartialAsyncRead`] or [`PartialAsyncWrite`].
9//!
10//! Once `quickcheck` has identified a failing test case, it will shrink the
11//! sequence of `PartialOp`s and find a minimal test case. This minimal case can
12//! then be used to reproduce the issue.
13//!
14//! To generate random sequences of operations, write a `quickcheck` test with a
15//! `PartialWithErrors<GE>` input, where `GE` implements [`GenError`]. Then pass
16//! the sequence in as the second argument to the partial wrapper.
17//!
18//! Several implementations of `GenError` are provided. These can be used to
19//! customize the sorts of errors generated. For even more customization, you
20//! can write your own `GenError` implementation.
21//!
22//! # Examples
23//!
24//! ```rust
25//! use partial_io::quickcheck_types::{GenInterrupted, PartialWithErrors};
26//! use quickcheck::quickcheck;
27//!
28//! quickcheck! {
29//!     fn test_something(seq: PartialWithErrors<GenInterrupted>) -> bool {
30//!         // Example buffer to read from, substitute with your own.
31//!         let reader = std::io::repeat(42);
32//!         let partial_reader = PartialRead::new(reader, seq);
33//!         // ...
34//!
35//!         true
36//!     }
37//! }
38//! ```
39//!
40//! For a detailed example, see `examples/buggy_write.rs` in this repository.
41//!
42//! For a real-world example, see the [tests in `bzip2-rs`].
43//!
44//! [`PartialOp`]: ../struct.PartialOp.html
45//! [`PartialRead`]: ../struct.PartialRead.html
46//! [`PartialWrite`]: ../struct.PartialWrite.html
47//! [`PartialAsyncRead`]: ../struct.PartialAsyncRead.html
48//! [`PartialAsyncWrite`]: ../struct.PartialAsyncWrite.html
49//! [`GenError`]: trait.GenError.html
50//! [tests in `bzip2-rs`]: https://github.com/alexcrichton/bzip2-rs/blob/master/src/write.rs
51
52use crate::PartialOp;
53use quickcheck::{empty_shrinker, Arbitrary, Gen};
54use rand::{rngs::SmallRng, Rng, SeedableRng};
55use std::{io, marker::PhantomData, ops::Deref};
56
57/// Given a custom error generator, randomly generate a list of `PartialOp`s.
58#[derive(Clone, Debug)]
59pub struct PartialWithErrors<GE> {
60    items: Vec<PartialOp>,
61    _marker: PhantomData<GE>,
62}
63
64impl<GE> IntoIterator for PartialWithErrors<GE> {
65    type Item = PartialOp;
66    type IntoIter = ::std::vec::IntoIter<PartialOp>;
67
68    fn into_iter(self) -> Self::IntoIter {
69        self.items.into_iter()
70    }
71}
72
73impl<GE> Deref for PartialWithErrors<GE> {
74    type Target = [PartialOp];
75    fn deref(&self) -> &Self::Target {
76        self.items.deref()
77    }
78}
79
80/// Represents a way to generate `io::ErrorKind` instances.
81///
82/// See [the module level documentation](index.html) for more.
83pub trait GenError: Clone + Default + Send {
84    /// Optionally generate an `io::ErrorKind` instance.
85    fn gen_error(&mut self, g: &mut Gen) -> Option<io::ErrorKind>;
86}
87
88/// Generate an `ErrorKind::Interrupted` error 20% of the time.
89///
90/// See [the module level documentation](index.html) for more.
91#[derive(Clone, Debug, Default)]
92pub struct GenInterrupted;
93
94/// Generate an `ErrorKind::WouldBlock` error 20% of the time.
95///
96/// See [the module level documentation](index.html) for more.
97#[derive(Clone, Debug, Default)]
98pub struct GenWouldBlock;
99
100/// Generate `Interrupted` and `WouldBlock` errors 10% of the time each.
101///
102/// See [the module level documentation](index.html) for more.
103#[derive(Clone, Debug, Default)]
104pub struct GenInterruptedWouldBlock;
105
106macro_rules! impl_gen_error {
107    ($id: ident, [$($errors:expr),+]) => {
108        impl GenError for $id {
109            fn gen_error(&mut self, g: &mut Gen) -> Option<io::ErrorKind> {
110                // 20% chance to generate an error.
111                let mut rng = SmallRng::from_entropy();
112                if rng.gen_ratio(1, 5) {
113                    Some(g.choose(&[$($errors,)*]).unwrap().clone())
114                } else {
115                    None
116                }
117            }
118        }
119    }
120}
121
122impl_gen_error!(GenInterrupted, [io::ErrorKind::Interrupted]);
123impl_gen_error!(GenWouldBlock, [io::ErrorKind::WouldBlock]);
124impl_gen_error!(
125    GenInterruptedWouldBlock,
126    [io::ErrorKind::Interrupted, io::ErrorKind::WouldBlock]
127);
128
129/// Do not generate any errors. The only operations generated will be
130/// `PartialOp::Limited` instances.
131///
132/// See [the module level documentation](index.html) for more.
133#[derive(Clone, Debug, Default)]
134pub struct GenNoErrors;
135
136impl GenError for GenNoErrors {
137    fn gen_error(&mut self, _g: &mut Gen) -> Option<io::ErrorKind> {
138        None
139    }
140}
141
142impl<GE> Arbitrary for PartialWithErrors<GE>
143where
144    GE: GenError + 'static,
145{
146    fn arbitrary(g: &mut Gen) -> Self {
147        let size = g.size();
148        // Generate a sequence of operations. A uniform distribution for this is
149        // fine because the goal is to shake bugs out relatively effectively.
150        let mut gen_error = GE::default();
151        let items: Vec<_> = (0..size)
152            .map(|_| {
153                match gen_error.gen_error(g) {
154                    Some(err) => PartialOp::Err(err),
155                    // Don't generate 0 because for writers it can mean that
156                    // writes are no longer accepted.
157                    None => {
158                        let mut rng = SmallRng::from_entropy();
159                        PartialOp::Limited(rng.gen_range(1..size))
160                    }
161                }
162            })
163            .collect();
164        PartialWithErrors {
165            items,
166            _marker: PhantomData,
167        }
168    }
169
170    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
171        Box::new(self.items.clone().shrink().map(|items| PartialWithErrors {
172            items,
173            _marker: PhantomData,
174        }))
175    }
176}
177
178impl Arbitrary for PartialOp {
179    fn arbitrary(_g: &mut Gen) -> Self {
180        // We only use this for shrink, so we don't need to implement this.
181        unimplemented!();
182    }
183
184    fn shrink(&self) -> Box<dyn Iterator<Item = Self>> {
185        match *self {
186            // Skip 0 because for writers it can mean that writes are no longer
187            // accepted.
188            PartialOp::Limited(n) => {
189                Box::new(n.shrink().filter(|k| k != &0).map(PartialOp::Limited))
190            }
191            _ => empty_shrinker(),
192        }
193    }
194}