prop_check 0.6.0

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.
@@ -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