quickercheck/
quick_check.rs

1use generate::GenerateCtx;
2use testable::{IntoTestable, Testable, TestStatus, TestResult};
3use rose::Rose;
4
5use std::{self, cmp};
6use rand::{self, Rng, StdRng, SeedableRng};
7use log::LogLevel;
8
9pub type Result<T> = std::result::Result<T, QuickCheckError>;
10
11#[derive(Clone, Debug)]
12pub enum QuickCheckError {
13    GaveUp {
14        successful_tests: usize,
15        attempts: usize
16    },
17    Failure {
18        input: String,
19        successful_tests: usize,
20        seed: usize,
21        size: usize
22    },
23    NoExpectedFailure
24}
25
26pub struct QuickCheck {
27    tests: usize,
28    max_discard_ratio: usize,
29    max_size: usize,
30    rng: rand::ThreadRng
31}
32
33struct QuickCheckState {
34    successful_tests: usize,
35    recently_discarded_tests: usize
36}
37
38impl QuickCheckState {
39    fn new() -> Self {
40        QuickCheckState { successful_tests: 0, recently_discarded_tests: 0 }
41    }
42
43    fn test_passed(&mut self) {
44        self.successful_tests += 1;
45        self.recently_discarded_tests = 0;
46    }
47
48    fn test_discarded(&mut self) {
49        self.recently_discarded_tests += 1;
50    }
51
52    fn gave_up_after(&self, attempts: usize) -> Result<usize> {
53        Err(QuickCheckError::GaveUp {
54            successful_tests: self.successful_tests,
55            attempts: attempts
56        })
57    }
58
59    fn test_failed<T: Testable>(&self, testable: T, result: TestResult, seed: usize, size: usize) -> Result<usize> {
60        match testable.is_expected_to_fail() {
61            true => Ok(self.successful_tests),
62            false => Err(QuickCheckError::Failure {
63                input: result.input,
64                successful_tests: self.successful_tests,
65                seed: seed,
66                size: size
67            })
68        }
69    }
70}
71
72impl QuickCheck
73{
74    pub fn new() -> Self {
75        QuickCheck {
76            tests: 100,
77            max_discard_ratio: 10,
78            max_size: 100,
79            rng: rand::thread_rng()
80        }
81    }
82
83    pub fn max_size(self, max_size: usize) -> Self {
84        QuickCheck {
85            max_size: max_size,
86            ..self
87        }
88    }
89
90    pub fn tests(self, tests: usize) -> Self {
91        QuickCheck {
92            tests: tests,
93            ..self
94        }
95    }
96
97    pub fn quicktest<T: IntoTestable>(&mut self, t: T) -> Result<usize> {
98        let _ = ::env_logger::init();
99
100        let testable = t.into_testable();
101        let max_tests = self.tests * self.max_discard_ratio;
102
103        let mut state = QuickCheckState::new();
104
105        for _ in 0..max_tests {
106            if state.successful_tests >= self.tests { return Ok(state.successful_tests) }
107
108            let seed = self.rng.gen();
109            let mut test_rng = StdRng::from_seed(&[seed]);
110            let size = self.size(&state);
111            let mut ctx = GenerateCtx::new(&mut test_rng, size);
112
113            let rose_result = testable.test(&mut ctx);
114            self.log_result(&rose_result.value);
115
116            match rose_result.value.status {
117                TestStatus::Pass => state.test_passed(),
118                TestStatus::Discard => state.test_discarded(),
119                TestStatus::Fail => {
120                    info!("Attempting to reduce to a minimal failing case...");
121                    let minimal_witness = self.shrink_failure(rose_result);
122                    return state.test_failed(testable, minimal_witness, seed, size);
123                }
124            }
125        }
126
127        state.gave_up_after(max_tests)
128    }
129
130    fn log_result(&self, result: &TestResult) {
131        let log_level = match result.status {
132            TestStatus::Discard => LogLevel::Trace,
133            TestStatus::Fail => LogLevel::Info,
134            _ => LogLevel::Debug
135        };
136        log!(log_level, "{:?}: {}", result.status, result.input);
137    }
138
139    fn shrink_failure<'a>(&self, rose_result: Rose<TestResult>) -> TestResult {
140        for shrunk_result in rose_result.iterator {
141            assert!(shrunk_result.value.input != rose_result.value.input);
142            self.log_result(&shrunk_result.value);
143            match shrunk_result.value.status {
144                TestStatus::Fail => return self.shrink_failure(shrunk_result),
145                _ => continue
146            }
147        }
148
149        rose_result.value
150    }
151
152    fn size(&self, state: &QuickCheckState) -> usize {
153        let n = state.successful_tests;
154        let d = state.recently_discarded_tests;
155        let max_size = self.max_size;
156
157        fn round_down_to(value: usize, multiple: usize) -> usize { (value / multiple) * multiple }
158
159        let proposed_size = {
160            if (round_down_to(n, max_size) + max_size <= self.tests) || ((self.tests % max_size) == 0) {
161                (n % max_size) + d / 10
162            } else {
163                ((n % max_size) * max_size) / (self.tests % max_size) + d / 10
164            }
165        };
166
167        cmp::min(proposed_size, max_size)
168    }
169
170    pub fn quickcheck<T: IntoTestable>(&mut self, t: T) {
171        match self.quicktest(t) {
172            Ok(ntests) => info!("(Passed {} QuickCheck tests.)", ntests),
173            Err(err) => {
174                match err {
175                    QuickCheckError::Failure{ successful_tests, input, .. } =>
176                        panic!("Falsifiable after {} tests with input {}", successful_tests, input),
177                    _ => panic!("Failed: {:?}", err)
178                }
179            }
180        }
181    }
182}
183
184pub fn quicktest<T: IntoTestable>(t: T) -> Result<usize> { QuickCheck::new().quicktest(t) }
185pub fn quickcheck<T: IntoTestable>(t: T) { QuickCheck::new().quickcheck(t) }