minitest-proptest 0.0.2 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/minitest/proptest/gen/value_generator.rb +129 -0
- data/lib/minitest/proptest/gen.rb +34 -157
- data/lib/minitest/proptest/property.rb +67 -27
- data/lib/minitest/proptest/status.rb +2 -0
- data/lib/minitest/proptest/version.rb +1 -1
- data/lib/minitest/proptest.rb +130 -0
- data/lib/minitest/proptest_plugin.rb +41 -7
- 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: d2f1cde4cf0a022319a3336a06e5509275ea420b5b36d39835f938c6ef830fae
|
4
|
+
data.tar.gz: 50a1ce6ade048840831ad943840c914cda998da1e6f7b98c07d02b7ee859d2db
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6af2348e841f27a7401a64cc777dde8aad6a1487289cea278ecb6ec2771602143bce354dad192c55d2c57c4aa828ed6a34842d048a1dcb41d45d09fcc5e9e96a
|
7
|
+
data.tar.gz: 8919b2d754966201a1f489c0b6594128e36fcf3301774cbabb0f67848b363d2702143829301042e0c8344dc0950c62995f2bf10147d94e4bbedc1e5f853cf063
|
@@ -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_max) { bound_max }
|
23
|
+
define_singleton_method(:bound_min) { bound_min }
|
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
|
@@ -268,7 +152,7 @@ module Minitest
|
|
268
152
|
elsif xs2.empty?
|
269
153
|
[[]]
|
270
154
|
else
|
271
|
-
[xs2] + list_remove.call(k, (n-k), xs2).map { |ys| xs1 + ys }
|
155
|
+
[xs2] + list_remove.call(k, (n - k), xs2).map { |ys| xs1 + ys }
|
272
156
|
end
|
273
157
|
end
|
274
158
|
|
@@ -308,11 +192,11 @@ module Minitest
|
|
308
192
|
else
|
309
193
|
h1 = xs1.reduce({}) { |c, e| c.merge({ e => h[e] }) }
|
310
194
|
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) }
|
195
|
+
[h1, h2] + list_remove.call(k, (n - k), h2).map { |ys| h1.merge(ys.to_h) }
|
312
196
|
end
|
313
197
|
end
|
314
198
|
|
315
|
-
hash_shrink = ->(
|
199
|
+
hash_shrink = ->(_fk, _fv, h) do
|
316
200
|
candidates = []
|
317
201
|
n = h.length
|
318
202
|
k = n
|
@@ -333,40 +217,40 @@ module Minitest
|
|
333
217
|
-(((r & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
|
334
218
|
end
|
335
219
|
end.with_shrink_function do |i|
|
336
|
-
|
220
|
+
j = if (i & SIGN_BIT).zero?
|
337
221
|
i
|
338
222
|
else
|
339
223
|
-(((i & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
|
340
224
|
end
|
341
|
-
integral_shrink.call(
|
225
|
+
integral_shrink.call(j)
|
342
226
|
end
|
343
227
|
|
344
228
|
generator_for(Int8) do
|
345
229
|
r = sized(0xff)
|
346
230
|
(r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
|
347
231
|
end.with_shrink_function do |i|
|
348
|
-
|
349
|
-
integral_shrink.call(
|
232
|
+
j = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
|
233
|
+
integral_shrink.call(j)
|
350
234
|
end
|
351
235
|
|
352
236
|
generator_for(Int16) do
|
353
237
|
r = sized(0xffff)
|
354
238
|
(r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
|
355
239
|
end.with_shrink_function do |i|
|
356
|
-
|
357
|
-
integral_shrink.call(
|
240
|
+
j = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
|
241
|
+
integral_shrink.call(j)
|
358
242
|
end
|
359
243
|
|
360
244
|
generator_for(Int32) do
|
361
245
|
r = sized(0xffffffff)
|
362
246
|
(r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
|
363
247
|
end.with_shrink_function do |i|
|
364
|
-
|
248
|
+
j = if (i & 0x80000000).zero?
|
365
249
|
i
|
366
250
|
else
|
367
251
|
-(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
|
368
252
|
end
|
369
|
-
integral_shrink.call(
|
253
|
+
integral_shrink.call(j)
|
370
254
|
end
|
371
255
|
|
372
256
|
generator_for(Int64) do
|
@@ -377,12 +261,12 @@ module Minitest
|
|
377
261
|
-(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
|
378
262
|
end
|
379
263
|
end.with_shrink_function do |i|
|
380
|
-
|
264
|
+
j = if (i & 0x8000000000000000).zero?
|
381
265
|
i
|
382
266
|
else
|
383
267
|
-(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
|
384
268
|
end
|
385
|
-
integral_shrink.call(
|
269
|
+
integral_shrink.call(j)
|
386
270
|
end
|
387
271
|
|
388
272
|
generator_for(UInt8) do
|
@@ -416,8 +300,7 @@ module Minitest
|
|
416
300
|
(0..3)
|
417
301
|
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
418
302
|
.join
|
419
|
-
.
|
420
|
-
.first
|
303
|
+
.unpack1('f')
|
421
304
|
end.with_shrink_function do |f|
|
422
305
|
float_shrink.call(f)
|
423
306
|
end.with_score_function do |f|
|
@@ -433,8 +316,7 @@ module Minitest
|
|
433
316
|
(0..7)
|
434
317
|
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
435
318
|
.join
|
436
|
-
.
|
437
|
-
.first
|
319
|
+
.unpack1('d')
|
438
320
|
end.with_shrink_function do |f|
|
439
321
|
float_shrink.call(f)
|
440
322
|
end.with_score_function do |f|
|
@@ -450,8 +332,7 @@ module Minitest
|
|
450
332
|
(0..7)
|
451
333
|
.map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
|
452
334
|
.join
|
453
|
-
.
|
454
|
-
.first
|
335
|
+
.unpack1('d')
|
455
336
|
end.with_shrink_function do |f|
|
456
337
|
float_shrink.call(f)
|
457
338
|
end.with_score_function do |f|
|
@@ -466,17 +347,13 @@ module Minitest
|
|
466
347
|
sized(0x7f).chr
|
467
348
|
end.with_shrink_function do |c|
|
468
349
|
integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
|
469
|
-
end.with_score_function
|
470
|
-
c.ord
|
471
|
-
end
|
350
|
+
end.with_score_function(&:ord)
|
472
351
|
|
473
352
|
generator_for(Char) do
|
474
353
|
sized(0xff).chr
|
475
354
|
end.with_shrink_function do |c|
|
476
355
|
integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
|
477
|
-
end.with_score_function
|
478
|
-
c.ord
|
479
|
-
end
|
356
|
+
end.with_score_function(&:ord)
|
480
357
|
|
481
358
|
generator_for(String) do
|
482
359
|
sized(0xff).chr
|
@@ -490,7 +367,7 @@ module Minitest
|
|
490
367
|
end
|
491
368
|
end.with_append(0, 0x20) do |x, y|
|
492
369
|
x + y
|
493
|
-
end.with_empty {
|
370
|
+
end.with_empty { '' }
|
494
371
|
|
495
372
|
generator_for(Array) do |x|
|
496
373
|
[x]
|
@@ -513,10 +390,10 @@ module Minitest
|
|
513
390
|
end
|
514
391
|
end.with_append(0, 0x10) do |xm, ym|
|
515
392
|
xm.merge(ym)
|
516
|
-
end.with_empty {
|
393
|
+
end.with_empty { {} }
|
517
394
|
|
518
395
|
generator_for(Bool) do
|
519
|
-
sized(0x1).
|
396
|
+
sized(0x1).odd?
|
520
397
|
end.with_score_function do |_|
|
521
398
|
1
|
522
399
|
end
|
@@ -1,8 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Minitest
|
2
4
|
module Proptest
|
5
|
+
# Property evaluation - status, scoring, shrinking
|
3
6
|
class Property
|
4
|
-
|
5
7
|
attr_reader :result, :status, :trivial
|
8
|
+
|
6
9
|
def initialize(
|
7
10
|
# The function which proves the property
|
8
11
|
test_proc,
|
@@ -19,7 +22,10 @@ module Minitest
|
|
19
22
|
max_size: 0x100,
|
20
23
|
# Maximum number of shrink attempts (default of half of max unsigned int
|
21
24
|
# on the system architecture adopted from QuickCheck
|
22
|
-
max_shrinks: 0x7fffffffffffffff
|
25
|
+
max_shrinks: 0x7fffffffffffffff,
|
26
|
+
# Previously discovered counter-example. If this exists, it should be
|
27
|
+
# run before any test cases are generated.
|
28
|
+
previous_failure: []
|
23
29
|
)
|
24
30
|
@test_proc = test_proc
|
25
31
|
@random = random.call
|
@@ -36,9 +42,11 @@ module Minitest
|
|
36
42
|
@valid_test_cases = 0
|
37
43
|
@generated = []
|
38
44
|
@arbitrary = nil
|
45
|
+
@previous_failure = previous_failure.to_a
|
39
46
|
end
|
40
47
|
|
41
48
|
def run!
|
49
|
+
rerun!
|
42
50
|
iterate!
|
43
51
|
shrink!
|
44
52
|
end
|
@@ -56,31 +64,31 @@ module Minitest
|
|
56
64
|
|
57
65
|
def explain
|
58
66
|
prop = if @status.valid?
|
59
|
-
|
67
|
+
'The property was proved to satsfaction across ' \
|
60
68
|
"#{@valid_test_cases} assertions."
|
61
69
|
elsif @status.invalid?
|
62
|
-
|
63
|
-
"#{@exception.class.name}: #{@exception.message}\n"
|
64
|
-
@exception.backtrace.map { |l| " #{l}" }.join("\n")
|
70
|
+
'The property was determined to be invalid due to ' \
|
71
|
+
"#{@exception.class.name}: #{@exception.message}\n" \
|
72
|
+
"#{@exception.backtrace.map { |l| " #{l}" }.join("\n")}"
|
65
73
|
elsif @status.overrun?
|
66
|
-
"The property attempted to generate more than #{@max_size} "
|
67
|
-
"bytes of entropy, violating the property's maximum
|
68
|
-
|
74
|
+
"The property attempted to generate more than #{@max_size} " \
|
75
|
+
"bytes of entropy, violating the property's maximum " \
|
76
|
+
'size. This might be rectified by increasing max_size.'
|
69
77
|
elsif @status.unknown?
|
70
|
-
|
78
|
+
'The property has not yet been tested.'
|
71
79
|
elsif @status.interesting?
|
72
|
-
|
73
|
-
"#{@valid_test_cases} valid "
|
74
|
-
"example#{@valid_test_cases == 1 ? '' : 's'}:\n"
|
75
|
-
@generated.map(&:value).inspect
|
80
|
+
'The property has found the following counterexample after ' \
|
81
|
+
"#{@valid_test_cases} valid " \
|
82
|
+
"example#{@valid_test_cases == 1 ? '' : 's'}:\n" \
|
83
|
+
"#{@generated.map(&:value).inspect}"
|
76
84
|
end
|
77
85
|
trivial = if @trivial
|
78
|
-
"\nThe test does not appear to use any generated values "
|
79
|
-
|
80
|
-
|
81
|
-
|
86
|
+
"\nThe test does not appear to use any generated values " \
|
87
|
+
'and as such is likely not generating much value. ' \
|
88
|
+
'Consider reworking this test to make use of arbitrary ' \
|
89
|
+
'data.'
|
82
90
|
else
|
83
|
-
|
91
|
+
''
|
84
92
|
end
|
85
93
|
prop + trivial
|
86
94
|
end
|
@@ -104,11 +112,43 @@ module Minitest
|
|
104
112
|
rescue => e
|
105
113
|
@status = Status.invalid
|
106
114
|
@exception = e
|
107
|
-
|
115
|
+
end
|
116
|
+
|
117
|
+
def rerun!
|
118
|
+
return if @previous_failure.empty?
|
119
|
+
|
120
|
+
old_generator = @generator
|
121
|
+
old_random = @random
|
122
|
+
old_arbitrary = @arbitrary
|
123
|
+
|
124
|
+
index = -1
|
125
|
+
@arbitrary = ->(*classes) do
|
126
|
+
index += 1
|
127
|
+
raise IndexError if index >= @previous_failure.length
|
128
|
+
|
129
|
+
a = @generator.for(*classes)
|
130
|
+
a = a.force(@previous_failure[index])
|
131
|
+
@generated << a
|
132
|
+
@previous_failure[index]
|
133
|
+
end
|
134
|
+
|
135
|
+
@generator = ::Minitest::Proptest::Gen.new(@random)
|
136
|
+
if instance_eval(&@test_proc)
|
137
|
+
@generated = []
|
138
|
+
else
|
139
|
+
@result = @generated
|
140
|
+
@status = Status.interesting
|
141
|
+
end
|
142
|
+
|
143
|
+
# Clean up after we're done
|
144
|
+
@generator = old_generator
|
145
|
+
@random = old_random
|
146
|
+
@arbitrary = old_arbitrary
|
108
147
|
end
|
109
148
|
|
110
149
|
def shrink!
|
111
150
|
return if @result.nil?
|
151
|
+
|
112
152
|
old_random = @random
|
113
153
|
old_generator = @generator
|
114
154
|
best_score = @generated.map(&:score).reduce(&:+)
|
@@ -117,10 +157,10 @@ module Minitest
|
|
117
157
|
old_arbitrary = @arbitrary
|
118
158
|
|
119
159
|
to_test = candidates
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
160
|
+
.map { |x| x.map { |y| [y] } }
|
161
|
+
.reduce { |c, e| c.flat_map { |a| e.map { |b| a + b } } }
|
162
|
+
.sort { |x, y| x.map(&:first).reduce(&:+) <=> y.map(&:first).reduce(&:+) }
|
163
|
+
.uniq
|
124
164
|
run = { run: 0, index: -1 }
|
125
165
|
|
126
166
|
@arbitrary = ->(*classes) do
|
@@ -141,8 +181,8 @@ module Minitest
|
|
141
181
|
if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
|
142
182
|
unless instance_eval(&@test_proc)
|
143
183
|
best_generated = @generated
|
144
|
-
#
|
145
|
-
#
|
184
|
+
# The first hit is guaranteed to be the best scoring due to the
|
185
|
+
# shrink candidates are pre-sorted.
|
146
186
|
break
|
147
187
|
end
|
148
188
|
end
|
@@ -169,7 +209,7 @@ module Minitest
|
|
169
209
|
def continue_shrink?
|
170
210
|
!@trivial &&
|
171
211
|
!@status.invalid? &&
|
172
|
-
!@status.overrun?
|
212
|
+
!@status.overrun? &&
|
173
213
|
@calls < @max_shrinks
|
174
214
|
end
|
175
215
|
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,8 +21,30 @@ 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
|
|
@@ -34,17 +59,26 @@ module Minitest
|
|
34
59
|
Proptest::DEFAULT_RANDOM
|
35
60
|
end
|
36
61
|
|
62
|
+
file, methodname = caller.first.split(/:\d+:in +/)
|
63
|
+
classname = self.class.name
|
64
|
+
methodname.gsub!(/(?:^`|'$)/, '')
|
65
|
+
|
37
66
|
prop = Minitest::Proptest::Property.new(
|
38
67
|
f,
|
39
68
|
random: random_thunk,
|
40
|
-
max_success: Proptest
|
41
|
-
max_discard_ratio: Proptest
|
42
|
-
max_size: Proptest
|
43
|
-
max_shrinks: Proptest
|
69
|
+
max_success: Proptest.max_success,
|
70
|
+
max_discard_ratio: Proptest.max_discard_ratio,
|
71
|
+
max_size: Proptest.max_size,
|
72
|
+
max_shrinks: Proptest.max_shrinks,
|
73
|
+
previous_failure: Proptest.reporter.lookup(file, classname, methodname)
|
44
74
|
)
|
45
75
|
prop.run!
|
46
76
|
|
47
|
-
|
77
|
+
if prop.status.valid? && !prop.trivial
|
78
|
+
Proptest.strike_failure(file, classname, methodname)
|
79
|
+
else
|
80
|
+
Proptest.record_failure(file, classname, methodname, prop.result.map(&:value))
|
81
|
+
|
48
82
|
raise Minitest::Assertion, prop.explain
|
49
83
|
end
|
50
84
|
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.1.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-13 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
|