minitest-proptest 0.0.2 → 0.1.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: d2f1cde4cf0a022319a3336a06e5509275ea420b5b36d39835f938c6ef830fae
4
+ data.tar.gz: 50a1ce6ade048840831ad943840c914cda998da1e6f7b98c07d02b7ee859d2db
5
5
  SHA512:
6
- metadata.gz: 2a4abe803c73e8e254dfa2022e5bcbb84f3bdf6c1e502f7fa34d3d95c06dad05f44be371a935907135f8bb7f67cd1240b24835c7cf233e1fd85f52608ed53ac8
7
- data.tar.gz: dd81efa85c34580d0d29cfbf21ab5042f0af14f493b7041712a61fd491638c5a6e02d79e758a459a03671110bf057474f9fae34c96c96607e8c0f1e7853c2e31
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 = ->(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
@@ -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 = ->(fk, fv, h) do
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
- i = if (i & SIGN_BIT).zero?
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(i)
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
- i = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
349
- integral_shrink.call(i)
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
- i = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
357
- integral_shrink.call(i)
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
- i = if (i & 0x80000000).zero?
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(i)
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
- i = if (i & 0x8000000000000000).zero?
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(i)
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
- .unpack('f')
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
- .unpack('d')
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
- .unpack('d')
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 do |c|
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 do |c|
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 { Hash.new }
393
+ end.with_empty { {} }
517
394
 
518
395
  generator_for(Bool) do
519
- sized(0x1).even? ? false : true
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
- "The property was proved to satsfaction across " +
67
+ 'The property was proved to satsfaction across ' \
60
68
  "#{@valid_test_cases} assertions."
61
69
  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")
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 size." +
68
- "This might be rectified by increasing max_size."
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
- "The property has not yet been tested."
78
+ 'The property has not yet been tested.'
71
79
  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
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
- "and as such is likely not generating much value. " +
80
- "Consider reworking this test to make use of arbitrary " +
81
- "data."
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
- raise e
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
- .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
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
- # Because we pre-sorted our shrink candidates, the first hit is
145
- # necessarily the best scoring
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
@@ -1,3 +1,5 @@
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.
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Minitest
4
4
  module Proptest
5
- VERSION = '0.0.2'
5
+ VERSION = '0.1.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,8 +21,30 @@ 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
 
@@ -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::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
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
- unless prop.status.valid? && !prop.trivial
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.2
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: 2022-09-15 00:00:00.000000000 Z
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.7
64
+ rubygems_version: 3.5.3
63
65
  signing_key:
64
66
  specification_version: 4
65
67
  summary: Property testing in Minitest