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 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