zizek 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +115 -0
- data/GUIDE.md +774 -0
- data/LICENSE +201 -0
- data/README.md +201 -0
- data/docs/assets/zizek-rb-logo-dark.svg +16 -0
- data/docs/assets/zizek-rb-logo.svg +16 -0
- data/examples/combinators.rb +93 -0
- data/examples/coverage_assertions.rb +138 -0
- data/examples/database_workflow.rb +120 -0
- data/examples/die_hard.rb +122 -0
- data/examples/hypothesis_quickstart.rb +44 -0
- data/examples/keyed_draws.rb +123 -0
- data/examples/ledger.rb +114 -0
- data/examples/min_stack.rb +108 -0
- data/examples/minitest_suite.rb +64 -0
- data/examples/multi.rb +46 -0
- data/examples/queue.rb +122 -0
- data/examples/roundtrip.rb +71 -0
- data/examples/stack.rb +68 -0
- data/examples/tour.rb +126 -0
- data/lib/zizek/database.rb +211 -0
- data/lib/zizek/gen.rb +10377 -0
- data/lib/zizek/generators.rb +879 -0
- data/lib/zizek/minitest.rb +94 -0
- data/lib/zizek/rspec.rb +67 -0
- data/lib/zizek/settings.rb +249 -0
- data/lib/zizek/shell.rb +2938 -0
- data/lib/zizek/stateful.rb +349 -0
- data/lib/zizek/version.rb +8 -0
- data/lib/zizek.rb +8 -0
- metadata +109 -0
|
@@ -0,0 +1,879 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "set"
|
|
4
|
+
|
|
5
|
+
module Zizek
|
|
6
|
+
module Generators
|
|
7
|
+
# Transformations every generator carries. Neither records a choice of its
|
|
8
|
+
# own, so composing them leaves the reproduce blob byte-compatible with the
|
|
9
|
+
# other packages.
|
|
10
|
+
module Combinators
|
|
11
|
+
# Apply a block to each drawn value.
|
|
12
|
+
def map(&transform)
|
|
13
|
+
MappedGenerator.new(self, transform)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Keep only values satisfying a block. Draws are retried a few times
|
|
17
|
+
# before the example is rejected, matching how the engine treats a
|
|
18
|
+
# filtered generator: a rejection discards the example rather than
|
|
19
|
+
# failing the property.
|
|
20
|
+
def filter(&predicate)
|
|
21
|
+
FilteredGenerator.new(self, predicate)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Build the next generator from each drawn value — monadic bind.
|
|
25
|
+
#
|
|
26
|
+
# Generators.integers(0..3).flat_map do |length|
|
|
27
|
+
# Generators.lists(Generators.booleans, min: length, max: length)
|
|
28
|
+
# end
|
|
29
|
+
#
|
|
30
|
+
# Both draws share one `flat_map` span, so the shrinker can delete or
|
|
31
|
+
# simplify the whole unit together: shrinking the source value changes
|
|
32
|
+
# which inner generator runs, and the inner draws replay or pun by the
|
|
33
|
+
# ordinary prefix rules.
|
|
34
|
+
def flat_map(&transform)
|
|
35
|
+
FlatMappedGenerator.new(self, transform)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Either generator, shrinking towards the left one.
|
|
39
|
+
#
|
|
40
|
+
# Generators.integers(max: 0) | Generators.text(max: 4)
|
|
41
|
+
#
|
|
42
|
+
# Chains flatten: `a | b | c` is the three-way `one_of(a, b, c)`, not a
|
|
43
|
+
# two-way choice nested inside another, so the shrinker sees one index
|
|
44
|
+
# to lower rather than two.
|
|
45
|
+
def |(other)
|
|
46
|
+
Generators.one_of(*Generators.__send__(:one_of_parts, self), *Generators.__send__(:one_of_parts, other))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# One value, for a REPL or a docstring.
|
|
50
|
+
#
|
|
51
|
+
# This is not a generator you can shrink or replay — it runs no property
|
|
52
|
+
# and records nothing. It exists so `Generators.text(max: 5).example`
|
|
53
|
+
# answers "what does this actually produce?" without writing a test.
|
|
54
|
+
def example(seed: nil)
|
|
55
|
+
rng = Random.new(seed.nil? ? Random.new_seed : seed)
|
|
56
|
+
attempts = 0
|
|
57
|
+
begin
|
|
58
|
+
attempts += 1
|
|
59
|
+
TestCase.new(rng: rng).draw(self)
|
|
60
|
+
rescue AssumptionRejected, ChoiceOverrun
|
|
61
|
+
retry if attempts < 32
|
|
62
|
+
raise Unsatisfiable, "no example after #{attempts} attempts; the filter may be too strict"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class MappedGenerator
|
|
68
|
+
include Combinators
|
|
69
|
+
|
|
70
|
+
def initialize(source, transform)
|
|
71
|
+
@source = source
|
|
72
|
+
@transform = transform
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def do_draw(test_case)
|
|
76
|
+
@transform.call(@source.do_draw(test_case))
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
class FlatMappedGenerator
|
|
81
|
+
include Combinators
|
|
82
|
+
|
|
83
|
+
def initialize(source, transform)
|
|
84
|
+
@source = source
|
|
85
|
+
@transform = transform
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def do_draw(test_case)
|
|
89
|
+
test_case.start_span("flat_map")
|
|
90
|
+
value = @source.do_draw(test_case)
|
|
91
|
+
result = Generators.__send__(:coerce, @transform.call(value)).do_draw(test_case)
|
|
92
|
+
test_case.stop_span
|
|
93
|
+
result
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class FilteredGenerator
|
|
98
|
+
include Combinators
|
|
99
|
+
|
|
100
|
+
def initialize(source, predicate)
|
|
101
|
+
@source = source
|
|
102
|
+
@predicate = predicate
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def do_draw(test_case)
|
|
106
|
+
# The attempt budget is the same one a unique collection spends per
|
|
107
|
+
# element, so it comes from ir/many.zir rather than a constant here.
|
|
108
|
+
attempts = 0
|
|
109
|
+
while Core.filter_retry_allowed(attempts)
|
|
110
|
+
value = @source.do_draw(test_case)
|
|
111
|
+
attempts += 1
|
|
112
|
+
return value if @predicate.call(value)
|
|
113
|
+
end
|
|
114
|
+
test_case.assume(false)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Always the same value, recording no choice at all.
|
|
119
|
+
class JustGenerator
|
|
120
|
+
include Combinators
|
|
121
|
+
|
|
122
|
+
def initialize(value)
|
|
123
|
+
@value = value
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def do_draw(_test_case)
|
|
127
|
+
@value
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
class BooleanGenerator
|
|
132
|
+
include Combinators
|
|
133
|
+
|
|
134
|
+
def do_draw(test_case)
|
|
135
|
+
test_case.boolean
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
class WeightedBooleanGenerator
|
|
140
|
+
include Combinators
|
|
141
|
+
|
|
142
|
+
attr_reader :probability
|
|
143
|
+
private :probability
|
|
144
|
+
|
|
145
|
+
def initialize(probability)
|
|
146
|
+
@probability = probability
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def do_draw(test_case)
|
|
150
|
+
test_case.weighted(@probability)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
class BytesGenerator
|
|
155
|
+
include Combinators
|
|
156
|
+
|
|
157
|
+
attr_reader :minimum, :maximum
|
|
158
|
+
private :minimum, :maximum
|
|
159
|
+
|
|
160
|
+
def initialize(minimum = 0, maximum = 16)
|
|
161
|
+
@minimum = minimum
|
|
162
|
+
@maximum = maximum
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def min_size(value)
|
|
166
|
+
self.class.new(value, maximum)
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def max_size(value)
|
|
170
|
+
self.class.new(minimum, value)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def do_draw(test_case)
|
|
174
|
+
test_case.bytes(minimum, maximum)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# One of a fixed sequence, shrinking towards the first element. The index
|
|
179
|
+
# is a real integer choice, so it shrinks and replays like any other.
|
|
180
|
+
class SampledGenerator
|
|
181
|
+
include Combinators
|
|
182
|
+
|
|
183
|
+
def initialize(values)
|
|
184
|
+
@values = values
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def do_draw(test_case)
|
|
188
|
+
@values[test_case.integer(0, @values.length - 1)]
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
# One of several generators, shrinking towards the first.
|
|
193
|
+
class OneOfGenerator
|
|
194
|
+
include Combinators
|
|
195
|
+
|
|
196
|
+
attr_reader :choices
|
|
197
|
+
private :choices
|
|
198
|
+
|
|
199
|
+
def initialize(choices)
|
|
200
|
+
@choices = choices
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def do_draw(test_case)
|
|
204
|
+
@choices[test_case.integer(0, @choices.length - 1)].do_draw(test_case)
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Each generator in order, collected into an array.
|
|
209
|
+
class TupleGenerator
|
|
210
|
+
include Combinators
|
|
211
|
+
|
|
212
|
+
def initialize(parts)
|
|
213
|
+
@parts = parts
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def do_draw(test_case)
|
|
217
|
+
@parts.map { |part| part.do_draw(test_case) }
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# A generator assembled by a block that draws as it goes.
|
|
222
|
+
class CompositeGenerator
|
|
223
|
+
include Combinators
|
|
224
|
+
|
|
225
|
+
def initialize(build)
|
|
226
|
+
@build = build
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
# The block is handed the test case itself rather than a bare draw
|
|
230
|
+
# lambda, so a composite can `assume`, `note`, and name its draws the
|
|
231
|
+
# same way a property can.
|
|
232
|
+
def do_draw(test_case)
|
|
233
|
+
@build.call(test_case)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
# A generator that builds an object out of drawn parts.
|
|
238
|
+
class BuiltGenerator
|
|
239
|
+
include Combinators
|
|
240
|
+
|
|
241
|
+
def initialize(builder, parts, keyword_parts)
|
|
242
|
+
@builder = builder
|
|
243
|
+
@parts = parts
|
|
244
|
+
@keyword_parts = keyword_parts
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def do_draw(test_case)
|
|
248
|
+
args = @parts.map { |part| part.do_draw(test_case) }
|
|
249
|
+
if @keyword_parts.empty?
|
|
250
|
+
@builder.call(*args)
|
|
251
|
+
else
|
|
252
|
+
kwargs = @keyword_parts.transform_values { |part| part.do_draw(test_case) }
|
|
253
|
+
@builder.call(*args, **kwargs)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
module Zizek
|
|
261
|
+
module Generators
|
|
262
|
+
class IntegerGenerator
|
|
263
|
+
include Combinators
|
|
264
|
+
|
|
265
|
+
attr_reader :minimum, :maximum, :target
|
|
266
|
+
private :minimum, :maximum, :target
|
|
267
|
+
|
|
268
|
+
def initialize(minimum = -(1 << 63), maximum = (1 << 63) - 1, target = nil)
|
|
269
|
+
@minimum = minimum
|
|
270
|
+
@maximum = maximum
|
|
271
|
+
@target = target
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def min_value(value)
|
|
275
|
+
self.class.new(value, maximum, target)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
def max_value(value)
|
|
279
|
+
self.class.new(minimum, value, target)
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def shrink_towards(value)
|
|
283
|
+
self.class.new(minimum, maximum, value)
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def do_draw(test_case)
|
|
287
|
+
# An explicit target is passed through unclamped: integer_simplest
|
|
288
|
+
# already clamps it to the nearer bound. The default is compiled so the
|
|
289
|
+
# shells cannot drift on it.
|
|
290
|
+
destination = target
|
|
291
|
+
destination = Core.integer_default_shrink_target(minimum, maximum) if destination.nil?
|
|
292
|
+
test_case.integer(minimum, maximum, shrink_towards: destination)
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# Text over an alphabet, by default every Unicode scalar value. Values
|
|
297
|
+
# shrink shortlex over the alphabet's shrink order — shorter first, then
|
|
298
|
+
# towards "0", the digits, and the uppercase letters — so a minimal
|
|
299
|
+
# failing string is one a reader can retype.
|
|
300
|
+
class TextGenerator
|
|
301
|
+
include Combinators
|
|
302
|
+
|
|
303
|
+
attr_reader :minimum, :maximum, :characters
|
|
304
|
+
private :minimum, :maximum, :characters
|
|
305
|
+
|
|
306
|
+
def initialize(minimum = 0, maximum = nil, characters = nil)
|
|
307
|
+
@minimum = minimum
|
|
308
|
+
@maximum = maximum
|
|
309
|
+
@characters = characters
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def min_size(value)
|
|
313
|
+
self.class.new(value, maximum, characters)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def max_size(value)
|
|
317
|
+
self.class.new(minimum, value, characters)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Restrict the characters drawn. Takes either the characters themselves
|
|
321
|
+
# or an interval set from Zizek.alphabet_range.
|
|
322
|
+
def alphabet(value)
|
|
323
|
+
value = Zizek.alphabet(value) if value.is_a?(String) && value.encoding != Encoding::BINARY
|
|
324
|
+
self.class.new(minimum, maximum, value)
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
def do_draw(test_case)
|
|
328
|
+
limit = maximum.nil? ? Core.text_widened_max(minimum) : maximum
|
|
329
|
+
test_case.string(minimum, limit, alphabet: characters)
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
class FloatGenerator
|
|
334
|
+
include Combinators
|
|
335
|
+
|
|
336
|
+
attr_reader :minimum, :maximum, :allow_nan_values, :allow_infinity_values,
|
|
337
|
+
:smallest_nonzero
|
|
338
|
+
private :minimum, :maximum, :allow_nan_values, :allow_infinity_values, :smallest_nonzero
|
|
339
|
+
|
|
340
|
+
def initialize(
|
|
341
|
+
minimum = -Float::INFINITY,
|
|
342
|
+
maximum = Float::INFINITY,
|
|
343
|
+
allow_nan_values = false,
|
|
344
|
+
allow_infinity_values = false,
|
|
345
|
+
smallest_nonzero = 0.0
|
|
346
|
+
)
|
|
347
|
+
@minimum = minimum
|
|
348
|
+
@maximum = maximum
|
|
349
|
+
@allow_nan_values = allow_nan_values
|
|
350
|
+
@allow_infinity_values = allow_infinity_values
|
|
351
|
+
@smallest_nonzero = smallest_nonzero
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
def copy(**changes)
|
|
355
|
+
self.class.new(
|
|
356
|
+
changes.fetch(:minimum, minimum),
|
|
357
|
+
changes.fetch(:maximum, maximum),
|
|
358
|
+
changes.fetch(:allow_nan_values, allow_nan_values),
|
|
359
|
+
changes.fetch(:allow_infinity_values, allow_infinity_values),
|
|
360
|
+
changes.fetch(:smallest_nonzero, smallest_nonzero)
|
|
361
|
+
)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
private :copy
|
|
365
|
+
|
|
366
|
+
def min_value(value)
|
|
367
|
+
copy(minimum: value)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def max_value(value)
|
|
371
|
+
copy(maximum: value)
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def allow_nan(value = true)
|
|
375
|
+
copy(allow_nan_values: value)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def allow_infinity(value = true)
|
|
379
|
+
copy(allow_infinity_values: value)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def smallest_nonzero_magnitude(value)
|
|
383
|
+
copy(smallest_nonzero: value)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def do_draw(test_case)
|
|
387
|
+
test_case.float(
|
|
388
|
+
minimum,
|
|
389
|
+
maximum,
|
|
390
|
+
allow_nan: allow_nan_values,
|
|
391
|
+
allow_infinity: allow_infinity_values,
|
|
392
|
+
smallest_nonzero_magnitude: smallest_nonzero
|
|
393
|
+
)
|
|
394
|
+
end
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
# A variable-length list of values drawn from one element generator.
|
|
398
|
+
class ListGenerator
|
|
399
|
+
include Combinators
|
|
400
|
+
|
|
401
|
+
attr_reader :element, :minimum, :maximum
|
|
402
|
+
private :element, :minimum, :maximum
|
|
403
|
+
|
|
404
|
+
def initialize(element, minimum = 0, maximum = nil)
|
|
405
|
+
@element = element
|
|
406
|
+
@minimum = minimum
|
|
407
|
+
@maximum = maximum
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def min_size(value)
|
|
411
|
+
self.class.new(element, value, maximum)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def max_size(value)
|
|
415
|
+
self.class.new(element, minimum, value)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
def do_draw(test_case)
|
|
419
|
+
test_case.__send__(:collect, element, minimum, maximum, "list.element")
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# A variable-length collection whose elements are distinct under a key.
|
|
424
|
+
class UniqueListGenerator
|
|
425
|
+
include Combinators
|
|
426
|
+
|
|
427
|
+
attr_reader :element, :key, :minimum, :maximum, :label
|
|
428
|
+
private :element, :key, :minimum, :maximum, :label
|
|
429
|
+
|
|
430
|
+
def initialize(element, key = nil, minimum = 0, maximum = nil, label = "list.element")
|
|
431
|
+
@element = element
|
|
432
|
+
@key = key
|
|
433
|
+
@minimum = minimum
|
|
434
|
+
@maximum = maximum
|
|
435
|
+
@label = label
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def min_size(value)
|
|
439
|
+
self.class.new(element, key, value, maximum, label)
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
def max_size(value)
|
|
443
|
+
self.class.new(element, key, minimum, value, label)
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def unique_by(&block)
|
|
447
|
+
self.class.new(element, block, minimum, maximum, label)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
def do_draw(test_case)
|
|
451
|
+
test_case.__send__(:collect_unique, element, key, minimum, maximum, label)
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# A set of distinct values, materialized as a Ruby Set.
|
|
456
|
+
class SetGenerator < UniqueListGenerator
|
|
457
|
+
def initialize(element, minimum = 0, maximum = nil)
|
|
458
|
+
super(element, nil, minimum, maximum, "set.element")
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
def min_size(value)
|
|
462
|
+
self.class.new(element, value, maximum)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def max_size(value)
|
|
466
|
+
self.class.new(element, minimum, value)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def do_draw(test_case)
|
|
470
|
+
Set.new(super)
|
|
471
|
+
end
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
# One key/value pair, drawn as a unit so that deleting its span deletes both
|
|
475
|
+
# halves together.
|
|
476
|
+
class PairGenerator
|
|
477
|
+
include Combinators
|
|
478
|
+
|
|
479
|
+
attr_reader :keys, :values
|
|
480
|
+
private :keys, :values
|
|
481
|
+
|
|
482
|
+
def initialize(keys, values)
|
|
483
|
+
@keys = keys
|
|
484
|
+
@values = values
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def do_draw(test_case)
|
|
488
|
+
[test_case.draw(keys), test_case.draw(values)]
|
|
489
|
+
end
|
|
490
|
+
end
|
|
491
|
+
|
|
492
|
+
# A Hash whose keys are distinct.
|
|
493
|
+
class HashGenerator
|
|
494
|
+
include Combinators
|
|
495
|
+
|
|
496
|
+
attr_reader :keys, :values, :minimum, :maximum
|
|
497
|
+
private :keys, :values, :minimum, :maximum
|
|
498
|
+
|
|
499
|
+
def initialize(keys, values, minimum = 0, maximum = nil)
|
|
500
|
+
@keys = keys
|
|
501
|
+
@values = values
|
|
502
|
+
@minimum = minimum
|
|
503
|
+
@maximum = maximum
|
|
504
|
+
end
|
|
505
|
+
|
|
506
|
+
def min_size(value)
|
|
507
|
+
self.class.new(keys, values, value, maximum)
|
|
508
|
+
end
|
|
509
|
+
|
|
510
|
+
def max_size(value)
|
|
511
|
+
self.class.new(keys, values, minimum, value)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
def do_draw(test_case)
|
|
515
|
+
entries = test_case.__send__(:collect_unique,
|
|
516
|
+
PairGenerator.new(keys, values), ->(entry) { entry[0] }, minimum, maximum, "hash.entry"
|
|
517
|
+
)
|
|
518
|
+
entries.to_h
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
class RecursiveLeafBudget < StandardError; end
|
|
523
|
+
|
|
524
|
+
class RecursiveState
|
|
525
|
+
attr_accessor :attempt, :leaves, :start_price, :closed_children,
|
|
526
|
+
:closed_branches, :open_branches, :reprices
|
|
527
|
+
attr_reader :max_depth, :max_leaves, :target
|
|
528
|
+
|
|
529
|
+
def initialize(max_depth, max_leaves, target)
|
|
530
|
+
@max_depth = max_depth
|
|
531
|
+
@max_leaves = max_leaves
|
|
532
|
+
@target = target
|
|
533
|
+
@attempt = @leaves = @closed_children = @closed_branches = @reprices = 0
|
|
534
|
+
@start_price = 0.0
|
|
535
|
+
@open_branches = []
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def observe_decision(depth)
|
|
539
|
+
while !open_branches.empty? && open_branches.last[0] >= depth
|
|
540
|
+
_, children = open_branches.pop
|
|
541
|
+
self.closed_children += children
|
|
542
|
+
self.closed_branches += 1
|
|
543
|
+
end
|
|
544
|
+
open_branches.last[1] += 1 if depth.positive? && !open_branches.empty?
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def close_branches
|
|
548
|
+
until open_branches.empty?
|
|
549
|
+
_, children = open_branches.pop
|
|
550
|
+
self.closed_children += children
|
|
551
|
+
self.closed_branches += 1
|
|
552
|
+
end
|
|
553
|
+
end
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
class RecursiveSubtreeGenerator
|
|
557
|
+
include Combinators
|
|
558
|
+
|
|
559
|
+
def initialize(owner, state, depth)
|
|
560
|
+
@owner = owner
|
|
561
|
+
@state = state
|
|
562
|
+
@depth = depth
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def do_draw(test_case)
|
|
566
|
+
test_case.start_span("recursive")
|
|
567
|
+
@state.observe_decision(@depth)
|
|
568
|
+
probability = Core.recursive_decision_probability(
|
|
569
|
+
@state.max_depth, @depth, @state.target, @state.attempt,
|
|
570
|
+
@state.closed_children, @state.closed_branches
|
|
571
|
+
)
|
|
572
|
+
branch = if @depth >= @state.max_depth
|
|
573
|
+
test_case.__send__(:forced_boolean, false)
|
|
574
|
+
else
|
|
575
|
+
test_case.weighted(probability)
|
|
576
|
+
end
|
|
577
|
+
value = if branch
|
|
578
|
+
@state.open_branches << [@depth, 0]
|
|
579
|
+
@owner.__send__(:branch).call(
|
|
580
|
+
self.class.new(@owner, @state, @depth + 1)
|
|
581
|
+
).do_draw(test_case)
|
|
582
|
+
else
|
|
583
|
+
raise RecursiveLeafBudget if @state.leaves >= @state.max_leaves
|
|
584
|
+
|
|
585
|
+
@state.leaves += 1
|
|
586
|
+
@owner.__send__(:leaf).do_draw(test_case)
|
|
587
|
+
end
|
|
588
|
+
test_case.stop_span unless @depth.zero?
|
|
589
|
+
value
|
|
590
|
+
end
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
class RecursiveGenerator
|
|
594
|
+
include Combinators
|
|
595
|
+
|
|
596
|
+
attr_reader :leaf, :branch, :max_depth_limit, :max_leaves_limit
|
|
597
|
+
private :leaf, :branch, :max_depth_limit, :max_leaves_limit
|
|
598
|
+
|
|
599
|
+
def initialize(leaf, branch, max_depth = 32, max_leaves = 100)
|
|
600
|
+
@leaf = leaf
|
|
601
|
+
@branch = branch
|
|
602
|
+
@max_depth_limit = max_depth
|
|
603
|
+
@max_leaves_limit = max_leaves
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
def max_depth(value)
|
|
607
|
+
raise ArgumentError, "max_depth must be nonnegative" if value.negative?
|
|
608
|
+
|
|
609
|
+
self.class.new(leaf, branch, value, max_leaves_limit)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def max_leaves(value)
|
|
613
|
+
if value.negative? || value > (1 << 63) - 1
|
|
614
|
+
raise ArgumentError, "max_leaves must be in [0, 2^63 - 1]"
|
|
615
|
+
end
|
|
616
|
+
|
|
617
|
+
self.class.new(leaf, branch, max_depth_limit, value)
|
|
618
|
+
end
|
|
619
|
+
|
|
620
|
+
def do_draw(test_case)
|
|
621
|
+
base_depth = test_case.__send__(:open_span_depth)
|
|
622
|
+
target = 1
|
|
623
|
+
if Core.recursive_target_drawn(max_leaves_limit) &&
|
|
624
|
+
test_case.weighted(Core.recursive_large_target_probability)
|
|
625
|
+
target = test_case.integer(2, max_leaves_limit, _uniform: true)
|
|
626
|
+
end
|
|
627
|
+
state = RecursiveState.new(max_depth_limit, max_leaves_limit, target)
|
|
628
|
+
state.start_price = Core.recursive_priced_probability(0, 0, 0, target)
|
|
629
|
+
loop do
|
|
630
|
+
begin
|
|
631
|
+
value = RecursiveSubtreeGenerator.new(self, state, 0).do_draw(test_case)
|
|
632
|
+
rescue RecursiveLeafBudget
|
|
633
|
+
test_case.__send__(:reset_open_spans_to, base_depth)
|
|
634
|
+
state.open_branches.clear
|
|
635
|
+
state.leaves = 0
|
|
636
|
+
state.attempt += 1
|
|
637
|
+
test_case.assume(false) unless Core.recursive_retry_allowed(state.attempt)
|
|
638
|
+
state.start_price = Core.recursive_priced_probability(
|
|
639
|
+
state.closed_children, state.closed_branches, state.attempt, state.target
|
|
640
|
+
)
|
|
641
|
+
next
|
|
642
|
+
end
|
|
643
|
+
state.close_branches
|
|
644
|
+
unless Core.recursive_finish_mispriced(
|
|
645
|
+
state.max_depth, state.target, state.attempt, state.leaves,
|
|
646
|
+
state.closed_children, state.closed_branches, state.start_price, state.reprices
|
|
647
|
+
)
|
|
648
|
+
test_case.stop_span
|
|
649
|
+
return value
|
|
650
|
+
end
|
|
651
|
+
test_case.__send__(:reset_open_spans_to, base_depth)
|
|
652
|
+
state.leaves = 0
|
|
653
|
+
state.reprices += 1
|
|
654
|
+
state.start_price = Core.recursive_priced_probability(
|
|
655
|
+
state.closed_children, state.closed_branches, state.attempt, state.target
|
|
656
|
+
)
|
|
657
|
+
end
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
module_function
|
|
662
|
+
|
|
663
|
+
# Whole numbers, biased towards interesting boundaries and shrinking
|
|
664
|
+
# towards `shrink_towards` (zero, clamped into range, by default).
|
|
665
|
+
#
|
|
666
|
+
# Generators.integers(min: 0, max: 100)
|
|
667
|
+
# Generators.integers(min: -100, max: 100, shrink_towards: 10)
|
|
668
|
+
# Generators.integers(0..100) # the same, as a range
|
|
669
|
+
#
|
|
670
|
+
# The fluent form stays available for building a generator in stages, and
|
|
671
|
+
# means the same thing: `integers(min: 0, max: 100)` is
|
|
672
|
+
# `integers.min_value(0).max_value(100)`.
|
|
673
|
+
def integers(range = nil, min: nil, max: nil, shrink_towards: nil)
|
|
674
|
+
min, max = range_bounds(range, min, max, :integers, exclusive_end: true)
|
|
675
|
+
generator = IntegerGenerator.new
|
|
676
|
+
generator = generator.min_value(min) unless min.nil?
|
|
677
|
+
generator = generator.max_value(max) unless max.nil?
|
|
678
|
+
generator = generator.shrink_towards(shrink_towards) unless shrink_towards.nil?
|
|
679
|
+
generator
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
# Interpret the optional range argument the bounded factories accept.
|
|
683
|
+
#
|
|
684
|
+
# A range and explicit bounds are mutually exclusive rather than merged:
|
|
685
|
+
# `integers(0..10, min: 5)` reads as if one of them wins, and guessing
|
|
686
|
+
# which would be worse than refusing.
|
|
687
|
+
def range_bounds(range, min, max, name, exclusive_end:)
|
|
688
|
+
return [min, max] if range.nil?
|
|
689
|
+
unless min.nil? && max.nil?
|
|
690
|
+
raise ArgumentError, "#{name} takes either a range or min:/max:, not both"
|
|
691
|
+
end
|
|
692
|
+
raise ArgumentError, "#{name} takes a Range, got #{range.class}" unless range.is_a?(Range)
|
|
693
|
+
|
|
694
|
+
high = range.end
|
|
695
|
+
if range.exclude_end? && !high.nil?
|
|
696
|
+
unless exclusive_end
|
|
697
|
+
raise ArgumentError, "#{name} needs an inclusive range; #{range.inspect} excludes its end"
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
high -= 1
|
|
701
|
+
end
|
|
702
|
+
[range.begin, high]
|
|
703
|
+
end
|
|
704
|
+
private_class_method :range_bounds
|
|
705
|
+
|
|
706
|
+
# Generate a list whose elements each occupy a deletable span.
|
|
707
|
+
#
|
|
708
|
+
# Unlike the scalar generators this one has structure the shrinker can
|
|
709
|
+
# reduce: it deletes whole elements before it simplifies the values inside
|
|
710
|
+
# them, so a failing list minimizes towards the shortest list that still
|
|
711
|
+
# fails rather than towards small elements in a long list.
|
|
712
|
+
def lists(element, min: 0, max: nil, unique: false, unique_by: nil)
|
|
713
|
+
return ListGenerator.new(element, min, max) unless unique || unique_by
|
|
714
|
+
|
|
715
|
+
UniqueListGenerator.new(element, unique_by, min, max)
|
|
716
|
+
end
|
|
717
|
+
|
|
718
|
+
# Generate a Set of distinct values.
|
|
719
|
+
#
|
|
720
|
+
# A set is a unique list the shrinker can still delete elements from: each
|
|
721
|
+
# element keeps its own span, and a candidate that duplicates an earlier one
|
|
722
|
+
# is discarded rather than failing the example.
|
|
723
|
+
def sets(element, min: 0, max: nil)
|
|
724
|
+
SetGenerator.new(element, min, max)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
# Generate a Hash with distinct keys.
|
|
728
|
+
#
|
|
729
|
+
# Uniqueness is judged on the key alone, so the value generator is free to
|
|
730
|
+
# repeat.
|
|
731
|
+
def hashes(keys, values, min: 0, max: nil)
|
|
732
|
+
HashGenerator.new(keys, values, min, max)
|
|
733
|
+
end
|
|
734
|
+
|
|
735
|
+
# Generate text over an alphabet, by default every Unicode scalar value.
|
|
736
|
+
def text(min: 0, max: nil, alphabet: nil)
|
|
737
|
+
alphabet = Zizek.alphabet(alphabet) if alphabet.is_a?(String) && alphabet.encoding != Encoding::BINARY
|
|
738
|
+
TextGenerator.new(min, max, alphabet)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
# IEEE-754 doubles, biased towards interesting bit patterns.
|
|
742
|
+
#
|
|
743
|
+
# Generators.floats(min: 0.0, max: 1.0)
|
|
744
|
+
# Generators.floats(allow_nan: true, allow_infinity: true)
|
|
745
|
+
# Generators.floats(0.0..1.0)
|
|
746
|
+
#
|
|
747
|
+
# NaN and the infinities are excluded by default: a property that has an
|
|
748
|
+
# opinion about them should say so.
|
|
749
|
+
def floats(range = nil, min: nil, max: nil, allow_nan: nil, allow_infinity: nil,
|
|
750
|
+
smallest_nonzero_magnitude: nil)
|
|
751
|
+
min, max = range_bounds(range, min, max, :floats, exclusive_end: false)
|
|
752
|
+
generator = FloatGenerator.new
|
|
753
|
+
generator = generator.min_value(min) unless min.nil?
|
|
754
|
+
generator = generator.max_value(max) unless max.nil?
|
|
755
|
+
generator = generator.allow_nan(allow_nan) unless allow_nan.nil?
|
|
756
|
+
generator = generator.allow_infinity(allow_infinity) unless allow_infinity.nil?
|
|
757
|
+
unless smallest_nonzero_magnitude.nil?
|
|
758
|
+
generator = generator.smallest_nonzero_magnitude(smallest_nonzero_magnitude)
|
|
759
|
+
end
|
|
760
|
+
generator
|
|
761
|
+
end
|
|
762
|
+
|
|
763
|
+
# Generate booleans, shrinking towards false.
|
|
764
|
+
def booleans
|
|
765
|
+
BooleanGenerator.new
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
# Generate booleans that are true with the given probability. Recorded as
|
|
769
|
+
# an ordinary boolean choice: the weight biases generation only, so a
|
|
770
|
+
# blob replays the same whatever probability produced it.
|
|
771
|
+
def weighted_booleans(probability)
|
|
772
|
+
unless probability.is_a?(Numeric) && probability.finite? && probability >= 0.0 && probability <= 1.0
|
|
773
|
+
raise ArgumentError, "weighted_booleans requires a probability in [0.0, 1.0]"
|
|
774
|
+
end
|
|
775
|
+
|
|
776
|
+
WeightedBooleanGenerator.new(probability)
|
|
777
|
+
end
|
|
778
|
+
|
|
779
|
+
# Generate byte strings, shrinking towards shorter and lower values.
|
|
780
|
+
def binary(min: 0, max: 16)
|
|
781
|
+
BytesGenerator.new(min, max)
|
|
782
|
+
end
|
|
783
|
+
|
|
784
|
+
# Always produce one value, recording no choice.
|
|
785
|
+
def just(value)
|
|
786
|
+
JustGenerator.new(value)
|
|
787
|
+
end
|
|
788
|
+
|
|
789
|
+
# Always produce nil.
|
|
790
|
+
def none
|
|
791
|
+
JustGenerator.new(nil)
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
# Pick one of a fixed sequence, shrinking towards the first element.
|
|
795
|
+
def sampled_from(values)
|
|
796
|
+
values = values.to_a
|
|
797
|
+
raise ArgumentError, "sampled_from requires at least one value" if values.empty?
|
|
798
|
+
|
|
799
|
+
SampledGenerator.new(values)
|
|
800
|
+
end
|
|
801
|
+
|
|
802
|
+
# Draw from one of several generators, shrinking towards the first.
|
|
803
|
+
def one_of(*choices)
|
|
804
|
+
choices = choices.flatten(1)
|
|
805
|
+
raise ArgumentError, "one_of requires at least one generator" if choices.empty?
|
|
806
|
+
return choices.first if choices.length == 1
|
|
807
|
+
|
|
808
|
+
OneOfGenerator.new(choices)
|
|
809
|
+
end
|
|
810
|
+
|
|
811
|
+
# The parts `|` should flatten a generator into: its own alternatives if it
|
|
812
|
+
# is already a one_of, otherwise just itself.
|
|
813
|
+
def one_of_parts(generator)
|
|
814
|
+
generator.is_a?(OneOfGenerator) ? generator.__send__(:choices) : [generator]
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
# Build an object out of drawn parts.
|
|
818
|
+
#
|
|
819
|
+
# Generators.builds(Point, x: Generators.integers, y: Generators.integers)
|
|
820
|
+
# Generators.builds(->(a, b) { a..b }, Generators.integers, Generators.integers)
|
|
821
|
+
#
|
|
822
|
+
# The first argument is anything that responds to `call` or `new`.
|
|
823
|
+
# Arguments that are not generators are passed through unchanged, so a
|
|
824
|
+
# constant does not need wrapping in `just`.
|
|
825
|
+
def builds(target, *parts, **keyword_parts)
|
|
826
|
+
builder =
|
|
827
|
+
if target.respond_to?(:call) then target
|
|
828
|
+
elsif target.respond_to?(:new) then target.method(:new)
|
|
829
|
+
else raise ArgumentError, "builds needs something callable or a class, got #{target.inspect}"
|
|
830
|
+
end
|
|
831
|
+
|
|
832
|
+
BuiltGenerator.new(
|
|
833
|
+
builder,
|
|
834
|
+
parts.map { |part| coerce(part) },
|
|
835
|
+
keyword_parts.transform_values { |part| coerce(part) }
|
|
836
|
+
)
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
# Accept a plain value anywhere a generator is expected.
|
|
840
|
+
def coerce(value)
|
|
841
|
+
value.respond_to?(:do_draw) ? value : just(value)
|
|
842
|
+
end
|
|
843
|
+
|
|
844
|
+
# Draw each generator in order and collect the values into an array.
|
|
845
|
+
def tuples(*parts)
|
|
846
|
+
TupleGenerator.new(parts)
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
# Build a generator from a block that draws as it goes.
|
|
850
|
+
#
|
|
851
|
+
# The block is handed the test case, so it can do anything a property can
|
|
852
|
+
# — draw, assume, note:
|
|
853
|
+
#
|
|
854
|
+
# ordered = Generators.composite do |t|
|
|
855
|
+
# low = t.draw(Generators.integers(max: 10))
|
|
856
|
+
# [low, t.draw(Generators.integers(min: low))]
|
|
857
|
+
# end
|
|
858
|
+
def composite(&build)
|
|
859
|
+
CompositeGenerator.new(build)
|
|
860
|
+
end
|
|
861
|
+
|
|
862
|
+
# Generate recursively-defined values such as trees or JSON documents.
|
|
863
|
+
def recursive(leaf, &branch)
|
|
864
|
+
raise ArgumentError, "recursive requires a branch block" unless branch
|
|
865
|
+
|
|
866
|
+
RecursiveGenerator.new(coerce(leaf), branch)
|
|
867
|
+
end
|
|
868
|
+
|
|
869
|
+
private_class_method :coerce, :one_of_parts
|
|
870
|
+
private_constant :MappedGenerator, :FlatMappedGenerator, :FilteredGenerator,
|
|
871
|
+
:JustGenerator, :BooleanGenerator, :WeightedBooleanGenerator,
|
|
872
|
+
:BytesGenerator, :SampledGenerator, :OneOfGenerator,
|
|
873
|
+
:TupleGenerator, :CompositeGenerator, :BuiltGenerator,
|
|
874
|
+
:IntegerGenerator, :TextGenerator, :FloatGenerator,
|
|
875
|
+
:ListGenerator, :UniqueListGenerator, :SetGenerator,
|
|
876
|
+
:PairGenerator, :HashGenerator, :RecursiveLeafBudget,
|
|
877
|
+
:RecursiveState, :RecursiveSubtreeGenerator, :RecursiveGenerator
|
|
878
|
+
end
|
|
879
|
+
end
|