hypothesis-specs 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 23fd4398a4ec86d30001513e3f003dcc76ebc1e5
4
- data.tar.gz: 733ad33a2f5518119e6d0336131b8b46d4c6135e
3
+ metadata.gz: 7e8eb07704e06280a79469b76b6b7267cf356797
4
+ data.tar.gz: edd8bb7476a3a84dedf2751016385dbe2d6afcac
5
5
  SHA512:
6
- metadata.gz: 9cc1fcf63b61384f84a92cd7289b0ac1104f2f579bd45bf31089a3cf3cfd9d7caff604063be04c3102c12e40e1e557a6fe26cd520c845201d9058b2881c1b21d
7
- data.tar.gz: 84aa608a1d2076778e2f686a15c1d01ad7ac557ce057303151ea4b0e72d89129c9a17314ee26df29a312ad956151fdc35d571a06a733d81346a614c1a621da36
6
+ metadata.gz: 8e72ef276034266146155092d14827e75f5f20ec3986dd57245f94694e1f7da09d5cc62b74fced3829e0d8e5dbff522ad44d6fc88e2e49d274979eaae3f06e0e
7
+ data.tar.gz: b6917a3a24f98febffaab647ecec0393827e3f116390762d3163cd9d2fe7e1cac49cfd79bc2db1e448beb044e862b0c96c79a5e3e8d7aed986ff718eef755d40
@@ -1,3 +1,11 @@
1
+ ## Hypothesis for Ruby 0.0.9 (2018-04-20)
2
+
3
+ This improves Hypothesis for Ruby's shrinking to be much closer
4
+ to Hypothesis for Python's. It's still far from complete, and even
5
+ in cases where it has the same level of quality it will often be
6
+ significantly slower, but examples should now be much more consistent,
7
+ especially in cases where you are using e.g. `built_as`.
8
+
1
9
  ## Hypothesis for Ruby 0.0.8 (2018-02-20)
2
10
 
3
11
  This release fixes the dependency on Rake to be in a more sensible range.
@@ -30,7 +30,9 @@ module Hypothesis
30
30
  begin
31
31
  @depth += 1
32
32
  possible ||= block
33
+ @wrapped_data.start_draw
33
34
  result = possible.provide(&block)
35
+ @wrapped_data.stop_draw
34
36
  if top_level
35
37
  draws&.push(result)
36
38
  print_log&.push([name, result.inspect])
@@ -2,6 +2,7 @@
2
2
  // needs.
3
3
 
4
4
  use rand::{ChaChaRng, Rng};
5
+ use std::collections::HashSet;
5
6
 
6
7
  pub type DataStream = Vec<u64>;
7
8
 
@@ -14,6 +15,22 @@ enum BitGenerator {
14
15
  Recorded(DataStream),
15
16
  }
16
17
 
18
+ // Records information corresponding to a single draw call.
19
+ #[derive(Debug, Clone)]
20
+ pub struct DrawInProgress {
21
+ depth: usize,
22
+ start: usize,
23
+ end: Option<usize>,
24
+ }
25
+
26
+ // Records information corresponding to a single draw call.
27
+ #[derive(Debug, Clone)]
28
+ pub struct Draw {
29
+ pub depth: usize,
30
+ pub start: usize,
31
+ pub end: usize,
32
+ }
33
+
17
34
  // Main entry point for running a test:
18
35
  // A test function takes a DataSource, uses it to
19
36
  // produce some data, and the DataSource records the
