• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..03-May-2022-

examples/H03-May-2022-241134

proptest-regressions/H03-May-2022-32

src/H03-May-2022-22,55413,932

test-persistence-location/H03-May-2022-6554

.cargo-checksum.jsonH A D03-May-202289 11

.cargo_vcs_info.jsonH A D01-Jan-197074 65

CHANGELOG.mdH A D01-Jan-197030.5 KiB886579

Cargo.lockH A D01-Jan-19709.1 KiB353311

Cargo.tomlH A D01-Jan-19702.3 KiB9277

Cargo.toml.orig-cargoH A D01-Jan-19702.8 KiB12093

LICENSE-APACHEH A D01-Jan-197010.6 KiB202169

LICENSE-MITH A D01-Jan-19701 KiB2622

README.mdH A D01-Jan-197014.7 KiB400322

README.md

1# Proptest
2
3[![Build Status](https://github.com/AltSysrq/proptest/workflows/Rust/badge.svg?branch=master)](https://github.com/AltSysrq/proptest/actions)
4[![Build status](https://ci.appveyor.com/api/projects/status/ofe98xfthbx1m608/branch/master?svg=true)](https://ci.appveyor.com/project/AltSysrq/proptest/branch/master)
5[![](http://meritbadge.herokuapp.com/proptest)](https://crates.io/crates/proptest)
6[![](https://img.shields.io/website/https/altsysrq.github.io/proptest-book.svg)][book]
7[![](https://docs.rs/proptest/badge.svg)][api-docs]
8
9[book]: https://altsysrq.github.io/proptest-book/intro.html
10[api-docs]: https://altsysrq.github.io/rustdoc/proptest/latest/proptest/
11## Introduction
12
13Proptest is a property testing framework (i.e., the QuickCheck family)
14inspired by the [Hypothesis](http://hypothesis.works/) framework for
15Python. It allows to test that certain properties of your code hold for
16arbitrary inputs, and if a failure is found, automatically finds the
17minimal test case to reproduce the problem. Unlike QuickCheck, generation
18and shrinking is defined on a per-value basis instead of per-type, which
19makes it more flexible and simplifies composition.
20
21### Status of this crate
22
23The crate is fairly close to being feature-complete and has not seen
24substantial architectural changes in quite some time. At this point, it mainly
25sees passive maintenance.
26
27See the [changelog](https://github.com/AltSysrq/proptest/blob/master/proptest/CHANGELOG.md)
28for a full list of substantial historical changes, breaking and otherwise.
29
30### What is property testing?
31
32_Property testing_ is a system of testing code by checking that certain
33properties of its output or behaviour are fulfilled for all inputs. These
34inputs are generated automatically, and, critically, when a failing input
35is found, the input is automatically reduced to a _minimal_ test case.
36
37Property testing is best used to compliment traditional unit testing (i.e.,
38using specific inputs chosen by hand). Traditional tests can test specific
39known edge cases, simple inputs, and inputs that were known in the past to
40reveal bugs, whereas property tests will search for more complicated inputs
41that cause problems.
42## Getting Started
43
44Let's say we want to make a function that parses dates of the form
45`YYYY-MM-DD`. We're not going to worry about _validating_ the date, any
46triple of integers is fine. So let's bang something out real quick.
47
48```rust,no_run
49fn parse_date(s: &str) -> Option<(u32, u32, u32)> {
50    if 10 != s.len() { return None; }
51    if "-" != &s[4..5] || "-" != &s[7..8] { return None; }
52
53    let year = &s[0..4];
54    let month = &s[6..7];
55    let day = &s[8..10];
56
57    year.parse::<u32>().ok().and_then(
58        |y| month.parse::<u32>().ok().and_then(
59            |m| day.parse::<u32>().ok().map(
60                |d| (y, m, d))))
61}
62```
63
64It compiles, that means it works, right? Maybe not, let's add some tests.
65
66```rust,ignore
67#[test]
68fn test_parse_date() {
69    assert_eq!(None, parse_date("2017-06-1"));
70    assert_eq!(None, parse_date("2017-06-170"));
71    assert_eq!(None, parse_date("2017006-17"));
72    assert_eq!(None, parse_date("2017-06017"));
73    assert_eq!(Some((2017, 06, 17)), parse_date("2017-06-17"));
74}
75```
76
77Tests pass, deploy to production! But now your application starts crashing,
78and people are upset that you moved Christmas to February. Maybe we need to
79be a bit more thorough.
80
81In `Cargo.toml`, add
82
83```toml
84[dev-dependencies]
85proptest = "1.0.0"
86```
87
88Now we can add some property tests to our date parser. But how do we test
89the date parser for arbitrary inputs, without making another date parser in
90the test to validate it? We won't need to as long as we choose our inputs
91and properties correctly. But before correctness, there's actually an even
92simpler property to test: _The function should not crash._ Let's start
93there.
94
95```rust,ignore
96// Bring the macros and other important things into scope.
97use proptest::prelude::*;
98
99proptest! {
100    #[test]
101    fn doesnt_crash(s in "\\PC*") {
102        parse_date(&s);
103    }
104}
105```
106
107What this does is take a literally random `&String` (ignore `\\PC*` for the
108moment, we'll get back to that — if you've already figured it out, contain
109your excitement for a bit) and give it to `parse_date()` and then throw the
110output away.
111
112When we run this, we get a bunch of scary-looking output, eventually ending
113with
114
115```text
116thread 'main' panicked at 'Test failed: byte index 4 is not a char boundary; it is inside 'ௗ' (bytes 2..5) of `aAௗ0㌀0`; minimal failing input: s = "aAௗ0㌀0"
117	successes: 102
118	local rejects: 0
119	global rejects: 0
120'
121```
122
123If we look at the top directory after the test fails, we'll see a new
124`proptest-regressions` directory, which contains some files corresponding to
125source files containing failing test cases. These are [_failure
126persistence_](https://altsysrq.github.io/proptest-book/proptest/failure-persistence.html)
127files. The first thing we should do is add these to source control.
128
129```text
130$ git add proptest-regressions
131```
132
133The next thing we should do is copy the failing case to a traditional unit
134test since it has exposed a bug not similar to what we've tested in the
135past.
136
137```rust,ignore
138#[test]
139fn test_unicode_gibberish() {
140    assert_eq!(None, parse_date("aAௗ0㌀0"));
141}
142```
143
144Now, let's see what happened... we forgot about UTF-8! You can't just
145blindly slice strings since you could split a character, in this case that
146Tamil diacritic placed atop other characters in the string.
147
148In the interest of making the code changes as small as possible, we'll just
149check that the string is ASCII and reject anything that isn't.
150
151```rust,no_run
152fn parse_date(s: &str) -> Option<(u32, u32, u32)> {
153    if 10 != s.len() { return None; }
154
155    // NEW: Ignore non-ASCII strings so we don't need to deal with Unicode.
156    if !s.is_ascii() { return None; }
157
158    if "-" != &s[4..5] || "-" != &s[7..8] { return None; }
159
160    let year = &s[0..4];
161    let month = &s[6..7];
162    let day = &s[8..10];
163
164    year.parse::<u32>().ok().and_then(
165        |y| month.parse::<u32>().ok().and_then(
166            |m| day.parse::<u32>().ok().map(
167                |d| (y, m, d))))
168}
169```
170
171The tests pass now! But we know there are still more problems, so let's
172test more properties.
173
174Another property we want from our code is that it parses every valid date.
175We can add another test to the `proptest!` section:
176
177```rust,ignore
178proptest! {
179    // snip...
180
181    #[test]
182    fn parses_all_valid_dates(s in "[0-9]{4}-[0-9]{2}-[0-9]{2}") {
183        parse_date(&s).unwrap();
184    }
185}
186```
187
188The thing to the right-hand side of `in` is actually a *regular
189expression*, and `s` is chosen from strings which match it. So in our
190previous test, `"\\PC*"` was generating arbitrary strings composed of
191arbitrary non-control characters. Now, we generate things in the YYYY-MM-DD
192format.
193
194The new test passes, so let's move on to something else.
195
196The final property we want to check is that the dates are actually parsed
197_correctly_. Now, we can't do this by generating strings — we'd end up just
198reimplementing the date parser in the test! Instead, we start from the
199expected output, generate the string, and check that it gets parsed back.
200
201```rust,ignore
202proptest! {
203    // snip...
204
205    #[test]
206    fn parses_date_back_to_original(y in 0u32..10000,
207                                    m in 1u32..13, d in 1u32..32) {
208        let (y2, m2, d2) = parse_date(
209            &format!("{:04}-{:02}-{:02}", y, m, d)).unwrap();
210        // prop_assert_eq! is basically the same as assert_eq!, but doesn't
211        // cause a bunch of panic messages to be printed on intermediate
212        // test failures. Which one to use is largely a matter of taste.
213        prop_assert_eq!((y, m, d), (y2, m2, d2));
214    }
215}
216```
217
218Here, we see that besides regexes, we can use any expression which is a
219`proptest::strategy::Strategy`, in this case, integer ranges.
220
221The test fails when we run it. Though there's not much output this time.
222
223```text
224thread 'main' panicked at 'Test failed: assertion failed: `(left == right)` (left: `(0, 10, 1)`, right: `(0, 0, 1)`) at examples/dateparser_v2.rs:46; minimal failing input: y = 0, m = 10, d = 1
225	successes: 2
226	local rejects: 0
227	global rejects: 0
228', examples/dateparser_v2.rs:33
229note: Run with `RUST_BACKTRACE=1` for a backtrace.
230```
231
232The failing input is `(y, m, d) = (0, 10, 1)`, which is a rather specific
233output. Before thinking about why this breaks the code, let's look at what
234proptest did to arrive at this value. At the start of our test function,
235insert
236
237```rust,ignore
238    println!("y = {}, m = {}, d = {}", y, m, d);
239```
240
241Running the test again, we get something like this:
242
243```text
244y = 2497, m = 8, d = 27
245y = 9641, m = 8, d = 18
246y = 7360, m = 12, d = 20
247y = 3680, m = 12, d = 20
248y = 1840, m = 12, d = 20
249y = 920, m = 12, d = 20
250y = 460, m = 12, d = 20
251y = 230, m = 12, d = 20
252y = 115, m = 12, d = 20
253y = 57, m = 12, d = 20
254y = 28, m = 12, d = 20
255y = 14, m = 12, d = 20
256y = 7, m = 12, d = 20
257y = 3, m = 12, d = 20
258y = 1, m = 12, d = 20
259y = 0, m = 12, d = 20
260y = 0, m = 6, d = 20
261y = 0, m = 9, d = 20
262y = 0, m = 11, d = 20
263y = 0, m = 10, d = 20
264y = 0, m = 10, d = 10
265y = 0, m = 10, d = 5
266y = 0, m = 10, d = 3
267y = 0, m = 10, d = 2
268y = 0, m = 10, d = 1
269```
270
271The test failure message said there were two successful cases; we see these
272at the very top, `2497-08-27` and `9641-08-18`. The next case,
273`7360-12-20`, failed. There's nothing immediately obviously special about
274this date. Fortunately, proptest reduced it to a much simpler case. First,
275it rapidly reduced the `y` input to `0` at the beginning, and similarly
276reduced the `d` input to the minimum allowable value of `1` at the end.
277Between those two, though, we see something different: it tried to shrink
278`12` to `6`, but then ended up raising it back up to `10`. This is because
279the `0000-06-20` and `0000-09-20` test cases _passed_.
280
281In the end, we get the date `0000-10-01`, which apparently gets parsed as
282`0000-00-01`. Again, this failing case was added to the failure persistence
283file, and we should add this as its own unit test:
284
285```text
286$ git add proptest-regressions
287```
288
289```rust,ignore
290#[test]
291fn test_october_first() {
292    assert_eq!(Some((0, 10, 1)), parse_date("0000-10-01"));
293}
294```
295
296Now to figure out what's broken in the code. Even without the intermediate
297input, we can say with reasonable confidence that the year and day parts
298don't come into the picture since both were reduced to the minimum
299allowable input. The month input was _not_, but was reduced to `10`. This
300means we can infer that there's something special about `10` that doesn't
301hold for `9`. In this case, that "special something" is being two digits
302wide. In our code:
303
304```rust,ignore
305    let month = &s[6..7];
306```
307
308We were off by one, and need to use the range `5..7`. After fixing this,
309the test passes.
310
311The `proptest!` macro has some additional syntax, including for setting
312configuration for things like the number of test cases to generate. See its
313[documentation](https://altsysrq.github.io/rustdoc/proptest/latest/proptest/macro.proptest.html)
314for more details.
315## Differences between QuickCheck and Proptest
316
317QuickCheck and Proptest are similar in many ways: both generate random
318inputs for a function to check certain properties, and automatically shrink
319inputs to minimal failing cases.
320
321The one big difference is that QuickCheck generates and shrinks values
322based on type alone, whereas Proptest uses explicit `Strategy` objects. The
323QuickCheck approach has a lot of disadvantages in comparison:
324
325- QuickCheck can only define one generator and shrinker per type. If you need a
326  custom generation strategy, you need to wrap it in a newtype and implement
327  traits on that by hand. In Proptest, you can define arbitrarily many
328  different strategies for the same type, and there are plenty built-in.
329
330- For the same reason, QuickCheck has a single "size" configuration that tries
331  to define the range of values generated. If you need an integer between 0 and
332  100 and another between 0 and 1000, you probably need to do another newtype.
333  In Proptest, you can directly just express that you want a `0..100` integer
334  and a `0..1000` integer.
335
336- Types in QuickCheck are not easily composable. Defining `Arbitrary` and
337  `Shrink` for a new struct which is simply produced by the composition of its
338  fields requires implementing both by hand, including a bidirectional mapping
339  between the struct and a tuple of its fields. In Proptest, you can make a
340  tuple of the desired components and then `prop_map` it into the desired form.
341  Shrinking happens automatically in terms of the input types.
342
343- Because constraints on values cannot be expressed in QuickCheck, generation
344  and shrinking may lead to a lot of input rejections. Strategies in Proptest
345  are aware of simple constraints and do not generate or shrink to values that
346  violate them.
347
348The author of Hypothesis also has an [article on this
349topic](http://hypothesis.works/articles/integrated-shrinking/).
350
351Of course, there's also some relative downsides that fall out of what
352Proptest does differently:
353
354- Generating complex values in Proptest can be up to an order of magnitude
355  slower than in QuickCheck. This is because QuickCheck performs stateless
356  shrinking based on the output value, whereas Proptest must hold on to all the
357  intermediate states and relationships in order for its richer shrinking model
358  to work.
359## Limitations of Property Testing
360
361Given infinite time, property testing will eventually explore the whole
362input space to a test. However, time is not infinite, so only a randomly
363sampled portion of the input space can be explored. This means that
364property testing is extremely unlikely to find single-value edge cases in a
365large space. For example, the following test will virtually always pass:
366
367```rust
368use proptest::prelude::*;
369
370proptest! {
371    #[test]
372    fn i64_abs_is_never_negative(a: i64) {
373        // This actually fails if a == i64::MIN, but randomly picking one
374        // specific value out of 2⁶⁴ is overwhelmingly unlikely.
375        assert!(a.abs() >= 0);
376    }
377}
378```
379
380Because of this, traditional unit testing with intelligently selected cases
381is still necessary for many kinds of problems.
382
383Similarly, in some cases it can be hard or impossible to define a strategy
384which actually produces useful inputs. A strategy of `.{1,4096}` may be
385great to fuzz a C parser, but is highly unlikely to produce anything that
386makes it to a code generator.
387
388# Acknowledgements
389
390This crate wouldn't have come into existence had it not been for the [Rust port
391of QuickCheck](https://github.com/burntsushi/quickcheck) and the
392[`regex_generate`](https://github.com/CryptArchy/regex_generate) crate which
393gave wonderful examples of what is possible.
394
395## Contribution
396
397Unless you explicitly state otherwise, any contribution intentionally submitted
398for inclusion in the work by you, as defined in the Apache-2.0 license, shall
399be dual licensed as above, without any additional terms or conditions.
400