minitest-proptest 0.0.2 → 0.2.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 +4 -4
- data/lib/minitest/proptest/gen/value_generator.rb +129 -0
- data/lib/minitest/proptest/gen.rb +142 -191
- data/lib/minitest/proptest/property.rb +130 -35
- data/lib/minitest/proptest/status.rb +15 -0
- data/lib/minitest/proptest/version.rb +1 -1
- data/lib/minitest/proptest.rb +130 -0
- data/lib/minitest/proptest_plugin.rb +44 -9
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: '0596e6f6a66614dba0199eccc4e18a5bf6b869fd0db46dff7636a8772eacb75b'
|
4
|
+
data.tar.gz: a956c579c5bb355a5b687f64109917918870dd7dac1323a18a72d44e4ca623a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d6206c7a778b9b55e0674e204afea71721eb8b6e4fe2257691cc1cefd5e76885eff08d648f9a61172806872f1abe5f1b232186fcc803e6fcc560241fd71fac8c
|
7
|
+
data.tar.gz: 1ca1d04ddcdecb9f661e0af6cffc87dda30af6a9b06bb6fe406a5f27ec445a0cf3cd9ac4d9050b4a243ab40d76d066358f6cf9612181cc546a2076b76a7d9041
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
module Proptest
|
5
|
+
class Gen
|
6
|
+
# Methods for value generation
|
7
|
+
class ValueGenerator
|
8
|
+
attr_accessor :entropy
|
9
|
+
attr_writer :type_parameters
|
10
|
+
|
11
|
+
def self.with_shrink_function(&f)
|
12
|
+
define_method(:shrink_function, &f)
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.with_score_function(&f)
|
17
|
+
define_method(:score_function, &f)
|
18
|
+
self
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.with_append(bound_min, bound_max, &f)
|
22
|
+
define_singleton_method(:bound_min) { bound_min }
|
23
|
+
define_singleton_method(:bound_max) { bound_max }
|
24
|
+
define_method(:append) do |other|
|
25
|
+
@value = f.call(value, other.value)
|
26
|
+
self
|
27
|
+
end
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.with_empty(&f)
|
32
|
+
define_singleton_method(:empty) do |gen|
|
33
|
+
temp = new(gen)
|
34
|
+
temp.instance_variable_set(:@value, f.call)
|
35
|
+
temp
|
36
|
+
end
|
37
|
+
self
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.bound_max
|
41
|
+
1
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.bound_min
|
45
|
+
0
|
46
|
+
end
|
47
|
+
|
48
|
+
# append is not expected to be called unless overridden
|
49
|
+
def append(_other)
|
50
|
+
self
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.empty(gen)
|
54
|
+
self.new(gen)
|
55
|
+
end
|
56
|
+
|
57
|
+
def force(v)
|
58
|
+
temp = self.class.new(ArgumentError)
|
59
|
+
temp.instance_variable_set(:@value, v)
|
60
|
+
temp.type_parameters = @type_parameters
|
61
|
+
temp
|
62
|
+
end
|
63
|
+
|
64
|
+
def generate_value
|
65
|
+
gen = @generated.reduce(@generator) do |g, val|
|
66
|
+
g.call(val)
|
67
|
+
g
|
68
|
+
end
|
69
|
+
|
70
|
+
while gen.is_a?(Proc) || gen.is_a?(Method)
|
71
|
+
gen = gen.call(*@type_parameters.map(&:value))
|
72
|
+
gen = gen.value if gen.is_a?(ValueGenerator)
|
73
|
+
end
|
74
|
+
|
75
|
+
gen
|
76
|
+
end
|
77
|
+
|
78
|
+
def value
|
79
|
+
return false if @value == false
|
80
|
+
|
81
|
+
@value ||= generate_value
|
82
|
+
end
|
83
|
+
|
84
|
+
def prefix_entropy_generation(vals)
|
85
|
+
@generated = vals + @generated
|
86
|
+
end
|
87
|
+
|
88
|
+
def score
|
89
|
+
value
|
90
|
+
fs = @type_parameters.map { |x| x.method(:score_function) }
|
91
|
+
score_function(*fs, value)
|
92
|
+
end
|
93
|
+
|
94
|
+
def score_function(v)
|
95
|
+
v.to_i.abs
|
96
|
+
end
|
97
|
+
|
98
|
+
def shrink_candidates
|
99
|
+
fs = @type_parameters.map { |x| x.method(:shrink_function) }
|
100
|
+
os = score
|
101
|
+
candidates = shrink_function(*fs, value)
|
102
|
+
candidates
|
103
|
+
.map { |c| [force(c).score, c] }
|
104
|
+
.reject { |(s, _)| s > os }
|
105
|
+
.sort { |x, y| x.first <=> y.first }
|
106
|
+
.uniq
|
107
|
+
end
|
108
|
+
|
109
|
+
def shrink_function(x)
|
110
|
+
[x.itself]
|
111
|
+
end
|
112
|
+
|
113
|
+
def shrink_parameter(x)
|
114
|
+
@shrink_parameter.call(x)
|
115
|
+
end
|
116
|
+
|
117
|
+
# Generator helpers
|
118
|
+
|
119
|
+
def sized(n)
|
120
|
+
entropy.call(n + 1)
|
121
|
+
end
|
122
|
+
|
123
|
+
def one_of(r)
|
124
|
+
r.to_a[sized(r.to_a.length - 1)]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -1,128 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'minitest/proptest/gen/value_generator'
|
4
|
+
|
1
5
|
module Minitest
|
2
6
|
module Proptest
|
7
|
+
# Generic value generation and shrinking implementations, and
|
8
|
+
# support for built-in types.
|
3
9
|
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
10
|
class Int8 < Integer; end
|
127
11
|
class Int16 < Integer; end
|
128
12
|
class Int32 < Integer; end
|
@@ -146,7 +30,7 @@ module Minitest
|
|
146
30
|
instance_variable_set(:@_generators, {})
|
147
31
|
|
148
32
|
def self.create_type_constructor(arity, classes)
|
149
|
-
constructor = ->(
|
33
|
+
constructor = ->(_c1) do
|
150
34
|
if classes.length == arity
|
151
35
|
f.call(*classes)
|
152
36
|
else
|
@@ -168,8 +52,8 @@ module Minitest
|
|
168
52
|
|
169
53
|
new_class.define_method(:generator, &f)
|
170
54
|
|
171
|
-
instance_variable_get(:@_generators)[klass] = new_class
|
172
|
-
self.const_set(
|
55
|
+
instance_variable_get(:@_generators)[klass] = new_class
|
56
|
+
self.const_set("#{klass.name}Gen".split('::').last, new_class)
|
173
57
|
new_class
|
174
58
|
end
|
175
59
|
|
@@ -197,11 +81,11 @@ module Minitest
|
|
197
81
|
end
|
198
82
|
else
|
199
83
|
classgen = ->() do
|
200
|
-
classes[1
|
201
|
-
if
|
202
|
-
self.for(*
|
84
|
+
classes[1..].map do |k|
|
85
|
+
if k.is_a?(Array)
|
86
|
+
self.for(*k)
|
203
87
|
else
|
204
|
-
self.for(
|
88
|
+
self.for(k)
|
205
89
|
end
|
206
90
|
end
|
207
91
|
end
|
@@ -235,28 +119,60 @@ module Minitest
|
|
235
119
|
|
236
120
|
until y == 0
|
237
121
|
candidates << (x - y)
|
238
|
-
candidates << y
|
239
|
-
# Prevent negative integral from preventing termination
|
122
|
+
candidates << y if y.abs < x.abs
|
240
123
|
y = (y / 2.0).to_i
|
241
124
|
end
|
242
125
|
|
243
126
|
candidates
|
244
|
-
|
245
|
-
|
127
|
+
end
|
128
|
+
|
129
|
+
score_float = ->(f) do
|
130
|
+
if f.nan? || f.infinite?
|
131
|
+
0
|
132
|
+
else
|
133
|
+
f.abs.ceil
|
134
|
+
end
|
246
135
|
end
|
247
136
|
|
248
137
|
float_shrink = ->(x) do
|
138
|
+
return [] if x.nan? || x.infinite? || x.zero?
|
139
|
+
|
249
140
|
candidates = [Float::NAN, Float::INFINITY]
|
250
141
|
y = x
|
251
142
|
|
252
|
-
until y
|
143
|
+
until y.zero? || y.to_f.infinite? || y.to_f.nan?
|
253
144
|
candidates << (x - y)
|
254
145
|
y = (y / 2.0).to_i
|
255
146
|
end
|
256
147
|
|
257
|
-
|
258
|
-
|
259
|
-
|
148
|
+
score = score_float.call(x)
|
149
|
+
candidates.reduce([]) do |cs, c|
|
150
|
+
cs + (score_float.call(c) < score ? [c.to_f] : [])
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
score_complex = ->(c) do
|
155
|
+
r = if c.real.to_f.nan? || c.real.to_f.infinite?
|
156
|
+
0
|
157
|
+
else
|
158
|
+
c.real.abs.ceil
|
159
|
+
end
|
160
|
+
i = if c.imaginary.to_f.nan? || c.imaginary.to_f.infinite?
|
161
|
+
0
|
162
|
+
else
|
163
|
+
c.imaginary.abs.ceil
|
164
|
+
end
|
165
|
+
r + i
|
166
|
+
end
|
167
|
+
|
168
|
+
complex_shrink = ->(x) do
|
169
|
+
rs = float_shrink.call(x.real)
|
170
|
+
is = float_shrink.call(x.imaginary)
|
171
|
+
|
172
|
+
score = score_complex.call(x)
|
173
|
+
rs.flat_map { |real| is.map { |imag| Complex(real, imag) } }
|
174
|
+
.reject { |c| score_complex.call(c) >= score }
|
175
|
+
.uniq
|
260
176
|
end
|
261
177
|
|
262
178
|
# List shrink adapted from QuickCheck
|
@@ -268,7 +184,7 @@ module Minitest
|
|
268
184
|
elsif xs2.empty?
|
269
185
|
[[]]
|
270
186
|
else
|
271
|
-
[xs2] + list_remove.call(k, (n-k), xs2).map { |ys| xs1 + ys }
|
187
|
+
[xs2] + list_remove.call(k, (n - k), xs2).map { |ys| xs1 + ys }
|
272
188
|
end
|
273
189
|
end
|
274
190
|
|
@@ -308,11 +224,39 @@ module Minitest
|
|
308
224
|
else
|
309
225
|
h1 = xs1.reduce({}) { |c, e| c.merge({ e => h[e] }) }
|
310
226
|
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) }
|
227
|
+
[h1, h2] + list_remove.call(k, (n - k), h2).map { |ys| h1.merge(ys.to_h) }
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
range_shrink = ->(f, r) do
|
232
|
+
xs = f.call(r.first)
|
233
|
+
ys = f.call(r.last)
|
234
|
+
|
235
|
+
xs.flat_map { |x| ys.map { |y| x <= y ? (x..y) : (y..x) } }
|
236
|
+
end
|
237
|
+
|
238
|
+
score_rational = ->(r) do
|
239
|
+
(r.numerator * r.denominator).abs
|
240
|
+
end
|
241
|
+
|
242
|
+
rational_shrink = ->(r) do
|
243
|
+
ns = integral_shrink.call(r.numerator)
|
244
|
+
ds = integral_shrink.call(r.denominator)
|
245
|
+
|
246
|
+
score = score_rational.call(r)
|
247
|
+
ns.flat_map do |n|
|
248
|
+
ds.reduce([]) do |rs, d|
|
249
|
+
if d.zero?
|
250
|
+
rs
|
251
|
+
else
|
252
|
+
rational = Rational(n, d)
|
253
|
+
rs + (score_rational.call(rational) < score ? [rational] : [])
|
254
|
+
end
|
255
|
+
end
|
312
256
|
end
|
313
257
|
end
|
314
258
|
|
315
|
-
hash_shrink = ->(
|
259
|
+
hash_shrink = ->(_fk, _fv, h) do
|
316
260
|
candidates = []
|
317
261
|
n = h.length
|
318
262
|
k = n
|
@@ -333,40 +277,40 @@ module Minitest
|
|
333
277
|
-(((r & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
|
334
278
|
end
|
335
279
|
end.with_shrink_function do |i|
|
336
|
-
|
280
|
+
j = if (i & SIGN_BIT).zero?
|
337
281
|
i
|
338
282
|
else
|
339
283
|
-(((i & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
|
340
284
|
end
|
341
|
-
integral_shrink.call(
|
285
|
+
integral_shrink.call(j)
|
342
286
|
end
|
343
287
|
|
344
288
|
generator_for(Int8) do
|
345
289
|
r = sized(0xff)
|
346
290
|
(r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
|
347
291
|
end.with_shrink_function do |i|
|
348
|
-
|
349
|
-
integral_shrink.call(
|
292
|
+
j = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
|
293
|
+
integral_shrink.call(j)
|
350
294
|
end
|
351
295
|
|
352
296
|
generator_for(Int16) do
|
353
297
|
r = sized(0xffff)
|
354
298
|
(r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
|
355
299
|
end.with_shrink_function do |i|
|
356
|
-
|
357
|
-
integral_shrink.call(
|
300
|
+
j = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
|
301
|
+
integral_shrink.call(j)
|
358
302
|
end
|
359
303
|
|
360
304
|
generator_for(Int32) do
|
361
305
|
r = sized(0xffffffff)
|
362
306
|
(r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
|
363
307
|
end.with_shrink_function do |i|
|
364
|
-
|
308
|
+
j = if (i & 0x80000000).zero?
|
365
309
|
i
|
366
310
|
else
|
367
311
|
-(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
|
368
312
|
end
|
369
|
-
integral_shrink.call(
|
313
|
+
integral_shrink.call(j)
|
370
314
|
end
|
371
315
|
|
372
316
|
generator_for(Int64) do
|
@@ -377,12 +321,12 @@ module Minitest
|
|
377
321
|
-(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
|
378
322
|
end
|
379
323
|
end.with_shrink_function do |i|
|
380
|
-
|
324
|
+
j = if (i & 0x8000000000000000).zero?
|
381
325
|
i
|
382
326
|
else
|
383
327
|
-(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
|
384
328
|
end
|
385
|
-
integral_shrink.call(
|
329
|
+
integral_shrink.call(j)
|
386
330
|
end
|
387
331
|
|
388
332
|
generator_for(UInt8) do
|
@@ -416,67 +360,51 @@ module Minitest
|
|
416
360
|
(0..3)
|
417
361
|
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
418
362
|
.join
|
419
|
-
.
|
420
|
-
.first
|
363
|
+
.unpack1('f')
|
421
364
|
end.with_shrink_function do |f|
|
422
365
|
float_shrink.call(f)
|
423
|
-
end.with_score_function
|
424
|
-
if f.nan? || f.infinite?
|
425
|
-
0
|
426
|
-
else
|
427
|
-
f.abs.ceil
|
428
|
-
end
|
429
|
-
end
|
366
|
+
end.with_score_function(&score_float)
|
430
367
|
|
431
|
-
|
432
|
-
bits = sized(0xffffffffffffffff)
|
368
|
+
float64build = ->(bits) do
|
433
369
|
(0..7)
|
434
370
|
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
435
371
|
.join
|
436
|
-
.
|
437
|
-
|
372
|
+
.unpack1('d')
|
373
|
+
end
|
374
|
+
|
375
|
+
generator_for(Float64) do
|
376
|
+
bits = sized(0xffffffffffffffff)
|
377
|
+
float64build.call(bits)
|
438
378
|
end.with_shrink_function do |f|
|
439
379
|
float_shrink.call(f)
|
440
|
-
end.with_score_function
|
441
|
-
if f.nan? || f.infinite?
|
442
|
-
0
|
443
|
-
else
|
444
|
-
f.abs.ceil
|
445
|
-
end
|
446
|
-
end
|
380
|
+
end.with_score_function(&score_float)
|
447
381
|
|
448
382
|
generator_for(Float) do
|
449
383
|
bits = sized(0xffffffffffffffff)
|
450
|
-
(
|
451
|
-
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
452
|
-
.join
|
453
|
-
.unpack('d')
|
454
|
-
.first
|
384
|
+
float64build.call(bits)
|
455
385
|
end.with_shrink_function do |f|
|
456
386
|
float_shrink.call(f)
|
457
|
-
end.with_score_function
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
end
|
387
|
+
end.with_score_function(&score_float)
|
388
|
+
|
389
|
+
generator_for(Complex) do
|
390
|
+
real = sized(0xffffffffffffffff)
|
391
|
+
imag = sized(0xffffffffffffffff)
|
392
|
+
Complex(float64build.call(real), float64build.call(imag))
|
393
|
+
end.with_shrink_function do |c|
|
394
|
+
complex_shrink.call(c)
|
395
|
+
end.with_score_function(&score_complex)
|
464
396
|
|
465
397
|
generator_for(ASCIIChar) do
|
466
398
|
sized(0x7f).chr
|
467
399
|
end.with_shrink_function do |c|
|
468
400
|
integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
|
469
|
-
end.with_score_function
|
470
|
-
c.ord
|
471
|
-
end
|
401
|
+
end.with_score_function(&:ord)
|
472
402
|
|
473
403
|
generator_for(Char) do
|
474
404
|
sized(0xff).chr
|
475
405
|
end.with_shrink_function do |c|
|
476
406
|
integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
|
477
|
-
end.with_score_function
|
478
|
-
c.ord
|
479
|
-
end
|
407
|
+
end.with_score_function(&:ord)
|
480
408
|
|
481
409
|
generator_for(String) do
|
482
410
|
sized(0xff).chr
|
@@ -490,7 +418,7 @@ module Minitest
|
|
490
418
|
end
|
491
419
|
end.with_append(0, 0x20) do |x, y|
|
492
420
|
x + y
|
493
|
-
end.with_empty {
|
421
|
+
end.with_empty { '' }
|
494
422
|
|
495
423
|
generator_for(Array) do |x|
|
496
424
|
[x]
|
@@ -513,10 +441,33 @@ module Minitest
|
|
513
441
|
end
|
514
442
|
end.with_append(0, 0x10) do |xm, ym|
|
515
443
|
xm.merge(ym)
|
516
|
-
end.with_empty {
|
444
|
+
end.with_empty { {} }
|
445
|
+
|
446
|
+
generator_for(Range) do |x|
|
447
|
+
(x..x)
|
448
|
+
end.with_shrink_function(&range_shrink).with_score_function do |f, r|
|
449
|
+
r.to_a.reduce(1) do |c, x|
|
450
|
+
y = f.call(x).abs
|
451
|
+
c * (y > 0 ? y + 1 : 1)
|
452
|
+
end.to_i * r.to_a.length
|
453
|
+
end.with_append(2, 2) do |ra, rb|
|
454
|
+
xs = [ra.first, ra.last, rb.first, rb.last].sort
|
455
|
+
(xs.first..xs.last)
|
456
|
+
end
|
457
|
+
|
458
|
+
generator_for(Rational) do
|
459
|
+
n = sized(MAX_SIZE)
|
460
|
+
d = sized(MAX_SIZE - 1) + 1
|
461
|
+
if (n & SIGN_BIT).zero?
|
462
|
+
Rational(n, d)
|
463
|
+
else
|
464
|
+
Rational(-(((n & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT)), d)
|
465
|
+
end
|
466
|
+
end.with_shrink_function(&rational_shrink)
|
467
|
+
.with_score_function(&score_rational)
|
517
468
|
|
518
469
|
generator_for(Bool) do
|
519
|
-
sized(0x1).
|
470
|
+
sized(0x1).odd?
|
520
471
|
end.with_score_function do |_|
|
521
472
|
1
|
522
473
|
end
|
@@ -1,8 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Minitest
|
2
4
|
module Proptest
|
5
|
+
# Property evaluation - status, scoring, shrinking
|
3
6
|
class Property
|
7
|
+
require 'minitest/assertions'
|
8
|
+
include Minitest::Assertions
|
9
|
+
|
10
|
+
attr_reader :calls, :result, :status, :trivial
|
11
|
+
|
12
|
+
attr_accessor :assertions
|
4
13
|
|
5
|
-
attr_reader :result, :status, :trivial
|
6
14
|
def initialize(
|
7
15
|
# The function which proves the property
|
8
16
|
test_proc,
|
@@ -19,7 +27,10 @@ module Minitest
|
|
19
27
|
max_size: 0x100,
|
20
28
|
# Maximum number of shrink attempts (default of half of max unsigned int
|
21
29
|
# on the system architecture adopted from QuickCheck
|
22
|
-
max_shrinks: 0x7fffffffffffffff
|
30
|
+
max_shrinks: 0x7fffffffffffffff,
|
31
|
+
# Previously discovered counter-example. If this exists, it should be
|
32
|
+
# run before any test cases are generated.
|
33
|
+
previous_failure: []
|
23
34
|
)
|
24
35
|
@test_proc = test_proc
|
25
36
|
@random = random.call
|
@@ -30,15 +41,19 @@ module Minitest
|
|
30
41
|
@max_shrinks = max_shrinks
|
31
42
|
@status = Status.unknown
|
32
43
|
@trivial = false
|
44
|
+
@valid_test_case = true
|
33
45
|
@result = nil
|
34
46
|
@exception = nil
|
35
47
|
@calls = 0
|
48
|
+
@assertions = 0
|
36
49
|
@valid_test_cases = 0
|
37
50
|
@generated = []
|
38
51
|
@arbitrary = nil
|
52
|
+
@previous_failure = previous_failure.to_a
|
39
53
|
end
|
40
54
|
|
41
55
|
def run!
|
56
|
+
rerun!
|
42
57
|
iterate!
|
43
58
|
shrink!
|
44
59
|
end
|
@@ -54,33 +69,43 @@ module Minitest
|
|
54
69
|
end
|
55
70
|
end
|
56
71
|
|
72
|
+
def where(&b)
|
73
|
+
@valid_test_case &= b.call
|
74
|
+
end
|
75
|
+
|
57
76
|
def explain
|
58
77
|
prop = if @status.valid?
|
59
|
-
|
78
|
+
'The property was proved to satsfaction across ' \
|
60
79
|
"#{@valid_test_cases} assertions."
|
61
80
|
elsif @status.invalid?
|
62
|
-
|
63
|
-
"#{@exception.class.name}: #{@exception.message}\n"
|
64
|
-
@exception.backtrace.map { |l| " #{l}" }.join("\n")
|
81
|
+
'The property was determined to be invalid due to ' \
|
82
|
+
"#{@exception.class.name}: #{@exception.message}\n" \
|
83
|
+
"#{@exception.backtrace.map { |l| " #{l}" }.join("\n")}"
|
65
84
|
elsif @status.overrun?
|
66
|
-
"The property attempted to generate more than #{@max_size} "
|
67
|
-
"bytes of entropy, violating the property's maximum
|
68
|
-
|
85
|
+
"The property attempted to generate more than #{@max_size} " \
|
86
|
+
"bytes of entropy, violating the property's maximum " \
|
87
|
+
'size. This might be rectified by increasing max_size.'
|
69
88
|
elsif @status.unknown?
|
70
|
-
|
89
|
+
'The property has not yet been tested.'
|
71
90
|
elsif @status.interesting?
|
72
|
-
|
73
|
-
"#{@valid_test_cases} valid "
|
74
|
-
"example#{@valid_test_cases == 1 ? '' : 's'}:\n"
|
75
|
-
@generated.map(&:value).inspect
|
91
|
+
'The property has found the following counterexample after ' \
|
92
|
+
"#{@valid_test_cases} valid " \
|
93
|
+
"example#{@valid_test_cases == 1 ? '' : 's'}:\n" \
|
94
|
+
"#{@generated.map(&:value).inspect}"
|
95
|
+
elsif @status.exhausted?
|
96
|
+
"The property was unable to generate #{@max_success} test " \
|
97
|
+
'cases before generating ' \
|
98
|
+
"#{@max_success * @max_discard_ratio} rejected test " \
|
99
|
+
"cases. This might be a problem with the property's " \
|
100
|
+
'`where` blocks.'
|
76
101
|
end
|
77
102
|
trivial = if @trivial
|
78
|
-
"\nThe test does not appear to use any generated values "
|
79
|
-
|
80
|
-
|
81
|
-
|
103
|
+
"\nThe test does not appear to use any generated values " \
|
104
|
+
'and as such is likely not generating much value. ' \
|
105
|
+
'Consider reworking this test to make use of arbitrary ' \
|
106
|
+
'data.'
|
82
107
|
else
|
83
|
-
|
108
|
+
''
|
84
109
|
end
|
85
110
|
prop + trivial
|
86
111
|
end
|
@@ -88,27 +113,84 @@ module Minitest
|
|
88
113
|
private
|
89
114
|
|
90
115
|
def iterate!
|
91
|
-
while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
|
116
|
+
while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success
|
117
|
+
@valid_test_case = true
|
92
118
|
@generated = []
|
93
119
|
@generator = ::Minitest::Proptest::Gen.new(@random)
|
94
120
|
@calls += 1
|
95
|
-
|
121
|
+
|
122
|
+
success = begin
|
123
|
+
instance_eval(&@test_proc)
|
124
|
+
rescue Minitest::Assertion
|
125
|
+
if @valid_test_case
|
126
|
+
@result = @generated
|
127
|
+
@status = Status.interesting
|
128
|
+
end
|
129
|
+
rescue => e
|
130
|
+
raise e if @valid_test_case
|
131
|
+
end
|
132
|
+
if @valid_test_case && success
|
96
133
|
@status = Status.valid if @status.unknown?
|
97
134
|
@valid_test_cases += 1
|
98
|
-
|
135
|
+
elsif @valid_test_case
|
99
136
|
@result = @generated
|
100
137
|
@status = Status.interesting
|
101
138
|
end
|
139
|
+
|
140
|
+
@status = Status.exhausted if @calls >= @max_success * (@max_discard_ratio + 1)
|
102
141
|
@trivial = true if @generated.empty?
|
103
142
|
end
|
104
143
|
rescue => e
|
105
144
|
@status = Status.invalid
|
106
145
|
@exception = e
|
107
|
-
|
146
|
+
end
|
147
|
+
|
148
|
+
def rerun!
|
149
|
+
return if @previous_failure.empty?
|
150
|
+
|
151
|
+
old_generator = @generator
|
152
|
+
old_random = @random
|
153
|
+
old_arbitrary = @arbitrary
|
154
|
+
|
155
|
+
index = -1
|
156
|
+
@arbitrary = ->(*classes) do
|
157
|
+
index += 1
|
158
|
+
raise IndexError if index >= @previous_failure.length
|
159
|
+
|
160
|
+
a = @generator.for(*classes)
|
161
|
+
a = a.force(@previous_failure[index])
|
162
|
+
@generated << a
|
163
|
+
@previous_failure[index]
|
164
|
+
end
|
165
|
+
|
166
|
+
@generator = ::Minitest::Proptest::Gen.new(@random)
|
167
|
+
success = begin
|
168
|
+
instance_eval(&@test_proc)
|
169
|
+
rescue Minitest::Assertion
|
170
|
+
!@valid_test_case
|
171
|
+
rescue => e
|
172
|
+
if @valid_test_case
|
173
|
+
@status = Status.invalid
|
174
|
+
@exception = e
|
175
|
+
false
|
176
|
+
end
|
177
|
+
end
|
178
|
+
if success || !@valid_test_case
|
179
|
+
@generated = []
|
180
|
+
elsif @valid_test_case
|
181
|
+
@result = @generated
|
182
|
+
@status = Status.interesting
|
183
|
+
end
|
184
|
+
|
185
|
+
# Clean up after we're done
|
186
|
+
@generator = old_generator
|
187
|
+
@random = old_random
|
188
|
+
@arbitrary = old_arbitrary
|
108
189
|
end
|
109
190
|
|
110
191
|
def shrink!
|
111
192
|
return if @result.nil?
|
193
|
+
|
112
194
|
old_random = @random
|
113
195
|
old_generator = @generator
|
114
196
|
best_score = @generated.map(&:score).reduce(&:+)
|
@@ -117,10 +199,10 @@ module Minitest
|
|
117
199
|
old_arbitrary = @arbitrary
|
118
200
|
|
119
201
|
to_test = candidates
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
202
|
+
.map { |x| x.map { |y| [y] } }
|
203
|
+
.reduce { |c, e| c.flat_map { |a| e.map { |b| a + b } } }
|
204
|
+
.sort { |x, y| x.map(&:first).reduce(&:+) <=> y.map(&:first).reduce(&:+) }
|
205
|
+
.uniq
|
124
206
|
run = { run: 0, index: -1 }
|
125
207
|
|
126
208
|
@arbitrary = ->(*classes) do
|
@@ -134,15 +216,28 @@ module Minitest
|
|
134
216
|
end
|
135
217
|
|
136
218
|
while continue_shrink? && run[:run] < to_test.length
|
137
|
-
@generated
|
138
|
-
run[:index]
|
219
|
+
@generated = []
|
220
|
+
run[:index] = -1
|
221
|
+
@valid_test_case = true
|
139
222
|
|
140
223
|
@generator = ::Minitest::Proptest::Gen.new(@random)
|
141
224
|
if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
|
142
|
-
|
225
|
+
success = begin
|
226
|
+
instance_eval(&@test_proc)
|
227
|
+
rescue Minitest::Assertion
|
228
|
+
false
|
229
|
+
rescue => e
|
230
|
+
next unless @valid_test_case
|
231
|
+
|
232
|
+
@status = Status.invalid
|
233
|
+
@excption = e
|
234
|
+
break
|
235
|
+
end
|
236
|
+
|
237
|
+
if !success && @valid_test_case
|
238
|
+
# The first hit is guaranteed to be the best scoring due to the
|
239
|
+
# shrink candidates are pre-sorted.
|
143
240
|
best_generated = @generated
|
144
|
-
# Because we pre-sorted our shrink candidates, the first hit is
|
145
|
-
# necessarily the best scoring
|
146
241
|
break
|
147
242
|
end
|
148
243
|
end
|
@@ -162,14 +257,14 @@ module Minitest
|
|
162
257
|
!@trivial &&
|
163
258
|
!@status.invalid? &&
|
164
259
|
!@status.overrun? &&
|
165
|
-
|
166
|
-
@
|
260
|
+
!@status.exhausted? &&
|
261
|
+
@valid_test_cases < @max_success
|
167
262
|
end
|
168
263
|
|
169
264
|
def continue_shrink?
|
170
265
|
!@trivial &&
|
171
266
|
!@status.invalid? &&
|
172
|
-
!@status.overrun?
|
267
|
+
!@status.overrun? &&
|
173
268
|
@calls < @max_shrinks
|
174
269
|
end
|
175
270
|
end
|
@@ -1,7 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Minitest
|
2
4
|
module Proptest
|
3
5
|
# Sum type representing the possible statuses of a test run.
|
4
6
|
# Invalid, Overrun, and Interesting represent different failure classes.
|
7
|
+
# Exhausted represents having generated too many invalid test cases to
|
8
|
+
# verify the property. This precipitates as a failure class (the property is
|
9
|
+
# not proved) but can be a matter of inappropriate predicates in `where`
|
10
|
+
# blocks.
|
5
11
|
# Unknown represents a lack of information about the test run (typically
|
6
12
|
# having not run to satisfaction).
|
7
13
|
# Valid represents a test which has run to satisfaction.
|
@@ -21,16 +27,21 @@ module Minitest
|
|
21
27
|
class Valid < Status
|
22
28
|
end
|
23
29
|
|
30
|
+
class Exhausted < Status
|
31
|
+
end
|
32
|
+
|
24
33
|
invalid = Invalid.new.freeze
|
25
34
|
interesting = Interesting.new.freeze
|
26
35
|
overrun = Overrun.new.freeze
|
27
36
|
unknown = Unknown.new.freeze
|
37
|
+
exhausted = Exhausted.new.freeze
|
28
38
|
valid = Valid.new.freeze
|
29
39
|
|
30
40
|
define_singleton_method(:invalid) { invalid }
|
31
41
|
define_singleton_method(:interesting) { interesting }
|
32
42
|
define_singleton_method(:overrun) { overrun }
|
33
43
|
define_singleton_method(:unknown) { unknown }
|
44
|
+
define_singleton_method(:exhausted) { exhausted }
|
34
45
|
define_singleton_method(:valid) { valid }
|
35
46
|
|
36
47
|
def invalid?
|
@@ -45,6 +56,10 @@ module Minitest
|
|
45
56
|
self.is_a?(Unknown)
|
46
57
|
end
|
47
58
|
|
59
|
+
def exhausted?
|
60
|
+
self.is_a?(Exhausted)
|
61
|
+
end
|
62
|
+
|
48
63
|
def valid?
|
49
64
|
self.is_a?(Valid)
|
50
65
|
end
|
data/lib/minitest/proptest.rb
CHANGED
@@ -1,20 +1,150 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'minitest'
|
2
4
|
require 'minitest/proptest/gen'
|
3
5
|
require 'minitest/proptest/property'
|
4
6
|
require 'minitest/proptest/status'
|
5
7
|
require 'minitest/proptest/version'
|
8
|
+
require 'yaml'
|
6
9
|
|
7
10
|
module Minitest
|
11
|
+
class ResultsDatabase < Minitest::AbstractReporter
|
12
|
+
def initialize(pathname)
|
13
|
+
super()
|
14
|
+
|
15
|
+
results = if File.file?(pathname)
|
16
|
+
YAML.load_file(pathname)
|
17
|
+
else
|
18
|
+
{}
|
19
|
+
end
|
20
|
+
self.class.instance_variable_set(:@_results, results) unless self.class.instance_variable_defined?(:@_results)
|
21
|
+
end
|
22
|
+
|
23
|
+
def report
|
24
|
+
return unless Proptest.use_db?
|
25
|
+
|
26
|
+
File.write(Proptest.result_db, self.class.instance_variable_get(:@_results).to_yaml)
|
27
|
+
end
|
28
|
+
|
29
|
+
def lookup(file, classname, methodname)
|
30
|
+
self.class.instance_variable_get(:@_results)
|
31
|
+
.dig(file, classname, methodname)
|
32
|
+
.to_a
|
33
|
+
end
|
34
|
+
|
35
|
+
def record_failure(file, classname, methodname, generated)
|
36
|
+
return unless Proptest.use_db?
|
37
|
+
|
38
|
+
results = self.class.instance_variable_get(:@_results)
|
39
|
+
results[file] ||= {}
|
40
|
+
results[file][classname] ||= {}
|
41
|
+
results[file][classname][methodname] = generated
|
42
|
+
end
|
43
|
+
|
44
|
+
def strike_failure(file, classname, methodname)
|
45
|
+
return unless Proptest.use_db?
|
46
|
+
|
47
|
+
results = self.class.instance_variable_get(:@_results)
|
48
|
+
return unless results.key?(file)
|
49
|
+
|
50
|
+
return unless results[file].key?(classname)
|
51
|
+
|
52
|
+
results[file][classname].delete(methodname)
|
53
|
+
results[file].delete(classname) if results[file][classname].empty?
|
54
|
+
results.delete(file) if results[file].empty?
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
8
58
|
module Proptest
|
9
59
|
DEFAULT_RANDOM = Random.method(:new)
|
10
60
|
DEFAULT_MAX_SUCCESS = 100
|
11
61
|
DEFAULT_MAX_DISCARD_RATIO = 10
|
12
62
|
DEFAULT_MAX_SIZE = 0x100
|
13
63
|
DEFAULT_MAX_SHRINKS = (((1 << (1.size * 8)) - 1) / 2)
|
64
|
+
DEFAULT_DB_LOCATION = File.join(Dir.pwd, '.proptest_failures.yml')
|
65
|
+
|
66
|
+
self.instance_variable_set(:@_random, DEFAULT_RANDOM)
|
67
|
+
self.instance_variable_set(:@_max_success, DEFAULT_MAX_SUCCESS)
|
68
|
+
self.instance_variable_set(:@_max_discard_ratio, DEFAULT_MAX_DISCARD_RATIO)
|
69
|
+
self.instance_variable_set(:@_max_size, DEFAULT_MAX_SIZE)
|
70
|
+
self.instance_variable_set(:@_max_shrinks, DEFAULT_MAX_SHRINKS)
|
71
|
+
self.instance_variable_set(:@_result_db, DEFAULT_DB_LOCATION)
|
72
|
+
self.instance_variable_set(:@_use_db, false)
|
14
73
|
|
15
74
|
def self.set_seed(seed)
|
16
75
|
self.instance_variable_set(:@_random_seed, seed)
|
17
76
|
end
|
77
|
+
|
78
|
+
def self.max_success=(success)
|
79
|
+
self.instance_variable_set(:@_max_success, success)
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.max_discard_ratio=(discards)
|
83
|
+
self.instance_variable_set(:@_max_discard_ratio, discards)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.max_size=(size)
|
87
|
+
self.instance_variable_set(:@_max_size, size)
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.max_shrinks=(shrinks)
|
91
|
+
self.instance_variable_set(:@_max_shrinks, shrinks)
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.result_db=(location)
|
95
|
+
self.instance_variable_set(:@_result_db, File.expand_path(location))
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.use_db!(use = true)
|
99
|
+
self.instance_variable_set(:@_use_db, use)
|
100
|
+
end
|
101
|
+
|
102
|
+
def self.seed
|
103
|
+
self.instance_variable_get(:@_random_seed)
|
104
|
+
end
|
105
|
+
|
106
|
+
def self.max_success
|
107
|
+
self.instance_variable_get(:@_max_success)
|
108
|
+
end
|
109
|
+
|
110
|
+
def self.max_discard_ratio
|
111
|
+
self.instance_variable_get(:@_max_discard_ratio)
|
112
|
+
end
|
113
|
+
|
114
|
+
def self.max_size
|
115
|
+
self.instance_variable_get(:@_max_size)
|
116
|
+
end
|
117
|
+
|
118
|
+
def self.max_shrinks
|
119
|
+
self.instance_variable_get(:@_max_shrinks)
|
120
|
+
end
|
121
|
+
|
122
|
+
def self.result_db
|
123
|
+
self.instance_variable_get(:@_result_db)
|
124
|
+
end
|
125
|
+
|
126
|
+
def self.use_db?
|
127
|
+
self.instance_variable_get(:@_use_db)
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.record_failure(file, classname, methodname, generated)
|
131
|
+
self.instance_variable_get(:@_results)
|
132
|
+
.record_failure(file, classname, methodname, generated)
|
133
|
+
end
|
134
|
+
|
135
|
+
def self.strike_failure(file, classname, methodname)
|
136
|
+
self.instance_variable_get(:@_results)
|
137
|
+
.strike_failure(file, classname, methodname)
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.reporter
|
141
|
+
return self.instance_variable_get(:@_results) if self.instance_variable_defined?(:@_results)
|
142
|
+
|
143
|
+
reporter = Minitest::ResultsDatabase.new(result_db)
|
144
|
+
self.instance_variable_set(:@_results, reporter)
|
145
|
+
|
146
|
+
reporter
|
147
|
+
end
|
18
148
|
end
|
19
149
|
end
|
20
150
|
|
@@ -1,9 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'minitest'
|
2
4
|
require 'minitest/proptest'
|
3
5
|
require 'minitest/proptest/gen'
|
4
6
|
require 'minitest/proptest/property'
|
5
7
|
require 'minitest/proptest/status'
|
6
8
|
require 'minitest/proptest/version'
|
9
|
+
require 'yaml'
|
7
10
|
|
8
11
|
module Minitest
|
9
12
|
def self.plugin_proptest_init(options)
|
@@ -18,15 +21,35 @@ module Minitest
|
|
18
21
|
end
|
19
22
|
end
|
20
23
|
|
21
|
-
|
22
|
-
|
24
|
+
self.reporter << Proptest.reporter
|
25
|
+
|
26
|
+
Proptest.set_seed(options[:seed]) if options.key?(:seed)
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.plugin_proptest_options(opts, _options)
|
30
|
+
opts.on('--max-success', Integer, "Maximum number of successful cases to verify for each property (Default: #{Minitest::Proptest::DEFAULT_MAX_SUCCESS})") do |max_success|
|
31
|
+
Proptest.max_success = max_success
|
32
|
+
end
|
33
|
+
opts.on('--max-discard-ratio', Integer, "Maximum ratio of successful cases versus discarded cases per property (Default: #{Minitest::Proptest::DEFAULT_MAX_DISCARD_RATIO}:1)") do |max_success|
|
34
|
+
Proptest.max_success = max_success
|
35
|
+
end
|
36
|
+
opts.on('--max-size', Integer, "Maximum amount of entropy a single case may use in bytes (Default: #{Minitest::Proptest::DEFAULT_MAX_SIZE} bytes)") do |max_size|
|
37
|
+
Proptest.max_size = max_size
|
38
|
+
end
|
39
|
+
opts.on('--max-shrinks', Integer, "Maximum number of shrink iterations a single failure reduction may use (Default: #{Minitest::Proptest::DEFAULT_MAX_SHRINKS})") do |max_shrinks|
|
40
|
+
Proptest.max_shrinks = max_shrinks
|
41
|
+
end
|
42
|
+
opts.on('--results-db', String, "Location of the file to persist most recent failure cases. Implies --use-db. (Default: #{Minitest::Proptest::DEFAULT_DB_LOCATION})") do |db_path|
|
43
|
+
Proptest.result_db = db_path
|
44
|
+
Proptest.use_db!
|
45
|
+
end
|
46
|
+
opts.on('--use-db', 'Persist previous failures in a database and use them before generating new values. Helps prevent flaky builds. (Default: false)') do
|
47
|
+
Proptest.use_db!
|
23
48
|
end
|
24
49
|
end
|
25
50
|
|
26
51
|
module Assertions
|
27
52
|
def property(&f)
|
28
|
-
self.assertions += 1
|
29
|
-
|
30
53
|
random_thunk = if Proptest.instance_variable_defined?(:@_random_seed)
|
31
54
|
r = Proptest.instance_variable_get(:@_random_seed)
|
32
55
|
->() { Proptest::DEFAULT_RANDOM.call(r) }
|
@@ -34,17 +57,29 @@ module Minitest
|
|
34
57
|
Proptest::DEFAULT_RANDOM
|
35
58
|
end
|
36
59
|
|
60
|
+
file, methodname = caller.first.split(/:\d+:in +/)
|
61
|
+
classname = self.class.name
|
62
|
+
methodname.gsub!(/(?:^`|'$)/, '')
|
63
|
+
|
37
64
|
prop = Minitest::Proptest::Property.new(
|
38
65
|
f,
|
39
66
|
random: random_thunk,
|
40
|
-
max_success: Proptest
|
41
|
-
max_discard_ratio: Proptest
|
42
|
-
max_size: Proptest
|
43
|
-
max_shrinks: Proptest
|
67
|
+
max_success: Proptest.max_success,
|
68
|
+
max_discard_ratio: Proptest.max_discard_ratio,
|
69
|
+
max_size: Proptest.max_size,
|
70
|
+
max_shrinks: Proptest.max_shrinks,
|
71
|
+
previous_failure: Proptest.reporter.lookup(file, classname, methodname)
|
44
72
|
)
|
45
73
|
prop.run!
|
74
|
+
self.assertions += prop.calls
|
75
|
+
|
76
|
+
if prop.status.valid? && !prop.trivial
|
77
|
+
Proptest.strike_failure(file, classname, methodname)
|
78
|
+
else
|
79
|
+
unless prop.status.exhausted? || prop.status.invalid?
|
80
|
+
Proptest.record_failure(file, classname, methodname, prop.result.map(&:value))
|
81
|
+
end
|
46
82
|
|
47
|
-
unless prop.status.valid? && !prop.trivial
|
48
83
|
raise Minitest::Assertion, prop.explain
|
49
84
|
end
|
50
85
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: minitest-proptest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tina Wuest
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-03-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: minitest
|
@@ -33,6 +33,7 @@ extra_rdoc_files: []
|
|
33
33
|
files:
|
34
34
|
- lib/minitest/proptest.rb
|
35
35
|
- lib/minitest/proptest/gen.rb
|
36
|
+
- lib/minitest/proptest/gen/value_generator.rb
|
36
37
|
- lib/minitest/proptest/property.rb
|
37
38
|
- lib/minitest/proptest/status.rb
|
38
39
|
- lib/minitest/proptest/version.rb
|
@@ -44,6 +45,7 @@ metadata:
|
|
44
45
|
homepage_uri: https://github.com/wuest/minitest-proptest
|
45
46
|
source_code_uri: https://github.com/wuest/minitest-proptest
|
46
47
|
changelog_uri: https://github.com/wuest/minitest-proptest/blob/main/CHANGELOG.md
|
48
|
+
rubygems_mfa_required: 'true'
|
47
49
|
post_install_message:
|
48
50
|
rdoc_options: []
|
49
51
|
require_paths:
|
@@ -59,7 +61,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
59
61
|
- !ruby/object:Gem::Version
|
60
62
|
version: '0'
|
61
63
|
requirements: []
|
62
|
-
rubygems_version: 3.3
|
64
|
+
rubygems_version: 3.5.3
|
63
65
|
signing_key:
|
64
66
|
specification_version: 4
|
65
67
|
summary: Property testing in Minitest
|