minitest-proptest 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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: []
|