quickercheck/
quick_check.rs1use 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) }