minitest-proptest 0.0.2

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e01d66af6e62a9d5931cd1da501b08679e8f78a4aa625aa694c28029ae5228a9
4
+ data.tar.gz: 3d99ae675bbeac0c89c807a535c7400fc0806a223a68c0d1a50eb2cb2917c148
5
+ SHA512:
6
+ metadata.gz: 2a4abe803c73e8e254dfa2022e5bcbb84f3bdf6c1e502f7fa34d3d95c06dad05f44be371a935907135f8bb7f67cd1240b24835c7cf233e1fd85f52608ed53ac8
7
+ data.tar.gz: dd81efa85c34580d0d29cfbf21ab5042f0af14f493b7041712a61fd491638c5a6e02d79e758a459a03671110bf057474f9fae34c96c96607e8c0f1e7853c2e31
@@ -0,0 +1,525 @@
1
+ module Minitest
2
+ module Proptest
3
+ 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
+ class Int8 < Integer; end
127
+ class Int16 < Integer; end
128
+ class Int32 < Integer; end
129
+ class Int64 < Integer; end
130
+ class UInt8 < Integer; end
131
+ class UInt16 < Integer; end
132
+ class UInt32 < Integer; end
133
+ class UInt64 < Integer; end
134
+ class Float32 < Float; end
135
+ class Float64 < Float; end
136
+ class ASCIIChar < String; end
137
+ class Char < String; end
138
+ class Bool < TrueClass; end
139
+
140
+ # Default maximum random value size is the local architecture's word size
141
+ MAX_SIZE = ((1 << (1.size * 8)) - 1)
142
+ SIGN_BIT = (1 << ((1.size * 8) - 1))
143
+
144
+ attr_reader :generated
145
+
146
+ instance_variable_set(:@_generators, {})
147
+
148
+ def self.create_type_constructor(arity, classes)
149
+ constructor = ->(c1) do
150
+ if classes.length == arity
151
+ f.call(*classes)
152
+ else
153
+ ->(c2) { constructor.call(c2) }
154
+ end
155
+ end
156
+ end
157
+
158
+ def self.generator_for(klass, &f)
159
+ new_class = Class.new(ValueGenerator)
160
+ new_class.define_method(:initialize) do |g|
161
+ @entropy = ->(b = MAX_SIZE) { (@generated << g.rand(b)).last }
162
+ @generated = []
163
+ @generator = self.method(:generator).curry
164
+ @parent_gen = g
165
+ @value = nil
166
+ @type_parameters = []
167
+ end
168
+
169
+ new_class.define_method(:generator, &f)
170
+
171
+ instance_variable_get(:@_generators)[klass] = new_class #.method(:new)
172
+ self.const_set((klass.name + 'Gen').split('::').last, new_class)
173
+ new_class
174
+ end
175
+
176
+ def initialize(random)
177
+ @random = random
178
+ @generated = []
179
+ end
180
+
181
+ def rand(max_size = MAX_SIZE)
182
+ (@generated << @random.rand(max_size)).last
183
+ end
184
+
185
+ def for(*classes)
186
+ generators = self.class.instance_variable_get(:@_generators)
187
+ case classes.length
188
+ when 0
189
+ raise(TypeError, "A generator for #{classes.join(' ')} is not known. Try adding it with Gen.generator_for.")
190
+ when 1
191
+ gen = generators[classes.first]
192
+ if gen.bound_max > 1
193
+ c = rand(gen.bound_max - gen.bound_min + 1) + gen.bound_min
194
+ c.times.reduce(gen.empty(self)) { |g, _| g.append(gen.new(self)) }
195
+ else
196
+ gen.new(self)
197
+ end
198
+ else
199
+ classgen = ->() do
200
+ classes[1..-1].map do |c|
201
+ if c.is_a?(Array)
202
+ self.for(*c)
203
+ else
204
+ self.for(c)
205
+ end
206
+ end
207
+ end
208
+ cs = classgen.call
209
+
210
+ gen = generators[classes.first]
211
+ typegen = gen.bound_min < 1 ? gen.empty(self) : gen.new(self)
212
+ typegen.type_parameters = cs
213
+ typegen.prefix_entropy_generation(cs)
214
+
215
+ if gen.bound_max > 1
216
+ c = rand(gen.bound_max - gen.bound_min + 1) + gen.bound_min
217
+ c.times.reduce(typegen) do |g, _|
218
+ cs2 = classgen.call
219
+ g2 = gen.new(self)
220
+ g2.type_parameters = cs2
221
+ g2.prefix_entropy_generation(cs2)
222
+ g.append(g2)
223
+ end
224
+ else
225
+ typegen
226
+ end
227
+ end
228
+ end
229
+
230
+ # Common shrinking machinery for all integral types. This includes chars,
231
+ # etc.
232
+ integral_shrink = ->(x) do
233
+ candidates = []
234
+ y = x
235
+
236
+ until y == 0
237
+ candidates << (x - y)
238
+ candidates << y
239
+ # Prevent negative integral from preventing termination
240
+ y = (y / 2.0).to_i
241
+ end
242
+
243
+ candidates
244
+ .flat_map { |i| [i - 1, i, i + 1] }
245
+ .reject { |i| i.abs >= x.abs }
246
+ end
247
+
248
+ float_shrink = ->(x) do
249
+ candidates = [Float::NAN, Float::INFINITY]
250
+ y = x
251
+
252
+ until y == 0 || y
253
+ candidates << (x - y)
254
+ y = (y / 2.0).to_i
255
+ end
256
+
257
+ candidates
258
+ .flat_map { |i| [i - 1, i, i + 1] }
259
+ .reject { |i| i.abs >= x.abs }
260
+ end
261
+
262
+ # List shrink adapted from QuickCheck
263
+ list_remove = ->(k, n, xs) do
264
+ xs1 = xs.take(k)
265
+ xs2 = xs.drop(k)
266
+ if k > n
267
+ []
268
+ elsif xs2.empty?
269
+ [[]]
270
+ else
271
+ [xs2] + list_remove.call(k, (n-k), xs2).map { |ys| xs1 + ys }
272
+ end
273
+ end
274
+
275
+ shrink_one = ->(f, xs) do
276
+ if xs.empty?
277
+ []
278
+ else
279
+ x = xs.first
280
+ xs = xs.drop(1)
281
+
282
+ ys = f.call(x).map { |y| [y] + xs }
283
+ zs = shrink_one.call(f, xs).map { |z| [x] + z }
284
+ ys + zs
285
+ end
286
+ end
287
+
288
+ list_shrink = ->(f, xs) do
289
+ candidates = [[]]
290
+ n = xs.length
291
+ k = n
292
+ while k > 0
293
+ candidates += list_remove.call(k, n, xs)
294
+ k /= 2
295
+ end
296
+ candidates + shrink_one.call(f, xs)
297
+ end
298
+
299
+ hash_remove = ->(k, n, h) do
300
+ xs = h.keys
301
+ xs1 = xs.take(k)
302
+ xs2 = xs.drop(k)
303
+
304
+ if k > n
305
+ []
306
+ elsif xs2.empty?
307
+ [{}]
308
+ else
309
+ h1 = xs1.reduce({}) { |c, e| c.merge({ e => h[e] }) }
310
+ 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) }
312
+ end
313
+ end
314
+
315
+ hash_shrink = ->(fk, fv, h) do
316
+ candidates = []
317
+ n = h.length
318
+ k = n
319
+ while k > 0
320
+ candidates += hash_remove.call(k, n, h)
321
+ k /= 2
322
+ end
323
+ candidates
324
+ end
325
+
326
+ # Use two's complement for all signed integers in order to optimize for
327
+ # random values to shrink towards 0.
328
+ generator_for(Integer) do
329
+ r = sized(MAX_SIZE)
330
+ if (r & SIGN_BIT).zero?
331
+ r
332
+ else
333
+ -(((r & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
334
+ end
335
+ end.with_shrink_function do |i|
336
+ i = if (i & SIGN_BIT).zero?
337
+ i
338
+ else
339
+ -(((i & (MAX_SIZE ^ SIGN_BIT)) - 1) ^ (MAX_SIZE ^ SIGN_BIT))
340
+ end
341
+ integral_shrink.call(i)
342
+ end
343
+
344
+ generator_for(Int8) do
345
+ r = sized(0xff)
346
+ (r & 0x80).zero? ? r : -(((r & 0x7f) - 1) ^ 0x7f)
347
+ end.with_shrink_function do |i|
348
+ i = (i & 0x80).zero? ? i : -(((i & 0x7f) - 1) ^ 0x7f)
349
+ integral_shrink.call(i)
350
+ end
351
+
352
+ generator_for(Int16) do
353
+ r = sized(0xffff)
354
+ (r & 0x8000).zero? ? r : -(((r & 0x7fff) - 1) ^ 0x7fff)
355
+ end.with_shrink_function do |i|
356
+ i = (i & 0x8000).zero? ? i : -(((i & 0x7fff) - 1) ^ 0x7fff)
357
+ integral_shrink.call(i)
358
+ end
359
+
360
+ generator_for(Int32) do
361
+ r = sized(0xffffffff)
362
+ (r & 0x80000000).zero? ? r : -(((r & 0x7fffffff) - 1) ^ 0x7fffffff)
363
+ end.with_shrink_function do |i|
364
+ i = if (i & 0x80000000).zero?
365
+ i
366
+ else
367
+ -(((i & 0x7fffffff) - 1) ^ 0x7fffffff)
368
+ end
369
+ integral_shrink.call(i)
370
+ end
371
+
372
+ generator_for(Int64) do
373
+ r = sized(0xffffffffffffffff)
374
+ if (r & 0x8000000000000000).zero?
375
+ r
376
+ else
377
+ -(((r & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
378
+ end
379
+ end.with_shrink_function do |i|
380
+ i = if (i & 0x8000000000000000).zero?
381
+ i
382
+ else
383
+ -(((i & 0x7fffffffffffffff) - 1) ^ 0x7fffffffffffffff)
384
+ end
385
+ integral_shrink.call(i)
386
+ end
387
+
388
+ generator_for(UInt8) do
389
+ sized(0xff)
390
+ end.with_shrink_function do |i|
391
+ integral_shrink.call(i).reject(&:negative?)
392
+ end
393
+
394
+ generator_for(UInt16) do
395
+ sized(0xffff)
396
+ end.with_shrink_function do |i|
397
+ integral_shrink.call(i).reject(&:negative?)
398
+ end
399
+
400
+ generator_for(UInt32) do
401
+ sized(0xffffffff)
402
+ end.with_shrink_function do |i|
403
+ integral_shrink.call(i).reject(&:negative?)
404
+ end
405
+
406
+ generator_for(UInt64) do
407
+ sized(0xffffffffffffffff)
408
+ end.with_shrink_function do |i|
409
+ integral_shrink.call(i).reject(&:negative?)
410
+ end
411
+
412
+ generator_for(Float32) do
413
+ # There is most likely a faster way to do this which doesn't involve
414
+ # FFI, but it was faster than manual bit twiddling in ruby
415
+ bits = sized(0xffffffff)
416
+ (0..3)
417
+ .map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
418
+ .join
419
+ .unpack('f')
420
+ .first
421
+ end.with_shrink_function do |f|
422
+ 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
430
+
431
+ generator_for(Float64) do
432
+ bits = sized(0xffffffffffffffff)
433
+ (0..7)
434
+ .map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
435
+ .join
436
+ .unpack('d')
437
+ .first
438
+ end.with_shrink_function do |f|
439
+ 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
447
+
448
+ generator_for(Float) do
449
+ bits = sized(0xffffffffffffffff)
450
+ (0..7)
451
+ .map { |y| ((bits & (0xff << (8 * y))) >> (8 * y)).chr }
452
+ .join
453
+ .unpack('d')
454
+ .first
455
+ end.with_shrink_function do |f|
456
+ 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
464
+
465
+ generator_for(ASCIIChar) do
466
+ sized(0x7f).chr
467
+ end.with_shrink_function do |c|
468
+ integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
469
+ end.with_score_function do |c|
470
+ c.ord
471
+ end
472
+
473
+ generator_for(Char) do
474
+ sized(0xff).chr
475
+ end.with_shrink_function do |c|
476
+ integral_shrink.call(c.ord).reject(&:negative?).map(&:chr)
477
+ end.with_score_function do |c|
478
+ c.ord
479
+ end
480
+
481
+ generator_for(String) do
482
+ sized(0xff).chr
483
+ end.with_shrink_function do |s|
484
+ xs = list_shrink.call(integral_shrink, s.chars.map(&:ord))
485
+ xs.map { |str| str.map { |t| t & 0xff }.map(&:chr).join }
486
+ end.with_score_function do |s|
487
+ s.chars.map(&:ord).reduce(1) do |c, x|
488
+ y = x.abs
489
+ c * (y > 0 ? y + 1 : 1)
490
+ end
491
+ end.with_append(0, 0x20) do |x, y|
492
+ x + y
493
+ end.with_empty { "" }
494
+
495
+ generator_for(Array) do |x|
496
+ [x]
497
+ end.with_shrink_function(&list_shrink).with_score_function do |f, xs|
498
+ xs.reduce(1) do |c, x|
499
+ y = f.call(x).abs
500
+ c * (y > 0 ? y + 1 : 1)
501
+ end.to_i * xs.length
502
+ end.with_append(0, 0x10) do |xs, ys|
503
+ xs + ys
504
+ end.with_empty { [] }
505
+
506
+ generator_for(Hash) do |key, value|
507
+ { key => value }
508
+ end.with_shrink_function(&hash_shrink).with_score_function do |fk, fv, h|
509
+ h.reduce(1) do |c, (k, v)|
510
+ sk = fk.call(k).abs
511
+ sv = fv.call(v).abs
512
+ c * ((sk > 0 ? sk + 1 : 1) + (sv > 0 ? sv + 1 : 1))
513
+ end
514
+ end.with_append(0, 0x10) do |xm, ym|
515
+ xm.merge(ym)
516
+ end.with_empty { Hash.new }
517
+
518
+ generator_for(Bool) do
519
+ sized(0x1).even? ? false : true
520
+ end.with_score_function do |_|
521
+ 1
522
+ end
523
+ end
524
+ end
525
+ end
@@ -0,0 +1,177 @@
1
+ module Minitest
2
+ module Proptest
3
+ class Property
4
+
5
+ attr_reader :result, :status, :trivial
6
+ def initialize(
7
+ # The function which proves the property
8
+ test_proc,
9
+ # Any class which provides `rand` accepting both an Integer and a Range
10
+ # is acceptable. The default value is Ruby's standard Mersenne Twister
11
+ # implementation.
12
+ random: Random.method(:new),
13
+ # Maximum number of successful cases before considering the test a
14
+ # success.
15
+ max_success: 100,
16
+ # Maximum ratio of discarded tests per successful test before giving up.
17
+ max_discard_ratio: 10,
18
+ # Maximum amount of entropy to generate in a single run
19
+ max_size: 0x100,
20
+ # Maximum number of shrink attempts (default of half of max unsigned int
21
+ # on the system architecture adopted from QuickCheck
22
+ max_shrinks: 0x7fffffffffffffff
23
+ )
24
+ @test_proc = test_proc
25
+ @random = random.call
26
+ @generator = ::Minitest::Proptest::Gen.new(@random)
27
+ @max_success = max_success
28
+ @max_discard_ratio = max_discard_ratio
29
+ @max_size = max_size
30
+ @max_shrinks = max_shrinks
31
+ @status = Status.unknown
32
+ @trivial = false
33
+ @result = nil
34
+ @exception = nil
35
+ @calls = 0
36
+ @valid_test_cases = 0
37
+ @generated = []
38
+ @arbitrary = nil
39
+ end
40
+
41
+ def run!
42
+ iterate!
43
+ shrink!
44
+ end
45
+
46
+ def arbitrary(*classes)
47
+ if @arbitrary
48
+ @arbitrary.call(*classes)
49
+ else
50
+ a = @generator.for(*classes)
51
+ @generated << a
52
+ @status = Status.overrun unless @generated.length <= @max_size
53
+ a.value
54
+ end
55
+ end
56
+
57
+ def explain
58
+ prop = if @status.valid?
59
+ "The property was proved to satsfaction across " +
60
+ "#{@valid_test_cases} assertions."
61
+ 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")
65
+ 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."
69
+ elsif @status.unknown?
70
+ "The property has not yet been tested."
71
+ 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
76
+ end
77
+ 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."
82
+ else
83
+ ""
84
+ end
85
+ prop + trivial
86
+ end
87
+
88
+ private
89
+
90
+ def iterate!
91
+ while continue_iterate? && @result.nil? && @valid_test_cases <= @max_success / 2
92
+ @generated = []
93
+ @generator = ::Minitest::Proptest::Gen.new(@random)
94
+ @calls += 1
95
+ if instance_eval(&@test_proc)
96
+ @status = Status.valid if @status.unknown?
97
+ @valid_test_cases += 1
98
+ else
99
+ @result = @generated
100
+ @status = Status.interesting
101
+ end
102
+ @trivial = true if @generated.empty?
103
+ end
104
+ rescue => e
105
+ @status = Status.invalid
106
+ @exception = e
107
+ raise e
108
+ end
109
+
110
+ def shrink!
111
+ return if @result.nil?
112
+ old_random = @random
113
+ old_generator = @generator
114
+ best_score = @generated.map(&:score).reduce(&:+)
115
+ best_generated = @generated
116
+ candidates = @generated.map(&:shrink_candidates)
117
+ old_arbitrary = @arbitrary
118
+
119
+ 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
124
+ run = { run: 0, index: -1 }
125
+
126
+ @arbitrary = ->(*classes) do
127
+ run[:index] += 1
128
+ raise IndexError if run[:index] >= to_test[run[:run]].length
129
+
130
+ a = @generator.for(*classes)
131
+ a = a.force(to_test[run[:run]][run[:index]].last)
132
+ @generated << a
133
+ to_test[run[:run]][run[:index]].last
134
+ end
135
+
136
+ while continue_shrink? && run[:run] < to_test.length
137
+ @generated = []
138
+ run[:index] = -1
139
+
140
+ @generator = ::Minitest::Proptest::Gen.new(@random)
141
+ if to_test[run[:run]].map(&:first).reduce(&:+) < best_score
142
+ unless instance_eval(&@test_proc)
143
+ best_generated = @generated
144
+ # Because we pre-sorted our shrink candidates, the first hit is
145
+ # necessarily the best scoring
146
+ break
147
+ end
148
+ end
149
+
150
+ @calls += 1
151
+ run[:run] += 1
152
+ end
153
+ # Clean up after we're done
154
+ @generated = best_generated
155
+ @result = best_generated
156
+ @generator = old_generator
157
+ @random = old_random
158
+ @arbitrary = old_arbitrary
159
+ end
160
+
161
+ def continue_iterate?
162
+ !@trivial &&
163
+ !@status.invalid? &&
164
+ !@status.overrun? &&
165
+ @valid_test_cases < @max_success &&
166
+ @calls < @max_success * @max_discard_ratio
167
+ end
168
+
169
+ def continue_shrink?
170
+ !@trivial &&
171
+ !@status.invalid? &&
172
+ !@status.overrun?
173
+ @calls < @max_shrinks
174
+ end
175
+ end
176
+ end
177
+ end
@@ -0,0 +1,61 @@
1
+ module Minitest
2
+ module Proptest
3
+ # Sum type representing the possible statuses of a test run.
4
+ # Invalid, Overrun, and Interesting represent different failure classes.
5
+ # Unknown represents a lack of information about the test run (typically
6
+ # having not run to satisfaction).
7
+ # Valid represents a test which has run to satisfaction.
8
+ class Status
9
+ class Interesting < Status
10
+ end
11
+
12
+ class Invalid < Status
13
+ end
14
+
15
+ class Overrun < Status
16
+ end
17
+
18
+ class Unknown < Status
19
+ end
20
+
21
+ class Valid < Status
22
+ end
23
+
24
+ invalid = Invalid.new.freeze
25
+ interesting = Interesting.new.freeze
26
+ overrun = Overrun.new.freeze
27
+ unknown = Unknown.new.freeze
28
+ valid = Valid.new.freeze
29
+
30
+ define_singleton_method(:invalid) { invalid }
31
+ define_singleton_method(:interesting) { interesting }
32
+ define_singleton_method(:overrun) { overrun }
33
+ define_singleton_method(:unknown) { unknown }
34
+ define_singleton_method(:valid) { valid }
35
+
36
+ def invalid?
37
+ self.is_a?(Invalid)
38
+ end
39
+
40
+ def overrun?
41
+ self.is_a?(Overrun)
42
+ end
43
+
44
+ def unknown?
45
+ self.is_a?(Unknown)
46
+ end
47
+
48
+ def valid?
49
+ self.is_a?(Valid)
50
+ end
51
+
52
+ def interesting?
53
+ self.is_a?(Interesting)
54
+ end
55
+
56
+ def initialize
57
+ raise 'Please use singleton instances'
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Proptest
5
+ VERSION = '0.0.2'
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ require 'minitest'
2
+ require 'minitest/proptest/gen'
3
+ require 'minitest/proptest/property'
4
+ require 'minitest/proptest/status'
5
+ require 'minitest/proptest/version'
6
+
7
+ module Minitest
8
+ module Proptest
9
+ DEFAULT_RANDOM = Random.method(:new)
10
+ DEFAULT_MAX_SUCCESS = 100
11
+ DEFAULT_MAX_DISCARD_RATIO = 10
12
+ DEFAULT_MAX_SIZE = 0x100
13
+ DEFAULT_MAX_SHRINKS = (((1 << (1.size * 8)) - 1) / 2)
14
+
15
+ def self.set_seed(seed)
16
+ self.instance_variable_set(:@_random_seed, seed)
17
+ end
18
+ end
19
+ end
20
+
21
+ module Kernel
22
+ def generator_for(klass, &f)
23
+ ::Minitest::Proptest::Gen.generator_for(klass, &f)
24
+ end
25
+ private :generator_for
26
+ end
@@ -0,0 +1,52 @@
1
+ require 'minitest'
2
+ require 'minitest/proptest'
3
+ require 'minitest/proptest/gen'
4
+ require 'minitest/proptest/property'
5
+ require 'minitest/proptest/status'
6
+ require 'minitest/proptest/version'
7
+
8
+ module Minitest
9
+ def self.plugin_proptest_init(options)
10
+ %i[Int8 Int16 Int32 Int64
11
+ UInt8 UInt16 UInt32 UInt64
12
+ Float32 Float64
13
+ ASCIIChar Char
14
+ Bool
15
+ ].each do |const|
16
+ unless Minitest::Assertions.const_defined?(const)
17
+ ::Minitest::Assertions.const_set(const, ::Minitest::Proptest::Gen.const_get(const))
18
+ end
19
+ end
20
+
21
+ if options.has_key?(:seed)
22
+ Proptest.set_seed(options[:seed])
23
+ end
24
+ end
25
+
26
+ module Assertions
27
+ def property(&f)
28
+ self.assertions += 1
29
+
30
+ random_thunk = if Proptest.instance_variable_defined?(:@_random_seed)
31
+ r = Proptest.instance_variable_get(:@_random_seed)
32
+ ->() { Proptest::DEFAULT_RANDOM.call(r) }
33
+ else
34
+ Proptest::DEFAULT_RANDOM
35
+ end
36
+
37
+ prop = Minitest::Proptest::Property.new(
38
+ f,
39
+ 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
44
+ )
45
+ prop.run!
46
+
47
+ unless prop.status.valid? && !prop.trivial
48
+ raise Minitest::Assertion, prop.explain
49
+ end
50
+ end
51
+ end
52
+ end
metadata ADDED
@@ -0,0 +1,66 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: minitest-proptest
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ platform: ruby
6
+ authors:
7
+ - Tina Wuest
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-09-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5'
27
+ description: Property testing in Minitest, a la Haskell's QuickCheck and Python's
28
+ Hypothesis
29
+ email: tina@wuest.me
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - lib/minitest/proptest.rb
35
+ - lib/minitest/proptest/gen.rb
36
+ - lib/minitest/proptest/property.rb
37
+ - lib/minitest/proptest/status.rb
38
+ - lib/minitest/proptest/version.rb
39
+ - lib/minitest/proptest_plugin.rb
40
+ homepage: https://github.com/wuest/minitest-proptest
41
+ licenses:
42
+ - MIT
43
+ metadata:
44
+ homepage_uri: https://github.com/wuest/minitest-proptest
45
+ source_code_uri: https://github.com/wuest/minitest-proptest
46
+ changelog_uri: https://github.com/wuest/minitest-proptest/blob/main/CHANGELOG.md
47
+ post_install_message:
48
+ rdoc_options: []
49
+ require_paths:
50
+ - lib
51
+ required_ruby_version: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.7.0
56
+ required_rubygems_version: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ requirements: []
62
+ rubygems_version: 3.3.7
63
+ signing_key:
64
+ specification_version: 4
65
+ summary: Property testing in Minitest
66
+ test_files: []