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}