minitest-proptest 0.0.2 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e01d66af6e62a9d5931cd1da501b08679e8f78a4aa625aa694c28029ae5228a9
4
- data.tar.gz: 3d99ae675bbeac0c89c807a535c7400fc0806a223a68c0d1a50eb2cb2917c148
3
+ metadata.gz: '0596e6f6a66614dba0199eccc4e18a5bf6b869fd0db46dff7636a8772eacb75b'
4
+ data.tar.gz: a956c579c5bb355a5b687f64109917918870dd7dac1323a18a72d44e4ca623a6
5
5
  SHA512:
6
- metadata.gz: 2a4abe803c73e8e254dfa2022e5bcbb84f3bdf6c1e502f7fa34d3d95c06dad05f44be371a935907135f8bb7f67cd1240b24835c7cf233e1fd85f52608ed53ac8
7
- data.tar.gz: dd81efa85c34580d0d29cfbf21ab5042f0af14f493b7041712a61fd491638c5a6e02d79e758a459a03671110bf057474f9fae34c96c96607e8c0f1e7853c2e31
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 = ->(c1) do
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 #.method(:new)
172
- self.const_set((klass.name + 'Gen').split('::').last, new_class)
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..-1].map do |c|
201
- if c.is_a?(Array)
202
- self.for(*c)
84
+ classes[1..].map do |k|
85
+ if k.is_a?(Array)
86
+ self.for(*k)
203
87
  else
204
- self.for(c)
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
- .flat_map { |i| [i - 1, i, i + 1] }
245
- .reject { |i| i.abs >= x.abs }
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 == 0 || 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
- candidates
258
- .flat_map { |i| [i - 1, i, i + 1] }
259
- .reject { |i| i.abs >= x.abs }
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 = ->(fk, fv, h) do
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
- i = if (i & SIGN_BIT).zero?
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(i)
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
- i = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
349
- integral_shrink.call(i)
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
- i = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
357
- integral_shrink.call(i)
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
- i = if (i & 0x80000000).zero?
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(i)
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
- i = if (i & 0x8000000000000000).zero?
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(i)
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
- .unpack('f')
420
- .first
363
+ .unpack1('f')
421
364
  end.with_shrink_function do |f|
422
365
  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
366
+ end.with_score_function(&score_float)
430
367
 
431
- generator_for(Float64) do
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
- .unpack('d')
437
- .first
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 do |f|
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
- (0..7)
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 do |f|
458
- if f.nan? || f.infinite?
459
- 0
460
- else
461
- f.abs.ceil
462
- end
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 do |c|
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 do |c|
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 { Hash.new }
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).even? ? false : true
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
- "The property was proved to satsfaction across " +
78
+ 'The property was proved to satsfaction across ' \
60
79
  "#{@valid_test_cases} assertions."
61
80
  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")
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 size." +
68
- "This might be rectified by increasing max_size."
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
- "The property has not yet been tested."
89
+ 'The property has not yet been tested.'
71
90
  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
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
- "and as such is likely not generating much value. " +
80
- "Consider reworking this test to make use of arbitrary " +
81
- "data."
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 / 2
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
- if instance_eval(&@test_proc)
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
- else
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
- raise e
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
- .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
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] = -1
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
- unless instance_eval(&@test_proc)
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
- @valid_test_cases < @max_success &&
166
- @calls < @max_success * @max_discard_ratio
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
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Proptest
5
- VERSION = '0.0.2'
5
+ VERSION = '0.2.0'
6
6
  end
7
7
  end
@@ -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
- if options.has_key?(:seed)
22
- Proptest.set_seed(options[:seed])
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::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
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.2
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: 2022-09-15 00:00:00.000000000 Z
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.7
64
+ rubygems_version: 3.5.3
63
65
  signing_key:
64
66
  specification_version: 4
65
67
  summary: Property testing in Minitest