minitest-proptest 0.0.2
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/lib/minitest/proptest/gen.rb +525 -0
- data/lib/minitest/proptest/property.rb +177 -0
- data/lib/minitest/proptest/status.rb +61 -0
- data/lib/minitest/proptest/version.rb +7 -0
- data/lib/minitest/proptest.rb +26 -0
- data/lib/minitest/proptest_plugin.rb +52 -0
- metadata +66 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: e01d66af6e62a9d5931cd1da501b08679e8f78a4aa625aa694c28029ae5228a9
|
4
|
+
data.tar.gz: 3d99ae675bbeac0c89c807a535c7400fc0806a223a68c0d1a50eb2cb2917c148
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2a4abe803c73e8e254dfa2022e5bcbb84f3bdf6c1e502f7fa34d3d95c06dad05f44be371a935907135f8bb7f67cd1240b24835c7cf233e1fd85f52608ed53ac8
|
7
|
+
data.tar.gz: dd81efa85c34580d0d29cfbf21ab5042f0af14f493b7041712a61fd491638c5a6e02d79e758a459a03671110bf057474f9fae34c96c96607e8c0f1e7853c2e31
|
@@ -0,0 +1,525 @@
|
|
1
|
+
module Minitest
|
2
|
+
module Proptest
|
3
|
+
class Gen
|
4
|
+
class ValueGenerator
|
5
|
+
attr_accessor :entropy
|
6
|
+
attr_writer :type_parameters
|
7
|
+
|
8
|
+
def self.with_shrink_function(&f)
|
9
|
+
define_method(:shrink_function, &f)
|
10
|
+
self
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.with_score_function(&f)
|
14
|
+
define_method(:score_function, &f)
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.with_append(bound_min, bound_max, &f)
|
19
|
+
define_singleton_method(:bound_max) { bound_max }
|
20
|
+
define_singleton_method(:bound_min) { bound_min }
|
21
|
+
define_method(:append) do |other|
|
22
|
+
@value = f.call(value, other.value)
|
23
|
+
self
|
24
|
+
end
|
25
|
+
self
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.with_empty(&f)
|
29
|
+
define_singleton_method(:empty) do |gen|
|
30
|
+
temp = new(gen)
|
31
|
+
temp.instance_variable_set(:@value, f.call)
|
32
|
+
temp
|
33
|
+
end
|
34
|
+
self
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.bound_max
|
38
|
+
1
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.bound_min
|
42
|
+
0
|
43
|
+
end
|
44
|
+
|
45
|
+
# append is not expected to be called unless overridden
|
46
|
+
def append(other)
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.empty(gen)
|
51
|
+
self.new(gen)
|
52
|
+
end
|
53
|
+
|
54
|
+
def force(v)
|
55
|
+
temp = self.class.new(ArgumentError)
|
56
|
+
temp.instance_variable_set(:@value, v)
|
57
|
+
temp.type_parameters = @type_parameters
|
58
|
+
temp
|
59
|
+
end
|
60
|
+
|
61
|
+
def generate_value
|
62
|
+
gen = @generated.reduce(@generator) do |gen, val|
|
63
|
+
gen.call(val)
|
64
|
+
gen
|
65
|
+
end
|
66
|
+
|
67
|
+
while gen.is_a?(Proc) || gen.is_a?(Method)
|
68
|
+
gen = gen.call(*@type_parameters.map(&:value))
|
69
|
+
if gen.is_a?(ValueGenerator)
|
70
|
+
gen = gen.value
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
gen
|
75
|
+
end
|
76
|
+
|
77
|
+
def value
|
78
|
+
return false if @value == false
|
79
|
+
@value ||= generate_value
|
80
|
+
end
|
81
|
+
|
82
|
+
def prefix_entropy_generation(vals)
|
83
|
+
@generated = vals + @generated
|
84
|
+
end
|
85
|
+
|
86
|
+
def score
|
87
|
+
value
|
88
|
+
fs = @type_parameters.map { |x| x.method(:score_function) }
|
89
|
+
score_function(*fs, value)
|
90
|
+
end
|
91
|
+
|
92
|
+
def score_function(v)
|
93
|
+
v.to_i.abs
|
94
|
+
end
|
95
|
+
|
96
|
+
def shrink_candidates
|
97
|
+
fs = @type_parameters.map { |x| x.method(:shrink_function) }
|
98
|
+
os = score
|
99
|
+
candidates = shrink_function(*fs, value)
|
100
|
+
candidates
|
101
|
+
.map { |c| [force(c).score, c] }
|
102
|
+
.reject { |(s, _)| s > os }
|
103
|
+
.sort { |x, y| x.first <=> y.first }
|
104
|
+
.uniq
|
105
|
+
end
|
106
|
+
|
107
|
+
def shrink_function(x)
|
108
|
+
[x.itself]
|
109
|
+
end
|
110
|
+
|
111
|
+
def shrink_parameter(x)
|
112
|
+
@shrink_parameter.call(x)
|
113
|
+
end
|
114
|
+
|
115
|
+
# Generator helpers
|
116
|
+
|
117
|
+
def sized(n)
|
118
|
+
entropy.call(n + 1)
|
119
|
+
end
|
120
|
+
|
121
|
+
def one_of(r)
|
122
|
+
r.to_a[sized(r.to_a.length - 1)]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
class Int8 < Integer; end
|
127
|
+
class Int16 < Integer; end
|
128
|
+
class Int32 < Integer; end
|
129
|
+
class Int64 < Integer; end
|
130
|
+
class UInt8 < Integer; end
|
131
|
+
class UInt16 < Integer; end
|
132
|
+
class UInt32 < Integer; end
|
133
|
+
class UInt64 < Integer; end
|
134
|
+
class Float32 < Float; end
|
135
|
+
class Float64 < Float; end
|
136
|
+
class ASCIIChar < String; end
|
137
|
+
class Char < String; end
|
138
|
+
class Bool < TrueClass; end
|
139
|
+
|
140
|
+
# Default maximum random value size is the local architecture's word size
|
141
|
+
MAX_SIZE = ((1 << (1.size * 8)) - 1)
|
142
|
+
SIGN_BIT = (1 << ((1.size * 8) - 1))
|
143
|
+
|
144
|
+
attr_reader :generated
|
145
|
+
|
146
|
+
instance_variable_set(:@_generators, {})
|
147
|
+
|
148
|
+
def self.create_type_constructor(arity, classes)
|
149
|
+
constructor = ->(c1) do
|
150
|
+
if classes.length == arity
|
151
|
+
f.call(*classes)
|
152
|
+
else
|
153
|
+
->(c2) { constructor.call(c2) }
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.generator_for(klass, &f)
|
159
|
+
new_class = Class.new(ValueGenerator)
|
160
|
+
new_class.define_method(:initialize) do |g|
|
161
|
+
@entropy = ->(b = MAX_SIZE) { (@generated << g.rand(b)).last }
|
162
|
+
@generated = []
|
163
|
+
@generator = self.method(:generator).curry
|
164
|
+
@parent_gen = g
|
165
|
+
@value = nil
|
166
|
+
@type_parameters = []
|
167
|
+
end
|
168
|
+
|
169
|
+
new_class.define_method(:generator, &f)
|
170
|
+
|
171
|
+
instance_variable_get(:@_generators)[klass] = new_class #.method(:new)
|
172
|
+
self.const_set((klass.name + 'Gen').split('::').last, new_class)
|
173
|
+
new_class
|
174
|
+
end
|
175
|
+
|
176
|
+
def initialize(random)
|
177
|
+
@random = random
|
178
|
+
@generated = []
|
179
|
+
end
|
180
|
+
|
181
|
+
def rand(max_size = MAX_SIZE)
|
182
|
+
(@generated << @random.rand(max_size)).last
|
183
|
+
end
|
184
|
+
|
185
|
+
def for(*classes)
|
186
|
+
generators = self.class.instance_variable_get(:@_generators)
|
187
|
+
case classes.length
|
188
|
+
when 0
|
189
|
+
raise(TypeError, "A generator for #{classes.join(' ')} is not known. Try adding it with Gen.generator_for.")
|
190
|
+
when 1
|
191
|
+
gen = generators[classes.first]
|
192
|
+
if gen.bound_max > 1
|
193
|
+
c = rand(gen.bound_max - gen.bound_min + 1) + gen.bound_min
|
194
|
+
c.times.reduce(gen.empty(self)) { |g, _| g.append(gen.new(self)) }
|
195
|
+
else
|
196
|
+
gen.new(self)
|
197
|
+
end
|
198
|
+
else
|
199
|
+
classgen = ->() do
|
200
|
+
classes[1..-1].map do |c|
|
201
|
+
if c.is_a?(Array)
|
202
|
+
self.for(*c)
|
203
|
+
else
|
204
|
+
self.for(c)
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
cs = classgen.call
|
209
|
+
|
210
|
+
gen = generators[classes.first]
|
211
|
+
typegen = gen.bound_min < 1 ? gen.empty(self) : gen.new(self)
|
212
|
+
typegen.type_parameters = cs
|
213
|
+
typegen.prefix_entropy_generation(cs)
|
214
|
+
|
215
|
+
if gen.bound_max > 1
|
216
|
+
c = rand(gen.bound_max - gen.bound_min + 1) + gen.bound_min
|
217
|
+
c.times.reduce(typegen) do |g, _|
|
218
|
+
cs2 = classgen.call
|
219
|
+
g2 = gen.new(self)
|
220
|
+
g2.type_parameters = cs2
|
221
|
+
g2.prefix_entropy_generation(cs2)
|
222
|
+
g.append(g2)
|
223
|
+
end
|
224
|
+
else
|
225
|
+
typegen
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
# Common shrinking machinery for all integral types. This includes chars,
|
231
|
+
# etc.
|
232
|
+
integral_shrink = ->(x) do
|
233
|
+
candidates = []
|
234
|
+
y = x
|
235
|
+
|
236
|
+
until y == 0
|
237
|
+
candidates << (x - y)
|
238
|
+
candidates << y
|
239
|
+
# Prevent negative integral from preventing termination
|
240
|
+
y = (y / 2.0).to_i
|
241
|
+
end
|
242
|
+
|
243
|
+
candidates
|
244
|
+
.flat_map { |i| [i - 1, i, i + 1] }
|
245
|
+
.reject { |i| i.abs >= x.abs }
|
246
|
+
end
|
247
|
+
|
248
|
+
float_shrink = ->(x) do
|
249
|
+
candidates = [Float::NAN, Float::INFINITY]
|
250
|
+
y = x
|
251
|
+
|
252
|
+
until y == 0 || y
|
253
|
+
candidates << (x - y)
|
254
|
+
y = (y / 2.0).to_i
|
255
|
+
end
|
256
|
+
|
257
|
+
candidates
|
258
|
+
.flat_map { |i| [i - 1, i, i + 1] }
|
259
|
+
.reject { |i| i.abs >= x.abs }
|
260
|
+
end
|
261
|
+
|
262
|
+
# List shrink adapted from QuickCheck
|
263
|
+
list_remove = ->(k, n, xs) do
|
264
|
+
xs1 = xs.take(k)
|
265
|
+
xs2 = xs.drop(k)
|
266
|
+
if k > n
|
267
|
+
[]
|
268
|
+
elsif xs2.empty?
|
269
|
+
[[]]
|
270
|
+
else
|
271
|
+
[xs2] + list_remove.call(k, (n-k), xs2).map { |ys| xs1 + ys }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
shrink_one = ->(f, xs) do
|
276
|
+
if xs.empty?
|
277
|
+
[]
|
278
|
+
else
|
279
|
+
x = xs.first
|
280
|
+
xs = xs.drop(1)
|
281
|
+
|
282
|
+
ys = f.call(x).map { |y| [y] + xs }
|
283
|
+
zs = shrink_one.call(f, xs).map { |z| [x] + z }
|
284
|
+
ys + zs
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
list_shrink = ->(f, xs) do
|
289
|
+
candidates = [[]]
|
290
|
+
n = xs.length
|
291
|
+
k = n
|
292
|
+
while k > 0
|
293
|
+
candidates += list_remove.call(k, n, xs)
|
294
|
+
k /= 2
|
295
|
+
end
|
296
|
+
candidates + shrink_one.call(f, xs)
|
297
|
+
end
|
298
|
+
|
299
|
+
hash_remove = ->(k, n, h) do
|
300
|
+
xs = h.keys
|
301
|
+
xs1 = xs.take(k)
|
302
|
+
xs2 = xs.drop(k)
|
303
|
+
|
304
|
+
if k > n
|
305
|
+
[]
|
306
|
+
elsif xs2.empty?
|
307
|
+
[{}]
|
308
|
+
else
|
309
|
+
h1 = xs1.reduce({}) { |c, e| c.merge({ e => h[e] }) }
|
310
|
+
h2 = xs2.reduce({}) { |c, e| c.merge({ e => h[e] }) }
|
311
|
+
[h1, h2] + list_remove.call(k, (n-k), h2).map { |ys| h1.merge(ys.to_h) }
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
hash_shrink = ->(fk, fv, h) do
|
316
|
+
candidates = []
|
317
|
+
n = h.length
|
318
|
+
k = n
|
319
|
+
while k > 0
|
320
|
+
candidates += hash_remove.call(k, n, h)
|
321
|
+
k /= 2
|
322
|
+
end
|
323
|
+
candidates
|
324
|
+
end
|
325
|
+
|
326
|
+
# Use two's complement for all signed integers in order to optimize for
|
327
|
+
# random values to shrink towards 0.
|
328
|
+
generator_for(Integer) do
|
329
|
+
r = sized(MAX_SIZE)
|
330
|
+
if (r & SIGN_BIT).zero?
|
331
|
+
r
|
332
|
+
else
|
333
|
+
-(((r & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
|
334
|
+
end
|
335
|
+
end.with_shrink_function do |i|
|
336
|
+
i = if (i & SIGN_BIT).zero?
|
337
|
+
i
|
338
|
+
else
|
339
|
+
-(((i & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
|
340
|
+
end
|
341
|
+
integral_shrink.call(i)
|
342
|
+
end
|
343
|
+
|
344
|
+
generator_for(Int8) do
|
345
|
+
r = sized(0xff)
|
346
|
+
(r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
|
347
|
+
end.with_shrink_function do |i|
|
348
|
+
i = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
|
349
|
+
integral_shrink.call(i)
|
350
|
+
end
|
351
|
+
|
352
|
+
generator_for(Int16) do
|
353
|
+
r = sized(0xffff)
|
354
|
+
(r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
|
355
|
+
end.with_shrink_function do |i|
|
356
|
+
i = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
|
357
|
+
integral_shrink.call(i)
|
358
|
+
end
|
359
|
+
|
360
|
+
generator_for(Int32) do
|
361
|
+
r = sized(0xffffffff)
|
362
|
+
(r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
|
363
|
+
end.with_shrink_function do |i|
|
364
|
+
i = if (i & 0x80000000).zero?
|
365
|
+
i
|
366
|
+
else
|
367
|
+
-(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
|
368
|
+
end
|
369
|
+
integral_shrink.call(i)
|
370
|
+
end
|
371
|
+
|
372
|
+
generator_for(Int64) do
|
373
|
+
r = sized(0xffffffffffffffff)
|
374
|
+
if (r & 0x8000000000000000).zero?
|
375
|
+
r
|
376
|
+
else
|
377
|
+
-(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
|
378
|
+
end
|
379
|
+
end.with_shrink_function do |i|
|
380
|
+
i = if (i & 0x8000000000000000).zero?
|
381
|
+
i
|
382
|
+
else
|
383
|
+
-(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
|
384
|
+
end
|
385
|
+
integral_shrink.call(i)
|
386
|
+
end
|
387
|
+
|
388
|
+
generator_for(UInt8) do
|
389
|
+
sized(0xff)
|
390
|
+
end.with_shrink_function do |i|
|
391
|
+
integral_shrink.call(i).reject(&:negative?)
|
392
|
+
end
|
393
|
+
|
394
|
+
generator_for(UInt16) do
|
395
|
+
sized(0xffff)
|
396
|
+
end.with_shrink_function do |i|
|
397
|
+
integral_shrink.call(i).reject(&:negative?)
|
398
|
+
end
|
399
|
+
|
400
|
+
generator_for(UInt32) do
|
401
|
+
sized(0xffffffff)
|
402
|
+
end.with_shrink_function do |i|
|
403
|
+
integral_shrink.call(i).reject(&:negative?)
|
404
|
+
end
|
405
|
+
|
406
|
+
generator_for(UInt64) do
|
407
|
+
sized(0xffffffffffffffff)
|
408
|
+
end.with_shrink_function do |i|
|
409
|
+
integral_shrink.call(i).reject(&:negative?)
|
410
|
+
end
|
411
|
+
|
412
|
+
generator_for(Float32) do
|
413
|
+
# There is most likely a faster way to do this which doesn't involve
|
414
|
+
# FFI, but it was faster than manual bit twiddling in ruby
|
415
|
+
bits = sized(0xffffffff)
|
416
|
+
(0..3)
|
417
|
+
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
418
|
+
.join
|
419
|
+
.unpack('f')
|
420
|
+
.first
|
421
|
+
end.with_shrink_function do |f|
|
422
|
+
float_shrink.call(f)
|
423
|
+
end.with_score_function do |f|
|
424
|
+
if f.nan? || f.infinite?
|
425
|
+
0
|
426
|
+
else
|
427
|
+
f.abs.ceil
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
generator_for(Float64) do
|
432
|
+
bits = sized(0xffffffffffffffff)
|
433
|
+
(0..7)
|
434
|
+
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
435
|
+
.join
|
436
|
+
.unpack('d')
|
437
|
+
.first
|
438
|
+
end.with_shrink_function do |f|
|
439
|
+
float_shrink.call(f)
|
440
|
+
end.with_score_function do |f|
|
441
|
+
if f.nan? || f.infinite?
|
442
|
+
0
|
443
|
+
else
|
444
|
+
f.abs.ceil
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
generator_for(Float) do
|
449
|
+
bits = sized(0xffffffffffffffff)
|
450
|
+
(0..7)
|
451
|
+
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
452
|
+
.join
|
453
|
+
.unpack('d')
|
454
|
+
.first
|
455
|
+
end.with_shrink_function do |f|
|
456
|
+
float_shrink.call(f)
|
457
|
+
end.with_score_function do |f|
|
458
|
+
if f.nan? || f.infinite?
|
459
|
+
0
|
460
|
+
else
|
461
|
+
f.abs.ceil
|
462
|
+
end
|
463
|
+
end
|
464
|
+
|
465
|
+
generator_for(ASCIIChar) do
|
466
|
+
sized(0x7f).chr
|
467
|
+
end.with_shrink_function do |c|
|
468
|
+
integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
|
469
|
+
end.with_score_function do |c|
|
470
|
+
c.ord
|
471
|
+
end
|
472
|
+
|
473
|
+
generator_for(Char) do
|
474
|
+
sized(0xff).chr
|
475
|
+
end.with_shrink_function do |c|
|
476
|
+
integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
|
477
|
+
end.with_score_function do |c|
|
478
|
+
c.ord
|
479
|
+
end
|
480
|
+
|
481
|
+
generator_for(String) do
|
482
|
+
sized(0xff).chr
|
483
|
+
end.with_shrink_function do |s|
|
484
|
+
xs = list_shrink.call(integral_shrink, s.chars.map(&:ord))
|
485
|
+
xs.map { |str| str.map { |t| t & 0xff }.map(&:chr).join }
|
486
|
+
end.with_score_function do |s|
|
487
|
+
s.chars.map(&:ord).reduce(1) do |c, x|
|
488
|
+
y = x.abs
|
489
|
+
c * (y > 0 ? y + 1 : 1)
|
490
|
+
end
|
491
|
+
end.with_append(0, 0x20) do |x, y|
|
492
|
+
x + y
|
493
|
+
end.with_empty { "" }
|
494
|
+
|
495
|
+
generator_for(Array) do |x|
|
496
|
+
[x]
|
497
|
+
end.with_shrink_function(&list_shrink).with_score_function do |f, xs|
|
498
|
+
xs.reduce(1) do |c, x|
|
499
|
+
y = f.call(x).abs
|
500
|
+
c * (y > 0 ? y + 1 : 1)
|
501
|
+
end.to_i * xs.length
|
502
|
+
end.with_append(0, 0x10) do |xs, ys|
|
503
|
+
xs + ys
|
504
|
+
end.with_empty { [] }
|
505
|
+
|
506
|
+
generator_for(Hash) do |key, value|
|
507
|
+
{ key => value }
|
508
|
+
end.with_shrink_function(&hash_shrink).with_score_function do |fk, fv, h|
|
509
|
+
h.reduce(1) do |c, (k, v)|
|
510
|
+
sk = fk.call(k).abs
|
511
|
+
sv = fv.call(v).abs
|
512
|
+
c * ((sk > 0 ? sk + 1 : 1) + (sv > 0 ? sv + 1 : 1))
|
513
|
+
end
|
514
|
+
end.with_append(0, 0x10) do |xm, ym|
|
515
|
+
xm.merge(ym)
|
516
|
+
end.with_empty { Hash.new }
|
517
|
+
|
518
|
+
generator_for(Bool) do
|
519
|
+
sized(0x1).even? ? false : true
|
520
|
+
end.with_score_function do |_|
|
521
|
+
1
|
522
|
+
end
|
523
|
+
end
|
524
|
+
end
|
525
|
+
end
|
@@ -0,0 +1,177 @@
|
|
1
|
+
module Minitest
|
2
|
+
module Proptest
|
3
|
+
class Property
|
4
|
+
|
5
|
+
attr_reader :result, :status, :trivial
|
6
|
+
def initialize(
|
7
|
+
# The function which proves the property
|
8
|
+
test_proc,
|
9
|
+
# Any class which provides `rand` accepting both an Integer and a Range
|
10
|
+
# is acceptable. The default value is Ruby's standard Mersenne Twister
|
11
|
+
# implementation.
|
12
|
+
random: Random.method(:new),
|
13
|
+
# Maximum number of successful cases before considering the test a
|
14
|
+
# success.
|
15
|
+
max_success: 100,
|
16
|
+
# Maximum ratio of discarded tests per successful test before giving up.
|
17
|
+
max_discard_ratio: 10,
|
18
|
+
# Maximum amount of entropy to generate in a single run
|
19
|
+
max_size: 0x100,
|
20
|
+
# Maximum number of shrink attempts (default of half of max unsigned int
|
21
|
+
# on the system architecture adopted from QuickCheck
|
22
|
+
max_shrinks: 0x7fffffffffffffff
|
23
|
+
)
|
24
|
+
@test_proc = test_proc
|
25
|
+
@random = random.call
|
26
|
+
@generator = ::Minitest::Proptest::Gen.new(@random)
|
27
|
+
@max_success = max_success
|
28
|
+
@max_discard_ratio = max_discard_ratio
|
29
|
+
@max_size = max_size
|
30
|
+
@max_shrinks = max_shrinks
|
31
|
+
@status = Status.unknown
|
32
|
+
@trivial = false
|
33
|
+
@result = nil
|
34
|
+
@exception = nil
|
35
|
+
@calls = 0
|
36
|
+
@valid_test_cases = 0
|
37
|
+
@generated = []
|
38
|
+
@arbitrary = nil
|
39
|
+
end
|
40
|
+
|
41
|
+
def run!
|
42
|
+
iterate!
|
43
|
+
shrink!
|
44
|
+
end
|
45
|
+
|
46
|
+
def arbitrary(*classes)
|
47
|
+
if @arbitrary
|
48
|
+
@arbitrary.call(*classes)
|
49
|
+
else
|
50
|
+
a = @generator.for(*classes)
|
51
|
+
@generated << a
|
52
|
+
@status = Status.overrun unless @generated.length <= @max_size
|
53
|
+
a.value
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def explain
|
58
|
+
prop = if @status.valid?
|
59
|
+
"The property was proved to satsfaction across " +
|
60
|
+
"#{@valid_test_cases} assertions."
|
61
|
+
elsif @status.invalid?
|
62
|
+
"The property was determined to be invalid due to " +
|
63
|
+
"#{@exception.class.name}: #{@exception.message}\n" +
|
64
|
+
@exception.backtrace.map { |l| " #{l}" }.join("\n")
|
65
|
+
elsif @status.overrun?
|
66
|
+
"The property attempted to generate more than #{@max_size} " +
|
67
|
+
"bytes of entropy, violating the property's maximum size." +
|
68
|
+
"This might be rectified by increasing max_size."
|
69
|
+
elsif @status.unknown?
|
70
|
+
"The property has not yet been tested."
|
71
|
+
elsif @status.interesting?
|
72
|
+
"The property has found the following counterexample after " +
|
73
|
+
"#{@valid_test_cases} valid " +
|
74
|
+
"example#{@valid_test_cases == 1 ? '' : 's'}:\n" +
|
75
|
+
@generated.map(&:value).inspect
|
76
|
+
end
|
77
|
+
trivial = if @trivial
|
78
|
+
"\nThe test does not appear to use any generated values " +
|
79
|
+
"and as such is likely not generating much value. " +
|
80
|
+
"Consider reworking this test to make use of arbitrary " +
|
81
|
+
"data."
|
82
|
+
else
|
83
|
+
""
|
84
|
+
end
|
85
|
+
prop + trivial
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def iterate!
|
91
|
+
while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success / 2
|
92
|
+
@generated = []
|
93
|
+
@generator = ::Minitest::Proptest::Gen.new(@random)
|
94
|
+
@calls += 1
|
95
|
+
if instance_eval(&@test_proc)
|
96
|
+
@status = Status.valid if @status.unknown?
|
97
|
+
@valid_test_cases += 1
|
98
|
+
else
|
99
|
+
@result = @generated
|
100
|
+
@status = Status.interesting
|
101
|
+
end
|
102
|
+
@trivial = true if @generated.empty?
|
103
|
+
end
|
104
|
+
rescue => e
|
105
|
+
@status = Status.invalid
|
106
|
+
@exception = e
|
107
|
+
raise e
|
108
|
+
end
|
109
|
+
|
110
|
+
def shrink!
|
111
|
+
return if @result.nil?
|
112
|
+
old_random = @random
|
113
|
+
old_generator = @generator
|
114
|
+
best_score = @generated.map(&:score).reduce(&:+)
|
115
|
+
best_generated = @generated
|
116
|
+
candidates = @generated.map(&:shrink_candidates)
|
117
|
+
old_arbitrary = @arbitrary
|
118
|
+
|
119
|
+
to_test = candidates
|
120
|
+
.map { |x| x.map { |y| [y] } }
|
121
|
+
.reduce { |c, e| c.flat_map { |a| e.map { |b| a + b } } }
|
122
|
+
.sort { |x, y| x.map(&:first).reduce(&:+) <=> y.map(&:first).reduce(&:+) }
|
123
|
+
.uniq
|
124
|
+
run = { run: 0, index: -1 }
|
125
|
+
|
126
|
+
@arbitrary = ->(*classes) do
|
127
|
+
run[:index] += 1
|
128
|
+
raise IndexError if run[:index] >= to_test[run[:run]].length
|
129
|
+
|
130
|
+
a = @generator.for(*classes)
|
131
|
+
a = a.force(to_test[run[:run]][run[:index]].last)
|
132
|
+
@generated << a
|
133
|
+
to_test[run[:run]][run[:index]].last
|
134
|
+
end
|
135
|
+
|
136
|
+
while continue_shrink? && run[:run] < to_test.length
|
137
|
+
@generated = []
|
138
|
+
run[:index] = -1
|
139
|
+
|
140
|
+
@generator = ::Minitest::Proptest::Gen.new(@random)
|
141
|
+
if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
|
142
|
+
unless instance_eval(&@test_proc)
|
143
|
+
best_generated = @generated
|
144
|
+
# Because we pre-sorted our shrink candidates, the first hit is
|
145
|
+
# necessarily the best scoring
|
146
|
+
break
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
@calls += 1
|
151
|
+
run[:run] += 1
|
152
|
+
end
|
153
|
+
# Clean up after we're done
|
154
|
+
@generated = best_generated
|
155
|
+
@result = best_generated
|
156
|
+
@generator = old_generator
|
157
|
+
@random = old_random
|
158
|
+
@arbitrary = old_arbitrary
|
159
|
+
end
|
160
|
+
|
161
|
+
def continue_iterate?
|
162
|
+
!@trivial &&
|
163
|
+
!@status.invalid? &&
|
164
|
+
!@status.overrun? &&
|
165
|
+
@valid_test_cases < @max_success &&
|
166
|
+
@calls < @max_success * @max_discard_ratio
|
167
|
+
end
|
168
|
+
|
169
|
+
def continue_shrink?
|
170
|
+
!@trivial &&
|
171
|
+
!@status.invalid? &&
|
172
|
+
!@status.overrun?
|
173
|
+
@calls < @max_shrinks
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
module Minitest
|
2
|
+
module Proptest
|
3
|
+
# Sum type representing the possible statuses of a test run.
|
4
|
+
# Invalid, Overrun, and Interesting represent different failure classes.
|
5
|
+
# Unknown represents a lack of information about the test run (typically
|
6
|
+
# having not run to satisfaction).
|
7
|
+
# Valid represents a test which has run to satisfaction.
|
8
|
+
class Status
|
9
|
+
class Interesting < Status
|
10
|
+
end
|
11
|
+
|
12
|
+
class Invalid < Status
|
13
|
+
end
|
14
|
+
|
15
|
+
class Overrun < Status
|
16
|
+
end
|
17
|
+
|
18
|
+
class Unknown < Status
|
19
|
+
end
|
20
|
+
|
21
|
+
class Valid < Status
|
22
|
+
end
|
23
|
+
|
24
|
+
invalid = Invalid.new.freeze
|
25
|
+
interesting = Interesting.new.freeze
|
26
|
+
overrun = Overrun.new.freeze
|
27
|
+
unknown = Unknown.new.freeze
|
28
|
+
valid = Valid.new.freeze
|
29
|
+
|
30
|
+
define_singleton_method(:invalid) { invalid }
|
31
|
+
define_singleton_method(:interesting) { interesting }
|
32
|
+
define_singleton_method(:overrun) { overrun }
|
33
|
+
define_singleton_method(:unknown) { unknown }
|
34
|
+
define_singleton_method(:valid) { valid }
|
35
|
+
|
36
|
+
def invalid?
|
37
|
+
self.is_a?(Invalid)
|
38
|
+
end
|
39
|
+
|
40
|
+
def overrun?
|
41
|
+
self.is_a?(Overrun)
|
42
|
+
end
|
43
|
+
|
44
|
+
def unknown?
|
45
|
+
self.is_a?(Unknown)
|
46
|
+
end
|
47
|
+
|
48
|
+
def valid?
|
49
|
+
self.is_a?(Valid)
|
50
|
+
end
|
51
|
+
|
52
|
+
def interesting?
|
53
|
+
self.is_a?(Interesting)
|
54
|
+
end
|
55
|
+
|
56
|
+
def initialize
|
57
|
+
raise 'Please use singleton instances'
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require 'minitest/proptest/gen'
|
3
|
+
require 'minitest/proptest/property'
|
4
|
+
require 'minitest/proptest/status'
|
5
|
+
require 'minitest/proptest/version'
|
6
|
+
|
7
|
+
module Minitest
|
8
|
+
module Proptest
|
9
|
+
DEFAULT_RANDOM = Random.method(:new)
|
10
|
+
DEFAULT_MAX_SUCCESS = 100
|
11
|
+
DEFAULT_MAX_DISCARD_RATIO = 10
|
12
|
+
DEFAULT_MAX_SIZE = 0x100
|
13
|
+
DEFAULT_MAX_SHRINKS = (((1 << (1.size * 8)) - 1) / 2)
|
14
|
+
|
15
|
+
def self.set_seed(seed)
|
16
|
+
self.instance_variable_set(:@_random_seed, seed)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Kernel
|
22
|
+
def generator_for(klass, &f)
|
23
|
+
::Minitest::Proptest::Gen.generator_for(klass, &f)
|
24
|
+
end
|
25
|
+
private :generator_for
|
26
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'minitest'
|
2
|
+
require 'minitest/proptest'
|
3
|
+
require 'minitest/proptest/gen'
|
4
|
+
require 'minitest/proptest/property'
|
5
|
+
require 'minitest/proptest/status'
|
6
|
+
require 'minitest/proptest/version'
|
7
|
+
|
8
|
+
module Minitest
|
9
|
+
def self.plugin_proptest_init(options)
|
10
|
+
%i[Int8 Int16 Int32 Int64
|
11
|
+
UInt8 UInt16 UInt32 UInt64
|
12
|
+
Float32 Float64
|
13
|
+
ASCIIChar Char
|
14
|
+
Bool
|
15
|
+
].each do |const|
|
16
|
+
unless Minitest::Assertions.const_defined?(const)
|
17
|
+
::Minitest::Assertions.const_set(const, ::Minitest::Proptest::Gen.const_get(const))
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
if options.has_key?(:seed)
|
22
|
+
Proptest.set_seed(options[:seed])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
module Assertions
|
27
|
+
def property(&f)
|
28
|
+
self.assertions += 1
|
29
|
+
|
30
|
+
random_thunk = if Proptest.instance_variable_defined?(:@_random_seed)
|
31
|
+
r = Proptest.instance_variable_get(:@_random_seed)
|
32
|
+
->() { Proptest::DEFAULT_RANDOM.call(r) }
|
33
|
+
else
|
34
|
+
Proptest::DEFAULT_RANDOM
|
35
|
+
end
|
36
|
+
|
37
|
+
prop = Minitest::Proptest::Property.new(
|
38
|
+
f,
|
39
|
+
random: random_thunk,
|
40
|
+
max_success: Proptest::DEFAULT_MAX_SUCCESS,
|
41
|
+
max_discard_ratio: Proptest::DEFAULT_MAX_DISCARD_RATIO,
|
42
|
+
max_size: Proptest::DEFAULT_MAX_SIZE,
|
43
|
+
max_shrinks: Proptest::DEFAULT_MAX_SHRINKS
|
44
|
+
)
|
45
|
+
prop.run!
|
46
|
+
|
47
|
+
unless prop.status.valid? && !prop.trivial
|
48
|
+
raise Minitest::Assertion, prop.explain
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
metadata
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: minitest-proptest
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tina Wuest
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2022-09-15 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: minitest
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '5'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '5'
|
27
|
+
description: Property testing in Minitest, a la Haskell's QuickCheck and Python's
|
28
|
+
Hypothesis
|
29
|
+
email: tina@wuest.me
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- lib/minitest/proptest.rb
|
35
|
+
- lib/minitest/proptest/gen.rb
|
36
|
+
- lib/minitest/proptest/property.rb
|
37
|
+
- lib/minitest/proptest/status.rb
|
38
|
+
- lib/minitest/proptest/version.rb
|
39
|
+
- lib/minitest/proptest_plugin.rb
|
40
|
+
homepage: https://github.com/wuest/minitest-proptest
|
41
|
+
licenses:
|
42
|
+
- MIT
|
43
|
+
metadata:
|
44
|
+
homepage_uri: https://github.com/wuest/minitest-proptest
|
45
|
+
source_code_uri: https://github.com/wuest/minitest-proptest
|
46
|
+
changelog_uri: https://github.com/wuest/minitest-proptest/blob/main/CHANGELOG.md
|
47
|
+
post_install_message:
|
48
|
+
rdoc_options: []
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: 2.7.0
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubygems_version: 3.3.7
|
63
|
+
signing_key:
|
64
|
+
specification_version: 4
|
65
|
+
summary: Property testing in Minitest
|
66
|
+
test_files: []
|