@@ -22,9 +39,61 @@ enum BitGenerator {
22
39
  pub struct DataSource {
23
40
  bitgenerator: BitGenerator,
24
41
  record: DataStream,
42
+ draws: Vec<DrawInProgress>,
43
+ draw_stack: Vec<usize>,
44
+ written_indices: HashSet<usize>,
25
45
  }
26
46
 
27
47
  impl DataSource {
48
+ fn new(generator: BitGenerator) -> DataSource {
49
+ return DataSource {
50
+ bitgenerator: generator,
51
+ record: DataStream::new(),
52
+ draws: Vec::new(),
53
+ draw_stack: Vec::new(),
54
+ written_indices: HashSet::new(),
55
+ };
56
+ }
57
+
58
+ pub fn from_random(random: ChaChaRng) -> DataSource {
59
+ return DataSource::new(BitGenerator::Random(random));
60
+ }
61
+
62
+ pub fn from_vec(record: DataStream) -> DataSource {
63
+ return DataSource::new(BitGenerator::Recorded(record));
64
+ }
65
+
66
+ pub fn start_draw(&mut self) {
67
+ let i = self.draws.len();
68
+ let depth = self.draw_stack.len();
69
+ let start = self.record.len();
70
+
71
+ self.draw_stack.push(i);
72
+ self.draws.push(DrawInProgress {
73
+ start: start,
74
+ end: None,
75
+ depth: depth,
76
+ });
77
+ }
78
+
79
+ pub fn stop_draw(&mut self) {
80
+ assert!(self.draws.len() > 0);
81
+ assert!(self.draw_stack.len() > 0);
82
+ let i = self.draw_stack.pop().unwrap();
83
+ let end = self.record.len();
84
+ self.draws[i].end = Some(end);
85
+ }
86
+
87
+ pub fn write(&mut self, value: u64) -> Result<(), FailedDraw> {
88
+ match self.bitgenerator {
89
+ BitGenerator::Recorded(ref mut v) if self.record.len() >= v.len() => Err(FailedDraw),
90
+ _ => {
91
+ self.record.push(value);
92
+ Ok(())
93
+ }
94
+ }
95
+ }
96
+
28
97
  pub fn bits(&mut self, n_bits: u64) -> Result<u64, FailedDraw> {
29
98
  let mut result = match self.bitgenerator {
30
99
  BitGenerator::Random(ref mut random) => random.next_u64(),
@@ -45,32 +114,40 @@ impl DataSource {
45
114
  return Ok(result);
46
115
  }
47
116
 
48
- fn new(generator: BitGenerator) -> DataSource {
49
- return DataSource {
50
- bitgenerator: generator,
51
- record: DataStream::new(),
52
- };
53
- }
54
-
55
- pub fn from_random(random: ChaChaRng) -> DataSource {
56
- return DataSource::new(BitGenerator::Random(random));
57
- }
58
-
59
- pub fn from_vec(record: DataStream) -> DataSource {
60
- return DataSource::new(BitGenerator::Recorded(record));
61
- }
62
-
63
- pub fn to_result(self, status: Status) -> TestResult {
117
+ pub fn to_result(mut self, status: Status) -> TestResult {
64
118
  TestResult {
65
119
  record: self.record,
66
120
  status: status,
121
+ written_indices: self.written_indices,
122
+ draws: self.draws
123
+ .drain(..)
124
+ .filter_map(|d| match d {
125
+ DrawInProgress {
126
+ depth,
127
+ start,
128
+ end: Some(end),
129
+ } if start < end =>
130
+ {
131
+ Some(Draw {
132
+ start: start,
133
+ end: end,
134
+ depth: depth,
135
+ })
136
+ }
137
+ DrawInProgress { end: None, .. } => {
138
+ assert!(status == Status::Invalid || status == Status::Overflow);
139
+ None
140
+ }
141
+ _ => None,
142
+ })
143
+ .collect(),
67
144
  }
68
145
  }
69
146
  }
70
147
 
71
148
  // Status indicates the result that we got from completing
72
149
  // a single test execution.
73
- #[derive(Debug, Clone, Eq, PartialEq)]
150
+ #[derive(Debug, Clone, Eq, PartialEq, Copy)]
74
151
  pub enum Status {
75
152
  // The test tried to read more data than we had for it.
76
153
  Overflow,
@@ -96,4 +173,6 @@ pub enum Status {
96
173
  pub struct TestResult {
97
174
  pub record: DataStream,
98
175
  pub status: Status,
176
+ pub draws: Vec<Draw>,
177
+ pub written_indices: HashSet<usize>,
99
178
  }
@@ -4,6 +4,8 @@ use std::collections::BinaryHeap;
4
4
  use std::mem;
5
5
  use std::cmp::{Ord, Ordering, PartialOrd, Reverse};
6
6
 
7
+ use std::u64::MAX as MAX64;
8
+
7
9
  type Draw<T> = Result<T, FailedDraw>;
8
10
 
9
11
  pub fn weighted(source: &mut DataSource, probability: f64) -> Result<bool, FailedDraw> {
@@ -11,12 +13,19 @@ pub fn weighted(source: &mut DataSource, probability: f64) -> Result<bool, Faile
11
13
 
12
14
  let truthy = (probability * (u64::max_value() as f64 + 1.0)).floor() as u64;
13
15
  let probe = source.bits(64)?;
14
- return Ok(probe >= u64::max_value() - truthy + 1);
16
+ Ok(match (truthy, probe) {
17
+ (0, _) => false,
18
+ (MAX64, _) => true,
19
+ (_, 0) => false,
20
+ (_, 1) => true,
21
+ _ => probe >= MAX64 - truthy,
22
+ })
15
23
  }
16
24
 
17
25
  pub fn bounded_int(source: &mut DataSource, max: u64) -> Draw<u64> {
18
26
  let bitlength = 64 - max.leading_zeros() as u64;
19
27
  if bitlength == 0 {
28
+ source.write(0)?;
20
29
  return Ok(0);
21
30
  }
22
31
  loop {
@@ -46,30 +55,25 @@ impl Repeat {
46
55
  }
47
56
  }
48
57
 
49
- fn draw_until(&self, source: &mut DataSource, value: bool) -> Result<(), FailedDraw> {
50
- // Force a draw until we get the desired outcome. By having this we get much better
51
- // shrinking when min_size or max_size are set because all decisions are represented
52
- // somewhere in the bit stream.
53
- loop {
54
- let d = weighted(source, self.p_continue)?;
55
- if d == value {
56
- return Ok(());
57
- }
58
- }
59
- }
60
-
61
58
  pub fn reject(&mut self) {
62
59
  assert!(self.current_count > 0);
63
60
  self.current_count -= 1;
64
61
  }
65
62
 
66
63
  pub fn should_continue(&mut self, source: &mut DataSource) -> Result<bool, FailedDraw> {
67
- if self.current_count < self.min_count {
68
- self.draw_until(source, true)?;
64
+ if self.min_count == self.max_count {
65
+ if self.current_count < self.max_count {
66
+ self.current_count += 1;
67
+ return Ok(true);
68
+ } else {
69
+ return Ok(false);
70
+ }
71
+ } else if self.current_count < self.min_count {
72
+ source.write(1)?;
69
73
  self.current_count += 1;
70
74
  return Ok(true);
71
75
  } else if self.current_count >= self.max_count {
72
- self.draw_until(source, false)?;
76
+ source.write(0)?;
73
77
  return Ok(false);
74
78
  }
75
79
 
@@ -13,9 +13,7 @@ use data::{DataSource, DataStream, Status, TestResult};
13
13
  enum LoopExitReason {
14
14
  Complete,
15
15
  MaxExamples,
16
- //MaxShrinks,
17
16
  Shutdown,
18
- //Error(String),
19
17
  }
20
18
 
21
19
  #[derive(Debug)]
@@ -108,6 +106,7 @@ struct Shrinker<'owner, Predicate> {
108
106
  _predicate: Predicate,
109
107
  shrink_target: TestResult,
110
108
  changes: u64,
109
+ expensive_passes_enabled: bool,
111
110
  main_loop: &'owner mut MainGenerationLoop,
112
111
  }
113
112
 
@@ -126,12 +125,22 @@ where
126
125
  _predicate: predicate,
127
126
  shrink_target: shrink_target,
128
127
  changes: 0,
128
+ expensive_passes_enabled: false,
129
129
  }
130
130
  }
131
131
 
132
132
  fn predicate(&mut self, result: &TestResult) -> bool {
133
133
  let succeeded = (self._predicate)(result);
134
- if succeeded {
134
+ if succeeded
135
+ && (
136
+ // In the presence of writes it may be the case that we thought
137
+ // we were going to shrink this but didn't actually succeed because
138
+ // the written value was used.
139
+ result.record.len() < self.shrink_target.record.len() || (
140
+ result.record.len() == self.shrink_target.record.len() &&
141
+ result.record < self.shrink_target.record
142
+ )
143
+ ) {
135
144
  self.changes += 1;
136
145
  self.shrink_target = result.clone();
137
146
  }
@@ -143,15 +152,167 @@ where
143
152
 
144
153
  while prev != self.changes {
145
154
  prev = self.changes;
155
+ self.adaptive_delete()?;
146
156
  self.binary_search_blocks()?;
147
- self.remove_intervals()?;
157
+ if prev == self.changes {
158
+ self.expensive_passes_enabled = true;
159
+ }
160
+ if !self.expensive_passes_enabled {
161
+ continue;
162
+ }
163
+
164
+ self.reorder_blocks()?;
165
+ self.lower_and_delete()?;
166
+ self.delete_all_ranges()?;
148
167
  }
149
168
  Ok(())
150
169
  }
151
170
 
152
- fn remove_intervals(&mut self) -> StepResult {
153
- // TODO: Actually track the data we need to make this
154
- // not quadratic.
171
+ fn lower_and_delete(&mut self) -> StepResult {
172
+ let mut i = 0;
173
+ while i < self.shrink_target.record.len() {
174
+ if self.shrink_target.record[i] > 0 {
175
+ let mut attempt = self.shrink_target.record.clone();
176
+ attempt[i] -= 1;
177
+ let (succeeded, result) = self.execute(&attempt)?;
178
+ if !succeeded && result.record.len() < self.shrink_target.record.len() {
179
+ let mut j = 0;
180
+ while j < self.shrink_target.draws.len() {
181
+ // Having to copy this is an annoying consequence of lexical lifetimes -
182
+ // if we borrowed it immutably then we'd not be allowed to call self.incorporate
183
+ // down below. Fortunately these things are tiny structs of integers so it doesn't
184
+ // really matter.
185
+ let d = self.shrink_target.draws[j].clone();
186
+ if d.start > i {
187
+ let mut attempt2 = attempt.clone();
188
+ attempt2.drain(d.start..d.end);
189
+ if self.incorporate(&attempt2)? {
190
+ break;
191
+ }
192
+ }
193
+ j += 1;
194
+ }
195
+ }
196
+ }
197
+ i += 1;
198
+ }
199
+ Ok(())
200
+ }
201
+
202
+ fn reorder_blocks(&mut self) -> StepResult {
203
+ let mut i = 0;
204
+ while i < self.shrink_target.record.len() {
205
+ let mut j = i + 1;
206
+ while j < self.shrink_target.record.len() {
207
+ assert!(i < self.shrink_target.record.len());
208
+ if self.shrink_target.record[i] == 0 {
209
+ break;
210
+ }
211
+ if self.shrink_target.record[j] < self.shrink_target.record[i] {
212
+ let mut attempt = self.shrink_target.record.clone();
213
+ attempt.swap(i, j);
214
+ self.incorporate(&attempt)?;
215
+ }
216
+ j += 1;
217
+ }
218
+ i += 1;
219
+ }
220
+ Ok(())
221
+ }
222
+
223
+ fn try_delete_range(
224
+ &mut self,
225
+ target: &TestResult,
226
+ i: usize,
227
+ k: usize,
228
+ ) -> Result<bool, LoopExitReason> {
229
+ // Attempts to delete k non-overlapping draws starting from the draw at index i.
230
+
231
+ let mut stack: Vec<(usize, usize)> = Vec::new();
232
+ let mut j = i;
233
+ while j < target.draws.len() && stack.len() < k {
234
+ let m = target.draws[j].start;
235
+ let n = target.draws[j].end;
236
+ assert!(m < n);
237
+ if m < n && (stack.len() == 0 || stack[stack.len() - 1].1 <= m) {
238
+ stack.push((m, n))
239
+ }
240
+ j += 1;
241
+ }
242
+
243
+ let mut attempt = target.record.clone();
244
+ while stack.len() > 0 {
245
+ let (m, n) = stack.pop().unwrap();
246
+ attempt.drain(m..n);
247
+ }
248
+
249
+ if attempt.len() >= self.shrink_target.record.len() {
250
+ Ok(false)
251
+ } else {
252
+ self.incorporate(&attempt)
253
+ }
254
+ }
255
+
256
+ fn adaptive_delete(&mut self) -> StepResult {
257
+ let mut i = 0;
258
+ let target = self.shrink_target.clone();
259
+
260
+ while i < target.draws.len() {
261
+ // This is an adaptive pass loosely modelled after timsort. If
262
+ // little or nothing is deletable here then we don't try any more
263
+ // deletions than the naive greedy algorithm would, but if it looks
264
+ // like we have an opportunity to delete a lot then we try to do so.
265
+
266
+ // What we're trying to do is to find a large k such that we can
267
+ // delete k but not k + 1 draws starting from this point, and we
268
+ // want to do that in O(log(k)) rather than O(k) test executions.
269
+
270
+ // We try a quite careful sequence of small shrinks here before we
271
+ // move on to anything big. This is because if we try to be
272
+ // aggressive too early on we'll tend to find that we lose out when
273
+ // the example is "nearly minimal".
274
+ if self.try_delete_range(&target, i, 2)? {
275
+ if self.try_delete_range(&target, i, 3)? && self.try_delete_range(&target, i, 4)? {
276
+ let mut hi = 5;
277
+ // At this point it looks like we've got a pretty good
278
+ // opportunity for a long run here. We do an exponential
279
+ // probe upwards to try and find some k where we can't
280
+ // delete many intervals. We do this rather than choosing
281
+ // that upper bound to immediately be large because we
282
+ // don't really expect k to be huge. If it turns out that
283
+ // it is, the subsequent example is going to be so tiny that
284
+ // it doesn't really matter if we waste a bit of extra time
285
+ // here.
286
+ while self.try_delete_range(&target, i, hi)? {
287
+ assert!(hi <= target.draws.len());
288
+ hi *= 2;
289
+ }
290
+ // We now know that we can delete the first lo intervals but
291
+ // not the first hi. We preserve that property while doing
292
+ // a binary search to find the point at which we stop being
293
+ // able to delete intervals.
294
+ let mut lo = 4;
295
+ while lo + 1 < hi {
296
+ let mid = lo + (hi - lo) / 2;
297
+ if self.try_delete_range(&target, i, mid)? {
298
+ lo = mid;
299
+ } else {
300
+ hi = mid;
301
+ }
302
+ }
303
+ }
304
+ } else {
305
+ self.try_delete_range(&target, i, 1)?;
306
+ }
307
+ // We unconditionally bump i because we have always tried deleting
308
+ // one more example than we succeeded at deleting, so we expect the
309
+ // next example to be undeletable.
310
+ i += 1;
311
+ }
312
+ return Ok(());
313
+ }
314
+
315
+ fn delete_all_ranges(&mut self) -> StepResult {
155
316
  let mut i = 0;
156
317
  while i < self.shrink_target.record.len() {
157
318
  let start_length = self.shrink_target.record.len();
@@ -174,38 +335,45 @@ where
174
335
  Ok(())
175
336
  }
176
337
 
177
- fn binary_search_blocks(&mut self) -> StepResult {
178
- let mut i = 0;
338
+ fn try_lowering_value(&mut self, i: usize, v: u64) -> Result<bool, LoopExitReason> {
339
+ if v >= self.shrink_target.record[i] {
340
+ return Ok(false);
341
+ }
179
342
 
180
343
  let mut attempt = self.shrink_target.record.clone();
344
+ attempt[i] = v;
345
+ let (succeeded, result) = self.execute(&attempt)?;
346
+ assert!(result.record.len() <= self.shrink_target.record.len());
347
+ let lost_bytes = self.shrink_target.record.len() - result.record.len();
348
+ if !succeeded && result.status == Status::Valid && lost_bytes > 0 {
349
+ attempt.drain(i + 1..i + lost_bytes + 1);
350
+ assert!(attempt.len() + lost_bytes == self.shrink_target.record.len());
351
+ self.incorporate(&attempt)
352
+ } else {
353
+ Ok(succeeded)
354
+ }
355
+ }
181
356
 
182
- while i < self.shrink_target.record.len() {
183
- assert!(attempt.len() >= self.shrink_target.record.len());
357
+ fn binary_search_blocks(&mut self) -> StepResult {
358
+ let mut i = 0;
184
359
 
360
+ while i < self.shrink_target.record.len() {
185
361
  let mut hi = self.shrink_target.record[i];
186
362
 
187
- if hi > 0 {
188
- attempt[i] = 0;
189
- let zeroed = self.incorporate(&attempt)?;
363
+ if hi > 0 && !self.shrink_target.written_indices.contains(&i) {
364
+ let zeroed = self.try_lowering_value(i, 0)?;
190
365
  if !zeroed {
191
366
  let mut lo = 0;
192
367
  // Binary search to find the smallest value we can
193
368
  // replace this with.
194
369
  while lo + 1 < hi {
195
370
  let mid = lo + (hi - lo) / 2;
196
- attempt[i] = mid;
197
- let succeeded = self.incorporate(&attempt)?;
198
- if succeeded {
199
- attempt = self.shrink_target.record.clone();
371
+ if self.try_lowering_value(i, mid)? {
200
372
  hi = mid;
201
373
  } else {
202
- attempt[i] = self.shrink_target.record[i];
203
374
  lo = mid;
204
375
  }
205
376
  }
206
- attempt[i] = hi;
207
- } else {
208
- attempt = self.shrink_target.record.clone();
209
377
  }
210
378
  }
211
379
 
@@ -215,6 +383,12 @@ where
215
383
  Ok(())
216
384
  }
217
385
 
386
+ fn execute(&mut self, buf: &DataStream) -> Result<(bool, TestResult), LoopExitReason> {
387
+ // TODO: Later there will be caching here
388
+ let result = self.main_loop.execute(DataSource::from_vec(buf.clone()))?;
389
+ Ok((self.predicate(&result), result))
390
+ }
391
+
218
392
  fn incorporate(&mut self, buf: &DataStream) -> Result<bool, LoopExitReason> {
219
393
  assert!(
220
394
  buf.len() <= self.shrink_target.record.len(),
@@ -229,8 +403,8 @@ where
229
403
  if self.shrink_target.record.starts_with(buf) {
230
404
  return Ok(false);
231
405
  }
232
- let result = self.main_loop.execute(DataSource::from_vec(buf.clone()))?;
233
- return Ok(self.predicate(&result));
406
+ let (succeeded, _) = self.execute(buf)?;
407
+ Ok(succeeded)
234
408
  }
235
409
  }
236
410
 
data/src/lib.rs CHANGED
@@ -33,6 +33,18 @@ ruby! {
33
33
  mem::swap(&mut result.source, &mut engine.pending);
34
34
  return result;
35
35
  }
36
+
37
+ def start_draw(&mut self){
38
+ if let &mut Some(ref mut source) = &mut self.source {
39
+ source.start_draw();
40
+ }
41
+ }
42
+
43
+ def stop_draw(&mut self){
44
+ if let &mut Some(ref mut source) = &mut self.source {
45
+ source.stop_draw();
46
+ }
47
+ }
36
48
  }
37
49
 
38
50
  class HypothesisCoreEngine {
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hypothesis-specs
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.8
4
+ version: 0.0.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - David R. Maciver
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-20 00:00:00.000000000 Z
11
+ date: 2018-04-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: helix_runtime