prop_check 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,415 @@
1
+ require 'prop_check/generator'
2
+ require 'prop_check/lazy_tree'
3
+ module PropCheck
4
+ ##
5
+ # Contains common generators.
6
+ # Use this module by including it in the class (e.g. in your test suite)
7
+ # where you want to use them.
8
+ module Generators
9
+ extend self
10
+ ##
11
+ # Always returns the same value, regardless of `size` or `rng` (random number generator state)
12
+ #
13
+ # No shrinking (only considers the current single value `val`).
14
+ #
15
+ # >> Generators.constant("pie").sample(5, size: 10, rng: Random.new(42))
16
+ # => ["pie", "pie", "pie", "pie", "pie"]
17
+ def constant(val)
18
+ Generator.wrap(val)
19
+ end
20
+
21
+ private def integer_shrink(val)
22
+ # 0 cannot shrink further; base case
23
+ return [] if val.zero?
24
+
25
+ # Numbers are shrunken by
26
+ # subtracting themselves, their half, quarter, eight, ... (rounded towards zero!)
27
+ # from themselves, until the number itself is reached.
28
+ # So: for 20 we have [0, 10, 15, 18, 19, 20]
29
+ halvings =
30
+ Helper
31
+ .scanl(val) { |x| (x / 2.0).truncate }
32
+ .take_while { |x| !x.zero? }
33
+ .map { |x| val - x }
34
+ .map { |x| LazyTree.new(x, integer_shrink(x)) }
35
+
36
+ # For negative numbers, we also attempt if the positive number has the same result.
37
+ if val.abs > val
38
+ [LazyTree.new(val.abs, halvings)].lazy
39
+ else
40
+ halvings
41
+ end
42
+ end
43
+
44
+ ##
45
+ # Returns a random integer in the given range (if a range is given)
46
+ # or between 0..num (if a single integer is given).
47
+ #
48
+ # Does not scale when `size` changes.
49
+ # This means `choose` is useful for e.g. picking an element out of multiple possibilities,
50
+ # but for other purposes you probably want to use `integer` et co.
51
+ #
52
+ # Shrinks to integers closer to zero.
53
+ #
54
+ # >> r = Random.new(42); Generators.choose(0..5).sample(size: 10, rng: r)
55
+ # => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
56
+ # >> r = Random.new(42); Generators.choose(0..5).sample(size: 20000, rng: r)
57
+ # => [3, 4, 2, 4, 4, 1, 2, 2, 2, 4]
58
+ def choose(range)
59
+ Generator.new do |_size, rng|
60
+ val = rng.rand(range)
61
+ LazyTree.new(val, integer_shrink(val))
62
+ end
63
+ end
64
+
65
+ ##
66
+ # A random integer which scales with `size`.
67
+ # Integers start small (around 0)
68
+ # and become more extreme (both higher and lower, negative) when `size` increases.
69
+ #
70
+ #
71
+ # Shrinks to integers closer to zero.
72
+ #
73
+ # >> Generators.integer.call(2, Random.new(42))
74
+ # => 1
75
+ # >> Generators.integer.call(10000, Random.new(42))
76
+ # => 5795
77
+ # >> r = Random.new(42); Generators.integer.sample(size: 20000, rng: r)
78
+ # => [-4205, -19140, 18158, -8716, -13735, -3150, 17194, 1962, -3977, -18315]
79
+ def integer
80
+ Generator.new do |size, rng|
81
+ val = rng.rand(-size..size)
82
+ LazyTree.new(val, integer_shrink(val))
83
+ end
84
+ end
85
+
86
+ ##
87
+ # Only returns integers that are zero or larger.
88
+ # See `integer` for more information.
89
+ def nonnegative_integer
90
+ integer.map(&:abs)
91
+ end
92
+
93
+ ##
94
+ # Only returns integers that are larger than zero.
95
+ # See `integer` for more information.
96
+ def positive_integer
97
+ nonnegative_integer.map { |x| x + 1 }
98
+ end
99
+
100
+ ##
101
+ # Only returns integers that are zero or smaller.
102
+ # See `integer` for more information.
103
+ def nonpositive_integer
104
+ nonnegative_integer.map(&:-@)
105
+ end
106
+
107
+ ##
108
+ # Only returns integers that are smaller than zero.
109
+ # See `integer` for more information.
110
+ def negative_integer
111
+ positive_integer.map(&:-@)
112
+ end
113
+
114
+ private def fraction(num_a, num_b, num_c)
115
+ num_a.to_f + num_b.to_f / (num_c.to_f.abs + 1.0)
116
+ end
117
+
118
+ ##
119
+ # Generates floating point numbers
120
+ # These start small (around 0)
121
+ # and become more extreme (large positive and large negative numbers)
122
+ #
123
+ #
124
+ # Shrinks to numbers closer to zero.
125
+ #
126
+ # TODO testing for NaN, Infinity?
127
+ def float
128
+ # integer.bind do |a|
129
+ # integer.bind do |b|
130
+ # integer.bind do |c|
131
+ # Generator.wrap(fraction(a, b, c))
132
+ # end
133
+ # end
134
+ # end
135
+ tuple(integer, integer, integer).map do |a, b, c|
136
+ fraction(a, b, c)
137
+ end
138
+ end
139
+
140
+ ##
141
+ # Picks one of the given generators in `choices` at random uniformly every time.
142
+ #
143
+ # Shrinks to values earlier in the list of `choices`.
144
+ #
145
+ # >> Generators.one_of(Generators.constant(true), Generators.constant(false)).sample(5, size: 10, rng: Random.new(42))
146
+ # => [true, false, true, true, true]
147
+ def one_of(*choices)
148
+ choose(choices.length).bind do |index|
149
+ choices[index]
150
+ end
151
+ end
152
+
153
+ ##
154
+ # Picks one of the choices given in `frequencies` at random every time.
155
+ # `frequencies` expects keys to be numbers
156
+ # (representing the relative frequency of this generator)
157
+ # and values to be generators.
158
+ #
159
+ # Shrinks to arbitrary elements (since hashes are not ordered).
160
+ #
161
+ # >> Generators.frequency(5 => Generators.integer, 1 => Generators.printable_ascii_char).sample(size: 10, rng: Random.new(42))
162
+ # => [4, -3, 10, 8, 0, -7, 10, 1, "E", 10]
163
+ def frequency(frequencies)
164
+ choices = frequencies.reduce([]) do |acc, elem|
165
+ freq, val = elem
166
+ acc + ([val] * freq)
167
+ end
168
+ one_of(*choices)
169
+ end
170
+
171
+ ##
172
+ # Generates an array containing always exactly one value from each of the passed generators,
173
+ # in the same order as specified:
174
+ #
175
+ # Shrinks element generators, one at a time (trying last one first).
176
+ #
177
+ # >> Generators.tuple(Generators.integer, Generators.float).call(10, Random.new(42))
178
+ # => [-4, 13.0]
179
+ def tuple(*generators)
180
+ Generator.new do |size, rng|
181
+ LazyTree.zip(generators.map do |generator|
182
+ generator.generate(size, rng)
183
+ end)
184
+ end
185
+ end
186
+
187
+ ##
188
+ # Given a `hash` where the values are generators,
189
+ # creates a generator that returns hashes
190
+ # with the same keys, and their corresponding values from their corresponding generators.
191
+ #
192
+ # Shrinks element generators.
193
+ #
194
+ # >> Generators.fixed_hash(a: Generators.integer(), b: Generators.float(), c: Generators.integer()).call(10, Random.new(42))
195
+ # => {:a=>-4, :b=>13.0, :c=>-3}
196
+ def fixed_hash(hash)
197
+ keypair_generators =
198
+ hash.map do |key, generator|
199
+ generator.map { |val| [key, val] }
200
+ end
201
+
202
+ tuple(*keypair_generators)
203
+ .map(&:to_h)
204
+ end
205
+
206
+ ##
207
+ # Generates an array of elements, where each of the elements
208
+ # is generated by `element_generator`.
209
+ #
210
+ # Shrinks to shorter arrays (with shrunken elements).
211
+ #
212
+ # >> Generators.array(Generators.positive_integer).sample(5, size: 10, rng: Random.new(42))
213
+ # => [[10, 5, 1, 4], [5, 9, 1, 1, 11, 8, 4, 9, 11, 10], [6], [11, 11, 2, 2, 7, 2, 6, 5, 5], [2, 10, 9, 7, 9, 5, 11, 3]]
214
+ def array(element_generator)
215
+ nonnegative_integer.bind do |generator|
216
+ generators = (0...generator).map do
217
+ element_generator.clone
218
+ end
219
+
220
+ tuple(*generators)
221
+ end
222
+ end
223
+
224
+ ##
225
+ # Generates a hash of key->values,
226
+ # where each of the keys is made using the `key_generator`
227
+ # and each of the values using the `value_generator`.
228
+ #
229
+ # Shrinks to hashes with less key/value pairs.
230
+ #
231
+ # >> Generators.hash(Generators.printable_ascii_string, Generators.positive_integer).sample(5, size: 3, rng: Random.new(42))
232
+ # => [{""=>2, "g\\4"=>4, "rv"=>2}, {"7"=>2}, {"!"=>1, "E!"=>1}, {"kY5"=>2}, {}]
233
+ def hash(key_generator, value_generator)
234
+ array(tuple(key_generator, value_generator))
235
+ .map(&:to_h)
236
+ end
237
+
238
+
239
+ @alphanumeric_chars = [('a'..'z'), ('A'..'Z'), ('0'..'9')].flat_map(&:to_a).freeze
240
+ ##
241
+ # Generates a single-character string
242
+ # containing one of a..z, A..Z, 0..9
243
+ #
244
+ # Shrinks towards lowercase 'a'.
245
+ #
246
+ def alphanumeric_char
247
+ one_of(*@alphanumeric_chars.map(&method(:constant)))
248
+ end
249
+
250
+ ##
251
+ # Generates a string
252
+ # containing only the characters a..z, A..Z, 0..9
253
+ # Shrinks towards fewer characters, and towards lowercase 'a'.
254
+ def alphanumeric_string
255
+ array(alphanumeric_char).map(&:join)
256
+ end
257
+
258
+ @printable_ascii_chars = (' '..'~').to_a.freeze
259
+
260
+ ##
261
+ # Generates a single-character string
262
+ # from the printable ASCII character set.
263
+ #
264
+ # Shrinks towards ' '.
265
+ #
266
+ # >> Generators.printable_ascii_char.sample(size: 10, rng: Random.new(42))
267
+ # => ["S", "|", ".", "g", "\\", "4", "r", "v", "j", "j"]
268
+ def printable_ascii_char
269
+ one_of(*@printable_ascii_chars.map(&method(:constant)))
270
+ end
271
+
272
+ ##
273
+ # Generates strings
274
+ # from the printable ASCII character set.
275
+ # Shrinks towards fewer characters, and towards ' '.
276
+ def printable_ascii_string
277
+ array(printable_ascii_char).map(&:join)
278
+ end
279
+
280
+ @ascii_chars = [
281
+ @printable_ascii_chars,
282
+ [
283
+ "\n",
284
+ "\r",
285
+ "\t",
286
+ "\v",
287
+ "\b",
288
+ "\f",
289
+ "\e",
290
+ "\d",
291
+ "\a"
292
+ ]
293
+ ].flat_map(&:to_a).freeze
294
+
295
+ ##
296
+ # Generates a single-character string
297
+ # from the printable ASCII character set.
298
+ # Shrinks towards '\n'.
299
+ def ascii_char
300
+ one_of(*@ascii_chars.map(&method(:constant)))
301
+ end
302
+
303
+ ##
304
+ # Generates strings
305
+ # from the printable ASCII character set.
306
+ # Shrinks towards fewer characters, and towards '\n'.
307
+ def ascii_string
308
+ array(ascii_char).map(&:join)
309
+ end
310
+
311
+ @printable_chars = [
312
+ @ascii_chars,
313
+ "\u{A0}".."\u{D7FF}",
314
+ "\u{E000}".."\u{FFFD}",
315
+ "\u{10000}".."\u{10FFFF}"
316
+ ].flat_map(&:to_a).freeze
317
+
318
+ ##
319
+ # Generates a single-character printable string
320
+ # both ASCII characters and Unicode.
321
+ #
322
+ # Shrinks towards characters with lower codepoints, e.g. ASCII
323
+ #
324
+ def printable_char
325
+ one_of(*@printable_chars.map(&method(:constant)))
326
+ end
327
+
328
+ ##
329
+ # Generates a printable string
330
+ # both ASCII characters and Unicode.
331
+ #
332
+ # Shrinks towards shorter strings, and towards characters with lower codepoints, e.g. ASCII
333
+ #
334
+ def printable_string
335
+ array(printable_char).map(&:join)
336
+ end
337
+
338
+ ##
339
+ # Generates a single unicode character
340
+ # (both printable and non-printable).
341
+ #
342
+ # Shrinks towards characters with lower codepoints, e.g. ASCII
343
+ #
344
+ def char
345
+ choose(0..0x10FFFF).map do |num|
346
+ [num].pack('U')
347
+ end
348
+ end
349
+
350
+ ##
351
+ # Generates a string of unicode characters
352
+ # (which might contain both printable and non-printable characters).
353
+ #
354
+ # Shrinks towards characters with lower codepoints, e.g. ASCII
355
+ #
356
+ def string
357
+ array(char).map(&:join)
358
+ end
359
+
360
+ ##
361
+ # Generates either `true` or `false`
362
+ #
363
+ # Shrinks towards `false`
364
+ #
365
+ def boolean
366
+ one_of(constant(false), constant(true))
367
+ end
368
+
369
+ ##
370
+ # Generates always `nil`.
371
+ #
372
+ # Does not shrink.
373
+ def nil
374
+ constant(nil)
375
+ end
376
+
377
+ ##
378
+ # Generates `nil` or `false`.
379
+ #
380
+ # Shrinks towards `nil`.
381
+ #
382
+ def falsey
383
+ one_of(constant(nil), constant(false))
384
+ end
385
+
386
+ ##
387
+ # Generates common terms that are not `nil` or `false`.
388
+ #
389
+ # Shrinks towards simpler terms, like `true`, an empty array, a single character or an integer.
390
+ #
391
+ def truthy
392
+ one_of(constant(true),
393
+ constant([]),
394
+ char,
395
+ integer,
396
+ float,
397
+ string,
398
+ array(integer),
399
+ array(float),
400
+ array(char),
401
+ array(string),
402
+ hash(symbol, integer),
403
+ hash(string, integer),
404
+ hash(string, string)
405
+ )
406
+ end
407
+
408
+ ##
409
+ # Generates whatever `other_generator` generates
410
+ # but sometimes instead `nil`.`
411
+ def nillable(other_generator)
412
+ frequency(9 => other_generator, 1 => constant(nil))
413
+ end
414
+ end
415
+ end
@@ -0,0 +1,27 @@
1
+ module PropCheck
2
+ ##
3
+ # Helper functions that have no other place to live
4
+ module Helper
5
+ extend self
6
+ ##
7
+ # Creates a (potentially lazy) Enumerator
8
+ # starting with `elem`
9
+ # with each consecutive element obtained
10
+ # by calling `operation` on the previous element.
11
+ #
12
+ # >> Helper.scanl(0, &:next).take(10).force
13
+ # => [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
14
+ # >> Helper.scanl([0, 1]) { |curr, next_elem| [next_elem, curr + next_elem] }.map(&:first).take(10).force
15
+ # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
16
+ def scanl(elem, &operation)
17
+ Enumerator.new do |yielder|
18
+ acc = elem
19
+ loop do
20
+ # p acc
21
+ yielder << acc
22
+ acc = operation.call(acc)
23
+ end
24
+ end.lazy
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,142 @@
1
+ ##
2
+ # A refinement for enumerators
3
+ # to allow lazy appending of two (potentially lazy) enumerators:
4
+ module LazyAppend
5
+ refine Enumerable do
6
+ ## >> [1,2,3].lazy_append([4,5.6]).to_a
7
+ ## => [1,2,3,4,5,6]
8
+ def lazy_append(other_enumerator)
9
+ [self, other_enumerator].lazy.flat_map(&:lazy)
10
+ end
11
+ end
12
+ end
13
+
14
+ module PropCheck
15
+ ##
16
+ # A Rose tree with the root being eager,
17
+ # and the children computed lazily, on demand.
18
+ class LazyTree
19
+ using LazyAppend
20
+
21
+ attr_accessor :root, :children
22
+ def initialize(root, children = [].lazy)
23
+ @root = root
24
+ @children = children
25
+ end
26
+
27
+ ##
28
+ # Maps `block` eagerly over `root` and lazily over `children`, returning a new LazyTree as result.
29
+ #
30
+ # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).map(&:next).to_a
31
+ # => LazyTree.new(2, [LazyTree.new(3, [LazyTree.new(4)]), LazyTree.new(5)]).to_a
32
+ def map(&block)
33
+ new_root = block.call(root)
34
+ new_children = children.map { |child_tree| child_tree.map(&block) }
35
+ LazyTree.new(new_root, new_children)
36
+ end
37
+
38
+ ##
39
+ # Turns a tree of trees
40
+ # in a single flattened tree, with subtrees that are closer to the root
41
+ # and the left subtree earlier in the list of children.
42
+ # TODO: Check for correctness
43
+ # def flatten
44
+ # root_tree = root
45
+ # root_root = root_tree.root
46
+
47
+ # root_children = root_tree.children
48
+ # flattened_children = children.map(&:flatten)
49
+
50
+ # combined_children = root_children.lazy_append(flattened_children)
51
+
52
+ # LazyTree.new(root_root, combined_children)
53
+ # end
54
+
55
+ def self.wrap(val)
56
+ LazyTree.new(val)
57
+ end
58
+
59
+ def bind(&fun)
60
+ inner_tree = fun.call(root)
61
+ inner_root = inner_tree.root
62
+ inner_children = inner_tree.children
63
+ mapped_children = children.map { |child| child.bind(&fun) }
64
+
65
+ combined_children = inner_children.lazy_append(mapped_children)
66
+
67
+ LazyTree.new(inner_root, combined_children)
68
+ end
69
+
70
+ ##
71
+ # Turns a LazyTree in a long lazy enumerable, with the root first followed by its children
72
+ # (and the first children's result before later children; i.e. a depth-first traversal.)
73
+ #
74
+ # Be aware that this lazy enumerable is potentially infinite,
75
+ # possibly uncountably so.
76
+ #
77
+ # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).each.force
78
+ # => [1, 4, 2, 3]
79
+ def each(&block)
80
+ squish = lambda do |tree, list|
81
+ new_children = tree.children.reduce(list) { |acc, elem| squish.call(elem, acc) }
82
+ [tree.root].lazy_append(new_children)
83
+ end
84
+
85
+ squish.call(self, [])
86
+
87
+ # base = [root]
88
+ # recursive = children.map(&:each)
89
+ # res = base.lazy_append(recursive)
90
+
91
+ # return res.each(&block) if block_given?
92
+
93
+ # res
94
+
95
+ # res = [[root], children.flat_map(&:each)].lazy.flat_map(&:lazy)
96
+ # res = res.map(&block) if block_given?
97
+ # res
98
+ end
99
+
100
+ ##
101
+ # Fully evaluate the LazyTree into an eager array, with the root first followed by its children
102
+ # (and the first children's result before later children; i.e. a depth-first traversal.)
103
+ #
104
+ # Be aware that calling this might make Ruby attempt to evaluate an infinite collection.
105
+ # Therefore, it is mostly useful for debugging; in production you probably want to use
106
+ # the other mechanisms this class provides..
107
+ #
108
+ # >> LazyTree.new(1, [LazyTree.new(2, [LazyTree.new(3)]), LazyTree.new(4)]).to_a
109
+ # => [1, 4, 2, 3]
110
+ def to_a
111
+ each.force
112
+ end
113
+
114
+ # TODO: fix implementation
115
+ def self.zip(trees)
116
+ # p "TREES: "
117
+ # p trees.to_a
118
+ # p "END TREES"
119
+ # raise "Boom!" unless trees.to_a.is_a?(Array) && trees.to_a.first.is_a?(LazyTree)
120
+ # p self
121
+ new_root = trees.to_a.map(&:root)
122
+ # p new_root
123
+ # new_children = trees.permutations.flat_map(&:children)
124
+ new_children = permutations(trees).map { |children| LazyTree.zip(children) }
125
+ # p new_children
126
+ LazyTree.new(new_root, new_children)
127
+ end
128
+
129
+ private_class_method def self.permutations(trees)
130
+ # p trees
131
+ trees.lazy.each_with_index.flat_map do |tree, index|
132
+ tree.children.map do |child|
133
+ child_trees = trees.to_a.clone
134
+ child_trees[index] = child
135
+ # p "CHILD TREES:"
136
+ # p child_trees
137
+ child_trees.lazy
138
+ end
139
+ end
140
+ end
141
+ end
142
+ end